OSDN Git Service

layer composite ops: initial implementation
[mypaint-anime/master.git] / gui / layerswindow.py
1 import gtk
2 gdk = gtk.gdk
3 from gettext import gettext as _
4 import gobject
5 import pango
6
7 import dialogs
8 import stock
9
10 def stock_button(stock_id):
11     b = gtk.Button()
12     img = gtk.Image()
13     img.set_from_stock(stock_id, gtk.ICON_SIZE_MENU)
14     b.add(img)
15     return b
16
17
18 class ToolWidget (gtk.VBox):
19
20     stock_id = stock.TOOL_LAYERS
21
22     def __init__(self, app):
23         gtk.VBox.__init__(self)
24         self.app = app
25         #self.set_size_request(200, 250)
26
27         # Layer treeview
28         # The 'object' column is a layer. All displayed columns use data from it.
29         store = self.liststore = gtk.ListStore(object)
30         store.connect("row-deleted", self.liststore_drag_row_deleted_cb)
31         view = self.treeview = gtk.TreeView(store)
32         view.connect("cursor-changed", self.treeview_cursor_changed_cb)
33         view.set_reorderable(True)
34         view.set_headers_visible(False)
35         view.connect("button-press-event", self.treeview_button_press_cb)
36         view_scroll = gtk.ScrolledWindow()
37         view_scroll.set_shadow_type(gtk.SHADOW_ETCHED_IN)
38         view_scroll.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
39         view_scroll.add(view)
40
41         renderer = gtk.CellRendererPixbuf()
42         col = self.visible_col = gtk.TreeViewColumn(_("Visible"))
43         col.pack_start(renderer, expand=False)
44         col.set_cell_data_func(renderer, self.layer_visible_datafunc)
45         view.append_column(col)
46
47         renderer = gtk.CellRendererPixbuf()
48         col = self.locked_col = gtk.TreeViewColumn(_("Locked"))
49         col.pack_start(renderer, expand=False)
50         col.set_cell_data_func(renderer, self.layer_locked_datafunc)
51         view.append_column(col)
52
53         renderer = gtk.CellRendererText()
54         col = self.name_col = gtk.TreeViewColumn(_("Name"))
55         col.pack_start(renderer, expand=True)
56         col.set_cell_data_func(renderer, self.layer_name_datafunc)
57         view.append_column(col)
58
59         # Common controls
60         self.layer_mode = gtk.combo_box_new_text()
61         # FIXME: layer modes must be managed in some module or class, and should not be written hard coded.
62         for t in ["normal", "multiply", "screen", "dodge", "burn"]:
63             self.layer_mode.append_text(t)
64         
65         adj = gtk.Adjustment(lower=0, upper=100, step_incr=1, page_incr=10)
66         self.opacity_scale = gtk.HScale(adj)
67         self.opacity_scale.set_value_pos(gtk.POS_RIGHT)
68         opacity_lbl = gtk.Label(_('Opacity:'))
69         opacity_hbox = gtk.HBox()
70         opacity_hbox.pack_start(self.layer_mode, expand=False)
71         opacity_hbox.pack_start(opacity_lbl, expand=False)
72         opacity_hbox.pack_start(self.opacity_scale, expand=True)
73
74         add_button = self.add_button = stock_button(gtk.STOCK_ADD)
75         move_up_button = self.move_up_button = stock_button(gtk.STOCK_GO_UP)
76         move_down_button = self.move_down_button = stock_button(gtk.STOCK_GO_DOWN)
77
78         b = gtk.Button()
79         img = gtk.Image()
80         img.set_from_pixbuf(self.app.pixmaps.layer_duplicate)
81         b.add(img)
82         tooltip = gtk.Tooltips()
83         tooltip.set_tip(b, _("Create a duplicate of this channel and add it to the image"))
84         duplicate_button = self.duplicate_button = b
85
86
87         merge_down_button = self.merge_down_button = stock_button(gtk.STOCK_DND_MULTIPLE)  # XXX need a better one
88         del_button = self.del_button = stock_button(gtk.STOCK_DELETE)
89
90         add_button.connect('clicked', self.on_layer_add)
91         move_up_button.connect('clicked', self.move_layer, 'up')
92         move_down_button.connect('clicked', self.move_layer, 'down')
93         duplicate_button.connect('clicked', self.duplicate_layer)
94         merge_down_button.connect('clicked', self.merge_layer_down)
95         del_button.connect('clicked', self.on_layer_del)
96
97         merge_down_button.set_tooltip_text(_('Merge Down'))
98
99         buttons_hbox = gtk.HBox()
100         buttons_hbox.pack_start(add_button)
101         buttons_hbox.pack_start(move_up_button)
102         buttons_hbox.pack_start(move_down_button)
103         buttons_hbox.pack_start(duplicate_button)
104         buttons_hbox.pack_start(merge_down_button)
105         buttons_hbox.pack_start(del_button)
106
107         # Pack and add to toplevel
108         self.pack_start(view_scroll)
109         self.pack_start(buttons_hbox, expand=False)
110         self.pack_start(opacity_hbox, expand=False)
111
112         # Names for anonymous layers
113         self.init_anon_layer_names()
114         app.filehandler.file_opened_observers.append(self.init_anon_layer_names)
115
116         # Updates
117         doc = app.doc.model
118         doc.doc_observers.append(self.update)
119         self.opacity_scale.connect('value-changed', self.on_opacity_changed)
120         self.layer_mode.connect('changed', self.on_layer_mode_changed)
121
122         self.is_updating = False
123         self.update(doc)
124
125
126     def update(self, doc):
127         if self.is_updating:
128             return
129         self.is_updating = True
130
131         # Update the liststore and the selection to match the master layers
132         # list in doc
133         current_layer = doc.get_current_layer()
134         self.treeview.get_selection().unselect_all()
135         liststore_layers = [row[0] for row in self.liststore]
136         liststore_layers.reverse()
137         if doc.layers != liststore_layers:
138             self.liststore.clear()
139             for layer in doc.layers:
140                 self.liststore.prepend([layer])
141         selected_path = (len(doc.layers) - (doc.layer_idx + 1), )
142
143         # Queue a selection update too...
144         gobject.idle_add(self.update_selection)
145
146         # Update the common widgets
147         self.opacity_scale.set_value(current_layer.opacity*100)
148         mode  = current_layer.compositeop
149         if mode == "over":
150             mode = "normal"
151         layer_mode_combo = self.layer_mode
152         model = layer_mode_combo.get_model()
153         def find_iter(model, path, iter, data):
154             value = model.get_value(iter, 0)
155             if value == mode:
156                 layer_mode_combo.set_active_iter(iter)
157 #        self.layer_mode.
158         model.foreach(find_iter, None)
159         self.is_updating = False
160
161
162     def update_selection(self):
163         doc = self.app.doc.model
164
165         # ... select_path() ust be queued with gobject.idle_add to avoid
166         # glitches in the update after dragging the current row downwards.
167         selected_path = (len(doc.layers) - (doc.layer_idx + 1), )
168         self.treeview.get_selection().select_path(selected_path)
169         self.treeview.scroll_to_cell(selected_path)
170
171         ## Reflect position of current layer in the list
172         sel_is_top = sel_is_bottom = False
173         sel_is_bottom = doc.layer_idx == 0
174         sel_is_top = doc.layer_idx == len(doc.layers)-1
175         self.move_up_button.set_sensitive(not sel_is_top)
176         self.move_down_button.set_sensitive(not sel_is_bottom)
177         self.merge_down_button.set_sensitive(not sel_is_bottom)
178
179
180     def treeview_cursor_changed_cb(self, treeview, *data):
181         if self.is_updating:
182             return
183         store, t_iter = treeview.get_selection().get_selected()
184         if t_iter is None:
185             return
186         layer = store.get_value(t_iter, 0)
187         doc = self.app.doc
188         if doc.model.get_current_layer() != layer:
189             idx = doc.model.layers.index(layer)
190             doc.model.select_layer(idx)
191             doc.layerblink_state.activate()
192
193
194     def treeview_button_press_cb(self, treeview, event):
195         x, y = int(event.x), int(event.y)
196         bw_x, bw_y = treeview.convert_widget_to_bin_window_coords(x, y)
197         path_info = treeview.get_path_at_pos(bw_x, bw_y)
198         if path_info is None:
199             return False
200         clicked_path, clicked_col, cell_x, cell_y = path_info
201         layer, = self.liststore[clicked_path[0]]
202         doc = self.app.doc.model
203         if clicked_col is self.visible_col:
204             doc.set_layer_visibility(not layer.visible, layer)
205             self.treeview.queue_draw()
206             return True
207         elif clicked_col is self.locked_col:
208             doc.set_layer_locked(not layer.locked, layer)
209             self.treeview.queue_draw()
210             return True
211         elif clicked_col is self.name_col:
212             if event.type == gdk._2BUTTON_PRESS:
213                 new_name = dialogs.ask_for_name(self, _("Name"), layer.name)
214                 if new_name:
215                     layer.name = new_name
216                     self.treeview.queue_draw()
217                 return True
218         return False
219
220
221     def liststore_drag_row_deleted_cb(self, liststore, path):
222         if self.is_updating:
223             return
224         # Must be internally generated
225         # The only way this can happen is at the end of a drag which reorders the list.
226         self.resync_doc_layers()
227
228
229     def resync_doc_layers(self):
230         assert not self.is_updating
231         new_order = [row[0] for row in self.liststore]
232         new_order.reverse()
233         doc = self.app.doc.model
234         if new_order != doc.layers:
235             doc.reorder_layers(new_order)
236         else:
237             doc.select_layer(doc.layer_idx)
238             # otherwise the current layer selection is visually lost
239
240
241     def on_opacity_changed(self, *ignore):
242         if self.is_updating:
243             return
244         self.is_updating = True
245         doc = self.app.doc.model
246         doc.set_layer_opacity(self.opacity_scale.get_value()/100.0)
247         self.is_updating = False
248
249
250     def move_layer(self, widget, action):
251         doc = self.app.doc.model
252         current_layer_pos = doc.layer_idx
253         if action == 'up':
254             new_layer_pos = current_layer_pos + 1
255         elif action == 'down':
256             new_layer_pos = current_layer_pos - 1
257         else:
258             return
259         if new_layer_pos < len(doc.layers) and new_layer_pos >= 0:
260             doc.move_layer(current_layer_pos, new_layer_pos, select_new=True)
261
262     def duplicate_layer(self, widget):
263         doc = self.app.doc.model
264         layer = doc.layers[doc.layer_idx]
265         name = layer.name
266         copy_of = _("Copy of ")
267         if name:
268             name = copy_of+name
269         else:
270             layer_num = self.anon_layer_num.get(id(layer), None)
271             name = copy_of+_("Untitled layer #%d") % layer_num
272         doc.duplicate_layer(doc.layer_idx, name)
273
274     def merge_layer_down(self, widget):
275         self.app.doc.model.merge_layer_down()
276
277
278     def on_layer_add(self, button):
279         doc = self.app.doc.model
280         doc.add_layer(after=doc.get_current_layer())
281
282
283     def on_layer_del(self, button):
284         doc = self.app.doc.model
285         doc.remove_layer(layer=doc.get_current_layer())
286
287
288     def init_anon_layer_names(self, *a):
289         self.anon_layer_num = {}
290         self.anon_layer_next = 1
291
292
293     def layer_name_datafunc(self, column, renderer, model, tree_iter):
294         layer = model.get_value(tree_iter, 0)
295         path = model.get_path(tree_iter)
296         name = layer.name
297         attrs = pango.AttrList()
298         if not name:
299             layer_num = self.anon_layer_num.get(id(layer), None)
300             if layer_num is None:
301                 # Name every layer without a name or an entry in the cache now,
302                 # in document order (not list order, which is reversed)
303                 for l in self.app.doc.model.layers:
304                     if l.name:
305                         continue
306                     n = self.anon_layer_num.get(id(l), None)
307                     if n is None:
308                         n = self.anon_layer_next
309                         self.anon_layer_next += 1
310                         self.anon_layer_num[id(l)] = n
311                 layer_num = self.anon_layer_num[id(layer)]
312             attrs.change(pango.AttrScale(pango.SCALE_SMALL, 0, -1))
313             attrs.change(pango.AttrStyle(pango.STYLE_ITALIC, 0, -1))
314             name = _("Untitled layer #%d") % layer_num
315         renderer.set_property("attributes", attrs)
316         renderer.set_property("text", name)
317
318
319     def layer_visible_datafunc(self, column, renderer, model, tree_iter):
320         layer = model.get_value(tree_iter, 0)
321         if layer.visible:
322             pixbuf = self.app.pixmaps.eye_open
323         else:
324             pixbuf = self.app.pixmaps.eye_closed
325         renderer.set_property("pixbuf", pixbuf)
326
327
328     def layer_locked_datafunc(self, column, renderer, model, tree_iter):
329         layer = model.get_value(tree_iter, 0)
330         if layer.locked:
331             pixbuf = self.app.pixmaps.lock_closed
332         else:
333             pixbuf = self.app.pixmaps.lock_open
334         renderer.set_property("pixbuf", pixbuf)
335
336
337     def on_layer_mode_changed(self,*ignored):
338         if self.is_updating:
339             return
340         self.is_updating = True
341         doc = self.app.doc.model
342         mode = self.layer_mode.get_active_text()
343         if mode == "normal":
344             mode = "over"
345         print ('Change layer mode to : %s' % mode)
346         doc.set_layer_compositeop(mode)
347         self.is_updating = False
348
349