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