OpenShot Video Editor  2.0.0
properties_model.py
Go to the documentation of this file.
1 ##
2 #
3 # @file
4 # @brief This file contains the clip properties model, used by the properties view
5 # @author Jonathan Thomas <jonathan@openshot.org>
6 #
7 # @section LICENSE
8 #
9 # Copyright (c) 2008-2016 OpenShot Studios, LLC
10 # (http://www.openshotstudios.com). This file is part of
11 # OpenShot Video Editor (http://www.openshot.org), an open-source project
12 # dedicated to delivering high quality video editing and animation solutions
13 # to the world.
14 #
15 # OpenShot Video Editor is free software: you can redistribute it and/or modify
16 # it under the terms of the GNU General Public License as published by
17 # the Free Software Foundation, either version 3 of the License, or
18 # (at your option) any later version.
19 #
20 # OpenShot Video Editor is distributed in the hope that it will be useful,
21 # but WITHOUT ANY WARRANTY; without even the implied warranty of
22 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
23 # GNU General Public License for more details.
24 #
25 # You should have received a copy of the GNU General Public License
26 # along with OpenShot Library. If not, see <http://www.gnu.org/licenses/>.
27 #
28 
29 import os
30 from collections import OrderedDict
31 
32 from PyQt5.QtCore import QMimeData, Qt, QLocale, QTimer
33 from PyQt5.QtGui import *
34 
35 from classes import updates
36 from classes import info
37 from classes.query import Clip, Transition, Effect
38 from classes.logger import log
39 from classes.app import get_app
40 
41 try:
42  import json
43 except ImportError:
44  import simplejson as json
45 
46 
47 class ClipStandardItemModel(QStandardItemModel):
48  def __init__(self, parent=None):
49  QStandardItemModel.__init__(self)
50 
51  def mimeData(self, indexes):
52  # Create MimeData for drag operation
53  data = QMimeData()
54 
55  # Get list of all selected file ids
56  property_names = []
57  for item in indexes:
58  selected_row = self.itemFromIndex(item).row()
59  property_names.append(self.item(selected_row, 0).data())
60  data.setText(json.dumps(property_names))
61 
62  # Return Mimedata
63  return data
64 
65 
67  # This method is invoked by the UpdateManager each time a change happens (i.e UpdateInterface)
68  def changed(self, action):
69 
70  # Handle change
71  if action.key and action.key[0] in ["clips", "effects"] and action.type in ["update", "insert"]:
72  log.info(action.values)
73  # Update the model data
74  self.update_model(get_app().window.txtPropertyFilter.text())
75 
76  # Update the selected item (which drives what properties show up)
77  def update_item(self, item_id, item_type):
78  # Keep track of id and type
79  self.next_item_id = item_id
80  self.next_item_type = item_type
81 
82  # Update the model data
83  self.update_timer.start()
84 
85  # Update the next item (once the timer runs out)
87  # Get the next item id, and type
88  item_id = self.next_item_id
89  item_type = self.next_item_type
90 
91  # Clear previous selection
92  self.selected = []
94 
95  log.info("Update item: %s" % item_type)
96 
97  if item_type == "clip":
98  c = None
99  clips = get_app().window.timeline_sync.timeline.Clips()
100  for clip in clips:
101  if clip.Id() == item_id:
102  c = clip
103  break
104 
105  # Append to selected clips
106  self.selected.append((c, item_type))
107 
108  if item_type == "transition":
109  t = None
110  trans = get_app().window.timeline_sync.timeline.Effects()
111  for tran in trans:
112  if tran.Id() == item_id:
113  t = tran
114  break
115 
116  # Append to selected clips
117  self.selected.append((t, item_type))
118 
119  if item_type == "effect":
120  e = None
121  clips = get_app().window.timeline_sync.timeline.Clips()
122  for clip in clips:
123  for effect in clip.Effects():
124  if effect.Id() == item_id:
125  e = effect
126  break
127 
128  # Filter out basic properties, since this is an effect on a clip
129  self.filter_base_properties = ["position", "layer", "start", "end", "duration"]
130 
131  # Append to selected items
132  self.selected.append((e, item_type))
133 
134 
135  # Get ID of item
136  self.new_item = True
137 
138  # Update the model data
139  self.update_model(get_app().window.txtPropertyFilter.text())
140 
141  # Update the values of the selected clip, based on the current frame
142  def update_frame(self, frame_number):
143 
144  # Check for a selected clip
145  if self.selected:
146  clip, item_type = self.selected[0]
147 
148  if not clip:
149  # Ignore null clip
150  return
151 
152  # If effect, find the position of the parent clip
153  if item_type == "effect":
154  # find parent clip
155  parent_clip_id = Effect.get(id=clip.Id()).parent["id"]
156 
157  # Find this clip object
158  clips = get_app().window.timeline_sync.timeline.Clips()
159  for c in clips:
160  if c.Id() == parent_clip_id:
161  # Override the selected clip object (so the effect gets the correct starting position)
162  clip = c
163  break
164 
165  # Get FPS from project
166  fps = get_app().project.get(["fps"])
167  fps_float = float(fps["num"]) / float(fps["den"])
168 
169  # Requested time
170  requested_time = float(frame_number - 1) / fps_float
171 
172  # Determine the frame needed for this clip (based on the position on the timeline)
173  time_diff = (requested_time - clip.Position()) + clip.Start()
174  self.frame_number = int(time_diff * fps_float) + 1
175 
176  # Calculate biggest and smallest possible frames
177  min_frame_number = int((clip.Start() * fps_float)) + 1
178  max_frame_number = int((clip.End() * fps_float)) + 1
179 
180  # Adjust frame number if out of range
181  if self.frame_number < min_frame_number:
182  self.frame_number = min_frame_number
183  if self.frame_number > max_frame_number:
184  self.frame_number = max_frame_number
185 
186  log.info("Update frame to %s" % self.frame_number)
187 
188  # Update the model data
189  self.update_model(get_app().window.txtPropertyFilter.text())
190 
191  ##
192  # Remove an existing keyframe (if any)
193  def remove_keyframe(self, item):
194 
195  # Determine what was changed
196  property = self.model.item(item.row(), 0).data()
197  property_name = property[1]["name"]
198  property_type = property[1]["type"]
199  closest_point_x = property[1]["closest_point_x"]
200  property_type = property[1]["type"]
201  property_key = property[0]
202  clip_id, item_type = item.data()
203 
204  # Find this clip
205  c = None
206  clip_updated = False
207 
208  if item_type == "clip":
209  # Get clip object
210  c = Clip.get(id=clip_id)
211  elif item_type == "transition":
212  # Get transition object
213  c = Transition.get(id=clip_id)
214  elif item_type == "effect":
215  # Get effect object
216  c = Effect.get(id=clip_id)
217 
218  if c:
219  # Update clip attribute
220  if property_key in c.data:
221  log.info("remove keyframe: %s" % c.data)
222 
223  # Determine type of keyframe (normal or color)
224  keyframe_list = []
225  if property_type == "color":
226  keyframe_list = [c.data[property_key]["red"], c.data[property_key]["blue"], c.data[property_key]["green"]]
227  else:
228  keyframe_list = [c.data[property_key]]
229 
230  # Loop through each keyframe (red, blue, and green)
231  for keyframe in keyframe_list:
232 
233  # Keyframe
234  # Loop through points, find a matching points on this frame
235  closest_point = None
236  point_to_delete = None
237  for point in keyframe["Points"]:
238  if point["co"]["X"] == self.frame_number:
239  # Found point, Update value
240  clip_updated = True
241  point_to_delete = point
242  break
243  if point["co"]["X"] == closest_point_x:
244  closest_point = point
245 
246  # If no point found, use closest point x
247  if not point_to_delete:
248  point_to_delete = closest_point
249 
250  # Delete point (if needed)
251  if point_to_delete:
252  clip_updated = True
253  log.info("Found point to delete at X=%s" % point_to_delete["co"]["X"])
254  keyframe["Points"].remove(point_to_delete)
255 
256  # Reduce # of clip properties we are saving (performance boost)
257  c.data = {property_key: c.data[property_key]}
258 
259  # Save changes
260  if clip_updated:
261  # Save
262  c.save()
263 
264  # Update the preview
265  get_app().window.refreshFrameSignal.emit()
266 
267  # Clear selection
268  self.parent.clearSelection()
269 
270  ##
271  # Insert/Update a color keyframe for the selected row
272  def color_update(self, item, new_color, interpolation=-1):
273 
274  # Determine what was changed
275  property = self.model.item(item.row(), 0).data()
276  property_type = property[1]["type"]
277  closest_point_x = property[1]["closest_point_x"]
278  property_key = property[0]
279  clip_id, item_type = item.data()
280 
281  if property_type == "color":
282  # Find this clip
283  c = None
284  clip_updated = False
285 
286  if item_type == "clip":
287  # Get clip object
288  c = Clip.get(id=clip_id)
289  elif item_type == "transition":
290  # Get transition object
291  c = Transition.get(id=clip_id)
292  elif item_type == "effect":
293  # Get effect object
294  c = Effect.get(id=clip_id)
295 
296  if c:
297  # Update clip attribute
298  if property_key in c.data:
299  log.info("color update: %s" % c.data)
300 
301  # Loop through each keyframe (red, blue, and green)
302  for color, new_value in [("red", new_color.red()), ("blue", new_color.blue()), ("green", new_color.green())]:
303 
304  # Keyframe
305  # Loop through points, find a matching points on this frame
306  found_point = False
307  for point in c.data[property_key][color]["Points"]:
308  log.info("looping points: co.X = %s" % point["co"]["X"])
309  if interpolation == -1 and point["co"]["X"] == self.frame_number:
310  # Found point, Update value
311  found_point = True
312  clip_updated = True
313  # Update point
314  point["co"]["Y"] = new_value
315  log.info("updating point: co.X = %s to value: %s" % (point["co"]["X"], float(new_value)))
316  break
317 
318  elif interpolation > -1 and point["co"]["X"] == closest_point_x:
319  # Only update interpolation type
320  found_point = True
321  clip_updated = True
322  point["interpolation"] = interpolation
323  log.info("updating interpolation mode point: co.X = %s to %s" % (point["co"]["X"], interpolation))
324  break
325 
326  # Create new point (if needed)
327  if not found_point:
328  clip_updated = True
329  log.info("Created new point at X=%s" % self.frame_number)
330  c.data[property_key][color]["Points"].append({'co': {'X': self.frame_number, 'Y': new_value}, 'interpolation': 1})
331 
332  # Reduce # of clip properties we are saving (performance boost)
333  c.data = {property_key: c.data[property_key]}
334 
335  # Save changes
336  if clip_updated:
337  # Save
338  c.save()
339 
340  # Update the preview
341  get_app().window.refreshFrameSignal.emit()
342 
343  # Clear selection
344  self.parent.clearSelection()
345 
346  ##
347  # Table cell change event - also handles context menu to update interpolation value
348  def value_updated(self, item, interpolation=-1, value=None):
349 
350  if self.ignore_update_signal:
351  return
352 
353  # Get translation method
354  _ = get_app()._tr
355 
356  # Determine what was changed
357  property = self.model.item(item.row(), 0).data()
358  property_name = property[1]["name"]
359  closest_point_x = property[1]["closest_point_x"]
360  property_type = property[1]["type"]
361  property_key = property[0]
362  clip_id, item_type = item.data()
363 
364  # Get value (if any)
365  if item.text():
366  # Set and format value based on property type
367  if value != None:
368  # Override value
369  new_value = value
370  elif property_type == "string":
371  # Use string value
372  new_value = item.text()
373  elif property_type == "bool":
374  # Use boolean value
375  if item.text() == _("False"):
376  new_value = False
377  else:
378  new_value = True
379  elif property_type == "int":
380  # Use int value
381  new_value = QLocale().system().toInt(item.text())[0]
382  else:
383  # Use decimal value
384  new_value = QLocale().system().toFloat(item.text())[0]
385  else:
386  new_value = None
387 
388  log.info("%s for %s changed to %s at frame %s with interpolation: %s at closest x: %s" % (property_key, clip_id, new_value, self.frame_number, interpolation, closest_point_x))
389 
390 
391  # Find this clip
392  c = None
393  clip_updated = False
394 
395  if item_type == "clip":
396  # Get clip object
397  c = Clip.get(id=clip_id)
398  elif item_type == "transition":
399  # Get transition object
400  c = Transition.get(id=clip_id)
401  elif item_type == "effect":
402  # Get effect object
403  c = Effect.get(id=clip_id)
404 
405  if c:
406  # Update clip attribute
407  if property_key in c.data:
408  log.info("value updated: %s" % c.data)
409 
410  # Check the type of property (some are keyframe, and some are not)
411  if type(c.data[property_key]) == dict:
412  # Keyframe
413  # Loop through points, find a matching points on this frame
414  found_point = False
415  point_to_delete = None
416  for point in c.data[property_key]["Points"]:
417  log.info("looping points: co.X = %s" % point["co"]["X"])
418  if interpolation == -1 and point["co"]["X"] == self.frame_number:
419  # Found point, Update value
420  found_point = True
421  clip_updated = True
422  # Update or delete point
423  if new_value != None:
424  point["co"]["Y"] = float(new_value)
425  log.info("updating point: co.X = %s to value: %s" % (point["co"]["X"], float(new_value)))
426  else:
427  point_to_delete = point
428  break
429 
430  elif interpolation > -1 and point["co"]["X"] == closest_point_x:
431  # Only update interpolation type
432  found_point = True
433  clip_updated = True
434  point["interpolation"] = interpolation
435  log.info("updating interpolation mode point: co.X = %s to %s" % (point["co"]["X"], interpolation))
436  break
437 
438  # Delete point (if needed)
439  if point_to_delete:
440  clip_updated = True
441  log.info("Found point to delete at X=%s" % point_to_delete["co"]["X"])
442  c.data[property_key]["Points"].remove(point_to_delete)
443 
444  # Create new point (if needed)
445  elif not found_point and new_value != None:
446  clip_updated = True
447  log.info("Created new point at X=%s" % self.frame_number)
448  c.data[property_key]["Points"].append({'co': {'X': self.frame_number, 'Y': new_value}, 'interpolation': 1})
449 
450  elif property_type == "int":
451  # Integer
452  clip_updated = True
453  c.data[property_key] = int(new_value)
454 
455  elif property_type == "float":
456  # Float
457  clip_updated = True
458  c.data[property_key] = new_value
459 
460  elif property_type == "bool":
461  # Boolean
462  clip_updated = True
463  c.data[property_key] = bool(new_value)
464 
465  elif property_type == "string":
466  # String
467  clip_updated = True
468  c.data[property_key] = str(new_value)
469 
470 
471  # Reduce # of clip properties we are saving (performance boost)
472  c.data = {property_key: c.data.get(property_key)}
473 
474  # Save changes
475  if clip_updated:
476  # Save
477  c.save()
478 
479  # Update the preview
480  get_app().window.refreshFrameSignal.emit()
481 
482  # Clear selection
483  self.parent.clearSelection()
484 
485  def update_model(self, filter=""):
486  log.info("updating clip properties model.")
487  app = get_app()
488  _ = app._tr
489 
490  # Stop QTimer
491  self.update_timer.stop()
492 
493  # Check for a selected clip
494  if self.selected and self.selected[0]:
495  c, item_type = self.selected[0]
496 
497  # Skip blank clips
498  # TODO: Determine why c is occasional = None
499  if not c:
500  return
501 
502  # Get raw unordered JSON properties
503  raw_properties = json.loads(c.PropertiesJSON(self.frame_number))
504  all_properties = OrderedDict(sorted(raw_properties.items(), key=lambda x: x[1]['name']))
505  log.info("Getting properties for frame %s: %s" % (self.frame_number, str(all_properties)))
506 
507  # Check if filter was changed (if so, wipe previous model data)
508  if self.previous_filter != filter:
509  self.previous_filter = filter
510  self.new_item = True # filter changed, so we need to regenerate the entire model
511 
512  # Ignore any events from this method
514 
515  # Clear previous model data (if item is different)
516  if self.new_item:
517  # Prepare for new properties
518  self.items = {}
519  self.model.clear()
520 
521  # Add Headers
522  self.model.setHorizontalHeaderLabels([_("Property"), _("Value")])
523 
524 
525  # Loop through properties, and build a model
526  for property in all_properties.items():
527  label = property[1]["name"]
528  name = property[0]
529  value = property[1]["value"]
530  type = property[1]["type"]
531  memo = property[1]["memo"]
532  readonly = property[1]["readonly"]
533  keyframe = property[1]["keyframe"]
534  points = property[1]["points"]
535  interpolation = property[1]["interpolation"]
536  closest_point_x = property[1]["closest_point_x"]
537  choices = property[1]["choices"]
538 
539  # Adding Transparency to translation file
540  transparency_label = _("Transparency")
541 
542  selected_choice = None
543  if choices:
544  selected_choice = [c for c in choices if c["selected"] == True][0]["name"]
545 
546  # Hide filtered out properties
547  if filter and filter.lower() not in name.lower():
548  continue
549 
550  # Hide unused base properties (if any)
551  if name in self.filter_base_properties:
552  continue
553 
554  # Insert new data into model, or update existing values
555  row = []
556  if self.new_item:
557 
558  # Append Property Name
559  col = QStandardItem("Property")
560  col.setText(_(label))
561  col.setData(property)
562  if keyframe and points > 1:
563  col.setBackground(QColor("green")) # Highlight keyframe background
564  elif points > 1:
565  col.setBackground(QColor(42, 130, 218)) # Highlight interpolated value background
566  if readonly:
567  col.setFlags(Qt.ItemIsEnabled)
568  else:
569  col.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled | Qt.ItemIsUserCheckable)
570  row.append(col)
571 
572  # Append Value
573  col = QStandardItem("Value")
574  if selected_choice:
575  col.setText(_(selected_choice))
576  elif type == "string":
577  # Use string value
578  col.setText(memo)
579  elif type == "bool":
580  # Use boolean value
581  if value:
582  col.setText(_("True"))
583  else:
584  col.setText(_("False"))
585  elif type == "color":
586  # Don't output a value for colors
587  col.setText("")
588  elif type == "int":
589  col.setText("%d" % value)
590  else:
591  # Use numeric value
592  col.setText(QLocale().system().toString(float(value), "f", precision=2))
593  col.setData((c.Id(), item_type))
594  if points > 1:
595  # Apply icon to cell
596  my_icon = QPixmap(os.path.join(info.IMAGES_PATH, "keyframe-%s.png" % interpolation))
597  col.setData(my_icon, Qt.DecorationRole)
598 
599  log.info(os.path.join(info.IMAGES_PATH, "keyframe-%s.png" % interpolation))
600 
601  # Set the background color of the cell
602  if keyframe:
603  col.setBackground(QColor("green")) # Highlight keyframe background
604  else:
605  col.setBackground(QColor(42, 130, 218)) # Highlight interpolated value background
606 
607  if type == "color":
608  # Color needs to be handled special
609  red = property[1]["red"]["value"]
610  green = property[1]["green"]["value"]
611  blue = property[1]["blue"]["value"]
612  col.setBackground(QColor(red, green, blue))
613 
614  if readonly or type == "color" or choices:
615  col.setFlags(Qt.ItemIsEnabled)
616  else:
617  col.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled | Qt.ItemIsUserCheckable | Qt.ItemIsEditable)
618  row.append(col)
619 
620  # Append ROW to MODEL (if does not already exist in model)
621  self.model.appendRow(row)
622 
623  else:
624  # Update the value of the existing model
625  # Get 1st Column
626  col = self.items[name]["row"][0]
627  col.setData(property)
628 
629  # For non-color types, update the background color
630  if keyframe and points > 1:
631  col.setBackground(QColor("green")) # Highlight keyframe background
632  elif points > 1:
633  col.setBackground(QColor(42, 130, 218)) # Highlight interpolated value background
634  else:
635  col.setBackground(QStandardItem("Empty").background())
636 
637  # Update helper dictionary
638  row.append(col)
639 
640  # Get 2nd Column
641  col = self.items[name]["row"][1]
642  if selected_choice:
643  col.setText(_(selected_choice))
644  elif type == "string":
645  # Use string value
646  col.setText(memo)
647  elif type == "bool":
648  # Use boolean value
649  if value:
650  col.setText(_("True"))
651  else:
652  col.setText(_("False"))
653  elif type == "color":
654  # Don't output a value for colors
655  col.setText("")
656  elif type == "int":
657  col.setText("%d" % value)
658  else:
659  # Use numeric value
660  col.setText(QLocale().system().toString(float(value), "f", precision=2))
661 
662  if points > 1:
663  # Apply icon to cell
664  my_icon = QPixmap(os.path.join(info.IMAGES_PATH, "keyframe-%s.png" % interpolation))
665  col.setData(my_icon, Qt.DecorationRole)
666 
667  # Set the background color of the cell
668  if keyframe:
669  col.setBackground(QColor("green")) # Highlight keyframe background
670  else:
671  col.setBackground(QColor(42, 130, 218)) # Highlight interpolated value background
672 
673  else:
674  # clear background color
675  col.setBackground(QStandardItem("Empty").background())
676 
677  # clear icon
678  my_icon = QPixmap()
679  col.setData(my_icon, Qt.DecorationRole)
680 
681  if type == "color":
682  # Update the color based on the color curves
683  red = property[1]["red"]["value"]
684  green = property[1]["green"]["value"]
685  blue = property[1]["blue"]["value"]
686  col.setBackground(QColor(red, green, blue))
687 
688  # Update helper dictionary
689  row.append(col)
690 
691  # Keep track of items in a dictionary (for quick look up)
692  self.items[name] = {"row": row, "property": property}
693 
694  # Update the values on the next call to this method (instead of adding rows)
695  self.new_item = False
696 
697  else:
698  # Clear previous properties hash
699  self.previous_hash = ""
700 
701  # Clear previous model data (if any)
702  self.model.clear()
703 
704  # Add Headers
705  self.model.setHorizontalHeaderLabels([_("Property"), _("Value")])
706 
707 
708  # Done updating model
709  self.ignore_update_signal = False
710 
711  def __init__(self, parent, *args):
712 
713  # Keep track of the selected items (clips, transitions, etc...)
714  self.selected = []
715  self.current_item_id = None
716  self.frame_number = 1
717  self.previous_hash = ""
718  self.new_item = True
719  self.items = {}
720  self.ignore_update_signal = False
721  self.parent = parent
722  self.previous_filter = None
723  self.filter_base_properties = []
724 
725  # Create standard model
727  self.model.setColumnCount(2)
728 
729  # Timer to use a delay before showing properties (to prevent a mass selection from trying
730  # to update the property model hundreds of times)
731  self.update_timer = QTimer()
732  self.update_timer.setInterval(100)
733  self.update_timer.timeout.connect(self.update_item_timeout)
734  self.update_timer.stop()
735  self.next_item_id = None
736  self.next_item_type = None
737 
738  # Connect data changed signal
739  self.model.itemChanged.connect(self.value_updated)
740 
741  # Add self as listener to project data updates (used to update the timeline)
742  get_app().updates.add_listener(self)
def get_app()
Returns the current QApplication instance of OpenShot.
Definition: app.py:54
def value_updated(self, item, interpolation=-1, value=None)
Table cell change event - also handles context menu to update interpolation value.
def color_update(self, item, new_color, interpolation=-1)
Insert/Update a color keyframe for the selected row.
def __init__(self, parent, args)
def remove_keyframe(self, item)
Remove an existing keyframe (if any)
Interface for classes that listen for changes (insert, update, and delete).
Definition: updates.py:51
def update_item(self, item_id, item_type)
def update_frame(self, frame_number)