1
# -*- coding: utf-8 -*-
4
# pitivi/medialibrary.py
6
# Copyright (c) 2005, Edward Hervey <bilboed@bilboed.com>
7
# Copyright (c) 2009, Alessandro Decina <alessandro.d@gmail.com>
8
# Copyright (c) 2012, Jean-François Fortin Tam <nekohayo@gmail.com>
10
# This program is free software; you can redistribute it and/or
11
# modify it under the terms of the GNU Lesser General Public
12
# License as published by the Free Software Foundation; either
13
# version 2.1 of the License, or (at your option) any later version.
15
# This program is distributed in the hope that it will be useful,
16
# but WITHOUT ANY WARRANTY; without even the implied warranty of
17
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
18
# Lesser General Public License for more details.
20
# You should have received a copy of the GNU Lesser General Public
21
# License along with this program; if not, write to the
22
# Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
23
# Boston, MA 02110-1301, USA.
25
from gi.repository import Gst
26
from gi.repository import GES
27
from gi.repository import Gio
28
from gi.repository import GLib
29
from gi.repository import GObject
30
from gi.repository import Gtk
31
from gi.repository import Gdk
32
from gi.repository import Pango
33
from gi.repository import GdkPixbuf
39
from urllib import unquote
40
from gettext import ngettext, gettext as _
41
from urlparse import urlparse
42
from hashlib import md5
43
from gi.repository.GstPbutils import DiscovererVideoInfo
45
from pitivi.check import GNOMEDESKTOP_SOFT_DEPENDENCY
46
from pitivi.configure import get_ui_dir, get_pixmap_dir
47
from pitivi.settings import GlobalSettings
48
from pitivi.mediafilespreviewer import PreviewWidget
49
from pitivi.dialogs.filelisterrordialog import FileListErrorDialog
50
from pitivi.dialogs.clipmediaprops import ClipMediaPropsDialog
51
from pitivi.utils.ui import beautify_length
52
from pitivi.utils.misc import PathWalker, quote_uri, path_from_uri
53
from pitivi.utils.signal import SignalGroup
54
from pitivi.utils.loggable import Loggable
55
import pitivi.utils.ui as dnd
56
from pitivi.utils.ui import beautify_info, info_name, SPACING
58
from pitivi.utils.ui import TYPE_PITIVI_FILESOURCE
60
# Values used in the settings file.
64
GlobalSettings.addConfigSection('clip-library')
65
GlobalSettings.addConfigOption('lastImportFolder',
66
section='clip-library',
68
environment='PITIVI_IMPORT_FOLDER',
69
default=os.path.expanduser("~"))
70
GlobalSettings.addConfigOption('closeImportDialog',
71
section='clip-library',
72
key='close-import-dialog-after-import',
74
GlobalSettings.addConfigOption('lastClipView',
75
section='clip-library',
78
default=SHOW_ICONVIEW)
80
STORE_MODEL_STRUCTURE = (
81
GdkPixbuf.Pixbuf, GdkPixbuf.Pixbuf,
82
str, object, str, str, str)
90
COL_SEARCH_TEXT) = range(len(STORE_MODEL_STRUCTURE))
94
<accelerator action="RemoveSources" />
95
<accelerator action="InsertEnd" />
99
# This whitelist is made from personal knowledge of file extensions in the wild,
100
# from gst-inspect |grep demux,
101
# http://en.wikipedia.org/wiki/Comparison_of_container_formats and
102
# http://en.wikipedia.org/wiki/List_of_file_formats#Video
103
# ...and looking at the contents of /usr/share/mime
104
SUPPORTED_FILE_FORMATS = {"video": ("3gpp", "3gpp2", "dv", "mp2t", "mp4", "mpeg", "ogg", "quicktime", "webm", "x-flv", "x-matroska", "x-mng", "x-ms-asf", "x-msvideo", "x-ms-wmp", "x-ms-wmv", "x-ogm+ogg", "x-theora+ogg"),
105
"application": ("mxf",),
106
# Don't forget audio formats
107
"audio": ("aac", "ac3", "basic", "flac", "mp2", "mp4", "mpeg", "ogg", "opus", "webm", "x-adpcm", "x-aifc", "x-aiff", "x-aiffc", "x-ape", "x-flac+ogg", "x-m4b", "x-matroska", "x-ms-asx", "x-ms-wma", "x-speex", "x-speex+ogg", "x-vorbis+ogg", "x-wav"),
108
# ...and image formats
109
"image": ("jp2", "jpeg", "png", "svg+xml")}
110
# Stuff that we're not too confident about but might improve eventually:
111
OTHER_KNOWN_FORMATS = ("video/mp2t",)
114
class MediaLibraryWidget(Gtk.VBox, Loggable):
115
""" Widget for listing sources """
118
'play': (GObject.SignalFlags.RUN_LAST, None,
119
(GObject.TYPE_PYOBJECT,))}
121
def __init__(self, app, uiman):
122
Gtk.VBox.__init__(self)
123
Loggable.__init__(self)
125
self.pending_rows = []
129
self._missing_thumbs = []
131
self._draggedPaths = None
133
self.clip_view = self.app.settings.lastClipView
135
builder = Gtk.Builder()
136
builder.add_from_file(os.path.join(get_ui_dir(), "medialibrary.ui"))
137
builder.connect_signals(self)
138
self._welcome_infobar = builder.get_object("welcome_infobar")
139
self._import_warning_infobar = builder.get_object("warning_infobar")
140
self._warning_label = builder.get_object("warning_label")
141
self._view_error_button = builder.get_object("view_error_button")
142
toolbar = builder.get_object("medialibrary_toolbar")
143
toolbar.get_style_context().add_class(Gtk.STYLE_CLASS_INLINE_TOOLBAR)
144
self._remove_button = builder.get_object("media_remove_button")
145
self._clipprops_button = builder.get_object("media_props_button")
146
self._insert_button = builder.get_object("media_insert_button")
147
self._listview_button = builder.get_object("media_listview_button")
148
searchEntry = builder.get_object("media_search_entry")
151
self.storemodel = Gtk.ListStore(*STORE_MODEL_STRUCTURE)
152
self.storemodel.set_sort_func(COL_URI, MediaLibraryWidget.compare_basename)
153
# Prefer to sort the media library elements by URI
154
# rather than show them randomly.
155
self.storemodel.set_sort_column_id(COL_URI, Gtk.SortType.ASCENDING)
158
self.treeview_scrollwin = Gtk.ScrolledWindow()
159
self.treeview_scrollwin.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC)
160
self.treeview_scrollwin.set_shadow_type(Gtk.ShadowType.ETCHED_IN)
161
self.treeview_scrollwin.get_accessible().set_name("media_listview_scrollwindow")
163
self.iconview_scrollwin = Gtk.ScrolledWindow()
164
self.iconview_scrollwin.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
165
self.iconview_scrollwin.set_shadow_type(Gtk.ShadowType.ETCHED_IN)
166
self.iconview_scrollwin.get_accessible().set_name("media_iconview_scrollwindow")
168
# import sources dialogbox
169
self._importDialog = None
171
# Filtering model for the search box.
172
# Use this instead of using self.storemodel directly
173
self.modelFilter = self.storemodel.filter_new()
174
self.modelFilter.set_visible_func(self._setRowVisible, data=searchEntry)
177
# Displays icon, name, type, length
178
self.treeview = Gtk.TreeView(model=self.modelFilter)
179
self.treeview_scrollwin.add(self.treeview)
180
self.treeview.connect("button-press-event", self._treeViewButtonPressEventCb)
181
self.treeview.connect("button-release-event", self._treeViewButtonReleaseEventCb)
182
self.treeview.connect("row-activated", self._itemOrRowActivatedCb)
183
self.treeview.set_property("rules_hint", True)
184
self.treeview.set_headers_visible(False)
185
self.treeview.set_property("search_column", COL_SEARCH_TEXT)
186
tsel = self.treeview.get_selection()
187
tsel.set_mode(Gtk.SelectionMode.MULTIPLE)
188
tsel.connect("changed", self._viewSelectionChangedCb)
190
pixbufcol = Gtk.TreeViewColumn(_("Icon"))
191
pixbufcol.set_expand(False)
192
pixbufcol.set_spacing(SPACING)
193
self.treeview.append_column(pixbufcol)
194
pixcell = Gtk.CellRendererPixbuf()
195
pixcell.props.xpad = 6
196
pixbufcol.pack_start(pixcell, True)
197
pixbufcol.add_attribute(pixcell, 'pixbuf', COL_ICON_64)
199
namecol = Gtk.TreeViewColumn(_("Information"))
200
self.treeview.append_column(namecol)
201
namecol.set_expand(True)
202
namecol.set_spacing(SPACING)
203
namecol.set_sizing(Gtk.TreeViewColumnSizing.GROW_ONLY)
204
namecol.set_min_width(150)
205
txtcell = Gtk.CellRendererText()
206
txtcell.set_property("ellipsize", Pango.EllipsizeMode.END)
207
namecol.pack_start(txtcell, True)
208
namecol.add_attribute(txtcell, "markup", COL_INFOTEXT)
210
namecol = Gtk.TreeViewColumn(_("Duration"))
211
namecol.set_expand(False)
212
self.treeview.append_column(namecol)
213
txtcell = Gtk.CellRendererText()
214
txtcell.set_property("yalign", 0.0)
215
namecol.pack_start(txtcell, True)
216
namecol.add_attribute(txtcell, "markup", COL_LENGTH)
219
self.iconview = Gtk.IconView(model=self.modelFilter)
220
self.iconview_scrollwin.add(self.iconview)
221
self.iconview.connect("button-press-event", self._iconViewButtonPressEventCb)
222
self.iconview.connect("button-release-event", self._iconViewButtonReleaseEventCb)
223
self.iconview.connect("item-activated", self._itemOrRowActivatedCb)
224
self.iconview.connect("selection-changed", self._viewSelectionChangedCb)
225
self.iconview.set_item_orientation(Gtk.Orientation.VERTICAL)
226
self.iconview.set_property("has_tooltip", True)
227
self.iconview.set_tooltip_column(COL_INFOTEXT)
228
self.iconview.props.item_padding = 3
229
self.iconview.props.margin = 3
230
self.iconview_cursor_pos = None
232
cell = Gtk.CellRendererPixbuf()
233
self.iconview.pack_start(cell, False)
234
self.iconview.add_attribute(cell, "pixbuf", COL_ICON_128)
236
cell = Gtk.CellRendererText()
237
cell.props.alignment = Pango.Alignment.CENTER
238
cell.props.xalign = 0.5
239
cell.props.yalign = 0.0
242
cell.set_property("ellipsize", Pango.EllipsizeMode.END)
243
self.iconview.pack_start(cell, False)
244
self.iconview.add_attribute(cell, "markup", COL_SEARCH_TEXT)
246
self.iconview.set_selection_mode(Gtk.SelectionMode.MULTIPLE)
248
# The _progressbar that shows up when importing clips
249
self._progressbar = Gtk.ProgressBar()
250
self._progressbar.set_show_text(True)
252
# Connect to project. We must remove and reset the callbacks when
254
self.project_signals = SignalGroup()
255
self.app.connect("new-project-created", self._newProjectCreatedCb)
256
self.app.connect("new-project-loaded", self._newProjectLoadedCb)
257
self.app.connect("new-project-failed", self._newProjectFailedCb)
260
self.drag_dest_set(Gtk.DestDefaults.DROP | Gtk.DestDefaults.MOTION,
261
[dnd.URI_TARGET_ENTRY, dnd.FILE_TARGET_ENTRY],
263
self.drag_dest_add_uri_targets()
264
self.connect("drag_data_received", self._dndDataReceivedCb)
266
self._setup_view_for_drag_and_drop(self.treeview, [("pitivi/file-source", 0, TYPE_PITIVI_FILESOURCE)])
267
self._setup_view_for_drag_and_drop(self.iconview, [Gtk.TargetEntry.new("pitivi/file-source", 0, TYPE_PITIVI_FILESOURCE)])
269
# Hack so that the views have the same method as self
270
self.treeview.getSelectedItems = self.getSelectedItems
272
# Keyboard shortcuts for some items in the gtkbuilder file
273
selection_actions = (
274
("RemoveSources", Gtk.STOCK_DELETE, _("_Remove from Project"),
275
"<Control>Delete", None, self._removeSourcesCb),
277
("InsertEnd", Gtk.STOCK_COPY, _("Insert at _End of Timeline"),
278
"Insert", None, self._insertEndCb),
280
self.selection_actions = Gtk.ActionGroup(name="medialibraryselection")
281
self.selection_actions.add_actions(selection_actions)
282
self.selection_actions.set_sensitive(False)
283
uiman.insert_action_group(self.selection_actions, 0)
284
uiman.add_ui_from_string(ui)
286
# Set the state of the view mode toggle button.
287
self._listview_button.set_active(self.clip_view == SHOW_TREEVIEW)
288
# Make sure the proper view is displayed.
289
self._displayClipView()
291
# Add all the child widgets.
292
self.pack_start(toolbar, False, False, 0)
293
self.pack_start(self._welcome_infobar, False, False, 0)
294
self.pack_start(self._import_warning_infobar, False, False, 0)
295
self.pack_start(self.iconview_scrollwin, True, True, 0)
296
self.pack_start(self.treeview_scrollwin, True, True, 0)
297
self.pack_start(self._progressbar, False, True, 0)
299
self.thumbnailer = MediaLibraryWidget._getThumbnailer()
302
def _getThumbnailer():
303
if not GNOMEDESKTOP_SOFT_DEPENDENCY:
305
from gi.repository import GnomeDesktop
306
# We need to instanciate the thumbnail factory on the main thread...
307
size_normal = GnomeDesktop.DesktopThumbnailSize.NORMAL
308
return GnomeDesktop.DesktopThumbnailFactory.new(size_normal)
311
def compare_basename(model, iter1, iter2, unused_user_data):
313
Compare the model elements identified by the L{Gtk.TreeIter} elements.
315
uri1 = model[iter1][COL_URI]
316
uri2 = model[iter2][COL_URI]
317
basename1 = GLib.path_get_basename(uri1).lower()
318
basename2 = GLib.path_get_basename(uri2).lower()
319
if basename1 < basename2:
321
if basename1 == basename2:
326
def getAssetForUri(self, uri):
328
uri = filter(lambda c: c != '\n' and c != '\r', uri)
329
for path in self.modelFilter:
330
asset = path[COL_ASSET]
331
info = asset.get_info()
332
asset_uri = info.get_uri()
336
def _setup_view_for_drag_and_drop(self, view, target_entries):
337
view.drag_source_set(0, [], Gdk.DragAction.COPY)
338
view.enable_model_drag_source(Gdk.ModifierType.BUTTON1_MASK, target_entries, Gdk.DragAction.COPY)
339
view.drag_source_set_target_list([])
340
view.drag_source_add_uri_targets()
341
view.drag_source_add_text_targets()
342
view.connect("drag-data-get", self._dndDragDataGetCb)
343
view.connect("drag-begin", self._dndDragBeginCb)
344
view.connect("drag-end", self._dndDragEndCb)
346
def _importSourcesCb(self, unused_action):
347
self.showImportSourcesDialog()
349
def _removeSourcesCb(self, unused_action):
350
self._removeSources()
352
def _insertEndCb(self, unused_action):
353
self.app.gui.timeline_ui.insertEnd(self.getSelectedAssets())
355
def _searchEntryChangedCb(self, entry):
356
# With many hundred clips in an iconview with dynamic columns and
357
# ellipsizing, doing needless searches is very expensive.
358
# Realistically, nobody expects to search for only one character,
359
# and skipping that makes a huge difference in responsiveness.
360
if len(entry.get_text()) != 1:
361
self.modelFilter.refilter()
363
def _searchEntryIconClickedCb(self, entry, icon_pos, unused_event):
364
if icon_pos == Gtk.EntryIconPosition.SECONDARY:
366
elif icon_pos == Gtk.EntryIconPosition.PRIMARY:
367
self._selectUnusedSources()
369
def _setRowVisible(self, model, iter, data):
371
Toggle the visibility of a liststore row.
372
Used for the search box.
374
text = data.get_text().lower()
376
return True # Avoid silly warnings
377
# We must convert to markup form to be able to search for &, ', etc.
378
text = GLib.markup_escape_text(text)
379
return text in model.get_value(iter, COL_INFOTEXT).lower()
381
def _getIcon(self, iconname, alternate=None, size=48):
382
icontheme = Gtk.IconTheme.get_default()
383
pixdir = get_pixmap_dir()
386
icon = icontheme.load_icon(iconname, size, 0)
388
# empty except clause is bad but load_icon raises Gio.Error.
391
icon = GdkPixbuf.Pixbuf.new_from_file(os.path.join(pixdir, alternate))
393
icon = icontheme.load_icon("dialog-question", size, 0)
396
def _connectToProject(self, project):
397
"""Connect signal handlers to a project.
399
This first disconnects any handlers connected to an old project.
400
If project is None, this just disconnects any connected handlers.
402
self.project_signals.connect(project, "asset-added", None,
404
self.project_signals.connect(project, "asset-removed", None,
405
self._assetRemovedCb)
406
self.project_signals.connect(project, "error-loading-asset",
407
None, self._errorCreatingAssetCb)
408
self.project_signals.connect(project, "done-importing", None,
409
self._sourcesStoppedImportingCb)
410
self.project_signals.connect(project, "start-importing", None,
411
self._sourcesStartedImportingCb)
413
# The start-importing signal would have already been emited at that
414
# time, make sure to catch if it is the case
415
if project.nb_remaining_file_to_import > 0:
416
self._sourcesStartedImportingCb(project)
418
def _setClipView(self, view_type):
420
Set which clip view to use when medialibrary is showing clips.
421
view_type: one of SHOW_TREEVIEW or SHOW_ICONVIEW
423
self.app.settings.lastClipView = view_type
424
# Gather some info before switching views
425
paths = self.getSelectedPaths()
426
self._viewUnselectAll()
427
# Now that we've got all the info, we can actually change the view type
428
self.clip_view = view_type
429
self._displayClipView()
431
self._viewSelectPath(path)
433
def _displayClipView(self):
434
if self.clip_view == SHOW_TREEVIEW:
435
self.iconview_scrollwin.hide()
436
self.treeview_scrollwin.show_all()
437
elif self.clip_view == SHOW_ICONVIEW:
438
self.treeview_scrollwin.hide()
439
self.iconview_scrollwin.show_all()
441
if not len(self.storemodel):
442
self._welcome_infobar.show_all()
444
def showImportSourcesDialog(self):
445
"""Pop up the "Import Sources" dialog box"""
446
if self._importDialog:
449
chooser_action = Gtk.FileChooserAction.OPEN
450
dialogtitle = _("Select One or More Files")
452
close_after = Gtk.CheckButton(label=_("Close after importing files"))
453
close_after.set_active(self.app.settings.closeImportDialog)
455
self._importDialog = Gtk.FileChooserDialog(title=dialogtitle, transient_for=None,
456
action=chooser_action)
458
self._importDialog.add_buttons(Gtk.STOCK_CLOSE, Gtk.ResponseType.CLOSE,
459
Gtk.STOCK_ADD, Gtk.ResponseType.OK)
460
self._importDialog.set_icon_name("pitivi")
461
self._importDialog.props.extra_widget = close_after
462
self._importDialog.set_default_response(Gtk.ResponseType.OK)
463
self._importDialog.set_select_multiple(True)
464
self._importDialog.set_modal(True)
465
self._importDialog.set_transient_for(self.app.gui)
466
self._importDialog.set_current_folder(self.app.settings.lastImportFolder)
467
self._importDialog.connect('response', self._dialogBoxResponseCb)
468
self._importDialog.connect('close', self._dialogBoxCloseCb)
469
previewer = PreviewWidget(self.app.settings)
470
self._importDialog.set_preview_widget(previewer)
471
self._importDialog.set_use_preview_label(False)
472
self._importDialog.connect('update-preview', previewer.add_preview_request)
473
# Filter for the "known good" formats by default
474
filt_supported = Gtk.FileFilter()
475
filt_known = Gtk.FileFilter()
476
filt_supported.set_name(_("Supported file formats"))
477
for category, mime_types in SUPPORTED_FILE_FORMATS.iteritems():
478
for mime in mime_types:
479
filt_supported.add_mime_type(category + "/" + mime)
480
filt_known.add_mime_type(category + "/" + mime)
481
# Also allow showing known but not reliable demuxers
482
filt_known.set_name(_("All known file formats"))
483
for fullmime in OTHER_KNOWN_FORMATS:
484
filt_known.add_mime_type(fullmime)
485
# ...and allow the user to override our whitelists
486
default = Gtk.FileFilter()
487
default.set_name(_("All files"))
488
default.add_pattern("*")
489
self._importDialog.add_filter(filt_supported)
490
self._importDialog.add_filter(filt_known)
491
self._importDialog.add_filter(default)
492
self._importDialog.show()
494
def _updateProgressbar(self):
496
Update the _progressbar with the ratio of clips imported vs the total
498
# The clip iter has a +1 offset in the progressbar label (to refer to
499
# the actual # of the clip we're processing), but there is no offset
500
# in the progressbar itself (to reflect the process being incomplete).
501
current_clip_iter = self.app.current_project.nb_imported_files
502
total_clips = self.app.current_project.nb_remaining_file_to_import + current_clip_iter
504
progressbar_text = _("Importing clip %(current_clip)d of %(total)d" %
505
{"current_clip": current_clip_iter + 1,
506
"total": total_clips})
507
self._progressbar.set_text(progressbar_text)
508
if current_clip_iter == 0:
509
self._progressbar.set_fraction(0.0)
510
elif total_clips != 0:
511
self._progressbar.set_fraction(current_clip_iter / float(total_clips))
513
def _getThumbnailInDir(self, dir, hash):
515
For a given thumbnail cache directory and file URI hash, see if there're
516
thumbnails available and return them in resolutions that pitivi expects.
518
The cache dirs might have resolutions of 256 and/or 128,
519
while we need 128 (for iconview) and 64 (for listview).
521
path_256 = dir + "large/" + hash + ".png"
522
path_128 = dir + "normal/" + hash + ".png"
523
interpolation = GdkPixbuf.InterpType.BILINEAR
525
# First, try the 128 version since that's the native resolution we want:
527
thumb_128 = GdkPixbuf.Pixbuf.new_from_file(path_128)
528
w, h = thumb_128.get_width(), thumb_128.get_height()
529
thumb_64 = thumb_128.scale_simple(w / 2, h / 2, interpolation)
530
return thumb_64, thumb_128
532
# path_128 doesn't exist, try the 256 version
534
thumb_256 = GdkPixbuf.Pixbuf.new_from_file(path_256)
535
w, h = thumb_256.get_width(), thumb_256.get_height()
536
thumb_128 = thumb_256.scale_simple(w / 2, h / 2, interpolation)
537
thumb_64 = thumb_256.scale_simple(w / 4, h / 4, interpolation)
538
return thumb_64, thumb_128
542
def _generateThumbnails(self, uri):
543
if not self.thumbnailer:
544
# TODO: Use thumbnails generated with GStreamer.
546
# This way of getting the mimetype feels awfully convoluted but
547
# seems to be the proper/reliable way in a GNOME context
548
asset_file = Gio.file_new_for_uri(uri)
549
info = asset_file.query_info(attributes="standard::*",
550
flags=Gio.FileQueryInfoFlags.NONE,
552
mime = Gio.content_type_get_mime_type(info.get_content_type())
553
mtime = os.path.getmtime(path_from_uri(uri))
554
if not self.thumbnailer.can_thumbnail(uri, mime, mtime):
555
self.debug("Thumbnailer says it can't thumbnail %s", uri)
557
pixbuf_128 = self.thumbnailer.generate_thumbnail(uri, mime)
559
self.debug("Thumbnailer failed thumbnailing %s", uri)
561
self.thumbnailer.save_thumbnail(pixbuf_128, uri, mtime)
562
pixbuf_64 = pixbuf_128.scale_simple(64, 64, GdkPixbuf.InterpType.BILINEAR)
563
return pixbuf_128, pixbuf_64
565
def _addAsset(self, asset):
566
# 128 is the normal size for thumbnails, but for *icons* it looks insane
568
info = asset.get_info()
570
# The code below tries to read existing thumbnails from the freedesktop
571
# thumbnails directory (~/.thumbnails). The filenames are simply
572
# the file URI hashed with md5, so we can retrieve them easily.
573
video_streams = [i for i in info.get_stream_list() if isinstance(i, DiscovererVideoInfo)]
574
if len(video_streams) > 0:
575
# From the freedesktop spec: "if the environment variable
576
# $XDG_CACHE_HOME is set and not blank then the directory
577
# $XDG_CACHE_HOME/thumbnails will be used, otherwise
578
# $HOME/.cache/thumbnails will be used."
579
# Older version of the spec also mentioned $HOME/.thumbnails
580
quoted_uri = quote_uri(info.get_uri())
581
thumbnail_hash = md5(quoted_uri).hexdigest()
583
thumb_dir = os.environ['XDG_CACHE_HOME']
584
thumb_64, thumb_128 = self._getThumbnailInDir(thumb_dir, thumbnail_hash)
586
thumb_64, thumb_128 = (None, None)
588
thumb_dir = os.path.expanduser("~/.cache/thumbnails/")
589
thumb_64, thumb_128 = self._getThumbnailInDir(thumb_dir, thumbnail_hash)
591
thumb_dir = os.path.expanduser("~/.thumbnails/")
592
thumb_64, thumb_128 = self._getThumbnailInDir(thumb_dir, thumbnail_hash)
595
thumb_64 = self._getIcon("image-x-generic")
596
thumb_128 = self._getIcon("image-x-generic", None, LARGE_SIZE)
598
thumb_64 = self._getIcon("video-x-generic")
599
thumb_128 = self._getIcon("video-x-generic", None, LARGE_SIZE)
600
# TODO ideally gst discoverer should create missing thumbnails.
601
self.log("Missing a thumbnail for %s, queuing", path_from_uri(quoted_uri))
602
self._missing_thumbs.append(quoted_uri)
604
thumb_64 = self._getIcon("audio-x-generic")
605
thumb_128 = self._getIcon("audio-x-generic", None, LARGE_SIZE)
607
if info.get_duration() == Gst.CLOCK_TIME_NONE:
610
duration = beautify_length(info.get_duration())
612
name = info_name(info)
614
self.pending_rows.append((thumb_64,
621
if len(self.pending_rows) > 50:
622
self.flush_pending_rows()
624
def flush_pending_rows(self):
625
self.debug("Flushing %d pending model rows", len(self.pending_rows))
626
for row in self.pending_rows:
627
self.storemodel.append(row)
628
del self.pending_rows[:]
630
# medialibrary callbacks
632
def _assetAddedCb(self, unused_project, asset,
633
unused_current_clip_iter=None, unused_total_clips=None):
634
""" a file was added to the medialibrary """
635
if isinstance(asset, GES.UriClipAsset):
636
self._updateProgressbar()
637
self._addAsset(asset)
639
def _assetRemovedCb(self, unused_project, asset):
640
""" the given uri was removed from the medialibrary """
641
# find the good line in the storemodel and remove it
642
model = self.storemodel
645
if uri == row[COL_URI]:
646
model.remove(row.iter)
649
self._welcome_infobar.show_all()
650
self.debug("Removing: %s", uri)
652
def _errorCreatingAssetCb(self, unused_project, error, id, type):
653
""" The given uri isn't a media file """
654
if GObject.type_is_a(type, GES.UriClip):
655
error = (id, str(error.domain), error)
656
self._errors.append(error)
657
self._updateProgressbar()
659
def _sourcesStartedImportingCb(self, unused_project):
660
self.import_start_time = time.time()
661
self._welcome_infobar.hide()
662
self._progressbar.show()
664
def _sourcesStoppedImportingCb(self, unused_project):
665
self.debug("Importing took %.3f seconds", time.time() - self.import_start_time)
666
self.flush_pending_rows()
667
self._progressbar.hide()
669
errors_amount = len(self._errors)
670
btn_text = ngettext("View error", "View errors", errors_amount)
671
# Translators: {0:d} is just like %d (integer number variable)
672
text = ngettext("An error occurred while importing.",
673
"{0:d} errors occurred while importing.",
675
# Do the {0:d} (aka "%d") substitution using "format" instead of %,
676
# avoiding tracebacks as %d would be missing in the singular form:
677
text = text.format(errors_amount)
679
self._view_error_button.set_label(btn_text)
680
self._warning_label.set_text(text)
681
self._import_warning_infobar.show_all()
683
missing_thumbs = self._missing_thumbs
684
self._missing_thumbs = []
686
self.info("Generating missing thumbnails: %d", len(missing_thumbs))
687
self._thumbs_process = threading.Thread(target=MediaLibraryWidget._generateThumbnailsThread, args=(self, missing_thumbs))
688
self._thumbs_process.start()
690
def _generateThumbnailsThread(self, missing_thumbs):
691
for uri in missing_thumbs:
692
thumbnails = self._generateThumbnails(uri)
695
pixbuf_128, pixbuf_64 = thumbnails
696
# Search through the model for the row corresponding to the asset.
698
for row in self.storemodel:
699
if uri == row[COL_URI]:
701
# Finally, show the new pixbuf in the UI
703
row[COL_ICON_128] = pixbuf_128
705
row[COL_ICON_64] = pixbuf_64
708
# Can happen if the user removed the asset in the meanwhile.
709
self.log("%s needed a thumbnail, but vanished from storemodel", uri)
711
## Error Dialog Box callbacks
713
def _errorDialogBoxCloseCb(self, dialog):
716
def _errorDialogBoxResponseCb(self, dialog, unused_response):
719
## Import Sources Dialog Box callbacks
721
def _dialogBoxResponseCb(self, dialogbox, response):
722
self.debug("response: %r", response)
723
if response == Gtk.ResponseType.OK:
724
lastfolder = dialogbox.get_current_folder()
725
self.app.settings.lastImportFolder = lastfolder
726
self.app.settings.closeImportDialog = \
727
dialogbox.props.extra_widget.get_active()
728
filenames = dialogbox.get_uris()
729
self.app.current_project.addUris(filenames)
730
if self.app.settings.closeImportDialog:
732
self._importDialog = None
735
self._importDialog = None
737
def _dialogBoxCloseCb(self, unused_dialogbox):
738
self.debug("closing")
739
self._importDialog = None
741
def _removeSources(self):
743
Determine which clips are selected in the icon or list view,
744
and ask MediaLibrary to remove them from the project.
746
model = self.treeview.get_model()
747
paths = self.getSelectedPaths()
748
if paths is None or paths < 1:
750
# use row references so we don't have to care if a path has been removed
753
row = Gtk.TreeRowReference.new(model, path)
756
self.app.action_log.begin("remove clip from source list")
758
asset = model[row.get_path()][COL_ASSET]
759
self.app.current_project.remove_asset(asset)
760
self.app.action_log.commit()
762
def _sourceIsUsed(self, asset):
763
"""Check if a given URI is present in the timeline"""
764
layers = self.app.current_project.timeline.get_layers()
766
for clip in layer.get_clips():
767
if clip.get_asset() == asset:
771
def _selectUnusedSources(self):
773
Select, in the media library, unused sources in the project.
775
assets = self.app.current_project.list_assets(GES.UriClip)
776
unused_sources_uris = []
778
model = self.treeview.get_model()
779
selection = self.treeview.get_selection()
781
if not self._sourceIsUsed(asset):
782
unused_sources_uris.append(asset.get_id())
784
# Hack around the fact that making selections (in a treeview/iconview)
785
# deselects what was previously selected
786
if self.clip_view == SHOW_TREEVIEW:
787
self.treeview.get_selection().select_all()
788
elif self.clip_view == SHOW_ICONVIEW:
789
self.iconview.select_all()
792
if row[COL_URI] not in unused_sources_uris:
793
if self.clip_view == SHOW_TREEVIEW:
794
selection.unselect_iter(row.iter)
796
self.iconview.unselect_path(row.path)
800
def _removeClickedCb(self, unused_widget=None):
801
""" Called when a user clicks on the remove button """
802
self._removeSources()
804
def _clipPropertiesCb(self, unused_widget=None):
806
Show the clip properties (resolution, framerate, audio channels...)
807
and allow setting them as the new project settings.
809
paths = self.getSelectedPaths()
811
self.debug("No item selected")
813
# Only use the first item.
815
asset = self.storemodel[path][COL_ASSET]
816
dialog = ClipMediaPropsDialog(self.app.current_project, asset)
819
def _warningInfoBarDismissedCb(self, unused_button):
820
self._resetErrorList()
822
def _resetErrorList(self):
824
self._import_warning_infobar.hide()
826
def _viewErrorsButtonClickedCb(self, unused_button):
828
Show a FileListErrorDialog to display import _errors.
830
if len(self._errors) > 1:
831
msgs = (_("Error while analyzing files"),
832
_("The following files can not be used with Pitivi."))
834
msgs = (_("Error while analyzing a file"),
835
_("The following file can not be used with Pitivi."))
836
error_dialogbox = FileListErrorDialog(*msgs)
837
error_dialogbox.connect("close", self._errorDialogBoxCloseCb)
838
error_dialogbox.connect("response", self._errorDialogBoxResponseCb)
839
for uri, reason, extra in self._errors:
840
error_dialogbox.addFailedFile(uri, reason, extra)
841
error_dialogbox.window.show()
842
# Reset the error list, since the user has read them.
843
self._resetErrorList()
845
def _toggleViewTypeCb(self, widget):
846
if widget.get_active():
847
self._setClipView(SHOW_TREEVIEW)
849
self._setClipView(SHOW_ICONVIEW)
851
def _rowUnderMouseSelected(self, view, event):
852
if isinstance(view, Gtk.TreeView):
854
tup = view.get_path_at_pos(int(event.x), int(event.y))
856
path, column, x, y = tup
858
selection = view.get_selection()
859
return selection.path_is_selected(path) and selection.count_selected_rows() > 0
860
elif isinstance(view, Gtk.IconView):
861
path = view.get_path_at_pos(int(event.x), int(event.y))
863
selection = view.get_selected_items()
864
return view.path_is_selected(path) and len(selection)
866
raise RuntimeError("Unknown media library view type: %s" % type(view))
870
def _nothingUnderMouse(self, view, event):
871
return not bool(view.get_path_at_pos(int(event.x), int(event.y)))
873
def _viewGetFirstSelected(self):
874
paths = self.getSelectedPaths()
877
def _viewHasSelection(self):
878
paths = self.getSelectedPaths()
879
return bool(len(paths))
881
def _viewGetPathAtPos(self, event):
882
if self.clip_view == SHOW_TREEVIEW:
883
pathinfo = self.treeview.get_path_at_pos(int(event.x), int(event.y))
885
elif self.clip_view == SHOW_ICONVIEW:
886
return self.iconview.get_path_at_pos(int(event.x), int(event.y))
888
def _viewSelectPath(self, path):
889
if self.clip_view == SHOW_TREEVIEW:
890
selection = self.treeview.get_selection()
891
selection.select_path(path)
892
elif self.clip_view == SHOW_ICONVIEW:
893
self.iconview.select_path(path)
895
def _viewUnselectAll(self):
896
if self.clip_view == SHOW_TREEVIEW:
897
selection = self.treeview.get_selection()
898
selection.unselect_all()
899
elif self.clip_view == SHOW_ICONVIEW:
900
self.iconview.unselect_all()
902
def _treeViewButtonPressEventCb(self, treeview, event):
903
self._updateDraggedPaths(treeview, event)
905
Gtk.TreeView.do_button_press_event(treeview, event)
907
ts = self.treeview.get_selection()
908
if self._draggedPaths:
909
for path in self._draggedPaths:
914
def _updateDraggedPaths(self, view, event):
915
if event.type == getattr(Gdk.EventType, '2BUTTON_PRESS'):
916
# It is possible to double-click outside of clips:
917
if self.getSelectedPaths():
918
# Here we used to emit "play", but
919
# this is now handled by _itemOrRowActivatedCb instead.
922
elif not event.get_state() & (Gdk.ModifierType.CONTROL_MASK | Gdk.ModifierType.SHIFT_MASK):
923
chain_up = not self._rowUnderMouseSelected(view, event)
928
self._draggedPaths = self.getSelectedPaths()
930
self._draggedPaths = None
932
def _treeViewButtonReleaseEventCb(self, unused_treeview, event):
933
ts = self.treeview.get_selection()
934
state = event.get_state() & (Gdk.ModifierType.CONTROL_MASK | Gdk.ModifierType.SHIFT_MASK)
935
path = self.treeview.get_path_at_pos(event.x, event.y)
937
if not state and not self.dragged:
940
ts.select_path(path[0])
942
def _viewSelectionChangedCb(self, unused):
943
selected_items = len(self.getSelectedPaths())
945
self.selection_actions.set_sensitive(True)
946
self._remove_button.set_sensitive(True)
947
self._insert_button.set_sensitive(True)
948
# Some actions can only be done on a single item at a time:
949
self._clipprops_button.set_sensitive(False)
950
if selected_items == 1:
951
self._clipprops_button.set_sensitive(True)
953
self.selection_actions.set_sensitive(False)
954
self._remove_button.set_sensitive(False)
955
self._insert_button.set_sensitive(False)
956
self._clipprops_button.set_sensitive(False)
958
def _itemOrRowActivatedCb(self, unused_view, path, *unused_column):
960
When an item is double-clicked, or
961
Space, Shift+Space, Return or Enter is pressed, preview the clip.
962
This method is the same for both iconview and treeview.
964
asset = self.modelFilter[path][COL_ASSET]
965
self.emit('play', asset)
967
def _iconViewButtonPressEventCb(self, iconview, event):
968
self._updateDraggedPaths(iconview, event)
970
Gtk.IconView.do_button_press_event(iconview, event)
972
if self._draggedPaths:
973
for path in self._draggedPaths:
974
self.iconview.select_path(path)
976
self.iconview_cursor_pos = self.iconview.get_path_at_pos(event.x, event.y)
980
def _iconViewButtonReleaseEventCb(self, iconview, event):
981
control_mask = event.get_state() & Gdk.ModifierType.CONTROL_MASK
982
shift_mask = event.get_state() & Gdk.ModifierType.SHIFT_MASK
983
modifier_active = control_mask or shift_mask
984
if not modifier_active and self.iconview_cursor_pos:
985
current_cursor_pos = self.iconview.get_path_at_pos(event.x, event.y)
987
if current_cursor_pos == self.iconview_cursor_pos:
988
if iconview.path_is_selected(current_cursor_pos):
989
iconview.unselect_all()
990
iconview.select_path(current_cursor_pos)
992
def _newProjectCreatedCb(self, unused_app, project):
993
if not self._project is project:
994
self._project = project
995
self._resetErrorList()
996
self.storemodel.clear()
997
self._welcome_infobar.show_all()
998
self._connectToProject(project)
1000
def _newProjectLoadedCb(self, unused_pitivi, project):
1001
if not self._project is project:
1002
self._project = project
1003
self.storemodel.clear()
1004
self._connectToProject(project)
1006
# Make sure that the sources added to the project are added added
1007
self.flush_pending_rows()
1009
def _newProjectFailedCb(self, unused_pitivi, unused_reason, unused_uri):
1010
self.storemodel.clear()
1011
self.project_signals.disconnectAll()
1012
self._project = None
1014
def _addUris(self, uris):
1015
if self.app.current_project:
1016
self.app.current_project.addUris(uris)
1018
self.warning("Adding uris to project, but the project has changed in the meantime")
1022
def _dndDataReceivedCb(self, unused_widget, unused_context, unused_x,
1023
unused_y, selection, targettype, unused_time):
1024
self.debug("targettype: %d, selection.data: %r", targettype, selection.get_data())
1029
uris = selection.get_data().split("\r\n")
1030
# Filter out the empty uris.
1031
uris = filter(lambda x: x, uris)
1032
for raw_uri in uris:
1033
# Strip out NULL chars first.
1034
raw_uri = raw_uri.strip('\x00')
1035
uri = urlparse(raw_uri)
1036
if uri.scheme == 'file':
1037
path = unquote(uri.path)
1038
if os.path.isfile(path):
1039
filenames.append(raw_uri)
1040
elif os.path.isdir(path):
1041
directories.append(raw_uri)
1043
self.warning("Unusable file: %s, %s", raw_uri, path)
1045
self.fixme("Importing remote files is not implemented: %s", raw_uri)
1048
# Recursively import from folders that were dragged into the library
1049
self.app.threads.addThread(PathWalker, directories, self._addUris)
1051
self.app.current_project.addUris(filenames)
1053
#used with TreeView and IconView
1054
def _dndDragDataGetCb(self, unused_view, unused_context, data, unused_info, unused_timestamp):
1055
paths = self.getSelectedPaths()
1056
uris = [self.modelFilter[path][COL_URI] for path in paths]
1059
def _dndDragBeginCb(self, unused_view, context):
1060
self.info("Drag operation begun")
1062
paths = self.getSelectedPaths()
1065
context.drag_abort(int(time.time()))
1067
row = self.modelFilter[paths[0]]
1068
Gtk.drag_set_icon_pixbuf(context, row[COL_ICON_64], 0, 0)
1070
def _dndDragEndCb(self, unused_view, unused_context):
1071
self.info("Drag operation ended")
1072
self.dragged = False
1074
def getSelectedPaths(self):
1075
""" Returns a list of selected treeview or iconview items """
1076
if self.clip_view == SHOW_TREEVIEW:
1077
return self._getSelectedPathsTreeView()
1078
elif self.clip_view == SHOW_ICONVIEW:
1079
return self._getSelectedPathsIconView()
1081
def _getSelectedPathsTreeView(self):
1082
model, rows = self.treeview.get_selection().get_selected_rows()
1085
def _getSelectedPathsIconView(self):
1086
paths = self.iconview.get_selected_items()
1090
def getSelectedItems(self):
1091
""" Returns a list of selected items URIs """
1092
if self._draggedPaths:
1093
return [self.modelFilter[path][COL_URI]
1094
for path in self._draggedPaths]
1095
return [self.modelFilter[path][COL_URI]
1096
for path in self.getSelectedPaths()]
1098
def getSelectedAssets(self):
1099
""" Returns a list of selected items URIs """
1100
if self._draggedPaths:
1101
return [self.modelFilter[path][COL_ASSET]
1102
for path in self._draggedPaths]
1103
return [self.modelFilter[path][COL_ASSET]
1104
for path in self.getSelectedPaths()]