예제 #1
0
파일: vdf.py 프로젝트: kozec/sc-controller
	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()
예제 #2
0
	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
예제 #3
0
	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])
예제 #4
0
 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()
예제 #5
0
	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
예제 #6
0
	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
예제 #7
0
    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
예제 #8
0
 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)
예제 #10
0
    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)
예제 #11
0
		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
예제 #12
0
    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
예제 #13
0
	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
예제 #14
0
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()
예제 #15
0
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()
예제 #16
0
	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
예제 #17
0
    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
예제 #18
0
	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 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
예제 #20
0
	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
예제 #21
0
    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)
예제 #22
0
    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()
예제 #23
0
	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
예제 #25
0
 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 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
예제 #27
0
	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)
예제 #28
0
	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()
예제 #29
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.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
예제 #30
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 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)
예제 #31
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.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
예제 #32
0
	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())
예제 #33
0
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:
예제 #34
0
 def on_profile_changed(self, daemon, filename):
     profile = Profile(TalkingActionParser()).load(filename)
     Generator(SVGEditor(self.background), profile)
예제 #35
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 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)