OpenShot Video Editor  2.0.0
add_to_timeline.py
Go to the documentation of this file.
1 ##
2 #
3 # @file
4 # @brief This file loads the Addtotimeline dialog (i.e add several clips in the timeline)
5 # @author Jonathan Thomas <jonathan@openshot.org>
6 # @author Olivier Girard <olivier@openshot.org>
7 #
8 # @section LICENSE
9 #
10 # Copyright (c) 2008-2018 OpenShot Studios, LLC
11 # (http://www.openshotstudios.com). This file is part of
12 # OpenShot Video Editor (http://www.openshot.org), an open-source project
13 # dedicated to delivering high quality video editing and animation solutions
14 # to the world.
15 #
16 # OpenShot Video Editor is free software: you can redistribute it and/or modify
17 # it under the terms of the GNU General Public License as published by
18 # the Free Software Foundation, either version 3 of the License, or
19 # (at your option) any later version.
20 #
21 # OpenShot Video Editor is distributed in the hope that it will be useful,
22 # but WITHOUT ANY WARRANTY; without even the implied warranty of
23 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
24 # GNU General Public License for more details.
25 #
26 # You should have received a copy of the GNU General Public License
27 # along with OpenShot Library. If not, see <http://www.gnu.org/licenses/>.
28 #
29 
30 import os, math
31 from operator import itemgetter
32 from random import shuffle, randint, uniform
33 
34 from PyQt5.QtWidgets import *
35 from PyQt5.QtGui import QIcon
36 
37 from classes import settings
38 from classes import info, ui_util
39 from classes.logger import log
40 from classes.query import Track, Clip, Transition
41 from classes.app import get_app
42 from classes.metrics import *
43 from windows.views.add_to_timeline_treeview import TimelineTreeView
44 
45 import openshot
46 
47 try:
48  import json
49 except ImportError:
50  import simplejson as json
51 
52 
53 ##
54 # Add To timeline Dialog
55 class AddToTimeline(QDialog):
56 
57  ui_path = os.path.join(info.PATH, 'windows', 'ui', 'add-to-timeline.ui')
58 
59  ##
60  # Callback for move up button click
61  def btnMoveUpClicked(self, event):
62  log.info("btnMoveUpClicked")
63 
64  # Get selected file
65  files = self.treeFiles.timeline_model.files
66 
67  selected_index = None
68  if self.treeFiles.selected:
69  selected_index = self.treeFiles.selected.row()
70 
71  # Ignore if empty files
72  if not files or selected_index == None:
73  return
74 
75  # New index
76  new_index = max(selected_index - 1, 0)
77  log.info(new_index)
78 
79  # Remove item and move it
80  files.insert(new_index, files.pop(selected_index))
81 
82  # Refresh tree
83  self.treeFiles.refresh_view()
84 
85  # Select new position
86  idx = self.treeFiles.timeline_model.model.index(new_index, 0)
87  self.treeFiles.setCurrentIndex(idx)
88 
89  ##
90  # Callback for move up button click
91  def btnMoveDownClicked(self, event):
92  log.info("btnMoveDownClicked")
93 
94  # Get selected file
95  files = self.treeFiles.timeline_model.files
96 
97  selected_index = None
98  if self.treeFiles.selected:
99  selected_index = self.treeFiles.selected.row()
100 
101  # Ignore if empty files
102  if not files or selected_index == None:
103  return
104 
105  # New index
106  new_index = min(selected_index + 1, len(files) - 1)
107  log.info(new_index)
108 
109  # Remove item and move it
110  files.insert(new_index, files.pop(selected_index))
111 
112  # Refresh tree
113  self.treeFiles.refresh_view()
114 
115  # Select new position
116  idx = self.treeFiles.timeline_model.model.index(new_index, 0)
117  self.treeFiles.setCurrentIndex(idx)
118 
119  ##
120  # Callback for move up button click
121  def btnShuffleClicked(self, event):
122  log.info("btnShuffleClicked")
123 
124  # Shuffle files
125  files = shuffle(self.treeFiles.timeline_model.files)
126 
127  # Refresh tree
128  self.treeFiles.refresh_view()
129 
130  ##
131  # Callback for move up button click
132  def btnRemoveClicked(self, event):
133  log.info("btnRemoveClicked")
134 
135  # Get selected file
136  files = self.treeFiles.timeline_model.files
137 
138  selected_index = None
139  if self.treeFiles.selected:
140  selected_index = self.treeFiles.selected.row()
141 
142  # Ignore if empty files
143  if not files or selected_index == None:
144  return
145 
146  # Remove item
147  files.pop(selected_index)
148 
149  # Refresh tree
150  self.treeFiles.refresh_view()
151 
152  # Select next item (if any)
153  new_index = max(len(files) - 1, 0)
154 
155  # Select new position
156  idx = self.treeFiles.timeline_model.model.index(new_index, 0)
157  self.treeFiles.setCurrentIndex(idx)
158 
159  # Update total
160  self.updateTotal()
161 
162  ##
163  # Ok button clicked
164  def accept(self):
165  log.info('accept')
166 
167  # Get settings from form
168  start_position = self.txtStartTime.value()
169  track_num = self.cmbTrack.currentData()
170  fade_value = self.cmbFade.currentData()
171  fade_length = self.txtFadeLength.value()
172  transition_path = self.cmbTransition.currentData()
173  transition_length = self.txtTransitionLength.value()
174  image_length = self.txtImageLength.value()
175  zoom_value = self.cmbZoom.currentData()
176 
177  # Init position
178  position = start_position
179 
180  random_transition = False
181  if transition_path == "random":
182  random_transition = True
183 
184  # Get frames per second
185  fps = get_app().project.get(["fps"])
186  fps_float = float(fps["num"]) / float(fps["den"])
187 
188  # Loop through each file (in the current order)
189  for file in self.treeFiles.timeline_model.files:
190  # Create a clip
191  clip = Clip()
192  clip.data = {}
193 
194  if (file.data["media_type"] == "video" or file.data["media_type"] == "image"):
195  # Determine thumb path
196  thumb_path = os.path.join(info.THUMBNAIL_PATH, "%s.png" % file.data["id"])
197  else:
198  # Audio file
199  thumb_path = os.path.join(info.PATH, "images", "AudioThumbnail.png")
200 
201  # Get file name
202  path, filename = os.path.split(file.data["path"])
203 
204  # Convert path to the correct relative path (based on this folder)
205  file_path = file.absolute_path()
206 
207  # Create clip object for this file
208  c = openshot.Clip(file_path)
209 
210  # Append missing attributes to Clip JSON
211  new_clip = json.loads(c.Json())
212  new_clip["position"] = position
213  new_clip["layer"] = track_num
214  new_clip["file_id"] = file.id
215  new_clip["title"] = filename
216  new_clip["image"] = thumb_path
217 
218  # Skip any clips that are missing a 'reader' attribute
219  # TODO: Determine why this even happens, as it shouldn't be possible
220  if not new_clip.get("reader"):
221  continue # Skip to next file
222 
223  # Overwrite frame rate (incase the user changed it in the File Properties)
224  file_properties_fps = float(file.data["fps"]["num"]) / float(file.data["fps"]["den"])
225  file_fps = float(new_clip["reader"]["fps"]["num"]) / float(new_clip["reader"]["fps"]["den"])
226  fps_diff = file_fps / file_properties_fps
227  new_clip["reader"]["fps"]["num"] = file.data["fps"]["num"]
228  new_clip["reader"]["fps"]["den"] = file.data["fps"]["den"]
229  # Scale duration / length / and end properties
230  new_clip["reader"]["duration"] *= fps_diff
231  new_clip["end"] *= fps_diff
232  new_clip["duration"] *= fps_diff
233 
234  # Check for optional start and end attributes
235  start_time = 0
236  end_time = new_clip["reader"]["duration"]
237 
238  if 'start' in file.data.keys():
239  start_time = file.data['start']
240  new_clip["start"] = start_time
241  if 'end' in file.data.keys():
242  end_time = file.data['end']
243  new_clip["end"] = end_time
244 
245  # Adjust clip duration, start, and end
246  new_clip["duration"] = new_clip["reader"]["duration"]
247  if file.data["media_type"] == "image":
248  end_time = image_length
249  new_clip["end"] = end_time
250 
251  # Adjust Fade of Clips (if no transition is chosen)
252  if not transition_path:
253  if fade_value != None:
254  # Overlap this clip with the previous one (if any)
255  position = max(start_position, new_clip["position"] - fade_length)
256  new_clip["position"] = position
257 
258  if fade_value == 'Fade In' or fade_value == 'Fade In & Out':
259  start = openshot.Point(round(start_time * fps_float) + 1, 0.0, openshot.BEZIER)
260  start_object = json.loads(start.Json())
261  end = openshot.Point(min(round((start_time + fade_length) * fps_float) + 1, round(end_time * fps_float) + 1), 1.0, openshot.BEZIER)
262  end_object = json.loads(end.Json())
263  new_clip['alpha']["Points"].append(start_object)
264  new_clip['alpha']["Points"].append(end_object)
265 
266  if fade_value == 'Fade Out' or fade_value == 'Fade In & Out':
267  start = openshot.Point(max(round((end_time * fps_float) + 1) - (round(fade_length * fps_float) + 1), round(start_time * fps_float) + 1), 1.0, openshot.BEZIER)
268  start_object = json.loads(start.Json())
269  end = openshot.Point(round(end_time * fps_float) + 1, 0.0, openshot.BEZIER)
270  end_object = json.loads(end.Json())
271  new_clip['alpha']["Points"].append(start_object)
272  new_clip['alpha']["Points"].append(end_object)
273 
274  # Adjust zoom amount
275  if zoom_value != None:
276  # Location animation
277  if zoom_value == "Random":
278  animate_start_x = uniform(-0.5, 0.5)
279  animate_end_x = uniform(-0.15, 0.15)
280  animate_start_y = uniform(-0.5, 0.5)
281  animate_end_y = uniform(-0.15, 0.15)
282 
283  # Scale animation
284  start_scale = uniform(0.5, 1.5)
285  end_scale = uniform(0.85, 1.15)
286 
287  elif zoom_value == "Zoom In":
288  animate_start_x = 0.0
289  animate_end_x = 0.0
290  animate_start_y = 0.0
291  animate_end_y = 0.0
292 
293  # Scale animation
294  start_scale = 1.0
295  end_scale = 1.25
296 
297  elif zoom_value == "Zoom Out":
298  animate_start_x = 0.0
299  animate_end_x = 0.0
300  animate_start_y = 0.0
301  animate_end_y = 0.0
302 
303  # Scale animation
304  start_scale = 1.25
305  end_scale = 1.0
306 
307  # Add keyframes
308  start = openshot.Point(round(start_time * fps_float) + 1, start_scale, openshot.BEZIER)
309  start_object = json.loads(start.Json())
310  end = openshot.Point(round(end_time * fps_float) + 1, end_scale, openshot.BEZIER)
311  end_object = json.loads(end.Json())
312  new_clip["gravity"] = openshot.GRAVITY_CENTER
313  new_clip["scale_x"]["Points"].append(start_object)
314  new_clip["scale_x"]["Points"].append(end_object)
315  new_clip["scale_y"]["Points"].append(start_object)
316  new_clip["scale_y"]["Points"].append(end_object)
317 
318  # Add keyframes
319  start_x = openshot.Point(round(start_time * fps_float) + 1, animate_start_x, openshot.BEZIER)
320  start_x_object = json.loads(start_x.Json())
321  end_x = openshot.Point(round(end_time * fps_float) + 1, animate_end_x, openshot.BEZIER)
322  end_x_object = json.loads(end_x.Json())
323  start_y = openshot.Point(round(start_time * fps_float) + 1, animate_start_y, openshot.BEZIER)
324  start_y_object = json.loads(start_y.Json())
325  end_y = openshot.Point(round(end_time * fps_float) + 1, animate_end_y, openshot.BEZIER)
326  end_y_object = json.loads(end_y.Json())
327  new_clip["gravity"] = openshot.GRAVITY_CENTER
328  new_clip["location_x"]["Points"].append(start_x_object)
329  new_clip["location_x"]["Points"].append(end_x_object)
330  new_clip["location_y"]["Points"].append(start_y_object)
331  new_clip["location_y"]["Points"].append(end_y_object)
332 
333  if transition_path:
334  # Add transition for this clip (if any)
335  # Open up QtImageReader for transition Image
336  if random_transition:
337  random_index = randint(0, len(self.transitions))
338  transition_path = self.transitions[random_index]
339 
340  # Get reader for transition
341  transition_reader = openshot.QtImageReader(transition_path)
342 
343  brightness = openshot.Keyframe()
344  brightness.AddPoint(1, 1.0, openshot.BEZIER)
345  brightness.AddPoint(round(min(transition_length, end_time - start_time) * fps_float) + 1, -1.0, openshot.BEZIER)
346  contrast = openshot.Keyframe(3.0)
347 
348  # Create transition dictionary
349  transitions_data = {
350  "layer": track_num,
351  "title": "Transition",
352  "type": "Mask",
353  "start": 0,
354  "end": min(transition_length, end_time - start_time),
355  "brightness": json.loads(brightness.Json()),
356  "contrast": json.loads(contrast.Json()),
357  "reader": json.loads(transition_reader.Json()),
358  "replace_image": False
359  }
360 
361  # Overlap this clip with the previous one (if any)
362  position = max(start_position, position - transition_length)
363  transitions_data["position"] = position
364  new_clip["position"] = position
365 
366  # Create transition
367  tran = Transition()
368  tran.data = transitions_data
369  tran.save()
370 
371 
372  # Save Clip
373  clip.data = new_clip
374  clip.save()
375 
376  # Increment position by length of clip
377  position += (end_time - start_time)
378 
379 
380  # Accept dialog
381  super(AddToTimeline, self).accept()
382 
383  ##
384  # Handle callback for image length being changed
385  def ImageLengthChanged(self, value):
386  self.updateTotal()
387 
388  ##
389  # Calculate the total length of what's about to be added to the timeline
390  def updateTotal(self):
391  fade_value = self.cmbFade.currentData()
392  fade_length = self.txtFadeLength.value()
393  transition_path = self.cmbTransition.currentData()
394  transition_length = self.txtTransitionLength.value()
395 
396  total = 0.0
397  for file in self.treeFiles.timeline_model.files:
398  # Adjust clip duration, start, and end
399  duration = file.data["duration"]
400  if file.data["media_type"] == "image":
401  duration = self.txtImageLength.value()
402 
403  if total != 0.0:
404  # Don't subtract time from initial clip
405  if not transition_path:
406  # No transitions
407  if fade_value != None:
408  # Fade clip - subtract the fade length
409  duration -= fade_length
410  else:
411  # Transition
412  duration -= transition_length
413 
414  # Append duration to total
415  total += duration
416 
417  # Get frames per second
418  fps = get_app().project.get(["fps"])
419 
420  # Update label
421  total_parts = self.secondsToTime(total, fps["num"], fps["den"])
422  timestamp = "%s:%s:%s:%s" % (total_parts["hour"], total_parts["min"], total_parts["sec"], total_parts["frame"])
423  self.lblTotalLengthValue.setText(timestamp)
424 
425  def padNumber(self, value, pad_length):
426  format_mask = '%%0%sd' % pad_length
427  return format_mask % value
428 
429  def secondsToTime(self, secs, fps_num, fps_den):
430  # calculate time of playhead
431  milliseconds = secs * 1000
432  sec = math.floor(milliseconds/1000)
433  milli = milliseconds % 1000
434  min = math.floor(sec/60)
435  sec = sec % 60
436  hour = math.floor(min/60)
437  min = min % 60
438  day = math.floor(hour/24)
439  hour = hour % 24
440  week = math.floor(day/7)
441  day = day % 7
442 
443  frame = round((milli / 1000.0) * (fps_num / fps_den)) + 1
444  return { "week":self.padNumber(week,2), "day":self.padNumber(day,2), "hour":self.padNumber(hour,2), "min":self.padNumber(min,2), "sec":self.padNumber(sec,2), "milli":self.padNumber(milli,2), "frame":self.padNumber(frame,2) };
445 
446  ##
447  # Cancel button clicked
448  def reject(self):
449  log.info('reject')
450 
451  # Accept dialog
452  super(AddToTimeline, self).reject()
453 
454  def __init__(self, files=None, position=0.0):
455  # Create dialog class
456  QDialog.__init__(self)
457 
458  # Load UI from Designer
459  ui_util.load_ui(self, self.ui_path)
460 
461  # Init UI
462  ui_util.init_ui(self)
463 
464  # Get settings
466 
467  # Get translation object
468  self.app = get_app()
469  _ = self.app._tr
470 
471  # Track metrics
472  track_metric_screen("add-to-timeline-screen")
473 
474  # Add custom treeview to window
475  self.treeFiles = TimelineTreeView(self)
476  self.vboxTreeParent.insertWidget(0, self.treeFiles)
477 
478  # Update data in model
479  self.treeFiles.timeline_model.update_model(files)
480 
481  # Refresh view
482  self.treeFiles.refresh_view()
483 
484  # Init start position
485  self.txtStartTime.setValue(position)
486 
487  # Init default image length
488  self.txtImageLength.setValue(self.settings.get("default-image-length"))
489  self.txtImageLength.valueChanged.connect(self.updateTotal)
490  self.cmbTransition.currentIndexChanged.connect(self.updateTotal)
491  self.cmbFade.currentIndexChanged.connect(self.updateTotal)
492  self.txtFadeLength.valueChanged.connect(self.updateTotal)
493  self.txtTransitionLength.valueChanged.connect(self.updateTotal)
494 
495  # Find display track number
496  all_tracks = get_app().project.get(["layers"])
497  display_count = len(all_tracks)
498  for track in reversed(sorted(all_tracks, key=itemgetter('number'))):
499  # Add to dropdown
500  track_name = track.get('label') or _("Track %s") % display_count
501  self.cmbTrack.addItem(track_name, track.get('number'))
502  display_count -= 1
503 
504  # Add all fade options
505  self.cmbFade.addItem(_('None'), None)
506  self.cmbFade.addItem(_('Fade In'), 'Fade In')
507  self.cmbFade.addItem(_('Fade Out'), 'Fade Out')
508  self.cmbFade.addItem(_('Fade In & Out'), 'Fade In & Out')
509 
510  # Add all zoom options
511  self.cmbZoom.addItem(_('None'), None)
512  self.cmbZoom.addItem(_('Random'), 'Random')
513  self.cmbZoom.addItem(_('Zoom In'), 'Zoom In')
514  self.cmbZoom.addItem(_('Zoom Out'), 'Zoom Out')
515 
516  # Add all transitions
517  transitions_dir = os.path.join(info.PATH, "transitions")
518  common_dir = os.path.join(transitions_dir, "common")
519  extra_dir = os.path.join(transitions_dir, "extra")
520  transition_groups = [{"type": "common", "dir": common_dir, "files": os.listdir(common_dir)},
521  {"type": "extra", "dir": extra_dir, "files": os.listdir(extra_dir)}]
522 
523  self.cmbTransition.addItem(_('None'), None)
524  self.cmbTransition.addItem(_('Random'), 'random')
525  self.transitions = []
526  for group in transition_groups:
527  type = group["type"]
528  dir = group["dir"]
529  files = group["files"]
530 
531  for filename in sorted(files):
532  path = os.path.join(dir, filename)
533  (fileBaseName, fileExtension) = os.path.splitext(filename)
534 
535  # Skip hidden files (such as .DS_Store, etc...)
536  if filename[0] == "." or "thumbs.db" in filename.lower():
537  continue
538 
539  # split the name into parts (looking for a number)
540  suffix_number = None
541  name_parts = fileBaseName.split("_")
542  if name_parts[-1].isdigit():
543  suffix_number = name_parts[-1]
544 
545  # get name of transition
546  trans_name = fileBaseName.replace("_", " ").capitalize()
547 
548  # replace suffix number with placeholder (if any)
549  if suffix_number:
550  trans_name = trans_name.replace(suffix_number, "%s")
551  trans_name = _(trans_name) % suffix_number
552  else:
553  trans_name = _(trans_name)
554 
555  # Check for thumbnail path (in build-in cache)
556  thumb_path = os.path.join(info.IMAGES_PATH, "cache", "{}.png".format(fileBaseName))
557 
558  # Check built-in cache (if not found)
559  if not os.path.exists(thumb_path):
560  # Check user folder cache
561  thumb_path = os.path.join(info.CACHE_PATH, "{}.png".format(fileBaseName))
562 
563  # Add item
564  self.transitions.append(path)
565  self.cmbTransition.addItem(QIcon(thumb_path), _(trans_name), path)
566 
567  # Connections
568  self.btnMoveUp.clicked.connect(self.btnMoveUpClicked)
569  self.btnMoveDown.clicked.connect(self.btnMoveDownClicked)
570  self.btnShuffle.clicked.connect(self.btnShuffleClicked)
571  self.btnRemove.clicked.connect(self.btnRemoveClicked)
572  self.btnBox.accepted.connect(self.accept)
573  self.btnBox.rejected.connect(self.reject)
574 
575  # Update total
576  self.updateTotal()
def track_metric_screen
Track a GUI screen being shown.
Definition: metrics.py:96
def get_app
Returns the current QApplication instance of OpenShot.
Definition: app.py:55
def btnMoveDownClicked
Callback for move up button click.
def btnRemoveClicked
Callback for move up button click.
def btnMoveUpClicked
Callback for move up button click.
def ImageLengthChanged
Handle callback for image length being changed.
Add To timeline Dialog.
def load_ui
Load a Qt *.ui file, and also load an XML parsed version.
Definition: ui_util.py:66
def accept
Ok button clicked.
def updateTotal
Calculate the total length of what's about to be added to the timeline.
def init_ui
Initialize all child widgets and action of a window or dialog.
Definition: ui_util.py:215
def btnShuffleClicked
Callback for move up button click.
def reject
Cancel button clicked.
def get_settings
Get the current QApplication's settings instance.
Definition: settings.py:44