diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index de1d7e7..db323d2 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -15,12 +15,38 @@ body: label: Mouser version description: > Which version of Mouser are you using? Check the release you downloaded - from the [releases page](https://github.com/TomBadash/MouseControl/releases), - or look at the zip/dmg filename (e.g. "v3.5.0"). - placeholder: "e.g. v3.5.0" + from the [releases page](https://github.com/TomBadash/Mouser/releases), + the version shown inside the app, or the exact asset filename you ran. + If you are running from source or a self-built package, say that here too. + placeholder: "e.g. v3.5.3, source checkout, or self-built from main" validations: required: true + - type: dropdown + id: install-method + attributes: + label: How are you running Mouser? + options: + - Official GitHub release asset + - Run from source (`python main_qml.py`) + - Self-built app / executable + - Other + validations: + required: true + + - type: input + id: build-details + attributes: + label: Build / asset details + description: > + Include the exact zip/dmg/exe filename, architecture, and commit / branch + if you built it yourself. This is critical when a source checkout works + but a packaged build does not. + placeholder: > + e.g. Mouser-macOS-intel.zip, Windows zip from v3.5.3, or main @ abc1234 + validations: + required: false + - type: dropdown id: os attributes: @@ -123,8 +149,12 @@ body: label: Log file contents description: > Logs help a lot when diagnosing issues. Paste the relevant portion of - your log file (the last ~100 lines around the time of the bug are - usually enough). + your log file, including the startup lines if possible. The startup + lines identify the Mouser version, commit, and whether it was launched + from a packaged app or a source checkout. + + + The last ~100 lines around the time of the bug are usually enough. **Log file locations:** diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 337a4a6..196cd2a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -8,13 +8,19 @@ on: permissions: contents: write +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true" + MOUSER_VERSION: ${{ startsWith(github.ref, 'refs/tags/') && github.ref_name || '' }} + MOUSER_GIT_COMMIT: ${{ github.sha }} + MOUSER_GIT_DIRTY: "false" + jobs: build-windows: runs-on: windows-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - - uses: actions/setup-python@v5 + - uses: actions/setup-python@v6 with: python-version: "3.12" @@ -27,7 +33,7 @@ jobs: - name: Create archive run: Compress-Archive -Path dist\Mouser -DestinationPath Mouser-Windows.zip - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@v7 with: name: Mouser-Windows path: Mouser-Windows.zip @@ -52,9 +58,9 @@ jobs: archive_name: Mouser-macOS-intel.zip runs-on: ${{ matrix.runner }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - - uses: actions/setup-python@v5 + - uses: actions/setup-python@v6 with: python-version: "3.12" architecture: ${{ matrix.python_architecture }} @@ -72,7 +78,7 @@ jobs: - name: Create archive run: cd dist && zip -r -y "../${{ matrix.archive_name }}" Mouser.app - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@v7 with: name: ${{ matrix.artifact_name }} path: ${{ matrix.archive_name }} @@ -80,9 +86,9 @@ jobs: build-linux: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - - uses: actions/setup-python@v5 + - uses: actions/setup-python@v6 with: python-version: "3.12" @@ -98,7 +104,7 @@ jobs: - name: Create archive run: cd dist && zip -r -y ../Mouser-Linux.zip Mouser - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@v7 with: name: Mouser-Linux path: Mouser-Linux.zip @@ -108,7 +114,7 @@ jobs: needs: [build-windows, build-macos, build-linux] runs-on: ubuntu-latest steps: - - uses: actions/download-artifact@v4 + - uses: actions/download-artifact@v8 - name: Create or update GitHub Release env: diff --git a/Mouser-linux.spec b/Mouser-linux.spec index e3d0264..c7f471d 100644 --- a/Mouser-linux.spec +++ b/Mouser-linux.spec @@ -9,8 +9,73 @@ Output: dist/Mouser/ (directory with Mouser executable + dependencies) """ import os +import json +import subprocess ROOT = os.path.abspath(".") +BUILD_INFO_PATH = os.path.join(ROOT, "build", "mouser_build_info.json") + + +def _load_app_version() -> str: + version_path = os.path.join(ROOT, "core", "version.py") + namespace = {"__file__": version_path} + with open(version_path, encoding="utf-8") as version_file: + exec(version_file.read(), namespace) + return namespace["APP_VERSION"] + + +def _run_git(args): + try: + return subprocess.check_output( + ["git", *args], + cwd=ROOT, + stderr=subprocess.DEVNULL, + text=True, + timeout=0.5, + ).strip() + except (OSError, subprocess.SubprocessError): + return "" + + +def _git_dirty(): + try: + result = subprocess.run( + ["git", "status", "--porcelain", "--untracked-files=no"], + cwd=ROOT, + stdout=subprocess.PIPE, + stderr=subprocess.DEVNULL, + text=True, + timeout=0.5, + check=False, + ) + except (OSError, subprocess.SubprocessError): + return False + return result.returncode == 0 and bool(result.stdout.strip()) + + +def _write_build_info(version: str) -> str: + commit = os.environ.get("MOUSER_GIT_COMMIT", "").strip() or _run_git(["rev-parse", "HEAD"]) + dirty_env = os.environ.get("MOUSER_GIT_DIRTY") + if dirty_env: + dirty = dirty_env.strip().lower() in {"1", "true", "yes", "on"} + else: + dirty = _git_dirty() + + os.makedirs(os.path.dirname(BUILD_INFO_PATH), exist_ok=True) + with open(BUILD_INFO_PATH, "w", encoding="utf-8") as build_info_file: + json.dump( + { + "version": version, + "commit": commit, + "dirty": dirty, + }, + build_info_file, + ) + return BUILD_INFO_PATH + + +APP_VERSION = _load_app_version() +BUILD_INFO_DATA = _write_build_info(APP_VERSION) a = Analysis( ["main_qml.py"], @@ -19,6 +84,7 @@ a = Analysis( datas=[ (os.path.join(ROOT, "ui", "qml"), os.path.join("ui", "qml")), (os.path.join(ROOT, "images"), "images"), + (BUILD_INFO_DATA, "."), ], hiddenimports=[ "hid", diff --git a/Mouser-mac.spec b/Mouser-mac.spec index 5c39c9e..2d943be 100644 --- a/Mouser-mac.spec +++ b/Mouser-mac.spec @@ -10,10 +10,13 @@ environment supports it: """ import os +import json +import subprocess ROOT = os.path.abspath(".") COMMITTED_ICON = os.path.join(ROOT, "images", "AppIcon.icns") GENERATED_ICON = os.path.join(ROOT, "build", "macos", "Mouser.icns") +BUILD_INFO_PATH = os.path.join(ROOT, "build", "mouser_build_info.json") TARGET_ARCH = os.environ.get("PYINSTALLER_TARGET_ARCH", "").strip() or None if TARGET_ARCH not in (None, "arm64", "x86_64", "universal2"): raise SystemExit( @@ -27,6 +30,68 @@ else: ICON_PATH = None BUNDLE_ID = "io.github.tombadash.mouser" + +def _load_app_version() -> str: + version_path = os.path.join(ROOT, "core", "version.py") + namespace = {"__file__": version_path} + with open(version_path, encoding="utf-8") as version_file: + exec(version_file.read(), namespace) + return namespace["APP_VERSION"] + + +def _run_git(args): + try: + return subprocess.check_output( + ["git", *args], + cwd=ROOT, + stderr=subprocess.DEVNULL, + text=True, + timeout=0.5, + ).strip() + except (OSError, subprocess.SubprocessError): + return "" + + +def _git_dirty(): + try: + result = subprocess.run( + ["git", "status", "--porcelain", "--untracked-files=no"], + cwd=ROOT, + stdout=subprocess.PIPE, + stderr=subprocess.DEVNULL, + text=True, + timeout=0.5, + check=False, + ) + except (OSError, subprocess.SubprocessError): + return False + return result.returncode == 0 and bool(result.stdout.strip()) + + +def _write_build_info(version: str) -> str: + commit = os.environ.get("MOUSER_GIT_COMMIT", "").strip() or _run_git(["rev-parse", "HEAD"]) + dirty_env = os.environ.get("MOUSER_GIT_DIRTY") + if dirty_env: + dirty = dirty_env.strip().lower() in {"1", "true", "yes", "on"} + else: + dirty = _git_dirty() + + os.makedirs(os.path.dirname(BUILD_INFO_PATH), exist_ok=True) + with open(BUILD_INFO_PATH, "w", encoding="utf-8") as build_info_file: + json.dump( + { + "version": version, + "commit": commit, + "dirty": dirty, + }, + build_info_file, + ) + return BUILD_INFO_PATH + + +APP_VERSION = _load_app_version() +BUILD_INFO_DATA = _write_build_info(APP_VERSION) + a = Analysis( ["main_qml.py"], pathex=[ROOT], @@ -34,6 +99,7 @@ a = Analysis( datas=[ (os.path.join(ROOT, "ui", "qml"), os.path.join("ui", "qml")), (os.path.join(ROOT, "images"), "images"), + (BUILD_INFO_DATA, "."), ], hiddenimports=[ "hid", @@ -242,8 +308,8 @@ app = BUNDLE( info_plist={ "CFBundleDisplayName": "Mouser", "CFBundleName": "Mouser", - "CFBundleShortVersionString": "3.5.3", - "CFBundleVersion": "3.5.3", + "CFBundleShortVersionString": APP_VERSION, + "CFBundleVersion": APP_VERSION, "LSMinimumSystemVersion": "12.0", "LSUIElement": True, "NSHighResolutionCapable": True, diff --git a/Mouser.spec b/Mouser.spec index cc5e133..0619a44 100644 --- a/Mouser.spec +++ b/Mouser.spec @@ -8,11 +8,76 @@ Run: pyinstaller Mouser.spec import os import sys import shutil +import json +import subprocess import PySide6 block_cipher = None ROOT = os.path.abspath(".") PYSIDE6_DIR = os.path.dirname(PySide6.__file__) +BUILD_INFO_PATH = os.path.join(ROOT, "build", "mouser_build_info.json") + + +def _load_app_version() -> str: + version_path = os.path.join(ROOT, "core", "version.py") + namespace = {"__file__": version_path} + with open(version_path, encoding="utf-8") as version_file: + exec(version_file.read(), namespace) + return namespace["APP_VERSION"] + + +def _run_git(args): + try: + return subprocess.check_output( + ["git", *args], + cwd=ROOT, + stderr=subprocess.DEVNULL, + text=True, + timeout=0.5, + ).strip() + except (OSError, subprocess.SubprocessError): + return "" + + +def _git_dirty(): + try: + result = subprocess.run( + ["git", "status", "--porcelain", "--untracked-files=no"], + cwd=ROOT, + stdout=subprocess.PIPE, + stderr=subprocess.DEVNULL, + text=True, + timeout=0.5, + check=False, + ) + except (OSError, subprocess.SubprocessError): + return False + return result.returncode == 0 and bool(result.stdout.strip()) + + +def _write_build_info(version: str) -> str: + commit = os.environ.get("MOUSER_GIT_COMMIT", "").strip() or _run_git(["rev-parse", "HEAD"]) + dirty_env = os.environ.get("MOUSER_GIT_DIRTY") + if dirty_env: + dirty = dirty_env.strip().lower() in {"1", "true", "yes", "on"} + else: + dirty = _git_dirty() + + os.makedirs(os.path.dirname(BUILD_INFO_PATH), exist_ok=True) + with open(BUILD_INFO_PATH, "w", encoding="utf-8") as build_info_file: + json.dump( + { + "version": version, + "commit": commit, + "dirty": dirty, + }, + build_info_file, + ) + return BUILD_INFO_PATH + + +APP_VERSION = _load_app_version() +BUILD_INFO_DATA = _write_build_info(APP_VERSION) a = Analysis( ["main_qml.py"], @@ -23,6 +88,7 @@ a = Analysis( (os.path.join(ROOT, "ui", "qml"), os.path.join("ui", "qml")), # Image assets (os.path.join(ROOT, "images"), "images"), + (BUILD_INFO_DATA, "."), ], hiddenimports=[ # conditional / lazy imports PyInstaller may miss @@ -257,8 +323,8 @@ if sys.platform == 'darwin': icon='images/AppIcon.icns', bundle_identifier='com.mouser.app', info_plist={ - 'CFBundleShortVersionString': '3.5.3', - 'CFBundleVersion': '3.5.3', + 'CFBundleShortVersionString': APP_VERSION, + 'CFBundleVersion': APP_VERSION, 'LSUIElement': True, # Runs in background (menu bar app) 'NSHighResolutionCapable': True, }, diff --git a/core/version.py b/core/version.py new file mode 100644 index 0000000..fdf7c78 --- /dev/null +++ b/core/version.py @@ -0,0 +1,116 @@ +"""Canonical Mouser version and build metadata.""" + +from __future__ import annotations + +import json +import os +from pathlib import Path +import subprocess +import sys + + +_DEFAULT_APP_VERSION = "3.5.3" +_BUILD_INFO_FILENAME = "mouser_build_info.json" +_REPO_ROOT = Path(__file__).resolve().parent.parent + + +def _normalize_version(raw_value: str) -> str: + value = (raw_value or "").strip() + if not value: + return _DEFAULT_APP_VERSION + return value[1:] if value.startswith("v") else value + + +def _parse_bool(raw_value: str | None) -> bool | None: + if raw_value is None: + return None + value = raw_value.strip().lower() + if value in {"1", "true", "yes", "on"}: + return True + if value in {"0", "false", "no", "off"}: + return False + return None + + +def _load_bundled_build_info() -> dict[str, object]: + if not getattr(sys, "frozen", False): + return {} + + candidate_roots = [ + Path(getattr(sys, "_MEIPASS", "")), + Path(getattr(sys, "executable", "")).resolve().parent, + ] + for root in candidate_roots: + if not root: + continue + path = root / _BUILD_INFO_FILENAME + if not path.exists(): + continue + try: + return json.loads(path.read_text(encoding="utf-8")) + except (OSError, json.JSONDecodeError): + return {} + return {} + + +def _run_git(args: list[str]) -> str: + try: + return subprocess.check_output( + ["git", *args], + cwd=_REPO_ROOT, + stderr=subprocess.DEVNULL, + text=True, + timeout=0.4, + ).strip() + except (OSError, subprocess.SubprocessError): + return "" + + +def _git_dirty() -> bool: + try: + result = subprocess.run( + ["git", "status", "--porcelain", "--untracked-files=no"], + cwd=_REPO_ROOT, + stdout=subprocess.PIPE, + stderr=subprocess.DEVNULL, + text=True, + timeout=0.5, + check=False, + ) + except (OSError, subprocess.SubprocessError): + return False + + if result.returncode != 0: + return False + return bool(result.stdout.strip()) + + +_BUNDLED_BUILD_INFO = _load_bundled_build_info() +APP_VERSION = _normalize_version( + str( + _BUNDLED_BUILD_INFO.get("version") + or os.environ.get("MOUSER_VERSION", _DEFAULT_APP_VERSION) + ) +) + +_APP_COMMIT_FULL = str( + _BUNDLED_BUILD_INFO.get("commit") + or os.environ.get("MOUSER_GIT_COMMIT", "") + or _run_git(["rev-parse", "HEAD"]) +).strip() +APP_COMMIT = _APP_COMMIT_FULL +APP_COMMIT_SHORT = _APP_COMMIT_FULL[:12] if _APP_COMMIT_FULL else "" + +_DIRTY_OVERRIDE = _parse_bool(os.environ.get("MOUSER_GIT_DIRTY")) +if "dirty" in _BUNDLED_BUILD_INFO: + APP_COMMIT_DIRTY = bool(_BUNDLED_BUILD_INFO.get("dirty")) +elif _DIRTY_OVERRIDE is not None: + APP_COMMIT_DIRTY = _DIRTY_OVERRIDE +else: + APP_COMMIT_DIRTY = _git_dirty() + +APP_BUILD_MODE = "Packaged app" if getattr(sys, "frozen", False) else "Source checkout" +APP_COMMIT_DISPLAY = ( + f"{APP_COMMIT_SHORT} (dirty)" if APP_COMMIT_SHORT and APP_COMMIT_DIRTY + else APP_COMMIT_SHORT or "Unavailable" +) diff --git a/images/icons/info.svg b/images/icons/info.svg new file mode 100644 index 0000000..4e51427 --- /dev/null +++ b/images/icons/info.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/main_qml.py b/main_qml.py index 89b1b4a..fe081c5 100644 --- a/main_qml.py +++ b/main_qml.py @@ -52,6 +52,7 @@ from core.engine import Engine from core.hid_gesture import set_backend_preference as set_hid_backend_preference from core.accessibility import is_process_trusted +from core.version import APP_BUILD_MODE, APP_COMMIT_DISPLAY, APP_VERSION from ui.backend import Backend from ui.locale_manager import LocaleManager _t4 = _time.perf_counter() @@ -363,6 +364,12 @@ def _check_accessibility(locale_mgr: "LocaleManager") -> bool: return True +def _runtime_launch_path() -> str: + if getattr(sys, "frozen", False): + return os.path.abspath(sys.executable) + return os.path.abspath(__file__) + + def main(): _print_startup_times() _t5 = _time.perf_counter() @@ -379,12 +386,17 @@ def main(): QCoreApplication.setAttribute(Qt.ApplicationAttribute.AA_ShareOpenGLContexts) app = QApplication(argv) app.setApplicationName("Mouser") + app.setApplicationVersion(APP_VERSION) app.setOrganizationName("Mouser") app.setWindowIcon(_app_icon()) app.setQuitOnLastWindowClosed(False) _configure_macos_app_mode() ui_state = UiState(app) + print(f"[Mouser] Version: {APP_VERSION} ({APP_BUILD_MODE})") + print(f"[Mouser] Commit: {APP_COMMIT_DISPLAY}") + print(f"[Mouser] Launch path: {_runtime_launch_path()}") + # ── Locale Manager ───────────────────────────────────────── initial_lang = cfg_settings.get("language", "en") locale_mgr = LocaleManager(language=initial_lang) @@ -428,6 +440,11 @@ def _dump_threads(sig, frame): qml_engine.rootContext().setContextProperty("uiState", ui_state) qml_engine.rootContext().setContextProperty("lm", locale_mgr) qml_engine.rootContext().setContextProperty("launchHidden", launch_hidden) + qml_engine.rootContext().setContextProperty("appVersion", APP_VERSION) + qml_engine.rootContext().setContextProperty("appBuildMode", APP_BUILD_MODE) + qml_engine.rootContext().setContextProperty("appCommit", APP_COMMIT_DISPLAY) + qml_engine.rootContext().setContextProperty( + "appLaunchPath", _runtime_launch_path().replace("\\", "/")) qml_engine.rootContext().setContextProperty( "applicationDirPath", ROOT.replace("\\", "/")) diff --git a/ui/locale_manager.py b/ui/locale_manager.py index 212dfc3..7dd1cd8 100644 --- a/ui/locale_manager.py +++ b/ui/locale_manager.py @@ -11,6 +11,7 @@ # Navigation sidebar "nav.mouse_profiles": "Mouse & Profiles", "nav.point_scroll": "Point & Scroll", + "nav.about": "About", # Mouse page — profile list "mouse.profiles": "Profiles", @@ -167,6 +168,15 @@ ), "accessibility.info": "System Settings -> Privacy & Security -> Accessibility", + # About dialog + "about.title": "About Mouser", + "about.subtitle": "Runtime and build details for support and debugging.", + "about.version": "Version", + "about.build_mode": "Build mode", + "about.commit": "Commit", + "about.launch_path": "Launch path", + "about.close": "Close", + # Language names "lang.en": "English", "lang.zh_CN": "\u7b80\u4f53\u4e2d\u6587", @@ -177,6 +187,7 @@ "zh_CN": { "nav.mouse_profiles": "\u9f20\u6807\u4e0e\u914d\u7f6e\u6587\u4ef6", "nav.point_scroll": "\u6307\u9488\u4e0e\u6eda\u8f6e", + "nav.about": "\u5173\u4e8e", "mouse.profiles": "\u914d\u7f6e\u6587\u4ef6", "mouse.all_applications": "\u6240\u6709\u5e94\u7528\u7a0b\u5e8f", @@ -319,6 +330,14 @@ ), "accessibility.info": "\u7cfb\u7edf\u8bbe\u7f6e -> \u9690\u79c1\u4e0e\u5b89\u5168\u6027 -> \u8f85\u52a9\u529f\u80fd", + "about.title": "\u5173\u4e8e Mouser", + "about.subtitle": "\u7528\u4e8e\u652f\u6301\u548c\u8c03\u8bd5\u7684\u8fd0\u884c\u65f6\u4e0e\u6784\u5efa\u4fe1\u606f\u3002", + "about.version": "\u7248\u672c", + "about.build_mode": "\u6784\u5efa\u6a21\u5f0f", + "about.commit": "\u63d0\u4ea4", + "about.launch_path": "\u542f\u52a8\u8def\u5f84", + "about.close": "\u5173\u95ed", + "lang.en": "English", "lang.zh_CN": "\u7b80\u4f53\u4e2d\u6587", "lang.zh_TW": "\u7e41\u9ad4\u4e2d\u6587", @@ -328,6 +347,7 @@ "zh_TW": { "nav.mouse_profiles": "\u6ed1\u9f20\u8207\u8a2d\u5b9a\u6a94", "nav.point_scroll": "\u6307\u6a19\u8207\u6372\u8ef8", + "nav.about": "\u95dc\u65bc", "mouse.profiles": "\u8a2d\u5b9a\u6a94", "mouse.all_applications": "\u6240\u6709\u61c9\u7528\u7a0b\u5f0f", @@ -470,6 +490,14 @@ ), "accessibility.info": "\u7cfb\u7d71\u8a2d\u5b9a -> \u96b1\u79c1\u6b0a\u8207\u5b89\u5168\u6027 -> \u8f14\u52a9\u4f7f\u7528", + "about.title": "\u95dc\u65bc Mouser", + "about.subtitle": "\u63d0\u4f9b\u652f\u63f4\u8207\u9664\u932f\u7528\u7684\u57f7\u884c\u6642\u8207\u5efa\u7f6e\u8cc7\u8a0a\u3002", + "about.version": "\u7248\u672c", + "about.build_mode": "\u5efa\u7f6e\u6a21\u5f0f", + "about.commit": "\u63d0\u4ea4", + "about.launch_path": "\u555f\u52d5\u8def\u5f91", + "about.close": "\u95dc\u9589", + "lang.en": "English", "lang.zh_CN": "\u7b80\u4f53\u4e2d\u6587", "lang.zh_TW": "\u7e41\u9ad4\u4e2d\u6587", diff --git a/ui/qml/Main.qml b/ui/qml/Main.qml index 561e73b..8aa2718 100644 --- a/ui/qml/Main.qml +++ b/ui/qml/Main.qml @@ -11,15 +11,21 @@ ApplicationWindow { height: 700 minimumWidth: 920 minimumHeight: 620 + readonly property string versionLabel: "v" + appVersion title: backend.mouseConnected - ? "Mouser — " + backend.deviceDisplayName - : "Mouser" + ? "Mouser " + versionLabel + " — " + backend.deviceDisplayName + : "Mouser " + versionLabel property string appearanceMode: uiState.appearanceMode readonly property bool darkMode: appearanceMode === "dark" || (appearanceMode === "system" && uiState.systemDarkMode) readonly property var theme: Theme.palette(darkMode) + readonly property string monoFontFamily: Qt.platform.os === "osx" + ? "Menlo" + : (Qt.platform.os === "windows" + ? "Consolas" + : "monospace") property int currentPage: 0 property Item hoveredNavItem: null property string hoveredNavText: "" @@ -44,114 +50,189 @@ ApplicationWindow { Layout.fillHeight: true color: root.theme.bgSidebar - Column { + Item { anchors { fill: parent topMargin: 20 + bottomMargin: 16 } - spacing: 6 - Rectangle { - width: 44 - height: 44 - radius: 14 - color: root.theme.accent - anchors.horizontalCenter: parent.horizontalCenter + Column { + anchors { + top: parent.top + left: parent.left + right: parent.right + } + spacing: 6 - Text { - anchors.centerIn: parent - text: "M" - font { - family: uiState.fontFamily - pixelSize: 20 - bold: true + Rectangle { + width: 44 + height: 44 + radius: 14 + color: root.theme.accent + anchors.horizontalCenter: parent.horizontalCenter + + Text { + anchors.centerIn: parent + text: "M" + font { + family: uiState.fontFamily + pixelSize: 20 + bold: true + } + color: root.theme.bgSidebar } - color: root.theme.bgSidebar } - } - - Item { width: 1; height: 18 } - Repeater { - model: [ - { icon: "mouse-simple", tipKey: "nav.mouse_profiles", page: 0 }, - { icon: "sliders-horizontal", tipKey: "nav.point_scroll", page: 1 } - ] + Item { width: 1; height: 18 } - delegate: FocusScope { - id: navItem - width: sidebar.width - height: 56 - activeFocusOnTab: true + Repeater { + model: [ + { icon: "mouse-simple", tipKey: "nav.mouse_profiles", page: 0 }, + { icon: "sliders-horizontal", tipKey: "nav.point_scroll", page: 1 } + ] - Accessible.role: Accessible.Button - Accessible.name: lm.strings[modelData.tipKey] || modelData.tipKey - Accessible.description: "Open " + (lm.strings[modelData.tipKey] || modelData.tipKey) + delegate: FocusScope { + id: navItem + width: sidebar.width + height: 56 + activeFocusOnTab: true - Keys.onReturnPressed: root.currentPage = modelData.page - Keys.onEnterPressed: root.currentPage = modelData.page - Keys.onSpacePressed: root.currentPage = modelData.page + Accessible.role: Accessible.Button + Accessible.name: lm.strings[modelData.tipKey] || modelData.tipKey + Accessible.description: "Open " + (lm.strings[modelData.tipKey] || modelData.tipKey) - Rectangle { - anchors.centerIn: parent - width: 46 - height: 46 - radius: 14 - color: root.currentPage === modelData.page - ? Qt.rgba(0, 0.83, 0.67, root.darkMode ? 0.14 : 0.16) - : navMouse.containsMouse || navItem.activeFocus - ? Qt.rgba(1, 1, 1, root.darkMode ? 0.06 : 0.22) - : "transparent" + Keys.onReturnPressed: root.currentPage = modelData.page + Keys.onEnterPressed: root.currentPage = modelData.page + Keys.onSpacePressed: root.currentPage = modelData.page - border.width: navItem.activeFocus ? 1 : 0 - border.color: root.theme.accent + Rectangle { + anchors.centerIn: parent + width: 46 + height: 46 + radius: 14 + color: root.currentPage === modelData.page + ? Qt.rgba(0, 0.83, 0.67, root.darkMode ? 0.14 : 0.16) + : navMouse.containsMouse || navItem.activeFocus + ? Qt.rgba(1, 1, 1, root.darkMode ? 0.06 : 0.22) + : "transparent" + + border.width: navItem.activeFocus ? 1 : 0 + border.color: root.theme.accent + + Behavior on color { ColorAnimation { duration: 150 } } + + AppIcon { + anchors.centerIn: parent + width: 22 + height: 22 + name: modelData.icon + iconColor: root.currentPage === modelData.page + ? root.theme.accent + : navMouse.containsMouse || navItem.activeFocus + ? root.theme.textPrimary + : root.theme.textSecondary + } + } - Behavior on color { ColorAnimation { duration: 150 } } + Rectangle { + width: 3 + height: 24 + radius: 2 + color: root.theme.accent + anchors { + left: parent.left + verticalCenter: parent.verticalCenter + } + visible: root.currentPage === modelData.page + } - AppIcon { - anchors.centerIn: parent - width: 22 - height: 22 - name: modelData.icon - iconColor: root.currentPage === modelData.page - ? root.theme.accent - : navMouse.containsMouse || navItem.activeFocus - ? root.theme.textPrimary - : root.theme.textSecondary + MouseArea { + id: navMouse + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: root.currentPage = modelData.page + onContainsMouseChanged: { + if (containsMouse) { + var p = navItem.mapToItem(overlayLayer, navItem.width, navItem.height / 2) + root.hoveredNavItem = navItem + root.hoveredNavTipKey = modelData.tipKey + root.hoveredNavText = lm.strings[modelData.tipKey] || modelData.tipKey + root.hoveredNavCenterX = p.x + root.hoveredNavCenterY = p.y + } else if (root.hoveredNavItem === navItem) { + root.hoveredNavItem = null + root.hoveredNavTipKey = "" + root.hoveredNavText = "" + } + } } } + } + } - Rectangle { - width: 3 - height: 24 - radius: 2 - color: root.theme.accent - anchors { - left: parent.left - verticalCenter: parent.verticalCenter - } - visible: root.currentPage === modelData.page + FocusScope { + id: aboutButton + anchors { + bottom: parent.bottom + horizontalCenter: parent.horizontalCenter + } + width: sidebar.width + height: 56 + activeFocusOnTab: true + + Accessible.role: Accessible.Button + Accessible.name: lm.strings["nav.about"] || "About" + Accessible.description: "Open " + (lm.strings["nav.about"] || "About") + + Keys.onReturnPressed: aboutDialog.open() + Keys.onEnterPressed: aboutDialog.open() + Keys.onSpacePressed: aboutDialog.open() + + Rectangle { + anchors.centerIn: parent + width: 46 + height: 46 + radius: 14 + color: aboutMouse.containsMouse || aboutButton.activeFocus || aboutDialog.visible + ? Qt.rgba(1, 1, 1, root.darkMode ? 0.06 : 0.22) + : "transparent" + + border.width: aboutButton.activeFocus ? 1 : 0 + border.color: root.theme.accent + + Behavior on color { ColorAnimation { duration: 150 } } + + AppIcon { + anchors.centerIn: parent + width: 20 + height: 20 + name: "info" + iconColor: aboutMouse.containsMouse || aboutButton.activeFocus || aboutDialog.visible + ? root.theme.textPrimary + : root.theme.textSecondary } + } - MouseArea { - id: navMouse - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: root.currentPage = modelData.page - onContainsMouseChanged: { - if (containsMouse) { - var p = navItem.mapToItem(overlayLayer, navItem.width, navItem.height / 2) - root.hoveredNavItem = navItem - root.hoveredNavTipKey = modelData.tipKey - root.hoveredNavText = lm.strings[modelData.tipKey] || modelData.tipKey - root.hoveredNavCenterX = p.x - root.hoveredNavCenterY = p.y - } else if (root.hoveredNavItem === navItem) { - root.hoveredNavItem = null - root.hoveredNavTipKey = "" - root.hoveredNavText = "" - } + MouseArea { + id: aboutMouse + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: aboutDialog.open() + onContainsMouseChanged: { + if (containsMouse) { + var p = aboutButton.mapToItem(overlayLayer, aboutButton.width, aboutButton.height / 2) + root.hoveredNavItem = aboutButton + root.hoveredNavTipKey = "nav.about" + root.hoveredNavText = lm.strings["nav.about"] || "About" + root.hoveredNavCenterX = p.x + root.hoveredNavCenterY = p.y + } else if (root.hoveredNavItem === aboutButton) { + root.hoveredNavItem = null + root.hoveredNavTipKey = "" + root.hoveredNavText = "" } } } @@ -208,6 +289,289 @@ ApplicationWindow { } } + Dialog { + id: aboutDialog + parent: Overlay.overlay + modal: true + focus: true + title: "" + width: 500 + height: 430 + x: Math.round((parent.width - width) / 2) + y: Math.round((parent.height - height) / 2) + padding: 0 + + background: Rectangle { + radius: 24 + color: theme.bgElevated + border.width: 1 + border.color: theme.border + } + + contentItem: Item { + width: aboutDialog.width + height: aboutDialog.height + + Item { + id: aboutHeader + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.topMargin: 16 + anchors.leftMargin: 24 + anchors.rightMargin: 24 + height: 44 + + Row { + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + spacing: 12 + + Rectangle { + width: 36 + height: 36 + radius: 12 + color: root.theme.accentDim + + AppIcon { + anchors.centerIn: parent + width: 18 + height: 18 + name: "info" + iconColor: root.theme.accent + } + } + + Column { + anchors.verticalCenter: parent.verticalCenter + spacing: 3 + + Text { + text: lm.strings["about.title"] || "About Mouser" + font { family: uiState.fontFamily; pixelSize: 17; bold: true } + color: theme.textPrimary + } + + Text { + text: lm.strings["about.subtitle"] || "" + font { family: uiState.fontFamily; pixelSize: 11 } + color: theme.textSecondary + } + } + } + + Rectangle { + width: 34 + height: 34 + radius: 12 + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + color: closeAboutMouse.containsMouse + ? Qt.rgba(1, 1, 1, uiState.darkMode ? 0.08 : 0.65) + : "transparent" + + AppIcon { + anchors.centerIn: parent + width: 14 + height: 14 + name: "x" + iconColor: theme.textSecondary + } + + MouseArea { + id: closeAboutMouse + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: aboutDialog.close() + } + } + } + + Column { + anchors { + top: aboutHeader.bottom + topMargin: 20 + left: parent.left + right: parent.right + leftMargin: 24 + rightMargin: 24 + } + spacing: 14 + + Rectangle { + width: parent.width + height: versionHero.implicitHeight + 24 + radius: 20 + color: root.theme.accentDim + border.width: 1 + border.color: Qt.rgba(0, 0, 0, root.darkMode ? 0.0 : 0.04) + + Column { + id: versionHero + anchors.fill: parent + anchors.margins: 18 + spacing: 10 + + Text { + text: lm.strings["about.version"] || "Version" + font { family: uiState.fontFamily; pixelSize: 11; bold: true } + color: root.theme.textSecondary + } + + Row { + spacing: 10 + + Text { + text: root.versionLabel + font { family: uiState.fontFamily; pixelSize: 28; bold: true } + color: root.theme.textPrimary + } + + Rectangle { + anchors.verticalCenter: parent.verticalCenter + radius: 999 + color: Qt.rgba(1, 1, 1, root.darkMode ? 0.08 : 0.6) + border.width: 1 + border.color: Qt.rgba(1, 1, 1, root.darkMode ? 0.05 : 0.18) + width: buildModeChipLabel.implicitWidth + 22 + height: 30 + + Text { + id: buildModeChipLabel + anchors.centerIn: parent + text: appBuildMode + font { family: uiState.fontFamily; pixelSize: 11; bold: true } + color: root.theme.textPrimary + } + } + } + + Text { + text: (lm.strings["about.commit"] || "Commit") + ": " + appCommit + font { family: root.monoFontFamily; pixelSize: 12 } + color: root.theme.textSecondary + } + } + } + + Rectangle { + width: parent.width + height: metadataColumn.implicitHeight + 2 + radius: 18 + color: theme.bgSubtle + border.width: 1 + border.color: theme.border + + Column { + id: metadataColumn + anchors.fill: parent + spacing: 0 + + Item { + width: parent.width + height: 64 + + Text { + anchors.left: parent.left + anchors.leftMargin: 18 + anchors.verticalCenter: parent.verticalCenter + width: 92 + text: lm.strings["about.build_mode"] || "Build mode" + font { family: uiState.fontFamily; pixelSize: 11; bold: true } + color: theme.textSecondary + } + + Text { + anchors.left: parent.left + anchors.leftMargin: 126 + anchors.right: parent.right + anchors.rightMargin: 18 + anchors.verticalCenter: parent.verticalCenter + text: appBuildMode + font { family: uiState.fontFamily; pixelSize: 14 } + color: theme.textPrimary + } + } + + Rectangle { + width: parent.width - 36 + height: 1 + x: 18 + color: theme.border + opacity: 0.9 + } + + Item { + width: parent.width + height: 64 + + Text { + anchors.left: parent.left + anchors.leftMargin: 18 + anchors.verticalCenter: parent.verticalCenter + width: 92 + text: lm.strings["about.commit"] || "Commit" + font { family: uiState.fontFamily; pixelSize: 11; bold: true } + color: theme.textSecondary + } + + Text { + anchors.left: parent.left + anchors.leftMargin: 126 + anchors.right: parent.right + anchors.rightMargin: 18 + anchors.verticalCenter: parent.verticalCenter + text: appCommit + font { family: root.monoFontFamily; pixelSize: 13 } + color: theme.textPrimary + } + } + + Rectangle { + width: parent.width - 36 + height: 1 + x: 18 + color: theme.border + opacity: 0.9 + } + + Item { + width: parent.width + height: launchPathValue.implicitHeight + 34 + + Text { + anchors.left: parent.left + anchors.leftMargin: 18 + anchors.top: parent.top + anchors.topMargin: 18 + width: 92 + text: lm.strings["about.launch_path"] || "Launch path" + font { family: uiState.fontFamily; pixelSize: 11; bold: true } + color: theme.textSecondary + } + + Text { + id: launchPathValue + anchors.left: parent.left + anchors.leftMargin: 126 + anchors.right: parent.right + anchors.rightMargin: 18 + anchors.top: parent.top + anchors.topMargin: 18 + text: appLaunchPath + wrapMode: Text.WrapAnywhere + font { family: root.monoFontFamily; pixelSize: 12 } + color: theme.textPrimary + } + } + } + } + } + + } + } + Rectangle { id: toast anchors {