OpenShot Video Editor  2.0.0
main_window.py
Go to the documentation of this file.
1 ##
2 #
3 # @file
4 # @brief This file loads the main window (i.e. the primary user-interface)
5 # @author Noah Figg <eggmunkee@hotmail.com>
6 # @author Jonathan Thomas <jonathan@openshot.org>
7 # @author Olivier Girard <olivier@openshot.org>
8 #
9 # @section LICENSE
10 #
11 # Copyright (c) 2008-2018 OpenShot Studios, LLC
12 # (http://www.openshotstudios.com). This file is part of
13 # OpenShot Video Editor (http://www.openshot.org), an open-source project
14 # dedicated to delivering high quality video editing and animation solutions
15 # to the world.
16 #
17 # OpenShot Video Editor is free software: you can redistribute it and/or modify
18 # it under the terms of the GNU General Public License as published by
19 # the Free Software Foundation, either version 3 of the License, or
20 # (at your option) any later version.
21 #
22 # OpenShot Video Editor is distributed in the hope that it will be useful,
23 # but WITHOUT ANY WARRANTY; without even the implied warranty of
24 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
25 # GNU General Public License for more details.
26 #
27 # You should have received a copy of the GNU General Public License
28 # along with OpenShot Library. If not, see <http://www.gnu.org/licenses/>.
29 #
30 
31 import os
32 import sys
33 import platform
34 import shutil
35 import webbrowser
36 from uuid import uuid4
37 from copy import deepcopy
38 
39 from PyQt5.QtCore import *
40 from PyQt5.QtGui import QIcon, QCursor, QKeySequence
41 from PyQt5.QtWidgets import *
42 import openshot # Python module for libopenshot (required video editing module installed separately)
43 
44 from windows.views.timeline_webview import TimelineWebView
45 from classes import info, ui_util, settings, qt_types, updates
46 from classes.app import get_app
47 from classes.logger import log
48 from classes.timeline import TimelineSync
49 from classes.query import File, Clip, Transition, Marker, Track
50 from classes.metrics import *
51 from classes.version import *
52 from classes.conversion import zoomToSeconds, secondsToZoom
53 from images import openshot_rc
54 from windows.views.files_treeview import FilesTreeView
55 from windows.views.files_listview import FilesListView
56 from windows.views.transitions_treeview import TransitionsTreeView
57 from windows.views.transitions_listview import TransitionsListView
58 from windows.views.effects_treeview import EffectsTreeView
59 from windows.views.effects_listview import EffectsListView
60 from windows.views.properties_tableview import PropertiesTableView, SelectionLabel
61 from windows.views.tutorial import TutorialManager
62 from windows.video_widget import VideoWidget
63 from windows.preview_thread import PreviewParent
64 
65 
66 ##
67 # This class contains the logic for the main window widget
68 class MainWindow(QMainWindow, updates.UpdateWatcher):
69 
70  # Path to ui file
71  ui_path = os.path.join(info.PATH, 'windows', 'ui', 'main-window.ui')
72 
73  previewFrameSignal = pyqtSignal(int)
74  refreshFrameSignal = pyqtSignal()
75  LoadFileSignal = pyqtSignal(str)
76  PlaySignal = pyqtSignal(int)
77  PauseSignal = pyqtSignal()
78  StopSignal = pyqtSignal()
79  SeekSignal = pyqtSignal(int)
80  SpeedSignal = pyqtSignal(float)
81  RecoverBackup = pyqtSignal()
82  FoundVersionSignal = pyqtSignal(str)
83  WaveformReady = pyqtSignal(str, list)
84  TransformSignal = pyqtSignal(str)
85  ExportStarted = pyqtSignal(str, int, int)
86  ExportFrame = pyqtSignal(str, int, int, int)
87  ExportEnded = pyqtSignal(str)
88  MaxSizeChanged = pyqtSignal(object)
89  InsertKeyframe = pyqtSignal(object)
90 
91  # Save window settings on close
92  def closeEvent(self, event):
93 
94  # Close any tutorial dialogs
95  self.tutorial_manager.exit_manager()
96 
97  # Prompt user to save (if needed)
98  if get_app().project.needs_save() and not self.mode == "unittest":
99  log.info('Prompt user to save project')
100  # Translate object
101  _ = get_app()._tr
102 
103  # Handle exception
104  ret = QMessageBox.question(self, _("Unsaved Changes"), _("Save changes to project before closing?"), QMessageBox.Cancel | QMessageBox.No | QMessageBox.Yes)
105  if ret == QMessageBox.Yes:
106  # Save project
107  self.actionSave_trigger(event)
108  event.accept()
109  elif ret == QMessageBox.Cancel:
110  # User canceled prompt - don't quit
111  event.ignore()
112  return
113 
114  # Save settings
115  self.save_settings()
116 
117  # Track end of session
118  track_metric_session(False)
119 
120  # Stop threads
121  self.StopSignal.emit()
122 
123  # Process any queued events
124  QCoreApplication.processEvents()
125 
126  # Stop preview thread (and wait for it to end)
127  self.preview_thread.player.CloseAudioDevice()
128  self.preview_thread.kill()
129  self.preview_parent.background.exit()
130  self.preview_parent.background.wait(5000)
131 
132  # Close & Stop libopenshot logger
133  openshot.ZmqLogger.Instance().Close()
134  get_app().logger_libopenshot.kill()
135 
136  # Destroy lock file
137  self.destroy_lock_file()
138 
139  ##
140  # Recover the backup file (if any)
141  def recover_backup(self):
142  log.info("recover_backup")
143  # Check for backup.osp file
144  recovery_path = os.path.join(info.BACKUP_PATH, "backup.osp")
145 
146  # Load recovery project
147  if os.path.exists(recovery_path):
148  log.info("Recovering backup file: %s" % recovery_path)
149  self.open_project(recovery_path, clear_thumbnails=False)
150 
151  # Clear the file_path (which is set by saving the project)
152  get_app().project.current_filepath = None
153  get_app().project.has_unsaved_changes = True
154 
155  # Set Window title
156  self.SetWindowTitle()
157 
158  # Show message to user
159  msg = QMessageBox()
160  _ = get_app()._tr
161  msg.setWindowTitle(_("Backup Recovered"))
162  msg.setText(_("Your most recent unsaved project has been recovered."))
163  msg.exec_()
164 
165  else:
166  # No backup project found
167  # Load a blank project (to propagate the default settings)
168  get_app().project.load("")
169  self.actionUndo.setEnabled(False)
170  self.actionRedo.setEnabled(False)
171  self.SetWindowTitle()
172 
173  ##
174  # Create a lock file
175  def create_lock_file(self):
176  lock_path = os.path.join(info.USER_PATH, ".lock")
177  lock_value = str(uuid4())
178 
179  # Check if it already exists
180  if os.path.exists(lock_path):
181  # Walk the libopenshot log (if found), and try and find last line before this launch
182  log_path = os.path.join(info.USER_PATH, "libopenshot.log")
183  last_log_line = ""
184  last_stack_trace = ""
185  found_stack = False
186  log_start_counter = 0
187  if os.path.exists(log_path):
188  with open(log_path, "rb") as f:
189  # Read from bottom up
190  for raw_line in reversed(self.tail_file(f, 500)):
191  line = str(raw_line, 'utf-8')
192  # Detect stack trace
193  if "End of Stack Trace" in line:
194  found_stack = True
195  continue
196  elif "Unhandled Exception: Stack Trace" in line:
197  found_stack = False
198  continue
199  elif "libopenshot logging:" in line:
200  log_start_counter += 1
201  if log_start_counter > 1:
202  # Found the previous log start, too old now
203  break
204 
205  if found_stack:
206  # Append line to beginning of stacktrace
207  last_stack_trace = line + last_stack_trace
208 
209  # Ignore certain unuseful lines
210  if line.strip() and "---" not in line and "libopenshot logging:" not in line and not last_log_line:
211  last_log_line = line
212 
213  # Split last stack trace (if any)
214  if last_stack_trace:
215  # Get top line of stack trace (for metrics)
216  last_log_line = last_stack_trace.split("\n")[0].strip()
217 
218  # Send stacktrace for debugging (if send metrics is enabled)
219  track_exception_stacktrace(last_stack_trace, "libopenshot")
220 
221  # Clear / normalize log line (so we can roll them up in the analytics)
222  if last_log_line:
223  # Format last log line based on OS (since each OS can be formatted differently)
224  if platform.system() == "Darwin":
225  last_log_line = "mac-%s" % last_log_line[58:].strip()
226  elif platform.system() == "Windows":
227  last_log_line = "windows-%s" % last_log_line
228  elif platform.system() == "Linux":
229  last_log_line = "linux-%s" % last_log_line.replace("/usr/local/lib/", "")
230 
231  # Remove '()' from line, and split. Trying to grab the beginning of the log line.
232  last_log_line = last_log_line.replace("()", "")
233  log_parts = last_log_line.split("(")
234  if len(log_parts) == 2:
235  last_log_line = "-%s" % log_parts[0].replace("logger_libopenshot:INFO ", "").strip()[:64]
236  elif len(log_parts) >= 3:
237  last_log_line = "-%s (%s" % (log_parts[0].replace("logger_libopenshot:INFO ", "").strip()[:64], log_parts[1])
238  else:
239  last_log_line = ""
240 
241  # Throw exception (with last libopenshot line... if found)
242  log.error("Unhandled crash detected... will attempt to recover backup project: %s" % info.BACKUP_PATH)
243  track_metric_error("unhandled-crash%s" % last_log_line, True)
244 
245  # Remove file
246  self.destroy_lock_file()
247 
248  else:
249  # Normal startup, clear thumbnails
250  self.clear_all_thumbnails()
251 
252  # Create lock file
253  with open(lock_path, 'w') as f:
254  f.write(lock_value)
255 
256  ##
257  # Destroy the lock file
258  def destroy_lock_file(self):
259  lock_path = os.path.join(info.USER_PATH, ".lock")
260 
261  # Check if it already exists
262  if os.path.exists(lock_path):
263  # Remove file
264  os.remove(lock_path)
265 
266  ##
267  # Read the end of a file (n number of lines)
268  def tail_file(self, f, n, offset=None):
269  avg_line_length = 90
270  to_read = n + (offset or 0)
271 
272  while True:
273  try:
274  # Seek to byte position
275  f.seek(-(avg_line_length * to_read), 2)
276  except IOError:
277  # Byte position not found
278  f.seek(0)
279  pos = f.tell()
280  lines = f.read().splitlines()
281  if len(lines) >= to_read or pos == 0:
282  # Return the lines
283  return lines[-to_read:offset and -offset or None]
284  avg_line_length *= 2
285 
286  def actionNew_trigger(self, event):
287 
288  app = get_app()
289  _ = app._tr # Get translation function
290 
291  # Do we have unsaved changes?
292  if get_app().project.needs_save():
293  ret = QMessageBox.question(self, _("Unsaved Changes"), _("Save changes to project first?"), QMessageBox.Cancel | QMessageBox.No | QMessageBox.Yes)
294  if ret == QMessageBox.Yes:
295  # Save project
296  self.actionSave_trigger(event)
297  elif ret == QMessageBox.Cancel:
298  # User canceled prompt
299  return
300 
301  # Clear any previous thumbnails
302  self.clear_all_thumbnails()
303 
304  # clear data and start new project
305  get_app().project.load("")
306  get_app().updates.reset()
307  self.updateStatusChanged(False, False)
308 
309  # Reset selections
310  self.clearSelections()
311 
312  self.filesTreeView.refresh_view()
313  log.info("New Project created.")
314 
315  # Set Window title
316  self.SetWindowTitle()
317 
318  def actionAnimatedTitle_trigger(self, event):
319  # show dialog
320  from windows.animated_title import AnimatedTitle
321  win = AnimatedTitle()
322  # Run the dialog event loop - blocking interaction on this window during that time
323  result = win.exec_()
324  if result == QDialog.Accepted:
325  log.info('animated title add confirmed')
326  else:
327  log.info('animated title add cancelled')
328 
329  def actionAnimation_trigger(self, event):
330  # show dialog
331  from windows.animation import Animation
332  win = Animation()
333  # Run the dialog event loop - blocking interaction on this window during that time
334  result = win.exec_()
335  if result == QDialog.Accepted:
336  log.info('animation confirmed')
337  else:
338  log.info('animation cancelled')
339 
340  def actionTitle_trigger(self, event):
341  # show dialog
342  from windows.title_editor import TitleEditor
343  win = TitleEditor()
344  # Run the dialog event loop - blocking interaction on this window during that time
345  result = win.exec_()
346  if result == QDialog.Accepted:
347  log.info('title editor add confirmed')
348  else:
349  log.info('title editor add cancelled')
350 
351  def actionEditTitle_trigger(self, event):
352 
353  # Get selected svg title file
354  selected_file_id = self.selected_files[0]
355  file = File.get(id=selected_file_id)
356  file_path = file.data.get("path")
357 
358  # Delete thumbnail for this file (it will be recreated soon)
359  thumb_path = os.path.join(info.THUMBNAIL_PATH, "{}.png".format(file.id))
360 
361  # Check if thumb exists (and delete it)
362  if os.path.exists(thumb_path):
363  os.remove(thumb_path)
364 
365  # show dialog for editing title
366  from windows.title_editor import TitleEditor
367  win = TitleEditor(file_path)
368  # Run the dialog event loop - blocking interaction on this window during that time
369  result = win.exec_()
370 
371  # Force update of files model (which will rebuild missing thumbnails)
372  get_app().window.filesTreeView.refresh_view()
373 
375 
376  # Get selected svg title file
377  selected_file_id = self.selected_files[0]
378  file = File.get(id=selected_file_id)
379  file_path = file.data.get("path")
380 
381  # show dialog for editing title
382  from windows.title_editor import TitleEditor
383  win = TitleEditor(file_path, duplicate=True)
384  # Run the dialog event loop - blocking interaction on this window during that time
385  result = win.exec_()
386 
388  # show dialog
389  from windows.Import_image_seq import ImportImageSeq
390  win = ImportImageSeq()
391  # Run the dialog event loop - blocking interaction on this window during that time
392  result = win.exec_()
393  if result == QDialog.Accepted:
394  log.info('Import image sequence add confirmed')
395  else:
396  log.info('Import image sequence add cancelled')
397 
398  ##
399  # Clear history for current project
400  def actionClearHistory_trigger(self, event):
401  app = get_app()
402  app.updates.reset()
403  log.info('History cleared')
404 
405  ##
406  # Save a project to a file path, and refresh the screen
407  def save_project(self, file_path):
408  app = get_app()
409  _ = app._tr # Get translation function
410 
411  try:
412  # Update history in project data
414  app.updates.save_history(app.project, s.get("history-limit"))
415 
416  # Save project to file
417  app.project.save(file_path)
418 
419  # Set Window title
420  self.SetWindowTitle()
421 
422  # Load recent projects again
423  self.load_recent_menu()
424 
425  log.info("Saved project {}".format(file_path))
426 
427  except Exception as ex:
428  log.error("Couldn't save project %s. %s" % (file_path, str(ex)))
429  QMessageBox.warning(self, _("Error Saving Project"), str(ex))
430 
431  ##
432  # Open a project from a file path, and refresh the screen
433  def open_project(self, file_path, clear_thumbnails=True):
434 
435  app = get_app()
436  _ = app._tr # Get translation function
437 
438  # Set cursor to waiting
439  get_app().setOverrideCursor(QCursor(Qt.WaitCursor))
440 
441  try:
442  if os.path.exists(file_path):
443  # Clear any previous thumbnails
444  if clear_thumbnails:
445  self.clear_all_thumbnails()
446 
447  # Load project file
448  app.project.load(file_path)
449 
450  # Set Window title
451  self.SetWindowTitle()
452 
453  # Reset undo/redo history
454  app.updates.reset()
455  app.updates.load_history(app.project)
456 
457  # Reset selections
458  self.clearSelections()
459 
460  # Refresh file tree
461  self.filesTreeView.refresh_view()
462 
463  # Load recent projects again
464  self.load_recent_menu()
465 
466  log.info("Loaded project {}".format(file_path))
467 
468  except Exception as ex:
469  log.error("Couldn't open project {}".format(file_path))
470  QMessageBox.warning(self, _("Error Opening Project"), str(ex))
471 
472  # Restore normal cursor
473  get_app().restoreOverrideCursor()
474 
475  ##
476  # Clear all user thumbnails
478  try:
479  if os.path.exists(info.THUMBNAIL_PATH):
480  log.info("Clear all thumbnails: %s" % info.THUMBNAIL_PATH)
481  # Remove thumbnail folder
482  shutil.rmtree(info.THUMBNAIL_PATH)
483  # Create thumbnail folder
484  os.mkdir(info.THUMBNAIL_PATH)
485 
486  # Clear any blender animations
487  if os.path.exists(info.BLENDER_PATH):
488  log.info("Clear all animations: %s" % info.BLENDER_PATH)
489  # Remove blender folder
490  shutil.rmtree(info.BLENDER_PATH)
491  # Create blender folder
492  os.mkdir(info.BLENDER_PATH)
493 
494  # Clear any assets folder
495  if os.path.exists(info.ASSETS_PATH):
496  log.info("Clear all assets: %s" % info.ASSETS_PATH)
497  # Remove assets folder
498  shutil.rmtree(info.ASSETS_PATH)
499  # Create assets folder
500  os.mkdir(info.ASSETS_PATH)
501 
502  # Clear any backups
503  if os.path.exists(info.BACKUP_PATH):
504  log.info("Clear all backups: %s" % info.BACKUP_PATH)
505  # Remove backup folder
506  shutil.rmtree(info.BACKUP_PATH)
507  # Create backup folder
508  os.mkdir(info.BACKUP_PATH)
509  except:
510  log.info("Failed to clear thumbnails: %s" % info.THUMBNAIL_PATH)
511 
512  def actionOpen_trigger(self, event):
513  app = get_app()
514  _ = app._tr
515  recommended_path = app.project.current_filepath
516  if not recommended_path:
517  recommended_path = info.HOME_PATH
518 
519  # Do we have unsaved changes?
520  if get_app().project.needs_save():
521  ret = QMessageBox.question(self, _("Unsaved Changes"), _("Save changes to project first?"), QMessageBox.Cancel | QMessageBox.No | QMessageBox.Yes)
522  if ret == QMessageBox.Yes:
523  # Save project
524  self.actionSave_trigger(event)
525  elif ret == QMessageBox.Cancel:
526  # User canceled prompt
527  return
528 
529  # Prompt for open project file
530  file_path, file_type = QFileDialog.getOpenFileName(self, _("Open Project..."), recommended_path, _("OpenShot Project (*.osp)"))
531 
532  # Load project file
533  self.open_project(file_path)
534 
535  def actionSave_trigger(self, event):
536  app = get_app()
537  _ = app._tr
538 
539  # Get current filepath if any, otherwise ask user
540  file_path = app.project.current_filepath
541  if not file_path:
542  recommended_path = os.path.join(info.HOME_PATH, "%s.osp" % _("Untitled Project"))
543  file_path, file_type = QFileDialog.getSaveFileName(self, _("Save Project..."), recommended_path, _("OpenShot Project (*.osp)"))
544 
545  if file_path:
546  # Append .osp if needed
547  if ".osp" not in file_path:
548  file_path = "%s.osp" % file_path
549 
550  # Save project
551  self.save_project(file_path)
552 
553  ##
554  # Auto save the project
555  def auto_save_project(self):
556  log.info("auto_save_project")
557 
558  # Get current filepath (if any)
559  file_path = get_app().project.current_filepath
560  if get_app().project.needs_save():
561  if file_path:
562  # A Real project file exists
563  # Append .osp if needed
564  if ".osp" not in file_path:
565  file_path = "%s.osp" % file_path
566 
567  # Save project
568  log.info("Auto save project file: %s" % file_path)
569  self.save_project(file_path)
570 
571  else:
572  # No saved project found
573  recovery_path = os.path.join(info.BACKUP_PATH, "backup.osp")
574  log.info("Creating backup of project file: %s" % recovery_path)
575  get_app().project.save(recovery_path, move_temp_files=False, make_paths_relative=False)
576 
577  # Clear the file_path (which is set by saving the project)
578  get_app().project.current_filepath = None
579  get_app().project.has_unsaved_changes = True
580 
581  def actionSaveAs_trigger(self, event):
582  app = get_app()
583  _ = app._tr
584 
585  recommended_path = app.project.current_filepath
586  if not recommended_path:
587  recommended_path = os.path.join(info.HOME_PATH, "%s.osp" % _("Untitled Project"))
588  file_path, file_type = QFileDialog.getSaveFileName(self, _("Save Project As..."), recommended_path, _("OpenShot Project (*.osp)"))
589  if file_path:
590  # Append .osp if needed
591  if ".osp" not in file_path:
592  file_path = "%s.osp" % file_path
593 
594  # Save new project
595  self.save_project(file_path)
596 
597  def actionImportFiles_trigger(self, event):
598  app = get_app()
599  _ = app._tr
600  recommended_path = app.project.get(["import_path"])
601  if not recommended_path or not os.path.exists(recommended_path):
602  recommended_path = os.path.join(info.HOME_PATH)
603  files = QFileDialog.getOpenFileNames(self, _("Import File..."), recommended_path)[0]
604  for file_path in files:
605  self.filesTreeView.add_file(file_path)
606  self.filesTreeView.refresh_view()
607  app.updates.update(["import_path"], os.path.dirname(file_path))
608  log.info("Imported media file {}".format(file_path))
609 
611  # Loop through selected files
612  f = None
613  files = []
614  for file_id in self.selected_files:
615  # Find matching file
616  files.append(File.get(id=file_id))
617 
618  # Get current position of playhead
619  fps = get_app().project.get(["fps"])
620  fps_float = float(fps["num"]) / float(fps["den"])
621  pos = (self.preview_thread.player.Position() - 1) / fps_float
622 
623  # show window
624  from windows.add_to_timeline import AddToTimeline
625  win = AddToTimeline(files, pos)
626  # Run the dialog event loop - blocking interaction on this window during this time
627  result = win.exec_()
628  if result == QDialog.Accepted:
629  log.info('confirmed')
630  else:
631  log.info('canceled')
632 
633  def actionUploadVideo_trigger(self, event):
634  # show window
635  from windows.upload_video import UploadVideo
636  win = UploadVideo()
637  # Run the dialog event loop - blocking interaction on this window during this time
638  result = win.exec_()
639  if result == QDialog.Accepted:
640  log.info('Upload Video add confirmed')
641  else:
642  log.info('Upload Video add cancelled')
643 
644  def actionExportVideo_trigger(self, event):
645  # show window
646  from windows.export import Export
647  win = Export()
648  # Run the dialog event loop - blocking interaction on this window during this time
649  result = win.exec_()
650  if result == QDialog.Accepted:
651  log.info('Export Video add confirmed')
652  else:
653  log.info('Export Video add cancelled')
654 
655  def actionUndo_trigger(self, event):
656  log.info('actionUndo_trigger')
657  app = get_app()
658  app.updates.undo()
659 
660  # Update the preview
661  self.refreshFrameSignal.emit()
662 
663  def actionRedo_trigger(self, event):
664  log.info('actionRedo_trigger')
665  app = get_app()
666  app.updates.redo()
667 
668  # Update the preview
669  self.refreshFrameSignal.emit()
670 
671  def actionPreferences_trigger(self, event):
672  # Stop preview thread
673  self.SpeedSignal.emit(0)
674  ui_util.setup_icon(self, self.actionPlay, "actionPlay", "media-playback-start")
675  self.actionPlay.setChecked(False)
676 
677  # Show dialog
678  from windows.preferences import Preferences
679  win = Preferences()
680  # Run the dialog event loop - blocking interaction on this window during this time
681  result = win.exec_()
682  if result == QDialog.Accepted:
683  log.info('Preferences add confirmed')
684  else:
685  log.info('Preferences add cancelled')
686 
687  # Save settings
689  s.save()
690 
691  def actionFilesShowAll_trigger(self, event):
692  self.filesTreeView.refresh_view()
693 
695  self.filesTreeView.refresh_view()
696 
698  self.filesTreeView.refresh_view()
699 
701  self.filesTreeView.refresh_view()
702 
704  self.transitionsTreeView.refresh_view()
705 
707  self.transitionsTreeView.refresh_view()
708 
710  self.effectsTreeView.refresh_view()
711 
713  self.effectsTreeView.refresh_view()
714 
716  self.effectsTreeView.refresh_view()
717 
718  def actionHelpContents_trigger(self, event):
719  try:
720  webbrowser.open("http://%s.openshot.org/files/user-guide/?app-menu" % info.website_language())
721  log.info("Help Contents is open")
722  except:
723  QMessageBox.information(self, "Error !", "Unable to open the Help Contents. Please ensure the openshot-doc package is installed.")
724  log.info("Unable to open the Help Contents")
725 
726  ##
727  # Show about dialog
728  def actionAbout_trigger(self, event):
729  from windows.about import About
730  win = About()
731  # Run the dialog event loop - blocking interaction on this window during this time
732  result = win.exec_()
733  if result == QDialog.Accepted:
734  log.info('About Openshot add confirmed')
735  else:
736  log.info('About Openshot add cancelled')
737 
738  def actionReportBug_trigger(self, event):
739  try:
740  webbrowser.open("https://github.com/OpenShot/openshot-qt/issues/?app-menu-bug")
741  log.info("Open the Bug Report GitHub Issues web page with success")
742  except:
743  QMessageBox.information(self, "Error !", "Unable to open the Bug Report GitHub Issues web page")
744  log.info("Unable to open the Bug Report GitHub Issues web page")
745 
746  def actionAskQuestion_trigger(self, event):
747  try:
748  webbrowser.open("https://github.com/OpenShot/openshot-qt/issues/?app-menu-question")
749  log.info("Open the Questions GitHub Issues web page with success")
750  except:
751  QMessageBox.information(self, "Error !", "Unable to open the Questions GitHub Issues web page")
752  log.info("Unable to open the Questions GitHub Issues web page")
753 
754  def actionTranslate_trigger(self, event):
755  try:
756  webbrowser.open("https://translations.launchpad.net/openshot/2.0")
757  log.info("Open the Translate launchpad web page with success")
758  except:
759  QMessageBox.information(self, "Error !", "Unable to open the Translation web page")
760  log.info("Unable to open the Translation web page")
761 
762  def actionDonate_trigger(self, event):
763  try:
764  webbrowser.open("http://%s.openshot.org/donate/?app-menu" % info.website_language())
765  log.info("Open the Donate web page with success")
766  except:
767  QMessageBox.information(self, "Error !", "Unable to open the Donate web page")
768  log.info("Unable to open the Donate web page")
769 
770  def actionUpdate_trigger(self, event):
771  try:
772  webbrowser.open("http://%s.openshot.org/download/?app-toolbar" % info.website_language())
773  log.info("Open the Download web page with success")
774  except:
775  QMessageBox.information(self, "Error !", "Unable to open the Download web page")
776  log.info("Unable to open the Download web page")
777 
778  def actionPlay_trigger(self, event, force=None):
779 
780  # Determine max frame (based on clips)
781  timeline_length = 0.0
782  fps = get_app().window.timeline_sync.timeline.info.fps.ToFloat()
783  clips = get_app().window.timeline_sync.timeline.Clips()
784  for clip in clips:
785  clip_last_frame = clip.Position() + clip.Duration()
786  if clip_last_frame > timeline_length:
787  # Set max length of timeline
788  timeline_length = clip_last_frame
789 
790  # Convert to int and round
791  timeline_length_int = round(timeline_length * fps) + 1
792 
793  if force == "pause":
794  self.actionPlay.setChecked(False)
795  elif force == "play":
796  self.actionPlay.setChecked(True)
797 
798  if self.actionPlay.isChecked():
799  ui_util.setup_icon(self, self.actionPlay, "actionPlay", "media-playback-pause")
800  self.PlaySignal.emit(timeline_length_int)
801 
802  else:
803  ui_util.setup_icon(self, self.actionPlay, "actionPlay") # to default
804  self.PauseSignal.emit()
805 
806  ##
807  # Preview the selected media file
808  def actionPreview_File_trigger(self, event):
809  log.info('actionPreview_File_trigger')
810 
811  # Loop through selected files (set 1 selected file if more than 1)
812  f = None
813  for file_id in self.selected_files:
814  # Find matching file
815  f = File.get(id=file_id)
816 
817  # Bail out if no file selected
818  if not f:
819  log.info(self.selected_files)
820  return
821 
822  # show dialog
823  from windows.cutting import Cutting
824  win = Cutting(f, preview=True)
825  win.show()
826 
827  ##
828  # Preview a specific frame
829  def previewFrame(self, position_seconds, position_frames, time_code):
830  # Notify preview thread
831  self.previewFrameSignal.emit(position_frames)
832 
833  # Notify properties dialog
834  self.propertyTableView.select_frame(position_frames)
835 
836  ##
837  # Handle the pause signal, by refreshing the properties dialog
838  def handlePausedVideo(self):
839  self.propertyTableView.select_frame(self.preview_thread.player.Position())
840 
841  ##
842  # Update playhead position
843  def movePlayhead(self, position_frames):
844  # Notify preview thread
845  self.timeline.movePlayhead(position_frames)
846 
847  def actionFastForward_trigger(self, event):
848 
849  # Get the video player object
850  player = self.preview_thread.player
851 
852  if player.Speed() + 1 != 0:
853  self.SpeedSignal.emit(player.Speed() + 1)
854  else:
855  self.SpeedSignal.emit(player.Speed() + 2)
856 
857  if player.Mode() == openshot.PLAYBACK_PAUSED:
858  self.actionPlay.trigger()
859 
860  def actionRewind_trigger(self, event):
861 
862  # Get the video player object
863  player = self.preview_thread.player
864 
865  if player.Speed() - 1 != 0:
866  self.SpeedSignal.emit(player.Speed() - 1)
867  else:
868  self.SpeedSignal.emit(player.Speed() - 2)
869 
870  if player.Mode() == openshot.PLAYBACK_PAUSED:
871  self.actionPlay.trigger()
872 
873  def actionJumpStart_trigger(self, event):
874  log.info("actionJumpStart_trigger")
875 
876  # Seek to the 1st frame
877  self.SeekSignal.emit(1)
878 
879  def actionJumpEnd_trigger(self, event):
880  log.info("actionJumpEnd_trigger")
881 
882  # Determine max frame (based on clips)
883  timeline_length = 0.0
884  fps = get_app().window.timeline_sync.timeline.info.fps.ToFloat()
885  clips = get_app().window.timeline_sync.timeline.Clips()
886  for clip in clips:
887  clip_last_frame = clip.Position() + clip.Duration()
888  if clip_last_frame > timeline_length:
889  # Set max length of timeline
890  timeline_length = clip_last_frame
891 
892  # Convert to int and round
893  timeline_length_int = round(timeline_length * fps) + 1
894 
895  # Seek to the 1st frame
896  self.SeekSignal.emit(timeline_length_int)
897 
898  def actionAddTrack_trigger(self, event):
899  log.info("actionAddTrack_trigger")
900 
901  # Get # of tracks
902  track_number = len(get_app().project.get(["layers"]))
903 
904  # Create new track above existing layer(s)
905  track = Track()
906  track.data = {"number": track_number, "y": 0, "label": "", "lock": False}
907  track.save()
908 
909  def actionAddTrackAbove_trigger(self, event):
910  log.info("actionAddTrackAbove_trigger")
911 
912  # Get # of tracks
913  max_track_number = len(get_app().project.get(["layers"]))
914  selected_layer_id = self.selected_tracks[0]
915 
916  # Get selected track data
917  existing_track = Track.get(id=selected_layer_id)
918  selected_layer_number = int(existing_track.data["number"])
919 
920  # log.info("Adding track above #{} (id {})".format(selected_layer_number, selected_layer_id))
921 
922  # Loop through tracks above insert point (in descending order), renumbering layers
923  for existing_layer in list(reversed(range(selected_layer_number+1, max_track_number))):
924  existing_track = Track.get(number=existing_layer)
925  # log.info("Renumbering track id {} from {} to {}".format(existing_track.data["id"], existing_layer, existing_layer+1))
926  existing_track.data["number"] = existing_layer + 1
927  existing_track.save()
928 
929  # Loop through clips and transitions for track, moving up to new layer
930  for clip in Clip.filter(layer=existing_layer):
931  # log.info("Moving clip id {} from layer {} to {}".format(clip.data["id"], int(clip.data["layer"]), int(clip.data["layer"])+1))
932  clip.data["layer"] = int(clip.data["layer"]) + 1
933  clip.save()
934 
935  for trans in Transition.filter(layer=existing_layer):
936  # log.info("Moving transition id {} from layer {} to {}".format(trans.data["id"], int(trans.data["layer"]), int(trans.data["layer"])+1))
937  trans.data["layer"] = int(trans.data["layer"]) + 1
938  trans.save()
939 
940  # Create new track at vacated layer
941  track = Track()
942  track.data = {"number": selected_layer_number+1, "y": 0, "label": "", "lock": False}
943  track.save()
944  # log.info("Created new track id {} at layer number {}".format(track.data["id"], track.data["number"]))
945 
946  def actionAddTrackBelow_trigger(self, event):
947  log.info("actionAddTrackBelow_trigger")
948 
949  # Get # of tracks
950  max_track_number = len(get_app().project.get(["layers"]))
951  selected_layer_id = self.selected_tracks[0]
952 
953  # Get selected track data
954  existing_track = Track.get(id=selected_layer_id)
955  selected_layer_number = int(existing_track.data["number"])
956 
957  # log.info("Adding track below #{} (id {})".format(selected_layer_number, selected_layer_id))
958 
959  # Loop through tracks from insert point up (in descending order), renumbering layers
960  for existing_layer in list(reversed(range(selected_layer_number, max_track_number))):
961  existing_track = Track.get(number=existing_layer)
962  # log.info("Renumbering track id {} from {} to {}".format(existing_track.data["id"], existing_layer, existing_layer+1))
963  existing_track.data["number"] = existing_layer + 1
964  existing_track.save()
965 
966  # Loop through clips and transitions for track, moving up to new layer
967  for clip in Clip.filter(layer=existing_layer):
968  # log.info("Moving clip id {} from layer {} to {}".format(clip.data["id"], int(clip.data["layer"]), int(clip.data["layer"])+1))
969  clip.data["layer"] = int(clip.data["layer"]) + 1
970  clip.save()
971 
972  for trans in Transition.filter(layer=existing_layer):
973  # log.info("Moving transition id {} from layer {} to {}".format(trans.data["id"], int(trans.data["layer"]), int(trans.data["layer"])+1))
974  trans.data["layer"] = int(trans.data["layer"]) + 1
975  trans.save()
976 
977  # Create new track at vacated layer
978  track = Track()
979  track.data = {"number": selected_layer_number, "y": 0, "label": "", "lock": False}
980  track.save()
981  # log.info("Created new track id {} at layer number {}".format(track.data["id"], track.data["number"]))
982 
983  def actionArrowTool_trigger(self, event):
984  log.info("actionArrowTool_trigger")
985 
986  def actionSnappingTool_trigger(self, event):
987  log.info("actionSnappingTool_trigger")
988  log.info(self.actionSnappingTool.isChecked())
989 
990  # Enable / Disable snapping mode
991  self.timeline.SetSnappingMode(self.actionSnappingTool.isChecked())
992 
993  ##
994  # Toggle razor tool on and off
995  def actionRazorTool_trigger(self, event):
996  log.info('actionRazorTool_trigger')
997 
998  # Enable / Disable razor mode
999  self.timeline.SetRazorMode(self.actionRazorTool.isChecked())
1000 
1001  def actionAddMarker_trigger(self, event):
1002  log.info("actionAddMarker_trigger")
1003 
1004  # Get player object
1005  player = self.preview_thread.player
1006 
1007  # Calculate frames per second
1008  fps = get_app().project.get(["fps"])
1009  fps_float = float(fps["num"]) / float(fps["den"])
1010 
1011  # Calculate position in seconds
1012  position = (player.Position() - 1) / fps_float
1013 
1014  # Look for existing Marker
1015  marker = Marker()
1016  marker.data = {"position": position, "icon": "blue.png"}
1017  marker.save()
1018 
1020  log.info("actionPreviousMarker_trigger")
1021 
1022  # Calculate current position (in seconds)
1023  fps = get_app().project.get(["fps"])
1024  fps_float = float(fps["num"]) / float(fps["den"])
1025  current_position = (self.preview_thread.current_frame - 1) / fps_float
1026  all_marker_positions = []
1027 
1028  # Get list of marker and important positions (like selected clip bounds)
1029  for marker in Marker.filter():
1030  all_marker_positions.append(marker.data["position"])
1031 
1032  # Loop through selected clips (and add key positions)
1033  for clip_id in self.selected_clips:
1034  # Get selected object
1035  selected_clip = Clip.get(id=clip_id)
1036  if selected_clip:
1037  all_marker_positions.append(selected_clip.data["position"])
1038  all_marker_positions.append(selected_clip.data["position"] + (selected_clip.data["end"] - selected_clip.data["start"]))
1039 
1040  # Loop through selected transitions (and add key positions)
1041  for tran_id in self.selected_transitions:
1042  # Get selected object
1043  selected_tran = Transition.get(id=tran_id)
1044  if selected_tran:
1045  all_marker_positions.append(selected_tran.data["position"])
1046  all_marker_positions.append(selected_tran.data["position"] + (selected_tran.data["end"] - selected_tran.data["start"]))
1047 
1048  # Loop through all markers, and find the closest one to the left
1049  closest_position = None
1050  for marker_position in sorted(all_marker_positions):
1051  # Is marker smaller than position?
1052  if marker_position < current_position and (abs(marker_position - current_position) > 0.1):
1053  # Is marker larger than previous marker
1054  if closest_position and marker_position > closest_position:
1055  # Set a new closest marker
1056  closest_position = marker_position
1057  elif not closest_position:
1058  # First one found
1059  closest_position = marker_position
1060 
1061  # Seek to marker position (if any)
1062  if closest_position != None:
1063  # Seek
1064  frame_to_seek = round(closest_position * fps_float) + 1
1065  self.SeekSignal.emit(frame_to_seek)
1066 
1067  # Update the preview and reselct current frame in properties
1068  get_app().window.refreshFrameSignal.emit()
1069  get_app().window.propertyTableView.select_frame(frame_to_seek)
1070 
1071  def actionNextMarker_trigger(self, event):
1072  log.info("actionNextMarker_trigger")
1073  log.info(self.preview_thread.current_frame)
1074 
1075  # Calculate current position (in seconds)
1076  fps = get_app().project.get(["fps"])
1077  fps_float = float(fps["num"]) / float(fps["den"])
1078  current_position = (self.preview_thread.current_frame - 1) / fps_float
1079  all_marker_positions = []
1080 
1081  # Get list of marker and important positions (like selected clip bounds)
1082  for marker in Marker.filter():
1083  all_marker_positions.append(marker.data["position"])
1084 
1085  # Loop through selected clips (and add key positions)
1086  for clip_id in self.selected_clips:
1087  # Get selected object
1088  selected_clip = Clip.get(id=clip_id)
1089  if selected_clip:
1090  all_marker_positions.append(selected_clip.data["position"])
1091  all_marker_positions.append(selected_clip.data["position"] + (selected_clip.data["end"] - selected_clip.data["start"]))
1092 
1093  # Loop through selected transitions (and add key positions)
1094  for tran_id in self.selected_transitions:
1095  # Get selected object
1096  selected_tran = Transition.get(id=tran_id)
1097  if selected_tran:
1098  all_marker_positions.append(selected_tran.data["position"])
1099  all_marker_positions.append(selected_tran.data["position"] + (selected_tran.data["end"] - selected_tran.data["start"]))
1100 
1101  # Loop through all markers, and find the closest one to the right
1102  closest_position = None
1103  for marker_position in sorted(all_marker_positions):
1104  # Is marker smaller than position?
1105  if marker_position > current_position and (abs(marker_position - current_position) > 0.1):
1106  # Is marker larger than previous marker
1107  if closest_position and marker_position < closest_position:
1108  # Set a new closest marker
1109  closest_position = marker_position
1110  elif not closest_position:
1111  # First one found
1112  closest_position = marker_position
1113 
1114  # Seek to marker position (if any)
1115  if closest_position != None:
1116  # Seek
1117  frame_to_seek = round(closest_position * fps_float) + 1
1118  self.SeekSignal.emit(frame_to_seek)
1119 
1120  # Update the preview and reselct current frame in properties
1121  get_app().window.refreshFrameSignal.emit()
1122  get_app().window.propertyTableView.select_frame(frame_to_seek)
1123 
1124  ##
1125  # Get a key sequence back from the setting name
1126  def getShortcutByName(self, setting_name):
1127  s = settings.get_settings()
1128  shortcut = QKeySequence(s.get(setting_name))
1129  return shortcut
1130 
1131  ##
1132  # Get a key sequence back from the setting name
1134  keyboard_shortcuts = []
1135  all_settings = settings.get_settings()._data
1136  for setting in all_settings:
1137  if setting.get('category') == 'Keyboard':
1138  keyboard_shortcuts.append(setting)
1139  return keyboard_shortcuts
1140 
1141  ##
1142  # Process key press events and match with known shortcuts
1143  def keyPressEvent(self, event):
1144  # Detect the current KeySequence pressed (including modifier keys)
1145  key_value = event.key()
1146  print(key_value)
1147  modifiers = int(event.modifiers())
1148  if (key_value > 0 and key_value != Qt.Key_Shift and key_value != Qt.Key_Alt and
1149  key_value != Qt.Key_Control and key_value != Qt.Key_Meta):
1150  # A valid keysequence was detected
1151  key = QKeySequence(modifiers + key_value)
1152  else:
1153  # No valid keysequence detected
1154  return
1155 
1156  # Debug
1157  log.info("keyPressEvent: %s" % (key.toString()))
1158 
1159  # Get the video player object
1160  player = self.preview_thread.player
1161 
1162  # Get framerate
1163  fps = get_app().project.get(["fps"])
1164  fps_float = float(fps["num"]) / float(fps["den"])
1165  playhead_position = float(self.preview_thread.current_frame - 1) / fps_float
1166 
1167  # Basic shortcuts i.e just a letter
1168  if key.matches(self.getShortcutByName("seekPreviousFrame")) == QKeySequence.ExactMatch:
1169  # Pause video
1170  self.actionPlay_trigger(event, force="pause")
1171  # Set speed to 0
1172  if player.Speed() != 0:
1173  self.SpeedSignal.emit(0)
1174  # Seek to previous frame
1175  self.SeekSignal.emit(player.Position() - 1)
1176 
1177  # Notify properties dialog
1178  self.propertyTableView.select_frame(player.Position())
1179 
1180  elif key.matches(self.getShortcutByName("seekNextFrame")) == QKeySequence.ExactMatch:
1181  # Pause video
1182  self.actionPlay_trigger(event, force="pause")
1183  # Set speed to 0
1184  if player.Speed() != 0:
1185  self.SpeedSignal.emit(0)
1186  # Seek to next frame
1187  self.SeekSignal.emit(player.Position() + 1)
1188 
1189  # Notify properties dialog
1190  self.propertyTableView.select_frame(player.Position())
1191 
1192  elif key.matches(self.getShortcutByName("rewindVideo")) == QKeySequence.ExactMatch:
1193  # Toggle rewind and start playback
1194  self.actionRewind.trigger()
1195  ui_util.setup_icon(self, self.actionPlay, "actionPlay", "media-playback-pause")
1196  self.actionPlay.setChecked(True)
1197 
1198  elif key.matches(self.getShortcutByName("fastforwardVideo")) == QKeySequence.ExactMatch:
1199  # Toggle fastforward button and start playback
1200  self.actionFastForward.trigger()
1201  ui_util.setup_icon(self, self.actionPlay, "actionPlay", "media-playback-pause")
1202  self.actionPlay.setChecked(True)
1203 
1204  elif key.matches(self.getShortcutByName("playToggle")) == QKeySequence.ExactMatch or \
1205  key.matches(self.getShortcutByName("playToggle1")) == QKeySequence.ExactMatch or \
1206  key.matches(self.getShortcutByName("playToggle2")) == QKeySequence.ExactMatch or \
1207  key.matches(self.getShortcutByName("playToggle3")) == QKeySequence.ExactMatch:
1208  # Toggle playbutton and show properties
1209  self.actionPlay.trigger()
1210  self.propertyTableView.select_frame(player.Position())
1211 
1212  elif key.matches(self.getShortcutByName("deleteItem")) == QKeySequence.ExactMatch or \
1213  key.matches(self.getShortcutByName("deleteItem1")) == QKeySequence.ExactMatch:
1214  # Delete selected clip / transition
1215  self.actionRemoveClip.trigger()
1216  self.actionRemoveTransition.trigger()
1217 
1218  # Boiler plate key mappings (mostly for menu support on Ubuntu/Unity)
1219  elif key.matches(self.getShortcutByName("actionNew")) == QKeySequence.ExactMatch:
1220  self.actionNew.trigger()
1221  elif key.matches(self.getShortcutByName("actionOpen")) == QKeySequence.ExactMatch:
1222  self.actionOpen.trigger()
1223  elif key.matches(self.getShortcutByName("actionSave")) == QKeySequence.ExactMatch:
1224  self.actionSave.trigger()
1225  elif key.matches(self.getShortcutByName("actionUndo")) == QKeySequence.ExactMatch:
1226  self.actionUndo.trigger()
1227  elif key.matches(self.getShortcutByName("actionSaveAs")) == QKeySequence.ExactMatch:
1228  self.actionSaveAs.trigger()
1229  elif key.matches(self.getShortcutByName("actionImportFiles")) == QKeySequence.ExactMatch:
1230  self.actionImportFiles.trigger()
1231  elif key.matches(self.getShortcutByName("actionRedo")) == QKeySequence.ExactMatch:
1232  self.actionRedo.trigger()
1233  elif key.matches(self.getShortcutByName("actionExportVideo")) == QKeySequence.ExactMatch:
1234  self.actionExportVideo.trigger()
1235  elif key.matches(self.getShortcutByName("actionQuit")) == QKeySequence.ExactMatch:
1236  self.actionQuit.trigger()
1237  elif key.matches(self.getShortcutByName("actionPreferences")) == QKeySequence.ExactMatch:
1238  self.actionPreferences.trigger()
1239  elif key.matches(self.getShortcutByName("actionAddTrack")) == QKeySequence.ExactMatch:
1240  self.actionAddTrack.trigger()
1241  elif key.matches(self.getShortcutByName("actionAddMarker")) == QKeySequence.ExactMatch:
1242  self.actionAddMarker.trigger()
1243  elif key.matches(self.getShortcutByName("actionPreviousMarker")) == QKeySequence.ExactMatch:
1244  self.actionPreviousMarker.trigger()
1245  elif key.matches(self.getShortcutByName("actionNextMarker")) == QKeySequence.ExactMatch:
1246  self.actionNextMarker.trigger()
1247  elif key.matches(self.getShortcutByName("actionTimelineZoomIn")) == QKeySequence.ExactMatch:
1248  self.actionTimelineZoomIn.trigger()
1249  elif key.matches(self.getShortcutByName("actionTimelineZoomOut")) == QKeySequence.ExactMatch:
1250  self.actionTimelineZoomOut.trigger()
1251  elif key.matches(self.getShortcutByName("actionTitle")) == QKeySequence.ExactMatch:
1252  self.actionTitle.trigger()
1253  elif key.matches(self.getShortcutByName("actionAnimatedTitle")) == QKeySequence.ExactMatch:
1254  self.actionAnimatedTitle.trigger()
1255  elif key.matches(self.getShortcutByName("actionFullscreen")) == QKeySequence.ExactMatch:
1256  self.actionFullscreen.trigger()
1257  elif key.matches(self.getShortcutByName("actionAbout")) == QKeySequence.ExactMatch:
1258  self.actionAbout.trigger()
1259  elif key.matches(self.getShortcutByName("actionThumbnailView")) == QKeySequence.ExactMatch:
1260  self.actionThumbnailView.trigger()
1261  elif key.matches(self.getShortcutByName("actionDetailsView")) == QKeySequence.ExactMatch:
1262  self.actionDetailsView.trigger()
1263  elif key.matches(self.getShortcutByName("actionProfile")) == QKeySequence.ExactMatch:
1264  self.actionProfile.trigger()
1265  elif key.matches(self.getShortcutByName("actionAdd_to_Timeline")) == QKeySequence.ExactMatch:
1266  self.actionAdd_to_Timeline.trigger()
1267  elif key.matches(self.getShortcutByName("actionSplitClip")) == QKeySequence.ExactMatch:
1268  self.actionSplitClip.trigger()
1269  elif key.matches(self.getShortcutByName("actionSnappingTool")) == QKeySequence.ExactMatch:
1270  self.actionSnappingTool.trigger()
1271  elif key.matches(self.getShortcutByName("actionJumpStart")) == QKeySequence.ExactMatch:
1272  self.actionJumpStart.trigger()
1273  elif key.matches(self.getShortcutByName("actionJumpEnd")) == QKeySequence.ExactMatch:
1274  self.actionJumpEnd.trigger()
1275  elif key.matches(self.getShortcutByName("actionProperties")) == QKeySequence.ExactMatch:
1276  self.actionProperties.trigger()
1277  elif key.matches(self.getShortcutByName("actionTransform")) == QKeySequence.ExactMatch:
1278  if not self.is_transforming and self.selected_clips:
1279  self.TransformSignal.emit(self.selected_clips[0])
1280  else:
1281  self.TransformSignal.emit("")
1282 
1283  elif key.matches(self.getShortcutByName("actionInsertKeyframe")) == QKeySequence.ExactMatch:
1284  print("actionInsertKeyframe")
1285  if self.selected_clips or self.selected_transitions:
1286  self.InsertKeyframe.emit(event)
1287 
1288  # Timeline keyboard shortcuts
1289  elif key.matches(self.getShortcutByName("sliceAllKeepBothSides")) == QKeySequence.ExactMatch:
1290  intersecting_clips = Clip.filter(intersect=playhead_position)
1291  intersecting_trans = Transition.filter(intersect=playhead_position)
1292  if intersecting_clips or intersecting_trans:
1293  # Get list of clip ids
1294  clip_ids = [c.id for c in intersecting_clips]
1295  trans_ids = [t.id for t in intersecting_trans]
1296  self.timeline.Slice_Triggered(0, clip_ids, trans_ids, playhead_position)
1297  elif key.matches(self.getShortcutByName("sliceAllKeepLeftSide")) == QKeySequence.ExactMatch:
1298  intersecting_clips = Clip.filter(intersect=playhead_position)
1299  intersecting_trans = Transition.filter(intersect=playhead_position)
1300  if intersecting_clips or intersecting_trans:
1301  # Get list of clip ids
1302  clip_ids = [c.id for c in intersecting_clips]
1303  trans_ids = [t.id for t in intersecting_trans]
1304  self.timeline.Slice_Triggered(1, clip_ids, trans_ids, playhead_position)
1305  elif key.matches(self.getShortcutByName("sliceAllKeepRightSide")) == QKeySequence.ExactMatch:
1306  intersecting_clips = Clip.filter(intersect=playhead_position)
1307  intersecting_trans = Transition.filter(intersect=playhead_position)
1308  if intersecting_clips or intersecting_trans:
1309  # Get list of clip ids
1310  clip_ids = [c.id for c in intersecting_clips]
1311  trans_ids = [t.id for t in intersecting_trans]
1312  self.timeline.Slice_Triggered(2, clip_ids, trans_ids, playhead_position)
1313  elif key.matches(self.getShortcutByName("copyAll")) == QKeySequence.ExactMatch:
1314  self.timeline.Copy_Triggered(-1, self.selected_clips, self.selected_transitions)
1315  elif key.matches(self.getShortcutByName("pasteAll")) == QKeySequence.ExactMatch:
1316  self.timeline.Paste_Triggered(9, float(playhead_position), -1, [], [])
1317 
1318  # Select All / None
1319  elif key.matches(self.getShortcutByName("selectAll")) == QKeySequence.ExactMatch:
1320  self.timeline.SelectAll()
1321 
1322  elif key.matches(self.getShortcutByName("selectNone")) == QKeySequence.ExactMatch:
1323  self.timeline.ClearAllSelections()
1324 
1325  # Bubble event on
1326  event.ignore()
1327 
1328 
1329  def actionProfile_trigger(self, event):
1330  # Show dialog
1331  from windows.profile import Profile
1332  win = Profile()
1333  # Run the dialog event loop - blocking interaction on this window during this time
1334  result = win.exec_()
1335  if result == QDialog.Accepted:
1336  log.info('Profile add confirmed')
1337 
1338 
1339  def actionSplitClip_trigger(self, event):
1340  log.info("actionSplitClip_trigger")
1341 
1342  # Loop through selected files (set 1 selected file if more than 1)
1343  f = None
1344  for file_id in self.selected_files:
1345  # Find matching file
1346  f = File.get(id=file_id)
1347 
1348  # Bail out if no file selected
1349  if not f:
1350  log.info(self.selected_files)
1351  return
1352 
1353  # show dialog
1354  from windows.cutting import Cutting
1355  win = Cutting(f)
1356  # Run the dialog event loop - blocking interaction on this window during that time
1357  result = win.exec_()
1358  if result == QDialog.Accepted:
1359  log.info('Cutting Finished')
1360  else:
1361  log.info('Cutting Cancelled')
1362 
1364  log.info("actionRemove_from_Project_trigger")
1365 
1366  # Loop through selected files
1367  for file_id in self.selected_files:
1368  # Find matching file
1369  f = File.get(id=file_id)
1370  if f:
1371  # Remove file
1372  f.delete()
1373 
1374  # Find matching clips (if any)
1375  clips = Clip.filter(file_id=file_id)
1376  for c in clips:
1377  # Remove clip
1378  c.delete()
1379 
1380  # Clear selected files
1381  self.selected_files = []
1382 
1383  def actionRemoveClip_trigger(self, event):
1384  log.info('actionRemoveClip_trigger')
1385 
1386  # Loop through selected clips
1387  for clip_id in deepcopy(self.selected_clips):
1388  # Find matching file
1389  clips = Clip.filter(id=clip_id)
1390  for c in clips:
1391  # Clear selected clips
1392  self.removeSelection(clip_id, "clip")
1393 
1394  # Remove clip
1395  c.delete()
1396 
1397  def actionProperties_trigger(self, event):
1398  log.info('actionProperties_trigger')
1399 
1400  # Show properties dock
1401  if not self.dockProperties.isVisible():
1402  self.dockProperties.show()
1403 
1404  def actionRemoveEffect_trigger(self, event):
1405  log.info('actionRemoveEffect_trigger')
1406 
1407  # Loop through selected clips
1408  for effect_id in deepcopy(self.selected_effects):
1409  log.info("effect id: %s" % effect_id)
1410 
1411  # Find matching file
1412  clips = Clip.filter()
1413  found_effect = None
1414  for c in clips:
1415  found_effect = False
1416  log.info("c.data[effects]: %s" % c.data["effects"])
1417 
1418  for effect in c.data["effects"]:
1419  if effect["id"] == effect_id:
1420  found_effect = effect
1421  break
1422 
1423  if found_effect:
1424  # Remove found effect from clip data and save clip
1425  c.data["effects"].remove(found_effect)
1426 
1427  # Remove unneeded attributes from JSON
1428  c.data.pop("reader")
1429 
1430  # Save clip
1431  c.save()
1432 
1433  # Clear selected effects
1434  self.removeSelection(effect_id, "effect")
1435 
1437  log.info('actionRemoveTransition_trigger')
1438 
1439  # Loop through selected clips
1440  for tran_id in deepcopy(self.selected_transitions):
1441  # Find matching file
1442  transitions = Transition.filter(id=tran_id)
1443  for t in transitions:
1444  # Clear selected clips
1445  self.removeSelection(tran_id, "transition")
1446 
1447  # Remove transition
1448  t.delete()
1449 
1450  def actionRemoveTrack_trigger(self, event):
1451  log.info('actionRemoveTrack_trigger')
1452 
1453  # Get translation function
1454  _ = get_app()._tr
1455 
1456  track_id = self.selected_tracks[0]
1457  max_track_number = len(get_app().project.get(["layers"]))
1458 
1459  # Get details of selected track
1460  selected_track = Track.get(id=track_id)
1461  selected_track_number = int(selected_track.data["number"])
1462 
1463  # Don't allow user to delete final track
1464  if max_track_number == 1:
1465  # Show error and do nothing
1466  QMessageBox.warning(self, _("Error Removing Track"), _("You must keep at least 1 track"))
1467  return
1468 
1469  # Revove all clips on this track first
1470  for clip in Clip.filter(layer=selected_track_number):
1471  clip.delete()
1472 
1473  # Revove all transitions on this track first
1474  for trans in Transition.filter(layer=selected_track_number):
1475  trans.delete()
1476 
1477  # Remove track
1478  selected_track.delete()
1479 
1480  # Loop through all layers above, and renumber elements (to keep thing in numerical order)
1481  for existing_layer in list(range(selected_track_number + 1, max_track_number)):
1482  # Update existing layer number
1483  track = Track.get(number=existing_layer)
1484  track.data["number"] = existing_layer - 1
1485  track.save()
1486 
1487  for clip in Clip.filter(layer=existing_layer):
1488  clip.data["layer"] = int(clip.data["layer"]) - 1
1489  clip.save()
1490 
1491  for trans in Transition.filter(layer=existing_layer):
1492  trans.data["layer"] = int(trans.data["layer"]) - 1
1493  trans.save()
1494 
1495 
1496  # Clear selected track
1498 
1499  ##
1500  # Callback for locking a track
1501  def actionLockTrack_trigger(self, event):
1502  log.info('actionLockTrack_trigger')
1503 
1504  # Get details of track
1505  track_id = self.selected_tracks[0]
1506  selected_track = Track.get(id=track_id)
1507 
1508  # Lock track and save
1509  selected_track.data['lock'] = True
1510  selected_track.save()
1511 
1512  ##
1513  # Callback for unlocking a track
1514  def actionUnlockTrack_trigger(self, event):
1515  log.info('actionUnlockTrack_trigger')
1516 
1517  # Get details of track
1518  track_id = self.selected_tracks[0]
1519  selected_track = Track.get(id=track_id)
1520 
1521  # Lock track and save
1522  selected_track.data['lock'] = False
1523  selected_track.save()
1524 
1525  ##
1526  # Callback for renaming track
1527  def actionRenameTrack_trigger(self, event):
1528  log.info('actionRenameTrack_trigger')
1529 
1530  # Get translation function
1531  _ = get_app()._tr
1532 
1533  # Get details of track
1534  track_id = self.selected_tracks[0]
1535  selected_track = Track.get(id=track_id)
1536  track_name = selected_track.data["label"] or _("Track %s") % selected_track.data["number"]
1537 
1538  text, ok = QInputDialog.getText(self, _('Rename Track'), _('Track Name:'), text=track_name)
1539  if ok:
1540  # Update track
1541  selected_track.data["label"] = text
1542  selected_track.save()
1543 
1544  def actionRemoveMarker_trigger(self, event):
1545  log.info('actionRemoveMarker_trigger')
1546 
1547  for marker_id in self.selected_markers:
1548  marker = Marker.filter(id=marker_id)
1549  for m in marker:
1550  # Remove track
1551  m.delete()
1552 
1554  self.sliderZoom.setValue(self.sliderZoom.value() - self.sliderZoom.singleStep())
1555 
1557  self.sliderZoom.setValue(self.sliderZoom.value() + self.sliderZoom.singleStep())
1558 
1559  def actionFullscreen_trigger(self, event):
1560  # Toggle fullscreen mode
1561  if not self.isFullScreen():
1562  self.showFullScreen()
1563  else:
1564  self.showNormal()
1565 
1567  log.info("Show file properties")
1568 
1569  # Loop through selected files (set 1 selected file if more than 1)
1570  f = None
1571  for file_id in self.selected_files:
1572  # Find matching file
1573  f = File.get(id=file_id)
1574 
1575  # show dialog
1576  from windows.file_properties import FileProperties
1577  win = FileProperties(f)
1578  # Run the dialog event loop - blocking interaction on this window during that time
1579  result = win.exec_()
1580  if result == QDialog.Accepted:
1581  log.info('File Properties Finished')
1582  else:
1583  log.info('File Properties Cancelled')
1584 
1585  def actionDetailsView_trigger(self, event):
1586  log.info("Switch to Details View")
1587 
1588  # Get settings
1589  app = get_app()
1590  s = settings.get_settings()
1591 
1592  # Prepare treeview for deletion
1593  if self.filesTreeView:
1594  self.filesTreeView.prepare_for_delete()
1595 
1596  # Files
1597  if app.context_menu_object == "files":
1598  s.set("file_view", "details")
1599  self.tabFiles.layout().removeWidget(self.filesTreeView)
1600  self.filesTreeView.deleteLater()
1601  self.filesTreeView = None
1602  self.filesTreeView = FilesTreeView(self)
1603  self.tabFiles.layout().addWidget(self.filesTreeView)
1604 
1605  # Transitions
1606  elif app.context_menu_object == "transitions":
1607  s.set("transitions_view", "details")
1608  self.tabTransitions.layout().removeWidget(self.transitionsTreeView)
1609  self.transitionsTreeView.deleteLater()
1611  self.transitionsTreeView = TransitionsTreeView(self)
1612  self.tabTransitions.layout().addWidget(self.transitionsTreeView)
1613 
1614  # Effects
1615  elif app.context_menu_object == "effects":
1616  s.set("effects_view", "details")
1617  self.tabEffects.layout().removeWidget(self.effectsTreeView)
1618  self.effectsTreeView.deleteLater()
1619  self.effectsTreeView = None
1620  self.effectsTreeView = EffectsTreeView(self)
1621  self.tabEffects.layout().addWidget(self.effectsTreeView)
1622 
1624  log.info("Switch to Thumbnail View")
1625 
1626  # Get settings
1627  app = get_app()
1628  s = settings.get_settings()
1629 
1630  # Prepare treeview for deletion
1631  if self.filesTreeView:
1632  self.filesTreeView.prepare_for_delete()
1633 
1634  # Files
1635  if app.context_menu_object == "files":
1636  s.set("file_view", "thumbnail")
1637  self.tabFiles.layout().removeWidget(self.filesTreeView)
1638  self.filesTreeView.deleteLater()
1639  self.filesTreeView = None
1640  self.filesTreeView = FilesListView(self)
1641  self.tabFiles.layout().addWidget(self.filesTreeView)
1642 
1643  # Transitions
1644  elif app.context_menu_object == "transitions":
1645  s.set("transitions_view", "thumbnail")
1646  self.tabTransitions.layout().removeWidget(self.transitionsTreeView)
1647  self.transitionsTreeView.deleteLater()
1648  self.transitionsTreeView = None
1649  self.transitionsTreeView = TransitionsListView(self)
1650  self.tabTransitions.layout().addWidget(self.transitionsTreeView)
1651 
1652  # Effects
1653  elif app.context_menu_object == "effects":
1654  s.set("effects_view", "thumbnail")
1655  self.tabEffects.layout().removeWidget(self.effectsTreeView)
1656  self.effectsTreeView.deleteLater()
1657  self.effectsTreeView = None
1658  self.effectsTreeView = EffectsListView(self)
1659  self.tabEffects.layout().addWidget(self.effectsTreeView)
1660 
1661  def resize_contents(self):
1662  if self.filesTreeView:
1663  self.filesTreeView.resize_contents()
1664 
1665  ##
1666  # Get a list of all dockable widgets
1667  def getDocks(self):
1668  return [self.dockFiles,
1669  self.dockTransitions,
1670  self.dockEffects,
1671  self.dockVideo,
1672  self.dockProperties]
1673 
1674  ##
1675  # Remove all dockable widgets on main screen
1676  def removeDocks(self):
1677  for dock in self.getDocks():
1678  self.removeDockWidget(dock)
1679 
1680  ##
1681  # Add all dockable widgets to the same dock area on the main screen
1682  def addDocks(self, docks, area):
1683  for dock in docks:
1684  self.addDockWidget(area, dock)
1685 
1686  ##
1687  # Float or Un-Float all dockable widgets above main screen
1688  def floatDocks(self, is_floating):
1689  for dock in self.getDocks():
1690  dock.setFloating(is_floating)
1691 
1692  ##
1693  # Show all dockable widgets on the main screen
1694  def showDocks(self, docks):
1695  for dock in docks:
1696  if get_app().window.dockWidgetArea(dock) != Qt.NoDockWidgetArea:
1697  # Only show correctly docked widgets
1698  dock.show()
1699 
1700  ##
1701  # Freeze all dockable widgets on the main screen (no float, moving, or closing)
1702  def freezeDocks(self):
1703  for dock in self.getDocks():
1704  dock.setFeatures(QDockWidget.NoDockWidgetFeatures)
1705 
1706  ##
1707  # Un-freeze all dockable widgets on the main screen (allow them to be moved, closed, and floated)
1708  def unFreezeDocks(self):
1709  for dock in self.getDocks():
1710  dock.setFeatures(QDockWidget.AllDockWidgetFeatures)
1711 
1712  ##
1713  # Hide all dockable widgets on the main screen
1714  def hideDocks(self):
1715  for dock in self.getDocks():
1716  dock.hide()
1717 
1718  ##
1719  # Switch to the default / simple view
1720  def actionSimple_View_trigger(self, event):
1721  self.removeDocks()
1722  self.addDocks([self.dockFiles, self.dockTransitions, self.dockEffects, self.dockVideo], Qt.TopDockWidgetArea)
1723  self.floatDocks(False)
1724  self.tabifyDockWidget(self.dockFiles, self.dockTransitions)
1725  self.tabifyDockWidget(self.dockTransitions, self.dockEffects)
1726  self.showDocks([self.dockFiles, self.dockTransitions, self.dockEffects, self.dockVideo])
1727 
1728  # Set initial size of docks
1729  simple_state = "AAAA/wAAAAD9AAAAAwAAAAAAAAD8AAAA9PwCAAAAAfwAAAILAAAA9AAAAAAA////+v////8CAAAAAvsAAAAcAGQAbwBjAGsAUAByAG8AcABlAHIAdABpAGUAcwAAAAAA/////wAAAKEA////+wAAABgAZABvAGMAawBLAGUAeQBmAHIAYQBtAGUAAAAAAP////8AAAATAP///wAAAAEAAAEcAAABQPwCAAAAAfsAAAAYAGQAbwBjAGsASwBlAHkAZgByAGEAbQBlAQAAAVgAAAAVAAAAAAAAAAAAAAACAAAEqwAAAdz8AQAAAAL8AAAAAAAAAWQAAAB7AP////oAAAAAAgAAAAP7AAAAEgBkAG8AYwBrAEYAaQBsAGUAcwEAAAAA/////wAAAJgA////+wAAAB4AZABvAGMAawBUAHIAYQBuAHMAaQB0AGkAbwBuAHMBAAAAAP////8AAACYAP////sAAAAWAGQAbwBjAGsARQBmAGYAZQBjAHQAcwEAAAAA/////wAAAJgA////+wAAABIAZABvAGMAawBWAGkAZABlAG8BAAABagAAA0EAAAA6AP///wAABKsAAAD2AAAABAAAAAQAAAAIAAAACPwAAAABAAAAAgAAAAEAAAAOAHQAbwBvAGwAQgBhAHIBAAAAAP////8AAAAAAAAAAA=="
1730  self.restoreState(qt_types.str_to_bytes(simple_state))
1731  QCoreApplication.processEvents()
1732 
1733 
1734  ##
1735  # Switch to an alternative view
1737  self.removeDocks()
1738 
1739  # Add Docks
1740  self.addDocks([self.dockFiles, self.dockTransitions, self.dockVideo], Qt.TopDockWidgetArea)
1741  self.addDocks([self.dockEffects], Qt.RightDockWidgetArea)
1742  self.addDocks([self.dockProperties], Qt.LeftDockWidgetArea)
1743 
1744  self.floatDocks(False)
1745  self.showDocks([self.dockFiles, self.dockTransitions, self.dockVideo, self.dockEffects, self.dockProperties])
1746 
1747  # Set initial size of docks
1748  advanced_state = "AAAA/wAAAAD9AAAAAwAAAAAAAAD8AAABQPwCAAAAAfwAAAG/AAABQAAAAKEA////+gAAAAACAAAAAvsAAAAcAGQAbwBjAGsAUAByAG8AcABlAHIAdABpAGUAcwEAAAAA/////wAAAKEA////+wAAABgAZABvAGMAawBLAGUAeQBmAHIAYQBtAGUAAAAAAP////8AAAATAP///wAAAAEAAAEcAAABQPwCAAAAAvsAAAAYAGQAbwBjAGsASwBlAHkAZgByAGEAbQBlAQAAAVgAAAAVAAAAAAAAAAD7AAAAFgBkAG8AYwBrAEUAZgBmAGUAYwB0AHMBAAABvwAAAUAAAACYAP///wAAAAIAAASrAAABkvwBAAAAA/sAAAASAGQAbwBjAGsARgBpAGwAZQBzAQAAAAAAAAFeAAAAcAD////7AAAAHgBkAG8AYwBrAFQAcgBhAG4AcwBpAHQAaQBvAG4AcwEAAAFkAAABAAAAAHAA////+wAAABIAZABvAGMAawBWAGkAZABlAG8BAAACagAAAkEAAAA6AP///wAAAocAAAFAAAAABAAAAAQAAAAIAAAACPwAAAABAAAAAgAAAAEAAAAOAHQAbwBvAGwAQgBhAHIBAAAAAP////8AAAAAAAAAAA=="
1749  self.restoreState(qt_types.str_to_bytes(advanced_state))
1750  QCoreApplication.processEvents()
1751 
1752  ##
1753  # Freeze all dockable widgets on the main screen
1754  def actionFreeze_View_trigger(self, event):
1755  self.freezeDocks()
1756  self.actionFreeze_View.setVisible(False)
1757  self.actionUn_Freeze_View.setVisible(True)
1758 
1759  ##
1760  # Un-Freeze all dockable widgets on the main screen
1762  self.unFreezeDocks()
1763  self.actionFreeze_View.setVisible(True)
1764  self.actionUn_Freeze_View.setVisible(False)
1765 
1766  ##
1767  # Show all dockable widgets
1768  def actionShow_All_trigger(self, event):
1769  self.showDocks(self.getDocks())
1770 
1771  ##
1772  # Show tutorial again
1773  def actionTutorial_trigger(self, event):
1774  s = settings.get_settings()
1775 
1776  # Clear tutorial settings
1777  s.set("tutorial_enabled", True)
1778  s.set("tutorial_ids", "")
1779 
1780  # Show first tutorial dialog again
1781  if self.tutorial_manager:
1782  self.tutorial_manager.exit_manager()
1783  self.tutorial_manager = TutorialManager(self)
1784 
1785  ##
1786  # Set the window title based on a variety of factors
1787  def SetWindowTitle(self, profile=None):
1788 
1789  # Get translation function
1790  _ = get_app()._tr
1791 
1792  if not profile:
1793  profile = get_app().project.get(["profile"])
1794 
1795  # Determine if the project needs saving (has any unsaved changes)
1796  save_indicator = ""
1797  if get_app().project.needs_save():
1798  save_indicator = "*"
1799  self.actionSave.setEnabled(True)
1800  else:
1801  self.actionSave.setEnabled(False)
1802 
1803  # Is this a saved project?
1804  if not get_app().project.current_filepath:
1805  # Not saved yet
1806  self.setWindowTitle("%s %s [%s] - %s" % (save_indicator, _("Untitled Project"), profile, "OpenShot Video Editor"))
1807  else:
1808  # Yes, project is saved
1809  # Get just the filename
1810  parent_path, filename = os.path.split(get_app().project.current_filepath)
1811  filename, ext = os.path.splitext(filename)
1812  filename = filename.replace("_", " ").replace("-", " ").capitalize()
1813  self.setWindowTitle("%s %s [%s] - %s" % (save_indicator, filename, profile, "OpenShot Video Editor"))
1814 
1815  # Update undo and redo buttons enabled/disabled to available changes
1816  def updateStatusChanged(self, undo_status, redo_status):
1817  log.info('updateStatusChanged')
1818  self.actionUndo.setEnabled(undo_status)
1819  self.actionRedo.setEnabled(redo_status)
1820  self.SetWindowTitle()
1821 
1822  # Add to the selected items
1823  def addSelection(self, item_id, item_type, clear_existing=False):
1824  log.info('main::addSelection: item_id: %s, item_type: %s, clear_existing: %s' % (item_id, item_type, clear_existing))
1825 
1826  # Clear existing selection (if needed)
1827  if clear_existing:
1828  if item_type == "clip":
1829  self.selected_clips.clear()
1830  elif item_type == "transition":
1831  self.selected_transitions.clear()
1832  elif item_type == "effect":
1833  self.selected_effects.clear()
1834 
1835  # Clear transform (if any)
1836  self.TransformSignal.emit("")
1837 
1838  if item_id:
1839  # If item_id is not blank, store it
1840  if item_type == "clip" and item_id not in self.selected_clips:
1841  self.selected_clips.append(item_id)
1842  elif item_type == "transition" and item_id not in self.selected_transitions:
1843  self.selected_transitions.append(item_id)
1844  elif item_type == "effect" and item_id not in self.selected_effects:
1845  self.selected_effects.append(item_id)
1846 
1847  # Change selected item in properties view
1848  self.show_property_id = item_id
1849  self.show_property_type = item_type
1850  self.show_property_timer.start()
1851 
1852  # Remove from the selected items
1853  def removeSelection(self, item_id, item_type):
1854  # Remove existing selection (if any)
1855  if item_id:
1856  if item_type == "clip" and item_id in self.selected_clips:
1857  self.selected_clips.remove(item_id)
1858  elif item_type == "transition" and item_id in self.selected_transitions:
1859  self.selected_transitions.remove(item_id)
1860  elif item_type == "effect" and item_id in self.selected_effects:
1861  self.selected_effects.remove(item_id)
1862 
1863  # Clear transform (if any)
1864  get_app().window.TransformSignal.emit("")
1865 
1866  # Move selection to next selected clip (if any)
1867  self.show_property_id = ""
1868  self.show_property_type = ""
1869  if item_type == "clip" and self.selected_clips:
1870  self.show_property_id = self.selected_clips[0]
1871  self.show_property_type = item_type
1872  elif item_type == "transition" and self.selected_transitions:
1873  self.show_property_id = self.selected_transitions[0]
1874  self.show_property_type = item_type
1875  elif item_type == "effect" and self.selected_effects:
1876  self.show_property_id = self.selected_effects[0]
1877  self.show_property_type = item_type
1878 
1879  # Change selected item in properties view
1880  self.show_property_timer.start()
1881 
1882  # Update window settings in setting store
1883  def save_settings(self):
1884  s = settings.get_settings()
1885 
1886  # Save window state and geometry (saves toolbar and dock locations)
1887  s.set('window_state', qt_types.bytes_to_str(self.saveState()))
1888  s.set('window_geometry', qt_types.bytes_to_str(self.saveGeometry()))
1889 
1890  # Get window settings from setting store
1891  def load_settings(self):
1892  s = settings.get_settings()
1893 
1894  # Window state and geometry (also toolbar and dock locations)
1895  if s.get('window_geometry'): self.restoreGeometry(qt_types.str_to_bytes(s.get('window_geometry')))
1896  if s.get('window_state'): self.restoreState(qt_types.str_to_bytes(s.get('window_state')))
1897 
1898  # Load Recent Projects
1899  self.load_recent_menu()
1900 
1901  ##
1902  # Clear and load the list of recent menu items
1903  def load_recent_menu(self):
1904  s = settings.get_settings()
1905  _ = get_app()._tr # Get translation function
1906 
1907  # Get list of recent projects
1908  recent_projects = s.get("recent_projects")
1909 
1910  # Add Recent Projects menu (after Open File)
1911  import functools
1912  if not self.recent_menu:
1913  # Create a new recent menu
1914  self.recent_menu = self.menuFile.addMenu(QIcon.fromTheme("document-open-recent"), _("Recent Projects"))
1915  self.menuFile.insertMenu(self.actionRecent_Placeholder, self.recent_menu)
1916  else:
1917  # Clear the existing children
1918  self.recent_menu.clear()
1919 
1920  # Add recent projects to menu
1921  for file_path in reversed(recent_projects):
1922  new_action = self.recent_menu.addAction(file_path)
1923  new_action.triggered.connect(functools.partial(self.recent_project_clicked, file_path))
1924 
1925  ##
1926  # Load a recent project when clicked
1927  def recent_project_clicked(self, file_path):
1928 
1929  # Load project file
1930  self.open_project(file_path)
1931 
1932  def setup_toolbars(self):
1933  _ = get_app()._tr # Get translation function
1934 
1935  # Start undo and redo actions disabled
1936  self.actionUndo.setEnabled(False)
1937  self.actionRedo.setEnabled(False)
1938 
1939  # Add files toolbar =================================================================================
1940  self.filesToolbar = QToolBar("Files Toolbar")
1941  self.filesActionGroup = QActionGroup(self)
1942  self.filesActionGroup.setExclusive(True)
1943  self.filesActionGroup.addAction(self.actionFilesShowAll)
1944  self.filesActionGroup.addAction(self.actionFilesShowVideo)
1945  self.filesActionGroup.addAction(self.actionFilesShowAudio)
1946  self.filesActionGroup.addAction(self.actionFilesShowImage)
1947  self.actionFilesShowAll.setChecked(True)
1948  self.filesToolbar.addAction(self.actionFilesShowAll)
1949  self.filesToolbar.addAction(self.actionFilesShowVideo)
1950  self.filesToolbar.addAction(self.actionFilesShowAudio)
1951  self.filesToolbar.addAction(self.actionFilesShowImage)
1952  self.filesFilter = QLineEdit()
1953  self.filesFilter.setObjectName("filesFilter")
1954  self.filesFilter.setPlaceholderText(_("Filter"))
1955  self.filesToolbar.addWidget(self.filesFilter)
1956  self.actionFilesClear.setEnabled(False)
1957  self.filesToolbar.addAction(self.actionFilesClear)
1958  self.tabFiles.layout().addWidget(self.filesToolbar)
1959 
1960  # Add transitions toolbar =================================================================================
1961  self.transitionsToolbar = QToolBar("Transitions Toolbar")
1962  self.transitionsActionGroup = QActionGroup(self)
1963  self.transitionsActionGroup.setExclusive(True)
1964  self.transitionsActionGroup.addAction(self.actionTransitionsShowAll)
1965  self.transitionsActionGroup.addAction(self.actionTransitionsShowCommon)
1966  self.actionTransitionsShowAll.setChecked(True)
1967  self.transitionsToolbar.addAction(self.actionTransitionsShowAll)
1968  self.transitionsToolbar.addAction(self.actionTransitionsShowCommon)
1969  self.transitionsFilter = QLineEdit()
1970  self.transitionsFilter.setObjectName("transitionsFilter")
1971  self.transitionsFilter.setPlaceholderText(_("Filter"))
1972  self.transitionsToolbar.addWidget(self.transitionsFilter)
1973  self.actionTransitionsClear.setEnabled(False)
1974  self.transitionsToolbar.addAction(self.actionTransitionsClear)
1975  self.tabTransitions.layout().addWidget(self.transitionsToolbar)
1976 
1977  # Add effects toolbar =================================================================================
1978  self.effectsToolbar = QToolBar("Effects Toolbar")
1979  self.effectsActionGroup = QActionGroup(self)
1980  self.effectsActionGroup.setExclusive(True)
1981  self.effectsActionGroup.addAction(self.actionEffectsShowAll)
1982  self.effectsActionGroup.addAction(self.actionEffectsShowVideo)
1983  self.effectsActionGroup.addAction(self.actionEffectsShowAudio)
1984  self.actionEffectsShowAll.setChecked(True)
1985  self.effectsToolbar.addAction(self.actionEffectsShowAll)
1986  self.effectsToolbar.addAction(self.actionEffectsShowVideo)
1987  self.effectsToolbar.addAction(self.actionEffectsShowAudio)
1988  self.effectsFilter = QLineEdit()
1989  self.effectsFilter.setObjectName("effectsFilter")
1990  self.effectsFilter.setPlaceholderText(_("Filter"))
1991  self.effectsToolbar.addWidget(self.effectsFilter)
1992  self.actionEffectsClear.setEnabled(False)
1993  self.effectsToolbar.addAction(self.actionEffectsClear)
1994  self.tabEffects.layout().addWidget(self.effectsToolbar)
1995 
1996  # Add Video Preview toolbar ==========================================================================
1997  self.videoToolbar = QToolBar("Video Toolbar")
1998 
1999  # Add left spacer
2000  spacer = QWidget(self)
2001  spacer.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
2002  self.videoToolbar.addWidget(spacer)
2003 
2004  # Playback controls
2005  self.videoToolbar.addAction(self.actionJumpStart)
2006  self.videoToolbar.addAction(self.actionRewind)
2007  self.videoToolbar.addAction(self.actionPlay)
2008  self.videoToolbar.addAction(self.actionFastForward)
2009  self.videoToolbar.addAction(self.actionJumpEnd)
2010  self.actionPlay.setCheckable(True)
2011 
2012  # Add right spacer
2013  spacer = QWidget(self)
2014  spacer.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
2015  self.videoToolbar.addWidget(spacer)
2016 
2017  self.tabVideo.layout().addWidget(self.videoToolbar)
2018 
2019  # Add Timeline toolbar ================================================================================
2020  self.timelineToolbar = QToolBar("Timeline Toolbar", self)
2021 
2022  self.timelineToolbar.addAction(self.actionAddTrack)
2023  self.timelineToolbar.addSeparator()
2024 
2025  # rest of options
2026  self.timelineToolbar.addAction(self.actionSnappingTool)
2027  self.timelineToolbar.addAction(self.actionRazorTool)
2028  self.timelineToolbar.addSeparator()
2029  self.timelineToolbar.addAction(self.actionAddMarker)
2030  self.timelineToolbar.addAction(self.actionPreviousMarker)
2031  self.timelineToolbar.addAction(self.actionNextMarker)
2032  self.timelineToolbar.addSeparator()
2033 
2034  # Get project's initial zoom value
2035  initial_scale = get_app().project.get(["scale"]) or 16
2036  # Round non-exponential scale down to next lowest power of 2
2037  initial_zoom = secondsToZoom(initial_scale)
2038 
2039  # Setup Zoom slider
2040  self.sliderZoom = QSlider(Qt.Horizontal, self)
2041  self.sliderZoom.setPageStep(1)
2042  self.sliderZoom.setRange(0, 30)
2043  self.sliderZoom.setValue(initial_zoom)
2044  self.sliderZoom.setInvertedControls(True)
2045  self.sliderZoom.resize(100, 16)
2046 
2047  self.zoomScaleLabel = QLabel( _("{} seconds").format(zoomToSeconds(self.sliderZoom.value())) )
2048 
2049  # add zoom widgets
2050  self.timelineToolbar.addAction(self.actionTimelineZoomIn)
2051  self.timelineToolbar.addWidget(self.sliderZoom)
2052  self.timelineToolbar.addAction(self.actionTimelineZoomOut)
2053  self.timelineToolbar.addWidget(self.zoomScaleLabel)
2054 
2055  # Add timeline toolbar to web frame
2056  self.frameWeb.addWidget(self.timelineToolbar)
2057 
2058  ##
2059  # Clear all selection containers
2060  def clearSelections(self):
2061  self.selected_files = []
2062  self.selected_clips = []
2065  self.selected_tracks = []
2067 
2068  # Clear selection in properties view
2069  if self.propertyTableView:
2070  self.propertyTableView.loadProperties.emit("", "")
2071 
2072  ##
2073  # Handle the callback for detecting the current version on openshot.org
2074  def foundCurrentVersion(self, version):
2075  log.info('foundCurrentVersion: Found the latest version: %s' % version)
2076  _ = get_app()._tr
2077 
2078  # Compare versions (alphabetical compare of version strings should work fine)
2079  if info.VERSION < version:
2080  # Add spacer and 'New Version Available' toolbar button (default hidden)
2081  spacer = QWidget(self)
2082  spacer.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)
2083  self.toolBar.addWidget(spacer)
2084 
2085  # Update text for QAction
2086  self.actionUpdate.setVisible(True)
2087  self.actionUpdate.setText(_("Update Available"))
2088  self.actionUpdate.setToolTip(_("Update Available: <b>%s</b>") % version)
2089 
2090  # Add update available button (with icon and text)
2091  updateButton = QToolButton()
2092  updateButton.setDefaultAction(self.actionUpdate)
2093  updateButton.setToolButtonStyle(Qt.ToolButtonTextBesideIcon)
2094  self.toolBar.addWidget(updateButton)
2095 
2096  ##
2097  # Move tutorial dialogs also (if any)
2098  def moveEvent(self, event):
2099  if self.tutorial_manager:
2100  self.tutorial_manager.re_position_dialog()
2101 
2102  ##
2103  # Filter out certain types of window events
2104  def eventFilter(self, object, e):
2105  if e.type() == QEvent.WindowActivate:
2106  self.tutorial_manager.re_show_dialog()
2107  elif e.type() == QEvent.WindowStateChange and self.isMinimized():
2108  self.tutorial_manager.minimize()
2109 
2110  return False
2111 
2112  ##
2113  # Callback for show property timer
2115 
2116  # Stop timer
2117  self.show_property_timer.stop()
2118 
2119  # Emit load properties signal
2120  self.propertyTableView.loadProperties.emit(self.show_property_id, self.show_property_type)
2121 
2122  ##
2123  # Initialize all keyboard shortcuts from the settings file
2125 
2126  # Translate object
2127  _ = get_app()._tr
2128 
2129  # Update all action-based shortcuts (from settings file)
2130  for shortcut in self.getAllKeyboardShortcuts():
2131  for action in self.findChildren(QAction):
2132  if shortcut.get('setting') == action.objectName():
2133  action.setShortcut(QKeySequence(_(shortcut.get('value'))))
2134 
2135  ##
2136  # Set the correct cache settings for the timeline
2138  # Load user settings
2139  s = settings.get_settings()
2140  log.info("InitCacheSettings")
2141  log.info("cache-mode: %s" % s.get("cache-mode"))
2142  log.info("cache-limit-mb: %s" % s.get("cache-limit-mb"))
2143 
2144  # Get MB limit of cache (and convert to bytes)
2145  cache_limit = s.get("cache-limit-mb") * 1024 * 1024 # Convert MB to Bytes
2146 
2147  # Clear old cache
2148  new_cache_object = None
2149  if s.get("cache-mode") == "CacheMemory":
2150  # Create CacheMemory object, and set on timeline
2151  log.info("Creating CacheMemory object with %s byte limit" % cache_limit)
2152  new_cache_object = openshot.CacheMemory(cache_limit)
2153  self.timeline_sync.timeline.SetCache(new_cache_object)
2154 
2155  elif s.get("cache-mode") == "CacheDisk":
2156  # Create CacheDisk object, and set on timeline
2157  log.info("Creating CacheDisk object with %s byte limit at %s" % (cache_limit, info.PREVIEW_CACHE_PATH))
2158  image_format = s.get("cache-image-format")
2159  image_quality = s.get("cache-quality")
2160  image_scale = s.get("cache-scale")
2161  new_cache_object = openshot.CacheDisk(info.PREVIEW_CACHE_PATH, image_format, image_quality, image_scale, cache_limit)
2162  self.timeline_sync.timeline.SetCache(new_cache_object)
2163 
2164  # Clear old cache before it goes out of scope
2165  if self.cache_object:
2166  self.cache_object.Clear()
2167  # Update cache reference, so it doesn't go out of scope
2168  self.cache_object = new_cache_object
2169 
2170  ##
2171  # Connect to Unity launcher (for Linux)
2172  def FrameExported(self, path, start_frame, end_frame, current_frame):
2173  try:
2174  if sys.platform == "linux" and self.has_launcher:
2175  if not self.unity_launchers:
2176  # Get launcher only once
2177  from gi.repository import Unity
2178  self.unity_launchers.append(Unity.LauncherEntry.get_for_desktop_id("openshot-qt.desktop"))
2179  self.unity_launchers.append(Unity.LauncherEntry.get_for_desktop_id("appimagekit-openshot-qt.desktop"))
2180 
2181  # Set progress and show progress bar
2182  for launcher in self.unity_launchers:
2183  launcher.set_property("progress", current_frame / (end_frame - start_frame))
2184  launcher.set_property("progress_visible", True)
2185 
2186  except:
2187  # Just ignore
2188  self.has_launcher = False
2189 
2190  ##
2191  # Export has completed
2192  def ExportFinished(self, path):
2193  try:
2194  if sys.platform == "linux" and self.has_launcher:
2195  for launcher in self.unity_launchers:
2196  # Set progress on Unity launcher and hide progress bar
2197  launcher.set_property("progress", 0.0)
2198  launcher.set_property("progress_visible", False)
2199  except:
2200  pass
2201 
2202  ##
2203  # Handle transform signal (to keep track of whether a transform is happening or not)
2204  def transformTriggered(self, clip_id):
2205  if clip_id and clip_id in self.selected_clips:
2206  self.is_transforming = True
2207  else:
2208  self.is_transforming = False
2209 
2210 
2211  def __init__(self, mode=None):
2212 
2213  # Create main window base class
2214  QMainWindow.__init__(self)
2215  self.mode = mode # None or unittest (None is normal usage)
2216  self.initialized = False
2217 
2218  # set window on app for reference during initialization of children
2219  get_app().window = self
2220  _ = get_app()._tr
2221 
2222  # Load user settings for window
2223  s = settings.get_settings()
2224  self.recent_menu = None
2225 
2226  # Track metrics
2227  track_metric_session() # start session
2228 
2229  # Set unique install id (if blank)
2230  if not s.get("unique_install_id"):
2231  s.set("unique_install_id", str(uuid4()))
2232 
2233  # Track 1st launch metric
2234  track_metric_screen("initial-launch-screen")
2235 
2236  # Track main screen
2237  track_metric_screen("main-screen")
2238 
2239  # Create blank tutorial manager
2240  self.tutorial_manager = None
2241 
2242  # Load UI from designer
2243  ui_util.load_ui(self, self.ui_path)
2244 
2245  # Set all keyboard shortcuts from the settings file
2246  self.InitKeyboardShortcuts()
2247 
2248  # Init UI
2249  ui_util.init_ui(self)
2250 
2251  # Setup toolbars that aren't on main window, set initial state of items, etc
2252  self.setup_toolbars()
2253 
2254  # Add window as watcher to receive undo/redo status updates
2255  get_app().updates.add_watcher(self)
2256 
2257  # Get current version of OpenShot via HTTP
2258  self.FoundVersionSignal.connect(self.foundCurrentVersion)
2260 
2261  # Connect signals
2262  self.is_transforming = False
2263  self.TransformSignal.connect(self.transformTriggered)
2264  if not self.mode == "unittest":
2265  self.RecoverBackup.connect(self.recover_backup)
2266 
2267  # Create the timeline sync object (used for previewing timeline)
2268  self.timeline_sync = TimelineSync(self)
2269 
2270  # Setup timeline
2271  self.timeline = TimelineWebView(self)
2272  self.frameWeb.layout().addWidget(self.timeline)
2273 
2274  # Setup files tree
2275  if s.get("file_view") == "details":
2276  self.filesTreeView = FilesTreeView(self)
2277  else:
2278  self.filesTreeView = FilesListView(self)
2279  self.tabFiles.layout().addWidget(self.filesTreeView)
2280  self.filesTreeView.setFocus()
2281 
2282  # Setup transitions tree
2283  if s.get("transitions_view") == "details":
2284  self.transitionsTreeView = TransitionsTreeView(self)
2285  else:
2286  self.transitionsTreeView = TransitionsListView(self)
2287  self.tabTransitions.layout().addWidget(self.transitionsTreeView)
2288 
2289  # Setup effects tree
2290  if s.get("effects_view") == "details":
2291  self.effectsTreeView = EffectsTreeView(self)
2292  else:
2293  self.effectsTreeView = EffectsListView(self)
2294  self.tabEffects.layout().addWidget(self.effectsTreeView)
2295 
2296  # Process events before continuing
2297  # TODO: Figure out why this is needed for a backup recovery to correctly show up on the timeline
2298  get_app().processEvents()
2299 
2300  # Setup properties table
2301  self.txtPropertyFilter.setPlaceholderText(_("Filter"))
2302  self.propertyTableView = PropertiesTableView(self)
2303  self.selectionLabel = SelectionLabel(self)
2304  self.dockPropertiesContent.layout().addWidget(self.selectionLabel, 0, 1)
2305  self.dockPropertiesContent.layout().addWidget(self.propertyTableView, 2, 1)
2306 
2307  # Init selection containers
2308  self.clearSelections()
2309 
2310  # Show Property timer
2311  # Timer to use a delay before showing properties (to prevent a mass selection from trying
2312  # to update the property model hundreds of times)
2313  self.show_property_id = None
2314  self.show_property_type = None
2315  self.show_property_timer = QTimer()
2316  self.show_property_timer.setInterval(100)
2317  self.show_property_timer.timeout.connect(self.show_property_timeout)
2318  self.show_property_timer.stop()
2319 
2320  # Setup video preview QWidget
2321  self.videoPreview = VideoWidget()
2322  self.tabVideo.layout().insertWidget(0, self.videoPreview)
2323 
2324  # Load window state and geometry
2325  self.load_settings()
2326 
2327  # Setup Cache settings
2328  self.cache_object = None
2329  self.InitCacheSettings()
2330 
2331  # Start the preview thread
2332  self.preview_parent = PreviewParent()
2333  self.preview_parent.Init(self, self.timeline_sync.timeline, self.videoPreview)
2334  self.preview_thread = self.preview_parent.worker
2335 
2336  # Set pause callback
2337  self.PauseSignal.connect(self.handlePausedVideo)
2338 
2339  # QTimer for Autosave
2340  self.auto_save_timer = QTimer(self)
2341  self.auto_save_timer.setInterval(s.get("autosave-interval") * 1000 * 60)
2342  self.auto_save_timer.timeout.connect(self.auto_save_project)
2343  if s.get("enable-auto-save"):
2344  self.auto_save_timer.start()
2345 
2346  # Set hardware decode environment variable
2347  if s.get("hardware_decode"):
2348  os.environ['OS2_DECODE_HW'] = "1"
2349  else:
2350  os.environ['OS2_DECODE_HW'] = "0"
2351 
2352  # Create lock file
2353  self.create_lock_file()
2354 
2355  # Show window
2356  if not self.mode == "unittest":
2357  self.show()
2358 
2359  # Create tutorial manager
2360  self.tutorial_manager = TutorialManager(self)
2361 
2362  # Connect to Unity DBus signal (if linux)
2363  if sys.platform == "linux":
2365  self.has_launcher = True
2366  self.ExportFrame.connect(self.FrameExported)
2367  self.ExportEnded.connect(self.ExportFinished)
2368 
2369  # Install event filter
2370  self.installEventFilter(self)
2371 
2372  # Save settings
2373  s.save()
2374 
2375  # Refresh frame
2376  QTimer.singleShot(100, self.refreshFrameSignal.emit)
2377 
2378  # Main window is initialized
2379  self.initialized = True
def actionLockTrack_trigger
Callback for locking a track.
def actionAdvanced_View_trigger
Switch to an alternative view.
def InitCacheSettings
Set the correct cache settings for the timeline.
def secondsToZoom
Convert a number of seconds to a timeline zoom factor.
Definition: conversion.py:70
def save_project
Save a project to a file path, and refresh the screen.
Definition: main_window.py:407
def recent_project_clicked
Load a recent project when clicked.
def track_metric_screen
Track a GUI screen being shown.
Definition: metrics.py:96
def actionAbout_trigger
Show about dialog.
Definition: main_window.py:728
def open_project
Open a project from a file path, and refresh the screen.
Definition: main_window.py:433
def get_app
Returns the current QApplication instance of OpenShot.
Definition: app.py:55
def actionSimple_View_trigger
Switch to the default / simple view.
def str_to_bytes
This is required to save Qt byte arrays into a base64 string (to save screen preferences) ...
Definition: qt_types.py:39
def setup_icon
Using the window xml, set the icon on the given element, or if theme_name passed load that icon...
Definition: ui_util.py:149
def getDocks
Get a list of all dockable widgets.
def freezeDocks
Freeze all dockable widgets on the main screen (no float, moving, or closing)
def floatDocks
Float or Un-Float all dockable widgets above main screen.
def foundCurrentVersion
Handle the callback for detecting the current version on openshot.org.
def actionRazorTool_trigger
Toggle razor tool on and off.
Definition: main_window.py:995
def actionRenameTrack_trigger
Callback for renaming track.
def eventFilter
Filter out certain types of window events.
def actionUnlockTrack_trigger
Callback for unlocking a track.
def getShortcutByName
Get a key sequence back from the setting name.
def actionUn_Freeze_View_trigger
Un-Freeze all dockable widgets on the main screen.
def previewFrame
Preview a specific frame.
Definition: main_window.py:829
def track_metric_error
Track an error has occurred.
Definition: metrics.py:121
def clear_all_thumbnails
Clear all user thumbnails.
Definition: main_window.py:477
def InitKeyboardShortcuts
Initialize all keyboard shortcuts from the settings file.
def FrameExported
Connect to Unity launcher (for Linux)
def transformTriggered
Handle transform signal (to keep track of whether a transform is happening or not) ...
def keyPressEvent
Process key press events and match with known shortcuts.
def removeDocks
Remove all dockable widgets on main screen.
def actionShow_All_trigger
Show all dockable widgets.
def load_ui
Load a Qt *.ui file, and also load an XML parsed version.
Definition: ui_util.py:66
def recover_backup
Recover the backup file (if any)
Definition: main_window.py:141
def showDocks
Show all dockable widgets on the main screen.
def show_property_timeout
Callback for show property timer.
def tail_file
Read the end of a file (n number of lines)
Definition: main_window.py:268
def getAllKeyboardShortcuts
Get a key sequence back from the setting name.
def actionPreview_File_trigger
Preview the selected media file.
Definition: main_window.py:808
def website_language
Get the current website language code for URLs.
Definition: info.py:123
def moveEvent
Move tutorial dialogs also (if any)
def unFreezeDocks
Un-freeze all dockable widgets on the main screen (allow them to be moved, closed, and floated)
def destroy_lock_file
Destroy the lock file.
Definition: main_window.py:258
def zoomToSeconds
Convert zoom factor (slider position) into scale-seconds.
Definition: conversion.py:64
def SetWindowTitle
Set the window title based on a variety of factors.
def auto_save_project
Auto save the project.
Definition: main_window.py:555
def clearSelections
Clear all selection containers.
def get_current_Version
Get the current version.
Definition: version.py:42
def track_exception_stacktrace
Track an exception/stacktrace has occurred.
Definition: metrics.py:134
Interface for classes that listen for 'undo' and 'redo' events.
Definition: updates.py:42
def handlePausedVideo
Handle the pause signal, by refreshing the properties dialog.
Definition: main_window.py:838
def load_recent_menu
Clear and load the list of recent menu items.
def init_ui
Initialize all child widgets and action of a window or dialog.
Definition: ui_util.py:220
def actionTransitionsShowCommon_trigger
Definition: main_window.py:706
This class contains the logic for the main window widget.
Definition: main_window.py:68
def updateStatusChanged
Easily be notified each time there are 'undo' or 'redo' actions available in the UpdateManager.
Definition: updates.py:46
def actionTutorial_trigger
Show tutorial again.
def hideDocks
Hide all dockable widgets on the main screen.
def addDocks
Add all dockable widgets to the same dock area on the main screen.
def track_metric_session
Track a GUI screen being shown.
Definition: metrics.py:140
def bytes_to_str
This is required to load base64 Qt byte array strings into a Qt byte array (to load screen preference...
Definition: qt_types.py:45
def movePlayhead
Update playhead position.
Definition: main_window.py:843
def ExportFinished
Export has completed.
def create_lock_file
Create a lock file.
Definition: main_window.py:175
def actionFreeze_View_trigger
Freeze all dockable widgets on the main screen.
def get_settings
Get the current QApplication's settings instance.
Definition: settings.py:44
def actionClearHistory_trigger
Clear history for current project.
Definition: main_window.py:400