OpenShot Video Editor  2.0.0
files_treeview.py
Go to the documentation of this file.
1 ##
2 #
3 # @file
4 # @brief This file contains the project file treeview, used by the main window
5 # @author Noah Figg <eggmunkee@hotmail.com>
6 # @author Jonathan Thomas <jonathan@openshot.org>
7 # @author Olivier Girard <eolinwen@gmail.com>
8 #
9 # @section LICENSE
10 #
11 # Copyright (c) 2008-2016 OpenShot Studios, LLC
12 # (http://www.openshotstudios.com). This file is part of
13 # OpenShot Video Editor (http://www.openshot.org), an open-source project
14 # dedicated to delivering high quality video editing and animation solutions
15 # to the world.
16 #
17 # OpenShot Video Editor is free software: you can redistribute it and/or modify
18 # it under the terms of the GNU General Public License as published by
19 # the Free Software Foundation, either version 3 of the License, or
20 # (at your option) any later version.
21 #
22 # OpenShot Video Editor is distributed in the hope that it will be useful,
23 # but WITHOUT ANY WARRANTY; without even the implied warranty of
24 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
25 # GNU General Public License for more details.
26 #
27 # You should have received a copy of the GNU General Public License
28 # along with OpenShot Library. If not, see <http://www.gnu.org/licenses/>.
29 #
30 
31 import os
32 import glob
33 import re
34 from urllib.parse import urlparse
35 
36 from PyQt5.QtCore import QSize, Qt, QPoint
37 from PyQt5.QtGui import *
38 from PyQt5.QtWidgets import QTreeView, QMessageBox, QAbstractItemView, QMenu, QSizePolicy
39 import openshot # Python module for libopenshot (required video editing module installed separately)
40 
41 from classes.query import File
42 from classes.logger import log
43 from classes.app import get_app
44 from windows.models.files_model import FilesModel
45 
46 try:
47  import json
48 except ImportError:
49  import simplejson as json
50 
51 
52 ##
53 # A TreeView QWidget used on the main window
54 class FilesTreeView(QTreeView):
55  drag_item_size = 48
56 
57  def updateSelection(self):
58 
59  # Track selected items
60  self.selected = self.selectionModel().selectedIndexes()
61 
62  # Track selected file ids on main window
63  rows = []
64  self.win.selected_files = []
65  for selection in self.selected:
66  selected_row = self.files_model.model.itemFromIndex(selection).row()
67  if selected_row not in rows:
68  self.win.selected_files.append(self.files_model.model.item(selected_row, 5).text())
69  rows.append(selected_row)
70 
71  def contextMenuEvent(self, event):
72  # Update selection
73  self.updateSelection()
74 
75  # Set context menu mode
76  app = get_app()
77  app.context_menu_object = "files"
78 
79  menu = QMenu(self)
80 
81  menu.addAction(self.win.actionImportFiles)
82  menu.addSeparator()
83  if self.selected:
84  # If file selected, show file related options
85  menu.addAction(self.win.actionFile_Properties)
86  menu.addAction(self.win.actionPreview_File)
87  menu.addAction(self.win.actionSplitClip)
88  menu.addAction(self.win.actionAdd_to_Timeline)
89  menu.addSeparator()
90  #menu.addAction(self.win.actionFile_Properties)
91  menu.addAction(self.win.actionRemove_from_Project)
92  menu.addSeparator()
93  menu.addAction(self.win.actionThumbnailView)
94 
95  # Show menu
96  menu.exec_(QCursor.pos())
97 
98  def dragEnterEvent(self, event):
99  # If dragging urls onto widget, accept
100  if event.mimeData().hasUrls():
101  event.setDropAction(Qt.CopyAction)
102  event.accept()
103 
104  ##
105  # Override startDrag method to display custom icon
106  def startDrag(self, event):
107 
108  # Get image of selected item
109  selected_row = self.files_model.model.itemFromIndex(self.selectionModel().selectedIndexes()[0]).row()
110  icon = self.files_model.model.item(selected_row, 0).icon()
111 
112  # Start drag operation
113  drag = QDrag(self)
114  drag.setMimeData(self.files_model.model.mimeData(self.selectionModel().selectedIndexes()))
115  drag.setPixmap(icon.pixmap(QSize(self.drag_item_size, self.drag_item_size)))
116  drag.setHotSpot(QPoint(self.drag_item_size / 2, self.drag_item_size / 2))
117  drag.exec_()
118 
119  # Without defining this method, the 'copy' action doesn't show with cursor
120  def dragMoveEvent(self, event):
121  pass
122 
123  def is_image(self, file):
124  path = file["path"].lower()
125 
126  if path.endswith((".jpg", ".jpeg", ".png", ".bmp", ".svg", ".thm", ".gif", ".bmp", ".pgm", ".tif", ".tiff")):
127  return True
128  else:
129  return False
130 
131  def add_file(self, filepath):
132  path, filename = os.path.split(filepath)
133 
134  # Add file into project
135  app = get_app()
136  _ = get_app()._tr
137 
138  # Check for this path in our existing project data
139  file = File.get(path=filepath)
140 
141  # If this file is already found, exit
142  if file:
143  return
144 
145  # Load filepath in libopenshot clip object (which will try multiple readers to open it)
146  clip = openshot.Clip(filepath)
147 
148  # Get the JSON for the clip's internal reader
149  try:
150  reader = clip.Reader()
151  file_data = json.loads(reader.Json())
152 
153  # Determine media type
154  if file_data["has_video"] and not self.is_image(file_data):
155  file_data["media_type"] = "video"
156  elif file_data["has_video"] and self.is_image(file_data):
157  file_data["media_type"] = "image"
158  elif file_data["has_audio"] and not file_data["has_video"]:
159  file_data["media_type"] = "audio"
160 
161  # Save new file to the project data
162  file = File()
163  file.data = file_data
164 
165  # Is this file an image sequence / animation?
166  image_seq_details = self.get_image_sequence_details(filepath)
167  if image_seq_details:
168  # Update file with correct path
169  folder_path = image_seq_details["folder_path"]
170  file_name = image_seq_details["file_path"]
171  base_name = image_seq_details["base_name"]
172  fixlen = image_seq_details["fixlen"]
173  digits = image_seq_details["digits"]
174  extension = image_seq_details["extension"]
175 
176  if not fixlen:
177  zero_pattern = "%d"
178  else:
179  zero_pattern = "%%0%sd" % digits
180 
181  # Generate the regex pattern for this image sequence
182  pattern = "%s%s.%s" % (base_name, zero_pattern, extension)
183 
184  # Split folder name
185  (parentPath, folderName) = os.path.split(folder_path)
186  if not base_name:
187  # Give alternate name
188  file.data["name"] = "%s (%s)" % (folderName, pattern)
189 
190  # Load image sequence (to determine duration and video_length)
191  image_seq = openshot.Clip(os.path.join(folder_path, pattern))
192 
193  # Update file details
194  file.data["path"] = os.path.join(folder_path, pattern)
195  file.data["media_type"] = "video"
196  file.data["duration"] = image_seq.Reader().info.duration
197  file.data["video_length"] = image_seq.Reader().info.video_length
198 
199  # Save file
200  file.save()
201  return True
202 
203  except:
204  # Handle exception
205  msg = QMessageBox()
206  msg.setText(_("{} is not a valid video, audio, or image file.".format(filename)))
207  msg.exec_()
208  return False
209 
210  ##
211  # Inspect a file path and determine if this is an image sequence
212  def get_image_sequence_details(self, file_path):
213 
214  # Get just the file name
215  (dirName, fileName) = os.path.split(file_path)
216  extensions = ["png", "jpg", "jpeg", "gif", "tif"]
217  match = re.findall(r"(.*[^\d])?(0*)(\d+)\.(%s)" % "|".join(extensions), fileName, re.I)
218 
219  if not match:
220  # File name does not match an image sequence
221  return None
222  else:
223  # Get the parts of image name
224  base_name = match[0][0]
225  fixlen = match[0][1] > ""
226  number = int(match[0][2])
227  digits = len(match[0][1] + match[0][2])
228  extension = match[0][3]
229 
230  full_base_name = os.path.join(dirName, base_name)
231 
232  # Check for images which the file names have the different length
233  fixlen = fixlen or not (glob.glob("%s%s.%s" % (full_base_name, "[0-9]" * (digits + 1), extension))
234  or glob.glob(
235  "%s%s.%s" % (full_base_name, "[0-9]" * ((digits - 1) if digits > 1 else 3), extension)))
236 
237  # Check for previous or next image
238  for x in range(max(0, number - 100), min(number + 101, 50000)):
239  if x != number and os.path.exists("%s%s.%s" % (
240  full_base_name, str(x).rjust(digits, "0") if fixlen else str(x), extension)):
241  is_sequence = True
242  break
243  else:
244  is_sequence = False
245 
246  if is_sequence and dirName not in self.ignore_image_sequence_paths:
247  log.info('Prompt user to import image sequence')
248  # Ignore this path (temporarily)
249  self.ignore_image_sequence_paths.append(dirName)
250 
251  # Translate object
252  _ = get_app()._tr
253 
254  # Handle exception
255  ret = QMessageBox.question(self, _("Import Image Sequence"), _("Would you like to import %s as an image sequence?") % fileName, QMessageBox.No | QMessageBox.Yes)
256  if ret == QMessageBox.Yes:
257  # Yes, import image sequence
258  parameters = {"file_path":file_path, "folder_path":dirName, "base_name":base_name, "fixlen":fixlen, "digits":digits, "extension":extension}
259  return parameters
260  else:
261  return None
262  else:
263  return None
264 
265  # Handle a drag and drop being dropped on widget
266  def dropEvent(self, event):
267  # Reset list of ignored image sequences paths
269 
270  # log.info('Dropping file(s) on files tree.')
271  for uri in event.mimeData().urls():
272  file_url = urlparse(uri.toString())
273  if file_url.scheme == "file":
274  filepath = file_url.path
275  if filepath[0] == "/" and ":" in filepath:
276  filepath = filepath[1:]
277  if os.path.exists(filepath.encode('UTF-8')) and os.path.isfile(filepath.encode('UTF-8')):
278  log.info('Adding file: {}'.format(filepath))
279  if self.add_file(filepath):
280  event.accept()
281 
282  def clear_filter(self):
283  if self:
284  self.win.filesFilter.setText("")
285 
286  def filter_changed(self):
287  if self:
288  if self.win.filesFilter.text() == "":
289  self.win.actionFilesClear.setEnabled(False)
290  else:
291  self.win.actionFilesClear.setEnabled(True)
292  self.refresh_view()
293 
294  def refresh_view(self):
295  self.files_model.update_model()
296 
297  ##
298  # Resize and hide certain columns
299  def refresh_columns(self):
300  if type(self) == FilesTreeView:
301  # Only execute when the treeview is active
302  self.hideColumn(3)
303  self.hideColumn(4)
304  self.hideColumn(5)
305  self.resize_contents()
306 
307  def resize_contents(self):
308  self.resizeColumnToContents(2)
309  self.resizeColumnToContents(1)
310 
311  def currentChanged(self, selected, deselected):
312  log.info('currentChanged')
313  self.updateSelection()
314 
315  ##
316  # Name or tags updated
317  def value_updated(self, item):
318  # Get translation method
319  _ = get_app()._tr
320 
321  # Determine what was changed
322  file_id = self.files_model.model.item(item.row(), 5).text()
323  name = self.files_model.model.item(item.row(), 1).text()
324  tags = self.files_model.model.item(item.row(), 2).text()
325 
326  # Get file object and update friendly name and tags attribute
327  f = File.get(id=file_id)
328  if name != f.data["path"]:
329  f.data["name"] = name
330  else:
331  f.data["name"] = ""
332  if "tags" in f.data.keys():
333  if tags != f.data["tags"]:
334  f.data["tags"] = tags
335  elif tags:
336  f.data["tags"] = tags
337 
338  # Tell file model to ignore updates (since this treeview will already be updated)
339  self.files_model.ignore_update_signal = True
340 
341  # Save File
342  f.save()
343 
344  # Re-enable updates
345  self.files_model.ignore_update_signal = False
346 
347  def __init__(self, *args):
348  # Invoke parent init
349  QTreeView.__init__(self, *args)
350 
351  # Get a reference to the window object
352  self.win = get_app().window
353 
354  # Get Model data
355  self.files_model = FilesModel()
356 
357  # Keep track of mouse press start position to determine when to start drag
358  self.setAcceptDrops(True)
359  self.setDragEnabled(True)
360  self.setDropIndicatorShown(True)
361  self.selected = []
362 
363  # Setup header columns
364  self.setModel(self.files_model.model)
365  self.setIconSize(QSize(75, 62))
366  self.setIndentation(0)
367  self.setSelectionBehavior(QTreeView.SelectRows)
368  self.setSelectionMode(QAbstractItemView.ExtendedSelection)
369  self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
370  self.setWordWrap(True)
371  self.setStyleSheet('QTreeView::item { padding-top: 2px; }')
372  self.files_model.model.ModelRefreshed.connect(self.refresh_columns)
373 
374  # Refresh view
375  self.refresh_view()
376  self.refresh_columns()
377 
378  # setup filter events
379  app = get_app()
380  app.window.filesFilter.textChanged.connect(self.filter_changed)
381  app.window.actionFilesClear.triggered.connect(self.clear_filter)
382  self.files_model.model.itemChanged.connect(self.value_updated)
def startDrag(self, event)
Override startDrag method to display custom icon.
def get_app()
Returns the current QApplication instance of OpenShot.
Definition: app.py:54
def contextMenuEvent(self, event)
A TreeView QWidget used on the main window.
def refresh_columns(self)
Resize and hide certain columns.
def get_image_sequence_details(self, file_path)
Inspect a file path and determine if this is an image sequence.
def currentChanged(self, selected, deselected)
def add_file(self, filepath)
def dragEnterEvent(self, event)
def value_updated(self, item)
Name or tags updated.
def dragMoveEvent(self, event)