3 from gettext import gettext as _
10 def stock_button(stock_id):
13 img.set_from_stock(stock_id, gtk.ICON_SIZE_MENU)
18 class ToolWidget (gtk.VBox):
20 stock_id = stock.TOOL_LAYERS
22 def __init__(self, app):
23 gtk.VBox.__init__(self)
25 #self.set_size_request(200, 250)
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)
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)
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)
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)
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)
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)
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)
80 img.set_from_pixbuf(self.app.pixmaps.layer_duplicate)
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
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)
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)
97 merge_down_button.set_tooltip_text(_('Merge Down'))
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)
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)
112 # Names for anonymous layers
113 self.init_anon_layer_names()
114 app.filehandler.file_opened_observers.append(self.init_anon_layer_names)
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)
122 self.is_updating = False
126 def update(self, doc):
129 self.is_updating = True
131 # Update the liststore and the selection to match the master layers
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), )
143 # Queue a selection update too...
144 gobject.idle_add(self.update_selection)
146 # Update the common widgets
147 self.opacity_scale.set_value(current_layer.opacity*100)
148 mode = current_layer.compositeop
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)
156 layer_mode_combo.set_active_iter(iter)
158 model.foreach(find_iter, None)
159 self.is_updating = False
162 def update_selection(self):
163 doc = self.app.doc.model
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)
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)
180 def treeview_cursor_changed_cb(self, treeview, *data):
183 store, t_iter = treeview.get_selection().get_selected()
186 layer = store.get_value(t_iter, 0)
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()
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:
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()
207 elif clicked_col is self.locked_col:
208 doc.set_layer_locked(not layer.locked, layer)
209 self.treeview.queue_draw()
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)
215 layer.name = new_name
216 self.treeview.queue_draw()
221 def liststore_drag_row_deleted_cb(self, liststore, path):
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()
229 def resync_doc_layers(self):
230 assert not self.is_updating
231 new_order = [row[0] for row in self.liststore]
233 doc = self.app.doc.model
234 if new_order != doc.layers:
235 doc.reorder_layers(new_order)
237 doc.select_layer(doc.layer_idx)
238 # otherwise the current layer selection is visually lost
241 def on_opacity_changed(self, *ignore):
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
250 def move_layer(self, widget, action):
251 doc = self.app.doc.model
252 current_layer_pos = doc.layer_idx
254 new_layer_pos = current_layer_pos + 1
255 elif action == 'down':
256 new_layer_pos = current_layer_pos - 1
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)
262 def duplicate_layer(self, widget):
263 doc = self.app.doc.model
264 layer = doc.layers[doc.layer_idx]
266 copy_of = _("Copy of ")
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)
274 def merge_layer_down(self, widget):
275 self.app.doc.model.merge_layer_down()
278 def on_layer_add(self, button):
279 doc = self.app.doc.model
280 doc.add_layer(after=doc.get_current_layer())
283 def on_layer_del(self, button):
284 doc = self.app.doc.model
285 doc.remove_layer(layer=doc.get_current_layer())
288 def init_anon_layer_names(self, *a):
289 self.anon_layer_num = {}
290 self.anon_layer_next = 1
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)
297 attrs = pango.AttrList()
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:
306 n = self.anon_layer_num.get(id(l), 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)
319 def layer_visible_datafunc(self, column, renderer, model, tree_iter):
320 layer = model.get_value(tree_iter, 0)
322 pixbuf = self.app.pixmaps.eye_open
324 pixbuf = self.app.pixmaps.eye_closed
325 renderer.set_property("pixbuf", pixbuf)
328 def layer_locked_datafunc(self, column, renderer, model, tree_iter):
329 layer = model.get_value(tree_iter, 0)
331 pixbuf = self.app.pixmaps.lock_closed
333 pixbuf = self.app.pixmaps.lock_open
334 renderer.set_property("pixbuf", pixbuf)
337 def on_layer_mode_changed(self,*ignored):
340 self.is_updating = True
341 doc = self.app.doc.model
342 mode = self.layer_mode.get_active_text()
345 print ('Change layer mode to : %s' % mode)
346 doc.set_layer_compositeop(mode)
347 self.is_updating = False