def __init__(self, name = "Unnamed"): Profile.__init__(self, ActionParser()) self.name = name self.next_menu_id = 1 self.action_set_id = 0 self.action_sets = { 'default' : self } self.action_set_switches = set()
def load_osk(self): cbStickAction = self.builder.get_object("cbStickAction") cbTriggersAction = self.builder.get_object("cbTriggersAction") profile = Profile(GuiActionParser()) profile.load(find_profile(OSDKeyboard.OSK_PROF_NAME)) self._recursing = True # Load triggers triggers = "%s|%s" % ( profile.triggers[LEFT].to_string(), profile.triggers[RIGHT].to_string() ) if not self.set_cb(cbTriggersAction, triggers, keyindex=1): self.add_custom(cbTriggersAction, triggers) # Load stick if not self.set_cb(cbStickAction, profile.stick.to_string(), keyindex=1): self.add_custom(cbStickAction, profile.stick.to_string()) # Load sensitivity s = profile.pads[LEFT].compress().speed self.builder.get_object("sclSensX").set_value(s[0]) self.builder.get_object("sclSensY").set_value(s[1]) self._recursing = False
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 __init__(self, name="Unnamed"): Profile.__init__(self, ActionParser()) self.name = name self.next_menu_id = 1 self.action_set_id = 0 self.action_sets = {'default': self} self.action_set_switches = set()
def _load_osk_profile(self): """ Loads and returns on-screen keyboard profile. Used by methods that are changing it. """ profile = Profile(GuiActionParser()) profile.load(find_profile(OSDKeyboard.OSK_PROF_NAME)) return profile
def __init__(self, app): BindingEditor.__init__(self, app) self.app = app self.gladepath = app.gladepath self.imagepath = app.imagepath self.current = Profile(GuiActionParser()) self.current.load(find_profile(OSDKeyboard.OSK_PROF_NAME)) self.setup_widgets()
def load_profile(self, giofile): """ Loads profile from 'giofile' into 'profile' object Calls on_profiles_loaded when done """ # This may get asynchronous later, but that load runs under 1ms... profile = Profile(GuiActionParser()) profile.load(giofile.get_path()) self.on_profile_loaded(profile, giofile)
def export_profile(tar, filename): profile = Profile(TalkingActionParser()) try: out = tempfile.NamedTemporaryFile() profile.load(filename) profile.save(out.name) tar.add(out.name, arcname=os.path.split(filename)[-1], recursive=False) except Exception, e: # Profile that cannot be parsed shouldn't be exported log.error(e) return False
def _export(self, giofile, target_filename): """ Performs actual exporting. This method is used when only profile with no referenced files is to be exported and works pretty simple - load, parse, save in new file. """ profile = Profile(TalkingActionParser()) try: profile.load(giofile.get_path()) except Exception, e: # Profile that cannot be parsed shouldn't be exported log.error(e) return False
class OSKBindingEditor(Editor, BindingEditor): GLADE = "osk_binding_editor.glade" def __init__(self, app): BindingEditor.__init__(self, app) self.app = app self.gladepath = app.gladepath self.imagepath = app.imagepath self.current = Profile(GuiActionParser()) self.current.load(find_profile(OSDKeyboard.OSK_PROF_NAME)) self.setup_widgets() def setup_widgets(self): Editor.setup_widgets(self) self.create_binding_buttons(use_icons=False, enable_press=False) def show_editor(self, id): if id in STICKS: ae = self.choose_editor(self.current.stick, _("Stick")) ae.set_input(STICK, self.current.stick, mode=Action.AC_OSK) ae.show(self.window) elif id in SCButtons: title = _("%s Button") % (id.name,) ae = self.choose_editor(self.current.buttons[id], title) ae.set_input(id, self.current.buttons[id], mode=Action.AC_OSK) ae.show(self.window) elif id in TRIGGERS: ae = self.choose_editor(self.current.triggers[id], _("%s Trigger") % (id,)) ae.set_input(id, self.current.triggers[id], mode=Action.AC_OSK) ae.show(self.window) def on_action_chosen(self, id, action): self.set_action(self.current, id, action) self.save_profile() def save_profile(self, *a): """ Saves osk profile from 'profile' object into 'giofile'. Calls on_profile_saved when done """ self.current.save(os.path.join(get_profiles_path(), OSDKeyboard.OSK_PROF_NAME + ".sccprofile")) # OSK reloads profile when daemon reports configuration change self.app.dm.reconfigure()
class OSKBindingEditor(Editor, BindingEditor): GLADE = "osk_binding_editor.glade" def __init__(self, app): BindingEditor.__init__(self, app) self.app = app self.gladepath = app.gladepath self.imagepath = app.imagepath self.current = Profile(GuiActionParser()) self.current.load(find_profile(OSDKeyboard.OSK_PROF_NAME)) self.setup_widgets() def setup_widgets(self): Editor.setup_widgets(self) self.create_binding_buttons(use_icons=False, enable_press=False) def show_editor(self, id): if id in STICKS: ae = self.choose_editor(self.current.stick, _("Stick")) ae.set_input(STICK, self.current.stick, mode=Action.AC_OSK) ae.show(self.window) elif id in SCButtons: title = _("%s Button") % (id.name,) ae = self.choose_editor(self.current.buttons[id], title) ae.set_input(id, self.current.buttons[id], mode=Action.AC_OSK) ae.show(self.window) elif id in TRIGGERS: ae = self.choose_editor(self.current.triggers[id], _("%s Trigger") % (id,)) ae.set_input(id, self.current.triggers[id], mode=Action.AC_OSK) ae.show(self.window) def on_action_chosen(self, id, action, mark_changed=True): self.set_action(self.current, id, action) self.save_profile() def save_profile(self, *a): """ Saves osk profile from 'profile' object into 'giofile'. Calls on_profile_saved when done """ self.current.save(os.path.join(get_profiles_path(), OSDKeyboard.OSK_PROF_NAME + ".sccprofile")) # OSK reloads profile when daemon reports configuration change self.app.dm.reconfigure()
def _add_refereced_profile(self, model, giofile, used): """ Loads profile file and recursively adds all profiles and menus referenced by it into 'package' list. Returns True on success or False if something cannot be parsed. """ # Load & parse selected profile and check every action in it profile = Profile(ActionParser()) try: profile.load(giofile.get_path()) except Exception, e: # Profile that cannot be parsed shouldn't be exported log.error(e) return False
def import_scc(self, filename): """ Imports simple, single-file scc-profile. Just loads it, checks for shell() actions and asks user to enter name. """ files = self.builder.get_object("lstImportPackage") # Load profile profile = Profile(GuiActionParser()) try: profile.load(filename) except Exception, e: # Profile cannot be parsed. Display error message and let user to quit # Error message reuses page from VDF import, because they are # basically the same log.error(e) self.error(str(e)) return
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 __init__(self, config=None): self.kbimage = os.path.join(get_config_path(), 'keyboard.svg') if not os.path.exists(self.kbimage): # Prefer image in ~/.config/scc, but load default one as fallback self.kbimage = os.path.join(get_share_path(), "images", 'keyboard.svg') TimerManager.__init__(self) OSDWindow.__init__(self, "osd-keyboard") self.daemon = None self.mapper = None self.keymap = Gdk.Keymap.get_default() self.keymap.connect('state-changed', self.on_keymap_state_changed) Action.register_all(sys.modules['scc.osd.osk_actions'], prefix="OSK") self.profile = Profile(TalkingActionParser()) self.config = config or Config() self.dpy = X.Display(hash(GdkX11.x11_get_default_xdisplay())) self.group = None self.limits = {} self.background = None 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.cursors[CPAD] = Gtk.Image.new_from_file(cursor) self.cursors[CPAD].set_name("osd-keyboard-cursor") self._eh_ids = [] self._controller = None 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._pressed_areas = {} self.c = Gtk.Box() self.c.set_name("osd-keyboard-container") self.f = Gtk.Fixed()
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 import_scc_tar(self, filename): """ Imports packaged profiles. Checks for shell() actions everywhere and ask user to enter main name, check generated ones and optionaly change them as he wish. """ files = self.builder.get_object("lstImportPackage") try: # Open tar tar = tarfile.open(filename, "r:gz") files.clear() # Grab 1st profile name = tar.extractfile(Export.PN_NAME).read() main_profile = "%s.sccprofile" % name parser = GuiActionParser() o = GObject.GObject() o.obj = Profile(parser).load_fileobj(tar.extractfile(main_profile)) files.append((2, name, name, _("(profile)"), o)) for x in tar: name = ".".join(x.name.split(".")[0:-1]) if x.name.endswith(".sccprofile") and x.name != main_profile: o = GObject.GObject() o.obj = Profile(parser).load_fileobj(tar.extractfile(x)) files.append((True, name, name, _("(profile)"), o)) elif x.name.endswith(".menu"): o = GObject.GObject() o.obj = MenuData.from_fileobj(tar.extractfile(x), parser) files.append((True, name, name, _("(menu)"), o)) except Exception, e: # Either entire tar or some profile cannot be parsed. # Display error message and let user to quit # Error message reuses same page as above. log.error(e) self.error(str(e)) return
def wrapper(*a): _time = time.time def fake_time(): return fake_time.t def add(n): fake_time.t += n fake_time.t = _time() fake_time.add = add time.time = fake_time controller = FakeController(0) profile = Profile(parser) scheduler = Scheduler() mapper = Mapper(profile, scheduler, keyboard=False, mouse=False, gamepad=False, poller=None) mapper.keyboard = RememberingDummy() mapper.gamepad = RememberingDummy() mapper.mouse = RememberingDummy() mapper.set_controller(controller) mapper._testing = True mapper._tick_rate = 0.01 _mapper_input = mapper.input def mapper_input(*a): add(mapper._tick_rate) _mapper_input(*a) scheduler.run() mapper.input = mapper_input a = list(a) + [mapper] try: return fn(*a) finally: time.time = _time
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 __init__(self, config=None): self.kbimage = os.path.join(get_config_path(), 'keyboard.svg') if not os.path.exists(self.kbimage): # Prefer image in ~/.config/scc, but load default one as fallback self.kbimage = os.path.join(get_share_path(), "images", 'keyboard.svg') TimerManager.__init__(self) OSDWindow.__init__(self, "osd-keyboard") self.daemon = None self.mapper = None self.keymap = Gdk.Keymap.get_default() self.keymap.connect('state-changed', self.on_keymap_state_changed) Action.register_all(sys.modules['scc.osd.osk_actions'], prefix="OSK") self.profile = Profile(TalkingActionParser()) self.config = config or Config() self.dpy = X.Display(hash(GdkX11.x11_get_default_xdisplay())) self.group = None self.limits = {} self.background = None 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.cursors[CPAD] = Gtk.Image.new_from_file(cursor) self.cursors[CPAD].set_name("osd-keyboard-cursor") self._eh_ids = [] self._controller = None 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._pressed_areas = {} self.c = Gtk.Box() self.c.set_name("osd-keyboard-container") self.f = Gtk.Fixed()
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
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 keyboard is displayed. 3 - erorr, failed to lock input stick, pad or button(s) """ OSK_PROF_NAME = ".scc-osd.keyboard" 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, config=None): self.kbimage = os.path.join(get_config_path(), 'keyboard.svg') if not os.path.exists(self.kbimage): # Prefer image in ~/.config/scc, but load default one as fallback self.kbimage = os.path.join(get_share_path(), "images", 'keyboard.svg') TimerManager.__init__(self) OSDWindow.__init__(self, "osd-keyboard") self.daemon = None self.mapper = None self.keymap = Gdk.Keymap.get_default() self.keymap.connect('state-changed', self.on_keymap_state_changed) Action.register_all(sys.modules['scc.osd.osk_actions'], prefix="OSK") self.profile = Profile(TalkingActionParser()) self.config = config or Config() self.dpy = X.Display(hash(GdkX11.x11_get_default_xdisplay())) self.group = None self.limits = {} self.background = None 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.cursors[CPAD] = Gtk.Image.new_from_file(cursor) self.cursors[CPAD].set_name("osd-keyboard-cursor") self._eh_ids = [] self._controller = None 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._pressed_areas = {} self.c = Gtk.Box() self.c.set_name("osd-keyboard-container") self.f = Gtk.Fixed() def _create_background(self): self.background = KeyboardImage(self.args.image) self.recolor() self.limits = {} self.limits[LEFT] = self.background.get_limit("LIMIT_LEFT") self.limits[RIGHT] = self.background.get_limit("LIMIT_RIGHT") self.limits[CPAD] = self.background.get_limit("LIMIT_CPAD") self._pack() def _pack(self): self.f.add(self.background) self.f.add(self.cursors[LEFT]) self.f.add(self.cursors[RIGHT]) self.f.add(self.cursors[CPAD]) self.c.add(self.f) self.add(self.c) def recolor(self): # TODO: keyboard description is probably not needed anymore _get = lambda a: SVGWidget.color_to_float(self.config['osk_colors']. get(a, "")) self.background.color_button1 = _get("button1") self.background.color_button1_border = _get("button1_border") self.background.color_button2 = _get("button2") self.background.color_button2_border = _get("button2_border") self.background.color_hilight = _get("hilight") self.background.color_pressed = _get("pressed") self.background.color_text = _get("text") 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_keymap_state_changed(self, x11keymap): if not self.timer_active('labels'): self.timer('labels', 0.1, self.update_labels) def set_help(self): """ Updates help shown on keyboard image. Keyboard bindings don't change on the fly, so this is done only right after start or when daemon is reconfigured. """ if self._controller is None: # Not yet connected return gui_config = self._controller.load_gui_config( os.path.join(get_share_path(), "images")) l_lines, r_lines, used = [], [], set() def add_action(side, button, a): if not a: return if isinstance(a, scc.osd.osk_actions.OSKCursorAction): if a.side != CPAD: return if isinstance(a, ModeModifier): for x in a.get_child_actions(): add_action(side, button, x) return desc = a.describe(Action.AC_OSK) if desc in used: if isinstance(a, scc.osd.osk_actions.OSKPressAction): # Special case, both triggers are set to "press a key" pass else: return icon = self._controller.get_button_name(gui_config, button) side.append((icon, desc)) used.add(desc) def add_button(side, b): add_action(side, b, self.profile.buttons[b]) if self._controller.get_flags() & ControllerFlags.NO_GRIPS == 0: add_button(l_lines, SCButtons.LGRIP) add_button(r_lines, SCButtons.RGRIP) add_action(l_lines, SCButtons.LT, self.profile.triggers[LEFT]) add_action(r_lines, SCButtons.RT, self.profile.triggers[RIGHT]) for b in (SCButtons.LB, SCButtons.Y, SCButtons.X): add_button(l_lines, b) for b in (SCButtons.RB, SCButtons.B, SCButtons.A): add_button(r_lines, b) if self._controller.get_flags() & ControllerFlags.HAS_CPAD != 0: for lst in (l_lines, r_lines): while len(lst) > 3: lst.pop() while len(lst) < 3: lst.append((None, "")) add_action(r_lines, CPAD, self.profile.pads[CPAD]) add_action(l_lines, SCButtons.STICKPRESS, self.profile.stick) self.background.set_help(l_lines, r_lines) def update_labels(self): """ Updates keyboard labels based on active X keymap """ labels = {} # Get current layout group self.group = X.get_xkb_state(self.dpy).group # Get state of shift/alt/ctrl key mt = Gdk.ModifierType(self.keymap.get_modifier_state()) for button in self.background.buttons: if getattr(Keys, button.name, None) in KEY_TO_KEYCODE: keycode = KEY_TO_KEYCODE[getattr(Keys, button.name)] translation = self.keymap.translate_keyboard_state( keycode, mt, self.group) if hasattr(translation, "keyval"): code = Gdk.keyval_to_unicode(translation.keyval) else: code = Gdk.keyval_to_unicode(translation[1]) if code >= 33: # Printable chars, w/out space labels[button] = unichr(code).strip() else: labels[button] = SPECIAL_KEYS.get(code) self.background.set_labels(labels) def _add_arguments(self): OSDWindow._add_arguments(self) self.argparser.add_argument('image', type=str, nargs="?", default=self.kbimage, help="keyboard image to use") 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, self.daemon.connect('dead', self.on_daemon_died)), (self.daemon, self.daemon.connect('error', self.on_daemon_died)), (self.daemon, self.daemon.connect('reconfigured', self.on_reconfigured)), (self.daemon, self.daemon.connect('alive', self.on_daemon_connected)), ] def run(self): self.daemon = DaemonManager() self._cononect_handlers() OSDWindow.run(self) def load_profile(self): self.profile.load(find_profile(Keyboard.OSK_PROF_NAME)).compress() self.set_help() def on_reconfigured(self, *a): self.load_profile() log.debug("Reloaded profile") def on_daemon_connected(self, *a): def success(*a): log.info("Sucessfully locked input") pass c = self.choose_controller(self.daemon) if c is None or not c.is_connected(): # There is no controller connected to daemon self.on_failed_to_lock("Controller not connected") return self._eh_ids += [ (c, c.connect('event', self.on_event)), (c, c.connect('lost', self.on_controller_lost)), ] # TODO: Single-handed mode for PS4 posponed locks = [LEFT, RIGHT, STICK, "STICKPRESS" ] + [b.name for b in SCButtons] if (c.get_flags() & ControllerFlags.HAS_CPAD) == 0: # Two pads, two hands locks = [LEFT, RIGHT, STICK, "STICKPRESS" ] + [b.name for b in SCButtons] self.cursors[CPAD].hide() else: # Single-handed mode locks = [CPAD, "CPADPRESS", STICK, "STICKPRESS" ] + [b.name for b in SCButtons] self._hovers[self.cursors[RIGHT]] = None self._hovers = {self.cursors[CPAD]: None} self._pressed = {self.cursors[CPAD]: None} self.cursors[LEFT].hide() self.cursors[RIGHT].hide() # There is no configurable nor default mapping for CPDAD, # so situable mappings are hardcoded here self.profile.pads[CPAD] = scc.osd.osk_actions.OSKCursorAction(CPAD) self.profile.pads[CPAD].speed = [0.85, 1.2] self.profile.buttons[ SCButtons.CPADPRESS] = scc.osd.osk_actions.OSKPressAction(CPAD) for i in (LEFT, RIGHT): if isinstance(self.profile.triggers[i], scc.osd.osk_actions.OSKPressAction): self.profile.triggers[ i] = scc.osd.osk_actions.OSKPressAction(CPAD) self._controller = c c.lock(success, self.on_failed_to_lock, *locks) self.set_help() def quit(self, code=-1): if self.get_controller(): self.get_controller().unlock_all() for source, eid in self._eh_ids: source.disconnect(eid) self._eh_ids = [] del self.mapper OSDWindow.quit(self, code) def show(self, *a): if self.background is None: self._create_background() OSDWindow.show(self, *a) self.load_profile() self.mapper = SlaveMapper(self.profile, None, keyboard=b"SCC OSD Keyboard", mouse=b"SCC OSD Mouse") 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]) self.set_cursor_position(0, 0, self.cursors[CPAD], self.limits[CPAD]) self.timer('labels', 0.1, self.update_labels) def on_event(self, daemon, what, data): """ Called when button press, button release or stick / pad update is send by daemon. """ group = X.get_xkb_state(self.dpy).group if self.group != group: self.group = group self.timer('labels', 0.1, self.update_labels) 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. """ if cursor not in self._hovers: return 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) 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.position = int(x), int(y) self.f.move(cursor, x - cursor.get_allocation().width * 0.5, y - cursor.get_allocation().height * 0.5) for button in self.background.buttons: if button.contains(x, y): if button != self._hovers[cursor]: self._hovers[cursor] = button 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('update'): self.timer('update', 0.01, self.update_background) break def update_background(self, *whatever): """ Updates hilighted keys on bacgkround image. """ self.background.hilight( set([a for a in self._hovers.values() if a]), set([a for a in self._pressed_areas.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 button in self.background.buttons: if button.contains(x, y): if button.name.startswith("KEY_") and hasattr( Keys, button.name): key = getattr(Keys, button.name) if self._pressed[cursor] is not None: self.mapper.keyboard.releaseEvent( [self._pressed[cursor]]) self.mapper.keyboard.pressEvent([key]) self._pressed[cursor] = key self._pressed_areas[cursor] = button break elif self._pressed[cursor] is not None: self.mapper.keyboard.releaseEvent([self._pressed[cursor]]) self._pressed[cursor] = None del self._pressed_areas[cursor] if not self.timer_active('update'): self.timer('update', 0.01, self.update_background)
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
def __init__(self): svg = SVGEditor(file("images/binding-display.svg").read()) background = SVGEditor.get_element(svg, "background") self.label_template = SVGEditor.get_element(svg, "label_template") self.line_height = int(float(self.label_template.attrib.get("height") or 8)) self.char_width = int(float(self.label_template.attrib.get("width") or 8)) self.full_width = int(float(background.attrib.get("width") or 800)) self.full_height = int(float(background.attrib.get("height") or 800)) profile = Profile(TalkingActionParser()).load("test.sccprofile") boxes = [] box_bcs = Box(0, self.PADDING, Align.TOP, "bcs") box_bcs.add("BACK", Action.AC_BUTTON, profile.buttons.get(SCButtons.BACK)) box_bcs.add("C", Action.AC_BUTTON, profile.buttons.get(SCButtons.C)) box_bcs.add("START", Action.AC_BUTTON, profile.buttons.get(SCButtons.START)) boxes.append(box_bcs) box_left = Box(self.PADDING, self.PADDING, Align.LEFT | Align.TOP, "left", min_height = self.full_height * 0.5, min_width = self.full_width * 0.2) box_left.add("LEFT", Action.AC_TRIGGER, profile.triggers.get(profile.LEFT)) box_left.add("LB", Action.AC_BUTTON, profile.buttons.get(SCButtons.LB)) box_left.add("LGRIP", Action.AC_BUTTON, profile.buttons.get(SCButtons.LGRIP)) box_left.add("LPAD", Action.AC_PAD, profile.pads.get(profile.LEFT)) boxes.append(box_left) box_right = Box(self.PADDING, self.PADDING, Align.RIGHT | Align.TOP, "right", min_height = self.full_height * 0.5, min_width = self.full_width * 0.2) box_right.add("RIGHT", Action.AC_TRIGGER, profile.triggers.get(profile.RIGHT)) box_right.add("RB", Action.AC_BUTTON, profile.buttons.get(SCButtons.RB)) box_right.add("RGRIP", Action.AC_BUTTON, profile.buttons.get(SCButtons.RGRIP)) box_right.add("RPAD", Action.AC_PAD, profile.pads.get(profile.RIGHT)) boxes.append(box_right) box_abxy = Box(4 * self.PADDING, self.PADDING, Align.RIGHT | Align.BOTTOM, "abxy") box_abxy.add("A", Action.AC_BUTTON, profile.buttons.get(SCButtons.A)) box_abxy.add("B", Action.AC_BUTTON, profile.buttons.get(SCButtons.B)) box_abxy.add("X", Action.AC_BUTTON, profile.buttons.get(SCButtons.X)) box_abxy.add("Y", Action.AC_BUTTON, profile.buttons.get(SCButtons.Y)) boxes.append(box_abxy) box_stick = Box(4 * self.PADDING, self.PADDING, Align.LEFT | Align.BOTTOM, "stick") box_stick.add("STICK", Action.AC_STICK, profile.stick) boxes.append(box_stick) w = int(float(background.attrib.get("width") or 800)) h = int(float(background.attrib.get("height") or 800)) root = SVGEditor.get_element(svg, "root") for b in boxes: b.calculate(self) # Set ABXY and Stick size & position box_abxy.height = box_stick.height = self.full_height * 0.25 box_abxy.width = box_stick.width = self.full_width * 0.3 box_abxy.y = self.full_height - self.PADDING - box_abxy.height box_stick.y = self.full_height - self.PADDING - box_stick.height box_abxy.x = self.full_width - self.PADDING - box_abxy.width self.equal_width(box_left, box_right) self.equal_height(box_left, box_right) for b in boxes: b.place_marker(self, root) for b in boxes: b.place(self, root) file("out.svg", "w").write(svg.to_string())
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 on_profile_changed(self, daemon, filename): profile = Profile(TalkingActionParser()).load(filename) Generator(SVGEditor(self.background), profile)
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 keyboard is displayed. 3 - erorr, failed to lock input stick, pad or button(s) """ OSK_PROF_NAME = ".scc-osd.keyboard" 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, config=None): self.kbimage = os.path.join(get_config_path(), 'keyboard.svg') if not os.path.exists(self.kbimage): # Prefer image in ~/.config/scc, but load default one as fallback self.kbimage = os.path.join(get_share_path(), "images", 'keyboard.svg') TimerManager.__init__(self) OSDWindow.__init__(self, "osd-keyboard") self.daemon = None self.mapper = None self.keymap = Gdk.Keymap.get_default() self.keymap.connect('state-changed', self.on_keymap_state_changed) Action.register_all(sys.modules['scc.osd.osk_actions'], prefix="OSK") self.profile = Profile(TalkingActionParser()) self.config = config or Config() self.dpy = X.Display(hash(GdkX11.x11_get_default_xdisplay())) self.group = None self.limits = {} self.background = None 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.cursors[CPAD] = Gtk.Image.new_from_file(cursor) self.cursors[CPAD].set_name("osd-keyboard-cursor") self._eh_ids = [] self._controller = None 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._pressed_areas = {} self.c = Gtk.Box() self.c.set_name("osd-keyboard-container") self.f = Gtk.Fixed() def _create_background(self): self.background = KeyboardImage(self.args.image) self.recolor() self.limits = {} self.limits[LEFT] = self.background.get_limit("LIMIT_LEFT") self.limits[RIGHT] = self.background.get_limit("LIMIT_RIGHT") self.limits[CPAD] = self.background.get_limit("LIMIT_CPAD") self._pack() def _pack(self): self.f.add(self.background) self.f.add(self.cursors[LEFT]) self.f.add(self.cursors[RIGHT]) self.f.add(self.cursors[CPAD]) self.c.add(self.f) self.add(self.c) def recolor(self): # TODO: keyboard description is probably not needed anymore _get = lambda a: SVGWidget.color_to_float(self.config['osk_colors'].get(a, "")) self.background.color_button1 = _get("button1") self.background.color_button1_border = _get("button1_border") self.background.color_button2 = _get("button2") self.background.color_button2_border = _get("button2_border") self.background.color_hilight = _get("hilight") self.background.color_pressed = _get("pressed") self.background.color_text = _get("text") 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_keymap_state_changed(self, x11keymap): if not self.timer_active('labels'): self.timer('labels', 0.1, self.update_labels) def set_help(self): """ Updates help shown on keyboard image. Keyboard bindings don't change on the fly, so this is done only right after start or when daemon is reconfigured. """ if self._controller is None: # Not yet connected return gui_config = self._controller.load_gui_config(os.path.join(get_share_path(), "images")) l_lines, r_lines, used = [], [], set() def add_action(side, button, a): if not a: return if isinstance(a, scc.osd.osk_actions.OSKCursorAction): if a.side != CPAD: return if isinstance(a, ModeModifier): for x in a.get_child_actions(): add_action(side, button, x) return desc = a.describe(Action.AC_OSK) if desc in used: if isinstance(a, scc.osd.osk_actions.OSKPressAction): # Special case, both triggers are set to "press a key" pass else: return icon = self._controller.get_button_name(gui_config, button) side.append(( icon, desc )) used.add(desc) def add_button(side, b): add_action(side, b, self.profile.buttons[b]) if self._controller.get_flags() & ControllerFlags.NO_GRIPS == 0: add_button(l_lines, SCButtons.LGRIP) add_button(r_lines, SCButtons.RGRIP) add_action(l_lines, SCButtons.LT, self.profile.triggers[LEFT]) add_action(r_lines, SCButtons.RT, self.profile.triggers[RIGHT]) for b in (SCButtons.LB, SCButtons.Y, SCButtons.X): add_button(l_lines, b) for b in (SCButtons.RB, SCButtons.B, SCButtons.A): add_button(r_lines, b) if self._controller.get_flags() & ControllerFlags.HAS_CPAD != 0: for lst in (l_lines, r_lines): while len(lst) > 3: lst.pop() while len(lst) < 3: lst.append((None, "")) add_action(r_lines, CPAD, self.profile.pads[CPAD]) add_action(l_lines, SCButtons.STICKPRESS, self.profile.stick) self.background.set_help(l_lines, r_lines) def update_labels(self): """ Updates keyboard labels based on active X keymap """ labels = {} # Get current layout group self.group = X.get_xkb_state(self.dpy).group # Get state of shift/alt/ctrl key mt = Gdk.ModifierType(self.keymap.get_modifier_state()) for button in self.background.buttons: if getattr(Keys, button.name, None) in KEY_TO_KEYCODE: keycode = KEY_TO_KEYCODE[getattr(Keys, button.name)] translation = self.keymap.translate_keyboard_state(keycode, mt, self.group) if hasattr(translation, "keyval"): code = Gdk.keyval_to_unicode(translation.keyval) else: code = Gdk.keyval_to_unicode(translation[1]) if code >= 33: # Printable chars, w/out space labels[button] = unichr(code).strip() else: labels[button] = SPECIAL_KEYS.get(code) self.background.set_labels(labels) def _add_arguments(self): OSDWindow._add_arguments(self) self.argparser.add_argument('image', type=str, nargs="?", default = self.kbimage, help="keyboard image to use") 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, self.daemon.connect('dead', self.on_daemon_died) ), ( self.daemon, self.daemon.connect('error', self.on_daemon_died) ), ( self.daemon, self.daemon.connect('reconfigured', self.on_reconfigured) ), ( self.daemon, self.daemon.connect('alive', self.on_daemon_connected) ), ] def run(self): self.daemon = DaemonManager() self._cononect_handlers() OSDWindow.run(self) def load_profile(self): self.profile.load(find_profile(Keyboard.OSK_PROF_NAME)).compress() self.set_help() def on_reconfigured(self, *a): self.load_profile() log.debug("Reloaded profile") def on_daemon_connected(self, *a): def success(*a): log.info("Sucessfully locked input") pass c = self.choose_controller(self.daemon) if c is None or not c.is_connected(): # There is no controller connected to daemon self.on_failed_to_lock("Controller not connected") return self._eh_ids += [ (c, c.connect('event', self.on_event)), (c, c.connect('lost', self.on_controller_lost)), ] # TODO: Single-handed mode for PS4 posponed locks = [ LEFT, RIGHT, STICK, "STICKPRESS" ] + [ b.name for b in SCButtons ] if (c.get_flags() & ControllerFlags.HAS_CPAD) == 0: # Two pads, two hands locks = [ LEFT, RIGHT, STICK, "STICKPRESS" ] + [ b.name for b in SCButtons ] self.cursors[CPAD].hide() else: # Single-handed mode locks = [ CPAD, "CPADPRESS", STICK, "STICKPRESS" ] + [ b.name for b in SCButtons ] self._hovers[self.cursors[RIGHT]] = None self._hovers = { self.cursors[CPAD] : None } self._pressed = { self.cursors[CPAD] : None } self.cursors[LEFT].hide() self.cursors[RIGHT].hide() # There is no configurable nor default mapping for CPDAD, # so situable mappings are hardcoded here self.profile.pads[CPAD] = scc.osd.osk_actions.OSKCursorAction(CPAD) self.profile.pads[CPAD].speed = [ 0.85, 1.2 ] self.profile.buttons[SCButtons.CPADPRESS] = scc.osd.osk_actions.OSKPressAction(CPAD) for i in (LEFT, RIGHT): if isinstance(self.profile.triggers[i], scc.osd.osk_actions.OSKPressAction): self.profile.triggers[i] = scc.osd.osk_actions.OSKPressAction(CPAD) self._controller = c c.lock(success, self.on_failed_to_lock, *locks) self.set_help() def quit(self, code=-1): if self.get_controller(): self.get_controller().unlock_all() for source, eid in self._eh_ids: source.disconnect(eid) self._eh_ids = [] del self.mapper OSDWindow.quit(self, code) def show(self, *a): if self.background is None: self._create_background() OSDWindow.show(self, *a) self.load_profile() self.mapper = SlaveMapper(self.profile, None, keyboard=b"SCC OSD Keyboard", mouse=b"SCC OSD Mouse") 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]) self.set_cursor_position(0, 0, self.cursors[CPAD], self.limits[CPAD]) self.timer('labels', 0.1, self.update_labels) def on_event(self, daemon, what, data): """ Called when button press, button release or stick / pad update is send by daemon. """ group = X.get_xkb_state(self.dpy).group if self.group != group: self.group = group self.timer('labels', 0.1, self.update_labels) 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. """ if cursor not in self._hovers: return 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 ) 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.position = int(x), int(y) self.f.move(cursor, x - cursor.get_allocation().width * 0.5, y - cursor.get_allocation().height * 0.5) for button in self.background.buttons: if button.contains(x, y): if button != self._hovers[cursor]: self._hovers[cursor] = button 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('update'): self.timer('update', 0.01, self.update_background) break def update_background(self, *whatever): """ Updates hilighted keys on bacgkround image. """ self.background.hilight( set([ a for a in self._hovers.values() if a ]), set([ a for a in self._pressed_areas.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 button in self.background.buttons: if button.contains(x, y): if button.name.startswith("KEY_") and hasattr(Keys, button.name): key = getattr(Keys, button.name) if self._pressed[cursor] is not None: self.mapper.keyboard.releaseEvent([ self._pressed[cursor] ]) self.mapper.keyboard.pressEvent([ key ]) self._pressed[cursor] = key self._pressed_areas[cursor] = button break elif self._pressed[cursor] is not None: self.mapper.keyboard.releaseEvent([ self._pressed[cursor] ]) self._pressed[cursor] = None del self._pressed_areas[cursor] if not self.timer_active('update'): self.timer('update', 0.01, self.update_background)