OSDN Git Service

version bump
[mypaint-anime/master.git] / gui / drawwindow.py
index 39bbefb..f5ba5f9 100644 (file)
@@ -13,21 +13,25 @@ This is the main drawing window, containing menu actions.
 Painting is done in tileddrawwidget.py.
 """
 
-MYPAINT_VERSION="0.9.1+git"
+MYPAINT_VERSION="1.0.0"
 
-import os, math, time
+import os, math, time, webbrowser
 from gettext import gettext as _
 
 import gtk, gobject
 from gtk import gdk, keysyms
+import pango
 
-import colorselectionwindow, historypopup, stategroup, colorpicker, windowing, layout
+import colorselectionwindow, historypopup, stategroup, colorpicker, windowing, layout, toolbar
 import dialogs
 from lib import helpers
 import stock
 
 import xml.etree.ElementTree as ET
 
+# palette support
+from lib.scratchpad_palette import GimpPalette, hatch_squiggle, squiggle, draw_palette
+
 # TODO: put in a helper file?
 def with_wait_cursor(func):
     """python decorator that adds a wait cursor around a function"""
@@ -50,9 +54,101 @@ def with_wait_cursor(func):
             self.app.doc.tdw.grab_remove()
     return wrapper
 
+def button_press_cb_abstraction(drawwindow, win, event, doc):
+    #print event.device, event.button
+
+    ## Ignore accidentals
+    # Single button-presses only, not 2ble/3ple
+    if event.type != gdk.BUTTON_PRESS:
+        # ignore the extra double-click event
+        return False
+
+    if event.button != 1:
+        # check whether we are painting (accidental)
+        if event.state & gdk.BUTTON1_MASK:
+            # Do not allow dragging in the middle of
+            # painting. This often happens by accident with wacom
+            # tablet's stylus button.
+            #
+            # However we allow dragging if the user's pressure is
+            # still below the click threshold.  This is because
+            # some tablet PCs are not able to produce a
+            # middle-mouse click without reporting pressure.
+            # https://gna.org/bugs/index.php?15907
+            return False
+
+    # Pick a suitable config option
+    ctrl = event.state & gdk.CONTROL_MASK
+    alt  = event.state & gdk.MOD1_MASK
+    shift = event.state & gdk.SHIFT_MASK
+    if shift:
+        modifier_str = "_shift"
+    elif alt or ctrl:
+        modifier_str = "_ctrl"
+    else:
+        modifier_str = ""
+    prefs_name = "input.button%d%s_action" % (event.button, modifier_str)
+    action_name = drawwindow.app.preferences.get(prefs_name, "no_action")
+
+    # No-ops
+    if action_name == 'no_action':
+        return True  # We handled it by doing nothing
+
+    # Straight line
+    # Really belongs in the tdw, but this is the only object with access
+    # to the application preferences.
+    if action_name == 'straight_line':
+        doc.tdw.straight_line_from_last_pos(is_sequence=False)
+        return True
+    if action_name == 'straight_line_sequence':
+        doc.tdw.straight_line_from_last_pos(is_sequence=True)
+        return True
+
+    # View control
+    if action_name.endswith("_canvas"):
+        dragfunc = None
+        if action_name == "pan_canvas":
+            dragfunc = doc.dragfunc_translate
+        elif action_name == "zoom_canvas":
+            dragfunc = doc.dragfunc_zoom
+        elif action_name == "rotate_canvas":
+            dragfunc = doc.dragfunc_rotate
+        if dragfunc is not None:
+            doc.tdw.start_drag(dragfunc)
+            return True
+        return False
+
+    # Application menu
+    if action_name == 'popup_menu':
+        drawwindow.show_popupmenu(event=event)
+        return True
+
+    if action_name in drawwindow.popup_states:
+        state = drawwindow.popup_states[action_name]
+        state.activate(event)
+        return True
+
+    # Dispatch regular GTK events.
+    action = drawwindow.app.find_action(action_name)
+    if action is not None:
+        action.activate()
+        return True
+
+def button_release_cb_abstraction(win, event, doc):
+    #print event.device, event.button
+    tdw = doc.tdw
+    if tdw.dragfunc is not None:
+        tdw.stop_drag(doc.dragfunc_translate)
+        tdw.stop_drag(doc.dragfunc_rotate)
+        tdw.stop_drag(doc.dragfunc_zoom)
+    return False
 
 class Window (windowing.MainWindow, layout.MainWindow):
 
+    MENUISHBAR_RADIO_MENUBAR = 1
+    MENUISHBAR_RADIO_MAIN_TOOLBAR = 2
+    MENUISHBAR_RADIO_BOTH_BARS = 3
+
     def __init__(self, app):
         windowing.MainWindow.__init__(self, app)
         self.app = app
@@ -85,6 +181,14 @@ class Window (windowing.MainWindow, layout.MainWindow):
 
         lm = app.layout_manager
         layout.MainWindow.__init__(self, lm)
+
+        # Park the focus on the main tdw rather than on the toolbar. Default
+        # activation doesn't really mean much for MyPaint's main window, so
+        # it's safe to do this and it looks better.
+        self.main_widget.set_can_default(True)
+        self.main_widget.set_can_focus(True)
+        self.main_widget.grab_focus()
+
         self.main_widget.connect("button-press-event", self.button_press_cb)
         self.main_widget.connect("button-release-event",self.button_release_cb)
         self.main_widget.connect("scroll-event", self.scroll_cb)
@@ -116,16 +220,34 @@ class Window (windowing.MainWindow, layout.MainWindow):
             ('ColorMenu',    None, _('Color')),
             ('ColorPickerPopup',    gtk.STOCK_COLOR_PICKER, _('Pick Color'), 'r', None, self.popup_cb),
             ('ColorHistoryPopup',  None, _('Color History'), 'x', None, self.popup_cb),
-            ('ColorChangerPopup', None, _('Color Changer'), 'v', None, self.popup_cb),
+            ('ColorChangerCrossedBowlPopup', None, _('Color Changer (crossed bowl)'), 'v', None, self.popup_cb),
+            ('ColorChangerWashPopup', None, _('Color Changer (washed)'), 'c', None, self.popup_cb),
             ('ColorRingPopup',  None, _('Color Ring'), None, None, self.popup_cb),
+            ('ColorDetailsDialog', None, _("Color Details"), None, None, self.color_details_dialog_cb),
 
             ('ContextMenu',  None, _('Brushkeys')),
             ('ContextHelp',  gtk.STOCK_HELP, _('Help!'), None, None, self.show_infodialog_cb),
 
             ('LayerMenu',    None, _('Layers')),
 
+            # Scratchpad menu items
+            ('ScratchMenu',    None, _('Scratchpad')),
+            ('ScratchNew',  gtk.STOCK_NEW, _('New Scratchpad'), '', None, self.new_scratchpad_cb),
+            ('ScratchLoad',  gtk.STOCK_OPEN, _('Load Scratchpad...'), '', None, self.load_scratchpad_cb),
+            ('ScratchSaveNow',  gtk.STOCK_SAVE, _('Save Scratchpad Now'), '', None, self.save_current_scratchpad_cb),
+            ('ScratchSaveAs',  gtk.STOCK_SAVE_AS, _('Save Scratchpad As...'), '', None, self.save_as_scratchpad_cb),
+            ('ScratchRevert',  gtk.STOCK_UNDO, _('Revert Scratchpad'), '', None, self.revert_current_scratchpad_cb),
+            ('ScratchSaveAsDefault',  None, _('Save Scratchpad as Default'), None, None, self.save_scratchpad_as_default_cb),
+            ('ScratchClearDefault',  None, _('Clear the Default Scratchpad'), None, None, self.clear_default_scratchpad_cb),
+            ('ScratchPaletteOptions', None, _('Render a Palette')),
+            ('ScratchLoadPalette',  None, _('Load Palette File...'), None, None, self.draw_palette_cb),
+            ('ScratchDrawSatPalette',  None, _('Different Saturations of the current Color'), None, None, self.draw_sat_spectrum_cb),
+            ('ScratchCopyBackground',  None, _('Copy Background to Scratchpad'), None, None, self.scratchpad_copy_background_cb),
+
             ('BrushMenu',    None, _('Brush')),
-            ('ImportBrushPack',       gtk.STOCK_OPEN, _('Import brush package...'), '', None, self.import_brush_pack_cb),
+            ('BrushChooserPopup', stock.TOOL_BRUSH, _("Change Brush..."), 'b', None, self.brush_chooser_popup_cb),
+            ('DownloadBrushPack', gtk.STOCK_OPEN, _('Download more brushes (in web browser)'), '', None, self.download_brush_pack_cb),
+            ('ImportBrushPack', gtk.STOCK_OPEN, _('Import brush package...'), '', None, self.import_brush_pack_cb),
 
             ('HelpMenu',   None, _('Help')),
             ('Docu', gtk.STOCK_INFO, _('Where is the Documentation?'), None, None, self.show_infodialog_cb),
@@ -140,12 +262,15 @@ class Window (windowing.MainWindow, layout.MainWindow):
 
 
             ('ViewMenu', None, _('View')),
+            ('MenuishBarMenu', None, _('Toolbars')),
             ('ShowPopupMenu',    None, _('Popup Menu'), 'Menu', None, self.popupmenu_show_cb),
-            ('Fullscreen',   gtk.STOCK_FULLSCREEN, _('Fullscreen'), 'F11', None, self.fullscreen_cb),
+            ('Fullscreen',   gtk.STOCK_FULLSCREEN, None, 'F11', None, self.fullscreen_cb),
             ('ViewHelp',  gtk.STOCK_HELP, _('Help'), None, None, self.show_infodialog_cb),
             ]
         ag = self.action_group = gtk.ActionGroup('WindowActions')
+        self.app.add_action_group(ag)
         ag.add_actions(actions)
+        self.update_fullscreen_action()
 
         # Toggle actions
         toggle_actions = [
@@ -161,10 +286,12 @@ class Window (windowing.MainWindow, layout.MainWindow):
             ('BackgroundWindow', gtk.STOCK_PAGE_SETUP,
                     _('Background'), None, None, self.toggle_window_cb),
             ('BrushSelectionWindow', stock.TOOL_BRUSH,
-                    None, None, _("Toggle the Brush selector"),
+                    None, None,
+                    _("Edit and reorganise Brush Lists"),
                     self.toggle_window_cb),
             ('BrushSettingsWindow', gtk.STOCK_PROPERTIES,
-                    _('Brush Editor'), '<control>b', None,
+                    _('Brush Settings Editor'), '<control>b',
+                    _("Change Brush Settings in detail"),
                     self.toggle_window_cb),
             ('ColorSelectionWindow', stock.TOOL_COLOR_SELECTOR,
                     None, None, _("Toggle the Colour Triangle"),
@@ -172,6 +299,9 @@ class Window (windowing.MainWindow, layout.MainWindow):
             ('ColorSamplerWindow', stock.TOOL_COLOR_SAMPLER,
                     None, None, _("Toggle the advanced Colour Sampler"),
                     self.toggle_window_cb),
+            ('ScratchWindow',  stock.TOOL_SCRATCHPAD, 
+                    None, None, _('Toggle the scratchpad'),
+                    self.toggle_window_cb),
             ]
         ag.add_toggle_actions(toggle_actions)
 
@@ -193,37 +323,63 @@ class Window (windowing.MainWindow, layout.MainWindow):
 
         # More toggle actions - ones which don't control windows.
         toggle_actions = [
-            ('ToggleToolbar', None, _('Toolbar'), None,
-                    _("Show toolbar"), self.toggle_toolbar_cb,
-                    self.get_show_toolbar()),
             ('ToggleSubwindows', None, _('Subwindows'), 'Tab',
                     _("Show subwindows"), self.toggle_subwindows_cb,
                     self.get_show_subwindows()),
             ]
         ag.add_toggle_actions(toggle_actions)
 
+        # Radio actions
+        menuishbar_radio_actions = [
+            ('MenuishBarRadioMenubar', None, _('Menubar only'), None,
+                _("Show menu bar"),
+                self.MENUISHBAR_RADIO_MENUBAR),
+            ('MenuishBarRadioMainToolbar', None, _('Toolbar only'), None,
+                _("Show toolbar"),
+                self.MENUISHBAR_RADIO_MAIN_TOOLBAR),
+            ('MenuishBarRadioMenubarAndMainToolbar', None, _('Both'), None,
+                _("Show both the menu bar and the toolbar"),
+                self.MENUISHBAR_RADIO_BOTH_BARS),
+            ]
+        menuishbar_state = 0
+        if self.get_ui_part_enabled("menubar"):
+            menuishbar_state += self.MENUISHBAR_RADIO_MENUBAR
+        if self.get_ui_part_enabled("main_toolbar"):
+            menuishbar_state += self.MENUISHBAR_RADIO_MAIN_TOOLBAR
+        if menuishbar_state == 0:
+            menuishbar_state = self.MENUISHBAR_RADIO_MAIN_TOOLBAR
+        ag.add_radio_actions(menuishbar_radio_actions,
+            menuishbar_state, self.on_menuishbar_radio_change)
+        gobject.idle_add(lambda: self.update_ui_parts())
+
         # Keyboard handling
         for action in self.action_group.list_actions():
             self.app.kbm.takeover_action(action)
-        self.app.ui_manager.insert_action_group(ag, -1)
 
     def init_stategroups(self):
         sg = stategroup.StateGroup()
         p2s = sg.create_popup_state
-        changer = p2s(colorselectionwindow.ColorChangerPopup(self.app))
+        changer_crossed_bowl = p2s(colorselectionwindow.ColorChangerCrossedBowlPopup(self.app))
+        changer_wash = p2s(colorselectionwindow.ColorChangerWashPopup(self.app))
         ring = p2s(colorselectionwindow.ColorRingPopup(self.app))
         hist = p2s(historypopup.HistoryPopup(self.app, self.app.doc.model))
         pick = self.colorpick_state = p2s(colorpicker.ColorPicker(self.app, self.app.doc.model))
 
         self.popup_states = {
-            'ColorChangerPopup': changer,
+            'ColorChangerCrossedBowlPopup': changer_crossed_bowl,
+            'ColorChangerWashPopup': changer_wash,
             'ColorRingPopup': ring,
             'ColorHistoryPopup': hist,
             'ColorPickerPopup': pick,
             }
-        changer.next_state = ring
-        ring.next_state = changer
-        changer.autoleave_timeout = None
+
+        # not sure how useful this is; we can't cycle at the moment
+        changer_crossed_bowl.next_state = ring
+        ring.next_state = changer_wash
+        changer_wash.next_state = ring
+
+        changer_wash.autoleave_timeout = None
+        changer_crossed_bowl.autoleave_timeout = None
         ring.autoleave_timeout = None
 
         pick.max_key_hit_duration = 0.0
@@ -240,19 +396,14 @@ class Window (windowing.MainWindow, layout.MainWindow):
         menupath = os.path.join(self.app.datapath, 'gui/menu.xml')
         menubar_xml = open(menupath).read()
         self.app.ui_manager.add_ui_from_string(menubar_xml)
-        self._init_popupmenu(menubar_xml)
+        self.popupmenu = self._clone_menu(menubar_xml, 'PopupMenu', self.app.doc.tdw)
         self.menubar = self.app.ui_manager.get_widget('/Menubar')
 
     def init_toolbar(self):
-        toolbarpath = os.path.join(self.app.datapath, 'gui/toolbar.xml')
-        toolbarbar_xml = open(toolbarpath).read()
-        self.app.ui_manager.add_ui_from_string(toolbarbar_xml)
-        self.toolbar = self.app.ui_manager.get_widget('/toolbar1')
-        if not self.get_show_toolbar():
-            gobject.idle_add(self.toolbar.hide)
-        tb.set_style(gtk.TOOLBAR_ICONS)
-
-    def _init_popupmenu(self, xml):
+        self.toolbar_manager = toolbar.ToolbarManager(self)
+        self.toolbar = self.toolbar_manager.toolbar1
+
+    def _clone_menu(self, xml, name, owner=None):
         """
         Hopefully temporary hack for converting UIManager XML describing the
         main menubar into a rebindable popup menu. UIManager by itself doesn't
@@ -261,27 +412,22 @@ class Window (windowing.MainWindow, layout.MainWindow):
         """
         ui_elt = ET.fromstring(xml)
         rootmenu_elt = ui_elt.find("menubar")
-        rootmenu_elt.attrib["name"] = "PopupMenu"
-        ## XML-style menu jiggling. No need for this really though.
-        #for menu_elt in rootmenu_elt.findall("menu"):
-        #    for item_elt in menu_elt.findall("menuitem"):
-        #        if item_elt.attrib.get("action", "") == "ShowPopupMenu":
-        #            menu_elt.remove(item_elt)
-        ## Maybe shift a small number of frequently-used items to the top?
+        rootmenu_elt.attrib["name"] = name
         xml = ET.tostring(ui_elt)
         self.app.ui_manager.add_ui_from_string(xml)
-        tmp_menubar = self.app.ui_manager.get_widget('/PopupMenu')
-        self.popupmenu = gtk.Menu()
+        tmp_menubar = self.app.ui_manager.get_widget('/' + name)
+        popupmenu = gtk.Menu()
         for item in tmp_menubar.get_children():
             tmp_menubar.remove(item)
-            self.popupmenu.append(item)
-        self.popupmenu.attach_to_widget(self.app.doc.tdw, None)
-        #self.popupmenu.set_title("MyPaint")
-        #self.popupmenu.set_take_focus(True)
-        self.popupmenu.connect("selection-done", self.popupmenu_done_cb)
-        self.popupmenu.connect("deactivate", self.popupmenu_done_cb)
-        self.popupmenu.connect("cancel", self.popupmenu_done_cb)
+            popupmenu.append(item)
+        if owner is not None:
+            popupmenu.attach_to_widget(owner, None)
+        popupmenu.set_title("MyPaint")
+        popupmenu.connect("selection-done", self.popupmenu_done_cb)
+        popupmenu.connect("deactivate", self.popupmenu_done_cb)
+        popupmenu.connect("cancel", self.popupmenu_done_cb)
         self.popupmenu_last_active = None
+        return popupmenu
 
 
     def update_title(self, filename):
@@ -347,24 +493,38 @@ class Window (windowing.MainWindow, layout.MainWindow):
         #if event.state & ANY_MODIFIER:
         #    # allow user shortcuts with modifiers
         #    return False
+
+        # This may need a stateful flag
+        if self.app.scratchpad_doc.tdw.has_pointer:
+            thisdoc = self.app.scratchpad_doc
+            # Stop dragging on the main window
+            self.app.doc.tdw.dragfunc = None
+        else:
+            thisdoc = self.app.doc
+            # Stop dragging on the other window
+            self.app.scratchpad_doc.tdw.dragfunc = None
         if key == keysyms.space:
             if shift:
-                self.app.doc.tdw.start_drag(self.app.doc.dragfunc_rotate)
+                 thisdoc.tdw.start_drag(thisdoc.dragfunc_rotate)
             elif ctrl:
-                self.app.doc.tdw.start_drag(self.app.doc.dragfunc_zoom)
+                thisdoc.tdw.start_drag(thisdoc.dragfunc_zoom)
             elif alt:
-                self.app.doc.tdw.start_drag(self.app.doc.dragfunc_frame)
+                thisdoc.tdw.start_drag(thisdoc.dragfunc_frame)
             else:
-                self.app.doc.tdw.start_drag(self.app.doc.dragfunc_translate)
+                thisdoc.tdw.start_drag(thisdoc.dragfunc_translate)
         else: return False
         return True
 
     def key_release_event_cb_before(self, win, event):
+        if self.app.scratchpad_doc.tdw.has_pointer:
+            thisdoc = self.app.scratchpad_doc
+        else:
+            thisdoc = self.app.doc
         if event.keyval == keysyms.space:
-            self.app.doc.tdw.stop_drag(self.app.doc.dragfunc_translate)
-            self.app.doc.tdw.stop_drag(self.app.doc.dragfunc_rotate)
-            self.app.doc.tdw.stop_drag(self.app.doc.dragfunc_zoom)
-            self.app.doc.tdw.stop_drag(self.app.doc.dragfunc_frame)
+            thisdoc.tdw.stop_drag(thisdoc.dragfunc_translate)
+            thisdoc.tdw.stop_drag(thisdoc.dragfunc_rotate)
+            thisdoc.tdw.stop_drag(thisdoc.dragfunc_zoom)
+            thisdoc.tdw.stop_drag(thisdoc.dragfunc_frame)
             return True
         return False
 
@@ -380,94 +540,10 @@ class Window (windowing.MainWindow, layout.MainWindow):
         return False
 
     def button_press_cb(self, win, event):
-        #print event.device, event.button
-
-        ## Ignore accidentals
-        # Single button-presses only, not 2ble/3ple
-        if event.type != gdk.BUTTON_PRESS:
-            # ignore the extra double-click event
-            return False
-
-        if event.button != 1:
-            # check whether we are painting (accidental)
-            if event.state & gdk.BUTTON1_MASK:
-                # Do not allow dragging in the middle of
-                # painting. This often happens by accident with wacom
-                # tablet's stylus button.
-                #
-                # However we allow dragging if the user's pressure is
-                # still below the click threshold.  This is because
-                # some tablet PCs are not able to produce a
-                # middle-mouse click without reporting pressure.
-                # https://gna.org/bugs/index.php?15907
-                return False
-
-        # Pick a suitable config option
-        ctrl = event.state & gdk.CONTROL_MASK
-        alt  = event.state & gdk.MOD1_MASK
-        shift = event.state & gdk.SHIFT_MASK
-        if shift:
-            modifier_str = "_shift"
-        elif alt or ctrl:
-            modifier_str = "_ctrl"
-        else:
-            modifier_str = ""
-        prefs_name = "input.button%d%s_action" % (event.button, modifier_str)
-        action_name = self.app.preferences.get(prefs_name, "no_action")
-
-        # No-ops
-        if action_name == 'no_action':
-            return True  # We handled it by doing nothing
-
-        # Straight line
-        # Really belongs in the tdw, but this is the only object with access
-        # to the application preferences.
-        if action_name == 'straight_line':
-            self.app.doc.tdw.straight_line_from_last_pos(is_sequence=False)
-            return True
-        if action_name == 'straight_line_sequence':
-            self.app.doc.tdw.straight_line_from_last_pos(is_sequence=True)
-            return True
-
-        # View control
-        if action_name.endswith("_canvas"):
-            dragfunc = None
-            if action_name == "pan_canvas":
-                dragfunc = self.app.doc.dragfunc_translate
-            elif action_name == "zoom_canvas":
-                dragfunc = self.app.doc.dragfunc_zoom
-            elif action_name == "rotate_canvas":
-                dragfunc = self.app.doc.dragfunc_rotate
-            if dragfunc is not None:
-                self.app.doc.tdw.start_drag(dragfunc)
-                return True
-            return False
-
-        # Application menu
-        if action_name == 'popup_menu':
-            self.show_popupmenu(event=event)
-            return True
-
-        if action_name in self.popup_states:
-            state = self.popup_states[action_name]
-            state.activate(event)
-            return True
-
-        # Dispatch regular GTK events.
-        for ag in self.action_group, self.app.doc.action_group:
-            action = ag.get_action(action_name)
-            if action is not None:
-                action.activate()
-                return True
+        return button_press_cb_abstraction(self, win, event, self.app.doc)
 
     def button_release_cb(self, win, event):
-        #print event.device, event.button
-        tdw = self.app.doc.tdw
-        if tdw.dragfunc is not None:
-            tdw.stop_drag(self.app.doc.dragfunc_translate)
-            tdw.stop_drag(self.app.doc.dragfunc_rotate)
-            tdw.stop_drag(self.app.doc.dragfunc_zoom)
-        return False
+        return button_release_cb_abstraction(win, event, self.app.doc)
 
     def scroll_cb(self, win, event):
         d = event.direction
@@ -536,33 +612,86 @@ class Window (windowing.MainWindow, layout.MainWindow):
         state.activate(action)
 
 
-    # Show Toolbar
-    # Saved in the user prefs between sessions.
-    # Controlled via its ToggleAction only.
+    def brush_chooser_popup_cb(self, action):
+        # It may be even nicer to do this as a real popup state with
+        # mouse-out to cancel. The Action is named accordingly. For now
+        # though a modal dialog will do as an implementation.
+        dialogs.change_current_brush_quick(self.app)
+
+    def color_details_dialog_cb(self, action):
+        dialogs.change_current_color_detailed(self.app)
+
+
+    # User-toggleable UI pieces: things like toolbars, status bars, menu bars.
+    # Saved between sessions.
 
-    def set_show_toolbar(self, show_toolbar):
-        """Programatically set the Show Toolbar option.
+
+    def get_ui_part_enabled(self, part_name):
+        """Returns whether the named UI part is enabled in the prefs.
         """
-        action = self.action_group.get_action("ToggleToolbar")
-        if show_toolbar:
-            if not action.get_active():
-                action.set_active(True)
-            self.app.preferences["ui.toolbar"] = True
-        else:
-            if action.get_active():
-                action.set_active(False)
-            self.app.preferences["ui.toolbar"] = False
+        parts = self.app.preferences["ui.parts"]
+        return bool(parts.get(part_name, False))
 
-    def get_show_toolbar(self):
-        return self.app.preferences.get("ui.toolbar", True)
 
-    def toggle_toolbar_cb(self, action):
-        active = action.get_active()
-        if active:
+    def update_ui_parts(self, **updates):
+        """Updates the UI part prefs, then hide/show widgets to match.
+
+        Called without arguments, this updates the UI to match the
+        boolean-valued hash ``ui.parts`` in the app preferences. With keyword
+        arguments, the prefs are updated first, then changes are reflected in
+        the set of visible widgets. Current known parts:
+
+            :``main_toolbar``:
+                The primary toolbar and its menu button.
+            :``menubar``:
+                A conventional menu bar.
+
+        Currently the user cannot turn off both the main toolbar and the
+        menubar: the toolbar will be forced on if an attempt is made.
+        """
+        new_state = self.app.preferences["ui.parts"].copy()
+        new_state.update(updates)
+        # Menu bar
+        if new_state.get("menubar", False):
+            self.menubar.show_all()
+        else:
+            self.menubar.hide()
+            if not new_state.get("main_toolbar", False):
+                new_state["main_toolbar"] = True
+        # Toolbar
+        if new_state.get("main_toolbar", False):
             self.toolbar.show_all()
         else:
             self.toolbar.hide()
-        self.app.preferences["ui.toolbar"] = active
+        self.app.preferences["ui.parts"] = new_state
+        self.update_menu_button()
+
+
+    def update_menu_button(self):
+        """Updates the menu button to match toolbar and menubar visibility.
+
+        The menu button is visible when the menu bar is hidden. Since the user
+        must have either a toolbar or a menu or both, this ensures that a menu
+        is on-screen at all times in non-fullscreen mode.
+        """
+        toolbar_visible = self.toolbar.get_property("visible")
+        menubar_visible = self.menubar.get_property("visible")
+        if toolbar_visible and menubar_visible:
+            self.toolbar_manager.menu_button.hide()
+        else:
+            self.toolbar_manager.menu_button.show_all()
+
+
+    def on_menuishbar_radio_change(self, radioaction, current):
+        """Respond to a change of the 'menu bar/toolbar' radio menu items.
+        """
+        value = radioaction.get_current_value()
+        if value == self.MENUISHBAR_RADIO_MENUBAR:
+            self.update_ui_parts(main_toolbar=False, menubar=True)
+        elif value == self.MENUISHBAR_RADIO_MAIN_TOOLBAR:
+            self.update_ui_parts(main_toolbar=True, menubar=False)
+        else:
+            self.update_ui_parts(main_toolbar=True, menubar=True)
 
 
     # Show Subwindows
@@ -628,21 +757,36 @@ class Window (windowing.MainWindow, layout.MainWindow):
             while gtk.events_pending():
                 gtk.main_iteration()
             if getattr(self, "_restore_menubar_on_unfullscreen", False):
-                self.menubar.show()
+                if self.get_ui_part_enabled("menubar"):
+                    self.menubar.show()
                 del self._restore_menubar_on_unfullscreen
             if getattr(self, "_restore_toolbar_on_unfullscreen", False):
-                if self.get_show_toolbar():
+                if self.get_ui_part_enabled("main_toolbar"):
                     self.toolbar.show()
                 del self._restore_toolbar_on_unfullscreen
             if getattr(self, "_restore_subwindows_on_unfullscreen", False):
                 self.set_show_subwindows(True)
                 del self._restore_subwindows_on_unfullscreen
+        self.update_menu_button()
+        self.update_fullscreen_action()
+
+    def update_fullscreen_action(self):
+        action = self.action_group.get_action("Fullscreen")
+        if self.is_fullscreen:
+            action.set_stock_id(gtk.STOCK_LEAVE_FULLSCREEN)
+            action.set_tooltip(_("Leave Fullscreen Mode"))
+            action.set_label(_("UnFullscreen"))
+        else:
+            action.set_stock_id(gtk.STOCK_FULLSCREEN)
+            action.set_tooltip(_("Enter Fullscreen Mode"))
+            action.set_label(_("Fullscreen"))
 
     def popupmenu_show_cb(self, action):
         self.show_popupmenu()
 
     def show_popupmenu(self, event=None):
         self.menubar.set_sensitive(False)   # excessive feedback?
+        self.toolbar_manager.menu_button.set_sensitive(False)
         button = 1
         time = 0
         if event is not None:
@@ -660,12 +804,102 @@ class Window (windowing.MainWindow, layout.MainWindow):
 
     def popupmenu_done_cb(self, *a, **kw):
         # Not sure if we need to bother with this level of feedback,
-        # but it actaully looks quite nice to see one menu taking over
+        # but it actually looks quite nice to see one menu taking over
         # the other. Makes it clear that the popups are the same thing as
         # the full menu, maybe.
         self.menubar.set_sensitive(True)
+        self.toolbar_manager.menu_button.set_sensitive(True)
         self.popupmenu_last_active = self.popupmenu.get_active()
 
+    # BEGIN -- Scratchpad menu options
+    def save_scratchpad_as_default_cb(self, action):
+        self.app.filehandler.save_scratchpad(self.app.filehandler.get_scratchpad_default(), export = True)
+
+    def clear_default_scratchpad_cb(self, action):
+        self.app.filehandler.delete_default_scratchpad()
+
+    # Unneeded since 'Save blank canvas' bug has been addressed.
+    #def clear_autosave_scratchpad_cb(self, action):
+    #    self.app.filehandler.delete_autosave_scratchpad()
+
+    def new_scratchpad_cb(self, action):
+        if os.path.isfile(self.app.filehandler.get_scratchpad_default()):
+            self.app.filehandler.open_scratchpad(self.app.filehandler.get_scratchpad_default())
+        else:
+            self.app.scratchpad_doc.model.clear()
+            # With no default - adopt the currently chosen background
+            bg = self.app.doc.model.background
+            if self.app.scratchpad_doc:
+                self.app.scratchpad_doc.model.set_background(bg)
+
+        self.app.scratchpad_filename = self.app.preferences['scratchpad.last_opened'] = self.app.filehandler.get_scratchpad_autosave()
+
+    def load_scratchpad_cb(self, action):
+        if self.app.scratchpad_filename:
+            self.save_current_scratchpad_cb(action)
+            current_pad = self.app.scratchpad_filename
+        else:
+            current_pad = self.app.filehandler.get_scratchpad_autosave()
+        self.app.filehandler.open_scratchpad_dialog()
+        # Check to see if a file has been opened outside of the scratchpad directory
+        if not os.path.abspath(self.app.scratchpad_filename).startswith(os.path.abspath(self.app.filehandler.get_scratchpad_prefix())):
+            # file is NOT within the scratchpad directory - load copy as current scratchpad
+            self.app.scratchpad_filename = self.app.preferences['scratchpad.last_opened'] = current_pad
+
+    def save_as_scratchpad_cb(self, action):
+        self.app.filehandler.save_scratchpad_as_dialog()
+
+    def revert_current_scratchpad_cb(self, action):
+        if os.path.isfile(self.app.scratchpad_filename):
+            self.app.filehandler.open_scratchpad(self.app.scratchpad_filename)
+            print "Reverted to %s" % self.app.scratchpad_filename
+        else:
+            print "No file to revert to yet."
+
+    def save_current_scratchpad_cb(self, action):
+        self.app.filehandler.save_scratchpad(self.app.scratchpad_filename)
+
+    def scratchpad_copy_background_cb(self, action):
+        bg = self.app.doc.model.background
+        if self.app.scratchpad_doc:
+            self.app.scratchpad_doc.model.set_background(bg)
+
+    def draw_palette_cb(self, action):
+        # test functionality:
+        file_filters = [
+        (_("Gimp Palette Format"), ("*.gpl",)),
+        (_("All Files"), ("*.*",)),
+        ]
+        gimp_path = os.path.join(self.app.filehandler.get_gimp_prefix(), "palettes")
+        dialog = self.app.filehandler.get_open_dialog(start_in_folder=gimp_path,
+                                                  file_filters = file_filters)
+        try:
+            if dialog.run() == gtk.RESPONSE_OK:
+                dialog.hide()
+                filename = dialog.get_filename().decode('utf-8')
+                if filename:
+                    #filename = "/home/ben/.gimp-2.6/palettes/Nature_Grass.gpl" # TEMP HACK TO TEST
+                    g = GimpPalette(filename)
+                    grid_size = 30.0
+                    column_limit = 7
+                    # IGNORE Gimp Palette 'columns'?
+                    if g.columns != 0:
+                        column_limit = g.columns   # use the value for columns in the palette
+                    draw_palette(self.app, g, self.app.scratchpad_doc, columns=column_limit, grid_size=grid_size, swatch_method=hatch_squiggle, scale = 25.0)
+        finally:
+            dialog.destroy()
+
+    def draw_sat_spectrum_cb(self, action):
+        g = GimpPalette()
+        hsv = self.app.brush.get_color_hsv()
+        g.append_sat_spectrum(hsv)
+        grid_size = 30.0
+        off_x = off_y = grid_size / 2.0
+        column_limit = 8
+        draw_palette(self.app, g, self.app.scratchpad_doc, columns=column_limit, grid_size=grid_size)
+
+    # END -- Scratchpad menu options
+
     def quit_cb(self, *junk):
         self.app.doc.model.split_stroke()
         self.app.save_gui_config() # FIXME: should do this periodically, not only on quit
@@ -680,6 +914,11 @@ class Window (windowing.MainWindow, layout.MainWindow):
         enabled = self.app.doc.model.frame_enabled
         self.app.doc.model.set_frame_enabled(not enabled)
 
+    def download_brush_pack_cb(self, *junk):
+        url = 'http://wiki.mypaint.info/index.php?title=Brush_Packages/redirect_mypaint_1.0_gui'
+        print 'URL:', url
+        webbrowser.open(url)
+
     def import_brush_pack_cb(self, *junk):
         format_id, filename = dialogs.open_dialog(_("Import brush package..."), self,
                                  [(_("MyPaint brush package (*.zip)"), "*.zip")])
@@ -709,30 +948,36 @@ class Window (windowing.MainWindow, layout.MainWindow):
         d.set_authors([
             # (in order of appearance)
             u"Martin Renold (%s)" % _('programming'),
-            u"Artis Rozentāls (%s)" % _('brushes'),
             u"Yves Combe (%s)" % _('portability'),
-            u"Popolon (%s)" % _('brushes, programming'),
+            u"Popolon (%s)" % _('programming'),
             u"Clement Skau (%s)" % _('programming'),
-            u"Marcelo 'Tanda' Cerviño (%s)" % _('patterns, brushes'),
             u"Jon Nordby (%s)" % _('programming'),
             u"Álinson Santos (%s)" % _('programming'),
             u"Tumagonx (%s)" % _('portability'),
             u"Ilya Portnov (%s)" % _('programming'),
-            u"David Revoy (%s)" % _('brushes'),
-            u"Ramón Miranda (%s)" % _('brushes, patterns'),
-            u"Enrico Guarnieri 'Ico_dY' (%s)" % _('brushes'),
             u"Jonas Wagner (%s)" % _('programming'),
             u"Luka Čehovin (%s)" % _('programming'),
             u"Andrew Chadwick (%s)" % _('programming'),
             u"Till Hartmann (%s)" % _('programming'),
-            u"Nicola Lunghi (%s)" % _('patterns'),
-            u"Toni Kasurinen (%s)" % _('brushes'),
-            u"Сан Саныч (%s)" % _('patterns'),
             u'David Grundberg (%s)' % _('programming'),
             u"Krzysztof Pasek (%s)" % _('programming'),
+            u"Ben O'Steen (%s)" % _('programming'),
+            u"Ferry Jérémie (%s)" % _('programming'),
+            u"しげっち 'sigetch' (%s)" % _('programming'),
             ])
         d.set_artists([
+            u"Artis Rozentāls (%s)" % _('brushes'),
+            u"Popolon (%s)" % _('brushes'),
+            u"Marcelo 'Tanda' Cerviño (%s)" % _('patterns, brushes'),
+            u"David Revoy (%s)" % _('brushes'),
+            u"Ramón Miranda (%s)" % _('brushes, patterns'),
+            u"Enrico Guarnieri 'Ico_dY' (%s)" % _('brushes'),
             u'Sebastian Kraft (%s)' % _('desktop icon'),
+            u"Nicola Lunghi (%s)" % _('patterns'),
+            u"Toni Kasurinen (%s)" % _('brushes'),
+            u"Сан Саныч 'MrMamurk' (%s)" % _('patterns'),
+            u"Andrew Chadwick (%s)" % _('tool icons'),
+            u"Ben O'Steen (%s)" % _('tool icons'),
             ])
         # list all translators, not only those of the current language
         d.set_translator_credits(
@@ -791,3 +1036,5 @@ class Window (windowing.MainWindow, layout.MainWindow):
                  "\n"),
         }
         self.app.message_dialog(text[action.get_name()])
+
+