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