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