Created
February 16, 2017 19:18
-
-
Save shivam-tripathi/0ad383ed3dcdd0e9780cdef2bc7cce3f to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# -*- coding: utf-8 -*- | |
# Pitivi video editor | |
# Copyright (c) 2005, Edward Hervey <[email protected]> | |
# Copyright (c) 2009, Alessandro Decina <[email protected]> | |
# Copyright (c) 2012, Jean-François Fortin Tam <[email protected]> | |
# | |
# This program is free software; you can redistribute it and/or | |
# modify it under the terms of the GNU Lesser General Public | |
# License as published by the Free Software Foundation; either | |
# version 2.1 of the License, or (at your option) any later version. | |
# | |
# This program is distributed in the hope that it will be useful, | |
# but WITHOUT ANY WARRANTY; without even the implied warranty of | |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |
# Lesser General Public License for more details. | |
# | |
# You should have received a copy of the GNU Lesser General Public | |
# License along with this program; if not, write to the | |
# Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, | |
# Boston, MA 02110-1301, USA. | |
import os | |
import time | |
from gettext import gettext as _ | |
from gettext import ngettext | |
from hashlib import md5 | |
from urllib.parse import unquote | |
from urllib.parse import urlparse | |
from gi.repository import Gdk | |
from gi.repository import GdkPixbuf | |
from gi.repository import GES | |
from gi.repository import Gio | |
from gi.repository import GLib | |
from gi.repository import GObject | |
from gi.repository import Gst | |
from gi.repository import GstPbutils | |
from gi.repository import Gtk | |
from gi.repository import Pango | |
from pitivi.configure import get_pixmap_dir | |
from pitivi.configure import get_ui_dir | |
from pitivi.dialogs.clipmediaprops import ClipMediaPropsDialog | |
from pitivi.dialogs.filelisterrordialog import FileListErrorDialog | |
from pitivi.mediafilespreviewer import PreviewWidget | |
from pitivi.settings import GlobalSettings | |
from pitivi.timeline.previewers import ThumbnailCache | |
from pitivi.utils.loggable import Loggable | |
from pitivi.utils.misc import disconnectAllByFunc | |
from pitivi.utils.misc import get_proxy_target | |
from pitivi.utils.misc import path_from_uri | |
from pitivi.utils.misc import PathWalker | |
from pitivi.utils.misc import quote_uri | |
from pitivi.utils.proxy import ProxyingStrategy | |
from pitivi.utils.proxy import ProxyManager | |
from pitivi.utils.ui import beautify_asset | |
from pitivi.utils.ui import beautify_ETA | |
from pitivi.utils.ui import beautify_length | |
from pitivi.utils.ui import FILE_TARGET_ENTRY | |
from pitivi.utils.ui import fix_infobar | |
from pitivi.utils.ui import info_name | |
from pitivi.utils.ui import LARGE_THUMB_WIDTH | |
from pitivi.utils.ui import SMALL_THUMB_WIDTH | |
from pitivi.utils.ui import SPACING | |
from pitivi.utils.ui import URI_TARGET_ENTRY | |
# Values used in the settings file. | |
SHOW_TREEVIEW = 1 | |
SHOW_ICONVIEW = 2 | |
GlobalSettings.addConfigSection('clip-library') | |
GlobalSettings.addConfigOption('lastImportFolder', | |
section='clip-library', | |
key='last-folder', | |
environment='PITIVI_IMPORT_FOLDER', | |
default=os.path.expanduser("~")) | |
GlobalSettings.addConfigOption('closeImportDialog', | |
section='clip-library', | |
key='close-import-dialog-after-import', | |
default=True) | |
GlobalSettings.addConfigOption('lastClipView', | |
section='clip-library', | |
key='last-clip-view', | |
type_=int, | |
default=SHOW_ICONVIEW) | |
STORE_MODEL_STRUCTURE = ( | |
GdkPixbuf.Pixbuf, GdkPixbuf.Pixbuf, | |
str, object, str, str, object) | |
(COL_ICON_64, | |
COL_ICON_128, | |
COL_INFOTEXT, | |
COL_ASSET, | |
COL_URI, | |
COL_SEARCH_TEXT, | |
COL_THUMB_DECORATOR) = list(range(len(STORE_MODEL_STRUCTURE))) | |
# This whitelist is made from personal knowledge of file extensions in the wild, | |
# from gst-inspect |grep demux, | |
# http://en.wikipedia.org/wiki/Comparison_of_container_formats and | |
# http://en.wikipedia.org/wiki/List_of_file_formats#Video | |
# ...and looking at the contents of /usr/share/mime | |
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", "mp2t"), # noqa | |
"application": ("mxf",), | |
# Don't forget audio formats | |
"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"), # noqa | |
# ...and image formats | |
"image": ("jp2", "jpeg", "png", "svg+xml")} | |
SUPPORTED_MIMETYPES = [] | |
for category, mime_types in SUPPORTED_FILE_FORMATS.items(): | |
for mime in mime_types: | |
SUPPORTED_MIMETYPES.append(category + "/" + mime) | |
class FileChooserExtraWidget(Gtk.Grid, Loggable): | |
def __init__(self, app): | |
Loggable.__init__(self) | |
Gtk.Grid.__init__(self) | |
self.app = app | |
self.set_row_spacing(SPACING) | |
self.set_column_spacing(SPACING) | |
self.__close_after = Gtk.CheckButton(label=_("Close after importing files")) | |
self.__close_after.set_active(self.app.settings.closeImportDialog) | |
self.attach(self.__close_after, 0, 0, 1, 2) | |
self.__automatic_proxies = Gtk.RadioButton.new_with_label( | |
None, _("Create proxies when the media format is not supported officially")) | |
self.__automatic_proxies.set_tooltip_markup( | |
_("Let Pitivi decide when to" | |
" create proxy files and when not. The decision will be made" | |
" depending on the file format, and how well it is supported." | |
" For example H.264, FLAC files contained in QuickTime will" | |
" not be proxied, but AAC, H.264 contained in MPEG-TS will.\n\n" | |
"<i>This is the only option officially supported by the" | |
" Pitivi developers and thus is the safest." | |
"</i>")) | |
self.__force_proxies = Gtk.RadioButton.new_with_label_from_widget( | |
self.__automatic_proxies, _("Create proxies for all files")) | |
self.__force_proxies.set_tooltip_markup( | |
_("Use proxies for every imported file" | |
" whatever its current media format is.")) | |
self.__no_proxies = Gtk.RadioButton.new_with_label_from_widget( | |
self.__automatic_proxies, _("Do not use proxy files")) | |
if self.app.settings.proxyingStrategy == ProxyingStrategy.ALL: | |
self.__force_proxies.set_active(True) | |
elif self.app.settings.proxyingStrategy == ProxyingStrategy.NOTHING: | |
self.__no_proxies.set_active(True) | |
else: | |
self.__automatic_proxies.set_active(True) | |
self.attach(self.__automatic_proxies, 1, 0, 1, 1) | |
self.attach(self.__force_proxies, 1, 1, 1, 1) | |
self.attach(self.__no_proxies, 1, 2, 1, 1) | |
self.show_all() | |
def saveValues(self): | |
self.app.settings.closeImportDialog = self.__close_after.get_active() | |
if self.__force_proxies.get_active(): | |
self.app.settings.proxyingStrategy = ProxyingStrategy.ALL | |
elif self.__no_proxies.get_active(): | |
self.app.settings.proxyingStrategy = ProxyingStrategy.NOTHING | |
else: | |
self.app.settings.proxyingStrategy = ProxyingStrategy.AUTOMATIC | |
class AssetThumbnail(Loggable): | |
"""Provider of decorated thumbnails for an asset.""" | |
EMBLEMS = {} | |
PROXIED = "asset-proxied" | |
NO_PROXY = "no-proxy" | |
IN_PROGRESS = "asset-proxy-in-progress" | |
ASSET_PROXYING_ERROR = "asset-proxying-error" | |
DEFAULT_ALPHA = 255 | |
icons_by_name = {} | |
for status in [PROXIED, IN_PROGRESS, ASSET_PROXYING_ERROR]: | |
EMBLEMS[status] = [] | |
for size in [32, 64]: | |
EMBLEMS[status].append(GdkPixbuf.Pixbuf.new_from_file_at_size( | |
os.path.join(get_pixmap_dir(), "%s.svg" % status), size, size)) | |
def __init__(self, asset, proxy_manager): | |
Loggable.__init__(self) | |
self.__asset = asset | |
self.src_small, self.src_large = self.__get_thumbnails() | |
self.proxy_manager = proxy_manager | |
self.decorate() | |
def __get_thumbnails(self): | |
"""Gets the base source thumbnails. | |
Returns: | |
List[GdkPixbuf.Pixbuf]: The small thumbnail and the large thumbnail | |
to be decorated. | |
""" | |
video_streams = [ | |
stream_info | |
for stream_info in self.__asset.get_info().get_stream_list() | |
if isinstance(stream_info, GstPbutils.DiscovererVideoInfo)] | |
if video_streams: | |
# Check if the files have thumbnails in the user's cache directory. | |
real_uri = get_proxy_target(self.__asset).props.id | |
small_thumb, large_thumb = self.__get_thumbnails_from_xdg_cache(real_uri) | |
if not small_thumb: | |
if self.__asset.is_image(): | |
path = Gst.uri_get_location(real_uri) | |
try: | |
pixbuf = GdkPixbuf.Pixbuf.new_from_file(path) | |
width = pixbuf.props.width | |
height = pixbuf.props.height | |
small_thumb = pixbuf.scale_simple( | |
SMALL_THUMB_WIDTH, | |
SMALL_THUMB_WIDTH * height / width, | |
GdkPixbuf.InterpType.BILINEAR) | |
large_thumb = pixbuf.scale_simple( | |
LARGE_THUMB_WIDTH, | |
LARGE_THUMB_WIDTH * height / width, | |
GdkPixbuf.InterpType.BILINEAR) | |
except GLib.Error as error: | |
self.debug("Failed loading thumbnail because: %s", error) | |
small_thumb, large_thumb = self.__get_icons("image-x-generic") | |
else: | |
# Build or reuse a ThumbnailCache. | |
thumb_cache = ThumbnailCache.get(self.__asset) | |
small_thumb = thumb_cache.getPreviewThumbnail() | |
if not small_thumb: | |
small_thumb, large_thumb = self.__get_icons("video-x-generic") | |
else: | |
width = small_thumb.props.width | |
height = small_thumb.props.height | |
large_thumb = small_thumb.scale_simple( | |
LARGE_THUMB_WIDTH, | |
LARGE_THUMB_WIDTH * height / width, | |
GdkPixbuf.InterpType.BILINEAR) | |
if width > SMALL_THUMB_WIDTH: | |
small_thumb = small_thumb.scale_simple( | |
SMALL_THUMB_WIDTH, | |
SMALL_THUMB_WIDTH * height / width, | |
GdkPixbuf.InterpType.BILINEAR) | |
else: | |
small_thumb, large_thumb = self.__get_icons("audio-x-generic") | |
return small_thumb, large_thumb | |
def __get_thumbnails_from_xdg_cache(self, real_uri): | |
"""Gets pixbufs for the specified thumbnail from the user's cache dir. | |
Looks for thumbnails according to the [Thumbnail Managing Standard](https://specifications.freedesktop.org/thumbnail-spec/thumbnail-spec-latest.html#DIRECTORY). | |
Args: | |
real_uri (str): The URI of the asset. | |
Returns: | |
List[GdkPixbuf.Pixbuf]: The small thumbnail and the large thumbnail, | |
if available in the user's cache directory, otherwise (None, None). | |
""" | |
quoted_uri = quote_uri(real_uri) | |
thumbnail_hash = md5(quoted_uri.encode()).hexdigest() | |
thumb_dir = os.path.join(GLib.get_user_cache_dir(), "thumbnail") | |
path_128 = thumb_dir + "normal/" + thumbnail_hash + ".png" | |
interpolation = GdkPixbuf.InterpType.BILINEAR | |
# The cache dirs might have resolutions of 256 and/or 128, | |
# while we need 128 (for iconview) and 64 (for listview). | |
# First, try the 128 version since that's the native resolution we want. | |
try: | |
large_thumb = GdkPixbuf.Pixbuf.new_from_file(path_128) | |
w, h = large_thumb.get_width(), large_thumb.get_height() | |
small_thumb = large_thumb.scale_simple(w / 2, h / 2, interpolation) | |
return small_thumb, large_thumb | |
except GLib.GError: | |
# path_128 doesn't exist, try the 256 version. | |
path_256 = thumb_dir + "large/" + thumbnail_hash + ".png" | |
try: | |
thumb_256 = GdkPixbuf.Pixbuf.new_from_file(path_256) | |
w, h = thumb_256.get_width(), thumb_256.get_height() | |
large_thumb = thumb_256.scale_simple(w / 2, h / 2, interpolation) | |
small_thumb = thumb_256.scale_simple(w / 4, h / 4, interpolation) | |
return small_thumb, large_thumb | |
except GLib.GError: | |
return None, None | |
@classmethod | |
def __get_icons(cls, icon_name): | |
if icon_name not in cls.icons_by_name: | |
small_icon = cls.__get_icon(icon_name, SMALL_THUMB_WIDTH) | |
large_icon = cls.__get_icon(icon_name, LARGE_THUMB_WIDTH) | |
cls.icons_by_name[icon_name] = (small_icon, large_icon) | |
return cls.icons_by_name[icon_name] | |
@classmethod | |
def __get_icon(cls, icon_name, size): | |
icon_theme = Gtk.IconTheme.get_default() | |
try: | |
icon = icon_theme.load_icon(icon_name, size, 0) | |
except GLib.Error: | |
icon = icon_theme.load_icon("dialog-question", size, 0) | |
return icon | |
def __setState(self): | |
asset = self.__asset | |
target = asset.get_proxy_target() | |
if target and not target.get_error(): | |
# The asset is a proxy. | |
self.state = self.PROXIED | |
elif asset.proxying_error: | |
self.state = self.ASSET_PROXYING_ERROR | |
elif self.proxy_manager.is_asset_queued(asset): | |
self.state = self.IN_PROGRESS | |
else: | |
self.state = self.NO_PROXY | |
def decorate(self): | |
self.__setState() | |
if self.state == self.NO_PROXY: | |
self.small_thumb = self.src_small | |
self.large_thumb = self.src_large | |
return | |
self.small_thumb = self.src_small.copy() | |
self.large_thumb = self.src_large.copy() | |
for thumb, src in zip([self.small_thumb, self.large_thumb], | |
self.EMBLEMS[self.state]): | |
# We need to set dest_y == offset_y for the source image | |
# not to be cropped, that API is weird. | |
if thumb.get_height() < src.get_height(): | |
src = src.copy() | |
src = src.scale_simple(src.get_width(), | |
thumb.get_height(), | |
GdkPixbuf.InterpType.BILINEAR) | |
src.composite(thumb, dest_x=0, | |
dest_y=thumb.get_height() - src.get_height(), | |
dest_width=src.get_width(), | |
dest_height=src.get_height(), | |
offset_x=0, | |
offset_y=thumb.get_height() - src.get_height(), | |
scale_x=1.0, scale_y=1.0, | |
interp_type=GdkPixbuf.InterpType.BILINEAR, | |
overall_alpha=self.DEFAULT_ALPHA) | |
class MediaLibraryWidget(Gtk.Box, Loggable): | |
"""Widget for managing assets. | |
Attributes: | |
app (Pitivi): The app. | |
""" | |
__gsignals__ = { | |
'play': (GObject.SignalFlags.RUN_LAST, None, | |
(GObject.TYPE_PYOBJECT,))} | |
def __init__(self, app): | |
Gtk.Box.__init__(self) | |
Loggable.__init__(self) | |
self.pending_rows = [] | |
self.app = app | |
self._errors = [] | |
self._project = None | |
self._draggedPaths = None | |
self.dragged = False | |
self.clip_view = self.app.settings.lastClipView | |
if self.clip_view not in (SHOW_TREEVIEW, SHOW_ICONVIEW): | |
self.clip_view = SHOW_ICONVIEW | |
self.import_start_time = time.time() | |
self._last_imported_uris = set() | |
self.__last_proxying_estimate_time = _("Unknown") | |
self.set_orientation(Gtk.Orientation.VERTICAL) | |
builder = Gtk.Builder() | |
builder.add_from_file(os.path.join(get_ui_dir(), "medialibrary.ui")) | |
builder.connect_signals(self) | |
self._welcome_infobar = builder.get_object("welcome_infobar") | |
fix_infobar(self._welcome_infobar) | |
self._project_settings_infobar = Gtk.InfoBar() | |
self._project_settings_infobar.hide() | |
self._project_settings_infobar.set_message_type(Gtk.MessageType.OTHER) | |
self._project_settings_infobar.set_show_close_button(True) | |
self._project_settings_infobar.add_button(_("Project Settings"), Gtk.ResponseType.OK) | |
self._project_settings_infobar.connect("response", self.__projectSettingsSetInfobarCb) | |
self._project_settings_label = Gtk.Label() | |
self._project_settings_label.set_line_wrap(True) | |
self._project_settings_label.show() | |
content_area = self._project_settings_infobar.get_content_area() | |
content_area.add(self._project_settings_label) | |
fix_infobar(self._project_settings_infobar) | |
self._import_warning_infobar = builder.get_object("warning_infobar") | |
fix_infobar(self._import_warning_infobar) | |
self._import_warning_infobar.hide() | |
self._import_warning_infobar.connect("response", self.__warningInfobarCb) | |
self._warning_label = builder.get_object("warning_label") | |
self._view_error_button = builder.get_object("view_error_button") | |
toolbar = builder.get_object("medialibrary_toolbar") | |
toolbar.get_style_context().add_class(Gtk.STYLE_CLASS_INLINE_TOOLBAR) | |
self._import_button = builder.get_object("media_import_button") | |
self._clipprops_button = builder.get_object("media_props_button") | |
self._listview_button = builder.get_object("media_listview_button") | |
searchEntry = builder.get_object("media_search_entry") | |
# Store | |
self.storemodel = Gtk.ListStore(*STORE_MODEL_STRUCTURE) | |
self.storemodel.set_sort_func( | |
COL_URI, MediaLibraryWidget.compare_basename) | |
# Prefer to sort the media library elements by URI | |
# rather than show them randomly. | |
self.storemodel.set_sort_column_id(COL_URI, Gtk.SortType.ASCENDING) | |
self.storemodel.connect("row-deleted", self.__updateViewCb) | |
self.storemodel.connect("row-inserted", self.__updateViewCb) | |
# Scrolled Windows | |
self.treeview_scrollwin = Gtk.ScrolledWindow() | |
self.treeview_scrollwin.set_policy( | |
Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC) | |
self.treeview_scrollwin.set_shadow_type(Gtk.ShadowType.ETCHED_IN) | |
self.treeview_scrollwin.get_accessible().set_name( | |
"media_listview_scrollwindow") | |
self.iconview_scrollwin = Gtk.ScrolledWindow() | |
self.iconview_scrollwin.set_policy( | |
Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) | |
self.iconview_scrollwin.set_shadow_type(Gtk.ShadowType.ETCHED_IN) | |
self.iconview_scrollwin.get_accessible().set_name( | |
"media_iconview_scrollwindow") | |
# Filtering model for the search box. | |
# Use this instead of using self.storemodel directly | |
self.modelFilter = self.storemodel.filter_new() | |
self.modelFilter.set_visible_func( | |
self._setRowVisible, data=searchEntry) | |
# TreeView | |
# Displays icon, name, type, length | |
self.treeview = Gtk.TreeView(model=self.modelFilter) | |
self.treeview_scrollwin.add(self.treeview) | |
self.treeview.connect( | |
"button-press-event", self._treeViewButtonPressEventCb) | |
self.treeview.connect( | |
"button-release-event", self._treeViewButtonReleaseEventCb) | |
self.treeview.connect("row-activated", self._itemOrRowActivatedCb) | |
self.treeview.set_headers_visible(False) | |
self.treeview.set_property("search_column", COL_SEARCH_TEXT) | |
tsel = self.treeview.get_selection() | |
tsel.set_mode(Gtk.SelectionMode.MULTIPLE) | |
tsel.connect("changed", self._viewSelectionChangedCb) | |
# pixbufcol = Gtk.TreeViewColumn(_("Icon")) | |
# pixbufcol.set_expand(False) | |
# pixbufcol.set_spacing(SPACING) | |
# self.treeview.append_column(pixbufcol) | |
# pixcell = Gtk.CellRendererPixbuf() | |
# pixcell.props.xpad = 6 | |
# pixbufcol.pack_start(pixcell, True) | |
# pixbufcol.add_attribute(pixcell, 'pixbuf', COL_ICON_64) | |
namecol = Gtk.TreeViewColumn(_("Information")) | |
self.treeview.append_column(namecol) | |
namecol.set_expand(True) | |
namecol.set_spacing(SPACING) | |
namecol.set_sizing(Gtk.TreeViewColumnSizing.GROW_ONLY) | |
namecol.set_min_width(150) | |
pixcell = Gtk.CellRendererPixbuf() | |
pixcell.props.xpad = 6 | |
namecol.pack_start(pixcell, True) | |
namecol.add_attribute(pixcell, 'pixbuf', COL_ICON_64) | |
txtcell = Gtk.CellRendererText() | |
txtcell.set_property("ellipsize", Pango.EllipsizeMode.START) | |
namecol.pack_start(txtcell, True) | |
namecol.add_attribute(txtcell, "markup", COL_INFOTEXT) | |
# IconView | |
self.iconview = Gtk.IconView(model=self.modelFilter) | |
self.iconview_scrollwin.add(self.iconview) | |
self.iconview.connect( | |
"button-press-event", self._iconViewButtonPressEventCb) | |
self.iconview.connect( | |
"button-release-event", self._iconViewButtonReleaseEventCb) | |
self.iconview.connect("item-activated", self._itemOrRowActivatedCb) | |
self.iconview.connect( | |
"selection-changed", self._viewSelectionChangedCb) | |
self.iconview.set_item_orientation(Gtk.Orientation.VERTICAL) | |
self.iconview.set_property("has_tooltip", True) | |
self.iconview.set_tooltip_column(COL_INFOTEXT) | |
self.iconview.props.item_padding = 3 | |
self.iconview.props.margin = 3 | |
self.iconview_cursor_pos = None | |
cell = Gtk.CellRendererPixbuf() | |
self.iconview.pack_start(cell, False) | |
# self.iconview.add_attribute(cell, "pixbuf", COL_ICON_128) | |
cell = Gtk.CellRendererText() | |
cell.props.alignment = Pango.Alignment.CENTER | |
cell.props.xalign = 0.5 | |
cell.props.yalign = 0.0 | |
cell.props.xpad = 0 | |
cell.props.ypad = 0 | |
cell.set_property("ellipsize", Pango.EllipsizeMode.START) | |
self.iconview.pack_start(cell, False) | |
self.iconview.add_attribute(cell, "markup", COL_SEARCH_TEXT) | |
self.iconview.set_selection_mode(Gtk.SelectionMode.MULTIPLE) | |
# The _progressbar that shows up when importing clips | |
self._progressbar = Gtk.ProgressBar() | |
self._progressbar.set_show_text(True) | |
# Connect to project. We must remove and reset the callbacks when | |
# changing project. | |
project_manager = self.app.project_manager | |
project_manager.connect( | |
"new-project-loading", self._new_project_loading_cb) | |
project_manager.connect("new-project-loaded", self._newProjectLoadedCb) | |
project_manager.connect("new-project-failed", self._newProjectFailedCb) | |
project_manager.connect("project-closed", self._projectClosedCb) | |
# Drag and Drop | |
self.drag_dest_set(Gtk.DestDefaults.DROP | Gtk.DestDefaults.MOTION, | |
[URI_TARGET_ENTRY, FILE_TARGET_ENTRY], | |
Gdk.DragAction.COPY) | |
self.drag_dest_add_uri_targets() | |
self.connect("drag_data_received", self._drag_data_received_cb) | |
self._setupViewAsDragAndDropSource(self.treeview) | |
self._setupViewAsDragAndDropSource(self.iconview) | |
# Hack so that the views have the same method as self | |
self.treeview.getSelectedItems = self.getSelectedItems | |
actions_group = Gio.SimpleActionGroup() | |
self.insert_action_group("medialibrary", actions_group) | |
self.app.shortcuts.register_group("medialibrary", _("Media Library"), position=50) | |
self.remove_assets_action = Gio.SimpleAction.new("remove-assets", None) | |
self.remove_assets_action.connect("activate", self._removeAssetsCb) | |
actions_group.add_action(self.remove_assets_action) | |
self.app.shortcuts.add("medialibrary.remove-assets", ["<Primary>Delete"], | |
_("Remove the selected assets")) | |
self.insert_at_end_action = Gio.SimpleAction.new("insert-assets-at-end", None) | |
self.insert_at_end_action.connect("activate", self._insertEndCb) | |
actions_group.add_action(self.insert_at_end_action) | |
self.app.shortcuts.add("medialibrary.insert-assets-at-end", ["Insert"], | |
_("Insert selected assets at the end of the timeline")) | |
self._updateActions() | |
# Set the state of the view mode toggle button. | |
self._listview_button.set_active(self.clip_view == SHOW_TREEVIEW) | |
# Make sure the proper view is displayed. | |
self._displayClipView() | |
# Add all the child widgets. | |
self.pack_start(toolbar, False, False, 0) | |
self.pack_start(self._welcome_infobar, False, False, 0) | |
self.pack_start(self._project_settings_infobar, False, False, 0) | |
self.pack_start(self._import_warning_infobar, False, False, 0) | |
self.pack_start(self.iconview_scrollwin, True, True, 0) | |
self.pack_start(self.treeview_scrollwin, True, True, 0) | |
self.pack_start(self._progressbar, False, False, 0) | |
def finalize(self): | |
self.debug("Finalizing %s", self) | |
self.app.project_manager.disconnect_by_func(self._new_project_loading_cb) | |
self.app.project_manager.disconnect_by_func(self._newProjectLoadedCb) | |
self.app.project_manager.disconnect_by_func(self._newProjectFailedCb) | |
self.app.project_manager.disconnect_by_func(self._projectClosedCb) | |
if not self._project: | |
self.debug("No project set...") | |
return | |
for asset in self._project.list_assets(GES.Extractable): | |
disconnectAllByFunc(asset, self.__assetProxiedCb) | |
disconnectAllByFunc(asset, self.__assetProxyingCb) | |
self.__disconnectFromProject() | |
@staticmethod | |
def compare_basename(model, iter1, iter2, unused_user_data): | |
"""Compares two model elements. | |
Args: | |
iter1 (Gtk.TreeIter): The iter identifying the first model element. | |
iter2 (Gtk.TreeIter): The iter identifying the second model element. | |
""" | |
uri1 = model[iter1][COL_URI] | |
uri2 = model[iter2][COL_URI] | |
basename1 = GLib.path_get_basename(uri1).lower() | |
basename2 = GLib.path_get_basename(uri2).lower() | |
if basename1 < basename2: | |
return -1 | |
if basename1 == basename2: | |
if uri1 < uri2: | |
return -1 | |
return 1 | |
def getAssetForUri(self, uri): | |
for path in self.modelFilter: | |
asset = path[COL_ASSET] | |
info = asset.get_info() | |
asset_uri = info.get_uri() | |
if asset_uri == uri: | |
self.debug("Found asset: %s for uri: %s", asset, uri) | |
return asset | |
self.warning("Did not find any asset for uri: %s", uri) | |
def _setupViewAsDragAndDropSource(self, view): | |
view.drag_source_set(0, [], Gdk.DragAction.COPY) | |
view.enable_model_drag_source( | |
Gdk.ModifierType.BUTTON1_MASK, [URI_TARGET_ENTRY], Gdk.DragAction.COPY) | |
view.drag_source_add_uri_targets() | |
view.connect("drag-data-get", self._dndDragDataGetCb) | |
view.connect("drag-begin", self._dndDragBeginCb) | |
view.connect("drag-end", self._dndDragEndCb) | |
def __updateViewCb(self, unused_model, unused_path, unused_iter=None): | |
if not len(self.storemodel): | |
self._welcome_infobar.show_all() | |
else: | |
self._welcome_infobar.hide() | |
def _importSourcesCb(self, unused_action): | |
self.show_import_assets_dialog() | |
def _removeAssetsCb(self, unused_action, unused_parameter): | |
"""Removes the selected assets from the project.""" | |
model = self.treeview.get_model() | |
paths = self.getSelectedPaths() | |
if not paths: | |
return | |
# use row references so we don't have to care if a path has been | |
# removed | |
rows = [Gtk.TreeRowReference.new(model, path) | |
for path in paths] | |
with self.app.action_log.started("remove asset from media library"): | |
for row in rows: | |
asset = model[row.get_path()][COL_ASSET] | |
target = asset.get_proxy_target() | |
self._project.remove_asset(asset) | |
self.app.gui.timeline_ui.purgeAsset(asset.props.id) | |
if target: | |
self._project.remove_asset(target) | |
self.app.gui.timeline_ui.purgeAsset(target.props.id) | |
# The treeview can make some of the remaining items selected, so | |
# make sure none are selected. | |
self._unselectAll() | |
def _insertEndCb(self, unused_action, unused_parameter): | |
self.app.gui.timeline_ui.insertAssets(self.getSelectedAssets(), -1) | |
def _searchEntryChangedCb(self, entry): | |
# With many hundred clips in an iconview with dynamic columns and | |
# ellipsizing, doing needless searches is very expensive. | |
# Realistically, nobody expects to search for only one character, | |
# and skipping that makes a huge difference in responsiveness. | |
if len(entry.get_text()) != 1: | |
self.modelFilter.refilter() | |
def _searchEntryIconClickedCb(self, entry, icon_pos, unused_event): | |
if icon_pos == Gtk.EntryIconPosition.SECONDARY: | |
entry.set_text("") | |
elif icon_pos == Gtk.EntryIconPosition.PRIMARY: | |
self._selectUnusedSources() | |
# Focus the container so the user can use Ctrl+Delete, for example. | |
if self.clip_view == SHOW_TREEVIEW: | |
self.treeview.grab_focus() | |
elif self.clip_view == SHOW_ICONVIEW: | |
self.iconview.grab_focus() | |
def _setRowVisible(self, model, iter, data): | |
"""Toggles the visibility of a liststore row.""" | |
text = data.get_text().lower() | |
if not text: | |
# Avoid silly warnings. | |
return True | |
# We must convert to markup form to be able to search for &, ', etc. | |
text = GLib.markup_escape_text(text) | |
return text in model.get_value(iter, COL_INFOTEXT).lower() | |
def _connectToProject(self, project): | |
"""Connects signal handlers to the specified project.""" | |
project.connect("asset-added", self._assetAddedCb) | |
project.connect("asset-loading-progress", self._assetLoadingProgressCb) | |
project.connect("asset-removed", self._assetRemovedCb) | |
project.connect("error-loading-asset", self._errorCreatingAssetCb) | |
project.connect("proxying-error", self._proxyingErrorCb) | |
project.connect("settings-set-from-imported-asset", self.__projectSettingsSetFromImportedAssetCb) | |
def _setClipView(self, view_type): | |
"""Sets which clip view to use when medialibrary is showing clips. | |
Args: | |
view_type (int): One of SHOW_TREEVIEW or SHOW_ICONVIEW. | |
""" | |
self.app.settings.lastClipView = view_type | |
# Gather some info before switching views | |
paths = self.getSelectedPaths() | |
self._viewUnselectAll() | |
# Now that we've got all the info, we can actually change the view type | |
self.clip_view = view_type | |
self._displayClipView() | |
for path in paths: | |
self._viewSelectPath(path) | |
def _displayClipView(self): | |
if self.clip_view == SHOW_TREEVIEW: | |
self.iconview_scrollwin.hide() | |
self.treeview_scrollwin.show_all() | |
elif self.clip_view == SHOW_ICONVIEW: | |
self.treeview_scrollwin.hide() | |
self.iconview_scrollwin.show_all() | |
def __filter_unsupported(self, filter_info): | |
"""Returns whether the specified item should be displayed.""" | |
if filter_info.mime_type not in SUPPORTED_MIMETYPES: | |
return False | |
if ProxyManager.is_proxy_asset(filter_info.uri): | |
return False | |
return True | |
def show_import_assets_dialog(self): | |
"""Pops up the "Import Sources" dialog box.""" | |
dialog = Gtk.FileChooserDialog() | |
dialog.set_title(_("Select One or More Files")) | |
dialog.set_action(Gtk.FileChooserAction.OPEN) | |
dialog.set_icon_name("pitivi") | |
dialog.add_buttons(_("Cancel"), Gtk.ResponseType.CANCEL, | |
_("Add"), Gtk.ResponseType.OK) | |
dialog.props.extra_widget = FileChooserExtraWidget(self.app) | |
dialog.set_default_response(Gtk.ResponseType.OK) | |
dialog.set_select_multiple(True) | |
dialog.set_modal(True) | |
dialog.set_transient_for(self.app.gui) | |
dialog.set_current_folder(self.app.settings.lastImportFolder) | |
dialog.connect('response', self._importDialogBoxResponseCb) | |
previewer = PreviewWidget(self.app.settings) | |
dialog.set_preview_widget(previewer) | |
dialog.set_use_preview_label(False) | |
dialog.connect('update-preview', previewer.update_preview_cb) | |
# Filter for the "known good" formats by default | |
filter = Gtk.FileFilter() | |
filter.set_name(_("Supported file formats")) | |
filter.add_custom(Gtk.FileFilterFlags.URI | | |
Gtk.FileFilterFlags.MIME_TYPE, | |
self.__filter_unsupported) | |
dialog.add_filter(filter) | |
# ...and allow the user to override our whitelists | |
default = Gtk.FileFilter() | |
default.set_name(_("All files")) | |
default.add_pattern("*") | |
dialog.add_filter(default) | |
dialog.show() | |
def _addAsset(self, asset): | |
info = asset.get_info() | |
if self.app.proxy_manager.is_proxy_asset(asset) and \ | |
not asset.props.proxy_target: | |
self.info("%s is a proxy asset but has no target, " | |
"not displaying it.", asset.props.id) | |
return | |
self.debug("Adding asset %s", asset.props.id) | |
thumbs_decorator = AssetThumbnail(asset, self.app.proxy_manager) | |
name = info_name(asset) | |
self.pending_rows.append((thumbs_decorator.small_thumb, | |
thumbs_decorator.large_thumb, | |
beautify_asset(asset), | |
asset, | |
asset.props.id, | |
name, | |
thumbs_decorator)) | |
self._flushPendingRows() | |
def _flushPendingRows(self): | |
self.debug("Flushing %d pending model rows", len(self.pending_rows)) | |
for row in self.pending_rows: | |
self.storemodel.append(row) | |
del self.pending_rows[:] | |
# medialibrary callbacks | |
def _assetLoadingProgressCb(self, project, progress, estimated_time): | |
self._progressbar.set_fraction(progress / 100) | |
for row in self.storemodel: | |
row[COL_INFOTEXT] = beautify_asset(row[COL_ASSET]) | |
if progress == 0: | |
self._startImporting(project) | |
return | |
if project.loaded: | |
proxying_files = [asset | |
for asset in project.loading_assets | |
if not asset.ready] | |
if estimated_time: | |
self.__last_proxying_estimate_time = beautify_ETA(int( | |
estimated_time * Gst.SECOND)) | |
# Translators: this string indicates the estimated time | |
# remaining until an action (such as rendering) completes. | |
# The "%s" is an already-localized human-readable duration, | |
# such as "31 seconds", "1 minute" or "1 hours, 14 minutes". | |
# In some languages, "About %s left" can be expressed roughly as | |
# "There remains approximatively %s" (to handle gender and plurals) | |
template = ngettext("Transcoding %d asset: %d%% (About %s left)", | |
"Transcoding %d assets: %d%% (About %s left)", | |
len(proxying_files)) | |
progress_message = template % ( | |
len(proxying_files), progress, | |
self.__last_proxying_estimate_time) | |
self._progressbar.set_text(progress_message) | |
self._last_imported_uris.update([asset.props.id for asset in | |
project.loading_assets]) | |
if progress == 100: | |
self._doneImporting() | |
def __assetProxyingCb(self, proxy, unused_pspec): | |
self.debug("Proxy is %s", proxy.props.id) | |
self.__removeAsset(proxy) | |
if proxy.get_proxy_target() is not None: | |
# Re add the proxy so its emblem icon is updated. | |
self._addAsset(proxy) | |
def __assetProxiedCb(self, asset, unused_pspec): | |
self.debug("Asset proxied: %s -- %s", asset, asset.props.id) | |
proxy = asset.props.proxy | |
self.__removeAsset(asset) | |
if not proxy: | |
self._addAsset(asset) | |
if self._project.loaded: | |
self.app.gui.timeline_ui.switchProxies(asset) | |
def _assetAddedCb(self, unused_project, asset): | |
"""Checks whether the asset added to the project should be shown.""" | |
if asset in [row[COL_ASSET] for row in self.storemodel]: | |
self.info("Asset %s already in!", asset.props.id) | |
return | |
if isinstance(asset, GES.UriClipAsset) and not asset.error: | |
self.debug("Asset %s added: %s", asset, asset.props.id) | |
asset.connect("notify::proxy", self.__assetProxiedCb) | |
asset.connect("notify::proxy-target", self.__assetProxyingCb) | |
if asset.get_proxy(): | |
self.debug("Not adding asset %s, its proxy is used instead: %s", | |
asset.props.id, | |
asset.get_proxy().props.id) | |
return | |
self._addAsset(asset) | |
def _assetRemovedCb(self, unused_project, asset): | |
if isinstance(asset, GES.UriClipAsset): | |
self.debug("Disconnecting %s - %s", asset, asset.props.id) | |
asset.disconnect_by_func(self.__assetProxiedCb) | |
asset.disconnect_by_func(self.__assetProxyingCb) | |
self.__removeAsset(asset) | |
def __removeAsset(self, asset): | |
"""Removes the specified asset.""" | |
uri = asset.get_id() | |
# Find the corresponding line in the storemodel and remove it. | |
found = False | |
for row in self.storemodel: | |
if uri == row[COL_URI]: | |
self.storemodel.remove(row.iter) | |
found = True | |
break | |
if not found: | |
self.info("Failed to remove %s as it was not found" | |
"in the liststore", uri) | |
def _proxyingErrorCb(self, unused_project, asset): | |
self.__removeAsset(asset) | |
self._addAsset(asset) | |
def _errorCreatingAssetCb(self, unused_project, error, id, type): | |
"""Gathers asset loading errors.""" | |
if GObject.type_is_a(type, GES.UriClip): | |
if self.app.proxy_manager.is_proxy_asset(id): | |
self.debug("Error %s with a proxy" | |
", not showing the error message", error) | |
return | |
error = (id, str(error.domain), error) | |
self._errors.append(error) | |
def _startImporting(self, project): | |
self.__last_proxying_estimate_time = _("Unknown") | |
self.import_start_time = time.time() | |
self._welcome_infobar.hide() | |
self._progressbar.show() | |
def _doneImporting(self): | |
self.debug("Importing took %.3f seconds", | |
time.time() - self.import_start_time) | |
self._flushPendingRows() | |
self._progressbar.hide() | |
if self._errors: | |
errors_amount = len(self._errors) | |
btn_text = ngettext("View error", "View errors", errors_amount) | |
# Translators: {0:d} is just like %d (integer number variable) | |
text = ngettext("An error occurred while importing.", | |
"{0:d} errors occurred while importing.", | |
errors_amount) | |
# Do the {0:d} (aka "%d") substitution using "format" instead of %, | |
# avoiding tracebacks as %d would be missing in the singular form: | |
text = text.format(errors_amount) | |
self._view_error_button.set_label(btn_text) | |
self._warning_label.set_text(text) | |
self._import_warning_infobar.show_all() | |
self._selectLastImportedUris() | |
def __projectSettingsSetFromImportedAssetCb(self, unused_project, asset): | |
asset_path = path_from_uri(asset.get_id()) | |
file_name = os.path.basename(asset_path) | |
message = _("The project settings have been set to match file '%s'") % file_name | |
self._project_settings_label.set_text(message) | |
self._project_settings_infobar.show() | |
def _selectLastImportedUris(self): | |
if not self._last_imported_uris: | |
return | |
self._selectSources(self._last_imported_uris) | |
self._last_imported_uris = set() | |
# Error Dialog Box callbacks | |
def _errorDialogBoxCloseCb(self, dialog): | |
dialog.destroy() | |
def _errorDialogBoxResponseCb(self, dialog, unused_response): | |
dialog.destroy() | |
# Import Sources Dialog Box callbacks | |
def _importDialogBoxResponseCb(self, dialogbox, response): | |
self.debug("response: %r", response) | |
if response == Gtk.ResponseType.OK: | |
lastfolder = dialogbox.get_current_folder() | |
self.app.settings.lastImportFolder = lastfolder | |
dialogbox.props.extra_widget.saveValues() | |
filenames = dialogbox.get_uris() | |
self._project.addUris(filenames) | |
if self.app.settings.closeImportDialog: | |
dialogbox.destroy() | |
else: | |
dialogbox.destroy() | |
def _sourceIsUsed(self, asset): | |
"""Checks whether the specified asset is present in the timeline.""" | |
layers = self._project.ges_timeline.get_layers() | |
for layer in layers: | |
for clip in layer.get_clips(): | |
if clip.get_asset() == asset: | |
return True | |
return False | |
def _selectUnusedSources(self): | |
"""Selects the assets not used by any clip in the project's timeline.""" | |
unused_sources_uris = [] | |
for asset in self._project.list_assets(GES.UriClip): | |
if not self._sourceIsUsed(asset): | |
unused_sources_uris.append(asset.get_id()) | |
self._selectSources(unused_sources_uris) | |
def _selectSources(self, sources_uris): | |
# Hack around the fact that making selections (in a treeview/iconview) | |
# deselects what was previously selected | |
if self.clip_view == SHOW_TREEVIEW: | |
self.treeview.get_selection().select_all() | |
elif self.clip_view == SHOW_ICONVIEW: | |
self.iconview.select_all() | |
model = self.treeview.get_model() | |
selection = self.treeview.get_selection() | |
for row in model: | |
if row[COL_URI] not in sources_uris: | |
if self.clip_view == SHOW_TREEVIEW: | |
selection.unselect_iter(row.iter) | |
elif self.clip_view == SHOW_ICONVIEW: | |
self.iconview.unselect_path(row.path) | |
def _unselectAll(self): | |
if self.clip_view == SHOW_TREEVIEW: | |
self.treeview.get_selection().unselect_all() | |
elif self.clip_view == SHOW_ICONVIEW: | |
self.iconview.unselect_all() | |
# UI callbacks | |
def __projectSettingsSetInfobarCb(self, infobar, response_id): | |
if response_id == Gtk.ResponseType.OK: | |
self.app.gui.showProjectSettingsDialog() | |
infobar.hide() | |
def _clipPropertiesCb(self, unused_widget): | |
"""Shows the clip properties in a dialog. | |
Allows selecting and applying them as the new project settings. | |
""" | |
paths = self.getSelectedPaths() | |
if not paths: | |
self.debug("No item selected") | |
return | |
# Only use the first item. | |
path = paths[0] | |
asset = self.storemodel[path][COL_ASSET] | |
dialog = ClipMediaPropsDialog(self._project, asset) | |
dialog.dialog.set_transient_for(self.app.gui) | |
dialog.run() | |
def __warningInfobarCb(self, infobar, response_id): | |
if response_id == Gtk.ResponseType.OK: | |
self.__show_errors() | |
self._resetErrorList() | |
infobar.hide() | |
def _resetErrorList(self): | |
self._errors = [] | |
self._import_warning_infobar.hide() | |
def __show_errors(self): | |
"""Shows a dialog with the import errors.""" | |
title = ngettext("Error while analyzing a file", | |
"Error while analyzing files", | |
len(self._errors)) | |
headline = ngettext("The following file can not be used with Pitivi.", | |
"The following files can not be used with Pitivi.", | |
len(self._errors)) | |
error_dialogbox = FileListErrorDialog(title, headline) | |
error_dialogbox.connect("close", self._errorDialogBoxCloseCb) | |
error_dialogbox.connect("response", self._errorDialogBoxResponseCb) | |
for uri, reason, extra in self._errors: | |
error_dialogbox.addFailedFile(uri, reason, extra) | |
error_dialogbox.window.set_transient_for(self.app.gui) | |
error_dialogbox.window.show() | |
def _toggleViewTypeCb(self, widget): | |
if widget.get_active(): | |
self._setClipView(SHOW_TREEVIEW) | |
else: | |
self._setClipView(SHOW_ICONVIEW) | |
def __get_path_under_mouse(self, view, event): | |
"""Gets the path of the item under the mouse cursor. | |
Returns: | |
Gtk.TreePath: The item at the current mouse position, if any. | |
""" | |
if isinstance(view, Gtk.TreeView): | |
path = None | |
tup = view.get_path_at_pos(int(event.x), int(event.y)) | |
if tup: | |
path, column, x, y = tup | |
return path | |
elif isinstance(view, Gtk.IconView): | |
return view.get_path_at_pos(int(event.x), int(event.y)) | |
else: | |
raise RuntimeError("Unknown view type: %s" % type(view)) | |
def _rowUnderMouseSelected(self, view, event): | |
path = self.__get_path_under_mouse(view, event) | |
if not path: | |
return False | |
if isinstance(view, Gtk.TreeView): | |
tree_selection = view.get_selection() | |
return tree_selection.path_is_selected(path) | |
elif isinstance(view, Gtk.IconView): | |
return view.path_is_selected(path) | |
else: | |
raise RuntimeError("Unknown view type: %s" % type(view)) | |
def _viewGetFirstSelected(self): | |
paths = self.getSelectedPaths() | |
return paths[0] | |
def _viewHasSelection(self): | |
paths = self.getSelectedPaths() | |
return bool(len(paths)) | |
def _viewGetPathAtPos(self, event): | |
if self.clip_view == SHOW_TREEVIEW: | |
pathinfo = self.treeview.get_path_at_pos( | |
int(event.x), int(event.y)) | |
return pathinfo[0] | |
elif self.clip_view == SHOW_ICONVIEW: | |
return self.iconview.get_path_at_pos(int(event.x), int(event.y)) | |
def _viewSelectPath(self, path): | |
if self.clip_view == SHOW_TREEVIEW: | |
selection = self.treeview.get_selection() | |
selection.select_path(path) | |
elif self.clip_view == SHOW_ICONVIEW: | |
self.iconview.select_path(path) | |
def _viewUnselectAll(self): | |
if self.clip_view == SHOW_TREEVIEW: | |
selection = self.treeview.get_selection() | |
selection.unselect_all() | |
elif self.clip_view == SHOW_ICONVIEW: | |
self.iconview.unselect_all() | |
def __stopUsingProxyCb(self, unused_action, unused_parameter): | |
self._project.disable_proxies_for_assets(self.getSelectedAssets()) | |
def __useProxiesCb(self, unused_action, unused_parameter): | |
self._project.use_proxies_for_assets(self.getSelectedAssets()) | |
def __deleteProxiesCb(self, unused_action, unused_parameter): | |
self._project.disable_proxies_for_assets(self.getSelectedAssets(), | |
delete_proxy_file=True) | |
def __createMenuModel(self): | |
if self.app.proxy_manager.proxyingUnsupported: | |
return None, None | |
assets = self.getSelectedAssets() | |
if not assets: | |
return None, None | |
action_group = Gio.SimpleActionGroup() | |
menu_model = Gio.Menu() | |
proxies = [asset.get_proxy_target() for asset in assets | |
if asset.get_proxy_target()] | |
in_progress = [asset.creation_progress for asset in assets | |
if asset.creation_progress < 100] | |
if proxies or in_progress: | |
action = Gio.SimpleAction.new("unproxy-asset", None) | |
action.connect("activate", self.__stopUsingProxyCb) | |
action_group.insert(action) | |
text = ngettext("Do not use proxy for selected asset", | |
"Do not use proxies for selected assets", | |
len(proxies) + len(in_progress)) | |
menu_model.append(text, "assets.%s" % | |
action.get_name().replace(" ", ".")) | |
action = Gio.SimpleAction.new("delete-proxies", None) | |
action.connect("activate", self.__deleteProxiesCb) | |
action_group.insert(action) | |
text = ngettext("Delete corresponding proxy file", | |
"Delete corresponding proxy files", | |
len(proxies) + len(in_progress)) | |
menu_model.append(text, "assets.%s" % | |
action.get_name().replace(" ", ".")) | |
if len(proxies) != len(assets) and len(in_progress) != len(assets): | |
action = Gio.SimpleAction.new("use-proxies", None) | |
action.connect("activate", self.__useProxiesCb) | |
action_group.insert(action) | |
text = ngettext("Use proxy for selected asset", | |
"Use proxies for selected assets", len(assets)) | |
menu_model.append(text, "assets.%s" % | |
action.get_name().replace(" ", ".")) | |
return menu_model, action_group | |
def __maybeShowPopoverMenu(self, view, event): | |
res, button = event.get_button() | |
if not res or button != 3: | |
return False | |
if not self._rowUnderMouseSelected(view, event): | |
path = self.__get_path_under_mouse(view, event) | |
if path: | |
if isinstance(view, Gtk.IconView): | |
view.unselect_all() | |
view.select_path(path) | |
else: | |
selection = view.get_selection() | |
selection.unselect_all() | |
selection.select_path(path) | |
model, action_group = self.__createMenuModel() | |
if not model: | |
return True | |
popover = Gtk.Popover.new_from_model(view, model) | |
popover.insert_action_group("assets", action_group) | |
popover.props.position = Gtk.PositionType.BOTTOM | |
if self.clip_view == SHOW_TREEVIEW: | |
scrollwindow = self.treeview_scrollwin | |
elif self.clip_view == SHOW_ICONVIEW: | |
scrollwindow = self.iconview_scrollwin | |
rect = Gdk.Rectangle() | |
rect.x = event.x - scrollwindow.props.hadjustment.props.value | |
rect.y = event.y - scrollwindow.props.vadjustment.props.value | |
rect.width = 1 | |
rect.height = 1 | |
popover.set_pointing_to(rect) | |
popover.show_all() | |
return True | |
def _treeViewButtonPressEventCb(self, treeview, event): | |
self._updateDraggedPaths(treeview, event) | |
Gtk.TreeView.do_button_press_event(treeview, event) | |
selection = self.treeview.get_selection() | |
if self._draggedPaths: | |
for path in self._draggedPaths: | |
selection.select_path(path) | |
return True | |
def _updateDraggedPaths(self, view, event): | |
if event.type == getattr(Gdk.EventType, '2BUTTON_PRESS'): | |
# It is possible to double-click outside of clips: | |
if self.getSelectedPaths(): | |
# Here we used to emit "play", but | |
# this is now handled by _itemOrRowActivatedCb instead. | |
pass | |
chain_up = False | |
elif not event.get_state() & (Gdk.ModifierType.CONTROL_MASK | Gdk.ModifierType.SHIFT_MASK): | |
chain_up = not self._rowUnderMouseSelected(view, event) | |
else: | |
chain_up = True | |
if not chain_up: | |
self._draggedPaths = self.getSelectedPaths() | |
else: | |
self._draggedPaths = None | |
def _treeViewButtonReleaseEventCb(self, treeview, event): | |
self._draggedPaths = None | |
selection = self.treeview.get_selection() | |
state = event.get_state() & ( | |
Gdk.ModifierType.CONTROL_MASK | Gdk.ModifierType.SHIFT_MASK) | |
path = self.treeview.get_path_at_pos(event.x, event.y) | |
if self.__maybeShowPopoverMenu(treeview, event): | |
self.debug("Returning after showing popup menu") | |
return | |
if not state and not self.dragged: | |
selection.unselect_all() | |
if path: | |
selection.select_path(path[0]) | |
def _viewSelectionChangedCb(self, unused): | |
self._updateActions() | |
def _updateActions(self): | |
selected_count = len(self.getSelectedPaths()) | |
self.remove_assets_action.set_enabled(selected_count) | |
self.insert_at_end_action.set_enabled(selected_count) | |
# Some actions can only be done on a single item at a time: | |
self._clipprops_button.set_sensitive(selected_count == 1) | |
def _itemOrRowActivatedCb(self, unused_view, path, *unused_args): | |
"""Plays the asset identified by the specified path. | |
This can happen when an item is double-clicked, or | |
Space, Shift+Space, Return or Enter is pressed. | |
This method is the same for both iconview and treeview. | |
""" | |
asset = self.modelFilter[path][COL_ASSET] | |
self.emit('play', asset) | |
def _iconViewButtonPressEventCb(self, iconview, event): | |
self._updateDraggedPaths(iconview, event) | |
Gtk.IconView.do_button_press_event(iconview, event) | |
if self._draggedPaths: | |
for path in self._draggedPaths: | |
self.iconview.select_path(path) | |
self.iconview_cursor_pos = self.iconview.get_path_at_pos( | |
event.x, event.y) | |
return True | |
def _iconViewButtonReleaseEventCb(self, iconview, event): | |
self._draggedPaths = None | |
control_mask = event.get_state() & Gdk.ModifierType.CONTROL_MASK | |
shift_mask = event.get_state() & Gdk.ModifierType.SHIFT_MASK | |
modifier_active = control_mask or shift_mask | |
if self.__maybeShowPopoverMenu(iconview, event): | |
self.debug("Returning after showing popup menu") | |
return | |
if not modifier_active and self.iconview_cursor_pos: | |
current_cursor_pos = self.iconview.get_path_at_pos( | |
event.x, event.y) | |
if current_cursor_pos == self.iconview_cursor_pos: | |
if iconview.path_is_selected(current_cursor_pos): | |
iconview.unselect_all() | |
iconview.select_path(current_cursor_pos) | |
def __disconnectFromProject(self): | |
self._project.disconnect_by_func(self._assetAddedCb) | |
self._project.disconnect_by_func(self._assetLoadingProgressCb) | |
self._project.disconnect_by_func(self._assetRemovedCb) | |
self._project.disconnect_by_func(self._proxyingErrorCb) | |
self._project.disconnect_by_func(self._errorCreatingAssetCb) | |
self._project.disconnect_by_func(self.__projectSettingsSetFromImportedAssetCb) | |
def _new_project_loading_cb(self, unused_project_manager, project): | |
assert not self._project | |
self._project = project | |
self._resetErrorList() | |
self.storemodel.clear() | |
self._welcome_infobar.show_all() | |
self._connectToProject(project) | |
def _newProjectLoadedCb(self, unused_project_manager, project): | |
# Make sure that the sources added to the project are added | |
self._flushPendingRows() | |
def _newProjectFailedCb(self, unused_project_manager, unused_uri, unused_reason): | |
self.storemodel.clear() | |
self._project = None | |
def _projectClosedCb(self, unused_project_manager, unused_project): | |
self.__disconnectFromProject() | |
self._project_settings_infobar.hide() | |
self.storemodel.clear() | |
self._project = None | |
def __paths_walked_cb(self, uris): | |
"""Handles the end of the path walking when importing files and dirs.""" | |
if not uris: | |
return | |
if not self._project: | |
self.warning("Cannot add URIs, project missing") | |
self._last_imported_uris = set(uris) | |
assets = self._project.assetsForUris(uris) | |
if assets: | |
# All the files have already been added. | |
self._selectLastImportedUris() | |
else: | |
self._project.addUris(uris) | |
def _drag_data_received_cb(self, unused_widget, unused_context, unused_x, | |
unused_y, selection, targettype, unused_time): | |
"""Handles data being dragged onto self.""" | |
self.debug("targettype: %d, selection.data: %r", | |
targettype, selection.get_data()) | |
uris = selection.get_uris() | |
# Scan in the background what was dragged and | |
# import whatever can be imported. | |
self.app.threads.addThread(PathWalker, uris, self.__paths_walked_cb) | |
# Used with TreeView and IconView | |
def _dndDragDataGetCb(self, unused_view, unused_context, data, unused_info, unused_timestamp): | |
paths = self.getSelectedPaths() | |
uris = [self.modelFilter[path][COL_URI] for path in paths] | |
data.set_uris(uris) | |
def _dndDragBeginCb(self, unused_view, context): | |
self.info("Drag operation begun") | |
self.dragged = True | |
paths = self.getSelectedPaths() | |
if not paths: | |
context.drag_abort(int(time.time())) | |
else: | |
row = self.modelFilter[paths[0]] | |
Gtk.drag_set_icon_pixbuf(context, row[COL_ICON_64], 0, 0) | |
def _dndDragEndCb(self, unused_view, unused_context): | |
self.info("Drag operation ended") | |
self.dragged = False | |
def getSelectedPaths(self): | |
"""Gets which treeview or iconview items are selected. | |
Returns: | |
List[Gtk.TreePath]: The paths identifying the items. | |
""" | |
if self.clip_view == SHOW_TREEVIEW: | |
return self._getSelectedPathsTreeView() | |
elif self.clip_view == SHOW_ICONVIEW: | |
return self._getSelectedPathsIconView() | |
def _getSelectedPathsTreeView(self): | |
model, rows = self.treeview.get_selection().get_selected_rows() | |
return rows | |
def _getSelectedPathsIconView(self): | |
paths = self.iconview.get_selected_items() | |
paths.reverse() | |
return paths | |
def getSelectedItems(self): | |
"""Gets the URIs of the selected items.""" | |
if self._draggedPaths: | |
return [self.modelFilter[path][COL_URI] | |
for path in self._draggedPaths] | |
return [self.modelFilter[path][COL_URI] | |
for path in self.getSelectedPaths()] | |
def getSelectedAssets(self): | |
"""Gets the selected assets.""" | |
if self._draggedPaths: | |
return [self.modelFilter[path][COL_ASSET] | |
for path in self._draggedPaths] | |
return [self.modelFilter[path][COL_ASSET] | |
for path in self.getSelectedPaths()] | |
def activateCompactMode(self): | |
self._import_button.set_is_important(False) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment