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