class Menu(OSDWindow): EPILOG = """Exit codes: 0 - clean exit, user selected option -1 - clean exit, user canceled menu -2 - clean exit, menu closed from callback method 1 - error, invalid arguments 2 - error, failed to access sc-daemon, sc-daemon reported error or died while menu is displayed. 3 - erorr, failed to lock input stick, pad or button(s) """ SUBMENU_OFFSET = 50 def __init__(self, cls="osd-menu"): OSDWindow.__init__(self, cls) self.daemon = None self.config = None self.xdisplay = X.Display(hash( GdkX11.x11_get_default_xdisplay())) # Magic cursor = os.path.join(get_share_path(), "images", 'menu-cursor.svg') self.cursor = Gtk.Image.new_from_file(cursor) self.cursor.set_name("osd-menu-cursor") self.parent = self.create_parent() self.f = Gtk.Fixed() self.f.add(self.parent) self.add(self.f) self._submenu = None self._scon = StickController() self._scon.connect("direction", self.on_stick_direction) self._is_submenu = False self._selected = None self._menuid = None self._use_cursor = False self._eh_ids = [] self._control_with = STICK self._confirm_with = 'A' self._cancel_with = 'B' def set_is_submenu(self): """ Marks menu as submenu. This changes behaviour of some methods, especially disables (un)locking of input stick and buttons. """ self._is_submenu = True def create_parent(self): v = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) v.set_name("osd-menu") return v def pack_items(self, parent, items): for item in items: parent.pack_start(item.widget, True, True, 0) def use_daemon(self, d): """ Allows (re)using already existing DaemonManager instance in same process. use_config() should be be called before parse_argumets() if this is used. """ self.daemon = d if not self._is_submenu: self._cononect_handlers() self.on_daemon_connected(self.daemon) def use_config(self, c): """ Allows reusing already existin Config instance in same process. Has to be called before parse_argumets() """ self.config = c def get_menuid(self): """ Returns ID of used menu. """ return self._menuid def get_selected_item_id(self): """ Returns ID of selected item or None if nothing is selected. """ if self._selected: return self._selected.id return None def _add_arguments(self): OSDWindow._add_arguments(self) self.argparser.add_argument( '--control-with', '-c', type=str, metavar="option", default=STICK, choices=(LEFT, RIGHT, STICK), help= "which pad or stick should be used to navigate menu (default: %s)" % (STICK, )) self.argparser.add_argument( '--confirm-with', type=str, metavar="button", default='A', help="button used to confirm choice (default: A)") self.argparser.add_argument( '--cancel-with', type=str, metavar="button", default='B', help="button used to cancel menu (default: B)") self.argparser.add_argument( '--confirm-with-release', action='store_true', help="confirm choice with button release instead of button press") self.argparser.add_argument( '--cancel-with-release', action='store_true', help="cancel menu with button release instead of button press") self.argparser.add_argument('--use-cursor', '-u', action='store_true', help="display and use cursor") self.argparser.add_argument('--from-profile', '-p', type=str, metavar="profile_file menu_name", help="load menu items from profile file") self.argparser.add_argument('--from-file', '-f', type=str, metavar="filename", help="load menu items from json file") self.argparser.add_argument('--print-items', action='store_true', help="prints menu items to stdout") self.argparser.add_argument('items', type=str, nargs='*', metavar='id title', help="Menu items") def parse_argumets(self, argv): if not OSDWindow.parse_argumets(self, argv): return False if not self.config: self.config = Config() if self.args.from_profile: try: self._menuid = self.args.items[0] self.items = MenuData.from_profile(self.args.from_profile, self._menuid) except IOError: print >> sys.stderr, '%s: error: profile file not found' % ( sys.argv[0]) return False except ValueError: print >> sys.stderr, '%s: error: menu not found' % ( sys.argv[0]) return False elif self.args.from_file: #try: data = json.loads(open(self.args.from_file, "r").read()) self._menuid = self.args.from_file self.items = MenuData.from_json_data(data) #except: # print >>sys.stderr, '%s: error: failed to load menu file' % (sys.argv[0]) # return False else: try: self.items = MenuData.from_args(self.args.items) self._menuid = None except ValueError: print >> sys.stderr, '%s: error: invalid number of arguments' % ( sys.argv[0]) return False # Parse simpler arguments self._control_with = self.args.control_with self._confirm_with = self.args.confirm_with self._cancel_with = self.args.cancel_with if self.args.use_cursor: self.enable_cursor() # Create buttons that are displayed on screen self.items = self.items.generate(self) for item in self.items: item.widget = self.generate_widget(item) self.pack_items(self.parent, self.items) if len(self.items) == 0: print >> sys.stderr, '%s: error: no items in menu' % (sys.argv[0]) return False if self.args.print_items: max_id_len = max(*[len(x.id) for x in self.items]) row_format = "{:>%s}:\t{}" % (max_id_len, ) for item in self.items: print row_format.format(item.id, item.label) return True def enable_cursor(self): if not self._use_cursor: self.f.add(self.cursor) self.f.show_all() self._use_cursor = True def generate_widget(self, item): """ Generates gtk widget for specified menutitem """ if isinstance(item, Separator) and item.label: widget = Gtk.Button.new_with_label(item.label) widget.set_relief(Gtk.ReliefStyle.NONE) widget.set_name("osd-menu-separator") return widget elif isinstance(item, Separator): widget = Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL) widget.set_name("osd-menu-separator") return widget else: widget = Gtk.Button.new_with_label(item.label) widget.set_relief(Gtk.ReliefStyle.NONE) if hasattr(widget.get_children()[0], "set_xalign"): widget.get_children()[0].set_xalign(0) else: widget.get_children()[0].set_halign(Gtk.Align.START) if isinstance(item, Submenu): item.callback = self.show_submenu label1 = widget.get_children()[0] label2 = Gtk.Label(_(">>")) label2.set_property("margin-left", 30) box = Gtk.Box(Gtk.Orientation.HORIZONTAL) widget.remove(label1) box.pack_start(label1, True, True, 1) box.pack_start(label2, False, True, 1) widget.add(box) widget.set_name("osd-menu-item") elif item.id is None: widget.set_name("osd-menu-dummy") else: widget.set_name("osd-menu-item") return widget def select(self, index): if self._selected: self._selected.widget.set_name("osd-menu-item") if self.items[index].id: self._selected = self.items[index] self._selected.widget.set_name("osd-menu-item-selected") return True return False def _cononect_handlers(self): self._eh_ids += [ self.daemon.connect('dead', self.on_daemon_died), self.daemon.connect('error', self.on_daemon_died), self.daemon.connect('event', self.on_event), self.daemon.connect('alive', self.on_daemon_connected), ] def run(self): self.daemon = DaemonManager() self._cononect_handlers() OSDWindow.run(self) def show(self, *a): if not self.select(0): self.next_item(1) OSDWindow.show(self, *a) def on_daemon_died(self, *a): log.error("Daemon died") self.quit(2) def on_failed_to_lock(self, error): log.error("Failed to lock input: %s", error) self.quit(3) def on_daemon_connected(self, *a): def success(*a): log.error("Sucessfully locked input") pass if not self.config: self.config = Config() locks = [self._control_with, self._confirm_with, self._cancel_with] self.daemon.lock(success, self.on_failed_to_lock, *locks) def quit(self, code=-2): if not self._is_submenu: self.daemon.unlock_all() for x in self._eh_ids: self.daemon.disconnect(x) self._eh_ids = [] OSDWindow.quit(self, code) def next_item(self, direction): """ Selects next menu item, based on self._direction """ start, i = -1, 0 try: start = self.items.index(self._selected) i = start + direction except: pass while True: if i == start: # Cannot find valid menu item self.select(start) break if i >= len(self.items): i = 0 continue if i < 0: i = len(self.items) - 1 continue if self.select(i): # Not a separator break i += direction if start < 0: start = 0 def on_submenu_closed(self, *a): if self._submenu.get_exit_code() in (0, -2): self.quit(self._submenu.get_exit_code()) self._selected = self._submenu._selected self._submenu = None def show_submenu(self, trash, trash2, menuitem): """ Called when user chooses menu item pointing to submenu """ filename = find_menu(menuitem.filename) if filename: self._submenu = self.__class__() sub_pos = list(self.position) for i in (0, 1): sub_pos[i] = (sub_pos[i] - self.SUBMENU_OFFSET if sub_pos[i] < 0 else sub_pos[i] + self.SUBMENU_OFFSET) self._submenu.use_config(self.config) self._submenu.parse_argumets([ "menu.py", "-x", str(sub_pos[0]), "-y", str(sub_pos[1]), "--from-file", filename, "--control-with", self._control_with, "--confirm-with", self._confirm_with, "--cancel-with", self._cancel_with ]) self._submenu.set_is_submenu() self._submenu.use_daemon(self.daemon) self._submenu.connect('destroy', self.on_submenu_closed) self._submenu.show() def _control_equals_cancel(self, daemon, x, y): """ Called by on_event in that very special case when both confirm_with and cancel_with are set to STICK. Separated because RadialMenu overrides on_event and still needs to call this. Returns True if menu was canceled. """ distance = sqrt(x * x + y * y) if distance < STICK_PAD_MAX / 8: self.quit(-1) return True return False def on_stick_direction(self, trash, x, y): if y != 0: self.next_item(y) def on_event(self, daemon, what, data): if self._submenu: return self._submenu.on_event(daemon, what, data) if what == self._control_with: x, y = data if self._use_cursor: # Special case, both confirm_with and cancel_with # can be set to STICK if self._cancel_with == STICK and self._control_with == STICK: if self._control_equals_cancel(daemon, x, y): return max_w = self.get_allocation().width - ( self.cursor.get_allocation().width * 0.8) max_h = self.get_allocation().height - ( self.cursor.get_allocation().height * 1.0) x = ((x / (STICK_PAD_MAX * 2.0)) + 0.5) * max_w y = (0.5 - (y / (STICK_PAD_MAX * 2.0))) * max_h x -= self.cursor.get_allocation().width * 0.5 y -= self.cursor.get_allocation().height * 0.5 self.f.move(self.cursor, int(x), int(y)) for i in self.items: if point_in_gtkrect(i.widget.get_allocation(), x, y): self.select(self.items.index(i)) else: self._scon.set_stick(x, y) elif what == self._cancel_with: if data[0] == 0: # Button released self.quit(-1) elif what == self._confirm_with: if data[0] == 0: # Button released if self._selected and self._selected.callback: self._selected.callback(self, self.daemon, self._selected) elif self._selected: self.quit(0) else: self.quit(-1)
class Dialog(OSDWindow): EPILOG="""Exit codes: 0 - clean exit, user selected option -1 - clean exit, user canceled dialog 1 - error, invalid arguments 2 - error, failed to access sc-daemon, sc-daemon reported error or died while dialog is displayed. 3 - erorr, failed to lock input stick, pad or button(s) """ def __init__(self, cls="osd-menu"): self._buttons = None self._text = None OSDWindow.__init__(self, cls) self.daemon = None self.config = None self.feedback = None self.controller = None self.xdisplay = X.Display(hash(GdkX11.x11_get_default_xdisplay())) # Magic self.parent = self.create_parent() self.f = Gtk.Fixed() self.f.add(self.parent) self.add(self.f) self._scon = StickController() self._scon.connect("direction", self.on_stick_direction) self._selected = None self._eh_ids = [] def create_parent(self): self._text = Gtk.Label() self._buttons = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) dialog = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) dialog.pack_start(self._text, True, True, 0) dialog.pack_start(self._buttons, True, True, 0) dialog.set_name("osd-dialog") self._buttons.set_name("osd-dialog-buttons") self._text.set_name("osd-dialog-text") return dialog def pack_items(self, parent, items): for item in items: if hasattr(item.widget, "set_alignment"): item.widget.set_alignment(0.5, 0.5) self._buttons.pack_end(item.widget, True, True, 0) def use_daemon(self, d): """ Allows (re)using already existing DaemonManager instance in same process. use_config() should be be called before parse_argumets() if this is used. """ self.daemon = d self._connect_handlers() self.on_daemon_connected(self.daemon) def use_config(self, c): """ Allows reusing already existin Config instance in same process. Has to be called before parse_argumets() """ self.config = c def get_menuid(self): # Just to be compatibile with menus when called from scc-osd-daemon return None def get_selected_item_id(self): """ Returns ID of selected item or None if nothing is selected. """ if self._selected: return self._selected.id return None def _add_arguments(self): OSDWindow._add_arguments(self) self.argparser.add_argument('--confirm-with', type=str, metavar="button", default=DEFAULT, help="button used to confirm choice") self.argparser.add_argument('--cancel-with', type=str, metavar="button", default=DEFAULT, help="button used to cancel dialog") self.argparser.add_argument('--feedback-amplitude', type=int, help="enables and sets power of feedback effect generated when active menu option is changed") self.argparser.add_argument('--text', type=str, metavar='text', help="Dialog text") self.argparser.add_argument('items', type=str, nargs='*', metavar='id text', help="Dialog buttons") def parse_argumets(self, argv): if not OSDWindow.parse_argumets(self, argv): return False if not self.config: self.config = Config() try: self.items = MenuData.from_args(self.args.items) self._menuid = None except ValueError: print >>sys.stderr, '%s: error: invalid number of arguments' % (sys.argv[0]) return False self._text.set_label(self.args.text) if self.args.feedback_amplitude: side = "LEFT" self.feedback = side, int(self.args.feedback_amplitude) # Create buttons that are displayed on screen items = self.items.generate(self) self.items = [] for item in items: item.widget = self.generate_widget(item) if item.widget is not None: self.items.append(item) self.pack_items(self.parent, self.items) if len(self.items) == 0: print >>sys.stderr, '%s: error: no items in menu' % (sys.argv[0]) return False return True def generate_widget(self, item): """ Generates gtk widget for specified menutitem """ widget = Gtk.Button.new_with_label(item.label) widget.set_relief(Gtk.ReliefStyle.NONE) if hasattr(widget.get_children()[0], "set_xalign"): widget.get_children()[0].set_xalign(0) else: widget.get_children()[0].set_halign(Gtk.Align.START) widget.set_name("osd-menu-item") return widget def select(self, index): if self._selected: self._selected.widget.set_name(self._selected.widget.get_name() .replace("-selected", "")) if self.items[index].id: if self._selected != self.items[index]: if self.feedback and self.controller: self.controller.feedback(*self.feedback) self._selected = self.items[index] self._selected.widget.set_name( self._selected.widget.get_name() + "-selected") return True return False def _connect_handlers(self): self._eh_ids += [ (self.daemon, self.daemon.connect('dead', self.on_daemon_died)), (self.daemon, self.daemon.connect('error', self.on_daemon_died)), (self.daemon, self.daemon.connect('alive', self.on_daemon_connected)), ] def run(self): self.daemon = DaemonManager() self._connect_handlers() OSDWindow.run(self) def show(self, *a): if not self.select(0): self.next_item(1) OSDWindow.show(self, *a) def on_daemon_died(self, *a): log.error("Daemon died") self.quit(2) def on_failed_to_lock(self, error): log.error("Failed to lock input: %s", error) self.quit(3) def on_daemon_connected(self, *a): def success(*a): log.error("Sucessfully locked input") if not self.config: self.config = Config() self.controller = self.choose_controller(self.daemon) if self.controller is None or not self.controller.is_connected(): # There is no controller connected to daemon self.on_failed_to_lock("Controller not connected") return ccfg = self.config.get_controller_config(self.controller.get_id()) self._control_with = ccfg["menu_control"] self._confirm_with = ccfg["menu_confirm"] if self.args.confirm_with == DEFAULT else self.args.confirm_with self._cancel_with = ccfg["menu_cancel"] if self.args.cancel_with == DEFAULT else self.args.cancel_with self._eh_ids += [ (self.controller, self.controller.connect('event', self.on_event)) ] locks = [ self._control_with, self._confirm_with, self._cancel_with ] self.controller.lock(success, self.on_failed_to_lock, *locks) def quit(self, code=-2): if self.get_controller(): self.get_controller().unlock_all() for source, eid in self._eh_ids: source.disconnect(eid) self._eh_ids = [] OSDWindow.quit(self, code) def next_item(self, direction): """ Selects next menu item, based on self._direction """ start, i = -1, 0 try: start = self.items.index(self._selected) i = start + direction except: pass while True: if i == start: # Cannot find valid menu item self.select(start) break if i >= len(self.items): i = 0 continue if i < 0: i = len(self.items) - 1 continue if self.select(i): break i += direction if start < 0: start = 0 def on_stick_direction(self, trash, x, y): if x != 0: self.next_item(x) def on_event(self, daemon, what, data): if what == self._control_with: self._scon.set_stick(*data) elif what == self._cancel_with: if data[0] == 0: # Button released self.quit(-1) elif what == self._confirm_with: if data[0] == 0: # Button released if self._selected and self._selected.callback: self._selected.callback(self, self.daemon, self.controller, self._selected) elif self._selected: self.quit(0) else: self.quit(-1)
class Menu(OSDWindow): EPILOG = """Exit codes: 0 - clean exit, user selected option -1 - clean exit, user canceled menu -2 - clean exit, menu closed from callback method 1 - error, invalid arguments 2 - error, failed to access sc-daemon, sc-daemon reported error or died while menu is displayed. 3 - erorr, failed to lock input stick, pad or button(s) """ SUBMENU_OFFSET = 50 PREFER_BW_ICONS = True def __init__(self, cls="osd-menu"): OSDWindow.__init__(self, cls) self.daemon = None self.config = None self.feedback = None self.controller = None self.xdisplay = X.Display(hash( GdkX11.x11_get_default_xdisplay())) # Magic cursor = os.path.join(get_share_path(), "images", 'menu-cursor.svg') self.cursor = Gtk.Image.new_from_file(cursor) self.cursor.set_name("osd-menu-cursor") self.parent = self.create_parent() self.f = Gtk.Fixed() self.f.add(self.parent) self.add(self.f) self._submenu = None self._scon = StickController() self._scon.connect("direction", self.on_stick_direction) self._is_submenu = False self._selected = None self._menuid = None self._use_cursor = False self._eh_ids = [] self._control_with = STICK self._confirm_with = 'A' self._cancel_with = 'B' def set_is_submenu(self): """ Marks menu as submenu. This changes behaviour of some methods, especially disables (un)locking of input stick and buttons. """ self._is_submenu = True def create_parent(self): v = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) v.set_name("osd-menu") return v def pack_items(self, parent, items): for item in items: parent.pack_start(item.widget, True, True, 0) def use_daemon(self, d): """ Allows (re)using already existing DaemonManager instance in same process. use_config() should be be called before parse_argumets() if this is used. """ self.daemon = d if not self._is_submenu: self._connect_handlers() self.on_daemon_connected(self.daemon) def use_config(self, c): """ Allows reusing already existin Config instance in same process. Has to be called before parse_argumets() """ self.config = c def get_menuid(self): """ Returns ID of used menu. """ return self._menuid def get_selected_item_id(self): """ Returns ID of selected item or None if nothing is selected. """ if self._selected: return self._selected.id return None def _add_arguments(self): OSDWindow._add_arguments(self) self.argparser.add_argument( '--control-with', '-c', type=str, metavar="option", default=DEFAULT, choices=(DEFAULT, LEFT, RIGHT, STICK), help="which pad or stick should be used to navigate menu") self.argparser.add_argument('--confirm-with', type=str, metavar="button", default=DEFAULT, help="button used to confirm choice") self.argparser.add_argument('--cancel-with', type=str, metavar="button", default=DEFAULT, help="button used to cancel menu") self.argparser.add_argument( '--confirm-with-release', action='store_true', help="confirm choice with button release instead of button press") self.argparser.add_argument( '--cancel-with-release', action='store_true', help="cancel menu with button release instead of button press") self.argparser.add_argument('--use-cursor', '-u', action='store_true', help="display and use cursor") self.argparser.add_argument('--size', type=int, help="sets prefered width or height") self.argparser.add_argument( '--feedback-amplitude', type=int, help= "enables and sets power of feedback effect generated when active menu option is changed" ) self.argparser.add_argument('--from-profile', '-p', type=str, metavar="profile_file menu_name", help="load menu items from profile file") self.argparser.add_argument('--from-file', '-f', type=str, metavar="filename", help="load menu items from json file") self.argparser.add_argument('--print-items', action='store_true', help="prints menu items to stdout") self.argparser.add_argument('items', type=str, nargs='*', metavar='id title', help="Menu items") @staticmethod def _get_on_screen_position(w): a = w.get_allocation() parent = w.get_parent() if parent: if isinstance(parent, Menu) and parent.get_window() is not None: x, y = parent.get_window().get_position() else: x, y = Menu._get_on_screen_position(parent) return a.x + x, a.y + y else: return a.x, a.y def parse_menu(self): if self.args.from_profile: try: self._menuid = self.args.items[0] self.items = MenuData.from_profile(self.args.from_profile, self._menuid) except IOError: print >> sys.stderr, '%s: error: profile file not found' % ( sys.argv[0]) return False except ValueError: print >> sys.stderr, '%s: error: menu not found' % ( sys.argv[0]) return False elif self.args.from_file: try: self._menuid = self.args.from_file self.items = MenuData.from_file(self.args.from_file) except: print >> sys.stderr, '%s: error: failed to load menu file' % ( sys.argv[0]) return False else: try: self.items = MenuData.from_args(self.args.items) self._menuid = None except ValueError: print >> sys.stderr, '%s: error: invalid number of arguments' % ( sys.argv[0]) return False return True def parse_argumets(self, argv): if not OSDWindow.parse_argumets(self, argv): return False if not self.parse_menu(): return False if not self.config: self.config = Config() # Parse simpler arguments self._size = self.args.size # Create buttons that are displayed on screen items = self.items.generate(self) self.items = [] for item in items: item.widget = self.generate_widget(item) if item.widget is not None: self.items.append(item) self.pack_items(self.parent, self.items) if len(self.items) == 0: print >> sys.stderr, '%s: error: no items in menu' % (sys.argv[0]) return False if self.args.print_items: max_id_len = max(*[len(x.id) for x in self.items]) row_format = "{:>%s}:\t{}" % (max_id_len, ) for item in self.items: print row_format.format(item.id, item.label) return True def enable_cursor(self): if not self._use_cursor: self.f.add(self.cursor) self.f.show_all() self._use_cursor = True def generate_widget(self, item): """ Generates gtk widget for specified menutitem """ if isinstance(item, Separator) and item.label: widget = Gtk.Button.new_with_label(item.label) widget.set_relief(Gtk.ReliefStyle.NONE) widget.set_name("osd-menu-separator") return widget elif isinstance(item, Separator): widget = Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL) widget.set_name("osd-menu-separator") return widget else: widget = Gtk.Button.new_with_label(item.label) widget.set_relief(Gtk.ReliefStyle.NONE) if hasattr(widget.get_children()[0], "set_xalign"): widget.get_children()[0].set_xalign(0) else: widget.get_children()[0].set_halign(Gtk.Align.START) if isinstance(item, Submenu): item.callback = self.show_submenu label1 = widget.get_children()[0] label2 = Gtk.Label(_(">>")) label2.set_property("margin-left", 30) box = Gtk.Box(Gtk.Orientation.HORIZONTAL) widget.remove(label1) box.pack_start(label1, True, True, 1) box.pack_start(label2, False, True, 1) widget.add(box) widget.set_name("osd-menu-item") elif item.id is None: widget.set_name("osd-menu-dummy") else: widget.set_name("osd-menu-item") if isinstance(item.icon, Gio.FileIcon): icon_file = item.icon.get_file().get_path() has_colors = True elif isinstance(item.icon, Gio.ThemedIcon): icon = Gtk.IconTheme.get_default().choose_icon( item.icon.get_names(), 64, 0) icon_file = icon.get_filename() if icon else None has_colors = True else: icon_file, has_colors = find_icon(item.icon, self.PREFER_BW_ICONS) if icon_file: icon = MenuIcon(icon_file, has_colors) label = widget.get_children()[0] for c in [] + widget.get_children(): widget.remove(c) box = Gtk.Box() box.pack_start(icon, False, True, 0) box.pack_start(label, True, True, 10) widget.add(box) return widget def select(self, index): if self._selected: self._selected.widget.set_name( self._selected.widget.get_name().replace("-selected", "")) if self.items[index].id: if self._selected != self.items[index]: if self.feedback and self.controller: self.controller.feedback(*self.feedback) self._selected = self.items[index] self._selected.widget.set_name(self._selected.widget.get_name() + "-selected") GLib.timeout_add(2, self._check_on_screen_position) return True return False def _check_on_screen_position(self): x, y = Menu._get_on_screen_position(self._selected.widget) screen_height = self.get_window().get_screen().get_height() if y < 50: wx, wy = self.get_window().get_position() wy += 5 self.get_window().move(wx, wy) GLib.timeout_add(2, self._check_on_screen_position) if y > screen_height - 100: wx, wy = self.get_window().get_position() wy -= 5 self.get_window().move(wx, wy) GLib.timeout_add(2, self._check_on_screen_position) def _connect_handlers(self): self._eh_ids += [ (self.daemon, self.daemon.connect('dead', self.on_daemon_died)), (self.daemon, self.daemon.connect('error', self.on_daemon_died)), (self.daemon, self.daemon.connect('alive', self.on_daemon_connected)), ] def run(self): self.daemon = DaemonManager() self._connect_handlers() OSDWindow.run(self) def show(self, *a): if not self.select(0): self.next_item(1) OSDWindow.show(self, *a) def on_daemon_connected(self, *a): if not self.config: self.config = Config() self.controller = self.choose_controller(self.daemon) if self.controller is None or not self.controller.is_connected(): # There is no controller connected to daemon self.on_failed_to_lock("Controller not connected") return self.use_controller(self.controller) self._eh_ids += [ (self.controller, self.controller.connect('event', self.on_event)), (self.controller, self.controller.connect('lost', self.on_controller_lost)), ] self.lock_inputs() def use_controller(self, controller): ccfg = self.config.get_controller_config(controller.get_id()) self._control_with = ccfg[ "menu_control"] if self.args.control_with == DEFAULT else self.args.control_with self._cancel_with = ccfg[ "menu_cancel"] if self.args.cancel_with == DEFAULT else self.args.cancel_with if self.args.confirm_with == DEFAULT: self._confirm_with = ccfg["menu_confirm"] elif self.args.confirm_with == SAME: if self._control_with == RIGHT: self._confirm_with = SCButtons.RPADTOUCH.name else: self._confirm_with = SCButtons.LPADTOUCH.name else: self._confirm_with = self.args.confirm_with if self.args.use_cursor: # As special case, using LEFT pad on controller with # actual DPAD should not display cursor if self._control_with != LEFT or (controller.get_flags() & ControllerFlags.HAS_DPAD) == 0: self.enable_cursor() if self.args.feedback_amplitude: side = "LEFT" if self._control_with == "RIGHT": side = "RIGHT" elif self._control_with == "STICK": side = "BOTH" self.feedback = side, int(self.args.feedback_amplitude) def lock_inputs(self): def success(*a): log.error("Sucessfully locked input") locks = [self._control_with, self._confirm_with, self._cancel_with] self.controller.lock(success, self.on_failed_to_lock, *locks) def quit(self, code=-2): if not self._is_submenu: if self.get_controller(): self.get_controller().unlock_all() for source, eid in self._eh_ids: source.disconnect(eid) self._eh_ids = [] OSDWindow.quit(self, code) def next_item(self, direction): """ Selects next menu item, based on self._direction """ start, i = -1, 0 try: start = self.items.index(self._selected) i = start + direction except: pass while True: if i == start: # Cannot find valid menu item self.select(start) break if i >= len(self.items): i = 0 continue if i < 0: i = len(self.items) - 1 continue if self.select(i): # Not a separator break i += direction if start < 0: start = 0 def on_submenu_closed(self, *a): self.set_name("osd-menu") if self._submenu.get_exit_code() in (0, -2): self._menuid = self._submenu._menuid self._selected = self._submenu._selected self.quit(self._submenu.get_exit_code()) self._submenu = None def show_submenu(self, trash, trash2, trash3, menuitem): """ Called when user chooses menu item pointing to submenu """ filename = find_menu(menuitem.filename) if filename: self._submenu = self.__class__() sub_pos = list(self.position) for i in (0, 1): sub_pos[i] = (sub_pos[i] - self.SUBMENU_OFFSET if sub_pos[i] < 0 else sub_pos[i] + self.SUBMENU_OFFSET) self._submenu.use_config(self.config) self._submenu.parse_argumets([ "menu.py", "-x", str(sub_pos[0]), "-y", str(sub_pos[1]), "--from-file", filename, "--control-with", self._control_with, "--confirm-with", self._confirm_with, "--cancel-with", self._cancel_with ]) self._submenu.set_is_submenu() self._submenu.use_daemon(self.daemon) self._submenu.use_controller(self.controller) self._submenu.controller = self.controller self._submenu.connect('destroy', self.on_submenu_closed) self._submenu.show() self.set_name("osd-menu-inactive") def _control_equals_cancel(self, daemon, x, y): """ Called by on_event in that very special case when both confirm_with and cancel_with are set to STICK. Separated because RadialMenu overrides on_event and still needs to call this. Returns True if menu was canceled. """ distance = sqrt(x * x + y * y) if distance < STICK_PAD_MAX / 8: self.quit(-1) return True return False def on_stick_direction(self, trash, x, y): if y != 0: self.next_item(y) def on_event(self, daemon, what, data): if self._submenu: return self._submenu.on_event(daemon, what, data) if what == self._control_with: x, y = data if self._use_cursor: # Special case, both confirm_with and cancel_with # can be set to STICK if self._cancel_with == STICK and self._control_with == STICK: if self._control_equals_cancel(daemon, x, y): return pad_w = self.cursor.get_allocation().width * 0.5 pad_h = self.cursor.get_allocation().height * 0.5 max_w = self.get_allocation().width - 2 * pad_w max_h = self.get_allocation().height - 2 * pad_h x, y = circle_to_square(x / (STICK_PAD_MAX * 2.0), y / (STICK_PAD_MAX * 2.0)) x = clamp(pad_w, (pad_w + max_w) * 0.5 + x * max_w, max_w - pad_w) y = clamp(pad_h, (pad_h + max_h) * 0.5 + y * max_h * -1, max_h - pad_h) self.f.move(self.cursor, int(x), int(y)) for i in self.items: if point_in_gtkrect(i.widget.get_allocation(), x, y): self.select(self.items.index(i)) else: self._scon.set_stick(x, y) elif what == self._cancel_with: if data[0] == 0: # Button released self.quit(-1) elif what == self._confirm_with: if data[0] == 0: # Button released if self._selected and self._selected.callback: self._selected.callback(self, self.daemon, self.controller, self._selected) elif self._selected: self.quit(0) else: self.quit(-1)
class Menu(OSDWindow): EPILOG="""Exit codes: 0 - clean exit, user selected option -1 - clean exit, user canceled menu -2 - clean exit, menu closed from callback method 1 - error, invalid arguments 2 - error, failed to access sc-daemon, sc-daemon reported error or died while menu is displayed. 3 - erorr, failed to lock input stick, pad or button(s) """ SUBMENU_OFFSET = 50 PREFER_BW_ICONS = True def __init__(self, cls="osd-menu"): OSDWindow.__init__(self, cls) self.daemon = None self.config = None self.feedback = None self.controller = None self.xdisplay = X.Display(hash(GdkX11.x11_get_default_xdisplay())) # Magic cursor = os.path.join(get_share_path(), "images", 'menu-cursor.svg') self.cursor = Gtk.Image.new_from_file(cursor) self.cursor.set_name("osd-menu-cursor") self.parent = self.create_parent() self.f = Gtk.Fixed() self.f.add(self.parent) self.add(self.f) self._submenu = None self._scon = StickController() self._scon.connect("direction", self.on_stick_direction) self._is_submenu = False self._selected = None self._menuid = None self._use_cursor = False self._eh_ids = [] self._control_with = STICK self._control_with_dpad = False self._confirm_with = 'A' self._cancel_with = 'B' def set_is_submenu(self): """ Marks menu as submenu. This changes behaviour of some methods, especially disables (un)locking of input stick and buttons. """ self._is_submenu = True def create_parent(self): v = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) v.set_name("osd-menu") return v def pack_items(self, parent, items): for item in items: parent.pack_start(item.widget, True, True, 0) def use_daemon(self, d): """ Allows (re)using already existing DaemonManager instance in same process. use_config() should be be called before parse_argumets() if this is used. """ self.daemon = d if not self._is_submenu: self._connect_handlers() self.on_daemon_connected(self.daemon) def use_config(self, c): """ Allows reusing already existin Config instance in same process. Has to be called before parse_argumets() """ self.config = c def get_menuid(self): """ Returns ID of used menu. """ return self._menuid def get_selected_item_id(self): """ Returns ID of selected item or None if nothing is selected. """ if self._selected: return self._selected.id return None def _add_arguments(self): OSDWindow._add_arguments(self) self.argparser.add_argument('--control-with', '-c', type=str, metavar="option", default=DEFAULT, choices=(DEFAULT, LEFT, RIGHT, STICK), help="which pad or stick should be used to navigate menu") self.argparser.add_argument('--confirm-with', type=str, metavar="button", default=DEFAULT, help="button used to confirm choice") self.argparser.add_argument('--cancel-with', type=str, metavar="button", default=DEFAULT, help="button used to cancel menu") self.argparser.add_argument('--confirm-with-release', action='store_true', help="confirm choice with button release instead of button press") self.argparser.add_argument('--cancel-with-release', action='store_true', help="cancel menu with button release instead of button press") self.argparser.add_argument('--use-cursor', '-u', action='store_true', help="display and use cursor") self.argparser.add_argument('--size', type=int, help="sets prefered width or height") self.argparser.add_argument('--feedback-amplitude', type=int, help="enables and sets power of feedback effect generated when active menu option is changed") self.argparser.add_argument('--from-profile', '-p', type=str, metavar="profile_file menu_name", help="load menu items from profile file") self.argparser.add_argument('--from-file', '-f', type=str, metavar="filename", help="load menu items from json file") self.argparser.add_argument('--print-items', action='store_true', help="prints menu items to stdout") self.argparser.add_argument('items', type=str, nargs='*', metavar='id title', help="Menu items") @staticmethod def _get_on_screen_position(w): a = w.get_allocation() parent = w.get_parent() if parent: if isinstance(parent, Menu) and parent.get_window() is not None: x, y = parent.get_window().get_position() else: x, y = Menu._get_on_screen_position(parent) return a.x + x, a.y + y else: return a.x, a.y def parse_menu(self): if self.args.from_profile: try: self._menuid = self.args.items[0] self.items = MenuData.from_profile(self.args.from_profile, self._menuid) except IOError: print >>sys.stderr, '%s: error: profile file not found' % (sys.argv[0]) return False except ValueError: print >>sys.stderr, '%s: error: menu not found' % (sys.argv[0]) return False elif self.args.from_file: try: self._menuid = self.args.from_file self.items = MenuData.from_file(self.args.from_file) except: print >>sys.stderr, '%s: error: failed to load menu file' % (sys.argv[0]) return False else: try: self.items = MenuData.from_args(self.args.items) self._menuid = None except ValueError: print >>sys.stderr, '%s: error: invalid number of arguments' % (sys.argv[0]) return False return True def parse_argumets(self, argv): if not OSDWindow.parse_argumets(self, argv): return False if not self.parse_menu(): return False if not self.config: self.config = Config() # Parse simpler arguments self._size = self.args.size # Create buttons that are displayed on screen items = self.items.generate(self) self.items = [] for item in items: item.widget = self.generate_widget(item) if item.widget is not None: self.items.append(item) self.pack_items(self.parent, self.items) if len(self.items) == 0: print >>sys.stderr, '%s: error: no items in menu' % (sys.argv[0]) return False if self.args.print_items: max_id_len = max(*[ len(x.id) for x in self.items ]) row_format ="{:>%s}:\t{}" % (max_id_len,) for item in self.items: print row_format.format(item.id, item.label) return True def enable_cursor(self): if not self._use_cursor: self.f.add(self.cursor) self.f.show_all() self._use_cursor = True def generate_widget(self, item): """ Generates gtk widget for specified menutitem """ if isinstance(item, Separator) and item.label: widget = Gtk.Button.new_with_label(item.label) widget.set_relief(Gtk.ReliefStyle.NONE) widget.set_name("osd-menu-separator") return widget elif isinstance(item, Separator): widget = Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL) widget.set_name("osd-menu-separator") return widget else: widget = Gtk.Button.new_with_label(item.label) widget.set_relief(Gtk.ReliefStyle.NONE) if hasattr(widget.get_children()[0], "set_xalign"): widget.get_children()[0].set_xalign(0) else: widget.get_children()[0].set_halign(Gtk.Align.START) if isinstance(item, Submenu): item.callback = self.show_submenu label1 = widget.get_children()[0] label2 = Gtk.Label(_(">>")) label2.set_property("margin-left", 30) box = Gtk.Box(Gtk.Orientation.HORIZONTAL) widget.remove(label1) box.pack_start(label1, True, True, 1) box.pack_start(label2, False, True, 1) widget.add(box) widget.set_name("osd-menu-item") elif item.id is None: widget.set_name("osd-menu-dummy") else: widget.set_name("osd-menu-item") if isinstance(item.icon, Gio.FileIcon): icon_file = item.icon.get_file().get_path() has_colors = True elif isinstance(item.icon, Gio.ThemedIcon): icon = Gtk.IconTheme.get_default().choose_icon( item.icon.get_names(), 64, 0) icon_file = icon.get_filename() if icon else None has_colors = True else: icon_file, has_colors = find_icon(item.icon, self.PREFER_BW_ICONS) if icon_file: icon = MenuIcon(icon_file, has_colors) label = widget.get_children()[0] for c in [] + widget.get_children(): widget.remove(c) box = Gtk.Box() box.pack_start(icon, False, True, 0) box.pack_start(label, True, True, 10) widget.add(box) return widget def select(self, index): if self._selected: self._selected.widget.set_name(self._selected.widget.get_name() .replace("-selected", "")) if self.items[index].id: if self._selected != self.items[index]: if self.feedback and self.controller: self.controller.feedback(*self.feedback) self._selected = self.items[index] self._selected.widget.set_name( self._selected.widget.get_name() + "-selected") GLib.timeout_add(2, self._check_on_screen_position) return True return False def _check_on_screen_position(self, quick=False): x, y = Menu._get_on_screen_position(self._selected.widget) try: m = self.get_window().get_display().get_monitor_at_window(self.get_window()) assert m y_offset = m.get_geometry().y screen_height = m.get_geometry().height except: y_offset = 0 screen_height = self.get_window().get_screen().get_height() y -= y_offset if y < 50: wx, wy = self.get_window().get_position() if quick: wy = 50 - (y - wy) else: wy += 5 GLib.timeout_add(2, self._check_on_screen_position) self.get_window().move(wx, wy) if y > screen_height - 100: wx, wy = self.get_window().get_position() if quick: wy = screen_height - 100 - (y - wy) else: wy -= 5 GLib.timeout_add(2, self._check_on_screen_position) self.get_window().move(wx, wy) def _connect_handlers(self): self._eh_ids += [ (self.daemon, self.daemon.connect('dead', self.on_daemon_died)), (self.daemon, self.daemon.connect('error', self.on_daemon_died)), (self.daemon, self.daemon.connect('alive', self.on_daemon_connected)), ] def run(self): self.daemon = DaemonManager() self._connect_handlers() OSDWindow.run(self) def show(self, *a): if not self.select(0): self.next_item(1) OSDWindow.show(self, *a) GLib.timeout_add(1, self._check_on_screen_position, True) def on_daemon_connected(self, *a): if not self.config: self.config = Config() self.controller = self.choose_controller(self.daemon) if self.controller is None or not self.controller.is_connected(): # There is no controller connected to daemon self.on_failed_to_lock("Controller not connected") return self.use_controller(self.controller) self._eh_ids += [ (self.controller, self.controller.connect('event', self.on_event)), (self.controller, self.controller.connect('lost', self.on_controller_lost)), ] self.lock_inputs() def use_controller(self, controller): ccfg = self.config.get_controller_config(controller.get_id()) self._control_with = ccfg["menu_control"] if self.args.control_with == DEFAULT else self.args.control_with self._cancel_with = ccfg["menu_cancel"] if self.args.cancel_with == DEFAULT else self.args.cancel_with if self.args.confirm_with == DEFAULT: self._confirm_with = ccfg["menu_confirm"] elif self.args.confirm_with == SAME: if self._control_with == RIGHT: self._confirm_with = SCButtons.RPADTOUCH.name else: self._confirm_with = SCButtons.LPADTOUCH.name else: self._confirm_with = self.args.confirm_with if self.args.use_cursor: # As special case, using LEFT pad on controller with # actual DPAD should not display cursor if self._control_with != LEFT or (controller.get_flags() & ControllerFlags.HAS_DPAD) == 0: self.enable_cursor() if self.args.feedback_amplitude: side = "LEFT" if self._control_with == "RIGHT": side = "RIGHT" elif self._control_with == "STICK": side = "BOTH" self.feedback = side, int(self.args.feedback_amplitude) def lock_inputs(self): def success(*a): log.error("Sucessfully locked input") locks = [ self._control_with, self._confirm_with, self._cancel_with ] if self._control_with == "STICK": if self.controller.get_flags() & ControllerFlags.HAS_DPAD != 0: self._control_with_dpad = True locks += [ "LEFT" ] self.controller.lock(success, self.on_failed_to_lock, *locks) def quit(self, code=-2): if not self._is_submenu: if self.get_controller(): self.get_controller().unlock_all() for source, eid in self._eh_ids: source.disconnect(eid) self._eh_ids = [] OSDWindow.quit(self, code) def next_item(self, direction): """ Selects next menu item, based on self._direction """ start, i = -1, 0 try: start = self.items.index(self._selected) i = start + direction except: pass while True: if i == start: # Cannot find valid menu item self.select(start) break if i >= len(self.items): i = 0 GLib.timeout_add(1, self._check_on_screen_position, True) continue if i < 0: i = len(self.items) - 1 GLib.timeout_add(1, self._check_on_screen_position, True) continue if self.select(i): # Not a separator break i += direction if start < 0: start = 0 def on_submenu_closed(self, *a): self.set_name("osd-menu") if self._submenu.get_exit_code() in (0, -2): self._menuid = self._submenu._menuid self._selected = self._submenu._selected self.quit(self._submenu.get_exit_code()) self._submenu = None def show_submenu(self, trash, trash2, trash3, menuitem): """ Called when user chooses menu item pointing to submenu """ filename = find_menu(menuitem.filename) if filename: self._submenu = self.__class__() sub_pos = list(self.position) for i in (0, 1): sub_pos[i] = (sub_pos[i] - self.SUBMENU_OFFSET if sub_pos[i] < 0 else sub_pos[i] + self.SUBMENU_OFFSET) self._submenu.use_config(self.config) self._submenu.parse_argumets(["menu.py", "-x", str(sub_pos[0]), "-y", str(sub_pos[1]), "--from-file", filename, "--control-with", self._control_with, "--confirm-with", self._confirm_with, "--cancel-with", self._cancel_with ]) self._submenu.set_is_submenu() self._submenu.use_daemon(self.daemon) self._submenu.use_controller(self.controller) self._submenu.controller = self.controller self._submenu.connect('destroy', self.on_submenu_closed) self._submenu.show() self.set_name("osd-menu-inactive") def _control_equals_cancel(self, daemon, x, y): """ Called by on_event in that very special case when both confirm_with and cancel_with are set to STICK. Separated because RadialMenu overrides on_event and still needs to call this. Returns True if menu was canceled. """ distance = sqrt(x*x + y*y) if distance < STICK_PAD_MAX / 8: self.quit(-1) return True return False def on_stick_direction(self, trash, x, y): if y != 0: self.next_item(y) def on_event(self, daemon, what, data): if self._submenu: return self._submenu.on_event(daemon, what, data) if what == self._control_with or what == "LEFT" and self._control_with_dpad: x, y = data if self._use_cursor: # Special case, both confirm_with and cancel_with # can be set to STICK if self._cancel_with == STICK and self._control_with == STICK: if self._control_equals_cancel(daemon, x, y): return pad_w = self.cursor.get_allocation().width * 0.5 pad_h = self.cursor.get_allocation().height * 0.5 max_w = self.get_allocation().width - 2 * pad_w max_h = self.get_allocation().height - 2 * pad_h x, y = circle_to_square(x / (STICK_PAD_MAX * 2.0), y / (STICK_PAD_MAX * 2.0)) x = clamp(pad_w, (pad_w + max_w) * 0.5 + x * max_w, max_w - pad_w) y = clamp(pad_h, (pad_h + max_h) * 0.5 + y * max_h * -1, max_h - pad_h) self.f.move(self.cursor, int(x), int(y)) for i in self.items: if point_in_gtkrect(i.widget.get_allocation(), x, y): self.select(self.items.index(i)) else: self._scon.set_stick(x, y) elif what == self._cancel_with: if data[0] == 0: # Button released self.quit(-1) elif what == self._confirm_with: if data[0] == 0: # Button released if self._selected and self._selected.callback: self._selected.callback(self, self.daemon, self.controller, self._selected) elif self._selected: self.quit(0) else: self.quit(-1)
class Dialog(OSDWindow): EPILOG = """Exit codes: 0 - clean exit, user selected option -1 - clean exit, user canceled dialog 1 - error, invalid arguments 2 - error, failed to access sc-daemon, sc-daemon reported error or died while dialog is displayed. 3 - erorr, failed to lock input stick, pad or button(s) """ def __init__(self, cls="osd-menu"): self._buttons = None self._text = None OSDWindow.__init__(self, cls) self.daemon = None self.config = None self.feedback = None self.controller = None self.xdisplay = X.Display(hash( GdkX11.x11_get_default_xdisplay())) # Magic self.parent = self.create_parent() self.f = Gtk.Fixed() self.f.add(self.parent) self.add(self.f) self._scon = StickController() self._scon.connect("direction", self.on_stick_direction) self._selected = None self._eh_ids = [] def create_parent(self): self._text = Gtk.Label() self._buttons = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) dialog = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) dialog.pack_start(self._text, True, True, 0) dialog.pack_start(self._buttons, True, True, 0) dialog.set_name("osd-dialog") self._buttons.set_name("osd-dialog-buttons") self._text.set_name("osd-dialog-text") return dialog def pack_items(self, parent, items): for item in items: if hasattr(item.widget, "set_alignment"): item.widget.set_alignment(0.5, 0.5) self._buttons.pack_end(item.widget, True, True, 0) def use_daemon(self, d): """ Allows (re)using already existing DaemonManager instance in same process. use_config() should be be called before parse_argumets() if this is used. """ self.daemon = d self._connect_handlers() self.on_daemon_connected(self.daemon) def use_config(self, c): """ Allows reusing already existin Config instance in same process. Has to be called before parse_argumets() """ self.config = c def get_menuid(self): # Just to be compatibile with menus when called from scc-osd-daemon return None def get_selected_item_id(self): """ Returns ID of selected item or None if nothing is selected. """ if self._selected: return self._selected.id return None def _add_arguments(self): OSDWindow._add_arguments(self) self.argparser.add_argument('--confirm-with', type=str, metavar="button", default=DEFAULT, help="button used to confirm choice") self.argparser.add_argument('--cancel-with', type=str, metavar="button", default=DEFAULT, help="button used to cancel dialog") self.argparser.add_argument( '--feedback-amplitude', type=int, help= "enables and sets power of feedback effect generated when active menu option is changed" ) self.argparser.add_argument('--text', type=str, metavar='text', help="Dialog text") self.argparser.add_argument('items', type=str, nargs='*', metavar='id text', help="Dialog buttons") def parse_argumets(self, argv): if not OSDWindow.parse_argumets(self, argv): return False if not self.config: self.config = Config() try: self.items = MenuData.from_args(self.args.items) self._menuid = None except ValueError: print >> sys.stderr, '%s: error: invalid number of arguments' % ( sys.argv[0]) return False self._text.set_label(self.args.text) if self.args.feedback_amplitude: side = "LEFT" self.feedback = side, int(self.args.feedback_amplitude) # Create buttons that are displayed on screen items = self.items.generate(self) self.items = [] for item in items: item.widget = self.generate_widget(item) if item.widget is not None: self.items.append(item) self.pack_items(self.parent, self.items) if len(self.items) == 0: print >> sys.stderr, '%s: error: no items in menu' % (sys.argv[0]) return False return True def generate_widget(self, item): """ Generates gtk widget for specified menutitem """ widget = Gtk.Button.new_with_label(item.label) widget.set_relief(Gtk.ReliefStyle.NONE) if hasattr(widget.get_children()[0], "set_xalign"): widget.get_children()[0].set_xalign(0) else: widget.get_children()[0].set_halign(Gtk.Align.START) widget.set_name("osd-menu-item") return widget def select(self, index): if self._selected: self._selected.widget.set_name( self._selected.widget.get_name().replace("-selected", "")) if self.items[index].id: if self._selected != self.items[index]: if self.feedback and self.controller: self.controller.feedback(*self.feedback) self._selected = self.items[index] self._selected.widget.set_name(self._selected.widget.get_name() + "-selected") return True return False def _connect_handlers(self): self._eh_ids += [ (self.daemon, self.daemon.connect('dead', self.on_daemon_died)), (self.daemon, self.daemon.connect('error', self.on_daemon_died)), (self.daemon, self.daemon.connect('alive', self.on_daemon_connected)), ] def run(self): self.daemon = DaemonManager() self._connect_handlers() OSDWindow.run(self) def show(self, *a): if not self.select(0): self.next_item(1) OSDWindow.show(self, *a) def on_daemon_died(self, *a): log.error("Daemon died") self.quit(2) def on_failed_to_lock(self, error): log.error("Failed to lock input: %s", error) self.quit(3) def on_daemon_connected(self, *a): def success(*a): log.error("Sucessfully locked input") if not self.config: self.config = Config() self.controller = self.choose_controller(self.daemon) if self.controller is None or not self.controller.is_connected(): # There is no controller connected to daemon self.on_failed_to_lock("Controller not connected") return ccfg = self.config.get_controller_config(self.controller.get_id()) self._control_with = ccfg["menu_control"] self._confirm_with = ccfg[ "menu_confirm"] if self.args.confirm_with == DEFAULT else self.args.confirm_with self._cancel_with = ccfg[ "menu_cancel"] if self.args.cancel_with == DEFAULT else self.args.cancel_with self._eh_ids += [(self.controller, self.controller.connect('event', self.on_event))] locks = [self._control_with, self._confirm_with, self._cancel_with] self.controller.lock(success, self.on_failed_to_lock, *locks) def quit(self, code=-2): if self.get_controller(): self.get_controller().unlock_all() for source, eid in self._eh_ids: source.disconnect(eid) self._eh_ids = [] OSDWindow.quit(self, code) def next_item(self, direction): """ Selects next menu item, based on self._direction """ start, i = -1, 0 try: start = self.items.index(self._selected) i = start + direction except: pass while True: if i == start: # Cannot find valid menu item self.select(start) break if i >= len(self.items): i = 0 continue if i < 0: i = len(self.items) - 1 continue if self.select(i): break i += direction if start < 0: start = 0 def on_stick_direction(self, trash, x, y): if x != 0: self.next_item(x) def on_event(self, daemon, what, data): if what == self._control_with: self._scon.set_stick(*data) elif what == self._cancel_with: if data[0] == 0: # Button released self.quit(-1) elif what == self._confirm_with: if data[0] == 0: # Button released if self._selected and self._selected.callback: self._selected.callback(self, self.daemon, self.controller, self._selected) elif self._selected: self.quit(0) else: self.quit(-1)
class Menu(OSDWindow): EPILOG="""Exit codes: 0 - clean exit, user selected option -1 - clean exit, user canceled menu -2 - clean exit, menu closed from callback method 1 - error, invalid arguments 2 - error, failed to access sc-daemon, sc-daemon reported error or died while menu is displayed. 3 - erorr, failed to lock input stick, pad or button(s) """ SUBMENU_OFFSET = 50 def __init__(self, cls="osd-menu"): OSDWindow.__init__(self, cls) self.daemon = None self.config = None self.xdisplay = X.Display(hash(GdkX11.x11_get_default_xdisplay())) # Magic cursor = os.path.join(get_share_path(), "images", 'menu-cursor.svg') self.cursor = Gtk.Image.new_from_file(cursor) self.cursor.set_name("osd-menu-cursor") self.parent = self.create_parent() self.f = Gtk.Fixed() self.f.add(self.parent) self.add(self.f) self._submenu = None self._scon = StickController() self._scon.connect("direction", self.on_stick_direction) self._is_submenu = False self._selected = None self._menuid = None self._use_cursor = False self._eh_ids = [] self._control_with = STICK self._confirm_with = 'A' self._cancel_with = 'B' def set_is_submenu(self): """ Marks menu as submenu. This changes behaviour of some methods, especially disables (un)locking of input stick and buttons. """ self._is_submenu = True def create_parent(self): v = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) v.set_name("osd-menu") return v def pack_items(self, parent, items): for item in items: parent.pack_start(item.widget, True, True, 0) def use_daemon(self, d): """ Allows (re)using already existing DaemonManager instance in same process. use_config() should be be called before parse_argumets() if this is used. """ self.daemon = d if not self._is_submenu: self._connect_handlers() self.on_daemon_connected(self.daemon) def use_config(self, c): """ Allows reusing already existin Config instance in same process. Has to be called before parse_argumets() """ self.config = c def get_menuid(self): """ Returns ID of used menu. """ return self._menuid def get_selected_item_id(self): """ Returns ID of selected item or None if nothing is selected. """ if self._selected: return self._selected.id return None def _add_arguments(self): OSDWindow._add_arguments(self) self.argparser.add_argument('--control-with', '-c', type=str, metavar="option", default=STICK, choices=(LEFT, RIGHT, STICK), help="which pad or stick should be used to navigate menu (default: %s)" % (STICK,)) self.argparser.add_argument('--confirm-with', type=str, metavar="button", default='A', help="button used to confirm choice (default: A)") self.argparser.add_argument('--cancel-with', type=str, metavar="button", default='B', help="button used to cancel menu (default: B)") self.argparser.add_argument('--confirm-with-release', action='store_true', help="confirm choice with button release instead of button press") self.argparser.add_argument('--cancel-with-release', action='store_true', help="cancel menu with button release instead of button press") self.argparser.add_argument('--use-cursor', '-u', action='store_true', help="display and use cursor") self.argparser.add_argument('--from-profile', '-p', type=str, metavar="profile_file menu_name", help="load menu items from profile file") self.argparser.add_argument('--from-file', '-f', type=str, metavar="filename", help="load menu items from json file") self.argparser.add_argument('--print-items', action='store_true', help="prints menu items to stdout") self.argparser.add_argument('items', type=str, nargs='*', metavar='id title', help="Menu items") def parse_argumets(self, argv): if not OSDWindow.parse_argumets(self, argv): return False if not self.config: self.config = Config() if self.args.from_profile: try: self._menuid = self.args.items[0] self.items = MenuData.from_profile(self.args.from_profile, self._menuid) except IOError: print >>sys.stderr, '%s: error: profile file not found' % (sys.argv[0]) return False except ValueError: print >>sys.stderr, '%s: error: menu not found' % (sys.argv[0]) return False elif self.args.from_file: #try: data = json.loads(open(self.args.from_file, "r").read()) self._menuid = self.args.from_file self.items = MenuData.from_json_data(data) #except: # print >>sys.stderr, '%s: error: failed to load menu file' % (sys.argv[0]) # return False else: try: self.items = MenuData.from_args(self.args.items) self._menuid = None except ValueError: print >>sys.stderr, '%s: error: invalid number of arguments' % (sys.argv[0]) return False # Parse simpler arguments self._control_with = self.args.control_with self._confirm_with = self.args.confirm_with self._cancel_with = self.args.cancel_with print "_control_with", self._control_with if self.args.use_cursor: self.enable_cursor() # Create buttons that are displayed on screen self.items = self.items.generate(self) for item in self.items: item.widget = self.generate_widget(item) self.pack_items(self.parent, self.items) if len(self.items) == 0: print >>sys.stderr, '%s: error: no items in menu' % (sys.argv[0]) return False if self.args.print_items: max_id_len = max(*[ len(x.id) for x in self.items ]) row_format ="{:>%s}:\t{}" % (max_id_len,) for item in self.items: print row_format.format(item.id, item.label) return True def enable_cursor(self): if not self._use_cursor: self.f.add(self.cursor) self.f.show_all() self._use_cursor = True def generate_widget(self, item): """ Generates gtk widget for specified menutitem """ if isinstance(item, Separator) and item.label: widget = Gtk.Button.new_with_label(item.label) widget.set_relief(Gtk.ReliefStyle.NONE) widget.set_name("osd-menu-separator") return widget elif isinstance(item, Separator): widget = Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL) widget.set_name("osd-menu-separator") return widget else: widget = Gtk.Button.new_with_label(item.label) widget.set_relief(Gtk.ReliefStyle.NONE) if hasattr(widget.get_children()[0], "set_xalign"): widget.get_children()[0].set_xalign(0) else: widget.get_children()[0].set_halign(Gtk.Align.START) if isinstance(item, Submenu): item.callback = self.show_submenu label1 = widget.get_children()[0] label2 = Gtk.Label(_(">>")) label2.set_property("margin-left", 30) box = Gtk.Box(Gtk.Orientation.HORIZONTAL) widget.remove(label1) box.pack_start(label1, True, True, 1) box.pack_start(label2, False, True, 1) widget.add(box) widget.set_name("osd-menu-item") elif item.id is None: widget.set_name("osd-menu-dummy") else: widget.set_name("osd-menu-item") return widget def select(self, index): if self._selected: self._selected.widget.set_name("osd-menu-item") if self.items[index].id: self._selected = self.items[index] self._selected.widget.set_name("osd-menu-item-selected") return True return False def _connect_handlers(self): self._eh_ids += [ (self.daemon, self.daemon.connect('dead', self.on_daemon_died)), (self.daemon, self.daemon.connect('error', self.on_daemon_died)), (self.daemon, self.daemon.connect('alive', self.on_daemon_connected)), ] def run(self): self.daemon = DaemonManager() self._connect_handlers() OSDWindow.run(self) def show(self, *a): if not self.select(0): self.next_item(1) OSDWindow.show(self, *a) def on_daemon_died(self, *a): log.error("Daemon died") self.quit(2) def on_failed_to_lock(self, error): log.error("Failed to lock input: %s", error) self.quit(3) def on_daemon_connected(self, *a): def success(*a): log.error("Sucessfully locked input") if not self.config: self.config = Config() locks = [ self._control_with, self._confirm_with, self._cancel_with ] c = self.choose_controller(self.daemon) if c is None or not c.is_connected(): # There is no controller connected to daemon self.on_failed_to_lock("Controller not connected") self._eh_ids += [ (c, c.connect('event', self.on_event)) ] c.lock(success, self.on_failed_to_lock, *locks) def quit(self, code=-2): if not self._is_submenu: if self.get_controller(): self.get_controller().unlock_all() for source, eid in self._eh_ids: source.disconnect(eid) self._eh_ids = [] OSDWindow.quit(self, code) def next_item(self, direction): """ Selects next menu item, based on self._direction """ start, i = -1, 0 try: start = self.items.index(self._selected) i = start + direction except: pass while True: if i == start: # Cannot find valid menu item self.select(start) break if i >= len(self.items): i = 0 continue if i < 0: i = len(self.items) - 1 continue if self.select(i): # Not a separator break i += direction if start < 0: start = 0 def on_submenu_closed(self, *a): if self._submenu.get_exit_code() in (0, -2): self._menuid = self._submenu._menuid self._selected = self._submenu._selected self.quit(self._submenu.get_exit_code()) self._submenu = None def show_submenu(self, trash, trash2, menuitem): """ Called when user chooses menu item pointing to submenu """ filename = find_menu(menuitem.filename) if filename: self._submenu = self.__class__() sub_pos = list(self.position) for i in (0, 1): sub_pos[i] = (sub_pos[i] - self.SUBMENU_OFFSET if sub_pos[i] < 0 else sub_pos[i] + self.SUBMENU_OFFSET) self._submenu.use_config(self.config) self._submenu.parse_argumets(["menu.py", "-x", str(sub_pos[0]), "-y", str(sub_pos[1]), "--from-file", filename, "--control-with", self._control_with, "--confirm-with", self._confirm_with, "--cancel-with", self._cancel_with ]) self._submenu.set_is_submenu() self._submenu.use_daemon(self.daemon) self._submenu.connect('destroy', self.on_submenu_closed) self._submenu.show() def _control_equals_cancel(self, daemon, x, y): """ Called by on_event in that very special case when both confirm_with and cancel_with are set to STICK. Separated because RadialMenu overrides on_event and still needs to call this. Returns True if menu was canceled. """ distance = sqrt(x*x + y*y) if distance < STICK_PAD_MAX / 8: self.quit(-1) return True return False def on_stick_direction(self, trash, x, y): if y != 0: self.next_item(y) def on_event(self, daemon, what, data): if self._submenu: return self._submenu.on_event(daemon, what, data) if what == self._control_with: x, y = data if self._use_cursor: # Special case, both confirm_with and cancel_with # can be set to STICK if self._cancel_with == STICK and self._control_with == STICK: if self._control_equals_cancel(daemon, x, y): return max_w = self.get_allocation().width - (self.cursor.get_allocation().width * 0.8) max_h = self.get_allocation().height - (self.cursor.get_allocation().height * 1.0) x = ((x / (STICK_PAD_MAX * 2.0)) + 0.5) * max_w y = (0.5 - (y / (STICK_PAD_MAX * 2.0))) * max_h x -= self.cursor.get_allocation().width * 0.5 y -= self.cursor.get_allocation().height * 0.5 self.f.move(self.cursor, int(x), int(y)) for i in self.items: if point_in_gtkrect(i.widget.get_allocation(), x, y): self.select(self.items.index(i)) else: self._scon.set_stick(x, y) elif what == self._cancel_with: if data[0] == 0: # Button released self.quit(-1) elif what == self._confirm_with: if data[0] == 0: # Button released if self._selected and self._selected.callback: self._selected.callback(self, self.daemon, self._selected) elif self._selected: self.quit(0) else: self.quit(-1)
class Launcher(OSDWindow): EPILOG="""Exit codes: 0 - clean exit, user selected option -1 - clean exit, user canceled dialog 1 - error, invalid arguments 2 - error, failed to access sc-daemon, sc-daemon reported error or died while dialog is displayed. 3 - erorr, failed to lock input stick, pad or button(s) """ BUTTONS = [ "1", "2 ABC", "3 DEF", "5 GHI", "5 JKL", "6 MNO", "7 PQRS", "8 TUV", "9 WXYZ", "", "0" ] VALID_CHARS = "12ABC3DEF5GHI5JKL6MNO7PQRS8TUV9WXYZ0" CHAR_TO_NUMBER = { } # Generated on runtime MAX_ROWS = 5 _app_db = None # Static list of all know applications def __init__(self, cls="osd-menu"): self._buttons = None self._string = "" OSDWindow.__init__(self, cls) self.daemon = None self.config = None self.feedback = None self.controller = None self.xdisplay = X.Display(hash(GdkX11.x11_get_default_xdisplay())) # Magic self.create_parent() self.create_app_list() self.create_buttons() cursor = os.path.join(get_share_path(), "images", 'menu-cursor.svg') self.cursors = [ Gtk.Image.new_from_file(cursor), Gtk.Image.new_from_file(cursor) ] for c in self.cursors: c.set_name("osd-menu-cursor") c.selected = None self.f.add(c) self.f.show_all() self._scon = StickController() self._scon.connect("direction", self.on_stick_direction) self._selected = None self._menuid = None self._eh_ids = [] self._confirm_with = 'A' self._cancel_with = 'B' if Launcher._app_db is None: Launcher._app_db = [] for x in Launcher.BUTTONS: for c in x: Launcher.CHAR_TO_NUMBER[c] = x[0] for x in Gio.AppInfo.get_all(): try: Launcher._app_db.append(( Launcher.name_to_keys(x), x )) except UnicodeDecodeError: # Just f**k them... pass @staticmethod def name_to_keys(appinfo): name = "".join([ Launcher.CHAR_TO_NUMBER[x] for x in appinfo.get_display_name().upper() if x in Launcher.VALID_CHARS ]) return name @staticmethod def string_to_keys_and_spaces(string): name = "".join([ Launcher.CHAR_TO_NUMBER[x] if x in Launcher.VALID_CHARS else " " for x in string.upper() ]) return name def create_parent(self): self.parent = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) self.parent.set_name("osd-dialog") self.f = Gtk.Fixed() self.f.add(self.parent) self.add(self.f) def create_app_list(self): lst = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) lst.set_name("osd-application-list") self.items = [ self.generate_widget("") for x in xrange(self.MAX_ROWS) ] for a in self.items: lst.pack_start(a, False, True, 0) self.parent.pack_start(lst, True, True, 0) self._set_launchers([ ]) lst.show_all() def create_buttons(self): self.grid = Gtk.Grid() self.parent.pack_start(self.grid, True, True, 0) self._buttons = [] x, y = 0, 0 for label in self.BUTTONS: if label: w = self.generate_widget(label) w.set_name("osd-key-buton") self.grid.attach(w, x, y, 1, 1) self._buttons.append(w) x += 1 if x > 2: x = 0 y += 1 w = self.generate_widget(_("Run")) self.grid.attach(w, x, y, 1, 1) self.grid.set_name("osd-dialog-buttons") def pack_items(self, parent, items): for item in items: if hasattr(item.widget, "set_alignment"): item.widget.set_alignment(0.5, 0.5) self._buttons.pack_end(item.widget, True, True, 0) def use_daemon(self, d): """ Allows (re)using already existing DaemonManager instance in same process. use_config() should be be called before parse_argumets() if this is used. """ self.daemon = d self._connect_handlers() self.on_daemon_connected(self.daemon) def use_config(self, c): """ Allows reusing already existin Config instance in same process. Has to be called before parse_argumets() """ self.config = c def get_menuid(self): """ Returns ID of used menu. """ return None def get_selected_item_id(self): """ Returns ID of selected item or None if nothing is selected. """ return None def _launch(self): self._selected.launcher.launch() def _add_arguments(self): OSDWindow._add_arguments(self) self.argparser.add_argument('--confirm-with', type=str, metavar="button", default='A', help="button used to confirm choice (default: A)") self.argparser.add_argument('--cancel-with', type=str, metavar="button", default='B', help="button used to cancel menu (default: B)") self.argparser.add_argument('--feedback-amplitude', type=int, help="enables and sets power of feedback effect generated when active menu option is changed") def parse_argumets(self, argv): if not OSDWindow.parse_argumets(self, argv): return False if not self.config: self.config = Config() # Parse simpler arguments self._confirm_with = self.args.confirm_with self._cancel_with = self.args.cancel_with if self.args.feedback_amplitude: side = "LEFT" self.feedback = side, int(self.args.feedback_amplitude) # Create buttons that are displayed on screen return True def _set_launchers(self, launchers): launchers = launchers[0:self.MAX_ROWS] for x in self.items: x.set_label("") x.set_name("osd-hidden-item") x.launcher = None for i in xrange(0, len(launchers)): self.items[i].set_name("osd-launcher-item") self.items[i].launcher = launchers[i] label = self.items[i].get_children()[0] label.set_markup(self._format_label_markup(launchers[i])) label.set_max_width_chars(1) label.set_ellipsize(Pango.EllipsizeMode.MIDDLE) label.set_xalign(0) def _format_label_markup(self, label): if hasattr(label, "get_display_name"): label = label.get_display_name() else: label = str(label) def _check(substr): i, ch = 0, self._string while len(substr) > 0 and substr[0] == ch[0]: ch = ch[1:] substr = substr[1:] i += 1 if len(ch) == 0: return i while len(substr) > 0 and substr[0] == " ": substr = substr[1:] i += 1 return -1 keys = Launcher.string_to_keys_and_spaces(label) index1, index2 = -1, -1 for i in xrange(0, len(keys)): if keys[i] == self._string[0]: index2 = _check(keys[i:]) if index2 > 0: index1 = i index2 = i + index2 break label = "%s<span color='#%s'>%s</span>%s" % ( label[0:index1], self.config["osd_colors"]["menuitem_hilight_text"], label[index1:index2], label[index2:] ) return label def _update_items(self): if len(self._string) > 0: gen = ( item for (keys, item) in self._app_db if self._string in keys ) launchers = [] for i in gen: launchers.append(i) if len(launchers) > self.MAX_ROWS: break self._set_launchers(launchers) self.select(0) else: self._set_launchers([]) def generate_widget(self, label): """ Generates gtk widget for specified menutitem """ if hasattr(label, "label"): label = label.label widget = Gtk.Button.new_with_label(label) widget.set_relief(Gtk.ReliefStyle.NONE) if hasattr(widget.get_children()[0], "set_xalign"): widget.get_children()[0].set_xalign(0) else: widget.get_children()[0].set_halign(Gtk.Align.START) widget.set_name("osd-menu-item") return widget def select(self, index): if self._selected: self._selected.set_name(self._selected.get_name() .replace("-selected", "")) self._selected = None if self.items[index].launcher is not None: self._selected = self.items[index] self._selected.set_name( self._selected.get_name() + "-selected") return True return False def _connect_handlers(self): self._eh_ids += [ (self.daemon, self.daemon.connect('dead', self.on_daemon_died)), (self.daemon, self.daemon.connect('error', self.on_daemon_died)), (self.daemon, self.daemon.connect('alive', self.on_daemon_connected)), ] def run(self): self.daemon = DaemonManager() self._connect_handlers() OSDWindow.run(self) def show(self, *a): if not self.select(0): self.next_item(1) OSDWindow.show(self, *a) for c in self.cursors: c.set_visible(False) def on_daemon_died(self, *a): log.error("Daemon died") self.quit(2) def on_failed_to_lock(self, error): log.error("Failed to lock input: %s", error) self.quit(3) def on_daemon_connected(self, *a): def success(*a): log.error("Sucessfully locked input") if not self.config: self.config = Config() locks = [ LEFT, RIGHT, STICK, "LPAD", "RPAD", "LB", self._confirm_with, self._cancel_with ] self.controller = self.choose_controller(self.daemon) if self.controller is None or not self.controller.is_connected(): # There is no controller connected to daemon self.on_failed_to_lock("Controller not connected") return self._eh_ids += [ (self.controller, self.controller.connect('event', self.on_event)) ] self.controller.lock(success, self.on_failed_to_lock, *locks) def quit(self, code=-2): if self.get_controller(): self.get_controller().unlock_all() for source, eid in self._eh_ids: source.disconnect(eid) self._eh_ids = [] OSDWindow.quit(self, code) def next_item(self, direction): """ Selects next menu item, based on self._direction """ start, i = -1, 0 try: start = self.items.index(self._selected) i = start + direction except: pass while True: if i == start: # Cannot find valid menu item self.select(start) break if i >= len(self.items): i = 0 continue if i < 0: i = len(self.items) - 1 continue if self.select(i): break i += direction if start < 0: start = 0 def on_stick_direction(self, trash, x, y): if y != 0: self.next_item(y) def _move_cursor(self, cursor, x, y): if (x, y) == (0, 0): cursor.set_visible(False) return cursor.set_visible(True) pad_w = cursor.get_allocation().width * 0.5 pad_h = cursor.get_allocation().height * 0.5 max_w = self.grid.get_allocation().width - 2 * pad_w max_h = self.grid.get_allocation().height - 2 * pad_h x, y = circle_to_square(x / (STICK_PAD_MAX * 2.0), y / (STICK_PAD_MAX * 2.0)) x = clamp(pad_w, (pad_w + max_w) * 0.5 + x * max_w, max_w - pad_w) y = clamp(pad_h, (pad_h + max_h) * 0.5 + y * max_h * -1, max_h - pad_h) x += self.grid.get_allocation().x y += self.grid.get_allocation().y self.f.move(cursor, int(x), int(y)) for i in self._buttons: if point_in_gtkrect(i.get_allocation(), x, y): if cursor.selected: cursor.selected.set_name("osd-key-buton") cursor.selected = i cursor.selected.set_name("osd-key-buton-hilight") break def _get_under_cursor(self, cursor): x, y = self.f.child_get(cursor, "x", "y") for i in self._buttons: if point_in_gtkrect(i.get_allocation(), x, y): return i return None def on_event(self, daemon, what, data): if what == LEFT: self._move_cursor(self.cursors[0], *data) elif what == "LPAD" and data[0] == 1: b = self._get_under_cursor(self.cursors[0]) if b: self._string += b.get_label()[0] self._update_items() elif what == RIGHT: self._move_cursor(self.cursors[1], *data) elif what == "RPAD" and data[0] == 1: b = self._get_under_cursor(self.cursors[1]) if b: self._string += b.get_label()[0] self._update_items() elif what == "LB": if len(self._string) > 0: self._string = self._string[:-1] self._update_items() elif what == STICK: self._scon.set_stick(*data) elif what == self._cancel_with: if data[0] == 0: # Button released self.quit(-1) elif what == self._confirm_with: if data[0] == 0: # Button released if self._selected: self._launch() self.quit(0) else: self.quit(-1)
class Launcher(OSDWindow): EPILOG="""Exit codes: 0 - clean exit, user selected option -1 - clean exit, user canceled dialog 1 - error, invalid arguments 2 - error, failed to access sc-daemon, sc-daemon reported error or died while dialog is displayed. 3 - erorr, failed to lock input stick, pad or button(s) """ BUTTONS = [ "1", "2 ABC", "3 DEF", "5 GHI", "5 JKL", "6 MNO", "7 PQRS", "8 TUV", "9 WXYZ", "", "0" ] VALID_CHARS = "12ABC3DEF5GHI5JKL6MNO7PQRS8TUV9WXYZ0" CHAR_TO_NUMBER = { } # Generated on runtime MAX_ROWS = 5 _app_db = None # Static list of all know applications def __init__(self, cls="osd-menu"): self._buttons = None self._string = "" OSDWindow.__init__(self, cls) self.daemon = None self.config = None self.feedback = None self.controller = None self.xdisplay = X.Display(hash(GdkX11.x11_get_default_xdisplay())) # Magic self.create_parent() self.create_app_list() self.create_buttons() cursor = os.path.join(get_share_path(), "images", 'menu-cursor.svg') self.cursors = [ Gtk.Image.new_from_file(cursor), Gtk.Image.new_from_file(cursor) ] for c in self.cursors: c.set_name("osd-menu-cursor") c.selected = None self.f.add(c) self.f.show_all() self._scon = StickController() self._scon.connect("direction", self.on_stick_direction) self._selected = None self._menuid = None self._eh_ids = [] self._confirm_with = 'A' self._cancel_with = 'B' if Launcher._app_db is None: Launcher._app_db = [] for x in Launcher.BUTTONS: for c in x: Launcher.CHAR_TO_NUMBER[c] = x[0] for x in Gio.AppInfo.get_all(): try: Launcher._app_db.append(( Launcher.name_to_keys(x), x )) except UnicodeDecodeError: # Just f**k them... pass @staticmethod def name_to_keys(appinfo): name = "".join([ Launcher.CHAR_TO_NUMBER[x] for x in appinfo.get_display_name().upper() if x in Launcher.VALID_CHARS ]) return name @staticmethod def string_to_keys_and_spaces(string): name = "".join([ Launcher.CHAR_TO_NUMBER[x] if x in Launcher.VALID_CHARS else " " for x in string.upper() ]) return name def create_parent(self): self.parent = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) self.parent.set_name("osd-dialog") self.f = Gtk.Fixed() self.f.add(self.parent) self.add(self.f) def create_app_list(self): lst = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) lst.set_name("osd-application-list") self.items = [ self.generate_widget("") for x in xrange(self.MAX_ROWS) ] for a in self.items: lst.pack_start(a, False, True, 0) self.parent.pack_start(lst, True, True, 0) self._set_launchers([ ]) lst.show_all() def create_buttons(self): self.grid = Gtk.Grid() self.parent.pack_start(self.grid, True, True, 0) self._buttons = [] x, y = 0, 0 for label in self.BUTTONS: if label: w = self.generate_widget(label) w.set_name("osd-key-buton") self.grid.attach(w, x, y, 1, 1) self._buttons.append(w) x += 1 if x > 2: x = 0 y += 1 w = self.generate_widget(_("Run")) self.grid.attach(w, x, y, 1, 1) self.grid.set_name("osd-dialog-buttons") def pack_items(self, parent, items): for item in items: if hasattr(item.widget, "set_alignment"): item.widget.set_alignment(0.5, 0.5) self._buttons.pack_end(item.widget, True, True, 0) def use_daemon(self, d): """ Allows (re)using already existing DaemonManager instance in same process. use_config() should be be called before parse_argumets() if this is used. """ self.daemon = d self._connect_handlers() self.on_daemon_connected(self.daemon) def use_config(self, c): """ Allows reusing already existin Config instance in same process. Has to be called before parse_argumets() """ self.config = c def get_menuid(self): """ Returns ID of used menu. """ return None def get_selected_item_id(self): """ Returns ID of selected item or None if nothing is selected. """ return None def _launch(self): self._selected.launcher.launch() def _add_arguments(self): OSDWindow._add_arguments(self) self.argparser.add_argument('--confirm-with', type=str, metavar="button", default=DEFAULT, help="button used to confirm choice") self.argparser.add_argument('--cancel-with', type=str, metavar="button", default=DEFAULT, help="button used to cancel dialog") self.argparser.add_argument('--feedback-amplitude', type=int, help="enables and sets power of feedback effect generated when active menu option is changed") def parse_argumets(self, argv): if not OSDWindow.parse_argumets(self, argv): return False if not self.config: self.config = Config() if self.args.feedback_amplitude: side = "LEFT" self.feedback = side, int(self.args.feedback_amplitude) # Create buttons that are displayed on screen return True def _set_launchers(self, launchers): launchers = launchers[0:self.MAX_ROWS] for x in self.items: x.set_label("") x.set_name("osd-hidden-item") x.launcher = None for i in xrange(0, len(launchers)): self.items[i].set_name("osd-launcher-item") self.items[i].launcher = launchers[i] label = self.items[i].get_children()[0] label.set_markup(self._format_label_markup(launchers[i])) label.set_max_width_chars(1) label.set_ellipsize(Pango.EllipsizeMode.MIDDLE) label.set_xalign(0) def _format_label_markup(self, label): if hasattr(label, "get_display_name"): label = label.get_display_name() else: label = str(label) def _check(substr): i, ch = 0, self._string while len(substr) > 0 and substr[0] == ch[0]: ch = ch[1:] substr = substr[1:] i += 1 if len(ch) == 0: return i while len(substr) > 0 and substr[0] == " ": substr = substr[1:] i += 1 return -1 keys = Launcher.string_to_keys_and_spaces(label) index1, index2 = -1, -1 for i in xrange(0, len(keys)): if keys[i] == self._string[0]: index2 = _check(keys[i:]) if index2 > 0: index1 = i index2 = i + index2 break label = "%s<span color='#%s'>%s</span>%s" % ( label[0:index1], self.config["osd_colors"]["menuitem_hilight_text"], label[index1:index2], label[index2:] ) return label def _update_items(self): if len(self._string) > 0: gen = ( item for (keys, item) in self._app_db if self._string in keys ) launchers = [] for i in gen: launchers.append(i) if len(launchers) > self.MAX_ROWS: break self._set_launchers(launchers) self.select(0) else: self._set_launchers([]) def generate_widget(self, label): """ Generates gtk widget for specified menutitem """ if hasattr(label, "label"): label = label.label widget = Gtk.Button.new_with_label(label) widget.set_relief(Gtk.ReliefStyle.NONE) if hasattr(widget.get_children()[0], "set_xalign"): widget.get_children()[0].set_xalign(0) else: widget.get_children()[0].set_halign(Gtk.Align.START) widget.set_name("osd-menu-item") return widget def select(self, index): if self._selected: self._selected.set_name(self._selected.get_name() .replace("-selected", "")) self._selected = None if self.items[index].launcher is not None: self._selected = self.items[index] self._selected.set_name( self._selected.get_name() + "-selected") return True return False def _connect_handlers(self): self._eh_ids += [ (self.daemon, self.daemon.connect('dead', self.on_daemon_died)), (self.daemon, self.daemon.connect('error', self.on_daemon_died)), (self.daemon, self.daemon.connect('alive', self.on_daemon_connected)), ] def run(self): self.daemon = DaemonManager() self._connect_handlers() OSDWindow.run(self) def show(self, *a): if not self.select(0): self.next_item(1) OSDWindow.show(self, *a) for c in self.cursors: c.set_visible(False) def on_daemon_connected(self, *a): def success(*a): log.error("Sucessfully locked input") if not self.config: self.config = Config() self.controller = self.choose_controller(self.daemon) if self.controller is None or not self.controller.is_connected(): # There is no controller connected to daemon self.on_failed_to_lock("Controller not connected") return ccfg = self.config.get_controller_config(self.controller.get_id()) self._confirm_with = ccfg["menu_confirm"] if self.args.confirm_with == DEFAULT else self.args.confirm_with self._cancel_with = ccfg["menu_cancel"] if self.args.cancel_with == DEFAULT else self.args.cancel_with self._eh_ids += [ (self.controller, self.controller.connect('event', self.on_event)), (self.controller, self.controller.connect('lost', self.on_controller_lost)), ] locks = [ LEFT, RIGHT, STICK, "LPAD", "RPAD", "LB", self._confirm_with, self._cancel_with ] self.controller.lock(success, self.on_failed_to_lock, *locks) def quit(self, code=-2): if self.get_controller(): self.get_controller().unlock_all() for source, eid in self._eh_ids: source.disconnect(eid) self._eh_ids = [] OSDWindow.quit(self, code) def next_item(self, direction): """ Selects next menu item, based on self._direction """ start, i = -1, 0 try: start = self.items.index(self._selected) i = start + direction except: pass while True: if i == start: # Cannot find valid menu item self.select(start) break if i >= len(self.items): i = 0 continue if i < 0: i = len(self.items) - 1 continue if self.select(i): break i += direction if start < 0: start = 0 def on_stick_direction(self, trash, x, y): if y != 0: self.next_item(y) def _move_cursor(self, cursor, x, y): if (x, y) == (0, 0): cursor.set_visible(False) return cursor.set_visible(True) pad_w = cursor.get_allocation().width * 0.5 pad_h = cursor.get_allocation().height * 0.5 max_w = self.grid.get_allocation().width - 2 * pad_w max_h = self.grid.get_allocation().height - 2 * pad_h x, y = circle_to_square(x / (STICK_PAD_MAX * 2.0), y / (STICK_PAD_MAX * 2.0)) x = clamp(pad_w, (pad_w + max_w) * 0.5 + x * max_w, max_w - pad_w) y = clamp(pad_h, (pad_h + max_h) * 0.5 + y * max_h * -1, max_h - pad_h) x += self.grid.get_allocation().x y += self.grid.get_allocation().y self.f.move(cursor, int(x), int(y)) for i in self._buttons: if point_in_gtkrect(i.get_allocation(), x, y): if cursor.selected: cursor.selected.set_name("osd-key-buton") cursor.selected = i cursor.selected.set_name("osd-key-buton-hilight") break def _get_under_cursor(self, cursor): x, y = self.f.child_get(cursor, "x", "y") for i in self._buttons: if point_in_gtkrect(i.get_allocation(), x, y): return i return None def on_event(self, daemon, what, data): if what == LEFT: self._move_cursor(self.cursors[0], *data) elif what == "LPAD" and data[0] == 1: b = self._get_under_cursor(self.cursors[0]) if b: self._string += b.get_label()[0] self._update_items() elif what == RIGHT: self._move_cursor(self.cursors[1], *data) elif what == "RPAD" and data[0] == 1: b = self._get_under_cursor(self.cursors[1]) if b: self._string += b.get_label()[0] self._update_items() elif what == "LB": if len(self._string) > 0: self._string = self._string[:-1] self._update_items() elif what == STICK: self._scon.set_stick(*data) elif what == self._cancel_with: if data[0] == 0: # Button released self.quit(-1) elif what == self._confirm_with: if data[0] == 0: # Button released if self._selected: self._launch() self.quit(0) else: self.quit(-1)