1# This file is part of MyPaint. 2# Copyright (C) 2013-2018 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"""Widgets and popup dialogs for making quick choices""" 10 11## Imports 12 13from __future__ import division, print_function 14import abc 15 16from lib.gibindings import Gtk 17 18from .pixbuflist import PixbufList 19from . import brushmanager 20from . import brushselectionwindow 21from . import widgets 22from . import spinbox 23from . import windowing 24from lib.observable import event 25import gui.colortools 26from lib.pycompat import add_metaclass 27 28 29## Module consts 30 31_DEFAULT_PREFS_ID = u"default" 32 33 34## Interfaces 35 36@add_metaclass(abc.ABCMeta) 37class Advanceable: 38 """Interface for choosers which can be advanced by pressing keys. 39 40 Advancing happens if the chooser is already visible and its key is 41 pressed again. This can happen repeatedly. The actual action 42 performed is up to the implementation: advancing some some choosers 43 may move them forward through pages of alternatives, while other 44 choosers may actually change a brush setting as they advance. 45 46 """ 47 48 @abc.abstractmethod 49 def advance(self): 50 """Advances the chooser to the next page or choice. 51 52 Choosers should remain open when their advance() method is 53 invoked. The actual action performed is up to the concrete 54 implementation: see the class docs. 55 56 """ 57 58 59## Class defs 60 61class QuickBrushChooser (Gtk.VBox): 62 """A quick chooser widget for brushes""" 63 64 ## Class constants 65 66 _PREFS_KEY_TEMPLATE = u"brush_chooser.%s.selected_group" 67 ICON_SIZE = 48 68 69 ## Method defs 70 71 def __init__(self, app, prefs_id=_DEFAULT_PREFS_ID): 72 """Initialize""" 73 Gtk.VBox.__init__(self) 74 self.app = app 75 self.bm = app.brushmanager 76 77 self._prefs_key = self._PREFS_KEY_TEMPLATE % (prefs_id,) 78 active_group_name = app.preferences.get(self._prefs_key, None) 79 80 model = self._make_groups_sb_model() 81 self.groups_sb = spinbox.ItemSpinBox(model, self._groups_sb_changed_cb, 82 active_group_name) 83 active_group_name = self.groups_sb.get_value() 84 85 brushes = self.bm.get_group_brushes(active_group_name) 86 87 self.brushlist = PixbufList( 88 brushes, self.ICON_SIZE, self.ICON_SIZE, 89 namefunc=brushselectionwindow.managedbrush_namefunc, 90 pixbuffunc=brushselectionwindow.managedbrush_pixbuffunc, 91 idfunc=brushselectionwindow.managedbrush_idfunc 92 ) 93 self.brushlist.dragging_allowed = False 94 self.bm.groups_changed += self._groups_changed_cb 95 self.bm.brushes_changed += self._brushes_changed_cb 96 self.brushlist.item_selected += self._item_selected_cb 97 98 scrolledwin = Gtk.ScrolledWindow() 99 scrolledwin.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.ALWAYS) 100 scrolledwin.add(self.brushlist) 101 w = int(self.ICON_SIZE * 4.5) 102 h = int(self.ICON_SIZE * 5.0) 103 scrolledwin.set_min_content_width(w) 104 scrolledwin.set_min_content_height(h) 105 scrolledwin.get_child().set_size_request(w, h) 106 107 self.pack_start(self.groups_sb, False, False, 0) 108 self.pack_start(scrolledwin, True, True, 0) 109 self.set_spacing(widgets.SPACING_TIGHT) 110 111 def _item_selected_cb(self, pixbuf_list, brush): 112 """Internal: call brush_selected event when an item is chosen""" 113 self.brush_selected(brush) 114 115 @event 116 def brush_selected(self, brush): 117 """Event: a brush was selected 118 119 :param brush: The newly chosen brush 120 """ 121 122 def _make_groups_sb_model(self): 123 """Internal: create the model for the group choice spinbox""" 124 group_names = sorted(self.bm.groups.keys()) 125 model = [] 126 for name in group_names: 127 label_text = brushmanager.translate_group_name(name) 128 model.append((name, label_text)) 129 return model 130 131 def _groups_changed_cb(self, bm): 132 """Internal: update the spinbox model at the top of the widget""" 133 model = self._make_groups_sb_model() 134 self.groups_sb.set_model(model) 135 # In case the group has been deleted and recreated, we do this: 136 group_name = self.groups_sb.get_value() 137 group_brushes = self.bm.groups.get(group_name, []) 138 self.brushlist.itemlist = group_brushes 139 self.brushlist.update() 140 # See https://github.com/mypaint/mypaint/issues/654 141 142 def _brushes_changed_cb(self, bm, brushes): 143 """Internal: update the PixbufList if its group was changed.""" 144 # CARE: this might be called in response to the group being deleted. 145 # Don't recreate it by accident. 146 group_name = self.groups_sb.get_value() 147 group_brushes = self.bm.groups.get(group_name) 148 if brushes is group_brushes: 149 self.brushlist.update() 150 151 def _groups_sb_changed_cb(self, group_name): 152 """Internal: update the list of brush icons when the group changes""" 153 self.app.preferences[self._prefs_key] = group_name 154 group_brushes = self.bm.groups.get(group_name, []) 155 self.brushlist.itemlist = group_brushes 156 self.brushlist.update() 157 158 def advance(self): 159 """Advances to the next page of brushes.""" 160 self.groups_sb.next() 161 162 163class BrushChooserPopup (windowing.ChooserPopup): 164 """Speedy brush chooser popup""" 165 166 def __init__(self, app, prefs_id=_DEFAULT_PREFS_ID): 167 """Initialize. 168 169 :param gui.application.Application app: main app instance 170 :param unicode prefs_id: prefs identifier for the chooser 171 172 The prefs identifier forms part of preferences key which store 173 layout and which page of the chooser is selected. It should 174 follow the same syntax rules as Python simple identifiers. 175 176 """ 177 windowing.ChooserPopup.__init__( 178 self, 179 app = app, 180 actions = [ 181 'ColorChooserPopup', 182 'ColorChooserPopupFastSubset', 183 'BrushChooserPopup', 184 ], 185 config_name = "brush_chooser.%s" % (prefs_id,), 186 ) 187 self._chosen_brush = None 188 self._chooser = QuickBrushChooser(app, prefs_id=prefs_id) 189 self._chooser.brush_selected += self._brush_selected_cb 190 191 bl = self._chooser.brushlist 192 bl.connect("button-release-event", self._brushlist_button_release_cb) 193 194 self.add(self._chooser) 195 196 def _brush_selected_cb(self, chooser, brush): 197 """Internal: update the response brush when an icon is clicked""" 198 self._chosen_brush = brush 199 200 def _brushlist_button_release_cb(self, *junk): 201 """Internal: send an accept response on a button release 202 203 We only send the response (and close the dialog) on button release to 204 avoid accidental dabs with the stylus. 205 """ 206 if self._chosen_brush is not None: 207 bm = self.app.brushmanager 208 bm.select_brush(self._chosen_brush) 209 self.hide() 210 self._chosen_brush = None 211 212 def advance(self): 213 """Advances to the next page of brushes.""" 214 self._chooser.advance() 215 216 217class QuickColorChooser (Gtk.VBox): 218 """A quick chooser widget for colors""" 219 220 ## Class constants 221 _PREFS_KEY_TEMPLATE = u"color_chooser.%s.selected_adjuster" 222 _ALL_ADJUSTER_CLASSES = [ 223 gui.colortools.HCYWheelTool, 224 gui.colortools.HSVWheelTool, 225 gui.colortools.PaletteTool, 226 gui.colortools.HSVCubeTool, 227 gui.colortools.HSVSquareTool, 228 gui.colortools.ComponentSlidersTool, 229 gui.colortools.RingsColorChangerTool, 230 gui.colortools.WashColorChangerTool, 231 gui.colortools.CrossedBowlColorChangerTool, 232 ] 233 _SINGLE_CLICK_ADJUSTER_CLASSES = [ 234 gui.colortools.PaletteTool, 235 gui.colortools.WashColorChangerTool, 236 gui.colortools.CrossedBowlColorChangerTool, 237 ] 238 239 def __init__(self, app, prefs_id=_DEFAULT_PREFS_ID, single_click=False): 240 Gtk.VBox.__init__(self) 241 self._app = app 242 self._spinbox_model = [] 243 self._adjs = {} 244 self._pages = [] 245 mgr = app.brush_color_manager 246 if single_click: 247 adjuster_classes = self._SINGLE_CLICK_ADJUSTER_CLASSES 248 else: 249 adjuster_classes = self._ALL_ADJUSTER_CLASSES 250 for page_class in adjuster_classes: 251 name = page_class.__name__ 252 page = page_class() 253 self._pages.append(page) 254 self._spinbox_model.append((name, page.tool_widget_title)) 255 self._adjs[name] = page 256 page.set_color_manager(mgr) 257 if page_class in self._SINGLE_CLICK_ADJUSTER_CLASSES: 258 page.connect_after( 259 "button-release-event", 260 self._ccwidget_btn_release_cb, 261 ) 262 self._prefs_key = self._PREFS_KEY_TEMPLATE % (prefs_id,) 263 active_page = app.preferences.get(self._prefs_key, None) 264 sb = spinbox.ItemSpinBox(self._spinbox_model, self._spinbox_changed_cb, 265 active_page) 266 active_page = sb.get_value() 267 self._spinbox = sb 268 self._active_adj = self._adjs[active_page] 269 self.pack_start(sb, False, False, 0) 270 self.pack_start(self._active_adj, True, True, 0) 271 self.set_spacing(widgets.SPACING_TIGHT) 272 273 def _spinbox_changed_cb(self, page_name): 274 self._app.preferences[self._prefs_key] = page_name 275 self.remove(self._active_adj) 276 new_adj = self._adjs[page_name] 277 self._active_adj = new_adj 278 self.pack_start(self._active_adj, True, True, 0) 279 self._active_adj.show_all() 280 281 def _ccwidget_btn_release_cb(self, ccwidget, event): 282 """Internal: fire "choice_completed" after clicking certain widgets""" 283 self.choice_completed() 284 return False 285 286 @event 287 def choice_completed(self): 288 """Event: a complete selection was made 289 290 This is emitted by button-release events on certain kinds of colour 291 chooser page. Not every page in the chooser emits this event, because 292 colour is a three-dimensional quantity: clicking on a two-dimensional 293 popup can't make a complete choice of colour with most pages. 294 295 The palette page does emit this event, and it's the default. 296 """ 297 298 def advance(self): 299 """Advances to the next color selector.""" 300 self._spinbox.next() 301 302 303class ColorChooserPopup (windowing.ChooserPopup): 304 """Speedy color chooser dialog""" 305 306 def __init__(self, app, prefs_id=_DEFAULT_PREFS_ID, single_click=False): 307 """Initialize. 308 309 :param gui.application.Application app: main app instance 310 :param unicode prefs_id: prefs identifier for the chooser 311 :param bool single_click: limit to just the single-click adjusters 312 313 The prefs identifier forms part of preferences key which store 314 layout and which page of the chooser is selected. It should 315 follow the same syntax rules as Python simple identifiers. 316 317 """ 318 windowing.ChooserPopup.__init__( 319 self, 320 app = app, 321 actions = [ 322 'ColorChooserPopup', 323 'ColorChooserPopupFastSubset', 324 'BrushChooserPopup', 325 ], 326 config_name = u"color_chooser.%s" % (prefs_id,), 327 ) 328 self._chooser = QuickColorChooser( 329 app, 330 prefs_id=prefs_id, 331 single_click=single_click, 332 ) 333 self._chooser.choice_completed += self._choice_completed_cb 334 self.add(self._chooser) 335 336 def _choice_completed_cb(self, chooser): 337 """Internal: close when a choice is (fully) made 338 339 Close the dialog on button release only to avoid accidental dabs 340 with the stylus. 341 """ 342 self.hide() 343 344 def advance(self): 345 """Advances to the next color selector.""" 346 self._chooser.advance() 347 348 349## Classes: interface registration 350 351Advanceable.register(QuickBrushChooser) 352Advanceable.register(QuickColorChooser) 353Advanceable.register(BrushChooserPopup) 354Advanceable.register(ColorChooserPopup) 355