def run(self): self.daemon = DaemonManager() self.config = Config() self.daemon.connect('alive', self.on_daemon_connected) self.daemon.connect('dead', self.on_daemon_died) self.daemon.connect('profile-changed', self.on_profile_changed) self.daemon.connect('reconfigured', self.on_daemon_reconfigured) self.daemon.connect('unknown-msg', self.on_unknown_message) self.mainloop.run()
def run(self): on_wayland = "WAYLAND_DISPLAY" in os.environ or not isinstance(Gdk.Display.get_default(), GdkX11.X11Display) if on_wayland: log.error("Cannot run on Wayland") self.exit_code = 8 return self.daemon = DaemonManager() self.config = Config() self._check_colorconfig_change() self.daemon.connect('alive', self.on_daemon_connected) self.daemon.connect('dead', self.on_daemon_died) self.daemon.connect('profile-changed', self.on_profile_changed) self.daemon.connect('reconfigured', self.on_daemon_reconfigured) self.daemon.connect('unknown-msg', self.on_unknown_message) self.mainloop.run()
def __init__(self, gladepath="/usr/share/scc", imagepath="/usr/share/scc/images"): Gtk.Application.__init__(self, application_id="me.kozec.scc", flags=Gio.ApplicationFlags.HANDLES_COMMAND_LINE | Gio.ApplicationFlags.NON_UNIQUE ) ProfileManager.__init__(self) # Setup Gtk.Application self.setup_commandline() # Setup DaemonManager self.dm = DaemonManager() self.dm.connect("alive", self.on_daemon_alive) self.dm.connect("profile-changed", self.on_daemon_profile_changed) self.dm.connect("error", self.on_daemon_error) self.dm.connect("dead", self.on_daemon_dead) # Set variables self.gladepath = gladepath self.imagepath = imagepath self.builder = None self.recursing = False self.daemon_changed_profile = False self.background = None self.current = Profile(GuiActionParser()) self.current_file = None self.just_started = True self.button_widgets = {} self.undo = [] self.redo = []
def __init__(self, gladepath="/usr/share/scc", imagepath="/usr/share/scc/images"): Gtk.Application.__init__(self, application_id="me.kozec.scc", flags=Gio.ApplicationFlags.HANDLES_COMMAND_LINE | Gio.ApplicationFlags.NON_UNIQUE ) UserDataManager.__init__(self) BindingEditor.__init__(self, self) # Setup Gtk.Application self.setup_commandline() # Setup DaemonManager self.dm = DaemonManager() self.dm.connect("alive", self.on_daemon_alive) self.dm.connect("controller-count-changed", self.on_daemon_ccunt_changed) self.dm.connect("dead", self.on_daemon_dead) self.dm.connect("error", self.on_daemon_error) self.dm.connect('reconfigured', self.on_daemon_reconfigured), self.dm.connect("version", self.on_daemon_version) # Set variables self.config = Config() self.gladepath = gladepath self.imagepath = imagepath self.builder = None self.recursing = False self.statusicon = None self.status = "unknown" self.context_menu_for = None self.daemon_changed_profile = False self.osd_mode = False # In OSD mode, only active profile can be editted self.osd_mode_mapper = None self.background = None self.outdated_version = None self.profile_switchers = [] self.current_file = None # Currently edited file self.controller_count = 0 self.current = Profile(GuiActionParser()) self.just_started = True self.button_widgets = {} self.hilights = { App.HILIGHT_COLOR : set(), App.OBSERVE_COLOR : set() } self.undo = [] self.redo = []
def __init__(self, gladepath="/usr/share/scc", imagepath="/usr/share/scc/images"): Gtk.Application.__init__(self, application_id="me.kozec.scc", flags=Gio.ApplicationFlags.HANDLES_COMMAND_LINE | Gio.ApplicationFlags.NON_UNIQUE ) UserDataManager.__init__(self) BindingEditor.__init__(self, self) # Setup Gtk.Application self.setup_commandline() # Setup DaemonManager self.dm = DaemonManager() self.dm.connect("alive", self.on_daemon_alive) self.dm.connect("controller-count-changed", self.on_daemon_ccunt_changed) self.dm.connect("dead", self.on_daemon_dead) self.dm.connect("error", self.on_daemon_error) self.dm.connect('reconfigured', self.on_daemon_reconfigured), self.dm.connect("version", self.on_daemon_version) # Set variables self.config = Config() self.gladepath = gladepath self.imagepath = imagepath self.builder = None self.recursing = False self.statusicon = None self.status = "unknown" self.context_menu_for = None self.daemon_changed_profile = False self.background = None self.outdated_version = None self.profile_switchers = [] self.current_file = None # Currently edited file self.controller_count = 0 self.current = Profile(GuiActionParser()) self.just_started = True self.button_widgets = {} self.hilights = { App.HILIGHT_COLOR : set(), App.OBSERVE_COLOR : set() } self.undo = [] self.redo = []
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 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 OSDDaemon(object): def __init__(self): self.exit_code = -1 self.mainloop = GLib.MainLoop() self.config = None self._window = None self._registered = False self._last_profile_change = 0 self._recent_profiles_undo = None OSDWindow._apply_css() def quit(self, code=-1): self.exit_code = code self.mainloop.quit() def get_exit_code(self): return self.exit_code def on_daemon_reconfigured(self, *a): log.debug("Reloading config...") self.config.reload() def on_profile_changed(self, daemon, profile): name = os.path.split(profile)[-1] if name.endswith(".sccprofile") and not name.startswith("."): # Ignore .mod and hidden files name = name[0:-11] recents = self.config['recent_profiles'] if len(recents) and recents[0] == name: # Already first in recent list return if time.time() - self._last_profile_change < 2.0: # Profiles are changing too fast, probably because user # is using scroll wheel over profile combobox if self._recent_profiles_undo: recents = [] + self._recent_profiles_undo self._last_profile_change = time.time() self._recent_profiles_undo = [] + recents while name in recents: recents.remove(name) recents.insert(0, name) if len(recents) > self.config['recent_max']: recents = recents[0:self.config['recent_max']] self.config['recent_profiles'] = recents self.config.save() log.debug("Updated recent profile list") def on_daemon_died(self, *a): log.error("Connection to daemon lost") self.quit(2) def on_daemon_connected(self, *a): def success(*a): log.info("Sucessfully registered as scc-osd-daemon") self._registered = True def failure(why): log.error("Failed to registered as scc-osd-daemon: %s", why) self.quit(1) if not self._registered: self.daemon.request('Register: osd', success, failure) def on_menu_closed(self, m): """ Called after OSD menu is hidden from screen """ self._window = None if m.get_exit_code() == 0: # 0 means that user selected item and confirmed selection self.daemon.request( 'Selected: %s %s' % (m.get_menuid(), m.get_selected_item_id()), lambda *a: False, lambda *a: False) def on_keyboard_closed(self, *a): """ Called after on-screen keyboard is hidden from the screen """ self._window = None def on_unknown_message(self, daemon, message): if not message.startswith("OSD:"): return if message.startswith("OSD: message"): args = split(message)[1:] m = Message() m.parse_argumets(args) m.show() elif message.startswith("OSD: keyboard"): if self._window: log.warning( "Another OSD is already visible - refusing to show keyboard" ) else: args = split(message)[1:] self._window = Keyboard() self._window.connect('destroy', self.on_keyboard_closed) # self._window.parse_argumets(args) # TODO: No arguments so far self._window.show() self._window.use_daemon(self.daemon) elif message.startswith("OSD: menu") or message.startswith( "OSD: gridmenu"): args = split(message)[1:] if self._window: log.warning( "Another OSD is already visible - refusing to show menu") else: self._window = GridMenu() if "gridmenu" in message else Menu() self._window.connect('destroy', self.on_menu_closed) self._window.use_config(self.config) if self._window.parse_argumets(args): self._window.show() self._window.use_daemon(self.daemon) else: log.error("Failed to show menu") self._window = None elif message.startswith("OSD: area"): args = split(message)[1:] if self._window: log.warning( "Another OSD is already visible - refusing to show area") else: args = split(message)[1:] self._window = Area() self._window.connect('destroy', self.on_keyboard_closed) if self._window.parse_argumets(args): self._window.show() else: self._window.quit() self._window = None elif message.startswith("OSD: clear"): # Clears active OSD window (if any) if self._window: self._window.quit() self._window = None else: log.warning("Unknown command from daemon: '%s'", message) def run(self): self.daemon = DaemonManager() self.config = Config() self.daemon.connect('alive', self.on_daemon_connected) self.daemon.connect('dead', self.on_daemon_died) self.daemon.connect('profile-changed', self.on_profile_changed) self.daemon.connect('reconfigured', self.on_daemon_reconfigured) self.daemon.connect('unknown-msg', self.on_unknown_message) self.mainloop.run()
class App(Gtk.Application, ProfileManager): """ Main application / window. """ IMAGE = "background.svg" HILIGHT_COLOR = "#FF00FF00" # ARGB CONFIG = "scc.config.json" def __init__(self, gladepath="/usr/share/scc", imagepath="/usr/share/scc/images"): Gtk.Application.__init__(self, application_id="me.kozec.scc", flags=Gio.ApplicationFlags.HANDLES_COMMAND_LINE | Gio.ApplicationFlags.NON_UNIQUE ) ProfileManager.__init__(self) # Setup Gtk.Application self.setup_commandline() # Setup DaemonManager self.dm = DaemonManager() self.dm.connect("alive", self.on_daemon_alive) self.dm.connect("profile-changed", self.on_daemon_profile_changed) self.dm.connect("error", self.on_daemon_error) self.dm.connect("dead", self.on_daemon_dead) # Set variables self.gladepath = gladepath self.imagepath = imagepath self.builder = None self.recursing = False self.daemon_changed_profile = False self.background = None self.current = Profile(GuiActionParser()) self.current_file = None self.just_started = True self.button_widgets = {} self.undo = [] self.redo = [] def setup_widgets(self): self.builder = Gtk.Builder() self.builder.add_from_file(os.path.join(self.gladepath, "app.glade")) self.builder.connect_signals(self) self.window = self.builder.get_object("window") self.add_window(self.window) self.window.set_title(_("SC Controller")) self.window.set_wmclass("SC Controller", "SC Controller") self.ribar = None for b in BUTTONS: self.button_widgets[b] = ControllerButton(self, b, self.builder.get_object("bt" + b.name)) for b in TRIGGERS: self.button_widgets[b] = ControllerTrigger(self, b, self.builder.get_object("btTrigger" + b)) for b in PADS: self.button_widgets[b] = ControllerPad(self, b, self.builder.get_object("bt" + b)) for b in STICKS: self.button_widgets[b] = ControllerStick(self, b, self.builder.get_object("bt" + b)) for b in GYROS: self.button_widgets[b] = ControllerGyro(self, b, self.builder.get_object("bt" + b)) self.builder.get_object("cbProfile").set_row_separator_func( lambda model, iter : model.get_value(iter, 1) is None and model.get_value(iter, 0) == "-" ) self.set_daemon_status("unknown") vbc = self.builder.get_object("vbC") main_area = self.builder.get_object("mainArea") vbc.get_parent().remove(vbc) vbc.connect('size-allocate', self.on_vbc_allocated) self.background = SVGWidget(self, os.path.join(self.imagepath, self.IMAGE)) self.background.connect('hover', self.on_background_area_hover) self.background.connect('leave', self.on_background_area_hover, None) self.background.connect('click', self.on_background_area_click) main_area.put(self.background, 0, 0) main_area.put(vbc, 0, 0) # (self.IMAGE_SIZE[0] / 2) - 90, self.IMAGE_SIZE[1] - 100) headerbar(self.builder.get_object("hbWindow")) def check(self): """ Performs various (two) checks and reports possible problems """ # TODO: Maybe not best place to do this if os.path.exists("/dev/uinput"): if not os.access("/dev/uinput", os.R_OK | os.W_OK): # Cannot acces uinput msg = _('You don\'t have required access to /dev/uinput.') msg += "\n" + _('This will most likely prevent emulation from working.') msg += "\n\n" + _('Please, consult your distribution manual on how to enable uinput') self.show_error(msg) else: # There is no uinput msg = _('/dev/uinput not found') msg += "\n" + _('Your kernel is either outdated or compiled without uinput support.') msg += "\n\n" + _('Please, consult your distribution manual on how to enable uinput') self.show_error(msg) def hilight(self, button): """ Hilights specified button on background image """ self.background.hilight({ button : App.HILIGHT_COLOR }) def hint(self, button): """ As hilight, but marks GTK Button as well """ active = None for b in self.button_widgets.values(): b.widget.set_state(Gtk.StateType.NORMAL) if b.name == button: active = b.widget if active is not None: active.set_state(Gtk.StateType.ACTIVE) self.hilight(button) def _choose_editor(self, action, title): if isinstance(action, SensitivityModifier): action = action.action if isinstance(action, ModeModifier) and not is_gyro_enable(action): e = ModeshiftEditor(self, self.on_action_chosen) e.set_title(_("Mode Shift for %s") % (title,)) elif isinstance(action, Macro): e = MacroEditor(self, self.on_action_chosen) e.set_title(_("Macro for %s") % (title,)) else: e = ActionEditor(self, self.on_action_chosen) e.set_title(title) return e def show_editor(self, id, press=False): if id in SCButtons: title = _("%s Button") % (id.name,) if press: title = _("%s Press") % (id.name,) ae = self._choose_editor(self.current.buttons[id], title) ae.set_button(id, self.current.buttons[id]) ae.show(self.window) elif id in TRIGGERS: ae = self._choose_editor(self.current.triggers[id], _("%s Trigger") % (id,)) ae.set_trigger(id, self.current.triggers[id]) ae.show(self.window) elif id in STICKS: ae = self._choose_editor(self.current.stick, _("Stick")) ae.set_stick(self.current.stick) ae.show(self.window) elif id in GYROS: ae = self._choose_editor(self.current.gyro, _("Gyro")) ae.set_gyro(self.current.gyro) ae.show(self.window) elif id in PADS: data = NoAction() if id == "LPAD": data = self.current.pads[Profile.LEFT] ae = self._choose_editor(data, _("Left Pad")) else: data = self.current.pads[Profile.RIGHT] ae = self._choose_editor(data, _("Right Pad")) ae.set_pad(id, data) ae.show(self.window) def on_btUndo_clicked(self, *a): if len(self.undo) < 1: return undo, self.undo = self.undo[-1], self.undo[0:-1] self._set_action(undo.id, undo.before) self.redo.append(undo) self.builder.get_object("btRedo").set_sensitive(True) if len(self.undo) < 1: self.builder.get_object("btUndo").set_sensitive(False) self.on_profile_changed() def on_btRedo_clicked(self, *a): if len(self.redo) < 1: return redo, self.redo = self.redo[-1], self.redo[0:-1] self._set_action(redo.id, redo.after) self.undo.append(redo) self.builder.get_object("btUndo").set_sensitive(True) if len(self.redo) < 1: self.builder.get_object("btRedo").set_sensitive(False) self.on_profile_changed() def on_profiles_loaded(self, profiles): cb = self.builder.get_object("cbProfile") model = cb.get_model() model.clear() i = 0 current_profile, current_index = self.load_profile_selection(), 0 for f in profiles: name = f.get_basename() if name.endswith(".mod"): continue if name.endswith(".sccprofile"): name = name[0:-11] if name == current_profile: current_index = i model.append((name, f, None)) i += 1 model.append(("-", None, None)) model.append((_("New profile..."), None, None)) if cb.get_active_iter() is None: cb.set_active(current_index) def on_cbProfile_changed(self, cb, *a): """ Called when user chooses profile in selection combo """ if self.recursing : return model = cb.get_model() iter = cb.get_active_iter() f = model.get_value(iter, 1) if f is None: if self.current_file is None: cb.set_active(0) else: self.select_profile(self.current_file.get_path()) new_name = os.path.split(self.current_file.get_path())[-1] if new_name.endswith(".mod"): new_name = new_name[0:-4] if new_name.endswith(".sccprofile"): new_name = new_name[0:-11] new_name = _("Copy of") + " " + new_name dlg = self.builder.get_object("dlgNewProfile") txNewProfile = self.builder.get_object("txNewProfile") txNewProfile.set_text(new_name) dlg.set_transient_for(self.window) dlg.show() else: self.load_profile(f) if not self.daemon_changed_profile: self.dm.set_profile(f.get_path()) self.save_profile_selection(f.get_path()) def on_dlgNewProfile_delete_event(self, dlg, *a): dlg.hide() return True def on_btNewProfile_clicked(self, *a): """ Called when new profile name is set and OK is clicked """ txNewProfile = self.builder.get_object("txNewProfile") dlg = self.builder.get_object("dlgNewProfile") cb = self.builder.get_object("cbProfile") name = txNewProfile.get_text() filename = txNewProfile.get_text() + ".sccprofile" path = os.path.join(get_profiles_path(), filename) self.current_file = Gio.File.new_for_path(path) self.recursing = True model = cb.get_model() model.insert(0, ( name, self.current_file, None )) cb.set_active(0) self.recursing = False self.save_profile(self.current_file, self.current) dlg.hide() def on_profile_loaded(self, profile, giofile): self.current = profile self.current_file = giofile for b in self.button_widgets.values(): b.update() self.on_profile_saved(giofile, False) # Just to indicate that there are no changes to save def on_profile_changed(self, *a): """ Called when selected profile is modified in memory. Displays "changed" next to profile name and shows Save button. If sccdaemon is alive, creates 'original.filename.mod' and loads it into daemon to activate changes right away. """ cb = self.builder.get_object("cbProfile") self.builder.get_object("rvProfileChanged").set_reveal_child(True) model = cb.get_model() for row in model: if self.current_file == model.get_value(row.iter, 1): model.set_value(row.iter, 2, _("(changed)")) break if self.dm.is_alive(): modfile = Gio.File.new_for_path(self.current_file.get_path() + ".mod") self.save_profile(modfile, self.current) def on_profile_saved(self, giofile, send=True): """ Called when selected profile is saved to disk """ if giofile.get_path().endswith(".mod"): # Special case, this one is saved only to be sent to daemon # and user doesn't need to know about it if self.dm.is_alive(): self.dm.set_profile(giofile.get_path()) return cb = self.builder.get_object("cbProfile") self.builder.get_object("rvProfileChanged").set_reveal_child(False) model = cb.get_model() for row in model: if model.get_value(row.iter, 1) == self.current_file: model.set_value(row.iter, 1, giofile) model.set_value(row.iter, 2, None) if send and self.dm.is_alive() and not self.daemon_changed_profile: self.dm.set_profile(giofile.get_path()) self.current_file = giofile def on_btSaveProfile_clicked(self, *a): self.save_profile(self.current_file, self.current) def on_action_chosen(self, id, action, reopen=False): before = self._set_action(id, action) if type(before) != type(action) or before.to_string() != action.to_string(): # TODO: Maybe better comparison self.undo.append(UndoRedo(id, before, action)) self.builder.get_object("btUndo").set_sensitive(True) self.on_profile_changed() if reopen: self.show_editor(id) def _set_action(self, id, action): """ Stores action in profile. Returns formely stored action. """ before = NoAction() if id in BUTTONS: before, self.current.buttons[id] = self.current.buttons[id], action self.button_widgets[id].update() if id in PRESSABLE: before, self.current.buttons[id] = self.current.buttons[id], action self.button_widgets[id.name].update() elif id in TRIGGERS: before, self.current.triggers[id] = self.current.triggers[id], action self.button_widgets[id].update() elif id in GYROS: before, self.current.gyro = self.current.gyro, action self.button_widgets[id].update() elif id in STICKS + PADS: if id in STICKS: before, self.current.stick = self.current.stick, action elif id == "LPAD": before, self.current.pads[Profile.LEFT] = self.current.pads[Profile.LEFT], action else: before, self.current.pads[Profile.RIGHT] = self.current.pads[Profile.RIGHT], action self.button_widgets[id].update() return before def on_background_area_hover(self, trash, area): self.hint(area) def on_background_area_click(self, trash, area): if area in [ x.name for x in BUTTONS ]: self.hint(None) self.show_editor(getattr(SCButtons, area)) elif area in TRIGGERS + STICKS + PADS: self.hint(None) self.show_editor(area) def on_vbc_allocated(self, vbc, allocation): """ Called when size of 'Button C' is changed. Centers button on background image """ main_area = self.builder.get_object("mainArea") x = (main_area.get_allocation().width - allocation.width) / 2 y = main_area.get_allocation().height - allocation.height main_area.move(vbc, x, y) def on_ebImage_motion_notify_event(self, box, event): self.background.on_mouse_moved(event.x, event.y) def on_mnuExit_activate(self, *a): self.quit() def on_daemon_alive(self, *a): self.set_daemon_status("alive") self.hide_error() if self.current_file is not None and not self.just_started: self.dm.set_profile(self.current_file.get_path()) def on_daemon_error(self, trash, error): log.debug("Daemon reported error '%s'", error) msg = _('There was an error with enabling emulation: <b>%s</b>') % (error,) # Known errors are handled with aditional message if "Device not found" in error: msg += "\n" + _("Please, check if you have reciever dongle connected to USB port.") elif "LIBUSB_ERROR_ACCESS" in error: msg += "\n" + _("You don't have access to controller device.") msg += "\n\n" + ( _("Consult your distribution manual, try installing Steam package or <a href='%s'>install required udev rules manually</a>.") % 'https://wiki.archlinux.org/index.php/Gamepad#Steam_Controller_Not_Pairing' ) # TODO: Write howto somewhere instead of linking to ArchWiki elif "LIBUSB_ERROR_BUSY" in error: msg += "\n" + _("Another application (most likely Steam) is using the controller.") elif "LIBUSB_ERROR_PIPE" in error: msg += "\n" + _("USB dongle was removed.") self.show_error(msg) self.set_daemon_status("dead") def show_error(self, message): if self.ribar is None: self.ribar = RIBar(message, Gtk.MessageType.ERROR) content = self.builder.get_object("content") content.pack_start(self.ribar, False, False, 1) content.reorder_child(self.ribar, 0) self.ribar.connect("close", self.hide_error) self.ribar.connect("response", self.hide_error) else: self.ribar.get_label().set_markup(message) self.ribar.show() self.ribar.set_reveal_child(True) def hide_error(self, *a): if self.ribar is not None: if self.ribar.get_parent() is not None: self.ribar.get_parent().remove(self.ribar) self.ribar = None def on_daemon_profile_changed(self, trash, profile): current_changed = self.builder.get_object("rvProfileChanged").get_reveal_child() if profile.endswith(".mod"): try: os.unlink(profile) except Exception, e: log.warning("Failed to remove .mod file") log.warning(e) if self.just_started or not current_changed: log.debug("Daemon uses profile '%s', selecting it in UI", profile) self.daemon_changed_profile = True found = self.select_profile(profile) self.daemon_changed_profile = False if not found: # Daemon uses unknown profile, override it with something I know about if self.current_file is not None: self.dm.set_profile(self.current_file.get_path()) self.just_started = False
class App(Gtk.Application, UserDataManager, BindingEditor): """ Main application / window. """ IMAGE = "background.svg" HILIGHT_COLOR = "#FF00FF00" # ARGB OBSERVE_COLOR = "#00007FFF" # ARGB CONFIG = "scc.config.json" def __init__(self, gladepath="/usr/share/scc", imagepath="/usr/share/scc/images"): Gtk.Application.__init__(self, application_id="me.kozec.scc", flags=Gio.ApplicationFlags.HANDLES_COMMAND_LINE | Gio.ApplicationFlags.NON_UNIQUE ) UserDataManager.__init__(self) BindingEditor.__init__(self, self) # Setup Gtk.Application self.setup_commandline() # Setup DaemonManager self.dm = DaemonManager() self.dm.connect("alive", self.on_daemon_alive) self.dm.connect("controller-count-changed", self.on_daemon_ccunt_changed) self.dm.connect("dead", self.on_daemon_dead) self.dm.connect("error", self.on_daemon_error) self.dm.connect('reconfigured', self.on_daemon_reconfigured), self.dm.connect("version", self.on_daemon_version) # Set variables self.config = Config() self.gladepath = gladepath self.imagepath = imagepath self.builder = None self.recursing = False self.statusicon = None self.status = "unknown" self.context_menu_for = None self.daemon_changed_profile = False self.background = None self.outdated_version = None self.profile_switchers = [] self.current_file = None # Currently edited file self.controller_count = 0 self.current = Profile(GuiActionParser()) self.just_started = True self.button_widgets = {} self.hilights = { App.HILIGHT_COLOR : set(), App.OBSERVE_COLOR : set() } self.undo = [] self.redo = [] def setup_widgets(self): # Important stuff self.builder = Gtk.Builder() self.builder.add_from_file(os.path.join(self.gladepath, "app.glade")) self.builder.connect_signals(self) self.window = self.builder.get_object("window") self.add_window(self.window) self.window.set_title(_("SC Controller")) self.window.set_wmclass("SC Controller", "SC Controller") self.ribar = None self.create_binding_buttons() ps = self.add_switcher(10, 10) ps.set_allow_new(True) ps.set_profile(self.load_profile_selection()) ps.connect('new-clicked', self.on_new_clicked) ps.connect('save-clicked', self.on_save_clicked) # Drag&drop target self.builder.get_object("content").drag_dest_set(Gtk.DestDefaults.ALL, [ Gtk.TargetEntry.new("text/uri-list", Gtk.TargetFlags.OTHER_APP, 0) ], Gdk.DragAction.COPY ) # 'C' button vbc = self.builder.get_object("vbC") self.main_area = self.builder.get_object("mainArea") vbc.get_parent().remove(vbc) vbc.connect('size-allocate', self.on_vbc_allocated) # Background self.background = SVGWidget(self, os.path.join(self.imagepath, self.IMAGE)) self.background.connect('hover', self.on_background_area_hover) self.background.connect('leave', self.on_background_area_hover, None) self.background.connect('click', self.on_background_area_click) self.main_area.put(self.background, 0, 0) self.main_area.put(vbc, 0, 0) # (self.IMAGE_SIZE[0] / 2) - 90, self.IMAGE_SIZE[1] - 100) # Test markers (those blue circles over PADs and sticks) self.lpadTest = Gtk.Image.new_from_file(os.path.join(self.imagepath, "test-cursor.svg")) self.rpadTest = Gtk.Image.new_from_file(os.path.join(self.imagepath, "test-cursor.svg")) self.stickTest = Gtk.Image.new_from_file(os.path.join(self.imagepath, "test-cursor.svg")) self.main_area.put(self.lpadTest, 40, 40) self.main_area.put(self.rpadTest, 290, 90) self.main_area.put(self.stickTest, 150, 40) # Headerbar headerbar(self.builder.get_object("hbWindow")) def setup_statusicon(self): menu = self.builder.get_object("mnuDaemon") self.statusicon = get_status_icon(self.imagepath, menu) self.statusicon.connect('clicked', self.on_statusicon_clicked) GLib.idle_add(self.statusicon.set, "scc-%s" % (self.status,), _("SC-Controller")) def destroy_statusicon(self): self.statusicon.destroy() self.statusicon = None def check(self): """ Performs various (three) checks and reports possible problems """ # TODO: Maybe not best place to do this try: kernel_mods = [ line.split(" ")[0] for line in file("/proc/modules", "r").read().split("\n") ] except Exception: # Maybe running on BSD or Windows... kernel_mods = [ ] if len(kernel_mods) > 0 and "uinput" not in kernel_mods: # There is no uinput msg = _('uinput kernel module not loaded') msg += "\n\n" + _('Please, consult your distribution manual on how to enable uinput') self.show_error(msg) elif not os.path.exists("/dev/uinput"): # /dev/uinput missing msg = _('/dev/uinput doesn\'t exists') msg += "\n" + _('uinput kernel module is loaded, but /dev/uinput is missing.') #msg += "\n\n" + _('Please, consult your distribution manual on what in the world could cause this.') msg += "\n\n" + _('Please, consult your distribution manual on how to enable uinput') self.show_error(msg) elif not check_access("/dev/uinput"): # Cannot acces uinput msg = _('You don\'t have required access to /dev/uinput.') msg += "\n" + _('This will most likely prevent emulation from working.') msg += "\n\n" + _('Please, consult your distribution manual on how to enable uinput') self.show_error(msg) def hilight(self, button): """ Hilights specified button on background image """ if button: self.hilights[App.HILIGHT_COLOR] = set([button]) else: self.hilights[App.HILIGHT_COLOR] = set() self._update_background() def _update_background(self): h = {} for color in self.hilights: for i in self.hilights[color]: h[i] = color self.background.hilight(h) def hint(self, button): """ As hilight, but marks GTK Button as well """ active = None for b in self.button_widgets.values(): b.widget.set_state(Gtk.StateType.NORMAL) if b.name == button: active = b.widget if active is not None: active.set_state(Gtk.StateType.ACTIVE) self.hilight(button) def show_editor(self, id): action = self.get_action(self.current, id) ae = self.choose_editor(action, "") ae.set_input(id, action) ae.show(self.window) def show_context_menu(self, for_id): """ Sets sensitivity of popup menu items and displays it on screen """ mnuPopup = self.builder.get_object("mnuPopup") mnuCopy = self.builder.get_object("mnuCopy") mnuClear = self.builder.get_object("mnuClear") mnuPaste = self.builder.get_object("mnuPaste") self.context_menu_for = for_id clp = Gtk.Clipboard.get_default(Gdk.Display.get_default()) mnuCopy.set_sensitive(bool(self.get_action(self.current, for_id))) mnuClear.set_sensitive(bool(self.get_action(self.current, for_id))) mnuPaste.set_sensitive(clp.wait_is_text_available()) mnuPopup.popup(None, None, None, None, 3, Gtk.get_current_event_time()) def save_config(self): self.config.save() self.dm.reconfigure() self.enable_test_mode() def on_statusicon_clicked(self, *a): """ Handler for user clicking on tray icon button """ self.window.set_visible(not self.window.get_visible()) def on_window_delete_event(self, *a): """ Called when user tries to close window """ if not IS_UNITY and self.config['gui']['enable_status_icon'] and self.config['gui']['minimize_to_status_icon']: # Override closing and hide instead self.window.set_visible(False) return True return False # Allow def on_mnuClear_activate(self, *a): """ Handler for 'Clear' context menu item. Simply sets NoAction to input. """ self.on_action_chosen(self.context_menu_for, NoAction()) def on_mnuCopy_activate(self, *a): """ Handler for 'Copy' context menu item. Converts action to string and sends that string to clipboard. """ a = self.get_action(self.current, self.context_menu_for) if a: if a.name: a = NameModifier(a.name, a) clp = Gtk.Clipboard.get_default(Gdk.Display.get_default()) clp.set_text(a.to_string().encode('utf-8'), -1) clp.store() def on_mnuPaste_activate(self, *a): """ Handler for 'Paste' context menu item. Reads string from clipboard, parses it as action and sets that action on selected input. """ clp = Gtk.Clipboard.get_default(Gdk.Display.get_default()) text = clp.wait_for_text() if text: a = GuiActionParser().restart(text.decode('utf-8')).parse() if not isinstance(a, InvalidAction): self.on_action_chosen(self.context_menu_for, a) def on_mnuGlobalSettings_activate(self, *a): from scc.gui.global_settings import GlobalSettings gs = GlobalSettings(self) gs.show(self.window) def on_mnuImport_activate(self, *a): """ Handler for 'Import Steam Profile' context menu item. Displays apropriate dialog. """ from scc.gui.import_dialog import ImportDialog gs = ImportDialog(self) gs.show(self.window) def on_btUndo_clicked(self, *a): if len(self.undo) < 1: return undo, self.undo = self.undo[-1], self.undo[0:-1] self.set_action(self.current, undo.id, undo.before) self.redo.append(undo) self.builder.get_object("btRedo").set_sensitive(True) if len(self.undo) < 1: self.builder.get_object("btUndo").set_sensitive(False) self.on_profile_modified() def on_btRedo_clicked(self, *a): if len(self.redo) < 1: return redo, self.redo = self.redo[-1], self.redo[0:-1] self.set_action(self.current, redo.id, redo.after) self.undo.append(redo) self.builder.get_object("btUndo").set_sensitive(True) if len(self.redo) < 1: self.builder.get_object("btRedo").set_sensitive(False) self.on_profile_modified() def on_profiles_loaded(self, profiles): for ps in self.profile_switchers: ps.set_profile_list(profiles) def on_dlgNewProfile_delete_event(self, dlg, *a): dlg.hide() return True def on_btNewProfile_clicked(self, *a): """ Called when new profile name is set and OK is clicked """ txNewProfile = self.builder.get_object("txNewProfile") dlg = self.builder.get_object("dlgNewProfile") self.new_profile(self.current, txNewProfile.get_text()) dlg.hide() def on_profile_modified(self, *a): """ Called when selected profile is modified in memory. """ self.profile_switchers[0].set_profile_modified(True) if not self.current_file.get_path().endswith(".mod"): mod = self.current_file.get_path() + ".mod" self.current_file = Gio.File.new_for_path(mod) self.save_profile(self.current_file, self.current) def on_profile_loaded(self, profile, giofile): self.current = profile self.current_file = giofile self.profile_switchers[0].set_profile_modified(False) for b in self.button_widgets.values(): b.update() def on_profile_selected(self, ps, name, giofile): if ps == self.profile_switchers[0]: self.load_profile(giofile) if ps.get_controller(): ps.get_controller().set_profile(giofile.get_path()) def on_unknown_profile(self, ps, name): log.warn("Daemon reported unknown profile: '%s'; Overriding.", name) if self.current_file is not None: ps.get_controller().set_profile(self.current_file.get_path()) def on_save_clicked(self, *a): if self.current_file.get_path().endswith(".mod"): orig = self.current_file.get_path()[0:-4] self.current_file = Gio.File.new_for_path(orig) self.save_profile(self.current_file, self.current) def on_profile_saved(self, giofile, send=True): """ Called when selected profile is saved to disk """ if giofile.get_path().endswith(".mod"): # Special case, this one is saved only to be sent to daemon # and user doesn't need to know about it if self.dm.is_alive(): self.dm.set_profile(giofile.get_path()) return self.profile_switchers[0].set_profile_modified(False) if send and self.dm.is_alive() and not self.daemon_changed_profile: self.dm.set_profile(giofile.get_path()) self.current_file = giofile def on_new_clicked(self, ps, name): new_name = _("Copy of %s") % (name,) filename = os.path.join(get_profiles_path(), new_name + ".sccprofile") i = 0 while os.path.exists(filename): i += 1 new_name = _("Copy of %s (%s)") % (name, i) filename = os.path.join(get_profiles_path(), new_name + ".sccprofile") dlg = self.builder.get_object("dlgNewProfile") txNewProfile = self.builder.get_object("txNewProfile") txNewProfile.set_text(new_name) dlg.set_transient_for(self.window) dlg.show() def on_action_chosen(self, id, action): before = self.set_action(self.current, id, action) if before.to_string() != action.to_string(): # TODO: Maybe better comparison self.undo.append(UndoRedo(id, before, action)) self.builder.get_object("btUndo").set_sensitive(True) self.on_profile_modified() def on_background_area_hover(self, trash, area): self.hint(area) def on_background_area_click(self, trash, area): if area in [ x.name for x in BUTTONS ]: self.hint(None) self.show_editor(getattr(SCButtons, area)) elif area in TRIGGERS + STICKS + PADS: self.hint(None) self.show_editor(area) def on_vbc_allocated(self, vbc, allocation): """ Called when size of 'Button C' is changed. Centers button on background image """ main_area = self.builder.get_object("mainArea") x = (main_area.get_allocation().width - allocation.width) / 2 y = main_area.get_allocation().height - allocation.height main_area.move(vbc, x, y) def on_ebImage_motion_notify_event(self, box, event): self.background.on_mouse_moved(event.x, event.y) def on_mnuExit_activate(self, *a): self.quit() def on_mnuAbout_activate(self, *a): from scc.gui.aboutdialog import AboutDialog AboutDialog(self).show(self.window) def on_daemon_alive(self, *a): self.set_daemon_status("alive", True) self.hide_error() self.just_started = False if self.profile_switchers[0].get_file() is not None and not self.just_started: self.dm.set_profile(self.current_file.get_path()) GLib.timeout_add_seconds(1, self.check) self.enable_test_mode() def on_daemon_ccunt_changed(self, daemon, count): if (self.controller_count, count) == (0, 1): # First controller connected # # 'event' signal should be connected only on first controller, # so this block is executed only when number of connected # controllers changes from 0 to 1 c = self.dm.get_controllers()[0] c.connect('event', self.on_daemon_event_observer) elif count > self.controller_count: # Controller added while len(self.profile_switchers) < count: s = self.add_switcher() elif count < self.controller_count: # Controller removed while len(self.profile_switchers) > max(1, count): s = self.profile_switchers.pop() s.set_controller(None) self.remove_switcher(s) # Assign controllers to widgets for i in xrange(0, count): c = self.dm.get_controllers()[i] self.profile_switchers[i].set_controller(c) if count < 1: # Special case, no controllers are connected, but one widget # has to stay on screen self.profile_switchers[0].set_controller(None) self.controller_count = count def new_profile(self, profile, name): filename = os.path.join(get_profiles_path(), name + ".sccprofile") self.current_file = Gio.File.new_for_path(filename) self.save_profile(self.current_file, profile) def add_switcher(self, margin_left=30, margin_right=40, margin_bottom=2): """ Adds new profile switcher widgets on top of window. Called when new controller is connected to daemon. Returns generated ProfileSwitcher instance. """ vbAllProfiles = self.builder.get_object("vbAllProfiles") ps = ProfileSwitcher(self.imagepath, self.config) ps.set_margin_left(margin_left) ps.set_margin_right(margin_right) ps.set_margin_bottom(margin_bottom) ps.connect('right-clicked', self.on_profile_right_clicked) vbAllProfiles.pack_start(ps, False, False, 0) vbAllProfiles.reorder_child(ps, 0) vbAllProfiles.show_all() if len(self.profile_switchers) > 0: ps.set_profile_list(self.profile_switchers[0].get_profile_list()) self.profile_switchers.append(ps) ps.connect('changed', self.on_profile_selected) ps.connect('unknown-profile', self.on_unknown_profile) return ps def remove_switcher(self, s): """ Removes given profile switcher from UI. """ vbAllProfiles = self.builder.get_object("vbAllProfiles") vbAllProfiles.remove(s) s.destroy() def enable_test_mode(self): """ Disables and re-enables Input Test mode. If sniffing is disabled in daemon configuration, 2nd call fails and logs error. """ if self.dm.is_alive(): try: c = self.dm.get_controllers()[0] except IndexError: # Zero controllers return c.unlock_all() c.observe(DaemonManager.nocallback, self.on_observe_failed, 'A', 'B', 'C', 'X', 'Y', 'START', 'BACK', 'LB', 'RB', 'LPAD', 'RPAD', 'LGRIP', 'RGRIP', 'LT', 'RT', 'LEFT', 'RIGHT', 'STICK', 'STICKPRESS') def on_observe_failed(self, error): log.debug("Failed to enable test mode: %s", error) def on_daemon_version(self, daemon, version): """ Checks if reported version matches expected one. If not, daemon is restarted. """ if version != DAEMON_VERSION and self.outdated_version != version: log.warning( "Running daemon instance is too old (version %s, expected %s). Restarting...", version, DAEMON_VERSION) self.outdated_version = version self.set_daemon_status("unknown", False) self.dm.restart() def on_daemon_error(self, daemon, error): log.debug("Daemon reported error '%s'", error) msg = _('There was an error with enabling emulation: <b>%s</b>') % (error,) # Known errors are handled with aditional message if "Device not found" in error: msg += "\n" + _("Please, check if you have reciever dongle connected to USB port.") elif "LIBUSB_ERROR_ACCESS" in error: msg += "\n" + _("You don't have access to controller device.") msg += "\n\n" + ( _("Consult your distribution manual, try installing Steam package or <a href='%s'>install required udev rules manually</a>.") % 'https://wiki.archlinux.org/index.php/Gamepad#Steam_Controller_Not_Pairing' ) # TODO: Write howto somewhere instead of linking to ArchWiki elif "LIBUSB_ERROR_BUSY" in error: msg += "\n" + _("Another application (most likely Steam) is using the controller.") elif "LIBUSB_ERROR_PIPE" in error: msg += "\n" + _("USB dongle was removed.") self.show_error(msg) self.set_daemon_status("error", True) def on_daemon_event_observer(self, daemon, what, data): if what in (LEFT, RIGHT, STICK): widget, area = { LEFT : (self.lpadTest, "LPADTEST"), RIGHT : (self.rpadTest, "RPADTEST"), STICK : (self.stickTest, "STICKTEST"), }[what] # Check if stick or pad is released if data[0] == data[1] == 0: widget.hide() return if not widget.is_visible(): widget.show() # Grab values ax, ay, aw, trash = self.background.get_area_position(area) cw = widget.get_allocation().width # Compute center x, y = ax + aw * 0.5 - cw * 0.5, ay + 1.0 - cw * 0.5 # Add pad position x += data[0] * aw / STICK_PAD_MAX * 0.5 y -= data[1] * aw / STICK_PAD_MAX * 0.5 # Move circle self.main_area.move(widget, x, y) elif what in ("LT", "RT", "STICKPRESS"): what = { "LT" : "LEFT", "RT" : "RIGHT", "STICKPRESS" : "STICK" }[what] if data[0]: self.hilights[App.OBSERVE_COLOR].add(what) else: self.hilights[App.OBSERVE_COLOR].remove(what) self._update_background() elif hasattr(SCButtons, what): if data[0]: self.hilights[App.OBSERVE_COLOR].add(what) else: self.hilights[App.OBSERVE_COLOR].remove(what) self._update_background() else: print "event", what def on_profile_right_clicked(self, ps): for name in ("mnuConfigureController", "mnuTurnoffController"): # Disable controller-related menu items if controller is not connected obj = self.builder.get_object(name) obj.set_sensitive(ps.get_controller() is not None) mnuPS = self.builder.get_object("mnuPS") mnuPS.ps = ps mnuPS.popup(None, None, None, None, 3, Gtk.get_current_event_time()) def on_mnuConfigureController_activate(self, *a): from scc.gui.controller_settings import ControllerSettings mnuPS = self.builder.get_object("mnuPS") cs = ControllerSettings(self, mnuPS.ps.get_controller(), mnuPS.ps) cs.show(self.window) def mnuTurnoffController_activate(self, *a): mnuPS = self.builder.get_object("mnuPS") if mnuPS.ps.get_controller(): mnuPS.ps.get_controller().turnoff() def show_error(self, message): if self.ribar is None: self.ribar = RIBar(message, Gtk.MessageType.ERROR) content = self.builder.get_object("content") content.pack_start(self.ribar, False, False, 1) content.reorder_child(self.ribar, 0) self.ribar.connect("close", self.hide_error) self.ribar.connect("response", self.hide_error) else: self.ribar.get_label().set_markup(message) self.ribar.show() self.ribar.set_reveal_child(True) def hide_error(self, *a): if self.ribar is not None: if self.ribar.get_parent() is not None: self.ribar.get_parent().remove(self.ribar) self.ribar = None def on_daemon_reconfigured(self, *a): log.debug("Reloading config...") self.config.reload() for ps in self.profile_switchers: ps.set_controller(ps.get_controller()) def on_daemon_dead(self, *a): if self.just_started: self.dm.restart() self.just_started = False self.set_daemon_status("unknown", True) return for ps in self.profile_switchers: ps.set_controller(None) ps.on_daemon_dead() self.set_daemon_status("dead", False) def on_mnuEmulationEnabled_toggled(self, cb): if self.recursing : return if cb.get_active(): # Turning daemon on self.set_daemon_status("unknown", True) cb.set_sensitive(False) self.dm.start() else: # Turning daemon off self.set_daemon_status("unknown", False) cb.set_sensitive(False) self.hide_error() self.dm.stop() def do_startup(self, *a): Gtk.Application.do_startup(self, *a) self.load_profile_list() self.setup_widgets() if self.app.config['gui']['enable_status_icon']: self.setup_statusicon() self.set_daemon_status("unknown", True) def do_local_options(self, trash, lo): set_logging_level(lo.contains("verbose"), lo.contains("debug") ) return -1 def do_command_line(self, cl): Gtk.Application.do_command_line(self, cl) if len(cl.get_arguments()) > 1: filename = cl.get_arguments()[-1] giofile = Gio.File.new_for_path(filename) # Local file, looks like vdf profile from scc.gui.import_dialog import ImportDialog gs = ImportDialog(self) def i_told_you_to_quit(*a): sys.exit(0) gs.window.connect('destroy', i_told_you_to_quit) gs.show(self.window) # Skip first screen and try to import this file gs.on_preload_finished(gs.set_file, giofile.get_path()) else: self.activate() return 0 def do_activate(self, *a): self.builder.get_object("window").show() def remove_dot_profile(self): """ Checks if first profile in list begins with dot and if yes, removes it. This is done to undo automatic addition that is done when daemon reports selecting such profile. """ cb = self.builder.get_object("cbProfile") model = cb.get_model() if len(model) == 0: # Nothing to remove return if not model[0][0].startswith("."): # Not dot profile return active = model.get_path(cb.get_active_iter()) first = model[0].path if active == first: # Can't remove active item return model.remove(model[0].iter) def set_daemon_status(self, status, daemon_runs): """ Updates image that shows daemon status and menu shown when image is clicked """ log.debug("daemon status: %s", status) icon = os.path.join(self.imagepath, "scc-%s.svg" % (status,)) imgDaemonStatus = self.builder.get_object("imgDaemonStatus") btDaemon = self.builder.get_object("btDaemon") mnuEmulationEnabled = self.builder.get_object("mnuEmulationEnabled") imgDaemonStatus.set_from_file(icon) mnuEmulationEnabled.set_sensitive(True) self.window.set_icon_from_file(icon) self.status = status if self.statusicon: GLib.idle_add(self.statusicon.set, "scc-%s" % (self.status,), _("SC-Controller")) self.recursing = True if status == "alive": btDaemon.set_tooltip_text(_("Emulation is active")) elif status == "error": btDaemon.set_tooltip_text(_("Error enabling emulation")) elif status == "dead": btDaemon.set_tooltip_text(_("Emulation is inactive")) else: btDaemon.set_tooltip_text(_("Checking emulation status...")) mnuEmulationEnabled.set_active(daemon_runs) self.recursing = False def setup_commandline(self): def aso(long_name, short_name, description, arg=GLib.OptionArg.NONE, flags=GLib.OptionFlags.IN_MAIN): """ add_simple_option, adds program argument in simple way """ o = GLib.OptionEntry() if short_name: o.long_name = long_name o.short_name = short_name o.description = description o.flags = flags o.arg = arg self.add_main_option_entries([o]) self.connect('handle-local-options', self.do_local_options) aso("verbose", b"v", "Be verbose") aso("debug", b"d", "Be more verbose (debug mode)") def save_profile_selection(self, path): """ Saves name of profile into config file """ name = os.path.split(path)[-1] if name.endswith(".sccprofile"): name = name[0:-11] data = dict(current_profile=name) jstr = json.dumps(data, sort_keys=True, indent=4) open(os.path.join(get_config_path(), self.CONFIG), "w").write(jstr) def load_profile_selection(self): """ Returns name profile from config file or None if there is none saved """ try: data = json.loads(open(os.path.join(get_config_path(), self.CONFIG), "r").read()) return data['current_profile'] except: return None def on_drag_data_received(self, widget, context, x, y, data, info, time): """ Drag-n-drop handler """ if str(data.get_data_type()) == "text/uri-list": # Only file can be dropped here if len(data.get_uris()): uri = data.get_uris()[0] giofile = Gio.File.new_for_uri(uri) if giofile.get_path(): path = giofile.get_path() if path.endswith(".vdf") or path.endswith(".vdffz"): # Local file, looks like vdf profile from scc.gui.import_dialog import ImportDialog gs = ImportDialog(self) gs.show(self.window) # Skip first screen and try to import this file gs.on_preload_finished(gs.set_file, giofile.get_path())
class InputDisplay(OSDWindow): IMAGE = "inputdisplay.svg" HILIGHT_COLOR = "#FF00FF00" # ARGB OBSERVE_COLOR = "#00007FFF" # ARGB def __init__(self, imagepath="/usr/share/scc/images"): OSDWindow.__init__(self, "osd-menu") self.daemon = None self.config = None self.hilights = { self.HILIGHT_COLOR : set(), self.OBSERVE_COLOR : set() } self.imagepath = imagepath self._eh_ids = [] def show(self): self.main_area = Gtk.Fixed() self.background = SVGWidget(os.path.join(self.imagepath, self.IMAGE)) self.lpadTest = Gtk.Image.new_from_file(os.path.join(self.imagepath, "inputdisplay-cursor.svg")) self.rpadTest = Gtk.Image.new_from_file(os.path.join(self.imagepath, "inputdisplay-cursor.svg")) self.stickTest = Gtk.Image.new_from_file(os.path.join(self.imagepath, "inputdisplay-cursor.svg")) self.main_area.set_property("margin-left", 10) self.main_area.set_property("margin-right", 10) self.main_area.set_property("margin-top", 10) self.main_area.set_property("margin-bottom", 10) self.main_area.put(self.background, 0, 0) self.main_area.put(self.lpadTest, 40, 40) self.main_area.put(self.rpadTest, 290, 90) self.main_area.put(self.stickTest, 150, 40) self.add(self.main_area) OSDWindow.show(self) self.lpadTest.hide() self.rpadTest.hide() self.stickTest.hide() def run(self): self.daemon = DaemonManager() self._connect_handlers() OSDWindow.run(self) 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 _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 on_daemon_connected(self, *a): c = self.daemon.get_controllers()[0] c.unlock_all() c.observe(DaemonManager.nocallback, self.on_observe_failed, 'A', 'B', 'C', 'X', 'Y', 'START', 'BACK', 'LB', 'RB', 'LPAD', 'RPAD', 'LGRIP', 'RGRIP', 'LT', 'RT', 'LEFT', 'RIGHT', 'STICK', 'STICKPRESS') c.connect('event', self.on_daemon_event_observer) c.connect('lost', self.on_controller_lost) def on_observe_failed(self, error): log.error("Failed to enable test mode: %s", error) if "Sniffing" in error: log.error("") log.error("=================================================================================") log.error("[!!] Please, enable 'Input Test Mode' on 'Advanced' tab in SC-Controller settings") log.error("=================================================================================") self.quit(3) def on_daemon_event_observer(self, daemon, what, data): if what in (LEFT, RIGHT, STICK): widget, area = { LEFT : (self.lpadTest, "LPADTEST"), RIGHT : (self.rpadTest, "RPADTEST"), STICK : (self.stickTest, "STICKTEST"), }[what] # Check if stick or pad is released if data[0] == data[1] == 0: widget.hide() return if not widget.is_visible(): widget.show() # Grab values ax, ay, aw, trash = self.background.get_area_position(area) cw = widget.get_allocation().width # Compute center x, y = ax + aw * 0.5 - cw * 0.5, ay + 1.0 - cw * 0.5 # Add pad position x += data[0] * aw / STICK_PAD_MAX * 0.5 y -= data[1] * aw / STICK_PAD_MAX * 0.5 # Move circle self.main_area.move(widget, x, y) elif what in ("LT", "RT", "STICKPRESS"): what = { "LT" : "LEFT", "RT" : "RIGHT", "STICKPRESS" : "STICK" }[what] if data[0]: self.hilights[self.OBSERVE_COLOR].add(what) else: self.hilights[self.OBSERVE_COLOR].remove(what) self._update_background() elif hasattr(SCButtons, what): try: if data[0]: self.hilights[self.OBSERVE_COLOR].add(what) else: self.hilights[self.OBSERVE_COLOR].remove(what) self._update_background() except KeyError, e: # Non fatal pass else:
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 OSDDaemon(object): def __init__(self): self.exit_code = -1 self.mainloop = GLib.MainLoop() self._window = None self._registered = False OSDWindow._apply_css() def quit(self, code=-1): self.exit_code = code self.mainloop.quit() def get_exit_code(self): return self.exit_code def on_daemon_died(self, *a): log.error("Daemon died") self.quit(2) def on_daemon_connected(self, *a): def success(*a): log.info("Sucessfully registered as scc-osd-daemon") self._registered = True def failure(why): log.error("Failed to registered as scc-osd-daemon: %s", why) self.quit(1) if not self._registered: self.daemon.request('Register: osd', success, failure) def on_menu_closed(self, m): """ Called after OSD menu is hidden from screen """ self._window = None if m.get_exit_code() == 0: # 0 means that user selected item and confirmed selection self.daemon.request( 'Selected: %s %s' % (m.get_menuid(), m.get_selected_item_id()), lambda *a : False, lambda *a : False) def on_keyboard_closed(self, *a): """ Called after on-screen keyboard is hidden from the screen """ self._window = None def on_unknown_message(self, daemon, message): if not message.startswith("OSD:"): return if message.startswith("OSD: message"): args = split(message)[1:] m = Message() m.parse_argumets(args) m.show() elif message.startswith("OSD: keyboard"): if self._window: log.warning("Another OSD is already visible - refusing to show keyboard") else: args = split(message)[1:] self._window = Keyboard() self._window.connect('destroy', self.on_keyboard_closed) # self._window.parse_argumets(args) # TODO: No arguments so far self._window.show() self._window.use_daemon(self.daemon) elif message.startswith("OSD: menu") or message.startswith("OSD: gridmenu"): args = split(message)[1:] if self._window: log.warning("Another OSD is already visible - refusing to show menu") else: self._window = GridMenu() if "gridmenu" in message else Menu() self._window.connect('destroy', self.on_menu_closed) if self._window.parse_argumets(args): self._window.show() self._window.use_daemon(self.daemon) else: log.error("Failed to show menu") self._window = None elif message.startswith("OSD: area"): args = split(message)[1:] if self._window: log.warning("Another OSD is already visible - refusing to show area") else: args = split(message)[1:] self._window = Area() self._window.connect('destroy', self.on_keyboard_closed) if self._window.parse_argumets(args): self._window.show() else: self._window.quit() self._window = None elif message.startswith("OSD: clear"): # Clears active OSD window (if any) if self._window: self._window.quit() self._window = None else: log.warning("Unknown command from daemon: '%s'", message) def run(self): self.daemon = DaemonManager() self.daemon.connect('dead', self.on_daemon_died) self.daemon.connect('alive', self.on_daemon_connected) self.daemon.connect('unknown-msg', self.on_unknown_message) self.mainloop.run()
class InputDisplay(OSDWindow): IMAGE = "inputdisplay.svg" HILIGHT_COLOR = "#FF00FF00" # ARGB OBSERVE_COLOR = "#00007FFF" # ARGB def __init__(self, imagepath="/usr/share/scc/images"): OSDWindow.__init__(self, "osd-menu") self.daemon = None self.config = None self.hilights = {self.HILIGHT_COLOR: set(), self.OBSERVE_COLOR: set()} self.imagepath = imagepath self._eh_ids = [] def show(self): self.main_area = Gtk.Fixed() self.background = SVGWidget(self, os.path.join(self.imagepath, self.IMAGE)) self.lpadTest = Gtk.Image.new_from_file( os.path.join(self.imagepath, "inputdisplay-cursor.svg")) self.rpadTest = Gtk.Image.new_from_file( os.path.join(self.imagepath, "inputdisplay-cursor.svg")) self.stickTest = Gtk.Image.new_from_file( os.path.join(self.imagepath, "inputdisplay-cursor.svg")) self.main_area.set_property("margin-left", 10) self.main_area.set_property("margin-right", 10) self.main_area.set_property("margin-top", 10) self.main_area.set_property("margin-bottom", 10) self.main_area.put(self.background, 0, 0) self.main_area.put(self.lpadTest, 40, 40) self.main_area.put(self.rpadTest, 290, 90) self.main_area.put(self.stickTest, 150, 40) self.add(self.main_area) OSDWindow.show(self) self.lpadTest.hide() self.rpadTest.hide() self.stickTest.hide() def run(self): self.daemon = DaemonManager() self._connect_handlers() OSDWindow.run(self) 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 _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 on_daemon_connected(self, *a): c = self.daemon.get_controllers()[0] c.unlock_all() c.observe(DaemonManager.nocallback, self.on_observe_failed, 'A', 'B', 'C', 'X', 'Y', 'START', 'BACK', 'LB', 'RB', 'LPAD', 'RPAD', 'LGRIP', 'RGRIP', 'LT', 'RT', 'LEFT', 'RIGHT', 'STICK', 'STICKPRESS') c.connect('event', self.on_daemon_event_observer) def on_observe_failed(self, error): log.error("Failed to enable test mode: %s", error) self.quit(3) def on_daemon_died(self, *a): log.error("Daemon died") self.quit(2) def on_daemon_event_observer(self, daemon, what, data): if what in (LEFT, RIGHT, STICK): widget, area = { LEFT: (self.lpadTest, "LPADTEST"), RIGHT: (self.rpadTest, "RPADTEST"), STICK: (self.stickTest, "STICKTEST"), }[what] # Check if stick or pad is released if data[0] == data[1] == 0: widget.hide() return if not widget.is_visible(): widget.show() # Grab values ax, ay, aw, trash = self.background.get_area_position(area) cw = widget.get_allocation().width # Compute center x, y = ax + aw * 0.5 - cw * 0.5, ay + 1.0 - cw * 0.5 # Add pad position x += data[0] * aw / STICK_PAD_MAX * 0.5 y -= data[1] * aw / STICK_PAD_MAX * 0.5 # Move circle self.main_area.move(widget, x, y) elif what in ("LT", "RT", "STICKPRESS"): what = {"LT": "LEFT", "RT": "RIGHT", "STICKPRESS": "STICK"}[what] if data[0]: self.hilights[self.OBSERVE_COLOR].add(what) else: self.hilights[self.OBSERVE_COLOR].remove(what) self._update_background() elif hasattr(SCButtons, what): try: if data[0]: self.hilights[self.OBSERVE_COLOR].add(what) else: self.hilights[self.OBSERVE_COLOR].remove(what) self._update_background() except KeyError, e: # Non fatal pass else:
class App(Gtk.Application, UserDataManager, BindingEditor): """ Main application / window. """ IMAGE = "background.svg" HILIGHT_COLOR = "#FF00FF00" # ARGB OBSERVE_COLOR = "#00007FFF" # ARGB CONFIG = "scc.config.json" RELEASE_URL = "https://github.com/kozec/sc-controller/releases/tag/v%s" OSD_MODE_PROF_NAME = ".scc-osd.profile_editor" def __init__(self, gladepath="/usr/share/scc", imagepath="/usr/share/scc/images"): Gtk.Application.__init__(self, application_id="me.kozec.scc", flags=Gio.ApplicationFlags.HANDLES_COMMAND_LINE | Gio.ApplicationFlags.NON_UNIQUE ) UserDataManager.__init__(self) BindingEditor.__init__(self, self) # Setup Gtk.Application self.setup_commandline() # Setup DaemonManager self.dm = DaemonManager() self.dm.connect("alive", self.on_daemon_alive) self.dm.connect("controller-count-changed", self.on_daemon_ccunt_changed) self.dm.connect("dead", self.on_daemon_dead) self.dm.connect("error", self.on_daemon_error) self.dm.connect('reconfigured', self.on_daemon_reconfigured), self.dm.connect("version", self.on_daemon_version) # Set variables self.config = Config() self.gladepath = gladepath self.imagepath = imagepath self.builder = None self.recursing = False self.statusicon = None self.status = "unknown" self.context_menu_for = None self.daemon_changed_profile = False self.osd_mode = False # In OSD mode, only active profile can be editted self.osd_mode_mapper = None self.background = None self.outdated_version = None self.profile_switchers = [] self.current_file = None # Currently edited file self.controller_count = 0 self.current = Profile(GuiActionParser()) self.just_started = True self.button_widgets = {} self.hilights = { App.HILIGHT_COLOR : set(), App.OBSERVE_COLOR : set() } self.undo = [] self.redo = [] def setup_widgets(self): # Important stuff self.builder = Gtk.Builder() self.builder.add_from_file(os.path.join(self.gladepath, "app.glade")) self.builder.connect_signals(self) self.window = self.builder.get_object("window") self.add_window(self.window) self.window.set_title(_("SC Controller")) self.window.set_wmclass("SC Controller", "SC Controller") self.ribar = None self.create_binding_buttons() ps = self.add_switcher(10, 10) ps.set_allow_new(True) ps.set_profile(self.load_profile_selection()) ps.connect('new-clicked', self.on_new_clicked) ps.connect('save-clicked', self.on_save_clicked) # Drag&drop target self.builder.get_object("content").drag_dest_set(Gtk.DestDefaults.ALL, [ Gtk.TargetEntry.new("text/uri-list", Gtk.TargetFlags.OTHER_APP, 0), Gtk.TargetEntry.new("text/plain", Gtk.TargetFlags.OTHER_APP, 0) ], Gdk.DragAction.COPY ) # 'C' button vbc = self.builder.get_object("vbC") self.main_area = self.builder.get_object("mainArea") vbc.get_parent().remove(vbc) vbc.connect('size-allocate', self.on_vbc_allocated) # Background self.background = SVGWidget(self, os.path.join(self.imagepath, self.IMAGE)) self.background.connect('hover', self.on_background_area_hover) self.background.connect('leave', self.on_background_area_hover, None) self.background.connect('click', self.on_background_area_click) self.main_area.put(self.background, 0, 0) self.main_area.put(vbc, 0, 0) # (self.IMAGE_SIZE[0] / 2) - 90, self.IMAGE_SIZE[1] - 100) # Test markers (those blue circles over PADs and sticks) self.lpadTest = Gtk.Image.new_from_file(os.path.join(self.imagepath, "test-cursor.svg")) self.rpadTest = Gtk.Image.new_from_file(os.path.join(self.imagepath, "test-cursor.svg")) self.stickTest = Gtk.Image.new_from_file(os.path.join(self.imagepath, "test-cursor.svg")) self.main_area.put(self.lpadTest, 40, 40) self.main_area.put(self.rpadTest, 290, 90) self.main_area.put(self.stickTest, 150, 40) # OSD mode (if used) if self.osd_mode: self.builder.get_object("btDaemon").set_sensitive(False) self.window.set_title(_("Edit Profile")) # Headerbar headerbar(self.builder.get_object("hbWindow")) def setup_statusicon(self): menu = self.builder.get_object("mnuTray") self.statusicon = get_status_icon(self.imagepath, menu) self.statusicon.connect('clicked', self.on_statusicon_clicked) if not self.statusicon.is_clickable(): self.builder.get_object("mnuShowWindowTray").set_visible(True) GLib.idle_add(self.statusicon.set, "scc-%s" % (self.status,), _("SC Controller")) def destroy_statusicon(self): self.statusicon.destroy() self.statusicon = None def check(self): """ Performs various (three) checks and reports possible problems """ # TODO: Maybe not best place to do this try: # Dynamic modules rawlist = file("/proc/modules", "r").read().split("\n") kernel_mods = [ line.split(" ")[0] for line in rawlist ] # Built-in modules release = platform.uname()[2] rawlist = file("/lib/modules/%s/modules.builtin" % release, "r").read().split("\n") kernel_mods += [ os.path.split(x)[-1].split(".")[0] for x in rawlist ] except Exception: # Maybe running on BSD or Windows... kernel_mods = [ ] if len(kernel_mods) > 0 and "uinput" not in kernel_mods: # There is no uinput msg = _('uinput kernel module not loaded') msg += "\n\n" + _('Please, consult your distribution manual on how to enable uinput') msg += "\n" + _('or click on "Fix Temporary" button to attempt fix that should work until next restart.') ribar = self.show_error(msg) gksudo = find_gksudo() modprobe = find_binary("modprobe") if gksudo and not hasattr(ribar, "_fix_tmp"): button = Gtk.Button.new_with_label(_("Fix Temporary")) ribar._fix_tmp = button button.connect('clicked', self.apply_temporary_fix, gksudo + [modprobe, "uinput"], _("This will load missing uinput module.") ) ribar.add_button(button, -1) return True elif not os.path.exists("/dev/uinput"): # /dev/uinput missing msg = _('/dev/uinput doesn\'t exists') msg += "\n" + _('uinput kernel module is loaded, but /dev/uinput is missing.') #msg += "\n\n" + _('Please, consult your distribution manual on what in the world could cause this.') msg += "\n\n" + _('Please, consult your distribution manual on how to enable uinput') self.show_error(msg) return True elif not check_access("/dev/uinput"): # Cannot acces uinput msg = _('You don\'t have required access to /dev/uinput.') msg += "\n" + _('This will most likely prevent emulation from working.') msg += "\n\n" + _('Please, consult your distribution manual on how to enable uinput') msg += "\n" + _('or click on "Fix Temporary" button to attempt fix that should work until next restart.') ribar = self.show_error(msg) gksudo = find_gksudo() if gksudo and not hasattr(ribar, "_fix_tmp"): button = Gtk.Button.new_with_label(_("Fix Temporary")) ribar._fix_tmp = button button.connect('clicked', self.apply_temporary_fix, gksudo + ["chmod", "666", "/dev/uinput"], _("This will enable input emulation for <i>every application</i> and <i>all users</i> on this machine.") ) ribar.add_button(button, -1) return True return False def apply_temporary_fix(self, trash, shell_command, message): """ Displays MessageBox with confirmation, tries to run passed shell command and restarts daemon. Doing this allows user to teporary fix some uinput-related problems by his vaim belief I'll not format his harddrive. """ d = Gtk.MessageDialog(parent=self.window, flags = Gtk.DialogFlags.MODAL, type = Gtk.MessageType.WARNING, buttons = Gtk.ButtonsType.OK_CANCEL, message_format = _("sudo fix-my-pc") ) def on_response(dialog, response_id): if response_id == -5: # OK button, not defined anywhere sudo = Gio.Subprocess.new(shell_command, 0) sudo.communicate(None, None) if sudo.get_exit_status() == 0: self.dm.restart() else: d2 = Gtk.MessageDialog(parent=d, flags = Gtk.DialogFlags.MODAL, type = Gtk.MessageType.ERROR, buttons = Gtk.ButtonsType.OK, message_format = _("Command Failed") ) d2.run() d2.destroy() d.destroy() d.connect("response", on_response) d.format_secondary_markup( _("""Following command is going to be executed: <b>%s</b> %s""") % (" ".join(shell_command), message), ) d.show() def hilight(self, button): """ Hilights specified button on background image """ if button: self.hilights[App.HILIGHT_COLOR] = set([button]) else: self.hilights[App.HILIGHT_COLOR] = set() self._update_background() def _update_background(self): h = {} for color in self.hilights: for i in self.hilights[color]: h[i] = color self.background.hilight(h) def hint(self, button): """ As hilight, but marks GTK Button as well """ active = None for b in self.button_widgets.values(): b.widget.set_state(Gtk.StateType.NORMAL) if b.name == button: active = b.widget if active is not None: active.set_state(Gtk.StateType.ACTIVE) self.hilight(button) def show_editor(self, id): action = self.get_action(self.current, id) ae = self.choose_editor(action, "", id) ae.allow_first_page() ae.set_input(id, action) ae.show(self.window) def show_context_menu(self, for_id): """ Sets sensitivity of popup menu items and displays it on screen """ mnuPopup = self.builder.get_object("mnuPopup") mnuCopy = self.builder.get_object("mnuCopy") mnuClear = self.builder.get_object("mnuClear") mnuPaste = self.builder.get_object("mnuPaste") mnuEPress = self.builder.get_object("mnuEditPress") mnuEPressS = self.builder.get_object("mnuEditPressSeparator") self.context_menu_for = for_id clp = Gtk.Clipboard.get_default(Gdk.Display.get_default()) mnuCopy.set_sensitive(bool(self.get_action(self.current, for_id))) mnuClear.set_sensitive(bool(self.get_action(self.current, for_id))) mnuPaste.set_sensitive(clp.wait_is_text_available()) mnuEPress.set_visible(for_id in STICKS + PADS) mnuEPressS.set_visible(mnuEPress.get_visible()) mnuPopup.popup(None, None, None, None, 3, Gtk.get_current_event_time()) def save_config(self): self.config.save() self.dm.reconfigure() self.enable_test_mode() def on_statusicon_clicked(self, *a): """ Handler for user clicking on tray icon button """ self.window.set_visible(not self.window.get_visible()) def on_window_delete_event(self, *a): """ Called when user tries to close window """ if not IS_UNITY and self.config['gui']['enable_status_icon'] and self.config['gui']['minimize_to_status_icon']: # Override closing and hide instead self.window.set_visible(False) else: self.on_mnuExit_activate() return True def on_mnuClear_activate(self, *a): """ Handler for 'Clear' context menu item. Simply sets NoAction to input. """ self.on_action_chosen(self.context_menu_for, NoAction()) def on_mnuCopy_activate(self, *a): """ Handler for 'Copy' context menu item. Converts action to string and sends that string to clipboard. """ a = self.get_action(self.current, self.context_menu_for) if a: if a.name: a = NameModifier(a.name, a) clp = Gtk.Clipboard.get_default(Gdk.Display.get_default()) clp.set_text(a.to_string().encode('utf-8'), -1) clp.store() def on_mnuPaste_activate(self, *a): """ Handler for 'Paste' context menu item. Reads string from clipboard, parses it as action and sets that action on selected input. """ clp = Gtk.Clipboard.get_default(Gdk.Display.get_default()) text = clp.wait_for_text() if text: a = GuiActionParser().restart(text.decode('utf-8')).parse() if not isinstance(a, InvalidAction): self.on_action_chosen(self.context_menu_for, a) def on_mnuEditPress_activate(self, *a): """ Handler for 'Edit Pressed Action' context menu item. """ self.show_editor(getattr(SCButtons, self.context_menu_for)) def on_mnuGlobalSettings_activate(self, *a): from scc.gui.global_settings import GlobalSettings gs = GlobalSettings(self) gs.show(self.window) def on_mnuImport_activate(self, *a): """ Handler for 'Import Steam Profile' context menu item. Displays apropriate dialog. """ from scc.gui.importexport.dialog import Dialog ied = Dialog(self) ied.show(self.window) def on_btUndo_clicked(self, *a): if len(self.undo) < 1: return undo, self.undo = self.undo[-1], self.undo[0:-1] self.set_action(self.current, undo.id, undo.before) self.redo.append(undo) self.builder.get_object("btRedo").set_sensitive(True) if len(self.undo) < 1: self.builder.get_object("btUndo").set_sensitive(False) self.on_profile_modified() def on_btRedo_clicked(self, *a): if len(self.redo) < 1: return redo, self.redo = self.redo[-1], self.redo[0:-1] self.set_action(self.current, redo.id, redo.after) self.undo.append(redo) self.builder.get_object("btUndo").set_sensitive(True) if len(self.redo) < 1: self.builder.get_object("btRedo").set_sensitive(False) self.on_profile_modified() def on_profiles_loaded(self, profiles): for ps in self.profile_switchers: ps.set_profile_list(profiles) def undeletable_dialog(self, dlg, *a): dlg.hide() return True def on_btNewProfile_clicked(self, *a): """ Called when new profile name is set and OK is clicked """ txNewProfile = self.builder.get_object("txNewProfile") rbNewProfile = self.builder.get_object("rbNewProfile") dlg = self.builder.get_object("dlgNewProfile") if rbNewProfile.get_active(): # Creating blank profile is requested self.current.clear() else: self.current.is_template = False self.new_profile(self.current, txNewProfile.get_text()) dlg.hide() def on_rbNewProfile_group_changed(self, *a): """ Called when user clicks 'Copy current profile' button. If profile name was not changed by user before clicking it, it's automatically changed. """ txNewProfile = self.builder.get_object("txNewProfile") rbNewProfile = self.builder.get_object("rbNewProfile") if not txNewProfile._changed: self.recursing = True if rbNewProfile.get_active(): # Create empty profile txNewProfile.set_text(self.generate_new_name()) else: # Copy current profile txNewProfile.set_text(self.generate_copy_name(txNewProfile._name)) self.recursing = False def on_profile_modified(self, update_ui=True): """ Called when selected profile is modified in memory. """ if update_ui: self.profile_switchers[0].set_profile_modified(True, self.current.is_template) if not self.current_file.get_path().endswith(".mod"): mod = self.current_file.get_path() + ".mod" self.current_file = Gio.File.new_for_path(mod) self.save_profile(self.current_file, self.current) def on_profile_loaded(self, profile, giofile): self.current = profile self.current_file = giofile self.recursing = True self.profile_switchers[0].set_profile_modified(False, self.current.is_template) self.builder.get_object("txProfileFilename").set_text(giofile.get_path()) self.builder.get_object("txProfileDescription").get_buffer().set_text(self.current.description) self.builder.get_object("cbProfileIsTemplate").set_active(self.current.is_template) for b in self.button_widgets.values(): b.update() self.recursing = False def on_profile_selected(self, ps, name, giofile): if ps == self.profile_switchers[0]: self.load_profile(giofile) if ps.get_controller(): ps.get_controller().set_profile(giofile.get_path()) def on_unknown_profile(self, ps, name): log.warn("Daemon reported unknown profile: '%s'; Overriding.", name) if self.current_file is not None: ps.get_controller().set_profile(self.current_file.get_path()) def on_save_clicked(self, *a): if self.current_file.get_path().endswith(".mod"): orig = self.current_file.get_path()[0:-4] self.current_file = Gio.File.new_for_path(orig) if self.current.is_template: # Ask user if he is OK with overwriting template d = Gtk.MessageDialog(parent=self.window, flags = Gtk.DialogFlags.MODAL, type = Gtk.MessageType.QUESTION, buttons = Gtk.ButtonsType.YES_NO, message_format = _("You are about to save changes over template.\nAre you sure?") ) NEW_PROFILE_BUTTON = 7 d.add_button(_("Create New Profile"), NEW_PROFILE_BUTTON) r = d.run() d.destroy() if r == NEW_PROFILE_BUTTON: # New profile button clicked ps = self.profile_switchers[0] rbCopyProfile = self.builder.get_object("rbCopyProfile") self.on_new_clicked(ps, ps.get_profile_name()) rbCopyProfile.set_active(True) return if r != -8: # Bail out if user answers anything but yes return self.save_profile(self.current_file, self.current) def on_profile_saved(self, giofile, send=True): """ Called when selected profile is saved to disk """ if self.osd_mode: # Special case, profile shouldn't be changed while in osd_mode return if giofile.get_path().endswith(".mod"): # Special case, this one is saved only to be sent to daemon # and user doesn't need to know about it if self.dm.is_alive(): self.dm.set_profile(giofile.get_path()) return self.profile_switchers[0].set_profile_modified(False, self.current.is_template) if send and self.dm.is_alive() and not self.daemon_changed_profile: self.dm.set_profile(giofile.get_path()) self.current_file = giofile def generate_new_name(self): """ Generates name for new profile. That is 'New Profile X', where X is number that makes name unique. """ i = 1 new_name = _("New Profile %s") % (i,) filename = os.path.join(get_profiles_path(), new_name + ".sccprofile") while os.path.exists(filename): i += 1 new_name = _("New Profile %s") % (i,) filename = os.path.join(get_profiles_path(), new_name + ".sccprofile") return new_name def generate_copy_name(self, name): """ Generates name for profile copy. That is 'New Profile X', where X is number that makes name unique. """ new_name = _("%s (copy)") % (name,) filename = os.path.join(get_profiles_path(), new_name + ".sccprofile") i = 2 while os.path.exists(filename): new_name = _("%s (copy %s)") % (name,) filename = os.path.join(get_profiles_path(), new_name + ".sccprofile") i += 1 return new_name def on_txNewProfile_changed(self, tx): if self.recursing: return tx._changed = True def on_new_clicked(self, ps, name): dlg = self.builder.get_object("dlgNewProfile") txNewProfile = self.builder.get_object("txNewProfile") rbNewProfile = self.builder.get_object("rbNewProfile") self.recursing = True rbNewProfile.set_active(True) txNewProfile.set_text(self.generate_new_name()) txNewProfile._name = name txNewProfile._changed = False self.recursing = False dlg.set_transient_for(self.window) dlg.show() def on_action_chosen(self, id, action, mark_changed=True): before = self.set_action(self.current, id, action) if mark_changed: if before.to_string() != action.to_string(): # TODO: Maybe better comparison self.undo.append(UndoRedo(id, before, action)) self.builder.get_object("btUndo").set_sensitive(True) self.on_profile_modified() else: self.on_profile_modified(update_ui=False) return before def on_background_area_hover(self, trash, area): self.hint(area) def on_background_area_click(self, trash, area): if area in [ x.name for x in BUTTONS ]: self.hint(None) self.show_editor(getattr(SCButtons, area)) elif area in TRIGGERS + STICKS + PADS: self.hint(None) self.show_editor(area) def on_vbc_allocated(self, vbc, allocation): """ Called when size of 'Button C' is changed. Centers button on background image """ main_area = self.builder.get_object("mainArea") x = (main_area.get_allocation().width - allocation.width) / 2 y = main_area.get_allocation().height - allocation.height main_area.move(vbc, x, y) def on_ebImage_motion_notify_event(self, box, event): self.background.on_mouse_moved(event.x, event.y) def on_exiting_n_daemon_killed(self, *a): self.quit() def on_mnuExit_activate(self, *a): if self.app.config['gui']['autokill_daemon']: log.debug("Terminating scc-daemon") for x in ("content", "mnuEmulationEnabled", "mnuEmulationEnabledTray"): w = self.builder.get_object(x) w.set_sensitive(False) self.set_daemon_status("unknown", False) self.hide_error() if self.dm.is_alive(): self.dm.connect("dead", self.on_exiting_n_daemon_killed) self.dm.connect("error", self.on_exiting_n_daemon_killed) self.dm.stop() else: # Daemon appears to be dead, kill it just in case self.dm.stop() self.quit() else: self.quit() def on_mnuAbout_activate(self, *a): from scc.gui.aboutdialog import AboutDialog AboutDialog(self).show(self.window) def on_daemon_alive(self, *a): self.set_daemon_status("alive", True) if not self.release_notes_visible(): self.hide_error() self.just_started = False if self.osd_mode: self.enable_osd_mode() elif self.profile_switchers[0].get_file() is not None and not self.just_started: self.dm.set_profile(self.current_file.get_path()) GLib.timeout_add_seconds(1, self.check) self.enable_test_mode() def on_daemon_ccunt_changed(self, daemon, count): if (self.controller_count, count) == (0, 1): # First controller connected # # 'event' signal should be connected only on first controller, # so this block is executed only when number of connected # controllers changes from 0 to 1 c = self.dm.get_controllers()[0] c.connect('event', self.on_daemon_event_observer) elif count > self.controller_count: # Controller added while len(self.profile_switchers) < count: s = self.add_switcher() elif count < self.controller_count: # Controller removed while len(self.profile_switchers) > max(1, count): s = self.profile_switchers.pop() s.set_controller(None) self.remove_switcher(s) # Assign controllers to widgets for i in xrange(0, count): c = self.dm.get_controllers()[i] self.profile_switchers[i].set_controller(c) if count < 1: # Special case, no controllers are connected, but one widget # has to stay on screen self.profile_switchers[0].set_controller(None) self.controller_count = count def new_profile(self, profile, name): filename = os.path.join(get_profiles_path(), name + ".sccprofile") self.current_file = Gio.File.new_for_path(filename) self.save_profile(self.current_file, profile) def add_switcher(self, margin_left=30, margin_right=40, margin_bottom=2): """ Adds new profile switcher widgets on top of window. Called when new controller is connected to daemon. Returns generated ProfileSwitcher instance. """ vbAllProfiles = self.builder.get_object("vbAllProfiles") ps = ProfileSwitcher(self.imagepath, self.config) ps.set_margin_left(margin_left) ps.set_margin_right(margin_right) ps.set_margin_bottom(margin_bottom) ps.connect('right-clicked', self.on_profile_right_clicked) vbAllProfiles.pack_start(ps, False, False, 0) vbAllProfiles.reorder_child(ps, 0) vbAllProfiles.show_all() if self.osd_mode: ps.set_allow_switch(False) if len(self.profile_switchers) > 0: ps.set_profile_list(self.profile_switchers[0].get_profile_list()) self.profile_switchers.append(ps) ps.connect('changed', self.on_profile_selected) ps.connect('unknown-profile', self.on_unknown_profile) return ps def remove_switcher(self, s): """ Removes given profile switcher from UI. """ vbAllProfiles = self.builder.get_object("vbAllProfiles") vbAllProfiles.remove(s) s.destroy() def enable_test_mode(self): """ Disables and re-enables Input Test mode. If sniffing is disabled in daemon configuration, 2nd call fails and logs error. """ if self.dm.is_alive() and not self.osd_mode: try: c = self.dm.get_controllers()[0] except IndexError: # Zero controllers return c.unlock_all() c.observe(DaemonManager.nocallback, self.on_observe_failed, 'A', 'B', 'C', 'X', 'Y', 'START', 'BACK', 'LB', 'RB', 'LPAD', 'RPAD', 'LGRIP', 'RGRIP', 'LT', 'RT', 'LEFT', 'RIGHT', 'STICK', 'STICKPRESS') def enable_osd_mode(self): # TODO: Support for multiple controllers here self.osd_mode_controller = 0 osd_mode_profile = Profile(GuiActionParser()) osd_mode_profile.load(find_profile(App.OSD_MODE_PROF_NAME)) try: c = self.dm.get_controllers()[self.osd_mode_controller] except IndexError: log.error("osd_mode: Controller not connected") self.quit() return def on_lock_failed(*a): log.error("osd_mode: Locking failed") self.quit() def on_lock_success(*a): log.debug("osd_mode: Locked everything") from scc.gui.osd_mode_mapper import OSDModeMapper self.osd_mode_mapper = OSDModeMapper(osd_mode_profile) self.osd_mode_mapper.set_target_window(self.window.get_window()) GLib.timeout_add(10, self.osd_mode_mapper.run_scheduled) # Locks everything but pads. Pads are emulating mouse and this is # better left in daemon - involving socket in mouse controls # adds too much lags. c.lock(on_lock_success, on_lock_failed, 'A', 'B', 'X', 'Y', 'START', 'BACK', 'LB', 'RB', 'C', 'LPAD', 'RPAD', 'STICK', 'LGRIP', 'RGRIP', 'LT', 'RT', 'STICKPRESS') # Ask daemon to temporaly reconfigure pads for mouse emulation c.replace(DaemonManager.nocallback, on_lock_failed, LEFT, osd_mode_profile.pads[LEFT]) c.replace(DaemonManager.nocallback, on_lock_failed, RIGHT, osd_mode_profile.pads[RIGHT]) def on_observe_failed(self, error): log.debug("Failed to enable test mode: %s", error) def on_daemon_version(self, daemon, version): """ Checks if reported version matches expected one. If not, daemon is restarted. """ if version != DAEMON_VERSION and self.outdated_version != version: log.warning( "Running daemon instance is too old (version %s, expected %s). Restarting...", version, DAEMON_VERSION) self.outdated_version = version self.set_daemon_status("unknown", False) self.dm.restart() else: # At this point, correct daemon version of daemon is running # and we can check if there is anything new to inform user about if self.app.config['gui']['news']['last_version'] != App.get_release(): if self.app.config['gui']['news']['enabled']: if not self.osd_mode: self.check_release_notes() def on_daemon_error(self, daemon, error): log.debug("Daemon reported error '%s'", error) msg = _('There was an error with enabling emulation: <b>%s</b>') % (error,) # Known errors are handled with aditional message if "Device not found" in error: msg += "\n" + _("Please, check if you have reciever dongle connected to USB port.") elif "LIBUSB_ERROR_ACCESS" in error: msg += "\n" + _("You don't have access to controller device.") msg += "\n\n" + ( _("Consult your distribution manual, try installing Steam package or <a href='%s'>install required udev rules manually</a>.") % 'https://wiki.archlinux.org/index.php/Gamepad#Steam_Controller_Not_Pairing' ) # TODO: Write howto somewhere instead of linking to ArchWiki elif "LIBUSB_ERROR_BUSY" in error: msg += "\n" + _("Another application (most likely Steam) is using the controller.") elif "LIBUSB_ERROR_PIPE" in error: msg += "\n" + _("USB dongle was removed.") elif "Failed to create uinput device." in error: # Call check() method and try to determine what went wrong. if self.check(): # Check() returns True if error was "handled". return # If check() fails to find error reason, error message is displayed as it is if self.osd_mode: self.quit() self.show_error(msg) self.set_daemon_status("error", True) def on_daemon_event_observer(self, daemon, what, data): if self.osd_mode_mapper: self.osd_mode_mapper.handle_event(daemon, what, data) elif what in (LEFT, RIGHT, STICK): widget, area = { LEFT : (self.lpadTest, "LPADTEST"), RIGHT : (self.rpadTest, "RPADTEST"), STICK : (self.stickTest, "STICKTEST"), }[what] # Check if stick or pad is released if data[0] == data[1] == 0: widget.hide() return if not widget.is_visible(): widget.show() # Grab values ax, ay, aw, trash = self.background.get_area_position(area) cw = widget.get_allocation().width # Compute center x, y = ax + aw * 0.5 - cw * 0.5, ay + 1.0 - cw * 0.5 # Add pad position x += data[0] * aw / STICK_PAD_MAX * 0.5 y -= data[1] * aw / STICK_PAD_MAX * 0.5 # Move circle self.main_area.move(widget, x, y) elif what in ("LT", "RT", "STICKPRESS"): what = { "LT" : "LEFT", "RT" : "RIGHT", "STICKPRESS" : "STICK" }[what] if data[0]: self.hilights[App.OBSERVE_COLOR].add(what) else: self.hilights[App.OBSERVE_COLOR].remove(what) self._update_background() elif hasattr(SCButtons, what): try: if data[0]: self.hilights[App.OBSERVE_COLOR].add(what) else: self.hilights[App.OBSERVE_COLOR].remove(what) self._update_background() except KeyError, e: # Non fatal pass else:
def run(self): self.daemon = DaemonManager() self.daemon.connect('dead', self.on_daemon_died) self.daemon.connect('alive', self.on_daemon_connected) self.daemon.connect('unknown-msg', self.on_unknown_message) self.mainloop.run()
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 OSDDaemon(object): def __init__(self): self.exit_code = -1 self.mainloop = GLib.MainLoop() self.config = None # hash_of_colors is used to determine if css needs to be reapplied # after configuration change self._hash_of_colors = -1 self._window = None self._registered = False self._last_profile_change = 0 self._recent_profiles_undo = None def quit(self, code=-1): self.exit_code = code self.mainloop.quit() def get_exit_code(self): return self.exit_code def on_daemon_reconfigured(self, *a): log.debug("Reloading config...") self.config.reload() self._check_colorconfig_change() def on_profile_changed(self, daemon, profile): name = os.path.split(profile)[-1] if name.endswith(".sccprofile") and not name.startswith("."): # Ignore .mod and hidden files name = name[0:-11] recents = self.config['recent_profiles'] if len(recents) and recents[0] == name: # Already first in recent list return if time.time() - self._last_profile_change < 2.0: # Profiles are changing too fast, probably because user # is using scroll wheel over profile combobox if self._recent_profiles_undo: recents = [] + self._recent_profiles_undo self._last_profile_change = time.time() self._recent_profiles_undo = [] + recents while name in recents: recents.remove(name) recents.insert(0, name) if len(recents) > self.config['recent_max']: recents = recents[0:self.config['recent_max']] self.config['recent_profiles'] = recents self.config.save() log.debug("Updated recent profile list") def on_daemon_died(self, *a): log.error("Connection to daemon lost") self.quit(2) def on_daemon_connected(self, *a): def success(*a): log.info("Sucessfully registered as scc-osd-daemon") self._registered = True def failure(why): log.error("Failed to registered as scc-osd-daemon: %s", why) self.quit(1) if not self._registered: self.daemon.request('Register: osd', success, failure) def on_menu_closed(self, m): """ Called after OSD menu is hidden from screen """ self._window = None if m.get_exit_code() == 0: # 0 means that user selected item and confirmed selection self.daemon.request( 'Selected: %s' % (shjoin( [m.get_menuid(), m.get_selected_item_id()])), lambda *a: False, lambda *a: False) def on_keyboard_closed(self, *a): """ Called after on-screen keyboard is hidden from the screen """ self._window = None def on_gesture_recognized(self, gd): """ Called after on-screen keyboard is hidden from the screen """ self._window = None if gd.get_exit_code() == 0: self.daemon.request('Gestured: %s' % (gd.get_gesture(), ), lambda *a: False, lambda *a: False) else: self.daemon.request('Gestured: x', lambda *a: False, lambda *a: False) @staticmethod def _is_menu_message(m): """ Returns True if m starts with 'OSD: [grid|radial]menu' or "OSD: dialog" """ return (m.startswith("OSD: menu") or m.startswith("OSD: radialmenu") or m.startswith("OSD: quickmenu") or m.startswith("OSD: gridmenu") or m.startswith("OSD: dialog") or m.startswith("OSD: hmenu")) def on_unknown_message(self, daemon, message): if not message.startswith("OSD:"): return if message.startswith("OSD: message"): args = shsplit(message)[1:] m = Message() m.parse_argumets(args) m.show() elif message.startswith("OSD: keyboard"): if self._window: log.warning( "Another OSD is already visible - refusing to show keyboard" ) else: args = shsplit(message)[1:] self._window = Keyboard(self.config) self._window.connect('destroy', self.on_keyboard_closed) self._window.parse_argumets(args) self._window.show() self._window.use_daemon(self.daemon) elif message.startswith("OSD: gesture"): if self._window: log.warning( "Another OSD is already visible - refusing to show keyboard" ) else: args = shsplit(message)[1:] self._window = GestureDisplay(self.config) self._window.parse_argumets(args) self._window.use_daemon(self.daemon) self._window.show() self._window.connect('destroy', self.on_gesture_recognized) elif self._is_menu_message(message): args = shsplit(message)[1:] if self._window: log.warning( "Another OSD is already visible - refusing to show menu") else: if message.startswith("OSD: hmenu"): self._window = HorizontalMenu() elif message.startswith("OSD: radialmenu"): self._window = RadialMenu() elif message.startswith("OSD: quickmenu"): self._window = QuickMenu() elif message.startswith("OSD: gridmenu"): self._window = GridMenu() elif message.startswith("OSD: dialog"): self._window = Dialog() else: self._window = Menu() self._window.connect('destroy', self.on_menu_closed) self._window.use_config(self.config) if self._window.parse_argumets(args): self._window.show() self._window.use_daemon(self.daemon) else: log.error("Failed to show menu") self._window = None elif message.startswith("OSD: area"): args = shsplit(message)[1:] if self._window: log.warning( "Another OSD is already visible - refusing to show area") else: args = shsplit(message)[1:] self._window = Area() self._window.connect('destroy', self.on_keyboard_closed) if self._window.parse_argumets(args): self._window.show() else: self._window.quit() self._window = None elif message.startswith("OSD: clear"): # Clears active OSD window (if any) if self._window: self._window.quit() self._window = None else: log.warning("Unknown command from daemon: '%s'", message) def _check_colorconfig_change(self): """ Checks if OSD color configuration is changed and re-applies CSS if needed. """ h = sum([ hash(self.config['osd_colors'][x]) for x in self.config['osd_colors'] ]) h += sum([ hash(self.config['osk_colors'][x]) for x in self.config['osk_colors'] ]) if self._hash_of_colors != h: self._hash_of_colors = h OSDWindow._apply_css(self.config) if self._window and isinstance(self._window, Keyboard): self._window.recolor() self._window.update_labels() self._window.redraw_background() def run(self): on_wayland = "WAYLAND_DISPLAY" in os.environ or not isinstance( Gdk.Display.get_default(), GdkX11.X11Display) if on_wayland: log.error("Cannot run on Wayland") self.exit_code = 8 return self.daemon = DaemonManager() self.config = Config() self._check_colorconfig_change() self.daemon.connect('alive', self.on_daemon_connected) self.daemon.connect('dead', self.on_daemon_died) self.daemon.connect('profile-changed', self.on_profile_changed) self.daemon.connect('reconfigured', self.on_daemon_reconfigured) self.daemon.connect('unknown-msg', self.on_unknown_message) self.mainloop.run()
class OSDDaemon(object): def __init__(self): self.exit_code = -1 self.mainloop = GLib.MainLoop() self.config = None # hash_of_colors is used to determine if css needs to be reapplied # after configuration change self._hash_of_colors = -1 self._visible_messages = {} self._window = None self._registered = False self._last_profile_change = 0 self._recent_profiles_undo = None def quit(self, code=-1): self.exit_code = code self.mainloop.quit() def get_exit_code(self): return self.exit_code def on_daemon_reconfigured(self, *a): log.debug("Reloading config...") self.config.reload() self._check_colorconfig_change() def on_profile_changed(self, daemon, profile): name = os.path.split(profile)[-1] if name.endswith(".sccprofile") and not name.startswith("."): # Ignore .mod and hidden files name = name[0:-11] recents = self.config['recent_profiles'] if len(recents) and recents[0] == name: # Already first in recent list return if time.time() - self._last_profile_change < 2.0: # Profiles are changing too fast, probably because user # is using scroll wheel over profile combobox if self._recent_profiles_undo: recents = [] + self._recent_profiles_undo self._last_profile_change = time.time() self._recent_profiles_undo = [] + recents while name in recents: recents.remove(name) recents.insert(0, name) if len(recents) > self.config['recent_max']: recents = recents[0:self.config['recent_max']] self.config['recent_profiles'] = recents self.config.save() log.debug("Updated recent profile list") self.clear_messages() def on_daemon_died(self, *a): log.error("Connection to daemon lost") self.quit(2) def on_daemon_connected(self, *a): def success(*a): log.info("Sucessfully registered as scc-osd-daemon") self._registered = True def failure(why): log.error("Failed to registered as scc-osd-daemon: %s", why) self.quit(1) if not self._registered: self.daemon.request('Register: osd', success, failure) def on_menu_closed(self, m): """ Called after OSD menu is hidden from screen """ self._window = None if m.get_exit_code() == 0: # 0 means that user selected item and confirmed selection self.daemon.request( 'Selected: %s' % ( shjoin([ m.get_menuid(), m.get_selected_item_id() ])), lambda *a : False, lambda *a : False) def on_message_closed(self, m): hsh = m.hash() if hsh in self._visible_messages: del self._visible_messages[hsh] def on_keyboard_closed(self, *a): """ Called after on-screen keyboard is hidden from the screen """ self._window = None def on_gesture_recognized(self, gd): """ Called after on-screen keyboard is hidden from the screen """ self._window = None if gd.get_exit_code() == 0: self.daemon.request('Gestured: %s' % ( gd.get_gesture(), ), lambda *a : False, lambda *a : False) else: self.daemon.request('Gestured: x', lambda *a : False, lambda *a : False) @staticmethod def _is_menu_message(m): """ Returns True if m starts with 'OSD: [grid|radial]menu' or "OSD: dialog" """ return ( m.startswith("OSD: menu") or m.startswith("OSD: radialmenu") or m.startswith("OSD: quickmenu") or m.startswith("OSD: gridmenu") or m.startswith("OSD: dialog") or m.startswith("OSD: hmenu") ) def on_unknown_message(self, daemon, message): if not message.startswith("OSD:"): return if message.startswith("OSD: message"): args = shsplit(message)[1:] m = Message() m.parse_argumets(args) hsh = m.hash() if hsh in self._visible_messages: self._visible_messages[hsh].extend() m.destroy() else: # TODO: Do this only for default position once changing # TODO: is allowed if len(self._visible_messages): height = self._visible_messages.values()[0].get_size().height x, y = m.position while y in [ i.position[1] for i in self._visible_messages.values() ]: y -= height + 5 m.position = x, y m.show() self._visible_messages[hsh] = m m.connect("destroy", self.on_message_closed) elif message.startswith("OSD: keyboard"): if self._window: log.warning("Another OSD is already visible - refusing to show keyboard") else: args = shsplit(message)[1:] self._window = Keyboard(self.config) self._window.connect('destroy', self.on_keyboard_closed) self._window.parse_argumets(args) self._window.show() self._window.use_daemon(self.daemon) elif message.startswith("OSD: gesture"): if self._window: log.warning("Another OSD is already visible - refusing to show keyboard") else: args = shsplit(message)[1:] self._window = GestureDisplay(self.config) self._window.parse_argumets(args) self._window.use_daemon(self.daemon) self._window.show() self._window.connect('destroy', self.on_gesture_recognized) elif self._is_menu_message(message): args = shsplit(message)[1:] if self._window: log.warning("Another OSD is already visible - refusing to show menu") else: if message.startswith("OSD: hmenu"): self._window = HorizontalMenu() elif message.startswith("OSD: radialmenu"): self._window = RadialMenu() elif message.startswith("OSD: quickmenu"): self._window = QuickMenu() elif message.startswith("OSD: gridmenu"): self._window = GridMenu() elif message.startswith("OSD: dialog"): self._window = Dialog() else: self._window = Menu() self._window.connect('destroy', self.on_menu_closed) self._window.use_config(self.config) try: if self._window.parse_argumets(args): self._window.show() self._window.use_daemon(self.daemon) else: log.error("Failed to show menu") self._window = None except: log.error(traceback.format_exc()) log.error("Failed to show menu") self._window = None elif message.startswith("OSD: area"): args = shsplit(message)[1:] if self._window: log.warning("Another OSD is already visible - refusing to show area") else: args = shsplit(message)[1:] self._window = Area() self._window.connect('destroy', self.on_keyboard_closed) if self._window.parse_argumets(args): self._window.show() else: self._window.quit() self._window = None elif message.startswith("OSD: clear"): # Clears active OSD windows self.clear_windows() else: log.warning("Unknown command from daemon: '%s'", message) def clear_windows(self): if self._window: self._window.quit() self._window = None self.clear_messages(only_long_lasting=False) def clear_messages(self, only_long_lasting=True): """ Clears all OSD messages from screen. If only_long_lasting is True, which is default behaviour on profile change, only messages set to last more than 10s are hidden. """ to_destroy = [] + self._visible_messages.values() for m in to_destroy: if not only_long_lasting or m.timeout <= 0 or m.timeout > OSDAction.DEFAULT_TIMEOUT * 2: m.destroy() def _check_colorconfig_change(self): """ Checks if OSD color configuration is changed and re-applies CSS if needed. """ h = sum([ hash(self.config['osd_colors'][x]) for x in self.config['osd_colors'] ]) h += sum([ hash(self.config['osk_colors'][x]) for x in self.config['osk_colors'] ]) h += hash(self.config['osd_style']) if self._hash_of_colors != h: self._hash_of_colors = h OSDWindow._apply_css(self.config) if self._window and isinstance(self._window, Keyboard): self._window.recolor() self._window.update_labels() self._window.redraw_background() def run(self): on_wayland = "WAYLAND_DISPLAY" in os.environ or not isinstance(Gdk.Display.get_default(), GdkX11.X11Display) if on_wayland: log.error("Cannot run on Wayland") self.exit_code = 8 return self.daemon = DaemonManager() self.config = Config() self._check_colorconfig_change() self.daemon.connect('alive', self.on_daemon_connected) self.daemon.connect('dead', self.on_daemon_died) self.daemon.connect('profile-changed', self.on_profile_changed) self.daemon.connect('reconfigured', self.on_daemon_reconfigured) self.daemon.connect('unknown-msg', self.on_unknown_message) self.mainloop.run()
def run(self): self.daemon = DaemonManager() self._cononect_handlers() OSDWindow.run(self)
class OSDDaemon(object): def __init__(self): self.exit_code = -1 self.mainloop = GLib.MainLoop() self.config = None # hash_of_colors is used to determine if css needs to be reapplied # after configuration change self._hash_of_colors = -1 self._window = None self._registered = False self._last_profile_change = 0 self._recent_profiles_undo = None def quit(self, code=-1): self.exit_code = code self.mainloop.quit() def get_exit_code(self): return self.exit_code def on_daemon_reconfigured(self, *a): log.debug("Reloading config...") self.config.reload() self._check_colorconfig_change() def on_profile_changed(self, daemon, profile): name = os.path.split(profile)[-1] if name.endswith(".sccprofile") and not name.startswith("."): # Ignore .mod and hidden files name = name[0:-11] recents = self.config['recent_profiles'] if len(recents) and recents[0] == name: # Already first in recent list return if time.time() - self._last_profile_change < 2.0: # Profiles are changing too fast, probably because user # is using scroll wheel over profile combobox if self._recent_profiles_undo: recents = [] + self._recent_profiles_undo self._last_profile_change = time.time() self._recent_profiles_undo = [] + recents while name in recents: recents.remove(name) recents.insert(0, name) if len(recents) > self.config['recent_max']: recents = recents[0:self.config['recent_max']] self.config['recent_profiles'] = recents self.config.save() log.debug("Updated recent profile list") def on_daemon_died(self, *a): log.error("Connection to daemon lost") self.quit(2) def on_daemon_connected(self, *a): def success(*a): log.info("Sucessfully registered as scc-osd-daemon") self._registered = True def failure(why): log.error("Failed to registered as scc-osd-daemon: %s", why) self.quit(1) if not self._registered: self.daemon.request('Register: osd', success, failure) def on_menu_closed(self, m): """ Called after OSD menu is hidden from screen """ self._window = None if m.get_exit_code() == 0: # 0 means that user selected item and confirmed selection self.daemon.request( 'Selected: %s' % ( shjoin([ m.get_menuid(), m.get_selected_item_id() ])), lambda *a : False, lambda *a : False) def on_keyboard_closed(self, *a): """ Called after on-screen keyboard is hidden from the screen """ self._window = None @staticmethod def _is_menu_message(m): """ Returns True if m starts with 'OSD: [grid|radial]menu' """ return ( m.startswith("OSD: menu") or m.startswith("OSD: gridmenu") or m.startswith("OSD: radialmenu") ) def on_unknown_message(self, daemon, message): if not message.startswith("OSD:"): return if message.startswith("OSD: message"): args = shsplit(message)[1:] m = Message() m.parse_argumets(args) m.show() elif message.startswith("OSD: keyboard"): if self._window: log.warning("Another OSD is already visible - refusing to show keyboard") else: args = shsplit(message)[1:] self._window = Keyboard(self.config) self._window.connect('destroy', self.on_keyboard_closed) self._window.parse_argumets(args) self._window.show() self._window.use_daemon(self.daemon) elif self._is_menu_message(message): args = shsplit(message)[1:] if self._window: log.warning("Another OSD is already visible - refusing to show menu") else: if message.startswith("OSD: gridmenu"): self._window = GridMenu() elif message.startswith("OSD: radialmenu"): self._window = RadialMenu() else: self._window = Menu() self._window.connect('destroy', self.on_menu_closed) self._window.use_config(self.config) if self._window.parse_argumets(args): self._window.show() self._window.use_daemon(self.daemon) else: log.error("Failed to show menu") self._window = None elif message.startswith("OSD: area"): args = shsplit(message)[1:] if self._window: log.warning("Another OSD is already visible - refusing to show area") else: args = shsplit(message)[1:] self._window = Area() self._window.connect('destroy', self.on_keyboard_closed) if self._window.parse_argumets(args): self._window.show() else: self._window.quit() self._window = None elif message.startswith("OSD: clear"): # Clears active OSD window (if any) if self._window: self._window.quit() self._window = None else: log.warning("Unknown command from daemon: '%s'", message) def _check_colorconfig_change(self): """ Checks if OSD color configuration is changed and re-applies CSS if needed. """ h = sum([ hash(self.config['osd_colors'][x]) for x in self.config['osd_colors'] ]) h += sum([ hash(self.config['osk_colors'][x]) for x in self.config['osk_colors'] ]) if self._hash_of_colors != h: self._hash_of_colors = h OSDWindow._apply_css(self.config) if self._window and isinstance(self._window, Keyboard): self._window.recolor() self._window.update_labels() self._window.redraw_background() def run(self): self.daemon = DaemonManager() self.config = Config() self._check_colorconfig_change() self.daemon.connect('alive', self.on_daemon_connected) self.daemon.connect('dead', self.on_daemon_died) self.daemon.connect('profile-changed', self.on_profile_changed) self.daemon.connect('reconfigured', self.on_daemon_reconfigured) self.daemon.connect('unknown-msg', self.on_unknown_message) self.mainloop.run()
def run(self): self.daemon = DaemonManager() self._connect_handlers() OSDWindow.run(self)
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