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