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