1# abstract_tool.py 2# 3# Copyright 2018-2021 Romain F. T. 4# 5# This program is free software: you can redistribute it and/or modify 6# it under the terms of the GNU General Public License as published by 7# the Free Software Foundation, either version 3 of the License, or 8# (at your option) any later version. 9# 10# This program is distributed in the hope that it will be useful, 11# but WITHOUT ANY WARRANTY; without even the implied warranty of 12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13# GNU General Public License for more details. 14# 15# You should have received a copy of the GNU General Public License 16# along with this program. If not, see <http://www.gnu.org/licenses/>. 17 18import cairo 19from gi.repository import Gtk 20 21class WrongToolIdException(Exception): 22 def __init__(self, expected, actual): 23 # Context: an error message 24 msg = _("Can't start operation: wrong tool id (expected {0}, got {1})") 25 super().__init__(msg.format(expected, actual)) 26 27################################################################################ 28 29class AbstractAbstractTool(): 30 """Super-class implemented and extended by all tools.""" 31 32 __gtype_name__ = 'AbstractAbstractTool' 33 UI_PATH = '/com/github/maoschanz/drawing/tools/ui/' 34 35 def __init__(self, tool_id, label, icon_name, window, **kwargs): 36 self.window = window 37 # The tool's identity 38 self.id = tool_id 39 self.menu_id = 0 40 self.label = label 41 self.icon_name = icon_name 42 # The options it supports 43 self.accept_selection = False 44 self.use_color = False 45 self.use_operator = False 46 # The tool's state 47 self.cursor_name = 'cell' 48 self._ongoing_operation = False 49 # Once everything is set, build the UI 50 self.build_row() 51 self.try_build_pane() 52 53 ############################################################################ 54 # Utilities managing actions for tool's options ############################ 55 56 def add_tool_action_simple(self, action_name, callback): 57 """Convenient wrapper method adding a stateless action to the window. It 58 will be named 'action_name' (string) and activating the action will 59 trigger the method 'callback'.""" 60 # XXX allow to set shortcuts here? 61 self.window.add_action_simple(action_name, callback) 62 63 def add_tool_action_boolean(self, action_name, default): 64 self.window.options_manager.add_option_boolean(action_name, default) 65 66 def add_tool_action_enum(self, action_name, default): 67 self.window.options_manager.add_option_enum(action_name, default) 68 69 def load_tool_action_boolean(self, action_name, key_name): 70 om = self.window.options_manager 71 return om.add_option_from_bool_key(action_name, key_name) 72 73 def load_tool_action_enum(self, action_name, key_name): 74 om = self.window.options_manager 75 return om.add_option_from_enum_key(action_name, key_name) 76 77 def get_option_value(self, action_name): 78 return self.window.options_manager.get_value(action_name) 79 80 def set_action_sensitivity(self, action_name, state): 81 self.window.lookup_action(action_name).set_enabled(state) 82 83 def update_actions_state(self): 84 self.set_action_sensitivity('main_color', self.use_color) 85 self.set_action_sensitivity('secondary_color', self.use_color) 86 self.set_action_sensitivity('exchange_color', self.use_color) 87 self.set_action_sensitivity('cairo_operator', self.use_operator) 88 89 def get_settings(self): 90 return self.window.options_manager._tools_gsettings 91 92 ############################################################################ 93 # Various utilities ######################################################## 94 95 def show_error(self, error_text): 96 self.window.prompt_message(True, error_text) 97 98 ############################################################################ 99 # Bottom pane and menubar integration ###################################### 100 101 def try_build_pane(self): 102 pass 103 104 def build_bottom_pane(self): 105 return None 106 107 def on_apply_temp_pixbuf_tool_operation(self, *args): 108 pass # implemented only by transform tools 109 110 def get_options_label(self): 111 return _("No options") 112 113 def adapt_to_window_size(self, available_width): 114 pass 115 116 def add_item_to_menu(self, tools_menu): 117 tools_menu.append(self.label, 'win.active_tool::' + self.id) 118 119 def get_options_model(self): 120 """Returns a Gio.MenuModel corresponding to the tool's options. It'll be 121 shown in the menubar (if any) and in the bottom pane (if the tool's 122 bottom pane supports such a feature).""" 123 fpath = self.UI_PATH + 'tool-' + self.id + '.ui' 124 builder = Gtk.Builder.new_from_resource(fpath) 125 return builder.get_object('options-menu') 126 127 def get_options_widget(self): 128 """Returns a Gtk.Widget (normally a box) corresponding to the tool's 129 options. It'll be in the bottom pane (if the tool's bottom pane supports 130 such a feature) in replacement of the Gio.MenuModel if such a simple 131 menu can't provide all the features.""" 132 return None 133 134 def get_edition_status(self): 135 return self.label 136 137 ############################################################################ 138 # Side pane ################################################################ 139 140 def build_row(self): 141 """Build the GtkRadioButton for the sidebar. This method stores it as 142 'self.row', but does not pack it in the bar, and does not return it.""" 143 self.row = Gtk.RadioButton(relief=Gtk.ReliefStyle.NONE, \ 144 draw_indicator=False, valign=Gtk.Align.CENTER, \ 145 tooltip_text=self.label) 146 self.row.set_detailed_action_name('win.active_tool::' + self.id) 147 self.label_widget = Gtk.Label(label=self.label) #, use_underline=True) 148 if self.window.gsettings.get_boolean('big-icons'): 149 size = Gtk.IconSize.LARGE_TOOLBAR 150 else: 151 size = Gtk.IconSize.SMALL_TOOLBAR 152 image = Gtk.Image().new_from_icon_name(self.icon_name, size) 153 box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8) 154 box.add(image) 155 box.add(self.label_widget) 156 self.row.add(box) 157 self.row.show_all() 158 159 def set_show_label(self, label_visible): 160 self.label_widget.set_visible(label_visible) 161 if label_visible: 162 self.row.get_children()[0].set_halign(Gtk.Align.START) 163 else: 164 self.row.get_children()[0].set_halign(Gtk.Align.CENTER) 165 166 def update_icon_size(self): 167 image = self.row.get_children()[0].get_children()[0] 168 if self.window.gsettings.get_boolean('big-icons'): 169 size = Gtk.IconSize.LARGE_TOOLBAR 170 else: 171 size = Gtk.IconSize.SMALL_TOOLBAR 172 image.set_from_icon_name(self.icon_name, size) 173 174 ############################################################################ 175 # Activation or not ######################################################## 176 177 def on_tool_selected(self): 178 pass 179 180 def on_tool_unselected(self): 181 pass 182 183 def cancel_ongoing_operation(self): 184 self.on_tool_unselected() 185 self.give_back_control(self.accept_selection) # XXX pas sûr 186 self.on_tool_selected() 187 self.restore_pixbuf() 188 self.non_destructive_show_modif() 189 self._ongoing_operation = False 190 191 def give_back_control(self, preserve_selection): 192 self.restore_pixbuf() 193 self.non_destructive_show_modif() 194 195 ############################################################################ 196 # History ################################################################## 197 198 def has_ongoing_operation(self): 199 return self._ongoing_operation 200 201 def do_tool_operation(self, operation): 202 pass 203 204 def start_tool_operation(self, operation): 205 if operation['tool_id'] != self.id: 206 raise WrongToolIdException(operation['tool_id'], self.id) 207 self.restore_pixbuf() 208 self._ongoing_operation = True 209 210 def apply_operation(self, operation): 211 """Complete method to apply an operation: the operation is applied and 212 the image is updated as well as the state of actions.""" 213 self.simple_apply_operation(operation) 214 self.get_image().update_actions_state() 215 self.get_image().update_history_sensitivity() 216 217 def simple_apply_operation(self, operation): 218 """Simpler apply_operation, for the 'rebuild from history' method.""" 219 try: 220 self.do_tool_operation(operation) 221 self.get_image().add_to_history(operation) 222 except Exception as e: 223 self.show_error(str(e)) 224 self._ongoing_operation = False 225 self.non_destructive_show_modif() # XXX nécessaire ? 226 227 ############################################################################ 228 # Selection ################################################################ 229 230 def get_selection(self): 231 return self.get_image().selection 232 233 def selection_is_active(self): 234 return self.get_selection().is_active 235 236 def get_selection_pixbuf(self): 237 return self.get_selection().get_pixbuf() 238 239 def get_overlay_thickness(self): 240 return (1 / self.get_image().zoom_level) 241 242 ############################################################################ 243 # Image management ######################################################### 244 245 def get_image(self): 246 return self.window.get_active_image() 247 248 def get_surface(self): 249 return self.get_image().get_surface() 250 251 def scale_factor(self): 252 return self.get_image().SCALE_FACTOR 253 254 def get_context(self): 255 return cairo.Context(self.get_surface()) 256 257 def get_main_pixbuf(self): 258 return self.get_image().main_pixbuf 259 260 def non_destructive_show_modif(self): 261 self.get_image().update() 262 263 def restore_pixbuf(self): 264 self.get_image().use_stable_pixbuf() 265 266 ############################################################################ 267 # Signals handling ######################################################### 268 269 def on_press_on_area(self, event, surface, event_x, event_y): 270 pass 271 272 def on_motion_on_area(self, event, surface, event_x, event_y): 273 pass 274 275 def on_unclicked_motion_on_area(self, event, surface): 276 pass 277 278 def on_release_on_area(self, event, surface, event_x, event_y): 279 pass 280 281 def on_draw_above(self, area, cairo_context): 282 pass 283 284 ############################################################################ 285################################################################################ 286 287