transitions.py 14.7 KB
Newer Older
1
# -*- coding: utf-8 -*-
2
# Pitivi video editor
3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
# Copyright (c) 2012, Jean-François Fortin Tam <nekohayo@gmail.com>
#
# 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
20
from gettext import gettext as _
21

22
from gi.repository import GdkPixbuf
23
from gi.repository import GES
24
from gi.repository import GLib
25
from gi.repository import Gtk
26 27 28

from pitivi.configure import get_pixmap_dir
from pitivi.utils.loggable import Loggable
29
from pitivi.utils.misc import disconnectAllByFunc
30
from pitivi.utils.ui import fix_infobar
31 32
from pitivi.utils.ui import PADDING
from pitivi.utils.ui import SPACING
33

34

35
(COL_TRANSITION_ASSET,
36 37
 COL_NAME_TEXT,
 COL_DESC_TEXT,
38
 COL_ICON) = list(range(4))
39

40 41
BORDER_LOOP_THRESHOLD = 50000

42

43
class TransitionsListWidget(Gtk.Box, Loggable):
44
    """Widget for configuring the selected transition.
45

46 47 48
    Attributes:
        app (Pitivi): The app.
        element (GES.VideoTransition): The transition being configured.
49
    """
50

51
    def __init__(self, app):
52
        Gtk.Box.__init__(self)
53 54
        Loggable.__init__(self)

55
        self.app = app
56 57
        self.element = None
        self._pixdir = os.path.join(get_pixmap_dir(), "transitions")
58
        icon_theme = Gtk.IconTheme.get_default()
59
        self._question_icon = icon_theme.load_icon("dialog-question", 48, 0)
60
        self.set_orientation(Gtk.Orientation.VERTICAL)
61 62
        # Whether a child widget has the focus.
        self.container_focused = False
63

64
        # Tooltip handling
65 66 67
        self._current_transition_name = None
        self._current_tooltip_icon = None

68
        # Searchbox
69 70
        self.searchbar = Gtk.Box()
        self.searchbar.set_orientation(Gtk.Orientation.HORIZONTAL)
71 72
        # Prevents being flush against the notebook
        self.searchbar.set_border_width(3)
73
        self.searchEntry = Gtk.Entry()
74 75
        self.searchEntry.set_icon_from_icon_name(
            Gtk.EntryIconPosition.SECONDARY, "edit-clear-symbolic")
76
        self.searchEntry.set_placeholder_text(_("Search..."))
77
        self.searchbar.pack_end(self.searchEntry, True, True, 0)
78

79 80 81
        self.props_widgets = Gtk.Grid()
        self.props_widgets.props.margin = PADDING
        self.props_widgets.props.column_spacing = SPACING
82

83 84
        self.border_mode_normal = Gtk.RadioButton(
            group=None, label=_("Normal"))
85 86 87
        self.border_mode_normal.set_active(True)
        self.props_widgets.attach(self.border_mode_normal, 0, 0, 1, 1)

88 89
        self.border_mode_loop = Gtk.RadioButton(
            group=self.border_mode_normal, label=_("Loop"))
90 91
        self.props_widgets.attach(self.border_mode_loop, 0, 1, 1, 1)

92 93 94
        self.border_scale = Gtk.Scale.new(Gtk.Orientation.HORIZONTAL, None)
        self.border_scale.set_draw_value(False)
        self.props_widgets.attach(self.border_scale, 1, 0, 1, 2)
95

96
        self.invert_checkbox = Gtk.CheckButton(label=_("Reverse direction"))
97 98
        self.invert_checkbox.props.margin_top = SPACING
        self.props_widgets.attach(self.invert_checkbox, 1, 2, 1, 1)
99 100

        # Set the default values
101
        self.__updateBorderScale()
102

103
        self.infobar = Gtk.InfoBar()
104
        fix_infobar(self.infobar)
105
        self.infobar.props.message_type = Gtk.MessageType.OTHER
106
        txtlabel = Gtk.Label()
107 108 109 110 111
        txtlabel.set_line_wrap(True)
        txtlabel.set_text(
            _("Create a transition by overlapping two adjacent clips on the "
                "same layer. Click the transition on the timeline to change "
                "the transition type."))
112
        self.infobar.get_content_area().add(txtlabel)
113

114
        self.storemodel = Gtk.ListStore(GES.Asset, str, str, GdkPixbuf.Pixbuf)
115

116 117
        self.iconview_scrollwin = Gtk.ScrolledWindow()
        self.iconview_scrollwin.set_shadow_type(Gtk.ShadowType.ETCHED_IN)
118 119 120
        # FIXME: the "never" horizontal scroll policy in GTK2 messes up iconview
        # Re-enable this when we switch to GTK3
        # See also http://python.6.n6.nabble.com/Cannot-shrink-width-of-scrolled-textview-tp1945060.html
121
        # self.iconview_scrollwin.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC)
122

123
        self.iconview = Gtk.IconView(model=self.storemodel)
124 125
        self.iconview.set_pixbuf_column(COL_ICON)
        # We don't show text because we have a searchbar and the names are ugly
126
        # self.iconview.set_text_column(COL_NAME_TEXT)
127 128 129 130 131 132 133 134 135
        self.iconview.set_item_width(48 + 10)
        self.iconview_scrollwin.add(self.iconview)
        self.iconview.set_property("has_tooltip", True)

        self.searchEntry.connect("changed", self._searchEntryChangedCb)
        self.searchEntry.connect("icon-press", self._searchEntryIconClickedCb)
        self.iconview.connect("query-tooltip", self._queryTooltipCb)

        # Speed-up startup by only checking available transitions on idle
136
        GLib.idle_add(self._loadAvailableTransitionsCb)
137

138 139
        self.pack_start(self.infobar, False, False, 0)
        self.pack_start(self.searchbar, False, False, 0)
140
        self.pack_start(self.iconview_scrollwin, True, True, 0)
141
        self.pack_start(self.props_widgets, False, False, 0)
142 143 144 145 146

        # Create the filterModel for searching
        self.modelFilter = self.storemodel.filter_new()
        self.iconview.set_model(self.modelFilter)

147
        self.infobar.show_all()
148
        self.iconview_scrollwin.show_all()
149
        self.iconview.hide()
150
        self.props_widgets.set_sensitive(False)
151 152
        self.props_widgets.hide()
        self.searchbar.hide()
153

154 155 156 157 158 159 160 161 162 163
    def do_set_focus_child(self, child):
        Gtk.Box.do_set_focus_child(self, child)
        action_log = self.app.action_log
        if not action_log:
            # This happens when the user is editing a transition and
            # suddenly closes the window. Don't bother.
            return
        if child:
            if not self.container_focused:
                self.container_focused = True
164
                action_log.begin("Change transaction", toplevel=True)
165 166 167 168 169
        else:
            if self.container_focused:
                self.container_focused = False
                action_log.commit("Change transaction")

170 171
    def __connectUi(self):
        self.iconview.connect("selection-changed", self._transitionSelectedCb)
172
        self.border_scale.connect("value-changed", self._borderScaleCb)
173 174 175
        self.invert_checkbox.connect("toggled", self._invertCheckboxCb)
        self.border_mode_normal.connect("released", self._borderTypeChangedCb)
        self.border_mode_loop.connect("released", self._borderTypeChangedCb)
176 177 178 179 180 181
        self.element.connect("notify::border", self.__updated_cb)
        self.element.connect("notify::invert", self.__updated_cb)
        self.element.connect("notify::transition-type", self.__updated_cb)

    def __updated_cb(self, element, unused_param):
        self._update_ui()
182 183 184

    def __disconnectUi(self):
        self.iconview.disconnect_by_func(self._transitionSelectedCb)
185
        self.border_scale.disconnect_by_func(self._borderScaleCb)
186 187 188
        self.invert_checkbox.disconnect_by_func(self._invertCheckboxCb)
        self.border_mode_normal.disconnect_by_func(self._borderTypeChangedCb)
        self.border_mode_loop.disconnect_by_func(self._borderTypeChangedCb)
189
        disconnectAllByFunc(self.element, self.__updated_cb)
190

191 192
# UI callbacks

193
    def _transitionSelectedCb(self, unused_widget):
194 195 196
        transition_asset = self.getSelectedItem()
        if not transition_asset:
            # The user clicked between icons
197
            return False
198

199 200
        self.debug(
            "New transition type selected: %s", transition_asset.get_id())
201
        if transition_asset.get_id() == "crossfade":
202 203 204 205
            self.props_widgets.set_sensitive(False)
        else:
            self.props_widgets.set_sensitive(True)

206
        self.element.get_parent().set_asset(transition_asset)
207 208 209
        self.app.write_action("element-set-asset",
            asset_id=transition_asset.get_id(),
            element_name=self.element.get_name())
210
        self.app.project_manager.current_project.pipeline.flushSeek()
211

212 213
        return True

214 215
    def _borderScaleCb(self, widget):
        value = widget.get_value()
216
        self.debug("User changed the border property to %s", value)
217
        self.element.set_border(int(value))
218
        self.app.project_manager.current_project.pipeline.flushSeek()
219 220 221

    def _invertCheckboxCb(self, widget):
        value = widget.get_active()
222
        self.debug("User changed the invert property to %s", value)
223
        self.element.set_inverted(value)
224
        self.app.project_manager.current_project.pipeline.flushSeek()
225

226 227 228
    def _borderTypeChangedCb(self, widget):
        self.__updateBorderScale(widget == self.border_mode_loop)

229
    def __updateBorderScale(self, loop=False, border=None):
230 231
        # The "border" property in gstreamer is unlimited, but if you go over
        # 25 thousand it "loops" the transition instead of smoothing it.
232
        if border is not None:
233
            loop = border >= BORDER_LOOP_THRESHOLD
234
        if loop:
235 236 237
            self.border_scale.set_range(50000, 500000)
            self.border_scale.clear_marks()
            self.border_scale.add_mark(
238
                50000, Gtk.PositionType.BOTTOM, _("Slow"))
239
            self.border_scale.add_mark(
240
                200000, Gtk.PositionType.BOTTOM, _("Fast"))
241
            self.border_scale.add_mark(
242
                500000, Gtk.PositionType.BOTTOM, _("Epileptic"))
243
        else:
244 245 246 247
            self.border_scale.set_range(0, 25000)
            self.border_scale.clear_marks()
            self.border_scale.add_mark(0, Gtk.PositionType.BOTTOM, _("Sharp"))
            self.border_scale.add_mark(
248
                25000, Gtk.PositionType.BOTTOM, _("Smooth"))
249

250
    def _searchEntryChangedCb(self, unused_entry):
251 252
        self.modelFilter.refilter()

253
    def _searchEntryIconClickedCb(self, entry, unused, unused_1):
254 255 256 257 258
        entry.set_text("")

# UI methods

    def _loadAvailableTransitionsCb(self):
259
        """Loads the transitions types and icons into the storemodel."""
260
        for trans_asset in GES.list_assets(GES.BaseTransitionClip):
261 262 263
            trans_asset.icon = self._getIcon(trans_asset.get_id())
            self.storemodel.append([trans_asset,
                                    str(trans_asset.get_id()),
264 265
                                    str(trans_asset.get_meta(
                                        GES.META_DESCRIPTION)),
266
                                    trans_asset.icon])
267 268 269 270

        # Now that the UI is fully ready, enable searching
        self.modelFilter.set_visible_func(self._setRowVisible, data=None)
        # Alphabetical/name sorting instead of based on the ID number
271 272
        self.storemodel.set_sort_column_id(
            COL_NAME_TEXT, Gtk.SortType.ASCENDING)
273 274

    def activate(self, element):
275
        """Hides the infobar and shows the transitions UI."""
276 277
        if isinstance(element, GES.AudioTransition):
            return
278
        self.element = element
279
        self._update_ui()
280 281
        self.iconview.show_all()
        self.props_widgets.show_all()
282
        self.searchbar.show_all()
283
        self.__connectUi()
284 285 286 287
        # We REALLY want the infobar to be hidden as space is really constrained
        # and yet GTK 3.10 seems to be racy in showing/hiding infobars, so
        # this must happen *after* the tab has been made visible/switched to:
        self.infobar.hide()
288

289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304
    def _update_ui(self):
        transition_type = self.element.get_transition_type()
        self.props_widgets.set_sensitive(
            transition_type != GES.VideoStandardTransitionType.CROSSFADE)
        self.__select_transition(transition_type)
        border = self.element.get_border()
        self.__updateBorderScale(border=border)
        self.border_scale.set_value(border)
        self.invert_checkbox.set_active(self.element.is_inverted())
        loop = border >= BORDER_LOOP_THRESHOLD
        if loop:
            self.border_mode_loop.activate()
        else:
            self.border_mode_normal.activate()

    def __select_transition(self, transition_type):
305
        """Selects the specified transition type in the iconview."""
306 307
        model = self.iconview.get_model()
        for row in model:
308 309
            asset = row[COL_TRANSITION_ASSET]
            if transition_type.value_nick == asset.get_id():
310 311 312 313 314
                path = model.get_path(row.iter)
                self.iconview.select_path(path)
                self.iconview.scroll_to_path(path, False, 0, 0)

    def deactivate(self):
315
        """Shows the infobar and hides the transitions UI."""
316
        self.__disconnectUi()
317
        self.iconview.unselect_all()
318 319 320
        self.iconview.hide()
        self.props_widgets.hide()
        self.searchbar.hide()
321 322 323
        self.infobar.show()

    def _getIcon(self, transition_nick):
324
        """Gets an icon pixbuf for the specified transition nickname."""
325 326
        name = transition_nick + ".png"
        try:
327 328
            icon = GdkPixbuf.Pixbuf.new_from_file(
                os.path.join(self._pixdir, name))
329 330 331 332 333
        except:
            icon = self._question_icon
        return icon

    def _queryTooltipCb(self, view, x, y, keyboard_mode, tooltip):
334 335
        is_row, x, y, model, path, iter_ = view.get_tooltip_context(
            x, y, keyboard_mode)
336
        if not is_row:
337 338
            return False

339
        view.set_tooltip_item(tooltip, path)
340

341
        name = model.get_value(iter_, COL_TRANSITION_ASSET).get_id()
342 343
        if self._current_transition_name != name:
            self._current_transition_name = name
344
            icon = model.get_value(iter_, COL_ICON)
345 346
            self._current_tooltip_icon = icon

347 348
        longname = model.get_value(iter_, COL_NAME_TEXT).strip()
        description = model.get_value(iter_, COL_DESC_TEXT)
349 350
        txt = "<b>%s:</b>\n%s" % (GLib.markup_escape_text(longname),
                                  GLib.markup_escape_text(description),)
351 352 353 354 355
        tooltip.set_markup(txt)
        return True

    def getSelectedItem(self):
        path = self.iconview.get_selected_items()
356 357
        if path == []:
            return None
358
        return self.modelFilter[path[0]][COL_TRANSITION_ASSET]
359

360
    def _setRowVisible(self, model, iter, unused_data):
361
        """Filters the icon view to show only the search results."""
362 363
        text = self.searchEntry.get_text().lower()
        return text in model.get_value(iter, COL_DESC_TEXT).lower() or\
364
            text in model.get_value(iter, COL_NAME_TEXT).lower()