chore(repo): baseline du fork input-remapper (upstream v2.2.1)
Le dépôt ne contenait que README.md ; ajout de l'intégralité du code fonctionnel du fork comme socle stable de la branche de release. L'état runtime d'orchestration (.ideai/) est exclu du suivi. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
0
inputremapper/gui/components/__init__.py
Normal file
0
inputremapper/gui/components/__init__.py
Normal file
174
inputremapper/gui/components/common.py
Normal file
174
inputremapper/gui/components/common.py
Normal file
@ -0,0 +1,174 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# input-remapper - GUI for device specific keyboard mappings
|
||||
# Copyright (C) 2025 sezanzeb <b8x45ygc9@mozmail.com>
|
||||
#
|
||||
# This file is part of input-remapper.
|
||||
#
|
||||
# input-remapper is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# input-remapper is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with input-remapper. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
"""Components used in multiple places."""
|
||||
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
from gi.repository import Gtk
|
||||
|
||||
from typing import Optional
|
||||
|
||||
from inputremapper.configs.mapping import MappingData
|
||||
|
||||
from inputremapper.gui.controller import Controller
|
||||
from inputremapper.gui.messages.message_broker import (
|
||||
MessageBroker,
|
||||
MessageType,
|
||||
)
|
||||
from inputremapper.gui.messages.message_data import GroupData, PresetData
|
||||
from inputremapper.gui.utils import HandlerDisabled
|
||||
|
||||
|
||||
class FlowBoxEntry(Gtk.ToggleButton):
|
||||
"""A device that can be selected in the GUI.
|
||||
|
||||
For example a keyboard or a mouse.
|
||||
"""
|
||||
|
||||
__gtype_name__ = "FlowBoxEntry"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
message_broker: MessageBroker,
|
||||
controller: Controller,
|
||||
name: str,
|
||||
icon_name: Optional[str] = None,
|
||||
):
|
||||
super().__init__()
|
||||
self.icon_name = icon_name
|
||||
self.message_broker = message_broker
|
||||
self._controller = controller
|
||||
|
||||
box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
|
||||
|
||||
if icon_name:
|
||||
icon = Gtk.Image.new_from_icon_name(icon_name, Gtk.IconSize.DIALOG)
|
||||
box.add(icon)
|
||||
|
||||
label = Gtk.Label()
|
||||
label.set_label(name)
|
||||
self.name = name
|
||||
|
||||
# wrap very long names properly
|
||||
label.set_line_wrap(True)
|
||||
label.set_line_wrap_mode(2)
|
||||
# this affeects how many device entries fit next to each other
|
||||
label.set_width_chars(28)
|
||||
label.set_max_width_chars(28)
|
||||
|
||||
box.add(label)
|
||||
|
||||
box.set_margin_top(18)
|
||||
box.set_margin_bottom(18)
|
||||
box.set_homogeneous(True)
|
||||
box.set_spacing(12)
|
||||
|
||||
# self.set_relief(Gtk.ReliefStyle.NONE)
|
||||
|
||||
self.add(box)
|
||||
|
||||
self.show_all()
|
||||
|
||||
self.connect("toggled", self._on_gtk_toggle)
|
||||
|
||||
def _on_gtk_toggle(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def show_active(self, active):
|
||||
"""Show the active state without triggering anything."""
|
||||
with HandlerDisabled(self, self._on_gtk_toggle):
|
||||
self.set_active(active)
|
||||
|
||||
|
||||
class FlowBoxWrapper:
|
||||
"""A wrapper for a flowbox that contains FlowBoxEntry widgets."""
|
||||
|
||||
def __init__(self, flowbox: Gtk.FlowBox):
|
||||
self._gui = flowbox
|
||||
|
||||
def show_active_entry(self, name: Optional[str]):
|
||||
"""Activate the togglebutton that matches the name."""
|
||||
for child in self._gui.get_children():
|
||||
flow_box_entry: FlowBoxEntry = child.get_children()[0]
|
||||
flow_box_entry.show_active(flow_box_entry.name == name)
|
||||
|
||||
|
||||
class Breadcrumbs:
|
||||
"""Writes a breadcrumbs string into a given label."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
message_broker: MessageBroker,
|
||||
label: Gtk.Label,
|
||||
show_device_group: bool = False,
|
||||
show_preset: bool = False,
|
||||
show_mapping: bool = False,
|
||||
):
|
||||
self._message_broker = message_broker
|
||||
self._gui = label
|
||||
self._connect_message_listener()
|
||||
|
||||
self.show_device_group = show_device_group
|
||||
self.show_preset = show_preset
|
||||
self.show_mapping = show_mapping
|
||||
|
||||
self._group_key: str = ""
|
||||
self._preset_name: str = ""
|
||||
self._mapping_name: str = ""
|
||||
|
||||
label.set_max_width_chars(50)
|
||||
label.set_line_wrap(True)
|
||||
label.set_line_wrap_mode(2)
|
||||
|
||||
self._render()
|
||||
|
||||
def _connect_message_listener(self):
|
||||
self._message_broker.subscribe(MessageType.group, self._on_group_changed)
|
||||
self._message_broker.subscribe(MessageType.preset, self._on_preset_changed)
|
||||
self._message_broker.subscribe(MessageType.mapping, self._on_mapping_changed)
|
||||
|
||||
def _on_preset_changed(self, data: PresetData):
|
||||
self._preset_name = data.name or ""
|
||||
self._render()
|
||||
|
||||
def _on_group_changed(self, data: GroupData):
|
||||
self._group_key = data.group_key
|
||||
self._render()
|
||||
|
||||
def _on_mapping_changed(self, mapping_data: MappingData):
|
||||
self._mapping_name = mapping_data.format_name()
|
||||
self._render()
|
||||
|
||||
def _render(self):
|
||||
label = []
|
||||
|
||||
if self.show_device_group:
|
||||
label.append(self._group_key or "?")
|
||||
|
||||
if self.show_preset:
|
||||
label.append(self._preset_name or "?")
|
||||
|
||||
if self.show_mapping:
|
||||
label.append(self._mapping_name or "?")
|
||||
|
||||
self._gui.set_label(" / ".join(label))
|
||||
115
inputremapper/gui/components/device_groups.py
Normal file
115
inputremapper/gui/components/device_groups.py
Normal file
@ -0,0 +1,115 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# input-remapper - GUI for device specific keyboard mappings
|
||||
# Copyright (C) 2025 sezanzeb <b8x45ygc9@mozmail.com>
|
||||
#
|
||||
# This file is part of input-remapper.
|
||||
#
|
||||
# input-remapper is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# input-remapper is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with input-remapper. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Optional
|
||||
|
||||
from gi.repository import Gtk
|
||||
|
||||
from inputremapper.gui.components.common import FlowBoxEntry, FlowBoxWrapper
|
||||
from inputremapper.gui.components.editor import ICON_PRIORITIES, ICON_NAMES
|
||||
from inputremapper.gui.components.main import Stack
|
||||
from inputremapper.gui.controller import Controller
|
||||
from inputremapper.gui.messages.message_broker import (
|
||||
MessageBroker,
|
||||
MessageType,
|
||||
)
|
||||
from inputremapper.gui.messages.message_data import (
|
||||
GroupsData,
|
||||
GroupData,
|
||||
DoStackSwitch,
|
||||
)
|
||||
from inputremapper.logging.logger import logger
|
||||
|
||||
|
||||
class DeviceGroupEntry(FlowBoxEntry):
|
||||
"""A device that can be selected in the GUI.
|
||||
|
||||
For example a keyboard or a mouse.
|
||||
"""
|
||||
|
||||
__gtype_name__ = "DeviceGroupEntry"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
message_broker: MessageBroker,
|
||||
controller: Controller,
|
||||
icon_name: Optional[str],
|
||||
group_key: str,
|
||||
):
|
||||
super().__init__(
|
||||
message_broker=message_broker,
|
||||
controller=controller,
|
||||
icon_name=icon_name,
|
||||
name=group_key,
|
||||
)
|
||||
self.group_key = group_key
|
||||
|
||||
def _on_gtk_toggle(self, *_, **__):
|
||||
logger.debug('Selecting device "%s"', self.group_key)
|
||||
self._controller.load_group(self.group_key)
|
||||
self.message_broker.publish(DoStackSwitch(Stack.presets_page))
|
||||
|
||||
|
||||
class DeviceGroupSelection(FlowBoxWrapper):
|
||||
"""A wrapper for the container with our groups.
|
||||
|
||||
A group is a collection of devices.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
message_broker: MessageBroker,
|
||||
controller: Controller,
|
||||
flowbox: Gtk.FlowBox,
|
||||
):
|
||||
super().__init__(flowbox)
|
||||
|
||||
self._message_broker = message_broker
|
||||
self._controller = controller
|
||||
self._gui = flowbox
|
||||
|
||||
self._message_broker.subscribe(MessageType.groups, self._on_groups_changed)
|
||||
self._message_broker.subscribe(MessageType.group, self._on_group_changed)
|
||||
|
||||
def _on_groups_changed(self, data: GroupsData):
|
||||
self._gui.foreach(self._gui.remove)
|
||||
|
||||
for group_key, types in data.groups.items():
|
||||
if len(types) > 0:
|
||||
device_type = sorted(types, key=ICON_PRIORITIES.index)[0]
|
||||
icon_name = ICON_NAMES[device_type]
|
||||
else:
|
||||
icon_name = None
|
||||
|
||||
logger.debug("adding %s to device selection", group_key)
|
||||
device_group_entry = DeviceGroupEntry(
|
||||
self._message_broker,
|
||||
self._controller,
|
||||
icon_name,
|
||||
group_key,
|
||||
)
|
||||
self._gui.insert(device_group_entry, -1)
|
||||
|
||||
if self._controller.data_manager.active_group:
|
||||
self.show_active_entry(self._controller.data_manager.active_group.key)
|
||||
|
||||
def _on_group_changed(self, data: GroupData):
|
||||
self.show_active_entry(data.group_key)
|
||||
1211
inputremapper/gui/components/editor.py
Normal file
1211
inputremapper/gui/components/editor.py
Normal file
File diff suppressed because it is too large
Load Diff
134
inputremapper/gui/components/main.py
Normal file
134
inputremapper/gui/components/main.py
Normal file
@ -0,0 +1,134 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# input-remapper - GUI for device specific keyboard mappings
|
||||
# Copyright (C) 2025 sezanzeb <b8x45ygc9@mozmail.com>
|
||||
#
|
||||
# This file is part of input-remapper.
|
||||
#
|
||||
# input-remapper is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# input-remapper is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with input-remapper. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
"""Components that wrap everything."""
|
||||
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from gi.repository import Gtk, Pango
|
||||
|
||||
from inputremapper.gui.controller import Controller
|
||||
from inputremapper.gui.messages.message_broker import (
|
||||
MessageBroker,
|
||||
MessageType,
|
||||
)
|
||||
from inputremapper.gui.messages.message_data import StatusData, DoStackSwitch
|
||||
from inputremapper.gui.utils import CTX_ERROR, CTX_MAPPING, CTX_WARNING
|
||||
|
||||
|
||||
class Stack:
|
||||
"""Wraps the Stack, which contains the main menu pages."""
|
||||
|
||||
devices_page = 0
|
||||
presets_page = 1
|
||||
editor_page = 2
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
message_broker: MessageBroker,
|
||||
controller: Controller,
|
||||
stack: Gtk.Stack,
|
||||
):
|
||||
self._message_broker = message_broker
|
||||
self._controller = controller
|
||||
self._gui = stack
|
||||
|
||||
self._message_broker.subscribe(
|
||||
MessageType.do_stack_switch, self._do_stack_switch
|
||||
)
|
||||
|
||||
def _do_stack_switch(self, msg: DoStackSwitch):
|
||||
self._gui.set_visible_child(self._gui.get_children()[msg.page_index])
|
||||
|
||||
|
||||
class StatusBar:
|
||||
"""The status bar on the bottom of the main window."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
message_broker: MessageBroker,
|
||||
controller: Controller,
|
||||
status_bar: Gtk.Statusbar,
|
||||
error_icon: Gtk.Image,
|
||||
warning_icon: Gtk.Image,
|
||||
):
|
||||
self._message_broker = message_broker
|
||||
self._controller = controller
|
||||
self._gui = status_bar
|
||||
self._error_icon = error_icon
|
||||
self._warning_icon = warning_icon
|
||||
|
||||
label = self._gui.get_message_area().get_children()[0]
|
||||
label.set_ellipsize(Pango.EllipsizeMode.END)
|
||||
label.set_selectable(True)
|
||||
|
||||
self._message_broker.subscribe(MessageType.status_msg, self._on_status_update)
|
||||
|
||||
# keep track if there is an error or warning in the stack of statusbar
|
||||
# unfortunately this is not exposed over the api
|
||||
self._error = False
|
||||
self._warning = False
|
||||
|
||||
def _on_status_update(self, data: StatusData):
|
||||
"""Show a status message and set its tooltip.
|
||||
|
||||
If message is None, it will remove the newest message of the
|
||||
given context_id.
|
||||
"""
|
||||
context_id = data.ctx_id
|
||||
message = data.msg
|
||||
tooltip = data.tooltip
|
||||
status_bar = self._gui
|
||||
|
||||
if message is None:
|
||||
status_bar.remove_all(context_id)
|
||||
|
||||
if context_id in (CTX_ERROR, CTX_MAPPING):
|
||||
self._error_icon.hide()
|
||||
self._error = False
|
||||
if self._warning:
|
||||
self._warning_icon.show()
|
||||
|
||||
if context_id == CTX_WARNING:
|
||||
self._warning_icon.hide()
|
||||
self._warning = False
|
||||
if self._error:
|
||||
self._error_icon.show()
|
||||
|
||||
status_bar.set_tooltip_text("")
|
||||
return
|
||||
|
||||
if tooltip is None:
|
||||
tooltip = message
|
||||
|
||||
self._error_icon.hide()
|
||||
self._warning_icon.hide()
|
||||
|
||||
if context_id in (CTX_ERROR, CTX_MAPPING):
|
||||
self._error_icon.show()
|
||||
self._error = True
|
||||
|
||||
if context_id == CTX_WARNING:
|
||||
self._warning_icon.show()
|
||||
self._warning = True
|
||||
|
||||
status_bar.push(context_id, message)
|
||||
status_bar.set_tooltip_text(tooltip)
|
||||
3
inputremapper/gui/components/output_type_names.py
Normal file
3
inputremapper/gui/components/output_type_names.py
Normal file
@ -0,0 +1,3 @@
|
||||
class OutputTypeNames:
|
||||
analog_axis = "Analog Axis"
|
||||
key_or_macro = "Key or Macro"
|
||||
106
inputremapper/gui/components/presets.py
Normal file
106
inputremapper/gui/components/presets.py
Normal file
@ -0,0 +1,106 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# input-remapper - GUI for device specific keyboard mappings
|
||||
# Copyright (C) 2025 sezanzeb <b8x45ygc9@mozmail.com>
|
||||
#
|
||||
# This file is part of input-remapper.
|
||||
#
|
||||
# input-remapper is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# input-remapper is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with input-remapper. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
"""All components that are visible on the page that shows all the presets."""
|
||||
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from gi.repository import Gtk
|
||||
|
||||
from inputremapper.gui.components.common import FlowBoxEntry, FlowBoxWrapper
|
||||
from inputremapper.gui.components.main import Stack
|
||||
from inputremapper.gui.controller import Controller
|
||||
from inputremapper.gui.messages.message_broker import (
|
||||
MessageBroker,
|
||||
MessageType,
|
||||
)
|
||||
from inputremapper.gui.messages.message_data import (
|
||||
GroupData,
|
||||
PresetData,
|
||||
DoStackSwitch,
|
||||
)
|
||||
from inputremapper.logging.logger import logger
|
||||
|
||||
|
||||
class PresetEntry(FlowBoxEntry):
|
||||
"""A preset that can be selected in the GUI."""
|
||||
|
||||
__gtype_name__ = "PresetEntry"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
message_broker: MessageBroker,
|
||||
controller: Controller,
|
||||
preset_name: str,
|
||||
):
|
||||
super().__init__(
|
||||
message_broker=message_broker, controller=controller, name=preset_name
|
||||
)
|
||||
self.preset_name = preset_name
|
||||
|
||||
def _on_gtk_toggle(self, *_, **__):
|
||||
logger.debug('Selecting preset "%s"', self.preset_name)
|
||||
self._controller.load_preset(self.preset_name)
|
||||
self.message_broker.publish(DoStackSwitch(Stack.editor_page))
|
||||
|
||||
|
||||
class PresetSelection(FlowBoxWrapper):
|
||||
"""A wrapper for the container with our presets.
|
||||
|
||||
Selectes the active_preset.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
message_broker: MessageBroker,
|
||||
controller: Controller,
|
||||
flowbox: Gtk.FlowBox,
|
||||
):
|
||||
super().__init__(flowbox)
|
||||
|
||||
self._message_broker = message_broker
|
||||
self._controller = controller
|
||||
self._gui = flowbox
|
||||
self._connect_message_listener()
|
||||
|
||||
def _connect_message_listener(self):
|
||||
self._message_broker.subscribe(MessageType.group, self._on_group_changed)
|
||||
self._message_broker.subscribe(MessageType.preset, self._on_preset_changed)
|
||||
|
||||
def _on_group_changed(self, data: GroupData):
|
||||
self._gui.foreach(self._gui.remove)
|
||||
for preset_name in data.presets:
|
||||
preset_entry = PresetEntry(
|
||||
self._message_broker,
|
||||
self._controller,
|
||||
preset_name,
|
||||
)
|
||||
self._gui.insert(preset_entry, -1)
|
||||
|
||||
def _on_preset_changed(self, data: PresetData):
|
||||
self.show_active_entry(data.name)
|
||||
|
||||
def set_active_preset(self, preset_name: str):
|
||||
"""Change the currently selected preset."""
|
||||
# TODO might only be needed in tests
|
||||
for child in self._gui.get_children():
|
||||
preset_entry: PresetEntry = child.get_children()[0]
|
||||
preset_entry.set_active(preset_entry.preset_name == preset_name)
|
||||
Reference in New Issue
Block a user