~timo-jyrinki/ubuntu/trusty/pitivi/backport_utopic_fixes

« back to all changes in this revision

Viewing changes to pitivi/medialibrary.py

  • Committer: Package Import Robot
  • Author(s): Sebastian Dröge
  • Date: 2014-04-05 15:28:16 UTC
  • mfrom: (6.1.13 sid)
  • Revision ID: package-import@ubuntu.com-20140405152816-6lijoax4cngiz5j5
Tags: 0.93-3
* debian/control:
  + Depend on python-gi (>= 3.10), older versions do not work
    with pitivi (Closes: #732813).
  + Add missing dependency on gir1.2-clutter-gst-2.0 (Closes: #743692).
  + Add suggests on gir1.2-notify-0.7 and gir1.2-gnomedesktop-3.0.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# -*- coding: utf-8 -*-
 
2
# Pitivi video editor
 
3
#
 
4
#       pitivi/medialibrary.py
 
5
#
 
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>
 
9
#
 
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.
 
14
#
 
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.
 
19
#
 
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.
 
24
 
 
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
 
34
 
 
35
import os
 
36
import time
 
37
import threading
 
38
 
 
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
 
44
 
 
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
 
57
 
 
58
from pitivi.utils.ui import TYPE_PITIVI_FILESOURCE
 
59
 
 
60
# Values used in the settings file.
 
61
SHOW_TREEVIEW = 1
 
62
SHOW_ICONVIEW = 2
 
63
 
 
64
GlobalSettings.addConfigSection('clip-library')
 
65
GlobalSettings.addConfigOption('lastImportFolder',
 
66
    section='clip-library',
 
67
    key='last-folder',
 
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',
 
73
    default=True)
 
74
GlobalSettings.addConfigOption('lastClipView',
 
75
    section='clip-library',
 
76
    key='last-clip-view',
 
77
    type_=int,
 
78
    default=SHOW_ICONVIEW)
 
79
 
 
80
STORE_MODEL_STRUCTURE = (
 
81
    GdkPixbuf.Pixbuf, GdkPixbuf.Pixbuf,
 
82
    str, object, str, str, str)
 
83
 
 
84
(COL_ICON_64,
 
85
 COL_ICON_128,
 
86
 COL_INFOTEXT,
 
87
 COL_ASSET,
 
88
 COL_URI,
 
89
 COL_LENGTH,
 
90
 COL_SEARCH_TEXT) = range(len(STORE_MODEL_STRUCTURE))
 
91
 
 
92
ui = '''
 
93
<ui>
 
94
    <accelerator action="RemoveSources" />
 
95
    <accelerator action="InsertEnd" />
 
96
</ui>
 
97
'''
 
98
 
 
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",)
 
112
 
 
113
 
 
114
class MediaLibraryWidget(Gtk.VBox, Loggable):
 
115
    """ Widget for listing sources """
 
116
 
 
117
    __gsignals__ = {
 
118
        'play': (GObject.SignalFlags.RUN_LAST, None,
 
119
                (GObject.TYPE_PYOBJECT,))}
 
120
 
 
121
    def __init__(self, app, uiman):
 
122
        Gtk.VBox.__init__(self)
 
123
        Loggable.__init__(self)
 
124
 
 
125
        self.pending_rows = []
 
126
 
 
127
        self.app = app
 
128
        self._errors = []
 
129
        self._missing_thumbs = []
 
130
        self._project = None
 
131
        self._draggedPaths = None
 
132
        self.dragged = False
 
133
        self.clip_view = self.app.settings.lastClipView
 
134
 
 
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")
 
149
 
 
150
        # Store
 
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)
 
156
 
 
157
        # Scrolled Windows
 
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")
 
162
 
 
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")
 
167
 
 
168
        # import sources dialogbox
 
169
        self._importDialog = None
 
170
 
 
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)
 
175
 
 
176
        # TreeView
 
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)
 
189
 
 
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)
 
198
 
 
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)
 
209
 
 
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)
 
217
 
 
218
        # IconView
 
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
 
231
 
 
232
        cell = Gtk.CellRendererPixbuf()
 
233
        self.iconview.pack_start(cell, False)
 
234
        self.iconview.add_attribute(cell, "pixbuf", COL_ICON_128)
 
235
 
 
236
        cell = Gtk.CellRendererText()
 
237
        cell.props.alignment = Pango.Alignment.CENTER
 
238
        cell.props.xalign = 0.5
 
239
        cell.props.yalign = 0.0
 
240
        cell.props.xpad = 0
 
241
        cell.props.ypad = 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)
 
245
 
 
246
        self.iconview.set_selection_mode(Gtk.SelectionMode.MULTIPLE)
 
247
 
 
248
        # The _progressbar that shows up when importing clips
 
249
        self._progressbar = Gtk.ProgressBar()
 
250
        self._progressbar.set_show_text(True)
 
251
 
 
252
        # Connect to project.  We must remove and reset the callbacks when
 
253
        # changing project.
 
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)
 
258
 
 
259
        # Drag and Drop
 
260
        self.drag_dest_set(Gtk.DestDefaults.DROP | Gtk.DestDefaults.MOTION,
 
261
                           [dnd.URI_TARGET_ENTRY, dnd.FILE_TARGET_ENTRY],
 
262
                           Gdk.DragAction.COPY)
 
263
        self.drag_dest_add_uri_targets()
 
264
        self.connect("drag_data_received", self._dndDataReceivedCb)
 
265
 
 
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)])
 
268
 
 
269
        # Hack so that the views have the same method as self
 
270
        self.treeview.getSelectedItems = self.getSelectedItems
 
271
 
 
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),
 
276
 
 
277
            ("InsertEnd", Gtk.STOCK_COPY, _("Insert at _End of Timeline"),
 
278
            "Insert", None, self._insertEndCb),
 
279
        )
 
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)
 
285
 
 
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()
 
290
 
 
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)
 
298
 
 
299
        self.thumbnailer = MediaLibraryWidget._getThumbnailer()
 
300
 
 
301
    @staticmethod
 
302
    def _getThumbnailer():
 
303
        if not GNOMEDESKTOP_SOFT_DEPENDENCY:
 
304
            return None
 
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)
 
309
 
 
310
    @staticmethod
 
311
    def compare_basename(model, iter1, iter2, unused_user_data):
 
312
        """
 
313
        Compare the model elements identified by the L{Gtk.TreeIter} elements.
 
314
        """
 
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:
 
320
            return -1
 
321
        if basename1 == basename2:
 
322
            if uri1 < uri2:
 
323
                return -1
 
324
        return 1
 
325
 
 
326
    def getAssetForUri(self, uri):
 
327
        # Sanitization
 
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()
 
333
            if asset_uri == uri:
 
334
                return asset
 
335
 
 
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)
 
345
 
 
346
    def _importSourcesCb(self, unused_action):
 
347
        self.showImportSourcesDialog()
 
348
 
 
349
    def _removeSourcesCb(self, unused_action):
 
350
        self._removeSources()
 
351
 
 
352
    def _insertEndCb(self, unused_action):
 
353
        self.app.gui.timeline_ui.insertEnd(self.getSelectedAssets())
 
354
 
 
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()
 
362
 
 
363
    def _searchEntryIconClickedCb(self, entry, icon_pos, unused_event):
 
364
        if icon_pos == Gtk.EntryIconPosition.SECONDARY:
 
365
            entry.set_text("")
 
366
        elif icon_pos == Gtk.EntryIconPosition.PRIMARY:
 
367
            self._selectUnusedSources()
 
368
 
 
369
    def _setRowVisible(self, model, iter, data):
 
370
        """
 
371
        Toggle the visibility of a liststore row.
 
372
        Used for the search box.
 
373
        """
 
374
        text = data.get_text().lower()
 
375
        if not text:
 
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()
 
380
 
 
381
    def _getIcon(self, iconname, alternate=None, size=48):
 
382
        icontheme = Gtk.IconTheme.get_default()
 
383
        pixdir = get_pixmap_dir()
 
384
        icon = None
 
385
        try:
 
386
            icon = icontheme.load_icon(iconname, size, 0)
 
387
        except:
 
388
            # empty except clause is bad but load_icon raises Gio.Error.
 
389
            # Right, *gio*.
 
390
            if alternate:
 
391
                icon = GdkPixbuf.Pixbuf.new_from_file(os.path.join(pixdir, alternate))
 
392
            else:
 
393
                icon = icontheme.load_icon("dialog-question", size, 0)
 
394
        return icon
 
395
 
 
396
    def _connectToProject(self, project):
 
397
        """Connect signal handlers to a project.
 
398
 
 
399
        This first disconnects any handlers connected to an old project.
 
400
        If project is None, this just disconnects any connected handlers.
 
401
        """
 
402
        self.project_signals.connect(project, "asset-added", None,
 
403
                self._assetAddedCb)
 
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)
 
412
 
 
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)
 
417
 
 
418
    def _setClipView(self, view_type):
 
419
        """
 
420
        Set which clip view to use when medialibrary is showing clips.
 
421
        view_type: one of SHOW_TREEVIEW or SHOW_ICONVIEW
 
422
        """
 
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()
 
430
        for path in paths:
 
431
            self._viewSelectPath(path)
 
432
 
 
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()
 
440
 
 
441
        if not len(self.storemodel):
 
442
            self._welcome_infobar.show_all()
 
443
 
 
444
    def showImportSourcesDialog(self):
 
445
        """Pop up the "Import Sources" dialog box"""
 
446
        if self._importDialog:
 
447
            return
 
448
 
 
449
        chooser_action = Gtk.FileChooserAction.OPEN
 
450
        dialogtitle = _("Select One or More Files")
 
451
 
 
452
        close_after = Gtk.CheckButton(label=_("Close after importing files"))
 
453
        close_after.set_active(self.app.settings.closeImportDialog)
 
454
 
 
455
        self._importDialog = Gtk.FileChooserDialog(title=dialogtitle, transient_for=None,
 
456
                                           action=chooser_action)
 
457
 
 
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()
 
493
 
 
494
    def _updateProgressbar(self):
 
495
        """
 
496
        Update the _progressbar with the ratio of clips imported vs the total
 
497
        """
 
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
 
503
 
 
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))
 
512
 
 
513
    def _getThumbnailInDir(self, dir, hash):
 
514
        """
 
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.
 
517
 
 
518
        The cache dirs might have resolutions of 256 and/or 128,
 
519
        while we need 128 (for iconview) and 64 (for listview).
 
520
        """
 
521
        path_256 = dir + "large/" + hash + ".png"
 
522
        path_128 = dir + "normal/" + hash + ".png"
 
523
        interpolation = GdkPixbuf.InterpType.BILINEAR
 
524
 
 
525
        # First, try the 128 version since that's the native resolution we want:
 
526
        try:
 
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
 
531
        except GLib.GError:
 
532
            # path_128 doesn't exist, try the 256 version
 
533
            try:
 
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
 
539
            except GLib.GError:
 
540
                return None, None
 
541
 
 
542
    def _generateThumbnails(self, uri):
 
543
        if not self.thumbnailer:
 
544
            # TODO: Use thumbnails generated with GStreamer.
 
545
            return None
 
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,
 
551
                                    cancellable=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)
 
556
            return None
 
557
        pixbuf_128 = self.thumbnailer.generate_thumbnail(uri, mime)
 
558
        if not pixbuf_128:
 
559
            self.debug("Thumbnailer failed thumbnailing %s", uri)
 
560
            return None
 
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
 
564
 
 
565
    def _addAsset(self, asset):
 
566
        # 128 is the normal size for thumbnails, but for *icons* it looks insane
 
567
        LARGE_SIZE = 96
 
568
        info = asset.get_info()
 
569
 
 
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()
 
582
            try:
 
583
                thumb_dir = os.environ['XDG_CACHE_HOME']
 
584
                thumb_64, thumb_128 = self._getThumbnailInDir(thumb_dir, thumbnail_hash)
 
585
            except KeyError:
 
586
                thumb_64, thumb_128 = (None, None)
 
587
            if thumb_64 is None:
 
588
                thumb_dir = os.path.expanduser("~/.cache/thumbnails/")
 
589
                thumb_64, thumb_128 = self._getThumbnailInDir(thumb_dir, thumbnail_hash)
 
590
            if thumb_64 is None:
 
591
                thumb_dir = os.path.expanduser("~/.thumbnails/")
 
592
                thumb_64, thumb_128 = self._getThumbnailInDir(thumb_dir, thumbnail_hash)
 
593
            if thumb_64 is None:
 
594
                if asset.is_image():
 
595
                    thumb_64 = self._getIcon("image-x-generic")
 
596
                    thumb_128 = self._getIcon("image-x-generic", None, LARGE_SIZE)
 
597
                else:
 
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)
 
603
        else:
 
604
            thumb_64 = self._getIcon("audio-x-generic")
 
605
            thumb_128 = self._getIcon("audio-x-generic", None, LARGE_SIZE)
 
606
 
 
607
        if info.get_duration() == Gst.CLOCK_TIME_NONE:
 
608
            duration = ''
 
609
        else:
 
610
            duration = beautify_length(info.get_duration())
 
611
 
 
612
        name = info_name(info)
 
613
 
 
614
        self.pending_rows.append((thumb_64,
 
615
                                  thumb_128,
 
616
                                  beautify_info(info),
 
617
                                  asset,
 
618
                                  info.get_uri(),
 
619
                                  duration,
 
620
                                  name))
 
621
        if len(self.pending_rows) > 50:
 
622
            self.flush_pending_rows()
 
623
 
 
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[:]
 
629
 
 
630
    # medialibrary callbacks
 
631
 
 
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)
 
638
 
 
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
 
643
        uri = asset.get_id()
 
644
        for row in model:
 
645
            if uri == row[COL_URI]:
 
646
                model.remove(row.iter)
 
647
                break
 
648
        if not len(model):
 
649
            self._welcome_infobar.show_all()
 
650
        self.debug("Removing: %s", uri)
 
651
 
 
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()
 
658
 
 
659
    def _sourcesStartedImportingCb(self, unused_project):
 
660
        self.import_start_time = time.time()
 
661
        self._welcome_infobar.hide()
 
662
        self._progressbar.show()
 
663
 
 
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()
 
668
        if self._errors:
 
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.",
 
674
                            errors_amount)
 
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)
 
678
 
 
679
            self._view_error_button.set_label(btn_text)
 
680
            self._warning_label.set_text(text)
 
681
            self._import_warning_infobar.show_all()
 
682
 
 
683
        missing_thumbs = self._missing_thumbs
 
684
        self._missing_thumbs = []
 
685
        if 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()
 
689
 
 
690
    def _generateThumbnailsThread(self, missing_thumbs):
 
691
        for uri in missing_thumbs:
 
692
            thumbnails = self._generateThumbnails(uri)
 
693
            if not thumbnails:
 
694
                continue
 
695
            pixbuf_128, pixbuf_64 = thumbnails
 
696
            # Search through the model for the row corresponding to the asset.
 
697
            found = False
 
698
            for row in self.storemodel:
 
699
                if uri == row[COL_URI]:
 
700
                    found = True
 
701
                    # Finally, show the new pixbuf in the UI
 
702
                    if pixbuf_128:
 
703
                        row[COL_ICON_128] = pixbuf_128
 
704
                    if pixbuf_64:
 
705
                        row[COL_ICON_64] = pixbuf_64
 
706
                    break
 
707
            if not found:
 
708
                # Can happen if the user removed the asset in the meanwhile.
 
709
                self.log("%s needed a thumbnail, but vanished from storemodel", uri)
 
710
 
 
711
    ## Error Dialog Box callbacks
 
712
 
 
713
    def _errorDialogBoxCloseCb(self, dialog):
 
714
        dialog.destroy()
 
715
 
 
716
    def _errorDialogBoxResponseCb(self, dialog, unused_response):
 
717
        dialog.destroy()
 
718
 
 
719
    ## Import Sources Dialog Box callbacks
 
720
 
 
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:
 
731
                dialogbox.destroy()
 
732
                self._importDialog = None
 
733
        else:
 
734
            dialogbox.destroy()
 
735
            self._importDialog = None
 
736
 
 
737
    def _dialogBoxCloseCb(self, unused_dialogbox):
 
738
        self.debug("closing")
 
739
        self._importDialog = None
 
740
 
 
741
    def _removeSources(self):
 
742
        """
 
743
        Determine which clips are selected in the icon or list view,
 
744
        and ask MediaLibrary to remove them from the project.
 
745
        """
 
746
        model = self.treeview.get_model()
 
747
        paths = self.getSelectedPaths()
 
748
        if paths is None or paths < 1:
 
749
            return
 
750
        # use row references so we don't have to care if a path has been removed
 
751
        rows = []
 
752
        for path in paths:
 
753
            row = Gtk.TreeRowReference.new(model, path)
 
754
            rows.append(row)
 
755
 
 
756
        self.app.action_log.begin("remove clip from source list")
 
757
        for row in rows:
 
758
            asset = model[row.get_path()][COL_ASSET]
 
759
            self.app.current_project.remove_asset(asset)
 
760
        self.app.action_log.commit()
 
761
 
 
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()
 
765
        for layer in layers:
 
766
            for clip in layer.get_clips():
 
767
                if clip.get_asset() == asset:
 
768
                    return True
 
769
        return False
 
770
 
 
771
    def _selectUnusedSources(self):
 
772
        """
 
773
        Select, in the media library, unused sources in the project.
 
774
        """
 
775
        assets = self.app.current_project.list_assets(GES.UriClip)
 
776
        unused_sources_uris = []
 
777
 
 
778
        model = self.treeview.get_model()
 
779
        selection = self.treeview.get_selection()
 
780
        for asset in assets:
 
781
            if not self._sourceIsUsed(asset):
 
782
                unused_sources_uris.append(asset.get_id())
 
783
 
 
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()
 
790
 
 
791
        for row in model:
 
792
            if row[COL_URI] not in unused_sources_uris:
 
793
                if self.clip_view == SHOW_TREEVIEW:
 
794
                    selection.unselect_iter(row.iter)
 
795
                else:
 
796
                    self.iconview.unselect_path(row.path)
 
797
 
 
798
    ## UI callbacks
 
799
 
 
800
    def _removeClickedCb(self, unused_widget=None):
 
801
        """ Called when a user clicks on the remove button """
 
802
        self._removeSources()
 
803
 
 
804
    def _clipPropertiesCb(self, unused_widget=None):
 
805
        """
 
806
        Show the clip properties (resolution, framerate, audio channels...)
 
807
        and allow setting them as the new project settings.
 
808
        """
 
809
        paths = self.getSelectedPaths()
 
810
        if not paths:
 
811
            self.debug("No item selected")
 
812
            return
 
813
        # Only use the first item.
 
814
        path = paths[0]
 
815
        asset = self.storemodel[path][COL_ASSET]
 
816
        dialog = ClipMediaPropsDialog(self.app.current_project, asset)
 
817
        dialog.run()
 
818
 
 
819
    def _warningInfoBarDismissedCb(self, unused_button):
 
820
        self._resetErrorList()
 
821
 
 
822
    def _resetErrorList(self):
 
823
        self._errors = []
 
824
        self._import_warning_infobar.hide()
 
825
 
 
826
    def _viewErrorsButtonClickedCb(self, unused_button):
 
827
        """
 
828
        Show a FileListErrorDialog to display import _errors.
 
829
        """
 
830
        if len(self._errors) > 1:
 
831
            msgs = (_("Error while analyzing files"),
 
832
                    _("The following files can not be used with Pitivi."))
 
833
        else:
 
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()
 
844
 
 
845
    def _toggleViewTypeCb(self, widget):
 
846
        if widget.get_active():
 
847
            self._setClipView(SHOW_TREEVIEW)
 
848
        else:
 
849
            self._setClipView(SHOW_ICONVIEW)
 
850
 
 
851
    def _rowUnderMouseSelected(self, view, event):
 
852
        if isinstance(view, Gtk.TreeView):
 
853
            path = None
 
854
            tup = view.get_path_at_pos(int(event.x), int(event.y))
 
855
            if tup:
 
856
                path, column, x, y = tup
 
857
            if path:
 
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))
 
862
            if path:
 
863
                selection = view.get_selected_items()
 
864
                return view.path_is_selected(path) and len(selection)
 
865
        else:
 
866
            raise RuntimeError("Unknown media library view type: %s" % type(view))
 
867
 
 
868
        return False
 
869
 
 
870
    def _nothingUnderMouse(self, view, event):
 
871
        return not bool(view.get_path_at_pos(int(event.x), int(event.y)))
 
872
 
 
873
    def _viewGetFirstSelected(self):
 
874
        paths = self.getSelectedPaths()
 
875
        return paths[0]
 
876
 
 
877
    def _viewHasSelection(self):
 
878
        paths = self.getSelectedPaths()
 
879
        return bool(len(paths))
 
880
 
 
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))
 
884
            return pathinfo[0]
 
885
        elif self.clip_view == SHOW_ICONVIEW:
 
886
            return self.iconview.get_path_at_pos(int(event.x), int(event.y))
 
887
 
 
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)
 
894
 
 
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()
 
901
 
 
902
    def _treeViewButtonPressEventCb(self, treeview, event):
 
903
        self._updateDraggedPaths(treeview, event)
 
904
 
 
905
        Gtk.TreeView.do_button_press_event(treeview, event)
 
906
 
 
907
        ts = self.treeview.get_selection()
 
908
        if self._draggedPaths:
 
909
            for path in self._draggedPaths:
 
910
                ts.select_path(path)
 
911
 
 
912
        return True
 
913
 
 
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.
 
920
                pass
 
921
            chain_up = False
 
922
        elif not event.get_state() & (Gdk.ModifierType.CONTROL_MASK | Gdk.ModifierType.SHIFT_MASK):
 
923
            chain_up = not self._rowUnderMouseSelected(view, event)
 
924
        else:
 
925
            chain_up = True
 
926
 
 
927
        if not chain_up:
 
928
            self._draggedPaths = self.getSelectedPaths()
 
929
        else:
 
930
            self._draggedPaths = None
 
931
 
 
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)
 
936
 
 
937
        if not state and not self.dragged:
 
938
            ts.unselect_all()
 
939
            if path:
 
940
                ts.select_path(path[0])
 
941
 
 
942
    def _viewSelectionChangedCb(self, unused):
 
943
        selected_items = len(self.getSelectedPaths())
 
944
        if selected_items:
 
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)
 
952
        else:
 
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)
 
957
 
 
958
    def _itemOrRowActivatedCb(self, unused_view, path, *unused_column):
 
959
        """
 
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.
 
963
        """
 
964
        asset = self.modelFilter[path][COL_ASSET]
 
965
        self.emit('play', asset)
 
966
 
 
967
    def _iconViewButtonPressEventCb(self, iconview, event):
 
968
        self._updateDraggedPaths(iconview, event)
 
969
 
 
970
        Gtk.IconView.do_button_press_event(iconview, event)
 
971
 
 
972
        if self._draggedPaths:
 
973
            for path in self._draggedPaths:
 
974
                self.iconview.select_path(path)
 
975
 
 
976
        self.iconview_cursor_pos = self.iconview.get_path_at_pos(event.x, event.y)
 
977
 
 
978
        return True
 
979
 
 
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)
 
986
 
 
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)
 
991
 
 
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)
 
999
 
 
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)
 
1005
 
 
1006
        # Make sure that the sources added to the project are added added
 
1007
        self.flush_pending_rows()
 
1008
 
 
1009
    def _newProjectFailedCb(self, unused_pitivi, unused_reason, unused_uri):
 
1010
        self.storemodel.clear()
 
1011
        self.project_signals.disconnectAll()
 
1012
        self._project = None
 
1013
 
 
1014
    def _addUris(self, uris):
 
1015
        if self.app.current_project:
 
1016
            self.app.current_project.addUris(uris)
 
1017
        else:
 
1018
            self.warning("Adding uris to project, but the project has changed in the meantime")
 
1019
        return False
 
1020
 
 
1021
    ## Drag and Drop
 
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())
 
1025
 
 
1026
        directories = []
 
1027
        filenames = []
 
1028
 
 
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)
 
1042
                else:
 
1043
                    self.warning("Unusable file: %s, %s", raw_uri, path)
 
1044
            else:
 
1045
                self.fixme("Importing remote files is not implemented: %s", raw_uri)
 
1046
 
 
1047
        if directories:
 
1048
            # Recursively import from folders that were dragged into the library
 
1049
            self.app.threads.addThread(PathWalker, directories, self._addUris)
 
1050
        if filenames:
 
1051
            self.app.current_project.addUris(filenames)
 
1052
 
 
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]
 
1057
        data.set_uris(uris)
 
1058
 
 
1059
    def _dndDragBeginCb(self, unused_view, context):
 
1060
        self.info("Drag operation begun")
 
1061
        self.dragged = True
 
1062
        paths = self.getSelectedPaths()
 
1063
 
 
1064
        if not paths:
 
1065
            context.drag_abort(int(time.time()))
 
1066
        else:
 
1067
            row = self.modelFilter[paths[0]]
 
1068
            Gtk.drag_set_icon_pixbuf(context, row[COL_ICON_64], 0, 0)
 
1069
 
 
1070
    def _dndDragEndCb(self, unused_view, unused_context):
 
1071
        self.info("Drag operation ended")
 
1072
        self.dragged = False
 
1073
 
 
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()
 
1080
 
 
1081
    def _getSelectedPathsTreeView(self):
 
1082
        model, rows = self.treeview.get_selection().get_selected_rows()
 
1083
        return rows
 
1084
 
 
1085
    def _getSelectedPathsIconView(self):
 
1086
        paths = self.iconview.get_selected_items()
 
1087
        paths.reverse()
 
1088
        return paths
 
1089
 
 
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()]
 
1097
 
 
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()]