Пример #1
0
	def parse_argumets(self, argv):
		if not OSDWindow.parse_argumets(self, argv):
			return False
		if not self.config:
			self.config = Config()
		
		try:
			self.items = MenuData.from_args(self.args.items)
			self._menuid = None
		except ValueError:
			print >>sys.stderr, '%s: error: invalid number of arguments' % (sys.argv[0])
			return False
		
		self._text.set_label(self.args.text)
		
		if self.args.feedback_amplitude:
			side = "LEFT"
			self.feedback = side, int(self.args.feedback_amplitude)
		
		# Create buttons that are displayed on screen
		items = self.items.generate(self)
		self.items = []
		for item in items:
			item.widget = self.generate_widget(item)
			if item.widget is not None:
				self.items.append(item)
		self.pack_items(self.parent, self.items)
		if len(self.items) == 0:
			print >>sys.stderr, '%s: error: no items in menu' % (sys.argv[0])
			return False
		
		return True
Пример #2
0
	def parse_argumets(self, argv):
		if not OSDWindow.parse_argumets(self, argv):
			return False
		if not self.parse_menu():
			return False
		if not self.config:
			self.config = Config()
		
		# Parse simpler arguments
		self._size = self.args.size
		
		# Create buttons that are displayed on screen
		items = self.items.generate(self)
		self.items = []
		for item in items:
			item.widget = self.generate_widget(item)
			if item.widget is not None:
				self.items.append(item)
		self.pack_items(self.parent, self.items)
		if len(self.items) == 0:
			print >>sys.stderr, '%s: error: no items in menu' % (sys.argv[0])
			return False
		
		if self.args.print_items:
			max_id_len = max(*[ len(x.id) for x in self.items ])
			row_format ="{:>%s}:\t{}" % (max_id_len,)
			for item in self.items:
				print row_format.format(item.id, item.label)
		return True
Пример #3
0
	def parse_argumets(self, argv):
		if not OSDWindow.parse_argumets(self, argv):
			return False
		if not self.config:
			self.config = Config()
		
		if self.args.feedback_amplitude:
			side = "LEFT"
			self.feedback = side, int(self.args.feedback_amplitude)
		
		# Create buttons that are displayed on screen
		return True
Пример #4
0
	def on_daemon_connected(self, *a):
		if not self.config:
			self.config = Config()
		self.controller = self.choose_controller(self.daemon)
		if self.controller is None or not self.controller.is_connected():
			# There is no controller connected to daemon
			self.on_failed_to_lock("Controller not connected")
			return
		self.use_controller(self.controller)
		
		self._eh_ids += [
			(self.controller, self.controller.connect('event', self.on_event)),
			(self.controller, self.controller.connect('lost', self.on_controller_lost)),
		]
		self.lock_inputs()
Пример #5
0
    def parse_argumets(self, argv):
        if not OSDWindow.parse_argumets(self, argv):
            return False
        if not self.config:
            self.config = Config()

        # Parse simpler arguments
        self._confirm_with = self.args.confirm_with
        self._cancel_with = self.args.cancel_with

        if self.args.feedback_amplitude:
            side = "LEFT"
            self.feedback = side, int(self.args.feedback_amplitude)

        # Create buttons that are displayed on screen
        return True
Пример #6
0
    def on_daemon_connected(self, *a):
        def success(*a):
            log.error("Sucessfully locked %s pad", self._control_with)
            self._left_detector.enable()
            # self._right_detector.enable()

        if not self.config:
            self.config = Config()
        locks = [self._control_with]
        c = self.choose_controller(self.daemon)
        if c is None or not c.is_connected():
            # There is no controller connected to daemon
            self.on_failed_to_lock("Controller not connected")
            return
        self._eh_ids += [(c, c.connect('event', self.on_event))]
        c.lock(success, self.on_failed_to_lock, *locks)
Пример #7
0
 def run(self):
     on_wayland = "WAYLAND_DISPLAY" in os.environ or not isinstance(
         Gdk.Display.get_default(), GdkX11.X11Display)
     if on_wayland:
         log.error("Cannot run on Wayland")
         self.exit_code = 8
         return
     self.daemon = DaemonManager()
     self.config = Config()
     self._check_colorconfig_change()
     self.daemon.connect('alive', self.on_daemon_connected)
     self.daemon.connect('dead', self.on_daemon_died)
     self.daemon.connect('profile-changed', self.on_profile_changed)
     self.daemon.connect('reconfigured', self.on_daemon_reconfigured)
     self.daemon.connect('unknown-msg', self.on_unknown_message)
     self.mainloop.run()
Пример #8
0
    def on_daemon_connected(self, *a):
        def success(*a):
            log.error("Sucessfully locked input")

        if not self.config:
            self.config = Config()
        locks = [self._control_with, self._confirm_with, self._cancel_with]
        self.controller = self.choose_controller(self.daemon)
        if self.controller is None or not self.controller.is_connected():
            # There is no controller connected to daemon
            self.on_failed_to_lock("Controller not connected")
            return

        self._eh_ids += [(self.controller,
                          self.controller.connect('event', self.on_event))]
        self.controller.lock(success, self.on_failed_to_lock, *locks)
Пример #9
0
    def parse_argumets(self, argv):
        if not OSDWindow.parse_argumets(self, argv):
            return False
        if not self.config: self.config = Config()
        self.parse_menu()

        # Parse simpler arguments
        self._control_with = self.args.control_with
        self._confirm_with = self.args.confirm_with
        self._cancel_with = self.args.cancel_with
        self._size = self.args.size

        if self.args.use_cursor:
            self.enable_cursor()

        if self.args.feedback_amplitude:
            side = "LEFT"
            if self._control_with == "RIGHT":
                side = "RIGHT"
            elif self._control_with == "STICK":
                side = "BOTH"
            self.feedback = side, int(self.args.feedback_amplitude)

        if self._confirm_with == SAME:
            if self._control_with == RIGHT:
                self._confirm_with = SCButtons.RPADTOUCH.name
            else:
                self._confirm_with = SCButtons.LPADTOUCH.name

        # Create buttons that are displayed on screen
        items = self.items.generate(self)
        self.items = []
        for item in items:
            item.widget = self.generate_widget(item)
            if item.widget is not None:
                self.items.append(item)
        self.pack_items(self.parent, self.items)
        if len(self.items) == 0:
            print >> sys.stderr, '%s: error: no items in menu' % (sys.argv[0])
            return False

        if self.args.print_items:
            max_id_len = max(*[len(x.id) for x in self.items])
            row_format = "{:>%s}:\t{}" % (max_id_len, )
            for item in self.items:
                print row_format.format(item.id, item.label)
        return True
Пример #10
0
	def __init__(self, config=None):
		self.bdisplay = os.path.join(get_config_path(), 'binding-display.svg')
		if not os.path.exists(self.bdisplay):
			# Prefer image in ~/.config/scc, but load default one as fallback
			self.bdisplay = os.path.join(get_share_path(), "images", 'binding-display.svg')
		
		OSDWindow.__init__(self, "osd-keyboard")
		self.daemon = None
		self.config = config or Config()
		self.group = None
		self.limits = {}
		self.background = None
		
		self._eh_ids = []
		self._stick = 0, 0
		
		self.c = Gtk.Box()
		self.c.set_name("osd-keyboard-container")
Пример #11
0
	def __init__(self, wmclass):
		Gtk.Window.__init__(self)
		OSDWindow._apply_css(Config())
		
		self.argparser = argparse.ArgumentParser(description=__doc__,
			formatter_class=argparse.RawDescriptionHelpFormatter,
			epilog=self.EPILOG)
		self._add_arguments()
		self.exit_code = -1
		self.position = (20, -20)
		self.mainloop = None
		self.set_name(wmclass)
		self.set_wmclass(wmclass, wmclass)
		self.set_decorated(False)
		self.stick()
		self.set_skip_taskbar_hint(True)
		self.set_skip_pager_hint(True)
		self.set_keep_above(True)
		self.set_type_hint(Gdk.WindowTypeHint.NOTIFICATION)
Пример #12
0
	def on_daemon_connected(self, *a):
		def success(*a):
			log.error("Sucessfully locked input")
		
		if not self.config:
			self.config = Config()
		self.controller = self.choose_controller(self.daemon)
		if self.controller is None or not self.controller.is_connected():
			# There is no controller connected to daemon
			self.on_failed_to_lock("Controller not connected")
			return
		
		ccfg = self.config.get_controller_config(self.controller.get_id())
		self._control_with = ccfg["menu_control"]
		self._confirm_with = ccfg["menu_confirm"] if self.args.confirm_with == DEFAULT else self.args.confirm_with
		self._cancel_with = ccfg["menu_cancel"] if self.args.cancel_with == DEFAULT else self.args.cancel_with
		
		self._eh_ids += [ (self.controller, self.controller.connect('event', self.on_event)) ]
		locks = [ self._control_with, self._confirm_with, self._cancel_with ]
		self.controller.lock(success, self.on_failed_to_lock, *locks)
Пример #13
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()
Пример #14
0
    def read_serial(self):
        """ Requests and reads serial number from controller """
        if Config()["ignore_serials"]:
            # Special exception for cases when controller drops instead of
            # sending serial number. See issue #103
            self.generate_serial()
            self.on_serial_got()
            return

        def cb(rawserial):
            size, serial = struct.unpack(">xBx12s49x", rawserial)
            if size > 1:
                serial = serial.strip(" \x00")
                self._serial = serial
                self.on_serial_got()
            else:
                self._driver._no_serial.append(self)

        self._driver.make_request(
            self._ccidx, cb,
            struct.pack('>BBB61x', SCPacketType.GET_SERIAL,
                        SCPacketLength.GET_SERIAL, 0x01))
Пример #15
0
    def _create_gamepad(self, enabled, poller):
        """ Parses gamepad configuration and creates apropriate unput device """
        if not enabled or "SCC_NOGAMEPAD" in os.environ:
            # Completly undocumented and for debuging purposes only.
            # If set, no gamepad is emulated
            self.gamepad = Dummy()
            return
        cfg = Config()
        keys = ALL_BUTTONS[0:cfg["output"]["buttons"]]
        vendor = int(cfg["output"]["vendor"], 16)
        product = int(cfg["output"]["product"], 16)
        version = int(cfg["output"]["version"], 16)
        name = cfg["output"]["name"]
        rumble = cfg["output"]["rumble"] and poller != None
        axes = []
        i = 0
        for min, max in cfg["output"]["axes"]:
            fuzz, flat = 0, 0
            if abs(max - min) > 32768:
                fuzz, flat = 16, 128
            try:
                axes.append((ALL_AXES[i], min, max, fuzz, flat))
            except IndexError:
                # Out of axes
                break
            i += 1

        ui = UInput(vendor=vendor,
                    product=product,
                    version=version,
                    name=name,
                    keys=keys,
                    axes=axes,
                    rels=[],
                    rumble=rumble)
        if poller:
            poller.register(ui.getDescriptor(), poller.POLLIN,
                            self._rumble_ready)
        return ui
Пример #16
0
	def parse_argumets(self, argv):
		if not OSDWindow.parse_argumets(self, argv):
			return False
		if not self.config: self.config = Config()
		
		self._cancel_with = self.args.cancel_with
		self.parse_menu()
		
		# Create buttons that are displayed on screen
		items = self.items.generate(self)
		self.items = []
		self._button_index = 0
		for item in items:
			item.widget = self.generate_widget(item)
			if item.widget is not None:
				self.items.append(item)
		self.pack_items(self.parent, self.items)
		if len(self.items) == 0:
			print >>sys.stderr, '%s: error: no items in menu' % (sys.argv[0])
			return False
		
		return True
Пример #17
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.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 = []
Пример #18
0
    def parse_argumets(self, argv):
        if not OSDWindow.parse_argumets(self, argv):
            return False
        if not self.config:
            self.config = Config()

        try:
            self.items = MenuData.from_args(self.args.items)
            self._menuid = None
        except ValueError:
            print >> sys.stderr, '%s: error: invalid number of arguments' % (
                sys.argv[0])
            return False

        self._text.set_label(self.args.text)

        # Parse simpler arguments
        self._confirm_with = self.args.confirm_with
        self._cancel_with = self.args.cancel_with

        if self.args.feedback_amplitude:
            side = "LEFT"
            self.feedback = side, int(self.args.feedback_amplitude)

        # Create buttons that are displayed on screen
        items = self.items.generate(self)
        self.items = []
        for item in items:
            item.widget = self.generate_widget(item)
            if item.widget is not None:
                self.items.append(item)
        self.pack_items(self.parent, self.items)
        if len(self.items) == 0:
            print >> sys.stderr, '%s: error: no items in menu' % (sys.argv[0])
            return False

        return True
Пример #19
0
class Menu(OSDWindow):
    EPILOG = """Exit codes:
   0  - clean exit, user selected option
  -1  - clean exit, user canceled menu
  -2  - clean exit, menu closed from callback method
   1  - error, invalid arguments
   2  - error, failed to access sc-daemon, sc-daemon reported error or died while menu is displayed.
   3  - erorr, failed to lock input stick, pad or button(s)
	"""
    SUBMENU_OFFSET = 50
    PREFER_BW_ICONS = True

    def __init__(self, cls="osd-menu"):
        OSDWindow.__init__(self, cls)
        self.daemon = None
        self.config = None
        self.feedback = None
        self.controller = None
        self.xdisplay = X.Display(hash(
            GdkX11.x11_get_default_xdisplay()))  # Magic

        cursor = os.path.join(get_share_path(), "images", 'menu-cursor.svg')
        self.cursor = Gtk.Image.new_from_file(cursor)
        self.cursor.set_name("osd-menu-cursor")

        self.parent = self.create_parent()
        self.f = Gtk.Fixed()
        self.f.add(self.parent)
        self.add(self.f)

        self._submenu = None
        self._scon = StickController()
        self._scon.connect("direction", self.on_stick_direction)
        self._is_submenu = False
        self._selected = None
        self._menuid = None
        self._use_cursor = False
        self._eh_ids = []
        self._control_with = STICK
        self._confirm_with = 'A'
        self._cancel_with = 'B'

    def set_is_submenu(self):
        """
		Marks menu as submenu. This changes behaviour of some methods,
		especially disables (un)locking of input stick and buttons.
		"""
        self._is_submenu = True

    def create_parent(self):
        v = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
        v.set_name("osd-menu")
        return v

    def pack_items(self, parent, items):
        for item in items:
            parent.pack_start(item.widget, True, True, 0)

    def use_daemon(self, d):
        """
		Allows (re)using already existing DaemonManager instance in same process.
		use_config() should be be called before parse_argumets() if this is used.
		"""
        self.daemon = d
        if not self._is_submenu:
            self._connect_handlers()
            self.on_daemon_connected(self.daemon)

    def use_config(self, c):
        """
		Allows reusing already existin Config instance in same process.
		Has to be called before parse_argumets()
		"""
        self.config = c

    def get_menuid(self):
        """
		Returns ID of used menu.
		"""
        return self._menuid

    def get_selected_item_id(self):
        """
		Returns ID of selected item or None if nothing is selected.
		"""
        if self._selected:
            return self._selected.id
        return None

    def _add_arguments(self):
        OSDWindow._add_arguments(self)
        self.argparser.add_argument(
            '--control-with',
            '-c',
            type=str,
            metavar="option",
            default=DEFAULT,
            choices=(DEFAULT, LEFT, RIGHT, STICK),
            help="which pad or stick should be used to navigate menu")
        self.argparser.add_argument('--confirm-with',
                                    type=str,
                                    metavar="button",
                                    default=DEFAULT,
                                    help="button used to confirm choice")
        self.argparser.add_argument('--cancel-with',
                                    type=str,
                                    metavar="button",
                                    default=DEFAULT,
                                    help="button used to cancel menu")
        self.argparser.add_argument(
            '--confirm-with-release',
            action='store_true',
            help="confirm choice with button release instead of button press")
        self.argparser.add_argument(
            '--cancel-with-release',
            action='store_true',
            help="cancel menu with button release instead of button press")
        self.argparser.add_argument('--use-cursor',
                                    '-u',
                                    action='store_true',
                                    help="display and use cursor")
        self.argparser.add_argument('--size',
                                    type=int,
                                    help="sets prefered width or height")
        self.argparser.add_argument(
            '--feedback-amplitude',
            type=int,
            help=
            "enables and sets power of feedback effect generated when active menu option is changed"
        )
        self.argparser.add_argument('--from-profile',
                                    '-p',
                                    type=str,
                                    metavar="profile_file menu_name",
                                    help="load menu items from profile file")
        self.argparser.add_argument('--from-file',
                                    '-f',
                                    type=str,
                                    metavar="filename",
                                    help="load menu items from json file")
        self.argparser.add_argument('--print-items',
                                    action='store_true',
                                    help="prints menu items to stdout")
        self.argparser.add_argument('items',
                                    type=str,
                                    nargs='*',
                                    metavar='id title',
                                    help="Menu items")

    @staticmethod
    def _get_on_screen_position(w):
        a = w.get_allocation()
        parent = w.get_parent()
        if parent:
            if isinstance(parent, Menu) and parent.get_window() is not None:
                x, y = parent.get_window().get_position()
            else:
                x, y = Menu._get_on_screen_position(parent)
            return a.x + x, a.y + y
        else:
            return a.x, a.y

    def parse_menu(self):
        if self.args.from_profile:
            try:
                self._menuid = self.args.items[0]
                self.items = MenuData.from_profile(self.args.from_profile,
                                                   self._menuid)
            except IOError:
                print >> sys.stderr, '%s: error: profile file not found' % (
                    sys.argv[0])
                return False
            except ValueError:
                print >> sys.stderr, '%s: error: menu not found' % (
                    sys.argv[0])
                return False
        elif self.args.from_file:
            try:
                self._menuid = self.args.from_file
                self.items = MenuData.from_file(self.args.from_file)
            except:
                print >> sys.stderr, '%s: error: failed to load menu file' % (
                    sys.argv[0])
                return False
        else:
            try:
                self.items = MenuData.from_args(self.args.items)
                self._menuid = None
            except ValueError:
                print >> sys.stderr, '%s: error: invalid number of arguments' % (
                    sys.argv[0])
                return False
        return True

    def parse_argumets(self, argv):
        if not OSDWindow.parse_argumets(self, argv):
            return False
        if not self.parse_menu():
            return False
        if not self.config:
            self.config = Config()

        # Parse simpler arguments
        self._size = self.args.size

        # Create buttons that are displayed on screen
        items = self.items.generate(self)
        self.items = []
        for item in items:
            item.widget = self.generate_widget(item)
            if item.widget is not None:
                self.items.append(item)
        self.pack_items(self.parent, self.items)
        if len(self.items) == 0:
            print >> sys.stderr, '%s: error: no items in menu' % (sys.argv[0])
            return False

        if self.args.print_items:
            max_id_len = max(*[len(x.id) for x in self.items])
            row_format = "{:>%s}:\t{}" % (max_id_len, )
            for item in self.items:
                print row_format.format(item.id, item.label)
        return True

    def enable_cursor(self):
        if not self._use_cursor:
            self.f.add(self.cursor)
            self.f.show_all()
            self._use_cursor = True

    def generate_widget(self, item):
        """ Generates gtk widget for specified menutitem """
        if isinstance(item, Separator) and item.label:
            widget = Gtk.Button.new_with_label(item.label)
            widget.set_relief(Gtk.ReliefStyle.NONE)
            widget.set_name("osd-menu-separator")
            return widget
        elif isinstance(item, Separator):
            widget = Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL)
            widget.set_name("osd-menu-separator")
            return widget
        else:
            widget = Gtk.Button.new_with_label(item.label)
            widget.set_relief(Gtk.ReliefStyle.NONE)
            if hasattr(widget.get_children()[0], "set_xalign"):
                widget.get_children()[0].set_xalign(0)
            else:
                widget.get_children()[0].set_halign(Gtk.Align.START)
            if isinstance(item, Submenu):
                item.callback = self.show_submenu
                label1 = widget.get_children()[0]
                label2 = Gtk.Label(_(">>"))
                label2.set_property("margin-left", 30)
                box = Gtk.Box(Gtk.Orientation.HORIZONTAL)
                widget.remove(label1)
                box.pack_start(label1, True, True, 1)
                box.pack_start(label2, False, True, 1)
                widget.add(box)
                widget.set_name("osd-menu-item")
            elif item.id is None:
                widget.set_name("osd-menu-dummy")
            else:
                widget.set_name("osd-menu-item")

            if isinstance(item.icon, Gio.FileIcon):
                icon_file = item.icon.get_file().get_path()
                has_colors = True
            elif isinstance(item.icon, Gio.ThemedIcon):
                icon = Gtk.IconTheme.get_default().choose_icon(
                    item.icon.get_names(), 64, 0)
                icon_file = icon.get_filename() if icon else None
                has_colors = True
            else:
                icon_file, has_colors = find_icon(item.icon,
                                                  self.PREFER_BW_ICONS)

            if icon_file:
                icon = MenuIcon(icon_file, has_colors)
                label = widget.get_children()[0]
                for c in [] + widget.get_children():
                    widget.remove(c)
                box = Gtk.Box()
                box.pack_start(icon, False, True, 0)
                box.pack_start(label, True, True, 10)
                widget.add(box)

            return widget

    def select(self, index):
        if self._selected:
            self._selected.widget.set_name(
                self._selected.widget.get_name().replace("-selected", ""))
        if self.items[index].id:
            if self._selected != self.items[index]:
                if self.feedback and self.controller:
                    self.controller.feedback(*self.feedback)
            self._selected = self.items[index]
            self._selected.widget.set_name(self._selected.widget.get_name() +
                                           "-selected")
            GLib.timeout_add(2, self._check_on_screen_position)
            return True
        return False

    def _check_on_screen_position(self):
        x, y = Menu._get_on_screen_position(self._selected.widget)
        screen_height = self.get_window().get_screen().get_height()
        if y < 50:
            wx, wy = self.get_window().get_position()
            wy += 5
            self.get_window().move(wx, wy)
            GLib.timeout_add(2, self._check_on_screen_position)
        if y > screen_height - 100:
            wx, wy = self.get_window().get_position()
            wy -= 5
            self.get_window().move(wx, wy)
            GLib.timeout_add(2, self._check_on_screen_position)

    def _connect_handlers(self):
        self._eh_ids += [
            (self.daemon, self.daemon.connect('dead', self.on_daemon_died)),
            (self.daemon, self.daemon.connect('error', self.on_daemon_died)),
            (self.daemon, self.daemon.connect('alive',
                                              self.on_daemon_connected)),
        ]

    def run(self):
        self.daemon = DaemonManager()
        self._connect_handlers()
        OSDWindow.run(self)

    def show(self, *a):
        if not self.select(0):
            self.next_item(1)
        OSDWindow.show(self, *a)

    def on_daemon_connected(self, *a):
        if not self.config:
            self.config = Config()
        self.controller = self.choose_controller(self.daemon)
        if self.controller is None or not self.controller.is_connected():
            # There is no controller connected to daemon
            self.on_failed_to_lock("Controller not connected")
            return
        self.use_controller(self.controller)

        self._eh_ids += [
            (self.controller, self.controller.connect('event', self.on_event)),
            (self.controller,
             self.controller.connect('lost', self.on_controller_lost)),
        ]
        self.lock_inputs()

    def use_controller(self, controller):
        ccfg = self.config.get_controller_config(controller.get_id())
        self._control_with = ccfg[
            "menu_control"] if self.args.control_with == DEFAULT else self.args.control_with
        self._cancel_with = ccfg[
            "menu_cancel"] if self.args.cancel_with == DEFAULT else self.args.cancel_with

        if self.args.confirm_with == DEFAULT:
            self._confirm_with = ccfg["menu_confirm"]
        elif self.args.confirm_with == SAME:
            if self._control_with == RIGHT:
                self._confirm_with = SCButtons.RPADTOUCH.name
            else:
                self._confirm_with = SCButtons.LPADTOUCH.name
        else:
            self._confirm_with = self.args.confirm_with

        if self.args.use_cursor:
            # As special case, using LEFT pad on controller with
            # actual DPAD should not display cursor
            if self._control_with != LEFT or (controller.get_flags()
                                              & ControllerFlags.HAS_DPAD) == 0:
                self.enable_cursor()

        if self.args.feedback_amplitude:
            side = "LEFT"
            if self._control_with == "RIGHT":
                side = "RIGHT"
            elif self._control_with == "STICK":
                side = "BOTH"
            self.feedback = side, int(self.args.feedback_amplitude)

    def lock_inputs(self):
        def success(*a):
            log.error("Sucessfully locked input")

        locks = [self._control_with, self._confirm_with, self._cancel_with]
        self.controller.lock(success, self.on_failed_to_lock, *locks)

    def quit(self, code=-2):
        if not self._is_submenu:
            if self.get_controller():
                self.get_controller().unlock_all()
            for source, eid in self._eh_ids:
                source.disconnect(eid)
            self._eh_ids = []
        OSDWindow.quit(self, code)

    def next_item(self, direction):
        """ Selects next menu item, based on self._direction """
        start, i = -1, 0
        try:
            start = self.items.index(self._selected)
            i = start + direction
        except:
            pass
        while True:
            if i == start:
                # Cannot find valid menu item
                self.select(start)
                break
            if i >= len(self.items):
                i = 0
                continue
            if i < 0:
                i = len(self.items) - 1
                continue
            if self.select(i):
                # Not a separator
                break
            i += direction
            if start < 0: start = 0

    def on_submenu_closed(self, *a):
        self.set_name("osd-menu")
        if self._submenu.get_exit_code() in (0, -2):
            self._menuid = self._submenu._menuid
            self._selected = self._submenu._selected
            self.quit(self._submenu.get_exit_code())
        self._submenu = None

    def show_submenu(self, trash, trash2, trash3, menuitem):
        """ Called when user chooses menu item pointing to submenu """
        filename = find_menu(menuitem.filename)
        if filename:
            self._submenu = self.__class__()
            sub_pos = list(self.position)
            for i in (0, 1):
                sub_pos[i] = (sub_pos[i] - self.SUBMENU_OFFSET
                              if sub_pos[i] < 0 else sub_pos[i] +
                              self.SUBMENU_OFFSET)

            self._submenu.use_config(self.config)
            self._submenu.parse_argumets([
                "menu.py", "-x",
                str(sub_pos[0]), "-y",
                str(sub_pos[1]), "--from-file", filename, "--control-with",
                self._control_with, "--confirm-with", self._confirm_with,
                "--cancel-with", self._cancel_with
            ])
            self._submenu.set_is_submenu()
            self._submenu.use_daemon(self.daemon)
            self._submenu.use_controller(self.controller)
            self._submenu.controller = self.controller
            self._submenu.connect('destroy', self.on_submenu_closed)
            self._submenu.show()
            self.set_name("osd-menu-inactive")

    def _control_equals_cancel(self, daemon, x, y):
        """
		Called by on_event in that very special case when both confirm_with
		and cancel_with are set to STICK.
		
		Separated because RadialMenu overrides on_event and still
		needs to call this.
		
		Returns True if menu was canceled.
		"""
        distance = sqrt(x * x + y * y)
        if distance < STICK_PAD_MAX / 8:
            self.quit(-1)
            return True
        return False

    def on_stick_direction(self, trash, x, y):
        if y != 0:
            self.next_item(y)

    def on_event(self, daemon, what, data):
        if self._submenu:
            return self._submenu.on_event(daemon, what, data)
        if what == self._control_with:
            x, y = data
            if self._use_cursor:
                # Special case, both confirm_with and cancel_with
                # can be set to STICK
                if self._cancel_with == STICK and self._control_with == STICK:
                    if self._control_equals_cancel(daemon, x, y):
                        return

                pad_w = self.cursor.get_allocation().width * 0.5
                pad_h = self.cursor.get_allocation().height * 0.5
                max_w = self.get_allocation().width - 2 * pad_w
                max_h = self.get_allocation().height - 2 * pad_h

                x, y = circle_to_square(x / (STICK_PAD_MAX * 2.0),
                                        y / (STICK_PAD_MAX * 2.0))
                x = clamp(pad_w, (pad_w + max_w) * 0.5 + x * max_w,
                          max_w - pad_w)
                y = clamp(pad_h, (pad_h + max_h) * 0.5 + y * max_h * -1,
                          max_h - pad_h)
                self.f.move(self.cursor, int(x), int(y))

                for i in self.items:
                    if point_in_gtkrect(i.widget.get_allocation(), x, y):
                        self.select(self.items.index(i))
            else:
                self._scon.set_stick(x, y)
        elif what == self._cancel_with:
            if data[0] == 0:  # Button released
                self.quit(-1)
        elif what == self._confirm_with:
            if data[0] == 0:  # Button released
                if self._selected and self._selected.callback:
                    self._selected.callback(self, self.daemon, self.controller,
                                            self._selected)
                elif self._selected:
                    self.quit(0)
                else:
                    self.quit(-1)
Пример #20
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:
Пример #21
0
class Dialog(OSDWindow):
	EPILOG="""Exit codes:
   0  - clean exit, user selected option
  -1  - clean exit, user canceled dialog
   1  - error, invalid arguments
   2  - error, failed to access sc-daemon, sc-daemon reported error or died while dialog is displayed.
   3  - erorr, failed to lock input stick, pad or button(s)
	"""
	
	def __init__(self, cls="osd-menu"):
		self._buttons = None
		self._text = None
		
		OSDWindow.__init__(self, cls)
		self.daemon = None
		self.config = None
		self.feedback = None
		self.controller = None
		self.xdisplay = X.Display(hash(GdkX11.x11_get_default_xdisplay()))	# Magic
		
		self.parent = self.create_parent()
		self.f = Gtk.Fixed()
		self.f.add(self.parent)
		self.add(self.f)
		
		self._scon = StickController()
		self._scon.connect("direction", self.on_stick_direction)
		self._selected = None
		self._eh_ids = []
	
	
	def create_parent(self):
		self._text = Gtk.Label()
		self._buttons = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
		dialog = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
		dialog.pack_start(self._text, True, True, 0)
		dialog.pack_start(self._buttons, True, True, 0)
		
		dialog.set_name("osd-dialog")
		self._buttons.set_name("osd-dialog-buttons")
		self._text.set_name("osd-dialog-text")
		return dialog
	
	
	def pack_items(self, parent, items):
		for item in items:
			if hasattr(item.widget, "set_alignment"):
				item.widget.set_alignment(0.5, 0.5)
			self._buttons.pack_end(item.widget, True, True, 0)
	
	
	def use_daemon(self, d):
		"""
		Allows (re)using already existing DaemonManager instance in same process.
		use_config() should be be called before parse_argumets() if this is used.
		"""
		self.daemon = d
		self._connect_handlers()
		self.on_daemon_connected(self.daemon)
	
	
	def use_config(self, c):
		"""
		Allows reusing already existin Config instance in same process.
		Has to be called before parse_argumets()
		"""
		self.config = c
	
	
	def get_menuid(self):
		# Just to be compatibile with menus when called from scc-osd-daemon
		return None
	
	
	def get_selected_item_id(self):
		"""
		Returns ID of selected item or None if nothing is selected.
		"""
		if self._selected:
			return self._selected.id
		return None
	
	
	def _add_arguments(self):
		OSDWindow._add_arguments(self)
		self.argparser.add_argument('--confirm-with', type=str,
			metavar="button", default=DEFAULT,
			help="button used to confirm choice")
		self.argparser.add_argument('--cancel-with', type=str,
			metavar="button", default=DEFAULT,
			help="button used to cancel dialog")
		self.argparser.add_argument('--feedback-amplitude', type=int,
			help="enables and sets power of feedback effect generated when active menu option is changed")
		self.argparser.add_argument('--text', type=str, metavar='text',
			help="Dialog text")
		self.argparser.add_argument('items', type=str, nargs='*', metavar='id text',
			help="Dialog buttons")
	
	
	def parse_argumets(self, argv):
		if not OSDWindow.parse_argumets(self, argv):
			return False
		if not self.config:
			self.config = Config()
		
		try:
			self.items = MenuData.from_args(self.args.items)
			self._menuid = None
		except ValueError:
			print >>sys.stderr, '%s: error: invalid number of arguments' % (sys.argv[0])
			return False
		
		self._text.set_label(self.args.text)
		
		if self.args.feedback_amplitude:
			side = "LEFT"
			self.feedback = side, int(self.args.feedback_amplitude)
		
		# Create buttons that are displayed on screen
		items = self.items.generate(self)
		self.items = []
		for item in items:
			item.widget = self.generate_widget(item)
			if item.widget is not None:
				self.items.append(item)
		self.pack_items(self.parent, self.items)
		if len(self.items) == 0:
			print >>sys.stderr, '%s: error: no items in menu' % (sys.argv[0])
			return False
		
		return True
	
	
	def generate_widget(self, item):
		""" Generates gtk widget for specified menutitem """
		widget = Gtk.Button.new_with_label(item.label)
		widget.set_relief(Gtk.ReliefStyle.NONE)
		if hasattr(widget.get_children()[0], "set_xalign"):
			widget.get_children()[0].set_xalign(0)
		else:
			widget.get_children()[0].set_halign(Gtk.Align.START)
		widget.set_name("osd-menu-item")
		
		return widget
	
	
	def select(self, index):
		if self._selected:
			self._selected.widget.set_name(self._selected.widget.get_name()
				.replace("-selected", ""))
		if self.items[index].id:
			if self._selected != self.items[index]:
				if self.feedback and self.controller:
					self.controller.feedback(*self.feedback)
			self._selected = self.items[index]
			self._selected.widget.set_name(
					self._selected.widget.get_name() + "-selected")
			return True
		return False
	
	
	def _connect_handlers(self):
		self._eh_ids += [
			(self.daemon, self.daemon.connect('dead', self.on_daemon_died)),
			(self.daemon, self.daemon.connect('error', self.on_daemon_died)),
			(self.daemon, self.daemon.connect('alive', self.on_daemon_connected)),
		]
	
	
	def run(self):
		self.daemon = DaemonManager()
		self._connect_handlers()
		OSDWindow.run(self)
	
	
	def show(self, *a):
		if not self.select(0):
			self.next_item(1)
		OSDWindow.show(self, *a)
	
	
	def on_daemon_died(self, *a):
		log.error("Daemon died")
		self.quit(2)
	
	
	def on_failed_to_lock(self, error):
		log.error("Failed to lock input: %s", error)
		self.quit(3)
	
	
	def on_daemon_connected(self, *a):
		def success(*a):
			log.error("Sucessfully locked input")
		
		if not self.config:
			self.config = Config()
		self.controller = self.choose_controller(self.daemon)
		if self.controller is None or not self.controller.is_connected():
			# There is no controller connected to daemon
			self.on_failed_to_lock("Controller not connected")
			return
		
		ccfg = self.config.get_controller_config(self.controller.get_id())
		self._control_with = ccfg["menu_control"]
		self._confirm_with = ccfg["menu_confirm"] if self.args.confirm_with == DEFAULT else self.args.confirm_with
		self._cancel_with = ccfg["menu_cancel"] if self.args.cancel_with == DEFAULT else self.args.cancel_with
		
		self._eh_ids += [ (self.controller, self.controller.connect('event', self.on_event)) ]
		locks = [ self._control_with, self._confirm_with, self._cancel_with ]
		self.controller.lock(success, self.on_failed_to_lock, *locks)
	
	
	def quit(self, code=-2):
		if self.get_controller():
			self.get_controller().unlock_all()
		for source, eid in self._eh_ids:
			source.disconnect(eid)
		self._eh_ids = []
		OSDWindow.quit(self, code)
	
	
	def next_item(self, direction):
		""" Selects next menu item, based on self._direction """
		start, i = -1, 0
		try:
			start = self.items.index(self._selected)
			i = start + direction
		except: pass
		while True:
			if i == start:
				# Cannot find valid menu item
				self.select(start)
				break
			if i >= len(self.items):
				i = 0
				continue
			if i < 0:
				i = len(self.items) - 1
				continue
			if self.select(i): break
			i += direction
			if start < 0: start = 0
	
	
	def on_stick_direction(self, trash, x, y):
		if x != 0:
			self.next_item(x)
	
	
	def on_event(self, daemon, what, data):
		if what == self._control_with:
			self._scon.set_stick(*data)
		elif what == self._cancel_with:
			if data[0] == 0:	# Button released
				self.quit(-1)
		elif what == self._confirm_with:
			if data[0] == 0:	# Button released
				if self._selected and self._selected.callback:
					self._selected.callback(self, self.daemon, self.controller, self._selected)
				elif self._selected:
					self.quit(0)
				else:
					self.quit(-1)
Пример #22
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"
	
	def __init__(self, gladepath="/usr/share/scc",
						imagepath="/usr/share/scc/images"):
		Gtk.Application.__init__(self,
				application_id="me.kozec.scc",
				flags=Gio.ApplicationFlags.HANDLES_COMMAND_LINE | Gio.ApplicationFlags.NON_UNIQUE )
		UserDataManager.__init__(self)
		BindingEditor.__init__(self, self)
		# Setup Gtk.Application
		self.setup_commandline()
		# Setup DaemonManager
		self.dm = DaemonManager()
		self.dm.connect("alive", self.on_daemon_alive)
		self.dm.connect("controller-count-changed", self.on_daemon_ccunt_changed)
		self.dm.connect("dead", self.on_daemon_dead)
		self.dm.connect("error", self.on_daemon_error)
		self.dm.connect('reconfigured', self.on_daemon_reconfigured),
		self.dm.connect("version", self.on_daemon_version)
		# Set variables
		self.config = Config()
		self.gladepath = gladepath
		self.imagepath = imagepath
		self.builder = None
		self.recursing = False
		self.statusicon = None
		self.status = "unknown"
		self.context_menu_for = None
		self.daemon_changed_profile = False
		self.background = None
		self.outdated_version = None
		self.profile_switchers = []
		self.current_file = None	# Currently edited file
		self.controller_count = 0
		self.current = Profile(GuiActionParser())
		self.just_started = True
		self.button_widgets = {}
		self.hilights = { App.HILIGHT_COLOR : set(), App.OBSERVE_COLOR : set() }
		self.undo = []
		self.redo = []
	
	
	def setup_widgets(self):
		# Important stuff
		self.builder = Gtk.Builder()
		self.builder.add_from_file(os.path.join(self.gladepath, "app.glade"))
		self.builder.connect_signals(self)
		self.window = self.builder.get_object("window")
		self.add_window(self.window)
		self.window.set_title(_("SC Controller"))
		self.window.set_wmclass("SC Controller", "SC Controller")
		self.ribar = None
		self.create_binding_buttons()
		
		ps = self.add_switcher(10, 10)
		ps.set_allow_new(True)
		ps.set_profile(self.load_profile_selection())
		ps.connect('new-clicked', self.on_new_clicked)
		ps.connect('save-clicked', self.on_save_clicked)
		
		# Drag&drop target
		self.builder.get_object("content").drag_dest_set(Gtk.DestDefaults.ALL, [
			Gtk.TargetEntry.new("text/uri-list", Gtk.TargetFlags.OTHER_APP, 0)
			], Gdk.DragAction.COPY
		)
		
		# 'C' button
		vbc = self.builder.get_object("vbC")
		self.main_area = self.builder.get_object("mainArea")
		vbc.get_parent().remove(vbc)
		vbc.connect('size-allocate', self.on_vbc_allocated)
		
		# Background
		self.background = SVGWidget(self, os.path.join(self.imagepath, self.IMAGE))
		self.background.connect('hover', self.on_background_area_hover)
		self.background.connect('leave', self.on_background_area_hover, None)
		self.background.connect('click', self.on_background_area_click)
		self.main_area.put(self.background, 0, 0)
		self.main_area.put(vbc, 0, 0) # (self.IMAGE_SIZE[0] / 2) - 90, self.IMAGE_SIZE[1] - 100)
		
		# Test markers (those blue circles over PADs and sticks)
		self.lpadTest = Gtk.Image.new_from_file(os.path.join(self.imagepath, "test-cursor.svg"))
		self.rpadTest = Gtk.Image.new_from_file(os.path.join(self.imagepath, "test-cursor.svg"))
		self.stickTest = Gtk.Image.new_from_file(os.path.join(self.imagepath, "test-cursor.svg"))
		self.main_area.put(self.lpadTest, 40, 40)
		self.main_area.put(self.rpadTest, 290, 90)
		self.main_area.put(self.stickTest, 150, 40)
		
		# Headerbar
		headerbar(self.builder.get_object("hbWindow"))
	
	
	def setup_statusicon(self):
		menu = self.builder.get_object("mnuDaemon")
		self.statusicon = get_status_icon(self.imagepath, menu)
		self.statusicon.connect('clicked', self.on_statusicon_clicked)
		GLib.idle_add(self.statusicon.set, "scc-%s" % (self.status,), _("SC-Controller"))
	
	
	def destroy_statusicon(self):
		self.statusicon.destroy()
		self.statusicon = None
	
	
	def check(self):
		""" Performs various (three) checks and reports possible problems """
		# TODO: Maybe not best place to do this
		try:
			kernel_mods = [ line.split(" ")[0] for line in file("/proc/modules", "r").read().split("\n") ]
		except Exception:
			# Maybe running on BSD or Windows...
			kernel_mods = [ ]
		
		if len(kernel_mods) > 0 and "uinput" not in kernel_mods:
			# There is no uinput
			msg = _('uinput kernel module not loaded')
			msg += "\n\n" + _('Please, consult your distribution manual on how to enable uinput')
			self.show_error(msg)
		elif not os.path.exists("/dev/uinput"):
			# /dev/uinput missing
			msg = _('/dev/uinput doesn\'t exists')
			msg += "\n" + _('uinput kernel module is loaded, but /dev/uinput is missing.')
			#msg += "\n\n" + _('Please, consult your distribution manual on what in the world could cause this.')
			msg += "\n\n" + _('Please, consult your distribution manual on how to enable uinput')
			self.show_error(msg)
		elif not check_access("/dev/uinput"):
			# Cannot acces uinput
			msg = _('You don\'t have required access to /dev/uinput.')
			msg += "\n" + _('This will most likely prevent emulation from working.')
			msg += "\n\n" + _('Please, consult your distribution manual on how to enable uinput')
			self.show_error(msg)
	
	
	def hilight(self, button):
		""" Hilights specified button on background image """
		if button:
			self.hilights[App.HILIGHT_COLOR] = set([button])
		else:
			self.hilights[App.HILIGHT_COLOR] = set()
		self._update_background()
	
	
	def _update_background(self):
		h = {}
		for color in self.hilights:
			for i in self.hilights[color]:
				h[i] = color
		self.background.hilight(h)
	
	
	def hint(self, button):
		""" As hilight, but marks GTK Button as well """
		active = None
		for b in self.button_widgets.values():
			b.widget.set_state(Gtk.StateType.NORMAL)
			if b.name == button:
				active = b.widget
		
		if active is not None:
			active.set_state(Gtk.StateType.ACTIVE)
		
		self.hilight(button)
	
	
	def show_editor(self, id):
		action = self.get_action(self.current, id)
		ae = self.choose_editor(action, "")
		ae.set_input(id, action)
		ae.show(self.window)
	
	
	def show_context_menu(self, for_id):
		""" Sets sensitivity of popup menu items and displays it on screen """
		mnuPopup = self.builder.get_object("mnuPopup")
		mnuCopy = self.builder.get_object("mnuCopy")
		mnuClear = self.builder.get_object("mnuClear")
		mnuPaste = self.builder.get_object("mnuPaste")
		self.context_menu_for = for_id
		clp = Gtk.Clipboard.get_default(Gdk.Display.get_default())
		mnuCopy.set_sensitive(bool(self.get_action(self.current, for_id)))
		mnuClear.set_sensitive(bool(self.get_action(self.current, for_id)))
		mnuPaste.set_sensitive(clp.wait_is_text_available())
		mnuPopup.popup(None, None, None, None,
			3, Gtk.get_current_event_time())
	
	
	def save_config(self):
		self.config.save()
		self.dm.reconfigure()
		self.enable_test_mode()
	
	
	def on_statusicon_clicked(self, *a):
		""" Handler for user clicking on tray icon button """
		self.window.set_visible(not self.window.get_visible())
	
	
	def on_window_delete_event(self, *a):
		""" Called when user tries to close window """
		if not IS_UNITY and self.config['gui']['enable_status_icon'] and self.config['gui']['minimize_to_status_icon']:
			# Override closing and hide instead
			self.window.set_visible(False)
			return True
		return False # Allow
	
	
	def on_mnuClear_activate(self, *a):
		"""
		Handler for 'Clear' context menu item.
		Simply sets NoAction to input.
		"""
		self.on_action_chosen(self.context_menu_for, NoAction())
	
	
	def on_mnuCopy_activate(self, *a):
		"""
		Handler for 'Copy' context menu item.
		Converts action to string and sends that string to clipboard.
		"""
		a = self.get_action(self.current, self.context_menu_for)
		if a:
			if a.name:
				a = NameModifier(a.name, a)
			clp = Gtk.Clipboard.get_default(Gdk.Display.get_default())
			clp.set_text(a.to_string().encode('utf-8'), -1)
			clp.store()
	
	
	def on_mnuPaste_activate(self, *a):
		"""
		Handler for 'Paste' context menu item.
		Reads string from clipboard, parses it as action and sets that action
		on selected input.
		"""
		clp = Gtk.Clipboard.get_default(Gdk.Display.get_default())
		text = clp.wait_for_text()
		if text:
			a = GuiActionParser().restart(text.decode('utf-8')).parse()
			if not isinstance(a, InvalidAction):
				self.on_action_chosen(self.context_menu_for, a)
		
	
	def on_mnuGlobalSettings_activate(self, *a):
		from scc.gui.global_settings import GlobalSettings
		gs = GlobalSettings(self)
		gs.show(self.window)
	
	
	def on_mnuImport_activate(self, *a):
		"""
		Handler for 'Import Steam Profile' context menu item.
		Displays apropriate dialog.
		"""
		from scc.gui.import_dialog import ImportDialog
		gs = ImportDialog(self)
		gs.show(self.window)
	
	
	def on_btUndo_clicked(self, *a):
		if len(self.undo) < 1: return
		undo, self.undo = self.undo[-1], self.undo[0:-1]
		self.set_action(self.current, undo.id, undo.before)
		self.redo.append(undo)
		self.builder.get_object("btRedo").set_sensitive(True)
		if len(self.undo) < 1:
			self.builder.get_object("btUndo").set_sensitive(False)
		self.on_profile_modified()
	
	
	def on_btRedo_clicked(self, *a):
		if len(self.redo) < 1: return
		redo, self.redo = self.redo[-1], self.redo[0:-1]
		self.set_action(self.current, redo.id, redo.after)
		self.undo.append(redo)
		self.builder.get_object("btUndo").set_sensitive(True)
		if len(self.redo) < 1:
			self.builder.get_object("btRedo").set_sensitive(False)
		self.on_profile_modified()
	
	
	def on_profiles_loaded(self, profiles):
		for ps in self.profile_switchers:
			ps.set_profile_list(profiles)
	
	
	def on_dlgNewProfile_delete_event(self, dlg, *a):
		dlg.hide()
		return True
	
	
	def on_btNewProfile_clicked(self, *a):
		""" Called when new profile name is set and OK is clicked """
		txNewProfile = self.builder.get_object("txNewProfile")
		dlg = self.builder.get_object("dlgNewProfile")
		self.new_profile(self.current, txNewProfile.get_text())
		dlg.hide()
	
	
	def on_profile_modified(self, *a):
		"""
		Called when selected profile is modified in memory.
		"""
		self.profile_switchers[0].set_profile_modified(True)
		
		if not self.current_file.get_path().endswith(".mod"):
			mod = self.current_file.get_path() + ".mod"
			self.current_file = Gio.File.new_for_path(mod)
		
		self.save_profile(self.current_file, self.current)
	
	
	def on_profile_loaded(self, profile, giofile):
		self.current = profile
		self.current_file = giofile
		self.profile_switchers[0].set_profile_modified(False)
		for b in self.button_widgets.values():
			b.update()
	
	
	def on_profile_selected(self, ps, name, giofile):
		if ps == self.profile_switchers[0]:
			self.load_profile(giofile)
		if ps.get_controller():
			ps.get_controller().set_profile(giofile.get_path())
	
	
	def on_unknown_profile(self, ps, name):
		log.warn("Daemon reported unknown profile: '%s'; Overriding.", name)
		if self.current_file is not None:
			ps.get_controller().set_profile(self.current_file.get_path())
	
	
	def on_save_clicked(self, *a):
		if self.current_file.get_path().endswith(".mod"):
			orig = self.current_file.get_path()[0:-4]
			self.current_file = Gio.File.new_for_path(orig)
		
		self.save_profile(self.current_file, self.current)
	
	
	def on_profile_saved(self, giofile, send=True):
		"""
		Called when selected profile is saved to disk
		"""
		if giofile.get_path().endswith(".mod"):
			# Special case, this one is saved only to be sent to daemon
			# and user doesn't need to know about it
			if self.dm.is_alive():
				self.dm.set_profile(giofile.get_path())
			return
		
		self.profile_switchers[0].set_profile_modified(False)
		if send and self.dm.is_alive() and not self.daemon_changed_profile:
			self.dm.set_profile(giofile.get_path())
		
		self.current_file = giofile	
	
	
	def on_new_clicked(self, ps, name):
		new_name = _("Copy of %s") % (name,)
		filename = os.path.join(get_profiles_path(), new_name + ".sccprofile")
		i = 0
		while os.path.exists(filename):
			i += 1
			new_name = _("Copy of %s (%s)") % (name, i)
			filename = os.path.join(get_profiles_path(), new_name + ".sccprofile")
		
		dlg = self.builder.get_object("dlgNewProfile")
		txNewProfile = self.builder.get_object("txNewProfile")
		txNewProfile.set_text(new_name)
		dlg.set_transient_for(self.window)
		dlg.show()
	
	
	def on_action_chosen(self, id, action):
		before = self.set_action(self.current, id, action)
		if before.to_string() != action.to_string():
			# TODO: Maybe better comparison
			self.undo.append(UndoRedo(id, before, action))
			self.builder.get_object("btUndo").set_sensitive(True)
		self.on_profile_modified()
	
	
	def on_background_area_hover(self, trash, area):
		self.hint(area)
	
	
	def on_background_area_click(self, trash, area):
		if area in [ x.name for x in BUTTONS ]:
			self.hint(None)
			self.show_editor(getattr(SCButtons, area))
		elif area in TRIGGERS + STICKS + PADS:
			self.hint(None)
			self.show_editor(area)
	
	
	def on_vbc_allocated(self, vbc, allocation):
		"""
		Called when size of 'Button C' is changed. Centers button
		on background image
		"""
		main_area = self.builder.get_object("mainArea")
		x = (main_area.get_allocation().width - allocation.width) / 2
		y = main_area.get_allocation().height - allocation.height
		main_area.move(vbc, x, y)
	
	
	def on_ebImage_motion_notify_event(self, box, event):
		self.background.on_mouse_moved(event.x, event.y)
	
	
	def on_mnuExit_activate(self, *a):
		self.quit()
	
	
	def on_mnuAbout_activate(self, *a):
		from scc.gui.aboutdialog import AboutDialog
		AboutDialog(self).show(self.window)
	
	
	def on_daemon_alive(self, *a):
		self.set_daemon_status("alive", True)
		self.hide_error()
		self.just_started = False
		if self.profile_switchers[0].get_file() is not None and not self.just_started:
			self.dm.set_profile(self.current_file.get_path())
		GLib.timeout_add_seconds(1, self.check)
		self.enable_test_mode()
	
	
	def on_daemon_ccunt_changed(self, daemon, count):
		if (self.controller_count, count) == (0, 1):
			# First controller connected
			# 
			# 'event' signal should be connected only on first controller,
			# so this block is executed only when number of connected
			# controllers changes from 0 to 1
			c = self.dm.get_controllers()[0]
			c.connect('event', self.on_daemon_event_observer)
		elif count > self.controller_count:
			# Controller added
			while len(self.profile_switchers) < count:
				s = self.add_switcher()
		elif count < self.controller_count:
			# Controller removed
			while len(self.profile_switchers) > max(1, count):
				s = self.profile_switchers.pop()
				s.set_controller(None)
				self.remove_switcher(s)
		
		# Assign controllers to widgets
		for i in xrange(0, count):
			c = self.dm.get_controllers()[i]
			self.profile_switchers[i].set_controller(c)
		
		if count < 1:
			# Special case, no controllers are connected, but one widget
			# has to stay on screen
			self.profile_switchers[0].set_controller(None)
		
		self.controller_count = count
	
	
	def new_profile(self, profile, name):
		filename = os.path.join(get_profiles_path(), name + ".sccprofile")
		self.current_file = Gio.File.new_for_path(filename)
		self.save_profile(self.current_file, profile)
	
	
	def add_switcher(self, margin_left=30, margin_right=40, margin_bottom=2):
		"""
		Adds new profile switcher widgets on top of window. Called
		when new controller is connected to daemon.
		
		Returns generated ProfileSwitcher instance.
		"""
		vbAllProfiles = self.builder.get_object("vbAllProfiles")
		
		ps = ProfileSwitcher(self.imagepath, self.config)
		ps.set_margin_left(margin_left)
		ps.set_margin_right(margin_right)
		ps.set_margin_bottom(margin_bottom)
		ps.connect('right-clicked', self.on_profile_right_clicked)
		
		vbAllProfiles.pack_start(ps, False, False, 0)
		vbAllProfiles.reorder_child(ps, 0)
		vbAllProfiles.show_all()
		
		if len(self.profile_switchers) > 0:
			ps.set_profile_list(self.profile_switchers[0].get_profile_list())
		
		self.profile_switchers.append(ps)
		ps.connect('changed', self.on_profile_selected)
		ps.connect('unknown-profile', self.on_unknown_profile)
		return ps
	
	
	def remove_switcher(self, s):
		"""
		Removes given profile switcher from UI.
		"""
		vbAllProfiles = self.builder.get_object("vbAllProfiles")
		vbAllProfiles.remove(s)
		s.destroy()
	
	
	def enable_test_mode(self):
		"""
		Disables and re-enables Input Test mode. If sniffing is disabled in
		daemon configuration, 2nd call fails and logs error.
		"""
		if self.dm.is_alive():
			try:
				c = self.dm.get_controllers()[0]
			except IndexError:
				# Zero controllers
				return
			c.unlock_all()
			c.observe(DaemonManager.nocallback, self.on_observe_failed,
				'A', 'B', 'C', 'X', 'Y', 'START', 'BACK', 'LB', 'RB',
				'LPAD', 'RPAD', 'LGRIP', 'RGRIP', 'LT', 'RT', 'LEFT',
				'RIGHT', 'STICK', 'STICKPRESS')
	
	
	def on_observe_failed(self, error):
		log.debug("Failed to enable test mode: %s", error)
	
	
	def on_daemon_version(self, daemon, version):
		"""
		Checks if reported version matches expected one.
		If not, daemon is restarted.
		"""
		if version != DAEMON_VERSION and self.outdated_version != version:
			log.warning(
				"Running daemon instance is too old (version %s, expected %s). Restarting...",
				version, DAEMON_VERSION)
			self.outdated_version = version
			self.set_daemon_status("unknown", False)
			self.dm.restart()
	
	
	def on_daemon_error(self, daemon, error):
		log.debug("Daemon reported error '%s'", error)
		msg = _('There was an error with enabling emulation: <b>%s</b>') % (error,)
		# Known errors are handled with aditional message
		if "Device not found" in error:
			msg += "\n" + _("Please, check if you have reciever dongle connected to USB port.")
		elif "LIBUSB_ERROR_ACCESS" in error:
			msg += "\n" + _("You don't have access to controller device.")
			msg += "\n\n" + ( _("Consult your distribution manual, try installing Steam package or <a href='%s'>install required udev rules manually</a>.") %
					'https://wiki.archlinux.org/index.php/Gamepad#Steam_Controller_Not_Pairing' )
			# TODO: Write howto somewhere instead of linking to ArchWiki
		elif "LIBUSB_ERROR_BUSY" in error:
			msg += "\n" + _("Another application (most likely Steam) is using the controller.")
		elif "LIBUSB_ERROR_PIPE" in error:
			msg += "\n" + _("USB dongle was removed.")
		
		self.show_error(msg)
		self.set_daemon_status("error", True)
	
	
	def on_daemon_event_observer(self, daemon, what, data):
		if what in (LEFT, RIGHT, STICK):
			widget, area = {
				LEFT  : (self.lpadTest,  "LPADTEST"),
				RIGHT : (self.rpadTest,  "RPADTEST"),
				STICK : (self.stickTest, "STICKTEST"),
			}[what]
			# Check if stick or pad is released
			if data[0] == data[1] == 0:
				widget.hide()
				return
			if not widget.is_visible():
				widget.show()
			# Grab values
			ax, ay, aw, trash = self.background.get_area_position(area)
			cw = widget.get_allocation().width
			# Compute center
			x, y = ax + aw * 0.5 - cw * 0.5, ay + 1.0 - cw * 0.5
			# Add pad position
			x += data[0] * aw / STICK_PAD_MAX * 0.5
			y -= data[1] * aw / STICK_PAD_MAX * 0.5
			# Move circle
			self.main_area.move(widget, x, y)
		elif what in ("LT", "RT", "STICKPRESS"):
			what = {
				"LT" : "LEFT",
				"RT" : "RIGHT",
				"STICKPRESS" : "STICK"
			}[what]
			if data[0]:
				self.hilights[App.OBSERVE_COLOR].add(what)
			else:
				self.hilights[App.OBSERVE_COLOR].remove(what)
			self._update_background()
		elif hasattr(SCButtons, what):
			if data[0]:
				self.hilights[App.OBSERVE_COLOR].add(what)
			else:
				self.hilights[App.OBSERVE_COLOR].remove(what)
			self._update_background()
		else:
			print "event", what
	
	
	def on_profile_right_clicked(self, ps):
		for name in ("mnuConfigureController", "mnuTurnoffController"):
			# Disable controller-related menu items if controller is not connected
			obj = self.builder.get_object(name)
			obj.set_sensitive(ps.get_controller() is not None)
		
		mnuPS = self.builder.get_object("mnuPS")
		mnuPS.ps = ps
		mnuPS.popup(None, None, None, None,
			3, Gtk.get_current_event_time())
	
	
	def on_mnuConfigureController_activate(self, *a):
		from scc.gui.controller_settings import ControllerSettings
		mnuPS = self.builder.get_object("mnuPS")
		cs = ControllerSettings(self, mnuPS.ps.get_controller(), mnuPS.ps)
		cs.show(self.window)
	
	
	def mnuTurnoffController_activate(self, *a):
		mnuPS = self.builder.get_object("mnuPS")
		if mnuPS.ps.get_controller():
			mnuPS.ps.get_controller().turnoff()
	
	def show_error(self, message):
		if self.ribar is None:
			self.ribar = RIBar(message, Gtk.MessageType.ERROR)
			content = self.builder.get_object("content")
			content.pack_start(self.ribar, False, False, 1)
			content.reorder_child(self.ribar, 0)
			self.ribar.connect("close", self.hide_error)
			self.ribar.connect("response", self.hide_error)
		else:
			self.ribar.get_label().set_markup(message)
		self.ribar.show()
		self.ribar.set_reveal_child(True)
	
	
	def hide_error(self, *a):
		if self.ribar is not None:
			if self.ribar.get_parent() is not None:
				self.ribar.get_parent().remove(self.ribar)
		self.ribar = None
	
	
	def on_daemon_reconfigured(self, *a):
		log.debug("Reloading config...")
		self.config.reload()
		for ps in self.profile_switchers:
			ps.set_controller(ps.get_controller())
	
	
	def on_daemon_dead(self, *a):
		if self.just_started:
			self.dm.restart()
			self.just_started = False
			self.set_daemon_status("unknown", True)
			return
		
		for ps in self.profile_switchers:
			ps.set_controller(None)
			ps.on_daemon_dead()
		self.set_daemon_status("dead", False)
	
	
	def on_mnuEmulationEnabled_toggled(self, cb):
		if self.recursing : return
		if cb.get_active():
			# Turning daemon on
			self.set_daemon_status("unknown", True)
			cb.set_sensitive(False)
			self.dm.start()
		else:
			# Turning daemon off
			self.set_daemon_status("unknown", False)
			cb.set_sensitive(False)
			self.hide_error()
			self.dm.stop()
			
	
	def do_startup(self, *a):
		Gtk.Application.do_startup(self, *a)
		self.load_profile_list()
		self.setup_widgets()
		if self.app.config['gui']['enable_status_icon']:
			self.setup_statusicon()
		self.set_daemon_status("unknown", True)
	
	
	def do_local_options(self, trash, lo):
		set_logging_level(lo.contains("verbose"), lo.contains("debug") )
		return -1
	
	
	def do_command_line(self, cl):
		Gtk.Application.do_command_line(self, cl)
		if len(cl.get_arguments()) > 1:
			filename = cl.get_arguments()[-1]
			giofile = Gio.File.new_for_path(filename)
			# Local file, looks like vdf profile
			from scc.gui.import_dialog import ImportDialog
			gs = ImportDialog(self)
			def i_told_you_to_quit(*a):
				sys.exit(0)
			gs.window.connect('destroy', i_told_you_to_quit)
			gs.show(self.window)
			# Skip first screen and try to import this file
			gs.on_preload_finished(gs.set_file, giofile.get_path())
		else:
			self.activate()
		return 0
	
	
	def do_activate(self, *a):
		self.builder.get_object("window").show()
	
	
	def remove_dot_profile(self):
		"""
		Checks if first profile in list begins with dot and if yes, removes it.
		This is done to undo automatic addition that is done when daemon reports
		selecting such profile.
		"""
		cb = self.builder.get_object("cbProfile")
		model = cb.get_model()
		if len(model) == 0:
			# Nothing to remove
			return
		if not model[0][0].startswith("."):
			# Not dot profile
			return
		active = model.get_path(cb.get_active_iter())
		first = model[0].path
		if active == first:
			# Can't remove active item
			return
		model.remove(model[0].iter)
	
	
	def set_daemon_status(self, status, daemon_runs):
		""" Updates image that shows daemon status and menu shown when image is clicked """
		log.debug("daemon status: %s", status)
		icon = os.path.join(self.imagepath, "scc-%s.svg" % (status,))
		imgDaemonStatus = self.builder.get_object("imgDaemonStatus")
		btDaemon = self.builder.get_object("btDaemon")
		mnuEmulationEnabled = self.builder.get_object("mnuEmulationEnabled")
		imgDaemonStatus.set_from_file(icon)
		mnuEmulationEnabled.set_sensitive(True)
		self.window.set_icon_from_file(icon)
		self.status = status
		if self.statusicon:
			GLib.idle_add(self.statusicon.set, "scc-%s" % (self.status,), _("SC-Controller"))
		self.recursing = True
		if status == "alive":
			btDaemon.set_tooltip_text(_("Emulation is active"))
		elif status == "error":
			btDaemon.set_tooltip_text(_("Error enabling emulation"))
		elif status == "dead":
			btDaemon.set_tooltip_text(_("Emulation is inactive"))
		else:
			btDaemon.set_tooltip_text(_("Checking emulation status..."))
		mnuEmulationEnabled.set_active(daemon_runs)
		self.recursing = False
	
	
	def setup_commandline(self):
		def aso(long_name, short_name, description,
				arg=GLib.OptionArg.NONE,
				flags=GLib.OptionFlags.IN_MAIN):
			""" add_simple_option, adds program argument in simple way """
			o = GLib.OptionEntry()
			if short_name:
				o.long_name = long_name
				o.short_name = short_name
			o.description = description
			o.flags = flags
			o.arg = arg
			self.add_main_option_entries([o])
		
		self.connect('handle-local-options', self.do_local_options)
		
		aso("verbose",	b"v", "Be verbose")
		aso("debug",	b"d", "Be more verbose (debug mode)")
	
	
	def save_profile_selection(self, path):
		""" Saves name of profile into config file """
		name = os.path.split(path)[-1]
		if name.endswith(".sccprofile"):
			name = name[0:-11]
		
		data = dict(current_profile=name)
		jstr = json.dumps(data, sort_keys=True, indent=4)
		
		open(os.path.join(get_config_path(), self.CONFIG), "w").write(jstr)
	
	
	def load_profile_selection(self):
		""" Returns name profile from config file or None if there is none saved """
		try:
			data = json.loads(open(os.path.join(get_config_path(), self.CONFIG), "r").read())
			return data['current_profile']
		except:
			return None
	
	
	def on_drag_data_received(self, widget, context, x, y, data, info, time):
		""" Drag-n-drop handler """
		if str(data.get_data_type()) == "text/uri-list":
			# Only file can be dropped here
			if len(data.get_uris()):
				uri = data.get_uris()[0]
				giofile = Gio.File.new_for_uri(uri)
				if giofile.get_path():
					path = giofile.get_path()
					if path.endswith(".vdf") or path.endswith(".vdffz"):
						# Local file, looks like vdf profile
						from scc.gui.import_dialog import ImportDialog
						gs = ImportDialog(self)
						gs.show(self.window)
						# Skip first screen and try to import this file
						gs.on_preload_finished(gs.set_file, giofile.get_path())
Пример #23
0
class Menu(OSDWindow):
	EPILOG="""Exit codes:
   0  - clean exit, user selected option
  -1  - clean exit, user canceled menu
  -2  - clean exit, menu closed from callback method
   1  - error, invalid arguments
   2  - error, failed to access sc-daemon, sc-daemon reported error or died while menu is displayed.
   3  - erorr, failed to lock input stick, pad or button(s)
	"""
	SUBMENU_OFFSET = 50
	PREFER_BW_ICONS = True
	
	
	def __init__(self, cls="osd-menu"):
		OSDWindow.__init__(self, cls)
		self.daemon = None
		self.config = None
		self.feedback = None
		self.controller = None
		self.xdisplay = X.Display(hash(GdkX11.x11_get_default_xdisplay()))	# Magic
		
		cursor = os.path.join(get_share_path(), "images", 'menu-cursor.svg')
		self.cursor = Gtk.Image.new_from_file(cursor)
		self.cursor.set_name("osd-menu-cursor")
		
		self.parent = self.create_parent()
		self.f = Gtk.Fixed()
		self.f.add(self.parent)
		self.add(self.f)
		
		self._submenu = None
		self._scon = StickController()
		self._scon.connect("direction", self.on_stick_direction)
		self._is_submenu = False
		self._selected = None
		self._menuid = None
		self._use_cursor = False
		self._eh_ids = []
		self._control_with = STICK
		self._control_with_dpad = False
		self._confirm_with = 'A'
		self._cancel_with = 'B'
	
	
	def set_is_submenu(self):
		"""
		Marks menu as submenu. This changes behaviour of some methods,
		especially disables (un)locking of input stick and buttons.
		"""
		self._is_submenu = True
	
	
	def create_parent(self):
		v = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
		v.set_name("osd-menu")
		return v
	
	
	def pack_items(self, parent, items):
		for item in items:
			parent.pack_start(item.widget, True, True, 0)
	
	
	def use_daemon(self, d):
		"""
		Allows (re)using already existing DaemonManager instance in same process.
		use_config() should be be called before parse_argumets() if this is used.
		"""
		self.daemon = d
		if not self._is_submenu:
			self._connect_handlers()
			self.on_daemon_connected(self.daemon)
	
	
	def use_config(self, c):
		"""
		Allows reusing already existin Config instance in same process.
		Has to be called before parse_argumets()
		"""
		self.config = c
	
	
	def get_menuid(self):
		"""
		Returns ID of used menu.
		"""
		return self._menuid
	
	
	def get_selected_item_id(self):
		"""
		Returns ID of selected item or None if nothing is selected.
		"""
		if self._selected:
			return self._selected.id
		return None
	
	
	def _add_arguments(self):
		OSDWindow._add_arguments(self)
		self.argparser.add_argument('--control-with', '-c', type=str,
			metavar="option", default=DEFAULT, choices=(DEFAULT, LEFT, RIGHT, STICK),
			help="which pad or stick should be used to navigate menu")
		self.argparser.add_argument('--confirm-with', type=str,
			metavar="button", default=DEFAULT,
			help="button used to confirm choice")
		self.argparser.add_argument('--cancel-with', type=str,
			metavar="button", default=DEFAULT,
			help="button used to cancel menu")
		self.argparser.add_argument('--confirm-with-release', action='store_true',
			help="confirm choice with button release instead of button press")
		self.argparser.add_argument('--cancel-with-release', action='store_true',
			help="cancel menu with button release instead of button press")
		self.argparser.add_argument('--use-cursor', '-u', action='store_true',
			help="display and use cursor")
		self.argparser.add_argument('--size', type=int,
			help="sets prefered width or height")
		self.argparser.add_argument('--feedback-amplitude', type=int,
			help="enables and sets power of feedback effect generated when active menu option is changed")
		self.argparser.add_argument('--from-profile', '-p', type=str,
			metavar="profile_file menu_name",
			help="load menu items from profile file")
		self.argparser.add_argument('--from-file', '-f', type=str,
			metavar="filename",
			help="load menu items from json file")
		self.argparser.add_argument('--print-items', action='store_true',
			help="prints menu items to stdout")
		self.argparser.add_argument('items', type=str, nargs='*', metavar='id title',
			help="Menu items")
	
	
	@staticmethod
	def _get_on_screen_position(w):
		a = w.get_allocation()
		parent = w.get_parent()
		if parent:
			if isinstance(parent, Menu) and parent.get_window() is not None:
				x, y = parent.get_window().get_position()
			else:
				x, y = Menu._get_on_screen_position(parent)
			return a.x + x, a.y + y
		else:
			return a.x, a.y
	
	
	def parse_menu(self):
		if self.args.from_profile:
			try:
				self._menuid = self.args.items[0]
				self.items = MenuData.from_profile(self.args.from_profile, self._menuid)
			except IOError:
				print >>sys.stderr, '%s: error: profile file not found' % (sys.argv[0])
				return False
			except ValueError:
				print >>sys.stderr, '%s: error: menu not found' % (sys.argv[0])
				return False
		elif self.args.from_file:
			try:
				self._menuid = self.args.from_file
				self.items = MenuData.from_file(self.args.from_file)
			except:
				print >>sys.stderr, '%s: error: failed to load menu file' % (sys.argv[0])
				return False
		else:
			try:
				self.items = MenuData.from_args(self.args.items)
				self._menuid = None
			except ValueError:
				print >>sys.stderr, '%s: error: invalid number of arguments' % (sys.argv[0])
				return False
		return True
	
	
	def parse_argumets(self, argv):
		if not OSDWindow.parse_argumets(self, argv):
			return False
		if not self.parse_menu():
			return False
		if not self.config:
			self.config = Config()
		
		# Parse simpler arguments
		self._size = self.args.size
		
		# Create buttons that are displayed on screen
		items = self.items.generate(self)
		self.items = []
		for item in items:
			item.widget = self.generate_widget(item)
			if item.widget is not None:
				self.items.append(item)
		self.pack_items(self.parent, self.items)
		if len(self.items) == 0:
			print >>sys.stderr, '%s: error: no items in menu' % (sys.argv[0])
			return False
		
		if self.args.print_items:
			max_id_len = max(*[ len(x.id) for x in self.items ])
			row_format ="{:>%s}:\t{}" % (max_id_len,)
			for item in self.items:
				print row_format.format(item.id, item.label)
		return True
	
	
	def enable_cursor(self):
		if not self._use_cursor:
			self.f.add(self.cursor)
			self.f.show_all()
			self._use_cursor = True
	
	
	def generate_widget(self, item):
		""" Generates gtk widget for specified menutitem """
		if isinstance(item, Separator) and item.label:
			widget = Gtk.Button.new_with_label(item.label)
			widget.set_relief(Gtk.ReliefStyle.NONE)
			widget.set_name("osd-menu-separator")
			return widget
		elif isinstance(item, Separator):
			widget = Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL)
			widget.set_name("osd-menu-separator")
			return widget
		else:
			widget = Gtk.Button.new_with_label(item.label)
			widget.set_relief(Gtk.ReliefStyle.NONE)
			if hasattr(widget.get_children()[0], "set_xalign"):
				widget.get_children()[0].set_xalign(0)
			else:
				widget.get_children()[0].set_halign(Gtk.Align.START)
			if isinstance(item, Submenu):
				item.callback = self.show_submenu
				label1 = widget.get_children()[0]
				label2 = Gtk.Label(_(">>"))
				label2.set_property("margin-left", 30)
				box = Gtk.Box(Gtk.Orientation.HORIZONTAL)
				widget.remove(label1)
				box.pack_start(label1, True, True, 1)
				box.pack_start(label2, False, True, 1)
				widget.add(box)
				widget.set_name("osd-menu-item")
			elif item.id is None:
				widget.set_name("osd-menu-dummy")
			else:
				widget.set_name("osd-menu-item")
			
			if isinstance(item.icon, Gio.FileIcon):
				icon_file = item.icon.get_file().get_path()
				has_colors = True
			elif isinstance(item.icon, Gio.ThemedIcon):
				icon = Gtk.IconTheme.get_default().choose_icon(
					item.icon.get_names(), 64, 0)
				icon_file = icon.get_filename() if icon else None
				has_colors = True
			else:
				icon_file, has_colors = find_icon(item.icon, self.PREFER_BW_ICONS)
			
			if icon_file:
				icon = MenuIcon(icon_file, has_colors)
				label = widget.get_children()[0]
				for c in [] + widget.get_children():
					widget.remove(c)
				box = Gtk.Box()
				box.pack_start(icon,  False, True, 0)
				box.pack_start(label, True, True, 10)
				widget.add(box)
				
			return widget
	
	
	def select(self, index):
		if self._selected:
			self._selected.widget.set_name(self._selected.widget.get_name()
				.replace("-selected", ""))
		if self.items[index].id:
			if self._selected != self.items[index]:
				if self.feedback and self.controller:
					self.controller.feedback(*self.feedback)
			self._selected = self.items[index]
			self._selected.widget.set_name(
					self._selected.widget.get_name() + "-selected")
			GLib.timeout_add(2, self._check_on_screen_position)
			return True
		return False
	
	
	def _check_on_screen_position(self, quick=False):
		x, y = Menu._get_on_screen_position(self._selected.widget)
		try:
			m = self.get_window().get_display().get_monitor_at_window(self.get_window())
			assert m
			y_offset = m.get_geometry().y
			screen_height = m.get_geometry().height
		except:
			y_offset = 0
			screen_height = self.get_window().get_screen().get_height()
		y -= y_offset
		if y < 50:
			wx, wy = self.get_window().get_position()
			if quick:
				wy = 50 - (y - wy)
			else:
				wy += 5
				GLib.timeout_add(2, self._check_on_screen_position)
			self.get_window().move(wx, wy)
		if y > screen_height - 100:
			wx, wy = self.get_window().get_position()
			if quick:
				wy = screen_height - 100 - (y - wy)
			else:
				wy -= 5
				GLib.timeout_add(2, self._check_on_screen_position)
			self.get_window().move(wx, wy)
	
	
	def _connect_handlers(self):
		self._eh_ids += [
			(self.daemon, self.daemon.connect('dead', self.on_daemon_died)),
			(self.daemon, self.daemon.connect('error', self.on_daemon_died)),
			(self.daemon, self.daemon.connect('alive', self.on_daemon_connected)),
		]
	
	
	def run(self):
		self.daemon = DaemonManager()
		self._connect_handlers()
		OSDWindow.run(self)
	
	
	def show(self, *a):
		if not self.select(0):
			self.next_item(1)
		OSDWindow.show(self, *a)
		GLib.timeout_add(1, self._check_on_screen_position, True)
	
	
	def on_daemon_connected(self, *a):
		if not self.config:
			self.config = Config()
		self.controller = self.choose_controller(self.daemon)
		if self.controller is None or not self.controller.is_connected():
			# There is no controller connected to daemon
			self.on_failed_to_lock("Controller not connected")
			return
		self.use_controller(self.controller)
		
		self._eh_ids += [
			(self.controller, self.controller.connect('event', self.on_event)),
			(self.controller, self.controller.connect('lost', self.on_controller_lost)),
		]
		self.lock_inputs()
	
	
	def use_controller(self, controller):
		ccfg = self.config.get_controller_config(controller.get_id())
		self._control_with = ccfg["menu_control"] if self.args.control_with == DEFAULT else self.args.control_with
		self._cancel_with = ccfg["menu_cancel"] if self.args.cancel_with == DEFAULT else self.args.cancel_with
		
		if self.args.confirm_with == DEFAULT:
			self._confirm_with = ccfg["menu_confirm"]
		elif self.args.confirm_with == SAME:
			if self._control_with == RIGHT:
				self._confirm_with = SCButtons.RPADTOUCH.name
			else:
				self._confirm_with = SCButtons.LPADTOUCH.name
		else:
			self._confirm_with = self.args.confirm_with
		
		if self.args.use_cursor:
			# As special case, using LEFT pad on controller with
			# actual DPAD should not display cursor
			if self._control_with != LEFT or (controller.get_flags() & ControllerFlags.HAS_DPAD) == 0:
				self.enable_cursor()
		
		if self.args.feedback_amplitude:
			side = "LEFT"
			if self._control_with == "RIGHT":
				side = "RIGHT"
			elif self._control_with == "STICK":
				side = "BOTH"
			self.feedback = side, int(self.args.feedback_amplitude)
	
	
	def lock_inputs(self):
		def success(*a):
			log.error("Sucessfully locked input")
		locks = [ self._control_with, self._confirm_with, self._cancel_with ]
		if self._control_with == "STICK":
			if self.controller.get_flags() & ControllerFlags.HAS_DPAD != 0:
				self._control_with_dpad = True
				locks += [ "LEFT" ]
		self.controller.lock(success, self.on_failed_to_lock, *locks)
	
	
	def quit(self, code=-2):
		if not self._is_submenu:
			if self.get_controller():
				self.get_controller().unlock_all()
			for source, eid in self._eh_ids:
				source.disconnect(eid)
			self._eh_ids = []
		OSDWindow.quit(self, code)
	
	
	def next_item(self, direction):
		""" Selects next menu item, based on self._direction """
		start, i = -1, 0
		try:
			start = self.items.index(self._selected)
			i = start + direction
		except: pass
		while True:
			if i == start:
				# Cannot find valid menu item
				self.select(start)
				break
			if i >= len(self.items):
				i = 0
				GLib.timeout_add(1, self._check_on_screen_position, True)
				continue
			if i < 0:
				i = len(self.items) - 1
				GLib.timeout_add(1, self._check_on_screen_position, True)
				continue
			if self.select(i):
				# Not a separator
				break
			i += direction
			if start < 0: start = 0
	
	
	def on_submenu_closed(self, *a):
		self.set_name("osd-menu")
		if self._submenu.get_exit_code() in (0, -2):
			self._menuid = self._submenu._menuid
			self._selected = self._submenu._selected
			self.quit(self._submenu.get_exit_code())
		self._submenu = None
	
	
	def show_submenu(self, trash, trash2, trash3, menuitem):
		""" Called when user chooses menu item pointing to submenu """
		filename = find_menu(menuitem.filename)
		if filename:
			self._submenu = self.__class__()
			sub_pos = list(self.position)
			for i in (0, 1):
				sub_pos[i] = (sub_pos[i] - self.SUBMENU_OFFSET
						if sub_pos[i] < 0 else sub_pos[i] + self.SUBMENU_OFFSET)
					
			self._submenu.use_config(self.config)
			self._submenu.parse_argumets(["menu.py",
				"-x", str(sub_pos[0]), "-y", str(sub_pos[1]),
			 	"--from-file", filename,
				"--control-with", self._control_with,
				"--confirm-with", self._confirm_with,
				"--cancel-with", self._cancel_with
			])
			self._submenu.set_is_submenu()
			self._submenu.use_daemon(self.daemon)
			self._submenu.use_controller(self.controller)
			self._submenu.controller = self.controller
			self._submenu.connect('destroy', self.on_submenu_closed)
			self._submenu.show()
			self.set_name("osd-menu-inactive")
	
	
	def _control_equals_cancel(self, daemon, x, y):
		"""
		Called by on_event in that very special case when both confirm_with
		and cancel_with are set to STICK.
		
		Separated because RadialMenu overrides on_event and still
		needs to call this.
		
		Returns True if menu was canceled.
		"""
		distance = sqrt(x*x + y*y)
		if distance < STICK_PAD_MAX / 8:
			self.quit(-1)
			return True
		return False
	
	
	def on_stick_direction(self, trash, x, y):
		if y != 0:
			self.next_item(y)
	
	
	def on_event(self, daemon, what, data):
		if self._submenu:
			return self._submenu.on_event(daemon, what, data)
		if what == self._control_with or what == "LEFT" and self._control_with_dpad:
			x, y = data
			if self._use_cursor:
				# Special case, both confirm_with and cancel_with
				# can be set to STICK
				if self._cancel_with == STICK and self._control_with == STICK:
					if self._control_equals_cancel(daemon, x, y):
						return
				
				pad_w = self.cursor.get_allocation().width * 0.5
				pad_h = self.cursor.get_allocation().height * 0.5
				max_w = self.get_allocation().width - 2 * pad_w
				max_h = self.get_allocation().height - 2 * pad_h
				
				x, y = circle_to_square(x / (STICK_PAD_MAX * 2.0), y / (STICK_PAD_MAX * 2.0))
				x = clamp(pad_w, (pad_w + max_w) * 0.5 + x * max_w, max_w - pad_w)
				y = clamp(pad_h, (pad_h + max_h) * 0.5 + y * max_h * -1, max_h - pad_h)
				self.f.move(self.cursor, int(x), int(y))
				
				for i in self.items:
					if point_in_gtkrect(i.widget.get_allocation(), x, y):
						self.select(self.items.index(i))
			else:
				self._scon.set_stick(x, y)
		elif what == self._cancel_with:
			if data[0] == 0:	# Button released
				self.quit(-1)
		elif what == self._confirm_with:
			if data[0] == 0:	# Button released
				if self._selected and self._selected.callback:
					self._selected.callback(self, self.daemon, self.controller, self._selected)
				elif self._selected:
					self.quit(0)
				else:
					self.quit(-1)
Пример #24
0
class Dialog(OSDWindow):
    EPILOG = """Exit codes:
   0  - clean exit, user selected option
  -1  - clean exit, user canceled dialog
   1  - error, invalid arguments
   2  - error, failed to access sc-daemon, sc-daemon reported error or died while dialog is displayed.
   3  - erorr, failed to lock input stick, pad or button(s)
	"""

    def __init__(self, cls="osd-menu"):
        self._buttons = None
        self._text = None

        OSDWindow.__init__(self, cls)
        self.daemon = None
        self.config = None
        self.feedback = None
        self.controller = None
        self.xdisplay = X.Display(hash(
            GdkX11.x11_get_default_xdisplay()))  # Magic

        self.parent = self.create_parent()
        self.f = Gtk.Fixed()
        self.f.add(self.parent)
        self.add(self.f)

        self._scon = StickController()
        self._scon.connect("direction", self.on_stick_direction)
        self._selected = None
        self._eh_ids = []

    def create_parent(self):
        self._text = Gtk.Label()
        self._buttons = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
        dialog = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
        dialog.pack_start(self._text, True, True, 0)
        dialog.pack_start(self._buttons, True, True, 0)

        dialog.set_name("osd-dialog")
        self._buttons.set_name("osd-dialog-buttons")
        self._text.set_name("osd-dialog-text")
        return dialog

    def pack_items(self, parent, items):
        for item in items:
            if hasattr(item.widget, "set_alignment"):
                item.widget.set_alignment(0.5, 0.5)
            self._buttons.pack_end(item.widget, True, True, 0)

    def use_daemon(self, d):
        """
		Allows (re)using already existing DaemonManager instance in same process.
		use_config() should be be called before parse_argumets() if this is used.
		"""
        self.daemon = d
        self._connect_handlers()
        self.on_daemon_connected(self.daemon)

    def use_config(self, c):
        """
		Allows reusing already existin Config instance in same process.
		Has to be called before parse_argumets()
		"""
        self.config = c

    def get_menuid(self):
        # Just to be compatibile with menus when called from scc-osd-daemon
        return None

    def get_selected_item_id(self):
        """
		Returns ID of selected item or None if nothing is selected.
		"""
        if self._selected:
            return self._selected.id
        return None

    def _add_arguments(self):
        OSDWindow._add_arguments(self)
        self.argparser.add_argument('--confirm-with',
                                    type=str,
                                    metavar="button",
                                    default=DEFAULT,
                                    help="button used to confirm choice")
        self.argparser.add_argument('--cancel-with',
                                    type=str,
                                    metavar="button",
                                    default=DEFAULT,
                                    help="button used to cancel dialog")
        self.argparser.add_argument(
            '--feedback-amplitude',
            type=int,
            help=
            "enables and sets power of feedback effect generated when active menu option is changed"
        )
        self.argparser.add_argument('--text',
                                    type=str,
                                    metavar='text',
                                    help="Dialog text")
        self.argparser.add_argument('items',
                                    type=str,
                                    nargs='*',
                                    metavar='id text',
                                    help="Dialog buttons")

    def parse_argumets(self, argv):
        if not OSDWindow.parse_argumets(self, argv):
            return False
        if not self.config:
            self.config = Config()

        try:
            self.items = MenuData.from_args(self.args.items)
            self._menuid = None
        except ValueError:
            print >> sys.stderr, '%s: error: invalid number of arguments' % (
                sys.argv[0])
            return False

        self._text.set_label(self.args.text)

        if self.args.feedback_amplitude:
            side = "LEFT"
            self.feedback = side, int(self.args.feedback_amplitude)

        # Create buttons that are displayed on screen
        items = self.items.generate(self)
        self.items = []
        for item in items:
            item.widget = self.generate_widget(item)
            if item.widget is not None:
                self.items.append(item)
        self.pack_items(self.parent, self.items)
        if len(self.items) == 0:
            print >> sys.stderr, '%s: error: no items in menu' % (sys.argv[0])
            return False

        return True

    def generate_widget(self, item):
        """ Generates gtk widget for specified menutitem """
        widget = Gtk.Button.new_with_label(item.label)
        widget.set_relief(Gtk.ReliefStyle.NONE)
        if hasattr(widget.get_children()[0], "set_xalign"):
            widget.get_children()[0].set_xalign(0)
        else:
            widget.get_children()[0].set_halign(Gtk.Align.START)
        widget.set_name("osd-menu-item")

        return widget

    def select(self, index):
        if self._selected:
            self._selected.widget.set_name(
                self._selected.widget.get_name().replace("-selected", ""))
        if self.items[index].id:
            if self._selected != self.items[index]:
                if self.feedback and self.controller:
                    self.controller.feedback(*self.feedback)
            self._selected = self.items[index]
            self._selected.widget.set_name(self._selected.widget.get_name() +
                                           "-selected")
            return True
        return False

    def _connect_handlers(self):
        self._eh_ids += [
            (self.daemon, self.daemon.connect('dead', self.on_daemon_died)),
            (self.daemon, self.daemon.connect('error', self.on_daemon_died)),
            (self.daemon, self.daemon.connect('alive',
                                              self.on_daemon_connected)),
        ]

    def run(self):
        self.daemon = DaemonManager()
        self._connect_handlers()
        OSDWindow.run(self)

    def show(self, *a):
        if not self.select(0):
            self.next_item(1)
        OSDWindow.show(self, *a)

    def on_daemon_died(self, *a):
        log.error("Daemon died")
        self.quit(2)

    def on_failed_to_lock(self, error):
        log.error("Failed to lock input: %s", error)
        self.quit(3)

    def on_daemon_connected(self, *a):
        def success(*a):
            log.error("Sucessfully locked input")

        if not self.config:
            self.config = Config()
        self.controller = self.choose_controller(self.daemon)
        if self.controller is None or not self.controller.is_connected():
            # There is no controller connected to daemon
            self.on_failed_to_lock("Controller not connected")
            return

        ccfg = self.config.get_controller_config(self.controller.get_id())
        self._control_with = ccfg["menu_control"]
        self._confirm_with = ccfg[
            "menu_confirm"] if self.args.confirm_with == DEFAULT else self.args.confirm_with
        self._cancel_with = ccfg[
            "menu_cancel"] if self.args.cancel_with == DEFAULT else self.args.cancel_with

        self._eh_ids += [(self.controller,
                          self.controller.connect('event', self.on_event))]
        locks = [self._control_with, self._confirm_with, self._cancel_with]
        self.controller.lock(success, self.on_failed_to_lock, *locks)

    def quit(self, code=-2):
        if self.get_controller():
            self.get_controller().unlock_all()
        for source, eid in self._eh_ids:
            source.disconnect(eid)
        self._eh_ids = []
        OSDWindow.quit(self, code)

    def next_item(self, direction):
        """ Selects next menu item, based on self._direction """
        start, i = -1, 0
        try:
            start = self.items.index(self._selected)
            i = start + direction
        except:
            pass
        while True:
            if i == start:
                # Cannot find valid menu item
                self.select(start)
                break
            if i >= len(self.items):
                i = 0
                continue
            if i < 0:
                i = len(self.items) - 1
                continue
            if self.select(i): break
            i += direction
            if start < 0: start = 0

    def on_stick_direction(self, trash, x, y):
        if x != 0:
            self.next_item(x)

    def on_event(self, daemon, what, data):
        if what == self._control_with:
            self._scon.set_stick(*data)
        elif what == self._cancel_with:
            if data[0] == 0:  # Button released
                self.quit(-1)
        elif what == self._confirm_with:
            if data[0] == 0:  # Button released
                if self._selected and self._selected.callback:
                    self._selected.callback(self, self.daemon, self.controller,
                                            self._selected)
                elif self._selected:
                    self.quit(0)
                else:
                    self.quit(-1)
Пример #25
0
class Launcher(OSDWindow):
	EPILOG="""Exit codes:
   0  - clean exit, user selected option
  -1  - clean exit, user canceled dialog
   1  - error, invalid arguments
   2  - error, failed to access sc-daemon, sc-daemon reported error or died while dialog is displayed.
   3  - erorr, failed to lock input stick, pad or button(s)
	"""
	
	BUTTONS = [
		"1",	 	"2 ABC",	"3 DEF",
		"5 GHI", 	"5 JKL",	"6 MNO",
		"7 PQRS",	"8 TUV",	"9 WXYZ",
		"", 		"0"
	]
	
	VALID_CHARS = "12ABC3DEF5GHI5JKL6MNO7PQRS8TUV9WXYZ0"
	CHAR_TO_NUMBER = { }	# Generated on runtime
	
	MAX_ROWS = 5
	
	_app_db = None	# Static list of all know applications
	
	def __init__(self, cls="osd-menu"):
		self._buttons = None
		self._string = ""
		
		OSDWindow.__init__(self, cls)
		self.daemon = None
		self.config = None
		self.feedback = None
		self.controller = None
		self.xdisplay = X.Display(hash(GdkX11.x11_get_default_xdisplay()))	# Magic
		
		self.create_parent()
		self.create_app_list()
		self.create_buttons()
		
		cursor = os.path.join(get_share_path(), "images", 'menu-cursor.svg')
		self.cursors = [ Gtk.Image.new_from_file(cursor), Gtk.Image.new_from_file(cursor) ]
		for c in self.cursors:
			c.set_name("osd-menu-cursor")
			c.selected = None
			self.f.add(c)
		self.f.show_all()
		
		self._scon = StickController()
		self._scon.connect("direction", self.on_stick_direction)
		self._selected = None
		self._menuid = None
		self._eh_ids = []
		self._confirm_with = 'A'
		self._cancel_with = 'B'
		
		if Launcher._app_db is None:
			Launcher._app_db = []
			for x in Launcher.BUTTONS:
				for c in x:
					Launcher.CHAR_TO_NUMBER[c] = x[0]
			
			for x in Gio.AppInfo.get_all():
				try:
					Launcher._app_db.append(( Launcher.name_to_keys(x), x ))
				except UnicodeDecodeError:
					# Just f**k them...
					pass
	
	
	@staticmethod
	def name_to_keys(appinfo):
		name = "".join([
			Launcher.CHAR_TO_NUMBER[x]
			for x in appinfo.get_display_name().upper()
			if x in Launcher.VALID_CHARS
		])
		return name
	
	
	@staticmethod
	def string_to_keys_and_spaces(string):
		name = "".join([
			Launcher.CHAR_TO_NUMBER[x] if x in Launcher.VALID_CHARS else " "
			for x in string.upper()
		])
		return name
	
	
	def create_parent(self):
		self.parent = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
		self.parent.set_name("osd-dialog")
		self.f = Gtk.Fixed()
		self.f.add(self.parent)
		self.add(self.f)
	
	
	def create_app_list(self):
		lst = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
		lst.set_name("osd-application-list")
		self.items = [ self.generate_widget("") for x in xrange(self.MAX_ROWS) ]
		for a in self.items:
			lst.pack_start(a, False, True, 0)
		self.parent.pack_start(lst, True, True, 0)
		self._set_launchers([  ])
		lst.show_all()
	
	
	def create_buttons(self):
		self.grid = Gtk.Grid()
		self.parent.pack_start(self.grid, True, True, 0)
		self._buttons = []
		
		x, y = 0, 0
		for label in self.BUTTONS:
			if label:
				w = self.generate_widget(label)
				w.set_name("osd-key-buton")
				self.grid.attach(w, x, y, 1, 1)
				self._buttons.append(w)
			x += 1
			if x > 2:
				x = 0
				y += 1
		
		
		w = self.generate_widget(_("Run"))
		self.grid.attach(w, x, y, 1, 1)
		
		
		self.grid.set_name("osd-dialog-buttons")
	
	
	def pack_items(self, parent, items):
		for item in items:
			if hasattr(item.widget, "set_alignment"):
				item.widget.set_alignment(0.5, 0.5)
			self._buttons.pack_end(item.widget, True, True, 0)
	
	
	def use_daemon(self, d):
		"""
		Allows (re)using already existing DaemonManager instance in same process.
		use_config() should be be called before parse_argumets() if this is used.
		"""
		self.daemon = d
		self._connect_handlers()
		self.on_daemon_connected(self.daemon)
	
	
	def use_config(self, c):
		"""
		Allows reusing already existin Config instance in same process.
		Has to be called before parse_argumets()
		"""
		self.config = c
	
	
	def get_menuid(self):
		"""
		Returns ID of used menu.
		"""
		return None
	
	
	def get_selected_item_id(self):
		"""
		Returns ID of selected item or None if nothing is selected.
		"""
		return None
	
	
	def _launch(self):
		self._selected.launcher.launch()
	
	
	def _add_arguments(self):
		OSDWindow._add_arguments(self)
		self.argparser.add_argument('--confirm-with', type=str,
			metavar="button", default=DEFAULT,
			help="button used to confirm choice")
		self.argparser.add_argument('--cancel-with', type=str,
			metavar="button", default=DEFAULT,
			help="button used to cancel dialog")
		self.argparser.add_argument('--feedback-amplitude', type=int,
			help="enables and sets power of feedback effect generated when active menu option is changed")
	
	
	def parse_argumets(self, argv):
		if not OSDWindow.parse_argumets(self, argv):
			return False
		if not self.config:
			self.config = Config()
		
		if self.args.feedback_amplitude:
			side = "LEFT"
			self.feedback = side, int(self.args.feedback_amplitude)
		
		# Create buttons that are displayed on screen
		return True
	
	
	def _set_launchers(self, launchers):
		launchers = launchers[0:self.MAX_ROWS]
		for x in self.items:
			x.set_label("")
			x.set_name("osd-hidden-item")
			x.launcher = None
		for i in xrange(0, len(launchers)):
			self.items[i].set_name("osd-launcher-item")
			self.items[i].launcher = launchers[i]
			label = self.items[i].get_children()[0]
			label.set_markup(self._format_label_markup(launchers[i]))
			label.set_max_width_chars(1)
			label.set_ellipsize(Pango.EllipsizeMode.MIDDLE)
			label.set_xalign(0)
	
	
	def _format_label_markup(self, label):
		if hasattr(label, "get_display_name"):
			label = label.get_display_name()
		else:
			label = str(label)
		
		def _check(substr):
			i, ch = 0, self._string
			while len(substr) > 0 and substr[0] == ch[0]:
				ch = ch[1:]
				substr = substr[1:]
				i += 1
				if len(ch) == 0: return i
			while len(substr) > 0 and substr[0] == " ":
				substr = substr[1:]
				i += 1
			return -1
			
		keys = Launcher.string_to_keys_and_spaces(label)
		index1, index2 = -1, -1
		for i in xrange(0, len(keys)):
			if keys[i] == self._string[0]:
				index2 = _check(keys[i:])
				if index2 > 0:
					index1 = i
					index2 = i + index2
					break
		
		label = "%s<span color='#%s'>%s</span>%s" % (
			label[0:index1],
			self.config["osd_colors"]["menuitem_hilight_text"],
			label[index1:index2],
			label[index2:]
		)
		return label
	
	
	def _update_items(self):
		if len(self._string) > 0:
			gen = ( item for (keys, item) in self._app_db if self._string in keys )
			launchers = []
			for i in gen:
				launchers.append(i)
				if len(launchers) > self.MAX_ROWS: break
			self._set_launchers(launchers)
			self.select(0)
		else:
			self._set_launchers([])
	
	
	def generate_widget(self, label):
		""" Generates gtk widget for specified menutitem """
		if hasattr(label, "label"): label = label.label
		widget = Gtk.Button.new_with_label(label)
		widget.set_relief(Gtk.ReliefStyle.NONE)
		if hasattr(widget.get_children()[0], "set_xalign"):
			widget.get_children()[0].set_xalign(0)
		else:
			widget.get_children()[0].set_halign(Gtk.Align.START)
		widget.set_name("osd-menu-item")
		
		return widget
	
	
	def select(self, index):
		if self._selected:
			self._selected.set_name(self._selected.get_name()
				.replace("-selected", ""))
			self._selected = None
		if self.items[index].launcher is not None:
			self._selected = self.items[index]
			self._selected.set_name(
					self._selected.get_name() + "-selected")
			return True
		return False
	
	
	def _connect_handlers(self):
		self._eh_ids += [
			(self.daemon, self.daemon.connect('dead', self.on_daemon_died)),
			(self.daemon, self.daemon.connect('error', self.on_daemon_died)),
			(self.daemon, self.daemon.connect('alive', self.on_daemon_connected)),
		]
	
	
	def run(self):
		self.daemon = DaemonManager()
		self._connect_handlers()
		OSDWindow.run(self)
	
	
	def show(self, *a):
		if not self.select(0):
			self.next_item(1)
		OSDWindow.show(self, *a)
		for c in self.cursors:
			c.set_visible(False)
	
	
	def on_daemon_connected(self, *a):
		def success(*a):
			log.error("Sucessfully locked input")
		
		if not self.config:
			self.config = Config()
		self.controller = self.choose_controller(self.daemon)
		if self.controller is None or not self.controller.is_connected():
			# There is no controller connected to daemon
			self.on_failed_to_lock("Controller not connected")
			return
		
		ccfg = self.config.get_controller_config(self.controller.get_id())
		self._confirm_with = ccfg["menu_confirm"] if self.args.confirm_with == DEFAULT else self.args.confirm_with
		self._cancel_with = ccfg["menu_cancel"] if self.args.cancel_with == DEFAULT else self.args.cancel_with
		
		self._eh_ids += [
			(self.controller, self.controller.connect('event', self.on_event)),
			(self.controller, self.controller.connect('lost', self.on_controller_lost)),
		]
		locks = [ LEFT, RIGHT, STICK, "LPAD", "RPAD", "LB",
			self._confirm_with, self._cancel_with ]
		self.controller.lock(success, self.on_failed_to_lock, *locks)
	
	
	def quit(self, code=-2):
		if self.get_controller():
			self.get_controller().unlock_all()
		for source, eid in self._eh_ids:
			source.disconnect(eid)
		self._eh_ids = []
		OSDWindow.quit(self, code)
	
	
	def next_item(self, direction):
		""" Selects next menu item, based on self._direction """
		start, i = -1, 0
		try:
			start = self.items.index(self._selected)
			i = start + direction
		except: pass
		while True:
			if i == start:
				# Cannot find valid menu item
				self.select(start)
				break
			if i >= len(self.items):
				i = 0
				continue
			if i < 0:
				i = len(self.items) - 1
				continue
			if self.select(i): break
			i += direction
			if start < 0: start = 0
	
	
	def on_stick_direction(self, trash, x, y):
		if y != 0:
			self.next_item(y)
	
	
	def _move_cursor(self, cursor, x, y):
		if (x, y) == (0, 0):
			cursor.set_visible(False)
			return
		cursor.set_visible(True)
		pad_w = cursor.get_allocation().width * 0.5
		pad_h = cursor.get_allocation().height * 0.5
		max_w = self.grid.get_allocation().width - 2 * pad_w
		max_h = self.grid.get_allocation().height - 2 * pad_h
		
		x, y = circle_to_square(x / (STICK_PAD_MAX * 2.0), y / (STICK_PAD_MAX * 2.0))
		x = clamp(pad_w, (pad_w + max_w) * 0.5 + x * max_w, max_w - pad_w)
		y = clamp(pad_h, (pad_h + max_h) * 0.5 + y * max_h * -1, max_h - pad_h)
		x += self.grid.get_allocation().x
		y += self.grid.get_allocation().y
		self.f.move(cursor, int(x), int(y))
		
		for i in self._buttons:
			if point_in_gtkrect(i.get_allocation(), x, y):
				if cursor.selected:
					cursor.selected.set_name("osd-key-buton")
				cursor.selected = i
				cursor.selected.set_name("osd-key-buton-hilight")
				break
	
	
	def _get_under_cursor(self, cursor):
		x, y = self.f.child_get(cursor, "x", "y")
		for i in self._buttons:
			if point_in_gtkrect(i.get_allocation(), x, y):
				return i
		return None
	
	
	def on_event(self, daemon, what, data):
		if what == LEFT:
			self._move_cursor(self.cursors[0], *data)
		elif what == "LPAD" and data[0] == 1:
			b = self._get_under_cursor(self.cursors[0])
			if b: self._string += b.get_label()[0]
			self._update_items()
		elif what == RIGHT:
			self._move_cursor(self.cursors[1], *data)
		elif what == "RPAD" and data[0] == 1:
			b = self._get_under_cursor(self.cursors[1])
			if b: self._string += b.get_label()[0]
			self._update_items()
		elif what == "LB":
			if len(self._string) > 0:
				self._string = self._string[:-1]
				self._update_items()
		elif what == STICK:
			self._scon.set_stick(*data)
		elif what == self._cancel_with:
			if data[0] == 0:	# Button released
				self.quit(-1)
		elif what == self._confirm_with:
			if data[0] == 0:	# Button released
				if self._selected:
					self._launch()
					self.quit(0)
				else:
					self.quit(-1)
Пример #26
0
    def parse_argumets(self, argv):
        if not OSDWindow.parse_argumets(self, argv):
            return False
        if not self.config:
            self.config = Config()
        if self.args.from_profile:
            try:
                self._menuid = self.args.items[0]
                self.items = MenuData.from_profile(self.args.from_profile,
                                                   self._menuid)
            except IOError:
                print >> sys.stderr, '%s: error: profile file not found' % (
                    sys.argv[0])
                return False
            except ValueError:
                print >> sys.stderr, '%s: error: menu not found' % (
                    sys.argv[0])
                return False
        elif self.args.from_file:
            #try:
            self._menuid = self.args.from_file
            self.items = MenuData.from_file(self.args.from_file)
            #except:
            #	print >>sys.stderr, '%s: error: failed to load menu file' % (sys.argv[0])
            #	return False
        else:
            try:
                self.items = MenuData.from_args(self.args.items)
                self._menuid = None
            except ValueError:
                print >> sys.stderr, '%s: error: invalid number of arguments' % (
                    sys.argv[0])
                return False

        # Parse simpler arguments
        self._control_with = self.args.control_with
        self._confirm_with = self.args.confirm_with
        self._cancel_with = self.args.cancel_with
        self._max_size = self.args.max_size

        if self.args.use_cursor:
            self.enable_cursor()

        # Create buttons that are displayed on screen
        items = self.items.generate(self)
        self.items = []
        for item in items:
            item.widget = self.generate_widget(item)
            if item.widget:
                self.items.append(item)
        self.pack_items(self.parent, self.items)
        if len(self.items) == 0:
            print >> sys.stderr, '%s: error: no items in menu' % (sys.argv[0])
            return False

        if self.args.print_items:
            max_id_len = max(*[len(x.id) for x in self.items])
            row_format = "{:>%s}:\t{}" % (max_id_len, )
            for item in self.items:
                print row_format.format(item.id, item.label)
        return True
Пример #27
0
 def disconnected(self):
     # If ignore_serials config option is enabled, fake serial used by this
     # controller is stored away and reused when next controller is connected
     if Config()["ignore_serials"]:
         self._driver._available_serials.add(self._serial)
Пример #28
0
class Launcher(OSDWindow):
    EPILOG = """Exit codes:
   0  - clean exit, user selected option
  -1  - clean exit, user canceled dialog
   1  - error, invalid arguments
   2  - error, failed to access sc-daemon, sc-daemon reported error or died while dialog is displayed.
   3  - erorr, failed to lock input stick, pad or button(s)
	"""

    BUTTONS = [
        "1", "2 ABC", "3 DEF", "5 GHI", "5 JKL", "6 MNO", "7 PQRS", "8 TUV",
        "9 WXYZ", "", "0"
    ]

    VALID_CHARS = "12ABC3DEF5GHI5JKL6MNO7PQRS8TUV9WXYZ0"
    CHAR_TO_NUMBER = {}  # Generated on runtime

    MAX_ROWS = 5

    _app_db = None  # Static list of all know applications

    def __init__(self, cls="osd-menu"):
        self._buttons = None
        self._string = ""

        OSDWindow.__init__(self, cls)
        self.daemon = None
        self.config = None
        self.feedback = None
        self.controller = None
        self.xdisplay = X.Display(hash(
            GdkX11.x11_get_default_xdisplay()))  # Magic

        self.create_parent()
        self.create_app_list()
        self.create_buttons()

        cursor = os.path.join(get_share_path(), "images", 'menu-cursor.svg')
        self.cursors = [
            Gtk.Image.new_from_file(cursor),
            Gtk.Image.new_from_file(cursor)
        ]
        for c in self.cursors:
            c.set_name("osd-menu-cursor")
            c.selected = None
            self.f.add(c)
        self.f.show_all()

        self._scon = StickController()
        self._scon.connect("direction", self.on_stick_direction)
        self._selected = None
        self._menuid = None
        self._eh_ids = []
        self._confirm_with = 'A'
        self._cancel_with = 'B'

        if Launcher._app_db is None:
            Launcher._app_db = []
            for x in Launcher.BUTTONS:
                for c in x:
                    Launcher.CHAR_TO_NUMBER[c] = x[0]

            for x in Gio.AppInfo.get_all():
                try:
                    Launcher._app_db.append((Launcher.name_to_keys(x), x))
                except UnicodeDecodeError:
                    # Just f**k them...
                    pass

    @staticmethod
    def name_to_keys(appinfo):
        name = "".join([
            Launcher.CHAR_TO_NUMBER[x]
            for x in appinfo.get_display_name().upper()
            if x in Launcher.VALID_CHARS
        ])
        return name

    @staticmethod
    def string_to_keys_and_spaces(string):
        name = "".join([
            Launcher.CHAR_TO_NUMBER[x] if x in Launcher.VALID_CHARS else " "
            for x in string.upper()
        ])
        return name

    def create_parent(self):
        self.parent = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
        self.parent.set_name("osd-dialog")
        self.f = Gtk.Fixed()
        self.f.add(self.parent)
        self.add(self.f)

    def create_app_list(self):
        lst = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
        lst.set_name("osd-application-list")
        self.items = [self.generate_widget("") for x in xrange(self.MAX_ROWS)]
        for a in self.items:
            lst.pack_start(a, False, True, 0)
        self.parent.pack_start(lst, True, True, 0)
        self._set_launchers([])
        lst.show_all()

    def create_buttons(self):
        self.grid = Gtk.Grid()
        self.parent.pack_start(self.grid, True, True, 0)
        self._buttons = []

        x, y = 0, 0
        for label in self.BUTTONS:
            if label:
                w = self.generate_widget(label)
                w.set_name("osd-key-buton")
                self.grid.attach(w, x, y, 1, 1)
                self._buttons.append(w)
            x += 1
            if x > 2:
                x = 0
                y += 1

        w = self.generate_widget(_("Run"))
        self.grid.attach(w, x, y, 1, 1)

        self.grid.set_name("osd-dialog-buttons")

    def pack_items(self, parent, items):
        for item in items:
            if hasattr(item.widget, "set_alignment"):
                item.widget.set_alignment(0.5, 0.5)
            self._buttons.pack_end(item.widget, True, True, 0)

    def use_daemon(self, d):
        """
		Allows (re)using already existing DaemonManager instance in same process.
		use_config() should be be called before parse_argumets() if this is used.
		"""
        self.daemon = d
        self._connect_handlers()
        self.on_daemon_connected(self.daemon)

    def use_config(self, c):
        """
		Allows reusing already existin Config instance in same process.
		Has to be called before parse_argumets()
		"""
        self.config = c

    def get_menuid(self):
        """
		Returns ID of used menu.
		"""
        return None

    def get_selected_item_id(self):
        """
		Returns ID of selected item or None if nothing is selected.
		"""
        return None

    def _launch(self):
        self._selected.launcher.launch()

    def _add_arguments(self):
        OSDWindow._add_arguments(self)
        self.argparser.add_argument('--confirm-with',
                                    type=str,
                                    metavar="button",
                                    default=DEFAULT,
                                    help="button used to confirm choice")
        self.argparser.add_argument('--cancel-with',
                                    type=str,
                                    metavar="button",
                                    default=DEFAULT,
                                    help="button used to cancel dialog")
        self.argparser.add_argument(
            '--feedback-amplitude',
            type=int,
            help=
            "enables and sets power of feedback effect generated when active menu option is changed"
        )

    def parse_argumets(self, argv):
        if not OSDWindow.parse_argumets(self, argv):
            return False
        if not self.config:
            self.config = Config()

        if self.args.feedback_amplitude:
            side = "LEFT"
            self.feedback = side, int(self.args.feedback_amplitude)

        # Create buttons that are displayed on screen
        return True

    def _set_launchers(self, launchers):
        launchers = launchers[0:self.MAX_ROWS]
        for x in self.items:
            x.set_label("")
            x.set_name("osd-hidden-item")
            x.launcher = None
        for i in xrange(0, len(launchers)):
            self.items[i].set_name("osd-launcher-item")
            self.items[i].launcher = launchers[i]
            label = self.items[i].get_children()[0]
            label.set_markup(self._format_label_markup(launchers[i]))
            label.set_max_width_chars(1)
            label.set_ellipsize(Pango.EllipsizeMode.MIDDLE)
            label.set_xalign(0)

    def _format_label_markup(self, label):
        if hasattr(label, "get_display_name"):
            label = label.get_display_name()
        else:
            label = str(label)

        def _check(substr):
            i, ch = 0, self._string
            while len(substr) > 0 and substr[0] == ch[0]:
                ch = ch[1:]
                substr = substr[1:]
                i += 1
                if len(ch) == 0: return i
            while len(substr) > 0 and substr[0] == " ":
                substr = substr[1:]
                i += 1
            return -1

        keys = Launcher.string_to_keys_and_spaces(label)
        index1, index2 = -1, -1
        for i in xrange(0, len(keys)):
            if keys[i] == self._string[0]:
                index2 = _check(keys[i:])
                if index2 > 0:
                    index1 = i
                    index2 = i + index2
                    break

        label = "%s<span color='#%s'>%s</span>%s" % (
            label[0:index1],
            self.config["osd_colors"]["menuitem_hilight_text"],
            label[index1:index2], label[index2:])
        return label

    def _update_items(self):
        if len(self._string) > 0:
            gen = (item for (keys, item) in self._app_db
                   if self._string in keys)
            launchers = []
            for i in gen:
                launchers.append(i)
                if len(launchers) > self.MAX_ROWS: break
            self._set_launchers(launchers)
            self.select(0)
        else:
            self._set_launchers([])

    def generate_widget(self, label):
        """ Generates gtk widget for specified menutitem """
        if hasattr(label, "label"): label = label.label
        widget = Gtk.Button.new_with_label(label)
        widget.set_relief(Gtk.ReliefStyle.NONE)
        if hasattr(widget.get_children()[0], "set_xalign"):
            widget.get_children()[0].set_xalign(0)
        else:
            widget.get_children()[0].set_halign(Gtk.Align.START)
        widget.set_name("osd-menu-item")

        return widget

    def select(self, index):
        if self._selected:
            self._selected.set_name(self._selected.get_name().replace(
                "-selected", ""))
            self._selected = None
        if self.items[index].launcher is not None:
            self._selected = self.items[index]
            self._selected.set_name(self._selected.get_name() + "-selected")
            return True
        return False

    def _connect_handlers(self):
        self._eh_ids += [
            (self.daemon, self.daemon.connect('dead', self.on_daemon_died)),
            (self.daemon, self.daemon.connect('error', self.on_daemon_died)),
            (self.daemon, self.daemon.connect('alive',
                                              self.on_daemon_connected)),
        ]

    def run(self):
        self.daemon = DaemonManager()
        self._connect_handlers()
        OSDWindow.run(self)

    def show(self, *a):
        if not self.select(0):
            self.next_item(1)
        OSDWindow.show(self, *a)
        for c in self.cursors:
            c.set_visible(False)

    def on_daemon_connected(self, *a):
        def success(*a):
            log.error("Sucessfully locked input")

        if not self.config:
            self.config = Config()
        self.controller = self.choose_controller(self.daemon)
        if self.controller is None or not self.controller.is_connected():
            # There is no controller connected to daemon
            self.on_failed_to_lock("Controller not connected")
            return

        ccfg = self.config.get_controller_config(self.controller.get_id())
        self._confirm_with = ccfg[
            "menu_confirm"] if self.args.confirm_with == DEFAULT else self.args.confirm_with
        self._cancel_with = ccfg[
            "menu_cancel"] if self.args.cancel_with == DEFAULT else self.args.cancel_with

        self._eh_ids += [
            (self.controller, self.controller.connect('event', self.on_event)),
            (self.controller,
             self.controller.connect('lost', self.on_controller_lost)),
        ]
        locks = [
            LEFT, RIGHT, STICK, "LPAD", "RPAD", "LB", self._confirm_with,
            self._cancel_with
        ]
        self.controller.lock(success, self.on_failed_to_lock, *locks)

    def quit(self, code=-2):
        if self.get_controller():
            self.get_controller().unlock_all()
        for source, eid in self._eh_ids:
            source.disconnect(eid)
        self._eh_ids = []
        OSDWindow.quit(self, code)

    def next_item(self, direction):
        """ Selects next menu item, based on self._direction """
        start, i = -1, 0
        try:
            start = self.items.index(self._selected)
            i = start + direction
        except:
            pass
        while True:
            if i == start:
                # Cannot find valid menu item
                self.select(start)
                break
            if i >= len(self.items):
                i = 0
                continue
            if i < 0:
                i = len(self.items) - 1
                continue
            if self.select(i): break
            i += direction
            if start < 0: start = 0

    def on_stick_direction(self, trash, x, y):
        if y != 0:
            self.next_item(y)

    def _move_cursor(self, cursor, x, y):
        if (x, y) == (0, 0):
            cursor.set_visible(False)
            return
        cursor.set_visible(True)
        pad_w = cursor.get_allocation().width * 0.5
        pad_h = cursor.get_allocation().height * 0.5
        max_w = self.grid.get_allocation().width - 2 * pad_w
        max_h = self.grid.get_allocation().height - 2 * pad_h

        x, y = circle_to_square(x / (STICK_PAD_MAX * 2.0),
                                y / (STICK_PAD_MAX * 2.0))
        x = clamp(pad_w, (pad_w + max_w) * 0.5 + x * max_w, max_w - pad_w)
        y = clamp(pad_h, (pad_h + max_h) * 0.5 + y * max_h * -1, max_h - pad_h)
        x += self.grid.get_allocation().x
        y += self.grid.get_allocation().y
        self.f.move(cursor, int(x), int(y))

        for i in self._buttons:
            if point_in_gtkrect(i.get_allocation(), x, y):
                if cursor.selected:
                    cursor.selected.set_name("osd-key-buton")
                cursor.selected = i
                cursor.selected.set_name("osd-key-buton-hilight")
                break

    def _get_under_cursor(self, cursor):
        x, y = self.f.child_get(cursor, "x", "y")
        for i in self._buttons:
            if point_in_gtkrect(i.get_allocation(), x, y):
                return i
        return None

    def on_event(self, daemon, what, data):
        if what == LEFT:
            self._move_cursor(self.cursors[0], *data)
        elif what == "LPAD" and data[0] == 1:
            b = self._get_under_cursor(self.cursors[0])
            if b: self._string += b.get_label()[0]
            self._update_items()
        elif what == RIGHT:
            self._move_cursor(self.cursors[1], *data)
        elif what == "RPAD" and data[0] == 1:
            b = self._get_under_cursor(self.cursors[1])
            if b: self._string += b.get_label()[0]
            self._update_items()
        elif what == "LB":
            if len(self._string) > 0:
                self._string = self._string[:-1]
                self._update_items()
        elif what == STICK:
            self._scon.set_stick(*data)
        elif what == self._cancel_with:
            if data[0] == 0:  # Button released
                self.quit(-1)
        elif what == self._confirm_with:
            if data[0] == 0:  # Button released
                if self._selected:
                    self._launch()
                    self.quit(0)
                else:
                    self.quit(-1)