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-2016 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 from urllib.parse import urlparse
34 
35 import openshot # Python module for libopenshot (required video editing module installed separately)
36 from PyQt5.QtCore import QSize, Qt, QPoint
37 from PyQt5.QtGui import *
38 from PyQt5.QtWidgets import QListView, QMessageBox, QAbstractItemView, QMenu
39 
40 from classes.app import get_app
41 from classes.logger import log
42 from classes.query import File
43 from windows.models.files_model import FilesModel
44 
45 try:
46  import json
47 except ImportError:
48  import simplejson as json
49 
50 
51 ##
52 # A ListView QWidget used on the main window
53 class FilesListView(QListView):
54  drag_item_size = 48
55 
56  def updateSelection(self):
57  log.info('updateSelection')
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.actionDetailsView)
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(QIcon.fromTheme('document-new').pixmap(QSize(self.drag_item_size,self.drag_item_size)))
116  drag.setPixmap(icon.pixmap(QSize(self.drag_item_size, self.drag_item_size)))
117  drag.setHotSpot(QPoint(self.drag_item_size / 2, self.drag_item_size / 2))
118  drag.exec_()
119 
120  # Without defining this method, the 'copy' action doesn't show with cursor
121  def dragMoveEvent(self, event):
122  pass
123 
124  def is_image(self, file):
125  path = file["path"].lower()
126 
127  if path.endswith((".jpg", ".jpeg", ".png", ".bmp", ".svg", ".thm", ".gif", ".bmp", ".pgm", ".tif", ".tiff")):
128  return True
129  else:
130  return False
131 
132  def add_file(self, filepath):
133  path, filename = os.path.split(filepath)
134 
135  # Add file into project
136  app = get_app()
137  _ = get_app()._tr
138 
139  # Check for this path in our existing project data
140  file = File.get(path=filepath)
141 
142  # If this file is already found, exit
143  if file:
144  return
145 
146  # Load filepath in libopenshot clip object (which will try multiple readers to open it)
147  clip = openshot.Clip(filepath)
148 
149  # Get the JSON for the clip's internal reader
150  try:
151  reader = clip.Reader()
152  file_data = json.loads(reader.Json())
153 
154  # Determine media type
155  if file_data["has_video"] and not self.is_image(file_data):
156  file_data["media_type"] = "video"
157  elif file_data["has_video"] and self.is_image(file_data):
158  file_data["media_type"] = "image"
159  elif file_data["has_audio"] and not file_data["has_video"]:
160  file_data["media_type"] = "audio"
161 
162  # Save new file to the project data
163  file = File()
164  file.data = file_data
165 
166  # Is this file an image sequence / animation?
167  image_seq_details = self.get_image_sequence_details(filepath)
168  if image_seq_details:
169  # Update file with correct path
170  folder_path = image_seq_details["folder_path"]
171  file_name = image_seq_details["file_path"]
172  base_name = image_seq_details["base_name"]
173  fixlen = image_seq_details["fixlen"]
174  digits = image_seq_details["digits"]
175  extension = image_seq_details["extension"]
176 
177  if not fixlen:
178  zero_pattern = "%d"
179  else:
180  zero_pattern = "%%0%sd" % digits
181 
182  # Generate the regex pattern for this image sequence
183  pattern = "%s%s.%s" % (base_name, zero_pattern, extension)
184 
185  # Split folder name
186  (parentPath, folderName) = os.path.split(folder_path)
187  if not base_name:
188  # Give alternate name
189  file.data["name"] = "%s (%s)" % (folderName, pattern)
190 
191  # Load image sequence (to determine duration and video_length)
192  image_seq = openshot.Clip(os.path.join(folder_path, pattern))
193 
194  # Update file details
195  file.data["path"] = os.path.join(folder_path, pattern)
196  file.data["media_type"] = "video"
197  file.data["duration"] = image_seq.Reader().info.duration
198  file.data["video_length"] = image_seq.Reader().info.video_length
199 
200  # Save file
201  file.save()
202  return True
203 
204  except:
205  # Handle exception
206  msg = QMessageBox()
207  msg.setText(_("{} is not a valid video, audio, or image file.".format(filename)))
208  msg.exec_()
209  return False
210 
211  ##
212  # Inspect a file path and determine if this is an image sequence
213  def get_image_sequence_details(self, file_path):
214 
215  # Get just the file name
216  (dirName, fileName) = os.path.split(file_path)
217  extensions = ["png", "jpg", "jpeg", "gif", "tif"]
218  match = re.findall(r"(.*[^\d])?(0*)(\d+)\.(%s)" % "|".join(extensions), fileName, re.I)
219 
220  if not match:
221  # File name does not match an image sequence
222  return None
223  else:
224  # Get the parts of image name
225  base_name = match[0][0]
226  fixlen = match[0][1] > ""
227  number = int(match[0][2])
228  digits = len(match[0][1] + match[0][2])
229  extension = match[0][3]
230 
231  full_base_name = os.path.join(dirName, base_name)
232 
233  # Check for images which the file names have the different length
234  fixlen = fixlen or not (glob.glob("%s%s.%s" % (full_base_name, "[0-9]" * (digits + 1), extension))
235  or glob.glob(
236  "%s%s.%s" % (full_base_name, "[0-9]" * ((digits - 1) if digits > 1 else 3), extension)))
237 
238  # Check for previous or next image
239  for x in range(max(0, number - 100), min(number + 101, 50000)):
240  if x != number and os.path.exists("%s%s.%s" % (
241  full_base_name, str(x).rjust(digits, "0") if fixlen else str(x), extension)):
242  is_sequence = True
243  break
244  else:
245  is_sequence = False
246 
247  if is_sequence and dirName not in self.ignore_image_sequence_paths:
248  log.info('Prompt user to import image sequence')
249  # Ignore this path (temporarily)
250  self.ignore_image_sequence_paths.append(dirName)
251 
252  # Translate object
253  _ = get_app()._tr
254 
255  # Handle exception
256  ret = QMessageBox.question(self, _("Import Image Sequence"), _("Would you like to import %s as an image sequence?") % fileName, QMessageBox.No | QMessageBox.Yes)
257  if ret == QMessageBox.Yes:
258  # Yes, import image sequence
259  parameters = {"file_path":file_path, "folder_path":dirName, "base_name":base_name, "fixlen":fixlen, "digits":digits, "extension":extension}
260  return parameters
261  else:
262  return None
263  else:
264  return None
265 
266  # Handle a drag and drop being dropped on widget
267  def dropEvent(self, event):
268  # Reset list of ignored image sequences paths
270 
271  # log.info('Dropping file(s) on files tree.')
272  for uri in event.mimeData().urls():
273  file_url = urlparse(uri.toString())
274  if file_url.scheme == "file":
275  filepath = file_url.path
276  if filepath[0] == "/" and ":" in filepath:
277  filepath = filepath[1:]
278  if os.path.exists(filepath.encode('UTF-8')) and os.path.isfile(filepath.encode('UTF-8')):
279  log.info('Adding file: {}'.format(filepath))
280  if self.add_file(filepath):
281  event.accept()
282 
283  def clear_filter(self):
284  if self:
285  self.win.filesFilter.setText("")
286 
287  def filter_changed(self):
288  if self:
289  if self.win.filesFilter.text() == "":
290  self.win.actionFilesClear.setEnabled(False)
291  else:
292  self.win.actionFilesClear.setEnabled(True)
293  self.refresh_view()
294 
295  def refresh_view(self):
296  self.files_model.update_model()
297 
298  def currentChanged(self, selected, deselected):
299  log.info('currentChanged')
300  self.updateSelection()
301 
302  def resize_contents(self):
303  pass
304 
305  def __init__(self, *args):
306  # Invoke parent init
307  QListView.__init__(self, *args)
308 
309  # Get a reference to the window object
310  self.win = get_app().window
311 
312  # Get Model data
313  self.files_model = FilesModel()
314  self.setAcceptDrops(True)
315  self.setDragEnabled(True)
316  self.setDropIndicatorShown(True)
317  self.selected = []
319 
320  # Setup header columns
321  self.setModel(self.files_model.model)
322  self.setIconSize(QSize(131, 108))
323  self.setViewMode(QListView.IconMode)
324  self.setResizeMode(QListView.Adjust)
325  self.setSelectionMode(QAbstractItemView.ExtendedSelection)
326  self.setUniformItemSizes(True)
327  self.setWordWrap(True)
328  self.setStyleSheet('QListView::item { padding-top: 2px; }')
329 
330  # Refresh view
331  self.refresh_view()
332 
333  # setup filter events
334  app = get_app()
335  app.window.filesFilter.textChanged.connect(self.filter_changed)
336  app.window.actionFilesClear.triggered.connect(self.clear_filter)
def get_app()
Returns the current QApplication instance of OpenShot.
Definition: app.py:54
def add_file(self, filepath)
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)