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
24 import colorselectionwindow, historypopup, stategroup, colorpicker, windowing, layout
26 from lib import helpers
29 import xml.etree.ElementTree as ET
32 from lib.scratchpad_palette import GimpPalette, squiggle
34 # TODO: put in a helper file?
35 def with_wait_cursor(func):
36 """python decorator that adds a wait cursor around a function"""
37 def wrapper(self, *args, **kwargs):
38 toplevels = [t for t in gtk.window_list_toplevels()
39 if t.window is not None]
40 for toplevel in toplevels:
41 toplevel.window.set_cursor(gdk.Cursor(gdk.WATCH))
42 toplevel.set_sensitive(False)
43 self.app.doc.tdw.grab_add()
45 func(self, *args, **kwargs)
46 # gtk main loop may be called in here...
48 for toplevel in toplevels:
49 toplevel.set_sensitive(True)
50 # ... which is why we need this check:
51 if toplevel.window is not None:
52 toplevel.window.set_cursor(None)
53 self.app.doc.tdw.grab_remove()
56 def button_press_cb_abstraction(drawwindow, win, event, doc):
57 #print event.device, event.button
60 # Single button-presses only, not 2ble/3ple
61 if event.type != gdk.BUTTON_PRESS:
62 # ignore the extra double-click event
66 # check whether we are painting (accidental)
67 if event.state & gdk.BUTTON1_MASK:
68 # Do not allow dragging in the middle of
69 # painting. This often happens by accident with wacom
70 # tablet's stylus button.
72 # However we allow dragging if the user's pressure is
73 # still below the click threshold. This is because
74 # some tablet PCs are not able to produce a
75 # middle-mouse click without reporting pressure.
76 # https://gna.org/bugs/index.php?15907
79 # Pick a suitable config option
80 ctrl = event.state & gdk.CONTROL_MASK
81 alt = event.state & gdk.MOD1_MASK
82 shift = event.state & gdk.SHIFT_MASK
84 modifier_str = "_shift"
86 modifier_str = "_ctrl"
89 prefs_name = "input.button%d%s_action" % (event.button, modifier_str)
90 action_name = drawwindow.app.preferences.get(prefs_name, "no_action")
93 if action_name == 'no_action':
94 return True # We handled it by doing nothing
97 # Really belongs in the tdw, but this is the only object with access
98 # to the application preferences.
99 if action_name == 'straight_line':
100 doc.tdw.straight_line_from_last_pos(is_sequence=False)
102 if action_name == 'straight_line_sequence':
103 doc.tdw.straight_line_from_last_pos(is_sequence=True)
107 if action_name.endswith("_canvas"):
109 if action_name == "pan_canvas":
110 dragfunc = doc.dragfunc_translate
111 elif action_name == "zoom_canvas":
112 dragfunc = doc.dragfunc_zoom
113 elif action_name == "rotate_canvas":
114 dragfunc = doc.dragfunc_rotate
115 if dragfunc is not None:
116 doc.tdw.start_drag(dragfunc)
121 if action_name == 'popup_menu':
122 drawwindow.show_popupmenu(event=event)
125 if action_name in drawwindow.popup_states:
126 state = drawwindow.popup_states[action_name]
127 state.activate(event)
130 # Dispatch regular GTK events.
131 for ag in drawwindow.action_group, doc.action_group:
132 action = ag.get_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 def __init__(self, app):
149 windowing.MainWindow.__init__(self, app)
153 self._updating_toggled_item = False
154 self._show_subwindows = True
155 self.is_fullscreen = False
158 self.drag_dest_set(gtk.DEST_DEFAULT_MOTION |
159 gtk.DEST_DEFAULT_HIGHLIGHT |
160 gtk.DEST_DEFAULT_DROP,
161 [("text/uri-list", 0, 1),
162 ("application/x-color", 0, 2)],
163 gtk.gdk.ACTION_DEFAULT|gtk.gdk.ACTION_COPY)
166 self.connect('delete-event', self.quit_cb)
167 self.connect('key-press-event', self.key_press_event_cb_before)
168 self.connect('key-release-event', self.key_release_event_cb_before)
169 self.connect_after('key-press-event', self.key_press_event_cb_after)
170 self.connect_after('key-release-event', self.key_release_event_cb_after)
171 self.connect("drag-data-received", self.drag_data_received)
172 self.connect("window-state-event", self.window_state_event_cb)
174 self.app.filehandler.current_file_observers.append(self.update_title)
178 lm = app.layout_manager
179 layout.MainWindow.__init__(self, lm)
180 self.main_widget.connect("button-press-event", self.button_press_cb)
181 self.main_widget.connect("button-release-event",self.button_release_cb)
182 self.main_widget.connect("scroll-event", self.scroll_cb)
185 kbm.add_extra_key('Menu', 'ShowPopupMenu')
186 kbm.add_extra_key('Tab', 'ToggleSubwindows')
188 self.init_stategroups()
192 print "DeprecationWarning: Use app.doc instead"
195 print "DeprecationWarning: Use app.doc.tdw instead"
196 return self.app.doc.tdw
197 tdw, doc = property(get_tdw), property(get_doc)
199 def init_actions(self):
201 # name, stock id, label, accelerator, tooltip, callback
202 ('FileMenu', None, _('File')),
203 ('Quit', gtk.STOCK_QUIT, _('Quit'), '<control>q', None, self.quit_cb),
204 ('FrameToggle', None, _('Toggle Document Frame'), None, None, self.toggle_frame_cb),
206 ('EditMenu', None, _('Edit')),
208 ('ColorMenu', None, _('Color')),
209 ('ColorPickerPopup', gtk.STOCK_COLOR_PICKER, _('Pick Color'), 'r', None, self.popup_cb),
210 ('ColorHistoryPopup', None, _('Color History'), 'x', None, self.popup_cb),
211 ('ColorChangerPopup', None, _('Color Changer'), 'v', None, self.popup_cb),
212 ('ColorRingPopup', None, _('Color Ring'), None, None, self.popup_cb),
214 ('ContextMenu', None, _('Brushkeys')),
215 ('ContextHelp', gtk.STOCK_HELP, _('Help!'), None, None, self.show_infodialog_cb),
217 ('LayerMenu', None, _('Layers')),
219 ('ScratchMenu', None, _('Scratchpad')),
220 ('ScratchWindow', None, _('Scratchpad...'), None, None, self.toggleWindow_cb),
221 ('ScratchSaveNow', None, _('Save Scratchpad Now'), None, None, self.save_current_scratchpad_cb),
222 ('ScratchSaveAsDefault', None, _('Save Scratchpad As Default'), None, None, self.save_scratchpad_as_default_cb),
223 ('ScratchClearDefault', None, _('Clear the Default Scratchpad'), None, None, self.clear_default_scratchpad_cb),
224 ('ScratchClearAutosave', None, _('Clear the Autosaved Scratchpad'), None, None, self.clear_autosave_scratchpad_cb),
225 ('ScratchLoadPalette', None, _('Draw a palette in the current Scratchpad'), None, None, self.draw_palette_cb),
228 ('BrushMenu', None, _('Brush')),
229 ('ImportBrushPack', gtk.STOCK_OPEN, _('Import brush package...'), '', None, self.import_brush_pack_cb),
231 ('HelpMenu', None, _('Help')),
232 ('Docu', gtk.STOCK_INFO, _('Where is the Documentation?'), None, None, self.show_infodialog_cb),
233 ('ShortcutHelp', gtk.STOCK_INFO, _('Change the Keyboard Shortcuts?'), None, None, self.show_infodialog_cb),
234 ('About', gtk.STOCK_ABOUT, _('About MyPaint'), None, None, self.about_cb),
236 ('DebugMenu', None, _('Debug')),
237 ('PrintMemoryLeak', None, _('Print Memory Leak Info to Console (Slow!)'), None, None, self.print_memory_leak_cb),
238 ('RunGarbageCollector', None, _('Run Garbage Collector Now'), None, None, self.run_garbage_collector_cb),
239 ('StartProfiling', gtk.STOCK_EXECUTE, _('Start/Stop Python Profiling (cProfile)'), None, None, self.start_profiling_cb),
240 ('GtkInputDialog', None, _('GTK input device dialog'), None, None, self.gtk_input_dialog_cb),
243 ('ViewMenu', None, _('View')),
244 ('ShowPopupMenu', None, _('Popup Menu'), 'Menu', None, self.popupmenu_show_cb),
245 ('Fullscreen', gtk.STOCK_FULLSCREEN, _('Fullscreen'), 'F11', None, self.fullscreen_cb),
246 ('ViewHelp', gtk.STOCK_HELP, _('Help'), None, None, self.show_infodialog_cb),
248 ag = self.action_group = gtk.ActionGroup('WindowActions')
249 ag.add_actions(actions)
253 ('PreferencesWindow', gtk.STOCK_PREFERENCES,
254 _('Preferences'), None, None, self.toggle_window_cb),
255 ('InputTestWindow', None,
256 _('Test input devices'), None, None, self.toggle_window_cb),
257 ('FrameWindow', None,
258 _('Document Frame...'), None, None, self.toggle_window_cb),
259 ('LayersWindow', stock.TOOL_LAYERS,
260 None, None, _("Toggle the Layers list"),
261 self.toggle_window_cb),
262 ('BackgroundWindow', gtk.STOCK_PAGE_SETUP,
263 _('Background'), None, None, self.toggle_window_cb),
264 ('BrushSelectionWindow', stock.TOOL_BRUSH,
265 None, None, _("Toggle the Brush selector"),
266 self.toggle_window_cb),
267 ('BrushSettingsWindow', gtk.STOCK_PROPERTIES,
268 _('Brush Editor'), '<control>b', None,
269 self.toggle_window_cb),
270 ('ColorSelectionWindow', stock.TOOL_COLOR_SELECTOR,
271 None, None, _("Toggle the Colour Triangle"),
272 self.toggle_window_cb),
273 ('ColorSamplerWindow', stock.TOOL_COLOR_SAMPLER,
274 None, None, _("Toggle the advanced Colour Sampler"),
275 self.toggle_window_cb),
277 ag.add_toggle_actions(toggle_actions)
279 # Reflect changes from other places (like tools' close buttons) into
280 # the proxys' visible states.
281 lm = self.app.layout_manager
282 lm.tool_visibility_observers.append(self.update_toggled_item_visibility)
283 lm.subwindow_visibility_observers.append(self.update_subwindow_visibility)
285 # Initial toggle state
286 for spec in toggle_actions:
288 action = ag.get_action(name)
289 role = name[0].lower() + name[1:]
290 visible = not lm.get_window_hidden_by_role(role)
291 # The sidebar machinery won't be up yet, so reveal windows that
292 # should be initially visible only in an idle handler
293 gobject.idle_add(action.set_active, visible)
295 # More toggle actions - ones which don't control windows.
297 ('ToggleToolbar', None, _('Toolbar'), None,
298 _("Show toolbar"), self.toggle_toolbar_cb,
299 self.get_show_toolbar()),
300 ('ToggleSubwindows', None, _('Subwindows'), 'Tab',
301 _("Show subwindows"), self.toggle_subwindows_cb,
302 self.get_show_subwindows()),
304 ag.add_toggle_actions(toggle_actions)
307 for action in self.action_group.list_actions():
308 self.app.kbm.takeover_action(action)
309 self.app.ui_manager.insert_action_group(ag, -1)
311 def init_stategroups(self):
312 sg = stategroup.StateGroup()
313 p2s = sg.create_popup_state
314 changer = p2s(colorselectionwindow.ColorChangerPopup(self.app))
315 ring = p2s(colorselectionwindow.ColorRingPopup(self.app))
316 hist = p2s(historypopup.HistoryPopup(self.app, self.app.doc.model))
317 pick = self.colorpick_state = p2s(colorpicker.ColorPicker(self.app, self.app.doc.model))
319 self.popup_states = {
320 'ColorChangerPopup': changer,
321 'ColorRingPopup': ring,
322 'ColorHistoryPopup': hist,
323 'ColorPickerPopup': pick,
325 changer.next_state = ring
326 ring.next_state = changer
327 changer.autoleave_timeout = None
328 ring.autoleave_timeout = None
330 pick.max_key_hit_duration = 0.0
331 pick.autoleave_timeout = None
333 hist.autoleave_timeout = 0.600
334 self.history_popup_state = hist
336 def init_main_widget(self): # override
337 self.main_widget = self.app.doc.tdw
339 def init_menubar(self): # override
340 # Load Menubar, duplicate into self.popupmenu
341 menupath = os.path.join(self.app.datapath, 'gui/menu.xml')
342 menubar_xml = open(menupath).read()
343 self.app.ui_manager.add_ui_from_string(menubar_xml)
344 self._init_popupmenu(menubar_xml)
345 self.menubar = self.app.ui_manager.get_widget('/Menubar')
347 def init_toolbar(self):
348 action_groups = self.app.ui_manager.get_action_groups()
349 def findaction(name):
350 for group in action_groups:
351 action = group.get_action(name)
352 if action is not None:
356 bar.insert(findaction("New").create_tool_item(), -1)
357 bar.insert(findaction("Open").create_tool_item(), -1)
358 bar.insert(findaction("Save").create_tool_item(), -1)
359 bar.insert(findaction("Undo").create_tool_item(), -1)
360 bar.insert(findaction("Redo").create_tool_item(), -1)
362 bar.insert(gtk.SeparatorToolItem(), -1)
363 bar.insert(findaction("ResetView").create_tool_item(), -1)
364 bar.insert(findaction("ZoomIn").create_tool_item(), -1)
365 bar.insert(findaction("ZoomOut").create_tool_item(), -1)
366 bar.insert(findaction("RotateLeft").create_tool_item(), -1)
367 bar.insert(findaction("RotateRight").create_tool_item(), -1)
368 bar.insert(findaction("MirrorVertical").create_tool_item(), -1)
369 bar.insert(findaction("MirrorHorizontal").create_tool_item(), -1)
371 bar.insert(gtk.SeparatorToolItem(), -1)
372 bar.insert(findaction("BlendModeNormal").create_tool_item(), -1)
373 bar.insert(findaction("BlendModeEraser").create_tool_item(), -1)
374 bar.insert(findaction("BlendModeLockAlpha").create_tool_item(), -1)
376 expander = gtk.SeparatorToolItem()
377 expander.set_expand(True)
378 expander.set_draw(False)
379 bar.insert(expander, -1)
381 for name in ["ColorSelectionWindow", "ColorSamplerWindow",
382 "BrushSelectionWindow", "LayersWindow"]:
383 action = self.action_group.get_action(name)
384 tool_item = action.create_tool_item()
385 bar.insert(tool_item, -1)
388 if not self.get_show_toolbar():
389 gobject.idle_add(self.toolbar.hide)
392 def _init_popupmenu(self, xml):
394 Hopefully temporary hack for converting UIManager XML describing the
395 main menubar into a rebindable popup menu. UIManager by itself doesn't
396 let you do this, by design, but we need a bigger menu than the little
397 things it allows you to build.
399 ui_elt = ET.fromstring(xml)
400 rootmenu_elt = ui_elt.find("menubar")
401 rootmenu_elt.attrib["name"] = "PopupMenu"
402 ## XML-style menu jiggling. No need for this really though.
403 #for menu_elt in rootmenu_elt.findall("menu"):
404 # for item_elt in menu_elt.findall("menuitem"):
405 # if item_elt.attrib.get("action", "") == "ShowPopupMenu":
406 # menu_elt.remove(item_elt)
407 ## Maybe shift a small number of frequently-used items to the top?
408 xml = ET.tostring(ui_elt)
409 self.app.ui_manager.add_ui_from_string(xml)
410 tmp_menubar = self.app.ui_manager.get_widget('/PopupMenu')
411 self.popupmenu = gtk.Menu()
412 for item in tmp_menubar.get_children():
413 tmp_menubar.remove(item)
414 self.popupmenu.append(item)
415 self.popupmenu.attach_to_widget(self.app.doc.tdw, None)
416 #self.popupmenu.set_title("MyPaint")
417 #self.popupmenu.set_take_focus(True)
418 self.popupmenu.connect("selection-done", self.popupmenu_done_cb)
419 self.popupmenu.connect("deactivate", self.popupmenu_done_cb)
420 self.popupmenu.connect("cancel", self.popupmenu_done_cb)
421 self.popupmenu_last_active = None
424 def update_title(self, filename):
426 self.set_title("MyPaint - %s" % os.path.basename(filename))
428 self.set_title("MyPaint")
430 # INPUT EVENT HANDLING
431 def drag_data_received(self, widget, context, x, y, selection, info, t):
434 uri = selection.data.split("\r\n")[0]
435 fn = helpers.uri2filename(uri)
436 if os.path.exists(fn):
437 if self.app.filehandler.confirm_destructive_action():
438 self.app.filehandler.open_file(fn)
439 elif info == 2: # color
440 color = [((ord(selection.data[v]) | (ord(selection.data[v+1]) << 8)) / 65535.0) for v in range(0,8,2)]
441 self.app.brush.set_color_rgb(color[:3])
442 self.app.ch.push_color(self.app.brush.get_color_hsv())
443 # Don't popup the color history for now, as I haven't managed to get it to cooperate.
445 def print_memory_leak_cb(self, action):
446 helpers.record_memory_leak_status(print_diff = True)
448 def run_garbage_collector_cb(self, action):
449 helpers.run_garbage_collector()
451 def start_profiling_cb(self, action):
452 if getattr(self, 'profiler_active', False):
453 self.profiler_active = False
458 profile = cProfile.Profile()
460 self.profiler_active = True
461 print '--- GUI Profiling starts ---'
462 while self.profiler_active:
463 profile.runcall(gtk.main_iteration, False)
464 if not gtk.events_pending():
465 time.sleep(0.050) # ugly trick to remove "user does nothing" from profile
466 print '--- GUI Profiling ends ---'
468 profile.dump_stats('profile_fromgui.pstats')
469 #print 'profile written to mypaint_profile.pstats'
470 os.system('gprof2dot.py -f pstats profile_fromgui.pstats | dot -Tpng -o profile_fromgui.png && feh profile_fromgui.png &')
472 gobject.idle_add(doit)
474 def gtk_input_dialog_cb(self, action):
475 d = gtk.InputDialog()
478 def key_press_event_cb_before(self, win, event):
480 ctrl = event.state & gdk.CONTROL_MASK
481 shift = event.state & gdk.SHIFT_MASK
482 alt = event.state & gdk.MOD1_MASK
483 #ANY_MODIFIER = gdk.SHIFT_MASK | gdk.MOD1_MASK | gdk.CONTROL_MASK
484 #if event.state & ANY_MODIFIER:
485 # # allow user shortcuts with modifiers
488 # This may need a stateful flag
489 if self.app.filehandler.scratchpad_doc.tdw.has_pointer:
490 thisdoc = self.app.filehandler.scratchpad_doc
491 # Stop dragging on the main window
492 self.app.doc.tdw.dragfunc = None
494 thisdoc = self.app.doc
495 # Stop dragging on the other window
496 self.app.filehandler.scratchpad_doc.tdw.dragfunc = None
497 if key == keysyms.space:
499 thisdoc.tdw.start_drag(thisdoc.dragfunc_rotate)
501 thisdoc.tdw.start_drag(thisdoc.dragfunc_zoom)
503 thisdoc.tdw.start_drag(thisdoc.dragfunc_frame)
505 thisdoc.tdw.start_drag(thisdoc.dragfunc_translate)
509 def key_release_event_cb_before(self, win, event):
510 if self.app.filehandler.scratchpad_doc.tdw.has_pointer:
511 thisdoc = self.app.filehandler.scratchpad_doc
513 thisdoc = self.app.doc
514 if event.keyval == keysyms.space:
515 thisdoc.tdw.stop_drag(thisdoc.dragfunc_translate)
516 thisdoc.tdw.stop_drag(thisdoc.dragfunc_rotate)
517 thisdoc.tdw.stop_drag(thisdoc.dragfunc_zoom)
518 thisdoc.tdw.stop_drag(thisdoc.dragfunc_frame)
522 def key_press_event_cb_after(self, win, event):
524 if self.is_fullscreen and key == keysyms.Escape:
530 def key_release_event_cb_after(self, win, event):
533 def button_press_cb(self, win, event):
534 return button_press_cb_abstraction(self, win, event, self.app.doc)
536 def button_release_cb(self, win, event):
537 return button_release_cb_abstraction(win, event, self.app.doc)
539 def scroll_cb(self, win, event):
541 if d == gdk.SCROLL_UP:
542 if event.state & gdk.SHIFT_MASK:
543 self.app.doc.rotate('RotateLeft')
545 self.app.doc.zoom('ZoomIn')
546 elif d == gdk.SCROLL_DOWN:
547 if event.state & gdk.SHIFT_MASK:
548 self.app.doc.rotate('RotateRight')
550 self.app.doc.zoom('ZoomOut')
551 elif d == gdk.SCROLL_RIGHT:
552 self.app.doc.rotate('RotateRight')
553 elif d == gdk.SCROLL_LEFT:
554 self.app.doc.rotate('RotateLeft')
557 def toggle_window_cb(self, action):
558 if self._updating_toggled_item:
560 s = action.get_name()
561 active = action.get_active()
562 window_name = s[0].lower() + s[1:] # WindowName -> windowName
563 # If it's a tool, get it to hide/show itself
564 t = self.app.layout_manager.get_tool_by_role(window_name)
566 t.set_hidden(not active)
568 # Otherwise, if it's a regular subwindow hide/show+present it.
569 w = self.app.layout_manager.get_subwindow_by_role(window_name)
572 onscreen = w.window is not None and w.window.is_visible()
583 def update_subwindow_visibility(self, window, active):
584 # Responds to non-tool subwindows being hidden and shown
585 role = window.get_role()
586 self.update_toggled_item_visibility(role, active)
588 def update_toggled_item_visibility(self, role, active, *a, **kw):
589 # Responds to any item with a role being hidden or shown by
590 # silently updating its ToggleAction to match.
591 action_name = role[0].upper() + role[1:]
592 action = self.action_group.get_action(action_name)
594 warn("Unable to find action %s" % action_name, RuntimeWarning, 1)
596 if action.get_active() != active:
597 self._updating_toggled_item = True
598 action.set_active(active)
599 self._updating_toggled_item = False
601 def popup_cb(self, action):
602 state = self.popup_states[action.get_name()]
603 state.activate(action)
607 # Saved in the user prefs between sessions.
608 # Controlled via its ToggleAction only.
610 def set_show_toolbar(self, show_toolbar):
611 """Programatically set the Show Toolbar option.
613 action = self.action_group.get_action("ToggleToolbar")
615 if not action.get_active():
616 action.set_active(True)
617 self.app.preferences["ui.toolbar"] = True
619 if action.get_active():
620 action.set_active(False)
621 self.app.preferences["ui.toolbar"] = False
623 def get_show_toolbar(self):
624 return self.app.preferences.get("ui.toolbar", True)
626 def toggle_toolbar_cb(self, action):
627 active = action.get_active()
629 self.toolbar.show_all()
632 self.app.preferences["ui.toolbar"] = active
636 # Not saved between sessions, defaults to on.
637 # Controlled via its ToggleAction, and entering or leaving fullscreen mode
638 # according to the setting of ui.hide_in_fullscreen in prefs.
640 def set_show_subwindows(self, show_subwindows):
641 """Programatically set the Show Subwindows option.
643 action = self.action_group.get_action("ToggleSubwindows")
644 currently_showing = action.get_active()
645 if show_subwindows != currently_showing:
646 action.set_active(show_subwindows)
647 self._show_subwindows = self._show_subwindows
649 def get_show_subwindows(self):
650 return self._show_subwindows
652 def toggle_subwindows_cb(self, action):
653 active = action.get_active()
654 lm = self.app.layout_manager
656 lm.toggle_user_tools(on=True)
658 lm.toggle_user_tools(on=False)
659 self._show_subwindows = active
663 # This implementation requires an ICCCM and EWMH-compliant window manager
664 # which supports the _NET_WM_STATE_FULLSCREEN hint. There are several
667 def fullscreen_cb(self, *junk):
668 if not self.is_fullscreen:
673 def window_state_event_cb(self, widget, event):
674 # Respond to changes of the fullscreen state only
675 if not event.changed_mask & gdk.WINDOW_STATE_FULLSCREEN:
677 lm = self.app.layout_manager
678 self.is_fullscreen = event.new_window_state & gdk.WINDOW_STATE_FULLSCREEN
679 if self.is_fullscreen:
681 if self.app.preferences.get("ui.hide_subwindows_in_fullscreen", True):
682 self.set_show_subwindows(False)
683 self._restore_subwindows_on_unfullscreen = True
684 if self.app.preferences.get("ui.hide_menubar_in_fullscreen", True):
686 self._restore_menubar_on_unfullscreen = True
687 if self.app.preferences.get("ui.hide_toolbar_in_fullscreen", True):
689 self._restore_toolbar_on_unfullscreen = True
690 # fix for fullscreen problem on Windows, https://gna.org/bugs/?15175
691 # on X11/Metacity it also helps a bit against flickering during the switch
692 while gtk.events_pending():
695 while gtk.events_pending():
697 if getattr(self, "_restore_menubar_on_unfullscreen", False):
699 del self._restore_menubar_on_unfullscreen
700 if getattr(self, "_restore_toolbar_on_unfullscreen", False):
701 if self.get_show_toolbar():
703 del self._restore_toolbar_on_unfullscreen
704 if getattr(self, "_restore_subwindows_on_unfullscreen", False):
705 self.set_show_subwindows(True)
706 del self._restore_subwindows_on_unfullscreen
708 def popupmenu_show_cb(self, action):
709 self.show_popupmenu()
711 def show_popupmenu(self, event=None):
712 self.menubar.set_sensitive(False) # excessive feedback?
715 if event is not None:
716 if event.type == gdk.BUTTON_PRESS:
717 button = event.button
719 self.popupmenu.popup(None, None, None, button, time)
721 # We're responding to an Action, most probably the menu key.
722 # Open out the last highlighted menu to speed key navigation up.
723 if self.popupmenu_last_active is None:
724 self.popupmenu.select_first(True) # one less keypress
726 self.popupmenu.select_item(self.popupmenu_last_active)
728 def popupmenu_done_cb(self, *a, **kw):
729 # Not sure if we need to bother with this level of feedback,
730 # but it actaully looks quite nice to see one menu taking over
731 # the other. Makes it clear that the popups are the same thing as
732 # the full menu, maybe.
733 self.menubar.set_sensitive(True)
734 self.popupmenu_last_active = self.popupmenu.get_active()
736 def toggle_subwindows_cb(self, action):
737 self.app.layout_manager.toggle_user_tools()
738 if self.app.layout_manager.saved_user_tools:
739 if self.is_fullscreen:
742 if not self.is_fullscreen:
745 def save_scratchpad_as_default_cb(self, action):
746 self.app.filehandler.save_scratchpad(self.app.filehandler.get_scratchpad_default(), export = True)
748 def clear_default_scratchpad_cb(self, action):
749 self.app.filehandler.delete_default_scratchpad()
751 def clear_autosave_scratchpad_cb(self, action):
752 self.app.filehandler.delete_autosave_scratchpad()
754 def save_current_scratchpad_cb(self, action):
755 self.app.filehandler.save_scratchpad(self.app.filehandler.scratchpad_filename)
757 def draw_palette_cb(self, action):
758 # test functionality:
760 (_("Gimp Palette Format"), ("*.gpl",)),
761 (_("All Files"), ("*.*",)),
763 gimp_path = os.path.join(self.app.filehandler.get_gimp_prefix(), "palettes")
764 dialog = self.app.filehandler.get_open_dialog(start_in_folder=gimp_path,
765 file_filters = file_filters)
767 if dialog.run() == gtk.RESPONSE_OK:
769 filename = dialog.get_filename().decode('utf-8')
771 #filename = "/home/ben/.gimp-2.6/palettes/Nature_Grass.gpl" # TEMP HACK TO TEST
772 g = GimpPalette(filename)
774 off_x = off_y = grid_size / 2.0
777 column_limit = g.columns # use the value for columns in the palette
778 for colour_idx in xrange(len(g)):
779 gen_events = squiggle(off_x, off_y, scale=13.0)
781 self.app.brush.set_color_rgb(g.rgb(colour_idx))
782 # simulate strokes on scratchpad
783 for t, x, y, pressure in gen_events:
784 cr = self.app.filehandler.scratchpad_doc.tdw.get_model_coordinates_cairo_context()
785 x, y = cr.device_to_user(x, y)
786 self.app.filehandler.scratchpad_doc.model.stroke_to(0.008, x, y, pressure, 0.0, 0.0)
787 self.app.filehandler.scratchpad_doc.model.split_stroke()
788 off_x = ((colour_idx % column_limit) + 0.5) * grid_size
789 if not (colour_idx % column_limit) and colour_idx:
794 def quit_cb(self, *junk):
795 self.app.doc.model.split_stroke()
796 self.app.save_gui_config() # FIXME: should do this periodically, not only on quit
798 if not self.app.filehandler.confirm_destructive_action(title=_('Quit'), question=_('Really Quit?')):
804 def toggle_frame_cb(self, action):
805 enabled = self.app.doc.model.frame_enabled
806 self.app.doc.model.set_frame_enabled(not enabled)
808 def import_brush_pack_cb(self, *junk):
809 format_id, filename = dialogs.open_dialog(_("Import brush package..."), self,
810 [(_("MyPaint brush package (*.zip)"), "*.zip")])
812 self.app.brushmanager.import_brushpack(filename, self)
815 # TODO: Move into dialogs.py?
816 def about_cb(self, action):
817 d = gtk.AboutDialog()
818 d.set_transient_for(self)
819 d.set_program_name("MyPaint")
820 d.set_version(MYPAINT_VERSION)
821 d.set_copyright(_("Copyright (C) 2005-2010\nMartin Renold and the MyPaint Development Team"))
822 d.set_website("http://mypaint.info/")
823 d.set_logo(self.app.pixmaps.mypaint_logo)
825 _(u"This program is free software; you can redistribute it and/or modify "
826 u"it under the terms of the GNU General Public License as published by "
827 u"the Free Software Foundation; either version 2 of the License, or "
828 u"(at your option) any later version.\n"
830 u"This program is distributed in the hope that it will be useful, "
831 u"but WITHOUT ANY WARRANTY. See the COPYING file for more details.")
833 d.set_wrap_license(True)
835 # (in order of appearance)
836 u"Martin Renold (%s)" % _('programming'),
837 u"Artis Rozentāls (%s)" % _('brushes'),
838 u"Yves Combe (%s)" % _('portability'),
839 u"Popolon (%s)" % _('brushes, programming'),
840 u"Clement Skau (%s)" % _('programming'),
841 u"Marcelo 'Tanda' Cerviño (%s)" % _('patterns, brushes'),
842 u"Jon Nordby (%s)" % _('programming'),
843 u"Álinson Santos (%s)" % _('programming'),
844 u"Tumagonx (%s)" % _('portability'),
845 u"Ilya Portnov (%s)" % _('programming'),
846 u"David Revoy (%s)" % _('brushes'),
847 u"Ramón Miranda (%s)" % _('brushes, patterns'),
848 u"Enrico Guarnieri 'Ico_dY' (%s)" % _('brushes'),
849 u"Jonas Wagner (%s)" % _('programming'),
850 u"Luka Čehovin (%s)" % _('programming'),
851 u"Andrew Chadwick (%s)" % _('programming'),
852 u"Till Hartmann (%s)" % _('programming'),
853 u"Nicola Lunghi (%s)" % _('patterns'),
854 u"Toni Kasurinen (%s)" % _('brushes'),
855 u"Сан Саныч (%s)" % _('patterns'),
856 u'David Grundberg (%s)' % _('programming'),
857 u"Krzysztof Pasek (%s)" % _('programming'),
860 u'Sebastian Kraft (%s)' % _('desktop icon'),
862 # list all translators, not only those of the current language
863 d.set_translator_credits(
864 u'Ilya Portnov (ru)\n'
865 u'Popolon (fr, zh_CN, ja)\n'
868 u'Tobias Jakobs (de)\n'
869 u'Martin Tabačan (cs)\n'
871 u'Manuel Quiñones (es)\n'
872 u'Gergely Aradszki (hu)\n'
873 u'Lamberto Tedaldi (it)\n'
874 u'Dong-Jun Wu (zh_TW)\n'
875 u'Luka Čehovin (sl)\n'
876 u'Geuntak Jeong (ko)\n'
877 u'Łukasz Lubojański (pl)\n'
878 u'Daniel Korostil (uk)\n'
879 u'Julian Aloofi (de)\n'
880 u'Tor Egil Hoftun Kvæstad (nn_NO)\n'
881 u'João S. O. Bueno (pt_BR)\n'
882 u'David Grundberg (sv)\n'
883 u'Elliott Sales de Andrade (en_CA)\n'
889 def show_infodialog_cb(self, action):
892 _("Move your mouse over a menu entry, then press the key to assign."),
894 _("You can also drag the canvas with the mouse while holding the middle "
895 "mouse button or spacebar. Or with the arrow keys."
897 "In contrast to earlier versions, scrolling and zooming are harmless now and "
898 "will not make you run out of memory. But you still require a lot of memory "
899 "if you paint all over while fully zoomed out."),
901 _("Brushkeys are used to quickly save/restore brush settings "
902 "using keyboard shortcuts. You can paint with one hand and "
903 "change brushes with the other without interruption."
905 "There are 10 memory slots to hold brush settings.\n"
906 "They are anonymous brushes, which are not visible in the "
907 "brush selector list. But they are remembered even if you "
910 _("There is a tutorial available on the MyPaint homepage. It "
911 "explains some features which are hard to discover yourself."
913 "Comments about the brush settings (opaque, hardness, etc.) and "
914 "inputs (pressure, speed, etc.) are available as tooltips. "
915 "Put your mouse over a label to see them. "
918 self.app.message_dialog(text[action.get_name()])