OpenShot Video Editor  2.0.0
timeline_webview.py
Go to the documentation of this file.
1 ##
2 #
3 # @file
4 # @brief This file loads the interactive HTML timeline
5 # @author Noah Figg <eggmunkee@hotmail.com>
6 # @author Jonathan Thomas <jonathan@openshot.org>
7 # @author Olivier Girard <eolinwen@gmail.com>
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 from copy import deepcopy
34 from functools import partial
35 from random import uniform
36 from urllib.parse import urlparse
37 from operator import itemgetter
38 
39 import openshot # Python module for libopenshot (required video editing module installed separately)
40 from PyQt5.QtCore import QFileInfo, pyqtSlot, QUrl, Qt, QCoreApplication, QTimer
41 from PyQt5.QtGui import QCursor, QKeySequence
42 from PyQt5.QtWebKitWidgets import QWebView
43 from PyQt5.QtWidgets import QMenu
44 
45 from classes import info, updates
46 from classes import settings
47 from classes.app import get_app
48 from classes.logger import log
49 from classes.query import File, Clip, Transition, Track
50 from classes.waveform import get_audio_data
51 from classes.thumbnail import GenerateThumbnail
52 from classes.conversion import zoomToSeconds, secondsToZoom
53 
54 try:
55  import json
56 except ImportError:
57  import simplejson as json
58 
59 # Constants used by this file
60 JS_SCOPE_SELECTOR = "$('body').scope()"
61 
62 MENU_FADE_NONE = 0
63 MENU_FADE_IN_FAST = 1
64 MENU_FADE_IN_SLOW = 2
65 MENU_FADE_OUT_FAST = 3
66 MENU_FADE_OUT_SLOW = 4
67 MENU_FADE_IN_OUT_FAST = 5
68 MENU_FADE_IN_OUT_SLOW = 6
69 
70 MENU_ROTATE_NONE = 0
71 MENU_ROTATE_90_RIGHT = 1
72 MENU_ROTATE_90_LEFT = 2
73 MENU_ROTATE_180_FLIP = 3
74 
75 MENU_LAYOUT_NONE = 0
76 MENU_LAYOUT_CENTER = 1
77 MENU_LAYOUT_TOP_LEFT = 2
78 MENU_LAYOUT_TOP_RIGHT = 3
79 MENU_LAYOUT_BOTTOM_LEFT = 4
80 MENU_LAYOUT_BOTTOM_RIGHT = 5
81 MENU_LAYOUT_ALL_WITH_ASPECT = 6
82 MENU_LAYOUT_ALL_WITHOUT_ASPECT = 7
83 
84 MENU_ALIGN_LEFT = 0
85 MENU_ALIGN_RIGHT = 1
86 
87 MENU_ANIMATE_NONE = 0
88 MENU_ANIMATE_IN_50_100 = 1
89 MENU_ANIMATE_IN_75_100 = 2
90 MENU_ANIMATE_IN_100_150 = 3
91 MENU_ANIMATE_OUT_100_75 = 4
92 MENU_ANIMATE_OUT_100_50 = 5
93 MENU_ANIMATE_OUT_150_100 = 6
94 MENU_ANIMATE_CENTER_TOP = 7
95 MENU_ANIMATE_CENTER_LEFT = 8
96 MENU_ANIMATE_CENTER_RIGHT = 9
97 MENU_ANIMATE_CENTER_BOTTOM = 10
98 MENU_ANIMATE_TOP_CENTER = 11
99 MENU_ANIMATE_LEFT_CENTER = 12
100 MENU_ANIMATE_RIGHT_CENTER = 13
101 MENU_ANIMATE_BOTTOM_CENTER = 14
102 MENU_ANIMATE_TOP_BOTTOM = 15
103 MENU_ANIMATE_LEFT_RIGHT = 16
104 MENU_ANIMATE_RIGHT_LEFT = 17
105 MENU_ANIMATE_BOTTOM_TOP = 18
106 MENU_ANIMATE_RANDOM = 19
107 
108 MENU_VOLUME_NONE = 1
109 MENU_VOLUME_FADE_IN_FAST = 2
110 MENU_VOLUME_FADE_IN_SLOW = 3
111 MENU_VOLUME_FADE_OUT_FAST = 4
112 MENU_VOLUME_FADE_OUT_SLOW = 5
113 MENU_VOLUME_FADE_IN_OUT_FAST = 6
114 MENU_VOLUME_FADE_IN_OUT_SLOW = 7
115 MENU_VOLUME_LEVEL_100 = 100
116 MENU_VOLUME_LEVEL_90 = 90
117 MENU_VOLUME_LEVEL_80 = 80
118 MENU_VOLUME_LEVEL_70 = 70
119 MENU_VOLUME_LEVEL_60 = 60
120 MENU_VOLUME_LEVEL_50 = 50
121 MENU_VOLUME_LEVEL_40 = 40
122 MENU_VOLUME_LEVEL_30 = 30
123 MENU_VOLUME_LEVEL_20 = 20
124 MENU_VOLUME_LEVEL_10 = 10
125 MENU_VOLUME_LEVEL_0 = 0
126 
127 MENU_TRANSFORM = 0
128 
129 MENU_TIME_NONE = 0
130 MENU_TIME_FORWARD = 1
131 MENU_TIME_BACKWARD = 2
132 MENU_TIME_FREEZE = 3
133 MENU_TIME_FREEZE_ZOOM = 4
134 
135 MENU_COPY_ALL = -1
136 MENU_COPY_CLIP = 0
137 MENU_COPY_KEYFRAMES_ALL = 1
138 MENU_COPY_KEYFRAMES_ALPHA = 2
139 MENU_COPY_KEYFRAMES_SCALE = 3
140 MENU_COPY_KEYFRAMES_ROTATE = 4
141 MENU_COPY_KEYFRAMES_LOCATION = 5
142 MENU_COPY_KEYFRAMES_TIME = 6
143 MENU_COPY_KEYFRAMES_VOLUME = 7
144 MENU_COPY_EFFECTS = 8
145 MENU_PASTE = 9
146 
147 MENU_COPY_TRANSITION = 10
148 MENU_COPY_KEYFRAMES_BRIGHTNESS = 11
149 MENU_COPY_KEYFRAMES_CONTRAST = 12
150 
151 MENU_SLICE_KEEP_BOTH = 0
152 MENU_SLICE_KEEP_LEFT = 1
153 MENU_SLICE_KEEP_RIGHT = 2
154 
155 MENU_SPLIT_AUDIO_SINGLE = 0
156 MENU_SPLIT_AUDIO_MULTIPLE = 1
157 
158 
159 ##
160 # A WebView QWidget used to load the Timeline
162 
163  # Path to html file
164  html_path = os.path.join(info.PATH, 'timeline', 'index.html')
165 
166  @pyqtSlot()
167  ##
168  # Document.Ready event has fired, and is initialized
169  def page_ready(self):
170  self.document_is_ready = True
171 
172  def eval_js(self, code):
173  # Check if document.Ready has fired in JS
174  if not self.document_is_ready:
175  # Not ready, try again in a few milliseconds
176  log.error("TimelineWebView::eval_js() called before document ready event. Script queued: %s" % code)
177  QTimer.singleShot(50, partial(self.eval_js, code))
178  return None
179  else:
180  # Execute JS code
181  return self.page().mainFrame().evaluateJavaScript(code)
182 
183  # This method is invoked by the UpdateManager each time a change happens (i.e UpdateInterface)
184  def changed(self, action):
185  # Remove unused action attribute (old_values)
186  action = deepcopy(action)
187  action.old_values = {}
188 
189  # Send a JSON version of the UpdateAction to the timeline webview method: ApplyJsonDiff()
190  if action.type == "load":
191  # Initialize translated track name
192  _ = get_app()._tr
193  self.eval_js(JS_SCOPE_SELECTOR + ".SetTrackLabel('" + _("Track %s") + "');")
194 
195  # Load entire project data
196  code = JS_SCOPE_SELECTOR + ".LoadJson(" + action.json() + ");"
197  else:
198  # Apply diff to part of project data
199  code = JS_SCOPE_SELECTOR + ".ApplyJsonDiff([" + action.json() + "]);"
200  self.eval_js(code)
201 
202  # Reset the scale when loading new JSON
203  if action.type == "load":
204  # Set the scale again (to project setting)
205  initial_scale = get_app().project.get(["scale"]) or 15
206  get_app().window.sliderZoom.setValue(secondsToZoom(initial_scale))
207 
208  # Javascript callable function to update the project data when a clip changes
209  @pyqtSlot(str, bool, bool, bool)
210  ##
211  # Create an updateAction and send it to the update manager
212  def update_clip_data(self, clip_json, only_basic_props=True, ignore_reader=False, ignore_refresh=False):
213 
214  # read clip json
215  try:
216  if not isinstance(clip_json, dict):
217  clip_data = json.loads(clip_json)
218  else:
219  clip_data = clip_json
220  except:
221  # Failed to parse json, do nothing
222  return
223 
224  # Search for matching clip in project data (if any)
225  existing_clip = Clip.get(id=clip_data["id"])
226  if not existing_clip:
227  # Create a new clip (if not exists)
228  existing_clip = Clip()
229 
230  # Determine if "start" changed
231  if existing_clip.data and existing_clip.data["start"] != clip_data["start"] and clip_data["reader"]["has_video"] and not clip_data["reader"]["has_single_image"]:
232  # Update thumbnail
233  self.UpdateClipThumbnail(clip_data)
234 
235  # Update clip data
236  existing_clip.data = clip_data
237 
238  # Remove unneeded properties (since they don't change here... this is a performance boost)
239  if only_basic_props:
240  existing_clip.data = {}
241  existing_clip.data["id"] = clip_data["id"]
242  existing_clip.data["layer"] = clip_data["layer"]
243  existing_clip.data["position"] = clip_data["position"]
244  existing_clip.data["image"] = clip_data["image"]
245  existing_clip.data["start"] = clip_data["start"]
246  existing_clip.data["end"] = clip_data["end"]
247 
248  # Always remove the Reader attribute (since nothing updates it, and we are wrapping clips in FrameMappers anyway)
249  if ignore_reader and "reader" in existing_clip.data:
250  existing_clip.data.pop("reader")
251 
252  # Save clip
253  existing_clip.save()
254 
255  # Update the preview and reselect current frame in properties
256  if not ignore_refresh:
257  get_app().window.refreshFrameSignal.emit()
258  get_app().window.propertyTableView.select_frame(self.window.preview_thread.player.Position())
259 
260  # Update Thumbnails for modified clips
261  ##
262  # Update the thumbnail image for clips
263  def UpdateClipThumbnail(self, clip_data):
264 
265  # Get project's frames per second
266  fps = clip_data["reader"]["fps"]
267  fps_float = float(fps["num"]) / float(fps["den"])
268 
269  # Get starting time of clip
270  start_frame = round(float(clip_data["start"]) * fps_float) + 1
271 
272  # Determine thumb path
273  thumb_path = os.path.join(info.THUMBNAIL_PATH, "{}-{}.png".format(clip_data["id"], start_frame))
274  log.info('Updating thumbnail image: %s' % thumb_path)
275 
276  # Check if thumb exists
277  if not os.path.exists(thumb_path):
278 
279  # Get file object
280  file = File.get(id=clip_data["file_id"])
281 
282  if not file:
283  # File not found, do nothing
284  return
285 
286  # Convert path to the correct relative path (based on this folder)
287  file_path = file.absolute_path()
288 
289  # Determine if video overlay should be applied to thumbnail
290  overlay_path = ""
291  if file.data["media_type"] == "video":
292  overlay_path = os.path.join(info.IMAGES_PATH, "overlay.png")
293 
294  # Create thumbnail image
295  GenerateThumbnail(file_path, thumb_path, start_frame, 98, 64, os.path.join(info.IMAGES_PATH, "mask.png"), overlay_path)
296 
297  # Update clip_data to point to new thumbnail image
298  clip_data["image"] = thumb_path
299 
300  # Add missing transition
301  @pyqtSlot(str)
302  def add_missing_transition(self, transition_json):
303 
304  transition_details = json.loads(transition_json)
305 
306  # Get FPS from project
307  fps = get_app().project.get(["fps"])
308  fps_float = float(fps["num"]) / float(fps["den"])
309 
310  # Open up QtImageReader for transition Image
311  transition_reader = openshot.QtImageReader(
312  os.path.join(info.PATH, "transitions", "common", "fade.svg"))
313 
314  # Generate transition object
315  transition_object = openshot.Mask()
316 
317  # Set brightness and contrast, to correctly transition for overlapping clips
318  brightness = transition_object.brightness
319  brightness.AddPoint(1, 1.0, openshot.BEZIER)
320  brightness.AddPoint(round(transition_details["end"] * fps_float) + 1, -1.0, openshot.BEZIER)
321  contrast = openshot.Keyframe(3.0)
322 
323  # Create transition dictionary
324  transitions_data = {
325  "id": get_app().project.generate_id(),
326  "layer": transition_details["layer"],
327  "title": "Transition",
328  "type": "Mask",
329  "position": transition_details["position"],
330  "start": transition_details["start"],
331  "end": transition_details["end"],
332  "brightness": json.loads(brightness.Json()),
333  "contrast": json.loads(contrast.Json()),
334  "reader": json.loads(transition_reader.Json()),
335  "replace_image": False
336  }
337 
338  # Send to update manager
339  self.update_transition_data(transitions_data, only_basic_props=False)
340 
341  # Javascript callable function to update the project data when a transition changes
342  @pyqtSlot(str, bool, bool)
343  ##
344  # Create an updateAction and send it to the update manager
345  def update_transition_data(self, transition_json, only_basic_props=True, ignore_refresh=False):
346 
347  # read clip json
348  if not isinstance(transition_json, dict):
349  transition_data = json.loads(transition_json)
350  else:
351  transition_data = transition_json
352 
353  # Search for matching clip in project data (if any)
354  existing_item = Transition.get(id=transition_data["id"])
355  needs_resize = True
356  if not existing_item:
357  # Create a new clip (if not exists)
358  existing_item = Transition()
359  needs_resize = False
360  existing_item.data = transition_data
361 
362  # Get FPS from project
363  fps = get_app().project.get(["fps"])
364  fps_float = float(fps["num"]) / float(fps["den"])
365  duration = existing_item.data["end"] - existing_item.data["start"]
366 
367  # Update the brightness and contrast keyframes to match the duration of the transition
368  # This is a hack until I can think of something better
369  brightness = None
370  contrast = None
371  if needs_resize:
372  # Adjust transition's brightness keyframes to match the size of the transition
373  brightness = existing_item.data["brightness"]
374  if len(brightness["Points"]) > 1:
375  # If multiple points, move the final one to the 'new' end
376  brightness["Points"][-1]["co"]["X"] = round(duration * fps_float) + 1
377 
378  # Adjust transition's contrast keyframes to match the size of the transition
379  contrast = existing_item.data["contrast"]
380  if len(contrast["Points"]) > 1:
381  # If multiple points, move the final one to the 'new' end
382  contrast["Points"][-1]["co"]["X"] = round(duration * fps_float) + 1
383  else:
384  # Create new brightness and contrast Keyframes
385  b = openshot.Keyframe()
386  b.AddPoint(1, 1.0, openshot.BEZIER)
387  b.AddPoint(round(duration * fps_float) + 1, -1.0, openshot.BEZIER)
388  brightness = json.loads(b.Json())
389 
390  # Only include the basic properties (performance boost)
391  if only_basic_props:
392  existing_item.data = {}
393  existing_item.data["id"] = transition_data["id"]
394  existing_item.data["layer"] = transition_data["layer"]
395  existing_item.data["position"] = transition_data["position"]
396  existing_item.data["start"] = transition_data["start"]
397  existing_item.data["end"] = transition_data["end"]
398 
399  log.info('transition start: %s' % transition_data["start"])
400  log.info('transition end: %s' % transition_data["end"])
401 
402  if brightness:
403  existing_item.data["brightness"] = brightness
404  if contrast:
405  existing_item.data["contrast"] = contrast
406 
407  # Save transition
408  existing_item.save()
409 
410  # Update the preview and reselct current frame in properties
411  if not ignore_refresh:
412  get_app().window.refreshFrameSignal.emit()
413  get_app().window.propertyTableView.select_frame(self.window.preview_thread.player.Position())
414 
415  # Prevent default context menu, and ignore, so that javascript can intercept
416  def contextMenuEvent(self, event):
417  event.ignore()
418 
419  # Javascript callable function to show clip or transition content menus, passing in type to show
420  @pyqtSlot(float)
421  def ShowPlayheadMenu(self, position=None):
422  log.info('ShowPlayheadMenu: %s' % position)
423 
424  # Get translation method
425  _ = get_app()._tr
426 
427  # Get list of intercepting clips with position (if any)
428  intersecting_clips = Clip.filter(intersect=position)
429  intersecting_trans = Transition.filter(intersect=position)
430 
431  menu = QMenu(self)
432  if intersecting_clips or intersecting_trans:
433  # Get list of clip ids
434  clip_ids = [c.id for c in intersecting_clips]
435  trans_ids = [t.id for t in intersecting_trans]
436 
437  # Add split clip menu
438  Slice_Menu = QMenu(_("Slice All"), self)
439  Slice_Keep_Both = Slice_Menu.addAction(_("Keep Both Sides"))
440  Slice_Keep_Both.setShortcut(QKeySequence(self.window.getShortcutByName("sliceAllKeepBothSides")))
441  Slice_Keep_Both.triggered.connect(partial(self.Slice_Triggered, MENU_SLICE_KEEP_BOTH, clip_ids, trans_ids, position))
442  Slice_Keep_Left = Slice_Menu.addAction(_("Keep Left Side"))
443  Slice_Keep_Left.setShortcut(QKeySequence(self.window.getShortcutByName("sliceAllKeepLeftSide")))
444  Slice_Keep_Left.triggered.connect(partial(self.Slice_Triggered, MENU_SLICE_KEEP_LEFT, clip_ids, trans_ids, position))
445  Slice_Keep_Right = Slice_Menu.addAction(_("Keep Right Side"))
446  Slice_Keep_Right.setShortcut(QKeySequence(self.window.getShortcutByName("sliceAllKeepRightSide")))
447  Slice_Keep_Right.triggered.connect(partial(self.Slice_Triggered, MENU_SLICE_KEEP_RIGHT, clip_ids, trans_ids, position))
448  menu.addMenu(Slice_Menu)
449  return menu.popup(QCursor.pos())
450 
451  @pyqtSlot(str)
452  def ShowEffectMenu(self, effect_id=None):
453  log.info('ShowEffectMenu: %s' % effect_id)
454 
455  # Set the selected clip (if needed)
456  self.window.addSelection(effect_id, 'effect', True)
457 
458  menu = QMenu(self)
459  # Properties
460  menu.addAction(self.window.actionProperties)
461 
462  # Remove Effect Menu
463  menu.addSeparator()
464  menu.addAction(self.window.actionRemoveEffect)
465  return menu.popup(QCursor.pos())
466 
467  @pyqtSlot(float, int)
468  def ShowTimelineMenu(self, position, layer_id):
469  log.info('ShowTimelineMenu: position: %s, layer: %s' % (position, layer_id))
470 
471  # Get translation method
472  _ = get_app()._tr
473 
474  # Get list of clipboard items (that are complete clips or transitions)
475  # i.e. ignore partial clipboard items (keyframes / effects / etc...)
476  clipboard_clip_ids = [k for k, v in self.copy_clipboard.items() if v.get('id')]
477  clipboard_tran_ids = [k for k, v in self.copy_transition_clipboard.items() if v.get('id')]
478 
479  # Paste Menu (if entire clips or transitions are copied)
481  if len(clipboard_clip_ids) + len(clipboard_tran_ids) > 0:
482  menu = QMenu(self)
483  Paste_Clip = menu.addAction(_("Paste"))
484  Paste_Clip.setShortcut(QKeySequence(self.window.getShortcutByName("pasteAll")))
485  Paste_Clip.triggered.connect(partial(self.Paste_Triggered, MENU_PASTE, float(position), int(layer_id), [], []))
486 
487  return menu.popup(QCursor.pos())
488 
489  @pyqtSlot(str)
490  def ShowClipMenu(self, clip_id=None):
491  log.info('ShowClipMenu: %s' % clip_id)
492 
493  # Get translation method
494  _ = get_app()._tr
495 
496  # Get existing clip object
497  clip = Clip.get(id=clip_id)
498  if not clip:
499  # Not a valid clip id
500  return
501 
502  # Set the selected clip (if needed)
503  if clip_id not in self.window.selected_clips:
504  self.window.addSelection(clip_id, 'clip')
505  # Get list of selected clips
506  clip_ids = self.window.selected_clips
507  tran_ids = self.window.selected_transitions
508 
509  # Get framerate
510  fps = get_app().project.get(["fps"])
511  fps_float = float(fps["num"]) / float(fps["den"])
512 
513  # Get playhead position
514  playhead_position = float(self.window.preview_thread.current_frame) / fps_float
515 
516  # Mark these strings for translation
517  translations = [_("Start of Clip"), _("End of Clip"), _("Entire Clip"), _("Normal"), _("Fast"), _("Slow"), _("Forward"), _("Backward")]
518 
519  # Create blank context menu
520  menu = QMenu(self)
521 
522  # Copy Menu
523  if len(tran_ids) + len(clip_ids) > 1:
524  # Show Copy All menu (clips and transitions are selected)
525  Copy_All = menu.addAction(_("Copy"))
526  Copy_All.setShortcut(QKeySequence(self.window.getShortcutByName("copyAll")))
527  Copy_All.triggered.connect(partial(self.Copy_Triggered, MENU_COPY_ALL, clip_ids, tran_ids))
528  else:
529  # Only a single clip is selected (Show normal copy menus)
530  Copy_Menu = QMenu(_("Copy"), self)
531  Copy_Clip = Copy_Menu.addAction(_("Clip"))
532  Copy_Clip.setShortcut(QKeySequence(self.window.getShortcutByName("copyAll")))
533  Copy_Clip.triggered.connect(partial(self.Copy_Triggered, MENU_COPY_CLIP, [clip_id], []))
534 
535  Keyframe_Menu = QMenu(_("Keyframes"), self)
536  Copy_Keyframes_All = Keyframe_Menu.addAction(_("All"))
537  Copy_Keyframes_All.triggered.connect(partial(self.Copy_Triggered, MENU_COPY_KEYFRAMES_ALL, [clip_id], []))
538  Keyframe_Menu.addSeparator()
539  Copy_Keyframes_Alpha = Keyframe_Menu.addAction(_("Alpha"))
540  Copy_Keyframes_Alpha.triggered.connect(partial(self.Copy_Triggered, MENU_COPY_KEYFRAMES_ALPHA, [clip_id], []))
541  Copy_Keyframes_Scale = Keyframe_Menu.addAction(_("Scale"))
542  Copy_Keyframes_Scale.triggered.connect(partial(self.Copy_Triggered, MENU_COPY_KEYFRAMES_SCALE, [clip_id], []))
543  Copy_Keyframes_Rotate = Keyframe_Menu.addAction(_("Rotation"))
544  Copy_Keyframes_Rotate.triggered.connect(partial(self.Copy_Triggered, MENU_COPY_KEYFRAMES_ROTATE, [clip_id], []))
545  Copy_Keyframes_Locate = Keyframe_Menu.addAction(_("Location"))
546  Copy_Keyframes_Locate.triggered.connect(partial(self.Copy_Triggered, MENU_COPY_KEYFRAMES_LOCATION, [clip_id], []))
547  Copy_Keyframes_Time = Keyframe_Menu.addAction(_("Time"))
548  Copy_Keyframes_Time.triggered.connect(partial(self.Copy_Triggered, MENU_COPY_KEYFRAMES_TIME, [clip_id], []))
549  Copy_Keyframes_Volume = Keyframe_Menu.addAction(_("Volume"))
550  Copy_Keyframes_Volume.triggered.connect(partial(self.Copy_Triggered, MENU_COPY_KEYFRAMES_VOLUME, [clip_id], []))
551 
552  # Only add copy->effects and copy->keyframes if 1 clip is selected
553  Copy_Effects = Copy_Menu.addAction(_("Effects"))
554  Copy_Effects.triggered.connect(partial(self.Copy_Triggered, MENU_COPY_EFFECTS, [clip_id], []))
555  Copy_Menu.addMenu(Keyframe_Menu)
556  menu.addMenu(Copy_Menu)
557 
558  # Get list of clipboard items (that are complete clips or transitions)
559  # i.e. ignore partial clipboard items (keyframes / effects / etc...)
560  clipboard_clip_ids = [k for k, v in self.copy_clipboard.items() if v.get('id')]
561  clipboard_tran_ids = [k for k, v in self.copy_transition_clipboard.items() if v.get('id')]
562  # Determine if the paste menu should be shown
563  if self.copy_clipboard and len(clipboard_clip_ids) + len(clipboard_tran_ids) == 0:
564  # Paste Menu (Only show if partial clipboard available)
565  Paste_Clip = menu.addAction(_("Paste"))
566  Paste_Clip.triggered.connect(partial(self.Paste_Triggered, MENU_PASTE, 0.0, 0, clip_ids, []))
567 
568  menu.addSeparator()
569 
570  # Alignment Menu (if multiple selections)
571  if len(clip_ids) > 1:
572  Alignment_Menu = QMenu(_("Align"), self)
573  Align_Left = Alignment_Menu.addAction(_("Left"))
574  Align_Left.triggered.connect(partial(self.Align_Triggered, MENU_ALIGN_LEFT, clip_ids, tran_ids))
575  Align_Right = Alignment_Menu.addAction(_("Right"))
576  Align_Right.triggered.connect(partial(self.Align_Triggered, MENU_ALIGN_RIGHT, clip_ids, tran_ids))
577 
578  # Add menu to parent
579  menu.addMenu(Alignment_Menu)
580 
581  # Fade In Menu
582  Fade_Menu = QMenu(_("Fade"), self)
583  Fade_None = Fade_Menu.addAction(_("No Fade"))
584  Fade_None.triggered.connect(partial(self.Fade_Triggered, MENU_FADE_NONE, clip_ids))
585  Fade_Menu.addSeparator()
586  for position in ["Start of Clip", "End of Clip", "Entire Clip"]:
587  Position_Menu = QMenu(_(position), self)
588 
589  if position == "Start of Clip":
590  Fade_In_Fast = Position_Menu.addAction(_("Fade In (Fast)"))
591  Fade_In_Fast.triggered.connect(partial(self.Fade_Triggered, MENU_FADE_IN_FAST, clip_ids, position))
592  Fade_In_Slow = Position_Menu.addAction(_("Fade In (Slow)"))
593  Fade_In_Slow.triggered.connect(partial(self.Fade_Triggered, MENU_FADE_IN_SLOW, clip_ids, position))
594 
595  elif position == "End of Clip":
596  Fade_Out_Fast = Position_Menu.addAction(_("Fade Out (Fast)"))
597  Fade_Out_Fast.triggered.connect(partial(self.Fade_Triggered, MENU_FADE_OUT_FAST, clip_ids, position))
598  Fade_Out_Slow = Position_Menu.addAction(_("Fade Out (Slow)"))
599  Fade_Out_Slow.triggered.connect(partial(self.Fade_Triggered, MENU_FADE_OUT_SLOW, clip_ids, position))
600 
601  else:
602  Fade_In_Out_Fast = Position_Menu.addAction(_("Fade In and Out (Fast)"))
603  Fade_In_Out_Fast.triggered.connect(partial(self.Fade_Triggered, MENU_FADE_IN_OUT_FAST, clip_ids, position))
604  Fade_In_Out_Slow = Position_Menu.addAction(_("Fade In and Out (Slow)"))
605  Fade_In_Out_Slow.triggered.connect(partial(self.Fade_Triggered, MENU_FADE_IN_OUT_SLOW, clip_ids, position))
606  Position_Menu.addSeparator()
607  Fade_In_Slow = Position_Menu.addAction(_("Fade In (Entire Clip)"))
608  Fade_In_Slow.triggered.connect(partial(self.Fade_Triggered, MENU_FADE_IN_SLOW, clip_ids, position))
609  Fade_Out_Slow = Position_Menu.addAction(_("Fade Out (Entire Clip)"))
610  Fade_Out_Slow.triggered.connect(partial(self.Fade_Triggered, MENU_FADE_OUT_SLOW, clip_ids, position))
611 
612  Fade_Menu.addMenu(Position_Menu)
613  menu.addMenu(Fade_Menu)
614 
615 
616  # Animate Menu
617  Animate_Menu = QMenu(_("Animate"), self)
618  Animate_None = Animate_Menu.addAction(_("No Animation"))
619  Animate_None.triggered.connect(partial(self.Animate_Triggered, MENU_ANIMATE_NONE, clip_ids))
620  Animate_Menu.addSeparator()
621  for position in ["Start of Clip", "End of Clip", "Entire Clip"]:
622  Position_Menu = QMenu(_(position), self)
623 
624  # Scale
625  Scale_Menu = QMenu(_("Zoom"), self)
626  Animate_In_50_100 = Scale_Menu.addAction(_("Zoom In (50% to 100%)"))
627  Animate_In_50_100.triggered.connect(partial(self.Animate_Triggered, MENU_ANIMATE_IN_50_100, clip_ids, position))
628  Animate_In_75_100 = Scale_Menu.addAction(_("Zoom In (75% to 100%)"))
629  Animate_In_75_100.triggered.connect(partial(self.Animate_Triggered, MENU_ANIMATE_IN_75_100, clip_ids, position))
630  Animate_In_100_150 = Scale_Menu.addAction(_("Zoom In (100% to 150%)"))
631  Animate_In_100_150.triggered.connect(partial(self.Animate_Triggered, MENU_ANIMATE_IN_100_150, clip_ids, position))
632  Animate_Out_100_75 = Scale_Menu.addAction(_("Zoom Out (100% to 75%)"))
633  Animate_Out_100_75.triggered.connect(partial(self.Animate_Triggered, MENU_ANIMATE_OUT_100_75, clip_ids, position))
634  Animate_Out_100_50 = Scale_Menu.addAction(_("Zoom Out (100% to 50%)"))
635  Animate_Out_100_50.triggered.connect(partial(self.Animate_Triggered, MENU_ANIMATE_OUT_100_50, clip_ids, position))
636  Animate_Out_150_100 = Scale_Menu.addAction(_("Zoom Out (150% to 100%)"))
637  Animate_Out_150_100.triggered.connect(partial(self.Animate_Triggered, MENU_ANIMATE_OUT_150_100, clip_ids, position))
638  Position_Menu.addMenu(Scale_Menu)
639 
640  # Center to Edge
641  Center_Edge_Menu = QMenu(_("Center to Edge"), self)
642  Animate_Center_Top = Center_Edge_Menu.addAction(_("Center to Top"))
643  Animate_Center_Top.triggered.connect(partial(self.Animate_Triggered, MENU_ANIMATE_CENTER_TOP, clip_ids, position))
644  Animate_Center_Left = Center_Edge_Menu.addAction(_("Center to Left"))
645  Animate_Center_Left.triggered.connect(partial(self.Animate_Triggered, MENU_ANIMATE_CENTER_LEFT, clip_ids, position))
646  Animate_Center_Right = Center_Edge_Menu.addAction(_("Center to Right"))
647  Animate_Center_Right.triggered.connect(partial(self.Animate_Triggered, MENU_ANIMATE_CENTER_RIGHT, clip_ids, position))
648  Animate_Center_Bottom = Center_Edge_Menu.addAction(_("Center to Bottom"))
649  Animate_Center_Bottom.triggered.connect(partial(self.Animate_Triggered, MENU_ANIMATE_CENTER_BOTTOM, clip_ids, position))
650  Position_Menu.addMenu(Center_Edge_Menu)
651 
652  # Edge to Center
653  Edge_Center_Menu = QMenu(_("Edge to Center"), self)
654  Animate_Top_Center = Edge_Center_Menu.addAction(_("Top to Center"))
655  Animate_Top_Center.triggered.connect(partial(self.Animate_Triggered, MENU_ANIMATE_TOP_CENTER, clip_ids, position))
656  Animate_Left_Center = Edge_Center_Menu.addAction(_("Left to Center"))
657  Animate_Left_Center.triggered.connect(partial(self.Animate_Triggered, MENU_ANIMATE_LEFT_CENTER, clip_ids, position))
658  Animate_Right_Center = Edge_Center_Menu.addAction(_("Right to Center"))
659  Animate_Right_Center.triggered.connect(partial(self.Animate_Triggered, MENU_ANIMATE_RIGHT_CENTER, clip_ids, position))
660  Animate_Bottom_Center = Edge_Center_Menu.addAction(_("Bottom to Center"))
661  Animate_Bottom_Center.triggered.connect(partial(self.Animate_Triggered, MENU_ANIMATE_BOTTOM_CENTER, clip_ids, position))
662  Position_Menu.addMenu(Edge_Center_Menu)
663 
664  # Edge to Edge
665  Edge_Edge_Menu = QMenu(_("Edge to Edge"), self)
666  Animate_Top_Bottom = Edge_Edge_Menu.addAction(_("Top to Bottom"))
667  Animate_Top_Bottom.triggered.connect(partial(self.Animate_Triggered, MENU_ANIMATE_TOP_BOTTOM, clip_ids, position))
668  Animate_Left_Right = Edge_Edge_Menu.addAction(_("Left to Right"))
669  Animate_Left_Right.triggered.connect(partial(self.Animate_Triggered, MENU_ANIMATE_LEFT_RIGHT, clip_ids, position))
670  Animate_Right_Left = Edge_Edge_Menu.addAction(_("Right to Left"))
671  Animate_Right_Left.triggered.connect(partial(self.Animate_Triggered, MENU_ANIMATE_RIGHT_LEFT, clip_ids, position))
672  Animate_Bottom_Top = Edge_Edge_Menu.addAction(_("Bottom to Top"))
673  Animate_Bottom_Top.triggered.connect(partial(self.Animate_Triggered, MENU_ANIMATE_BOTTOM_TOP, clip_ids, position))
674  Position_Menu.addMenu(Edge_Edge_Menu)
675 
676  # Random Animation
677  Position_Menu.addSeparator()
678  Random = Position_Menu.addAction(_("Random"))
679  Random.triggered.connect(partial(self.Animate_Triggered, MENU_ANIMATE_RANDOM, clip_ids, position))
680 
681  # Add Sub-Menu's to Position menu
682  Animate_Menu.addMenu(Position_Menu)
683 
684  # Add Each position menu
685  menu.addMenu(Animate_Menu)
686 
687  # Rotate Menu
688  Rotation_Menu = QMenu(_("Rotate"), self)
689  Rotation_None = Rotation_Menu.addAction(_("No Rotation"))
690  Rotation_None.triggered.connect(partial(self.Rotate_Triggered, MENU_ROTATE_NONE, clip_ids))
691  Rotation_Menu.addSeparator()
692  Rotation_90_Right = Rotation_Menu.addAction(_("Rotate 90 (Right)"))
693  Rotation_90_Right.triggered.connect(partial(self.Rotate_Triggered, MENU_ROTATE_90_RIGHT, clip_ids))
694  Rotation_90_Left = Rotation_Menu.addAction(_("Rotate 90 (Left)"))
695  Rotation_90_Left.triggered.connect(partial(self.Rotate_Triggered, MENU_ROTATE_90_LEFT, clip_ids))
696  Rotation_180_Flip = Rotation_Menu.addAction(_("Rotate 180 (Flip)"))
697  Rotation_180_Flip.triggered.connect(partial(self.Rotate_Triggered, MENU_ROTATE_180_FLIP, clip_ids))
698  menu.addMenu(Rotation_Menu)
699 
700  # Layout Menu
701  Layout_Menu = QMenu(_("Layout"), self)
702  Layout_None = Layout_Menu.addAction(_("Reset Layout"))
703  Layout_None.triggered.connect(partial(self.Layout_Triggered, MENU_LAYOUT_NONE, clip_ids))
704  Layout_Menu.addSeparator()
705  Layout_Center = Layout_Menu.addAction(_("1/4 Size - Center"))
706  Layout_Center.triggered.connect(partial(self.Layout_Triggered, MENU_LAYOUT_CENTER, clip_ids))
707  Layout_Top_Left = Layout_Menu.addAction(_("1/4 Size - Top Left"))
708  Layout_Top_Left.triggered.connect(partial(self.Layout_Triggered, MENU_LAYOUT_TOP_LEFT, clip_ids))
709  Layout_Top_Right = Layout_Menu.addAction(_("1/4 Size - Top Right"))
710  Layout_Top_Right.triggered.connect(partial(self.Layout_Triggered, MENU_LAYOUT_TOP_RIGHT, clip_ids))
711  Layout_Bottom_Left = Layout_Menu.addAction(_("1/4 Size - Bottom Left"))
712  Layout_Bottom_Left.triggered.connect(partial(self.Layout_Triggered, MENU_LAYOUT_BOTTOM_LEFT, clip_ids))
713  Layout_Bottom_Right = Layout_Menu.addAction(_("1/4 Size - Bottom Right"))
714  Layout_Bottom_Right.triggered.connect(partial(self.Layout_Triggered, MENU_LAYOUT_BOTTOM_RIGHT, clip_ids))
715  Layout_Menu.addSeparator()
716  Layout_Bottom_All_With_Aspect = Layout_Menu.addAction(_("Show All (Maintain Ratio)"))
717  Layout_Bottom_All_With_Aspect.triggered.connect(partial(self.Layout_Triggered, MENU_LAYOUT_ALL_WITH_ASPECT, clip_ids))
718  Layout_Bottom_All_Without_Aspect = Layout_Menu.addAction(_("Show All (Distort)"))
719  Layout_Bottom_All_Without_Aspect.triggered.connect(partial(self.Layout_Triggered, MENU_LAYOUT_ALL_WITHOUT_ASPECT, clip_ids))
720  menu.addMenu(Layout_Menu)
721 
722  # Time Menu
723  Time_Menu = QMenu(_("Time"), self)
724  Time_None = Time_Menu.addAction(_("Reset Time"))
725  Time_None.triggered.connect(partial(self.Time_Triggered, MENU_TIME_NONE, clip_ids, '1X'))
726  Time_Menu.addSeparator()
727  for speed, speed_values in [("Normal", ['1X']), ("Fast", ['2X', '4X', '8X', '16X']), ("Slow", ['1/2X', '1/4X', '1/8X', '1/16X'])]:
728  Speed_Menu = QMenu(_(speed), self)
729 
730  for direction, direction_value in [("Forward", MENU_TIME_FORWARD), ("Backward", MENU_TIME_BACKWARD)]:
731  Direction_Menu = QMenu(_(direction), self)
732 
733  for actual_speed in speed_values:
734  # Add menu option
735  Time_Option = Direction_Menu.addAction(_(actual_speed))
736  Time_Option.triggered.connect(partial(self.Time_Triggered, direction_value, clip_ids, actual_speed))
737 
738  # Add menu to parent
739  Speed_Menu.addMenu(Direction_Menu)
740  # Add menu to parent
741  Time_Menu.addMenu(Speed_Menu)
742 
743  # Add Freeze menu options
744  Time_Menu.addSeparator()
745  for freeze_type, trigger_type in [(_("Freeze"), MENU_TIME_FREEZE), (_("Freeze && Zoom"), MENU_TIME_FREEZE_ZOOM)]:
746  Freeze_Menu = QMenu(freeze_type, self)
747 
748  for freeze_seconds in [2, 4, 6, 8, 10, 20, 30]:
749  # Add menu option
750  Time_Option = Freeze_Menu.addAction(_('{} seconds').format(freeze_seconds))
751  Time_Option.triggered.connect(partial(self.Time_Triggered, trigger_type, clip_ids, freeze_seconds, playhead_position))
752 
753  # Add menu to parent
754  Time_Menu.addMenu(Freeze_Menu)
755 
756  # Add menu to parent
757  menu.addMenu(Time_Menu)
758 
759  # Volume Menu
760  Volume_Menu = QMenu(_("Volume"), self)
761  Volume_None = Volume_Menu.addAction(_("Reset Volume"))
762  Volume_None.triggered.connect(partial(self.Volume_Triggered, MENU_VOLUME_NONE, clip_ids))
763  Volume_Menu.addSeparator()
764  for position in ["Start of Clip", "End of Clip", "Entire Clip"]:
765  Position_Menu = QMenu(_(position), self)
766 
767  if position == "Start of Clip":
768  Fade_In_Fast = Position_Menu.addAction(_("Fade In (Fast)"))
769  Fade_In_Fast.triggered.connect(partial(self.Volume_Triggered, MENU_VOLUME_FADE_IN_FAST, clip_ids, position))
770  Fade_In_Slow = Position_Menu.addAction(_("Fade In (Slow)"))
771  Fade_In_Slow.triggered.connect(partial(self.Volume_Triggered, MENU_VOLUME_FADE_IN_SLOW, clip_ids, position))
772 
773  elif position == "End of Clip":
774  Fade_Out_Fast = Position_Menu.addAction(_("Fade Out (Fast)"))
775  Fade_Out_Fast.triggered.connect(partial(self.Volume_Triggered, MENU_VOLUME_FADE_OUT_FAST, clip_ids, position))
776  Fade_Out_Slow = Position_Menu.addAction(_("Fade Out (Slow)"))
777  Fade_Out_Slow.triggered.connect(partial(self.Volume_Triggered, MENU_VOLUME_FADE_OUT_SLOW, clip_ids, position))
778 
779  else:
780  Fade_In_Out_Fast = Position_Menu.addAction(_("Fade In and Out (Fast)"))
781  Fade_In_Out_Fast.triggered.connect(partial(self.Volume_Triggered, MENU_VOLUME_FADE_IN_OUT_FAST, clip_ids, position))
782  Fade_In_Out_Slow = Position_Menu.addAction(_("Fade In and Out (Slow)"))
783  Fade_In_Out_Slow.triggered.connect(partial(self.Volume_Triggered, MENU_VOLUME_FADE_IN_OUT_SLOW, clip_ids, position))
784  Position_Menu.addSeparator()
785  Fade_In_Slow = Position_Menu.addAction(_("Fade In (Entire Clip)"))
786  Fade_In_Slow.triggered.connect(partial(self.Volume_Triggered, MENU_VOLUME_FADE_IN_SLOW, clip_ids, position))
787  Fade_Out_Slow = Position_Menu.addAction(_("Fade Out (Entire Clip)"))
788  Fade_Out_Slow.triggered.connect(partial(self.Volume_Triggered, MENU_VOLUME_FADE_OUT_SLOW, clip_ids, position))
789 
790  # Add levels (100% to 0%)
791  Position_Menu.addSeparator()
792  Volume_100 = Position_Menu.addAction(_("Level 100%"))
793  Volume_100.triggered.connect(partial(self.Volume_Triggered, MENU_VOLUME_LEVEL_100, clip_ids, position))
794  Volume_90 = Position_Menu.addAction(_("Level 90%"))
795  Volume_90.triggered.connect(partial(self.Volume_Triggered, MENU_VOLUME_LEVEL_90, clip_ids, position))
796  Volume_80 = Position_Menu.addAction(_("Level 80%"))
797  Volume_80.triggered.connect(partial(self.Volume_Triggered, MENU_VOLUME_LEVEL_80, clip_ids, position))
798  Volume_70 = Position_Menu.addAction(_("Level 70%"))
799  Volume_70.triggered.connect(partial(self.Volume_Triggered, MENU_VOLUME_LEVEL_70, clip_ids, position))
800  Volume_60 = Position_Menu.addAction(_("Level 60%"))
801  Volume_60.triggered.connect(partial(self.Volume_Triggered, MENU_VOLUME_LEVEL_60, clip_ids, position))
802  Volume_50 = Position_Menu.addAction(_("Level 50%"))
803  Volume_50.triggered.connect(partial(self.Volume_Triggered, MENU_VOLUME_LEVEL_50, clip_ids, position))
804  Volume_40 = Position_Menu.addAction(_("Level 40%"))
805  Volume_40.triggered.connect(partial(self.Volume_Triggered, MENU_VOLUME_LEVEL_40, clip_ids, position))
806  Volume_30 = Position_Menu.addAction(_("Level 30%"))
807  Volume_30.triggered.connect(partial(self.Volume_Triggered, MENU_VOLUME_LEVEL_30, clip_ids, position))
808  Volume_20 = Position_Menu.addAction(_("Level 20%"))
809  Volume_20.triggered.connect(partial(self.Volume_Triggered, MENU_VOLUME_LEVEL_20, clip_ids, position))
810  Volume_10 = Position_Menu.addAction(_("Level 10%"))
811  Volume_10.triggered.connect(partial(self.Volume_Triggered, MENU_VOLUME_LEVEL_10, clip_ids, position))
812  Volume_0 = Position_Menu.addAction(_("Level 0%"))
813  Volume_0.triggered.connect(partial(self.Volume_Triggered, MENU_VOLUME_LEVEL_0, clip_ids, position))
814 
815  Volume_Menu.addMenu(Position_Menu)
816  menu.addMenu(Volume_Menu)
817 
818  # Add separate audio menu
819  Split_Audio_Channels_Menu = QMenu(_("Separate Audio"), self)
820  Split_Single_Clip = Split_Audio_Channels_Menu.addAction(_("Single Clip (all channels)"))
821  Split_Single_Clip.triggered.connect(partial(self.Split_Audio_Triggered, MENU_SPLIT_AUDIO_SINGLE, clip_ids))
822  Split_Multiple_Clips = Split_Audio_Channels_Menu.addAction(_("Multiple Clips (each channel)"))
823  Split_Multiple_Clips.triggered.connect(partial(self.Split_Audio_Triggered, MENU_SPLIT_AUDIO_MULTIPLE, clip_ids))
824  menu.addMenu(Split_Audio_Channels_Menu)
825 
826  # If Playhead overlapping clip
827  if clip:
828  start_of_clip = float(clip.data["start"])
829  end_of_clip = float(clip.data["end"])
830  position_of_clip = float(clip.data["position"])
831  if playhead_position >= position_of_clip and playhead_position <= (position_of_clip + (end_of_clip - start_of_clip)):
832  # Add split clip menu
833  Slice_Menu = QMenu(_("Slice"), self)
834  Slice_Keep_Both = Slice_Menu.addAction(_("Keep Both Sides"))
835  Slice_Keep_Both.triggered.connect(partial(self.Slice_Triggered, MENU_SLICE_KEEP_BOTH, [clip_id], [], playhead_position))
836  Slice_Keep_Left = Slice_Menu.addAction(_("Keep Left Side"))
837  Slice_Keep_Left.triggered.connect(partial(self.Slice_Triggered, MENU_SLICE_KEEP_LEFT, [clip_id], [], playhead_position))
838  Slice_Keep_Right = Slice_Menu.addAction(_("Keep Right Side"))
839  Slice_Keep_Right.triggered.connect(partial(self.Slice_Triggered, MENU_SLICE_KEEP_RIGHT, [clip_id], [], playhead_position))
840  menu.addMenu(Slice_Menu)
841 
842  # Transform menu
843  Transform_Action = self.window.actionTransform
844  Transform_Action.triggered.connect(partial(self.Transform_Triggered, MENU_TRANSFORM, clip_ids))
845  menu.addAction(Transform_Action)
846 
847  # Add clip display menu (waveform or thumbnail)
848  menu.addSeparator()
849  Waveform_Menu = QMenu(_("Display"), self)
850  ShowWaveform = Waveform_Menu.addAction(_("Show Waveform"))
851  ShowWaveform.triggered.connect(partial(self.Show_Waveform_Triggered, clip_ids))
852  HideWaveform = Waveform_Menu.addAction(_("Show Thumbnail"))
853  HideWaveform.triggered.connect(partial(self.Hide_Waveform_Triggered, clip_ids))
854  menu.addMenu(Waveform_Menu)
855 
856  # Properties
857  menu.addAction(self.window.actionProperties)
858 
859  # Remove Clip Menu
860  menu.addSeparator()
861  menu.addAction(self.window.actionRemoveClip)
862 
863  # Show Context menu
864  return menu.popup(QCursor.pos())
865 
866  def Transform_Triggered(self, action, clip_ids):
867  print("Transform_Triggered")
868 
869  # Emit signal to transform this clip (for the 1st clip id)
870  if clip_ids:
871  # Transform first clip in list
872  get_app().window.TransformSignal.emit(clip_ids[0])
873  else:
874  # Clear transform
875  get_app().window.TransformSignal.emit("")
876 
877  ##
878  # Show a waveform for the selected clip
879  def Show_Waveform_Triggered(self, clip_ids):
880 
881  # Loop through each selected clip
882  for clip_id in clip_ids:
883 
884  # Get existing clip object
885  clip = Clip.get(id=clip_id)
886  if not clip:
887  # Invalid clip, skip to next item
888  continue
889 
890  file_path = clip.data["reader"]["path"]
891 
892  # Find actual clip object from libopenshot
893  c = None
894  clips = get_app().window.timeline_sync.timeline.Clips()
895  for clip_object in clips:
896  if clip_object.Id() == clip_id:
897  c = clip_object
898 
899  if c and c.Reader() and not c.Reader().info.has_single_image:
900  # Find frame 1 channel_filter property
901  channel_filter = c.channel_filter.GetInt(1)
902 
903  # Set cursor to waiting
904  get_app().setOverrideCursor(QCursor(Qt.WaitCursor))
905 
906  # Get audio data in a separate thread (so it doesn't block the UI)
907  channel_filter = channel_filter
908  get_audio_data(clip_id, file_path, channel_filter, c.volume)
909 
910  ##
911  # Hide the waveform for the selected clip
912  def Hide_Waveform_Triggered(self, clip_ids):
913 
914  # Loop through each selected clip
915  for clip_id in clip_ids:
916 
917  # Get existing clip object
918  clip = Clip.get(id=clip_id)
919 
920  if clip:
921  # Pass to javascript timeline (and render)
922  cmd = JS_SCOPE_SELECTOR + ".hideAudioData('" + clip_id + "');"
923  self.page().mainFrame().evaluateJavaScript(cmd)
924 
925  ##
926  # Callback when audio waveform is ready
927  def Waveform_Ready(self, clip_id, audio_data):
928  log.info("Waveform_Ready for clip ID: %s" % (clip_id))
929 
930  # Convert waveform data to JSON
931  serialized_audio_data = json.dumps(audio_data)
932 
933  # Set waveform cache (with clip_id as key)
934  self.waveform_cache[clip_id] = serialized_audio_data
935 
936  # Pass to javascript timeline (and render)
937  cmd = JS_SCOPE_SELECTOR + ".setAudioData('" + clip_id + "', " + serialized_audio_data + ");"
938  self.page().mainFrame().evaluateJavaScript(cmd)
939 
940  # Restore normal cursor
941  get_app().restoreOverrideCursor()
942 
943  # Start timer to redraw audio
944  self.redraw_audio_timer.start()
945 
946  ##
947  # Callback when thumbnail needs to be updated
948  def Thumbnail_Updated(self, clip_id):
949  # Pass to javascript timeline (and render)
950  cmd = JS_SCOPE_SELECTOR + ".updateThumbnail('" + clip_id + "');"
951  self.page().mainFrame().evaluateJavaScript(cmd)
952 
953  ##
954  # Callback for split audio context menus
955  def Split_Audio_Triggered(self, action, clip_ids):
956  log.info("Split_Audio_Triggered")
957 
958  # Get translation method
959  _ = get_app()._tr
960 
961  # Loop through each selected clip
962  for clip_id in clip_ids:
963 
964  # Get existing clip object
965  clip = Clip.get(id=clip_id)
966  if not clip:
967  # Invalid clip, skip to next item
968  continue
969 
970  # Get # of tracks
971  all_tracks = get_app().project.get(["layers"])
972 
973  # Clear audio override
974  p = openshot.Point(1, -1.0, openshot.CONSTANT) # Override has_audio keyframe to False
975  p_object = json.loads(p.Json())
976  clip.data["has_audio"] = { "Points" : [p_object]}
977 
978  # Remove the ID property from the clip (so it becomes a new one)
979  clip.id = None
980  clip.type = 'insert'
981  clip.data.pop('id')
982  clip.key.pop(1)
983 
984  # Get title of clip
985  clip_title = clip.data["title"]
986 
987  if action == MENU_SPLIT_AUDIO_SINGLE:
988  # Clear channel filter on new clip
989  p = openshot.Point(1, -1.0, openshot.CONSTANT)
990  p_object = json.loads(p.Json())
991  clip.data["channel_filter"] = { "Points" : [p_object]}
992 
993  # Filter out video on the new clip
994  p = openshot.Point(1, 0.0, openshot.CONSTANT) # Override has_audio keyframe to False
995  p_object = json.loads(p.Json())
996  clip.data["has_video"] = { "Points" : [p_object]}
997 
998  # Get track below selected track (if any)
999  next_track_number = clip.data['layer']
1000  found_track = False
1001  for track in reversed(sorted(all_tracks, key=itemgetter('number'))):
1002  if found_track:
1003  next_track_number = track.get("number")
1004  break
1005  if track.get("number") == clip.data['layer']:
1006  found_track = True
1007  continue
1008 
1009  # Adjust the layer, so this new audio clip doesn't overlap the parent
1010  clip.data['layer'] = next_track_number # Add to layer below clip
1011 
1012  # Adjust the clip title
1013  channel_label = _("(all channels)")
1014  clip.data["title"] = clip_title + " " + channel_label
1015  # Save changes
1016  clip.save()
1017 
1018  # Generate waveform for new clip
1019  log.info("Generate waveform for split audio track clip id: %s" % clip.id)
1020  self.Show_Waveform_Triggered([clip.id])
1021 
1022  if action == MENU_SPLIT_AUDIO_MULTIPLE:
1023  # Get # of channels on clip
1024  channels = int(clip.data["reader"]["channels"])
1025 
1026  # Loop through each channel
1027  for channel in range(0, channels):
1028  log.info("Adding clip for channel %s" % channel)
1029 
1030  # Each clip is filtered to a different channel
1031  p = openshot.Point(1, channel, openshot.CONSTANT)
1032  p_object = json.loads(p.Json())
1033  clip.data["channel_filter"] = { "Points" : [p_object]}
1034 
1035  # Filter out video on the new clip
1036  p = openshot.Point(1, 0.0, openshot.CONSTANT) # Override has_audio keyframe to False
1037  p_object = json.loads(p.Json())
1038  clip.data["has_video"] = { "Points" : [p_object]}
1039 
1040  # Get track below selected track (if any)
1041  next_track_number = clip.data['layer']
1042  found_track = False
1043  for track in reversed(sorted(all_tracks, key=itemgetter('number'))):
1044  if found_track:
1045  next_track_number = track.get("number")
1046  break
1047  if track.get("number") == clip.data['layer']:
1048  found_track = True
1049  continue
1050 
1051  # Adjust the layer, so this new audio clip doesn't overlap the parent
1052  clip.data['layer'] = max(next_track_number, 0) # Add to layer below clip
1053 
1054  # Adjust the clip title
1055  channel_label = _("(channel %s)") % (channel + 1)
1056  clip.data["title"] = clip_title + " " + channel_label
1057 
1058  # Save changes
1059  clip.save()
1060 
1061  # Generate waveform for new clip
1062  log.info("Generate waveform for split audio track clip id: %s" % clip.id)
1063  self.Show_Waveform_Triggered([clip.id])
1064 
1065  # Remove the ID property from the clip (so next time, it will create a new clip)
1066  clip.id = None
1067  clip.type = 'insert'
1068  clip.data.pop('id')
1069 
1070  for clip_id in clip_ids:
1071 
1072  # Get existing clip object
1073  clip = Clip.get(id=clip_id)
1074  if not clip:
1075  # Invalid clip, skip to next item
1076  continue
1077 
1078  # Filter out audio on the original clip
1079  p = openshot.Point(1, 0.0, openshot.CONSTANT) # Override has_audio keyframe to False
1080  p_object = json.loads(p.Json())
1081  clip.data["has_audio"] = { "Points" : [p_object]}
1082 
1083  # Save filter on original clip
1084  self.update_clip_data(clip.data, only_basic_props=False, ignore_reader=True)
1085  clip.save()
1086 
1087  ##
1088  # Callback for the layout context menus
1089  def Layout_Triggered(self, action, clip_ids):
1090  log.info(action)
1091 
1092  # Loop through each selected clip
1093  for clip_id in clip_ids:
1094 
1095  # Get existing clip object
1096  clip = Clip.get(id=clip_id)
1097  if not clip:
1098  # Invalid clip, skip to next item
1099  continue
1100 
1101  new_gravity = openshot.GRAVITY_CENTER
1102  if action == MENU_LAYOUT_CENTER:
1103  new_gravity = openshot.GRAVITY_CENTER
1104  if action == MENU_LAYOUT_TOP_LEFT:
1105  new_gravity = openshot.GRAVITY_TOP_LEFT
1106  elif action == MENU_LAYOUT_TOP_RIGHT:
1107  new_gravity = openshot.GRAVITY_TOP_RIGHT
1108  elif action == MENU_LAYOUT_BOTTOM_LEFT:
1109  new_gravity = openshot.GRAVITY_BOTTOM_LEFT
1110  elif action == MENU_LAYOUT_BOTTOM_RIGHT:
1111  new_gravity = openshot.GRAVITY_BOTTOM_RIGHT
1112 
1113  if action == MENU_LAYOUT_NONE:
1114  # Reset scale mode
1115  clip.data["scale"] = openshot.SCALE_FIT
1116  clip.data["gravity"] = openshot.GRAVITY_CENTER
1117 
1118  # Clear scale keyframes
1119  p = openshot.Point(1, 1.0, openshot.BEZIER)
1120  p_object = json.loads(p.Json())
1121  clip.data["scale_x"] = { "Points" : [p_object]}
1122  clip.data["scale_y"] = { "Points" : [p_object]}
1123 
1124  # Clear location keyframes
1125  p = openshot.Point(1, 0.0, openshot.BEZIER)
1126  p_object = json.loads(p.Json())
1127  clip.data["location_x"] = { "Points" : [p_object]}
1128  clip.data["location_y"] = { "Points" : [p_object]}
1129 
1130  if action == MENU_LAYOUT_CENTER or \
1131  action == MENU_LAYOUT_TOP_LEFT or \
1132  action == MENU_LAYOUT_TOP_RIGHT or \
1133  action == MENU_LAYOUT_BOTTOM_LEFT or \
1134  action == MENU_LAYOUT_BOTTOM_RIGHT:
1135  # Reset scale mode
1136  clip.data["scale"] = openshot.SCALE_FIT
1137  clip.data["gravity"] = new_gravity
1138 
1139  # Add scale keyframes
1140  p = openshot.Point(1, 0.5, openshot.BEZIER)
1141  p_object = json.loads(p.Json())
1142  clip.data["scale_x"] = { "Points" : [p_object]}
1143  clip.data["scale_y"] = { "Points" : [p_object]}
1144 
1145  # Add location keyframes
1146  p = openshot.Point(1, 0.0, openshot.BEZIER)
1147  p_object = json.loads(p.Json())
1148  clip.data["location_x"] = { "Points" : [p_object]}
1149  clip.data["location_y"] = { "Points" : [p_object]}
1150 
1151 
1152  if action == MENU_LAYOUT_ALL_WITH_ASPECT:
1153  # Update all intersecting clips
1154  self.show_all_clips(clip, False)
1155 
1156  elif action == MENU_LAYOUT_ALL_WITHOUT_ASPECT:
1157  # Update all intersecting clips
1158  self.show_all_clips(clip, True)
1159 
1160  else:
1161  # Save changes
1162  self.update_clip_data(clip.data, only_basic_props=False, ignore_reader=True)
1163 
1164  ##
1165  # Callback for the animate context menus
1166  def Animate_Triggered(self, action, clip_ids, position="Entire Clip"):
1167  log.info(action)
1168 
1169  # Loop through each selected clip
1170  for clip_id in clip_ids:
1171 
1172  # Get existing clip object
1173  clip = Clip.get(id=clip_id)
1174  if not clip:
1175  # Invalid clip, skip to next item
1176  continue
1177 
1178  # Get framerate
1179  fps = get_app().project.get(["fps"])
1180  fps_float = float(fps["num"]) / float(fps["den"])
1181 
1182  # Get existing clip object
1183  start_of_clip = round(float(clip.data["start"]) * fps_float) + 1
1184  end_of_clip = round(float(clip.data["end"]) * fps_float) + 1
1185 
1186  # Determine the beginning and ending of this animation
1187  # ["Start of Clip", "End of Clip", "Entire Clip"]
1188  start_animation = start_of_clip
1189  end_animation = end_of_clip
1190  if position == "Start of Clip":
1191  start_animation = start_of_clip
1192  end_animation = min(start_of_clip + (1.0 * fps_float), end_of_clip)
1193  elif position == "End of Clip":
1194  start_animation = max(1.0, end_of_clip - (1.0 * fps_float))
1195  end_animation = end_of_clip
1196 
1197  if action == MENU_ANIMATE_NONE:
1198  # Clear all keyframes
1199  default_zoom = openshot.Point(start_animation, 1.0, openshot.BEZIER)
1200  default_zoom_object = json.loads(default_zoom.Json())
1201  default_loc = openshot.Point(start_animation, 0.0, openshot.BEZIER)
1202  default_loc_object = json.loads(default_loc.Json())
1203  clip.data["gravity"] = openshot.GRAVITY_CENTER
1204  clip.data["scale_x"] = { "Points" : [default_zoom_object]}
1205  clip.data["scale_y"] = { "Points" : [default_zoom_object]}
1206  clip.data["location_x"] = { "Points" : [default_loc_object]}
1207  clip.data["location_y"] = { "Points" : [default_loc_object]}
1208 
1209  if action in [MENU_ANIMATE_IN_50_100, MENU_ANIMATE_IN_75_100, MENU_ANIMATE_IN_100_150, MENU_ANIMATE_OUT_100_75, MENU_ANIMATE_OUT_100_50, MENU_ANIMATE_OUT_150_100]:
1210  # Scale animation
1211  start_scale = 1.0
1212  end_scale = 1.0
1213  if action == MENU_ANIMATE_IN_50_100:
1214  start_scale = 0.5
1215  elif action == MENU_ANIMATE_IN_75_100:
1216  start_scale = 0.75
1217  elif action == MENU_ANIMATE_IN_100_150:
1218  end_scale = 1.5
1219  elif action == MENU_ANIMATE_OUT_100_75:
1220  end_scale = 0.75
1221  elif action == MENU_ANIMATE_OUT_100_50:
1222  end_scale = 0.5
1223  elif action == MENU_ANIMATE_OUT_150_100:
1224  start_scale = 1.5
1225 
1226  # Add keyframes
1227  start = openshot.Point(start_animation, start_scale, openshot.BEZIER)
1228  start_object = json.loads(start.Json())
1229  end = openshot.Point(end_animation, end_scale, openshot.BEZIER)
1230  end_object = json.loads(end.Json())
1231  clip.data["gravity"] = openshot.GRAVITY_CENTER
1232  clip.data["scale_x"]["Points"].append(start_object)
1233  clip.data["scale_x"]["Points"].append(end_object)
1234  clip.data["scale_y"]["Points"].append(start_object)
1235  clip.data["scale_y"]["Points"].append(end_object)
1236 
1237 
1238  if action in [MENU_ANIMATE_CENTER_TOP, MENU_ANIMATE_CENTER_LEFT, MENU_ANIMATE_CENTER_RIGHT, MENU_ANIMATE_CENTER_BOTTOM,
1239  MENU_ANIMATE_TOP_CENTER, MENU_ANIMATE_LEFT_CENTER, MENU_ANIMATE_RIGHT_CENTER, MENU_ANIMATE_BOTTOM_CENTER,
1240  MENU_ANIMATE_TOP_BOTTOM, MENU_ANIMATE_LEFT_RIGHT, MENU_ANIMATE_RIGHT_LEFT, MENU_ANIMATE_BOTTOM_TOP]:
1241  # Location animation
1242  animate_start_x = 0.0
1243  animate_end_x = 0.0
1244  animate_start_y = 0.0
1245  animate_end_y = 0.0
1246  # Center to edge...
1247  if action == MENU_ANIMATE_CENTER_TOP:
1248  animate_end_y = -1.0
1249  elif action == MENU_ANIMATE_CENTER_LEFT:
1250  animate_end_x = -1.0
1251  elif action == MENU_ANIMATE_CENTER_RIGHT:
1252  animate_end_x = 1.0
1253  elif action == MENU_ANIMATE_CENTER_BOTTOM:
1254  animate_end_y = 1.0
1255 
1256  # Edge to Center
1257  elif action == MENU_ANIMATE_TOP_CENTER:
1258  animate_start_y = -1.0
1259  elif action == MENU_ANIMATE_LEFT_CENTER:
1260  animate_start_x = -1.0
1261  elif action == MENU_ANIMATE_RIGHT_CENTER:
1262  animate_start_x = 1.0
1263  elif action == MENU_ANIMATE_BOTTOM_CENTER:
1264  animate_start_y = 1.0
1265 
1266  # Edge to Edge
1267  elif action == MENU_ANIMATE_TOP_BOTTOM:
1268  animate_start_y = -1.0
1269  animate_end_y = 1.0
1270  elif action == MENU_ANIMATE_LEFT_RIGHT:
1271  animate_start_x = -1.0
1272  animate_end_x = 1.0
1273  elif action == MENU_ANIMATE_RIGHT_LEFT:
1274  animate_start_x = 1.0
1275  animate_end_x = -1.0
1276  elif action == MENU_ANIMATE_BOTTOM_TOP:
1277  animate_start_y = 1.0
1278  animate_end_y = -1.0
1279 
1280  # Add keyframes
1281  start_x = openshot.Point(start_animation, animate_start_x, openshot.BEZIER)
1282  start_x_object = json.loads(start_x.Json())
1283  end_x = openshot.Point(end_animation, animate_end_x, openshot.BEZIER)
1284  end_x_object = json.loads(end_x.Json())
1285  start_y = openshot.Point(start_animation, animate_start_y, openshot.BEZIER)
1286  start_y_object = json.loads(start_y.Json())
1287  end_y = openshot.Point(end_animation, animate_end_y, openshot.BEZIER)
1288  end_y_object = json.loads(end_y.Json())
1289  clip.data["gravity"] = openshot.GRAVITY_CENTER
1290  clip.data["location_x"]["Points"].append(start_x_object)
1291  clip.data["location_x"]["Points"].append(end_x_object)
1292  clip.data["location_y"]["Points"].append(start_y_object)
1293  clip.data["location_y"]["Points"].append(end_y_object)
1294 
1295  if action == MENU_ANIMATE_RANDOM:
1296  # Location animation
1297  animate_start_x = uniform(-0.5, 0.5)
1298  animate_end_x = uniform(-0.15, 0.15)
1299  animate_start_y = uniform(-0.5, 0.5)
1300  animate_end_y = uniform(-0.15, 0.15)
1301 
1302  # Scale animation
1303  start_scale = uniform(0.5, 1.5)
1304  end_scale = uniform(0.85, 1.15)
1305 
1306  # Add keyframes
1307  start = openshot.Point(start_animation, start_scale, openshot.BEZIER)
1308  start_object = json.loads(start.Json())
1309  end = openshot.Point(end_animation, end_scale, openshot.BEZIER)
1310  end_object = json.loads(end.Json())
1311  clip.data["gravity"] = openshot.GRAVITY_CENTER
1312  clip.data["scale_x"]["Points"].append(start_object)
1313  clip.data["scale_x"]["Points"].append(end_object)
1314  clip.data["scale_y"]["Points"].append(start_object)
1315  clip.data["scale_y"]["Points"].append(end_object)
1316 
1317  # Add keyframes
1318  start_x = openshot.Point(start_animation, animate_start_x, openshot.BEZIER)
1319  start_x_object = json.loads(start_x.Json())
1320  end_x = openshot.Point(end_animation, animate_end_x, openshot.BEZIER)
1321  end_x_object = json.loads(end_x.Json())
1322  start_y = openshot.Point(start_animation, animate_start_y, openshot.BEZIER)
1323  start_y_object = json.loads(start_y.Json())
1324  end_y = openshot.Point(end_animation, animate_end_y, openshot.BEZIER)
1325  end_y_object = json.loads(end_y.Json())
1326  clip.data["gravity"] = openshot.GRAVITY_CENTER
1327  clip.data["location_x"]["Points"].append(start_x_object)
1328  clip.data["location_x"]["Points"].append(end_x_object)
1329  clip.data["location_y"]["Points"].append(start_y_object)
1330  clip.data["location_y"]["Points"].append(end_y_object)
1331 
1332  # Save changes
1333  self.update_clip_data(clip.data, only_basic_props=False, ignore_reader=True)
1334 
1335  ##
1336  # Callback for copy context menus
1337  def Copy_Triggered(self, action, clip_ids, tran_ids):
1338  log.info(action)
1339 
1340  # Empty previous clipboard
1341  self.copy_clipboard = {}
1343 
1344  # Loop through clip objects
1345  for clip_id in clip_ids:
1346 
1347  # Get existing clip object
1348  clip = Clip.get(id=clip_id)
1349  if not clip:
1350  # Invalid clip, skip to next item
1351  continue
1352 
1353  self.copy_clipboard[clip_id] = {}
1354 
1355  if action == MENU_COPY_CLIP or action == MENU_COPY_ALL:
1356  self.copy_clipboard[clip_id] = clip.data
1357  elif action == MENU_COPY_KEYFRAMES_ALL:
1358  self.copy_clipboard[clip_id]['alpha'] = clip.data['alpha']
1359  self.copy_clipboard[clip_id]['gravity'] = clip.data['gravity']
1360  self.copy_clipboard[clip_id]['scale_x'] = clip.data['scale_x']
1361  self.copy_clipboard[clip_id]['scale_y'] = clip.data['scale_y']
1362  self.copy_clipboard[clip_id]['rotation'] = clip.data['rotation']
1363  self.copy_clipboard[clip_id]['location_x'] = clip.data['location_x']
1364  self.copy_clipboard[clip_id]['location_y'] = clip.data['location_y']
1365  self.copy_clipboard[clip_id]['time'] = clip.data['time']
1366  self.copy_clipboard[clip_id]['volume'] = clip.data['volume']
1367  elif action == MENU_COPY_KEYFRAMES_ALPHA:
1368  self.copy_clipboard[clip_id]['alpha'] = clip.data['alpha']
1369  elif action == MENU_COPY_KEYFRAMES_SCALE:
1370  self.copy_clipboard[clip_id]['gravity'] = clip.data['gravity']
1371  self.copy_clipboard[clip_id]['scale_x'] = clip.data['scale_x']
1372  self.copy_clipboard[clip_id]['scale_y'] = clip.data['scale_y']
1373  elif action == MENU_COPY_KEYFRAMES_ROTATE:
1374  self.copy_clipboard[clip_id]['gravity'] = clip.data['gravity']
1375  self.copy_clipboard[clip_id]['rotation'] = clip.data['rotation']
1376  elif action == MENU_COPY_KEYFRAMES_LOCATION:
1377  self.copy_clipboard[clip_id]['gravity'] = clip.data['gravity']
1378  self.copy_clipboard[clip_id]['location_x'] = clip.data['location_x']
1379  self.copy_clipboard[clip_id]['location_y'] = clip.data['location_y']
1380  elif action == MENU_COPY_KEYFRAMES_TIME:
1381  self.copy_clipboard[clip_id]['time'] = clip.data['time']
1382  elif action == MENU_COPY_KEYFRAMES_VOLUME:
1383  self.copy_clipboard[clip_id]['volume'] = clip.data['volume']
1384  elif action == MENU_COPY_EFFECTS:
1385  self.copy_clipboard[clip_id]['effects'] = clip.data['effects']
1386 
1387  # Loop through transition objects
1388  for tran_id in tran_ids:
1389 
1390  # Get existing transition object
1391  tran = Transition.get(id=tran_id)
1392  if not tran:
1393  # Invalid transition, skip to next item
1394  continue
1395 
1396  self.copy_transition_clipboard[tran_id] = {}
1397 
1398  if action == MENU_COPY_TRANSITION or action == MENU_COPY_ALL:
1399  self.copy_transition_clipboard[tran_id] = tran.data
1400  elif action == MENU_COPY_KEYFRAMES_ALL:
1401  self.copy_transition_clipboard[tran_id]['brightness'] = tran.data['brightness']
1402  self.copy_transition_clipboard[tran_id]['contrast'] = tran.data['contrast']
1403  elif action == MENU_COPY_KEYFRAMES_BRIGHTNESS:
1404  self.copy_transition_clipboard[tran_id]['brightness'] = tran.data['brightness']
1405  elif action == MENU_COPY_KEYFRAMES_CONTRAST:
1406  self.copy_transition_clipboard[tran_id]['contrast'] = tran.data['contrast']
1407 
1408  ##
1409  # Callback for paste context menus
1410  def Paste_Triggered(self, action, position, layer_id, clip_ids, tran_ids):
1411  log.info(action)
1412 
1413  # Get list of clipboard items (that are complete clips or transitions)
1414  # i.e. ignore partial clipboard items (keyframes / effects / etc...)
1415  clipboard_clip_ids = [k for k, v in self.copy_clipboard.items() if v.get('id')]
1416  clipboard_tran_ids = [k for k, v in self.copy_transition_clipboard.items() if v.get('id')]
1417 
1418  # Determine left most copied clip, and top most track (the top left point of the copied objects)
1419  if len(clipboard_clip_ids) + len(clipboard_tran_ids):
1420  left_most_position = -1.0
1421  top_most_layer = -1
1422  # Loop through each copied clip (looking for top left point)
1423  for clip_id in clipboard_clip_ids:
1424  # Get existing clip object
1425  clip = Clip()
1426  clip.data = self.copy_clipboard.get(clip_id, {})
1427  if clip.data['position'] < left_most_position or left_most_position == -1.0:
1428  left_most_position = clip.data['position']
1429  if clip.data['layer'] > top_most_layer or top_most_layer == -1.0:
1430  top_most_layer = clip.data['layer']
1431  # Loop through each copied transition (looking for top left point)
1432  for tran_id in clipboard_tran_ids:
1433  # Get existing transition object
1434  tran = Transition()
1435  tran.data = self.copy_transition_clipboard.get(tran_id, {})
1436  if tran.data['position'] < left_most_position or left_most_position == -1.0:
1437  left_most_position = tran.data['position']
1438  if tran.data['layer'] > top_most_layer or top_most_layer == -1.0:
1439  top_most_layer = tran.data['layer']
1440 
1441  # Default layer if not known
1442  if layer_id == -1:
1443  layer_id = top_most_layer
1444 
1445  # Determine difference from top left and paste location
1446  position_diff = position - left_most_position
1447  layer_diff = layer_id - top_most_layer
1448 
1449  # Loop through each copied clip
1450  for clip_id in clipboard_clip_ids:
1451  # Get existing clip object
1452  clip = Clip()
1453  clip.data = self.copy_clipboard.get(clip_id, {})
1454 
1455  # Remove the ID property from the clip (so it becomes a new one)
1456  clip.type = 'insert'
1457  clip.data.pop('id')
1458 
1459  # Adjust the position and track
1460  clip.data['position'] += position_diff
1461  clip.data['layer'] += layer_diff
1462 
1463  # Save changes
1464  clip.save()
1465 
1466  # Loop through all copied transitions
1467  for tran_id in clipboard_tran_ids:
1468  # Get existing transition object
1469  tran = Transition()
1470  tran.data = self.copy_transition_clipboard.get(tran_id, {})
1471 
1472  # Remove the ID property from the transition (so it becomes a new one)
1473  tran.type = 'insert'
1474  tran.data.pop('id')
1475 
1476  # Adjust the position and track
1477  tran.data['position'] += position_diff
1478  tran.data['layer'] += layer_diff
1479 
1480  # Save changes
1481  tran.save()
1482 
1483  # Loop through each full clip object copied
1484  if self.copy_clipboard:
1485  for clip_id in clip_ids:
1486 
1487  # Get existing clip object
1488  clip = Clip.get(id=clip_id)
1489  if not clip:
1490  # Invalid clip, skip to next item
1491  continue
1492 
1493  # Apply clipboard to clip (there should only be a single key in this dict)
1494  for k,v in self.copy_clipboard[list(self.copy_clipboard)[0]].items():
1495  if k != 'id':
1496  # Overwrite clips properties (which are in the clipboard)
1497  clip.data[k] = v
1498 
1499  # Save changes
1500  clip.save()
1501 
1502  # Loop through each full transition object copied
1503  if self.copy_transition_clipboard:
1504  for tran_id in tran_ids:
1505 
1506  # Get existing transition object
1507  tran = Transition.get(id=tran_id)
1508  if not tran:
1509  # Invalid transition, skip to next item
1510  continue
1511 
1512  # Apply clipboard to transition (there should only be a single key in this dict)
1513  for k, v in self.copy_transition_clipboard[list(self.copy_transition_clipboard)[0]].items():
1514  if k != 'id':
1515  # Overwrite transition properties (which are in the clipboard)
1516  tran.data[k] = v
1517 
1518  # Save changes
1519  tran.save()
1520 
1521  ##
1522  # Callback for clip nudges
1523  def Nudge_Triggered(self, action, clip_ids, tran_ids):
1524  log.info("Nudging clip(s) and/or transition(s)")
1525  left_edge = -1.0
1526  right_edge = -1.0
1527 
1528  # Determine how far we're going to nudge (1/2 frame or 0.01s, whichever is larger)
1529  fps = get_app().project.get(["fps"])
1530  fps_float = float(fps["num"]) / float(fps["den"])
1531  nudgeDistance = float(action) / float(fps_float)
1532  nudgeDistance /= 2.0 # 1/2 frame
1533  if abs(nudgeDistance) < 0.01:
1534  nudgeDistance = 0.01 * action # nudge is less than the minimum of +/- 0.01s
1535  log.info("Nudging by %s sec" % nudgeDistance)
1536 
1537  # Loop through each selected clip (find furthest left and right edge)
1538  for clip_id in clip_ids:
1539  # Get existing clip object
1540  clip = Clip.get(id=clip_id)
1541  if not clip:
1542  # Invalid clip, skip to next item
1543  continue
1544 
1545  position = float(clip.data["position"])
1546  start_of_clip = float(clip.data["start"])
1547  end_of_clip = float(clip.data["end"])
1548 
1549  if position < left_edge or left_edge == -1.0:
1550  left_edge = position
1551  if position + (end_of_clip - start_of_clip) > right_edge or right_edge == -1.0:
1552  right_edge = position + (end_of_clip - start_of_clip)
1553 
1554  # Do not nudge beyond the start of the timeline
1555  if left_edge + nudgeDistance < 0.0:
1556  log.info("Cannot nudge beyond start of timeline")
1557  nudgeDistance = 0
1558 
1559  # Loop through each selected transition (find furthest left and right edge)
1560  for tran_id in tran_ids:
1561  # Get existing transition object
1562  tran = Transition.get(id=tran_id)
1563  if not tran:
1564  # Invalid transition, skip to next item
1565  continue
1566 
1567  position = float(tran.data["position"])
1568  start_of_tran = float(tran.data["start"])
1569  end_of_tran = float(tran.data["end"])
1570 
1571  if position < left_edge or left_edge == -1.0:
1572  left_edge = position
1573  if position + (end_of_tran - start_of_tran) > right_edge or right_edge == -1.0:
1574  right_edge = position + (end_of_tran - start_of_tran)
1575 
1576  # Do not nudge beyond the start of the timeline
1577  if left_edge + nudgeDistance < 0.0:
1578  log.info("Cannot nudge beyond start of timeline")
1579  nudgeDistance = 0
1580 
1581  # Loop through each selected clip (update position to align clips)
1582  for clip_id in clip_ids:
1583  # Get existing clip object
1584  clip = Clip.get(id=clip_id)
1585  if not clip:
1586  # Invalid clip, skip to next item
1587  continue
1588 
1589  # Do the nudge
1590  clip.data['position'] += nudgeDistance
1591 
1592  # Save changes
1593  self.update_clip_data(clip.data, only_basic_props=False, ignore_reader=True)
1594 
1595  # Loop through each selected transition (update position to align clips)
1596  for tran_id in tran_ids:
1597  # Get existing transition object
1598  tran = Transition.get(id=tran_id)
1599  if not tran:
1600  # Invalid transition, skip to next item
1601  continue
1602 
1603  # Do the nudge
1604  tran.data['position'] += nudgeDistance
1605 
1606  # Save changes
1607  self.update_transition_data(tran.data, only_basic_props=False)
1608 
1609 
1610  ##
1611  # Callback for alignment context menus
1612  def Align_Triggered(self, action, clip_ids, tran_ids):
1613  log.info(action)
1614  prop_name = "position"
1615  left_edge = -1.0
1616  right_edge = -1.0
1617 
1618  # Loop through each selected clip (find furthest left and right edge)
1619  for clip_id in clip_ids:
1620  # Get existing clip object
1621  clip = Clip.get(id=clip_id)
1622  if not clip:
1623  # Invalid clip, skip to next item
1624  continue
1625 
1626  position = float(clip.data["position"])
1627  start_of_clip = float(clip.data["start"])
1628  end_of_clip = float(clip.data["end"])
1629 
1630  if position < left_edge or left_edge == -1.0:
1631  left_edge = position
1632  if position + (end_of_clip - start_of_clip) > right_edge or right_edge == -1.0:
1633  right_edge = position + (end_of_clip - start_of_clip)
1634 
1635  # Loop through each selected transition (find furthest left and right edge)
1636  for tran_id in tran_ids:
1637  # Get existing transition object
1638  tran = Transition.get(id=tran_id)
1639  if not tran:
1640  # Invalid transition, skip to next item
1641  continue
1642 
1643  position = float(tran.data["position"])
1644  start_of_tran = float(tran.data["start"])
1645  end_of_tran = float(tran.data["end"])
1646 
1647  if position < left_edge or left_edge == -1.0:
1648  left_edge = position
1649  if position + (end_of_tran - start_of_tran) > right_edge or right_edge == -1.0:
1650  right_edge = position + (end_of_tran - start_of_tran)
1651 
1652 
1653  # Loop through each selected clip (update position to align clips)
1654  for clip_id in clip_ids:
1655  # Get existing clip object
1656  clip = Clip.get(id=clip_id)
1657  if not clip:
1658  # Invalid clip, skip to next item
1659  continue
1660 
1661  if action == MENU_ALIGN_LEFT:
1662  clip.data['position'] = left_edge
1663  elif action == MENU_ALIGN_RIGHT:
1664  position = float(clip.data["position"])
1665  start_of_clip = float(clip.data["start"])
1666  end_of_clip = float(clip.data["end"])
1667  right_clip_edge = position + (end_of_clip - start_of_clip)
1668 
1669  clip.data['position'] = position + (right_edge - right_clip_edge)
1670 
1671  # Save changes
1672  self.update_clip_data(clip.data, only_basic_props=False, ignore_reader=True)
1673 
1674  # Loop through each selected transition (update position to align clips)
1675  for tran_id in tran_ids:
1676  # Get existing transition object
1677  tran = Transition.get(id=tran_id)
1678  if not tran:
1679  # Invalid transition, skip to next item
1680  continue
1681 
1682  if action == MENU_ALIGN_LEFT:
1683  tran.data['position'] = left_edge
1684  elif action == MENU_ALIGN_RIGHT:
1685  position = float(tran.data["position"])
1686  start_of_tran = float(tran.data["start"])
1687  end_of_tran = float(tran.data["end"])
1688  right_tran_edge = position + (end_of_tran - start_of_tran)
1689 
1690  tran.data['position'] = position + (right_edge - right_tran_edge)
1691 
1692  # Save changes
1693  self.update_transition_data(tran.data, only_basic_props=False)
1694 
1695  ##
1696  # Callback for fade context menus
1697  def Fade_Triggered(self, action, clip_ids, position="Entire Clip"):
1698  log.info(action)
1699  prop_name = "alpha"
1700 
1701  # Get FPS from project
1702  fps = get_app().project.get(["fps"])
1703  fps_float = float(fps["num"]) / float(fps["den"])
1704 
1705  # Loop through each selected clip
1706  for clip_id in clip_ids:
1707 
1708  # Get existing clip object
1709  clip = Clip.get(id=clip_id)
1710  if not clip:
1711  # Invalid clip, skip to next item
1712  continue
1713 
1714  start_of_clip = round(float(clip.data["start"]) * fps_float) + 1
1715  end_of_clip = round(float(clip.data["end"]) * fps_float) + 1
1716 
1717  # Determine the beginning and ending of this animation
1718  # ["Start of Clip", "End of Clip", "Entire Clip"]
1719  start_animation = start_of_clip
1720  end_animation = end_of_clip
1721  if position == "Start of Clip" and action in [MENU_FADE_IN_FAST, MENU_FADE_OUT_FAST]:
1722  start_animation = start_of_clip
1723  end_animation = min(start_of_clip + (1.0 * fps_float), end_of_clip)
1724  elif position == "Start of Clip" and action in [MENU_FADE_IN_SLOW, MENU_FADE_OUT_SLOW]:
1725  start_animation = start_of_clip
1726  end_animation = min(start_of_clip + (3.0 * fps_float), end_of_clip)
1727  elif position == "End of Clip" and action in [MENU_FADE_IN_FAST, MENU_FADE_OUT_FAST]:
1728  start_animation = max(1.0, end_of_clip - (1.0 * fps_float))
1729  end_animation = end_of_clip
1730  elif position == "End of Clip" and action in [MENU_FADE_IN_SLOW, MENU_FADE_OUT_SLOW]:
1731  start_animation = max(1.0, end_of_clip - (3.0 * fps_float))
1732  end_animation = end_of_clip
1733 
1734  # Fade in and out (special case)
1735  if position == "Entire Clip" and action == MENU_FADE_IN_OUT_FAST:
1736  # Call this method for the start and end of the clip
1737  self.Fade_Triggered(MENU_FADE_IN_FAST, clip_ids, "Start of Clip")
1738  self.Fade_Triggered(MENU_FADE_OUT_FAST, clip_ids, "End of Clip")
1739  return
1740  elif position == "Entire Clip" and action == MENU_FADE_IN_OUT_SLOW:
1741  # Call this method for the start and end of the clip
1742  self.Fade_Triggered(MENU_FADE_IN_SLOW, clip_ids, "Start of Clip")
1743  self.Fade_Triggered(MENU_FADE_OUT_SLOW, clip_ids, "End of Clip")
1744  return
1745 
1746  if action == MENU_FADE_NONE:
1747  # Clear all keyframes
1748  p = openshot.Point(1, 1.0, openshot.BEZIER)
1749  p_object = json.loads(p.Json())
1750  clip.data[prop_name] = { "Points" : [p_object]}
1751 
1752  if action in [MENU_FADE_IN_FAST, MENU_FADE_IN_SLOW]:
1753  # Add keyframes
1754  start = openshot.Point(start_animation, 0.0, openshot.BEZIER)
1755  start_object = json.loads(start.Json())
1756  end = openshot.Point(end_animation, 1.0, openshot.BEZIER)
1757  end_object = json.loads(end.Json())
1758  clip.data[prop_name]["Points"].append(start_object)
1759  clip.data[prop_name]["Points"].append(end_object)
1760 
1761  if action in [MENU_FADE_OUT_FAST, MENU_FADE_OUT_SLOW]:
1762  # Add keyframes
1763  start = openshot.Point(start_animation, 1.0, openshot.BEZIER)
1764  start_object = json.loads(start.Json())
1765  end = openshot.Point(end_animation, 0.0, openshot.BEZIER)
1766  end_object = json.loads(end.Json())
1767  clip.data[prop_name]["Points"].append(start_object)
1768  clip.data[prop_name]["Points"].append(end_object)
1769 
1770  # Save changes
1771  self.update_clip_data(clip.data, only_basic_props=False, ignore_reader=True)
1772 
1773  @pyqtSlot(str, str, float)
1774  ##
1775  # Callback from javascript that the razor tool was clicked
1776  def RazorSliceAtCursor(self, clip_id, trans_id, cursor_position):
1777 
1778  # Determine slice mode (keep both [default], keep left [shift], keep right [ctrl]
1779  slice_mode = MENU_SLICE_KEEP_BOTH
1780  if int(QCoreApplication.instance().keyboardModifiers() & Qt.ControlModifier) > 0:
1781  slice_mode = MENU_SLICE_KEEP_RIGHT
1782  elif int(QCoreApplication.instance().keyboardModifiers() & Qt.ShiftModifier) > 0:
1783  slice_mode = MENU_SLICE_KEEP_LEFT
1784 
1785  if clip_id:
1786  # Slice clip
1787  QTimer.singleShot(0, partial(self.Slice_Triggered, slice_mode, [clip_id], [], cursor_position))
1788  elif trans_id:
1789  # Slice transitions
1790  QTimer.singleShot(0, partial(self.Slice_Triggered, slice_mode, [], [trans_id], cursor_position))
1791 
1792  ##
1793  # Callback for slice context menus
1794  def Slice_Triggered(self, action, clip_ids, trans_ids, playhead_position=0):
1795  # Get FPS from project
1796  fps = get_app().project.get(["fps"])
1797  fps_num = float(fps["num"])
1798  fps_den = float(fps["den"])
1799  fps_float = fps_num / fps_den
1800  frame_duration = fps_den / fps_num
1801 
1802  # Get the nearest starting frame position to the playhead (this helps to prevent cutting
1803  # in-between frames, and thus less likely to repeat or skip a frame).
1804  playhead_position = float(round((playhead_position * fps_num) / fps_den ) * fps_den ) / fps_num
1805 
1806  # Loop through each clip (using the list of ids)
1807  for clip_id in clip_ids:
1808 
1809  # Get existing clip object
1810  clip = Clip.get(id=clip_id)
1811  if not clip:
1812  # Invalid clip, skip to next item
1813  continue
1814 
1815  # Determine if waveform needs to be redrawn
1816  has_audio_data = clip_id in self.waveform_cache
1817 
1818  if action == MENU_SLICE_KEEP_LEFT or action == MENU_SLICE_KEEP_BOTH:
1819  # Get details of original clip
1820  position_of_clip = float(clip.data["position"])
1821  start_of_clip = float(clip.data["start"])
1822 
1823  # Set new 'end' of clip
1824  clip.data["end"] = start_of_clip + (playhead_position - position_of_clip)
1825 
1826  elif action == MENU_SLICE_KEEP_RIGHT:
1827  # Get details of original clip
1828  position_of_clip = float(clip.data["position"])
1829  start_of_clip = float(clip.data["start"])
1830 
1831  # Set new 'end' of clip
1832  clip.data["position"] = playhead_position
1833  clip.data["start"] = start_of_clip + (playhead_position - position_of_clip)
1834 
1835  # Update thumbnail for right clip (after the clip has been created)
1836  self.UpdateClipThumbnail(clip.data)
1837 
1838  if action == MENU_SLICE_KEEP_BOTH:
1839  # Add the 2nd clip (the right side, since the left side has already been adjusted above)
1840  # Get right side clip object
1841  right_clip = Clip.get(id=clip_id)
1842  if not right_clip:
1843  # Invalid clip, skip to next item
1844  continue
1845 
1846  # Remove the ID property from the clip (so it becomes a new one)
1847  right_clip.id = None
1848  right_clip.type = 'insert'
1849  right_clip.data.pop('id')
1850  right_clip.key.pop(1)
1851 
1852  # Set new 'start' of right_clip (need to bump 1 frame duration more, so we don't repeat a frame)
1853  right_clip.data["position"] = (round(float(playhead_position) * fps_float) + 1) / fps_float
1854  right_clip.data["start"] = (round(float(clip.data["end"]) * fps_float) + 2) / fps_float
1855 
1856  # Save changes
1857  right_clip.save()
1858 
1859  # Update thumbnail for right clip (after the clip has been created)
1860  self.UpdateClipThumbnail(right_clip.data)
1861 
1862  # Save changes again (with new thumbnail)
1863  self.update_clip_data(right_clip.data, only_basic_props=False, ignore_reader=True)
1864 
1865  if has_audio_data:
1866  # Add right clip audio to cache
1867  self.waveform_cache[right_clip.id] = self.waveform_cache.get(clip_id, '[]')
1868 
1869  # Pass audio to javascript timeline (and render)
1870  cmd = JS_SCOPE_SELECTOR + ".setAudioData('" + right_clip.id + "', " + self.waveform_cache.get(right_clip.id) + ");"
1871  self.page().mainFrame().evaluateJavaScript(cmd)
1872 
1873  # Save changes
1874  self.update_clip_data(clip.data, only_basic_props=False, ignore_reader=True)
1875 
1876  # Start timer to redraw audio waveforms
1877  self.redraw_audio_timer.start()
1878 
1879  # Loop through each transition (using the list of ids)
1880  for trans_id in trans_ids:
1881  # Get existing transition object
1882  trans = Transition.get(id=trans_id)
1883  if not trans:
1884  # Invalid transition, skip to next item
1885  continue
1886 
1887  if action == MENU_SLICE_KEEP_LEFT or action == MENU_SLICE_KEEP_BOTH:
1888  # Get details of original transition
1889  position_of_tran = float(trans.data["position"])
1890 
1891  # Set new 'end' of transition
1892  trans.data["end"] = playhead_position - position_of_tran
1893 
1894  elif action == MENU_SLICE_KEEP_RIGHT:
1895  # Get details of transition clip
1896  position_of_tran = float(trans.data["position"])
1897  end_of_tran = float(trans.data["end"])
1898 
1899  # Set new 'end' of transition
1900  trans.data["position"] = playhead_position
1901  trans.data["end"] = end_of_tran - (playhead_position - position_of_tran)
1902 
1903  if action == MENU_SLICE_KEEP_BOTH:
1904  # Add the 2nd transition (the right side, since the left side has already been adjusted above)
1905  # Get right side transition object
1906  right_tran = Transition.get(id=trans_id)
1907  if not right_tran:
1908  # Invalid transition, skip to next item
1909  continue
1910 
1911  # Remove the ID property from the transition (so it becomes a new one)
1912  right_tran.id = None
1913  right_tran.type = 'insert'
1914  right_tran.data.pop('id')
1915  right_tran.key.pop(1)
1916 
1917  # Get details of original transition
1918  position_of_tran = float(right_tran.data["position"])
1919  end_of_tran = float(right_tran.data["end"])
1920 
1921  # Set new 'end' of right_tran
1922  right_tran.data["position"] = playhead_position + frame_duration
1923  right_tran.data["end"] = end_of_tran - (playhead_position - position_of_tran) + frame_duration
1924 
1925  # Save changes
1926  right_tran.save()
1927 
1928  # Save changes again (right side)
1929  self.update_transition_data(right_tran.data, only_basic_props=False)
1930 
1931  # Save changes (left side)
1932  self.update_transition_data(trans.data, only_basic_props=False)
1933 
1934  ##
1935  # Callback for volume context menus
1936  def Volume_Triggered(self, action, clip_ids, position="Entire Clip"):
1937  log.info(action)
1938  prop_name = "volume"
1939 
1940  # Get FPS from project
1941  fps = get_app().project.get(["fps"])
1942  fps_float = float(fps["num"]) / float(fps["den"])
1943 
1944  # Loop through each selected clip
1945  for clip_id in clip_ids:
1946 
1947  # Get existing clip object
1948  clip = Clip.get(id=clip_id)
1949  if not clip:
1950  # Invalid clip, skip to next item
1951  continue
1952 
1953  start_of_clip = round(float(clip.data["start"]) * fps_float) + 1
1954  end_of_clip = round(float(clip.data["end"]) * fps_float) + 1
1955 
1956  # Determine the beginning and ending of this animation
1957  # ["Start of Clip", "End of Clip", "Entire Clip"]
1958  start_animation = start_of_clip
1959  end_animation = end_of_clip
1960  if position == "Start of Clip" and action in [MENU_VOLUME_FADE_IN_FAST, MENU_VOLUME_FADE_OUT_FAST]:
1961  start_animation = start_of_clip
1962  end_animation = min(start_of_clip + (1.0 * fps_float), end_of_clip)
1963  elif position == "Start of Clip" and action in [MENU_VOLUME_FADE_IN_SLOW, MENU_VOLUME_FADE_OUT_SLOW]:
1964  start_animation = start_of_clip
1965  end_animation = min(start_of_clip + (3.0 * fps_float), end_of_clip)
1966  elif position == "End of Clip" and action in [MENU_VOLUME_FADE_IN_FAST, MENU_VOLUME_FADE_OUT_FAST]:
1967  start_animation = max(1.0, end_of_clip - (1.0 * fps_float))
1968  end_animation = end_of_clip
1969  elif position == "End of Clip" and action in [MENU_VOLUME_FADE_IN_SLOW, MENU_VOLUME_FADE_OUT_SLOW]:
1970  start_animation = max(1.0, end_of_clip - (3.0 * fps_float))
1971  end_animation = end_of_clip
1972  elif position == "Start of Clip":
1973  # Only used when setting levels (a single keyframe)
1974  start_animation = start_of_clip
1975  end_animation = start_of_clip
1976  elif position == "End of Clip":
1977  # Only used when setting levels (a single keyframe)
1978  start_animation = end_of_clip
1979  end_animation = end_of_clip
1980 
1981  # Fade in and out (special case)
1982  if position == "Entire Clip" and action == MENU_VOLUME_FADE_IN_OUT_FAST:
1983  # Call this method for the start and end of the clip
1984  self.Volume_Triggered(MENU_VOLUME_FADE_IN_FAST, clip_ids, "Start of Clip")
1985  self.Volume_Triggered(MENU_VOLUME_FADE_OUT_FAST, clip_ids, "End of Clip")
1986  return
1987  elif position == "Entire Clip" and action == MENU_VOLUME_FADE_IN_OUT_SLOW:
1988  # Call this method for the start and end of the clip
1989  self.Volume_Triggered(MENU_VOLUME_FADE_IN_SLOW, clip_ids, "Start of Clip")
1990  self.Volume_Triggered(MENU_VOLUME_FADE_OUT_SLOW, clip_ids, "End of Clip")
1991  return
1992 
1993  if action == MENU_VOLUME_NONE:
1994  # Clear all keyframes
1995  p = openshot.Point(1, 1.0, openshot.BEZIER)
1996  p_object = json.loads(p.Json())
1997  clip.data[prop_name] = { "Points" : [p_object]}
1998 
1999  if action in [MENU_VOLUME_FADE_IN_FAST, MENU_VOLUME_FADE_IN_SLOW]:
2000  # Add keyframes
2001  start = openshot.Point(start_animation, 0.0, openshot.BEZIER)
2002  start_object = json.loads(start.Json())
2003  end = openshot.Point(end_animation, 1.0, openshot.BEZIER)
2004  end_object = json.loads(end.Json())
2005  clip.data[prop_name]["Points"].append(start_object)
2006  clip.data[prop_name]["Points"].append(end_object)
2007 
2008  if action in [MENU_VOLUME_FADE_OUT_FAST, MENU_VOLUME_FADE_OUT_SLOW]:
2009  # Add keyframes
2010  start = openshot.Point(start_animation, 1.0, openshot.BEZIER)
2011  start_object = json.loads(start.Json())
2012  end = openshot.Point(end_animation, 0.0, openshot.BEZIER)
2013  end_object = json.loads(end.Json())
2014  clip.data[prop_name]["Points"].append(start_object)
2015  clip.data[prop_name]["Points"].append(end_object)
2016 
2017  if action in [MENU_VOLUME_LEVEL_100, MENU_VOLUME_LEVEL_90, MENU_VOLUME_LEVEL_80, MENU_VOLUME_LEVEL_70,
2018  MENU_VOLUME_LEVEL_60, MENU_VOLUME_LEVEL_50, MENU_VOLUME_LEVEL_40, MENU_VOLUME_LEVEL_30,
2019  MENU_VOLUME_LEVEL_20, MENU_VOLUME_LEVEL_10, MENU_VOLUME_LEVEL_0]:
2020  # Add keyframes
2021  p = openshot.Point(start_animation, float(action) / 100.0, openshot.BEZIER)
2022  p_object = json.loads(p.Json())
2023  clip.data[prop_name]["Points"].append(p_object)
2024 
2025  # Save changes
2026  self.update_clip_data(clip.data, only_basic_props=False, ignore_reader=True)
2027 
2028  # Determine if waveform needs to be redrawn
2029  has_audio_data = bool(self.eval_js(JS_SCOPE_SELECTOR + ".hasAudioData('" + clip.id + "');"))
2030  if has_audio_data:
2031  # Re-generate waveform since volume curve has changed
2032  self.Show_Waveform_Triggered(clip.id)
2033 
2034  ##
2035  # Callback for rotate context menus
2036  def Rotate_Triggered(self, action, clip_ids, position="Start of Clip"):
2037  log.info(action)
2038  prop_name = "rotation"
2039 
2040  # Get FPS from project
2041  fps = get_app().project.get(["fps"])
2042  fps_float = float(fps["num"]) / float(fps["den"])
2043 
2044  # Loop through each selected clip
2045  for clip_id in clip_ids:
2046 
2047  # Get existing clip object
2048  clip = Clip.get(id=clip_id)
2049  if not clip:
2050  # Invalid clip, skip to next item
2051  continue
2052 
2053  if action == MENU_ROTATE_NONE:
2054  # Clear all keyframes
2055  p = openshot.Point(1, 0.0, openshot.BEZIER)
2056  p_object = json.loads(p.Json())
2057  clip.data[prop_name] = { "Points" : [p_object]}
2058 
2059  if action == MENU_ROTATE_90_RIGHT:
2060  # Add keyframes
2061  p = openshot.Point(1, 90.0, openshot.BEZIER)
2062  p_object = json.loads(p.Json())
2063  clip.data[prop_name] = { "Points" : [p_object]}
2064 
2065  if action == MENU_ROTATE_90_LEFT:
2066  # Add keyframes
2067  p = openshot.Point(1, -90.0, openshot.BEZIER)
2068  p_object = json.loads(p.Json())
2069  clip.data[prop_name] = { "Points" : [p_object]}
2070 
2071  if action == MENU_ROTATE_180_FLIP:
2072  # Add keyframes
2073  p = openshot.Point(1, 180.0, openshot.BEZIER)
2074  p_object = json.loads(p.Json())
2075  clip.data[prop_name] = { "Points" : [p_object]}
2076 
2077  # Save changes
2078  self.update_clip_data(clip.data, only_basic_props=False, ignore_reader=True)
2079 
2080  ##
2081  # Callback for rotate context menus
2082  def Time_Triggered(self, action, clip_ids, speed="1X", playhead_position=0.0):
2083  log.info(action)
2084  prop_name = "time"
2085 
2086  # Get FPS from project
2087  fps = get_app().project.get(["fps"])
2088  fps_float = float(fps["num"]) / float(fps["den"])
2089 
2090  # Loop through each selected clip
2091  for clip_id in clip_ids:
2092 
2093  # Get existing clip object
2094  clip = Clip.get(id=clip_id)
2095  if not clip:
2096  # Invalid clip, skip to next item
2097  continue
2098 
2099  # Keep original 'end' and 'duration'
2100  if "original_data" not in clip.data.keys():
2101  clip.data["original_data"] = {"end": clip.data["end"],
2102  "duration": clip.data["duration"],
2103  "video_length": clip.data["reader"]["video_length"]}
2104 
2105  # Determine the beginning and ending of this animation
2106  start_animation = 1
2107 
2108  # Freeze or Speed?
2109  if action in [MENU_TIME_FREEZE, MENU_TIME_FREEZE_ZOOM]:
2110  # Get freeze details
2111  freeze_seconds = float(speed)
2112 
2113  original_duration = clip.data["duration"]
2114  if "original_data" in clip.data.keys():
2115  original_duration = clip.data["original_data"]["duration"]
2116 
2117  print('ORIGINAL DURATION: %s' % original_duration)
2118  print(clip.data)
2119 
2120  # Extend end & duration (due to freeze)
2121  clip.data["end"] = float(clip.data["end"]) + freeze_seconds
2122  clip.data["duration"] = float(clip.data["duration"]) + freeze_seconds
2123  clip.data["reader"]["video_length"] = float(clip.data["reader"]["video_length"]) + freeze_seconds
2124 
2125  # Determine start frame from position
2126  freeze_length_frames = round(freeze_seconds * fps_float) + 1
2127  start_animation_seconds = float(clip.data["start"]) + (playhead_position - float(clip.data["position"]))
2128  start_animation_frames = round(start_animation_seconds * fps_float) + 1
2129  start_animation_frames_value = start_animation_frames
2130  end_animation_seconds = start_animation_seconds + freeze_seconds
2131  end_animation_frames = round(end_animation_seconds * fps_float) + 1
2132  end_of_clip_seconds = float(clip.data["duration"])
2133  end_of_clip_frames = round((end_of_clip_seconds) * fps_float) + 1
2134  end_of_clip_frames_value = round((original_duration) * fps_float) + 1
2135 
2136  # Determine volume start and end
2137  start_volume_value = 1.0
2138 
2139  # Do we already have a time curve? Look up intersecting frame # from time curve
2140  if len(clip.data["time"]["Points"]) > 1:
2141  # Delete last time point (which should be the end of the clip). We have a new end of the clip
2142  # after inserting this freeze.
2143  del clip.data["time"]["Points"][-1]
2144 
2145  # Find actual clip object from libopenshot
2146  c = None
2147  clips = get_app().window.timeline_sync.timeline.Clips()
2148  for clip_object in clips:
2149  if clip_object.Id() == clip_id:
2150  c = clip_object
2151  break
2152  if c:
2153  # Look up correct position from time curve
2154  start_animation_frames_value = c.time.GetLong(start_animation_frames)
2155 
2156  # Do we already have a volume curve? Look up intersecting frame # from volume curve
2157  if len(clip.data["volume"]["Points"]) > 1:
2158  # Find actual clip object from libopenshot
2159  c = None
2160  clips = get_app().window.timeline_sync.timeline.Clips()
2161  for clip_object in clips:
2162  if clip_object.Id() == clip_id:
2163  c = clip_object
2164  break
2165  if c:
2166  # Look up correct volume from time curve
2167  start_volume_value = c.volume.GetValue(start_animation_frames)
2168 
2169  # Create Time Freeze keyframe points
2170  p = openshot.Point(start_animation_frames, start_animation_frames_value, openshot.LINEAR)
2171  p_object = json.loads(p.Json())
2172  clip.data[prop_name]["Points"].append(p_object)
2173  p1 = openshot.Point(end_animation_frames, start_animation_frames_value, openshot.LINEAR)
2174  p1_object = json.loads(p1.Json())
2175  clip.data[prop_name]["Points"].append(p1_object)
2176  p2 = openshot.Point(end_of_clip_frames, end_of_clip_frames_value, openshot.LINEAR)
2177  p2_object = json.loads(p2.Json())
2178  clip.data[prop_name]["Points"].append(p2_object)
2179 
2180  # Create Volume mute keyframe points (so the freeze is silent)
2181  p = openshot.Point(start_animation_frames - 1, start_volume_value, openshot.LINEAR)
2182  p_object = json.loads(p.Json())
2183  clip.data['volume']["Points"].append(p_object)
2184  p = openshot.Point(start_animation_frames, 0.0, openshot.LINEAR)
2185  p_object = json.loads(p.Json())
2186  clip.data['volume']["Points"].append(p_object)
2187  p2 = openshot.Point(end_animation_frames - 1, 0.0, openshot.LINEAR)
2188  p2_object = json.loads(p2.Json())
2189  clip.data['volume']["Points"].append(p2_object)
2190  p3 = openshot.Point(end_animation_frames, start_volume_value, openshot.LINEAR)
2191  p3_object = json.loads(p3.Json())
2192  clip.data['volume']["Points"].append(p3_object)
2193 
2194  # Create zoom keyframe points
2195  if action == MENU_TIME_FREEZE_ZOOM:
2196  p = openshot.Point(start_animation_frames, 1.0, openshot.BEZIER)
2197  p_object = json.loads(p.Json())
2198  clip.data['scale_x']["Points"].append(p_object)
2199  p = openshot.Point(start_animation_frames, 1.0, openshot.BEZIER)
2200  p_object = json.loads(p.Json())
2201  clip.data['scale_y']["Points"].append(p_object)
2202 
2203  diff_halfed = (end_animation_frames - start_animation_frames) / 2.0
2204  p1 = openshot.Point(start_animation_frames + diff_halfed, 1.05, openshot.BEZIER)
2205  p1_object = json.loads(p1.Json())
2206  clip.data['scale_x']["Points"].append(p1_object)
2207  p1 = openshot.Point(start_animation_frames + diff_halfed, 1.05, openshot.BEZIER)
2208  p1_object = json.loads(p1.Json())
2209  clip.data['scale_y']["Points"].append(p1_object)
2210 
2211  p1 = openshot.Point(end_animation_frames, 1.0, openshot.BEZIER)
2212  p1_object = json.loads(p1.Json())
2213  clip.data['scale_x']["Points"].append(p1_object)
2214  p1 = openshot.Point(end_animation_frames, 1.0, openshot.BEZIER)
2215  p1_object = json.loads(p1.Json())
2216  clip.data['scale_y']["Points"].append(p1_object)
2217 
2218  else:
2219 
2220  # Calculate speed factor
2221  speed_label = speed.replace('X', '')
2222  speed_parts = speed_label.split('/')
2223  even_multiple = 1
2224  if len(speed_parts) == 2:
2225  speed_factor = float(speed_parts[0]) / float(speed_parts[1])
2226  even_multiple = int(speed_parts[1])
2227  else:
2228  speed_factor = float(speed_label)
2229  even_multiple = int(speed_factor)
2230 
2231  # Clear all keyframes
2232  p = openshot.Point(start_animation, 0.0, openshot.LINEAR)
2233  p_object = json.loads(p.Json())
2234  clip.data[prop_name] = { "Points" : [p_object]}
2235 
2236  # Reset original end & duration (if available)
2237  if "original_data" in clip.data.keys():
2238  clip.data["end"] = clip.data["original_data"]["end"]
2239  clip.data["duration"] = clip.data["original_data"]["duration"]
2240  clip.data["reader"]["video_length"] = clip.data["original_data"]["video_length"]
2241  clip.data.pop("original_data")
2242 
2243  # Get the ending frame
2244  end_of_clip = round(float(clip.data["end"]) * fps_float) + 1
2245 
2246  # Determine the beginning and ending of this animation
2247  start_animation = round(float(clip.data["start"]) * fps_float) + 1
2248  duration_animation = self.round_to_multiple(end_of_clip - start_animation, even_multiple)
2249  end_animation = start_animation + duration_animation
2250 
2251  if action == MENU_TIME_FORWARD:
2252  # Add keyframes
2253  start = openshot.Point(start_animation, start_animation, openshot.LINEAR)
2254  start_object = json.loads(start.Json())
2255  clip.data[prop_name] = { "Points" : [start_object]}
2256  end = openshot.Point(start_animation + (duration_animation / speed_factor), end_animation, openshot.LINEAR)
2257  end_object = json.loads(end.Json())
2258  clip.data[prop_name]["Points"].append(end_object)
2259 
2260  # Adjust end & duration
2261  clip.data["end"] = (start_animation + (duration_animation / speed_factor)) / fps_float
2262  clip.data["duration"] = self.round_to_multiple(clip.data["duration"] / speed_factor, even_multiple)
2263  clip.data["reader"]["video_length"] = str(self.round_to_multiple(float(clip.data["reader"]["video_length"]) / speed_factor, even_multiple))
2264 
2265  if action == MENU_TIME_BACKWARD:
2266  # Add keyframes
2267  start = openshot.Point(start_animation, end_animation, openshot.LINEAR)
2268  start_object = json.loads(start.Json())
2269  clip.data[prop_name] = { "Points" : [start_object]}
2270  end = openshot.Point(start_animation + (duration_animation / speed_factor), start_animation, openshot.LINEAR)
2271  end_object = json.loads(end.Json())
2272  clip.data[prop_name]["Points"].append(end_object)
2273 
2274  # Adjust end & duration
2275  clip.data["end"] = (start_animation + (duration_animation / speed_factor)) / fps_float
2276  clip.data["duration"] = self.round_to_multiple(clip.data["duration"] / speed_factor, even_multiple)
2277  clip.data["reader"]["video_length"] = str(self.round_to_multiple(float(clip.data["reader"]["video_length"]) / speed_factor, even_multiple))
2278 
2279  # Save changes
2280  self.update_clip_data(clip.data, only_basic_props=False, ignore_reader=True)
2281 
2282  ##
2283  # Round this to the closest multiple of a given #
2284  def round_to_multiple(self, number, multiple):
2285  return number - (number % multiple)
2286 
2287  ##
2288  # Show all clips at the same time (arranged col by col, row by row)
2289  def show_all_clips(self, clip, stretch=False):
2290  from math import sqrt
2291 
2292  # Get list of nearby clips
2293  available_clips = []
2294  start_position = float(clip.data["position"])
2295  for c in Clip.filter():
2296  if float(c.data["position"]) >= (start_position - 0.5) and float(c.data["position"]) <= (start_position + 0.5):
2297  # add to list
2298  available_clips.append(c)
2299 
2300  # Get the number of rows
2301  number_of_clips = len(available_clips)
2302  number_of_rows = int(sqrt(number_of_clips))
2303  max_clips_on_row = float(number_of_clips) / float(number_of_rows)
2304 
2305  # Determine how many clips per row
2306  if max_clips_on_row > float(int(max_clips_on_row)):
2307  max_clips_on_row = int(max_clips_on_row + 1)
2308  else:
2309  max_clips_on_row = int(max_clips_on_row)
2310 
2311  # Calculate Height & Width
2312  height = 1.0 / float(number_of_rows)
2313  width = 1.0 / float(max_clips_on_row)
2314 
2315  clip_index = 0
2316 
2317  # Loop through each row of clips
2318  for row in range(0, number_of_rows):
2319 
2320  # Loop through clips on this row
2321  column_string = " - - - "
2322  for col in range(0, max_clips_on_row):
2323  if clip_index < number_of_clips:
2324  # Calculate X & Y
2325  X = float(col) * width
2326  Y = float(row) * height
2327 
2328  # Modify clip layout settings
2329  selected_clip = available_clips[clip_index]
2330  selected_clip.data["gravity"] = openshot.GRAVITY_TOP_LEFT
2331 
2332  if stretch:
2333  selected_clip.data["scale"] = openshot.SCALE_STRETCH
2334  else:
2335  selected_clip.data["scale"] = openshot.SCALE_FIT
2336 
2337  # Set scale keyframes
2338  w = openshot.Point(1, width, openshot.BEZIER)
2339  w_object = json.loads(w.Json())
2340  selected_clip.data["scale_x"] = { "Points" : [w_object]}
2341  h = openshot.Point(1, height, openshot.BEZIER)
2342  h_object = json.loads(h.Json())
2343  selected_clip.data["scale_y"] = { "Points" : [h_object]}
2344  x_point = openshot.Point(1, X, openshot.BEZIER)
2345  x_object = json.loads(x_point.Json())
2346  selected_clip.data["location_x"] = { "Points" : [x_object]}
2347  y_point = openshot.Point(1, Y, openshot.BEZIER)
2348  y_object = json.loads(y_point.Json())
2349  selected_clip.data["location_y"] = { "Points" : [y_object]}
2350 
2351  log.info('Updating clip id: %s' % selected_clip.data["id"])
2352  log.info('width: %s, height: %s' % (width, height))
2353 
2354  # Increment Clip Index
2355  clip_index += 1
2356 
2357  # Save changes
2358  self.update_clip_data(selected_clip.data, only_basic_props=False, ignore_reader=True)
2359 
2360  ##
2361  # Callback for reversing a transition
2362  def Reverse_Transition_Triggered(self, tran_ids):
2363  log.info("Reverse_Transition_Triggered")
2364 
2365  # Loop through all selected transitions
2366  for tran_id in tran_ids:
2367 
2368  # Get existing clip object
2369  tran = Transition.get(id=tran_id)
2370  if not tran:
2371  # Invalid transition, skip to next item
2372  continue
2373 
2374  # Loop through brightness keyframes
2375  tran_data_copy = deepcopy(tran.data)
2376  new_index = len(tran.data["brightness"]["Points"])
2377  for point in tran.data["brightness"]["Points"]:
2378  new_index -= 1
2379  tran_data_copy["brightness"]["Points"][new_index]["co"]["Y"] = point["co"]["Y"]
2380  if "handle_left" in point:
2381  tran_data_copy["brightness"]["Points"][new_index]["handle_left"]["Y"] = point["handle_left"]["Y"]
2382  tran_data_copy["brightness"]["Points"][new_index]["handle_right"]["Y"] = point["handle_right"]["Y"]
2383 
2384  # Save changes
2385  self.update_transition_data(tran_data_copy, only_basic_props=False)
2386 
2387  @pyqtSlot(str)
2388  def ShowTransitionMenu(self, tran_id=None):
2389  log.info('ShowTransitionMenu: %s' % tran_id)
2390 
2391  # Get translation method
2392  _ = get_app()._tr
2393 
2394  # Get existing transition object
2395  tran = Transition.get(id=tran_id)
2396  if not tran:
2397  # Not a valid transition id
2398  return
2399 
2400  # Set the selected transition (if needed)
2401  if tran_id not in self.window.selected_transitions:
2402  self.window.addSelection(tran_id, 'transition')
2403  # Get list of all selected transitions
2404  tran_ids = self.window.selected_transitions
2405  clip_ids = self.window.selected_clips
2406 
2407  # Get framerate
2408  fps = get_app().project.get(["fps"])
2409  fps_float = float(fps["num"]) / float(fps["den"])
2410 
2411  # Get playhead position
2412  playhead_position = float(self.window.preview_thread.current_frame) / fps_float
2413 
2414  menu = QMenu(self)
2415 
2416  # Copy Menu
2417  if len(tran_ids) + len(clip_ids) > 1:
2418  # Copy All Menu (Clips and/or transitions are selected)
2419  Copy_All = menu.addAction(_("Copy"))
2420  Copy_All.setShortcut(QKeySequence(self.window.getShortcutByName("copyAll")))
2421  Copy_All.triggered.connect(partial(self.Copy_Triggered, MENU_COPY_ALL, clip_ids, tran_ids))
2422  else:
2423  # Only a single transitions is selected (show normal transition copy menu)
2424  Copy_Menu = QMenu(_("Copy"), self)
2425  Copy_Tran = Copy_Menu.addAction(_("Transition"))
2426  Copy_Tran.setShortcut(QKeySequence(self.window.getShortcutByName("copyAll")))
2427  Copy_Tran.triggered.connect(partial(self.Copy_Triggered, MENU_COPY_TRANSITION, [], [tran_id]))
2428 
2429  Keyframe_Menu = QMenu(_("Keyframes"), self)
2430  Copy_Keyframes_All = Keyframe_Menu.addAction(_("All"))
2431  Copy_Keyframes_All.triggered.connect(partial(self.Copy_Triggered, MENU_COPY_KEYFRAMES_ALL, [], [tran_id]))
2432  Keyframe_Menu.addSeparator()
2433  Copy_Keyframes_Brightness = Keyframe_Menu.addAction(_("Brightness"))
2434  Copy_Keyframes_Brightness.triggered.connect(partial(self.Copy_Triggered, MENU_COPY_KEYFRAMES_BRIGHTNESS, [], [tran_id]))
2435  Copy_Keyframes_Scale = Keyframe_Menu.addAction(_("Contrast"))
2436  Copy_Keyframes_Scale.triggered.connect(partial(self.Copy_Triggered, MENU_COPY_KEYFRAMES_CONTRAST, [], [tran_id]))
2437 
2438  # Only show copy->keyframe if a single transitions is selected
2439  Copy_Menu.addMenu(Keyframe_Menu)
2440  menu.addMenu(Copy_Menu)
2441 
2442  # Get list of clipboard items (that are complete clips or transitions)
2443  # i.e. ignore partial clipboard items (keyframes / effects / etc...)
2444  clipboard_clip_ids = [k for k, v in self.copy_clipboard.items() if v.get('id')]
2445  clipboard_tran_ids = [k for k, v in self.copy_transition_clipboard.items() if v.get('id')]
2446  # Determine if the paste menu should be shown
2447  if self.copy_transition_clipboard and len(clipboard_clip_ids) + len(clipboard_tran_ids) == 0:
2448  # Paste Menu (Only show when partial transition clipboard available)
2449  Paste_Tran = menu.addAction(_("Paste"))
2450  Paste_Tran.triggered.connect(partial(self.Paste_Triggered, MENU_PASTE, 0.0, 0, [], tran_ids))
2451 
2452  menu.addSeparator()
2453 
2454  # Alignment Menu (if multiple selections)
2455  if len(clip_ids) > 1:
2456  Alignment_Menu = QMenu(_("Align"), self)
2457  Align_Left = Alignment_Menu.addAction(_("Left"))
2458  Align_Left.triggered.connect(partial(self.Align_Triggered, MENU_ALIGN_LEFT, clip_ids, tran_ids))
2459  Align_Right = Alignment_Menu.addAction(_("Right"))
2460  Align_Right.triggered.connect(partial(self.Align_Triggered, MENU_ALIGN_RIGHT, clip_ids, tran_ids))
2461 
2462  # Add menu to parent
2463  menu.addMenu(Alignment_Menu)
2464 
2465  # If Playhead overlapping transition
2466  if tran:
2467  start_of_tran = float(tran.data["start"])
2468  end_of_tran = float(tran.data["end"])
2469  position_of_tran = float(tran.data["position"])
2470  if playhead_position >= position_of_tran and playhead_position <= (position_of_tran + (end_of_tran - start_of_tran)):
2471  # Add split transition menu
2472  Slice_Menu = QMenu(_("Slice"), self)
2473  Slice_Keep_Both = Slice_Menu.addAction(_("Keep Both Sides"))
2474  Slice_Keep_Both.triggered.connect(partial(self.Slice_Triggered, MENU_SLICE_KEEP_BOTH, [], [tran_id], playhead_position))
2475  Slice_Keep_Left = Slice_Menu.addAction(_("Keep Left Side"))
2476  Slice_Keep_Left.triggered.connect(partial(self.Slice_Triggered, MENU_SLICE_KEEP_LEFT, [], [tran_id], playhead_position))
2477  Slice_Keep_Right = Slice_Menu.addAction(_("Keep Right Side"))
2478  Slice_Keep_Right.triggered.connect(partial(self.Slice_Triggered, MENU_SLICE_KEEP_RIGHT, [], [tran_id], playhead_position))
2479  menu.addMenu(Slice_Menu)
2480 
2481  # Reverse Transition menu
2482  Reverse_Transition = menu.addAction(_("Reverse Transition"))
2483  Reverse_Transition.triggered.connect(partial(self.Reverse_Transition_Triggered, tran_ids))
2484 
2485  # Properties
2486  menu.addSeparator()
2487  menu.addAction(self.window.actionProperties)
2488 
2489  # Remove transition menu
2490  menu.addSeparator()
2491  menu.addAction(self.window.actionRemoveTransition)
2492 
2493  # Show menu
2494  return menu.popup(QCursor.pos())
2495 
2496  @pyqtSlot(str)
2497  def ShowTrackMenu(self, layer_id=None):
2498  log.info('ShowTrackMenu: %s' % layer_id)
2499 
2500  if layer_id not in self.window.selected_tracks:
2501  self.window.selected_tracks = [layer_id]
2502 
2503  # Get track object
2504  track = Track.get(id=layer_id)
2505  locked = track.data.get("lock", False)
2506 
2507  menu = QMenu(self)
2508  menu.addAction(self.window.actionAddTrackAbove)
2509  menu.addAction(self.window.actionAddTrackBelow)
2510  menu.addAction(self.window.actionRenameTrack)
2511  if locked:
2512  menu.addAction(self.window.actionUnlockTrack)
2513  self.window.actionRemoveTrack.setEnabled(False)
2514  else:
2515  menu.addAction(self.window.actionLockTrack)
2516  self.window.actionRemoveTrack.setEnabled(True)
2517  menu.addSeparator()
2518  menu.addAction(self.window.actionRemoveTrack)
2519  return menu.popup(QCursor.pos())
2520 
2521  @pyqtSlot(str)
2522  def ShowMarkerMenu(self, marker_id=None):
2523  log.info('ShowMarkerMenu: %s' % marker_id)
2524 
2525  if marker_id not in self.window.selected_markers:
2526  self.window.selected_markers = [marker_id]
2527 
2528  menu = QMenu(self)
2529  menu.addAction(self.window.actionRemoveMarker)
2530  return menu.popup(QCursor.pos())
2531 
2532  @pyqtSlot(str, int)
2533  def PreviewClipFrame(self, clip_id, frame_number):
2534 
2535  # Get existing clip object
2536  clip = Clip.get(id=clip_id)
2537  if not clip:
2538  # Invalid clip
2539  return
2540 
2541  preview_path = clip.data['reader']['path']
2542 
2543  # Adjust frame # to valid range
2544  frame_number = max(frame_number, 1)
2545  frame_number = min(frame_number, int(clip.data['reader']['video_length']))
2546 
2547  # Load the clip into the Player (ignored if this has already happened)
2548  self.window.LoadFileSignal.emit(preview_path)
2549  self.window.SpeedSignal.emit(0)
2550 
2551  # Seek to frame
2552  self.window.SeekSignal.emit(frame_number)
2553 
2554  @pyqtSlot(float, int, str)
2555  def PlayheadMoved(self, position_seconds, position_frames, time_code):
2556 
2557  # Load the timeline into the Player (ignored if this has already happened)
2558  self.window.LoadFileSignal.emit('')
2559 
2560  if self.last_position_frames != position_frames:
2561  # Update time code (to prevent duplicate previews)
2562  self.last_position_frames = position_frames
2563 
2564  # Notify main window of current frame
2565  self.window.previewFrame(position_seconds, position_frames, time_code)
2566 
2567  @pyqtSlot(int)
2568  ##
2569  # Move the playhead since the position has changed inside OpenShot (probably due to the video player)
2570  def movePlayhead(self, position_frames):
2571 
2572  # Get access to timeline scope and set scale to zoom slider value (passed in)
2573  code = JS_SCOPE_SELECTOR + ".MovePlayheadToFrame(" + str(position_frames) + ");"
2574  self.eval_js(code)
2575 
2576  @pyqtSlot(int)
2577  ##
2578  # Enable / Disable snapping mode
2579  def SetSnappingMode(self, enable_snapping):
2580 
2581  # Init snapping state (1 = snapping, 0 = no snapping)
2582  self.eval_js(JS_SCOPE_SELECTOR + ".SetSnappingMode(%s);" % int(enable_snapping))
2583 
2584  @pyqtSlot(int)
2585  ##
2586  # Enable / Disable razor mode
2587  def SetRazorMode(self, enable_razor):
2588 
2589  # Init razor state (1 = razor, 0 = no razor)
2590  self.eval_js(JS_SCOPE_SELECTOR + ".SetRazorMode(%s);" % int(enable_razor))
2591 
2592  @pyqtSlot(str, str, bool)
2593  ##
2594  # Add the selected item to the current selection
2595  def addSelection(self, item_id, item_type, clear_existing=False):
2596 
2597  # Add to main window
2598  self.window.addSelection(item_id, item_type, clear_existing)
2599 
2600  @pyqtSlot(str, str)
2601  ##
2602  # Remove the selected clip from the selection
2603  def removeSelection(self, item_id, item_type):
2604 
2605  # Remove from main window
2606  self.window.removeSelection(item_id, item_type)
2607 
2608  @pyqtSlot(str)
2609  def qt_log(self, message=None):
2610  log.info(message)
2611 
2612  # Handle changes to zoom level, update js
2613  def update_zoom(self, newValue):
2614  _ = get_app()._tr
2615 
2616  # Convert slider value (passed in) to a scale (in seconds)
2617  newScale = zoomToSeconds(newValue)
2618 
2619  # Set zoom label
2620  self.window.zoomScaleLabel.setText(_("{} seconds").format(newScale))
2621 
2622  # Determine X coordinate of cursor (to center zoom on)
2623  cursor_y = self.mapFromGlobal(self.cursor().pos()).y()
2624  if cursor_y >= 0:
2625  cursor_x = self.mapFromGlobal(self.cursor().pos()).x()
2626  else:
2627  cursor_x = 0
2628 
2629  # Get access to timeline scope and set scale to new computed value
2630  cmd = JS_SCOPE_SELECTOR + ".setScale(" + str(newScale) + "," + str(cursor_x) + ");"
2631  self.page().mainFrame().evaluateJavaScript(cmd)
2632 
2633  # Start timer to redraw audio
2634  self.redraw_audio_timer.start()
2635 
2636  # Save current zoom
2637  get_app().updates.ignore_history = True
2638  get_app().updates.update(["scale"], newScale)
2639  get_app().updates.ignore_history = False
2640 
2641  ##
2642  # Keypress callback for timeline
2643  def keyPressEvent(self, event):
2644  key_value = event.key()
2645  if (key_value == Qt.Key_Shift or key_value == Qt.Key_Control):
2646 
2647  # Only pass a few keystrokes to the webview (CTRL and SHIFT)
2648  return QWebView.keyPressEvent(self, event)
2649 
2650  else:
2651  # Ignore most keypresses
2652  event.ignore()
2653 
2654  # Capture wheel event to alter zoom slider control
2655  def wheelEvent(self, event):
2656  if int(QCoreApplication.instance().keyboardModifiers() & Qt.ControlModifier) > 0:
2657  # For each 120 (standard scroll unit) adjust the zoom slider
2658  tick_scale = 120
2659  steps = int(event.angleDelta().y() / tick_scale)
2660  self.window.sliderZoom.setValue(self.window.sliderZoom.value() - self.window.sliderZoom.pageStep() * steps)
2661  else:
2662  # Otherwise pass on to implement default functionality (scroll in QWebView)
2663  super(type(self), self).wheelEvent(event)
2664 
2665  def setup_js_data(self):
2666  # Export self as a javascript object in webview
2667  self.page().mainFrame().addToJavaScriptWindowObject('timeline', self)
2668  self.page().mainFrame().addToJavaScriptWindowObject('mainWindow', self.window)
2669 
2670  # Initialize snapping mode
2671  self.SetSnappingMode(self.window.actionSnappingTool.isChecked())
2672 
2673  # An item is being dragged onto the timeline (mouse is entering the timeline now)
2674  def dragEnterEvent(self, event):
2675 
2676  # If a plain text drag accept
2677  if not self.new_item and not event.mimeData().hasUrls() and event.mimeData().html():
2678  # get type of dropped data
2679  self.item_type = event.mimeData().html()
2680 
2681  # Track that a new item is being 'added'
2682  self.new_item = True
2683 
2684  # Get the mime data (i.e. list of files, list of transitions, etc...)
2685  data = json.loads(event.mimeData().text())
2686  pos = event.posF()
2687 
2688  # create the item
2689  if self.item_type == "clip":
2690  self.addClip(data, pos)
2691  elif self.item_type == "transition":
2692  self.addTransition(data, pos)
2693 
2694  # accept all events, even if a new clip is not being added
2695  event.accept()
2696 
2697  # Accept a plain file URL (from the OS)
2698  elif not self.new_item and event.mimeData().hasUrls():
2699  # Track that a new item is being 'added'
2700  self.new_item = True
2701  self.item_type = "os_drop"
2702 
2703  # accept event
2704  event.accept()
2705 
2706  # Add Clip
2707  def addClip(self, data, position):
2708 
2709  # Get app object
2710  app = get_app()
2711 
2712  # Search for matching file in project data (if any)
2713  file_id = data[0]
2714  file = File.get(id=file_id)
2715 
2716  if not file:
2717  # File not found, do nothing
2718  return
2719 
2720  if (file.data["media_type"] == "video" or file.data["media_type"] == "image"):
2721  # Determine thumb path
2722  thumb_path = os.path.join(info.THUMBNAIL_PATH, "%s.png" % file.data["id"])
2723  else:
2724  # Audio file
2725  thumb_path = os.path.join(info.PATH, "images", "AudioThumbnail.png")
2726 
2727  # Get file name
2728  path, filename = os.path.split(file.data["path"])
2729 
2730  # Convert path to the correct relative path (based on this folder)
2731  file_path = file.absolute_path()
2732 
2733  # Create clip object for this file
2734  c = openshot.Clip(file_path)
2735 
2736  # Append missing attributes to Clip JSON
2737  new_clip = json.loads(c.Json())
2738  new_clip["file_id"] = file.id
2739  new_clip["title"] = filename
2740  new_clip["image"] = thumb_path
2741 
2742  # Skip any clips that are missing a 'reader' attribute
2743  # TODO: Determine why this even happens, as it shouldn't be possible
2744  if not new_clip.get("reader"):
2745  return # Do nothing
2746 
2747  # Check for optional start and end attributes
2748  start_frame = 1
2749  end_frame = new_clip["reader"]["duration"]
2750  if 'start' in file.data.keys():
2751  new_clip["start"] = file.data['start']
2752  if 'end' in file.data.keys():
2753  new_clip["end"] = file.data['end']
2754 
2755  # Find the closest track (from javascript)
2756  top_layer = int(self.eval_js(JS_SCOPE_SELECTOR + ".GetJavaScriptTrack(" + str(position.y()) + ");"))
2757  new_clip["layer"] = top_layer
2758 
2759  # Find position from javascript
2760  js_position = self.eval_js(JS_SCOPE_SELECTOR + ".GetJavaScriptPosition(" + str(position.x()) + ");")
2761  new_clip["position"] = js_position
2762 
2763  # Adjust clip duration, start, and end
2764  new_clip["duration"] = new_clip["reader"]["duration"]
2765  if file.data["media_type"] == "image":
2766  new_clip["end"] = self.settings_obj.get("default-image-length") # default to 8 seconds
2767 
2768  # Overwrite frame rate (incase the user changed it in the File Properties)
2769  file_properties_fps = float(file.data["fps"]["num"]) / float(file.data["fps"]["den"])
2770  file_fps = float(new_clip["reader"]["fps"]["num"]) / float(new_clip["reader"]["fps"]["den"])
2771  fps_diff = file_fps / file_properties_fps
2772  new_clip["reader"]["fps"]["num"] = file.data["fps"]["num"]
2773  new_clip["reader"]["fps"]["den"] = file.data["fps"]["den"]
2774  # Scale duration / length / and end properties
2775  new_clip["reader"]["duration"] *= fps_diff
2776  new_clip["end"] *= fps_diff
2777  new_clip["duration"] *= fps_diff
2778 
2779  # Add clip to timeline
2780  self.update_clip_data(new_clip, only_basic_props=False)
2781 
2782  # temp hold item_id
2783  self.item_id = new_clip.get('id')
2784 
2785  # Init javascript bounding box (for snapping support)
2786  code = JS_SCOPE_SELECTOR + ".StartManualMove('" + self.item_type + "', '" + self.item_id + "');"
2787  self.eval_js(code)
2788 
2789  # Resize timeline
2790  @pyqtSlot(float)
2791  ##
2792  # Resize the duration of the timeline
2793  def resizeTimeline(self, new_duration):
2794  get_app().updates.update(["duration"], new_duration)
2795 
2796  # Add Transition
2797  def addTransition(self, file_ids, position):
2798  log.info("addTransition...")
2799 
2800  # Find the closest track (from javascript)
2801  top_layer = int(self.eval_js(JS_SCOPE_SELECTOR + ".GetJavaScriptTrack(" + str(position.y()) + ");"))
2802 
2803  # Find position from javascript
2804  js_position = self.eval_js(JS_SCOPE_SELECTOR + ".GetJavaScriptPosition(" + str(position.x()) + ");")
2805 
2806  # Get FPS from project
2807  fps = get_app().project.get(["fps"])
2808  fps_float = float(fps["num"]) / float(fps["den"])
2809 
2810  # Open up QtImageReader for transition Image
2811  transition_reader = openshot.QtImageReader(file_ids[0])
2812 
2813  brightness = openshot.Keyframe()
2814  brightness.AddPoint(1, 1.0, openshot.BEZIER)
2815  brightness.AddPoint(round(10 * fps_float) + 1, -1.0, openshot.BEZIER)
2816  contrast = openshot.Keyframe(3.0)
2817 
2818  # Create transition dictionary
2819  transitions_data = {
2820  "id": get_app().project.generate_id(),
2821  "layer": top_layer,
2822  "title": "Transition",
2823  "type": "Mask",
2824  "position": js_position,
2825  "start": 0,
2826  "end": 10,
2827  "brightness": json.loads(brightness.Json()),
2828  "contrast": json.loads(contrast.Json()),
2829  "reader": json.loads(transition_reader.Json()),
2830  "replace_image": False
2831  }
2832 
2833  # Send to update manager
2834  self.update_transition_data(transitions_data, only_basic_props=False)
2835 
2836  # temp keep track of id
2837  self.item_id = transitions_data.get('id')
2838 
2839  # Init javascript bounding box (for snapping support)
2840  code = JS_SCOPE_SELECTOR + ".StartManualMove('" + self.item_type + "', '" + self.item_id + "');"
2841  self.eval_js(code)
2842 
2843  # Add Effect
2844  def addEffect(self, effect_names, position):
2845  log.info("addEffect: %s at %s" % (effect_names, position))
2846  # Get name of effect
2847  name = effect_names[0]
2848 
2849  # Find the closest track (from javascript)
2850  closest_layer = int(self.eval_js(JS_SCOPE_SELECTOR + ".GetJavaScriptTrack(" + str(position.y()) + ");"))
2851 
2852  # Find position from javascript
2853  js_position = self.eval_js(JS_SCOPE_SELECTOR + ".GetJavaScriptPosition(" + str(position.x()) + ");")
2854 
2855  # Loop through clips on the closest layer
2856  possible_clips = Clip.filter(layer=closest_layer)
2857  for clip in possible_clips:
2858  if js_position == 0 or (clip.data["position"] <= js_position <= clip.data["position"] + (
2859  clip.data["end"] - clip.data["start"])):
2860  log.info("Applying effect to clip")
2861  log.info(clip)
2862 
2863  # Create Effect
2864  effect = openshot.EffectInfo().CreateEffect(name)
2865 
2866  # Get Effect JSON
2867  effect.Id(get_app().project.generate_id())
2868  effect_json = json.loads(effect.Json())
2869 
2870  # Append effect JSON to clip
2871  clip.data["effects"].append(effect_json)
2872 
2873  # Update clip data for project
2874  self.update_clip_data(clip.data, only_basic_props=False, ignore_reader=True)
2875 
2876  # Without defining this method, the 'copy' action doesn't show with cursor
2877  def dragMoveEvent(self, event):
2878  # Accept all move events
2879  event.accept()
2880 
2881  # Get cursor position
2882  pos = event.posF()
2883 
2884  # Move clip on timeline
2885  if self.item_type in ["clip", "transition"]:
2886  code = JS_SCOPE_SELECTOR + ".MoveItem(" + str(pos.x()) + ", " + str(pos.y()) + ", '" + self.item_type + "');"
2887  self.eval_js(code)
2888 
2889  # Drop an item on the timeline
2890  def dropEvent(self, event):
2891  log.info("Dropping item on timeline - item_id: %s, item_type: %s" % (self.item_id, self.item_type))
2892 
2893  # Get position of cursor
2894  pos = event.posF()
2895 
2896  if self.item_type in ["clip", "transition"] and self.item_id:
2897  # Update most recent clip
2898  self.eval_js(JS_SCOPE_SELECTOR + ".UpdateRecentItemJSON('" + self.item_type + "', '" + self.item_id + "');")
2899 
2900  elif self.item_type == "effect":
2901  # Add effect only on drop
2902  data = json.loads(event.mimeData().text())
2903  self.addEffect(data, pos)
2904 
2905  elif self.item_type == "os_drop":
2906  # Add new files to project
2907  get_app().window.filesTreeView.dropEvent(event)
2908 
2909  # Add clips for each file dropped
2910  for uri in event.mimeData().urls():
2911  filepath = uri.toLocalFile()
2912  if os.path.exists(filepath) and os.path.isfile(filepath):
2913  # Valid file, so create clip for it
2914  log.info('Adding clip for {}'.format(os.path.basename(filepath)))
2915  for file in File.filter(path=filepath):
2916  # Insert clip for this file at this position
2917  self.addClip([file.id], pos)
2918 
2919  # Clear new clip
2920  self.new_item = False
2921  self.item_type = None
2922  self.item_id = None
2923 
2924  # Accept event
2925  event.accept()
2926 
2927  # Update the preview and reselct current frame in properties
2928  get_app().window.refreshFrameSignal.emit()
2929  get_app().window.propertyTableView.select_frame(self.window.preview_thread.player.Position())
2930 
2931  ##
2932  # A drag is in-progress and the user moves mouse outside of timeline
2933  def dragLeaveEvent(self, event):
2934  log.info('dragLeaveEvent - Undo drop')
2935  if self.item_type == "clip":
2936  get_app().window.actionRemoveClip.trigger()
2937  elif self.item_type == "transition":
2938  get_app().window.actionRemoveTransition.trigger()
2939 
2940  # Clear new clip
2941  self.new_item = False
2942  self.item_type = None
2943  self.item_id = None
2944 
2945  # Accept event
2946  event.accept()
2947 
2948  ##
2949  # Timer is ready to redraw audio (if any)
2951  log.info('redraw_audio_onTimeout')
2952 
2953  # Stop timer
2954  self.redraw_audio_timer.stop()
2955 
2956  # Pass to javascript timeline (and render)
2957  cmd = JS_SCOPE_SELECTOR + ".reDrawAllAudioData();"
2958  self.page().mainFrame().evaluateJavaScript(cmd)
2959 
2960  ##
2961  # Clear all selections in JavaScript
2963 
2964  # Call javascript command
2965  cmd = JS_SCOPE_SELECTOR + ".ClearAllSelections();"
2966  self.page().mainFrame().evaluateJavaScript(cmd)
2967 
2968  ##
2969  # Select all clips and transitions in JavaScript
2970  def SelectAll(self):
2971 
2972  # Call javascript command
2973  cmd = JS_SCOPE_SELECTOR + ".SelectAll();"
2974  self.page().mainFrame().evaluateJavaScript(cmd)
2975 
2976  ##
2977  # Render the cached frames to the timeline (called every X seconds), and only if changed
2979 
2980  # Get final cache object from timeline
2981  try:
2982  cache_object = get_app().window.timeline_sync.timeline.GetCache()
2983  if cache_object and cache_object.Count() > 0:
2984  # Get the JSON from the cache object (i.e. which frames are cached)
2985  cache_json = get_app().window.timeline_sync.timeline.GetCache().Json()
2986  cache_dict = json.loads(cache_json)
2987  cache_version = cache_dict["version"]
2988 
2989  if self.cache_renderer_version != cache_version:
2990  # Cache has changed, re-render it
2991  self.cache_renderer_version = cache_version
2992 
2993  cmd = JS_SCOPE_SELECTOR + ".RenderCache(" + cache_json + ");"
2994  self.page().mainFrame().evaluateJavaScript(cmd)
2995  finally:
2996  # ignore any errors inside the cache rendering
2997  pass
2998 
2999  def __init__(self, window):
3000  QWebView.__init__(self)
3001  self.window = window
3002  self.setAcceptDrops(True)
3003  self.last_position_frames = None
3004  self.document_is_ready = False
3005 
3006  # Disable image caching on timeline
3007  self.settings().setObjectCacheCapacities(0, 0, 0);
3008 
3009  # Get settings
3011 
3012  # Add self as listener to project data updates (used to update the timeline)
3013  get_app().updates.add_listener(self)
3014 
3015  # set url from configuration (QUrl takes absolute paths for file system paths, create from QFileInfo)
3016  self.setUrl(QUrl.fromLocalFile(QFileInfo(self.html_path).absoluteFilePath()))
3017 
3018  # Connect signal of javascript initialization to our javascript reference init function
3019  self.page().mainFrame().javaScriptWindowObjectCleared.connect(self.setup_js_data)
3020 
3021  # Connect zoom functionality
3022  window.sliderZoom.valueChanged.connect(self.update_zoom)
3023 
3024  # Connect waveform generation signal
3025  get_app().window.WaveformReady.connect(self.Waveform_Ready)
3026 
3027  # Local audio waveform cache
3028  self.waveform_cache = {}
3029 
3030  # Connect update thumbnail signal
3031  get_app().window.ThumbnailUpdated.connect(self.Thumbnail_Updated)
3032 
3033  # Copy clipboard
3034  self.copy_clipboard = {}
3035  self.copy_transition_clipboard = {}
3036 
3037  # Init New clip
3038  self.new_item = False
3039  self.item_type = None
3040  self.item_id = None
3041 
3042  # Delayed zoom audio redraw
3043  self.redraw_audio_timer = QTimer(self)
3044  self.redraw_audio_timer.setInterval(300)
3045  self.redraw_audio_timer.timeout.connect(self.redraw_audio_onTimeout)
3046 
3047  # QTimer for cache rendering
3048  self.cache_renderer_version = None
3049  self.cache_renderer = QTimer(self)
3050  self.cache_renderer.setInterval(0.5 * 1000)
3051  self.cache_renderer.timeout.connect(self.render_cache_json)
3052 
3053  # Delay the start of cache rendering
3054  QTimer.singleShot(1500, self.cache_renderer.start)
def secondsToZoom
Convert a number of seconds to a timeline zoom factor.
Definition: conversion.py:44
def Volume_Triggered
Callback for volume context menus.
def update_clip_data
Create an updateAction and send it to the update manager.
def Thumbnail_Updated
Callback when thumbnail needs to be updated.
def get_app
Returns the current QApplication instance of OpenShot.
Definition: app.py:55
def keyPressEvent
Keypress callback for timeline.
def Hide_Waveform_Triggered
Hide the waveform for the selected clip.
def Split_Audio_Triggered
Callback for split audio context menus.
def UpdateClipThumbnail
Update the thumbnail image for clips.
def SetRazorMode
Enable / Disable razor mode.
def update_transition_data
Create an updateAction and send it to the update manager.
def RazorSliceAtCursor
Callback from javascript that the razor tool was clicked.
def page_ready
Document.Ready event has fired, and is initialized.
def Paste_Triggered
Callback for paste context menus.
A WebView QWidget used to load the Timeline.
def show_all_clips
Show all clips at the same time (arranged col by col, row by row)
def movePlayhead
Move the playhead since the position has changed inside OpenShot (probably due to the video player) ...
def ClearAllSelections
Clear all selections in JavaScript.
def Reverse_Transition_Triggered
Callback for reversing a transition.
def Show_Waveform_Triggered
Show a waveform for the selected clip.
def render_cache_json
Render the cached frames to the timeline (called every X seconds), and only if changed.
def dragLeaveEvent
A drag is in-progress and the user moves mouse outside of timeline.
def SetSnappingMode
Enable / Disable snapping mode.
def GenerateThumbnail
Create thumbnail image, and check for rotate metadata (if any)
Definition: thumbnail.py:35
def removeSelection
Remove the selected clip from the selection.
def Rotate_Triggered
Callback for rotate context menus.
def Layout_Triggered
Callback for the layout context menus.
def round_to_multiple
Round this to the closest multiple of a given #.
def Copy_Triggered
Callback for copy context menus.
def addSelection
Add the selected item to the current selection.
def zoomToSeconds
Convert zoom factor (slider position) into scale-seconds.
Definition: conversion.py:36
def Fade_Triggered
Callback for fade context menus.
def resizeTimeline
Resize the duration of the timeline.
def SelectAll
Select all clips and transitions in JavaScript.
def Waveform_Ready
Callback when audio waveform is ready.
def Align_Triggered
Callback for alignment context menus.
def redraw_audio_onTimeout
Timer is ready to redraw audio (if any)
def get_audio_data
Get a Clip object form libopenshot, and grab audio data.
Definition: waveform.py:45
def Time_Triggered
Callback for rotate context menus.
Interface for classes that listen for changes (insert, update, and delete).
Definition: updates.py:54
def Slice_Triggered
Callback for slice context menus.
def Nudge_Triggered
Callback for clip nudges.
def get_settings
Get the current QApplication's settings instance.
Definition: settings.py:44
def Animate_Triggered
Callback for the animate context menus.