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 
709  def actionHelpContents_trigger(self, event):
710  try:
711  webbrowser.open("http://%s.openshot.org/files/user-guide/?app-menu" % info.website_language())
712  log.info("Help Contents is open")
713  except:
714  QMessageBox.information(self, "Error !", "Unable to open the Help Contents. Please ensure the openshot-doc package is installed.")
715  log.info("Unable to open the Help Contents")
716 
717  ##
718  # Show about dialog
719  def actionAbout_trigger(self, event):
720  from windows.about import About
721  win = About()
722  # Run the dialog event loop - blocking interaction on this window during this time
723  result = win.exec_()
724  if result == QDialog.Accepted:
725  log.info('About Openshot add confirmed')
726  else:
727  log.info('About Openshot add cancelled')
728 
729  def actionReportBug_trigger(self, event):
730  try:
731  webbrowser.open("https://github.com/OpenShot/openshot-qt/issues/?app-menu-bug")
732  log.info("Open the Bug Report GitHub Issues web page with success")
733  except:
734  QMessageBox.information(self, "Error !", "Unable to open the Bug Report GitHub Issues web page")
735  log.info("Unable to open the Bug Report GitHub Issues web page")
736 
737  def actionAskQuestion_trigger(self, event):
738  try:
739  webbrowser.open("https://github.com/OpenShot/openshot-qt/issues/?app-menu-question")
740  log.info("Open the Questions GitHub Issues web page with success")
741  except:
742  QMessageBox.information(self, "Error !", "Unable to open the Questions GitHub Issues web page")
743  log.info("Unable to open the Questions GitHub Issues web page")
744 
745  def actionTranslate_trigger(self, event):
746  try:
747  webbrowser.open("https://translations.launchpad.net/openshot/2.0")
748  log.info("Open the Translate launchpad web page with success")
749  except:
750  QMessageBox.information(self, "Error !", "Unable to open the Translation web page")
751  log.info("Unable to open the Translation web page")
752 
753  def actionDonate_trigger(self, event):
754  try:
755  webbrowser.open("http://%s.openshot.org/donate/?app-menu" % info.website_language())
756  log.info("Open the Donate web page with success")
757  except:
758  QMessageBox.information(self, "Error !", "Unable to open the Donate web page")
759  log.info("Unable to open the Donate web page")
760 
761  def actionUpdate_trigger(self, event):
762  try:
763  webbrowser.open("http://%s.openshot.org/download/?app-toolbar" % info.website_language())
764  log.info("Open the Download web page with success")
765  except:
766  QMessageBox.information(self, "Error !", "Unable to open the Download web page")
767  log.info("Unable to open the Download web page")
768 
769  def actionPlay_trigger(self, event, force=None):
770 
771  # Determine max frame (based on clips)
772  timeline_length = 0.0
773  fps = get_app().window.timeline_sync.timeline.info.fps.ToFloat()
774  clips = get_app().window.timeline_sync.timeline.Clips()
775  for clip in clips:
776  clip_last_frame = clip.Position() + clip.Duration()
777  if clip_last_frame > timeline_length:
778  # Set max length of timeline
779  timeline_length = clip_last_frame
780 
781  # Convert to int and round
782  timeline_length_int = round(timeline_length * fps) + 1
783 
784  if force == "pause":
785  self.actionPlay.setChecked(False)
786  elif force == "play":
787  self.actionPlay.setChecked(True)
788 
789  if self.actionPlay.isChecked():
790  ui_util.setup_icon(self, self.actionPlay, "actionPlay", "media-playback-pause")
791  self.PlaySignal.emit(timeline_length_int)
792 
793  else:
794  ui_util.setup_icon(self, self.actionPlay, "actionPlay") # to default
795  self.PauseSignal.emit()
796 
797  ##
798  # Preview the selected media file
799  def actionPreview_File_trigger(self, event):
800  log.info('actionPreview_File_trigger')
801 
802  # Loop through selected files (set 1 selected file if more than 1)
803  f = None
804  for file_id in self.selected_files:
805  # Find matching file
806  f = File.get(id=file_id)
807 
808  # Bail out if no file selected
809  if not f:
810  log.info(self.selected_files)
811  return
812 
813  # show dialog
814  from windows.cutting import Cutting
815  win = Cutting(f, preview=True)
816  win.show()
817 
818  ##
819  # Preview a specific frame
820  def previewFrame(self, position_seconds, position_frames, time_code):
821  # Notify preview thread
822  self.previewFrameSignal.emit(position_frames)
823 
824  # Notify properties dialog
825  self.propertyTableView.select_frame(position_frames)
826 
827  ##
828  # Handle the pause signal, by refreshing the properties dialog
829  def handlePausedVideo(self):
830  self.propertyTableView.select_frame(self.preview_thread.player.Position())
831 
832  ##
833  # Update playhead position
834  def movePlayhead(self, position_frames):
835  # Notify preview thread
836  self.timeline.movePlayhead(position_frames)
837 
838  def actionFastForward_trigger(self, event):
839 
840  # Get the video player object
841  player = self.preview_thread.player
842 
843  if player.Speed() + 1 != 0:
844  self.SpeedSignal.emit(player.Speed() + 1)
845  else:
846  self.SpeedSignal.emit(player.Speed() + 2)
847 
848  if player.Mode() == openshot.PLAYBACK_PAUSED:
849  self.actionPlay.trigger()
850 
851  def actionRewind_trigger(self, event):
852 
853  # Get the video player object
854  player = self.preview_thread.player
855 
856  if player.Speed() - 1 != 0:
857  self.SpeedSignal.emit(player.Speed() - 1)
858  else:
859  self.SpeedSignal.emit(player.Speed() - 2)
860 
861  if player.Mode() == openshot.PLAYBACK_PAUSED:
862  self.actionPlay.trigger()
863 
864  def actionJumpStart_trigger(self, event):
865  log.info("actionJumpStart_trigger")
866 
867  # Seek to the 1st frame
868  self.SeekSignal.emit(1)
869 
870  def actionJumpEnd_trigger(self, event):
871  log.info("actionJumpEnd_trigger")
872 
873  # Determine max frame (based on clips)
874  timeline_length = 0.0
875  fps = get_app().window.timeline_sync.timeline.info.fps.ToFloat()
876  clips = get_app().window.timeline_sync.timeline.Clips()
877  for clip in clips:
878  clip_last_frame = clip.Position() + clip.Duration()
879  if clip_last_frame > timeline_length:
880  # Set max length of timeline
881  timeline_length = clip_last_frame
882 
883  # Convert to int and round
884  timeline_length_int = round(timeline_length * fps) + 1
885 
886  # Seek to the 1st frame
887  self.SeekSignal.emit(timeline_length_int)
888 
889  def actionSaveFrame_trigger(self, event):
890  log.info("actionSaveFrame_trigger")
891 
892  # Translate object
893  _ = get_app()._tr
894 
895  # Prepare to use the status bar
896  self.statusBar = QStatusBar()
897  self.setStatusBar(self.statusBar)
898 
899  # Determine path for saved frame - Default export path
900  recommended_path = recommended_path = os.path.join(info.HOME_PATH)
901  if get_app().project.current_filepath:
902  recommended_path = os.path.dirname(get_app().project.current_filepath)
903 
904  # Determine path for saved frame - Project's export path
905  if get_app().project.get(["export_path"]):
906  recommended_path = get_app().project.get(["export_path"])
907 
908  framePath = _("%s/Frame-%05d.png" % (recommended_path, self.preview_thread.current_frame))
909  #log.info("Saving frame to %" % framePath)
910 
911  # Ask user to confirm or update framePath
912  framePath, file_type = QFileDialog.getSaveFileName(self, _("Save Frame..."), framePath, _("Image files (*.png)"))
913 
914  if framePath:
915  # Append .osp if needed
916  if ".png" not in framePath:
917  framePath = "%s.osp" % framePath
918  else:
919  # No path specified (save frame cancelled)
920  self.statusBar.showMessage(_("Save Frame cancelled..."), 5000);
921  return
922 
923  get_app().updates.update(["export_path"], os.path.dirname(framePath))
924  log.info(_("Saving frame to %s" % framePath ))
925 
926  # Pause playback (to prevent crash since we are fixing to change the timeline's max size)
927  get_app().window.actionPlay_trigger(None, force="pause")
928 
929  # Save current cache object and create a new CacheMemory object (ignore quality and scale prefs)
930  old_cache_object = self.cache_object
931  new_cache_object = openshot.CacheMemory(settings.get_settings().get("cache-limit-mb") * 1024 * 1024)
932  self.timeline_sync.timeline.SetCache(new_cache_object)
933 
934  # Set MaxSize to full project resolution and clear preview cache so we get a full resolution frame
935  self.timeline_sync.timeline.SetMaxSize(get_app().project.get(["width"]), get_app().project.get(["height"]))
936  self.cache_object.Clear()
937 
938  # Check if file exists, if it does, get the lastModified time
939  if os.path.exists(framePath):
940  framePathTime = QFileInfo(framePath).lastModified()
941  else:
942  framePathTime = QDateTime()
943 
944  # Get and Save the frame (return is void, so we cannot check for success/fail here - must use file modification timestamp)
945  openshot.Timeline.GetFrame(self.timeline_sync.timeline,self.preview_thread.current_frame).Save(framePath, 1.0)
946 
947  #log.info("orig framePathTime %s" % framePathTime.toString("yyMMdd hh:mm:ss.zzz") )
948  #log.info("new framePathTime %s" % QFileInfo(framePath).lastModified().toString("yyMMdd hh:mm:ss.zzz") )
949 
950  # Show message to user
951  if os.path.exists(framePath) and (QFileInfo(framePath).lastModified() > framePathTime):
952  #QMessageBox.information(self, _("Save Frame Successful"), _("Saved image to %s" % framePath))
953  self.statusBar.showMessage(_("Saved Frame to %s" % framePath), 5000);
954  else:
955  #QMessageBox.warning(self, _("Save Frame Failed"), _("Failed to save image to %s" % framePath))
956  self.statusBar.showMessage( _("Failed to save image to %s" % framePath), 5000);
957 
958  # Reset the MaxSize to match the preview and reset the preview cache
959  viewport_rect = self.videoPreview.centeredViewport(self.videoPreview.width(), self.videoPreview.height())
960  self.timeline_sync.timeline.SetMaxSize(viewport_rect.width(), viewport_rect.height())
961  self.cache_object.Clear()
962  self.timeline_sync.timeline.SetCache(old_cache_object)
963  self.cache_object = old_cache_object
964  old_cache_object = None
965  new_cache_object = None
966 
967  def actionAddTrack_trigger(self, event):
968  log.info("actionAddTrack_trigger")
969 
970  # Get # of tracks
971  track_number = len(get_app().project.get(["layers"]))
972 
973  # Create new track above existing layer(s)
974  track = Track()
975  track.data = {"number": track_number, "y": 0, "label": "", "lock": False}
976  track.save()
977 
978  def actionAddTrackAbove_trigger(self, event):
979  log.info("actionAddTrackAbove_trigger")
980 
981  # Get # of tracks
982  max_track_number = len(get_app().project.get(["layers"]))
983  selected_layer_id = self.selected_tracks[0]
984 
985  # Get selected track data
986  existing_track = Track.get(id=selected_layer_id)
987  selected_layer_number = int(existing_track.data["number"])
988 
989  # log.info("Adding track above #{} (id {})".format(selected_layer_number, selected_layer_id))
990 
991  # Loop through tracks above insert point (in descending order), renumbering layers
992  for existing_layer in list(reversed(range(selected_layer_number+1, max_track_number))):
993  existing_track = Track.get(number=existing_layer)
994  # log.info("Renumbering track id {} from {} to {}".format(existing_track.data["id"], existing_layer, existing_layer+1))
995  existing_track.data["number"] = existing_layer + 1
996  existing_track.save()
997 
998  # Loop through clips and transitions for track, moving up to new layer
999  for clip in Clip.filter(layer=existing_layer):
1000  # log.info("Moving clip id {} from layer {} to {}".format(clip.data["id"], int(clip.data["layer"]), int(clip.data["layer"])+1))
1001  clip.data["layer"] = int(clip.data["layer"]) + 1
1002  clip.save()
1003 
1004  for trans in Transition.filter(layer=existing_layer):
1005  # log.info("Moving transition id {} from layer {} to {}".format(trans.data["id"], int(trans.data["layer"]), int(trans.data["layer"])+1))
1006  trans.data["layer"] = int(trans.data["layer"]) + 1
1007  trans.save()
1008 
1009  # Create new track at vacated layer
1010  track = Track()
1011  track.data = {"number": selected_layer_number+1, "y": 0, "label": "", "lock": False}
1012  track.save()
1013  # log.info("Created new track id {} at layer number {}".format(track.data["id"], track.data["number"]))
1014 
1016  log.info("actionAddTrackBelow_trigger")
1017 
1018  # Get # of tracks
1019  max_track_number = len(get_app().project.get(["layers"]))
1020  selected_layer_id = self.selected_tracks[0]
1021 
1022  # Get selected track data
1023  existing_track = Track.get(id=selected_layer_id)
1024  selected_layer_number = int(existing_track.data["number"])
1025 
1026  # log.info("Adding track below #{} (id {})".format(selected_layer_number, selected_layer_id))
1027 
1028  # Loop through tracks from insert point up (in descending order), renumbering layers
1029  for existing_layer in list(reversed(range(selected_layer_number, max_track_number))):
1030  existing_track = Track.get(number=existing_layer)
1031  # log.info("Renumbering track id {} from {} to {}".format(existing_track.data["id"], existing_layer, existing_layer+1))
1032  existing_track.data["number"] = existing_layer + 1
1033  existing_track.save()
1034 
1035  # Loop through clips and transitions for track, moving up to new layer
1036  for clip in Clip.filter(layer=existing_layer):
1037  # log.info("Moving clip id {} from layer {} to {}".format(clip.data["id"], int(clip.data["layer"]), int(clip.data["layer"])+1))
1038  clip.data["layer"] = int(clip.data["layer"]) + 1
1039  clip.save()
1040 
1041  for trans in Transition.filter(layer=existing_layer):
1042  # log.info("Moving transition id {} from layer {} to {}".format(trans.data["id"], int(trans.data["layer"]), int(trans.data["layer"])+1))
1043  trans.data["layer"] = int(trans.data["layer"]) + 1
1044  trans.save()
1045 
1046  # Create new track at vacated layer
1047  track = Track()
1048  track.data = {"number": selected_layer_number, "y": 0, "label": "", "lock": False}
1049  track.save()
1050  # log.info("Created new track id {} at layer number {}".format(track.data["id"], track.data["number"]))
1051 
1052  def actionArrowTool_trigger(self, event):
1053  log.info("actionArrowTool_trigger")
1054 
1055  def actionSnappingTool_trigger(self, event):
1056  log.info("actionSnappingTool_trigger")
1057  log.info(self.actionSnappingTool.isChecked())
1058 
1059  # Enable / Disable snapping mode
1060  self.timeline.SetSnappingMode(self.actionSnappingTool.isChecked())
1061 
1062  ##
1063  # Toggle razor tool on and off
1064  def actionRazorTool_trigger(self, event):
1065  log.info('actionRazorTool_trigger')
1066 
1067  # Enable / Disable razor mode
1068  self.timeline.SetRazorMode(self.actionRazorTool.isChecked())
1069 
1070  def actionAddMarker_trigger(self, event):
1071  log.info("actionAddMarker_trigger")
1072 
1073  # Get player object
1074  player = self.preview_thread.player
1075 
1076  # Calculate frames per second
1077  fps = get_app().project.get(["fps"])
1078  fps_float = float(fps["num"]) / float(fps["den"])
1079 
1080  # Calculate position in seconds
1081  position = (player.Position() - 1) / fps_float
1082 
1083  # Look for existing Marker
1084  marker = Marker()
1085  marker.data = {"position": position, "icon": "blue.png"}
1086  marker.save()
1087 
1089  log.info("actionPreviousMarker_trigger")
1090 
1091  # Calculate current position (in seconds)
1092  fps = get_app().project.get(["fps"])
1093  fps_float = float(fps["num"]) / float(fps["den"])
1094  current_position = (self.preview_thread.current_frame - 1) / fps_float
1095  all_marker_positions = []
1096 
1097  # Get list of marker and important positions (like selected clip bounds)
1098  for marker in Marker.filter():
1099  all_marker_positions.append(marker.data["position"])
1100 
1101  # Loop through selected clips (and add key positions)
1102  for clip_id in self.selected_clips:
1103  # Get selected object
1104  selected_clip = Clip.get(id=clip_id)
1105  if selected_clip:
1106  all_marker_positions.append(selected_clip.data["position"])
1107  all_marker_positions.append(selected_clip.data["position"] + (selected_clip.data["end"] - selected_clip.data["start"]))
1108 
1109  # Loop through selected transitions (and add key positions)
1110  for tran_id in self.selected_transitions:
1111  # Get selected object
1112  selected_tran = Transition.get(id=tran_id)
1113  if selected_tran:
1114  all_marker_positions.append(selected_tran.data["position"])
1115  all_marker_positions.append(selected_tran.data["position"] + (selected_tran.data["end"] - selected_tran.data["start"]))
1116 
1117  # Loop through all markers, and find the closest one to the left
1118  closest_position = None
1119  for marker_position in sorted(all_marker_positions):
1120  # Is marker smaller than position?
1121  if marker_position < current_position and (abs(marker_position - current_position) > 0.1):
1122  # Is marker larger than previous marker
1123  if closest_position and marker_position > closest_position:
1124  # Set a new closest marker
1125  closest_position = marker_position
1126  elif not closest_position:
1127  # First one found
1128  closest_position = marker_position
1129 
1130  # Seek to marker position (if any)
1131  if closest_position != None:
1132  # Seek
1133  frame_to_seek = round(closest_position * fps_float) + 1
1134  self.SeekSignal.emit(frame_to_seek)
1135 
1136  # Update the preview and reselct current frame in properties
1137  get_app().window.refreshFrameSignal.emit()
1138  get_app().window.propertyTableView.select_frame(frame_to_seek)
1139 
1140  def actionNextMarker_trigger(self, event):
1141  log.info("actionNextMarker_trigger")
1142  log.info(self.preview_thread.current_frame)
1143 
1144  # Calculate current position (in seconds)
1145  fps = get_app().project.get(["fps"])
1146  fps_float = float(fps["num"]) / float(fps["den"])
1147  current_position = (self.preview_thread.current_frame - 1) / fps_float
1148  all_marker_positions = []
1149 
1150  # Get list of marker and important positions (like selected clip bounds)
1151  for marker in Marker.filter():
1152  all_marker_positions.append(marker.data["position"])
1153 
1154  # Loop through selected clips (and add key positions)
1155  for clip_id in self.selected_clips:
1156  # Get selected object
1157  selected_clip = Clip.get(id=clip_id)
1158  if selected_clip:
1159  all_marker_positions.append(selected_clip.data["position"])
1160  all_marker_positions.append(selected_clip.data["position"] + (selected_clip.data["end"] - selected_clip.data["start"]))
1161 
1162  # Loop through selected transitions (and add key positions)
1163  for tran_id in self.selected_transitions:
1164  # Get selected object
1165  selected_tran = Transition.get(id=tran_id)
1166  if selected_tran:
1167  all_marker_positions.append(selected_tran.data["position"])
1168  all_marker_positions.append(selected_tran.data["position"] + (selected_tran.data["end"] - selected_tran.data["start"]))
1169 
1170  # Loop through all markers, and find the closest one to the right
1171  closest_position = None
1172  for marker_position in sorted(all_marker_positions):
1173  # Is marker smaller than position?
1174  if marker_position > current_position and (abs(marker_position - current_position) > 0.1):
1175  # Is marker larger than previous marker
1176  if closest_position and marker_position < closest_position:
1177  # Set a new closest marker
1178  closest_position = marker_position
1179  elif not closest_position:
1180  # First one found
1181  closest_position = marker_position
1182 
1183  # Seek to marker position (if any)
1184  if closest_position != None:
1185  # Seek
1186  frame_to_seek = round(closest_position * fps_float) + 1
1187  self.SeekSignal.emit(frame_to_seek)
1188 
1189  # Update the preview and reselct current frame in properties
1190  get_app().window.refreshFrameSignal.emit()
1191  get_app().window.propertyTableView.select_frame(frame_to_seek)
1192 
1193  ##
1194  # Get a key sequence back from the setting name
1195  def getShortcutByName(self, setting_name):
1196  s = settings.get_settings()
1197  shortcut = QKeySequence(s.get(setting_name))
1198  return shortcut
1199 
1200  ##
1201  # Get a key sequence back from the setting name
1203  keyboard_shortcuts = []
1204  all_settings = settings.get_settings()._data
1205  for setting in all_settings:
1206  if setting.get('category') == 'Keyboard':
1207  keyboard_shortcuts.append(setting)
1208  return keyboard_shortcuts
1209 
1210  ##
1211  # Process key press events and match with known shortcuts
1212  def keyPressEvent(self, event):
1213  # Detect the current KeySequence pressed (including modifier keys)
1214  key_value = event.key()
1215  print(key_value)
1216  modifiers = int(event.modifiers())
1217  if (key_value > 0 and key_value != Qt.Key_Shift and key_value != Qt.Key_Alt and
1218  key_value != Qt.Key_Control and key_value != Qt.Key_Meta):
1219  # A valid keysequence was detected
1220  key = QKeySequence(modifiers + key_value)
1221  else:
1222  # No valid keysequence detected
1223  return
1224 
1225  # Debug
1226  log.info("keyPressEvent: %s" % (key.toString()))
1227 
1228  # Get the video player object
1229  player = self.preview_thread.player
1230 
1231  # Get framerate
1232  fps = get_app().project.get(["fps"])
1233  fps_float = float(fps["num"]) / float(fps["den"])
1234  playhead_position = float(self.preview_thread.current_frame - 1) / fps_float
1235 
1236  # Basic shortcuts i.e just a letter
1237  if key.matches(self.getShortcutByName("seekPreviousFrame")) == QKeySequence.ExactMatch:
1238  # Pause video
1239  self.actionPlay_trigger(event, force="pause")
1240  # Set speed to 0
1241  if player.Speed() != 0:
1242  self.SpeedSignal.emit(0)
1243  # Seek to previous frame
1244  self.SeekSignal.emit(player.Position() - 1)
1245 
1246  # Notify properties dialog
1247  self.propertyTableView.select_frame(player.Position())
1248 
1249  elif key.matches(self.getShortcutByName("seekNextFrame")) == QKeySequence.ExactMatch:
1250  # Pause video
1251  self.actionPlay_trigger(event, force="pause")
1252  # Set speed to 0
1253  if player.Speed() != 0:
1254  self.SpeedSignal.emit(0)
1255  # Seek to next frame
1256  self.SeekSignal.emit(player.Position() + 1)
1257 
1258  # Notify properties dialog
1259  self.propertyTableView.select_frame(player.Position())
1260 
1261  elif key.matches(self.getShortcutByName("rewindVideo")) == QKeySequence.ExactMatch:
1262  # Toggle rewind and start playback
1263  self.actionRewind.trigger()
1264  ui_util.setup_icon(self, self.actionPlay, "actionPlay", "media-playback-pause")
1265  self.actionPlay.setChecked(True)
1266 
1267  elif key.matches(self.getShortcutByName("fastforwardVideo")) == QKeySequence.ExactMatch:
1268  # Toggle fastforward button and start playback
1269  self.actionFastForward.trigger()
1270  ui_util.setup_icon(self, self.actionPlay, "actionPlay", "media-playback-pause")
1271  self.actionPlay.setChecked(True)
1272 
1273  elif key.matches(self.getShortcutByName("playToggle")) == QKeySequence.ExactMatch or \
1274  key.matches(self.getShortcutByName("playToggle1")) == QKeySequence.ExactMatch or \
1275  key.matches(self.getShortcutByName("playToggle2")) == QKeySequence.ExactMatch or \
1276  key.matches(self.getShortcutByName("playToggle3")) == QKeySequence.ExactMatch:
1277  # Toggle playbutton and show properties
1278  self.actionPlay.trigger()
1279  self.propertyTableView.select_frame(player.Position())
1280 
1281  elif key.matches(self.getShortcutByName("deleteItem")) == QKeySequence.ExactMatch or \
1282  key.matches(self.getShortcutByName("deleteItem1")) == QKeySequence.ExactMatch:
1283  # Delete selected clip / transition
1284  self.actionRemoveClip.trigger()
1285  self.actionRemoveTransition.trigger()
1286 
1287  # Boiler plate key mappings (mostly for menu support on Ubuntu/Unity)
1288  elif key.matches(self.getShortcutByName("actionNew")) == QKeySequence.ExactMatch:
1289  self.actionNew.trigger()
1290  elif key.matches(self.getShortcutByName("actionOpen")) == QKeySequence.ExactMatch:
1291  self.actionOpen.trigger()
1292  elif key.matches(self.getShortcutByName("actionSave")) == QKeySequence.ExactMatch:
1293  self.actionSave.trigger()
1294  elif key.matches(self.getShortcutByName("actionUndo")) == QKeySequence.ExactMatch:
1295  self.actionUndo.trigger()
1296  elif key.matches(self.getShortcutByName("actionSaveAs")) == QKeySequence.ExactMatch:
1297  self.actionSaveAs.trigger()
1298  elif key.matches(self.getShortcutByName("actionImportFiles")) == QKeySequence.ExactMatch:
1299  self.actionImportFiles.trigger()
1300  elif key.matches(self.getShortcutByName("actionRedo")) == QKeySequence.ExactMatch:
1301  self.actionRedo.trigger()
1302  elif key.matches(self.getShortcutByName("actionExportVideo")) == QKeySequence.ExactMatch:
1303  self.actionExportVideo.trigger()
1304  elif key.matches(self.getShortcutByName("actionQuit")) == QKeySequence.ExactMatch:
1305  self.actionQuit.trigger()
1306  elif key.matches(self.getShortcutByName("actionPreferences")) == QKeySequence.ExactMatch:
1307  self.actionPreferences.trigger()
1308  elif key.matches(self.getShortcutByName("actionAddTrack")) == QKeySequence.ExactMatch:
1309  self.actionAddTrack.trigger()
1310  elif key.matches(self.getShortcutByName("actionAddMarker")) == QKeySequence.ExactMatch:
1311  self.actionAddMarker.trigger()
1312  elif key.matches(self.getShortcutByName("actionPreviousMarker")) == QKeySequence.ExactMatch:
1313  self.actionPreviousMarker.trigger()
1314  elif key.matches(self.getShortcutByName("actionNextMarker")) == QKeySequence.ExactMatch:
1315  self.actionNextMarker.trigger()
1316  elif key.matches(self.getShortcutByName("actionTimelineZoomIn")) == QKeySequence.ExactMatch:
1317  self.actionTimelineZoomIn.trigger()
1318  elif key.matches(self.getShortcutByName("actionTimelineZoomOut")) == QKeySequence.ExactMatch:
1319  self.actionTimelineZoomOut.trigger()
1320  elif key.matches(self.getShortcutByName("actionTitle")) == QKeySequence.ExactMatch:
1321  self.actionTitle.trigger()
1322  elif key.matches(self.getShortcutByName("actionAnimatedTitle")) == QKeySequence.ExactMatch:
1323  self.actionAnimatedTitle.trigger()
1324  elif key.matches(self.getShortcutByName("actionFullscreen")) == QKeySequence.ExactMatch:
1325  self.actionFullscreen.trigger()
1326  elif key.matches(self.getShortcutByName("actionAbout")) == QKeySequence.ExactMatch:
1327  self.actionAbout.trigger()
1328  elif key.matches(self.getShortcutByName("actionThumbnailView")) == QKeySequence.ExactMatch:
1329  self.actionThumbnailView.trigger()
1330  elif key.matches(self.getShortcutByName("actionDetailsView")) == QKeySequence.ExactMatch:
1331  self.actionDetailsView.trigger()
1332  elif key.matches(self.getShortcutByName("actionProfile")) == QKeySequence.ExactMatch:
1333  self.actionProfile.trigger()
1334  elif key.matches(self.getShortcutByName("actionAdd_to_Timeline")) == QKeySequence.ExactMatch:
1335  self.actionAdd_to_Timeline.trigger()
1336  elif key.matches(self.getShortcutByName("actionSplitClip")) == QKeySequence.ExactMatch:
1337  self.actionSplitClip.trigger()
1338  elif key.matches(self.getShortcutByName("actionSnappingTool")) == QKeySequence.ExactMatch:
1339  self.actionSnappingTool.trigger()
1340  elif key.matches(self.getShortcutByName("actionJumpStart")) == QKeySequence.ExactMatch:
1341  self.actionJumpStart.trigger()
1342  elif key.matches(self.getShortcutByName("actionJumpEnd")) == QKeySequence.ExactMatch:
1343  self.actionJumpEnd.trigger()
1344  elif key.matches(self.getShortcutByName("actionSaveFrame")) == QKeySequence.ExactMatch:
1345  self.actionSaveFrame.trigger()
1346  elif key.matches(self.getShortcutByName("actionProperties")) == QKeySequence.ExactMatch:
1347  self.actionProperties.trigger()
1348  elif key.matches(self.getShortcutByName("actionTransform")) == QKeySequence.ExactMatch:
1349  if not self.is_transforming and self.selected_clips:
1350  self.TransformSignal.emit(self.selected_clips[0])
1351  else:
1352  self.TransformSignal.emit("")
1353 
1354  elif key.matches(self.getShortcutByName("actionInsertKeyframe")) == QKeySequence.ExactMatch:
1355  print("actionInsertKeyframe")
1356  if self.selected_clips or self.selected_transitions:
1357  self.InsertKeyframe.emit(event)
1358 
1359  # Timeline keyboard shortcuts
1360  elif key.matches(self.getShortcutByName("sliceAllKeepBothSides")) == QKeySequence.ExactMatch:
1361  intersecting_clips = Clip.filter(intersect=playhead_position)
1362  intersecting_trans = Transition.filter(intersect=playhead_position)
1363  if intersecting_clips or intersecting_trans:
1364  # Get list of clip ids
1365  clip_ids = [c.id for c in intersecting_clips]
1366  trans_ids = [t.id for t in intersecting_trans]
1367  self.timeline.Slice_Triggered(0, clip_ids, trans_ids, playhead_position)
1368  elif key.matches(self.getShortcutByName("sliceAllKeepLeftSide")) == QKeySequence.ExactMatch:
1369  intersecting_clips = Clip.filter(intersect=playhead_position)
1370  intersecting_trans = Transition.filter(intersect=playhead_position)
1371  if intersecting_clips or intersecting_trans:
1372  # Get list of clip ids
1373  clip_ids = [c.id for c in intersecting_clips]
1374  trans_ids = [t.id for t in intersecting_trans]
1375  self.timeline.Slice_Triggered(1, clip_ids, trans_ids, playhead_position)
1376  elif key.matches(self.getShortcutByName("sliceAllKeepRightSide")) == QKeySequence.ExactMatch:
1377  intersecting_clips = Clip.filter(intersect=playhead_position)
1378  intersecting_trans = Transition.filter(intersect=playhead_position)
1379  if intersecting_clips or intersecting_trans:
1380  # Get list of clip ids
1381  clip_ids = [c.id for c in intersecting_clips]
1382  trans_ids = [t.id for t in intersecting_trans]
1383  self.timeline.Slice_Triggered(2, clip_ids, trans_ids, playhead_position)
1384  elif key.matches(self.getShortcutByName("copyAll")) == QKeySequence.ExactMatch:
1385  self.timeline.Copy_Triggered(-1, self.selected_clips, self.selected_transitions)
1386  elif key.matches(self.getShortcutByName("pasteAll")) == QKeySequence.ExactMatch:
1387  self.timeline.Paste_Triggered(9, float(playhead_position), -1, [], [])
1388  elif key.matches(self.getShortcutByName("nudgeLeft")) == QKeySequence.ExactMatch:
1389  self.timeline.Nudge_Triggered(-1, self.selected_clips, self.selected_transitions)
1390  elif key.matches(self.getShortcutByName("nudgeRight")) == QKeySequence.ExactMatch:
1391  self.timeline.Nudge_Triggered(1, self.selected_clips, self.selected_transitions)
1392 
1393  # Select All / None
1394  elif key.matches(self.getShortcutByName("selectAll")) == QKeySequence.ExactMatch:
1395  self.timeline.SelectAll()
1396 
1397  elif key.matches(self.getShortcutByName("selectNone")) == QKeySequence.ExactMatch:
1398  self.timeline.ClearAllSelections()
1399 
1400  # Bubble event on
1401  event.ignore()
1402 
1403 
1404  def actionProfile_trigger(self, event):
1405  # Show dialog
1406  from windows.profile import Profile
1407  win = Profile()
1408  # Run the dialog event loop - blocking interaction on this window during this time
1409  result = win.exec_()
1410  if result == QDialog.Accepted:
1411  log.info('Profile add confirmed')
1412 
1413 
1414  def actionSplitClip_trigger(self, event):
1415  log.info("actionSplitClip_trigger")
1416 
1417  # Loop through selected files (set 1 selected file if more than 1)
1418  f = None
1419  for file_id in self.selected_files:
1420  # Find matching file
1421  f = File.get(id=file_id)
1422 
1423  # Bail out if no file selected
1424  if not f:
1425  log.info(self.selected_files)
1426  return
1427 
1428  # show dialog
1429  from windows.cutting import Cutting
1430  win = Cutting(f)
1431  # Run the dialog event loop - blocking interaction on this window during that time
1432  result = win.exec_()
1433  if result == QDialog.Accepted:
1434  log.info('Cutting Finished')
1435  else:
1436  log.info('Cutting Cancelled')
1437 
1439  log.info("actionRemove_from_Project_trigger")
1440 
1441  # Loop through selected files
1442  for file_id in self.selected_files:
1443  # Find matching file
1444  f = File.get(id=file_id)
1445  if f:
1446  # Remove file
1447  f.delete()
1448 
1449  # Find matching clips (if any)
1450  clips = Clip.filter(file_id=file_id)
1451  for c in clips:
1452  # Remove clip
1453  c.delete()
1454 
1455  # Clear selected files
1456  self.selected_files = []
1457 
1458  def actionRemoveClip_trigger(self, event):
1459  log.info('actionRemoveClip_trigger')
1460 
1461  # Loop through selected clips
1462  for clip_id in deepcopy(self.selected_clips):
1463  # Find matching file
1464  clips = Clip.filter(id=clip_id)
1465  for c in clips:
1466  # Clear selected clips
1467  self.removeSelection(clip_id, "clip")
1468 
1469  # Remove clip
1470  c.delete()
1471 
1472  def actionProperties_trigger(self, event):
1473  log.info('actionProperties_trigger')
1474 
1475  # Show properties dock
1476  if not self.dockProperties.isVisible():
1477  self.dockProperties.show()
1478 
1479  def actionRemoveEffect_trigger(self, event):
1480  log.info('actionRemoveEffect_trigger')
1481 
1482  # Loop through selected clips
1483  for effect_id in deepcopy(self.selected_effects):
1484  log.info("effect id: %s" % effect_id)
1485 
1486  # Find matching file
1487  clips = Clip.filter()
1488  found_effect = None
1489  for c in clips:
1490  found_effect = False
1491  log.info("c.data[effects]: %s" % c.data["effects"])
1492 
1493  for effect in c.data["effects"]:
1494  if effect["id"] == effect_id:
1495  found_effect = effect
1496  break
1497 
1498  if found_effect:
1499  # Remove found effect from clip data and save clip
1500  c.data["effects"].remove(found_effect)
1501 
1502  # Remove unneeded attributes from JSON
1503  c.data.pop("reader")
1504 
1505  # Save clip
1506  c.save()
1507 
1508  # Clear selected effects
1509  self.removeSelection(effect_id, "effect")
1510 
1512  log.info('actionRemoveTransition_trigger')
1513 
1514  # Loop through selected clips
1515  for tran_id in deepcopy(self.selected_transitions):
1516  # Find matching file
1517  transitions = Transition.filter(id=tran_id)
1518  for t in transitions:
1519  # Clear selected clips
1520  self.removeSelection(tran_id, "transition")
1521 
1522  # Remove transition
1523  t.delete()
1524 
1525  def actionRemoveTrack_trigger(self, event):
1526  log.info('actionRemoveTrack_trigger')
1527 
1528  # Get translation function
1529  _ = get_app()._tr
1530 
1531  track_id = self.selected_tracks[0]
1532  max_track_number = len(get_app().project.get(["layers"]))
1533 
1534  # Get details of selected track
1535  selected_track = Track.get(id=track_id)
1536  selected_track_number = int(selected_track.data["number"])
1537 
1538  # Don't allow user to delete final track
1539  if max_track_number == 1:
1540  # Show error and do nothing
1541  QMessageBox.warning(self, _("Error Removing Track"), _("You must keep at least 1 track"))
1542  return
1543 
1544  # Revove all clips on this track first
1545  for clip in Clip.filter(layer=selected_track_number):
1546  clip.delete()
1547 
1548  # Revove all transitions on this track first
1549  for trans in Transition.filter(layer=selected_track_number):
1550  trans.delete()
1551 
1552  # Remove track
1553  selected_track.delete()
1554 
1555  # Loop through all layers above, and renumber elements (to keep thing in numerical order)
1556  for existing_layer in list(range(selected_track_number + 1, max_track_number)):
1557  # Update existing layer number
1558  track = Track.get(number=existing_layer)
1559  track.data["number"] = existing_layer - 1
1560  track.save()
1561 
1562  for clip in Clip.filter(layer=existing_layer):
1563  clip.data["layer"] = int(clip.data["layer"]) - 1
1564  clip.save()
1565 
1566  for trans in Transition.filter(layer=existing_layer):
1567  trans.data["layer"] = int(trans.data["layer"]) - 1
1568  trans.save()
1569 
1570 
1571  # Clear selected track
1573 
1574  ##
1575  # Callback for locking a track
1576  def actionLockTrack_trigger(self, event):
1577  log.info('actionLockTrack_trigger')
1578 
1579  # Get details of track
1580  track_id = self.selected_tracks[0]
1581  selected_track = Track.get(id=track_id)
1582 
1583  # Lock track and save
1584  selected_track.data['lock'] = True
1585  selected_track.save()
1586 
1587  ##
1588  # Callback for unlocking a track
1589  def actionUnlockTrack_trigger(self, event):
1590  log.info('actionUnlockTrack_trigger')
1591 
1592  # Get details of track
1593  track_id = self.selected_tracks[0]
1594  selected_track = Track.get(id=track_id)
1595 
1596  # Lock track and save
1597  selected_track.data['lock'] = False
1598  selected_track.save()
1599 
1600  ##
1601  # Callback for renaming track
1602  def actionRenameTrack_trigger(self, event):
1603  log.info('actionRenameTrack_trigger')
1604 
1605  # Get translation function
1606  _ = get_app()._tr
1607 
1608  # Get details of track
1609  track_id = self.selected_tracks[0]
1610  selected_track = Track.get(id=track_id)
1611  track_name = selected_track.data["label"] or _("Track %s") % selected_track.data["number"]
1612 
1613  text, ok = QInputDialog.getText(self, _('Rename Track'), _('Track Name:'), text=track_name)
1614  if ok:
1615  # Update track
1616  selected_track.data["label"] = text
1617  selected_track.save()
1618 
1619  def actionRemoveMarker_trigger(self, event):
1620  log.info('actionRemoveMarker_trigger')
1621 
1622  for marker_id in self.selected_markers:
1623  marker = Marker.filter(id=marker_id)
1624  for m in marker:
1625  # Remove track
1626  m.delete()
1627 
1629  self.sliderZoom.setValue(self.sliderZoom.value() - self.sliderZoom.singleStep())
1630 
1632  self.sliderZoom.setValue(self.sliderZoom.value() + self.sliderZoom.singleStep())
1633 
1634  def actionFullscreen_trigger(self, event):
1635  # Toggle fullscreen mode
1636  if not self.isFullScreen():
1637  self.showFullScreen()
1638  else:
1639  self.showNormal()
1640 
1642  log.info("Show file properties")
1643 
1644  # Loop through selected files (set 1 selected file if more than 1)
1645  f = None
1646  for file_id in self.selected_files:
1647  # Find matching file
1648  f = File.get(id=file_id)
1649 
1650  # show dialog
1651  from windows.file_properties import FileProperties
1652  win = FileProperties(f)
1653  # Run the dialog event loop - blocking interaction on this window during that time
1654  result = win.exec_()
1655  if result == QDialog.Accepted:
1656  log.info('File Properties Finished')
1657  else:
1658  log.info('File Properties Cancelled')
1659 
1660  def actionDetailsView_trigger(self, event):
1661  log.info("Switch to Details View")
1662 
1663  # Get settings
1664  app = get_app()
1665  s = settings.get_settings()
1666 
1667  # Prepare treeview for deletion
1668  if self.filesTreeView:
1669  self.filesTreeView.prepare_for_delete()
1670 
1671  # Files
1672  if app.context_menu_object == "files":
1673  s.set("file_view", "details")
1674  self.tabFiles.layout().removeWidget(self.filesTreeView)
1675  self.filesTreeView.deleteLater()
1676  self.filesTreeView = None
1677  self.filesTreeView = FilesTreeView(self)
1678  self.tabFiles.layout().addWidget(self.filesTreeView)
1679 
1680  # Transitions
1681  elif app.context_menu_object == "transitions":
1682  s.set("transitions_view", "details")
1683  self.tabTransitions.layout().removeWidget(self.transitionsTreeView)
1684  self.transitionsTreeView.deleteLater()
1686  self.transitionsTreeView = TransitionsTreeView(self)
1687  self.tabTransitions.layout().addWidget(self.transitionsTreeView)
1688 
1689  # Effects
1690  elif app.context_menu_object == "effects":
1691  s.set("effects_view", "details")
1692  self.tabEffects.layout().removeWidget(self.effectsTreeView)
1693  self.effectsTreeView.deleteLater()
1694  self.effectsTreeView = None
1695  self.effectsTreeView = EffectsTreeView(self)
1696  self.tabEffects.layout().addWidget(self.effectsTreeView)
1697 
1699  log.info("Switch to Thumbnail View")
1700 
1701  # Get settings
1702  app = get_app()
1703  s = settings.get_settings()
1704 
1705  # Prepare treeview for deletion
1706  if self.filesTreeView:
1707  self.filesTreeView.prepare_for_delete()
1708 
1709  # Files
1710  if app.context_menu_object == "files":
1711  s.set("file_view", "thumbnail")
1712  self.tabFiles.layout().removeWidget(self.filesTreeView)
1713  self.filesTreeView.deleteLater()
1714  self.filesTreeView = None
1715  self.filesTreeView = FilesListView(self)
1716  self.tabFiles.layout().addWidget(self.filesTreeView)
1717 
1718  # Transitions
1719  elif app.context_menu_object == "transitions":
1720  s.set("transitions_view", "thumbnail")
1721  self.tabTransitions.layout().removeWidget(self.transitionsTreeView)
1722  self.transitionsTreeView.deleteLater()
1723  self.transitionsTreeView = None
1724  self.transitionsTreeView = TransitionsListView(self)
1725  self.tabTransitions.layout().addWidget(self.transitionsTreeView)
1726 
1727  # Effects
1728  elif app.context_menu_object == "effects":
1729  s.set("effects_view", "thumbnail")
1730  self.tabEffects.layout().removeWidget(self.effectsTreeView)
1731  self.effectsTreeView.deleteLater()
1732  self.effectsTreeView = None
1733  self.effectsTreeView = EffectsListView(self)
1734  self.tabEffects.layout().addWidget(self.effectsTreeView)
1735 
1736  def resize_contents(self):
1737  if self.filesTreeView:
1738  self.filesTreeView.resize_contents()
1739 
1740  ##
1741  # Get a list of all dockable widgets
1742  def getDocks(self):
1743  return [self.dockFiles,
1744  self.dockTransitions,
1745  self.dockEffects,
1746  self.dockVideo,
1747  self.dockProperties]
1748 
1749  ##
1750  # Remove all dockable widgets on main screen
1751  def removeDocks(self):
1752  for dock in self.getDocks():
1753  self.removeDockWidget(dock)
1754 
1755  ##
1756  # Add all dockable widgets to the same dock area on the main screen
1757  def addDocks(self, docks, area):
1758  for dock in docks:
1759  self.addDockWidget(area, dock)
1760 
1761  ##
1762  # Float or Un-Float all dockable widgets above main screen
1763  def floatDocks(self, is_floating):
1764  for dock in self.getDocks():
1765  dock.setFloating(is_floating)
1766 
1767  ##
1768  # Show all dockable widgets on the main screen
1769  def showDocks(self, docks):
1770  for dock in docks:
1771  if get_app().window.dockWidgetArea(dock) != Qt.NoDockWidgetArea:
1772  # Only show correctly docked widgets
1773  dock.show()
1774 
1775  ##
1776  # Freeze all dockable widgets on the main screen (no float, moving, or closing)
1777  def freezeDocks(self):
1778  for dock in self.getDocks():
1779  dock.setFeatures(QDockWidget.NoDockWidgetFeatures)
1780 
1781  ##
1782  # Un-freeze all dockable widgets on the main screen (allow them to be moved, closed, and floated)
1783  def unFreezeDocks(self):
1784  for dock in self.getDocks():
1785  dock.setFeatures(QDockWidget.AllDockWidgetFeatures)
1786 
1787  ##
1788  # Hide all dockable widgets on the main screen
1789  def hideDocks(self):
1790  for dock in self.getDocks():
1791  dock.hide()
1792 
1793  ##
1794  # Switch to the default / simple view
1795  def actionSimple_View_trigger(self, event):
1796  self.removeDocks()
1797  self.addDocks([self.dockFiles, self.dockTransitions, self.dockEffects, self.dockVideo], Qt.TopDockWidgetArea)
1798  self.floatDocks(False)
1799  self.tabifyDockWidget(self.dockFiles, self.dockTransitions)
1800  self.tabifyDockWidget(self.dockTransitions, self.dockEffects)
1801  self.showDocks([self.dockFiles, self.dockTransitions, self.dockEffects, self.dockVideo])
1802 
1803  # Set initial size of docks
1804  simple_state = "AAAA/wAAAAD9AAAAAwAAAAAAAAD8AAAA9PwCAAAAAfwAAAILAAAA9AAAAAAA////+v////8CAAAAAvsAAAAcAGQAbwBjAGsAUAByAG8AcABlAHIAdABpAGUAcwAAAAAA/////wAAAKEA////+wAAABgAZABvAGMAawBLAGUAeQBmAHIAYQBtAGUAAAAAAP////8AAAATAP///wAAAAEAAAEcAAABQPwCAAAAAfsAAAAYAGQAbwBjAGsASwBlAHkAZgByAGEAbQBlAQAAAVgAAAAVAAAAAAAAAAAAAAACAAAEqwAAAdz8AQAAAAL8AAAAAAAAAWQAAAB7AP////oAAAAAAgAAAAP7AAAAEgBkAG8AYwBrAEYAaQBsAGUAcwEAAAAA/////wAAAJgA////+wAAAB4AZABvAGMAawBUAHIAYQBuAHMAaQB0AGkAbwBuAHMBAAAAAP////8AAACYAP////sAAAAWAGQAbwBjAGsARQBmAGYAZQBjAHQAcwEAAAAA/////wAAAJgA////+wAAABIAZABvAGMAawBWAGkAZABlAG8BAAABagAAA0EAAAA6AP///wAABKsAAAD2AAAABAAAAAQAAAAIAAAACPwAAAABAAAAAgAAAAEAAAAOAHQAbwBvAGwAQgBhAHIBAAAAAP////8AAAAAAAAAAA=="
1805  self.restoreState(qt_types.str_to_bytes(simple_state))
1806  QCoreApplication.processEvents()
1807 
1808 
1809  ##
1810  # Switch to an alternative view
1812  self.removeDocks()
1813 
1814  # Add Docks
1815  self.addDocks([self.dockFiles, self.dockTransitions, self.dockVideo], Qt.TopDockWidgetArea)
1816  self.addDocks([self.dockEffects], Qt.RightDockWidgetArea)
1817  self.addDocks([self.dockProperties], Qt.LeftDockWidgetArea)
1818 
1819  self.floatDocks(False)
1820  self.showDocks([self.dockFiles, self.dockTransitions, self.dockVideo, self.dockEffects, self.dockProperties])
1821 
1822  # Set initial size of docks
1823  advanced_state = "AAAA/wAAAAD9AAAAAwAAAAAAAAD8AAAB5fwCAAAAAfwAAAHVAAAB5QAAAKEA////+gAAAAACAAAAAvsAAAAcAGQAbwBjAGsAUAByAG8AcABlAHIAdABpAGUAcwEAAAAA/////wAAAKEA////+wAAABgAZABvAGMAawBLAGUAeQBmAHIAYQBtAGUAAAAAAP////8AAAAAAAAAAAAAAAEAAAD2AAAB5fwCAAAAAvsAAAAYAGQAbwBjAGsASwBlAHkAZgByAGEAbQBlAQAAAVgAAAAVAAAAAAAAAAD7AAAAFgBkAG8AYwBrAEUAZgBmAGUAYwB0AHMBAAAB1QAAAeUAAACYAP///wAAAAIAAAVmAAABkvwBAAAAA/sAAAASAGQAbwBjAGsARgBpAGwAZQBzAQAAAAAAAAIZAAAAbAD////7AAAAHgBkAG8AYwBrAFQAcgBhAG4AcwBpAHQAaQBvAG4AcwEAAAIfAAABAAAAAGwA////+wAAABIAZABvAGMAawBWAGkAZABlAG8BAAADJQAAAkEAAAA6AP///wAAA2gAAAHlAAAABAAAAAQAAAAIAAAACPwAAAABAAAAAgAAAAEAAAAOAHQAbwBvAGwAQgBhAHIBAAAAAP////8AAAAAAAAAAA=="
1824  self.restoreState(qt_types.str_to_bytes(advanced_state))
1825  QCoreApplication.processEvents()
1826 
1827  ##
1828  # Freeze all dockable widgets on the main screen
1829  def actionFreeze_View_trigger(self, event):
1830  self.freezeDocks()
1831  self.actionFreeze_View.setVisible(False)
1832  self.actionUn_Freeze_View.setVisible(True)
1833 
1834  ##
1835  # Un-Freeze all dockable widgets on the main screen
1837  self.unFreezeDocks()
1838  self.actionFreeze_View.setVisible(True)
1839  self.actionUn_Freeze_View.setVisible(False)
1840 
1841  ##
1842  # Show all dockable widgets
1843  def actionShow_All_trigger(self, event):
1844  self.showDocks(self.getDocks())
1845 
1846  ##
1847  # Show tutorial again
1848  def actionTutorial_trigger(self, event):
1849  s = settings.get_settings()
1850 
1851  # Clear tutorial settings
1852  s.set("tutorial_enabled", True)
1853  s.set("tutorial_ids", "")
1854 
1855  # Show first tutorial dialog again
1856  if self.tutorial_manager:
1857  self.tutorial_manager.exit_manager()
1858  self.tutorial_manager = TutorialManager(self)
1859 
1860  ##
1861  # Set the window title based on a variety of factors
1862  def SetWindowTitle(self, profile=None):
1863 
1864  # Get translation function
1865  _ = get_app()._tr
1866 
1867  if not profile:
1868  profile = get_app().project.get(["profile"])
1869 
1870  # Determine if the project needs saving (has any unsaved changes)
1871  save_indicator = ""
1872  if get_app().project.needs_save():
1873  save_indicator = "*"
1874  self.actionSave.setEnabled(True)
1875  else:
1876  self.actionSave.setEnabled(False)
1877 
1878  # Is this a saved project?
1879  if not get_app().project.current_filepath:
1880  # Not saved yet
1881  self.setWindowTitle("%s %s [%s] - %s" % (save_indicator, _("Untitled Project"), profile, "OpenShot Video Editor"))
1882  else:
1883  # Yes, project is saved
1884  # Get just the filename
1885  parent_path, filename = os.path.split(get_app().project.current_filepath)
1886  filename, ext = os.path.splitext(filename)
1887  filename = filename.replace("_", " ").replace("-", " ").capitalize()
1888  self.setWindowTitle("%s %s [%s] - %s" % (save_indicator, filename, profile, "OpenShot Video Editor"))
1889 
1890  # Update undo and redo buttons enabled/disabled to available changes
1891  def updateStatusChanged(self, undo_status, redo_status):
1892  log.info('updateStatusChanged')
1893  self.actionUndo.setEnabled(undo_status)
1894  self.actionRedo.setEnabled(redo_status)
1895  self.SetWindowTitle()
1896 
1897  # Add to the selected items
1898  def addSelection(self, item_id, item_type, clear_existing=False):
1899  log.info('main::addSelection: item_id: %s, item_type: %s, clear_existing: %s' % (item_id, item_type, clear_existing))
1900 
1901  # Clear existing selection (if needed)
1902  if clear_existing:
1903  if item_type == "clip":
1904  self.selected_clips.clear()
1905  elif item_type == "transition":
1906  self.selected_transitions.clear()
1907  elif item_type == "effect":
1908  self.selected_effects.clear()
1909 
1910  # Clear transform (if any)
1911  self.TransformSignal.emit("")
1912 
1913  if item_id:
1914  # If item_id is not blank, store it
1915  if item_type == "clip" and item_id not in self.selected_clips:
1916  self.selected_clips.append(item_id)
1917  elif item_type == "transition" and item_id not in self.selected_transitions:
1918  self.selected_transitions.append(item_id)
1919  elif item_type == "effect" and item_id not in self.selected_effects:
1920  self.selected_effects.append(item_id)
1921 
1922  # Change selected item in properties view
1923  self.show_property_id = item_id
1924  self.show_property_type = item_type
1925  self.show_property_timer.start()
1926 
1927  # Remove from the selected items
1928  def removeSelection(self, item_id, item_type):
1929  # Remove existing selection (if any)
1930  if item_id:
1931  if item_type == "clip" and item_id in self.selected_clips:
1932  self.selected_clips.remove(item_id)
1933  elif item_type == "transition" and item_id in self.selected_transitions:
1934  self.selected_transitions.remove(item_id)
1935  elif item_type == "effect" and item_id in self.selected_effects:
1936  self.selected_effects.remove(item_id)
1937 
1938  # Clear transform (if any)
1939  get_app().window.TransformSignal.emit("")
1940 
1941  # Move selection to next selected clip (if any)
1942  self.show_property_id = ""
1943  self.show_property_type = ""
1944  if item_type == "clip" and self.selected_clips:
1945  self.show_property_id = self.selected_clips[0]
1946  self.show_property_type = item_type
1947  elif item_type == "transition" and self.selected_transitions:
1948  self.show_property_id = self.selected_transitions[0]
1949  self.show_property_type = item_type
1950  elif item_type == "effect" and self.selected_effects:
1951  self.show_property_id = self.selected_effects[0]
1952  self.show_property_type = item_type
1953 
1954  # Change selected item in properties view
1955  self.show_property_timer.start()
1956 
1957  # Update window settings in setting store
1958  def save_settings(self):
1959  s = settings.get_settings()
1960 
1961  # Save window state and geometry (saves toolbar and dock locations)
1962  s.set('window_state', qt_types.bytes_to_str(self.saveState()))
1963  s.set('window_geometry', qt_types.bytes_to_str(self.saveGeometry()))
1964 
1965  # Get window settings from setting store
1966  def load_settings(self):
1967  s = settings.get_settings()
1968 
1969  # Window state and geometry (also toolbar and dock locations)
1970  if s.get('window_geometry'): self.restoreGeometry(qt_types.str_to_bytes(s.get('window_geometry')))
1971  if s.get('window_state'): self.restoreState(qt_types.str_to_bytes(s.get('window_state')))
1972 
1973  # Load Recent Projects
1974  self.load_recent_menu()
1975 
1976  ##
1977  # Clear and load the list of recent menu items
1978  def load_recent_menu(self):
1979  s = settings.get_settings()
1980  _ = get_app()._tr # Get translation function
1981 
1982  # Get list of recent projects
1983  recent_projects = s.get("recent_projects")
1984 
1985  # Add Recent Projects menu (after Open File)
1986  import functools
1987  if not self.recent_menu:
1988  # Create a new recent menu
1989  self.recent_menu = self.menuFile.addMenu(QIcon.fromTheme("document-open-recent"), _("Recent Projects"))
1990  self.menuFile.insertMenu(self.actionRecent_Placeholder, self.recent_menu)
1991  else:
1992  # Clear the existing children
1993  self.recent_menu.clear()
1994 
1995  # Add recent projects to menu
1996  for file_path in reversed(recent_projects):
1997  new_action = self.recent_menu.addAction(file_path)
1998  new_action.triggered.connect(functools.partial(self.recent_project_clicked, file_path))
1999 
2000  ##
2001  # Load a recent project when clicked
2002  def recent_project_clicked(self, file_path):
2003 
2004  # Load project file
2005  self.open_project(file_path)
2006 
2007  def setup_toolbars(self):
2008  _ = get_app()._tr # Get translation function
2009 
2010  # Start undo and redo actions disabled
2011  self.actionUndo.setEnabled(False)
2012  self.actionRedo.setEnabled(False)
2013 
2014  # Add files toolbar =================================================================================
2015  self.filesToolbar = QToolBar("Files Toolbar")
2016  self.filesActionGroup = QActionGroup(self)
2017  self.filesActionGroup.setExclusive(True)
2018  self.filesActionGroup.addAction(self.actionFilesShowAll)
2019  self.filesActionGroup.addAction(self.actionFilesShowVideo)
2020  self.filesActionGroup.addAction(self.actionFilesShowAudio)
2021  self.filesActionGroup.addAction(self.actionFilesShowImage)
2022  self.actionFilesShowAll.setChecked(True)
2023  self.filesToolbar.addAction(self.actionFilesShowAll)
2024  self.filesToolbar.addAction(self.actionFilesShowVideo)
2025  self.filesToolbar.addAction(self.actionFilesShowAudio)
2026  self.filesToolbar.addAction(self.actionFilesShowImage)
2027  self.filesFilter = QLineEdit()
2028  self.filesFilter.setObjectName("filesFilter")
2029  self.filesFilter.setPlaceholderText(_("Filter"))
2030  self.filesToolbar.addWidget(self.filesFilter)
2031  self.actionFilesClear.setEnabled(False)
2032  self.filesToolbar.addAction(self.actionFilesClear)
2033  self.tabFiles.layout().addWidget(self.filesToolbar)
2034 
2035  # Add transitions toolbar =================================================================================
2036  self.transitionsToolbar = QToolBar("Transitions Toolbar")
2037  self.transitionsActionGroup = QActionGroup(self)
2038  self.transitionsActionGroup.setExclusive(True)
2039  self.transitionsActionGroup.addAction(self.actionTransitionsShowAll)
2040  self.transitionsActionGroup.addAction(self.actionTransitionsShowCommon)
2041  self.actionTransitionsShowAll.setChecked(True)
2042  self.transitionsToolbar.addAction(self.actionTransitionsShowAll)
2043  self.transitionsToolbar.addAction(self.actionTransitionsShowCommon)
2044  self.transitionsFilter = QLineEdit()
2045  self.transitionsFilter.setObjectName("transitionsFilter")
2046  self.transitionsFilter.setPlaceholderText(_("Filter"))
2047  self.transitionsToolbar.addWidget(self.transitionsFilter)
2048  self.actionTransitionsClear.setEnabled(False)
2049  self.transitionsToolbar.addAction(self.actionTransitionsClear)
2050  self.tabTransitions.layout().addWidget(self.transitionsToolbar)
2051 
2052  # Add effects toolbar =================================================================================
2053  self.effectsToolbar = QToolBar("Effects Toolbar")
2054  self.effectsFilter = QLineEdit()
2055  self.effectsFilter.setObjectName("effectsFilter")
2056  self.effectsFilter.setPlaceholderText(_("Filter"))
2057  self.effectsToolbar.addWidget(self.effectsFilter)
2058  self.actionEffectsClear.setEnabled(False)
2059  self.effectsToolbar.addAction(self.actionEffectsClear)
2060  self.tabEffects.layout().addWidget(self.effectsToolbar)
2061 
2062  # Add Video Preview toolbar ==========================================================================
2063  self.videoToolbar = QToolBar("Video Toolbar")
2064 
2065  # Add fixed spacer(s) (one for each "Other control" to keep playback controls centered)
2066  ospacer1 = QWidget(self)
2067  ospacer1.setMinimumSize(32, 1) # actionSaveFrame
2068  self.videoToolbar.addWidget(ospacer1)
2069  #ospacer2 = QWidget(self)
2070  #ospacer2.setMinimumSize(32, 1) # ???
2071  #self.videoToolbar.addWidget(ospacer2)
2072 
2073  # Add left spacer
2074  spacer = QWidget(self)
2075  spacer.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
2076  self.videoToolbar.addWidget(spacer)
2077 
2078  # Playback controls (centered)
2079  self.videoToolbar.addAction(self.actionJumpStart)
2080  self.videoToolbar.addAction(self.actionRewind)
2081  self.videoToolbar.addAction(self.actionPlay)
2082  self.videoToolbar.addAction(self.actionFastForward)
2083  self.videoToolbar.addAction(self.actionJumpEnd)
2084  self.actionPlay.setCheckable(True)
2085 
2086  # Add right spacer
2087  spacer = QWidget(self)
2088  spacer.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
2089  self.videoToolbar.addWidget(spacer)
2090 
2091  # Other controls (right-aligned)
2092  self.videoToolbar.addAction(self.actionSaveFrame)
2093 
2094  self.tabVideo.layout().addWidget(self.videoToolbar)
2095 
2096  # Add Timeline toolbar ================================================================================
2097  self.timelineToolbar = QToolBar("Timeline Toolbar", self)
2098 
2099  self.timelineToolbar.addAction(self.actionAddTrack)
2100  self.timelineToolbar.addSeparator()
2101 
2102  # rest of options
2103  self.timelineToolbar.addAction(self.actionSnappingTool)
2104  self.timelineToolbar.addAction(self.actionRazorTool)
2105  self.timelineToolbar.addSeparator()
2106  self.timelineToolbar.addAction(self.actionAddMarker)
2107  self.timelineToolbar.addAction(self.actionPreviousMarker)
2108  self.timelineToolbar.addAction(self.actionNextMarker)
2109  self.timelineToolbar.addSeparator()
2110 
2111  # Get project's initial zoom value
2112  initial_scale = get_app().project.get(["scale"]) or 15
2113  # Round non-exponential scale down to next lowest power of 2
2114  initial_zoom = secondsToZoom(initial_scale)
2115 
2116  # Setup Zoom slider
2117  self.sliderZoom = QSlider(Qt.Horizontal, self)
2118  self.sliderZoom.setPageStep(1)
2119  self.sliderZoom.setRange(0, 30)
2120  self.sliderZoom.setValue(initial_zoom)
2121  self.sliderZoom.setInvertedControls(True)
2122  self.sliderZoom.resize(100, 16)
2123 
2124  self.zoomScaleLabel = QLabel( _("{} seconds").format(zoomToSeconds(self.sliderZoom.value())) )
2125 
2126  # add zoom widgets
2127  self.timelineToolbar.addAction(self.actionTimelineZoomIn)
2128  self.timelineToolbar.addWidget(self.sliderZoom)
2129  self.timelineToolbar.addAction(self.actionTimelineZoomOut)
2130  self.timelineToolbar.addWidget(self.zoomScaleLabel)
2131 
2132  # Add timeline toolbar to web frame
2133  self.frameWeb.addWidget(self.timelineToolbar)
2134 
2135  ##
2136  # Clear all selection containers
2137  def clearSelections(self):
2138  self.selected_files = []
2139  self.selected_clips = []
2142  self.selected_tracks = []
2144 
2145  # Clear selection in properties view
2146  if self.propertyTableView:
2147  self.propertyTableView.loadProperties.emit("", "")
2148 
2149  ##
2150  # Handle the callback for detecting the current version on openshot.org
2151  def foundCurrentVersion(self, version):
2152  log.info('foundCurrentVersion: Found the latest version: %s' % version)
2153  _ = get_app()._tr
2154 
2155  # Compare versions (alphabetical compare of version strings should work fine)
2156  if info.VERSION < version:
2157  # Add spacer and 'New Version Available' toolbar button (default hidden)
2158  spacer = QWidget(self)
2159  spacer.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)
2160  self.toolBar.addWidget(spacer)
2161 
2162  # Update text for QAction
2163  self.actionUpdate.setVisible(True)
2164  self.actionUpdate.setText(_("Update Available"))
2165  self.actionUpdate.setToolTip(_("Update Available: <b>%s</b>") % version)
2166 
2167  # Add update available button (with icon and text)
2168  updateButton = QToolButton()
2169  updateButton.setDefaultAction(self.actionUpdate)
2170  updateButton.setToolButtonStyle(Qt.ToolButtonTextBesideIcon)
2171  self.toolBar.addWidget(updateButton)
2172 
2173  ##
2174  # Move tutorial dialogs also (if any)
2175  def moveEvent(self, event):
2176  QMainWindow.moveEvent(self, event)
2177  if self.tutorial_manager:
2178  #log.info("Sending move event to tutorial manager")
2179  self.tutorial_manager.re_position_dialog()
2180 
2181  def resizeEvent(self, event):
2182  QMainWindow.resizeEvent(self, event)
2183  if self.tutorial_manager:
2184  #log.info("Sending resize event to tutorial manager")
2185  self.tutorial_manager.re_position_dialog()
2186 
2187  ##
2188  # Have any child windows follow main-window state
2189  def showEvent(self, event):
2190  #log.info("Showing main window")
2191  QMainWindow.showEvent(self, event)
2192  for child in self.findChildren(QDockWidget):
2193  if child.isFloating() and child.isEnabled():
2194  # child.setWindowState(self.windowState())
2195  #log.info("Showing child {}".format(child.windowTitle()))
2196  child.raise_()
2197  child.show()
2198 
2199  ##
2200  # Have any child windows hide with main window
2201  def hideEvent(self, event):
2202  #log.info("Hiding main window")
2203  QMainWindow.hideEvent(self, event)
2204  for child in self.findChildren(QDockWidget):
2205  if child.isFloating() and child.isVisible():
2206  #log.info("Hiding child {}".format(child.windowTitle()))
2207  child.hide()
2208 
2209 
2210  ##
2211  # Callback for show property timer
2213 
2214  # Stop timer
2215  self.show_property_timer.stop()
2216 
2217  # Emit load properties signal
2218  self.propertyTableView.loadProperties.emit(self.show_property_id, self.show_property_type)
2219 
2220  ##
2221  # Initialize all keyboard shortcuts from the settings file
2223 
2224  # Translate object
2225  _ = get_app()._tr
2226 
2227  # Update all action-based shortcuts (from settings file)
2228  for shortcut in self.getAllKeyboardShortcuts():
2229  for action in self.findChildren(QAction):
2230  if shortcut.get('setting') == action.objectName():
2231  action.setShortcut(QKeySequence(_(shortcut.get('value'))))
2232 
2233  ##
2234  # Set the correct cache settings for the timeline
2236  # Load user settings
2237  s = settings.get_settings()
2238  log.info("InitCacheSettings")
2239  log.info("cache-mode: %s" % s.get("cache-mode"))
2240  log.info("cache-limit-mb: %s" % s.get("cache-limit-mb"))
2241 
2242  # Get MB limit of cache (and convert to bytes)
2243  cache_limit = s.get("cache-limit-mb") * 1024 * 1024 # Convert MB to Bytes
2244 
2245  # Clear old cache
2246  new_cache_object = None
2247  if s.get("cache-mode") == "CacheMemory":
2248  # Create CacheMemory object, and set on timeline
2249  log.info("Creating CacheMemory object with %s byte limit" % cache_limit)
2250  new_cache_object = openshot.CacheMemory(cache_limit)
2251  self.timeline_sync.timeline.SetCache(new_cache_object)
2252 
2253  elif s.get("cache-mode") == "CacheDisk":
2254  # Create CacheDisk object, and set on timeline
2255  log.info("Creating CacheDisk object with %s byte limit at %s" % (cache_limit, info.PREVIEW_CACHE_PATH))
2256  image_format = s.get("cache-image-format")
2257  image_quality = s.get("cache-quality")
2258  image_scale = s.get("cache-scale")
2259  new_cache_object = openshot.CacheDisk(info.PREVIEW_CACHE_PATH, image_format, image_quality, image_scale, cache_limit)
2260  self.timeline_sync.timeline.SetCache(new_cache_object)
2261 
2262  # Clear old cache before it goes out of scope
2263  if self.cache_object:
2264  self.cache_object.Clear()
2265  # Update cache reference, so it doesn't go out of scope
2266  self.cache_object = new_cache_object
2267 
2268  ##
2269  # Connect to Unity launcher (for Linux)
2270  def FrameExported(self, path, start_frame, end_frame, current_frame):
2271  try:
2272  if sys.platform == "linux" and self.has_launcher:
2273  if not self.unity_launchers:
2274  # Get launcher only once
2275  from gi.repository import Unity
2276  self.unity_launchers.append(Unity.LauncherEntry.get_for_desktop_id("openshot-qt.desktop"))
2277  self.unity_launchers.append(Unity.LauncherEntry.get_for_desktop_id("appimagekit-openshot-qt.desktop"))
2278 
2279  # Set progress and show progress bar
2280  for launcher in self.unity_launchers:
2281  launcher.set_property("progress", current_frame / (end_frame - start_frame))
2282  launcher.set_property("progress_visible", True)
2283 
2284  except:
2285  # Just ignore
2286  self.has_launcher = False
2287 
2288  ##
2289  # Export has completed
2290  def ExportFinished(self, path):
2291  try:
2292  if sys.platform == "linux" and self.has_launcher:
2293  for launcher in self.unity_launchers:
2294  # Set progress on Unity launcher and hide progress bar
2295  launcher.set_property("progress", 0.0)
2296  launcher.set_property("progress_visible", False)
2297  except:
2298  pass
2299 
2300  ##
2301  # Handle transform signal (to keep track of whether a transform is happening or not)
2302  def transformTriggered(self, clip_id):
2303  if clip_id and clip_id in self.selected_clips:
2304  self.is_transforming = True
2305  else:
2306  self.is_transforming = False
2307 
2308 
2309  def __init__(self, mode=None):
2310 
2311  # Create main window base class
2312  QMainWindow.__init__(self)
2313  self.mode = mode # None or unittest (None is normal usage)
2314  self.initialized = False
2315 
2316  # set window on app for reference during initialization of children
2317  get_app().window = self
2318  _ = get_app()._tr
2319 
2320  # Load user settings for window
2321  s = settings.get_settings()
2322  self.recent_menu = None
2323 
2324  # Track metrics
2325  track_metric_session() # start session
2326 
2327  # Set unique install id (if blank)
2328  if not s.get("unique_install_id"):
2329  s.set("unique_install_id", str(uuid4()))
2330 
2331  # Track 1st launch metric
2332  track_metric_screen("initial-launch-screen")
2333 
2334  # Track main screen
2335  track_metric_screen("main-screen")
2336 
2337  # Create blank tutorial manager
2338  self.tutorial_manager = None
2339 
2340  # Load UI from designer
2341  ui_util.load_ui(self, self.ui_path)
2342 
2343  # Set all keyboard shortcuts from the settings file
2344  self.InitKeyboardShortcuts()
2345 
2346  # Init UI
2347  ui_util.init_ui(self)
2348 
2349  # Setup toolbars that aren't on main window, set initial state of items, etc
2350  self.setup_toolbars()
2351 
2352  # Add window as watcher to receive undo/redo status updates
2353  get_app().updates.add_watcher(self)
2354 
2355  # Get current version of OpenShot via HTTP
2356  self.FoundVersionSignal.connect(self.foundCurrentVersion)
2358 
2359  # Connect signals
2360  self.is_transforming = False
2361  self.TransformSignal.connect(self.transformTriggered)
2362  if not self.mode == "unittest":
2363  self.RecoverBackup.connect(self.recover_backup)
2364 
2365  # Create the timeline sync object (used for previewing timeline)
2366  self.timeline_sync = TimelineSync(self)
2367 
2368  # Setup timeline
2369  self.timeline = TimelineWebView(self)
2370  self.frameWeb.layout().addWidget(self.timeline)
2371 
2372  # Setup files tree
2373  if s.get("file_view") == "details":
2374  self.filesTreeView = FilesTreeView(self)
2375  else:
2376  self.filesTreeView = FilesListView(self)
2377  self.tabFiles.layout().addWidget(self.filesTreeView)
2378  self.filesTreeView.setFocus()
2379 
2380  # Setup transitions tree
2381  if s.get("transitions_view") == "details":
2382  self.transitionsTreeView = TransitionsTreeView(self)
2383  else:
2384  self.transitionsTreeView = TransitionsListView(self)
2385  self.tabTransitions.layout().addWidget(self.transitionsTreeView)
2386 
2387  # Setup effects tree
2388  if s.get("effects_view") == "details":
2389  self.effectsTreeView = EffectsTreeView(self)
2390  else:
2391  self.effectsTreeView = EffectsListView(self)
2392  self.tabEffects.layout().addWidget(self.effectsTreeView)
2393 
2394  # Process events before continuing
2395  # TODO: Figure out why this is needed for a backup recovery to correctly show up on the timeline
2396  get_app().processEvents()
2397 
2398  # Setup properties table
2399  self.txtPropertyFilter.setPlaceholderText(_("Filter"))
2400  self.propertyTableView = PropertiesTableView(self)
2401  self.selectionLabel = SelectionLabel(self)
2402  self.dockPropertiesContent.layout().addWidget(self.selectionLabel, 0, 1)
2403  self.dockPropertiesContent.layout().addWidget(self.propertyTableView, 2, 1)
2404 
2405  # Init selection containers
2406  self.clearSelections()
2407 
2408  # Show Property timer
2409  # Timer to use a delay before showing properties (to prevent a mass selection from trying
2410  # to update the property model hundreds of times)
2411  self.show_property_id = None
2412  self.show_property_type = None
2413  self.show_property_timer = QTimer()
2414  self.show_property_timer.setInterval(100)
2415  self.show_property_timer.timeout.connect(self.show_property_timeout)
2416  self.show_property_timer.stop()
2417 
2418  # Setup video preview QWidget
2419  self.videoPreview = VideoWidget()
2420  self.tabVideo.layout().insertWidget(0, self.videoPreview)
2421 
2422  # Load window state and geometry
2423  self.load_settings()
2424 
2425  # Setup Cache settings
2426  self.cache_object = None
2427  self.InitCacheSettings()
2428 
2429  # Start the preview thread
2430  self.preview_parent = PreviewParent()
2431  self.preview_parent.Init(self, self.timeline_sync.timeline, self.videoPreview)
2432  self.preview_thread = self.preview_parent.worker
2433 
2434  # Set pause callback
2435  self.PauseSignal.connect(self.handlePausedVideo)
2436 
2437  # QTimer for Autosave
2438  self.auto_save_timer = QTimer(self)
2439  self.auto_save_timer.setInterval(s.get("autosave-interval") * 1000 * 60)
2440  self.auto_save_timer.timeout.connect(self.auto_save_project)
2441  if s.get("enable-auto-save"):
2442  self.auto_save_timer.start()
2443 
2444  # Set hardware decode environment variable
2445  if s.get("hardware_decode"):
2446  os.environ['OS2_DECODE_HW'] = "1"
2447  else:
2448  os.environ['OS2_DECODE_HW'] = "0"
2449 
2450  # Set OMP thread enabled flag (for stability)
2451  if s.get("omp_threads_enabled"):
2452  os.environ['OS2_OMP_THREADS'] = "1"
2453  else:
2454  os.environ['OS2_OMP_THREADS'] = "0"
2455 
2456  # Create lock file
2457  self.create_lock_file()
2458 
2459  # Show window
2460  if not self.mode == "unittest":
2461  self.show()
2462 
2463  # Create tutorial manager
2464  self.tutorial_manager = TutorialManager(self)
2465 
2466  # Connect to Unity DBus signal (if linux)
2467  if sys.platform == "linux":
2469  self.has_launcher = True
2470  self.ExportFrame.connect(self.FrameExported)
2471  self.ExportEnded.connect(self.ExportFinished)
2472 
2473  # Save settings
2474  s.save()
2475 
2476  # Refresh frame
2477  QTimer.singleShot(100, self.refreshFrameSignal.emit)
2478 
2479  # Main window is initialized
2480  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:44
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:719
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.
def actionRenameTrack_trigger
Callback for renaming track.
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:820
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 showEvent
Have any child windows follow main-window state.
def actionPreview_File_trigger
Preview the selected media file.
Definition: main_window.py:799
def website_language
Get the current website language code for URLs.
Definition: info.py:138
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:36
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:829
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 hideEvent
Have any child windows hide with main window.
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:834
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