OSDN Git Service

colorize: add brushmodifier action & toolbar entry
[mypaint-anime/master.git] / gui / dialogs.py
1 # This file is part of MyPaint.
2 # Copyright (C) 2009 by Martin Renold <martinxyz@gmx.ch>
3 #
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 2 of the License, or
7 # (at your option) any later version.
8
9 import os
10 import gtk
11 from gtk import gdk
12 from gettext import gettext as _
13 from fnmatch import fnmatch
14 import brushmanager
15 from pixbuflist import PixbufList
16 import widgets
17 import spinbox
18
19 OVERWRITE_THIS = 1
20 OVERWRITE_ALL  = 2
21 DONT_OVERWRITE_THIS = 3
22 DONT_OVERWRITE_ANYTHING = 4
23 CANCEL = 5
24
25 def confirm(widget, question):
26     window = widget.get_toplevel()
27     d = gtk.MessageDialog(
28         window,
29         gtk.DIALOG_MODAL,
30         gtk.MESSAGE_QUESTION,
31         gtk.BUTTONS_NONE,
32         question)
33     d.add_button(gtk.STOCK_NO, gtk.RESPONSE_REJECT)
34     d.add_button(gtk.STOCK_YES, gtk.RESPONSE_ACCEPT)
35     d.set_default_response(gtk.RESPONSE_ACCEPT)
36     response = d.run()
37     d.destroy()
38     return response == gtk.RESPONSE_ACCEPT
39
40 def ask_for_name(widget, title, default):
41     window = widget.get_toplevel()
42     d = gtk.Dialog(title,
43                    window,
44                    gtk.DIALOG_MODAL,
45                    (gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT,
46                     gtk.STOCK_OK, gtk.RESPONSE_ACCEPT))
47     d.set_position(gtk.WIN_POS_MOUSE)
48
49     hbox = gtk.HBox()
50     hbox.set_property("spacing", widgets.SPACING)
51     hbox.set_border_width(widgets.SPACING)
52
53     d.vbox.pack_start(hbox)
54     hbox.pack_start(gtk.Label(_('Name')), False, False)
55
56     d.e = e = gtk.Entry()
57     e.set_size_request(250, -1)
58     e.set_text(default)
59     e.select_region(0, len(default))
60     def responseToDialog(entry, dialog, response):  
61         dialog.response(response)  
62     e.connect("activate", responseToDialog, d, gtk.RESPONSE_ACCEPT)  
63
64     hbox.pack_start(e, True, True)
65     d.vbox.show_all()
66     if d.run() == gtk.RESPONSE_ACCEPT:
67         result = d.e.get_text().decode('utf-8')
68     else:
69         result = None
70     d.destroy()
71     return result
72
73 def error(widget, message):
74     window = widget.get_toplevel()
75     d = gtk.MessageDialog(window, gtk.DIALOG_MODAL, gtk.MESSAGE_ERROR, gtk.BUTTONS_OK, message)
76     d.run()
77     d.destroy()
78
79 def image_new_from_png_data(data):
80     loader = gtk.gdk.PixbufLoader("png")
81     loader.write(data)
82     loader.close()
83     pixbuf = loader.get_pixbuf()
84     image = gtk.Image()
85     image.set_from_pixbuf(pixbuf)
86     return image
87
88 def confirm_rewrite_brush(window, brushname, existing_preview_pixbuf, imported_preview_data):
89     dialog = gtk.Dialog(_("Overwrite brush?"),
90                         window, gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT)
91
92     cancel         = gtk.Button(stock=gtk.STOCK_CANCEL)
93     cancel.show_all()
94     img_yes        = gtk.Image()
95     img_yes.set_from_stock(gtk.STOCK_YES, gtk.ICON_SIZE_BUTTON)
96     img_no         = gtk.Image()
97     img_no.set_from_stock(gtk.STOCK_NO, gtk.ICON_SIZE_BUTTON)
98     overwrite_this = gtk.Button(_("Replace"))
99     overwrite_this.set_image(img_yes)
100     overwrite_this.show_all()
101     skip_this      = gtk.Button(_("Rename"))
102     skip_this.set_image(img_no)
103     skip_this.show_all()
104     overwrite_all  = gtk.Button(_("Replace all"))
105     overwrite_all.show_all()
106     skip_all       = gtk.Button(_("Rename all"))
107     skip_all.show_all()
108
109     buttons = [(cancel,         CANCEL),
110                (skip_all,       DONT_OVERWRITE_ANYTHING),
111                (overwrite_all,  OVERWRITE_ALL),
112                (skip_this,      DONT_OVERWRITE_THIS),
113                (overwrite_this, OVERWRITE_THIS)]
114     for button, code in buttons:
115         dialog.add_action_widget(button, code)
116
117     hbox   = gtk.HBox()
118     vbox_l = gtk.VBox()
119     vbox_r = gtk.VBox()
120     preview_r = gtk.image_new_from_pixbuf(existing_preview_pixbuf)
121     label_l = gtk.Label(_("Imported brush"))
122     label_r = gtk.Label(_("Existing brush"))
123
124     question = gtk.Label(_("<b>A brush named `%s' already exists.</b>\nDo you want to replace it, or should the new brush be renamed?") % brushname)
125     question.set_use_markup(True)
126
127     preview_l = image_new_from_png_data(imported_preview_data)
128
129     vbox_l.pack_start(preview_l, expand=True)
130     vbox_l.pack_start(label_l, expand=False)
131
132     vbox_r.pack_start(preview_r, expand=True)
133     vbox_r.pack_start(label_r, expand=False)
134
135     hbox.pack_start(vbox_l, expand=False)
136     hbox.pack_start(question, expand=True)
137     hbox.pack_start(vbox_r, expand=False)
138     hbox.show_all()
139
140     dialog.vbox.pack_start(hbox)
141
142     answer = dialog.run()
143     dialog.destroy()
144     return answer
145
146 def confirm_rewrite_group(window, groupname, deleted_groupname):
147     dialog = gtk.Dialog(_("Overwrite brush group?"),
148                         window, gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT)
149
150     cancel         = gtk.Button(stock=gtk.STOCK_CANCEL)
151     cancel.show_all()
152     img_yes        = gtk.Image()
153     img_yes.set_from_stock(gtk.STOCK_YES, gtk.ICON_SIZE_BUTTON)
154     img_no         = gtk.Image()
155     img_no.set_from_stock(gtk.STOCK_NO, gtk.ICON_SIZE_BUTTON)
156     overwrite_this = gtk.Button(_("Replace"))
157     overwrite_this.set_image(img_yes)
158     overwrite_this.show_all()
159     skip_this      = gtk.Button(_("Rename"))
160     skip_this.set_image(img_no)
161     skip_this.show_all()
162
163     buttons = [(cancel,         CANCEL),
164                (skip_this,      DONT_OVERWRITE_THIS),
165                (overwrite_this, OVERWRITE_THIS)]
166     for button, code in buttons:
167         dialog.add_action_widget(button, code)
168
169     question = gtk.Label(_("<b>A group named `%s' already exists.</b>\nDo you want to replace it, or should the new group be renamed?\nIf you replace it, the brushes may be moved to a group called `%s'.") % (groupname, deleted_groupname))
170     question.set_use_markup(True)
171
172     dialog.vbox.pack_start(question)
173     dialog.vbox.show_all()
174
175     answer = dialog.run()
176     dialog.destroy()
177     return answer
178
179 def open_dialog(title, window, filters):
180     """
181     filters should be a list of tuples: (filter title, glob pattern).
182     Returns a tuple: (file format, filename).
183     Here "file format" is index of filter that matches filename (None if no matches).
184     filename is None if no file was selected.
185     """
186     dialog = gtk.FileChooserDialog(title, window,
187                                    gtk.FILE_CHOOSER_ACTION_OPEN,
188                                    (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL,
189                                     gtk.STOCK_OPEN, gtk.RESPONSE_OK))
190     dialog.set_default_response(gtk.RESPONSE_OK)
191     for filter_title, pattern in filters:
192         f = gtk.FileFilter()
193         f.set_name(filter_title)
194         f.add_pattern(pattern)
195         dialog.add_filter(f)
196
197     result = (None, None)
198     if dialog.run() == gtk.RESPONSE_OK:
199         filename = dialog.get_filename().decode('utf-8')
200         file_format = None
201         for i, (_, pattern) in enumerate(filters):
202             if fnmatch(filename, pattern):
203                 file_format = i
204                 break
205         result = (file_format, filename)
206     dialog.hide()
207     return result
208
209 def save_dialog(title, window, filters, default_format=None):
210     """
211     filters should be a list of tuples: (filter title, glob pattern).
212     default_format may be a pair (format id, suffix). That suffix will be added to filename if
213     it does not match any of filters.
214     Returns a tuple: (file format, filename).
215     Here "file format" is index of filter that matches filename (None if no matches).
216     filename is None if no file was selected.
217     """
218     dialog = gtk.FileChooserDialog(title, window,
219                                    gtk.FILE_CHOOSER_ACTION_SAVE,
220                                    (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL,
221                                     gtk.STOCK_SAVE, gtk.RESPONSE_OK))
222     dialog.set_default_response(gtk.RESPONSE_OK)
223     dialog.set_do_overwrite_confirmation(True)
224
225     for filter_title, pattern in filters:
226         f = gtk.FileFilter()
227         f.set_name(filter_title)
228         f.add_pattern(pattern)
229         dialog.add_filter(f)
230
231     result = (None, None)
232     while dialog.run() == gtk.RESPONSE_OK:
233         filename = dialog.get_filename().decode('utf-8')
234         file_format = None
235         for i, (_, pattern) in enumerate(filters):
236             if fnmatch(filename, pattern):
237                 file_format = i
238                 break
239         if file_format is None and default_format is not None:
240             file_format, suffix = default_format
241             filename += suffix
242             dialog.set_current_name(filename)
243             dialog.response(gtk.RESPONSE_OK)
244         else:
245             result = (file_format, filename)
246             break
247     dialog.hide()
248     return result
249
250 def confirm_brushpack_import(packname, window=None, readme=None):
251     def show_text(text):
252         tv = gtk.TextView()
253         tv.set_wrap_mode(gtk.WRAP_WORD)
254         tv.get_buffer().set_text(text)
255         return tv
256
257     dialog = gtk.Dialog(_("Import brush package?"),
258                        window,
259                        gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
260                        (gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT,
261                         gtk.STOCK_OK, gtk.RESPONSE_ACCEPT))
262
263     if readme:
264         #readme_label = gtk.Label(_("readme.txt") % packname)
265         #dialog.vbox.pack_start(readme_label)
266         readme_tv = show_text(readme)
267         dialog.vbox.pack_start(readme_tv)
268
269     question = gtk.Label(_("<b>Do you really want to import package `%s'?</b>") % packname)
270     question.set_use_markup(True)
271     dialog.vbox.pack_start(question)
272     dialog.vbox.show_all()
273     answer = dialog.run()
274     dialog.destroy()
275     return answer
276
277 def change_current_color_detailed(app):
278     """Presents a `gtk.ColorSelectionDialog` for updating the current colour.
279
280     The dialog isn't particularly simple, but allows colours to be entered as
281     hex strings or using spinners.
282     """
283     previous_hsv = app.ch.colors[-1]
284     current_hsv = app.brush.get_color_hsv()
285     dialog = gtk.ColorSelectionDialog(_("Color details"))
286     dialog.set_position(gtk.WIN_POS_MOUSE)
287     dialog.colorsel.set_previous_color(gdk.color_from_hsv(*previous_hsv))
288     dialog.colorsel.set_current_color(gdk.color_from_hsv(*current_hsv))
289     dialog.ok_button.grab_focus()
290     dialog.set_transient_for(app.drawWindow)
291     dialog.set_resizable(False)
292     dialog.set_modal(True)
293     if dialog.run() == gtk.RESPONSE_OK:
294         col = dialog.colorsel.get_current_color()
295         hsv = (col.hue, col.saturation, col.value)
296         app.brush.set_color_hsv(hsv)
297     dialog.destroy()
298     return app.brush.get_color_hsv()
299
300
301 class QuickBrushChooser (gtk.VBox):
302     PREFS_KEY = 'widgets.brush_chooser.selected_group'
303     ICON_SIZE = 48
304
305     class _BrushList (PixbufList):
306         def __init__(self, chooser, brushes):
307             s = QuickBrushChooser.ICON_SIZE
308             PixbufList.__init__(self, brushes, s, s,
309                                 namefunc = lambda x: x.name,
310                                 pixbuffunc = lambda x: x.preview)
311             self.chooser = chooser
312
313         def on_select(self, brush):
314             self.chooser.on_select(brush)
315
316     def __init__(self, app, on_select):
317         gtk.VBox.__init__(self)
318         self.app = app
319         self.on_select = on_select
320         self.bm = app.brushmanager
321
322         active_group_name = app.preferences.get(self.PREFS_KEY, None)
323
324         model = self._make_groups_sb_model()
325         self.groups_sb = spinbox.ItemSpinBox(model, self.on_groups_sb_changed,
326                                              active_group_name)
327         active_group_name = self.groups_sb.get_value()
328
329         brushes = self.bm.groups[active_group_name][:]
330         self.brushlist = self._BrushList(self, brushes)
331         self.brushlist.dragging_allowed = False
332
333         scrolledwin = gtk.ScrolledWindow()
334         scrolledwin.set_policy(gtk.POLICY_NEVER, gtk.POLICY_ALWAYS)
335         scrolledwin.add_with_viewport(self.brushlist)
336         self.connect("style-set", self._on_style_set, scrolledwin)
337         icon_size = self.ICON_SIZE
338         w = icon_size * 5
339         h = icon_size * 5
340         scrolledwin.set_size_request(-1, h)
341         self.brushlist.set_size_request(w, -1)
342         scrolledwin.get_child().set_size_request(w, -1)
343
344
345
346         self.pack_start(self.groups_sb, False, False)
347         self.pack_start(scrolledwin, True, True)
348         self.set_spacing(widgets.SPACING_TIGHT)
349
350     def _on_style_set(self, widget, event, sw):
351         #vp = sw.get_child()
352         bl_style = self.brushlist.get_style()
353         #vp.modify_bg(gtk.STATE_NORMAL, bl_style.base[gtk.STATE_NORMAL])
354         #sw.modify_bg(gtk.STATE_NORMAL, bl_style.base[gtk.STATE_NORMAL])
355         self.brushlist.modify_bg(gtk.STATE_NORMAL, bl_style.base[gtk.STATE_NORMAL])
356         icon_size = self.ICON_SIZE
357
358     def _make_groups_sb_model(self):
359         group_names = self.bm.groups.keys()
360         group_names.sort()
361         model = []
362         for name in group_names:
363             label_text = brushmanager.translate_group_name(name)
364             model.append((name, label_text))
365         return model
366
367     def update_groups_sb(self):
368         model = self._make_groups_sb_model()
369         self.groups_sb.set_model(model)
370
371     def on_groups_sb_changed(self, group_name):
372         self.app.preferences[self.PREFS_KEY] = group_name
373         self.brushlist.itemlist[:] = self.bm.groups[group_name][:]
374         self.brushlist.update()
375
376
377 class BrushChooserDialog (gtk.Dialog):
378     """Speedy brush chooser dialog.
379     """
380
381     def __init__(self, app):
382         title = _("Change Brush")
383         parent = app.drawWindow
384         flags = gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT
385         buttons = (gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT)
386         gtk.Dialog.__init__(self, title, parent, flags, buttons)
387         self.set_position(gtk.WIN_POS_MOUSE)
388         self.app = app
389         self.response_brush = None
390         def on_select(brush):
391             self.response_brush = brush
392             self.response(gtk.RESPONSE_ACCEPT)
393         self.chooser = QuickBrushChooser(app, on_select)
394         vbox = self.get_content_area()
395         vbox.pack_start(self.chooser, True, True)
396         for w in vbox:
397             w.show_all()
398
399 def change_current_brush_quick(app):
400     dialog = BrushChooserDialog(app)
401     result = dialog.run()
402     if result == gtk.RESPONSE_ACCEPT:
403         app.brushmanager.select_brush(dialog.response_brush)
404     dialog.destroy()
405