1# This file is part of MyPaint.
2# Copyright (C) 2014-2017 by the MyPaint Development Team.
3#
4# This program is free software; you can redistribute it and/or modify
5# it under the terms of the GNU General Public License as published by
6# the Free Software Foundation; either version 2 of the License, or
7# (at your option) any later version.
8
9"""Layer manipulation GUI helper code"""
10
11
12## Imports
13
14from __future__ import division, print_function
15
16import lib.layer
17from lib.xml import escape
18from lib.observable import event
19from lib import helpers
20
21from lib.document import Document
22from lib.gettext import gettext as _
23from lib.gettext import C_
24from gui.layerprops import make_preview
25import gui.drawutils
26from lib.pycompat import unicode
27
28from lib.gibindings import Gtk
29from lib.gibindings import Gdk
30from lib.gibindings import GObject
31from lib.gibindings import GLib
32from lib.gibindings import Pango
33from lib.gibindings import GdkPixbuf
34
35import sys
36import logging
37
38
39## Module vars
40
41logger = logging.getLogger(__name__)
42
43
44## Class defs
45
46
47class RootStackTreeModelWrapper (GObject.GObject, Gtk.TreeModel):
48    """Tree model wrapper presenting a document model's layers stack
49
50    Together with the layers panel (defined in `gui.layerswindow`),
51    and `RootStackTreeView`, this forms part of the presentation logic
52    for the layer stack.
53
54    """
55
56    ## Class vars
57
58    INVALID_STAMP = 0
59    MIN_VALID_STAMP = 1
60    COLUMN_TYPES = (object,)
61    LAYER_COLUMN = 0
62
63    ## Setup
64
65    def __init__(self, docmodel):
66        """Initialize, presenting the root stack of a document model
67
68        :param Document docmodel: model to present
69        """
70        super(RootStackTreeModelWrapper, self).__init__()
71        self._docmodel = docmodel
72        root = docmodel.layer_stack
73        self._root = root
74        self._iter_stamp = 1
75        self._iter_id2path = {}  # {pathid: pathtuple}
76        self._iter_path2id = {}   # {pathtuple: pathid}
77        root.layer_properties_changed += self._layer_props_changed_cb
78        root.layer_inserted += self._layer_inserted_cb
79        root.layer_deleted += self._layer_deleted_cb
80        root.layer_thumbnail_updated += self._layer_thumbnail_updated_cb
81        lvm = docmodel.layer_view_manager
82        lvm.current_view_changed += self._lvm_current_view_changed_cb
83        self._drag = None
84
85    ## Python boilerplate
86
87    def __repr__(self):
88        nrows = len(list(self._root.deepiter()))
89        return "<%s n=%d>" % (self.__class__.__name__, nrows)
90
91    ## Event and update handling
92
93    def _layer_props_changed_cb(self, root, layerpath, layer, changed):
94        """Updates the display after a layer's properties change"""
95        treepath = Gtk.TreePath(layerpath)
96        it = self.get_iter(treepath)
97        self._row_changed_all_descendents(treepath, it)
98
99    def _layer_thumbnail_updated_cb(self, root, layerpath, layer):
100        """Updates the display after a layer's thumbnail changes."""
101        treepath = Gtk.TreePath(layerpath)
102        it = self.get_iter(treepath)
103        self.row_changed(treepath, it)
104
105    def _layer_inserted_cb(self, root, path):
106        """Updates the display after a layer is added"""
107        self.invalidate_iters()
108        it = self.get_iter(path)
109        self.row_inserted(Gtk.TreePath(path), it)
110        parent_path = path[:-1]
111        if not parent_path:
112            return
113        parent = self._root.deepget(parent_path)
114        if len(parent) == 1:
115            parent_it = self.get_iter(parent_path)
116            self.row_has_child_toggled(Gtk.TreePath(parent_path),
117                                       parent_it)
118
119    def _layer_deleted_cb(self, root, path):
120        """Updates the display after a layer is removed"""
121        self.invalidate_iters()
122        self.row_deleted(Gtk.TreePath(path))
123        parent_path = path[:-1]
124        if not parent_path:
125            return
126        parent = self._root.deepget(parent_path)
127        if len(parent) == 0:
128            parent_it = self.get_iter(parent_path)
129            self.row_has_child_toggled(Gtk.TreePath(parent_path),
130                                       parent_it)
131
132    def _row_dragged(self, src_path, dst_path):
133        """Handles the user dragging a row to a new location"""
134        self._docmodel.restack_layer(src_path, dst_path)
135
136    def _row_changed_all_descendents(self, treepath, it):
137        """Like GtkTreeModel.row_changed(), but all descendents too."""
138        self.row_changed(treepath, it)
139        if self.iter_n_children(it) <= 0:
140            return
141        ci = self.iter_nth_child(it, 0)
142        while ci is not None:
143            treepath = self.get_path(ci)
144            self._row_changed_all_descendents(treepath, ci)
145            ci = self.iter_next(ci)
146
147    def _row_changed_all(self):
148        """Like GtkTreeModel.row_changed(), but all rows."""
149        it = self.get_iter_first()
150        while it is not None:
151            treepath = self.get_path(it)
152            self._row_changed_all_descendents(treepath, it)
153            it = self.iter_next(it)
154
155    def _lvm_current_view_changed_cb(self, lvm):
156        """Respond to changes of/on the currently active layer-view.
157
158        For the sake of the related TreeView, announce a change to all
159        rows to make sure any bulk changes to the sensitive state of the
160        visibility column are visible instantly.
161
162        This is slightly incorrect, since it means that the TreeModel
163        needs to know what its TreeView does. Maybe the model
164        implemented here should expose its data in proper columns, with
165        effective-visibility, visibility-sensitive and so on.
166
167        """
168        self._row_changed_all()
169
170    ## Iterator management
171
172    def invalidate_iters(self):
173        """Invalidates all iters produced by this model"""
174        # No need to zap the lookup tables: tree paths have a tightly
175        # controlled vocabulary.
176        if self._iter_stamp == sys.maxsize:
177            self._iter_stamp = self.MIN_VALID_STAMP
178        else:
179            self._iter_stamp += 1
180
181    def iter_is_valid(self, it):
182        """True if an iterator produced by this model is valid"""
183        return it.stamp == self._iter_stamp
184
185    @classmethod
186    def _invalidate_iter(cls, it):
187        """Invalidates an iterator"""
188        it.stamp = cls.INVALID_STAMP
189        it.user_data = None
190
191    def _get_iter_path(self, it):
192        """Gets an iterator's path: None if invalid"""
193        if not self.iter_is_valid(it):
194            return None
195        else:
196            path = self._iter_id2path.get(it.user_data)
197            return tuple(path)
198
199    def _set_iter_path(self, it, path):
200        """Sets an iterator's path, invalidating it if path=None"""
201        if path is None:
202            self._invalidate_iter(it)
203        else:
204            it.stamp = self._iter_stamp
205            pathid = self._iter_path2id.get(path)
206            if pathid is None:
207                path = tuple(path)
208                pathid = id(path)
209                self._iter_path2id[path] = pathid
210                self._iter_id2path[pathid] = path
211            it.user_data = pathid
212
213    def _create_iter(self, path):
214        """Creates an iterator for the given path
215
216        The returned pair can be returned by the ``do_*()`` virtual
217        function implementations. Use this method in preference to the
218        regular `Gtk.TreeIter` constructor.
219        """
220        if not path:
221            return (False, None)
222        else:
223            it = Gtk.TreeIter()
224            self._set_iter_path(it, tuple(path))
225            return (True, it)
226
227    def _iter_bump(self, it, delta):
228        """Move an iter at its current level"""
229        path = self._get_iter_path(it)
230        if path is not None:
231            path = list(path)
232            path[-1] += delta
233            path = tuple(path)
234        if self.get_layer(treepath=path) is None:
235            self._invalidate_iter(it)
236            return False
237        else:
238            self._set_iter_path(it, path)
239            return True
240
241    ## Data lookup
242
243    def get_layer(self, treepath=None, it=None):
244        """Look up a layer using paths or iterators"""
245        if treepath is None:
246            if it is not None:
247                treepath = self._get_iter_path(it)
248        if treepath is None:
249            return None
250        if isinstance(treepath, Gtk.TreePath):
251            treepath = tuple(treepath.get_indices())
252        return self._root.deepget(treepath)
253
254    ## GtkTreeModel vfunc implementation
255
256    def do_get_flags(self):
257        """Fetches GtkTreeModel flags"""
258        return 0
259
260    def do_get_n_columns(self):
261        """Count of GtkTreeModel columns"""
262        return len(self.COLUMN_TYPES)
263
264    def do_get_column_type(self, n):
265        return self.COLUMN_TYPES[n]
266
267    def do_get_iter(self, treepath):
268        """New iterator pointing at a node identified by GtkTreePath"""
269        if not self.get_layer(treepath=treepath):
270            treepath = None
271        return self._create_iter(treepath)
272
273    def do_get_path(self, it):
274        """New GtkTreePath for a treeiter"""
275        path = self._get_iter_path(it)
276        if path is None:
277            return None
278        else:
279            return Gtk.TreePath(path)
280
281    def do_get_value(self, it, column):
282        """Value at a particular row-iterator and column index"""
283        if column != 0:
284            return None
285        return self.get_layer(it=it)
286
287    def do_iter_next(self, it):
288        """Move an iterator to the node after it, returning success"""
289        return self._iter_bump(it, 1)
290
291    def do_iter_previous(self, it):
292        """Move an iterator to the node before it, returning success"""
293        return self._iter_bump(it, -1)
294
295    def do_iter_children(self, parent):
296        """Fetch an iterator pointing at the first child of a parent"""
297        return self.do_iter_nth_child(parent, 0)
298
299    def do_iter_has_child(self, it):
300        """True if an iterator has children"""
301        layer = self.get_layer(it=it)
302        return isinstance(layer, lib.layer.LayerStack) and len(layer) > 0
303
304    def do_iter_n_children(self, it):
305        """Count of the children of a given iterator"""
306        layer = self.get_layer(it=it)
307        if not isinstance(layer, lib.layer.LayerStack):
308            return 0
309        else:
310            return len(layer)
311
312    def do_iter_nth_child(self, it, n):
313        """Fetch a specific child iterator of a parent iter"""
314        if it is None:
315            path = (n,)
316        else:
317            path = self._get_iter_path(it)
318            if path is not None:
319                path = list(self._get_iter_path(it))
320                path.append(n)
321                path = tuple(path)
322        if not self.get_layer(treepath=path):
323            path = None
324        return self._create_iter(path)
325
326    def do_iter_parent(self, it):
327        """Fetches the parent of a valid iterator"""
328        if it is None:
329            parent_path = None
330        else:
331            path = self._get_iter_path(it)
332            if path is None:
333                parent_path = None
334            else:
335                parent_path = list(path)
336                parent_path.pop(-1)
337                parent_path = tuple(parent_path)
338            if parent_path == ():
339                parent_path = None
340        return self._create_iter(parent_path)
341
342
343class RootStackTreeView (Gtk.TreeView):
344    """GtkTreeView tailored for a doc's root layer stack"""
345
346    DRAG_HOVER_EXPAND_TIME = 1.25   # seconds
347
348    def __init__(self, docmodel):
349        super(RootStackTreeView, self).__init__()
350        self._docmodel = docmodel
351
352        treemodel = RootStackTreeModelWrapper(docmodel)
353        self.set_model(treemodel)
354
355        target1 = Gtk.TargetEntry.new(
356            target = "GTK_TREE_MODEL_ROW",
357            flags = Gtk.TargetFlags.SAME_WIDGET,
358            info = 1,
359        )
360        self.drag_source_set(
361            start_button_mask = Gdk.ModifierType.BUTTON1_MASK,
362            targets = [target1],
363            actions = Gdk.DragAction.MOVE,
364        )
365        self.drag_dest_set(
366            flags = Gtk.DestDefaults.MOTION | Gtk.DestDefaults.DROP,
367            targets = [target1],
368            actions = Gdk.DragAction.MOVE,
369        )
370
371        self.connect("button-press-event", self._button_press_cb)
372
373        # Motion and modifier keys during drag
374        self.connect("drag-begin", self._drag_begin_cb)
375        self.connect("drag-motion", self._drag_motion_cb)
376        self.connect("drag-leave", self._drag_leave_cb)
377        self.connect("drag-drop", self._drag_drop_cb)
378        self.connect("drag-end", self._drag_end_cb)
379
380        # Track updates from the model
381        self._processing_model_updates = False
382        root = docmodel.layer_stack
383        root.current_path_updated += self._current_path_updated_cb
384        root.expand_layer += self._expand_layer_cb
385        root.collapse_layer += self._collapse_layer_cb
386        root.layer_content_changed += self._layer_content_changed_cb
387        root.current_layer_solo_changed += lambda *a: self.queue_draw()
388
389        # View behaviour and appearance
390        self.set_headers_visible(False)
391        selection = self.get_selection()
392        selection.set_mode(Gtk.SelectionMode.BROWSE)
393        self.set_size_request(150, 200)
394
395        # Visibility flag column
396        col = Gtk.TreeViewColumn(_("Visible"))
397        col.set_sizing(Gtk.TreeViewColumnSizing.FIXED)
398        self._flags1_col = col
399
400        # Visibility cell
401        cell = Gtk.CellRendererPixbuf()
402        col.pack_start(cell, False)
403        datafunc = self._layer_visible_pixbuf_datafunc
404        col.set_cell_data_func(cell, datafunc)
405
406        # Name and preview column: will be indented
407        col = Gtk.TreeViewColumn(_("Name"))
408        col.set_sizing(Gtk.TreeViewColumnSizing.GROW_ONLY)
409        self._name_col = col
410
411        # Preview cell
412        cell = Gtk.CellRendererPixbuf()
413        col.pack_start(cell, False)
414        datafunc = self._layer_preview_pixbuf_datafunc
415        col.set_cell_data_func(cell, datafunc)
416        self._preview_cell = cell
417
418        # Name cell
419        cell = Gtk.CellRendererText()
420        cell.set_property("ellipsize", Pango.EllipsizeMode.END)
421        col.pack_start(cell, True)
422        datafunc = self._layer_name_text_datafunc
423        col.set_cell_data_func(cell, datafunc)
424        col.set_expand(True)
425        col.set_min_width(48)
426
427        # Other flags column
428        col = Gtk.TreeViewColumn(_("Flags"))
429        col.set_sizing(Gtk.TreeViewColumnSizing.GROW_ONLY)
430        area = col.get_property("cell-area")
431        area.set_orientation(Gtk.Orientation.VERTICAL)
432        self._flags2_col = col
433
434        # Locked cell
435        cell = Gtk.CellRendererPixbuf()
436        col.pack_end(cell, False)
437        datafunc = self._layer_locked_pixbuf_datafunc
438        col.set_cell_data_func(cell, datafunc)
439
440        # Column order on screen
441        self._columns = [
442            self._flags1_col,
443            self._name_col,
444            self._flags2_col,
445        ]
446        for col in self._columns:
447            self.append_column(col)
448
449        # View appearance
450        self.set_show_expanders(True)
451        self.set_enable_tree_lines(True)
452        self.set_expander_column(self._name_col)
453
454        self.connect_after("show", self._post_show_cb)
455
456    ## Low-level GDK event handlers
457
458    def _button_press_cb(self, view, event):
459        """Handle button presses (visibility, locked, naming)"""
460
461        # Basic details about the click
462        single_click = (event.type == Gdk.EventType.BUTTON_PRESS)
463        double_click = (event.type == Gdk.EventType._2BUTTON_PRESS)
464        is_menu = event.triggers_context_menu()
465
466        # Determine which row & column was clicked
467        x, y = int(event.x), int(event.y)
468        bw_x, bw_y = view.convert_widget_to_bin_window_coords(x, y)
469        click_info = view.get_path_at_pos(bw_x, bw_y)
470        if click_info is None:
471            return True
472        treemodel = self.get_model()
473        click_treepath, click_col, cell_x, cell_y = click_info
474        click_layer = treemodel.get_layer(treepath=click_treepath)
475        click_layerpath = tuple(click_treepath.get_indices())
476
477        # Defer certain kinds of click to separate handlers. These
478        # handlers can return True to stop processing and indicate that
479        # the current layer should not be changed.
480        col_handlers = [
481            # (Column, CellRenderer, single, double, handler)
482            (self._flags1_col, None, True, False,
483             self._flags1_col_click_cb),
484            (self._flags2_col, None, True, False,
485             self._flags2_col_click_cb),
486            (self._name_col, None, False, True,
487             self._name_col_2click_cb),
488            (self._name_col, self._preview_cell, True, False,
489             self._preview_cell_click_cb),
490        ]
491        if not is_menu:
492            for col, cell, when_single, when_double, handler in col_handlers:
493                if when_single and not single_click:
494                    continue
495                if when_double and not double_click:
496                    continue
497                # Correct column?
498                if col is not click_col:
499                    continue
500                # Click inside the target column's entire area?
501                ca = view.get_cell_area(click_treepath, col)
502                if not (ca.x <= bw_x < (ca.x + ca.width)):
503                    continue
504                # Also, inside any target CellRenderer's area?
505                if cell:
506                    pos_info = col.cell_get_position(cell)
507                    cell_xoffs, cell_w = pos_info
508                    if None in (cell_xoffs, cell_w):
509                        continue
510                    cell_x = ca.x + cell_xoffs
511                    if not (cell_x <= bw_x < (cell_x + cell_w)):
512                        continue
513                # Run the delegated handler if we got here.
514                if handler(event, click_layer, click_layerpath, ca):
515                    return True
516
517        # Clicks that fall thru the above cause a layer change.
518        if click_layerpath != self._docmodel.layer_stack.current_path:
519            self._docmodel.select_layer(path=click_layerpath)
520            self.current_layer_changed()
521
522        # Context menu for the layer just (right) clicked.
523        if is_menu and single_click:
524            self.current_layer_menu_requested(event)
525            return True
526
527        # Default behaviours: allow expanders & drag-and-drop to work
528        return False
529
530    def _name_col_2click_cb(self, event, layer, path, area):
531        """Rename the current layer."""
532        # At this point, a layer will have already been selected by
533        # a single-click event.
534        self.current_layer_rename_requested()
535        return True
536
537    def _flags1_col_click_cb(self, event, layer, path, area):
538        """Toggle visibility or Layer Solo (with Ctrl held)."""
539        rootstack = self._docmodel.layer_stack
540        lvm = self._docmodel.layer_view_manager
541
542        # Always turn off solo mode, if it's on.
543        if rootstack.current_layer_solo:
544            rootstack.current_layer_solo = False
545
546        # Use Ctrl+click to torn solo mode on.
547        elif event.state & Gdk.ModifierType.CONTROL_MASK:
548            rootstack.current_layer_solo = True
549
550        # Normally, clicks set the layer visible state.
551        # The view can be locked elsewhere, which stops this.
552        elif not lvm.current_view_locked:
553            new_visible = not layer.visible
554            self._docmodel.set_layer_visibility(new_visible, layer)
555
556        return True
557
558    def _flags2_col_click_cb(self, event, layer, path, area):
559        """Toggle the clicked layer's visibility."""
560        new_locked = not layer.locked
561        self._docmodel.set_layer_locked(new_locked, layer)
562        return True
563
564    def _preview_cell_click_cb(self, event, layer, path, area):
565        """Expand the clicked layer if the preview is clicked."""
566        # The idea here is that the preview cell area acts as an extra
567        # expander. Some themes' expander arrows are very small.
568        treepath = Gtk.TreePath(path)
569        self.expand_to_path(treepath)
570        return False  # fallthru: allow the layer to be selected
571
572    def _drag_begin_cb(self, view, context):
573        self.drag_began()
574        src_path = self._docmodel.layer_stack.get_current_path()
575        self._drag_src_path = src_path
576        self._drag_dest_path = None
577        src_treepath = Gtk.TreePath(src_path)
578        src_icon_surf = self.create_row_drag_icon(src_treepath)
579        Gtk.drag_set_icon_surface(context, src_icon_surf)
580        self._hover_expand_timer_id = None
581
582    def _get_checked_dest_row_at_pos(self, x, y):
583        """Like get_dest_row_at_pos(), but with structural checks"""
584        # Some pre-flight checks
585        src_path = self._drag_src_path
586        if src_path is None:
587            dest_treepath = None
588            drop_pos = Gtk.TreeViewDropPosition.BEFORE
589        root = self._docmodel.layer_stack
590        assert len(root) > 0, "Unexpected row drag within an empty tree!"
591
592        # Get GTK's purely position-based opinion, and decide what that
593        # means within the real tree structure.
594        dest_info = self.get_dest_row_at_pos(x, y)
595        if dest_info is None:
596            # GTK found no reference point. But it just hitboxes rows.
597            # Therefore, for dropping, this indicates the big empty
598            # space below all the layers.
599            # Return the (nonexistent) path one below the end of the
600            # root, and ask for an insert before that.
601            dest_treepath = Gtk.TreePath([len(root)])
602            drop_pos = Gtk.TreeViewDropPosition.BEFORE
603        else:
604            # GTK thinks it points at a reference point that actually
605            # exists. Confirm that notion first...
606            dest_treepath, drop_pos = dest_info
607            dest_path = tuple(dest_treepath)
608            dest_layer = root.deepget(dest_path)
609            if dest_layer is None:
610                dest_treepath = None
611                drop_pos = Gtk.TreeViewDropPosition.BEFORE
612            # Can't move a layer to its own position, or into itself,
613            elif lib.layer.path_startswith(dest_path, src_path):
614                dest_treepath = None
615                drop_pos = Gtk.TreeViewDropPosition.BEFORE
616            # or into any other layer that isn't a group.
617            elif not isinstance(dest_layer, lib.layer.LayerStack):
618                if drop_pos == Gtk.TreeViewDropPosition.INTO_OR_AFTER:
619                    drop_pos = Gtk.TreeViewDropPosition.AFTER
620                elif drop_pos == Gtk.TreeViewDropPosition.INTO_OR_BEFORE:
621                    drop_pos = Gtk.TreeViewDropPosition.BEFORE
622
623        if dest_treepath is not None:
624            logger.debug(
625                "Checked destination: %s %r",
626                drop_pos.value_nick,
627                tuple(dest_treepath),
628            )
629        return (dest_treepath, drop_pos)
630
631    def _drag_motion_cb(self, view, context, x, y, t):
632        dest_treepath, drop_pos = self._get_checked_dest_row_at_pos(x, y)
633        self.set_drag_dest_row(dest_treepath, drop_pos)
634        if dest_treepath is None:
635            dest_path = None
636            self._stop_hover_expand_timer()
637        else:
638            dest_path = tuple(dest_treepath)
639        old_dest_path = self._drag_dest_path
640        if old_dest_path != dest_path:
641            self._drag_dest_path = dest_path
642            if dest_path is not None:
643                self._restart_hover_expand_timer(dest_path, x, y)
644        return True
645
646    def _restart_hover_expand_timer(self, path, x, y):
647        self._stop_hover_expand_timer()
648        root = self._docmodel.layer_stack
649        layer = root.deepget(path)
650        if not isinstance(layer, lib.layer.LayerStack):
651            return
652        if self.row_expanded(Gtk.TreePath(path)):
653            return
654        self._hover_expand_timer_id = GLib.timeout_add(
655            int(self.DRAG_HOVER_EXPAND_TIME * 1000),
656            self._hover_expand_timer_cb,
657            path,
658            x, y,
659        )
660
661    def _stop_hover_expand_timer(self):
662        if self._hover_expand_timer_id is None:
663            return
664        GLib.source_remove(self._hover_expand_timer_id)
665        self._hover_expand_timer_id = None
666
667    def _hover_expand_timer_cb(self, path, x, y):
668        self.expand_to_path(Gtk.TreePath(path))
669        # The insertion marker may need updating after the expand
670        dest_treepath, drop_pos = self._get_checked_dest_row_at_pos(x, y)
671        self.set_drag_dest_row(dest_treepath, drop_pos)
672        self._hover_expand_timer_id = None
673        return False
674
675    def _drag_leave_cb(self, view, context, t):
676        """Reset the insertion point when the drag leaves"""
677        logger.debug("drag-leave t=%d", t)
678        self._stop_hover_expand_timer()
679        self.set_drag_dest_row(None, Gtk.TreeViewDropPosition.BEFORE)
680
681    def _get_insert_path_for_dest_row(self, dest_treepath, drop_pos):
682        """Convert a GTK destination row to a tree insert point.
683
684        This adjusts some path indices to be closer to what's intuitive
685        at the end of the drag, based on what the user saw during it.
686        The returned value must be checked before passing to the model
687        to ensure it isn't the same as or within the dragged tree path.
688
689        """
690        root = self._docmodel.layer_stack
691        if dest_treepath is None:
692            n = len(root)
693            return (n,)
694        dest_path = tuple(dest_treepath)
695        assert len(dest_path) > 0
696        dest_layer = root.deepget(dest_path)
697        gtvdp = Gtk.TreeViewDropPosition
698        if isinstance(dest_layer, lib.layer.LayerStack):
699            # Interpret Gtk's "into or before" as "into AND at the
700            # start". Similar for "into or after".
701            if drop_pos == gtvdp.INTO_OR_BEFORE:
702                return tuple(list(dest_path) + [0])
703            elif drop_pos == gtvdp.INTO_OR_AFTER:
704                n = len(dest_layer)
705                return tuple(list(dest_path) + [n])
706        if drop_pos == gtvdp.BEFORE:
707            return dest_path
708        elif drop_pos == gtvdp.AFTER:
709            is_expanded_group = (
710                isinstance(dest_layer, lib.layer.LayerStack) and
711                self.row_expanded(dest_treepath)
712            )
713            if is_expanded_group:
714                # This highlights like an insert before its first item
715                return tuple(list(dest_path) + [0])
716            else:
717                dest_path = list(dest_path)
718                dest_path[-1] += 1
719                return tuple(dest_path)
720        else:
721            raise NotImplemented("Unhandled position %r", drop_pos)
722
723    def _drag_drop_cb(self, view, context, x, y, t):
724        self._stop_hover_expand_timer()
725        dest_treepath, drop_pos = self._get_checked_dest_row_at_pos(x, y)
726        if dest_treepath is not None:
727            src_path = self._drag_src_path
728            dest_insert_path = self._get_insert_path_for_dest_row(
729                dest_treepath,
730                drop_pos,
731            )
732            if not lib.layer.path_startswith(dest_insert_path, src_path):
733                logger.debug(
734                    "drag-drop: move %r to insert at %r",
735                    src_path,
736                    dest_insert_path,
737                )
738                self._docmodel.restack_layer(src_path, dest_insert_path)
739            Gtk.drag_finish(context, True, False, t)
740            return True
741        return False
742
743    def _drag_end_cb(self, view, context):
744        logger.debug("drag-end")
745        self._stop_hover_expand_timer()
746        self._drag_src_path = None
747        self._drag_dest_path = None
748        self.drag_ended()
749
750    ## Model compat
751
752    def do_drag_data_delete(self, context):
753        """Suppress the default GtkWidgetClass.drag_data_delete handler.
754
755        Suppress warning(s?) about missing default handlers, since our
756        model no longer implements GtkTreeDragSource.
757
758        """
759
760    ## Model change tracking
761
762    def _current_path_updated_cb(self, rootstack, layerpath):
763        """Respond to the current layer changing in the doc-model"""
764        self._update_selection()
765
766    def _expand_layer_cb(self, rootstack, path):
767        if not path:
768            return
769        treepath = Gtk.TreePath(path)
770        self.expand_to_path(treepath)
771
772    def _collapse_layer_cb(self, rootstack, path):
773        if not path:
774            return
775        treepath = Gtk.TreePath(path)
776        self.collapse_row(treepath)
777
778    def _layer_content_changed_cb(self, rootstack, layer, *args):
779        """Scroll to the current layer when it is modified."""
780        if layer and layer is rootstack.current:
781            self.scroll_to_current_layer()
782
783    def _update_selection(self):
784        sel = self.get_selection()
785        root = self._docmodel.layer_stack
786        layerpath = root.current_path
787        if not layerpath:
788            sel.unselect_all()
789            return
790        old_layerpath = None
791        model, selected_paths = sel.get_selected_rows()
792        if len(selected_paths) > 0:
793            old_treepath = selected_paths[0]
794            if old_treepath:
795                old_layerpath = tuple(old_treepath.get_indices())
796        if layerpath == old_layerpath:
797            return
798        sel.unselect_all()
799        if len(layerpath) > 1:
800            self.expand_to_path(Gtk.TreePath(layerpath[:-1]))
801        if len(layerpath) > 0:
802            sel.select_path(Gtk.TreePath(layerpath))
803            self.scroll_to_current_layer()
804
805    def scroll_to_current_layer(self, *_ignored):
806        """Scroll to show the current layer"""
807        sel = self.get_selection()
808        tree_model, sel_row_paths = sel.get_selected_rows()
809        if len(sel_row_paths) > 0:
810            sel_row_path = sel_row_paths[0]
811            if sel_row_path:
812                self.scroll_to_cell(sel_row_path)
813
814    ## Observable events (hook stuff here!)
815
816    @event
817    def current_layer_rename_requested(self):
818        """Event: user double-clicked the name of the current layer"""
819
820    @event
821    def current_layer_changed(self):
822        """Event: the current layer was just changed by clicking it"""
823
824    @event
825    def current_layer_menu_requested(self, gdkevent):
826        """Event: user invoked the menu action over the current layer"""
827
828    @event
829    def drag_began(self):
830        """Event: a drag has just started"""
831
832    @event
833    def drag_ended(self):
834        """Event: a drag has just ended"""
835
836    ## View datafuncs
837
838    def _layer_visible_pixbuf_datafunc(self, column, cell, model, it, data):
839        """Use an open/closed eye icon to show layer visibilities"""
840        layer = model.get_layer(it=it)
841        rootstack = model._root
842        visible = True
843        sensitive = not self._docmodel.layer_view_manager.current_view_locked
844        if layer:
845            # Layer visibility is based on the layer's natural hidden/
846            # visible flag, but the layer stack can override that.
847            if rootstack.current_layer_solo:
848                visible = layer is rootstack.current
849                sensitive = False
850            else:
851                visible = layer.visible
852                sensitive = sensitive and layer.branch_visible
853
854        icon_name = "mypaint-object-{}-symbolic".format(
855            "visible" if visible else "hidden",
856        )
857        cell.set_property("icon-name", icon_name)
858        cell.set_property("sensitive", sensitive)
859
860    @staticmethod
861    def _datafunc_get_pixbuf_height(initial, column, multiple=8, maximum=256):
862        """Nearest multiple-of-n height for a pixbuf data cell."""
863        ox, oy, w, h = column.cell_get_size(None)
864        s = initial
865        if h is not None:
866            s = helpers.clamp((int(h // 8) * 8), s, maximum)
867        return s
868
869    def _layer_preview_pixbuf_datafunc(self, column, cell, model, it, data):
870        """Render layer preview icons and type info."""
871
872        # Get the layer's thumbnail
873        layer = model.get_layer(it=it)
874        thumb = layer.thumbnail
875
876        # Scale it to a reasonable size for use as the preview.
877        s = self._datafunc_get_pixbuf_height(32, column)
878        preview = make_preview(thumb, s)
879        cell.set_property("pixbuf", preview)
880
881        # Add a watermark icon for non-painting layers.
882        # Not completely sure this is a good idea...
883        try:
884            cache = self.__icon_cache
885        except AttributeError:
886            cache = {}
887            self.__icon_cache = cache
888        icon_name = layer.get_icon_name()
889        icon_size = 16
890        icon_size += 2   # allow fopr the outline
891        icon = cache.get(icon_name, None)
892        if not icon:
893            icon = gui.drawutils.load_symbolic_icon(
894                icon_name, icon_size,
895                fg=(1, 1, 1, 1),
896                outline=(0, 0, 0, 1),
897            )
898            cache[icon_name] = icon
899
900        # Composite the watermark over the preview
901        x = (preview.get_width() - icon_size) // 2
902        y = (preview.get_height() - icon_size) // 2
903        icon.composite(
904            dest=preview,
905            dest_x=x,
906            dest_y=y,
907            dest_width=icon_size,
908            dest_height=icon_size,
909            offset_x=x,
910            offset_y=y,
911            scale_x=1,
912            scale_y=1,
913            interp_type=GdkPixbuf.InterpType.NEAREST,
914            overall_alpha=255/6,
915        )
916
917    @staticmethod
918    def _layer_description_markup(layer):
919        """GMarkup text description of a layer, used in the list."""
920        name_markup = None
921        description = None
922
923        if layer is None:
924            name_markup = escape(lib.layer.PlaceholderLayer.DEFAULT_NAME)
925            description = C_(
926                "Layers: description: no layer (\"never happens\" condition!)",
927                u"?layer",
928            )
929        elif layer.name is None:
930            name_markup = escape(layer.DEFAULT_NAME)
931        else:
932            name_markup = escape(layer.name)
933
934        if layer is not None:
935            desc_parts = []
936            if isinstance(layer, lib.layer.LayerStack):
937                name_markup = "<i>{}</i>".format(name_markup)
938
939            # Mode (if it's interesting)
940            if layer.mode in lib.modes.MODE_STRINGS:
941                if layer.mode != lib.modes.default_mode():
942                    s, d = lib.modes.MODE_STRINGS[layer.mode]
943                    desc_parts.append(s)
944            else:
945                desc_parts.append(C_(
946                    "Layers: description parts: unknown mode (fallback str!)",
947                    u"?mode",
948                ))
949
950            # Visibility and opacity (if interesting)
951            if not layer.visible:
952                desc_parts.append(C_(
953                    "Layers: description parts: layer hidden",
954                    u"Hidden",
955                ))
956            elif layer.opacity < 1.0:
957                desc_parts.append(C_(
958                    "Layers: description parts: opacity percentage",
959                    u"%d%% opaque" % (round(layer.opacity * 100),)
960                ))
961
962            # Locked flag (locked is interesting)
963            if layer.locked:
964                desc_parts.append(C_(
965                    "Layers dockable: description parts: layer locked flag",
966                    u"Locked",
967                ))
968
969            # Description of the layer's type.
970            # Currently always used, for visual rhythm reasons, but it goes
971            # on the end since it's perhaps the least interesting info.
972            if layer.TYPE_DESCRIPTION is not None:
973                desc_parts.append(layer.TYPE_DESCRIPTION)
974            else:
975                desc_parts.append(C_(
976                    "Layers: description parts: unknown type (fallback str!)",
977                    u"?type",
978                ))
979
980            # Stitch it all together
981            if desc_parts:
982                description = C_(
983                    "Layers dockable: description parts joiner text",
984                    u", ",
985                ).join(desc_parts)
986            else:
987                description = None
988
989        if description is None:
990            markup_template = C_(
991                "Layers dockable: markup for a layer with no description",
992                u"{layer_name}",
993            )
994        else:
995            markup_template = C_(
996                "Layers dockable: markup for a layer with a description",
997                '<span size="smaller">{layer_name}\n'
998                '<span size="smaller">{layer_description}</span>'
999                '</span>'
1000            )
1001
1002        markup = markup_template.format(
1003            layer_name=name_markup,
1004            layer_description=escape(description),
1005        )
1006        return markup
1007
1008    def _layer_name_text_datafunc(self, column, cell, model, it, data):
1009        """Show the layer name, with italics for layer groups"""
1010        layer = model.get_layer(it=it)
1011        markup = self._layer_description_markup(layer)
1012
1013        attrs = Pango.AttrList()
1014        parse_result = Pango.parse_markup(markup, -1, '\000')
1015        parse_ok, attrs, text, accel_char = parse_result
1016        assert parse_ok
1017        cell.set_property("attributes", attrs)
1018        cell.set_property("text", text)
1019
1020    @staticmethod
1021    def _get_layer_locked_icon_state(layer):
1022        icon_name = None
1023        sensitive = True
1024        if layer:
1025            locked = layer.locked
1026            sensitive = not layer.branch_locked
1027        if locked:
1028            icon_name = "mypaint-object-locked-symbolic"
1029        else:
1030            icon_name = "mypaint-object-unlocked-symbolic"
1031        return (icon_name, sensitive)
1032
1033    def _layer_locked_pixbuf_datafunc(self, column, cell, model, it, data):
1034        """Use a padlock icon to show layer immutability statuses"""
1035        layer = model.get_layer(it=it)
1036        icon_name, sensitive = self._get_layer_locked_icon_state(layer)
1037        icon_visible = (icon_name is not None)
1038        cell.set_property("icon-name", icon_name)
1039        cell.set_visible(icon_visible)
1040        cell.set_property("sensitive", sensitive)
1041
1042    ## Weird but necessary hacks
1043
1044    def _post_show_cb(self, widget):
1045        # Ensure the tree selection matches the root stack's current layer.
1046        self._update_selection()
1047
1048        # Match the flag column widths to the name column's height.
1049        # This only makes sense after the 1st text layout, sadly.
1050        GLib.idle_add(self._sizeify_flag_columns)
1051
1052        return False
1053
1054    def _sizeify_flag_columns(self):
1055        """Sneakily scale the fixed size of the flag icons to match texts.
1056
1057        This can only be called after the list has rendered once, because
1058        GTK doesn't know how tall the treeview's rows will be till then.
1059        Therefore it's called in an idle callback after the first show.
1060
1061        """
1062        # Get the maximum height for all columns.
1063        s = 0
1064        for col in self._columns:
1065            ox, oy, w, h = col.cell_get_size(None)
1066            if h > s:
1067                s = h
1068        if not s:
1069            return
1070
1071        # Set that as the fixed size of the flag icon columns,
1072        # within reason, and force a re-layout.
1073        h = helpers.clamp(s, 24, 48)
1074        w = helpers.clamp(s, 24, 48)
1075        for col in [self._flags1_col, self._flags2_col]:
1076            for cell in col.get_cells():
1077                cell.set_fixed_size(w, h)
1078            col.set_min_width(w)
1079        for col in self._columns:
1080            col.queue_resize()
1081
1082
1083# Helper functions
1084
1085def new_blend_mode_combo(modes, mode_strings):
1086    """Create and return a new blend mode combo box
1087    """
1088    store = Gtk.ListStore(int, str, bool, float)
1089    for mode in modes:
1090        label, desc = mode_strings.get(mode)
1091        sensitive = True
1092        scale = 1/1.2   # PANGO_SCALE_SMALL
1093        store.append([mode, label, sensitive, scale])
1094    combo = Gtk.ComboBox()
1095    combo.set_model(store)
1096    combo.set_hexpand(True)
1097    combo.set_vexpand(False)
1098    cell = Gtk.CellRendererText()
1099    combo.pack_start(cell, True)
1100    combo.add_attribute(cell, "text", 1)
1101    combo.add_attribute(cell, "sensitive", 2)
1102    combo.add_attribute(cell, "scale", 3)
1103    combo.set_wrap_width(2)
1104    combo.set_app_paintable(True)
1105    return combo
1106
1107## Testing
1108
1109
1110def _test():
1111    """Test the custom model in an ad-hoc GUI window"""
1112    from lib.layer import PaintingLayer, LayerStack
1113    doc_model = Document()
1114    root = doc_model.layer_stack
1115    root.clear()
1116    layer_info = [
1117        ((0,), LayerStack(name="Layer 0")),
1118        ((0, 0), PaintingLayer(name="Layer 0:0")),
1119        ((0, 1), PaintingLayer(name="Layer 0:1")),
1120        ((0, 2), LayerStack(name="Layer 0:2")),
1121        ((0, 2, 0), PaintingLayer(name="Layer 0:2:0")),
1122        ((0, 2, 1), PaintingLayer(name="Layer 0:2:1")),
1123        ((0, 3), PaintingLayer(name="Layer 0:3")),
1124        ((1,), LayerStack(name="Layer 1")),
1125        ((1, 0), PaintingLayer(name="Layer 1:0")),
1126        ((1, 1), PaintingLayer(name="Layer 1:1")),
1127        ((1, 2), LayerStack(name="Layer 1:2")),
1128        ((1, 2, 0), PaintingLayer(name="Layer 1:2:0")),
1129        ((1, 2, 1), PaintingLayer(name="Layer 1:2:1")),
1130        ((1, 2, 2), PaintingLayer(name="Layer 1:2:2")),
1131        ((1, 2, 3), PaintingLayer(name="Layer 1:2:3")),
1132        ((1, 3), PaintingLayer(name="Layer 1:3")),
1133        ((1, 4), PaintingLayer(name="Layer 1:4")),
1134        ((1, 5), PaintingLayer(name="Layer 1:5")),
1135        ((1, 6), PaintingLayer(name="Layer 1:6")),
1136        ((2,), PaintingLayer(name="Layer 2")),
1137        ((3,), PaintingLayer(name="Layer 3")),
1138        ((4,), PaintingLayer(name="Layer 4")),
1139        ((5,), PaintingLayer(name="Layer 5")),
1140        ((6,), LayerStack(name="Layer 6")),
1141        ((6, 0), PaintingLayer(name="Layer 6:0")),
1142        ((6, 1), PaintingLayer(name="Layer 6:1")),
1143        ((6, 2), PaintingLayer(name="Layer 6:2")),
1144        ((6, 3), PaintingLayer(name="Layer 6:3")),
1145        ((6, 4), PaintingLayer(name="Layer 6:4")),
1146        ((6, 5), PaintingLayer(name="Layer 6:5")),
1147        ((7,), PaintingLayer(name="Layer 7")),
1148    ]
1149    for path, layer in layer_info:
1150        root.deepinsert(path, layer)
1151    root.set_current_path([4])
1152
1153    icon_theme = Gtk.IconTheme.get_default()
1154    icon_theme.append_search_path("./desktop/icons")
1155
1156    view = RootStackTreeView(doc_model)
1157    view_scroll = Gtk.ScrolledWindow()
1158    view_scroll.set_shadow_type(Gtk.ShadowType.ETCHED_IN)
1159    scroll_pol = Gtk.PolicyType.AUTOMATIC
1160    view_scroll.set_policy(scroll_pol, scroll_pol)
1161    view_scroll.add(view)
1162    view_scroll.set_size_request(-1, 100)
1163
1164    win = Gtk.Window()
1165    win.set_title(unicode(__package__))
1166    win.connect("destroy", Gtk.main_quit)
1167    win.add(view_scroll)
1168    win.set_default_size(300, 500)
1169
1170    win.show_all()
1171    Gtk.main()
1172
1173
1174if __name__ == '__main__':
1175    logging.basicConfig(level=logging.DEBUG)
1176    _test()
1177