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