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-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 functools import partial
31 from operator import itemgetter
32 from PyQt5.QtCore import Qt, QRectF, QLocale, pyqtSignal, Qt, QObject, QTimer
33 from PyQt5.QtGui import *
34 from PyQt5.QtWidgets import QTableView, QAbstractItemView, QMenu, QSizePolicy, QHeaderView, QColorDialog, QItemDelegate, QStyle, QLabel, QPushButton, QHBoxLayout, QFrame
35 
36 from classes.logger import log
37 from classes.app import get_app
38 from classes import info
39 from classes.query import Clip, Effect, Transition, File
40 from windows.models.properties_model import PropertiesModel
41 from windows.models.transition_model import TransitionsModel
42 from windows.models.files_model import FilesModel
43 
44 import openshot
45 
46 try:
47  import json
48 except ImportError:
49  import simplejson as json
50 
51 
52 class PropertyDelegate(QItemDelegate):
53  def __init__(self, parent=None, *args):
54  QItemDelegate.__init__(self, parent, *args)
55 
56  # pixmaps for curve icons
57  self.curve_pixmaps = { openshot.BEZIER: QPixmap(os.path.join(info.IMAGES_PATH, "keyframe-%s.png" % openshot.BEZIER)),
58  openshot.LINEAR: QPixmap(os.path.join(info.IMAGES_PATH, "keyframe-%s.png" % openshot.LINEAR)),
59  openshot.CONSTANT: QPixmap(os.path.join(info.IMAGES_PATH, "keyframe-%s.png" % openshot.CONSTANT))
60  }
61 
62  def paint(self, painter, option, index):
63  painter.save()
64  painter.setRenderHint(QPainter.Antialiasing)
65 
66  # Get data model and selection
67  model = get_app().window.propertyTableView.clip_properties_model.model
68  row = model.itemFromIndex(index).row()
69  selected_label = model.item(row, 0)
70  selected_value = model.item(row, 1)
71  property = selected_label.data()
72 
73  # Get min/max values for this property
74  property_name = property[1]["name"]
75  property_type = property[1]["type"]
76  property_max = property[1]["max"]
77  property_min = property[1]["min"]
78  readonly = property[1]["readonly"]
79  keyframe = property[1]["keyframe"]
80  points = property[1]["points"]
81  interpolation = property[1]["interpolation"]
82 
83  # Calculate percentage value
84  if property_type in ["float", "int"]:
85  # Get the current value
86  current_value = QLocale().system().toDouble(selected_value.text())[0]
87 
88  # Shift my range to be positive
89  if property_min < 0.0:
90  property_shift = 0.0 - property_min
91  property_min += property_shift
92  property_max += property_shift
93  current_value += property_shift
94 
95  # Calculate current value as % of min/max range
96  min_max_range = float(property_max) - float(property_min)
97  value_percent = current_value / min_max_range
98  else:
99  value_percent = 0.0
100 
101  # set background color
102  painter.setPen(QPen(Qt.NoPen))
103  if property_type == "color":
104  # Color keyframe
105  red = property[1]["red"]["value"]
106  green = property[1]["green"]["value"]
107  blue = property[1]["blue"]["value"]
108  painter.setBrush(QBrush(QColor(QColor(red, green, blue))))
109  else:
110  # Normal Keyframe
111  if option.state & QStyle.State_Selected:
112  painter.setBrush(QBrush(QColor("#575757")))
113  else:
114  painter.setBrush(QBrush(QColor("#3e3e3e")))
115 
116  if not readonly:
117  path = QPainterPath()
118  path.addRoundedRect(QRectF(option.rect), 15, 15)
119  painter.fillPath(path, QColor("#3e3e3e"))
120  painter.drawPath(path)
121 
122  # Render mask rectangle
123  painter.setBrush(QBrush(QColor("#000000")))
124  mask_rect = QRectF(option.rect)
125  mask_rect.setWidth(option.rect.width() * value_percent)
126  painter.setClipRect(mask_rect, Qt.IntersectClip)
127 
128  # gradient for value box
129  gradient = QLinearGradient(option.rect.topLeft(), option.rect.topRight())
130  gradient.setColorAt(0, QColor("#828282"))
131  gradient.setColorAt(1, QColor("#828282"))
132 
133  # Render progress
134  painter.setBrush(gradient)
135  path = QPainterPath()
136  value_rect = QRectF(option.rect)
137  path.addRoundedRect(value_rect, 15, 15);
138  painter.fillPath(path, gradient)
139  painter.drawPath(path);
140  painter.setClipping(False)
141 
142  if points > 1:
143  # Draw interpolation icon on top
144  painter.drawPixmap(option.rect.x() + option.rect.width() - 30.0, option.rect.y() + 4, self.curve_pixmaps[interpolation])
145 
146  # set text color
147  painter.setPen(QPen(Qt.white))
148  value = index.data(Qt.DisplayRole)
149  if value:
150  painter.drawText(option.rect, Qt.AlignCenter, value)
151 
152  painter.restore()
153 
154 
155 ##
156 # A Properties Table QWidget used on the main window
157 class PropertiesTableView(QTableView):
158  loadProperties = pyqtSignal(str, str)
159 
160  def mouseMoveEvent(self, event):
161  # Get data model and selection
162  model = self.clip_properties_model.model
163  row = self.indexAt(event.pos()).row()
164  column = self.indexAt(event.pos()).column()
165  if model.item(row, 0):
166  self.selected_label = model.item(row, 0)
167  self.selected_item = model.item(row, 1)
168 
169  # Is the user dragging on the value column
170  if self.selected_label and self.selected_item:
171  frame_number = self.clip_properties_model.frame_number
172 
173  # Get the position of the cursor and % value
174  value_column_x = self.columnViewportPosition(1)
175  value_column_y = value_column_x + self.columnWidth(1)
176  cursor_value = event.x() - value_column_x
177  cursor_value_percent = cursor_value / self.columnWidth(1)
178 
179  try:
180  property = self.selected_label.data()
181  except Exception as ex:
182  # If item is deleted during this drag... an exception can occur
183  # Just ignore, since this is harmless
184  return
185 
186  property_key = property[0]
187  property_name = property[1]["name"]
188  property_type = property[1]["type"]
189  property_max = property[1]["max"]
190  property_min = property[1]["min"]
191  property_value = property[1]["value"]
192  readonly = property[1]["readonly"]
193  item_id, item_type = self.selected_item.data()
194 
195  # Bail if readonly
196  if readonly:
197  return
198 
199  # Get the original data of this item (prior to any updates, for the undo/redo system)
200  if not self.original_data:
201  # Ignore undo/redo history temporarily (to avoid a huge pile of undo/redo history)
202  get_app().updates.ignore_history = True
203 
204  # Find this clip
205  c = None
206  if item_type == "clip":
207  # Get clip object
208  c = Clip.get(id=item_id)
209  elif item_type == "transition":
210  # Get transition object
211  c = Transition.get(id=item_id)
212  elif item_type == "effect":
213  # Get effect object
214  c = Effect.get(id=item_id)
215 
216  if c:
217  if property_key in c.data:
218  # Grab the original data for this item/property
219  self.original_data = c.data
220 
221  # Calculate percentage value
222  if property_type in ["float", "int"] and property_name != "Track":
223  min_max_range = float(property_max) - float(property_min)
224 
225  # Determine if range is unreasonably long (such as position, start, end, etc.... which can be huge #'s)
226  if min_max_range > 1000.0:
227  # Get the current value
228  self.new_value = QLocale().system().toDouble(self.selected_item.text())[0]
229 
230  # Huge range - increment / decrement slowly
231  if self.previous_x == -1:
232  # init previous_x for the first time
233  self.previous_x = event.x()
234  # calculate # of pixels dragged
235  drag_diff = self.previous_x - event.x()
236  if drag_diff > 0:
237  # Move to the left by a small amount
238  self.new_value -= 0.50
239  elif drag_diff < 0:
240  # Move to the right by a small amount
241  self.new_value += 0.50
242  # update previous x
243  self.previous_x = event.x()
244  else:
245  # Small range - use cursor % to calculate new value
246  self.new_value = property_min + (min_max_range * cursor_value_percent)
247 
248  # Clamp value between min and max (just incase user drags too big)
249  self.new_value = max(property_min, self.new_value)
250  self.new_value = min(property_max, self.new_value)
251 
252  # Update value of this property
253  self.clip_properties_model.value_updated(self.selected_item, -1, self.new_value)
254 
255  # Repaint
256  self.viewport().update()
257 
258  def mouseReleaseEvent(self, event):
259  # Inform UpdateManager to accept updates, and only store our final update
260  get_app().updates.ignore_history = False
261 
262  # Add final update to undo/redo history
263  get_app().updates.apply_last_action_to_history(self.original_data)
264 
265  # Clear original data
266  self.original_data = None
267 
268  # Get data model and selection
269  model = self.clip_properties_model.model
270  row = self.indexAt(event.pos()).row()
271  column = self.indexAt(event.pos()).column()
272  if model.item(row, 0):
273  self.selected_label = model.item(row, 0)
274  self.selected_item = model.item(row, 1)
275 
276  ##
277  # Double click handler for the property table
278  def doubleClickedCB(self, model_index):
279 
280  # Get translation object
281  _ = get_app()._tr
282 
283  # Get data model and selection
284  model = self.clip_properties_model.model
285 
286  row = model_index.row()
287  selected_label = model.item(row, 0)
288  self.selected_item = model.item(row, 1)
289 
290  if selected_label:
291  property = selected_label.data()
292  property_type = property[1]["type"]
293 
294  if property_type == "color":
295  # Get current value of color
296  red = property[1]["red"]["value"]
297  green = property[1]["green"]["value"]
298  blue = property[1]["blue"]["value"]
299 
300  # Show color dialog
301  currentColor = QColor(red, green, blue)
302  newColor = QColorDialog.getColor(currentColor, self, _("Select a Color"),
303  QColorDialog.DontUseNativeDialog)
304 
305  # Set the new color keyframe
306  self.clip_properties_model.color_update(self.selected_item, newColor)
307 
308  ##
309  # Update the selected item in the properties window
310  def select_item(self, item_id, item_type):
311 
312  # Get translation object
313  _ = get_app()._tr
314 
315  # Update item
316  self.clip_properties_model.update_item(item_id, item_type)
317 
318  ##
319  # Update the values of the selected clip, based on the current frame
320  def select_frame(self, frame_number):
321 
322  # Update item
323  self.clip_properties_model.update_frame(frame_number)
324 
325  ##
326  # Filter the list of properties
327  def filter_changed(self, value=None):
328 
329  # Update model
330  self.clip_properties_model.update_model(value)
331 
332  def contextMenuEvent(self, event):
333  # Get data model and selection
334  model = self.clip_properties_model.model
335  row = self.indexAt(event.pos()).row()
336  selected_label = model.item(row, 0)
337  selected_value = model.item(row, 1)
338  self.selected_item = selected_value
339  frame_number = self.clip_properties_model.frame_number
340 
341  # Get translation object
342  _ = get_app()._tr
343 
344  # If item selected
345  if selected_label:
346  # Get data from selected item
347  property = selected_label.data()
348  property_name = property[1]["name"]
349  self.property_type = property[1]["type"]
350  memo = json.loads(property[1]["memo"] or "{}")
351  points = property[1]["points"]
352  self.choices = property[1]["choices"]
353  property_key = property[0]
354  clip_id, item_type = selected_value.data()
355  log.info("Context menu shown for %s (%s) for clip %s on frame %s" % (property_name, property_key, clip_id, frame_number))
356  log.info("Points: %s" % points)
357  log.info("Property: %s" % str(property))
358 
359  # Handle reader type values
360  if self.property_type == "reader" and not self.choices:
361 
362  # Refresh models
363  self.transition_model.update_model()
364  self.files_model.update_model()
365 
366  # Add all files
367  file_choices = []
368  for filesIndex in range(self.files_model.model.rowCount()):
369  modelIndex = self.files_model.model.index(filesIndex, 0)
370  fileItem = self.files_model.model.itemFromIndex(modelIndex)
371  fileIcon = self.files_model.model.item(fileItem.row(), 0).icon()
372  fileName = self.files_model.model.item(fileItem.row(), 1).text()
373  fileParentPath = self.files_model.model.item(fileItem.row(), 4).text()
374 
375  # Append file choice
376  file_choices.append({"name": fileName, "value": os.path.join(fileParentPath, fileName), "selected": False, "icon": fileIcon })
377 
378  # Add root file choice
379  self.choices.append({"name": _("Files"), "value": file_choices, "selected": False})
380 
381  # Add all transitions
382  trans_choices = []
383  for transIndex in range(self.transition_model.model.rowCount()):
384  modelIndex = self.transition_model.model.index(transIndex, 0)
385  transItem = self.transition_model.model.itemFromIndex(modelIndex)
386  transIcon = self.transition_model.model.item(transItem.row(), 0).icon()
387  transName = self.transition_model.model.item(transItem.row(), 1).text()
388  transPath = self.transition_model.model.item(transItem.row(), 3).text()
389 
390  # Append transition choice
391  trans_choices.append({"name": transName, "value": transPath, "selected": False, "icon": transIcon })
392 
393  # Add root transitions choice
394  self.choices.append({"name": _("Transitions"), "value": trans_choices, "selected": False})
395 
396  # Handle reader type values
397  if property_name =="Track" and self.property_type == "int" and not self.choices:
398  # Populate all display track names
399  track_choices = []
400  all_tracks = get_app().project.get(["layers"])
401  display_count = len(all_tracks)
402  for track in reversed(sorted(all_tracks, key=itemgetter('number'))):
403  # Append track choice
404  track_name = track.get("label") or _("Track %s") % display_count
405  self.choices.append({"name": track_name, "value": track.get("number"), "selected": False })
406  display_count -= 1
407 
408  # Define bezier presets
409  bezier_presets = [
410  (0.250, 0.100, 0.250, 1.000, _("Ease (Default)")),
411  (0.420, 0.000, 1.000, 1.000, _("Ease In")),
412  (0.000, 0.000, 0.580, 1.000, _("Ease Out")),
413  (0.420, 0.000, 0.580, 1.000, _("Ease In/Out")),
414 
415  (0.550, 0.085, 0.680, 0.530, _("Ease In (Quad)")),
416  (0.550, 0.055, 0.675, 0.190, _("Ease In (Cubic)")),
417  (0.895, 0.030, 0.685, 0.220, _("Ease In (Quart)")),
418  (0.755, 0.050, 0.855, 0.060, _("Ease In (Quint)")),
419  (0.470, 0.000, 0.745, 0.715, _("Ease In (Sine)")),
420  (0.950, 0.050, 0.795, 0.035, _("Ease In (Expo)")),
421  (0.600, 0.040, 0.980, 0.335, _("Ease In (Circ)")),
422  (0.600, -0.280, 0.735, 0.045, _("Ease In (Back)")),
423 
424  (0.250, 0.460, 0.450, 0.940, _("Ease Out (Quad)")),
425  (0.215, 0.610, 0.355, 1.000, _("Ease Out (Cubic)")),
426  (0.165, 0.840, 0.440, 1.000, _("Ease Out (Quart)")),
427  (0.230, 1.000, 0.320, 1.000, _("Ease Out (Quint)")),
428  (0.390, 0.575, 0.565, 1.000, _("Ease Out (Sine)")),
429  (0.190, 1.000, 0.220, 1.000, _("Ease Out (Expo)")),
430  (0.075, 0.820, 0.165, 1.000, _("Ease Out (Circ)")),
431  (0.175, 0.885, 0.320, 1.275, _("Ease Out (Back)")),
432 
433  (0.455, 0.030, 0.515, 0.955, _("Ease In/Out (Quad)")),
434  (0.645, 0.045, 0.355, 1.000, _("Ease In/Out (Cubic)")),
435  (0.770, 0.000, 0.175, 1.000, _("Ease In/Out (Quart)")),
436  (0.860, 0.000, 0.070, 1.000, _("Ease In/Out (Quint)")),
437  (0.445, 0.050, 0.550, 0.950, _("Ease In/Out (Sine)")),
438  (1.000, 0.000, 0.000, 1.000, _("Ease In/Out (Expo)")),
439  (0.785, 0.135, 0.150, 0.860, _("Ease In/Out (Circ)")),
440  (0.680, -0.550, 0.265, 1.550, _("Ease In/Out (Back)"))
441  ]
442 
443  bezier_icon = QIcon(QPixmap(os.path.join(info.IMAGES_PATH, "keyframe-%s.png" % openshot.BEZIER)))
444  linear_icon = QIcon(QPixmap(os.path.join(info.IMAGES_PATH, "keyframe-%s.png" % openshot.LINEAR)))
445  constant_icon = QIcon(QPixmap(os.path.join(info.IMAGES_PATH, "keyframe-%s.png" % openshot.CONSTANT)))
446 
447  # Add menu options for keyframes
448  menu = QMenu(self)
449  if points > 1:
450  # Menu for more than 1 point
451  Bezier_Menu = QMenu(_("Bezier"), self)
452  Bezier_Menu.setIcon(bezier_icon)
453  for bezier_preset in bezier_presets:
454  preset_action = Bezier_Menu.addAction(bezier_preset[4])
455  preset_action.triggered.connect(partial(self.Bezier_Action_Triggered, bezier_preset))
456  menu.addMenu(Bezier_Menu)
457  Linear_Action = menu.addAction(_("Linear"))
458  Linear_Action.setIcon(linear_icon)
459  Linear_Action.triggered.connect(self.Linear_Action_Triggered)
460  Constant_Action = menu.addAction(_("Constant"))
461  Constant_Action.setIcon(constant_icon)
462  Constant_Action.triggered.connect(self.Constant_Action_Triggered)
463  menu.addSeparator()
464  Insert_Action = menu.addAction(_("Insert Keyframe"))
465  Insert_Action.triggered.connect(self.Insert_Action_Triggered)
466  Remove_Action = menu.addAction(_("Remove Keyframe"))
467  Remove_Action.triggered.connect(self.Remove_Action_Triggered)
468  menu.popup(QCursor.pos())
469  elif points == 1:
470  # Menu for a single point
471  Insert_Action = menu.addAction(_("Insert Keyframe"))
472  Insert_Action.triggered.connect(self.Insert_Action_Triggered)
473  Remove_Action = menu.addAction(_("Remove Keyframe"))
474  Remove_Action.triggered.connect(self.Remove_Action_Triggered)
475  menu.popup(QCursor.pos())
476 
477  if self.choices:
478  # Menu for choices
479  for choice in self.choices:
480  if type(choice["value"]) != list:
481  # Add root choice items
482  Choice_Action = menu.addAction(_(choice["name"]))
483  Choice_Action.setData(choice["value"])
484  Choice_Action.triggered.connect(self.Choice_Action_Triggered)
485  else:
486  # Add sub-choice items (for nested choice lists)
487  # Divide into smaller QMenus (since large lists cover the entire screen)
488  # For example: Transitions -> 1 -> sub items
489  SubMenu = None
490  SubMenuRoot = QMenu(_(choice["name"]), self)
491  SubMenuSize = 24
492  SubMenuNumber = 0
493  for sub_choice in choice["value"]:
494  if len(choice["value"]) > SubMenuSize:
495  if not SubMenu or len(SubMenu.children()) == SubMenuSize or sub_choice == choice["value"][-1]:
496  SubMenuNumber += 1
497  if SubMenu:
498  SubMenuRoot.addMenu(SubMenu)
499  SubMenu = QMenu(str(SubMenuNumber), self)
500  Choice_Action = SubMenu.addAction(_(sub_choice["name"]))
501  else:
502  Choice_Action = SubMenuRoot.addAction(_(sub_choice["name"]))
503  Choice_Action.setData(sub_choice["value"])
504  subChoiceIcon = sub_choice.get("icon")
505  if subChoiceIcon:
506  Choice_Action.setIcon(subChoiceIcon)
507  Choice_Action.triggered.connect(self.Choice_Action_Triggered)
508  menu.addMenu(SubMenuRoot)
509 
510  # Show choice menu
511  menu.popup(QCursor.pos())
512 
513  def Bezier_Action_Triggered(self, preset=[]):
514  log.info("Bezier_Action_Triggered: %s" % str(preset))
515  if self.property_type != "color":
516  # Update keyframe interpolation mode
517  self.clip_properties_model.value_updated(self.selected_item, interpolation=0, interpolation_details=preset)
518  else:
519  # Update colors interpolation mode
520  self.clip_properties_model.color_update(self.selected_item, QColor("#000"), interpolation=0, interpolation_details=preset)
521 
522  def Linear_Action_Triggered(self, event):
523  log.info("Linear_Action_Triggered")
524  if self.property_type != "color":
525  # Update keyframe interpolation mode
526  self.clip_properties_model.value_updated(self.selected_item, interpolation=1)
527  else:
528  # Update colors interpolation mode
529  self.clip_properties_model.color_update(self.selected_item, QColor("#000"), interpolation=1, interpolation_details=[])
530 
531  def Constant_Action_Triggered(self, event):
532  log.info("Constant_Action_Triggered")
533  if self.property_type != "color":
534  # Update keyframe interpolation mode
535  self.clip_properties_model.value_updated(self.selected_item, interpolation=2)
536  else:
537  # Update colors interpolation mode
538  self.clip_properties_model.color_update(self.selected_item, QColor("#000"), interpolation=2, interpolation_details=[])
539 
540  def Insert_Action_Triggered(self, event):
541  log.info("Insert_Action_Triggered")
542  if self.selected_item:
543  current_value = QLocale().system().toDouble(self.selected_item.text())[0]
544  self.clip_properties_model.value_updated(self.selected_item, value=current_value)
545 
546  def Remove_Action_Triggered(self, event):
547  log.info("Remove_Action_Triggered")
548  self.clip_properties_model.remove_keyframe(self.selected_item)
549 
550  def Choice_Action_Triggered(self, event):
551  log.info("Choice_Action_Triggered")
552  choice_value = self.sender().data()
553 
554  # Update value of dropdown item
555  self.clip_properties_model.value_updated(self.selected_item, value=choice_value)
556 
557  def __init__(self, *args):
558  # Invoke parent init
559  QTableView.__init__(self, *args)
560 
561  # Get a reference to the window object
562  self.win = get_app().window
563 
564  # Get Model data
565  self.clip_properties_model = PropertiesModel(self)
566  self.transition_model = TransitionsModel(self)
567  self.files_model = FilesModel(self)
568 
569  # Keep track of mouse press start position to determine when to start drag
570  self.selected = []
571  self.selected_label = None
572  self.selected_item = None
573  self.new_value = None
574  self.original_data = None
575 
576  # Setup header columns
577  self.setModel(self.clip_properties_model.model)
578  self.setSelectionBehavior(QAbstractItemView.SelectRows)
579  self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
580  self.setWordWrap(True)
581 
582  # Set delegate
583  delegate = PropertyDelegate()
584  self.setItemDelegateForColumn(1, delegate)
585  self.previous_x = -1
586 
587  # Get table header
588  horizontal_header = self.horizontalHeader()
589  horizontal_header.setSectionResizeMode(QHeaderView.Stretch)
590  vertical_header = self.verticalHeader()
591  vertical_header.setVisible(False)
592 
593  # Refresh view
594  self.clip_properties_model.update_model()
595  self.transition_model.update_model()
596  self.files_model.update_model()
597 
598  # Resize columns
599  self.resizeColumnToContents(0)
600  self.resizeColumnToContents(1)
601 
602  # Connect filter signals
603  get_app().window.txtPropertyFilter.textChanged.connect(self.filter_changed)
604  get_app().window.InsertKeyframe.connect(self.Insert_Action_Triggered)
605  self.doubleClicked.connect(self.doubleClickedCB)
606  self.loadProperties.connect(self.select_item)
607 
608 
609 ##
610 # The label to display selections
611 class SelectionLabel(QFrame):
612 
613  def getMenu(self):
614  # Build menu for selection button
615  menu = QMenu(self)
616 
617  # Get translation object
618  _ = get_app()._tr
619 
620  # Look up item for more info
621  if self.item_type == "clip":
622  item = Clip.get(id=self.item_id)
623  if item:
624  self.item_name = item.title()
625  elif self.item_type == "transition":
626  item = Transition.get(id=self.item_id)
627  if item:
628  self.item_name = item.title()
629  elif self.item_type == "effect":
630  item = Effect.get(id=self.item_id)
631  if item:
632  self.item_name = item.title()
633 
634  # Bail if no item name was found
635  if not self.item_name:
636  return
637 
638  # Add selected clips
639  for item_id in get_app().window.selected_clips:
640  clip = Clip.get(id=item_id)
641  if clip:
642  item_name = clip.title()
643  item_icon = QIcon(QPixmap(clip.data.get('image')))
644  action = menu.addAction(item_name)
645  action.setIcon(item_icon)
646  action.setData({'item_id':item_id, 'item_type':'clip'})
647  action.triggered.connect(self.Action_Triggered)
648 
649  # Add effects for these clips (if any)
650  for effect in clip.data.get('effects'):
651  effect = Effect.get(id=effect.get('id'))
652  if effect:
653  item_name = effect.title()
654  item_icon = QIcon(QPixmap(os.path.join(info.PATH, "effects", "icons", "%s.png" % effect.data.get('class_name').lower())))
655  action = menu.addAction(' > %s' % _(item_name))
656  action.setIcon(item_icon)
657  action.setData({'item_id': effect.id, 'item_type': 'effect'})
658  action.triggered.connect(self.Action_Triggered)
659 
660  # Add selected transitions
661  for item_id in get_app().window.selected_transitions:
662  trans = Transition.get(id=item_id)
663  if trans:
664  item_name = _(trans.title())
665  item_icon = QIcon(QPixmap(trans.data.get('reader',{}).get('path')))
666  action = menu.addAction(_(item_name))
667  action.setIcon(item_icon)
668  action.setData({'item_id': item_id, 'item_type': 'transition'})
669  action.triggered.connect(self.Action_Triggered)
670 
671  # Add selected effects
672  for item_id in get_app().window.selected_effects:
673  effect = Effect.get(id=item_id)
674  if effect:
675  item_name = _(effect.title())
676  item_icon = QIcon(QPixmap(os.path.join(info.PATH, "effects", "icons", "%s.png" % effect.data.get('class_name').lower())))
677  action = menu.addAction(_(item_name))
678  action.setIcon(item_icon)
679  action.setData({'item_id': item_id, 'item_type': 'effect'})
680  action.triggered.connect(self.Action_Triggered)
681 
682  # Return the menu object
683  return menu
684 
685  def Action_Triggered(self, event):
686  # Switch selection
687  item_id = self.sender().data()['item_id']
688  item_type = self.sender().data()['item_type']
689  log.info('switch selection to %s:%s' % (item_id, item_type))
690 
691  # Set the property tableview to the new item
692  get_app().window.propertyTableView.loadProperties.emit(item_id, item_type)
693 
694  # Update selected item label
695  def select_item(self, item_id, item_type):
696  self.item_name = None
697  self.item_icon = None
698  self.item_type = item_type
699  self.item_id = item_id
700 
701  # Get translation object
702  _ = get_app()._tr
703 
704  # Look up item for more info
705  if self.item_type == "clip":
706  clip = Clip.get(id=self.item_id)
707  if clip:
708  self.item_name = clip.title()
709  self.item_icon = QIcon(QPixmap(clip.data.get('image')))
710  elif self.item_type == "transition":
711  trans = Transition.get(id=self.item_id)
712  if trans:
713  self.item_name = _(trans.title())
714  self.item_icon = QIcon(QPixmap(trans.data.get('reader', {}).get('path')))
715  elif self.item_type == "effect":
716  effect = Effect.get(id=self.item_id)
717  if effect:
718  self.item_name = _(effect.title())
719  self.item_icon = QIcon(QPixmap(os.path.join(info.PATH, "effects", "icons", "%s.png" % effect.data.get('class_name').lower())))
720 
721  # Truncate long text
722  if self.item_name and len(self.item_name) > 25:
723  self.item_name = "%s..." % self.item_name[:22]
724 
725  # Set label
726  if self.item_id:
727  self.lblSelection.setText("<strong>%s</strong>" % _("Selection:"))
728  self.btnSelectionName.setText(self.item_name)
729  self.btnSelectionName.setVisible(True)
730  if self.item_icon:
731  self.btnSelectionName.setIcon(self.item_icon)
732  else:
733  self.lblSelection.setText("<strong>%s</strong>" % _("No Selection"))
734  self.btnSelectionName.setVisible(False)
735 
736  # Set the menu on the button
737  self.btnSelectionName.setMenu(self.getMenu())
738 
739  def __init__(self, *args):
740  # Invoke parent init
741  QFrame.__init__(self, *args)
742  self.item_id = None
743  self.item_type = None
744 
745  # Get translation object
746  _ = get_app()._tr
747 
748  # Widgets
749  self.lblSelection = QLabel()
750  self.lblSelection.setText("<strong>%s</strong>" % _("No Selection"))
751  self.btnSelectionName = QPushButton()
752  self.btnSelectionName.setVisible(False)
753  self.btnSelectionName.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Minimum)
754 
755  # Support rich text
756  self.lblSelection.setTextFormat(Qt.RichText)
757 
758  hbox = QHBoxLayout()
759  hbox.setContentsMargins(0,0,0,0)
760  hbox.addWidget(self.lblSelection)
761  hbox.addWidget(self.btnSelectionName)
762  self.setLayout(hbox)
763 
764  # Connect signals
765  get_app().window.propertyTableView.loadProperties.connect(self.select_item)
def get_app
Returns the current QApplication instance of OpenShot.
Definition: app.py:55
def filter_changed
Filter the list of properties.
def select_item
Update the selected item in the properties window.
The label to display selections.
A Properties Table QWidget used on the main window.
def doubleClickedCB
Double click handler for the property table.
def select_frame
Update the values of the selected clip, based on the current frame.