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