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