blend modes: add missing ones, redo as templates
[mypaint:maxy-experimental.git] / gui / layerswindow.py
1 import pygtkcompat
2
3 import gtk
4 gdk = gtk.gdk
5 from gettext import gettext as _
6 import gobject
7 import pango
8
9 import dialogs
10 from lib.layer import COMPOSITE_OPS
11 from lib.helpers import escape
12
13 def stock_button(stock_id):
14     b = gtk.Button()
15     img = gtk.Image()
16     img.set_from_stock(stock_id, gtk.ICON_SIZE_MENU)
17     b.add(img)
18     return b
19
20 def action_button(action):
21     b = gtk.Button()
22     b.set_related_action(action)
23     if b.get_child() is not None:
24         b.remove(b.get_child())
25     img = action.create_icon(gtk.ICON_SIZE_MENU)
26     img.set_tooltip_text(action.get_tooltip())
27     b.add(img)
28     return b
29
30 def make_composite_op_model():
31     model = gtk.ListStore(str, str, str)
32     for name, display_name, description in COMPOSITE_OPS:
33         model.append([name, display_name, description])
34     return model
35
36
37 class ToolWidget (gtk.VBox):
38
39     stock_id = "mypaint-tool-layers"
40     tool_widget_title = _("Layers")
41     tooltip_format = _("<b>%s</b>\n%s")
42
43     def __init__(self, app):
44         gtk.VBox.__init__(self)
45         self.app = app
46         #self.set_size_request(200, 250)
47
48         # Layer treeview
49         # The 'object' column is a layer. All displayed columns use data from it.
50         store = self.liststore = gtk.ListStore(object)
51         store.connect("row-deleted", self.liststore_drag_row_deleted_cb)
52         view = self.treeview = gtk.TreeView(store)
53         view.connect("cursor-changed", self.treeview_cursor_changed_cb)
54         view.set_reorderable(True)
55         view.set_headers_visible(False)
56         view.connect("button-press-event", self.treeview_button_press_cb)
57         view_scroll = gtk.ScrolledWindow()
58         view_scroll.set_shadow_type(gtk.SHADOW_ETCHED_IN)
59         view_scroll.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
60         view_scroll.add(view)
61
62         renderer = gtk.CellRendererPixbuf()
63         col = self.visible_col = gtk.TreeViewColumn(_("Visible"))
64         col.pack_start(renderer, expand=False)
65         col.set_cell_data_func(renderer, self.layer_visible_datafunc)
66         view.append_column(col)
67
68         renderer = gtk.CellRendererPixbuf()
69         col = self.locked_col = gtk.TreeViewColumn(_("Locked"))
70         col.pack_start(renderer, expand=False)
71         col.set_cell_data_func(renderer, self.layer_locked_datafunc)
72         view.append_column(col)
73
74         renderer = gtk.CellRendererText()
75         col = self.name_col = gtk.TreeViewColumn(_("Name"))
76         col.pack_start(renderer, expand=True)
77         col.set_cell_data_func(renderer, self.layer_name_datafunc)
78         view.append_column(col)
79
80         # Common controls
81
82         common_table = gtk.Table()
83         row = 0
84
85         layer_mode_lbl = gtk.Label(_('Mode:'))
86         layer_mode_lbl.set_alignment(0, 0.5)
87         self.layer_mode_model = make_composite_op_model()
88         self.layer_mode_combo = gtk.ComboBox()
89         self.layer_mode_combo.set_model(self.layer_mode_model)
90         cell1 = gtk.CellRendererText()
91         self.layer_mode_combo.pack_start(cell1)
92         self.layer_mode_combo.add_attribute(cell1, "text", 1)
93         common_table.attach(layer_mode_lbl, 0, 1, row, row+1, gtk.FILL)
94         common_table.attach(self.layer_mode_combo, 1, 2, row, row+1, gtk.FILL|gtk.EXPAND)
95         row += 1
96
97         opacity_lbl = gtk.Label(_('Opacity:'))
98         opacity_lbl.set_alignment(0, 0.5)
99         adj = gtk.Adjustment(lower=0, upper=100, step_incr=1, page_incr=10)
100         self.opacity_scale = gtk.HScale(adj)
101         self.opacity_scale.set_value_pos(gtk.POS_RIGHT)
102         common_table.attach(opacity_lbl, 0, 1, row, row+1, gtk.FILL)
103         common_table.attach(self.opacity_scale, 1, 2, row, row+1, gtk.FILL|gtk.EXPAND)
104         row += 1
105
106         add_action = self.app.find_action("NewLayerFG")
107         move_up_action = self.app.find_action("RaiseLayerInStack")
108         move_down_action = self.app.find_action("LowerLayerInStack")
109         merge_down_action = self.app.find_action("MergeLayer")
110         del_action = self.app.find_action("RemoveLayer")
111         duplicate_action = self.app.find_action("DuplicateLayer")
112
113         add_button = self.add_button = action_button(add_action)
114         move_up_button = self.move_up_button = action_button(move_up_action)
115         move_down_button = self.move_down_button = action_button(move_down_action)
116         merge_down_button = self.merge_down_button = action_button(merge_down_action)
117         del_button = self.del_button = action_button(del_action)
118         duplicate_button = self.duplicate_button = action_button(duplicate_action)
119
120         buttons_hbox = gtk.HBox()
121         buttons_hbox.pack_start(add_button)
122         buttons_hbox.pack_start(move_up_button)
123         buttons_hbox.pack_start(move_down_button)
124         buttons_hbox.pack_start(duplicate_button)
125         buttons_hbox.pack_start(merge_down_button)
126         buttons_hbox.pack_start(del_button)
127
128         # Pack and add to toplevel
129         self.pack_start(view_scroll)
130         self.pack_start(buttons_hbox, expand=False)
131         self.pack_start(common_table, expand=False)
132
133         # Names for anonymous layers
134         # app.filehandler.file_opened_observers.append(self.init_anon_layer_names)
135         ## TODO: may need to reset them with the new system too
136
137         # Updates
138         doc = app.doc.model
139         doc.doc_observers.append(self.update)
140         self.opacity_scale.connect('value-changed', self.on_opacity_changed)
141         self.layer_mode_combo.connect('changed', self.on_layer_mode_changed)
142
143         self.is_updating = False
144         self.update(doc)
145
146
147     def update(self, doc):
148         if self.is_updating:
149             return
150         self.is_updating = True
151
152         # Update the liststore and the selection to match the master layers
153         # list in doc
154         current_layer = doc.get_current_layer()
155         self.treeview.get_selection().unselect_all()
156         liststore_layers = [row[0] for row in self.liststore]
157         liststore_layers.reverse()
158         if doc.layers != liststore_layers:
159             self.liststore.clear()
160             for layer in doc.layers:
161                 self.liststore.prepend([layer])
162         selected_path = (len(doc.layers) - (doc.layer_idx + 1), )
163
164         # Queue a selection update too...
165         gobject.idle_add(self.update_selection)
166
167         # Update the common widgets
168         self.opacity_scale.set_value(current_layer.opacity*100)
169         mode = current_layer.compositeop
170         def find_iter(model, path, iter, data):
171             md = model.get_value(iter, 0)
172             md_name = model.get_value(iter, 1)
173             md_desc = model.get_value(iter, 2)
174             if md == mode:
175                 self.layer_mode_combo.set_active_iter(iter)
176                 tooltip = self.tooltip_format % (
177                         escape(md_name), escape(md_desc))
178                 self.layer_mode_combo.set_tooltip_markup(tooltip)
179         self.layer_mode_model.foreach(find_iter, None)
180         self.is_updating = False
181
182
183     def update_selection(self):
184         doc = self.app.doc.model
185         # ... select_path() ust be queued with gobject.idle_add to avoid
186         # glitches in the update after dragging the current row downwards.
187         selected_path = (len(doc.layers) - (doc.layer_idx + 1), )
188         self.treeview.get_selection().select_path(selected_path)
189         self.treeview.scroll_to_cell(selected_path)
190
191         # Queue a redraw too - undoing/redoing lock and visible markers poke
192         # the underlying doc state, and we should always draw that.
193         self.treeview.queue_draw()
194
195
196     def treeview_cursor_changed_cb(self, treeview, *data):
197         if self.is_updating:
198             return
199         selection = treeview.get_selection()
200         if selection is None:
201             return
202         store, t_iter = selection.get_selected()
203         if t_iter is None:
204             return
205         layer = store.get_value(t_iter, 0)
206         doc = self.app.doc
207         if doc.model.get_current_layer() != layer:
208             idx = doc.model.layers.index(layer)
209             doc.model.select_layer(idx)
210             doc.layerblink_state.activate()
211
212
213     def treeview_button_press_cb(self, treeview, event):
214         x, y = int(event.x), int(event.y)
215         bw_x, bw_y = treeview.convert_widget_to_bin_window_coords(x, y)
216         path_info = treeview.get_path_at_pos(bw_x, bw_y)
217         if path_info is None:
218             return False
219         clicked_path, clicked_col, cell_x, cell_y = path_info
220         if pygtkcompat.USE_GTK3:
221             clicked_path = clicked_path.get_indices()
222         layer, = self.liststore[clicked_path[0]]
223         doc = self.app.doc.model
224         if clicked_col is self.visible_col:
225             doc.set_layer_visibility(not layer.visible, layer)
226             self.treeview.queue_draw()
227             return True
228         elif clicked_col is self.locked_col:
229             doc.set_layer_locked(not layer.locked, layer)
230             self.treeview.queue_draw()
231             return True
232         elif clicked_col is self.name_col:
233             if event.type == gdk._2BUTTON_PRESS:
234                 rename_action = self.app.find_action("RenameLayer")
235                 rename_action.activate()
236                 return True
237         return False
238
239
240     def liststore_drag_row_deleted_cb(self, liststore, path):
241         if self.is_updating:
242             return
243         # Must be internally generated
244         # The only way this can happen is at the end of a drag which reorders the list.
245         self.resync_doc_layers()
246
247
248     def resync_doc_layers(self):
249         assert not self.is_updating
250         new_order = [row[0] for row in self.liststore]
251         new_order.reverse()
252         doc = self.app.doc.model
253         if new_order != doc.layers:
254             doc.reorder_layers(new_order)
255         else:
256             doc.select_layer(doc.layer_idx)
257             # otherwise the current layer selection is visually lost
258
259
260     def on_opacity_changed(self, *ignore):
261         if self.is_updating:
262             return
263         self.is_updating = True
264         doc = self.app.doc.model
265         doc.set_layer_opacity(self.opacity_scale.get_value()/100.0)
266         self.is_updating = False
267
268
269     def on_layer_del(self, button):
270         doc = self.app.doc.model
271         doc.remove_layer(layer=doc.get_current_layer())
272
273
274     def layer_name_datafunc(self, column, renderer, model, tree_iter,
275                             *data_etc):
276         layer = model.get_value(tree_iter, 0)
277         path = model.get_path(tree_iter)
278         name = layer.name
279         attrs = pango.AttrList()
280         if not name:
281             layer_num = self.app.doc.get_number_for_nameless_layer(layer)
282             name = _(u"Untitled layer #%d") % layer_num
283             markup = "<small><i>%s</i></small> " % (escape(name),)
284             if pygtkcompat.USE_GTK3:
285                 parse_result = pango.parse_markup(markup, -1, '\000')
286                 parse_ok, attrs, name, accel_char = parse_result
287                 assert parse_ok
288             else:
289                 parse_result = pango.parse_markup(markup)
290                 attrs, name, accel_char = parse_result
291         renderer.set_property("attributes", attrs)
292         renderer.set_property("text", name)
293
294
295     def layer_visible_datafunc(self, column, renderer, model, tree_iter,
296                                *data_etc):
297         layer = model.get_value(tree_iter, 0)
298         if layer.visible:
299             pixbuf = self.app.pixmaps.eye_open
300         else:
301             pixbuf = self.app.pixmaps.eye_closed
302         renderer.set_property("pixbuf", pixbuf)
303
304
305     def layer_locked_datafunc(self, column, renderer, model, tree_iter,
306                               *data_etc):
307         layer = model.get_value(tree_iter, 0)
308         if layer.locked:
309             pixbuf = self.app.pixmaps.lock_closed
310         else:
311             pixbuf = self.app.pixmaps.lock_open
312         renderer.set_property("pixbuf", pixbuf)
313
314
315     def on_layer_mode_changed(self, *ignored):
316         if self.is_updating:
317             return
318         self.is_updating = True
319         doc = self.app.doc.model
320         i = self.layer_mode_combo.get_active_iter()
321         mode_name, display_name, desc = self.layer_mode_model.get(i, 0, 1, 2)
322         doc.set_layer_compositeop(mode_name)
323         tooltip = self.tooltip_format % (escape(display_name), escape(desc))
324         self.layer_mode_combo.set_tooltip_markup(tooltip)
325         self.is_updating = False