OpenShot Video Editor  2.0.0
properties_tableview.py
Go to the documentation of this file.
1 ##
2 #
3 # @file
4 # @brief This file contains the properties tableview, used by the main window
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 PyQt5.QtCore import Qt, QRectF, QLocale, pyqtSignal, Qt, QObject, QTimer
31 from PyQt5.QtGui import *
32 from PyQt5.QtWidgets import QTableView, QAbstractItemView, QMenu, QSizePolicy, QHeaderView, QColorDialog, QItemDelegate, QStyle, QLabel, QPushButton, QHBoxLayout, QFrame
33 
34 from classes.logger import log
35 from classes.app import get_app
36 from classes import info
37 from classes.query import Clip, Effect, Transition
38 from windows.models.properties_model import PropertiesModel
39 
40 import openshot
41 
42 try:
43  import json
44 except ImportError:
45  import simplejson as json
46 
47 
48 class PropertyDelegate(QItemDelegate):
49  def __init__(self, parent=None, *args):
50  QItemDelegate.__init__(self, parent, *args)
51 
52  # pixmaps for curve icons
53  self.curve_pixmaps = { openshot.BEZIER: QPixmap(os.path.join(info.IMAGES_PATH, "keyframe-%s.png" % openshot.BEZIER)),
54  openshot.LINEAR: QPixmap(os.path.join(info.IMAGES_PATH, "keyframe-%s.png" % openshot.LINEAR)),
55  openshot.CONSTANT: QPixmap(os.path.join(info.IMAGES_PATH, "keyframe-%s.png" % openshot.CONSTANT))
56  }
57 
58  def paint(self, painter, option, index):
59  painter.save()
60  painter.setRenderHint(QPainter.Antialiasing)
61 
62  # Get data model and selection
63  model = get_app().window.propertyTableView.clip_properties_model.model
64  row = model.itemFromIndex(index).row()
65  selected_label = model.item(row, 0)
66  selected_value = model.item(row, 1)
67  property = selected_label.data()
68 
69  # Get min/max values for this property
70  property_name = property[1]["name"]
71  property_type = property[1]["type"]
72  property_max = property[1]["max"]
73  property_min = property[1]["min"]
74  readonly = property[1]["readonly"]
75  keyframe = property[1]["keyframe"]
76  points = property[1]["points"]
77  interpolation = property[1]["interpolation"]
78 
79  # Calculate percentage value
80  if property_type in ["float", "int"]:
81  # Get the current value
82  current_value = QLocale().system().toDouble(selected_value.text())[0]
83 
84  # Shift my range to be positive
85  if property_min < 0.0:
86  property_shift = 0.0 - property_min
87  property_min += property_shift
88  property_max += property_shift
89  current_value += property_shift
90 
91  # Calculate current value as % of min/max range
92  min_max_range = float(property_max) - float(property_min)
93  value_percent = current_value / min_max_range
94  else:
95  value_percent = 0.0
96 
97  # set background color
98  painter.setPen(QPen(Qt.NoPen))
99  if property_type == "color":
100  # Color keyframe
101  red = property[1]["red"]["value"]
102  green = property[1]["green"]["value"]
103  blue = property[1]["blue"]["value"]
104  painter.setBrush(QBrush(QColor(QColor(red, green, blue))))
105  else:
106  # Normal Keyframe
107  if option.state & QStyle.State_Selected:
108  painter.setBrush(QBrush(QColor("#575757")))
109  else:
110  painter.setBrush(QBrush(QColor("#3e3e3e")))
111 
112  if not readonly:
113  path = QPainterPath()
114  path.addRoundedRect(QRectF(option.rect), 15, 15)
115  painter.fillPath(path, QColor("#3e3e3e"))
116  painter.drawPath(path)
117 
118  # Render mask rectangle
119  painter.setBrush(QBrush(QColor("#000000")))
120  mask_rect = QRectF(option.rect)
121  mask_rect.setWidth(option.rect.width() * value_percent)
122  painter.setClipRect(mask_rect, Qt.IntersectClip)
123 
124  # gradient for value box
125  gradient = QLinearGradient(option.rect.topLeft(), option.rect.topRight())
126  gradient.setColorAt(0, QColor("#828282"))
127  gradient.setColorAt(1, QColor("#828282"))
128 
129  # Render progress
130  painter.setBrush(gradient)
131  path = QPainterPath()
132  value_rect = QRectF(option.rect)
133  path.addRoundedRect(value_rect, 15, 15);
134  painter.fillPath(path, gradient)
135  painter.drawPath(path);
136  painter.setClipping(False)
137 
138  if points > 1:
139  # Draw interpolation icon on top
140  painter.drawPixmap(option.rect.x() + option.rect.width() - 30.0, option.rect.y() + 4, self.curve_pixmaps[interpolation])
141 
142  # set text color
143  painter.setPen(QPen(Qt.white))
144  value = index.data(Qt.DisplayRole)
145  if value:
146  painter.drawText(option.rect, Qt.AlignCenter, value)
147 
148  painter.restore()
149 
150 
151 ##
152 # A Properties Table QWidget used on the main window
153 class PropertiesTableView(QTableView):
154  loadProperties = pyqtSignal(str, str)
155 
156  def mouseMoveEvent(self, event):
157  # Get data model and selection
158  model = self.clip_properties_model.model
159  row = self.indexAt(event.pos()).row()
160  column = self.indexAt(event.pos()).column()
161  if model.item(row, 0):
162  self.selected_label = model.item(row, 0)
163  self.selected_item = model.item(row, 1)
164 
165  # Is the user dragging on the value column
166  if self.selected_label and self.selected_item:
167  frame_number = self.clip_properties_model.frame_number
168 
169  # Get the position of the cursor and % value
170  value_column_x = self.columnViewportPosition(1)
171  value_column_y = value_column_x + self.columnWidth(1)
172  cursor_value = event.x() - value_column_x
173  cursor_value_percent = cursor_value / self.columnWidth(1)
174 
175  property = self.selected_label.data()
176  property_name = property[1]["name"]
177  property_type = property[1]["type"]
178  property_max = property[1]["max"]
179  property_min = property[1]["min"]
180  property_value = property[1]["value"]
181  readonly = property[1]["readonly"]
182 
183  # Bail if readonly
184  if readonly:
185  return
186 
187  # Calculate percentage value
188  if property_type in ["float", "int"]:
189  min_max_range = float(property_max) - float(property_min)
190 
191  # Determine if range is unreasonably long (such as position, start, end, etc.... which can be huge #'s)
192  if min_max_range > 1000.0:
193  # Get the current value
194  new_value = QLocale().system().toDouble(self.selected_item.text())[0]
195 
196  # Huge range - increment / decrement slowly
197  if self.previous_x == -1:
198  # init previous_x for the first time
199  self.previous_x = event.x()
200  # calculate # of pixels dragged
201  drag_diff = self.previous_x - event.x()
202  if drag_diff > 0:
203  # Move to the left by a small amount
204  new_value -= 0.50
205  elif drag_diff < 0:
206  # Move to the right by a small amount
207  new_value += 0.50
208  # update previous x
209  self.previous_x = event.x()
210  else:
211  # Small range - use cursor % to calculate new value
212  new_value = property_min + (min_max_range * cursor_value_percent)
213 
214  # Clamp value between min and max (just incase user drags too big)
215  new_value = max(property_min, new_value)
216  new_value = min(property_max, new_value)
217 
218  # Update value of this property
219  self.clip_properties_model.value_updated(self.selected_item, -1, new_value)
220 
221  # Repaint
222  self.viewport().update()
223 
224 
225  ##
226  # Double click handler for the property table
227  def double_click(self, model_index):
228  # Get data model and selection
229  model = self.clip_properties_model.model
230 
231  row = model_index.row()
232  selected_label = model.item(row, 0)
233  self.selected_item = model.item(row, 1)
234 
235  if selected_label:
236  property = selected_label.data()
237  property_type = property[1]["type"]
238 
239  if property_type == "color":
240  # Get current value of color
241  red = property[1]["red"]["value"]
242  green = property[1]["green"]["value"]
243  blue = property[1]["blue"]["value"]
244 
245  # Show color dialog
246  currentColor = QColor(red, green, blue)
247  newColor = QColorDialog.getColor(currentColor)
248 
249  # Set the new color keyframe
250  self.clip_properties_model.color_update(self.selected_item, newColor)
251 
252  ##
253  # Update the selected item in the properties window
254  def select_item(self, item_id, item_type):
255 
256  # Get translation object
257  _ = get_app()._tr
258 
259  # Update item
260  self.clip_properties_model.update_item(item_id, item_type)
261 
262  ##
263  # Update the values of the selected clip, based on the current frame
264  def select_frame(self, frame_number):
265 
266  # Update item
267  self.clip_properties_model.update_frame(frame_number)
268 
269  ##
270  # Filter the list of properties
271  def filter_changed(self, value=None):
272 
273  # Update model
274  self.clip_properties_model.update_model(value)
275 
276  def contextMenuEvent(self, event):
277  # Get data model and selection
278  model = self.clip_properties_model.model
279  row = self.indexAt(event.pos()).row()
280  selected_label = model.item(row, 0)
281  selected_value = model.item(row, 1)
282  self.selected_item = selected_value
283  frame_number = self.clip_properties_model.frame_number
284 
285  # Get translation object
286  _ = get_app()._tr
287 
288  # If item selected
289  if selected_label:
290  # Get data from selected item
291  property = selected_label.data()
292  property_name = property[1]["name"]
293  self.property_type = property[1]["type"]
294  points = property[1]["points"]
295  self.choices = property[1]["choices"]
296  property_key = property[0]
297  clip_id, item_type = selected_value.data()
298 
299  log.info("Context menu shown for %s (%s) for clip %s on frame %s" % (
300  property_name, property_key, clip_id, frame_number))
301  log.info("Points: %s" % points)
302  log.info("Property: %s" % str(property))
303 
304  bezier_icon = QIcon(QPixmap(os.path.join(info.IMAGES_PATH, "keyframe-%s.png" % openshot.BEZIER)))
305  linear_icon = QIcon(QPixmap(os.path.join(info.IMAGES_PATH, "keyframe-%s.png" % openshot.LINEAR)))
306  constant_icon = QIcon(QPixmap(os.path.join(info.IMAGES_PATH, "keyframe-%s.png" % openshot.CONSTANT)))
307 
308  # Add menu options for keyframes
309  menu = QMenu(self)
310  if points > 1:
311  # Menu for more than 1 point
312  Bezier_Action = menu.addAction(_("Bezier"))
313  Bezier_Action.setIcon(bezier_icon)
314  Bezier_Action.triggered.connect(self.Bezier_Action_Triggered)
315  Linear_Action = menu.addAction(_("Linear"))
316  Linear_Action.setIcon(linear_icon)
317  Linear_Action.triggered.connect(self.Linear_Action_Triggered)
318  Constant_Action = menu.addAction(_("Constant"))
319  Constant_Action.setIcon(constant_icon)
320  Constant_Action.triggered.connect(self.Constant_Action_Triggered)
321  menu.addSeparator()
322  Remove_Action = menu.addAction(_("Remove Keyframe"))
323  Remove_Action.triggered.connect(self.Remove_Action_Triggered)
324  menu.popup(QCursor.pos())
325  elif points == 1:
326  # Menu for a single point
327  Remove_Action = menu.addAction(_("Remove Keyframe"))
328  Remove_Action.triggered.connect(self.Remove_Action_Triggered)
329  menu.popup(QCursor.pos())
330 
331  if self.choices:
332  # Menu for choices
333  for choice in self.choices:
334  Choice_Action = menu.addAction(_(choice["name"]))
335  Choice_Action.setData(choice["value"])
336  Choice_Action.triggered.connect(self.Choice_Action_Triggered)
337  # Show choice menu
338  menu.popup(QCursor.pos())
339 
340  def Bezier_Action_Triggered(self, event):
341  log.info("Bezier_Action_Triggered")
342  if self.property_type != "color":
343  # Update keyframe interpolation mode
344  self.clip_properties_model.value_updated(self.selected_item, 0)
345  else:
346  # Update colors interpolation mode
347  self.clip_properties_model.color_update(self.selected_item, QColor("#000"), 0)
348 
349  def Linear_Action_Triggered(self, event):
350  log.info("Linear_Action_Triggered")
351  if self.property_type != "color":
352  # Update keyframe interpolation mode
353  self.clip_properties_model.value_updated(self.selected_item, 1)
354  else:
355  # Update colors interpolation mode
356  self.clip_properties_model.color_update(self.selected_item, QColor("#000"), 1)
357 
358  def Constant_Action_Triggered(self, event):
359  log.info("Constant_Action_Triggered")
360  if self.property_type != "color":
361  # Update keyframe interpolation mode
362  self.clip_properties_model.value_updated(self.selected_item, 2)
363  else:
364  # Update colors interpolation mode
365  self.clip_properties_model.color_update(self.selected_item, QColor("#000"), 2)
366 
367  def Remove_Action_Triggered(self, event):
368  log.info("Remove_Action_Triggered")
369  self.clip_properties_model.remove_keyframe(self.selected_item)
370 
371  def Choice_Action_Triggered(self, event):
372  log.info("Choice_Action_Triggered")
373  choice_value = self.sender().data()
374 
375  # Update value of dropdown item
376  self.clip_properties_model.value_updated(self.selected_item, value=choice_value)
377 
378  def __init__(self, *args):
379  # Invoke parent init
380  QTableView.__init__(self, *args)
381 
382  # Get a reference to the window object
383  self.win = get_app().window
384 
385  # Get Model data
386  self.clip_properties_model = PropertiesModel(self)
387 
388  # Keep track of mouse press start position to determine when to start drag
389  self.selected = []
390  self.selected_item = None
391 
392  # Setup header columns
393  self.setModel(self.clip_properties_model.model)
394  self.setSelectionBehavior(QAbstractItemView.SelectRows)
395  self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
396  self.setWordWrap(True)
397 
398  # Set delegate
399  delegate = PropertyDelegate()
400  self.setItemDelegateForColumn(1, delegate)
401  self.previous_x = -1
402 
403  # Get table header
404  horizontal_header = self.horizontalHeader()
405  horizontal_header.setSectionResizeMode(QHeaderView.Stretch)
406  vertical_header = self.verticalHeader()
407  vertical_header.setVisible(False)
408 
409  # Refresh view
410  self.clip_properties_model.update_model()
411 
412  # Resize columns
413  self.resizeColumnToContents(0)
414  self.resizeColumnToContents(1)
415 
416  # Connect filter signals
417  get_app().window.txtPropertyFilter.textChanged.connect(self.filter_changed)
418  self.doubleClicked.connect(self.double_click)
419  self.loadProperties.connect(self.select_item)
420 
421 
422 ##
423 # The label to display selections
424 class SelectionLabel(QFrame):
425 
426  def getMenu(self):
427  # Build menu for selection button
428  menu = QMenu(self)
429 
430  # Get translation object
431  _ = get_app()._tr
432 
433  # Look up item for more info
434  if self.item_type == "clip":
435  self.item_name = Clip.get(id=self.item_id).title()
436  elif self.item_type == "transition":
437  self.item_name = Transition.get(id=self.item_id).title()
438  elif self.item_type == "effect":
439  self.item_name = Effect.get(id=self.item_id).title()
440 
441  # Add selected clips
442  for item_id in get_app().window.selected_clips:
443  clip = Clip.get(id=item_id)
444  item_name = clip.title()
445  item_icon = QIcon(QPixmap(clip.data.get('image')))
446  action = menu.addAction(item_name)
447  action.setIcon(item_icon)
448  action.setData({'item_id':item_id, 'item_type':'clip'})
449  action.triggered.connect(self.Action_Triggered)
450 
451  # Add effects for these clips (if any)
452  for effect in clip.data.get('effects'):
453  item_name = Effect.get(id=effect.get('id')).title()
454  item_icon = QIcon(QPixmap(os.path.join(info.PATH, "effects", "icons", "%s.png" % effect.get('class_name').lower())))
455  action = menu.addAction(' > %s' % _(item_name))
456  action.setIcon(item_icon)
457  action.setData({'item_id': effect.get('id'), 'item_type': 'effect'})
458  action.triggered.connect(self.Action_Triggered)
459 
460  # Add selected transitions
461  for item_id in get_app().window.selected_transitions:
462  trans = Transition.get(id=item_id)
463  item_name = _(trans.title())
464  item_icon = QIcon(QPixmap(trans.data.get('reader',{}).get('path')))
465  action = menu.addAction(_(item_name))
466  action.setIcon(item_icon)
467  action.setData({'item_id': item_id, 'item_type': 'transition'})
468  action.triggered.connect(self.Action_Triggered)
469 
470  # Add selected effects
471  for item_id in get_app().window.selected_effects:
472  effect = Effect.get(id=item_id)
473  item_name = _(effect.title())
474  item_icon = QIcon(QPixmap(os.path.join(info.PATH, "effects", "icons", "%s.png" % effect.data.get('class_name').lower())))
475  action = menu.addAction(_(item_name))
476  action.setIcon(item_icon)
477  action.setData({'item_id': item_id, 'item_type': 'effect'})
478  action.triggered.connect(self.Action_Triggered)
479 
480  # Return the menu object
481  return menu
482 
483  def Action_Triggered(self, event):
484  # Switch selection
485  item_id = self.sender().data()['item_id']
486  item_type = self.sender().data()['item_type']
487  log.info('switch selection to %s:%s' % (item_id, item_type))
488 
489  # Set the property tableview to the new item
490  get_app().window.propertyTableView.loadProperties.emit(item_id, item_type)
491 
492  def select_item(self, item_id, item_type):
493  # Keep track of id and type
494  self.next_item_id = item_id
495  self.next_item_type = item_type
496 
497  # Update the model data
498  self.update_timer.start()
499 
500  # Update the next item (once the timer runs out)
502  # Get the next item id, and type
503  self.item_id = self.next_item_id
504  self.item_type = self.next_item_type
505  self.item_name = None
506  self.item_icon = None
507 
508  # Stop timer
509  self.update_timer.stop()
510 
511  # Get translation object
512  _ = get_app()._tr
513 
514  # Look up item for more info
515  if self.item_type == "clip":
516  clip = Clip.get(id=self.item_id)
517  self.item_name = clip.title()
518  self.item_icon = QIcon(QPixmap(clip.data.get('image')))
519  elif self.item_type == "transition":
520  trans = Transition.get(id=self.item_id)
521  self.item_name = _(trans.title())
522  self.item_icon = QIcon(QPixmap(trans.data.get('reader', {}).get('path')))
523  elif self.item_type == "effect":
524  effect = Effect.get(id=self.item_id)
525  self.item_name = _(effect.title())
526  self.item_icon = QIcon(QPixmap(os.path.join(info.PATH, "effects", "icons", "%s.png" % effect.data.get('class_name').lower())))
527 
528  # Truncate long text
529  if self.item_name and len(self.item_name) > 25:
530  self.item_name = "%s..." % self.item_name[:22]
531 
532  # Set label
533  if self.item_id:
534  self.lblSelection.setText("<strong>%s</strong>" % _("Selection:"))
535  self.btnSelectionName.setText(self.item_name)
536  self.btnSelectionName.setVisible(True)
537  if self.item_icon:
538  self.btnSelectionName.setIcon(self.item_icon)
539  else:
540  self.lblSelection.setText("<strong>%s</strong>" % _("No Selection"))
541  self.btnSelectionName.setVisible(False)
542 
543  # Set the menu on the button
544  self.btnSelectionName.setMenu(self.getMenu())
545 
546  def __init__(self, *args):
547  # Invoke parent init
548  QFrame.__init__(self, *args)
549  self.item_id = None
550  self.item_type = None
551 
552  # Get translation object
553  _ = get_app()._tr
554 
555  # Widgets
556  self.lblSelection = QLabel()
557  self.lblSelection.setText("<strong>%s</strong>" % _("No Selection"))
558  self.btnSelectionName = QPushButton()
559  self.btnSelectionName.setVisible(False)
560  self.btnSelectionName.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Minimum)
561 
562  # Support rich text
563  self.lblSelection.setTextFormat(Qt.RichText)
564 
565  hbox = QHBoxLayout()
566  hbox.setContentsMargins(0,0,0,0)
567  hbox.addWidget(self.lblSelection)
568  hbox.addWidget(self.btnSelectionName)
569  self.setLayout(hbox)
570 
571  # Timer to use a delay before showing properties (to prevent a mass selection from trying
572  # to update the property model hundreds of times)
573  self.update_timer = QTimer()
574  self.update_timer.setInterval(100)
575  self.update_timer.timeout.connect(self.update_item_timeout)
576  self.update_timer.stop()
577  self.next_item_id = None
578  self.next_item_type = None
579 
580  # Connect signals
581  get_app().window.propertyTableView.loadProperties.connect(self.select_item)
def __init__(self, parent=None, args)
def get_app()
Returns the current QApplication instance of OpenShot.
Definition: app.py:54
def paint(self, painter, option, index)
def double_click(self, model_index)
Double click handler for the property table.
def select_item(self, item_id, item_type)
Update the selected item in the properties window.
def filter_changed(self, value=None)
Filter the list of properties.
The label to display selections.
def select_frame(self, frame_number)
Update the values of the selected clip, based on the current frame.
A Properties Table QWidget used on the main window.
def select_item(self, item_id, item_type)