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  self.cmbTrack.addItem(_('Track %s' % track.data['number']), track.data['number'])
499 
500  # Add all fade options
501  self.cmbFade.addItem(_('None'), None)
502  self.cmbFade.addItem(_('Fade In'), 'Fade In')
503  self.cmbFade.addItem(_('Fade Out'), 'Fade Out')
504  self.cmbFade.addItem(_('Fade In & Out'), 'Fade In & Out')
505 
506  # Add all zoom options
507  self.cmbZoom.addItem(_('None'), None)
508  self.cmbZoom.addItem(_('Random'), 'Random')
509  self.cmbZoom.addItem(_('Zoom In'), 'Zoom In')
510  self.cmbZoom.addItem(_('Zoom Out'), 'Zoom Out')
511 
512  # Add all transitions
513  transitions_dir = os.path.join(info.PATH, "transitions")
514  common_dir = os.path.join(transitions_dir, "common")
515  extra_dir = os.path.join(transitions_dir, "extra")
516  transition_groups = [{"type": "common", "dir": common_dir, "files": os.listdir(common_dir)},
517  {"type": "extra", "dir": extra_dir, "files": os.listdir(extra_dir)}]
518 
519  self.cmbTransition.addItem(_('None'), None)
520  self.cmbTransition.addItem(_('Random'), 'random')
521  self.transitions = []
522  for group in transition_groups:
523  type = group["type"]
524  dir = group["dir"]
525  files = group["files"]
526 
527  for filename in sorted(files):
528  path = os.path.join(dir, filename)
529  (fileBaseName, fileExtension) = os.path.splitext(filename)
530 
531  # Skip hidden files (such as .DS_Store, etc...)
532  if filename[0] == "." or "thumbs.db" in filename.lower():
533  continue
534 
535  # split the name into parts (looking for a number)
536  suffix_number = None
537  name_parts = fileBaseName.split("_")
538  if name_parts[-1].isdigit():
539  suffix_number = name_parts[-1]
540 
541  # get name of transition
542  trans_name = fileBaseName.replace("_", " ").capitalize()
543 
544  # replace suffix number with placeholder (if any)
545  if suffix_number:
546  trans_name = trans_name.replace(suffix_number, "%s")
547  trans_name = _(trans_name) % suffix_number
548  else:
549  trans_name = _(trans_name)
550 
551  # Check for thumbnail path (in build-in cache)
552  thumb_path = os.path.join(info.IMAGES_PATH, "cache", "{}.png".format(fileBaseName))
553 
554  # Check built-in cache (if not found)
555  if not os.path.exists(thumb_path):
556  # Check user folder cache
557  thumb_path = os.path.join(info.CACHE_PATH, "{}.png".format(fileBaseName))
558 
559  # Add item
560  self.transitions.append(path)
561  self.cmbTransition.addItem(QIcon(thumb_path), _(trans_name), path)
562 
563  # Connections
564  self.btnMoveUp.clicked.connect(self.btnMoveUpClicked)
565  self.btnMoveDown.clicked.connect(self.btnMoveDownClicked)
566  self.btnShuffle.clicked.connect(self.btnShuffleClicked)
567  self.btnRemove.clicked.connect(self.btnRemoveClicked)
568  self.btnBox.accepted.connect(self.accept)
569  self.btnBox.rejected.connect(self.reject)
570 
571  # Update total
572  self.updateTotal()
def btnShuffleClicked(self, event)
Callback for move up button click.
def get_app()
Returns the current QApplication instance of OpenShot.
Definition: app.py:55
def btnRemoveClicked(self, event)
Callback for move up button click.
def accept(self)
Ok button clicked.
def btnMoveDownClicked(self, event)
Callback for move up button click.
def padNumber(self, value, pad_length)
def __init__(self, files=None, position=0.0)
Add To timeline Dialog.
def secondsToTime(self, secs, fps_num, fps_den)
def btnMoveUpClicked(self, event)
Callback for move up button click.
def updateTotal(self)
Calculate the total length of what&#39;s about to be added to the timeline.
def get_settings()
Get the current QApplication&#39;s settings instance.
Definition: settings.py:44
def ImageLengthChanged(self, value)
Handle callback for image length being changed.
def init_ui(window)
Initialize all child widgets and action of a window or dialog.
Definition: ui_util.py:220
def track_metric_screen(screen_name)
Track a GUI screen being shown.
Definition: metrics.py:96
def load_ui(window, path)
Load a Qt *.ui file, and also load an XML parsed version.
Definition: ui_util.py:66
def reject(self)
Cancel button clicked.