diff options
Diffstat (limited to 'lvc/widgets/osx/osxmenus.py')
-rw-r--r-- | lvc/widgets/osx/osxmenus.py | 571 |
1 files changed, 571 insertions, 0 deletions
diff --git a/lvc/widgets/osx/osxmenus.py b/lvc/widgets/osx/osxmenus.py new file mode 100644 index 0000000..91baf3a --- /dev/null +++ b/lvc/widgets/osx/osxmenus.py @@ -0,0 +1,571 @@ +# @Base: Miro - an RSS based video player application +# Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011 +# Participatory Culture Foundation +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +# +# In addition, as a special exception, the copyright holders give +# permission to link the code of portions of this program with the OpenSSL +# library. +# +# You must obey the GNU General Public License in all respects for all of +# the code used other than OpenSSL. If you modify file(s) with this +# exception, you may extend this exception to your version of the file(s), +# but you are not obligated to do so. If you do not wish to do so, delete +# this exception statement from your version. If you delete this exception +# statement from all source files in the program, then also delete it here. + +"""menus.py -- Menu handling code.""" + +import logging +import struct + +from objc import nil, NO, YES +import AppKit +from AppKit import * +from Foundation import * + +from lvc import signals +from lvc.widgets import keyboard +# import these names directly into our namespace for easy access +from lvc.widgets.keyboard import Shortcut, MOD + +# XXX hacks +def _(text, *params): + if params: + return text % params[0] + return text + +MODIFIERS_MAP = { + keyboard.MOD: NSCommandKeyMask, + keyboard.CMD: NSCommandKeyMask, + keyboard.SHIFT: NSShiftKeyMask, + keyboard.CTRL: NSControlKeyMask, + keyboard.ALT: NSAlternateKeyMask +} + +if isinstance(NSBackspaceCharacter, int): + backspace = NSBackspaceCharacter +else: + backspace = ord(NSBackspaceCharacter) + +KEYS_MAP = { + keyboard.SPACE: " ", + keyboard.ENTER: "\r", + keyboard.BKSPACE: struct.pack("H", backspace), + keyboard.DELETE: NSDeleteFunctionKey, + keyboard.RIGHT_ARROW: NSRightArrowFunctionKey, + keyboard.LEFT_ARROW: NSLeftArrowFunctionKey, + keyboard.UP_ARROW: NSUpArrowFunctionKey, + keyboard.DOWN_ARROW: NSDownArrowFunctionKey, + '.': '.', + ',': ',' +} +# add function keys +for i in range(1, 13): + portable_key = getattr(keyboard, "F%s" % i) + osx_key = getattr(AppKit, "NSF%sFunctionKey" % i) + KEYS_MAP[portable_key] = osx_key + +REVERSE_MODIFIERS_MAP = dict((i[1], i[0]) for i in MODIFIERS_MAP.items()) +REVERSE_KEYS_MAP = dict((i[1], i[0]) for i in KEYS_MAP.items() + if i[0] != keyboard.BKSPACE) +REVERSE_KEYS_MAP[u'\x7f'] = keyboard.BKSPACE +REVERSE_KEYS_MAP[u'\x1b'] = keyboard.ESCAPE + +def make_modifier_mask(shortcut): + mask = 0 + for modifier in shortcut.modifiers: + mask |= MODIFIERS_MAP[modifier] + return mask + +VIEW_ITEM_MAP = {} + +def _remove_mnemonic(label): + """Remove the underscore used by GTK for mnemonics. + + We totally ignore them on OSX, since they are now deprecated. + """ + return label.replace("_", "") + +def handle_menu_activate(ns_menu_item): + """Handle a menu item being activated. + + This gets called by our application delegate. + """ + + menu_item = ns_menu_item.representedObject() + menu_item.emit("activate") + menubar = menu_item._find_menubar() + if menubar is not None: + menubar.emit("activate", menu_item.name) + +class MenuItemBase(signals.SignalEmitter): + """Base class for MenuItem and Separator""" + def __init__(self): + signals.SignalEmitter.__init__(self) + self.name = None + self.parent = None + + def show(self): + self._menu_item.setHidden_(False) + + def hide(self): + self._menu_item.setHidden_(True) + + def enable(self): + self._menu_item.setEnabled_(True) + + def disable(self): + self._menu_item.setEnabled_(False) + + def remove_from_parent(self): + """Remove this menu item from it's parent Menu.""" + if self.parent is not None: + self.parent.remove(self) + +class MenuItem(MenuItemBase): + """See the GTK version of this method for the current docstring.""" + + # map Miro action names to standard OSX actions. + _STD_ACTION_MAP = { + "HideMiro": (NSApp(), 'hide:'), + "HideOthers": (NSApp(), 'hideOtherApplications:'), + "ShowAll": (NSApp(), 'unhideAllApplications:'), + "Cut": (nil, 'cut:'), + "Copy": (nil, 'copy:'), + "Paste": (nil, 'paste:'), + "Delete": (nil, 'delete:'), + "SelectAll": (nil, 'selectAll:'), + "Zoom": (nil, 'performZoom:'), + "Minimize": (nil, 'performMiniaturize:'), + "BringAllToFront": (nil, 'arrangeInFront:'), + "CloseWindow": (nil, 'performClose:'), + } + + def __init__(self, label, name, shortcut=None): + MenuItemBase.__init__(self) + self.name = name + self._menu_item = self._make_menu_item(label) + self.create_signal('activate') + self._setup_shortcut(shortcut) + + def _make_menu_item(self, label): + menu_item = NSMenuItem.alloc().init() + menu_item.setTitle_(_remove_mnemonic(label)) + # we set ourselves as the represented object for the menu item so we + # can easily translate one to the other + menu_item.setRepresentedObject_(self) + if self.name in self._STD_ACTION_MAP: + menu_item.setTarget_(self._STD_ACTION_MAP[self.name][0]) + menu_item.setAction_(self._STD_ACTION_MAP[self.name][1]) + else: + menu_item.setTarget_(NSApp().delegate()) + menu_item.setAction_('handleMenuActivate:') + return menu_item + + def _setup_shortcut(self, shortcut): + if shortcut is None: + key = '' + modifier_mask = 0 + elif isinstance(shortcut.shortcut, str): + key = shortcut.shortcut + modifier_mask = make_modifier_mask(shortcut) + elif shortcut.shortcut in KEYS_MAP: + key = KEYS_MAP[shortcut.shortcut] + modifier_mask = make_modifier_mask(shortcut) + else: + logging.warn("Don't know how to handle shortcut: %s", shortcut) + return + self._menu_item.setKeyEquivalent_(key) + self._menu_item.setKeyEquivalentModifierMask_(modifier_mask) + + def _change_shortcut(self, shortcut): + self._setup_shortcut(shortcut) + + def set_label(self, new_label): + self._menu_item.setTitle_(new_label) + + def get_label(self): + self._menu_item.title() + + def _find_menubar(self): + """Remove this menu item from it's parent Menu.""" + menu_item = self + while menu_item.parent is not None: + menu_item = menu_item.parent + if isinstance(menu_item, MenuBar): + return menu_item + else: + return None + +class CheckMenuItem(MenuItem): + """See the GTK version of this method for the current docstring.""" + def set_state(self, active): + if active is None: + state = NSMixedState + elif active: + state = NSOnState + else: + state = NSOffState + self._menu_item.setState_(state) + + def get_state(self): + return self._menu_item.state() == NSOnState + + def do_activate(self): + if self._menu_item.state() == NSOffState: + self._menu_item.setState_(NSOnState) + else: + self._menu_item.setState_(NSOffState) + +class RadioMenuItem(CheckMenuItem): + """See the GTK version of this method for the current docstring.""" + def __init__(self, label, name, shortcut=None): + CheckMenuItem.__init__(self, label, name, shortcut) + # The leader of a radio group stores the list of all items in the + # group + self.group_leader = None + self.others_in_group = set() + + def set_group(self, group_item): + if self.group_leader is not None: + raise ValueError("%s is already in a group" % self) + if group_item.group_leader is None: + group_leader = group_item + else: + group_leader = group_item.group_leader + if group_leader.group_leader is not None: + raise AssertionError("group_leader structure is wrong") + self.group_leader = group_leader + group_leader.others_in_group.add(self) + + def remove_from_group(self): + """Remove this RadioMenuItem from its current group.""" + if self.group_leader is not None: + # we have a group leader, remove ourself from their list. + # Note that this code will work even if we're the last item in + # others_in_group. + self.group_leader.others_in_group.remove(self) + self.group_leader = None + elif len(self.others_in_group) > 1: + # we're the group leader, hand off the leader to a different item + first_item = iter(self.others_in_group).next() + for other in self.others_in_group: + if other is first_item: + other.others_in_group = self.others_in_group + other.others_in_group.remove(first_item) + other.group_leader = None + else: + other.group_leader = first_item + self.others_in_group = set() + elif len(self.others_in_group) == 1: + # we're the group leader, but there's only 1 other item. unset + # everything. + for other in self.others_in_group: + other.group_leader = None + self.others_in_group = set() + + def _items_in_group(self): + if self.group_leader is not None: # we have a group leader + yield self.group_leader + for other in self.group_leader.others_in_group: + yield other + elif self.others_in_group: # we're the group leader + yield self + for other in self.others_in_group: + yield other + else: # we don't have a group set + yield self + + def do_activate(self): + for item in self._items_in_group(): + if item is not self: + item.set_state(False) + CheckMenuItem.do_activate(self) + +class Separator(MenuItemBase): + """See the GTK version of this method for the current docstring.""" + def __init__(self): + MenuItemBase.__init__(self) + self._menu_item = NSMenuItem.separatorItem() + +class MenuShell(signals.SignalEmitter): + def __init__(self, nsmenu): + signals.SignalEmitter.__init__(self) + self._menu = nsmenu + self.children = [] + self.parent = None + + def append(self, menu_item): + """Add a menu item to the end of this menu.""" + self.children.append(menu_item) + self._menu.addItem_(menu_item._menu_item) + menu_item.parent = self + + def insert(self, index, menu_item): + """Insert a menu item in the middle of this menu.""" + self.children.insert(index, menu_item) + self._menu.insertItem_atIndex_(menu_item._menu_item, index) + menu_item.parent = self + + def index(self, name): + """Find the position of a child menu item.""" + for i, menu_item in enumerate(self.children): + if menu_item.name == name: + return i + return -1 + + def remove(self, menu_item): + """Remove a child menu item. + + :raises ValueError: menu_item is not a child of this menu + """ + self.children.remove(menu_item) + self._menu.removeItem_(menu_item._menu_item) + menu_item.parent = None + + def get_children(self): + """Get the child menu items in order.""" + return list(self.children) + + def find(self, name): + """Search for a menu or menu item + + This method recursively searches the entire menu structure for a Menu + or MenuItem object with a given name. + + :raises KeyError: name not found + """ + found = self._find(name) + if found is None: + raise KeyError(name) + else: + return found + + def _find(self, name): + """Low-level helper-method for find(). + + :returns: found menu item or None. + """ + for menu_item in self.get_children(): + if menu_item.name == name: + return menu_item + if isinstance(menu_item, Menu): + submenu_find = menu_item._find(name) + if submenu_find is not None: + return submenu_find + return None + +class Menu(MenuShell): + """See the GTK version of this method for the current docstring.""" + def __init__(self, label, name, child_items=None): + MenuShell.__init__(self, NSMenu.alloc().init()) + self._menu.setTitle_(_remove_mnemonic(label)) + # we will enable/disable menu items manually + self._menu.setAutoenablesItems_(False) + self.name = name + if child_items is not None: + for item in child_items: + self.append(item) + self._menu_item = NSMenuItem.alloc().init() + self._menu_item.setTitle_(_remove_mnemonic(label)) + self._menu_item.setSubmenu_(self._menu) + # Hack to set the services menu + if name == "ServicesMenu": + NSApp().setServicesMenu_(self._menu_item) + + def show(self): + self._menu_item.setHidden_(False) + + def hide(self): + self._menu_item.setHidden_(True) + + def enable(self): + self._menu_item.setEnabled_(True) + + def disable(self): + self._menu_item.setEnabled_(False) + +class AppMenu(MenuShell): + """Wrapper for the application menu (AKA the Miro menu) + + We need to special case this because OSX automatically creates the menu + item. + """ + def __init__(self): + MenuShell.__init__(self, NSApp().mainMenu().itemAtIndex_(0).submenu()) + self.name = "Libre Video Converter" + +class MenuBar(MenuShell): + """See the GTK version of this method for the current docstring.""" + def __init__(self): + MenuShell.__init__(self, NSApp().mainMenu()) + self.create_signal('activate') + self._add_app_menu() + + def _add_app_menu(self): + """Add the app menu to this menu bar. + + We need to special case this because OSX automatically adds the + NSMenuItem for the app menu, we just need to set up our wrappers. + """ + self._app_menu = AppMenu() + self.children.append(self._app_menu) + self._app_menu.parent = self + + def add_initial_menus(self, menus): + for menu in menus: + self.append(menu) + self._modify_initial_menus() + + def _extract_menu_item(self, name): + """Helper method for changing the portable menu structure.""" + menu_item = self.find(name) + menu_item.remove_from_parent() + return menu_item + + def _modify_initial_menus(self): + short_appname = "Libre Video Converter" # XXX + + # Application menu + miroMenuItems = [ + self._extract_menu_item("About"), + Separator(), + self._extract_menu_item("Quit") + ] + + for item in miroMenuItems: + self._app_menu.append(item) + + self._app_menu.find("Quit").set_label(_("Quit %(appname)s", + {"appname": short_appname})) + + # Help Menu + #helpItem = self.find("Help") + #helpItem.set_label(_("%(appname)s Help", {"appname": short_appname})) + #helpItem._change_shortcut(Shortcut("?", MOD)) + + self._update_present_menu() + self._connect_to_signals() + + def do_activate(self, name): + # We handle a couple OSX-specific actions here + if name == "PresentActualSize": + NSApp().delegate().present_movie('natural-size') + elif name == "PresentDoubleSize": + NSApp().delegate().present_movie('double-size') + elif name == "PresentHalfSize": + NSApp().delegate().present_movie('half-size') + elif name == "ShowMain": + app.widgetapp.window.nswindow.makeKeyAndOrderFront_(self) + + def _connect_to_signals(self): + return + app.playback_manager.connect("will-play", self._on_playback_change) + app.playback_manager.connect("will-stop", self._on_playback_change) + + def _on_playback_change(self, playback_manager, *args): + self._update_present_menu() + + def _update_present_menu(self): + return + if self._should_enable_present_menu(): + for menu_item in self.present_menu.get_children(): + menu_item.enable() + else: + for menu_item in self.present_menu.get_children(): + menu_item.disable() + + def _should_enable_present_menu(self): + return False + if (app.playback_manager.is_playing and + not app.playback_manager.is_playing_audio): + # we're currently playing video, allow the user to fullscreen + return True + selection_info = app.item_list_controller_manager.get_selection_info() + if (selection_info.has_download and + selection_info.has_file_type('video')): + # A downloaded video is selected, allow the user to start playback + # in fullscreen + return True + return False + +#class ContextMenuHandler(NSObject): +# def initWithCallback_(self, callback): +# self = super(ContextMenuHandler, self).init() +# self.callback = callback +# return self +# +# def handleMenuItem_(self, sender): +# self.callback() +# +#class MiroContextMenu(NSMenu): +# # Works exactly like NSMenu, except it keeps a reference to the menu +# # handler objects. +# def init(self): +# self = super(MiroContextMenu, self).init() +# self.handlers = set() +# return self +# +# def addItem_(self, item): +# if isinstance(item.target(), ContextMenuHandler): +# self.handlers.add(item.target()) +# return NSMenu.addItem_(self, item) +# +def make_context_menu(menu_items): + nsmenu = MiroContextMenu.alloc().init() + for item in menu_items: + if item is None: + nsitem = NSMenuItem.separatorItem() + else: + label, callback = item + nsitem = NSMenuItem.alloc().init() + if isinstance(label, tuple) and len(label) == 2: + label, icon_path = label + image = NSImage.alloc().initWithContentsOfFile_(icon_path) + nsitem.setImage_(image) + if callback is None: + font_size = NSFont.systemFontSize() + font = NSFont.fontWithName_size_("Lucida Sans Italic", font_size) + if font is None: + font = NSFont.systemFontOfSize_(font_size) + attributes = {NSFontAttributeName: font} + attributed_label = NSAttributedString.alloc().initWithString_attributes_(label, attributes) + nsitem.setAttributedTitle_(attributed_label) + else: + nsitem.setTitle_(label) + if isinstance(callback, list): + submenu = make_context_menu(callback) + nsmenu.setSubmenu_forItem_(submenu, nsitem) + else: + handler = ContextMenuHandler.alloc().initWithCallback_(callback) + nsitem.setTarget_(handler) + nsitem.setAction_('handleMenuItem:') + nsmenu.addItem_(nsitem) + return nsmenu + +def translate_event_modifiers(event): + mods = set() + flags = event.modifierFlags() + if flags & NSCommandKeyMask: + mods.add(keyboard.CMD) + if flags & NSControlKeyMask: + mods.add(keyboard.CTRL) + if flags & NSAlternateKeyMask: + mods.add(keyboard.ALT) + if flags & NSShiftKeyMask: + mods.add(keyboard.SHIFT) + return mods |