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