footer mode display: refactor, add icon tooltip
[mypaint:achadwick-mypaint.git] / gui / drawwindow.py
1 # -*- coding: utf-8 -*-
2 #
3 # This file is part of MyPaint.
4 # Copyright (C) 2007-2008 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 """Main drawing window.
12
13 Painting is done in tileddrawwidget.py.
14 """
15
16 ## Imports
17
18 import os
19 import time
20 import webbrowser
21 from warnings import warn
22 import logging
23 logger = logging.getLogger(__name__)
24
25 from gettext import gettext as _
26 import gtk
27 import gobject
28 from gtk import gdk
29 from gtk import keysyms
30
31 import colorselectionwindow
32 import historypopup
33 import stategroup
34 import colorpicker
35 import windowing
36 import toolbar
37 import previewwindow
38 import dialogs
39 from lib import helpers
40 import canvasevent
41 from colors import RGBColor, HSVColor
42
43 import brushselectionwindow
44
45 import gtk2compat
46 import xml.etree.ElementTree as ET
47
48 # palette support
49 from lib.scratchpad_palette import GimpPalette, draw_palette
50
51 from overlays import LastPaintPosOverlay, ScaleOverlay
52 from framewindow import FrameOverlay
53 from symmetry import SymmetryOverlay
54
55
56 ## Module constants
57
58 BRUSHPACK_URI = 'http://wiki.mypaint.info/index.php?title=Brush_Packages/redirect_mypaint_1.1_gui'
59
60
61 ## Helpers
62
63 def with_wait_cursor(func):
64     """python decorator that adds a wait cursor around a function"""
65     # TODO: put in a helper file?
66     def wrapper(self, *args, **kwargs):
67         toplevels = gtk.Window.list_toplevels()
68         toplevels = [t for t in toplevels if t.get_window() is not None]
69         for toplevel in toplevels:
70             toplevel_win = toplevel.get_window()
71             if toplevel_win is not None:
72                 toplevel_win.set_cursor(gdk.Cursor(gdk.WATCH))
73             toplevel.set_sensitive(False)
74         self.app.doc.tdw.grab_add()
75         try:
76             func(self, *args, **kwargs)
77             # gtk main loop may be called in here...
78         finally:
79             for toplevel in toplevels:
80                 toplevel.set_sensitive(True)
81                 # ... which is why we need this check:
82                 toplevel_win = toplevel.get_window()
83                 if toplevel_win is not None:
84                     toplevel_win.set_cursor(None)
85             self.app.doc.tdw.grab_remove()
86     return wrapper
87
88
89 ## Class definitions
90
91
92 class DrawWindow (gtk.Window):
93     """Main drawing window.
94     """
95
96     __gtype_name__ = 'MyPaintDrawWindow'
97
98     #TRANSLATORS: footer icon tooltip markup for the current mode
99     _MODE_ICON_TEMPLATE = _("<b>{name}</b>\n{description}")
100
101     def __init__(self):
102         super(DrawWindow, self).__init__()
103
104         import application
105         app = application.get_app()
106         self.app = app
107         self.app.kbm.add_window(self)
108
109         # Window handling
110         self._updating_toggled_item = False
111         self.is_fullscreen = False
112
113         # Enable drag & drop
114         if not gtk2compat.USE_GTK3:
115             self.drag_dest_set(gtk.DEST_DEFAULT_MOTION |
116                             gtk.DEST_DEFAULT_HIGHLIGHT |
117                             gtk.DEST_DEFAULT_DROP,
118                             [("text/uri-list", 0, 1),
119                              ("application/x-color", 0, 2)],
120                             gtk.gdk.ACTION_DEFAULT|gtk.gdk.ACTION_COPY)
121
122         # Connect events
123         self.connect('delete-event', self.quit_cb)
124         self.connect('key-press-event', self.key_press_event_cb)
125         self.connect('key-release-event', self.key_release_event_cb)
126         self.connect("drag-data-received", self.drag_data_received)
127         self.connect("window-state-event", self.window_state_event_cb)
128
129         # Deferred setup
130         self._done_realize = False
131         self.connect("realize", self._realize_cb)
132
133         self.app.filehandler.current_file_observers.append(self.update_title)
134
135         # Park the focus on the main tdw rather than on the toolbar. Default
136         # activation doesn't really mean much for MyPaint's main window, so
137         # it's safe to do this and it looks better.
138         #self.main_widget.set_can_default(True)
139         #self.main_widget.set_can_focus(True)
140         #self.main_widget.grab_focus()
141
142
143     def _realize_cb(self, drawwindow):
144         # Deferred setup: anything that needs to be done when self.app is fully
145         # initialized.
146         if self._done_realize:
147             return
148         self._done_realize = True
149
150         doc = self.app.doc
151         doc.tdw.display_overlays.append(FrameOverlay(doc))
152         self.update_overlays()
153         self._init_actions()
154         kbm = self.app.kbm
155         kbm.add_extra_key('Menu', 'ShowPopupMenu')
156         kbm.add_extra_key('Tab', 'FullscreenAutohide')
157         self._init_stategroups()
158
159         self._init_menubar()
160         self._init_toolbar()
161         topbar = self.app.builder.get_object("app_topbar")
162         topbar.menubar = self.menubar
163         topbar.toolbar = self.toolbar
164
165         # Workspace setup
166         ws = self.app.workspace
167         ws.tool_widget_shown += self.app_workspace_tool_widget_shown_cb
168         ws.tool_widget_hidden += self.app_workspace_tool_widget_hidden_cb
169
170         # Footer bar updates
171         self.app.brush.observers.append(self._update_footer_color_widgets)
172         doc.modes.observers.append(self._update_status_bar_mode_widgets)
173         context_id = self.app.statusbar.get_context_id("active-mode")
174         self._active_mode_context_id = context_id
175         self._update_status_bar_mode_widgets(doc.modes.top)
176         mode_img = self.app.builder.get_object("app_current_mode_icon")
177         mode_img.connect("query-tooltip", self._mode_icon_query_tooltip_cb)
178         mode_img.set_has_tooltip(True)
179
180
181     def _init_actions(self):
182         # Actions are defined in mypaint.xml: all we need to do here is connect
183         # some extra state management.
184
185         ag = self.action_group = self.app.builder.get_object("WindowActions")
186         self.update_fullscreen_action()
187
188         # Set initial state from user prefs
189         ag.get_action("ToggleScaleFeedback").set_active(
190                 self.app.preferences.get("ui.feedback.scale", False))
191         ag.get_action("ToggleLastPosFeedback").set_active(
192                 self.app.preferences.get("ui.feedback.last_pos", False))
193         ag.get_action("ToggleSymmetryFeedback").set_active(
194                 self.app.preferences.get("ui.feedback.symmetry", False))
195
196         # Follow frame toggled state
197         self.app.doc.model.frame_observers.append(self.frame_changed_cb)
198
199         # Keyboard handling
200         for action in self.action_group.list_actions():
201             self.app.kbm.takeover_action(action)
202
203         # Brush chooser
204         self._brush_chooser_dialog = None
205
206
207     def _init_stategroups(self):
208         sg = stategroup.StateGroup()
209         p2s = sg.create_popup_state
210         changer_crossed_bowl = p2s(colorselectionwindow.ColorChangerCrossedBowlPopup(self.app))
211         changer_wash = p2s(colorselectionwindow.ColorChangerWashPopup(self.app))
212         ring = p2s(colorselectionwindow.ColorRingPopup(self.app))
213         hist = p2s(historypopup.HistoryPopup(self.app, self.app.doc.model))
214
215         self.popup_states = {
216             'ColorChangerCrossedBowlPopup': changer_crossed_bowl,
217             'ColorChangerWashPopup': changer_wash,
218             'ColorRingPopup': ring,
219             'ColorHistoryPopup': hist,
220             }
221
222         # not sure how useful this is; we can't cycle at the moment
223         changer_crossed_bowl.next_state = ring
224         ring.next_state = changer_wash
225         changer_wash.next_state = ring
226
227         changer_wash.autoleave_timeout = None
228         changer_crossed_bowl.autoleave_timeout = None
229         ring.autoleave_timeout = None
230
231         hist.autoleave_timeout = 0.600
232         self.history_popup_state = hist
233
234         for action_name, popup_state in self.popup_states.iteritems():
235             label = self.app.find_action(action_name).get_label()
236             popup_state.label = label
237
238
239     def _init_menubar(self):
240         # Load Menubar, duplicate into self.popupmenu
241         menupath = os.path.join(self.app.datapath, 'gui/menu.xml')
242         menubar_xml = open(menupath).read()
243         self.app.ui_manager.add_ui_from_string(menubar_xml)
244         self.popupmenu = self._clone_menu(menubar_xml, 'PopupMenu', self.app.doc.tdw)
245         self.menubar = self.app.ui_manager.get_widget('/Menubar')
246
247
248     def _init_toolbar(self):
249         self.toolbar_manager = toolbar.ToolbarManager(self)
250         self.toolbar = self.toolbar_manager.toolbar1
251
252     def _clone_menu(self, xml, name, owner=None):
253         """Menu duplicator
254
255         Hopefully temporary hack for converting UIManager XML describing the
256         main menubar into a rebindable popup menu. UIManager by itself doesn't
257         let you do this, by design, but we need a bigger menu than the little
258         things it allows you to build.
259         """
260         ui_elt = ET.fromstring(xml)
261         rootmenu_elt = ui_elt.find("menubar")
262         rootmenu_elt.attrib["name"] = name
263         xml = ET.tostring(ui_elt)
264         self.app.ui_manager.add_ui_from_string(xml)
265         tmp_menubar = self.app.ui_manager.get_widget('/' + name)
266         popupmenu = gtk.Menu()
267         for item in tmp_menubar.get_children():
268             tmp_menubar.remove(item)
269             popupmenu.append(item)
270         if owner is not None:
271             popupmenu.attach_to_widget(owner, None)
272         popupmenu.set_title("MyPaint")
273         popupmenu.connect("selection-done", self.popupmenu_done_cb)
274         popupmenu.connect("deactivate", self.popupmenu_done_cb)
275         popupmenu.connect("cancel", self.popupmenu_done_cb)
276         self.popupmenu_last_active = None
277         return popupmenu
278
279
280     def update_title(self, filename):
281         if filename:
282             self.set_title("MyPaint - %s" % os.path.basename(filename))
283         else:
284             self.set_title("MyPaint")
285
286     # INPUT EVENT HANDLING
287     def drag_data_received(self, widget, context, x, y, selection, info, t):
288         if info == 1:
289             if selection.data:
290                 uri = selection.data.split("\r\n")[0]
291                 fn = helpers.uri2filename(uri)
292                 if os.path.exists(fn):
293                     if self.app.filehandler.confirm_destructive_action():
294                         self.app.filehandler.open_file(fn)
295         elif info == 2: # color
296             color = RGBColor.new_from_drag_data(selection.data)
297             self.app.brush_color_manager.set_color(color)
298             self.app.brush_color_manager.push_history(color)
299             # Don't popup the color history for now, as I haven't managed
300             # to get it to cooperate.
301
302     def print_memory_leak_cb(self, action):
303         helpers.record_memory_leak_status(print_diff = True)
304
305     def run_garbage_collector_cb(self, action):
306         helpers.run_garbage_collector()
307
308     def start_profiling_cb(self, action):
309         if getattr(self, 'profiler_active', False):
310             self.profiler_active = False
311             return
312
313         def doit():
314             import cProfile
315             profile = cProfile.Profile()
316
317             self.profiler_active = True
318             logger.info('--- GUI Profiling starts ---')
319             while self.profiler_active:
320                 profile.runcall(gtk.main_iteration_do, False)
321                 if not gtk.events_pending():
322                     time.sleep(0.050) # ugly trick to remove "user does nothing" from profile
323             logger.info('--- GUI Profiling ends ---')
324
325             profile.dump_stats('profile_fromgui.pstats')
326             logger.debug('profile written to mypaint_profile.pstats')
327             os.system('gprof2dot.py -f pstats profile_fromgui.pstats | dot -Tpng -o profile_fromgui.png && feh profile_fromgui.png &')
328
329         gobject.idle_add(doit)
330
331     def _get_active_doc(self):
332         # Determines which is the active doc for the purposes of keyboard
333         # event dispatch.
334         tdw_class = self.app.scratchpad_doc.tdw.__class__
335         tdw = tdw_class.get_active_tdw()
336         if tdw is not None:
337             if tdw is self.app.scratchpad_doc.tdw:
338                 return (self.app.scratchpad_doc, tdw)
339             elif tdw is self.app.doc.tdw:
340                 return (self.app.doc, tdw)
341         return (None, None)
342
343     def key_press_event_cb(self, win, event):
344         # Process keyboard events
345         target_doc, target_tdw = self._get_active_doc()
346         if target_doc is None:
347             return False
348         # Unfullscreen
349         if self.is_fullscreen and event.keyval == keysyms.Escape:
350             gobject.idle_add(self.unfullscreen)
351         # Forward the keypress to the active doc's active InteractionMode.
352         return target_doc.modes.top.key_press_cb(win, target_tdw, event)
353
354
355     def key_release_event_cb(self, win, event):
356         # Process key-release events
357         target_doc, target_tdw = self._get_active_doc()
358         if target_doc is None:
359             return False
360         # Forward the event (see above)
361         return target_doc.modes.top.key_release_cb(win, target_tdw, event)
362
363
364     # Window handling
365     def toggle_window_cb(self, action):
366         """Handles a variety of window-toggling GtkActions.
367
368         Handled here:
369
370         * Workspace-managed tool widgets which require no constructor args.
371         * Regular app subwindows, exposed via its get_subwindow() method.
372
373         """
374         action_name = action.get_name()
375         if action_name.endswith("Tool"):
376             gtype_name = "MyPaint%s" % (action.get_name(),)
377             workspace = self.app.workspace
378             showing = workspace.get_tool_widget_showing(gtype_name, [])
379             active = action.get_active()
380             if active and not showing:
381                 workspace.show_tool_widget(gtype_name, [])
382             elif showing and not active:
383                 workspace.hide_tool_widget(gtype_name, [])
384         elif self.app.has_subwindow(action_name):
385             window = self.app.get_subwindow(action_name)
386             active = action.get_active()
387             visible = window.get_visible()
388             if active:
389                 if not visible:
390                     window.show_all()
391                 window.present()
392             elif visible:
393                 if not active:
394                     window.hide()
395         else:
396             logger.warning("unknown window or tool %r" % (action_name,))
397
398
399     def app_workspace_tool_widget_shown_cb(self, ws, widget):
400         gtype_name = widget.__gtype_name__
401         assert gtype_name.startswith("MyPaint")
402         action_name = gtype_name.replace("MyPaint", "", 1)
403         action = self.app.builder.get_object(action_name)
404         if action and not action.get_active():
405             action.set_active(True)
406
407
408     def app_workspace_tool_widget_hidden_cb(self, ws, widget):
409         gtype_name = widget.__gtype_name__
410         assert gtype_name.startswith("MyPaint")
411         action_name = gtype_name.replace("MyPaint", "", 1)
412         action = self.app.builder.get_object(action_name)
413         if action and action.get_active():
414             action.set_active(False)
415
416
417     # Feedback and overlays
418     # It's not intended that all categories of feedback will use overlays, but
419     # they currently all do. This may change now we have a conventional
420     # statusbar for textual types of feedback.
421
422     def toggle_scale_feedback_cb(self, action):
423         self.app.preferences['ui.feedback.scale'] = action.get_active()
424         self.update_overlays()
425
426     def toggle_last_pos_feedback_cb(self, action):
427         self.app.preferences['ui.feedback.last_pos'] = action.get_active()
428         self.update_overlays()
429
430     def toggle_symmetry_feedback_cb(self, action):
431         self.app.preferences['ui.feedback.symmetry'] = action.get_active()
432         self.update_overlays()
433
434     def update_overlays(self):
435         # Updates the list of overlays on the main doc's TDW to match the prefs
436         doc = self.app.doc
437         disp_overlays = [
438             ('ui.feedback.scale', ScaleOverlay),
439             ('ui.feedback.last_pos', LastPaintPosOverlay),
440             ('ui.feedback.symmetry', SymmetryOverlay),
441             ]
442         overlays_changed = False
443         for key, class_ in disp_overlays:
444             current_instance = None
445             for ov in doc.tdw.display_overlays:
446                 if isinstance(ov, class_):
447                     current_instance = ov
448             active = self.app.preferences.get(key, False)
449             if active and not current_instance:
450                 doc.tdw.display_overlays.append(class_(doc))
451                 overlays_changed = True
452             elif current_instance and not active:
453                 doc.tdw.display_overlays.remove(current_instance)
454                 overlays_changed = True
455         if overlays_changed:
456             doc.tdw.queue_draw()
457
458
459     def popup_cb(self, action):
460         state = self.popup_states[action.get_name()]
461         state.activate(action)
462
463
464     def brush_chooser_popup_cb(self, action):
465         dialog = self._brush_chooser_dialog
466         if dialog is None:
467             dialog = dialogs.BrushChooserDialog(self.app)
468             dialog.connect("response", self._brush_chooser_dialog_response_cb)
469             self._brush_chooser_dialog = dialog
470         if not dialog.get_visible():
471             dialog.show_all()
472             dialog.present()
473         else:
474             dialog.response(gtk.RESPONSE_CANCEL)
475
476
477     def _brush_chooser_dialog_response_cb(self, dialog, response_id):
478         dialog.hide()
479
480
481     def color_details_dialog_cb(self, action):
482         mgr = self.app.brush_color_manager
483         new_col = RGBColor.new_from_dialog(
484           title=_("Set current color"),
485           color=mgr.get_color(),
486           previous_color=mgr.get_previous_color(),
487           parent=self)
488         if new_col is not None:
489             mgr.set_color(new_col)
490
491
492     # Show Subwindows
493
494     def fullscreen_autohide_toggled_cb(self, action):
495         workspace = self.app.workspace
496         workspace.autohide_enabled = action.get_active()
497
498
499     # Fullscreen mode
500     # This implementation requires an ICCCM and EWMH-compliant window manager
501     # which supports the _NET_WM_STATE_FULLSCREEN hint. There are several
502     # available.
503
504     def fullscreen_cb(self, *junk):
505         if not self.is_fullscreen:
506             self.fullscreen()
507         else:
508             self.unfullscreen()
509
510     def window_state_event_cb(self, widget, event):
511         # Respond to changes of the fullscreen state only
512         if not event.changed_mask & gdk.WINDOW_STATE_FULLSCREEN:
513             return
514         self.is_fullscreen = event.new_window_state & gdk.WINDOW_STATE_FULLSCREEN
515         self.update_fullscreen_action()
516
517     def update_fullscreen_action(self):
518         action = self.action_group.get_action("Fullscreen")
519         if self.is_fullscreen:
520             action.set_stock_id(gtk.STOCK_LEAVE_FULLSCREEN)
521             action.set_tooltip(_("Leave Fullscreen Mode"))
522             action.set_label(_("Leave Fullscreen"))
523         else:
524             action.set_stock_id(gtk.STOCK_FULLSCREEN)
525             action.set_tooltip(_("Enter Fullscreen Mode"))
526             action.set_label(_("Fullscreen"))
527
528     def popupmenu_show_cb(self, action):
529         self.show_popupmenu()
530
531     def show_popupmenu(self, event=None):
532         self.menubar.set_sensitive(False)   # excessive feedback?
533         button = 1
534         time = 0
535         if event is not None:
536             if event.type == gdk.BUTTON_PRESS:
537                 button = event.button
538                 time = event.time
539         # GTK3: arguments have a different order, and "data" is required.
540         # GTK3: Use keyword arguments for max compatibility.
541         self.popupmenu.popup(parent_menu_shell=None, parent_menu_item=None,
542                              func=None, button=button, activate_time=time,
543                              data=None)
544         if event is None:
545             # We're responding to an Action, most probably the menu key.
546             # Open out the last highlighted menu to speed key navigation up.
547             if self.popupmenu_last_active is None:
548                 self.popupmenu.select_first(True) # one less keypress
549             else:
550                 self.popupmenu.select_item(self.popupmenu_last_active)
551
552     def popupmenu_done_cb(self, *a, **kw):
553         # Not sure if we need to bother with this level of feedback,
554         # but it actually looks quite nice to see one menu taking over
555         # the other. Makes it clear that the popups are the same thing as
556         # the full menu, maybe.
557         self.menubar.set_sensitive(True)
558         self.popupmenu_last_active = self.popupmenu.get_active()
559
560     # BEGIN -- Scratchpad menu options
561     def save_scratchpad_as_default_cb(self, action):
562         self.app.filehandler.save_scratchpad(self.app.filehandler.get_scratchpad_default(), export = True)
563
564     def clear_default_scratchpad_cb(self, action):
565         self.app.filehandler.delete_default_scratchpad()
566
567     # Unneeded since 'Save blank canvas' bug has been addressed.
568     #def clear_autosave_scratchpad_cb(self, action):
569     #    self.app.filehandler.delete_autosave_scratchpad()
570
571     def new_scratchpad_cb(self, action):
572         if os.path.isfile(self.app.filehandler.get_scratchpad_default()):
573             self.app.filehandler.open_scratchpad(self.app.filehandler.get_scratchpad_default())
574         else:
575             self.app.scratchpad_doc.model.clear()
576             # With no default - adopt the currently chosen background
577             bg = self.app.doc.model.background
578             if self.app.scratchpad_doc:
579                 self.app.scratchpad_doc.model.set_background(bg)
580
581         self.app.scratchpad_filename = self.app.preferences['scratchpad.last_opened'] = self.app.filehandler.get_scratchpad_autosave()
582
583     def load_scratchpad_cb(self, action):
584         if self.app.scratchpad_filename:
585             self.save_current_scratchpad_cb(action)
586             current_pad = self.app.scratchpad_filename
587         else:
588             current_pad = self.app.filehandler.get_scratchpad_autosave()
589         self.app.filehandler.open_scratchpad_dialog()
590         # Check to see if a file has been opened outside of the scratchpad directory
591         if not os.path.abspath(self.app.scratchpad_filename).startswith(os.path.abspath(self.app.filehandler.get_scratchpad_prefix())):
592             # file is NOT within the scratchpad directory - load copy as current scratchpad
593             self.app.scratchpad_filename = self.app.preferences['scratchpad.last_opened'] = current_pad
594
595     def save_as_scratchpad_cb(self, action):
596         self.app.filehandler.save_scratchpad_as_dialog()
597
598     def revert_current_scratchpad_cb(self, action):
599         filename = self.app.scratchpad_filename
600         if os.path.isfile(filename):
601             self.app.filehandler.open_scratchpad(filename)
602             logger.info("Reverted scratchpad to %s" % (filename,))
603         else:
604             logger.warning("No file to revert to yet.")
605
606     def save_current_scratchpad_cb(self, action):
607         self.app.filehandler.save_scratchpad(self.app.scratchpad_filename)
608
609     def scratchpad_copy_background_cb(self, action):
610         bg = self.app.doc.model.background
611         if self.app.scratchpad_doc:
612             self.app.scratchpad_doc.model.set_background(bg)
613
614     def draw_sat_spectrum_cb(self, action):
615         g = GimpPalette()
616         hsv = self.app.brush.get_color_hsv()
617         g.append_sat_spectrum(hsv)
618         grid_size = 30.0
619         off_x = off_y = grid_size / 2.0
620         column_limit = 8
621         draw_palette(self.app, g, self.app.scratchpad_doc, columns=column_limit, grid_size=grid_size)
622
623     # END -- Scratchpad menu options
624
625
626     def palette_next_cb(self, action):
627         mgr = self.app.brush_color_manager
628         color = mgr.get_color()
629         newcolor = mgr.palette.move_match_position(1, mgr.get_color())
630         if newcolor:
631             mgr.set_color(newcolor)
632         # Show the palette panel if hidden
633         workspace = self.app.workspace
634         workspace.show_tool_widget("MyPaintPaletteTool", [])
635
636
637     def palette_prev_cb(self, action):
638         mgr = self.app.brush_color_manager
639         color = mgr.get_color()
640         newcolor = mgr.palette.move_match_position(-1, mgr.get_color())
641         if newcolor:
642             mgr.set_color(newcolor)
643         # Show the palette panel if hidden
644         workspace = self.app.workspace
645         workspace.show_tool_widget("MyPaintPaletteTool", [])
646
647
648     def palette_add_current_color_cb(self, *args, **kwargs):
649         """Append the current color to the palette (action or clicked cb)"""
650         mgr = self.app.brush_color_manager
651         color = mgr.get_color()
652         mgr.palette.append(color, name=None, unique=True, match=True)
653         # Show the palette panel if hidden
654         workspace = self.app.workspace
655         workspace.show_tool_widget("MyPaintPaletteTool", [])
656
657
658     def quit_cb(self, *junk):
659         self.app.doc.model.split_stroke()
660         self.app.save_gui_config() # FIXME: should do this periodically, not only on quit
661
662         if not self.app.filehandler.confirm_destructive_action(title=_('Quit'), question=_('Really Quit?')):
663             return True
664
665         gtk.main_quit()
666         return False
667
668
669     def trim_layer_cb(self, action):
670         """Trim the current layer to the frame"""
671         self.app.doc.model.trim_layer()
672
673
674     def toggle_frame_cb(self, action):
675         model = self.app.doc.model
676         enabled = bool(model.frame_enabled)
677         desired = bool(action.get_active())
678         if enabled != desired:
679             model.set_frame_enabled(desired, user_initiated=True)
680
681     def frame_changed_cb(self):
682         action = self.action_group.get_action("FrameToggle")
683         if getattr(action, "in_callback", False):
684             return
685         action.in_callback = True
686         enabled = bool(self.app.doc.model.frame_enabled)
687         action.set_active(enabled)
688         action.in_callback = False
689
690     def download_brush_pack_cb(self, *junk):
691         url = BRUSHPACK_URI
692         logger.info('Opening URL %r in web browser' % (url,))
693         webbrowser.open(url)
694
695     def import_brush_pack_cb(self, *junk):
696         format_id, filename = dialogs.open_dialog(_("Import brush package..."), self,
697                                  [(_("MyPaint brush package (*.zip)"), "*.zip")])
698         if not filename:
699             return
700         imported = self.app.brushmanager.import_brushpack(filename,  self)
701         logger.info("Imported brush groups %r", imported)
702         workspace = self.app.workspace
703         for groupname in imported:
704             workspace.show_tool_widget("MyPaintBrushGroupTool", (groupname,))
705
706     # INFORMATION
707     # TODO: Move into dialogs.py?
708     def about_cb(self, action):
709         d = gtk.AboutDialog()
710         d.set_transient_for(self)
711         d.set_program_name("MyPaint")
712         d.set_version(self.app.version)
713         d.set_copyright(_("Copyright (C) 2005-2012\nMartin Renold and the MyPaint Development Team"))
714         d.set_website("http://mypaint.info/")
715         d.set_logo(self.app.pixmaps.mypaint_logo)
716         d.set_license(
717             _(u"This program is free software; you can redistribute it and/or modify "
718               u"it under the terms of the GNU General Public License as published by "
719               u"the Free Software Foundation; either version 2 of the License, or "
720               u"(at your option) any later version.\n"
721               u"\n"
722               u"This program is distributed in the hope that it will be useful, "
723               u"but WITHOUT ANY WARRANTY. See the COPYING file for more details.")
724             )
725         d.set_wrap_license(True)
726         d.set_authors([
727             # (in order of appearance)
728             u"Martin Renold (%s)" % _('programming'),
729             u"Yves Combe (%s)" % _('portability'),
730             u"Popolon (%s)" % _('programming'),
731             u"Clement Skau (%s)" % _('programming'),
732             u"Jon Nordby (%s)" % _('programming'),
733             u"Álinson Santos (%s)" % _('programming'),
734             u"Tumagonx (%s)" % _('portability'),
735             u"Ilya Portnov (%s)" % _('programming'),
736             u"Jonas Wagner (%s)" % _('programming'),
737             u"Luka Čehovin (%s)" % _('programming'),
738             u"Andrew Chadwick (%s)" % _('programming'),
739             u"Till Hartmann (%s)" % _('programming'),
740             u'David Grundberg (%s)' % _('programming'),
741             u"Krzysztof Pasek (%s)" % _('programming'),
742             u"Ben O'Steen (%s)" % _('programming'),
743             u"Ferry Jérémie (%s)" % _('programming'),
744             u"しげっち 'sigetch' (%s)" % _('programming'),
745             u"Richard Jones (%s)" % _('programming'),
746             u"David Gowers (%s)" % _('programming'),
747             ])
748         d.set_artists([
749             u"Artis Rozentāls (%s)" % _('brushes'),
750             u"Popolon (%s)" % _('brushes'),
751             u"Marcelo 'Tanda' Cerviño (%s)" % _('patterns, brushes'),
752             u"David Revoy (%s)" % _('brushes, tool icons'),
753             u"Ramón Miranda (%s)" % _('brushes, patterns'),
754             u"Enrico Guarnieri 'Ico_dY' (%s)" % _('brushes'),
755             u'Sebastian Kraft (%s)' % _('desktop icon'),
756             u"Nicola Lunghi (%s)" % _('patterns'),
757             u"Toni Kasurinen (%s)" % _('brushes'),
758             u"Сан Саныч 'MrMamurk' (%s)" % _('patterns'),
759             u"Andrew Chadwick (%s)" % _('tool icons'),
760             u"Ben O'Steen (%s)" % _('tool icons'),
761             ])
762         d.set_translator_credits(_("translator-credits"));
763
764         d.run()
765         d.destroy()
766
767     def show_infodialog_cb(self, action):
768         text = {
769         'ShortcutHelp':
770                 _("Move your mouse over a menu entry, then press the key to assign."),
771         'ContextHelp':
772                 _("Brush shortcut keys are used to quickly save/restore brush "
773                  "settings. You can paint with one hand and change brushes with "
774                  "the other, even in mid-stroke."
775                  "\n\n"
776                  "There are 10 persistent memory slots available."),
777         'Docu':
778                 _("There is a tutorial available on the MyPaint homepage. It "
779                  "explains some features which are hard to discover yourself."
780                  "\n\n"
781                  "Comments about the brush settings (opaque, hardness, etc.) and "
782                  "inputs (pressure, speed, etc.) are available as tooltips. "
783                  "Put your mouse over a label to see them. "
784                  "\n"),
785         }
786         self.app.message_dialog(text[action.get_name()])
787
788
789     ## Footer bar stuff
790
791     def _update_footer_color_widgets(self, settings):
792         """Updates the footer bar color info when the brush color changes."""
793         if not settings.intersection(('color_h', 'color_s', 'color_v')):
794             return
795         bm_btn_name = "footer_bookmark_current_color_button"
796         bm_btn = self.app.builder.get_object(bm_btn_name)
797         brush_color = HSVColor(*self.app.brush.get_color_hsv())
798         palette = self.app.brush_color_manager.palette
799         bm_btn.set_sensitive(brush_color not in palette)
800
801     def _update_status_bar_mode_widgets(self, mode):
802         """Updates widgets on the status bar that reflect the current mode"""
803         # Update the status bar
804         statusbar = self.app.statusbar
805         context_id = self._active_mode_context_id
806         statusbar.pop(context_id)
807         statusbar_msg = u"{usage!s}".format(name=mode.get_name(),
808                                             usage=mode.get_usage())
809         statusbar.push(context_id, statusbar_msg)
810         # Icon
811         icon_name = mode.get_icon_name()
812         icon_size = gtk.ICON_SIZE_SMALL_TOOLBAR
813         mode_img = self.app.builder.get_object("app_current_mode_icon")
814         if not icon_name:
815             icon_name = "missing-image"
816         mode_img.set_from_icon_name(icon_name, icon_size)
817
818     def _mode_icon_query_tooltip_cb(self, widget, x, y, kbmode, tooltip):
819         mode = self.app.doc.modes.top
820         icon_name = mode.get_icon_name()
821         if not icon_name:
822             icon_name = "missing-image"
823         icon_size = gtk.ICON_SIZE_DIALOG
824         tooltip.set_icon_from_icon_name(icon_name, icon_size)
825         description = None
826         action = mode.get_action()
827         if action:
828             description = action.get_tooltip()
829         if not description:
830             description = mode.get_usage()
831         params = { "name": helpers.escape(mode.get_name()),
832                    "description": helpers.escape(description) }
833         markup = self._MODE_ICON_TEMPLATE.format(**params)
834         tooltip.set_markup(markup)
835         return True