File: brushmodifier.py

package info (click to toggle)
mypaint 2.0.1-14
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 27,884 kB
  • sloc: python: 43,893; cpp: 6,931; xml: 2,475; sh: 473; makefile: 25
file content (161 lines) | stat: -rw-r--r-- 6,552 bytes parent folder | download | duplicates (4)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
# This file is part of MyPaint.
# Copyright (C) 2011 by Martin Renold <martinxyz@gmx.ch>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.

from __future__ import division, print_function

from gettext import gettext as _
from lib.helpers import rgb_to_hsv, hsv_to_rgb

import gui.blendmodehandler


class BrushModifier (object):
    """Applies changed brush settings to the active brush, with overrides.

    A single instance of this lives within the main `application.Application`
    instance. The BrushModifier tracks brush settings like color, eraser and
    lock alpha mode that can be overridden by the GUI.::

      BrushManager ---select_brush---> BrushModifier --> TiledDrawWidget

    The `BrushManager` provides the brush settings as stored on disk, and
    the BrushModifier passes them via `TiledDrawWidget` to the brush engine.
    """

    MODE_FORCED_ON_SETTINGS = [1.0, {}]
    MODE_FORCED_OFF_SETTINGS = [0.0, {}]

    def __init__(self, app):
        object.__init__(self)
        self.app = app
        app.brushmanager.brush_selected += self.brush_selected_cb
        app.brush.observers.append(self.brush_modified_cb)
        self.unmodified_brushinfo = app.brush.clone()
        self._in_brush_selected_cb = False
        self._last_selected_color = app.brush.get_color_hsv()
        self.bm = gui.blendmodehandler.BlendModes()
        self.bm.mode_changed += self.update_blendmodes

    def update_blendmodes(self, bm, old, new):
        if old is new:
            return
        if old.setting_name:
            self.set_override_setting(old.setting_name, False)
        if new.setting_name:
            self.set_override_setting(new.setting_name, True)

    def set_override_setting(self, setting_name, override):
        """Overrides a boolean setting currently in effect.

        If `override` is true, the named setting will be forced to a base value
        greater than 0.9, and if it is false a base value less than 0.1 will be
        applied. Where possible, values from the base brush will be used. The
        setting from `unmodified_brushinfo`, including any input mapping, will
        be used if its base value is suitably large (or small). If not, a base
        value of either 1 or 0 and no input mapping will be applied.
        """
        unmod_b = self.unmodified_brushinfo
        modif_b = self.app.brush
        if override:
            if not modif_b.has_large_base_value(setting_name):
                settings = self.MODE_FORCED_ON_SETTINGS
                if unmod_b.has_large_base_value(setting_name):
                    settings = unmod_b.get_setting(setting_name)
                modif_b.set_setting(setting_name, settings)
        else:
            if not modif_b.has_small_base_value(setting_name):
                settings = self.MODE_FORCED_OFF_SETTINGS
                if unmod_b.has_small_base_value(setting_name):
                    settings = unmod_b.get_setting(setting_name)
                modif_b.set_setting(setting_name, settings)

    def restore_context_of_selected_brush(self):
        """Restores color from the unmodified base brush.

        After a brush has been selected, restore additional brush settings -
        currently just color - from `unmodified_brushinfo`. This is called
        after selecting a brush by picking a stroke from the canvas.
        """
        c = self.unmodified_brushinfo.get_color_hsv()
        self.app.brush.set_color_hsv(c)

    def brush_selected_cb(self, bm, managed_brush, brushinfo):
        """Responds to the user changing their brush.

        This observer callback is responsible for allocating the current brush
        settings to the current brush singleton in `self.app`. The Brush
        Selector, the Pick Context action, and the Brushkeys and
        Device-specific brush associations all cause this to be invoked.
        """
        self._in_brush_selected_cb = True
        b = self.app.brush
        prev_lock_alpha = b.is_alpha_locked()

        # Changing the effective brush
        b.begin_atomic()
        color = b.get_color_hsv()

        mix_old = b.get_base_value('restore_color')
        b.load_from_brushinfo(brushinfo)
        self.unmodified_brushinfo = b.clone()

        # Preserve color
        mix = b.get_base_value('restore_color')
        if mix:
            c1 = hsv_to_rgb(*color)
            c2 = hsv_to_rgb(*b.get_color_hsv())
            c3 = [(1.0-mix)*v1 + mix*v2 for v1, v2 in zip(c1, c2)]
            color = rgb_to_hsv(*c3)
        elif mix_old and self._last_selected_color:
            # switching from a brush with fixed color back to a normal one
            color = self._last_selected_color
        b.set_color_hsv(color)

        b.set_string_property("parent_brush_name", managed_brush.name)

        if b.is_eraser():
            # User picked a dedicated eraser brush
            # Unset any lock_alpha state (necessary?)
            self.set_override_setting("lock_alpha", False)
        else:
            # Preserve the old lock_alpha state
            self.set_override_setting("lock_alpha", prev_lock_alpha)

        b.end_atomic()

        # Updates the blend mode buttons to match the new settings.
        # First decide which blend mode is active and which aren't.
        active_blend_mode = self.bm.normal_mode
        for mode in self.bm.modes:
            setting_name = mode.setting_name
            if setting_name is not None:
                if b.has_large_base_value(setting_name):
                    active_blend_mode = mode
        active_blend_mode.active = True

        self._in_brush_selected_cb = False

    def _brush_is_dedicated_eraser(self):
        if self.unmodified_brushinfo is None:
            return False
        return self.unmodified_brushinfo.is_eraser()

    def brush_modified_cb(self, changed_settings):
        """Responds to changes of the brush settings.
        """
        if self._brush_is_dedicated_eraser():
            return

        if changed_settings.intersection(('color_h', 'color_s', 'color_v')):
            # Cancel eraser mode on ordinary brushes
            em = self.bm.eraser_mode
            if em.active and 'eraser_mode' not in changed_settings:
                em.active = False

            if not self._in_brush_selected_cb:
                self._last_selected_color = self.app.brush.get_color_hsv()