From 86ccc278a0a96aa5b8752c271249bd96d7eb95c8 Mon Sep 17 00:00:00 2001 From: AdrienPiechocki <112864425+AdrienPiechocki@users.noreply.github.com> Date: Fri, 28 Nov 2025 21:57:06 +0100 Subject: [PATCH 1/4] fix recurrent events in calendar --- Bin/calendar-events.py | 277 +++++++++++++++++++++++++++-------------- 1 file changed, 186 insertions(+), 91 deletions(-) diff --git a/Bin/calendar-events.py b/Bin/calendar-events.py index 2a954e258..ffc8be61a 100755 --- a/Bin/calendar-events.py +++ b/Bin/calendar-events.py @@ -1,140 +1,235 @@ #!/usr/bin/env python3 import gi - gi.require_version('EDataServer', '1.2') gi.require_version('ECal', '2.0') gi.require_version('ICalGLib', "3.0") -import json -import sys -import time -from datetime import datetime, timezone +import json, sys +from datetime import datetime, timedelta from gi.repository import ECal, EDataServer, ICalGLib start_time = int(sys.argv[1]) end_time = int(sys.argv[2]) +# for testing : +# ---------------------- +# now = datetime.now() +# start_date = now - timedelta(days=31) +# end_date = now + timedelta(days=14) +# start_time = int(start_date.timestamp()) +# end_time = int(end_date.timestamp()) +# ---------------------- + print(f"Starting with time range: {start_time} to {end_time}", file=sys.stderr) all_events = [] +# ---------------------- +# Utilitary Functions +# ---------------------- def safe_get_time(ical_time): - """Safely get time from ICalTime object""" if not ical_time: - return None - + return None, False try: - # Later we use `tzinfo=timezone.utc`, so we set all calendar events to UTC - if not ical_time.is_utc(): - ical_time = ical_time.convert_to_zone(ICalGLib.Timezone.get_utc_timezone()) - + is_all_day = hasattr(ical_time, "is_date") and ical_time.is_date() year = ical_time.get_year() month = ical_time.get_month() day = ical_time.get_day() - - if year < 1970 or year > 2100 or month < 1 or month > 12 or day < 1 or day > 31: - return None - - if ical_time.is_date(): - local_struct = time.struct_time((year, month, day, 0, 0, 0, 0, 0, -1)) - return int(time.mktime(local_struct)) - + if is_all_day: + return int(datetime(year, month, day).timestamp()), True hour = ical_time.get_hour() minute = ical_time.get_minute() second = ical_time.get_second() - - dt = datetime(year, month, day, hour, minute, second, tzinfo=timezone.utc) - return int(dt.timestamp()) - except Exception: - return None - -print("Getting registry...", file=sys.stderr) + dt = datetime(year, month, day, hour, minute, second) + return int(dt.timestamp()), False + except: + return None, False + +def add_event(summary, calendar_name, start_ts, end_ts, location="", description="", all_day=False): + all_events.append({ + 'calendar': calendar_name, + 'summary': summary, + 'start': start_ts, + 'end': end_ts, + 'location': location, + 'description': description + }) + +# ---------------------- +# Get Events from Calendars +# ---------------------- registry = EDataServer.SourceRegistry.new_sync(None) -print("Registry obtained", file=sys.stderr) - sources = registry.list_sources(EDataServer.SOURCE_EXTENSION_CALENDAR) -print(f"Found {len(sources)} calendar sources", file=sys.stderr) for source in sources: if not source.get_enabled(): - print(f"Skipping disabled calendar: {source.get_display_name()}", file=sys.stderr) continue calendar_name = source.get_display_name() print(f"\nProcessing calendar: {calendar_name}", file=sys.stderr) try: - print(f" Connecting to {calendar_name}...", file=sys.stderr) - client = ECal.Client.connect_sync( - source, - ECal.ClientSourceType.EVENTS, - 30, - None - ) - print(f" Connected to {calendar_name}", file=sys.stderr) - - start_dt = datetime.fromtimestamp(start_time, tz=timezone.utc) - end_dt = datetime.fromtimestamp(end_time, tz=timezone.utc) + client = ECal.Client.connect_sync(source, ECal.ClientSourceType.EVENTS, 30, None) - start_str = start_dt.strftime("%Y%m%dT%H%M%SZ") - end_str = end_dt.strftime("%Y%m%dT%H%M%SZ") + start_dt = datetime.fromtimestamp(start_time) + end_dt = datetime.fromtimestamp(end_time) + start_str = start_dt.strftime("%Y%m%dT%H%M%S") + end_str = end_dt.strftime("%Y%m%dT%H%M%S") query = f'(occur-in-time-range? (make-time "{start_str}") (make-time "{end_str}"))' - print(f" Query: {query}", file=sys.stderr) - - print(f" Getting object list for {calendar_name}...", file=sys.stderr) - success, ical_objects = client.get_object_list_sync(query, None) - print(f" Got object list for {calendar_name}: success={success}, count={len(ical_objects) if ical_objects else 0}", file=sys.stderr) - - if not success or not ical_objects: - print(f" No events found in {calendar_name}", file=sys.stderr) + success, raw_events = client.get_object_list_sync(query, None) + + if not success or not raw_events: continue - print(f" Processing {len(ical_objects)} events from {calendar_name}...", file=sys.stderr) - for idx, ical_obj in enumerate(ical_objects): - try: - if hasattr(ical_obj, 'get_summary'): - comp = ical_obj - else: - comp = ECal.Component.new_from_string(ical_obj) + for raw_obj in raw_events: + obj = raw_obj[1] if isinstance(raw_obj, tuple) else raw_obj + comp = None + + if isinstance(obj, ICalGLib.Component): + comp = obj + elif isinstance(obj, ECal.Component): + try: + ical_str = obj.to_string() + temp_comp = ICalGLib.Component.new_from_string(ical_str) + if temp_comp.getName() == "VEVENT": + comp = temp_comp + except Exception: + comp = None + + if not comp: + summary = getattr(obj, "get_summary", lambda: "(No title)")() + dtstart = getattr(obj, "get_dtstart", lambda: None)() + dtend = getattr(obj, "get_dtend", lambda: None)() + start_ts, all_day = safe_get_time(dtstart) + end_ts, _ = safe_get_time(dtend) + if start_ts: + if end_ts is None: + end_ts = start_ts + 3600 + add_event(summary, calendar_name, start_ts, end_ts) + continue - if not comp: - continue + summary = getattr(comp, "get_summary", lambda: "(No title)")() + dtstart = getattr(comp, "get_dtstart", lambda: None)() + dtend = getattr(comp, "get_dtend", lambda: None)() + start_ts, all_day = safe_get_time(dtstart) + end_ts, _ = safe_get_time(dtend) + if end_ts is None and start_ts is not None: + end_ts = start_ts + 3600 + + rrule_getter = getattr(comp, "get_first_property", None) + if rrule_getter: + rrule_prop = comp.get_first_property(73) # ICAL_RRULE_PROPERTY + if rrule_prop: + rrule_value = rrule_prop.get_value() # ICalGLib.Value + + try: + recurrence = rrule_value.get_recur() # -> ICalGLib.Recurrence + + except AttributeError: + rrule_str = str(rrule_value) + recurrence = ICalGLib.Recurrence.new_from_string(rrule_str) + + if recurrence: + freq = recurrence.get_freq() + + rdates = getattr(comp, "get_rdate_list", lambda: [])() + exdates = getattr(comp, "get_exdate_list", lambda: [])() + + # --- normal event --- + if not rrule_prop and not rdates: + add_event(summary, calendar_name, start_ts, end_ts) + continue + # --- recurrent events --- + if freq: summary = comp.get_summary() or "(No title)" + dtstart = comp.get_dtstart() + dtend = comp.get_dtend() + start_ts, all_day = safe_get_time(dtstart) + end_ts, _ = safe_get_time(dtend) + if end_ts is None and start_ts is not None: + end_ts = start_ts + 3600 # 1h default + + interval = recurrence.get_interval() or 1 + count = recurrence.get_count() + until_dt = recurrence.get_until() + until_ts, _ = safe_get_time(until_dt) if until_dt else (None, False) + if until_ts is None: + until_ts = end_time + + occurrences = [] + current_ts = start_ts + added = 0 + + match freq: + case 0: #SECONDLY + delta = timedelta(seconds=interval) + while (current_ts <= until_ts) and (not count or added < count): + occurrences.append((current_ts, current_ts + (end_ts - start_ts))) + current_ts += int(delta.total_seconds()) + added += 1 + + case 1: #MINUTELY + delta = timedelta(minutes=interval) + while (current_ts <= until_ts) and (not count or added < count): + occurrences.append((current_ts, current_ts + (end_ts - start_ts))) + current_ts += int(delta.total_seconds()) + added += 1 + + case 2: #HOURLY + delta = timedelta(hours=interval) + while (current_ts <= until_ts) and (not count or added < count): + occurrences.append((current_ts, current_ts + (end_ts - start_ts))) + current_ts += int(delta.total_seconds()) + added += 1 + + case 3: # DAILY + delta = timedelta(days=interval) + while (current_ts <= until_ts) and (not count or added < count): + occurrences.append((current_ts, current_ts + (end_ts - start_ts))) + current_ts += int(delta.total_seconds()) + added += 1 + + case 4: # WEEKLY + delta = timedelta(weeks=interval) + while (current_ts <= until_ts) and (not count or added < count): + occurrences.append((current_ts, current_ts + (end_ts - start_ts))) + current_ts += int(delta.total_seconds()) + added += 1 + + case 5: # MONTHLY + from dateutil.relativedelta import relativedelta + dt = datetime.fromtimestamp(current_ts) + while (current_ts <= until_ts) and (not count or added < count): + occurrences.append((current_ts, current_ts + (end_ts - start_ts))) + dt += relativedelta(months=interval) + current_ts = int(dt.timestamp()) + added += 1 + + case 6: # YEARLY + from dateutil.relativedelta import relativedelta + dt = datetime.fromtimestamp(current_ts) + while (current_ts <= until_ts) and (not count or added < count): + occurrences.append((current_ts, current_ts + (end_ts - start_ts))) + dt += relativedelta(years=interval) + current_ts = int(dt.timestamp()) + added += 1 + + case _: # NONE + occurrences.append((start_ts, end_ts)) + + # --- add occurences to all_events --- + for occ_start, occ_end in occurrences: + add_event(summary, calendar_name, occ_start, occ_end) - start_timestamp = safe_get_time(comp.get_dtstart()) - if start_timestamp is None: - continue - - end_timestamp = safe_get_time(comp.get_dtend()) - if end_timestamp is None or end_timestamp == start_timestamp: - end_timestamp = start_timestamp + 3600 - - location = comp.get_location() or "" - description = comp.get_description() or "" - - all_events.append({ - 'summary': summary, - 'start': start_timestamp, - 'end': end_timestamp, - 'location': location, - 'description': description, - 'calendar': calendar_name - }) - - if (idx + 1) % 10 == 0: - print(f" Processed {idx + 1} events from {calendar_name}...", file=sys.stderr) - except Exception as e: - print(f" Error processing event {idx} in {calendar_name}: {e}", file=sys.stderr) - continue - - print(f" Finished processing {calendar_name}, found {len([e for e in all_events if e['calendar'] == calendar_name])} events", file=sys.stderr) except Exception as e: print(f" Error for {calendar_name}: {e}", file=sys.stderr) -print(f"\nSorting {len(all_events)} total events...", file=sys.stderr) +# ---------------------- +# JSON output +# ---------------------- all_events.sort(key=lambda x: x['start']) -print("Done! Outputting JSON...", file=sys.stderr) -print(json.dumps(all_events)) +print(json.dumps(all_events, indent=4)) + From 4ad7b7f44a6ead413cd87d52c8ea2bb09083d0f0 Mon Sep 17 00:00:00 2001 From: AdrienPiechocki <112864425+AdrienPiechocki@users.noreply.github.com> Date: Sun, 30 Nov 2025 17:39:41 +0100 Subject: [PATCH 2/4] removed multi-line comments --- Bin/calendar-events.py | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/Bin/calendar-events.py b/Bin/calendar-events.py index ffc8be61a..065d9be5c 100755 --- a/Bin/calendar-events.py +++ b/Bin/calendar-events.py @@ -11,22 +11,10 @@ start_time = int(sys.argv[1]) end_time = int(sys.argv[2]) -# for testing : -# ---------------------- -# now = datetime.now() -# start_date = now - timedelta(days=31) -# end_date = now + timedelta(days=14) -# start_time = int(start_date.timestamp()) -# end_time = int(end_date.timestamp()) -# ---------------------- - print(f"Starting with time range: {start_time} to {end_time}", file=sys.stderr) all_events = [] -# ---------------------- -# Utilitary Functions -# ---------------------- def safe_get_time(ical_time): if not ical_time: return None, False @@ -55,9 +43,6 @@ def add_event(summary, calendar_name, start_ts, end_ts, location="", description 'description': description }) -# ---------------------- -# Get Events from Calendars -# ---------------------- registry = EDataServer.SourceRegistry.new_sync(None) sources = registry.list_sources(EDataServer.SOURCE_EXTENSION_CALENDAR) @@ -227,9 +212,6 @@ def add_event(summary, calendar_name, start_ts, end_ts, location="", description except Exception as e: print(f" Error for {calendar_name}: {e}", file=sys.stderr) -# ---------------------- -# JSON output -# ---------------------- all_events.sort(key=lambda x: x['start']) print(json.dumps(all_events, indent=4)) From 7caf48b586bb4500880f7e37a860c91519d15f7a Mon Sep 17 00:00:00 2001 From: AdrienPiechocki <112864425+AdrienPiechocki@users.noreply.github.com> Date: Sun, 30 Nov 2025 17:45:22 +0100 Subject: [PATCH 3/4] fix conflict --- Bin/type-key.py | 164 +++++++++++++++ Bin/ydotool_toggle_state.json | 1 + Modules/Bar/Widgets/VirtualKeyboard.qml | 21 ++ Modules/MainScreen/MainScreen.qml | 9 + Modules/MainScreen/SmartPanel.qml | 1 + .../Panels/Accessibility/KeyboardPanel.qml | 196 ++++++++++++++++++ Services/Control/IPCService.qml | 10 + Services/UI/BarWidgetRegistry.qml | 7 + 8 files changed, 409 insertions(+) create mode 100644 Bin/type-key.py create mode 100644 Bin/ydotool_toggle_state.json create mode 100644 Modules/Bar/Widgets/VirtualKeyboard.qml create mode 100644 Modules/Panels/Accessibility/KeyboardPanel.qml diff --git a/Bin/type-key.py b/Bin/type-key.py new file mode 100644 index 000000000..3f0e0e665 --- /dev/null +++ b/Bin/type-key.py @@ -0,0 +1,164 @@ +import subprocess +import sys +import time +import re +import os +import json +import glob + +# ---------- CONSTANTS ---------- +SPECIAL_KEYS = { + "return": 28, "space": 57, "tab": 15, "backspace": 14, "esc": 1, + "left": 105, "up": 103, "right": 106, "down": 108, "caps": 58, "*": 55 +} + +MODIFIER_KEYS = { + "shift": 42, "ctrl": 29, "alt": 56, "super": 125 +} + +AZERTY_TO_QWERTY = { + "a":"q","z":"w","q":"a","w":"z","m":";", + "&":"1","é":"2",'"':"3","'":"4","(":"5","-":"6","è":"7","_":"8","ç":"9","à":"0", + ")":"-","^":"[","$":"]","ù":"'",",":"m",";":",",":":".","!":"/" +} + +STATE_FILE = os.path.abspath(os.path.join(__file__, "../ydotool_toggle_state.json")) + +# ---------- FILE HELPERS ---------- +def load_state(): + if not os.path.isfile(STATE_FILE): + return {} + try: + with open(STATE_FILE, "r") as f: + return json.load(f) + except: + return {} + +def save_state(state): + os.makedirs(os.path.dirname(STATE_FILE), exist_ok=True) + with open(STATE_FILE, "w") as f: + json.dump(state, f) + +# ---------- SYSTEM HELPERS ---------- +def run(cmd): + subprocess.run(cmd, capture_output=False) + +def check_ydotool_service(): + try: + status = subprocess.run( + ["systemctl", "--user", "is-active", "ydotool.service"], + capture_output=True, text=True + ).stdout.strip() + if status != "active": + print("[INFO] Starting ydotool service...") + run(["systemctl", "--user", "start", "ydotool.service"]) + time.sleep(1) + except: + sys.exit("[ERROR] Could not manage ydotool service") + +def get_keyboard_layout(): + try: + out = subprocess.check_output(["localectl", "status"], text=True) + match = re.search(r"Layout:\s+(\w+)", out) + return match.group(1).lower() if match else "unknown" + except: + return "unknown" + +# ---------- KEY ACTIONS ---------- +def press_key(code, down=True): + run(["ydotool", "key", f"{code}:{1 if down else 0}"]) + +def toggle_special_key(name): + code = MODIFIER_KEYS.get(name) + if code is None: + sys.exit(f"[ERROR] Not toggleable: {name}") + + state = load_state() + pressed = state.get(name, False) + + # Toggle + state[name] = not pressed + save_state(state) + + print(f"[INFO] {name} {'DOWN' if state[name] else 'UP'}") + press_key(code, down=state[name]) + +def apply_layout(key, layout): + return AZERTY_TO_QWERTY.get(key.lower(), key) if layout == "fr" else key + +# ---------- SEND KEY ---------- +def send_key(key, modifiers): + layout = get_keyboard_layout() + _key = apply_layout(key, layout) + + # Auto-apply toggled modifiers + if not modifiers: + active = [k for k, v in load_state().items() if v] + if active: + send_key(key, active) + for m in active: + toggle_special_key(m) + return + + # Press modifiers + for m in modifiers: + if m in MODIFIER_KEYS: + press_key(MODIFIER_KEYS[m], True) + + # Special key + if _key in SPECIAL_KEYS: + code = SPECIAL_KEYS[_key] + press_key(code, True) + press_key(code, False) + else: + # Regular text + run(["ydotool", "type", _key]) + + print(f"Text sent: {_key}") + + # Release modifiers + for m in reversed(modifiers): + if m in MODIFIER_KEYS: + press_key(MODIFIER_KEYS[m], False) + +# ---------- RESET ---------- +def reset(): + state = load_state() + + # Release all toggled modifiers + for key, pressed in state.items(): + if pressed: + toggle_special_key(key) + press_key(MODIFIER_KEYS[key], False) + + # Reset CapsLock LED if needed + caps_paths = glob.glob("/sys/class/leds/input*::capslock/brightness") + if not caps_paths: + return + + caps_file = caps_paths[0] + if open(caps_file).read().strip() == "1": + press_key(SPECIAL_KEYS["caps"], True) + press_key(SPECIAL_KEYS["caps"], False) + +# ---------- MAIN ---------- +if __name__ == "__main__": + if len(sys.argv) < 2: + print("Usage: python type-key.py [modifiers...]") + sys.exit(1) + + key = sys.argv[1] + mods = [m.lower() for m in sys.argv[2:]] + + if key == "reset": + reset() + sys.exit(0) + + check_ydotool_service() + + # Toggle mode + if key.lower() in MODIFIER_KEYS and not mods: + toggle_special_key(key.lower()) + sys.exit(0) + + send_key(key.lower(), mods) diff --git a/Bin/ydotool_toggle_state.json b/Bin/ydotool_toggle_state.json new file mode 100644 index 000000000..64af65bca --- /dev/null +++ b/Bin/ydotool_toggle_state.json @@ -0,0 +1 @@ +{"shift": false, "ctrl": false, "super": false} \ No newline at end of file diff --git a/Modules/Bar/Widgets/VirtualKeyboard.qml b/Modules/Bar/Widgets/VirtualKeyboard.qml new file mode 100644 index 000000000..9646c2d94 --- /dev/null +++ b/Modules/Bar/Widgets/VirtualKeyboard.qml @@ -0,0 +1,21 @@ +import QtQuick +import QtQuick.Layouts +import Quickshell +import qs.Commons +import qs.Widgets +import qs.Modules.Bar.Extras +import qs.Services.UI + +NIconButton { + id: root + property ShellScreen screen + icon: "keyboard" + MouseArea { + anchors.fill: parent + onPressed: { + var panel = PanelService.getPanel("keyboardPanel", screen); + panel?.toggle(); + Logger.i("Keyboard", "Virtual Keyboard Toggled") + } + } +} \ No newline at end of file diff --git a/Modules/MainScreen/MainScreen.qml b/Modules/MainScreen/MainScreen.qml index 26fdbd47c..e7043a623 100644 --- a/Modules/MainScreen/MainScreen.qml +++ b/Modules/MainScreen/MainScreen.qml @@ -10,6 +10,7 @@ import qs.Commons // All panels import qs.Modules.Bar import qs.Modules.Bar.Extras +import qs.Modules.Panels.Accessibility import qs.Modules.Panels.Audio import qs.Modules.Panels.Battery import qs.Modules.Panels.Bluetooth @@ -50,6 +51,7 @@ PanelWindow { readonly property alias trayDrawerPanel: trayDrawerPanel readonly property alias wallpaperPanel: wallpaperPanel readonly property alias wifiPanel: wifiPanel + readonly property alias keyboardPanel: keyboardPanel // Expose panel backgrounds for AllBackgrounds readonly property var audioPanelPlaceholder: audioPanel.panelRegion @@ -67,6 +69,7 @@ PanelWindow { readonly property var trayDrawerPanelPlaceholder: trayDrawerPanel.panelRegion readonly property var wallpaperPanelPlaceholder: wallpaperPanel.panelRegion readonly property var wifiPanelPlaceholder: wifiPanel.panelRegion + readonly property var keyboardPanelPlaceholder: keyboardPanel.panelRegion Component.onCompleted: { Logger.d("MainScreen", "Initialized for screen:", screen?.name, "- Dimensions:", screen?.width, "x", screen?.height, "- Position:", screen?.x, ",", screen?.y); @@ -303,6 +306,12 @@ PanelWindow { z: 50 } + KeyboardPanel { + id: keyboardPanel + objectName: "keyboardPanel-" + (root.screen?.name || "unknown") + screen: root.screen + } + // ---------------------------------------------- // Bar background placeholder - just for background positioning (actual bar content is in BarContentWindow) Item { diff --git a/Modules/MainScreen/SmartPanel.qml b/Modules/MainScreen/SmartPanel.qml index 084452e42..7b45909a9 100644 --- a/Modules/MainScreen/SmartPanel.qml +++ b/Modules/MainScreen/SmartPanel.qml @@ -67,6 +67,7 @@ Item { property bool closeWithEscape: true property bool exclusiveKeyboard: true + property bool takesFocus: true // Keyboard event handlers - override these in specific panels to handle shortcuts // These are called from MainScreen's centralized shortcuts diff --git a/Modules/Panels/Accessibility/KeyboardPanel.qml b/Modules/Panels/Accessibility/KeyboardPanel.qml new file mode 100644 index 000000000..9144709dc --- /dev/null +++ b/Modules/Panels/Accessibility/KeyboardPanel.qml @@ -0,0 +1,196 @@ +import QtQuick +import QtQuick.Layouts +import Quickshell +import Quickshell.Io +import qs.Modules.MainScreen +import qs.Commons +import qs.Widgets +import qs.Services.Keyboard + +SmartPanel { + id: root + takesFocus: false + panelAnchorBottom: true + panelAnchorHorizontalCenter: true + preferredWidth: Math.round(1000 * Style.uiScaleRatio) + preferredHeight: Math.round(800 * Style.uiScaleRatio) + readonly property string typeKeyScript: Quickshell.shellDir + '/Bin/type-key.py' + + Process { + id: resetScript + command: ["python", root.typeKeyScript, "reset"] + stderr: StdioCollector { + onStreamFinished: { + Logger.d("Keyboard", "modifier toggles reset") + } + } + } + + onOpened: { + resetScript.running = false + } + onClosed: { + resetScript.running = true + activeModifiers = {"shift": false, "alt": false, "super": false, "ctrl": false, "caps": false} + } + + property var qwerty: [ + // line 1 + [ + { key: "esc", width: 60, txt: "esc" }, { key: "1", width: 60, txt: "1" }, { key: "2", width: 60, txt: "2" }, { key: "3", width: 60, txt: "3" }, + { key: "4", width: 60, txt: "4" }, { key: "5", width: 60, txt: "5" }, { key: "6", width: 60, txt: "6" }, { key: "7", width: 60, txt: "7" }, + { key: "8", width: 60, txt: "8" }, { key: "9", width: 60, txt: "9" }, { key: "0", width: 60, txt: "0" }, { key: "-",width: 60 , txt: "-" }, + { key: "=", width: 60, txt: "=" }, { key: "backspace", width: 100, txt: "⌫" } + ], + // line 2 + [ + { key: "tab", width: 80, txt: "↹" }, { key: "Q", width: 60, txt: "Q" }, { key: "W", width: 60, txt: "W" }, { key: "E", width: 60, txt: "E" }, + { key: "R", width: 60, txt: "R" }, { key: "T", width: 60, txt: "T" }, { key: "Y", width: 60, txt: "Y" }, { key: "U", width: 60, txt: "U" }, + { key: "I", width: 60, txt: "I" }, { key: "O", width: 60, txt: "O" }, { key: "P", width: 60, txt: "P" }, { key: "[", width: 60, txt: "[" }, + { key: "]", width: 60, txt: "]" } + ], + // line 3 + [ + { key: "caps", width: 90, txt: "⇪" }, { key: "A", width: 60, txt: "A" }, { key: "S", width: 60, txt: "S" }, { key: "D", width: 60, txt: "D" }, + { key: "F", width: 60, txt: "F" }, { key: "G", width: 60, txt: "G" }, { key: "H", width: 60, txt: "H" }, { key: "J", width: 60, txt: "J" }, + { key: "K", width: 60, txt: "K" }, { key: "L", width: 60, txt: "L" }, { key: ";", width: 60, txt: ";" }, { key: "'", width: 60, txt: "'" }, + { key: "return", width: 160, txt: "↵" } + ], + // line 4 + [ + { key: "shift", width: 120, txt: "⇧" }, { key: "Z", width: 60, txt: "Z" }, { key: "X", width: 60, txt: "X" }, { key: "C", width: 60, txt: "C" }, + { key: "V", width: 60, txt: "V" }, { key: "B", width: 60, txt: "B" }, { key: "N", width: 60, txt: "N" }, + { key: "M", width: 60, txt: "M" }, { key: ",", width: 60, txt: "," }, { key: ".", width: 60, txt: "." }, { key: "/", width: 60, txt: "/" }, + { key: "up", width: 60, txt: "⭡" } + ], + [ + { key: "ctrl", width: 70, txt: "ctrl" }, { key: "super", width: 60, txt: "⌘" }, { key: "alt", width: 60, txt: "alt" }, + { key: "space", width: 550, txt: "⎵" }, { key: "left", width: 60, txt: "⭠" }, { key: "down", width: 60, txt: "⭣" }, { key: "right", width: 60, txt: "⭢" } + ], + ] + property var azerty: [ + // line 1 + [ + { key: "esc", width: 60, txt: "esc" }, { key: "&", width: 60, txt: "&" }, { key: "é", width: 60, txt: "é" }, { key: "\"", width: 60, txt: "\"" }, + { key: "'", width: 60, txt: "'" }, { key: "(", width: 60, txt: "(" }, { key: "-", width: 60, txt: "-" }, { key: "è", width: 60, txt: "è" }, + { key: "_", width: 60, txt: "_" }, { key: "ç", width: 60, txt: "ç" }, { key: "à", width: 60, txt: "à" }, { key: ")",width: 60 , txt: ")" }, + { key: "=", width: 60, txt: "=" }, { key: "backspace", width: 100, txt: "⌫" } + ], + // line 2 + [ + { key: "tab", width: 80, txt: "↹" }, { key: "A", width: 60, txt: "A" }, { key: "Z", width: 60, txt: "Z" }, { key: "E", width: 60, txt: "E" }, + { key: "R", width: 60, txt: "R" }, { key: "T", width: 60, txt: "T" }, { key: "Y", width: 60, txt: "Y" }, { key: "U", width: 60, txt: "U" }, + { key: "I", width: 60, txt: "I" }, { key: "O", width: 60, txt: "O" }, { key: "P", width: 60, txt: "P" }, { key: "^", width: 60, txt: "^" }, + { key: "$", width: 60, txt: "$" } + ], + // line 3 + [ + { key: "caps", width: 90, txt: "⇪" }, { key: "Q", width: 60, txt: "Q" }, { key: "S", width: 60, txt: "S" }, { key: "D", width: 60, txt: "D" }, + { key: "F", width: 60, txt: "F" }, { key: "G", width: 60, txt: "G" }, { key: "H", width: 60, txt: "H" }, { key: "J", width: 60, txt: "J" }, + { key: "K", width: 60, txt: "K" }, { key: "L", width: 60, txt: "L" }, { key: "M", width: 60, txt: "M" }, { key: "ù", width: 60, txt: "ù" }, + { key: "*", width: 60, txt: "*" }, { key: "return", width: 100, txt: "↵" } + ], + // line 4 + [ + { key: "shift", width: 120, txt: "⇧" }, { key: "W", width: 60, txt: "W" }, { key: "X", width: 60, txt: "X" }, { key: "C", width: 60, txt: "C" }, + { key: "V", width: 60, txt: "V" }, { key: "B", width: 60, txt: "B" }, { key: "N", width: 60, txt: "N" }, + { key: ",", width: 60, txt: "," }, { key: ";", width: 60, txt: ";" }, { key: ":", width: 60, txt: ":" }, { key: "!", width: 60, txt: "!" }, + { key: "up", width: 60, txt: "⭡" } + ], + // line 5 + [ + { key: "ctrl", width: 70, txt: "ctrl" }, { key: "super", width: 60, txt: "⌘" }, { key: "alt", width: 60, txt: "alt" }, + { key: "space", width: 550, txt: "⎵" }, { key: "left", width: 60, txt: "⭠" }, { key: "down", width: 60, txt: "⭣" }, { key: "right", width: 60, txt: "⭢" } + ] + ] + + property var layout: KeyboardLayoutService.currentLayout === "fr" ? azerty : qwerty + + property var activeModifiers: {"shift": false, "alt": false, "super": false, "ctrl": false, "caps": false} + + panelContent: Item { + + property real contentPreferredHeight: mainColumn.implicitHeight + Style.marginL * 2 + + ColumnLayout { + id: mainColumn + anchors.fill: parent + anchors.margins: Style.marginL + spacing: Style.marginM + + Repeater { + model: root.layout + + RowLayout { + spacing: Style.marginL + + Repeater { + model: modelData + + NBox { + width: modelData.width + height: 60 + color: (runScript.running || (modelData.key in root.activeModifiers & root.activeModifiers[modelData.key])) ? Color.mPrimary : Color.mSurfaceVariant + + // refresh color ever 0.2 seconds for modifier keys only + Timer { + interval: 200; running: true; repeat: true + onTriggered: { + if (modelData.key in root.activeModifiers) { + color = (runScript.running || (modelData.key in root.activeModifiers & root.activeModifiers[modelData.key])) ? Color.mPrimary : Color.mSurfaceVariant + } + } + } + + NText { + anchors.centerIn: parent + text: modelData.txt + } + + function toggleModifier(mod) { + if (mod in root.activeModifiers) { + root.activeModifiers[mod] = !root.activeModifiers[mod] + } + else { + // pressed a non-modifier key, reset modifiers (exept caps-lock) + root.activeModifiers["shift"] = false + root.activeModifiers["ctrl"] = false + root.activeModifiers["alt"] = false + root.activeModifiers["super"] = false + } + } + + Process { + id: runScript + command: ["python", root.typeKeyScript, modelData.key.toString()] + stdout: StdioCollector { + onStreamFinished: { + toggleModifier(modelData.key.toString()) + } + } + stderr: StdioCollector { + onStreamFinished: { + if(text) { + Logger.w("Keyboard", text.trim()); + } + } + } + } + + MouseArea { + anchors.fill: parent + onPressed: { + runScript.running = true + Logger.d(modelData.key.toString()) + } + onReleased: { + runScript.running = false + } + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/Services/Control/IPCService.qml b/Services/Control/IPCService.qml index 5c676ed8f..90e05273d 100644 --- a/Services/Control/IPCService.qml +++ b/Services/Control/IPCService.qml @@ -52,6 +52,16 @@ Item { } } + IpcHandler { + target: "keyboard" + function toggle() { + root.withTargetScreen(screen => { + var keyboardPanel = PanelService.getPanel("keyboardPanel", screen); + keyboardPanel?.toggle(); + }); + } + } + IpcHandler { target: "notifications" function toggleHistory() { diff --git a/Services/UI/BarWidgetRegistry.qml b/Services/UI/BarWidgetRegistry.qml index d6f0a881e..9a0ead99b 100644 --- a/Services/UI/BarWidgetRegistry.qml +++ b/Services/UI/BarWidgetRegistry.qml @@ -35,6 +35,7 @@ Singleton { "Taskbar": taskbarComponent, "TaskbarGrouped": taskbarGroupedComponent, "Tray": trayComponent, + "VirtualKeyboard": virtualKeyboardComponent, "Volume": volumeComponent, "VPN": vpnComponent, "WiFi": wiFiComponent, @@ -230,6 +231,9 @@ Singleton { "hideUnoccupied": false, "characterCount": 2 }, + "VirtualKeyboard": { + "allowUserSettings": false + }, "Volume": { "allowUserSettings": true, "displayMode": "onhover" @@ -306,6 +310,9 @@ Singleton { property Component trayComponent: Component { Tray {} } + property Component virtualKeyboardComponent: Component { + VirtualKeyboard {} + } property Component volumeComponent: Component { Volume {} } From ad62adf924b3bb4bbd6a595d4431a444dc658cfb Mon Sep 17 00:00:00 2001 From: AdrienPiechocki <112864425+AdrienPiechocki@users.noreply.github.com> Date: Sun, 30 Nov 2025 17:55:06 +0100 Subject: [PATCH 4/4] fixed my mistake --- Bin/type-key.py | 164 --------------- Bin/ydotool_toggle_state.json | 1 - Modules/Bar/Widgets/VirtualKeyboard.qml | 21 -- Modules/MainScreen/MainScreen.qml | 9 - Modules/MainScreen/SmartPanel.qml | 1 - .../Panels/Accessibility/KeyboardPanel.qml | 196 ------------------ Services/Control/IPCService.qml | 10 - Services/UI/BarWidgetRegistry.qml | 7 - 8 files changed, 409 deletions(-) delete mode 100644 Bin/type-key.py delete mode 100644 Bin/ydotool_toggle_state.json delete mode 100644 Modules/Bar/Widgets/VirtualKeyboard.qml delete mode 100644 Modules/Panels/Accessibility/KeyboardPanel.qml diff --git a/Bin/type-key.py b/Bin/type-key.py deleted file mode 100644 index 3f0e0e665..000000000 --- a/Bin/type-key.py +++ /dev/null @@ -1,164 +0,0 @@ -import subprocess -import sys -import time -import re -import os -import json -import glob - -# ---------- CONSTANTS ---------- -SPECIAL_KEYS = { - "return": 28, "space": 57, "tab": 15, "backspace": 14, "esc": 1, - "left": 105, "up": 103, "right": 106, "down": 108, "caps": 58, "*": 55 -} - -MODIFIER_KEYS = { - "shift": 42, "ctrl": 29, "alt": 56, "super": 125 -} - -AZERTY_TO_QWERTY = { - "a":"q","z":"w","q":"a","w":"z","m":";", - "&":"1","é":"2",'"':"3","'":"4","(":"5","-":"6","è":"7","_":"8","ç":"9","à":"0", - ")":"-","^":"[","$":"]","ù":"'",",":"m",";":",",":":".","!":"/" -} - -STATE_FILE = os.path.abspath(os.path.join(__file__, "../ydotool_toggle_state.json")) - -# ---------- FILE HELPERS ---------- -def load_state(): - if not os.path.isfile(STATE_FILE): - return {} - try: - with open(STATE_FILE, "r") as f: - return json.load(f) - except: - return {} - -def save_state(state): - os.makedirs(os.path.dirname(STATE_FILE), exist_ok=True) - with open(STATE_FILE, "w") as f: - json.dump(state, f) - -# ---------- SYSTEM HELPERS ---------- -def run(cmd): - subprocess.run(cmd, capture_output=False) - -def check_ydotool_service(): - try: - status = subprocess.run( - ["systemctl", "--user", "is-active", "ydotool.service"], - capture_output=True, text=True - ).stdout.strip() - if status != "active": - print("[INFO] Starting ydotool service...") - run(["systemctl", "--user", "start", "ydotool.service"]) - time.sleep(1) - except: - sys.exit("[ERROR] Could not manage ydotool service") - -def get_keyboard_layout(): - try: - out = subprocess.check_output(["localectl", "status"], text=True) - match = re.search(r"Layout:\s+(\w+)", out) - return match.group(1).lower() if match else "unknown" - except: - return "unknown" - -# ---------- KEY ACTIONS ---------- -def press_key(code, down=True): - run(["ydotool", "key", f"{code}:{1 if down else 0}"]) - -def toggle_special_key(name): - code = MODIFIER_KEYS.get(name) - if code is None: - sys.exit(f"[ERROR] Not toggleable: {name}") - - state = load_state() - pressed = state.get(name, False) - - # Toggle - state[name] = not pressed - save_state(state) - - print(f"[INFO] {name} {'DOWN' if state[name] else 'UP'}") - press_key(code, down=state[name]) - -def apply_layout(key, layout): - return AZERTY_TO_QWERTY.get(key.lower(), key) if layout == "fr" else key - -# ---------- SEND KEY ---------- -def send_key(key, modifiers): - layout = get_keyboard_layout() - _key = apply_layout(key, layout) - - # Auto-apply toggled modifiers - if not modifiers: - active = [k for k, v in load_state().items() if v] - if active: - send_key(key, active) - for m in active: - toggle_special_key(m) - return - - # Press modifiers - for m in modifiers: - if m in MODIFIER_KEYS: - press_key(MODIFIER_KEYS[m], True) - - # Special key - if _key in SPECIAL_KEYS: - code = SPECIAL_KEYS[_key] - press_key(code, True) - press_key(code, False) - else: - # Regular text - run(["ydotool", "type", _key]) - - print(f"Text sent: {_key}") - - # Release modifiers - for m in reversed(modifiers): - if m in MODIFIER_KEYS: - press_key(MODIFIER_KEYS[m], False) - -# ---------- RESET ---------- -def reset(): - state = load_state() - - # Release all toggled modifiers - for key, pressed in state.items(): - if pressed: - toggle_special_key(key) - press_key(MODIFIER_KEYS[key], False) - - # Reset CapsLock LED if needed - caps_paths = glob.glob("/sys/class/leds/input*::capslock/brightness") - if not caps_paths: - return - - caps_file = caps_paths[0] - if open(caps_file).read().strip() == "1": - press_key(SPECIAL_KEYS["caps"], True) - press_key(SPECIAL_KEYS["caps"], False) - -# ---------- MAIN ---------- -if __name__ == "__main__": - if len(sys.argv) < 2: - print("Usage: python type-key.py [modifiers...]") - sys.exit(1) - - key = sys.argv[1] - mods = [m.lower() for m in sys.argv[2:]] - - if key == "reset": - reset() - sys.exit(0) - - check_ydotool_service() - - # Toggle mode - if key.lower() in MODIFIER_KEYS and not mods: - toggle_special_key(key.lower()) - sys.exit(0) - - send_key(key.lower(), mods) diff --git a/Bin/ydotool_toggle_state.json b/Bin/ydotool_toggle_state.json deleted file mode 100644 index 64af65bca..000000000 --- a/Bin/ydotool_toggle_state.json +++ /dev/null @@ -1 +0,0 @@ -{"shift": false, "ctrl": false, "super": false} \ No newline at end of file diff --git a/Modules/Bar/Widgets/VirtualKeyboard.qml b/Modules/Bar/Widgets/VirtualKeyboard.qml deleted file mode 100644 index 9646c2d94..000000000 --- a/Modules/Bar/Widgets/VirtualKeyboard.qml +++ /dev/null @@ -1,21 +0,0 @@ -import QtQuick -import QtQuick.Layouts -import Quickshell -import qs.Commons -import qs.Widgets -import qs.Modules.Bar.Extras -import qs.Services.UI - -NIconButton { - id: root - property ShellScreen screen - icon: "keyboard" - MouseArea { - anchors.fill: parent - onPressed: { - var panel = PanelService.getPanel("keyboardPanel", screen); - panel?.toggle(); - Logger.i("Keyboard", "Virtual Keyboard Toggled") - } - } -} \ No newline at end of file diff --git a/Modules/MainScreen/MainScreen.qml b/Modules/MainScreen/MainScreen.qml index e7043a623..26fdbd47c 100644 --- a/Modules/MainScreen/MainScreen.qml +++ b/Modules/MainScreen/MainScreen.qml @@ -10,7 +10,6 @@ import qs.Commons // All panels import qs.Modules.Bar import qs.Modules.Bar.Extras -import qs.Modules.Panels.Accessibility import qs.Modules.Panels.Audio import qs.Modules.Panels.Battery import qs.Modules.Panels.Bluetooth @@ -51,7 +50,6 @@ PanelWindow { readonly property alias trayDrawerPanel: trayDrawerPanel readonly property alias wallpaperPanel: wallpaperPanel readonly property alias wifiPanel: wifiPanel - readonly property alias keyboardPanel: keyboardPanel // Expose panel backgrounds for AllBackgrounds readonly property var audioPanelPlaceholder: audioPanel.panelRegion @@ -69,7 +67,6 @@ PanelWindow { readonly property var trayDrawerPanelPlaceholder: trayDrawerPanel.panelRegion readonly property var wallpaperPanelPlaceholder: wallpaperPanel.panelRegion readonly property var wifiPanelPlaceholder: wifiPanel.panelRegion - readonly property var keyboardPanelPlaceholder: keyboardPanel.panelRegion Component.onCompleted: { Logger.d("MainScreen", "Initialized for screen:", screen?.name, "- Dimensions:", screen?.width, "x", screen?.height, "- Position:", screen?.x, ",", screen?.y); @@ -306,12 +303,6 @@ PanelWindow { z: 50 } - KeyboardPanel { - id: keyboardPanel - objectName: "keyboardPanel-" + (root.screen?.name || "unknown") - screen: root.screen - } - // ---------------------------------------------- // Bar background placeholder - just for background positioning (actual bar content is in BarContentWindow) Item { diff --git a/Modules/MainScreen/SmartPanel.qml b/Modules/MainScreen/SmartPanel.qml index 7b45909a9..084452e42 100644 --- a/Modules/MainScreen/SmartPanel.qml +++ b/Modules/MainScreen/SmartPanel.qml @@ -67,7 +67,6 @@ Item { property bool closeWithEscape: true property bool exclusiveKeyboard: true - property bool takesFocus: true // Keyboard event handlers - override these in specific panels to handle shortcuts // These are called from MainScreen's centralized shortcuts diff --git a/Modules/Panels/Accessibility/KeyboardPanel.qml b/Modules/Panels/Accessibility/KeyboardPanel.qml deleted file mode 100644 index 9144709dc..000000000 --- a/Modules/Panels/Accessibility/KeyboardPanel.qml +++ /dev/null @@ -1,196 +0,0 @@ -import QtQuick -import QtQuick.Layouts -import Quickshell -import Quickshell.Io -import qs.Modules.MainScreen -import qs.Commons -import qs.Widgets -import qs.Services.Keyboard - -SmartPanel { - id: root - takesFocus: false - panelAnchorBottom: true - panelAnchorHorizontalCenter: true - preferredWidth: Math.round(1000 * Style.uiScaleRatio) - preferredHeight: Math.round(800 * Style.uiScaleRatio) - readonly property string typeKeyScript: Quickshell.shellDir + '/Bin/type-key.py' - - Process { - id: resetScript - command: ["python", root.typeKeyScript, "reset"] - stderr: StdioCollector { - onStreamFinished: { - Logger.d("Keyboard", "modifier toggles reset") - } - } - } - - onOpened: { - resetScript.running = false - } - onClosed: { - resetScript.running = true - activeModifiers = {"shift": false, "alt": false, "super": false, "ctrl": false, "caps": false} - } - - property var qwerty: [ - // line 1 - [ - { key: "esc", width: 60, txt: "esc" }, { key: "1", width: 60, txt: "1" }, { key: "2", width: 60, txt: "2" }, { key: "3", width: 60, txt: "3" }, - { key: "4", width: 60, txt: "4" }, { key: "5", width: 60, txt: "5" }, { key: "6", width: 60, txt: "6" }, { key: "7", width: 60, txt: "7" }, - { key: "8", width: 60, txt: "8" }, { key: "9", width: 60, txt: "9" }, { key: "0", width: 60, txt: "0" }, { key: "-",width: 60 , txt: "-" }, - { key: "=", width: 60, txt: "=" }, { key: "backspace", width: 100, txt: "⌫" } - ], - // line 2 - [ - { key: "tab", width: 80, txt: "↹" }, { key: "Q", width: 60, txt: "Q" }, { key: "W", width: 60, txt: "W" }, { key: "E", width: 60, txt: "E" }, - { key: "R", width: 60, txt: "R" }, { key: "T", width: 60, txt: "T" }, { key: "Y", width: 60, txt: "Y" }, { key: "U", width: 60, txt: "U" }, - { key: "I", width: 60, txt: "I" }, { key: "O", width: 60, txt: "O" }, { key: "P", width: 60, txt: "P" }, { key: "[", width: 60, txt: "[" }, - { key: "]", width: 60, txt: "]" } - ], - // line 3 - [ - { key: "caps", width: 90, txt: "⇪" }, { key: "A", width: 60, txt: "A" }, { key: "S", width: 60, txt: "S" }, { key: "D", width: 60, txt: "D" }, - { key: "F", width: 60, txt: "F" }, { key: "G", width: 60, txt: "G" }, { key: "H", width: 60, txt: "H" }, { key: "J", width: 60, txt: "J" }, - { key: "K", width: 60, txt: "K" }, { key: "L", width: 60, txt: "L" }, { key: ";", width: 60, txt: ";" }, { key: "'", width: 60, txt: "'" }, - { key: "return", width: 160, txt: "↵" } - ], - // line 4 - [ - { key: "shift", width: 120, txt: "⇧" }, { key: "Z", width: 60, txt: "Z" }, { key: "X", width: 60, txt: "X" }, { key: "C", width: 60, txt: "C" }, - { key: "V", width: 60, txt: "V" }, { key: "B", width: 60, txt: "B" }, { key: "N", width: 60, txt: "N" }, - { key: "M", width: 60, txt: "M" }, { key: ",", width: 60, txt: "," }, { key: ".", width: 60, txt: "." }, { key: "/", width: 60, txt: "/" }, - { key: "up", width: 60, txt: "⭡" } - ], - [ - { key: "ctrl", width: 70, txt: "ctrl" }, { key: "super", width: 60, txt: "⌘" }, { key: "alt", width: 60, txt: "alt" }, - { key: "space", width: 550, txt: "⎵" }, { key: "left", width: 60, txt: "⭠" }, { key: "down", width: 60, txt: "⭣" }, { key: "right", width: 60, txt: "⭢" } - ], - ] - property var azerty: [ - // line 1 - [ - { key: "esc", width: 60, txt: "esc" }, { key: "&", width: 60, txt: "&" }, { key: "é", width: 60, txt: "é" }, { key: "\"", width: 60, txt: "\"" }, - { key: "'", width: 60, txt: "'" }, { key: "(", width: 60, txt: "(" }, { key: "-", width: 60, txt: "-" }, { key: "è", width: 60, txt: "è" }, - { key: "_", width: 60, txt: "_" }, { key: "ç", width: 60, txt: "ç" }, { key: "à", width: 60, txt: "à" }, { key: ")",width: 60 , txt: ")" }, - { key: "=", width: 60, txt: "=" }, { key: "backspace", width: 100, txt: "⌫" } - ], - // line 2 - [ - { key: "tab", width: 80, txt: "↹" }, { key: "A", width: 60, txt: "A" }, { key: "Z", width: 60, txt: "Z" }, { key: "E", width: 60, txt: "E" }, - { key: "R", width: 60, txt: "R" }, { key: "T", width: 60, txt: "T" }, { key: "Y", width: 60, txt: "Y" }, { key: "U", width: 60, txt: "U" }, - { key: "I", width: 60, txt: "I" }, { key: "O", width: 60, txt: "O" }, { key: "P", width: 60, txt: "P" }, { key: "^", width: 60, txt: "^" }, - { key: "$", width: 60, txt: "$" } - ], - // line 3 - [ - { key: "caps", width: 90, txt: "⇪" }, { key: "Q", width: 60, txt: "Q" }, { key: "S", width: 60, txt: "S" }, { key: "D", width: 60, txt: "D" }, - { key: "F", width: 60, txt: "F" }, { key: "G", width: 60, txt: "G" }, { key: "H", width: 60, txt: "H" }, { key: "J", width: 60, txt: "J" }, - { key: "K", width: 60, txt: "K" }, { key: "L", width: 60, txt: "L" }, { key: "M", width: 60, txt: "M" }, { key: "ù", width: 60, txt: "ù" }, - { key: "*", width: 60, txt: "*" }, { key: "return", width: 100, txt: "↵" } - ], - // line 4 - [ - { key: "shift", width: 120, txt: "⇧" }, { key: "W", width: 60, txt: "W" }, { key: "X", width: 60, txt: "X" }, { key: "C", width: 60, txt: "C" }, - { key: "V", width: 60, txt: "V" }, { key: "B", width: 60, txt: "B" }, { key: "N", width: 60, txt: "N" }, - { key: ",", width: 60, txt: "," }, { key: ";", width: 60, txt: ";" }, { key: ":", width: 60, txt: ":" }, { key: "!", width: 60, txt: "!" }, - { key: "up", width: 60, txt: "⭡" } - ], - // line 5 - [ - { key: "ctrl", width: 70, txt: "ctrl" }, { key: "super", width: 60, txt: "⌘" }, { key: "alt", width: 60, txt: "alt" }, - { key: "space", width: 550, txt: "⎵" }, { key: "left", width: 60, txt: "⭠" }, { key: "down", width: 60, txt: "⭣" }, { key: "right", width: 60, txt: "⭢" } - ] - ] - - property var layout: KeyboardLayoutService.currentLayout === "fr" ? azerty : qwerty - - property var activeModifiers: {"shift": false, "alt": false, "super": false, "ctrl": false, "caps": false} - - panelContent: Item { - - property real contentPreferredHeight: mainColumn.implicitHeight + Style.marginL * 2 - - ColumnLayout { - id: mainColumn - anchors.fill: parent - anchors.margins: Style.marginL - spacing: Style.marginM - - Repeater { - model: root.layout - - RowLayout { - spacing: Style.marginL - - Repeater { - model: modelData - - NBox { - width: modelData.width - height: 60 - color: (runScript.running || (modelData.key in root.activeModifiers & root.activeModifiers[modelData.key])) ? Color.mPrimary : Color.mSurfaceVariant - - // refresh color ever 0.2 seconds for modifier keys only - Timer { - interval: 200; running: true; repeat: true - onTriggered: { - if (modelData.key in root.activeModifiers) { - color = (runScript.running || (modelData.key in root.activeModifiers & root.activeModifiers[modelData.key])) ? Color.mPrimary : Color.mSurfaceVariant - } - } - } - - NText { - anchors.centerIn: parent - text: modelData.txt - } - - function toggleModifier(mod) { - if (mod in root.activeModifiers) { - root.activeModifiers[mod] = !root.activeModifiers[mod] - } - else { - // pressed a non-modifier key, reset modifiers (exept caps-lock) - root.activeModifiers["shift"] = false - root.activeModifiers["ctrl"] = false - root.activeModifiers["alt"] = false - root.activeModifiers["super"] = false - } - } - - Process { - id: runScript - command: ["python", root.typeKeyScript, modelData.key.toString()] - stdout: StdioCollector { - onStreamFinished: { - toggleModifier(modelData.key.toString()) - } - } - stderr: StdioCollector { - onStreamFinished: { - if(text) { - Logger.w("Keyboard", text.trim()); - } - } - } - } - - MouseArea { - anchors.fill: parent - onPressed: { - runScript.running = true - Logger.d(modelData.key.toString()) - } - onReleased: { - runScript.running = false - } - } - } - } - } - } - } - } -} \ No newline at end of file diff --git a/Services/Control/IPCService.qml b/Services/Control/IPCService.qml index 90e05273d..5c676ed8f 100644 --- a/Services/Control/IPCService.qml +++ b/Services/Control/IPCService.qml @@ -52,16 +52,6 @@ Item { } } - IpcHandler { - target: "keyboard" - function toggle() { - root.withTargetScreen(screen => { - var keyboardPanel = PanelService.getPanel("keyboardPanel", screen); - keyboardPanel?.toggle(); - }); - } - } - IpcHandler { target: "notifications" function toggleHistory() { diff --git a/Services/UI/BarWidgetRegistry.qml b/Services/UI/BarWidgetRegistry.qml index 9a0ead99b..d6f0a881e 100644 --- a/Services/UI/BarWidgetRegistry.qml +++ b/Services/UI/BarWidgetRegistry.qml @@ -35,7 +35,6 @@ Singleton { "Taskbar": taskbarComponent, "TaskbarGrouped": taskbarGroupedComponent, "Tray": trayComponent, - "VirtualKeyboard": virtualKeyboardComponent, "Volume": volumeComponent, "VPN": vpnComponent, "WiFi": wiFiComponent, @@ -231,9 +230,6 @@ Singleton { "hideUnoccupied": false, "characterCount": 2 }, - "VirtualKeyboard": { - "allowUserSettings": false - }, "Volume": { "allowUserSettings": true, "displayMode": "onhover" @@ -310,9 +306,6 @@ Singleton { property Component trayComponent: Component { Tray {} } - property Component virtualKeyboardComponent: Component { - VirtualKeyboard {} - } property Component volumeComponent: Component { Volume {} }