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