1# This file is part of MyPaint.
2# -*- coding: utf-8 -*-
3# Copyright (C) 2010-2018 by the MyPaint Development Team.
4# Copyright (C) 2009-2013 by Martin Renold <martinxyz@gmx.ch>
5#
6# This program is free software; you can redistribute it and/or modify
7# it under the terms of the GNU General Public License as published by
8# the Free Software Foundation; either version 2 of the License, or
9# (at your option) any later version.
10
11"""Common dialog functions"""
12
13## Imports
14from __future__ import division, print_function
15
16from lib.gibindings import Gtk
17from lib.gibindings import Gdk
18from lib.gibindings import GdkPixbuf
19
20from gettext import gettext as _
21from fnmatch import fnmatch
22
23from . import widgets
24from lib.color import RGBColor
25from . import uicolor
26
27
28## Module constants
29
30OVERWRITE_THIS = 1
31OVERWRITE_ALL = 2
32DONT_OVERWRITE_THIS = 3
33DONT_OVERWRITE_ANYTHING = 4
34CANCEL = 5
35
36
37## Function defs
38
39def confirm(widget, question):
40    window = widget.get_toplevel()
41    d = Gtk.MessageDialog(
42        window,
43        Gtk.DialogFlags.MODAL,
44        Gtk.MessageType.QUESTION,
45        Gtk.ButtonsType.NONE,
46        question)
47    d.add_button(Gtk.STOCK_NO, Gtk.ResponseType.REJECT)
48    d.add_button(Gtk.STOCK_YES, Gtk.ResponseType.ACCEPT)
49    d.set_default_response(Gtk.ResponseType.ACCEPT)
50    response = d.run()
51    d.destroy()
52    return response == Gtk.ResponseType.ACCEPT
53
54
55def _entry_activate_dialog_response_cb(entry, dialog,
56                                       response=Gtk.ResponseType.ACCEPT):
57    dialog.response(response)
58
59
60def ask_for_name(widget, title, default):
61    window = widget.get_toplevel()
62    d = Gtk.Dialog(title,
63                   window,
64                   Gtk.DialogFlags.MODAL,
65                   (Gtk.STOCK_CANCEL, Gtk.ResponseType.REJECT,
66                    Gtk.STOCK_OK, Gtk.ResponseType.ACCEPT))
67    d.set_position(Gtk.WindowPosition.CENTER_ON_PARENT)
68
69    hbox = Gtk.HBox()
70    hbox.set_property("spacing", widgets.SPACING)
71    hbox.set_border_width(widgets.SPACING)
72
73    d.vbox.pack_start(hbox, True, True, 0)
74    hbox.pack_start(Gtk.Label(label=_('Name')), False, False, 0)
75
76    if default is None:
77        default = ""
78
79    d.e = e = Gtk.Entry()
80    e.set_size_request(250, -1)
81    e.set_text(default)
82    e.select_region(0, len(default))
83    e.set_input_hints(Gtk.InputHints.UPPERCASE_WORDS)
84    e.set_input_purpose(Gtk.InputPurpose.FREE_FORM)
85
86    e.connect("activate", _entry_activate_dialog_response_cb, d)
87
88    hbox.pack_start(e, True, True, 0)
89    d.vbox.show_all()
90    if d.run() == Gtk.ResponseType.ACCEPT:
91        result = d.e.get_text()
92        if isinstance(result, bytes):
93            result = result.decode('utf-8')
94    else:
95        result = None
96    d.destroy()
97    return result
98
99
100def error(widget, message):
101    window = widget.get_toplevel()
102    d = Gtk.MessageDialog(
103        window,
104        Gtk.DialogFlags.MODAL,
105        Gtk.MessageType.ERROR,
106        Gtk.ButtonsType.OK,
107        message,
108    )
109    d.run()
110    d.destroy()
111
112
113def image_new_from_png_data(data):
114    loader = GdkPixbuf.PixbufLoader.new_with_type("png")
115    loader.write(data)
116    loader.close()
117    pixbuf = loader.get_pixbuf()
118    image = Gtk.Image()
119    image.set_from_pixbuf(pixbuf)
120    return image
121
122
123def confirm_rewrite_brush(window, brushname, existing_preview_pixbuf,
124                          imported_preview_data):
125    flags = Gtk.DialogFlags.MODAL | Gtk.DialogFlags.DESTROY_WITH_PARENT
126    dialog = Gtk.Dialog(_("Overwrite brush?"), window, flags)
127
128    cancel = Gtk.Button(stock=Gtk.STOCK_CANCEL)
129    cancel.show_all()
130    img_yes = Gtk.Image()
131    img_yes.set_from_stock(Gtk.STOCK_YES, Gtk.IconSize.BUTTON)
132    img_no = Gtk.Image()
133    img_no.set_from_stock(Gtk.STOCK_NO, Gtk.IconSize.BUTTON)
134    overwrite_this = Gtk.Button(label=_("Replace"))
135    overwrite_this.set_image(img_yes)
136    overwrite_this.show_all()
137    skip_this = Gtk.Button(label=_("Rename"))
138    skip_this.set_image(img_no)
139    skip_this.show_all()
140    overwrite_all = Gtk.Button(label=_("Replace all"))
141    overwrite_all.show_all()
142    skip_all = Gtk.Button(label=_("Rename all"))
143    skip_all.show_all()
144
145    buttons = [
146        (cancel, CANCEL),
147        (skip_all, DONT_OVERWRITE_ANYTHING),
148        (overwrite_all, OVERWRITE_ALL),
149        (skip_this, DONT_OVERWRITE_THIS),
150        (overwrite_this, OVERWRITE_THIS),
151    ]
152    for button, code in buttons:
153        dialog.add_action_widget(button, code)
154
155    hbox = Gtk.HBox()
156    vbox_l = Gtk.VBox()
157    vbox_r = Gtk.VBox()
158    try:
159        preview_r = Gtk.image_new_from_pixbuf(existing_preview_pixbuf)
160    except AttributeError:
161        preview_r = Gtk.Image.new_from_pixbuf(existing_preview_pixbuf)
162    label_l = Gtk.Label(label=_("Imported brush"))
163    label_r = Gtk.Label(label=_("Existing brush"))
164
165    question = Gtk.Label(label=_(
166        u"<b>A brush named “{brush_name}” already exists.</b>\n"
167        u"Do you want to replace it, "
168        u"or should the new brush be renamed?"
169    ).format(
170        brush_name = brushname,
171    ))
172    question.set_use_markup(True)
173
174    preview_l = image_new_from_png_data(imported_preview_data)
175
176    vbox_l.pack_start(preview_l, True, True, 0)
177    vbox_l.pack_start(label_l, False, True, 0)
178
179    vbox_r.pack_start(preview_r, True, True, 0)
180    vbox_r.pack_start(label_r, False, True, 0)
181
182    hbox.pack_start(vbox_l, False, True, 0)
183    hbox.pack_start(question, True, True, 0)
184    hbox.pack_start(vbox_r, False, True, 0)
185    hbox.show_all()
186
187    dialog.vbox.pack_start(hbox, True, True, 0)
188
189    answer = dialog.run()
190    dialog.destroy()
191    return answer
192
193
194def confirm_rewrite_group(window, groupname, deleted_groupname):
195    flags = Gtk.DialogFlags.MODAL | Gtk.DialogFlags.DESTROY_WITH_PARENT
196    dialog = Gtk.Dialog(_("Overwrite brush group?"), window, flags)
197
198    cancel = Gtk.Button(stock=Gtk.STOCK_CANCEL)
199    cancel.show_all()
200    img_yes = Gtk.Image()
201    img_yes.set_from_stock(Gtk.STOCK_YES, Gtk.IconSize.BUTTON)
202    img_no = Gtk.Image()
203    img_no.set_from_stock(Gtk.STOCK_NO, Gtk.IconSize.BUTTON)
204    overwrite_this = Gtk.Button(label=_("Replace"))
205    overwrite_this.set_image(img_yes)
206    overwrite_this.show_all()
207    skip_this = Gtk.Button(label=_("Rename"))
208    skip_this.set_image(img_no)
209    skip_this.show_all()
210
211    buttons = [
212        (cancel, CANCEL),
213        (skip_this, DONT_OVERWRITE_THIS),
214        (overwrite_this, OVERWRITE_THIS),
215    ]
216    for button, code in buttons:
217        dialog.add_action_widget(button, code)
218
219    question = Gtk.Label(label=_(
220        u"<b>A group named “{groupname}” already exists.</b>\n"
221        u"Do you want to replace it, or should the new group be renamed?\n"
222        u"If you replace it, the brushes may be moved to a group called"
223        u" “{deleted_groupname}”."
224    ).format(
225        groupname=groupname,
226        deleted_groupname=deleted_groupname,
227    ))
228    question.set_use_markup(True)
229
230    dialog.vbox.pack_start(question, True, True, 0)
231    dialog.vbox.show_all()
232
233    answer = dialog.run()
234    dialog.destroy()
235    return answer
236
237
238def open_dialog(title, window, filters):
239    """Show a file chooser dialog.
240
241    Filters should be a list of tuples: (filtertitle, globpattern).
242
243    Returns a tuple of the form (fileformat, filename). Here
244    "fileformat" is the index of the filter that matched filename, or
245    None if there were no matches).  "filename" is None if no file was
246    selected.
247
248    """
249    dialog = Gtk.FileChooserDialog(title, window,
250                                   Gtk.FileChooserAction.OPEN,
251                                   (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
252                                    Gtk.STOCK_OPEN, Gtk.ResponseType.OK))
253    dialog.set_default_response(Gtk.ResponseType.OK)
254    for filter_title, pattern in filters:
255        f = Gtk.FileFilter()
256        f.set_name(filter_title)
257        f.add_pattern(pattern)
258        dialog.add_filter(f)
259
260    result = (None, None)
261    if dialog.run() == Gtk.ResponseType.OK:
262        filename = dialog.get_filename()
263        if isinstance(filename, bytes):
264            filename = filename.decode('utf-8')
265        file_format = None
266        for i, (_junk, pattern) in enumerate(filters):
267            if fnmatch(filename, pattern):
268                file_format = i
269                break
270        result = (file_format, filename)
271    dialog.hide()
272    return result
273
274
275def save_dialog(title, window, filters, default_format=None):
276    """Shows a file save dialog.
277
278    "filters" should be a list of tuples: (filter title, glob pattern).
279
280    "default_format" may be a pair (format id, suffix).
281    That suffix will be added to filename if it does not match any of filters.
282
283    Returns a tuple of the form (fileformat, filename).  Here
284    "fileformat" is index of filter that matches filename, or None if no
285    matches).  "filename" is None if no file was selected.
286
287    """
288    dialog = Gtk.FileChooserDialog(title, window,
289                                   Gtk.FileChooserAction.SAVE,
290                                   (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
291                                    Gtk.STOCK_SAVE, Gtk.ResponseType.OK))
292    dialog.set_default_response(Gtk.ResponseType.OK)
293    dialog.set_do_overwrite_confirmation(True)
294
295    for filter_title, pattern in filters:
296        f = Gtk.FileFilter()
297        f.set_name(filter_title)
298        f.add_pattern(pattern)
299        dialog.add_filter(f)
300
301    result = (None, None)
302    while dialog.run() == Gtk.ResponseType.OK:
303        filename = dialog.get_filename()
304        if isinstance(filename, bytes):
305            filename = filename.decode('utf-8')
306        file_format = None
307        for i, (_junk, pattern) in enumerate(filters):
308            if fnmatch(filename, pattern):
309                file_format = i
310                break
311        if file_format is None and default_format is not None:
312            file_format, suffix = default_format
313            filename += suffix
314            dialog.set_current_name(filename)
315            dialog.response(Gtk.ResponseType.OK)
316        else:
317            result = (file_format, filename)
318            break
319    dialog.hide()
320    return result
321
322
323def confirm_brushpack_import(packname, window=None, readme=None):
324
325    dialog = Gtk.Dialog(
326        _("Import brush package?"),
327        window,
328        Gtk.DialogFlags.MODAL | Gtk.DialogFlags.DESTROY_WITH_PARENT,
329        (
330            Gtk.STOCK_CANCEL,
331            Gtk.ResponseType.REJECT,
332            Gtk.STOCK_OK,
333            Gtk.ResponseType.ACCEPT
334        )
335    )
336
337    dialog.vbox.set_spacing(12)
338
339    if readme:
340        tv = Gtk.TextView()
341        tv.set_wrap_mode(Gtk.WrapMode.WORD_CHAR)
342        tv.get_buffer().set_text(readme)
343        tv.set_editable(False)
344        tv.set_left_margin(12)
345        tv.set_right_margin(12)
346        try:  # methods introduced in GTK 3.18
347            tv.set_top_margin(6)
348            tv.set_bottom_margin(6)
349        except AttributeError:
350            pass
351        scrolls = Gtk.ScrolledWindow()
352        scrolls.set_size_request(640, 480)
353        scrolls.add(tv)
354        dialog.vbox.pack_start(scrolls, True, True, 0)
355
356    question = Gtk.Label(label=_(
357        "<b>Do you really want to import package “{brushpack_name}”?</b>"
358    ).format(
359        brushpack_name=packname,
360    ))
361    question.set_use_markup(True)
362    dialog.vbox.pack_start(question, True, True, 0)
363    dialog.vbox.show_all()
364    answer = dialog.run()
365    dialog.destroy()
366    return answer
367
368
369def ask_for_color(title, color=None, previous_color=None, parent=None):
370    """Returns a color chosen by the user via a modal dialog.
371
372    The dialog is a standard `Gtk.ColorSelectionDialog`.
373    The returned value may be `None`,
374    which means that the user pressed Cancel in the dialog.
375
376    """
377    if color is None:
378        color = RGBColor(0.5, 0.5, 0.5)
379    if previous_color is None:
380        previous_color = RGBColor(0.5, 0.5, 0.5)
381    dialog = Gtk.ColorSelectionDialog(title)
382    sel = dialog.get_color_selection()
383    sel.set_current_color(uicolor.to_gdk_color(color))
384    sel.set_previous_color(uicolor.to_gdk_color(previous_color))
385    dialog.set_position(Gtk.WindowPosition.MOUSE)
386    dialog.set_modal(True)
387    dialog.set_resizable(False)
388    if parent is not None:
389        dialog.set_transient_for(parent)
390    # GNOME likes to darken the main window
391    # when it is set as the transient-for parent window.
392    # The setting is "Attached Modal Dialogs", which defaultss to ON.
393    # See https://github.com/mypaint/mypaint/issues/325 .
394    # This is unhelpful for art programs,
395    # but advertising the dialog
396    # as a utility window restores sensible behaviour.
397    dialog.set_type_hint(Gdk.WindowTypeHint.UTILITY)
398    dialog.set_default_response(Gtk.ResponseType.OK)
399    response_id = dialog.run()
400    result = None
401    if response_id == Gtk.ResponseType.OK:
402        col_gdk = sel.get_current_color()
403        result = uicolor.from_gdk_color(col_gdk)
404    dialog.destroy()
405    return result
406