示例#1
0
def configure_launcher_app(base_name, databases, default_platform_id,
                           default_titlebar_color):
    Product.base_name = base_name
    for option_name in fsgamesys.OPENRETRO_DEFAULT_DATABASES:
        Option.get(option_name)["default"] = "0"
        Settings.set_default(option_name, "0")
    for option_name in [
            Option.AMIGA_DATABASE,
            Option.CD32_DATABASE,
            Option.CDTV_DATABASE,
    ]:
        Option.get(option_name)["default"] = "0"
        Settings.set_default(option_name, "0")
    for option_name in databases:
        Option.get(option_name)["default"] = "1"
        Settings.set_default(option_name, "1")

    from fsgamesys.config.config import Config

    Config.set_default("platform", default_platform_id)
    Product.default_platform_id = default_platform_id

    if default_titlebar_color is not None:
        Settings.set_default("launcher_titlebar_bgcolor",
                             default_titlebar_color)
    # Settings.set_default("launcher_titlebar_fgcolor", "#cccccc")
    import fsboot

    fsboot.set("base_dir_name", base_name)
    return "SYS:Launcher"
    def on_config(self, key, value):
        if key == "platform":
            self.port = self.port_gui_index + 1
            if value == Platform.C64:
                if self.port_gui_index == 0:
                    self.port = 2
                elif self.port_gui_index == 1:
                    self.port = 1
            self._platform = value
            self.update_enabled()
            self.device_option_key = "{}_port_{}".format(
                self._platform, self.port)
            # Disable the control if the type option does not exist
            try:
                Option.get("{}_port_{}_type".format(self._platform, self.port))
            except KeyError:
                self.set_enabled(False)
            else:
                self.set_enabled(True)

            return
        # if key == self.mode_option_key or key == "amiga_model":
        #     value = DeviceManager.get_calculated_port_mode(
        #         get_config(self), self.port)
        #     for i, config in enumerate(self.joystick_mode_values):
        #         if config == value:
        #             if self.mode_choice is not None:
        #                 self.mode_choice.set_index(i)
        #                 if self.port >= 4:
        #                     self.set_enabled(i != 0)
        #             break
        #     else:
        #         print("FIXME: could not set mode")
        # elif key == self.type_option_key:
        elif key == self.device_option_key:
            value_lower = value.lower()
            for i, name in enumerate(self.device_values):
                if value_lower == name.lower():
                    self.set_index(i)
                    break

        # This is intended to catch all config changes for all ports (both
        # mode and device) to update the defaults

        # FIXME:
        # if key.startswith("joystick_port_") or key == "amiga_model":
        #     self.update_default_device()

        if key.startswith("{}_port_".format(self._platform)):
            self.update_default_device()
示例#3
0
 def create(self, parent, name, text=None, check=None, help=None):
     option = Option.get(name)
     return create_option_group(
         parent, self.options, option, name, option["type"].lower(),
         text if text is not None else gettext(option["description"]),
         check if check is not None else self.check,
         help if help is not None else self.help, self.label_spacing)
示例#4
0
 def create(self, parent, name, text=None, check=None, help=None):
     option = Option.get(name)
     return create_option_group(
         parent, self.options, option, name, option["type"].lower(),
         text if text is not None else gettext(option["description"]),
         check if check is not None else self.check,
         help if help is not None else self.help,
         self.label_spacing)
示例#5
0
 def update_options(self):
     try:
         option = Option.get(self._model_key)
     except KeyError:
         self._choice_values = ["0"]
         self._choice_labels = ["N/A"]
     else:
         choices = option["values"]
         self._choice_values = [x[0] for x in choices]
         self._choice_labels = [x[1] for x in choices]
     with self.changed.inhibit:
         self.clear()
         for label in self._choice_labels:
             self.add_item(label)
示例#6
0
 def update_options(self):
     try:
         option = Option.get(self._model_key)
     except KeyError:
         self._choice_values = ["0"]
         self._choice_labels = ["N/A"]
     else:
         choices = option["values"]
         self._choice_values = [x[0] for x in choices]
         self._choice_labels = [x[1] for x in choices]
     with self.changed.inhibit:
         self.clear()
         for label in self._choice_labels:
             self.add_item(label)
    def init_port(self, index, port_info):
        port = Port(port_info["description"])
        # FIXME: MOVE TO Port CONSTRUCTOR
        port.number = index + 1
        port.types = port_info["types"]
        port.type_option = port_info.get("type_option", "")
        port.device_option = port_info.get("device_option", "")
        # Set correct (port type index) based on type option
        # FIXME: MOVE CODE TO port CLASS
        if port.type_option:
            type_value = self.options[port.type_option]
            if type_value:
                print(
                    "[INPUT] Port option",
                    port.type_option,
                    "was set to",
                    type_value,
                )
            else:
                print("[INPUT] No value specified for", port.type_option)

            # Hack / Workaround for Amiga
            # FIXME: Move to driver subclasses?
            # if not type_value:
            #     if self.options[Option.AMIGA_MODEL].lower() == "cd32":
            #         type_value = "cd32 pad"

            if not type_value:
                # FIXME: RETRIEVE DEFAULT VALUE FOR TYPE OPTION
                try:
                    type_value = Option.get(port.type_option)["default"]
                except Exception:
                    print(
                        "[INPUT] Could not find default for", port.type_option
                    )
            for j, port_type in enumerate(port.types):
                print("[INPUT]", j, port_type["type"])
                if type_value == port_type["type"]:
                    print("[INPUT] Set index", j, "for type", type_value)
                    port.index = j
                    break
            else:
                print(
                    "[INPUT] WARNING: Could not find index for type",
                    type_value,
                )
        self.ports.append(port)
示例#8
0
    def init_port(self, index, port_info):
        port = Port(port_info["description"])
        # FIXME: MOVE TO Port CONSTRUCTOR
        port.number = index + 1
        port.types = port_info["types"]
        port.type_option = port_info.get("type_option", "")
        port.device_option = port_info.get("device_option", "")
        # Set correct (port type index) based on type option
        # FIXME: MOVE CODE TO port CLASS
        if port.type_option:
            type_value = self.options[port.type_option]
            if type_value:
                print(
                    "[INPUT] Port option",
                    port.type_option,
                    "was set to",
                    type_value,
                )
            else:
                print("[INPUT] No value specified for", port.type_option)

            # Hack / Workaround for Amiga
            # FIXME: Move to driver subclasses?
            # if not type_value:
            #     if self.options[Option.AMIGA_MODEL].lower() == "cd32":
            #         type_value = "cd32 pad"

            if not type_value:
                # FIXME: RETRIEVE DEFAULT VALUE FOR TYPE OPTION
                try:
                    type_value = Option.get(port.type_option)["default"]
                except Exception:
                    print(
                        "[INPUT] Could not find default for", port.type_option
                    )
            for j, port_type in enumerate(port.types):
                print("[INPUT]", j, port_type["type"])
                if type_value == port_type["type"]:
                    print("[INPUT] Set index", j, "for type", type_value)
                    port.index = j
                    break
            else:
                print(
                    "[INPUT] WARNING: Could not find index for type",
                    type_value,
                )
        self.ports.append(port)
    def update_options(self):
        try:
            option = Option.get(self._config_key)
        except KeyError:
            self._choice_values = ["0"]
            self._choice_labels = ["N/A"]
        else:
            choices = option["values"]
            self._choice_values = [x[0] for x in choices]
            self._choice_labels = [x[1] for x in choices]

            if "default" in option:
                for x in choices:
                    if x[0] == option["default"]:
                        default_label = x[1] + " (*)"
                        break
                else:
                    default_label = "??? (*)"
                self._choice_values.insert(0, "")
                self._choice_labels.insert(0, default_label)
        with self.changed.inhibit:
            self.clear()
            for label in self._choice_labels:
                self.add_item(label)
示例#10
0
def main():
    app_name = ""
    # Check deprecated/legacy app options.
    if "--server" in sys.argv:
        sys.argv.remove("--server")
        app_name = "fs-uae-netplay-server"
    if "--arcade" in sys.argv:
        sys.argv.remove("--arcade")
        app_name = "fs-uae-arcade"
    if "--fs-uae-arcade" in sys.argv:
        sys.argv.remove("--fs-uae-arcade")
        app_name = "fs-uae-arcade"
    if sys.argv[0].endswith("fs-game-center"):
        app_name = "fs-game-center"
    if len(sys.argv) > 1:
        if sys.argv[1] == "xdftool":
            app_name = "xdftool"
            del sys.argv[1]
    if "--xdftool" in sys.argv:
        sys.argv.remove("--xdftool")
        app_name = "xdftool"
    # Check new app option.
    for arg in sys.argv:
        if arg.startswith("--app="):
            app_name = arg[6:]
            sys.argv.remove(arg)

    import fsgs
    if "--openretro" in sys.argv:
        sys.argv.remove("--openretro")
        fsgs.product = "OpenRetro"
        fsgs.openretro = True
        for option_name in OPENRETRO_DEFAULT_DATABASES:
            Option.get(option_name)["default"] = "1"

    # Check for (fake) version override
    for arg in sys.argv:
        if arg.startswith("--") and "=" in arg:
            key, value = arg[2:].split("=", 1)
            key = key.replace("-", "_")
            if key == "fake_version":
                launcher.version.VERSION = value

    if app_name == "xdftool":
        sys.argv[0] = "xdftool"
        import amitools.tols.xdftool
        sys.exit(amitools.tools.xdftool.main())

    app_main = None
    if app_main is None:
        if app_name:
            app_main = find_app(app_name)
        elif len(sys.argv) > 1:
            app_main = find_app(sys.argv[1])
            if app_main is not None:
                # Remove app name from sys.argv
                del sys.argv[1]

    if app_main is None and not app_name:
        app_name = "fs-uae-launcher"
        app_main = find_app(app_name)
    # if openretro:
    #     if app_name == "fs-uae-launcher":
    #         app_name = "openretro-launcher"
    #     elif app_name == "fs-uae-arcade":
    #         app_name = "openretro-arcade"

    import socket
    socket.setdefaulttimeout(30.0)
    from fsbc.init import initialize_application
    initialize_application(app_name, version=launcher.version.VERSION)

    if app_main is None:
        print("No valid app specified", file=sys.stderr)
        sys.exit(1)
    app_main()
示例#11
0
    def create_group(
        cls, parent, name, description=None, help_button=True, thin=False
    ):
        group = fsui.Group(parent)
        group.layout = fsui.HorizontalLayout()
        if thin:
            thin_layout = fsui.VerticalLayout()
            thin_layout.add(group.layout, fill=True)
        option = Option.get(name)
        if description == "":
            description = gettext(option["description"])
        if description:
            group.label = fsui.Label(group, description + ":")
            group.layout.add(group.label, margin_right=10)
            group.layout.add(OverrideWarning(group, name), margin_right=10)

        if thin:
            group.layout = fsui.HorizontalLayout()
            if description:
                thin_layout.add(group.layout, fill=True, margin_top=6)
            else:
                thin_layout.add(group.layout, fill=True, margin_top=0)

        choice_values = []

        if description:
            default_tmpl = "{0} (*)"
            # default_tmpl = "Default - {0}"
        else:
            default_tmpl = "{0} (*)"
            # default_tmpl = "Default - {0}"

        if option["type"].lower() == "boolean":
            if option["default"] == "1":
                default_desc = gettext(default_tmpl).format(gettext("On"))
            elif option["default"] == "0":
                default_desc = gettext(default_tmpl).format(gettext("Off"))
            else:
                default_desc = gettext("Default")
            choice_values.append(("", default_desc))
            choice_values.append(("1", gettext("On")))
            choice_values.append(("0", gettext("Off")))

        elif option["type"].lower() == "choice":
            for i, value in enumerate(option["values"]):
                if option["default"] == value[0]:
                    default_desc = gettext(default_tmpl).format(
                        gettext(value[1])
                    )
                    break
            else:
                default_desc = gettext("Default")
            choice_values.append(("", default_desc))
            for option in option["values"]:
                choice_values.append((option[0], gettext(option[1])))

        elif option["type"].lower() == "string":

            def on_changed():
                val = text_field.get_text()
                LauncherSettings.set(name, val.strip())

            text_field = fsui.TextField(group)
            # text_field.set_min_width(400)
            text_field.set_text(LauncherSettings.get(name))
            text_field.on_changed = on_changed
            group.layout.add(text_field, expand=True)

        elif (
            option["type"].lower() == "integer"
            and "min" in option
            and "max" in option
        ):
            current = LauncherSettings.get(name)

            if name == Option.LAUNCHER_FONT_SIZE:
                font = app.qapplication.font()
                Option.get(Option.LAUNCHER_FONT_SIZE)[
                    "default"
                ] = font.pointSize()

            current_int = int(option["default"])
            if current:
                try:
                    current_int = int(current)
                except ValueError:
                    pass
            current_int = max(option["min"], min(option["max"], current_int))
            check_box = fsui.CheckBox(group, gettext("Default"))
            spin_ctrl = fsui.SpinCtrl(
                group, option["min"], option["max"], current_int
            )
            if current == "":
                check_box.check()
                spin_ctrl.disable()

            def on_checkbox():
                if check_box.is_checked():
                    spin_ctrl.set_value(int(option["default"]))
                    spin_ctrl.disable()
                    LauncherSettings.set(name, "")
                else:
                    spin_ctrl.enable()

            check_box.on_changed = on_checkbox

            def on_spin():
                val = spin_ctrl.get_value()
                val = max(option["min"], min(option["max"], val))
                LauncherSettings.set(name, str(val))

            spin_ctrl.on_changed = on_spin
            group.layout.add_spacer(0, expand=True)
            group.layout.add(check_box)
            group.layout.add(spin_ctrl, margin_left=10)

        if choice_values:

            def on_changed():
                index = choice.get_index()
                LauncherSettings.set(name, choice_values[index][0])

            choice_labels = [x[1] for x in choice_values]
            choice = fsui.Choice(group, choice_labels)
            current = LauncherSettings.get(name)
            for i, value in enumerate(choice_values):
                if current == value[0]:
                    choice.set_index(i)
                    break
            choice.on_changed = on_changed
            if thin:
                group.layout.add(choice, expand=True)
            else:
                group.layout.add_spacer(0, expand=True)
                group.layout.add(choice)
            group.widget = choice

        if help_button:
            option_url = "https://fs-uae.net/docs/options/" + name.replace(
                "_", "-"
            )
            group.help_button = HelpButton(parent, option_url)
            group.layout.add(group.help_button, margin_left=10)

        if thin:
            group.layout = thin_layout
        return group
示例#12
0
        def auto_fill(port, type):
            mode = config.get("{}_port_{}_type".format(platform, port))
            if not mode:
                # FIXME: DEFAULT
                # mode = cls.get_calculated_port_mode(config, port)
                # mode = "gamepad"
                try:
                    option = Option.get("{}_port_{}_type".format(
                        platform, port))
                    mode = option["default"]
                except KeyError:
                    # FIXME: How to handle?
                    mode = ""

            print("mode for", port, "is", mode)
            if cls.is_mouse_device(mode):
                mode = "mouse"

            val = config.get("{}_port_{}".format(platform, port))
            if val:
                # specific device chosen
                for dev in cls.devices:
                    if dev.id == val:
                        ports[port] = dev
                        break
                return

            if type == "mouse":
                # print("a", mode)
                if mode != "mouse":
                    return
                # print("b")
                for dev in cls.devices:
                    # print("c")
                    if dev.type == "mouse" and dev.port is None:
                        # print("d")
                        ports[port] = dev
                        dev.port = port
                        return
            elif type == "joystick":
                if mode == "none":
                    return
                elif mode == "mouse":
                    return
                # elif mode == "gamepad":
                #     prefs = cls.get_preferred_gamepads()
                # elif mode == "joystick":
                #     prefs = cls.get_preferred_joysticks()
                # else:
                #     return

                prefs = cls.get_preferred_gamepads()

                # try to find an available preferred device first
                for pref in prefs:
                    for dev in cls.devices:
                        if dev.cmp_id == pref and dev.port is None:
                            ports[port] = dev
                            dev.port = port
                            return
                # find first suitable device
                for dev in cls.devices:
                    if dev.type == "joystick" and dev.port is None:
                        ports[port] = dev
                        dev.port = port
                        return
                for dev in cls.devices:
                    if dev.type == "keyboard" and dev.port is None:
                        ports[port] = dev
                        dev.port = port
                        return
示例#13
0
    def create_group(cls,
                     parent,
                     name,
                     description=None,
                     help_button=True,
                     thin=False):
        group = fsui.Group(parent)
        group.layout = fsui.HorizontalLayout()
        if thin:
            thin_layout = fsui.VerticalLayout()
            thin_layout.add(group.layout, fill=True)
        option = Option.get(name)
        if description == "":
            description = gettext(option["description"])
        if description:
            group.label = fsui.Label(group, description + ":")
            group.layout.add(group.label, margin_right=10)
            group.layout.add(OverrideWarning(group, name), margin_right=10)

        if thin:
            group.layout = fsui.HorizontalLayout()
            if description:
                thin_layout.add(group.layout, fill=True, margin_top=6)
            else:
                thin_layout.add(group.layout, fill=True, margin_top=0)

        choice_values = []

        if description:
            default_tmpl = "{0} (*)"
            # default_tmpl = "Default - {0}"
        else:
            default_tmpl = "{0} (*)"
            # default_tmpl = "Default - {0}"

        if option["type"].lower() == "boolean":
            if option["default"] == "1":
                default_desc = gettext(default_tmpl).format(gettext("On"))
            elif option["default"] == "0":
                default_desc = gettext(default_tmpl).format(gettext("Off"))
            else:
                default_desc = gettext("Default")
            choice_values.append(("", default_desc))
            choice_values.append(("1", gettext("On")))
            choice_values.append(("0", gettext("Off")))

        elif option["type"].lower() == "choice":
            for i, value in enumerate(option["values"]):
                if option["default"] == value[0]:
                    default_desc = gettext(default_tmpl).format(
                        gettext(value[1]))
                    break
            else:
                default_desc = gettext("Default")
            choice_values.append(("", default_desc))
            for option in option["values"]:
                choice_values.append((option[0], gettext(option[1])))

        elif option["type"].lower() == "string":

            def on_changed():
                val = text_field.get_text()
                LauncherSettings.set(name, val.strip())

            text_field = fsui.TextField(group)
            # text_field.set_min_width(400)
            text_field.set_text(LauncherSettings.get(name))
            text_field.on_changed = on_changed
            group.layout.add(text_field, expand=True)

        elif (option["type"].lower() == "integer" and "min" in option
              and "max" in option):
            current = LauncherSettings.get(name)

            if name == Option.LAUNCHER_FONT_SIZE:
                font = app.qapplication.font()
                Option.get(
                    Option.LAUNCHER_FONT_SIZE)["default"] = font.pointSize()

            current_int = int(option["default"])
            if current:
                try:
                    current_int = int(current)
                except ValueError:
                    pass
            current_int = max(option["min"], min(option["max"], current_int))
            check_box = fsui.CheckBox(group, gettext("Default"))
            spin_ctrl = fsui.SpinCtrl(group, option["min"], option["max"],
                                      current_int)
            if current == "":
                check_box.check()
                spin_ctrl.disable()

            def on_checkbox():
                if check_box.is_checked():
                    spin_ctrl.set_value(int(option["default"]))
                    spin_ctrl.disable()
                    LauncherSettings.set(name, "")
                else:
                    spin_ctrl.enable()

            check_box.on_changed = on_checkbox

            def on_spin():
                val = spin_ctrl.get_value()
                val = max(option["min"], min(option["max"], val))
                LauncherSettings.set(name, str(val))

            spin_ctrl.on_changed = on_spin
            group.layout.add_spacer(0, expand=True)
            group.layout.add(check_box)
            group.layout.add(spin_ctrl, margin_left=10)

        if choice_values:

            def on_changed():
                index = choice.get_index()
                LauncherSettings.set(name, choice_values[index][0])

            choice_labels = [x[1] for x in choice_values]
            choice = fsui.Choice(group, choice_labels)
            current = LauncherSettings.get(name)
            for i, value in enumerate(choice_values):
                if current == value[0]:
                    choice.set_index(i)
                    break
            choice.on_changed = on_changed
            if thin:
                group.layout.add(choice, expand=True)
            else:
                group.layout.add_spacer(0, expand=True)
                group.layout.add(choice)
            group.widget = choice

        if help_button:
            option_url = "https://fs-uae.net/docs/options/" + name.replace(
                "_", "-")
            group.help_button = HelpButton(parent, option_url)
            group.layout.add(group.help_button, margin_left=10)

        if thin:
            group.layout = thin_layout
        return group
示例#14
0
def main():
    app_name = ""
    # Check deprecated/legacy app options.
    if "--server" in sys.argv:
        sys.argv.remove("--server")
        app_name = "fs-uae-netplay-server"
    if "--arcade" in sys.argv:
        sys.argv.remove("--arcade")
        app_name = "fs-uae-arcade"
    if "--fs-uae-arcade" in sys.argv:
        sys.argv.remove("--fs-uae-arcade")
        app_name = "fs-uae-arcade"
    if sys.argv[0].endswith("fs-game-center"):
        app_name = "fs-game-center"
    if len(sys.argv) > 1:
        if sys.argv[1] == "xdftool":
            app_name = "xdftool"
            del sys.argv[1]
    if "--xdftool" in sys.argv:
        sys.argv.remove("--xdftool")
        app_name = "xdftool"
    # Check new app option.
    for arg in sys.argv:
        if arg.startswith("--app="):
            app_name = arg[6:]
            sys.argv.remove(arg)

    import fsgs

    if "--openretro" in sys.argv:
        sys.argv.remove("--openretro")
        fsgs.product = "OpenRetro"
        fsgs.openretro = True
        for option_name in OPENRETRO_DEFAULT_DATABASES:
            Option.get(option_name)["default"] = "1"

    # Check for (fake) version override
    for arg in sys.argv:
        if arg.startswith("--") and "=" in arg:
            key, value = arg[2:].split("=", 1)
            key = key.replace("-", "_")
            if key == "fake_version":
                launcher.version.VERSION = value

    if app_name == "xdftool":
        sys.argv[0] = "xdftool"
        import amitools.tols.xdftool

        sys.exit(amitools.tools.xdftool.main())

    app_main = None
    if app_main is None:
        if app_name:
            app_main = find_app(app_name)
        elif len(sys.argv) > 1:
            app_main = find_app(sys.argv[1])
            if app_main is not None:
                # Remove app name from sys.argv
                del sys.argv[1]

    if app_main is None and not app_name:
        app_name = "fs-uae-launcher"
        app_main = find_app(app_name)
    # if openretro:
    #     if app_name == "fs-uae-launcher":
    #         app_name = "openretro-launcher"
    #     elif app_name == "fs-uae-arcade":
    #         app_name = "openretro-arcade"

    import socket

    socket.setdefaulttimeout(30.0)
    from fsbc.init import initialize_application

    initialize_application(app_name, version=launcher.version.VERSION)

    if app_main is None:
        print("No valid app specified", file=sys.stderr)
        sys.exit(1)
    app_main()
示例#15
0
def default_value(key):
    option = Option.get(key)
    return option["default"]
示例#16
0
def default_value(key):
    option = Option.get(key)
    return option["default"]
示例#17
0
        def auto_fill(port, type):
            mode = config.get("{}_port_{}_type".format(platform, port))
            if not mode:
                # FIXME: DEFAULT
                # mode = cls.get_calculated_port_mode(config, port)
                # mode = "gamepad"
                try:
                    option = Option.get(
                        "{}_port_{}_type".format(platform, port)
                    )
                    mode = option["default"]
                except KeyError:
                    # FIXME: How to handle?
                    mode = ""

            print("mode for", port, "is", mode)
            if cls.is_mouse_device(mode):
                mode = "mouse"

            val = config.get("{}_port_{}".format(platform, port))
            if val:
                # specific device chosen
                for dev in cls.devices:
                    if dev.id == val:
                        ports[port] = dev
                        break
                return

            if type == "mouse":
                # print("a", mode)
                if mode != "mouse":
                    return
                # print("b")
                for dev in cls.devices:
                    # print("c")
                    if dev.type == "mouse" and dev.port is None:
                        # print("d")
                        ports[port] = dev
                        dev.port = port
                        return
            elif type == "joystick":
                if mode == "none":
                    return
                elif mode == "mouse":
                    return
                # elif mode == "gamepad":
                #     prefs = cls.get_preferred_gamepads()
                # elif mode == "joystick":
                #     prefs = cls.get_preferred_joysticks()
                # else:
                #     return

                prefs = cls.get_preferred_gamepads()

                # try to find an available preferred device first
                for pref in prefs:
                    for dev in cls.devices:
                        if dev.cmp_id == pref and dev.port is None:
                            ports[port] = dev
                            dev.port = port
                            return
                # find first suitable device
                for dev in cls.devices:
                    if dev.type == "joystick" and dev.port is None:
                        ports[port] = dev
                        dev.port = port
                        return
                for dev in cls.devices:
                    if dev.type == "keyboard" and dev.port is None:
                        ports[port] = dev
                        dev.port = port
                        return