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