1# This file is part of MyPaint.
2# -*- coding: utf-8 -*-
3# Copyright (C) 2014-2019 by the MyPaint Development Team
4# Copyright (C) 2009 by Ilya Portnov <portnov@bk.ru>
5#
6# This program is free software; you can redistribute it and/or modify
7# it under the terms of the GNU General Public License as published by
8# the Free Software Foundation; either version 2 of the License, or
9# (at your option) any later version.
10
11"""Layers panel"""
12
13
14## Imports
15
16from __future__ import division, print_function
17
18from gettext import gettext as _
19import os.path
20from logging import getLogger
21
22from lib.gibindings import Gtk
23from lib.gibindings import GObject
24
25import lib.layer
26import lib.xml
27from . import widgets
28from .widgets import inline_toolbar
29from .toolstack import SizedVBoxToolWidget
30from . import layers
31from lib.modes import STACK_MODES
32from lib.modes import STANDARD_MODES
33from lib.modes import MODE_STRINGS
34from lib.modes import PASS_THROUGH_MODE
35import lib.modes
36import gui.layervis
37
38logger = getLogger(__name__)
39
40## Module constants
41
42#: UI XML for the current layer's class (framework: ``layerswindow.xml``)
43LAYER_CLASS_UI = [
44    (lib.layer.SurfaceBackedLayer, """
45        <popup name='LayersWindowPopup'>
46            <placeholder name="BasicLayerActions">
47                <menuitem action='CopyLayer'/>
48            </placeholder>
49        </popup>
50        """),
51    (lib.layer.PaintingLayer, """
52        <popup name='LayersWindowPopup'>
53            <placeholder name="BasicLayerActions">
54                <menuitem action='PasteLayer'/>
55                <menuitem action='ClearLayer'/>
56            </placeholder>
57            <placeholder name='AdvancedLayerActions'>
58                <menuitem action='TrimLayer'/>
59                <separator/>
60                <menuitem action='UniqLayerPixels'/>
61                <menuitem action='UniqLayerTiles'/>
62            </placeholder>
63        </popup>
64        """),
65    (lib.layer.ExternallyEditable, """
66        <popup name='LayersWindowPopup'>
67            <placeholder name='BasicLayerActions'>
68                <separator/>
69                <menuitem action='BeginExternalLayerEdit'/>
70                <menuitem action='CommitExternalLayerEdit'/>
71                <separator/>
72            </placeholder>
73        </popup>
74        """),
75    (lib.layer.LayerStack, """
76        <popup name='LayersWindowPopup'>
77            <placeholder name='AdvancedLayerActions'>
78                <menuitem action='RefactorLayerGroupPixels'/>
79                <menuitem action='RefactorLayerGroupTiles'/>
80            </placeholder>
81        </popup>
82        """),
83]
84
85
86## Class definitions
87
88
89class LayersTool (SizedVBoxToolWidget):
90    """Panel for arranging layers within a tree structure"""
91
92    ## Class properties
93
94    tool_widget_icon_name = "mypaint-layers-symbolic"
95    tool_widget_title = _("Layers")
96    tool_widget_description = _("Arrange layers and assign effects")
97
98    LAYER_MODE_TOOLTIP_MARKUP_TEMPLATE = "<b>{name}</b>\n{description}"
99
100    # TRANSLATORS: tooltip for the opacity slider (text)
101    # TRANSLATORS: note that "%%" turns into "%"
102    OPACITY_SCALE_TOOLTIP_TEXT_TEMPLATE = _("Layer opacity: %d%%")
103
104    # TRANSLATORS: label for the opacity slider (text)
105    # TRANSLATORS: note that "%%" turns into "%"
106    # TRANSLATORS: most of the time this can just be copied, or left alone
107    OPACITY_LABEL_TEXT_TEMPLATE = _(u"%d%%")
108
109    __gtype_name__ = 'MyPaintLayersTool'
110
111    STATUSBAR_CONTEXT = 'layerstool-dnd'
112
113    # TRANSLATORS: status bar messages for drag, without/with modifiers
114    STATUSBAR_DRAG_MSG = _(u"Move layer in stack…")
115    STATUSBAR_DRAG_INTO_MSG = _("Move layer in stack (dropping into a "
116                                "regular layer will create a new group)")
117
118    ## Construction
119
120    def __init__(self):
121        GObject.GObject.__init__(self)
122        from gui.application import get_app
123        app = get_app()
124        self.app = app
125        self.set_spacing(widgets.SPACING_CRAMPED)
126        self.set_border_width(widgets.SPACING_TIGHT)
127        # GtkTreeView init
128        docmodel = app.doc.model
129        view = layers.RootStackTreeView(docmodel)
130        self._treemodel = view.get_model()
131        self._treeview = view
132        # RootStackTreeView events
133        view.current_layer_rename_requested += self._layer_properties_cb
134        view.current_layer_changed += self._blink_current_layer_cb
135        view.current_layer_menu_requested += self._popup_menu_cb
136        # Drag and drop
137        view.drag_began += self._view_drag_began_cb
138        view.drag_ended += self._view_drag_ended_cb
139        statusbar_cid = app.statusbar.get_context_id(self.STATUSBAR_CONTEXT)
140        self._drag_statusbar_context_id = statusbar_cid
141        # View scrolls
142        view_scroll = Gtk.ScrolledWindow()
143        view_scroll.set_shadow_type(Gtk.ShadowType.ETCHED_IN)
144        vscroll_pol = Gtk.PolicyType.ALWAYS
145        hscroll_pol = Gtk.PolicyType.AUTOMATIC
146        view_scroll.set_policy(hscroll_pol, vscroll_pol)
147        view_scroll.add(view)
148        view_scroll.set_size_request(-1, 200)
149        view_scroll.set_hexpand(True)
150        view_scroll.set_vexpand(True)
151        # Context menu
152        ui_dir = os.path.dirname(os.path.abspath(__file__))
153        ui_path = os.path.join(ui_dir, "layerswindow.xml")
154        self.app.ui_manager.add_ui_from_file(ui_path)
155        menu = self.app.ui_manager.get_widget("/LayersWindowPopup")
156        menu.set_title(_("Layer"))
157        self.connect("popup-menu", self._popup_menu_cb)
158        menu.attach_to_widget(self, None)
159        self._menu = menu
160        self._layer_specific_ui_mergeids = []
161        self._layer_specific_ui_class = None
162
163        # Main layout grid
164        grid = Gtk.Grid()
165        grid.set_row_spacing(widgets.SPACING_TIGHT)
166        grid.set_column_spacing(widgets.SPACING)
167        row = -1
168
169        # Visibility set management
170        row += 1
171        layer_view_ui = gui.layervis.LayerViewUI(docmodel)
172        grid.attach(layer_view_ui.widget, 0, row, 6, 1)
173        self._layer_view_ui = layer_view_ui
174
175        # Mode dropdown
176        row += 1
177        # ComboBox w/ list model (mode_num, label, sensitive, scale)
178        modes = list(STACK_MODES + STANDARD_MODES)
179        modes.remove(lib.mypaintlib.CombineSpectralWGM)
180        modes.insert(0, lib.mypaintlib.CombineSpectralWGM)
181        combo = layers.new_blend_mode_combo(modes, MODE_STRINGS)
182        self._layer_mode_combo = combo
183        grid.attach(combo, 0, row, 5, 1)
184
185        # Opacity widgets
186        adj = Gtk.Adjustment(lower=0, upper=100,
187                             step_increment=1, page_increment=10)
188        sbut = Gtk.ScaleButton()
189        sbut.set_adjustment(adj)
190        sbut.remove(sbut.get_child())
191        sbut.set_hexpand(False)
192        sbut.set_vexpand(False)
193        label_text_widest = self.OPACITY_LABEL_TEXT_TEMPLATE % (100,)
194        label = Gtk.Label(label_text_widest)
195        label.set_width_chars(len(label_text_widest))
196        # prog = Gtk.ProgressBar()
197        # prog.set_show_text(False)
198        sbut.add(label)
199        self._opacity_scale_button = sbut
200        # self._opacity_progress = prog
201        self._opacity_label = label
202        self._opacity_adj = adj
203        grid.attach(sbut, 5, row, 1, 1)
204
205        # Layer list and controls
206        row += 1
207        layersbox = Gtk.VBox()
208        style = layersbox.get_style_context()
209        style.add_class(Gtk.STYLE_CLASS_LINKED)
210        style = view_scroll.get_style_context()
211        style.set_junction_sides(Gtk.JunctionSides.BOTTOM)
212        list_tools = inline_toolbar(
213            self.app,
214            [
215                ("NewLayerGroupAbove", "mypaint-layer-group-new-symbolic"),
216                ("NewPaintingLayerAbove", "mypaint-add-symbolic"),
217                ("RemoveLayer", "mypaint-remove-symbolic"),
218                ("RaiseLayerInStack", "mypaint-up-symbolic"),
219                ("LowerLayerInStack", "mypaint-down-symbolic"),
220                ("DuplicateLayer", None),
221                ("MergeLayerDown", None),
222            ]
223        )
224        style = list_tools.get_style_context()
225        style.set_junction_sides(Gtk.JunctionSides.TOP)
226        layersbox.pack_start(view_scroll, True, True, 0)
227        layersbox.pack_start(list_tools, False, False, 0)
228        layersbox.set_hexpand(True)
229        layersbox.set_vexpand(True)
230        grid.attach(layersbox, 0, row, 6, 1)
231
232        # Background layer controls
233        row += 1
234        show_bg_btn = Gtk.CheckButton()
235        change_bg_act = self.app.find_action("BackgroundWindow")
236        change_bg_btn = widgets.borderless_button(action=change_bg_act)
237        show_bg_act = self.app.find_action("ShowBackgroundToggle")
238        show_bg_btn.set_related_action(show_bg_act)
239        grid.attach(show_bg_btn, 0, row, 5, 1)
240        grid.attach(change_bg_btn, 5, row, 1, 1)
241
242        # Pack
243        self.pack_start(grid, False, True, 0)
244        # Updates from the real layers tree (TODO: move to lib/layers.py)
245        self._processing_model_updates = False
246        self._opacity_adj.connect('value-changed',
247                                  self._opacity_adj_changed_cb)
248        self._layer_mode_combo.connect('changed',
249                                       self._layer_mode_combo_changed_cb)
250        rootstack = docmodel.layer_stack
251        rootstack.layer_properties_changed += self._layer_propchange_cb
252        rootstack.current_path_updated += self._current_path_updated_cb
253        # Initial update
254        self.connect("show", self._show_cb)
255
256    def _show_cb(self, event):
257        self._processing_model_updates = True
258        self._update_all()
259        self._processing_model_updates = False
260
261    ## Updates from the model
262
263    def _current_path_updated_cb(self, rootstack, layerpath):
264        """Respond to the current layer changing in the doc-model"""
265        self._processing_model_updates = True
266        self._update_all()
267        self._processing_model_updates = False
268
269    def _layer_propchange_cb(self, rootstack, path, layer, changed):
270        if self._processing_model_updates:
271            logger.debug("Property change skipped: already processing "
272                         "an update from the document model")
273        if layer is not rootstack.current:
274            return
275        self._processing_model_updates = True
276        if "mode" in changed:
277            self._update_layer_mode_combo()
278        if "opacity" in changed or "mode" in changed:
279            self._update_opacity_widgets()
280        self._processing_model_updates = False
281
282    ## Model update processing
283
284    def _update_all(self):
285        assert self._processing_model_updates
286        self._update_context_menu()
287        self._update_layer_mode_combo()
288        self._update_opacity_widgets()
289
290    def _update_layer_mode_combo(self):
291        """Updates the layer mode combo's value from the model"""
292        assert self._processing_model_updates
293        combo = self._layer_mode_combo
294        rootstack = self.app.doc.model.layer_stack
295        current = rootstack.current
296        if current is rootstack or not current:
297            combo.set_sensitive(False)
298            return
299        elif not combo.get_sensitive():
300            combo.set_sensitive(True)
301        active_iter = None
302        current_mode = current.mode
303        for row in combo.get_model():
304            mode = row[0]
305            if mode == current_mode:
306                active_iter = row.iter
307            row[2] = (mode in current.PERMITTED_MODES)
308        combo.set_active_iter(active_iter)
309        label, desc = MODE_STRINGS.get(current_mode)
310        template = self.LAYER_MODE_TOOLTIP_MARKUP_TEMPLATE
311        tooltip = template.format(
312            name = lib.xml.escape(label),
313            description = lib.xml.escape(desc),
314        )
315        combo.set_tooltip_markup(tooltip)
316
317    def _update_opacity_widgets(self):
318        """Updates the opacity widgets from the model"""
319        assert self._processing_model_updates
320
321        # The opacity scale is only sensitive
322        # when the opacity can be adjusted.
323        sbut = self._opacity_scale_button
324        rootstack = self.app.doc.model.layer_stack
325        layer = rootstack.current
326        opacity_is_adjustable = not (
327            layer is None
328            or layer is rootstack
329            or layer.mode == PASS_THROUGH_MODE
330        )
331        sbut.set_sensitive(opacity_is_adjustable)
332
333        # Update labels, scales etc.
334        # to show an effective opacity value.
335        if opacity_is_adjustable:
336            opacity = layer.opacity
337        else:
338            opacity = 1.0
339
340        percentage = opacity * 100
341        adj = self._opacity_adj
342        adj.set_value(percentage)
343
344        template = self.OPACITY_SCALE_TOOLTIP_TEXT_TEMPLATE
345        tooltip = template % (percentage,)
346        sbut.set_tooltip_text(tooltip)
347
348        label = self._opacity_label
349        template = self.OPACITY_LABEL_TEXT_TEMPLATE
350        text = template % (percentage,)
351        label.set_text(text)
352
353    def _update_context_menu(self):
354        assert self._processing_model_updates
355        layer = self.app.doc.model.layer_stack.current
356        layer_class = layer.__class__
357        if layer_class is self._layer_specific_ui_class:
358            return
359        ui_manager = self.app.ui_manager
360        for old_mergeid in self._layer_specific_ui_mergeids:
361            ui_manager.remove_ui(old_mergeid)
362        self._layer_specific_ui_mergeids = []
363        new_ui_matches = []
364        for lclass, lui in LAYER_CLASS_UI:
365            if isinstance(layer, lclass):
366                new_ui_matches.append(lui)
367        for new_ui in new_ui_matches:
368            new_mergeid = ui_manager.add_ui_from_string(new_ui)
369            self._layer_specific_ui_mergeids.append(new_mergeid)
370        self._layer_specific_ui_class = layer_class
371
372    ## Updates from the user
373
374    def _layer_properties_cb(self, view):
375        action = self.app.find_action("LayerProperties")
376        action.activate()
377
378    def _blink_current_layer_cb(self, view):
379        self.app.doc.blink_layer()
380
381    def _view_drag_began_cb(self, view):
382        self._treeview_in_drag = True
383        statusbar = self.app.statusbar
384        statusbar_cid = self._drag_statusbar_context_id
385        statusbar.remove_all(statusbar_cid)
386        statusbar.push(statusbar_cid, self.STATUSBAR_DRAG_MSG)
387
388    def _view_drag_ended_cb(self, view):
389        self._treeview_in_drag = False
390        statusbar = self.app.statusbar
391        statusbar_cid = self._drag_statusbar_context_id
392        statusbar.remove_all(statusbar_cid)
393
394    def _opacity_adj_changed_cb(self, *ignore):
395        if self._processing_model_updates:
396            return
397        opacity = self._opacity_adj.get_value() / 100.0
398        docmodel = self.app.doc.model
399        docmodel.set_current_layer_opacity(opacity)
400        self._treeview.scroll_to_current_layer()
401
402    def _layer_mode_combo_changed_cb(self, *ignored):
403        """Propagate the user's choice of layer mode to the model"""
404        if self._processing_model_updates:
405            return
406        docmodel = self.app.doc.model
407        combo = self._layer_mode_combo
408        model = combo.get_model()
409        mode = model.get_value(combo.get_active_iter(), 0)
410        if docmodel.layer_stack.current.mode == mode:
411            return
412        label, desc = MODE_STRINGS.get(mode)
413        docmodel.set_current_layer_mode(mode)
414
415    ## Utility methods
416
417    def _popup_context_menu(self, event=None):
418        """Display the popup context menu"""
419        if event is None:
420            time = Gtk.get_current_event_time()
421            button = 0
422        else:
423            time = event.time
424            button = event.button
425        self._menu.popup(None, None, None, None, button, time)
426
427    def _popup_menu_cb(self, widget, event=None):
428        """Handler for "popup-menu" GtkEvents, and the view's @event"""
429        self._popup_context_menu(event=event)
430        return True
431