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