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