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