1 # -*- coding: utf-8 -*-
3 # This file is part of MyPaint.
4 # Copyright (C) 2007-2008 by Martin Renold <martinxyz@gmx.ch>
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.
12 This is the main drawing window, containing menu actions.
13 Painting is done in tileddrawwidget.py.
16 MYPAINT_VERSION="0.9.1+git"
19 from gettext import gettext as _
22 from gtk import gdk, keysyms
25 import colorselectionwindow, historypopup, stategroup, colorpicker, windowing, layout, toolbar
27 from lib import helpers
30 import xml.etree.ElementTree as ET
33 from lib.scratchpad_palette import GimpPalette, hatch_squiggle, squiggle, draw_palette
35 # TODO: put in a helper file?
36 def with_wait_cursor(func):
37 """python decorator that adds a wait cursor around a function"""
38 def wrapper(self, *args, **kwargs):
39 toplevels = [t for t in gtk.window_list_toplevels()
40 if t.window is not None]
41 for toplevel in toplevels:
42 toplevel.window.set_cursor(gdk.Cursor(gdk.WATCH))
43 toplevel.set_sensitive(False)
44 self.app.doc.tdw.grab_add()
46 func(self, *args, **kwargs)
47 # gtk main loop may be called in here...
49 for toplevel in toplevels:
50 toplevel.set_sensitive(True)
51 # ... which is why we need this check:
52 if toplevel.window is not None:
53 toplevel.window.set_cursor(None)
54 self.app.doc.tdw.grab_remove()
57 def button_press_cb_abstraction(drawwindow, win, event, doc):
58 #print event.device, event.button
61 # Single button-presses only, not 2ble/3ple
62 if event.type != gdk.BUTTON_PRESS:
63 # ignore the extra double-click event
67 # check whether we are painting (accidental)
68 if event.state & gdk.BUTTON1_MASK:
69 # Do not allow dragging in the middle of
70 # painting. This often happens by accident with wacom
71 # tablet's stylus button.
73 # However we allow dragging if the user's pressure is
74 # still below the click threshold. This is because
75 # some tablet PCs are not able to produce a
76 # middle-mouse click without reporting pressure.
77 # https://gna.org/bugs/index.php?15907
80 # Pick a suitable config option
81 ctrl = event.state & gdk.CONTROL_MASK
82 alt = event.state & gdk.MOD1_MASK
83 shift = event.state & gdk.SHIFT_MASK
85 modifier_str = "_shift"
87 modifier_str = "_ctrl"
90 prefs_name = "input.button%d%s_action" % (event.button, modifier_str)
91 action_name = drawwindow.app.preferences.get(prefs_name, "no_action")
94 if action_name == 'no_action':
95 return True # We handled it by doing nothing
98 # Really belongs in the tdw, but this is the only object with access
99 # to the application preferences.
100 if action_name == 'straight_line':
101 doc.tdw.straight_line_from_last_pos(is_sequence=False)
103 if action_name == 'straight_line_sequence':
104 doc.tdw.straight_line_from_last_pos(is_sequence=True)
108 if action_name.endswith("_canvas"):
110 if action_name == "pan_canvas":
111 dragfunc = doc.dragfunc_translate
112 elif action_name == "zoom_canvas":
113 dragfunc = doc.dragfunc_zoom
114 elif action_name == "rotate_canvas":
115 dragfunc = doc.dragfunc_rotate
116 if dragfunc is not None:
117 doc.tdw.start_drag(dragfunc)
122 if action_name == 'popup_menu':
123 drawwindow.show_popupmenu(event=event)
126 if action_name in drawwindow.popup_states:
127 state = drawwindow.popup_states[action_name]
128 state.activate(event)
131 # Dispatch regular GTK events.
132 action = drawwindow.app.find_action(action_name)
133 if action is not None:
137 def button_release_cb_abstraction(win, event, doc):
138 #print event.device, event.button
140 if tdw.dragfunc is not None:
141 tdw.stop_drag(doc.dragfunc_translate)
142 tdw.stop_drag(doc.dragfunc_rotate)
143 tdw.stop_drag(doc.dragfunc_zoom)
146 class Window (windowing.MainWindow, layout.MainWindow):
148 MENUISHBAR_RADIO_MENUBAR = 1
149 MENUISHBAR_RADIO_MAIN_TOOLBAR = 2
150 MENUISHBAR_RADIO_BOTH_BARS = 3
152 def __init__(self, app):
153 windowing.MainWindow.__init__(self, app)
157 self._updating_toggled_item = False
158 self._show_subwindows = True
159 self.is_fullscreen = False
162 self.drag_dest_set(gtk.DEST_DEFAULT_MOTION |
163 gtk.DEST_DEFAULT_HIGHLIGHT |
164 gtk.DEST_DEFAULT_DROP,
165 [("text/uri-list", 0, 1),
166 ("application/x-color", 0, 2)],
167 gtk.gdk.ACTION_DEFAULT|gtk.gdk.ACTION_COPY)
170 self.connect('delete-event', self.quit_cb)
171 self.connect('key-press-event', self.key_press_event_cb_before)
172 self.connect('key-release-event', self.key_release_event_cb_before)
173 self.connect_after('key-press-event', self.key_press_event_cb_after)
174 self.connect_after('key-release-event', self.key_release_event_cb_after)
175 self.connect("drag-data-received", self.drag_data_received)
176 self.connect("window-state-event", self.window_state_event_cb)
178 self.app.filehandler.current_file_observers.append(self.update_title)
182 lm = app.layout_manager
183 layout.MainWindow.__init__(self, lm)
184 self.main_widget.connect("button-press-event", self.button_press_cb)
185 self.main_widget.connect("button-release-event",self.button_release_cb)
186 self.main_widget.connect("scroll-event", self.scroll_cb)
189 kbm.add_extra_key('Menu', 'ShowPopupMenu')
190 kbm.add_extra_key('Tab', 'ToggleSubwindows')
192 self.init_stategroups()
196 print "DeprecationWarning: Use app.doc instead"
199 print "DeprecationWarning: Use app.doc.tdw instead"
200 return self.app.doc.tdw
201 tdw, doc = property(get_tdw), property(get_doc)
203 def init_actions(self):
205 # name, stock id, label, accelerator, tooltip, callback
206 ('FileMenu', None, _('File')),
207 ('Quit', gtk.STOCK_QUIT, _('Quit'), '<control>q', None, self.quit_cb),
208 ('FrameToggle', None, _('Toggle Document Frame'), None, None, self.toggle_frame_cb),
210 ('EditMenu', None, _('Edit')),
212 ('ColorMenu', None, _('Color')),
213 ('ColorPickerPopup', gtk.STOCK_COLOR_PICKER, _('Pick Color'), 'r', None, self.popup_cb),
214 ('ColorHistoryPopup', None, _('Color History'), 'x', None, self.popup_cb),
215 ('ColorChangerPopup', None, _('Color Changer'), 'v', None, self.popup_cb),
216 ('ColorRingPopup', None, _('Color Ring'), None, None, self.popup_cb),
217 ('ColorDetailsDialog', None, _("Color Details"), None, None, self.color_details_dialog_cb),
219 ('ContextMenu', None, _('Brushkeys')),
220 ('ContextHelp', gtk.STOCK_HELP, _('Help!'), None, None, self.show_infodialog_cb),
222 ('LayerMenu', None, _('Layers')),
224 # Scratchpad menu items
225 ('ScratchMenu', None, _('Scratchpad')),
226 ('ScratchNew', gtk.STOCK_NEW, _('New Scratchpad'), '', None, self.new_scratchpad_cb),
227 ('ScratchLoad', gtk.STOCK_OPEN, _('Load Scratchpad...'), '', None, self.load_scratchpad_cb),
228 ('ScratchSaveNow', gtk.STOCK_SAVE, _('Save Scratchpad Now'), '', None, self.save_current_scratchpad_cb),
229 ('ScratchSaveAs', gtk.STOCK_SAVE_AS, _('Save Scratchpad As...'), '', None, self.save_as_scratchpad_cb),
230 ('ScratchRevert', gtk.STOCK_UNDO, _('Revert Scratchpad'), '', None, self.revert_current_scratchpad_cb),
231 ('ScratchSaveAsDefault', None, _('Save Scratchpad as Default'), None, None, self.save_scratchpad_as_default_cb),
232 ('ScratchClearDefault', None, _('Clear the Default Scratchpad'), None, None, self.clear_default_scratchpad_cb),
233 ('ScratchPaletteOptions', None, _('Render a Palette')),
234 ('ScratchLoadPalette', None, _('Load Palette File...'), None, None, self.draw_palette_cb),
235 ('ScratchDrawSatPalette', None, _('Different Saturations of the current Color'), None, None, self.draw_sat_spectrum_cb),
236 ('ScratchCopyBackground', None, _('Copy Background to Scratchpad'), None, None, self.scratchpad_copy_background_cb),
238 ('BrushMenu', None, _('Brush')),
239 ('BrushChooserPopup', stock.TOOL_BRUSH, _("Change Brush..."), 'b', None, self.brush_chooser_popup_cb),
240 ('ImportBrushPack', gtk.STOCK_OPEN, _('Import brush package...'), '', None, self.import_brush_pack_cb),
242 ('HelpMenu', None, _('Help')),
243 ('Docu', gtk.STOCK_INFO, _('Where is the Documentation?'), None, None, self.show_infodialog_cb),
244 ('ShortcutHelp', gtk.STOCK_INFO, _('Change the Keyboard Shortcuts?'), None, None, self.show_infodialog_cb),
245 ('About', gtk.STOCK_ABOUT, _('About MyPaint'), None, None, self.about_cb),
247 ('DebugMenu', None, _('Debug')),
248 ('PrintMemoryLeak', None, _('Print Memory Leak Info to Console (Slow!)'), None, None, self.print_memory_leak_cb),
249 ('RunGarbageCollector', None, _('Run Garbage Collector Now'), None, None, self.run_garbage_collector_cb),
250 ('StartProfiling', gtk.STOCK_EXECUTE, _('Start/Stop Python Profiling (cProfile)'), None, None, self.start_profiling_cb),
251 ('GtkInputDialog', None, _('GTK input device dialog'), None, None, self.gtk_input_dialog_cb),
254 ('ViewMenu', None, _('View')),
255 ('MenuishBarMenu', None, _('Toolbars')),
256 ('ShowPopupMenu', None, _('Popup Menu'), 'Menu', None, self.popupmenu_show_cb),
257 ('Fullscreen', gtk.STOCK_FULLSCREEN, None, 'F11', None, self.fullscreen_cb),
258 ('ViewHelp', gtk.STOCK_HELP, _('Help'), None, None, self.show_infodialog_cb),
260 ag = self.action_group = gtk.ActionGroup('WindowActions')
261 self.app.add_action_group(ag)
262 ag.add_actions(actions)
263 self.update_fullscreen_action()
267 ('PreferencesWindow', gtk.STOCK_PREFERENCES,
268 _('Preferences'), None, None, self.toggle_window_cb),
269 ('InputTestWindow', None,
270 _('Test input devices'), None, None, self.toggle_window_cb),
271 ('FrameWindow', None,
272 _('Document Frame...'), None, None, self.toggle_window_cb),
273 ('LayersWindow', stock.TOOL_LAYERS,
274 None, None, _("Toggle the Layers list"),
275 self.toggle_window_cb),
276 ('BackgroundWindow', gtk.STOCK_PAGE_SETUP,
277 _('Background'), None, None, self.toggle_window_cb),
278 ('BrushSelectionWindow', stock.TOOL_BRUSH,
280 _("Edit and reorganise Brush Lists"),
281 self.toggle_window_cb),
282 ('BrushSettingsWindow', gtk.STOCK_PROPERTIES,
283 _('Brush Settings Editor'), '<control>b',
284 _("Change Brush Settings in detail"),
285 self.toggle_window_cb),
286 ('ColorSelectionWindow', stock.TOOL_COLOR_SELECTOR,
287 None, None, _("Toggle the Colour Triangle"),
288 self.toggle_window_cb),
289 ('ColorSamplerWindow', stock.TOOL_COLOR_SAMPLER,
290 None, None, _("Toggle the advanced Colour Sampler"),
291 self.toggle_window_cb),
292 ('ScratchWindow', stock.TOOL_SCRATCHPAD,
293 None, None, _('Toggle the scratchpad'),
294 self.toggle_window_cb),
296 ag.add_toggle_actions(toggle_actions)
298 # Reflect changes from other places (like tools' close buttons) into
299 # the proxys' visible states.
300 lm = self.app.layout_manager
301 lm.tool_visibility_observers.append(self.update_toggled_item_visibility)
302 lm.subwindow_visibility_observers.append(self.update_subwindow_visibility)
304 # Initial toggle state
305 for spec in toggle_actions:
307 action = ag.get_action(name)
308 role = name[0].lower() + name[1:]
309 visible = not lm.get_window_hidden_by_role(role)
310 # The sidebar machinery won't be up yet, so reveal windows that
311 # should be initially visible only in an idle handler
312 gobject.idle_add(action.set_active, visible)
314 # More toggle actions - ones which don't control windows.
316 ('ToggleSubwindows', None, _('Subwindows'), 'Tab',
317 _("Show subwindows"), self.toggle_subwindows_cb,
318 self.get_show_subwindows()),
320 ag.add_toggle_actions(toggle_actions)
323 menuishbar_radio_actions = [
324 ('MenuishBarRadioMenubar', None, _('Menubar only'), None,
326 self.MENUISHBAR_RADIO_MENUBAR),
327 ('MenuishBarRadioMainToolbar', None, _('Toolbar only'), None,
329 self.MENUISHBAR_RADIO_MAIN_TOOLBAR),
330 ('MenuishBarRadioMenubarAndMainToolbar', None, _('Both'), None,
331 _("Show both the menu bar and the toolbar"),
332 self.MENUISHBAR_RADIO_BOTH_BARS),
335 if self.get_ui_part_enabled("menubar"):
336 menuishbar_state += self.MENUISHBAR_RADIO_MENUBAR
337 if self.get_ui_part_enabled("main_toolbar"):
338 menuishbar_state += self.MENUISHBAR_RADIO_MAIN_TOOLBAR
339 if menuishbar_state == 0:
340 menuishbar_state = self.MENUISHBAR_RADIO_MAIN_TOOLBAR
341 ag.add_radio_actions(menuishbar_radio_actions,
342 menuishbar_state, self.on_menuishbar_radio_change)
343 gobject.idle_add(lambda: self.update_ui_parts())
346 for action in self.action_group.list_actions():
347 self.app.kbm.takeover_action(action)
349 def init_stategroups(self):
350 sg = stategroup.StateGroup()
351 p2s = sg.create_popup_state
352 changer = p2s(colorselectionwindow.ColorChangerPopup(self.app))
353 ring = p2s(colorselectionwindow.ColorRingPopup(self.app))
354 hist = p2s(historypopup.HistoryPopup(self.app, self.app.doc.model))
355 pick = self.colorpick_state = p2s(colorpicker.ColorPicker(self.app, self.app.doc.model))
357 self.popup_states = {
358 'ColorChangerPopup': changer,
359 'ColorRingPopup': ring,
360 'ColorHistoryPopup': hist,
361 'ColorPickerPopup': pick,
363 changer.next_state = ring
364 ring.next_state = changer
365 changer.autoleave_timeout = None
366 ring.autoleave_timeout = None
368 pick.max_key_hit_duration = 0.0
369 pick.autoleave_timeout = None
371 hist.autoleave_timeout = 0.600
372 self.history_popup_state = hist
374 def init_main_widget(self): # override
375 self.main_widget = self.app.doc.tdw
377 def init_menubar(self): # override
378 # Load Menubar, duplicate into self.popupmenu
379 menupath = os.path.join(self.app.datapath, 'gui/menu.xml')
380 menubar_xml = open(menupath).read()
381 self.app.ui_manager.add_ui_from_string(menubar_xml)
382 self.popupmenu = self._clone_menu(menubar_xml, 'PopupMenu', self.app.doc.tdw)
383 self.menubar = self.app.ui_manager.get_widget('/Menubar')
385 def init_toolbar(self):
386 self.toolbar = toolbar.MainToolbar(self)
388 def _clone_menu(self, xml, name, owner=None):
390 Hopefully temporary hack for converting UIManager XML describing the
391 main menubar into a rebindable popup menu. UIManager by itself doesn't
392 let you do this, by design, but we need a bigger menu than the little
393 things it allows you to build.
395 ui_elt = ET.fromstring(xml)
396 rootmenu_elt = ui_elt.find("menubar")
397 rootmenu_elt.attrib["name"] = name
398 xml = ET.tostring(ui_elt)
399 self.app.ui_manager.add_ui_from_string(xml)
400 tmp_menubar = self.app.ui_manager.get_widget('/' + name)
401 popupmenu = gtk.Menu()
402 for item in tmp_menubar.get_children():
403 tmp_menubar.remove(item)
404 popupmenu.append(item)
405 if owner is not None:
406 popupmenu.attach_to_widget(owner, None)
407 popupmenu.set_title("MyPaint")
408 popupmenu.connect("selection-done", self.popupmenu_done_cb)
409 popupmenu.connect("deactivate", self.popupmenu_done_cb)
410 popupmenu.connect("cancel", self.popupmenu_done_cb)
411 self.popupmenu_last_active = None
415 def update_title(self, filename):
417 self.set_title("MyPaint - %s" % os.path.basename(filename))
419 self.set_title("MyPaint")
421 # INPUT EVENT HANDLING
422 def drag_data_received(self, widget, context, x, y, selection, info, t):
425 uri = selection.data.split("\r\n")[0]
426 fn = helpers.uri2filename(uri)
427 if os.path.exists(fn):
428 if self.app.filehandler.confirm_destructive_action():
429 self.app.filehandler.open_file(fn)
430 elif info == 2: # color
431 color = [((ord(selection.data[v]) | (ord(selection.data[v+1]) << 8)) / 65535.0) for v in range(0,8,2)]
432 self.app.brush.set_color_rgb(color[:3])
433 self.app.ch.push_color(self.app.brush.get_color_hsv())
434 # Don't popup the color history for now, as I haven't managed to get it to cooperate.
436 def print_memory_leak_cb(self, action):
437 helpers.record_memory_leak_status(print_diff = True)
439 def run_garbage_collector_cb(self, action):
440 helpers.run_garbage_collector()
442 def start_profiling_cb(self, action):
443 if getattr(self, 'profiler_active', False):
444 self.profiler_active = False
449 profile = cProfile.Profile()
451 self.profiler_active = True
452 print '--- GUI Profiling starts ---'
453 while self.profiler_active:
454 profile.runcall(gtk.main_iteration, False)
455 if not gtk.events_pending():
456 time.sleep(0.050) # ugly trick to remove "user does nothing" from profile
457 print '--- GUI Profiling ends ---'
459 profile.dump_stats('profile_fromgui.pstats')
460 #print 'profile written to mypaint_profile.pstats'
461 os.system('gprof2dot.py -f pstats profile_fromgui.pstats | dot -Tpng -o profile_fromgui.png && feh profile_fromgui.png &')
463 gobject.idle_add(doit)
465 def gtk_input_dialog_cb(self, action):
466 d = gtk.InputDialog()
469 def key_press_event_cb_before(self, win, event):
471 ctrl = event.state & gdk.CONTROL_MASK
472 shift = event.state & gdk.SHIFT_MASK
473 alt = event.state & gdk.MOD1_MASK
474 #ANY_MODIFIER = gdk.SHIFT_MASK | gdk.MOD1_MASK | gdk.CONTROL_MASK
475 #if event.state & ANY_MODIFIER:
476 # # allow user shortcuts with modifiers
479 # This may need a stateful flag
480 if self.app.scratchpad_doc.tdw.has_pointer:
481 thisdoc = self.app.scratchpad_doc
482 # Stop dragging on the main window
483 self.app.doc.tdw.dragfunc = None
485 thisdoc = self.app.doc
486 # Stop dragging on the other window
487 self.app.scratchpad_doc.tdw.dragfunc = None
488 if key == keysyms.space:
490 thisdoc.tdw.start_drag(thisdoc.dragfunc_rotate)
492 thisdoc.tdw.start_drag(thisdoc.dragfunc_zoom)
494 thisdoc.tdw.start_drag(thisdoc.dragfunc_frame)
496 thisdoc.tdw.start_drag(thisdoc.dragfunc_translate)
500 def key_release_event_cb_before(self, win, event):
501 if self.app.scratchpad_doc.tdw.has_pointer:
502 thisdoc = self.app.scratchpad_doc
504 thisdoc = self.app.doc
505 if event.keyval == keysyms.space:
506 thisdoc.tdw.stop_drag(thisdoc.dragfunc_translate)
507 thisdoc.tdw.stop_drag(thisdoc.dragfunc_rotate)
508 thisdoc.tdw.stop_drag(thisdoc.dragfunc_zoom)
509 thisdoc.tdw.stop_drag(thisdoc.dragfunc_frame)
513 def key_press_event_cb_after(self, win, event):
515 if self.is_fullscreen and key == keysyms.Escape:
521 def key_release_event_cb_after(self, win, event):
524 def button_press_cb(self, win, event):
525 return button_press_cb_abstraction(self, win, event, self.app.doc)
527 def button_release_cb(self, win, event):
528 return button_release_cb_abstraction(win, event, self.app.doc)
530 def scroll_cb(self, win, event):
532 if d == gdk.SCROLL_UP:
533 if event.state & gdk.SHIFT_MASK:
534 self.app.doc.rotate('RotateLeft')
536 self.app.doc.zoom('ZoomIn')
537 elif d == gdk.SCROLL_DOWN:
538 if event.state & gdk.SHIFT_MASK:
539 self.app.doc.rotate('RotateRight')
541 self.app.doc.zoom('ZoomOut')
542 elif d == gdk.SCROLL_RIGHT:
543 self.app.doc.rotate('RotateRight')
544 elif d == gdk.SCROLL_LEFT:
545 self.app.doc.rotate('RotateLeft')
548 def toggle_window_cb(self, action):
549 if self._updating_toggled_item:
551 s = action.get_name()
552 active = action.get_active()
553 window_name = s[0].lower() + s[1:] # WindowName -> windowName
554 # If it's a tool, get it to hide/show itself
555 t = self.app.layout_manager.get_tool_by_role(window_name)
557 t.set_hidden(not active)
559 # Otherwise, if it's a regular subwindow hide/show+present it.
560 w = self.app.layout_manager.get_subwindow_by_role(window_name)
563 onscreen = w.window is not None and w.window.is_visible()
574 def update_subwindow_visibility(self, window, active):
575 # Responds to non-tool subwindows being hidden and shown
576 role = window.get_role()
577 self.update_toggled_item_visibility(role, active)
579 def update_toggled_item_visibility(self, role, active, *a, **kw):
580 # Responds to any item with a role being hidden or shown by
581 # silently updating its ToggleAction to match.
582 action_name = role[0].upper() + role[1:]
583 action = self.action_group.get_action(action_name)
585 warn("Unable to find action %s" % action_name, RuntimeWarning, 1)
587 if action.get_active() != active:
588 self._updating_toggled_item = True
589 action.set_active(active)
590 self._updating_toggled_item = False
592 def popup_cb(self, action):
593 state = self.popup_states[action.get_name()]
594 state.activate(action)
597 def brush_chooser_popup_cb(self, action):
598 # It may be even nicer to do this as a real popup state with
599 # mouse-out to cancel. The Action is named accordingly. For now
600 # though a modal dialog will do as an implementation.
601 dialogs.change_current_brush_quick(self.app)
603 def color_details_dialog_cb(self, action):
604 dialogs.change_current_color_detailed(self.app)
607 # User-toggleable UI pieces: things like toolbars, status bars, menu bars.
608 # Saved between sessions.
611 def get_ui_part_enabled(self, part_name):
612 """Returns whether the named UI part is enabled in the prefs.
614 parts = self.app.preferences["ui.parts"]
615 return bool(parts.get(part_name, False))
618 def update_ui_parts(self, **updates):
619 """Updates the UI part prefs, then hide/show widgets to match.
621 Called without arguments, this updates the UI to match the
622 boolean-valued hash ``ui.parts`` in the app preferences. With keyword
623 arguments, the prefs are updated first, then changes are reflected in
624 the set of visible widgets. Current known parts:
627 The primary toolbar and its menu button.
629 A conventional menu bar.
631 Currently the user cannot turn off both the main toolbar and the
632 menubar: the toolbar will be forced on if an attempt is made.
634 new_state = self.app.preferences["ui.parts"].copy()
635 new_state.update(updates)
637 if new_state.get("menubar", False):
638 self.menubar.show_all()
641 if not new_state.get("main_toolbar", False):
642 new_state["main_toolbar"] = True
644 if new_state.get("main_toolbar", False):
645 self.toolbar.show_all()
648 self.app.preferences["ui.parts"] = new_state
649 self.update_menu_button()
652 def update_menu_button(self):
653 """Updates the menu button to match toolbar and menubar visibility.
655 The menu button is visible when the menu bar is hidden. Since the user
656 must have either a toolbar or a menu or both, this ensures that a menu
657 is on-screen at all times in non-fullscreen mode.
659 toolbar_visible = self.toolbar.get_property("visible")
660 menubar_visible = self.menubar.get_property("visible")
661 if toolbar_visible and menubar_visible:
662 self.toolbar.menu_button.hide()
664 self.toolbar.menu_button.show_all()
667 def on_menuishbar_radio_change(self, radioaction, current):
668 """Respond to a change of the 'menu bar/toolbar' radio menu items.
670 value = radioaction.get_current_value()
671 if value == self.MENUISHBAR_RADIO_MENUBAR:
672 self.update_ui_parts(main_toolbar=False, menubar=True)
673 elif value == self.MENUISHBAR_RADIO_MAIN_TOOLBAR:
674 self.update_ui_parts(main_toolbar=True, menubar=False)
676 self.update_ui_parts(main_toolbar=True, menubar=True)
680 # Not saved between sessions, defaults to on.
681 # Controlled via its ToggleAction, and entering or leaving fullscreen mode
682 # according to the setting of ui.hide_in_fullscreen in prefs.
684 def set_show_subwindows(self, show_subwindows):
685 """Programatically set the Show Subwindows option.
687 action = self.action_group.get_action("ToggleSubwindows")
688 currently_showing = action.get_active()
689 if show_subwindows != currently_showing:
690 action.set_active(show_subwindows)
691 self._show_subwindows = self._show_subwindows
693 def get_show_subwindows(self):
694 return self._show_subwindows
696 def toggle_subwindows_cb(self, action):
697 active = action.get_active()
698 lm = self.app.layout_manager
700 lm.toggle_user_tools(on=True)
702 lm.toggle_user_tools(on=False)
703 self._show_subwindows = active
707 # This implementation requires an ICCCM and EWMH-compliant window manager
708 # which supports the _NET_WM_STATE_FULLSCREEN hint. There are several
711 def fullscreen_cb(self, *junk):
712 if not self.is_fullscreen:
717 def window_state_event_cb(self, widget, event):
718 # Respond to changes of the fullscreen state only
719 if not event.changed_mask & gdk.WINDOW_STATE_FULLSCREEN:
721 lm = self.app.layout_manager
722 self.is_fullscreen = event.new_window_state & gdk.WINDOW_STATE_FULLSCREEN
723 if self.is_fullscreen:
725 if self.app.preferences.get("ui.hide_subwindows_in_fullscreen", True):
726 self.set_show_subwindows(False)
727 self._restore_subwindows_on_unfullscreen = True
728 if self.app.preferences.get("ui.hide_menubar_in_fullscreen", True):
730 self._restore_menubar_on_unfullscreen = True
731 if self.app.preferences.get("ui.hide_toolbar_in_fullscreen", True):
733 self._restore_toolbar_on_unfullscreen = True
734 # fix for fullscreen problem on Windows, https://gna.org/bugs/?15175
735 # on X11/Metacity it also helps a bit against flickering during the switch
736 while gtk.events_pending():
739 while gtk.events_pending():
741 if getattr(self, "_restore_menubar_on_unfullscreen", False):
742 if self.get_ui_part_enabled("menubar"):
744 del self._restore_menubar_on_unfullscreen
745 if getattr(self, "_restore_toolbar_on_unfullscreen", False):
746 if self.get_ui_part_enabled("main_toolbar"):
748 del self._restore_toolbar_on_unfullscreen
749 if getattr(self, "_restore_subwindows_on_unfullscreen", False):
750 self.set_show_subwindows(True)
751 del self._restore_subwindows_on_unfullscreen
752 self.update_menu_button()
753 self.update_fullscreen_action()
755 def update_fullscreen_action(self):
756 action = self.action_group.get_action("Fullscreen")
757 if self.is_fullscreen:
758 action.set_stock_id(gtk.STOCK_LEAVE_FULLSCREEN)
759 action.set_tooltip(_("Leave Fullscreen Mode"))
760 action.set_label(_("UnFullscreen"))
762 action.set_stock_id(gtk.STOCK_FULLSCREEN)
763 action.set_tooltip(_("Enter Fullscreen Mode"))
764 action.set_label(_("Fullscreen"))
766 def popupmenu_show_cb(self, action):
767 self.show_popupmenu()
769 def show_popupmenu(self, event=None):
770 self.menubar.set_sensitive(False) # excessive feedback?
771 self.toolbar.menu_button.set_sensitive(False)
774 if event is not None:
775 if event.type == gdk.BUTTON_PRESS:
776 button = event.button
778 self.popupmenu.popup(None, None, None, button, time)
780 # We're responding to an Action, most probably the menu key.
781 # Open out the last highlighted menu to speed key navigation up.
782 if self.popupmenu_last_active is None:
783 self.popupmenu.select_first(True) # one less keypress
785 self.popupmenu.select_item(self.popupmenu_last_active)
787 def popupmenu_done_cb(self, *a, **kw):
788 # Not sure if we need to bother with this level of feedback,
789 # but it actually looks quite nice to see one menu taking over
790 # the other. Makes it clear that the popups are the same thing as
791 # the full menu, maybe.
792 self.menubar.set_sensitive(True)
793 self.toolbar.menu_button.set_sensitive(True)
794 self.popupmenu_last_active = self.popupmenu.get_active()
796 # BEGIN -- Scratchpad menu options
797 def save_scratchpad_as_default_cb(self, action):
798 self.app.filehandler.save_scratchpad(self.app.filehandler.get_scratchpad_default(), export = True)
800 def clear_default_scratchpad_cb(self, action):
801 self.app.filehandler.delete_default_scratchpad()
803 # Unneeded since 'Save blank canvas' bug has been addressed.
804 #def clear_autosave_scratchpad_cb(self, action):
805 # self.app.filehandler.delete_autosave_scratchpad()
807 def new_scratchpad_cb(self, action):
808 if os.path.isfile(self.app.filehandler.get_scratchpad_default()):
809 self.app.filehandler.open_scratchpad(self.app.filehandler.get_scratchpad_default())
811 self.app.scratchpad_doc.model.clear()
812 # With no default - adopt the currently chosen background
813 bg = self.app.doc.model.background
814 if self.app.scratchpad_doc:
815 self.app.scratchpad_doc.model.set_background(bg)
817 self.app.scratchpad_filename = self.app.preferences['scratchpad.last_opened'] = self.app.filehandler.get_scratchpad_autosave()
819 def load_scratchpad_cb(self, action):
820 if self.app.scratchpad_filename:
821 self.save_current_scratchpad_cb(action)
822 current_pad = self.app.scratchpad_filename
824 current_pad = self.app.filehandler.get_scratchpad_autosave()
825 self.app.filehandler.open_scratchpad_dialog()
826 # Check to see if a file has been opened outside of the scratchpad directory
827 if not os.path.abspath(self.app.scratchpad_filename).startswith(os.path.abspath(self.app.filehandler.get_scratchpad_prefix())):
828 # file is NOT within the scratchpad directory - load copy as current scratchpad
829 self.app.scratchpad_filename = self.app.preferences['scratchpad.last_opened'] = current_pad
831 def save_as_scratchpad_cb(self, action):
832 self.app.filehandler.save_scratchpad_as_dialog()
834 def revert_current_scratchpad_cb(self, action):
835 if os.path.isfile(self.app.scratchpad_filename):
836 self.app.filehandler.open_scratchpad(self.app.scratchpad_filename)
837 print "Reverted to %s" % self.app.scratchpad_filename
839 print "No file to revert to yet."
841 def save_current_scratchpad_cb(self, action):
842 self.app.filehandler.save_scratchpad(self.app.scratchpad_filename)
844 def scratchpad_copy_background_cb(self, action):
845 bg = self.app.doc.model.background
846 if self.app.scratchpad_doc:
847 self.app.scratchpad_doc.model.set_background(bg)
849 def draw_palette_cb(self, action):
850 # test functionality:
852 (_("Gimp Palette Format"), ("*.gpl",)),
853 (_("All Files"), ("*.*",)),
855 gimp_path = os.path.join(self.app.filehandler.get_gimp_prefix(), "palettes")
856 dialog = self.app.filehandler.get_open_dialog(start_in_folder=gimp_path,
857 file_filters = file_filters)
859 if dialog.run() == gtk.RESPONSE_OK:
861 filename = dialog.get_filename().decode('utf-8')
863 #filename = "/home/ben/.gimp-2.6/palettes/Nature_Grass.gpl" # TEMP HACK TO TEST
864 g = GimpPalette(filename)
867 # IGNORE Gimp Palette 'columns'?
869 column_limit = g.columns # use the value for columns in the palette
870 draw_palette(self.app, g, self.app.scratchpad_doc, columns=column_limit, grid_size=grid_size, swatch_method=hatch_squiggle, scale = 25.0)
874 def draw_sat_spectrum_cb(self, action):
876 hsv = self.app.brush.get_color_hsv()
877 g.append_sat_spectrum(hsv)
879 off_x = off_y = grid_size / 2.0
881 draw_palette(self.app, g, self.app.scratchpad_doc, columns=column_limit, grid_size=grid_size)
883 # END -- Scratchpad menu options
885 def quit_cb(self, *junk):
886 self.app.doc.model.split_stroke()
887 self.app.save_gui_config() # FIXME: should do this periodically, not only on quit
889 if not self.app.filehandler.confirm_destructive_action(title=_('Quit'), question=_('Really Quit?')):
895 def toggle_frame_cb(self, action):
896 enabled = self.app.doc.model.frame_enabled
897 self.app.doc.model.set_frame_enabled(not enabled)
899 def import_brush_pack_cb(self, *junk):
900 format_id, filename = dialogs.open_dialog(_("Import brush package..."), self,
901 [(_("MyPaint brush package (*.zip)"), "*.zip")])
903 self.app.brushmanager.import_brushpack(filename, self)
906 # TODO: Move into dialogs.py?
907 def about_cb(self, action):
908 d = gtk.AboutDialog()
909 d.set_transient_for(self)
910 d.set_program_name("MyPaint")
911 d.set_version(MYPAINT_VERSION)
912 d.set_copyright(_("Copyright (C) 2005-2010\nMartin Renold and the MyPaint Development Team"))
913 d.set_website("http://mypaint.info/")
914 d.set_logo(self.app.pixmaps.mypaint_logo)
916 _(u"This program is free software; you can redistribute it and/or modify "
917 u"it under the terms of the GNU General Public License as published by "
918 u"the Free Software Foundation; either version 2 of the License, or "
919 u"(at your option) any later version.\n"
921 u"This program is distributed in the hope that it will be useful, "
922 u"but WITHOUT ANY WARRANTY. See the COPYING file for more details.")
924 d.set_wrap_license(True)
926 # (in order of appearance)
927 u"Martin Renold (%s)" % _('programming'),
928 u"Yves Combe (%s)" % _('portability'),
929 u"Popolon (%s)" % _('programming'),
930 u"Clement Skau (%s)" % _('programming'),
931 u"Jon Nordby (%s)" % _('programming'),
932 u"Álinson Santos (%s)" % _('programming'),
933 u"Tumagonx (%s)" % _('portability'),
934 u"Ilya Portnov (%s)" % _('programming'),
935 u"Jonas Wagner (%s)" % _('programming'),
936 u"Luka Čehovin (%s)" % _('programming'),
937 u"Andrew Chadwick (%s)" % _('programming'),
938 u"Till Hartmann (%s)" % _('programming'),
939 u'David Grundberg (%s)' % _('programming'),
940 u"Krzysztof Pasek (%s)" % _('programming'),
941 u"Ben O'Steen (%s)" % _('programming'),
942 u"Ferry Jérémie (%s)" % _('programming'),
945 u"Artis Rozentāls (%s)" % _('brushes'),
946 u"Popolon (%s)" % _('brushes'),
947 u"Marcelo 'Tanda' Cerviño (%s)" % _('patterns, brushes'),
948 u"David Revoy (%s)" % _('brushes'),
949 u"Ramón Miranda (%s)" % _('brushes, patterns'),
950 u"Enrico Guarnieri 'Ico_dY' (%s)" % _('brushes'),
951 u'Sebastian Kraft (%s)' % _('desktop icon'),
952 u"Nicola Lunghi (%s)" % _('patterns'),
953 u"Toni Kasurinen (%s)" % _('brushes'),
954 u"Сан Саныч 'MrMamurk' (%s)" % _('patterns'),
955 u"Andrew Chadwick (%s)" % _('tool icons'),
956 u"Ben O'Steen (%s)" % _('tool icons'),
958 # list all translators, not only those of the current language
959 d.set_translator_credits(
960 u'Ilya Portnov (ru)\n'
961 u'Popolon (fr, zh_CN, ja)\n'
964 u'Tobias Jakobs (de)\n'
965 u'Martin Tabačan (cs)\n'
967 u'Manuel Quiñones (es)\n'
968 u'Gergely Aradszki (hu)\n'
969 u'Lamberto Tedaldi (it)\n'
970 u'Dong-Jun Wu (zh_TW)\n'
971 u'Luka Čehovin (sl)\n'
972 u'Geuntak Jeong (ko)\n'
973 u'Łukasz Lubojański (pl)\n'
974 u'Daniel Korostil (uk)\n'
975 u'Julian Aloofi (de)\n'
976 u'Tor Egil Hoftun Kvæstad (nn_NO)\n'
977 u'João S. O. Bueno (pt_BR)\n'
978 u'David Grundberg (sv)\n'
979 u'Elliott Sales de Andrade (en_CA)\n'
985 def show_infodialog_cb(self, action):
988 _("Move your mouse over a menu entry, then press the key to assign."),
990 _("You can also drag the canvas with the mouse while holding the middle "
991 "mouse button or spacebar. Or with the arrow keys."
993 "In contrast to earlier versions, scrolling and zooming are harmless now and "
994 "will not make you run out of memory. But you still require a lot of memory "
995 "if you paint all over while fully zoomed out."),
997 _("Brushkeys are used to quickly save/restore brush settings "
998 "using keyboard shortcuts. You can paint with one hand and "
999 "change brushes with the other without interruption."
1001 "There are 10 memory slots to hold brush settings.\n"
1002 "They are anonymous brushes, which are not visible in the "
1003 "brush selector list. But they are remembered even if you "
1006 _("There is a tutorial available on the MyPaint homepage. It "
1007 "explains some features which are hard to discover yourself."
1009 "Comments about the brush settings (opaque, hardness, etc.) and "
1010 "inputs (pressure, speed, etc.) are available as tooltips. "
1011 "Put your mouse over a label to see them. "
1014 self.app.message_dialog(text[action.get_name()])