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 Menu(OSDWindow, TimerManager): EPILOG="""Exit codes: 0 - clean exit, user selected option -1 - clean exit, user canceled menu 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) """ REPEAT_DELAY = 0.5 def __init__(self): OSDWindow.__init__(self, "osd-menu") TimerManager.__init__(self) self.daemon = None 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._direction = 0 # Movement direction 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 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 existin DaemonManager instance in same process """ self.daemon = d self._cononect_handlers() self.on_daemon_connected(self.daemon) 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('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 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 loade 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.f.add(self.cursor) self.f.show_all() self._use_cursor = True # Create buttons that are displayed on screen for item in self.items: item.widget = Gtk.Button.new_with_label(item.label) item.widget.set_name("osd-menu-item") item.widget.set_relief(Gtk.ReliefStyle.NONE) 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 select(self, index): if self._selected: self._selected.widget.set_name("osd-menu-item") self._selected = self.items[index] self._selected.widget.set_name("osd-menu-item-selected") 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): self.select(0) 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 locks = [ self._control_with, self._confirm_with, self._cancel_with ] self.daemon.lock(success, self.on_failed_to_lock, *locks) def quit(self, code=-1): self.daemon.unlock_all() for x in self._eh_ids: self.daemon.disconnect(x) self._eh_ids = [] OSDWindow.quit(self, code) def on_move(self): i = self.items.index(self._selected) + self._direction if i >= len(self.items): i = 0 elif i < 0: i = len(self.items) - 1 self.select(i) self.timer("move", self.REPEAT_DELAY, self.on_move) def on_event(self, daemon, what, data): if what == self._control_with: x, y = data if self._use_cursor: 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: if y < STICK_PAD_MIN / 3 and self._direction != 1: self._direction = 1 self.on_move() if y > STICK_PAD_MAX / 3 and self._direction != -1: self._direction = -1 self.on_move() if y < STICK_PAD_MAX / 3 and y > STICK_PAD_MIN / 3 and self._direction != 0: self._direction = 0 self.cancel_timer("move") 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 self.quit(0)
class Menu(OSDWindow, TimerManager): 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) """ REPEAT_DELAY = 0.5 SUBMENU_OFFSET = 50 def __init__(self, cls="osd-menu"): OSDWindow.__init__(self, cls) TimerManager.__init__(self) 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._is_submenu = False self._direction = 0 # Movement direction 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._direction = 1 self.next_item() self._direction = 0 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): """ Selects next menu item, based on self._direction """ start, i = -1, 0 try: start = self.items.index(self._selected) i = start + self._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 += self._direction if start < 0: start = 0 def on_move(self): self.next_item() self.timer("move", self.REPEAT_DELAY, self.on_move) 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 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: 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: if y < STICK_PAD_MIN / 3 and self._direction != 1: self._direction = 1 self.on_move() if y > STICK_PAD_MAX / 3 and self._direction != -1: self._direction = -1 self.on_move() if y < STICK_PAD_MAX / 3 and y > STICK_PAD_MIN / 3 and self._direction != 0: self._direction = 0 self.cancel_timer("move") 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 Keyboard(OSDWindow, TimerManager): EPILOG="""Exit codes: 0 - clean exit, user closed keyboard 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) """ HILIGHT_COLOR = "#00688D" BUTTON_MAP = { SCButtons.A.name : Keys.KEY_ENTER, SCButtons.B.name : Keys.KEY_ESC, SCButtons.LB.name : Keys.KEY_BACKSPACE, SCButtons.RB.name : Keys.KEY_SPACE, SCButtons.LGRIP.name : Keys.KEY_LEFTSHIFT, SCButtons.RGRIP.name : Keys.KEY_RIGHTALT, } def __init__(self): OSDWindow.__init__(self, "osd-keyboard") TimerManager.__init__(self) self.daemon = None self.keyboard = None self.keymap = Gdk.Keymap.get_default() self.keymap.connect('state-changed', self.on_state_changed) kbimage = os.path.join(get_config_path(), 'keyboard.svg') if not os.path.exists(kbimage): # Prefer image in ~/.config/scc, but load default one as fallback kbimage = os.path.join(get_share_path(), "images", 'keyboard.svg') self.background = SVGWidget(self, kbimage) self.limit_left = self.background.get_rect_area(self.background.get_element("LIMIT_LEFT")) self.limit_right = self.background.get_rect_area(self.background.get_element("LIMIT_RIGHT")) cursor = os.path.join(get_share_path(), "images", 'menu-cursor.svg') self.cursor_left = Gtk.Image.new_from_file(cursor) self.cursor_left.set_name("osd-keyboard-cursor") self.cursor_right = Gtk.Image.new_from_file(cursor) self.cursor_right.set_name("osd-keyboard-cursor") self._eh_ids = [] self._stick = 0, 0 self._hovers = { self.cursor_left : None, self.cursor_right : None } self._pressed = { self.cursor_left : None, self.cursor_right : None } self.c = Gtk.Box() self.c.set_name("osd-keyboard-container") self.f = Gtk.Fixed() self.f.add(self.background) self.f.add(self.cursor_left) self.f.add(self.cursor_right) self.c.add(self.f) self.add(self.c) self.set_cursor_position(0, 0, self.cursor_left, self.limit_left) self.set_cursor_position(0, 0, self.cursor_right, self.limit_right) self.timer('labels', 0.1, self.update_labels) def use_daemon(self, d): """ Allows (re)using already existing DaemonManager instance in same process """ self.daemon = d self._cononect_handlers() self.on_daemon_connected(self.daemon) def on_state_changed(self, x11keymap): if not self.timer_active('labels'): self.timer('labels', 0.1, self.update_labels) def update_labels(self): """ Updates keyboard labels based on active X keymap """ labels = {} # Get current layout group dpy = X.Display(hash(GdkX11.x11_get_default_xdisplay())) # Still no idea why... group = X.get_xkb_state(dpy).group # Get state of shift/alt/ctrl key mt = Gdk.ModifierType(self.keymap.get_modifier_state()) for a in self.background.areas: # Iterate over all translatable keys... if hasattr(Keys, a.name) and getattr(Keys, a.name) in KEY_TO_GDK: # Try to convert GKD key to keycode gdkkey = KEY_TO_GDK[getattr(Keys, a.name)] found, entries = self.keymap.get_entries_for_keyval(gdkkey) if gdkkey == Gdk.KEY_equal: # Special case, GDK reports nonsense here entries = [ [ e for e in entries if e.level == 0 ][-1] ] if not found: continue for k in sorted(entries, key=lambda a : a.level): # Try to convert keycode to label code = Gdk.keyval_to_unicode( self.keymap.translate_keyboard_state(k.keycode, mt, group) .keyval) if code != 0: labels[a.name] = unichr(code) break self.background.set_labels(labels) def parse_argumets(self, argv): if not OSDWindow.parse_argumets(self, argv): return False return True 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 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.info("Sucessfully locked input") pass # Lock everything just in case locks = [ LEFT, RIGHT, STICK ] + [ b.name for b in SCButtons ] self.daemon.lock(success, self.on_failed_to_lock, *locks) def quit(self, code=-1): self.daemon.unlock_all() for x in self._eh_ids: self.daemon.disconnect(x) self._eh_ids = [] del self.keyboard OSDWindow.quit(self, code) def show(self, *a): OSDWindow.show(self, *a) self.keyboard = uinputKeyboard(b"SCC OSD Keyboard") def set_cursor_position(self, x, y, cursor, limit): """ Moves cursor image. """ w = limit[2] - (cursor.get_allocation().width * 0.5) h = limit[3] - (cursor.get_allocation().height * 0.5) x = x / float(STICK_PAD_MAX) y = y / float(STICK_PAD_MAX) * -1.0 x, y = circle_to_square(x, y) x = (limit[0] + w * 0.5) + x * w * 0.5 y = (limit[1] + h * 0.5) + y * h * 0.5 self.f.move(cursor, int(x), int(y)) for a in self.background.areas: if a.contains(x, y): if a != self._hovers[cursor]: self._hovers[cursor] = a if self._pressed[cursor] is not None: self.keyboard.releaseEvent([ self._pressed[cursor] ]) self.key_from_cursor(cursor, True) if not self.timer_active('redraw'): self.timer('redraw', 0.01, self.redraw_background) break def redraw_background(self, *a): """ Updates hilighted keys on bacgkround image. """ self.background.hilight({ "AREA_" + a.name : Keyboard.HILIGHT_COLOR for a in [ a for a in self._hovers.values() if a ] }) def on_event(self, daemon, what, data): """ Called when button press, button release or stick / pad update is send by daemon. """ if what == LEFT: x, y = data self.set_cursor_position(x, y, self.cursor_left, self.limit_left) elif what == RIGHT: x, y = data self.set_cursor_position(x, y, self.cursor_right, self.limit_right) elif what == STICK: self._stick = data if not self.timer_active('stick'): self.timer("stick", 0.05, self._move_window) elif what == SCButtons.LPAD.name: self.key_from_cursor(self.cursor_left, data[0] == 1) elif what == SCButtons.RPAD.name: self.key_from_cursor(self.cursor_right, data[0] == 1) elif what in (SCButtons.RPADTOUCH.name, SCButtons.LPADTOUCH.name): pass elif what in self.BUTTON_MAP: if data[0]: self.keyboard.pressEvent([ self.BUTTON_MAP[what] ]) else: self.keyboard.releaseEvent([ self.BUTTON_MAP[what] ]) elif what in [ b.name for b in SCButtons ]: self.quit(0) def _move_window(self, *a): """ Called by timer while stick is tilted to move window around the screen. """ x, y = self._stick x = x * 50.0 / STICK_PAD_MAX y = y * -50.0 / STICK_PAD_MAX rx, ry = self.get_position() self.move(rx + x, ry + y) if abs(self._stick[0]) > 100 or abs(self._stick[1]) > 100: self.timer("stick", 0.05, self._move_window) def key_from_cursor(self, cursor, pressed): """ Sends keypress/keyrelease event to emulated keyboard, based on position of cursor on OSD keyboard. """ x = self.f.child_get_property(cursor, "x") y = self.f.child_get_property(cursor, "y") if pressed: for a in self.background.areas: if a.contains(x, y): if a.name.startswith("KEY_") and hasattr(Keys, a.name): key = getattr(Keys, a.name) if self._pressed[cursor] is not None: self.keyboard.releaseEvent([ self._pressed[cursor] ]) self.keyboard.pressEvent([ key ]) self._pressed[cursor] = key break elif self._pressed[cursor] is not None: self.keyboard.releaseEvent([ self._pressed[cursor] ]) self._pressed[cursor] = None
class Keyboard(OSDWindow, TimerManager): EPILOG = """Exit codes: 0 - clean exit, user closed keyboard 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) """ HILIGHT_COLOR = "#00688D" BUTTON_MAP = { SCButtons.A.name: Keys.KEY_ENTER, SCButtons.B.name: Keys.KEY_ESC, SCButtons.LB.name: Keys.KEY_BACKSPACE, SCButtons.RB.name: Keys.KEY_SPACE, SCButtons.LGRIP.name: Keys.KEY_LEFTSHIFT, SCButtons.RGRIP.name: Keys.KEY_RIGHTALT, } def __init__(self): OSDWindow.__init__(self, "osd-keyboard") TimerManager.__init__(self) self.daemon = None self.mapper = None self.keymap = Gdk.Keymap.get_default() self.keymap.connect('state-changed', self.on_state_changed) self.profile = Profile(TalkingActionParser()) kbimage = os.path.join(get_config_path(), 'keyboard.svg') if not os.path.exists(kbimage): # Prefer image in ~/.config/scc, but load default one as fallback kbimage = os.path.join(get_share_path(), "images", 'keyboard.svg') self.background = SVGWidget(self, kbimage) self.limits = {} self.limits[LEFT] = self.background.get_rect_area( self.background.get_element("LIMIT_LEFT")) self.limits[RIGHT] = self.background.get_rect_area( self.background.get_element("LIMIT_RIGHT")) cursor = os.path.join(get_share_path(), "images", 'menu-cursor.svg') self.cursors = {} self.cursors[LEFT] = Gtk.Image.new_from_file(cursor) self.cursors[LEFT].set_name("osd-keyboard-cursor") self.cursors[RIGHT] = Gtk.Image.new_from_file(cursor) self.cursors[RIGHT].set_name("osd-keyboard-cursor") self._eh_ids = [] self._stick = 0, 0 self._hovers = {self.cursors[LEFT]: None, self.cursors[RIGHT]: None} self._pressed = {self.cursors[LEFT]: None, self.cursors[RIGHT]: None} self.c = Gtk.Box() self.c.set_name("osd-keyboard-container") self.f = Gtk.Fixed() self.f.add(self.background) self.f.add(self.cursors[LEFT]) self.f.add(self.cursors[RIGHT]) self.c.add(self.f) self.add(self.c) self.timer('labels', 0.1, self.update_labels) def use_daemon(self, d): """ Allows (re)using already existing DaemonManager instance in same process """ self.daemon = d self._cononect_handlers() self.on_daemon_connected(self.daemon) def on_state_changed(self, x11keymap): if not self.timer_active('labels'): self.timer('labels', 0.1, self.update_labels) def update_labels(self): """ Updates keyboard labels based on active X keymap """ labels = {} # Get current layout group dpy = X.Display(hash( GdkX11.x11_get_default_xdisplay())) # Still no idea why... group = X.get_xkb_state(dpy).group # Get state of shift/alt/ctrl key mt = Gdk.ModifierType(self.keymap.get_modifier_state()) for a in self.background.areas: # Iterate over all translatable keys... if hasattr(Keys, a.name) and getattr(Keys, a.name) in KEY_TO_GDK: # Try to convert GKD key to keycode gdkkey = KEY_TO_GDK[getattr(Keys, a.name)] found, entries = self.keymap.get_entries_for_keyval(gdkkey) if gdkkey == Gdk.KEY_equal: # Special case, GDK reports nonsense here entries = [[e for e in entries if e.level == 0][-1]] if not found: continue for k in sorted(entries, key=lambda a: a.level): # Try to convert keycode to label translation = self.keymap.translate_keyboard_state( k.keycode, mt, group) if hasattr(translation, "keyval"): code = Gdk.keyval_to_unicode(translation.keyval) else: code = Gdk.keyval_to_unicode(translation[1]) if code != 0: labels[a.name] = unichr(code) break self.background.set_labels(labels) def parse_argumets(self, argv): if not OSDWindow.parse_argumets(self, argv): return False return True 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 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.info("Sucessfully locked input") pass # Lock everything locks = [LEFT, RIGHT, STICK] + [b.name for b in SCButtons] self.daemon.lock(success, self.on_failed_to_lock, *locks) def quit(self, code=-1): self.daemon.unlock_all() for x in self._eh_ids: self.daemon.disconnect(x) self._eh_ids = [] del self.mapper OSDWindow.quit(self, code) def show(self, *a): OSDWindow.show(self, *a) self.profile.load(find_profile(".scc-osd.keyboard")).compress() self.mapper = SlaveMapper(self.profile, keyboard=b"SCC OSD Keyboard") self.mapper.set_special_actions_handler(self) self.set_cursor_position(0, 0, self.cursors[LEFT], self.limits[LEFT]) self.set_cursor_position(0, 0, self.cursors[RIGHT], self.limits[RIGHT]) def on_event(self, daemon, what, data): """ Called when button press, button release or stick / pad update is send by daemon. """ self.mapper.handle_event(daemon, what, data) def on_sa_close(self, *a): """ Called by CloseOSDKeyboardAction """ self.quit(0) def on_sa_cursor(self, mapper, action, x, y): self.set_cursor_position(x * action.speed[0], y * action.speed[1], self.cursors[action.side], self.limits[action.side]) def on_sa_move(self, mapper, action, x, y): self._stick = x, y if not self.timer_active('stick'): self.timer("stick", 0.05, self._move_window) def on_sa_press(self, mapper, action, pressed): self.key_from_cursor(self.cursors[action.side], pressed) def set_cursor_position(self, x, y, cursor, limit): """ Moves cursor image. """ w = limit[2] - (cursor.get_allocation().width * 0.5) h = limit[3] - (cursor.get_allocation().height * 0.5) x = x / float(STICK_PAD_MAX) y = y / float(STICK_PAD_MAX) * -1.0 x, y = circle_to_square(x, y) x = clamp(cursor.get_allocation().width * 0.5, (limit[0] + w * 0.5) + x * w * 0.5, self.get_allocation().width - cursor.get_allocation().width ) - cursor.get_allocation().width * 0.5 y = clamp( cursor.get_allocation().height * 0.5, (limit[1] + h * 0.5) + y * h * 0.5, self.get_allocation().height - cursor.get_allocation().height ) - cursor.get_allocation().height * 0.5 cursor.position = int(x), int(y) self.f.move(cursor, *cursor.position) for a in self.background.areas: if a.contains(x, y): if a != self._hovers[cursor]: self._hovers[cursor] = a if self._pressed[cursor] is not None: self.mapper.keyboard.releaseEvent( [self._pressed[cursor]]) self.key_from_cursor(cursor, True) if not self.timer_active('redraw'): self.timer('redraw', 0.01, self.redraw_background) break def redraw_background(self, *a): """ Updates hilighted keys on bacgkround image. """ self.background.hilight({ "AREA_" + a.name: Keyboard.HILIGHT_COLOR for a in [a for a in self._hovers.values() if a] }) def _move_window(self, *a): """ Called by timer while stick is tilted to move window around the screen. """ x, y = self._stick x = x * 50.0 / STICK_PAD_MAX y = y * -50.0 / STICK_PAD_MAX rx, ry = self.get_position() self.move(rx + x, ry + y) if abs(self._stick[0]) > 100 or abs(self._stick[1]) > 100: self.timer("stick", 0.05, self._move_window) def key_from_cursor(self, cursor, pressed): """ Sends keypress/keyrelease event to emulated keyboard, based on position of cursor on OSD keyboard. """ x, y = cursor.position if pressed: for a in self.background.areas: if a.contains(x, y): if a.name.startswith("KEY_") and hasattr(Keys, a.name): key = getattr(Keys, a.name) if self._pressed[cursor] is not None: self.mapper.keyboard.releaseEvent( [self._pressed[cursor]]) self.mapper.keyboard.pressEvent([key]) self._pressed[cursor] = key break elif self._pressed[cursor] is not None: self.mapper.keyboard.releaseEvent([self._pressed[cursor]]) self._pressed[cursor] = None