diff --git a/.clang-format b/.clang-format index 610ee65..8ec602a 100644 --- a/.clang-format +++ b/.clang-format @@ -1,6 +1,6 @@ AlignArrayOfStructures: None AlignAfterOpenBracket: BlockIndent -AllowShortBlocksOnASingleLine: Always +AllowShortBlocksOnASingleLine: Empty AllowShortCaseLabelsOnASingleLine: true AllowShortEnumsOnASingleLine: true AllowShortFunctionsOnASingleLine: All diff --git a/.clang-tidy b/.clang-tidy index 002c444..c83ed8f 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -20,6 +20,7 @@ Checks: > -cppcoreguidelines-avoid-do-while, -cppcoreguidelines-pro-type-reinterpret-cast, -cppcoreguidelines-pro-type-vararg, + -cppcoreguidelines-use-enum-class, google-global-names-in-headers, google-readability-casting, google-runtime-int, @@ -63,6 +64,8 @@ CheckOptions: readability-identifier-naming.ParameterCase: camelBack readability-identifier-naming.VariableCase: camelBack + misc-const-correctness.WarnPointersAsPointers: false + # does not appear to work readability-operators-representation.BinaryOperators: '&&;&=;&;|;~;!;!=;||;|=;^;^=' readability-operators-representation.OverloadedOperators: '&&;&=;&;|;~;!;!=;||;|=;^;^=' diff --git a/.github/ISSUE_TEMPLATE/crash.yml b/.github/ISSUE_TEMPLATE/crash.yml index c8b4804..80fa827 100644 --- a/.github/ISSUE_TEMPLATE/crash.yml +++ b/.github/ISSUE_TEMPLATE/crash.yml @@ -1,4 +1,4 @@ -name: Crash Report +name: Crash Report (v1) description: Quickshell has crashed labels: ["bug", "crash"] body: diff --git a/.github/ISSUE_TEMPLATE/crash2.yml b/.github/ISSUE_TEMPLATE/crash2.yml new file mode 100644 index 0000000..84beef8 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/crash2.yml @@ -0,0 +1,49 @@ +name: Crash Report (v2) +description: Quickshell has crashed +labels: ["bug", "crash"] +body: + - type: textarea + id: userinfo + attributes: + label: What caused the crash + description: | + Any information likely to help debug the crash. What were you doing when the crash occurred, + what changes did you make, can you get it to happen again? + - type: textarea + id: report + attributes: + label: Report file + description: Attach `report.txt` here. + validations: + required: true + - type: textarea + id: logs + attributes: + label: Log file + description: | + Attach `log.qslog.log` here. If it is too big to upload, compress it. + + You can preview the log if you'd like using `quickshell read-log `. + validations: + required: true + - type: textarea + id: config + attributes: + label: Configuration + description: | + Attach your configuration here, preferrably in full (not just one file). + Compress it into a zip, tar, etc. + + This will help us reproduce the crash ourselves. + - type: textarea + id: bt + attributes: + label: Backtrace + description: | + GDB usually produces better stacktraces than quickshell can. Consider attaching a gdb backtrace + following the instructions below. + + 1. Run `coredumpctl debug ` where `pid` is the number shown after "Crashed process ID" + in the crash reporter. + 2. Once it loads, type `bt -full` (then enter) + 3. Copy the output and attach it as a file or in a spoiler. diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 93b8458..7b8cbce 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -6,17 +6,23 @@ jobs: name: Nix strategy: matrix: - qtver: [qt6.9.0, qt6.8.3, qt6.8.2, qt6.8.1, qt6.8.0, qt6.7.3, qt6.7.2, qt6.7.1, qt6.7.0, qt6.6.3, qt6.6.2, qt6.6.1, qt6.6.0] + qtver: [qt6.10.1, qt6.10.0, qt6.9.2, qt6.9.1, qt6.9.0, qt6.8.3, qt6.8.2, qt6.8.1, qt6.8.0, qt6.7.3, qt6.7.2, qt6.7.1, qt6.7.0, qt6.6.3, qt6.6.2, qt6.6.1, qt6.6.0] compiler: [clang, gcc] runs-on: ubuntu-latest + permissions: + contents: read + id-token: write steps: - uses: actions/checkout@v4 # Use cachix action over detsys for testing with act. # - uses: cachix/install-nix-action@v27 - uses: DeterminateSystems/nix-installer-action@main + - uses: DeterminateSystems/magic-nix-cache-action@main + with: + use-flakehub: false - name: Download Dependencies - run: nix-build --no-out-link --expr '((import ./ci/matrix.nix) { qtver = "${{ matrix.qtver }}"; compiler = "${{ matrix.compiler }}"; }).inputDerivation' + run: nix-build --no-out-link --expr '((import ./ci/matrix.nix) { qtver = "${{ matrix.qtver }}"; compiler = "${{ matrix.compiler }}"; }).unwrapped.inputDerivation' - name: Build run: nix-build --no-out-link --expr '(import ./ci/matrix.nix) { qtver = "${{ matrix.qtver }}"; compiler = "${{ matrix.compiler }}"; }' @@ -44,13 +50,16 @@ jobs: wayland-protocols \ wayland \ libdrm \ + vulkan-headers \ libxcb \ libpipewire \ cli11 \ - jemalloc + polkit \ + jemalloc \ + libunwind \ + git # for cpptrace clone - name: Build - # breakpad is annoying to build in ci due to makepkg not running as root run: | - cmake -GNinja -B build -DCRASH_REPORTER=OFF + cmake -GNinja -B build -DVENDOR_CPPTRACE=ON cmake --build build diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index da329cc..de0c304 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -5,11 +5,17 @@ jobs: lint: name: Lint runs-on: ubuntu-latest + permissions: + contents: read + id-token: write steps: - uses: actions/checkout@v4 # Use cachix action over detsys for testing with act. # - uses: cachix/install-nix-action@v27 - uses: DeterminateSystems/nix-installer-action@main + - uses: DeterminateSystems/magic-nix-cache-action@main + with: + use-flakehub: false - uses: nicknovitski/nix-develop@v1 - name: Check formatting diff --git a/BUILD.md b/BUILD.md index aa7c98a..29aecac 100644 --- a/BUILD.md +++ b/BUILD.md @@ -15,15 +15,7 @@ Please make this descriptive enough to identify your specific package, for examp - `Nixpkgs` - `Fedora COPR (errornointernet/quickshell)` -`-DDISTRIBUTOR_DEBUGINFO_AVAILABLE=YES/NO` - -If we can retrieve binaries and debug information for the package without actually running your -distribution (e.g. from an website), and you would like to strip the binary, please set this to `YES`. - -If we cannot retrieve debug information, please set this to `NO` and -**ensure you aren't distributing stripped (non debuggable) binaries**. - -In both cases you should build with `-DCMAKE_BUILD_TYPE=RelWithDebInfo` (then split or keep the debuginfo). +Please leave at least symbol names attached to the binary for debugging purposes. ### QML Module dir Currently all QML modules are statically linked to quickshell, but this is where @@ -55,7 +47,7 @@ On some distros, private Qt headers are in separate packages which you may have We currently require private headers for the following libraries: - `qt6declarative` -- `qt6wayland` +- `qt6wayland` (for Qt versions prior to 6.10) We recommend an implicit dependency on `qt6svg`. If it is not installed, svg images and svg icons will not work, including system ones. @@ -64,14 +56,18 @@ At least Qt 6.6 is required. All features are enabled by default and some have their own dependencies. -### Crash Reporter -The crash reporter catches crashes, restarts quickshell when it crashes, +### Crash Handler +The crash reporter catches crashes, restarts Quickshell when it crashes, and collects useful crash information in one place. Leaving this enabled will enable us to fix bugs far more easily. -To disable: `-DCRASH_REPORTER=OFF` +To disable: `-DCRASH_HANDLER=OFF` -Dependencies: `google-breakpad` (static library) +Dependencies: `cpptrace` + +Note: `-DVENDOR_CPPTRACE=ON` can be set to vendor cpptrace using FetchContent. + +When using FetchContent, `libunwind` is required, and `libdwarf` can be provided by the package manager or fetched with FetchContent. ### Jemalloc We recommend leaving Jemalloc enabled as it will mask memory fragmentation caused @@ -104,7 +100,7 @@ Currently supported Qt versions: `6.6`, `6.7`. To disable: `-DWAYLAND=OFF` Dependencies: - - `qt6wayland` + - `qt6wayland` (for Qt versions prior to 6.10) - `wayland` (libwayland-client) - `wayland-scanner` (build time) - `wayland-protocols` (static library) @@ -146,6 +142,7 @@ To disable: `-DSCREENCOPY=OFF` Dependencies: - `libdrm` - `libgbm` +- `vulkan-headers` (build-time) Specific protocols can also be disabled: - `DSCREENCOPY_ICC=OFF` - Disable screencopy via [ext-image-copy-capture-v1] @@ -192,6 +189,13 @@ To disable: `-DSERVICE_PAM=OFF` Dependencies: `pam` +### Polkit +This feature enables creating Polkit agents that can prompt user for authentication. + +To disable: `-DSERVICE_POLKIT=OFF` + +Dependencies: `polkit`, `glib` + ### Hyprland This feature enables hyprland specific integrations. It requires wayland support but has no extra dependencies. diff --git a/CMakeLists.txt b/CMakeLists.txt index 9ef5b98..d57e322 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,7 @@ cmake_minimum_required(VERSION 3.20) -project(quickshell VERSION "0.1.0" LANGUAGES CXX C) +project(quickshell VERSION "0.2.1" LANGUAGES CXX C) + +set(UNRELEASED_FEATURES) set(QT_MIN_VERSION "6.6.0") set(CMAKE_CXX_STANDARD 20) @@ -38,14 +40,17 @@ string(APPEND QS_BUILD_OPTIONS " Distributor: ${DISTRIBUTOR}") message(STATUS "Quickshell configuration") message(STATUS " Distributor: ${DISTRIBUTOR}") -boption(DISTRIBUTOR_DEBUGINFO_AVAILABLE "Distributor provided debuginfo" NO) boption(NO_PCH "Disable precompild headers (dev)" OFF) boption(BUILD_TESTING "Build tests (dev)" OFF) boption(ASAN "ASAN (dev)" OFF) # note: better output with gcc than clang boption(FRAME_POINTERS "Keep Frame Pointers (dev)" ${ASAN}) -boption(CRASH_REPORTER "Crash Handling" ON) -boption(USE_JEMALLOC "Use jemalloc" ON) +if (CMAKE_SYSTEM_NAME STREQUAL "FreeBSD") + boption(USE_JEMALLOC "Use jemalloc" OFF) +else() + boption(USE_JEMALLOC "Use jemalloc" ON) +endif() +boption(CRASH_HANDLER "Crash Handling" ON) boption(SOCKETS "Unix Sockets" ON) boption(WAYLAND "Wayland" ON) boption(WAYLAND_WLR_LAYERSHELL " Wlroots Layer-Shell" ON REQUIRES WAYLAND) @@ -67,10 +72,12 @@ boption(SERVICE_STATUS_NOTIFIER "System Tray" ON) boption(SERVICE_PIPEWIRE "PipeWire" ON) boption(SERVICE_MPRIS "Mpris" ON) boption(SERVICE_PAM "Pam" ON) +boption(SERVICE_POLKIT "Polkit" ON) boption(SERVICE_GREETD "Greetd" ON) boption(SERVICE_UPOWER "UPower" ON) boption(SERVICE_NOTIFICATIONS "Notifications" ON) boption(BLUETOOTH "Bluetooth" ON) +boption(NETWORK "Network" ON) include(cmake/install-qml-module.cmake) include(cmake/util.cmake) @@ -100,6 +107,7 @@ if (NOT CMAKE_BUILD_TYPE) endif() set(QT_FPDEPS Gui Qml Quick QuickControls2 Widgets ShaderTools) +set(QT_PRIVDEPS QuickPrivate) include(cmake/pch.cmake) @@ -115,9 +123,10 @@ endif() if (WAYLAND) list(APPEND QT_FPDEPS WaylandClient) + list(APPEND QT_PRIVDEPS WaylandClientPrivate) endif() -if (SERVICE_STATUS_NOTIFIER OR SERVICE_MPRIS OR SERVICE_UPOWER OR SERVICE_NOTIFICATIONS OR BLUETOOTH) +if (SERVICE_STATUS_NOTIFIER OR SERVICE_MPRIS OR SERVICE_UPOWER OR SERVICE_NOTIFICATIONS OR BLUETOOTH OR NETWORK) set(DBUS ON) endif() @@ -127,6 +136,13 @@ endif() find_package(Qt6 REQUIRED COMPONENTS ${QT_FPDEPS}) +# In Qt 6.10, private dependencies are required to be explicit, +# but they could not be explicitly depended on prior to 6.9. +if (Qt6_VERSION VERSION_GREATER_EQUAL "6.9.0") + set(QT_NO_PRIVATE_MODULE_WARNING ON) + find_package(Qt6 REQUIRED COMPONENTS ${QT_PRIVDEPS}) +endif() + set(CMAKE_AUTOUIC OFF) qt_standard_project_setup(REQUIRES 6.6) set(QT_QML_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/qml_modules) diff --git a/Justfile b/Justfile index f60771a..801eb2a 100644 --- a/Justfile +++ b/Justfile @@ -12,6 +12,9 @@ lint-ci: lint-changed: git diff --name-only HEAD | grep "^.*\.cpp\$" | parallel -j$(nproc) --no-notice --will-cite --tty --bar clang-tidy --load={{ env_var("TIDYFOX") }} +lint-staged: + git diff --staged --name-only --diff-filter=d HEAD | grep "^.*\.cpp\$" | parallel -j$(nproc) --no-notice --will-cite --tty --bar clang-tidy --load={{ env_var("TIDYFOX") }} + configure target='debug' *FLAGS='': cmake -GNinja -B {{builddir}} \ -DCMAKE_BUILD_TYPE={{ if target == "debug" { "Debug" } else { "RelWithDebInfo" } }} \ diff --git a/changelog/next.md b/changelog/next.md new file mode 100644 index 0000000..ef63323 --- /dev/null +++ b/changelog/next.md @@ -0,0 +1,60 @@ +## Breaking Changes + +### Config paths are no longer canonicalized + +This fixes nix configs changing shell-ids on rebuild as the shell id is now derived from +the symlink path. Configs with a symlink in their path will have a different shell id. + +Shell ids are used to derive the default config / state / cache folders, so those files +will need to be manually moved if using a config behind a symlinked path without an explicitly +set shell id. + +## New Features + +- Added support for creating Polkit agents. +- Added support for creating wayland idle inhibitors. +- Added support for wayland idle timeouts. +- Added support for inhibiting wayland compositor shortcuts for focused windows. +- Added the ability to override Quickshell.cacheDir with a custom path. +- Added minimized, maximized, and fullscreen properties to FloatingWindow. +- Added the ability to handle move and resize events to FloatingWindow. +- Pipewire service now reconnects if pipewire dies or a protocol error occurs. +- Added pipewire audio peak detection. +- Added initial support for network management. +- Added support for grabbing focus from popup windows. +- Added support for IPC signal listeners. +- Added Quickshell version checking and version gated preprocessing. +- Added a way to detect if an icon is from the system icon theme or not. +- Added vulkan support to screencopy. + +## Other Changes + +- FreeBSD is now partially supported. +- IPC operations filter available instances to the current display connection by default. +- PwNodeLinkTracker ignores sound level monitoring programs. +- Replaced breakpad with cpptrace. + +## Bug Fixes + +- Fixed volume control breaking with pipewire pro audio mode. +- Fixed volume control breaking with bluez streams and potentially others. +- Fixed volume control breaking for devices without route definitions. +- Fixed escape sequence handling in desktop entries. +- Fixed volumes not initializing if a pipewire device was already loaded before its node. +- Fixed hyprland active toplevel not resetting after window closes. +- Fixed hyprland ipc window names and titles being reversed. +- Fixed missing signals for system tray item title and description updates. +- Fixed asynchronous loaders not working after reload. +- Fixed asynchronous loaders not working before window creation. +- Fixed memory leak in IPC handlers. +- Fixed ClippingRectangle related crashes. +- Fixed crashes when monitors are unplugged. +- Fixed crashes when default pipewire devices are lost. +- Desktop action order is now preserved. + +## Packaging Changes + +- `glib` and `polkit` have been added as dependencies when compiling with polkit agent support. +- `vulkan-headers` has been added as a build-time dependency for screencopy (Vulkan backend support). +- `breakpad` has been replaced by `cpptrace`, which is far easier to package, and the `CRASH_REPORTER` cmake variable has been replaced with `CRASH_HANDLER` to stop this from being easy to ignore. +- `DISTRIBUTOR_DEBUGINFO_AVAILABLE` was removed as it is no longer important without breakpad. diff --git a/changelog/v0.1.0.md b/changelog/v0.1.0.md new file mode 100644 index 0000000..f8a032f --- /dev/null +++ b/changelog/v0.1.0.md @@ -0,0 +1 @@ +Initial release diff --git a/changelog/v0.2.0.md b/changelog/v0.2.0.md new file mode 100644 index 0000000..2fbf74d --- /dev/null +++ b/changelog/v0.2.0.md @@ -0,0 +1,84 @@ +## Breaking Changes + +- Files outside of the shell directory can no longer be referenced with relative paths, e.g. '../../foo.png'. +- PanelWindow's Automatic exclusion mode now adds an exclusion zone for panels with a single anchor. +- `QT_QUICK_CONTROLS_STYLE` and `QT_STYLE_OVERRIDE` are ignored unless `//@ pragma RespectSystemStyle` is set. + +## New Features + +### Root-Relative Imports + +Quickshell 0.2 comes with a new method to import QML modules which is supported by QMLLS. +This replaces "root:/" imports for QML modules. + +The new syntax is `import qs.path.to.module`, where `path/to/module` is the path to +a module/subdirectory relative to the config root (`qs`). + +### Better LSP support + +LSP support for Singletons and Root-Relative imports can be enabled by creating a file named +`.qmlls.ini` in the shell root directory. Quickshell will detect this file and automatically +populate it with an LSP configuration. This file should be gitignored in your configuration, +as it is system dependent. + +The generated configuration also includes QML import paths available to Quickshell, meaning +QMLLS no longer requires the `-E` flag. + +### Bluetooth Module + +Quickshell can now manage your bluetooth devices through BlueZ. While authenticated pairing +has not landed in 0.2, support for connecting and disconnecting devices, basic device information, +and non-authenticated pairing are now supported. + +### Other Features + +- Added `HyprlandToplevel` and related toplevel/window management APIs in the Hyprland module. +- Added `Quickshell.execDetached()`, which spawns a detached process without a `Process` object. +- Added `Process.exec()` for easier reconfiguration of process commands when starting them. +- Added `FloatingWindow.title`, which allows changing the title of a floating window. +- Added `signal QsWindow.closed()`, fired when a window is closed externally. +- Added support for inline replies in notifications, when supported by applications. +- Added `DesktopEntry.startupWmClass` and `DesktopEntry.heuristicLookup()` to better identify toplevels. +- Added `DesktopEntry.command` which can be run as an alternative to `DesktopEntry.execute()`. +- Added `//@ pragma Internal`, which makes a QML component impossible to import outside of its module. +- Added dead instance selection for some subcommands, such as `qs log` and `qs list`. + +## Other Changes + +- `Quickshell.shellRoot` has been renamed to `Quickshell.shellDir`. +- PanelWindow margins opposite the window's anchorpoint are now added to exclusion zone. +- stdout/stderr or detached processes and executed desktop entries are now hidden by default. +- Various warnings caused by other applications Quickshell communicates with over D-BUS have been hidden in logs. +- Quickshell's new logo is now shown in any floating windows. + +## Bug Fixes + +- Fixed pipewire device volume and mute states not updating before the device has been used. +- Fixed a crash when changing the volume of any pipewire device on a sound card another removed device was using. +- Fixed a crash when accessing a removed previous default pipewire node from the default sink/source changed signals. +- Fixed session locks crashing if all monitors are disconnected. +- Fixed session locks crashing if unsupported by the compositor. +- Fixed a crash when creating a session lock and destroying it before acknowledged by the compositor. +- Fixed window input masks not updating after a reload. +- Fixed PanelWindows being unconfigurable unless `screen` was set under X11. +- Fixed a crash when anchoring a popup to a zero sized `Item`. +- Fixed `FileView` crashing if `watchChanges` was used. +- Fixed `SocketServer` sockets disappearing after a reload. +- Fixed `ScreencopyView` having incorrect rotation when displaying a rotated monitor. +- Fixed `MarginWrapperManager` breaking pixel alignment of child items when centering. +- Fixed `IpcHandler`, `NotificationServer` and `GlobalShortcut` not activating with certain QML structures. +- Fixed tracking of QML incubator destruction and deregistration, which occasionally caused crashes. +- Fixed FloatingWindows being constrained to the smallest window manager supported size unless max size was set. +- Fixed `MprisPlayer.lengthSupported` not updating reactively. +- Fixed normal tray icon being ignored when status is `NeedsAttention` and no attention icon is provided. +- Fixed `HyprlandWorkspace.activate()` sending invalid commands to Hyprland for named or special workspaces. +- Fixed file watcher occasionally breaking when using VSCode to edit QML files. +- Fixed crashes when screencopy buffer creation fails. +- Fixed a crash when wayland layer surfaces are recreated for the same window. +- Fixed the `QsWindow` attached object not working when using `WlrLayershell` directly. +- Fixed a crash when attempting to create a window without available VRAM. +- Fixed OOM crash when failing to write to detailed log file. +- Prevented distro logging configurations for Qt from interfering with Quickshell commands. +- Removed the "QProcess destroyed for running process" warning when destroying `Process` objects. +- Fixed `ColorQuantizer` printing a pointer to an error message instead of an error message. +- Fixed notification pixmap rowstride warning showing for correct rowstrides. diff --git a/changelog/v0.2.1.md b/changelog/v0.2.1.md new file mode 100644 index 0000000..596b82f --- /dev/null +++ b/changelog/v0.2.1.md @@ -0,0 +1,17 @@ +## New Features + +- Changes to desktop entries are now tracked in real time. + +## Other Changes + +- Added support for Qt 6.10 + +## Bug Fixes + +- Fixed volumes getting stuck on change for pipewire devices with few volume steps. +- Fixed a crash when running out of disk space to write log files. +- Fixed a rare crash when disconnecting a monitor. +- Fixed build issues preventing cross compilation from working. +- Fixed dekstop entries with lower priority than a hidden entry not being hidden. +- Fixed desktop entry keys with mismatched modifier or country not being discarded. +- Fixed greetd hanging when authenticating with a fingerprint. diff --git a/ci/matrix.nix b/ci/matrix.nix index be2da61..dd20fa5 100644 --- a/ci/matrix.nix +++ b/ci/matrix.nix @@ -2,7 +2,10 @@ qtver, compiler, }: let - nixpkgs = (import ./nix-checkouts.nix).${builtins.replaceStrings ["."] ["_"] qtver}; + checkouts = import ./nix-checkouts.nix; + nixpkgs = checkouts.${builtins.replaceStrings ["."] ["_"] qtver}; compilerOverride = (nixpkgs.callPackage ./variations.nix {}).${compiler}; - pkg = (nixpkgs.callPackage ../default.nix {}).override compilerOverride; + pkg = (nixpkgs.callPackage ../default.nix {}).override (compilerOverride // { + wayland-protocols = checkouts.latest.wayland-protocols; + }); in pkg diff --git a/ci/nix-checkouts.nix b/ci/nix-checkouts.nix index 73c2415..945973c 100644 --- a/ci/nix-checkouts.nix +++ b/ci/nix-checkouts.nix @@ -7,9 +7,28 @@ let url = "https://github.com/nixos/nixpkgs/archive/${commit}.tar.gz"; inherit sha256; }) {}; -in { - # For old qt versions, grab the commit before the version bump that has all the patches - # instead of the bumped version. +in rec { + latest = qt6_10_0; + + qt6_10_1 = byCommit { + commit = "ffbc9f8cbaacfb331b6017d5a5abb21a492c9a38"; + sha256 = "0fvbizl7j5rv2rf8j76yw0xb3d9l06hahkjys2a7k1yraznvnafm"; + }; + + qt6_10_0 = byCommit { + commit = "c5ae371f1a6a7fd27823bc500d9390b38c05fa55"; + sha256 = "18g0f8cb9m8mxnz9cf48sks0hib79b282iajl2nysyszph993yp0"; + }; + + qt6_9_2 = byCommit { + commit = "e9f00bd893984bc8ce46c895c3bf7cac95331127"; + sha256 = "0s2mhbrgzxlgkg2yxb0q0hpk8lby1a7w67dxvfmaz4gsmc0bnvfj"; + }; + + qt6_9_1 = byCommit { + commit = "4c202d26483c5ccf3cb95e0053163facde9f047e"; + sha256 = "06l2w4bcgfw7dfanpzpjcf25ydf84in240yplqsss82qx405y9di"; + }; qt6_9_0 = byCommit { commit = "546c545bd0594809a28ab7e869b5f80dd7243ef6"; diff --git a/ci/variations.nix b/ci/variations.nix index b0889be..b1d2947 100644 --- a/ci/variations.nix +++ b/ci/variations.nix @@ -2,6 +2,6 @@ clangStdenv, gccStdenv, }: { - clang = { buildStdenv = clangStdenv; }; - gcc = { buildStdenv = gccStdenv; }; + clang = { stdenv = clangStdenv; }; + gcc = { stdenv = gccStdenv; }; } diff --git a/default.nix b/default.nix index 73cd8d1..02b8659 100644 --- a/default.nix +++ b/default.nix @@ -2,25 +2,31 @@ lib, nix-gitignore, pkgs, + stdenv, keepDebugInfo, - buildStdenv ? pkgs.clangStdenv, pkg-config, cmake, ninja, spirv-tools, qt6, - breakpad, + cpptrace ? null, + libunwind, + libdwarf, jemalloc, cli11, wayland, wayland-protocols, wayland-scanner, xorg, + libxcb ? xorg.libxcb, libdrm, libgbm ? null, + vulkan-headers, pipewire, pam, + polkit, + glib, gitRev ? (let headExists = builtins.pathExists ./.git/HEAD; @@ -43,64 +49,106 @@ withPam ? true, withHyprland ? true, withI3 ? true, -}: buildStdenv.mkDerivation { - pname = "quickshell${lib.optionalString debug "-debug"}"; - version = "0.1.0"; - src = nix-gitignore.gitignoreSource [] ./.; + withPolkit ? true, + withNetworkManager ? true, +}: let + withCrashHandler = withCrashReporter && cpptrace != null && lib.strings.compareVersions cpptrace.version "0.7.2" >= 0; - nativeBuildInputs = [ - cmake - ninja - qt6.qtshadertools - spirv-tools - qt6.wrapQtAppsHook - pkg-config - ] - ++ lib.optional withWayland wayland-scanner; + unwrapped = stdenv.mkDerivation { + pname = "quickshell${lib.optionalString debug "-debug"}"; + version = "0.2.1"; + src = nix-gitignore.gitignoreSource "/default.nix\n" ./.; - buildInputs = [ - qt6.qtbase - qt6.qtdeclarative - cli11 - ] - ++ lib.optional withQtSvg qt6.qtsvg - ++ lib.optional withCrashReporter breakpad - ++ lib.optional withJemalloc jemalloc - ++ lib.optionals withWayland [ qt6.qtwayland wayland wayland-protocols ] - ++ lib.optionals (withWayland && libgbm != null) [ libdrm libgbm ] - ++ lib.optional withX11 xorg.libxcb - ++ lib.optional withPam pam - ++ lib.optional withPipewire pipewire; + dontWrapQtApps = true; # see wrappers - cmakeBuildType = if debug then "Debug" else "RelWithDebInfo"; + nativeBuildInputs = [ + cmake + ninja + spirv-tools + pkg-config + ] + ++ lib.optional (withWayland && lib.strings.compareVersions qt6.qtbase.version "6.10.0" == -1) qt6.qtwayland + ++ lib.optionals withWayland [ + qt6.qtwayland # qtwaylandscanner required at build time + wayland-scanner + ]; - cmakeFlags = [ - (lib.cmakeFeature "DISTRIBUTOR" "Official-Nix-Flake") - (lib.cmakeFeature "INSTALL_QML_PREFIX" qt6.qtbase.qtQmlPrefix) - (lib.cmakeBool "DISTRIBUTOR_DEBUGINFO_AVAILABLE" true) - (lib.cmakeFeature "GIT_REVISION" gitRev) - (lib.cmakeBool "CRASH_REPORTER" withCrashReporter) - (lib.cmakeBool "USE_JEMALLOC" withJemalloc) - (lib.cmakeBool "WAYLAND" withWayland) - (lib.cmakeBool "SCREENCOPY" (libgbm != null)) - (lib.cmakeBool "SERVICE_PIPEWIRE" withPipewire) - (lib.cmakeBool "SERVICE_PAM" withPam) - (lib.cmakeBool "HYPRLAND" withHyprland) - (lib.cmakeBool "I3" withI3) - ]; + buildInputs = [ + qt6.qtbase + qt6.qtdeclarative + cli11 + ] + ++ lib.optional withQtSvg qt6.qtsvg + ++ lib.optional withCrashHandler (cpptrace.overrideAttrs (prev: { + cmakeFlags = prev.cmakeFlags ++ [ + "-DCPPTRACE_UNWIND_WITH_LIBUNWIND=TRUE" + ]; + buildInputs = prev.buildInputs ++ [ libunwind ]; + })) + ++ lib.optional withJemalloc jemalloc + ++ lib.optional (withWayland && lib.strings.compareVersions qt6.qtbase.version "6.10.0" == -1) qt6.qtwayland + ++ lib.optionals withWayland [ wayland wayland-protocols ] + ++ lib.optionals (withWayland && libgbm != null) [ libdrm libgbm vulkan-headers ] + ++ lib.optional withX11 libxcb + ++ lib.optional withPam pam + ++ lib.optional withPipewire pipewire + ++ lib.optionals withPolkit [ polkit glib ]; - # How to get debuginfo in gdb from a release build: - # 1. build `quickshell.debug` - # 2. set NIX_DEBUG_INFO_DIRS="/lib/debug" - # 3. launch gdb / coredumpctl and debuginfo will work - separateDebugInfo = !debug; - dontStrip = debug; + cmakeBuildType = if debug then "Debug" else "RelWithDebInfo"; - meta = with lib; { - homepage = "https://quickshell.outfoxxed.me"; - description = "Flexbile QtQuick based desktop shell toolkit"; - license = licenses.lgpl3Only; - platforms = platforms.linux; - mainProgram = "quickshell"; + cmakeFlags = [ + (lib.cmakeFeature "DISTRIBUTOR" "Official-Nix-Flake") + (lib.cmakeFeature "INSTALL_QML_PREFIX" qt6.qtbase.qtQmlPrefix) + (lib.cmakeBool "DISTRIBUTOR_DEBUGINFO_AVAILABLE" true) + (lib.cmakeFeature "GIT_REVISION" gitRev) + (lib.cmakeBool "CRASH_HANDLER" withCrashHandler) + (lib.cmakeBool "USE_JEMALLOC" withJemalloc) + (lib.cmakeBool "WAYLAND" withWayland) + (lib.cmakeBool "SCREENCOPY" (libgbm != null)) + (lib.cmakeBool "SERVICE_PIPEWIRE" withPipewire) + (lib.cmakeBool "SERVICE_PAM" withPam) + (lib.cmakeBool "SERVICE_NETWORKMANAGER" withNetworkManager) + (lib.cmakeBool "SERVICE_POLKIT" withPolkit) + (lib.cmakeBool "HYPRLAND" withHyprland) + (lib.cmakeBool "I3" withI3) + ]; + + # How to get debuginfo in gdb from a release build: + # 1. build `quickshell.debug` + # 2. set NIX_DEBUG_INFO_DIRS="/lib/debug" + # 3. launch gdb / coredumpctl and debuginfo will work + separateDebugInfo = !debug; + dontStrip = debug; + + meta = with lib; { + homepage = "https://quickshell.org"; + description = "Flexbile QtQuick based desktop shell toolkit"; + license = licenses.lgpl3Only; + platforms = platforms.linux; + mainProgram = "quickshell"; + }; }; -} + + wrapper = unwrapped.stdenv.mkDerivation { + inherit (unwrapped) version meta buildInputs; + pname = "${unwrapped.pname}-wrapped"; + + nativeBuildInputs = unwrapped.nativeBuildInputs ++ [ qt6.wrapQtAppsHook ]; + + dontUnpack = true; + dontConfigure = true; + dontBuild = true; + + installPhase = '' + mkdir -p $out + cp -r ${unwrapped}/* $out + ''; + + passthru = { + unwrapped = unwrapped; + withModules = modules: wrapper.overrideAttrs (prev: { + buildInputs = prev.buildInputs ++ modules; + }); + }; + }; +in wrapper diff --git a/flake.lock b/flake.lock index 7c25aa2..2f95a44 100644 --- a/flake.lock +++ b/flake.lock @@ -2,11 +2,11 @@ "nodes": { "nixpkgs": { "locked": { - "lastModified": 1749285348, - "narHash": "sha256-frdhQvPbmDYaScPFiCnfdh3B/Vh81Uuoo0w5TkWmmjU=", + "lastModified": 1768127708, + "narHash": "sha256-1Sm77VfZh3mU0F5OqKABNLWxOuDeHIlcFjsXeeiPazs=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "3e3afe5174c561dee0df6f2c2b2236990146329f", + "rev": "ffbc9f8cbaacfb331b6017d5a5abb21a492c9a38", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index 5de9c96..8edda2c 100644 --- a/flake.nix +++ b/flake.nix @@ -4,23 +4,28 @@ }; outputs = { self, nixpkgs }: let + overlayPkgs = p: p.appendOverlays [ self.overlays.default ]; + forEachSystem = fn: nixpkgs.lib.genAttrs nixpkgs.lib.platforms.linux - (system: fn system nixpkgs.legacyPackages.${system}); + (system: fn system (overlayPkgs nixpkgs.legacyPackages.${system})); in { - packages = forEachSystem (system: pkgs: rec { - quickshell = pkgs.callPackage ./default.nix { - gitRev = self.rev or self.dirtyRev; - }; + overlays.default = import ./overlay.nix { + rev = self.rev or self.dirtyRev; + }; + packages = forEachSystem (system: pkgs: rec { + quickshell = pkgs.quickshell; default = quickshell; }); devShells = forEachSystem (system: pkgs: rec { default = import ./shell.nix { inherit pkgs; - inherit (self.packages.${system}) quickshell; + quickshell = self.packages.${system}.quickshell.override { + stdenv = pkgs.clangStdenv; + }; }; }); }; diff --git a/overlay.nix b/overlay.nix new file mode 100644 index 0000000..d8ea137 --- /dev/null +++ b/overlay.nix @@ -0,0 +1,5 @@ +{ rev ? null }: (final: prev: { + quickshell = final.callPackage ./default.nix { + gitRev = rev; + }; +}) diff --git a/quickshell.scm b/quickshell.scm index 26abdc0..780bb96 100644 --- a/quickshell.scm +++ b/quickshell.scm @@ -42,6 +42,7 @@ libxcb libxkbcommon linux-pam + polkit mesa pipewire qtbase @@ -55,8 +56,7 @@ #~(list "-GNinja" "-DDISTRIBUTOR=\"In-tree Guix channel\"" "-DDISTRIBUTOR_DEBUGINFO_AVAILABLE=NO" - ;; Breakpad is not currently packaged for Guix. - "-DCRASH_REPORTER=OFF") + "-DCRASH_HANDLER=OFF") #:phases #~(modify-phases %standard-phases (replace 'build (lambda _ (invoke "cmake" "--build" "."))) diff --git a/shell.nix b/shell.nix index 82382f9..03a446d 100644 --- a/shell.nix +++ b/shell.nix @@ -1,14 +1,15 @@ { pkgs ? import {}, - quickshell ? pkgs.callPackage ./default.nix {}, + stdenv ? pkgs.clangStdenv, # faster compiles than gcc + quickshell ? pkgs.callPackage ./default.nix { inherit stdenv; }, ... }: let tidyfox = import (pkgs.fetchFromGitea { domain = "git.outfoxxed.me"; owner = "outfoxxed"; repo = "tidyfox"; - rev = "1f062cc198d1112d13e5128fa1f2ee3dbffe613b"; - sha256 = "kbt0Zc1qHE5fhqBkKz8iue+B+ZANjF1AR/RdgmX1r0I="; + rev = "9d85d7e7dea2602aa74ec3168955fee69967e92f"; + hash = "sha256-77ERiweF6lumonp2c/124rAoVG6/o9J+Aajhttwtu0w="; }) { inherit pkgs; }; in pkgs.mkShell.override { stdenv = quickshell.stdenv; } { inputsFrom = [ quickshell ]; diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 52db00a..0c05419 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -11,8 +11,9 @@ add_subdirectory(window) add_subdirectory(io) add_subdirectory(widgets) add_subdirectory(ui) +add_subdirectory(windowmanager) -if (CRASH_REPORTER) +if (CRASH_HANDLER) add_subdirectory(crash) endif() @@ -33,3 +34,7 @@ add_subdirectory(services) if (BLUETOOTH) add_subdirectory(bluetooth) endif() + +if (NETWORK) + add_subdirectory(network) +endif() diff --git a/src/bluetooth/adapter.cpp b/src/bluetooth/adapter.cpp index 92ab837..7f70a27 100644 --- a/src/bluetooth/adapter.cpp +++ b/src/bluetooth/adapter.cpp @@ -9,7 +9,6 @@ #include #include #include -#include #include #include "../core/logcat.hpp" @@ -53,6 +52,12 @@ QString BluetoothAdapter::adapterId() const { void BluetoothAdapter::setEnabled(bool enabled) { if (enabled == this->bEnabled) return; + + if (enabled && this->bState == BluetoothAdapterState::Blocked) { + qCCritical(logAdapter) << "Cannot enable adapter because it is blocked by rfkill."; + return; + } + this->bEnabled = enabled; this->pEnabled.write(); } diff --git a/src/bluetooth/device.cpp b/src/bluetooth/device.cpp index 7265b24..b140aa0 100644 --- a/src/bluetooth/device.cpp +++ b/src/bluetooth/device.cpp @@ -8,7 +8,6 @@ #include #include #include -#include #include #include diff --git a/src/build/CMakeLists.txt b/src/build/CMakeLists.txt index bb35da9..c1ffa59 100644 --- a/src/build/CMakeLists.txt +++ b/src/build/CMakeLists.txt @@ -9,16 +9,10 @@ if (NOT DEFINED GIT_REVISION) ) endif() -if (CRASH_REPORTER) - set(CRASH_REPORTER_DEF 1) +if (CRASH_HANDLER) + set(CRASH_HANDLER_DEF 1) else() - set(CRASH_REPORTER_DEF 0) -endif() - -if (DISTRIBUTOR_DEBUGINFO_AVAILABLE) - set(DEBUGINFO_AVAILABLE 1) -else() - set(DEBUGINFO_AVAILABLE 0) + set(CRASH_HANDLER_DEF 0) endif() configure_file(build.hpp.in build.hpp @ONLY ESCAPE_QUOTES) diff --git a/src/build/build.hpp.in b/src/build/build.hpp.in index 075abd1..2ab2db2 100644 --- a/src/build/build.hpp.in +++ b/src/build/build.hpp.in @@ -1,10 +1,14 @@ #pragma once // NOLINTBEGIN +#define QS_VERSION "@quickshell_VERSION@" +#define QS_VERSION_MAJOR @quickshell_VERSION_MAJOR@ +#define QS_VERSION_MINOR @quickshell_VERSION_MINOR@ +#define QS_VERSION_PATCH @quickshell_VERSION_PATCH@ +#define QS_UNRELEASED_FEATURES "@UNRELEASED_FEATURES@" #define GIT_REVISION "@GIT_REVISION@" #define DISTRIBUTOR "@DISTRIBUTOR@" -#define DISTRIBUTOR_DEBUGINFO_AVAILABLE @DEBUGINFO_AVAILABLE@ -#define CRASH_REPORTER @CRASH_REPORTER_DEF@ +#define CRASH_HANDLER @CRASH_HANDLER_DEF@ #define BUILD_TYPE "@CMAKE_BUILD_TYPE@" #define COMPILER "@CMAKE_CXX_COMPILER_ID@ (@CMAKE_CXX_COMPILER_VERSION@)" #define COMPILE_FLAGS "@CMAKE_CXX_FLAGS@" diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index eca7270..fb63f40 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -12,6 +12,7 @@ qt_add_library(quickshell-core STATIC singleton.cpp generation.cpp scan.cpp + scanenv.cpp qsintercept.cpp incubator.cpp lazyloader.cpp @@ -23,7 +24,7 @@ qt_add_library(quickshell-core STATIC model.cpp elapsedtimer.cpp desktopentry.cpp - objectrepeater.cpp + desktopentrymonitor.cpp platformmenu.cpp qsmenu.cpp retainable.cpp @@ -38,6 +39,7 @@ qt_add_library(quickshell-core STATIC iconprovider.cpp scriptmodel.cpp colorquantizer.cpp + toolsupport.cpp ) qt_add_qml_module(quickshell-core @@ -50,7 +52,7 @@ qt_add_qml_module(quickshell-core install_qml_module(quickshell-core) -target_link_libraries(quickshell-core PRIVATE Qt::Quick Qt::Widgets) +target_link_libraries(quickshell-core PRIVATE Qt::Quick Qt::QuickPrivate Qt::Widgets quickshell-build) qs_module_pch(quickshell-core SET large) diff --git a/src/core/colorquantizer.cpp b/src/core/colorquantizer.cpp index 6cfb05d..4ac850b 100644 --- a/src/core/colorquantizer.cpp +++ b/src/core/colorquantizer.cpp @@ -28,26 +28,28 @@ ColorQuantizerOperation::ColorQuantizerOperation(QUrl* source, qreal depth, qrea : source(source) , maxDepth(depth) , rescaleSize(rescaleSize) { - setAutoDelete(false); + this->setAutoDelete(false); } void ColorQuantizerOperation::quantizeImage(const QAtomicInteger& shouldCancel) { - if (shouldCancel.loadAcquire() || source->isEmpty()) return; + if (shouldCancel.loadAcquire() || this->source->isEmpty()) return; - colors.clear(); + this->colors.clear(); - auto image = QImage(source->toLocalFile()); - if ((image.width() > rescaleSize || image.height() > rescaleSize) && rescaleSize > 0) { + auto image = QImage(this->source->toLocalFile()); + if ((image.width() > this->rescaleSize || image.height() > this->rescaleSize) + && this->rescaleSize > 0) + { image = image.scaled( - static_cast(rescaleSize), - static_cast(rescaleSize), + static_cast(this->rescaleSize), + static_cast(this->rescaleSize), Qt::KeepAspectRatio, Qt::SmoothTransformation ); } if (image.isNull()) { - qCWarning(logColorQuantizer) << "Failed to load image from" << source->toString(); + qCWarning(logColorQuantizer) << "Failed to load image from" << this->source->toString(); return; } @@ -63,7 +65,7 @@ void ColorQuantizerOperation::quantizeImage(const QAtomicInteger& shouldCa auto startTime = QDateTime::currentDateTime(); - colors = quantization(pixels, 0); + this->colors = this->quantization(pixels, 0); auto endTime = QDateTime::currentDateTime(); auto milliseconds = startTime.msecsTo(endTime); @@ -77,7 +79,7 @@ QList ColorQuantizerOperation::quantization( ) { if (shouldCancel.loadAcquire()) return QList(); - if (depth >= maxDepth || rgbValues.isEmpty()) { + if (depth >= this->maxDepth || rgbValues.isEmpty()) { if (rgbValues.isEmpty()) return QList(); auto totalR = 0; @@ -114,8 +116,8 @@ QList ColorQuantizerOperation::quantization( auto rightHalf = rgbValues.mid(mid); QList result; - result.append(quantization(leftHalf, depth + 1)); - result.append(quantization(rightHalf, depth + 1)); + result.append(this->quantization(leftHalf, depth + 1)); + result.append(this->quantization(rightHalf, depth + 1)); return result; } @@ -159,7 +161,7 @@ void ColorQuantizerOperation::finishRun() { } void ColorQuantizerOperation::finished() { - emit this->done(colors); + emit this->done(this->colors); delete this; } @@ -178,39 +180,39 @@ void ColorQuantizerOperation::run() { void ColorQuantizerOperation::tryCancel() { this->shouldCancel.storeRelease(true); } void ColorQuantizer::componentComplete() { - componentCompleted = true; - if (!mSource.isEmpty()) quantizeAsync(); + this->componentCompleted = true; + if (!this->mSource.isEmpty()) this->quantizeAsync(); } void ColorQuantizer::setSource(const QUrl& source) { - if (mSource != source) { - mSource = source; + if (this->mSource != source) { + this->mSource = source; emit this->sourceChanged(); - if (this->componentCompleted && !mSource.isEmpty()) quantizeAsync(); + if (this->componentCompleted && !this->mSource.isEmpty()) this->quantizeAsync(); } } void ColorQuantizer::setDepth(qreal depth) { - if (mDepth != depth) { - mDepth = depth; + if (this->mDepth != depth) { + this->mDepth = depth; emit this->depthChanged(); - if (this->componentCompleted) quantizeAsync(); + if (this->componentCompleted) this->quantizeAsync(); } } void ColorQuantizer::setRescaleSize(int rescaleSize) { - if (mRescaleSize != rescaleSize) { - mRescaleSize = rescaleSize; + if (this->mRescaleSize != rescaleSize) { + this->mRescaleSize = rescaleSize; emit this->rescaleSizeChanged(); - if (this->componentCompleted) quantizeAsync(); + if (this->componentCompleted) this->quantizeAsync(); } } void ColorQuantizer::operationFinished(const QList& result) { - bColors = result; + this->bColors = result; this->liveOperation = nullptr; emit this->colorsChanged(); } @@ -219,7 +221,8 @@ void ColorQuantizer::quantizeAsync() { if (this->liveOperation) this->cancelAsync(); qCDebug(logColorQuantizer) << "Starting color quantization asynchronously"; - this->liveOperation = new ColorQuantizerOperation(&mSource, mDepth, mRescaleSize); + this->liveOperation = + new ColorQuantizerOperation(&this->mSource, this->mDepth, this->mRescaleSize); QObject::connect( this->liveOperation, diff --git a/src/core/colorquantizer.hpp b/src/core/colorquantizer.hpp index d35a15a..f6e158d 100644 --- a/src/core/colorquantizer.hpp +++ b/src/core/colorquantizer.hpp @@ -91,13 +91,13 @@ public: [[nodiscard]] QBindable> bindableColors() { return &this->bColors; } - [[nodiscard]] QUrl source() const { return mSource; } + [[nodiscard]] QUrl source() const { return this->mSource; } void setSource(const QUrl& source); - [[nodiscard]] qreal depth() const { return mDepth; } + [[nodiscard]] qreal depth() const { return this->mDepth; } void setDepth(qreal depth); - [[nodiscard]] qreal rescaleSize() const { return mRescaleSize; } + [[nodiscard]] qreal rescaleSize() const { return this->mRescaleSize; } void setRescaleSize(int rescaleSize); signals: diff --git a/src/core/desktopentry.cpp b/src/core/desktopentry.cpp index 4673881..637f758 100644 --- a/src/core/desktopentry.cpp +++ b/src/core/desktopentry.cpp @@ -1,21 +1,27 @@ #include "desktopentry.hpp" +#include +#include #include #include #include +#include #include -#include -#include #include #include #include #include +#include #include -#include +#include +#include #include +#include +#include #include #include "../io/processcore.hpp" +#include "desktopentrymonitor.hpp" #include "logcat.hpp" #include "model.hpp" #include "qmlglobal.hpp" @@ -55,12 +61,14 @@ struct Locale { [[nodiscard]] int matchScore(const Locale& other) const { if (this->language != other.language) return 0; - auto territoryMatches = !this->territory.isEmpty() && this->territory == other.territory; - auto modifierMatches = !this->modifier.isEmpty() && this->modifier == other.modifier; + + if (!other.modifier.isEmpty() && this->modifier != other.modifier) return 0; + if (!other.territory.isEmpty() && this->territory != other.territory) return 0; auto score = 1; - if (territoryMatches) score += 2; - if (modifierMatches) score += 1; + + if (!other.territory.isEmpty()) score += 2; + if (!other.modifier.isEmpty()) score += 1; return score; } @@ -86,56 +94,64 @@ struct Locale { QDebug operator<<(QDebug debug, const Locale& locale) { auto saver = QDebugStateSaver(debug); debug.nospace() << "Locale(language=" << locale.language << ", territory=" << locale.territory - << ", modifier" << locale.modifier << ')'; + << ", modifier=" << locale.modifier << ')'; return debug; } -void DesktopEntry::parseEntry(const QString& text) { +ParsedDesktopEntryData DesktopEntry::parseText(const QString& id, const QString& text) { + ParsedDesktopEntryData data; + data.id = id; const auto& system = Locale::system(); auto groupName = QString(); auto entries = QHash>(); - auto finishCategory = [this, &groupName, &entries]() { + auto actionOrder = QStringList(); + auto pendingActions = QHash(); + + auto finishCategory = [&data, &groupName, &entries, &actionOrder, &pendingActions]() { if (groupName == "Desktop Entry") { - if (entries["Type"].second != "Application") return; - if (entries.contains("Hidden") && entries["Hidden"].second == "true") return; + if (entries.value("Type").second != "Application") return; for (const auto& [key, pair]: entries.asKeyValueRange()) { auto& [_, value] = pair; - this->mEntries.insert(key, value); + data.entries.insert(key, value); - if (key == "Name") this->mName = value; - else if (key == "GenericName") this->mGenericName = value; - else if (key == "NoDisplay") this->mNoDisplay = value == "true"; - else if (key == "Comment") this->mComment = value; - else if (key == "Icon") this->mIcon = value; + if (key == "Name") data.name = value; + else if (key == "GenericName") data.genericName = value; + else if (key == "StartupWMClass") data.startupClass = value; + else if (key == "NoDisplay") data.noDisplay = value == "true"; + else if (key == "Hidden") data.hidden = value == "true"; + else if (key == "Comment") data.comment = value; + else if (key == "Icon") data.icon = value; else if (key == "Exec") { - this->mExecString = value; - this->mCommand = DesktopEntry::parseExecString(value); - } else if (key == "Path") this->mWorkingDirectory = value; - else if (key == "Terminal") this->mTerminal = value == "true"; - else if (key == "Categories") this->mCategories = value.split(u';', Qt::SkipEmptyParts); - else if (key == "Keywords") this->mKeywords = value.split(u';', Qt::SkipEmptyParts); + data.execString = value; + data.command = DesktopEntry::parseExecString(value); + } else if (key == "Path") data.workingDirectory = value; + else if (key == "Terminal") data.terminal = value == "true"; + else if (key == "Categories") data.categories = value.split(u';', Qt::SkipEmptyParts); + else if (key == "Keywords") data.keywords = value.split(u';', Qt::SkipEmptyParts); + else if (key == "Actions") actionOrder = value.split(u';', Qt::SkipEmptyParts); } } else if (groupName.startsWith("Desktop Action ")) { - auto actionName = groupName.sliced(16); - auto* action = new DesktopAction(actionName, this); + auto actionName = groupName.sliced(15); + DesktopActionData action; + action.id = actionName; for (const auto& [key, pair]: entries.asKeyValueRange()) { const auto& [_, value] = pair; - action->mEntries.insert(key, value); + action.entries.insert(key, value); - if (key == "Name") action->mName = value; - else if (key == "Icon") action->mIcon = value; + if (key == "Name") action.name = value; + else if (key == "Icon") action.icon = value; else if (key == "Exec") { - action->mExecString = value; - action->mCommand = DesktopEntry::parseExecString(value); + action.execString = value; + action.command = DesktopEntry::parseExecString(value); } } - this->mActions.insert(actionName, action); + pendingActions.insert(actionName, action); } entries.clear(); @@ -181,16 +197,73 @@ void DesktopEntry::parseEntry(const QString& text) { } finishCategory(); + + for (const auto& actionId: actionOrder) { + if (pendingActions.contains(actionId)) { + data.actions.append(pendingActions.value(actionId)); + } + } + + return data; +} + +void DesktopEntry::updateState(const ParsedDesktopEntryData& newState) { + Qt::beginPropertyUpdateGroup(); + this->bName = newState.name; + this->bGenericName = newState.genericName; + this->bStartupClass = newState.startupClass; + this->bNoDisplay = newState.noDisplay; + this->bComment = newState.comment; + this->bIcon = newState.icon; + this->bExecString = newState.execString; + this->bCommand = newState.command; + this->bWorkingDirectory = newState.workingDirectory; + this->bRunInTerminal = newState.terminal; + this->bCategories = newState.categories; + this->bKeywords = newState.keywords; + Qt::endPropertyUpdateGroup(); + + this->state = newState; + this->updateActions(newState.actions); +} + +void DesktopEntry::updateActions(const QVector& newActions) { + auto old = this->mActions; + this->mActions.clear(); + + for (const auto& d: newActions) { + DesktopAction* act = nullptr; + auto found = std::ranges::find(old, d.id, &DesktopAction::mId); + if (found != old.end()) { + act = *found; + old.erase(found); + } else { + act = new DesktopAction(d.id, this); + } + + Qt::beginPropertyUpdateGroup(); + act->bName = d.name; + act->bIcon = d.icon; + act->bExecString = d.execString; + act->bCommand = d.command; + Qt::endPropertyUpdateGroup(); + + act->mEntries = d.entries; + this->mActions.append(act); + } + + for (auto* leftover: old) { + leftover->deleteLater(); + } } void DesktopEntry::execute() const { - DesktopEntry::doExec(this->mCommand, this->mWorkingDirectory); + DesktopEntry::doExec(this->bCommand.value(), this->bWorkingDirectory.value()); } -bool DesktopEntry::isValid() const { return !this->mName.isEmpty(); } -bool DesktopEntry::noDisplay() const { return this->mNoDisplay; } +bool DesktopEntry::isValid() const { return !this->bName.value().isEmpty(); } -QVector DesktopEntry::actions() const { return this->mActions.values(); } +QVector DesktopEntry::actions() const { return this->mActions; } QVector DesktopEntry::parseExecString(const QString& execString) { QVector arguments; @@ -209,16 +282,22 @@ QVector DesktopEntry::parseExecString(const QString& execString) { currentArgument += '\\'; escape = 0; } + } else if (escape == 2) { + currentArgument += c; + escape = 0; } else if (escape != 0) { - if (escape != 2) { - // Technically this is an illegal state, but the spec has a terrible double escape - // rule in strings for no discernable reason. Assuming someone might understandably - // misunderstand it, treat it as a normal escape and log it. + switch (c.unicode()) { + case 's': currentArgument += u' '; break; + case 'n': currentArgument += u'\n'; break; + case 't': currentArgument += u'\t'; break; + case 'r': currentArgument += u'\r'; break; + case '\\': currentArgument += u'\\'; break; + default: qCWarning(logDesktopEntry).noquote() << "Illegal escape sequence in desktop entry exec string:" << execString; + currentArgument += c; + break; } - - currentArgument += c; escape = 0; } else if (c == u'"' || c == u'\'') { parsingString = false; @@ -264,59 +343,44 @@ void DesktopEntry::doExec(const QList& execString, const QString& worki } void DesktopAction::execute() const { - DesktopEntry::doExec(this->mCommand, this->entry->mWorkingDirectory); + DesktopEntry::doExec(this->bCommand.value(), this->entry->bWorkingDirectory.value()); } -DesktopEntryManager::DesktopEntryManager() { - this->scanDesktopEntries(); - this->populateApplications(); +DesktopEntryScanner::DesktopEntryScanner(DesktopEntryManager* manager): manager(manager) { + this->setAutoDelete(true); } -void DesktopEntryManager::scanDesktopEntries() { - QList dataPaths; +void DesktopEntryScanner::run() { + const auto& desktopPaths = DesktopEntryManager::desktopPaths(); + auto scanResults = QList(); - if (qEnvironmentVariableIsSet("XDG_DATA_HOME")) { - dataPaths.push_back(qEnvironmentVariable("XDG_DATA_HOME")); - } else if (qEnvironmentVariableIsSet("HOME")) { - dataPaths.push_back(qEnvironmentVariable("HOME") + "/.local/share"); + for (const auto& path: desktopPaths | std::views::reverse) { + auto file = QFileInfo(path); + if (!file.isDir()) continue; + + this->scanDirectory(QDir(path), QString(), scanResults); } - if (qEnvironmentVariableIsSet("XDG_DATA_DIRS")) { - auto var = qEnvironmentVariable("XDG_DATA_DIRS"); - dataPaths += var.split(u':', Qt::SkipEmptyParts); - } else { - dataPaths.push_back("/usr/local/share"); - dataPaths.push_back("/usr/share"); - } - - qCDebug(logDesktopEntry) << "Creating desktop entry scanners"; - - for (auto& path: std::ranges::reverse_view(dataPaths)) { - auto p = QDir(path).filePath("applications"); - auto file = QFileInfo(p); - - if (!file.isDir()) { - qCDebug(logDesktopEntry) << "Not scanning path" << p << "as it is not a directory"; - continue; - } - - qCDebug(logDesktopEntry) << "Scanning path" << p; - this->scanPath(p); - } + QMetaObject::invokeMethod( + this->manager, + "onScanCompleted", + Qt::QueuedConnection, + Q_ARG(QList, scanResults) + ); } -void DesktopEntryManager::populateApplications() { - for (auto& entry: this->desktopEntries.values()) { - if (!entry->noDisplay()) this->mApplications.insertObject(entry); - } -} +void DesktopEntryScanner::scanDirectory( + const QDir& dir, + const QString& idPrefix, + QList& entries +) { + auto dirEntries = dir.entryInfoList(QDir::Dirs | QDir::Files | QDir::NoDotAndDotDot); -void DesktopEntryManager::scanPath(const QDir& dir, const QString& prefix) { - auto entries = dir.entryInfoList(QDir::Dirs | QDir::Files | QDir::NoDotAndDotDot); - - for (auto& entry: entries) { - if (entry.isDir()) this->scanPath(entry.absoluteFilePath(), prefix + dir.dirName() + "-"); - else if (entry.isFile()) { + for (auto& entry: dirEntries) { + if (entry.isDir()) { + auto subdirPrefix = idPrefix.isEmpty() ? entry.fileName() : idPrefix + '-' + entry.fileName(); + this->scanDirectory(QDir(entry.absoluteFilePath()), subdirPrefix, entries); + } else if (entry.isFile()) { auto path = entry.filePath(); if (!path.endsWith(".desktop")) { qCDebug(logDesktopEntry) << "Skipping file" << path << "as it has no .desktop extension"; @@ -329,46 +393,42 @@ void DesktopEntryManager::scanPath(const QDir& dir, const QString& prefix) { continue; } - auto id = prefix + entry.fileName().sliced(0, entry.fileName().length() - 8); - auto lowerId = id.toLower(); + auto basename = QFileInfo(entry.fileName()).completeBaseName(); + auto id = idPrefix.isEmpty() ? basename : idPrefix + '-' + basename; + auto content = QString::fromUtf8(file.readAll()); - auto text = QString::fromUtf8(file.readAll()); - auto* dentry = new DesktopEntry(id, this); - dentry->parseEntry(text); - - if (!dentry->isValid()) { - qCDebug(logDesktopEntry) << "Skipping desktop entry" << path; - delete dentry; - continue; - } - - qCDebug(logDesktopEntry) << "Found desktop entry" << id << "at" << path; - - auto conflictingId = this->desktopEntries.contains(id); - - if (conflictingId) { - qCDebug(logDesktopEntry) << "Replacing old entry for" << id; - delete this->desktopEntries.value(id); - this->desktopEntries.remove(id); - this->lowercaseDesktopEntries.remove(lowerId); - } - - this->desktopEntries.insert(id, dentry); - - if (this->lowercaseDesktopEntries.contains(lowerId)) { - qCInfo(logDesktopEntry).nospace() - << "Multiple desktop entries have the same lowercased id " << lowerId - << ". This can cause ambiguity when byId requests are not made with the correct case " - "already."; - - this->lowercaseDesktopEntries.remove(lowerId); - } - - this->lowercaseDesktopEntries.insert(lowerId, dentry); + auto data = DesktopEntry::parseText(id, content); + entries.append(std::move(data)); } } } +DesktopEntryManager::DesktopEntryManager(): monitor(new DesktopEntryMonitor(this)) { + QObject::connect( + this->monitor, + &DesktopEntryMonitor::desktopEntriesChanged, + this, + &DesktopEntryManager::handleFileChanges + ); + + DesktopEntryScanner(this).run(); +} + +void DesktopEntryManager::scanDesktopEntries() { + qCDebug(logDesktopEntry) << "Starting desktop entry scan"; + + if (this->scanInProgress) { + qCDebug(logDesktopEntry) << "Scan already in progress, queuing another scan"; + this->scanQueued = true; + return; + } + + this->scanInProgress = true; + this->scanQueued = false; + auto* scanner = new DesktopEntryScanner(this); + QThreadPool::globalInstance()->start(scanner); +} + DesktopEntryManager* DesktopEntryManager::instance() { static auto* instance = new DesktopEntryManager(); // NOLINT return instance; @@ -384,14 +444,167 @@ DesktopEntry* DesktopEntryManager::byId(const QString& id) { } } +DesktopEntry* DesktopEntryManager::heuristicLookup(const QString& name) { + if (auto* entry = this->byId(name)) return entry; + + auto list = this->desktopEntries.values(); + + auto iter = std::ranges::find_if(list, [&](DesktopEntry* entry) { + return name == entry->bStartupClass.value(); + }); + + if (iter != list.end()) return *iter; + + iter = std::ranges::find_if(list, [&](DesktopEntry* entry) { + return name.toLower() == entry->bStartupClass.value().toLower(); + }); + + if (iter != list.end()) return *iter; + return nullptr; +} + ObjectModel* DesktopEntryManager::applications() { return &this->mApplications; } -DesktopEntries::DesktopEntries() { DesktopEntryManager::instance(); } +void DesktopEntryManager::handleFileChanges() { + qCDebug(logDesktopEntry) << "Directory change detected, performing full rescan"; + + if (this->scanInProgress) { + qCDebug(logDesktopEntry) << "Scan already in progress, queuing another scan"; + this->scanQueued = true; + return; + } + + this->scanInProgress = true; + this->scanQueued = false; + auto* scanner = new DesktopEntryScanner(this); + QThreadPool::globalInstance()->start(scanner); +} + +const QStringList& DesktopEntryManager::desktopPaths() { + static const auto paths = []() { + auto dataPaths = QStringList(); + + auto dataHome = qEnvironmentVariable("XDG_DATA_HOME"); + if (dataHome.isEmpty() && qEnvironmentVariableIsSet("HOME")) + dataHome = qEnvironmentVariable("HOME") + "/.local/share"; + if (!dataHome.isEmpty()) dataPaths.append(dataHome + "/applications"); + + auto dataDirs = qEnvironmentVariable("XDG_DATA_DIRS"); + if (dataDirs.isEmpty()) dataDirs = "/usr/local/share:/usr/share"; + + for (const auto& dir: dataDirs.split(':', Qt::SkipEmptyParts)) { + dataPaths.append(dir + "/applications"); + } + + return dataPaths; + }(); + + return paths; +} + +void DesktopEntryManager::onScanCompleted(const QList& scanResults) { + auto guard = qScopeGuard([this] { + this->scanInProgress = false; + if (this->scanQueued) { + this->scanQueued = false; + this->scanDesktopEntries(); + } + }); + + auto oldEntries = this->desktopEntries; + auto newEntries = QHash(); + auto newLowercaseEntries = QHash(); + + for (const auto& data: scanResults) { + auto lowerId = data.id.toLower(); + + if (data.hidden) { + if (auto* victim = newEntries.take(data.id)) victim->deleteLater(); + newLowercaseEntries.remove(lowerId); + + if (auto it = oldEntries.find(data.id); it != oldEntries.end()) { + it.value()->deleteLater(); + oldEntries.erase(it); + } + + qCDebug(logDesktopEntry) << "Masking hidden desktop entry" << data.id; + continue; + } + + DesktopEntry* dentry = nullptr; + + if (auto it = oldEntries.find(data.id); it != oldEntries.end()) { + dentry = it.value(); + oldEntries.erase(it); + dentry->updateState(data); + } else { + dentry = new DesktopEntry(data.id, this); + dentry->updateState(data); + } + + if (!dentry->isValid()) { + qCDebug(logDesktopEntry) << "Skipping desktop entry" << data.id; + if (!oldEntries.contains(data.id)) { + dentry->deleteLater(); + } + continue; + } + + qCDebug(logDesktopEntry) << "Found desktop entry" << data.id; + + auto conflictingId = newEntries.contains(data.id); + + if (conflictingId) { + qCDebug(logDesktopEntry) << "Replacing old entry for" << data.id; + if (auto* victim = newEntries.take(data.id)) victim->deleteLater(); + newLowercaseEntries.remove(lowerId); + } + + newEntries.insert(data.id, dentry); + + if (newLowercaseEntries.contains(lowerId)) { + qCInfo(logDesktopEntry).nospace() + << "Multiple desktop entries have the same lowercased id " << lowerId + << ". This can cause ambiguity when byId requests are not made with the correct case " + "already."; + + newLowercaseEntries.remove(lowerId); + } + + newLowercaseEntries.insert(lowerId, dentry); + } + + this->desktopEntries = newEntries; + this->lowercaseDesktopEntries = newLowercaseEntries; + + auto newApplications = QVector(); + for (auto* entry: this->desktopEntries.values()) + if (!entry->bNoDisplay) newApplications.append(entry); + + this->mApplications.diffUpdate(newApplications); + + emit this->applicationsChanged(); + + for (auto* e: oldEntries) e->deleteLater(); +} + +DesktopEntries::DesktopEntries() { + QObject::connect( + DesktopEntryManager::instance(), + &DesktopEntryManager::applicationsChanged, + this, + &DesktopEntries::applicationsChanged + ); +} DesktopEntry* DesktopEntries::byId(const QString& id) { return DesktopEntryManager::instance()->byId(id); } +DesktopEntry* DesktopEntries::heuristicLookup(const QString& name) { + return DesktopEntryManager::instance()->heuristicLookup(name); +} + ObjectModel* DesktopEntries::applications() { return DesktopEntryManager::instance()->applications(); } diff --git a/src/core/desktopentry.hpp b/src/core/desktopentry.hpp index ee8f511..0d1eff2 100644 --- a/src/core/desktopentry.hpp +++ b/src/core/desktopentry.hpp @@ -6,32 +6,68 @@ #include #include #include +#include #include +#include #include +#include "desktopentrymonitor.hpp" #include "doc.hpp" #include "model.hpp" class DesktopAction; +class DesktopEntryMonitor; + +struct DesktopActionData { + QString id; + QString name; + QString icon; + QString execString; + QVector command; + QHash entries; +}; + +struct ParsedDesktopEntryData { + QString id; + QString name; + QString genericName; + QString startupClass; + bool noDisplay = false; + bool hidden = false; + QString comment; + QString icon; + QString execString; + QVector command; + QString workingDirectory; + bool terminal = false; + QVector categories; + QVector keywords; + QHash entries; + QVector actions; +}; /// A desktop entry. See @@DesktopEntries for details. class DesktopEntry: public QObject { Q_OBJECT; Q_PROPERTY(QString id MEMBER mId CONSTANT); /// Name of the specific application, such as "Firefox". - Q_PROPERTY(QString name MEMBER mName CONSTANT); + // clang-format off + Q_PROPERTY(QString name READ default WRITE default NOTIFY nameChanged BINDABLE bindableName); /// Short description of the application, such as "Web Browser". May be empty. - Q_PROPERTY(QString genericName MEMBER mGenericName CONSTANT); + Q_PROPERTY(QString genericName READ default WRITE default NOTIFY genericNameChanged BINDABLE bindableGenericName); + /// Initial class or app id the app intends to use. May be useful for matching running apps + /// to desktop entries. + Q_PROPERTY(QString startupClass READ default WRITE default NOTIFY startupClassChanged BINDABLE bindableStartupClass); /// If true, this application should not be displayed in menus and launchers. - Q_PROPERTY(bool noDisplay MEMBER mNoDisplay CONSTANT); + Q_PROPERTY(bool noDisplay READ default WRITE default NOTIFY noDisplayChanged BINDABLE bindableNoDisplay); /// Long description of the application, such as "View websites on the internet". May be empty. - Q_PROPERTY(QString comment MEMBER mComment CONSTANT); + Q_PROPERTY(QString comment READ default WRITE default NOTIFY commentChanged BINDABLE bindableComment); /// Name of the icon associated with this application. May be empty. - Q_PROPERTY(QString icon MEMBER mIcon CONSTANT); + Q_PROPERTY(QString icon READ default WRITE default NOTIFY iconChanged BINDABLE bindableIcon); /// The raw `Exec` string from the desktop entry. /// /// > [!WARNING] This cannot be reliably run as a command. See @@command for one you can run. - Q_PROPERTY(QString execString MEMBER mExecString CONSTANT); + Q_PROPERTY(QString execString READ default WRITE default NOTIFY execStringChanged BINDABLE bindableExecString); /// The parsed `Exec` command in the desktop entry. /// /// The entry can be run with @@execute(), or by using this command in @@ -40,13 +76,14 @@ class DesktopEntry: public QObject { /// the invoked process. See @@execute() for details. /// /// > [!NOTE] The provided command does not invoke a terminal even if @@runInTerminal is true. - Q_PROPERTY(QVector command MEMBER mCommand CONSTANT); + Q_PROPERTY(QVector command READ default WRITE default NOTIFY commandChanged BINDABLE bindableCommand); /// The working directory to execute from. - Q_PROPERTY(QString workingDirectory MEMBER mWorkingDirectory CONSTANT); + Q_PROPERTY(QString workingDirectory READ default WRITE default NOTIFY workingDirectoryChanged BINDABLE bindableWorkingDirectory); /// If the application should run in a terminal. - Q_PROPERTY(bool runInTerminal MEMBER mTerminal CONSTANT); - Q_PROPERTY(QVector categories MEMBER mCategories CONSTANT); - Q_PROPERTY(QVector keywords MEMBER mKeywords CONSTANT); + Q_PROPERTY(bool runInTerminal READ default WRITE default NOTIFY runInTerminalChanged BINDABLE bindableRunInTerminal); + Q_PROPERTY(QVector categories READ default WRITE default NOTIFY categoriesChanged BINDABLE bindableCategories); + Q_PROPERTY(QVector keywords READ default WRITE default NOTIFY keywordsChanged BINDABLE bindableKeywords); + // clang-format on Q_PROPERTY(QVector actions READ actions CONSTANT); QML_ELEMENT; QML_UNCREATABLE("DesktopEntry instances must be retrieved from DesktopEntries"); @@ -54,7 +91,8 @@ class DesktopEntry: public QObject { public: explicit DesktopEntry(QString id, QObject* parent): QObject(parent), mId(std::move(id)) {} - void parseEntry(const QString& text); + static ParsedDesktopEntryData parseText(const QString& id, const QString& text); + void updateState(const ParsedDesktopEntryData& newState); /// Run the application. Currently ignores @@runInTerminal and field codes. /// @@ -70,30 +108,66 @@ public: Q_INVOKABLE void execute() const; [[nodiscard]] bool isValid() const; - [[nodiscard]] bool noDisplay() const; [[nodiscard]] QVector actions() const; + [[nodiscard]] QBindable bindableName() const { return &this->bName; } + [[nodiscard]] QBindable bindableGenericName() const { return &this->bGenericName; } + [[nodiscard]] QBindable bindableStartupClass() const { return &this->bStartupClass; } + [[nodiscard]] QBindable bindableNoDisplay() const { return &this->bNoDisplay; } + [[nodiscard]] QBindable bindableComment() const { return &this->bComment; } + [[nodiscard]] QBindable bindableIcon() const { return &this->bIcon; } + [[nodiscard]] QBindable bindableExecString() const { return &this->bExecString; } + [[nodiscard]] QBindable> bindableCommand() const { return &this->bCommand; } + [[nodiscard]] QBindable bindableWorkingDirectory() const { + return &this->bWorkingDirectory; + } + [[nodiscard]] QBindable bindableRunInTerminal() const { return &this->bRunInTerminal; } + [[nodiscard]] QBindable> bindableCategories() const { + return &this->bCategories; + } + [[nodiscard]] QBindable> bindableKeywords() const { return &this->bKeywords; } + // currently ignores all field codes. static QVector parseExecString(const QString& execString); static void doExec(const QList& execString, const QString& workingDirectory); +signals: + void nameChanged(); + void genericNameChanged(); + void startupClassChanged(); + void noDisplayChanged(); + void commentChanged(); + void iconChanged(); + void execStringChanged(); + void commandChanged(); + void workingDirectoryChanged(); + void runInTerminalChanged(); + void categoriesChanged(); + void keywordsChanged(); + public: QString mId; - QString mName; - QString mGenericName; - bool mNoDisplay = false; - QString mComment; - QString mIcon; - QString mExecString; - QVector mCommand; - QString mWorkingDirectory; - bool mTerminal = false; - QVector mCategories; - QVector mKeywords; + + // clang-format off + Q_OBJECT_BINDABLE_PROPERTY(DesktopEntry, QString, bName, &DesktopEntry::nameChanged); + Q_OBJECT_BINDABLE_PROPERTY(DesktopEntry, QString, bGenericName, &DesktopEntry::genericNameChanged); + Q_OBJECT_BINDABLE_PROPERTY(DesktopEntry, QString, bStartupClass, &DesktopEntry::startupClassChanged); + Q_OBJECT_BINDABLE_PROPERTY(DesktopEntry, bool, bNoDisplay, &DesktopEntry::noDisplayChanged); + Q_OBJECT_BINDABLE_PROPERTY(DesktopEntry, QString, bComment, &DesktopEntry::commentChanged); + Q_OBJECT_BINDABLE_PROPERTY(DesktopEntry, QString, bIcon, &DesktopEntry::iconChanged); + Q_OBJECT_BINDABLE_PROPERTY(DesktopEntry, QString, bExecString, &DesktopEntry::execStringChanged); + Q_OBJECT_BINDABLE_PROPERTY(DesktopEntry, QVector, bCommand, &DesktopEntry::commandChanged); + Q_OBJECT_BINDABLE_PROPERTY(DesktopEntry, QString, bWorkingDirectory, &DesktopEntry::workingDirectoryChanged); + Q_OBJECT_BINDABLE_PROPERTY(DesktopEntry, bool, bRunInTerminal, &DesktopEntry::runInTerminalChanged); + Q_OBJECT_BINDABLE_PROPERTY(DesktopEntry, QVector, bCategories, &DesktopEntry::categoriesChanged); + Q_OBJECT_BINDABLE_PROPERTY(DesktopEntry, QVector, bKeywords, &DesktopEntry::keywordsChanged); + // clang-format on private: - QHash mEntries; - QHash mActions; + void updateActions(const QVector& newActions); + + ParsedDesktopEntryData state; + QVector mActions; friend class DesktopAction; }; @@ -102,12 +176,13 @@ private: class DesktopAction: public QObject { Q_OBJECT; Q_PROPERTY(QString id MEMBER mId CONSTANT); - Q_PROPERTY(QString name MEMBER mName CONSTANT); - Q_PROPERTY(QString icon MEMBER mIcon CONSTANT); + // clang-format off + Q_PROPERTY(QString name READ default WRITE default NOTIFY nameChanged BINDABLE bindableName); + Q_PROPERTY(QString icon READ default WRITE default NOTIFY iconChanged BINDABLE bindableIcon); /// The raw `Exec` string from the action. /// /// > [!WARNING] This cannot be reliably run as a command. See @@command for one you can run. - Q_PROPERTY(QString execString MEMBER mExecString CONSTANT); + Q_PROPERTY(QString execString READ default WRITE default NOTIFY execStringChanged BINDABLE bindableExecString); /// The parsed `Exec` command in the action. /// /// The entry can be run with @@execute(), or by using this command in @@ -116,7 +191,8 @@ class DesktopAction: public QObject { /// the invoked process. /// /// > [!NOTE] The provided command does not invoke a terminal even if @@runInTerminal is true. - Q_PROPERTY(QVector command MEMBER mCommand CONSTANT); + Q_PROPERTY(QVector command READ default WRITE default NOTIFY commandChanged BINDABLE bindableCommand); + // clang-format on QML_ELEMENT; QML_UNCREATABLE("DesktopAction instances must be retrieved from a DesktopEntry"); @@ -132,18 +208,47 @@ public: /// and @@DesktopEntry.workingDirectory. Q_INVOKABLE void execute() const; + [[nodiscard]] QBindable bindableName() const { return &this->bName; } + [[nodiscard]] QBindable bindableIcon() const { return &this->bIcon; } + [[nodiscard]] QBindable bindableExecString() const { return &this->bExecString; } + [[nodiscard]] QBindable> bindableCommand() const { return &this->bCommand; } + +signals: + void nameChanged(); + void iconChanged(); + void execStringChanged(); + void commandChanged(); + private: DesktopEntry* entry; QString mId; - QString mName; - QString mIcon; - QString mExecString; - QVector mCommand; QHash mEntries; + // clang-format off + Q_OBJECT_BINDABLE_PROPERTY(DesktopAction, QString, bName, &DesktopAction::nameChanged); + Q_OBJECT_BINDABLE_PROPERTY(DesktopAction, QString, bIcon, &DesktopAction::iconChanged); + Q_OBJECT_BINDABLE_PROPERTY(DesktopAction, QString, bExecString, &DesktopAction::execStringChanged); + Q_OBJECT_BINDABLE_PROPERTY(DesktopAction, QVector, bCommand, &DesktopAction::commandChanged); + // clang-format on + friend class DesktopEntry; }; +class DesktopEntryManager; + +class DesktopEntryScanner: public QRunnable { +public: + explicit DesktopEntryScanner(DesktopEntryManager* manager); + + void run() override; + // clang-format off + void scanDirectory(const QDir& dir, const QString& idPrefix, QList& entries); + // clang-format on + +private: + DesktopEntryManager* manager; +}; + class DesktopEntryManager: public QObject { Q_OBJECT; @@ -151,20 +256,32 @@ public: void scanDesktopEntries(); [[nodiscard]] DesktopEntry* byId(const QString& id); + [[nodiscard]] DesktopEntry* heuristicLookup(const QString& name); [[nodiscard]] ObjectModel* applications(); static DesktopEntryManager* instance(); + static const QStringList& desktopPaths(); + +signals: + void applicationsChanged(); + +private slots: + void handleFileChanges(); + void onScanCompleted(const QList& scanResults); + private: explicit DesktopEntryManager(); - void populateApplications(); - void scanPath(const QDir& dir, const QString& prefix = QString()); - QHash desktopEntries; QHash lowercaseDesktopEntries; ObjectModel mApplications {this}; + DesktopEntryMonitor* monitor = nullptr; + bool scanInProgress = false; + bool scanQueued = false; + + friend class DesktopEntryScanner; }; ///! Desktop entry index. @@ -186,7 +303,17 @@ public: explicit DesktopEntries(); /// Look up a desktop entry by name. Includes NoDisplay entries. May return null. + /// + /// While this function requires an exact match, @@heuristicLookup() will correctly + /// find an entry more often and is generally more useful. Q_INVOKABLE [[nodiscard]] static DesktopEntry* byId(const QString& id); + /// Look up a desktop entry by name using heuristics. Unlike @@byId(), + /// if no exact matches are found this function will try to guess - potentially incorrectly. + /// May return null. + Q_INVOKABLE [[nodiscard]] static DesktopEntry* heuristicLookup(const QString& name); [[nodiscard]] static ObjectModel* applications(); + +signals: + void applicationsChanged(); }; diff --git a/src/core/desktopentrymonitor.cpp b/src/core/desktopentrymonitor.cpp new file mode 100644 index 0000000..bed6ef1 --- /dev/null +++ b/src/core/desktopentrymonitor.cpp @@ -0,0 +1,68 @@ +#include "desktopentrymonitor.hpp" + +#include +#include +#include +#include +#include +#include + +#include "desktopentry.hpp" + +namespace { +void addPathAndParents(QFileSystemWatcher& watcher, const QString& path) { + watcher.addPath(path); + + auto p = QFileInfo(path).absolutePath(); + while (!p.isEmpty()) { + watcher.addPath(p); + const auto parent = QFileInfo(p).dir().absolutePath(); + if (parent == p) break; + p = parent; + } +} +} // namespace + +DesktopEntryMonitor::DesktopEntryMonitor(QObject* parent): QObject(parent) { + this->debounceTimer.setSingleShot(true); + this->debounceTimer.setInterval(100); + + QObject::connect( + &this->watcher, + &QFileSystemWatcher::directoryChanged, + this, + &DesktopEntryMonitor::onDirectoryChanged + ); + QObject::connect( + &this->debounceTimer, + &QTimer::timeout, + this, + &DesktopEntryMonitor::processChanges + ); + + this->startMonitoring(); +} + +void DesktopEntryMonitor::startMonitoring() { + for (const auto& path: DesktopEntryManager::desktopPaths()) { + if (!QDir(path).exists()) continue; + addPathAndParents(this->watcher, path); + this->scanAndWatch(path); + } +} + +void DesktopEntryMonitor::scanAndWatch(const QString& dirPath) { + auto dir = QDir(dirPath); + if (!dir.exists()) return; + + this->watcher.addPath(dirPath); + + auto subdirs = dir.entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot | QDir::NoSymLinks); + for (const auto& subdir: subdirs) this->watcher.addPath(subdir.absoluteFilePath()); +} + +void DesktopEntryMonitor::onDirectoryChanged(const QString& /*path*/) { + this->debounceTimer.start(); +} + +void DesktopEntryMonitor::processChanges() { emit this->desktopEntriesChanged(); } \ No newline at end of file diff --git a/src/core/desktopentrymonitor.hpp b/src/core/desktopentrymonitor.hpp new file mode 100644 index 0000000..eb3251d --- /dev/null +++ b/src/core/desktopentrymonitor.hpp @@ -0,0 +1,32 @@ +#pragma once + +#include +#include +#include +#include + +class DesktopEntryMonitor: public QObject { + Q_OBJECT + +public: + explicit DesktopEntryMonitor(QObject* parent = nullptr); + ~DesktopEntryMonitor() override = default; + DesktopEntryMonitor(const DesktopEntryMonitor&) = delete; + DesktopEntryMonitor& operator=(const DesktopEntryMonitor&) = delete; + DesktopEntryMonitor(DesktopEntryMonitor&&) = delete; + DesktopEntryMonitor& operator=(DesktopEntryMonitor&&) = delete; + +signals: + void desktopEntriesChanged(); + +private slots: + void onDirectoryChanged(const QString& path); + void processChanges(); + +private: + void startMonitoring(); + void scanAndWatch(const QString& dirPath); + + QFileSystemWatcher watcher; + QTimer debounceTimer; +}; diff --git a/src/core/generation.cpp b/src/core/generation.cpp index 90a2939..c68af71 100644 --- a/src/core/generation.cpp +++ b/src/core/generation.cpp @@ -11,12 +11,12 @@ #include #include #include -#include #include #include #include #include #include +#include #include #include "iconimageprovider.hpp" @@ -49,7 +49,8 @@ EngineGeneration::EngineGeneration(const QDir& rootPath, QmlScanner scanner) this->engine->addImportPath("qs:@/"); this->engine->setNetworkAccessManagerFactory(&this->interceptNetFactory); - this->engine->setIncubationController(&this->delayedIncubationController); + this->incubationController.initLoop(); + this->engine->setIncubationController(&this->incubationController); this->engine->addImageProvider("icon", new IconImageProvider()); this->engine->addImageProvider("qsimage", new QsImageProvider()); @@ -134,7 +135,7 @@ void EngineGeneration::onReload(EngineGeneration* old) { // new generation acquires it then incubators will hang intermittently qCDebug(logIncubator) << "Locking incubation controllers of old generation" << old; old->incubationControllersLocked = true; - old->assignIncubationController(); + old->updateIncubationMode(); } QObject::connect(this->engine, &QQmlEngine::quit, this, &EngineGeneration::quit); @@ -161,8 +162,9 @@ void EngineGeneration::postReload() { if (this->engine == nullptr || this->root == nullptr) return; QsEnginePlugin::runOnReload(); - PostReloadHook::postReloadTree(this->root); - this->singletonRegistry.onPostReload(); + + emit this->firePostReload(); + QObject::disconnect(this, &EngineGeneration::firePostReload, nullptr, nullptr); } void EngineGeneration::setWatchingFiles(bool watching) { @@ -222,6 +224,11 @@ void EngineGeneration::onFileChanged(const QString& name) { if (!this->watcher->files().contains(name)) { this->deletedWatchedFiles.push_back(name); } else { + // some editors (e.g vscode) perform file saving in two steps: truncate + write + // ignore the first event (truncate) with size 0 to prevent incorrect live reloading + auto fileInfo = QFileInfo(name); + if (fileInfo.isFile() && fileInfo.size() == 0) return; + emit this->filesChanged(); } } @@ -236,90 +243,6 @@ void EngineGeneration::onDirectoryChanged() { } } -void EngineGeneration::registerIncubationController(QQmlIncubationController* controller) { - // We only want controllers that we can swap out if destroyed. - // This happens if the window owning the active controller dies. - auto* obj = dynamic_cast(controller); - if (!obj) { - qCWarning(logIncubator) << "Could not register incubation controller as it is not a QObject" - << controller; - - return; - } - - QObject::connect( - obj, - &QObject::destroyed, - this, - &EngineGeneration::incubationControllerDestroyed, - Qt::UniqueConnection - ); - - this->incubationControllers.push_back(obj); - qCDebug(logIncubator) << "Registered incubation controller" << obj << "to generation" << this; - - // This function can run during destruction. - if (this->engine == nullptr) return; - - if (this->engine->incubationController() == &this->delayedIncubationController) { - this->assignIncubationController(); - } -} - -// Multiple controllers may be destroyed at once. Dynamic casts must be performed before working -// with any controllers. The QQmlIncubationController destructor will already have run by the -// point QObject::destroyed is called, so we can't cast to that. -void EngineGeneration::deregisterIncubationController(QQmlIncubationController* controller) { - auto* obj = dynamic_cast(controller); - if (!obj) { - qCCritical(logIncubator) << "Deregistering incubation controller which is not a QObject, " - "however only QObject controllers should be registered."; - } - - QObject::disconnect(obj, nullptr, this, nullptr); - - if (this->incubationControllers.removeOne(obj)) { - qCDebug(logIncubator) << "Deregistered incubation controller" << obj << "from" << this; - } else { - qCCritical(logIncubator) << "Failed to deregister incubation controller" << obj << "from" - << this << "as it was not registered to begin with"; - qCCritical(logIncubator) << "Current registered incuabation controllers" - << this->incubationControllers; - } - - // This function can run during destruction. - if (this->engine == nullptr) return; - - if (this->engine->incubationController() == controller) { - qCDebug(logIncubator - ) << "Destroyed incubation controller was currently active, reassigning from pool"; - this->assignIncubationController(); - } -} - -void EngineGeneration::incubationControllerDestroyed() { - auto* sender = this->sender(); - - if (this->incubationControllers.removeAll(sender) != 0) { - qCDebug(logIncubator) << "Destroyed incubation controller" << sender << "deregistered from" - << this; - } else { - qCCritical(logIncubator) << "Destroyed incubation controller" << sender - << "was not registered, but its destruction was observed by" << this; - - return; - } - - // This function can run during destruction. - if (this->engine == nullptr) return; - - if (dynamic_cast(this->engine->incubationController()) == sender) { - qCDebug(logIncubator - ) << "Destroyed incubation controller was currently active, reassigning from pool"; - this->assignIncubationController(); - } -} - void EngineGeneration::onEngineWarnings(const QList& warnings) { for (const auto& error: warnings) { const auto& url = error.url(); @@ -361,20 +284,23 @@ void EngineGeneration::exit(int code) { this->destroy(); } -void EngineGeneration::assignIncubationController() { - QQmlIncubationController* controller = nullptr; +void EngineGeneration::trackWindowIncubationController(QQuickWindow* window) { + if (this->trackedWindows.contains(window)) return; - if (this->incubationControllersLocked || this->incubationControllers.isEmpty()) { - controller = &this->delayedIncubationController; - } else { - controller = dynamic_cast(this->incubationControllers.first()); - } + QObject::connect(window, &QObject::destroyed, this, &EngineGeneration::onTrackedWindowDestroyed); + this->trackedWindows.append(window); + this->updateIncubationMode(); +} - qCDebug(logIncubator) << "Assigning incubation controller" << controller << "to generation" - << this - << "fallback:" << (controller == &this->delayedIncubationController); +void EngineGeneration::onTrackedWindowDestroyed(QObject* object) { + this->trackedWindows.removeAll(static_cast(object)); // NOLINT + this->updateIncubationMode(); +} - this->engine->setIncubationController(controller); +void EngineGeneration::updateIncubationMode() { + // If we're in a situation with only hidden but tracked windows this might be wrong, + // but it seems to at least work. + this->incubationController.setIncubationMode(!this->trackedWindows.empty()); } EngineGeneration* EngineGeneration::currentGeneration() { diff --git a/src/core/generation.hpp b/src/core/generation.hpp index 5d3c5c6..4543408 100644 --- a/src/core/generation.hpp +++ b/src/core/generation.hpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include "incubator.hpp" @@ -40,8 +41,7 @@ public: void setWatchingFiles(bool watching); bool setExtraWatchedFiles(const QVector& files); - void registerIncubationController(QQmlIncubationController* controller); - void deregisterIncubationController(QQmlIncubationController* controller); + void trackWindowIncubationController(QQuickWindow* window); // takes ownership void registerExtension(const void* key, EngineGenerationExt* extension); @@ -65,7 +65,7 @@ public: QFileSystemWatcher* watcher = nullptr; QVector deletedWatchedFiles; QVector extraWatchedFiles; - DelayedQmlIncubationController delayedIncubationController; + QsIncubationController incubationController; bool reloadComplete = false; QuickshellGlobal* qsgInstance = nullptr; @@ -75,6 +75,7 @@ public: signals: void filesChanged(); void reloadFinished(); + void firePostReload(); public slots: void quit(); @@ -83,13 +84,13 @@ public slots: private slots: void onFileChanged(const QString& name); void onDirectoryChanged(); - void incubationControllerDestroyed(); + void onTrackedWindowDestroyed(QObject* object); static void onEngineWarnings(const QList& warnings); private: void postReload(); - void assignIncubationController(); - QVector incubationControllers; + void updateIncubationMode(); + QVector trackedWindows; bool incubationControllersLocked = false; QHash extensions; diff --git a/src/core/iconimageprovider.cpp b/src/core/iconimageprovider.cpp index 43e00fd..1dbe3e7 100644 --- a/src/core/iconimageprovider.cpp +++ b/src/core/iconimageprovider.cpp @@ -19,8 +19,7 @@ IconImageProvider::requestPixmap(const QString& id, QSize* size, const QSize& re if (splitIdx != -1) { iconName = id.sliced(0, splitIdx); path = id.sliced(splitIdx + 6); - qWarning() << "Searching custom icon paths is not yet supported. Icon path will be ignored for" - << id; + path = QString("/%1/%2").arg(path, iconName.sliced(iconName.lastIndexOf('/') + 1)); } else { splitIdx = id.indexOf("?fallback="); if (splitIdx != -1) { @@ -32,7 +31,8 @@ IconImageProvider::requestPixmap(const QString& id, QSize* size, const QSize& re } auto icon = QIcon::fromTheme(iconName); - if (icon.isNull()) icon = QIcon::fromTheme(fallbackName); + if (icon.isNull() && !fallbackName.isEmpty()) icon = QIcon::fromTheme(fallbackName); + if (icon.isNull() && !path.isEmpty()) icon = QPixmap(path); auto targetSize = requestedSize.isValid() ? requestedSize : QSize(100, 100); if (targetSize.width() == 0 || targetSize.height() == 0) targetSize = QSize(2, 2); diff --git a/src/core/iconprovider.cpp b/src/core/iconprovider.cpp index 99b423e..383f7e1 100644 --- a/src/core/iconprovider.cpp +++ b/src/core/iconprovider.cpp @@ -22,8 +22,8 @@ class PixmapCacheIconEngine: public QIconEngine { QIcon::Mode /*unused*/, QIcon::State /*unused*/ ) override { - qFatal( - ) << "Unexpected icon paint request bypassed pixmap method. Please report this as a bug."; + qFatal() + << "Unexpected icon paint request bypassed pixmap method. Please report this as a bug."; } QPixmap pixmap(const QSize& size, QIcon::Mode /*unused*/, QIcon::State /*unused*/) override { diff --git a/src/core/incubator.cpp b/src/core/incubator.cpp index c9d149a..f031b11 100644 --- a/src/core/incubator.cpp +++ b/src/core/incubator.cpp @@ -1,7 +1,16 @@ #include "incubator.hpp" +#include +#include +#include #include +#include +#include +#include +#include +#include #include +#include #include #include "logcat.hpp" @@ -15,3 +24,112 @@ void QsQmlIncubator::statusChanged(QQmlIncubator::Status status) { default: break; } } + +void QsIncubationController::initLoop() { + auto* app = static_cast(QGuiApplication::instance()); // NOLINT + this->renderLoop = QSGRenderLoop::instance(); + + QObject::connect( + app, + &QGuiApplication::screenAdded, + this, + &QsIncubationController::updateIncubationTime + ); + + QObject::connect( + app, + &QGuiApplication::screenRemoved, + this, + &QsIncubationController::updateIncubationTime + ); + + this->updateIncubationTime(); + + QObject::connect( + this->renderLoop, + &QSGRenderLoop::timeToIncubate, + this, + &QsIncubationController::incubate + ); + + QAnimationDriver* animationDriver = this->renderLoop->animationDriver(); + if (animationDriver) { + QObject::connect( + animationDriver, + &QAnimationDriver::stopped, + this, + &QsIncubationController::animationStopped + ); + } else { + qCInfo(logIncubator) << "Render loop does not have animation driver, animationStopped cannot " + "be used to trigger incubation."; + } +} + +void QsIncubationController::setIncubationMode(bool render) { + if (render == this->followRenderloop) return; + this->followRenderloop = render; + + if (render) { + qCDebug(logIncubator) << "Incubation mode changed: render loop driven"; + } else { + qCDebug(logIncubator) << "Incubation mode changed: event loop driven"; + } + + if (!render && this->incubatingObjectCount()) this->incubateLater(); +} + +void QsIncubationController::timerEvent(QTimerEvent* /*event*/) { + this->killTimer(this->timerId); + this->timerId = 0; + this->incubate(); +} + +void QsIncubationController::incubateLater() { + if (this->followRenderloop) { + if (this->timerId != 0) { + this->killTimer(this->timerId); + this->timerId = 0; + } + + // Incubate again at the end of the event processing queue + QMetaObject::invokeMethod(this, &QsIncubationController::incubate, Qt::QueuedConnection); + } else if (this->timerId == 0) { + // Wait for a while before processing the next batch. Using a + // timer to avoid starvation of system events. + this->timerId = this->startTimer(this->incubationTime); + } +} + +void QsIncubationController::incubate() { + if ((!this->followRenderloop || this->renderLoop) && this->incubatingObjectCount()) { + if (!this->followRenderloop) { + this->incubateFor(10); + if (this->incubatingObjectCount()) this->incubateLater(); + } else if (this->renderLoop->interleaveIncubation()) { + this->incubateFor(this->incubationTime); + } else { + this->incubateFor(this->incubationTime * 2); + if (this->incubatingObjectCount()) this->incubateLater(); + } + } +} + +void QsIncubationController::animationStopped() { this->incubate(); } + +void QsIncubationController::incubatingObjectCountChanged(int count) { + if (count + && (!this->followRenderloop + || (this->renderLoop && !this->renderLoop->interleaveIncubation()))) + { + this->incubateLater(); + } +} + +void QsIncubationController::updateIncubationTime() { + auto* screen = QGuiApplication::primaryScreen(); + if (!screen) return; + + // 1/3 frame on primary screen + this->incubationTime = qMax(1, static_cast(1000 / screen->refreshRate() / 3)); +} diff --git a/src/core/incubator.hpp b/src/core/incubator.hpp index 5ebb9a0..15dc49a 100644 --- a/src/core/incubator.hpp +++ b/src/core/incubator.hpp @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include @@ -25,7 +26,37 @@ signals: void failed(); }; -class DelayedQmlIncubationController: public QQmlIncubationController { - // Do nothing. - // This ensures lazy loaders don't start blocking before onReload creates windows. +class QSGRenderLoop; + +class QsIncubationController + : public QObject + , public QQmlIncubationController { + Q_OBJECT + +public: + void initLoop(); + void setIncubationMode(bool render); + void incubateLater(); + +protected: + void timerEvent(QTimerEvent* event) override; + +public slots: + void incubate(); + void animationStopped(); + void updateIncubationTime(); + +protected: + void incubatingObjectCountChanged(int count) override; + +private: +// QPointer did not work with forward declarations prior to 6.7 +#if QT_VERSION >= QT_VERSION_CHECK(6, 7, 0) + QPointer renderLoop = nullptr; +#else + QSGRenderLoop* renderLoop = nullptr; +#endif + int incubationTime = 0; + int timerId = 0; + bool followRenderloop = false; }; diff --git a/src/core/instanceinfo.cpp b/src/core/instanceinfo.cpp index 7f0132b..1f71b8a 100644 --- a/src/core/instanceinfo.cpp +++ b/src/core/instanceinfo.cpp @@ -3,12 +3,14 @@ #include QDataStream& operator<<(QDataStream& stream, const InstanceInfo& info) { - stream << info.instanceId << info.configPath << info.shellId << info.launchTime << info.pid; + stream << info.instanceId << info.configPath << info.shellId << info.launchTime << info.pid + << info.display; return stream; } QDataStream& operator>>(QDataStream& stream, InstanceInfo& info) { - stream >> info.instanceId >> info.configPath >> info.shellId >> info.launchTime >> info.pid; + stream >> info.instanceId >> info.configPath >> info.shellId >> info.launchTime >> info.pid + >> info.display; return stream; } diff --git a/src/core/instanceinfo.hpp b/src/core/instanceinfo.hpp index 98ce614..977e4c2 100644 --- a/src/core/instanceinfo.hpp +++ b/src/core/instanceinfo.hpp @@ -11,6 +11,7 @@ struct InstanceInfo { QString shellId; QDateTime launchTime; pid_t pid = -1; + QString display; static InstanceInfo CURRENT; // NOLINT }; @@ -34,6 +35,8 @@ namespace qs::crash { struct CrashInfo { int logFd = -1; + int traceFd = -1; + int infoFd = -1; static CrashInfo INSTANCE; // NOLINT }; diff --git a/src/core/lazyloader.hpp b/src/core/lazyloader.hpp index dbaad4b..56cc964 100644 --- a/src/core/lazyloader.hpp +++ b/src/core/lazyloader.hpp @@ -82,9 +82,6 @@ /// > Notably, @@Variants does not corrently support asynchronous /// > loading, meaning using it inside a LazyLoader will block similarly to not /// > having a loader to start with. -/// -/// > [!WARNING] LazyLoaders do not start loading before the first window is created, -/// > meaning if you create all windows inside of lazy loaders, none of them will ever load. class LazyLoader: public Reloadable { Q_OBJECT; /// The fully loaded item if the loader is @@loading or @@active, or `null` diff --git a/src/core/logging.cpp b/src/core/logging.cpp index 7f95e46..d24225b 100644 --- a/src/core/logging.cpp +++ b/src/core/logging.cpp @@ -27,7 +27,10 @@ #include #include #include +#ifdef __linux__ #include +#include +#endif #include "instanceinfo.hpp" #include "logcat.hpp" @@ -43,6 +46,57 @@ using namespace qt_logging_registry; QS_LOGGING_CATEGORY(logLogging, "quickshell.logging", QtWarningMsg); +namespace { +bool copyFileData(int sourceFd, int destFd, qint64 size) { + auto usize = static_cast(size); + +#ifdef __linux__ + off_t offset = 0; + auto remaining = usize; + + while (remaining > 0) { + auto r = sendfile(destFd, sourceFd, &offset, remaining); + if (r == -1) { + if (errno == EINTR) continue; + return false; + } + if (r == 0) break; + remaining -= static_cast(r); + } + + return true; +#else + std::array buffer = {}; + auto remaining = totalTarget; + + while (remaining > 0) { + auto chunk = std::min(remaining, buffer.size()); + auto r = ::read(sourceFd, buffer.data(), chunk); + if (r == -1) { + if (errno == EINTR) continue; + return false; + } + if (r == 0) break; + + auto readBytes = static_cast(r); + size_t written = 0; + while (written < readBytes) { + auto w = ::write(destFd, buffer.data() + written, readBytes - written); + if (w == -1) { + if (errno == EINTR) continue; + return false; + } + written += static_cast(w); + } + + remaining -= readBytes; + } + + return true; +#endif +} +} // namespace + bool LogMessage::operator==(const LogMessage& other) const { // note: not including time return this->type == other.type && this->category == other.category && this->body == other.body; @@ -313,8 +367,12 @@ void ThreadLogging::init() { if (logMfd != -1) { this->file = new QFile(); - this->file->open(logMfd, QFile::ReadWrite, QFile::AutoCloseHandle); - this->fileStream.setDevice(this->file); + + if (this->file->open(logMfd, QFile::ReadWrite, QFile::AutoCloseHandle)) { + this->fileStream.setDevice(this->file); + } else { + qCCritical(logLogging) << "Failed to open early logging memfd."; + } } if (dlogMfd != -1) { @@ -322,14 +380,19 @@ void ThreadLogging::init() { this->detailedFile = new QFile(); // buffered by WriteBuffer - this->detailedFile->open(dlogMfd, QFile::ReadWrite | QFile::Unbuffered, QFile::AutoCloseHandle); - this->detailedWriter.setDevice(this->detailedFile); + if (this->detailedFile + ->open(dlogMfd, QFile::ReadWrite | QFile::Unbuffered, QFile::AutoCloseHandle)) + { + this->detailedWriter.setDevice(this->detailedFile); - if (!this->detailedWriter.writeHeader()) { - qCCritical(logLogging) << "Could not write header for detailed logs."; - this->detailedWriter.setDevice(nullptr); - delete this->detailedFile; - this->detailedFile = nullptr; + if (!this->detailedWriter.writeHeader()) { + qCCritical(logLogging) << "Could not write header for detailed logs."; + this->detailedWriter.setDevice(nullptr); + delete this->detailedFile; + this->detailedFile = nullptr; + } + } else { + qCCritical(logLogging) << "Failed to open early detailed logging memfd."; } } @@ -352,7 +415,8 @@ void ThreadLogging::initFs() { auto* runDir = QsPaths::instance()->instanceRunDir(); if (!runDir) { - qCCritical(logLogging + qCCritical( + logLogging ) << "Could not start filesystem logging as the runtime directory could not be created."; return; } @@ -363,7 +427,8 @@ void ThreadLogging::initFs() { auto* detailedFile = new QFile(detailedPath); if (!file->open(QFile::ReadWrite | QFile::Truncate)) { - qCCritical(logLogging + qCCritical( + logLogging ) << "Could not start filesystem logger as the log file could not be created:" << path; delete file; @@ -374,13 +439,14 @@ void ThreadLogging::initFs() { // buffered by WriteBuffer if (!detailedFile->open(QFile::ReadWrite | QFile::Truncate | QFile::Unbuffered)) { - qCCritical(logLogging + qCCritical( + logLogging ) << "Could not start detailed filesystem logger as the log file could not be created:" << detailedPath; delete detailedFile; detailedFile = nullptr; } else { - auto lock = flock { + struct flock lock = { .l_type = F_WRLCK, .l_whence = SEEK_SET, .l_start = 0, @@ -402,7 +468,11 @@ void ThreadLogging::initFs() { auto* oldFile = this->file; if (oldFile) { oldFile->seek(0); - sendfile(file->handle(), oldFile->handle(), nullptr, oldFile->size()); + + if (!copyFileData(oldFile->handle(), file->handle(), oldFile->size())) { + qCritical(logLogging) << "Failed to copy log from memfd with error code " << errno + << qt_error_string(errno); + } } this->file = file; @@ -414,7 +484,10 @@ void ThreadLogging::initFs() { auto* oldFile = this->detailedFile; if (oldFile) { oldFile->seek(0); - sendfile(detailedFile->handle(), oldFile->handle(), nullptr, oldFile->size()); + if (!copyFileData(oldFile->handle(), detailedFile->handle(), oldFile->size())) { + qCritical(logLogging) << "Failed to copy detailed log from memfd with error code " << errno + << qt_error_string(errno); + } } crash::CrashInfo::INSTANCE.logFd = detailedFile->handle(); @@ -457,10 +530,14 @@ void ThreadLogging::onMessage(const LogMessage& msg, bool showInSparse) { this->fileStream << Qt::endl; } - if (this->detailedWriter.write(msg)) { - this->detailedFile->flush(); - } else if (this->detailedFile != nullptr) { - qCCritical(logLogging) << "Detailed logger failed to write. Ending detailed logs."; + if (!this->detailedWriter.write(msg) || (this->detailedFile && !this->detailedFile->flush())) { + this->detailedWriter.setDevice(nullptr); + + if (this->detailedFile) { + this->detailedFile->close(); + this->detailedFile = nullptr; + qCCritical(logLogging) << "Detailed logger failed to write. Ending detailed logs."; + } } } @@ -733,11 +810,11 @@ bool EncodedLogReader::readVarInt(quint32* slot) { if (!this->reader.skip(1)) return false; *slot = qFromLittleEndian(n); } else if ((bytes[1] != 0xff || bytes[2] != 0xff) && readLength >= 3) { - auto n = *reinterpret_cast(bytes.data() + 1); + auto n = *reinterpret_cast(bytes.data() + 1); // NOLINT if (!this->reader.skip(3)) return false; *slot = qFromLittleEndian(n); } else if (readLength == 7) { - auto n = *reinterpret_cast(bytes.data() + 3); + auto n = *reinterpret_cast(bytes.data() + 3); // NOLINT if (!this->reader.skip(7)) return false; *slot = qFromLittleEndian(n); } else return false; @@ -873,7 +950,7 @@ bool LogReader::continueReading() { } void LogFollower::FcntlWaitThread::run() { - auto lock = flock { + struct flock lock = { .l_type = F_RDLCK, // won't block other read locks when we take it .l_whence = SEEK_SET, .l_start = 0, diff --git a/src/core/model.cpp b/src/core/model.cpp index 165c606..47ef060 100644 --- a/src/core/model.cpp +++ b/src/core/model.cpp @@ -1,81 +1,14 @@ #include "model.hpp" -#include +#include #include #include -#include -#include -#include -#include -#include - -qint32 UntypedObjectModel::rowCount(const QModelIndex& parent) const { - if (parent != QModelIndex()) return 0; - return static_cast(this->valuesList.length()); -} - -QVariant UntypedObjectModel::data(const QModelIndex& index, qint32 role) const { - if (role != Qt::UserRole) return QVariant(); - return QVariant::fromValue(this->valuesList.at(index.row())); -} QHash UntypedObjectModel::roleNames() const { return {{Qt::UserRole, "modelData"}}; } -void UntypedObjectModel::insertObject(QObject* object, qsizetype index) { - auto iindex = index == -1 ? this->valuesList.length() : index; - emit this->objectInsertedPre(object, iindex); - - auto intIndex = static_cast(iindex); - this->beginInsertRows(QModelIndex(), intIndex, intIndex); - this->valuesList.insert(iindex, object); - this->endInsertRows(); - - emit this->valuesChanged(); - emit this->objectInsertedPost(object, iindex); -} - -void UntypedObjectModel::removeAt(qsizetype index) { - auto* object = this->valuesList.at(index); - emit this->objectRemovedPre(object, index); - - auto intIndex = static_cast(index); - this->beginRemoveRows(QModelIndex(), intIndex, intIndex); - this->valuesList.removeAt(index); - this->endRemoveRows(); - - emit this->valuesChanged(); - emit this->objectRemovedPost(object, index); -} - -bool UntypedObjectModel::removeObject(const QObject* object) { - auto index = this->valuesList.indexOf(object); - if (index == -1) return false; - - this->removeAt(index); - return true; -} - -void UntypedObjectModel::diffUpdate(const QVector& newValues) { - for (qsizetype i = 0; i < this->valuesList.length();) { - if (newValues.contains(this->valuesList.at(i))) i++; - else this->removeAt(i); - } - - qsizetype oi = 0; - for (auto* object: newValues) { - if (this->valuesList.length() == oi || this->valuesList.at(oi) != object) { - this->insertObject(object, oi); - } - - oi++; - } -} - -qsizetype UntypedObjectModel::indexOf(QObject* object) { return this->valuesList.indexOf(object); } - UntypedObjectModel* UntypedObjectModel::emptyInstance() { - static auto* instance = new UntypedObjectModel(nullptr); // NOLINT + static auto* instance = new ObjectModel(nullptr); return instance; } diff --git a/src/core/model.hpp b/src/core/model.hpp index 6346c96..0e88025 100644 --- a/src/core/model.hpp +++ b/src/core/model.hpp @@ -2,7 +2,7 @@ #include -#include +#include #include #include #include @@ -49,14 +49,11 @@ class UntypedObjectModel: public QAbstractListModel { public: explicit UntypedObjectModel(QObject* parent): QAbstractListModel(parent) {} - [[nodiscard]] qint32 rowCount(const QModelIndex& parent) const override; - [[nodiscard]] QVariant data(const QModelIndex& index, qint32 role) const override; [[nodiscard]] QHash roleNames() const override; - [[nodiscard]] QList values() const { return this->valuesList; }; - void removeAt(qsizetype index); + [[nodiscard]] virtual QList values() = 0; - Q_INVOKABLE qsizetype indexOf(QObject* object); + Q_INVOKABLE virtual qsizetype indexOf(QObject* object) const = 0; static UntypedObjectModel* emptyInstance(); @@ -71,15 +68,6 @@ signals: /// Sent immediately after an object is removed from the list. void objectRemovedPost(QObject* object, qsizetype index); -protected: - void insertObject(QObject* object, qsizetype index = -1); - bool removeObject(const QObject* object); - - // Assumes only one instance of a specific value - void diffUpdate(const QVector& newValues); - - QVector valuesList; - private: static qsizetype valuesCount(QQmlListProperty* property); static QObject* valueAt(QQmlListProperty* property, qsizetype index); @@ -90,14 +78,20 @@ class ObjectModel: public UntypedObjectModel { public: explicit ObjectModel(QObject* parent): UntypedObjectModel(parent) {} - [[nodiscard]] QVector& valueList() { return *std::bit_cast*>(&this->valuesList); } - - [[nodiscard]] const QVector& valueList() const { - return *std::bit_cast*>(&this->valuesList); - } + [[nodiscard]] const QList& valueList() const { return this->mValuesList; } + [[nodiscard]] QList& valueList() { return this->mValuesList; } void insertObject(T* object, qsizetype index = -1) { - this->UntypedObjectModel::insertObject(object, index); + auto iindex = index == -1 ? this->mValuesList.length() : index; + emit this->objectInsertedPre(object, iindex); + + auto intIndex = static_cast(iindex); + this->beginInsertRows(QModelIndex(), intIndex, intIndex); + this->mValuesList.insert(iindex, object); + this->endInsertRows(); + + emit this->valuesChanged(); + emit this->objectInsertedPost(object, iindex); } void insertObjectSorted(T* object, const std::function& compare) { @@ -110,17 +104,71 @@ public: } auto idx = iter - list.begin(); - this->UntypedObjectModel::insertObject(object, idx); + this->insertObject(object, idx); } - void removeObject(const T* object) { this->UntypedObjectModel::removeObject(object); } + bool removeObject(const T* object) { + auto index = this->mValuesList.indexOf(object); + if (index == -1) return false; + + this->removeAt(index); + return true; + } + + void removeAt(qsizetype index) { + auto* object = this->mValuesList.at(index); + emit this->objectRemovedPre(object, index); + + auto intIndex = static_cast(index); + this->beginRemoveRows(QModelIndex(), intIndex, intIndex); + this->mValuesList.removeAt(index); + this->endRemoveRows(); + + emit this->valuesChanged(); + emit this->objectRemovedPost(object, index); + } // Assumes only one instance of a specific value - void diffUpdate(const QVector& newValues) { - this->UntypedObjectModel::diffUpdate(*std::bit_cast*>(&newValues)); + void diffUpdate(const QList& newValues) { + for (qsizetype i = 0; i < this->mValuesList.length();) { + if (newValues.contains(this->mValuesList.at(i))) i++; + else this->removeAt(i); + } + + qsizetype oi = 0; + for (auto* object: newValues) { + if (this->mValuesList.length() == oi || this->mValuesList.at(oi) != object) { + this->insertObject(object, oi); + } + + oi++; + } } static ObjectModel* emptyInstance() { return static_cast*>(UntypedObjectModel::emptyInstance()); } + + [[nodiscard]] qint32 rowCount(const QModelIndex& parent) const override { + if (parent != QModelIndex()) return 0; + return static_cast(this->mValuesList.length()); + } + + [[nodiscard]] QVariant data(const QModelIndex& index, qint32 role) const override { + if (role != Qt::UserRole) return QVariant(); + // Values must be QObject derived, but we can't assert that here without breaking forward decls, + // so no static_cast. + return QVariant::fromValue(reinterpret_cast(this->mValuesList.at(index.row()))); + } + + qsizetype indexOf(QObject* object) const override { + return this->mValuesList.indexOf(reinterpret_cast(object)); + } + + [[nodiscard]] QList values() override { + return *reinterpret_cast*>(&this->mValuesList); + } + +private: + QList mValuesList; }; diff --git a/src/core/module.md b/src/core/module.md index b9404ea..41f065d 100644 --- a/src/core/module.md +++ b/src/core/module.md @@ -21,7 +21,6 @@ headers = [ "model.hpp", "elapsedtimer.hpp", "desktopentry.hpp", - "objectrepeater.hpp", "qsmenu.hpp", "retainable.hpp", "popupanchor.hpp", diff --git a/src/core/objectrepeater.cpp b/src/core/objectrepeater.cpp deleted file mode 100644 index 7971952..0000000 --- a/src/core/objectrepeater.cpp +++ /dev/null @@ -1,190 +0,0 @@ -#include "objectrepeater.hpp" -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -QVariant ObjectRepeater::model() const { return this->mModel; } - -void ObjectRepeater::setModel(QVariant model) { - if (model == this->mModel) return; - - if (this->itemModel != nullptr) { - QObject::disconnect(this->itemModel, nullptr, this, nullptr); - } - - this->mModel = std::move(model); - emit this->modelChanged(); - this->reloadElements(); -} - -void ObjectRepeater::onModelDestroyed() { - this->mModel.clear(); - this->itemModel = nullptr; - emit this->modelChanged(); - this->reloadElements(); -} - -QQmlComponent* ObjectRepeater::delegate() const { return this->mDelegate; } - -void ObjectRepeater::setDelegate(QQmlComponent* delegate) { - if (delegate == this->mDelegate) return; - - if (this->mDelegate != nullptr) { - QObject::disconnect(this->mDelegate, nullptr, this, nullptr); - } - - this->mDelegate = delegate; - - if (delegate != nullptr) { - QObject::connect( - this->mDelegate, - &QObject::destroyed, - this, - &ObjectRepeater::onDelegateDestroyed - ); - } - - emit this->delegateChanged(); - this->reloadElements(); -} - -void ObjectRepeater::onDelegateDestroyed() { - this->mDelegate = nullptr; - emit this->delegateChanged(); - this->reloadElements(); -} - -void ObjectRepeater::reloadElements() { - for (auto i = this->valuesList.length() - 1; i >= 0; i--) { - this->removeComponent(i); - } - - if (this->mDelegate == nullptr || !this->mModel.isValid()) return; - - if (this->mModel.canConvert()) { - auto* model = this->mModel.value(); - this->itemModel = model; - - this->insertModelElements(model, 0, model->rowCount() - 1); // -1 is fine - - // clang-format off - QObject::connect(model, &QObject::destroyed, this, &ObjectRepeater::onModelDestroyed); - QObject::connect(model, &QAbstractItemModel::rowsInserted, this, &ObjectRepeater::onModelRowsInserted); - QObject::connect(model, &QAbstractItemModel::rowsRemoved, this, &ObjectRepeater::onModelRowsRemoved); - QObject::connect(model, &QAbstractItemModel::rowsMoved, this, &ObjectRepeater::onModelRowsMoved); - QObject::connect(model, &QAbstractItemModel::modelAboutToBeReset, this, &ObjectRepeater::onModelAboutToBeReset); - // clang-format on - } else if (this->mModel.canConvert()) { - auto values = this->mModel.value(); - auto len = values.count(); - - for (auto i = 0; i != len; i++) { - this->insertComponent(i, {{"modelData", QVariant::fromValue(values.at(i))}}); - } - } else if (this->mModel.canConvert>()) { - auto values = this->mModel.value>(); - - for (auto& value: values) { - this->insertComponent(this->valuesList.length(), {{"modelData", value}}); - } - } else { - qCritical() << this - << "Cannot create components as the model is not compatible:" << this->mModel; - } -} - -void ObjectRepeater::insertModelElements(QAbstractItemModel* model, int first, int last) { - auto roles = model->roleNames(); - auto roleDataVec = QVector(); - for (auto id: roles.keys()) { - roleDataVec.push_back(QModelRoleData(id)); - } - - auto values = QModelRoleDataSpan(roleDataVec); - auto props = QVariantMap(); - - for (auto i = first; i != last + 1; i++) { - auto index = model->index(i, 0); - model->multiData(index, values); - - for (auto [id, name]: roles.asKeyValueRange()) { - props.insert(name, *values.dataForRole(id)); - } - - this->insertComponent(i, props); - - props.clear(); - } -} - -void ObjectRepeater::onModelRowsInserted(const QModelIndex& parent, int first, int last) { - if (parent != QModelIndex()) return; - - this->insertModelElements(this->itemModel, first, last); -} - -void ObjectRepeater::onModelRowsRemoved(const QModelIndex& parent, int first, int last) { - if (parent != QModelIndex()) return; - - for (auto i = last; i != first - 1; i--) { - this->removeComponent(i); - } -} - -void ObjectRepeater::onModelRowsMoved( - const QModelIndex& sourceParent, - int sourceStart, - int sourceEnd, - const QModelIndex& destParent, - int destStart -) { - auto hasSource = sourceParent != QModelIndex(); - auto hasDest = destParent != QModelIndex(); - - if (!hasSource && !hasDest) return; - - if (hasSource) { - this->onModelRowsRemoved(sourceParent, sourceStart, sourceEnd); - } - - if (hasDest) { - this->onModelRowsInserted(destParent, destStart, destStart + (sourceEnd - sourceStart)); - } -} - -void ObjectRepeater::onModelAboutToBeReset() { - auto last = static_cast(this->valuesList.length() - 1); - this->onModelRowsRemoved(QModelIndex(), 0, last); // -1 is fine -} - -void ObjectRepeater::insertComponent(qsizetype index, const QVariantMap& properties) { - auto* context = QQmlEngine::contextForObject(this); - auto* instance = this->mDelegate->createWithInitialProperties(properties, context); - - if (instance == nullptr) { - qWarning().noquote() << this->mDelegate->errorString(); - qWarning() << this << "failed to create object for model data" << properties; - } else { - QQmlEngine::setObjectOwnership(instance, QQmlEngine::CppOwnership); - instance->setParent(this); - } - - this->insertObject(instance, index); -} - -void ObjectRepeater::removeComponent(qsizetype index) { - auto* instance = this->valuesList.at(index); - this->removeAt(index); - delete instance; -} diff --git a/src/core/objectrepeater.hpp b/src/core/objectrepeater.hpp deleted file mode 100644 index 409b12d..0000000 --- a/src/core/objectrepeater.hpp +++ /dev/null @@ -1,85 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include -#include -#include - -#include "model.hpp" - -///! A Repeater / for loop / map for non Item derived objects. -/// > [!ERROR] Removed in favor of @@QtQml.Models.Instantiator -/// -/// The ObjectRepeater creates instances of the provided delegate for every entry in the -/// given model, similarly to a @@QtQuick.Repeater but for non visual types. -class ObjectRepeater: public ObjectModel { - Q_OBJECT; - /// The model providing data to the ObjectRepeater. - /// - /// Currently accepted model types are `list` lists, javascript arrays, - /// and [QAbstractListModel] derived models, though only one column will be repeated - /// from the latter. - /// - /// Note: @@ObjectModel is a [QAbstractListModel] with a single column. - /// - /// [QAbstractListModel]: https://doc.qt.io/qt-6/qabstractlistmodel.html - Q_PROPERTY(QVariant model READ model WRITE setModel NOTIFY modelChanged); - /// The delegate component to repeat. - /// - /// The delegate is given the same properties as in a Repeater, except `index` which - /// is not currently implemented. - /// - /// If the model is a `list` or javascript array, a `modelData` property will be - /// exposed containing the entry from the model. If the model is a [QAbstractListModel], - /// the roles from the model will be exposed. - /// - /// Note: @@ObjectModel has a single role named `modelData` for compatibility with normal lists. - /// - /// [QAbstractListModel]: https://doc.qt.io/qt-6/qabstractlistmodel.html - Q_PROPERTY(QQmlComponent* delegate READ delegate WRITE setDelegate NOTIFY delegateChanged); - Q_CLASSINFO("DefaultProperty", "delegate"); - QML_ELEMENT; - QML_UNCREATABLE("ObjectRepeater has been removed in favor of QtQml.Models.Instantiator."); - -public: - explicit ObjectRepeater(QObject* parent = nullptr): ObjectModel(parent) {} - - [[nodiscard]] QVariant model() const; - void setModel(QVariant model); - - [[nodiscard]] QQmlComponent* delegate() const; - void setDelegate(QQmlComponent* delegate); - -signals: - void modelChanged(); - void delegateChanged(); - -private slots: - void onDelegateDestroyed(); - void onModelDestroyed(); - void onModelRowsInserted(const QModelIndex& parent, int first, int last); - void onModelRowsRemoved(const QModelIndex& parent, int first, int last); - - void onModelRowsMoved( - const QModelIndex& sourceParent, - int sourceStart, - int sourceEnd, - const QModelIndex& destParent, - int destStart - ); - - void onModelAboutToBeReset(); - -private: - void reloadElements(); - void insertModelElements(QAbstractItemModel* model, int first, int last); - void insertComponent(qsizetype index, const QVariantMap& properties); - void removeComponent(qsizetype index); - - QVariant mModel; - QAbstractItemModel* itemModel = nullptr; - QQmlComponent* mDelegate = nullptr; -}; diff --git a/src/core/paths.cpp b/src/core/paths.cpp index 1f3c494..6555e54 100644 --- a/src/core/paths.cpp +++ b/src/core/paths.cpp @@ -27,12 +27,19 @@ QsPaths* QsPaths::instance() { return instance; } -void QsPaths::init(QString shellId, QString pathId, QString dataOverride, QString stateOverride) { +void QsPaths::init( + QString shellId, + QString pathId, + QString dataOverride, + QString stateOverride, + QString cacheOverride +) { auto* instance = QsPaths::instance(); instance->shellId = std::move(shellId); instance->pathId = std::move(pathId); instance->shellDataOverride = std::move(dataOverride); instance->shellStateOverride = std::move(stateOverride); + instance->shellCacheOverride = std::move(cacheOverride); } QDir QsPaths::crashDir(const QString& id) { @@ -135,13 +142,41 @@ QDir* QsPaths::instanceRunDir() { else return &this->mInstanceRunDir; } +QDir* QsPaths::shellVfsDir() { + if (this->shellVfsState == DirState::Unknown) { + if (auto* baseRunDir = this->baseRunDir()) { + this->mShellVfsDir = QDir(baseRunDir->filePath("vfs")); + this->mShellVfsDir = QDir(this->mShellVfsDir.filePath(this->shellId)); + + qCDebug(logPaths) << "Initialized runtime vfs path:" << this->mShellVfsDir.path(); + + if (!this->mShellVfsDir.mkpath(".")) { + qCCritical(logPaths) << "Could not create runtime vfs directory at" + << this->mShellVfsDir.path(); + this->shellVfsState = DirState::Failed; + } else { + this->shellVfsState = DirState::Ready; + } + } else { + qCCritical(logPaths) << "Could not create shell runtime vfs path as it was not possible to " + "create the base runtime path."; + + this->shellVfsState = DirState::Failed; + } + } + + if (this->shellVfsState == DirState::Failed) return nullptr; + else return &this->mShellVfsDir; +} + void QsPaths::linkRunDir() { if (auto* runDir = this->instanceRunDir()) { auto pidDir = QDir(this->baseRunDir()->filePath("by-pid")); auto* shellDir = this->shellRunDir(); if (!shellDir) { - qCCritical(logPaths + qCCritical( + logPaths ) << "Could not create by-id symlink as the shell runtime path could not be created."; } else { auto shellPath = shellDir->filePath(runDir->dirName()); @@ -289,9 +324,16 @@ QDir QsPaths::shellStateDir() { QDir QsPaths::shellCacheDir() { if (this->shellCacheState == DirState::Unknown) { - auto dir = QDir(QStandardPaths::writableLocation(QStandardPaths::CacheLocation)); - dir = QDir(dir.filePath("by-shell")); - dir = QDir(dir.filePath(this->shellId)); + QDir dir; + if (this->shellCacheOverride.isEmpty()) { + dir = QDir(QStandardPaths::writableLocation(QStandardPaths::CacheLocation)); + dir = QDir(dir.filePath("by-shell")); + dir = QDir(dir.filePath(this->shellId)); + } else { + auto basedir = QStandardPaths::writableLocation(QStandardPaths::GenericCacheLocation); + dir = QDir(this->shellCacheOverride.replace("$BASE", basedir)); + } + this->mShellCacheDir = dir; qCDebug(logPaths) << "Initialized cache path:" << dir.path(); @@ -319,7 +361,7 @@ void QsPaths::createLock() { return; } - auto lock = flock { + struct flock lock = { .l_type = F_WRLCK, .l_whence = SEEK_SET, .l_start = 0, @@ -337,7 +379,8 @@ void QsPaths::createLock() { qCDebug(logPaths) << "Created instance lock at" << path; } } else { - qCCritical(logPaths + qCCritical( + logPaths ) << "Could not create instance lock, as the instance runtime directory could not be created."; } } @@ -346,7 +389,7 @@ bool QsPaths::checkLock(const QString& path, InstanceLockInfo* info, bool allowD auto file = QFile(QDir(path).filePath("instance.lock")); if (!file.open(QFile::ReadOnly)) return false; - auto lock = flock { + struct flock lock = { .l_type = F_WRLCK, .l_whence = SEEK_SET, .l_start = 0, @@ -370,7 +413,7 @@ bool QsPaths::checkLock(const QString& path, InstanceLockInfo* info, bool allowD } QPair, QVector> -QsPaths::collectInstances(const QString& path) { +QsPaths::collectInstances(const QString& path, const QString& display) { qCDebug(logPaths) << "Collecting instances from" << path; auto liveInstances = QVector(); auto deadInstances = QVector(); @@ -384,6 +427,11 @@ QsPaths::collectInstances(const QString& path) { qCDebug(logPaths).nospace() << "Found instance " << info.instance.instanceId << " (pid " << info.pid << ") at " << path; + if (!display.isEmpty() && info.instance.display != display) { + qCDebug(logPaths) << "Skipped instance with mismatched display at" << path; + continue; + } + if (info.pid == -1) { deadInstances.push_back(info); } else { diff --git a/src/core/paths.hpp b/src/core/paths.hpp index 9646ca4..c2500ed 100644 --- a/src/core/paths.hpp +++ b/src/core/paths.hpp @@ -17,17 +17,24 @@ QDataStream& operator>>(QDataStream& stream, InstanceLockInfo& info); class QsPaths { public: static QsPaths* instance(); - static void init(QString shellId, QString pathId, QString dataOverride, QString stateOverride); + static void init( + QString shellId, + QString pathId, + QString dataOverride, + QString stateOverride, + QString cacheOverride + ); static QDir crashDir(const QString& id); static QString basePath(const QString& id); static QString ipcPath(const QString& id); static bool checkLock(const QString& path, InstanceLockInfo* info = nullptr, bool allowDead = false); static QPair, QVector> - collectInstances(const QString& path); + collectInstances(const QString& path, const QString& display); QDir* baseRunDir(); QDir* shellRunDir(); + QDir* shellVfsDir(); QDir* instanceRunDir(); void linkRunDir(); void linkPathDir(); @@ -48,9 +55,11 @@ private: QString pathId; QDir mBaseRunDir; QDir mShellRunDir; + QDir mShellVfsDir; QDir mInstanceRunDir; DirState baseRunState = DirState::Unknown; DirState shellRunState = DirState::Unknown; + DirState shellVfsState = DirState::Unknown; DirState instanceRunState = DirState::Unknown; QDir mShellDataDir; @@ -62,4 +71,5 @@ private: QString shellDataOverride; QString shellStateOverride; + QString shellCacheOverride; }; diff --git a/src/core/popupanchor.cpp b/src/core/popupanchor.cpp index bbcc3a5..151dd5d 100644 --- a/src/core/popupanchor.cpp +++ b/src/core/popupanchor.cpp @@ -28,7 +28,7 @@ void PopupAnchor::markClean() { this->lastState = this->state; } void PopupAnchor::markDirty() { this->lastState.reset(); } QWindow* PopupAnchor::backingWindow() const { - return this->mProxyWindow ? this->mProxyWindow->backingWindow() : nullptr; + return this->bProxyWindow ? this->bProxyWindow->backingWindow() : nullptr; } void PopupAnchor::setWindowInternal(QObject* window) { @@ -36,14 +36,14 @@ void PopupAnchor::setWindowInternal(QObject* window) { if (this->mWindow) { QObject::disconnect(this->mWindow, nullptr, this, nullptr); - QObject::disconnect(this->mProxyWindow, nullptr, this, nullptr); + QObject::disconnect(this->bProxyWindow, nullptr, this, nullptr); } if (window) { if (auto* proxy = qobject_cast(window)) { - this->mProxyWindow = proxy; + this->bProxyWindow = proxy; } else if (auto* interface = qobject_cast(window)) { - this->mProxyWindow = interface->proxyWindow(); + this->bProxyWindow = interface->proxyWindow(); } else { qWarning() << "Tried to set popup anchor window to" << window << "which is not a quickshell window."; @@ -55,7 +55,7 @@ void PopupAnchor::setWindowInternal(QObject* window) { QObject::connect(this->mWindow, &QObject::destroyed, this, &PopupAnchor::onWindowDestroyed); QObject::connect( - this->mProxyWindow, + this->bProxyWindow, &ProxyWindowBase::backerVisibilityChanged, this, &PopupAnchor::backingWindowVisibilityChanged @@ -70,7 +70,7 @@ void PopupAnchor::setWindowInternal(QObject* window) { setnull: if (this->mWindow) { this->mWindow = nullptr; - this->mProxyWindow = nullptr; + this->bProxyWindow = nullptr; emit this->windowChanged(); emit this->backingWindowVisibilityChanged(); @@ -100,7 +100,7 @@ void PopupAnchor::setItem(QQuickItem* item) { void PopupAnchor::onWindowDestroyed() { this->mWindow = nullptr; - this->mProxyWindow = nullptr; + this->bProxyWindow = nullptr; emit this->windowChanged(); emit this->backingWindowVisibilityChanged(); } @@ -186,11 +186,11 @@ void PopupAnchor::updatePlacement(const QPoint& anchorpoint, const QSize& size) } void PopupAnchor::updateAnchor() { - if (this->mItem && this->mProxyWindow) { + if (this->mItem && this->bProxyWindow) { auto baseRect = this->mUserRect.isEmpty() ? this->mItem->boundingRect() : this->mUserRect.qrect(); - auto rect = this->mProxyWindow->contentItem()->mapFromItem( + auto rect = this->bProxyWindow->contentItem()->mapFromItem( this->mItem, baseRect.marginsRemoved(this->mMargins.qmargins()) ); diff --git a/src/core/popupanchor.hpp b/src/core/popupanchor.hpp index a9b121e..9f08512 100644 --- a/src/core/popupanchor.hpp +++ b/src/core/popupanchor.hpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -139,7 +140,9 @@ public: void markDirty(); [[nodiscard]] QObject* window() const { return this->mWindow; } - [[nodiscard]] ProxyWindowBase* proxyWindow() const { return this->mProxyWindow; } + [[nodiscard]] QBindable bindableProxyWindow() const { + return &this->bProxyWindow; + } [[nodiscard]] QWindow* backingWindow() const; void setWindowInternal(QObject* window); void setWindow(QObject* window); @@ -193,11 +196,12 @@ private slots: private: QObject* mWindow = nullptr; QQuickItem* mItem = nullptr; - ProxyWindowBase* mProxyWindow = nullptr; PopupAnchorState state; Box mUserRect; Margins mMargins; std::optional lastState; + + Q_OBJECT_BINDABLE_PROPERTY(PopupAnchor, ProxyWindowBase*, bProxyWindow); }; class PopupPositioner { diff --git a/src/core/qmlglobal.cpp b/src/core/qmlglobal.cpp index 0aba306..6c26609 100644 --- a/src/core/qmlglobal.cpp +++ b/src/core/qmlglobal.cpp @@ -29,6 +29,7 @@ #include "paths.hpp" #include "qmlscreen.hpp" #include "rootwrapper.hpp" +#include "scanenv.hpp" QuickshellSettings::QuickshellSettings() { QObject::connect( @@ -210,10 +211,22 @@ void QuickshellGlobal::onClipboardChanged(QClipboard::Mode mode) { if (mode == QClipboard::Clipboard) emit this->clipboardTextChanged(); } -QString QuickshellGlobal::configDir() const { +QString QuickshellGlobal::shellDir() const { return EngineGeneration::findObjectGeneration(this)->rootPath.path(); } +QString QuickshellGlobal::configDir() const { + qWarning() << "Quickshell.configDir is deprecated and may be removed in a future release. Use " + "Quickshell.shellDir."; + return this->shellDir(); +} + +QString QuickshellGlobal::shellRoot() const { + qWarning() << "Quickshell.shellRoot is deprecated and may be removed in a future release. Use " + "Quickshell.shellDir."; + return this->shellDir(); +} + QString QuickshellGlobal::dataDir() const { // NOLINT return QsPaths::instance()->shellDataDir().path(); } @@ -226,8 +239,14 @@ QString QuickshellGlobal::cacheDir() const { // NOLINT return QsPaths::instance()->shellCacheDir().path(); } +QString QuickshellGlobal::shellPath(const QString& path) const { + return this->shellDir() % '/' % path; +} + QString QuickshellGlobal::configPath(const QString& path) const { - return this->configDir() % '/' % path; + qWarning() << "Quickshell.configPath() is deprecated and may be removed in a future release. Use " + "Quickshell.shellPath()."; + return this->shellPath(path); } QString QuickshellGlobal::dataPath(const QString& path) const { @@ -295,6 +314,16 @@ QString QuickshellGlobal::iconPath(const QString& icon, const QString& fallback) return IconImageProvider::requestString(icon, "", fallback); } +bool QuickshellGlobal::hasThemeIcon(const QString& icon) { return QIcon::hasThemeIcon(icon); } + +bool QuickshellGlobal::hasVersion(qint32 major, qint32 minor, const QStringList& features) { + return qs::scan::env::PreprocEnv::hasVersion(major, minor, features); +} + +bool QuickshellGlobal::hasVersion(qint32 major, qint32 minor) { + return QuickshellGlobal::hasVersion(major, minor, QStringList()); +} + QuickshellGlobal* QuickshellGlobal::create(QQmlEngine* engine, QJSEngine* /*unused*/) { auto* qsg = new QuickshellGlobal(); auto* generation = EngineGeneration::findEngineGeneration(engine); diff --git a/src/core/qmlglobal.hpp b/src/core/qmlglobal.hpp index d05b96d..94b42f6 100644 --- a/src/core/qmlglobal.hpp +++ b/src/core/qmlglobal.hpp @@ -108,9 +108,11 @@ class QuickshellGlobal: public QObject { /// /// The root directory is the folder containing the entrypoint to your shell, often referred /// to as `shell.qml`. + Q_PROPERTY(QString shellDir READ shellDir CONSTANT); + /// > [!WARNING] Deprecated: Renamed to @@shellDir for clarity. Q_PROPERTY(QString configDir READ configDir CONSTANT); - /// > [!WARNING] Deprecated: Returns @@configDir. - Q_PROPERTY(QString shellRoot READ configDir CONSTANT); + /// > [!WARNING] Deprecated: Renamed to @@shellDir for consistency. + Q_PROPERTY(QString shellRoot READ shellRoot CONSTANT); /// Quickshell's working directory. Defaults to whereever quickshell was launched from. Q_PROPERTY(QString workingDirectory READ workingDirectory WRITE setWorkingDirectory NOTIFY workingDirectoryChanged); /// If true then the configuration will be reloaded whenever any files change. @@ -125,18 +127,21 @@ class QuickshellGlobal: public QObject { /// Usually `~/.local/share/quickshell/by-shell/` /// /// Can be overridden using `//@ pragma DataDir $BASE/path` in the root qml file, where `$BASE` - /// corrosponds to `$XDG_DATA_HOME` (usually `~/.local/share`). + /// corresponds to `$XDG_DATA_HOME` (usually `~/.local/share`). Q_PROPERTY(QString dataDir READ dataDir CONSTANT); /// The per-shell state directory. /// /// Usually `~/.local/state/quickshell/by-shell/` /// /// Can be overridden using `//@ pragma StateDir $BASE/path` in the root qml file, where `$BASE` - /// corrosponds to `$XDG_STATE_HOME` (usually `~/.local/state`). + /// corresponds to `$XDG_STATE_HOME` (usually `~/.local/state`). Q_PROPERTY(QString stateDir READ stateDir CONSTANT); /// The per-shell cache directory. /// /// Usually `~/.cache/quickshell/by-shell/` + /// + /// Can be overridden using `//@ pragma CacheDir $BASE/path` in the root qml file, where `$BASE` + /// corresponds to `$XDG_CACHE_HOME` (usually `~/.cache`). Q_PROPERTY(QString cacheDir READ cacheDir CONSTANT); // clang-format on QML_SINGLETON; @@ -197,7 +202,11 @@ public: /// Setting the `fallback` parameter of `iconPath` will attempt to load the fallback /// icon if the requested one could not be loaded. Q_INVOKABLE static QString iconPath(const QString& icon, const QString& fallback); + /// Check if specified icon has an available icon in your icon theme + Q_INVOKABLE static bool hasThemeIcon(const QString& icon); /// Equivalent to `${Quickshell.configDir}/${path}` + Q_INVOKABLE [[nodiscard]] QString shellPath(const QString& path) const; + /// > [!WARNING] Deprecated: Renamed to @@shellPath() for clarity. Q_INVOKABLE [[nodiscard]] QString configPath(const QString& path) const; /// Equivalent to `${Quickshell.dataDir}/${path}` Q_INVOKABLE [[nodiscard]] QString dataPath(const QString& path) const; @@ -210,11 +219,28 @@ public: /// /// The popup can also be blocked by setting `QS_NO_RELOAD_POPUP=1`. Q_INVOKABLE void inhibitReloadPopup() { this->mInhibitReloadPopup = true; } + /// Check if Quickshell's version is at least `major.minor` and the listed + /// unreleased features are available. If Quickshell is newer than the given version + /// it is assumed that all unreleased features are present. The unreleased feature list + /// may be omitted. + /// + /// > [!NOTE] You can feature gate code blocks using Quickshell's preprocessor which + /// > has the same function available. + /// > + /// > ```qml + /// > //@ if hasVersion(0, 3, ["feature"]) + /// > ... + /// > //@ endif + /// > ``` + Q_INVOKABLE static bool hasVersion(qint32 major, qint32 minor, const QStringList& features); + Q_INVOKABLE static bool hasVersion(qint32 major, qint32 minor); void clearReloadPopupInhibit() { this->mInhibitReloadPopup = false; } [[nodiscard]] bool isReloadPopupInhibited() const { return this->mInhibitReloadPopup; } + [[nodiscard]] QString shellDir() const; [[nodiscard]] QString configDir() const; + [[nodiscard]] QString shellRoot() const; [[nodiscard]] QString workingDirectory() const; void setWorkingDirectory(QString workingDirectory); diff --git a/src/core/qsmenu.hpp b/src/core/qsmenu.hpp index 6684c68..90df8b9 100644 --- a/src/core/qsmenu.hpp +++ b/src/core/qsmenu.hpp @@ -46,8 +46,8 @@ class QsMenuHandle: public QObject { public: explicit QsMenuHandle(QObject* parent): QObject(parent) {} - virtual void refHandle() {}; - virtual void unrefHandle() {}; + virtual void refHandle() {} + virtual void unrefHandle() {} [[nodiscard]] virtual QsMenuEntry* menu() = 0; diff --git a/src/core/region.cpp b/src/core/region.cpp index e36ed7d..11892d6 100644 --- a/src/core/region.cpp +++ b/src/core/region.cpp @@ -1,13 +1,13 @@ #include "region.hpp" #include -#include #include #include #include #include #include #include +#include #include PendingRegion::PendingRegion(QObject* parent): QObject(parent) { @@ -19,7 +19,6 @@ PendingRegion::PendingRegion(QObject* parent): QObject(parent) { QObject::connect(this, &PendingRegion::widthChanged, this, &PendingRegion::changed); QObject::connect(this, &PendingRegion::heightChanged, this, &PendingRegion::changed); QObject::connect(this, &PendingRegion::childrenChanged, this, &PendingRegion::changed); - QObject::connect(this, &PendingRegion::regionsChanged, this, &PendingRegion::childrenChanged); } void PendingRegion::setItem(QQuickItem* item) { @@ -42,33 +41,21 @@ void PendingRegion::setItem(QQuickItem* item) { emit this->itemChanged(); } -void PendingRegion::onItemDestroyed() { - this->mItem = nullptr; - emit this->itemChanged(); -} +void PendingRegion::onItemDestroyed() { this->mItem = nullptr; } -void PendingRegion::onChildDestroyed() { - this->mRegions.removeAll(this->sender()); - emit this->regionsChanged(); -} +void PendingRegion::onChildDestroyed() { this->mRegions.removeAll(this->sender()); } -const QList& PendingRegion::regions() const { return this->mRegions; } - -void PendingRegion::setRegions(const QList& regions) { - if (regions == this->mRegions) return; - - for (auto* region: this->mRegions) { - QObject::disconnect(region, nullptr, this, nullptr); - } - - this->mRegions = regions; - - for (auto* region: regions) { - QObject::connect(region, &QObject::destroyed, this, &PendingRegion::onChildDestroyed); - QObject::connect(region, &PendingRegion::changed, this, &PendingRegion::childrenChanged); - } - - emit this->regionsChanged(); +QQmlListProperty PendingRegion::regions() { + return QQmlListProperty( + this, + nullptr, + &PendingRegion::regionsAppend, + &PendingRegion::regionsCount, + &PendingRegion::regionAt, + &PendingRegion::regionsClear, + &PendingRegion::regionsReplace, + &PendingRegion::regionsRemoveLast + ); } bool PendingRegion::empty() const { @@ -130,3 +117,58 @@ QRegion PendingRegion::applyTo(const QRect& rect) const { return this->applyTo(baseRegion); } } + +void PendingRegion::regionsAppend(QQmlListProperty* prop, PendingRegion* region) { + auto* self = static_cast(prop->object); // NOLINT + if (!region) return; + + QObject::connect(region, &QObject::destroyed, self, &PendingRegion::onChildDestroyed); + QObject::connect(region, &PendingRegion::changed, self, &PendingRegion::childrenChanged); + + self->mRegions.append(region); + + emit self->childrenChanged(); +} + +PendingRegion* PendingRegion::regionAt(QQmlListProperty* prop, qsizetype i) { + return static_cast(prop->object)->mRegions.at(i); // NOLINT +} + +void PendingRegion::regionsClear(QQmlListProperty* prop) { + auto* self = static_cast(prop->object); // NOLINT + + for (auto* region: self->mRegions) { + QObject::disconnect(region, nullptr, self, nullptr); + } + + self->mRegions.clear(); // NOLINT + emit self->childrenChanged(); +} + +qsizetype PendingRegion::regionsCount(QQmlListProperty* prop) { + return static_cast(prop->object)->mRegions.length(); // NOLINT +} + +void PendingRegion::regionsRemoveLast(QQmlListProperty* prop) { + auto* self = static_cast(prop->object); // NOLINT + + auto* last = self->mRegions.last(); + if (last != nullptr) QObject::disconnect(last, nullptr, self, nullptr); + + self->mRegions.removeLast(); + emit self->childrenChanged(); +} + +void PendingRegion::regionsReplace( + QQmlListProperty* prop, + qsizetype i, + PendingRegion* region +) { + auto* self = static_cast(prop->object); // NOLINT + + auto* old = self->mRegions.at(i); + if (old != nullptr) QObject::disconnect(old, nullptr, self, nullptr); + + self->mRegions.replace(i, region); + emit self->childrenChanged(); +} diff --git a/src/core/region.hpp b/src/core/region.hpp index 0335abb..6637d7b 100644 --- a/src/core/region.hpp +++ b/src/core/region.hpp @@ -82,7 +82,7 @@ class PendingRegion: public QObject { /// } /// } /// ``` - Q_PROPERTY(QList regions READ regions WRITE setRegions NOTIFY regionsChanged); + Q_PROPERTY(QQmlListProperty regions READ regions); Q_CLASSINFO("DefaultProperty", "regions"); QML_NAMED_ELEMENT(Region); @@ -91,8 +91,7 @@ public: void setItem(QQuickItem* item); - [[nodiscard]] const QList& regions() const; - void setRegions(const QList& regions); + QQmlListProperty regions(); [[nodiscard]] bool empty() const; [[nodiscard]] QRegion build() const; @@ -110,7 +109,6 @@ signals: void yChanged(); void widthChanged(); void heightChanged(); - void regionsChanged(); void childrenChanged(); /// Triggered when the region's geometry changes. @@ -124,6 +122,14 @@ private slots: void onChildDestroyed(); private: + static void regionsAppend(QQmlListProperty* prop, PendingRegion* region); + static PendingRegion* regionAt(QQmlListProperty* prop, qsizetype i); + static void regionsClear(QQmlListProperty* prop); + static qsizetype regionsCount(QQmlListProperty* prop); + static void regionsRemoveLast(QQmlListProperty* prop); + static void + regionsReplace(QQmlListProperty* prop, qsizetype i, PendingRegion* region); + QQuickItem* mItem = nullptr; qint32 mX = 0; diff --git a/src/core/reload.cpp b/src/core/reload.cpp index 0bdf8fc..ea2abbf 100644 --- a/src/core/reload.cpp +++ b/src/core/reload.cpp @@ -129,14 +129,18 @@ QObject* Reloadable::getChildByReloadId(QObject* parent, const QString& reloadId void PostReloadHook::componentComplete() { auto* engineGeneration = EngineGeneration::findObjectGeneration(this); if (!engineGeneration || engineGeneration->reloadComplete) this->postReload(); + else { + // disconnected by EngineGeneration::postReload + QObject::connect( + engineGeneration, + &EngineGeneration::firePostReload, + this, + &PostReloadHook::postReload + ); + } } void PostReloadHook::postReload() { this->isPostReload = true; this->onPostReload(); } - -void PostReloadHook::postReloadTree(QObject* root) { - for (auto* child: root->children()) PostReloadHook::postReloadTree(child); - if (auto* self = dynamic_cast(root)) self->postReload(); -} diff --git a/src/core/reload.hpp b/src/core/reload.hpp index 1d4e375..ae5d7c9 100644 --- a/src/core/reload.hpp +++ b/src/core/reload.hpp @@ -57,7 +57,7 @@ public: void reload(QObject* oldInstance = nullptr); - void classBegin() override {}; + void classBegin() override {} void componentComplete() override; // Reload objects in the parent->child graph recursively. @@ -122,15 +122,19 @@ private: class PostReloadHook : public QObject , public QQmlParserStatus { + Q_OBJECT; + QML_ANONYMOUS; + Q_INTERFACES(QQmlParserStatus); + public: PostReloadHook(QObject* parent = nullptr): QObject(parent) {} void classBegin() override {} void componentComplete() override; - void postReload(); virtual void onPostReload() = 0; - static void postReloadTree(QObject* root); +public slots: + void postReload(); protected: bool isPostReload = false; diff --git a/src/core/rootwrapper.cpp b/src/core/rootwrapper.cpp index 2968402..1e75819 100644 --- a/src/core/rootwrapper.cpp +++ b/src/core/rootwrapper.cpp @@ -4,6 +4,7 @@ #include #include +#include #include #include #include @@ -18,15 +19,26 @@ #include "instanceinfo.hpp" #include "qmlglobal.hpp" #include "scan.hpp" +#include "toolsupport.hpp" RootWrapper::RootWrapper(QString rootPath, QString shellId) : QObject(nullptr) , rootPath(std::move(rootPath)) , shellId(std::move(shellId)) , originalWorkingDirectory(QDir::current().absolutePath()) { - // clang-format off - QObject::connect(QuickshellSettings::instance(), &QuickshellSettings::watchFilesChanged, this, &RootWrapper::onWatchFilesChanged); - // clang-format on + QObject::connect( + QuickshellSettings::instance(), + &QuickshellSettings::watchFilesChanged, + this, + &RootWrapper::onWatchFilesChanged + ); + + QObject::connect( + &this->configDirWatcher, + &QFileSystemWatcher::directoryChanged, + this, + &RootWrapper::updateTooling + ); this->reloadGraph(true); @@ -46,10 +58,10 @@ void RootWrapper::reloadGraph(bool hard) { auto rootFile = QFileInfo(this->rootPath); auto rootPath = rootFile.dir(); auto scanner = QmlScanner(rootPath); - scanner.scanQmlFile(this->rootPath); + scanner.scanQmlRoot(this->rootPath); - auto* generation = new EngineGeneration(rootPath, std::move(scanner)); - generation->wrapper = this; + qs::core::QmlToolingSupport::updateTooling(rootPath, scanner); + this->configDirWatcher.addPath(rootPath.path()); // todo: move into EngineGeneration if (this->generation != nullptr) { @@ -59,6 +71,33 @@ void RootWrapper::reloadGraph(bool hard) { QDir::setCurrent(this->originalWorkingDirectory); + if (!scanner.scanErrors.isEmpty()) { + qCritical() << "Failed to load configuration"; + QString errorString = "Failed to load configuration"; + for (auto& error: scanner.scanErrors) { + const auto& file = error.file; + QString rel; + if (file.startsWith(rootPath.path() % '/')) { + rel = '@' % file.sliced(rootPath.path().length() + 1); + } else { + rel = file; + } + + auto msg = " error in " % rel % '[' % QString::number(error.line) % ":0]: " % error.message; + errorString += '\n' % msg; + qCritical().noquote() << msg; + } + + if (this->generation != nullptr && this->generation->qsgInstance != nullptr) { + emit this->generation->qsgInstance->reloadFailed(errorString); + } + + return; + } + + auto* generation = new EngineGeneration(rootPath, std::move(scanner)); + generation->wrapper = this; + QUrl url; url.setScheme("qs"); url.setPath("@/qs/" % rootFile.fileName()); @@ -168,3 +207,9 @@ void RootWrapper::onWatchFilesChanged() { } void RootWrapper::onWatchedFilesChanged() { this->reloadGraph(false); } + +void RootWrapper::updateTooling() { + if (!this->generation) return; + auto configDir = QFileInfo(this->rootPath).dir(); + qs::core::QmlToolingSupport::updateTooling(configDir, this->generation->scanner); +} diff --git a/src/core/rootwrapper.hpp b/src/core/rootwrapper.hpp index 02d7a14..1425d17 100644 --- a/src/core/rootwrapper.hpp +++ b/src/core/rootwrapper.hpp @@ -1,5 +1,6 @@ #pragma once +#include #include #include #include @@ -22,10 +23,12 @@ private slots: void generationDestroyed(); void onWatchFilesChanged(); void onWatchedFilesChanged(); + void updateTooling(); private: QString rootPath; QString shellId; EngineGeneration* generation = nullptr; QString originalWorkingDirectory; + QFileSystemWatcher configDirWatcher; }; diff --git a/src/core/scan.cpp b/src/core/scan.cpp index a29ee59..37b0fac 100644 --- a/src/core/scan.cpp +++ b/src/core/scan.cpp @@ -1,9 +1,11 @@ #include "scan.hpp" #include +#include #include #include #include +#include #include #include #include @@ -12,44 +14,57 @@ #include #include #include -#include #include #include "logcat.hpp" +#include "scanenv.hpp" QS_LOGGING_CATEGORY(logQmlScanner, "quickshell.qmlscanner", QtWarningMsg); -void QmlScanner::scanDir(const QString& path) { - if (this->scannedDirs.contains(path)) return; - this->scannedDirs.push_back(path); +void QmlScanner::scanDir(const QDir& dir) { + if (this->scannedDirs.contains(dir)) return; + this->scannedDirs.push_back(dir); + + const auto& path = dir.path(); qCDebug(logQmlScanner) << "Scanning directory" << path; - auto dir = QDir(path); + + struct Entry { + QString name; + bool singleton = false; + bool internal = false; + }; bool seenQmldir = false; - auto singletons = QVector(); - auto entries = QVector(); - for (auto& entry: dir.entryList(QDir::Files | QDir::NoDotAndDotDot)) { - if (entry == "qmldir") { - qCDebug(logQmlScanner + auto entries = QVector(); + + for (auto& name: dir.entryList(QDir::Files | QDir::NoDotAndDotDot)) { + if (name == "qmldir") { + qCDebug( + logQmlScanner ) << "Found qmldir file, qmldir synthesization will be disabled for directory" << path; seenQmldir = true; - } else if (entry.at(0).isUpper() && entry.endsWith(".qml")) { - if (this->scanQmlFile(dir.filePath(entry))) { - singletons.push_back(entry); + } else if (name.at(0).isUpper() && name.endsWith(".qml")) { + auto& entry = entries.emplaceBack(); + + if (this->scanQmlFile(dir.filePath(name), entry.singleton, entry.internal)) { + entry.name = name; } else { - entries.push_back(entry); + entries.pop_back(); + } + } else if (name.at(0).isUpper() && name.endsWith(".qml.json")) { + if (this->scanQmlJson(dir.filePath(name))) { + entries.push_back({ + .name = name.first(name.length() - 5), + .singleton = true, + }); } - } else if (entry.at(0).isUpper() && entry.endsWith(".qml.json")) { - this->scanQmlJson(dir.filePath(entry)); - singletons.push_back(entry.first(entry.length() - 5)); } } if (!seenQmldir) { - qCDebug(logQmlScanner) << "Synthesizing qmldir for directory" << path << "singletons" - << singletons; + qCDebug(logQmlScanner) << "Synthesizing qmldir for directory" << path; QString qmldir; auto stream = QTextStream(&qmldir); @@ -77,13 +92,10 @@ void QmlScanner::scanDir(const QString& path) { qCWarning(logQmlScanner) << "Module path" << path << "is outside of the config folder."; } - for (auto& singleton: singletons) { - stream << "singleton " << singleton.sliced(0, singleton.length() - 4) << " 1.0 " << singleton - << "\n"; - } - - for (auto& entry: entries) { - stream << entry.sliced(0, entry.length() - 4) << " 1.0 " << entry << "\n"; + for (const auto& entry: entries) { + if (entry.internal) stream << "internal "; + if (entry.singleton) stream << "singleton "; + stream << entry.name.sliced(0, entry.name.length() - 4) << " 1.0 " << entry.name << '\n'; } qCDebug(logQmlScanner) << "Synthesized qmldir for" << path << qPrintable("\n" + qmldir); @@ -91,7 +103,7 @@ void QmlScanner::scanDir(const QString& path) { } } -bool QmlScanner::scanQmlFile(const QString& path) { +bool QmlScanner::scanQmlFile(const QString& path, bool& singleton, bool& internal) { if (this->scannedFiles.contains(path)) return false; this->scannedFiles.push_back(path); @@ -106,60 +118,121 @@ bool QmlScanner::scanQmlFile(const QString& path) { auto stream = QTextStream(&file); auto imports = QVector(); - bool singleton = false; + bool inHeader = true; + auto ifScopes = QVector(); + bool sourceMasked = false; + int lineNum = 0; + QString overrideText; + bool isOverridden = false; + + auto pragmaEngine = QJSEngine(); + pragmaEngine.globalObject().setPrototype( + pragmaEngine.newQObject(new qs::scan::env::PreprocEnv()) + ); + + auto postError = [&, this](QString error) { + this->scanErrors.append({.file = path, .message = std::move(error), .line = lineNum}); + }; while (!stream.atEnd()) { - auto line = stream.readLine().trimmed(); - if (!singleton && line == "pragma Singleton") { - qCDebug(logQmlScanner) << "Discovered singleton" << path; - singleton = true; - } else if (line.startsWith("import")) { - // we dont care about "import qs" as we always load the root folder - if (auto importCursor = line.indexOf(" qs."); importCursor != -1) { - importCursor += 4; - QString path; + ++lineNum; + bool hideMask = false; + auto rawLine = stream.readLine(); + auto line = rawLine.trimmed(); + if (!sourceMasked && inHeader) { + if (!singleton && line == "pragma Singleton") { + singleton = true; + } else if (line.startsWith("import")) { + // we dont care about "import qs" as we always load the root folder + if (auto importCursor = line.indexOf(" qs."); importCursor != -1) { + importCursor += 4; + QString path; - while (importCursor != line.length()) { - auto c = line.at(importCursor); - if (c == '.') c = '/'; - else if (c == ' ') break; - else if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') - || c == '_') - { - } else { - qCWarning(logQmlScanner) << "Import line contains invalid characters: " << line; - goto next; + while (importCursor != line.length()) { + auto c = line.at(importCursor); + if (c == '.') c = '/'; + else if (c == ' ') break; + else if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') + || c == '_') + { + } else { + qCWarning(logQmlScanner) << "Import line contains invalid characters: " << line; + goto next; + } + + path.append(c); + importCursor += 1; } - path.append(c); - importCursor += 1; + imports.append(this->rootPath.filePath(path)); + } else if (auto startQuot = line.indexOf('"'); + startQuot != -1 && line.length() >= startQuot + 3) + { + auto endQuot = line.indexOf('"', startQuot + 1); + if (endQuot == -1) continue; + + auto name = line.sliced(startQuot + 1, endQuot - startQuot - 1); + imports.push_back(name); } - - imports.append(this->rootPath.filePath(path)); - } else if (auto startQuot = line.indexOf('"'); - startQuot != -1 && line.length() >= startQuot + 3) - { - auto endQuot = line.indexOf('"', startQuot + 1); - if (endQuot == -1) continue; - - auto name = line.sliced(startQuot + 1, endQuot - startQuot - 1); - imports.push_back(name); + } else if (!internal && line == "//@ pragma Internal") { + internal = true; + } else if (line.contains('{')) { + inHeader = false; } - } else if (line.contains('{')) break; + } + + if (line.startsWith("//@ if ")) { + auto code = line.sliced(7); + auto value = pragmaEngine.evaluate(code, path, 1234); + bool mask = true; + + if (value.isError()) { + postError(QString("Evaluating if: %0").arg(value.toString())); + } else if (!value.isBool()) { + postError(QString("If expression \"%0\" is not a boolean").arg(value.toString())); + } else if (value.toBool()) { + mask = false; + } + if (!sourceMasked && mask) hideMask = true; + mask = sourceMasked || mask; // cant unmask if a nested if passes + ifScopes.append(mask); + if (mask) isOverridden = true; + sourceMasked = mask; + } else if (line.startsWith("//@ endif")) { + if (ifScopes.isEmpty()) { + postError("endif without matching if"); + } else { + ifScopes.pop_back(); + + if (ifScopes.isEmpty()) sourceMasked = false; + else sourceMasked = ifScopes.last(); + } + } + + if (!hideMask && sourceMasked) overrideText.append("// MASKED: " % rawLine % '\n'); + else overrideText.append(rawLine % '\n'); next:; } + if (!ifScopes.isEmpty()) { + postError("unclosed preprocessor if block"); + } + file.close(); + if (isOverridden) { + this->fileIntercepts.insert(path, overrideText); + } + if (logQmlScanner().isDebugEnabled() && !imports.isEmpty()) { qCDebug(logQmlScanner) << "Found imports" << imports; } - auto currentdir = QDir(QFileInfo(path).canonicalPath()); + auto currentdir = QDir(QFileInfo(path).absolutePath()); // the root can never be a singleton so it dosent matter if we skip it - this->scanDir(currentdir.path()); + this->scanDir(currentdir); for (auto& import: imports) { QString ipath; @@ -172,9 +245,9 @@ bool QmlScanner::scanQmlFile(const QString& path) { } auto pathInfo = QFileInfo(ipath); - auto cpath = pathInfo.canonicalFilePath(); + auto cpath = pathInfo.absoluteFilePath(); - if (cpath.isEmpty()) { + if (!pathInfo.exists()) { qCWarning(logQmlScanner) << "Ignoring unresolvable import" << ipath << "from" << path; continue; } @@ -188,16 +261,22 @@ bool QmlScanner::scanQmlFile(const QString& path) { else this->scanDir(cpath); } - return singleton; + return true; } -void QmlScanner::scanQmlJson(const QString& path) { +void QmlScanner::scanQmlRoot(const QString& path) { + bool singleton = false; + bool internal = false; + this->scanQmlFile(path, singleton, internal); +} + +bool QmlScanner::scanQmlJson(const QString& path) { qCDebug(logQmlScanner) << "Scanning qml.json file" << path; auto file = QFile(path); if (!file.open(QFile::ReadOnly | QFile::Text)) { qCWarning(logQmlScanner) << "Failed to open file" << path; - return; + return false; } auto data = file.readAll(); @@ -209,7 +288,7 @@ void QmlScanner::scanQmlJson(const QString& path) { if (error.error != QJsonParseError::NoError) { qCCritical(logQmlScanner).nospace() << "Failed to parse qml.json file at " << path << ": " << error.errorString(); - return; + return false; } const QString body = @@ -219,6 +298,7 @@ void QmlScanner::scanQmlJson(const QString& path) { this->fileIntercepts.insert(path.first(path.length() - 5), body); this->scannedFiles.push_back(path); + return true; } QPair QmlScanner::jsonToQml(const QJsonValue& value, int indent) { diff --git a/src/core/scan.hpp b/src/core/scan.hpp index 6220bae..29f8f6a 100644 --- a/src/core/scan.hpp +++ b/src/core/scan.hpp @@ -16,18 +16,25 @@ public: QmlScanner() = default; QmlScanner(const QDir& rootPath): rootPath(rootPath) {} - // path must be canonical - void scanDir(const QString& path); - // returns if the file has a singleton - bool scanQmlFile(const QString& path); + void scanDir(const QDir& dir); + void scanQmlRoot(const QString& path); - QVector scannedDirs; + QVector scannedDirs; QVector scannedFiles; QHash fileIntercepts; + struct ScanError { + QString file; + QString message; + int line; + }; + + QVector scanErrors; + private: QDir rootPath; - void scanQmlJson(const QString& path); + bool scanQmlFile(const QString& path, bool& singleton, bool& internal); + bool scanQmlJson(const QString& path); [[nodiscard]] static QPair jsonToQml(const QJsonValue& value, int indent = 0); }; diff --git a/src/core/scanenv.cpp b/src/core/scanenv.cpp new file mode 100644 index 0000000..047f472 --- /dev/null +++ b/src/core/scanenv.cpp @@ -0,0 +1,31 @@ +#include "scanenv.hpp" + +#include +#include + +#include "build.hpp" + +namespace qs::scan::env { + +bool PreprocEnv::hasVersion(int major, int minor, const QStringList& features) { + if (QS_VERSION_MAJOR > major) return true; + if (QS_VERSION_MAJOR == major && QS_VERSION_MINOR > minor) return true; + + auto availFeatures = QString(QS_UNRELEASED_FEATURES).split(';'); + + for (const auto& feature: features) { + if (!availFeatures.contains(feature)) return false; + } + + return QS_VERSION_MAJOR == major && QS_VERSION_MINOR == minor; +} + +QString PreprocEnv::env(const QString& variable) { + return qEnvironmentVariable(variable.toStdString().c_str()); +} + +bool PreprocEnv::isEnvSet(const QString& variable) { + return qEnvironmentVariableIsSet(variable.toStdString().c_str()); +} + +} // namespace qs::scan::env diff --git a/src/core/scanenv.hpp b/src/core/scanenv.hpp new file mode 100644 index 0000000..c1c6814 --- /dev/null +++ b/src/core/scanenv.hpp @@ -0,0 +1,20 @@ +#pragma once + +#include +#include +#include + +namespace qs::scan::env { + +class PreprocEnv: public QObject { + Q_OBJECT; + +public: + Q_INVOKABLE static bool + hasVersion(int major, int minor, const QStringList& features = QStringList()); + + Q_INVOKABLE static QString env(const QString& variable); + Q_INVOKABLE static bool isEnvSet(const QString& variable); +}; + +} // namespace qs::scan::env diff --git a/src/core/scriptmodel.cpp b/src/core/scriptmodel.cpp index 6837c4a..5407e2b 100644 --- a/src/core/scriptmodel.cpp +++ b/src/core/scriptmodel.cpp @@ -19,7 +19,7 @@ void ScriptModel::updateValuesUnique(const QVariantList& newValues) { auto newIter = newValues.begin(); // TODO: cache this - auto getCmpKey = [&](const QVariant& v) { + auto getCmpKey = [this](const QVariant& v) { if (v.canConvert()) { auto vMap = v.value(); if (vMap.contains(this->cmpKey)) { @@ -30,7 +30,7 @@ void ScriptModel::updateValuesUnique(const QVariantList& newValues) { return v; }; - auto variantCmp = [&](const QVariant& a, const QVariant& b) { + auto variantCmp = [&, this](const QVariant& a, const QVariant& b) { if (!this->cmpKey.isEmpty()) return getCmpKey(a) == getCmpKey(b); else return a == b; }; @@ -72,8 +72,8 @@ void ScriptModel::updateValuesUnique(const QVariantList& newValues) { do { ++iter; } while (iter != this->mValues.end() - && std::find_if(newIter, newValues.end(), eqPredicate(*iter)) == newValues.end() - ); + && std::find_if(newIter, newValues.end(), eqPredicate(*iter)) + == newValues.end()); auto index = static_cast(std::distance(this->mValues.begin(), iter)); auto startIndex = static_cast(std::distance(this->mValues.begin(), startIter)); diff --git a/src/core/singleton.cpp b/src/core/singleton.cpp index 61ac992..15668c9 100644 --- a/src/core/singleton.cpp +++ b/src/core/singleton.cpp @@ -51,9 +51,3 @@ void SingletonRegistry::onReload(SingletonRegistry* old) { singleton->reload(old == nullptr ? nullptr : old->registry.value(url)); } } - -void SingletonRegistry::onPostReload() { - for (auto* singleton: this->registry.values()) { - PostReloadHook::postReloadTree(singleton); - } -} diff --git a/src/core/singleton.hpp b/src/core/singleton.hpp index e63ab12..200c97f 100644 --- a/src/core/singleton.hpp +++ b/src/core/singleton.hpp @@ -26,7 +26,6 @@ public: void registerSingleton(const QUrl& url, Singleton* singleton); void onReload(SingletonRegistry* old); - void onPostReload(); private: QHash registry; diff --git a/src/core/toolsupport.cpp b/src/core/toolsupport.cpp new file mode 100644 index 0000000..8aa5ac9 --- /dev/null +++ b/src/core/toolsupport.cpp @@ -0,0 +1,241 @@ +#include "toolsupport.hpp" +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "logcat.hpp" +#include "paths.hpp" +#include "scan.hpp" + +namespace qs::core { + +namespace { +QS_LOGGING_CATEGORY(logTooling, "quickshell.tooling", QtWarningMsg); +} + +bool QmlToolingSupport::updateTooling(const QDir& configRoot, QmlScanner& scanner) { + auto* vfs = QsPaths::instance()->shellVfsDir(); + + if (!vfs) { + qCCritical(logTooling) << "Tooling dir could not be created"; + return false; + } + + if (!QmlToolingSupport::lockTooling()) { + return false; + } + + if (!QmlToolingSupport::updateQmllsConfig(configRoot, false)) { + QDir(vfs->filePath("qs")).removeRecursively(); + return false; + } + + QmlToolingSupport::updateToolingFs(scanner, configRoot, vfs->filePath("qs")); + return true; +} + +bool QmlToolingSupport::lockTooling() { + if (QmlToolingSupport::toolingLock) return true; + + auto lockPath = QsPaths::instance()->shellVfsDir()->filePath("tooling.lock"); + auto* file = new QFile(lockPath); + + if (!file->open(QFile::WriteOnly)) { + qCCritical(logTooling) << "Could not open tooling lock for write"; + return false; + } + + struct flock lock = { + .l_type = F_WRLCK, + .l_whence = SEEK_SET, // NOLINT (fcntl.h??) + .l_start = 0, + .l_len = 0, + .l_pid = 0, + }; + + if (fcntl(file->handle(), F_SETLK, &lock) == 0) { + qCInfo(logTooling) << "Acquired tooling support lock"; + QmlToolingSupport::toolingLock = file; + return true; + } else if (errno == EACCES || errno == EAGAIN) { + qCInfo(logTooling) << "Tooling support locked by another instance"; + return false; + } else { + qCCritical(logTooling).nospace() << "Could not create tooling lock at " << lockPath + << " with error code " << errno << ": " << qt_error_string(); + return false; + } +} + +QString QmlToolingSupport::getQmllsConfig() { + static auto config = []() { + // We can't replicate the algorithm used to create the import path list as it can have distro + // specific patches, e.g. nixos. + auto importPaths = QQmlEngine().importPathList(); + importPaths.removeIf([](const QString& path) { return path.startsWith("qrc:"); }); + + auto vfsPath = QsPaths::instance()->shellVfsDir()->path(); + auto importPathsStr = importPaths.join(u':'); + + QString qmllsConfig; + auto print = QDebug(&qmllsConfig).nospace(); + print << "[General]\nno-cmake-calls=true\nbuildDir=" << vfsPath + << "\nimportPaths=" << importPathsStr << '\n'; + + return qmllsConfig; + }(); + + return config; +} + +bool QmlToolingSupport::updateQmllsConfig(const QDir& configRoot, bool create) { + auto shellConfigPath = configRoot.filePath(".qmlls.ini"); + auto vfsConfigPath = QsPaths::instance()->shellVfsDir()->filePath(".qmlls.ini"); + + auto shellFileInfo = QFileInfo(shellConfigPath); + if (!create && !shellFileInfo.exists() && !shellFileInfo.isSymLink()) { + if (QmlToolingSupport::toolingEnabled) { + qInfo() << "QML tooling support disabled"; + QmlToolingSupport::toolingEnabled = false; + } else { + qCInfo(logTooling) << "Not enabling QML tooling support, qmlls.ini is missing at path" + << shellConfigPath; + } + + QFile::remove(vfsConfigPath); + return false; + } + + auto vfsFile = QFile(vfsConfigPath); + + if (!vfsFile.open(QFile::ReadWrite | QFile::Text)) { + qCCritical(logTooling) << "Failed to create qmlls config in vfs"; + return false; + } + + auto config = QmlToolingSupport::getQmllsConfig(); + + if (vfsFile.readAll() != config) { + if (!vfsFile.resize(0) || !vfsFile.write(config.toUtf8())) { + qCCritical(logTooling) << "Failed to write qmlls config in vfs"; + return false; + } + + qCDebug(logTooling) << "Wrote qmlls config in vfs"; + } + + if (!shellFileInfo.isSymLink() || shellFileInfo.symLinkTarget() != vfsConfigPath) { + QFile::remove(shellConfigPath); + + if (!QFile::link(vfsConfigPath, shellConfigPath)) { + qCCritical(logTooling) << "Failed to create qmlls config symlink"; + return false; + } + + qCDebug(logTooling) << "Created qmlls config symlink"; + } + + if (!QmlToolingSupport::toolingEnabled) { + qInfo() << "QML tooling support enabled"; + QmlToolingSupport::toolingEnabled = true; + } + + return true; +} + +void QmlToolingSupport::updateToolingFs( + QmlScanner& scanner, + const QDir& scanDir, + const QDir& linkDir +) { + QList files; + QSet subdirs; + + auto scanPath = scanDir.path(); + + linkDir.mkpath("."); + + for (auto& path: scanner.scannedFiles) { + if (path.length() < scanPath.length() + 1 || !path.startsWith(scanPath)) continue; + auto name = path.sliced(scanPath.length() + 1); + + if (name.contains('/')) { + auto dirname = name.first(name.indexOf('/')); + subdirs.insert(dirname); + continue; + } + + auto fileInfo = QFileInfo(path); + if (!fileInfo.isFile()) continue; + + auto spath = linkDir.filePath(name); + auto sFileInfo = QFileInfo(spath); + + if (!sFileInfo.isSymLink() || sFileInfo.symLinkTarget() != path) { + QFile::remove(spath); + + if (QFile::link(path, spath)) { + qCDebug(logTooling) << "Created symlink to" << path << "at" << spath; + files.append(spath); + } else { + qCCritical(logTooling) << "Could not create symlink to" << path << "at" << spath; + } + } else { + files.append(spath); + } + } + + for (auto [path, text]: scanner.fileIntercepts.asKeyValueRange()) { + if (path.length() < scanPath.length() + 1 || !path.startsWith(scanPath)) continue; + auto name = path.sliced(scanPath.length() + 1); + + if (name.contains('/')) { + auto dirname = name.first(name.indexOf('/')); + subdirs.insert(dirname); + continue; + } + + auto spath = linkDir.filePath(name); + auto file = QFile(spath); + if (!file.open(QFile::ReadWrite | QFile::Text)) { + qCCritical(logTooling) << "Failed to open injected file" << spath; + continue; + } + + if (file.readAll() == text) { + files.append(spath); + continue; + } + + if (file.resize(0) && file.write(text.toUtf8())) { + files.append(spath); + qCDebug(logTooling) << "Wrote injected file" << spath; + } else { + qCCritical(logTooling) << "Failed to write injected file" << spath; + } + } + + for (auto& name: linkDir.entryList(QDir::Files | QDir::System)) { // System = broken symlinks + auto path = linkDir.filePath(name); + + if (!files.contains(path)) { + if (QFile::remove(path)) qCDebug(logTooling) << "Removed old file at" << path; + else qCWarning(logTooling) << "Failed to remove old file at" << path; + } + } + + for (const auto& subdir: subdirs) { + QmlToolingSupport::updateToolingFs(scanner, scanDir.filePath(subdir), linkDir.filePath(subdir)); + } +} + +} // namespace qs::core diff --git a/src/core/toolsupport.hpp b/src/core/toolsupport.hpp new file mode 100644 index 0000000..9fb7921 --- /dev/null +++ b/src/core/toolsupport.hpp @@ -0,0 +1,22 @@ +#pragma once + +#include + +#include "scan.hpp" + +namespace qs::core { + +class QmlToolingSupport { +public: + static bool updateTooling(const QDir& configRoot, QmlScanner& scanner); + +private: + static QString getQmllsConfig(); + static bool lockTooling(); + static bool updateQmllsConfig(const QDir& configRoot, bool create); + static void updateToolingFs(QmlScanner& scanner, const QDir& scanDir, const QDir& linkDir); + static inline bool toolingEnabled = false; + static inline QFile* toolingLock = nullptr; +}; + +} // namespace qs::core diff --git a/src/core/util.hpp b/src/core/util.hpp index 88583d0..bb8dd85 100644 --- a/src/core/util.hpp +++ b/src/core/util.hpp @@ -29,7 +29,7 @@ struct StringLiteral16 { } [[nodiscard]] constexpr const QChar* qCharPtr() const noexcept { - return std::bit_cast(&this->value); + return std::bit_cast(&this->value); // NOLINT } [[nodiscard]] Q_ALWAYS_INLINE operator QString() const noexcept { @@ -251,37 +251,6 @@ public: GuardedEmitBlocker block() { return GuardedEmitBlocker(&this->blocked); } }; -template -class SimpleObjectHandleOps { - using Traits = MemberPointerTraits; - -public: - static bool setObject(Traits::Class* parent, Traits::Type value) { - if (value == parent->*member) return false; - - if (parent->*member != nullptr) { - QObject::disconnect(parent->*member, &QObject::destroyed, parent, destroyedSlot); - } - - parent->*member = value; - - if (value != nullptr) { - QObject::connect(parent->*member, &QObject::destroyed, parent, destroyedSlot); - } - - if constexpr (changedSignal != nullptr) { - emit(parent->*changedSignal)(); - } - - return true; - } -}; - -template -bool setSimpleObjectHandle(auto* parent, auto* value) { - return SimpleObjectHandleOps::setObject(parent, value); -} - template class MethodFunctor { using PtrMeta = MemberPointerTraits; diff --git a/src/crash/CMakeLists.txt b/src/crash/CMakeLists.txt index 7fdd830..a891ee9 100644 --- a/src/crash/CMakeLists.txt +++ b/src/crash/CMakeLists.txt @@ -6,12 +6,51 @@ qt_add_library(quickshell-crash STATIC qs_pch(quickshell-crash SET large) -find_package(PkgConfig REQUIRED) -pkg_check_modules(breakpad REQUIRED IMPORTED_TARGET breakpad) -# only need client?? take only includes from pkg config todo -target_link_libraries(quickshell-crash PRIVATE PkgConfig::breakpad -lbreakpad_client) +if (VENDOR_CPPTRACE) + message(STATUS "Vendoring cpptrace...") + include(FetchContent) + + # For use without internet access see: https://cmake.org/cmake/help/latest/module/FetchContent.html#variable:FETCHCONTENT_SOURCE_DIR_%3CuppercaseName%3E + FetchContent_Declare( + cpptrace + GIT_REPOSITORY https://github.com/jeremy-rifkin/cpptrace.git + GIT_TAG v1.0.4 + ) + + set(CPPTRACE_UNWIND_WITH_LIBUNWIND TRUE) + FetchContent_MakeAvailable(cpptrace) +else () + find_package(cpptrace REQUIRED) + + # useful for cross after you have already checked cpptrace is built correctly + if (NOT DO_NOT_CHECK_CPPTRACE_USABILITY) + try_run(CPPTRACE_SIGNAL_SAFE_UNWIND CPPTRACE_SIGNAL_SAFE_UNWIND_COMP + SOURCE_FROM_CONTENT check.cxx " + #include + int main() { + return cpptrace::can_signal_safe_unwind() ? 0 : 1; + } + " + LOG_DESCRIPTION "Checking ${CPPTRACE_SIGNAL_SAFE_UNWIND}" + LINK_LIBRARIES cpptrace::cpptrace + COMPILE_OUTPUT_VARIABLE CPPTRACE_SIGNAL_SAFE_UNWIND_LOG + CXX_STANDARD 20 + CXX_STANDARD_REQUIRED ON + ) + + if (NOT CPPTRACE_SIGNAL_SAFE_UNWIND_COMP) + message(STATUS "${CPPTRACE_SIGNAL_SAFE_UNWIND_LOG}") + message(FATAL_ERROR "Failed to compile cpptrace signal safe unwind tester.") + endif() + + if (NOT CPPTRACE_SIGNAL_SAFE_UNWIND EQUAL 0) + message(STATUS "Cpptrace signal safe unwind test exited with: ${CPPTRACE_SIGNAL_SAFE_UNWIND}") + message(FATAL_ERROR "Cpptrace was built without CPPTRACE_UNWIND_WITH_LIBUNWIND set to true. Enable libunwind support in the package or set VENDOR_CPPTRACE to true when building Quickshell.") + endif() + endif () +endif () # quick linked for pch compat -target_link_libraries(quickshell-crash PRIVATE quickshell-build Qt::Quick Qt::Widgets) +target_link_libraries(quickshell-crash PRIVATE quickshell-build Qt::Quick Qt::Widgets cpptrace::cpptrace) target_link_libraries(quickshell PRIVATE quickshell-crash) diff --git a/src/crash/handler.cpp b/src/crash/handler.cpp index 1433a87..c875c2e 100644 --- a/src/crash/handler.cpp +++ b/src/crash/handler.cpp @@ -1,12 +1,13 @@ #include "handler.hpp" +#include #include +#include +#include #include #include -#include -#include -#include -#include +#include +#include #include #include #include @@ -19,91 +20,70 @@ extern char** environ; // NOLINT -using namespace google_breakpad; - namespace qs::crash { namespace { + QS_LOGGING_CATEGORY(logCrashHandler, "quickshell.crashhandler", QtWarningMsg); -} -struct CrashHandlerPrivate { - ExceptionHandler* exceptionHandler = nullptr; - int minidumpFd = -1; - int infoFd = -1; +void writeEnvInt(char* buf, const char* name, int value) { + // NOLINTBEGIN (cppcoreguidelines-pro-bounds-pointer-arithmetic) + while (*name != '\0') *buf++ = *name++; + *buf++ = '='; - static bool minidumpCallback(const MinidumpDescriptor& descriptor, void* context, bool succeeded); -}; - -CrashHandler::CrashHandler(): d(new CrashHandlerPrivate()) {} - -void CrashHandler::init() { - // MinidumpDescriptor has no move constructor and the copy constructor breaks fds. - auto createHandler = [this](const MinidumpDescriptor& desc) { - this->d->exceptionHandler = new ExceptionHandler( - desc, - nullptr, - &CrashHandlerPrivate::minidumpCallback, - this->d, - true, - -1 - ); - }; - - qCDebug(logCrashHandler) << "Starting crash handler..."; - - this->d->minidumpFd = memfd_create("quickshell:minidump", MFD_CLOEXEC); - - if (this->d->minidumpFd == -1) { - qCCritical(logCrashHandler - ) << "Failed to allocate minidump memfd, minidumps will be saved in the working directory."; - createHandler(MinidumpDescriptor(".")); - } else { - qCDebug(logCrashHandler) << "Created memfd" << this->d->minidumpFd - << "for holding possible minidumps."; - createHandler(MinidumpDescriptor(this->d->minidumpFd)); + if (value < 0) { + *buf++ = '-'; + value = -value; } - qCInfo(logCrashHandler) << "Crash handler initialized."; -} - -void CrashHandler::setRelaunchInfo(const RelaunchInfo& info) { - this->d->infoFd = memfd_create("quickshell:instance_info", MFD_CLOEXEC); - - if (this->d->infoFd == -1) { - qCCritical(logCrashHandler - ) << "Failed to allocate instance info memfd, crash recovery will not work."; + if (value == 0) { + *buf++ = '0'; + *buf = '\0'; return; } - QFile file; - file.open(this->d->infoFd, QFile::ReadWrite); - - QDataStream ds(&file); - ds << info; - file.flush(); - - qCDebug(logCrashHandler) << "Stored instance info in memfd" << this->d->infoFd; -} - -CrashHandler::~CrashHandler() { - delete this->d->exceptionHandler; - delete this->d; -} - -bool CrashHandlerPrivate::minidumpCallback( - const MinidumpDescriptor& /*descriptor*/, - void* context, - bool /*success*/ -) { - // A fork that just dies to ensure the coredump is caught by the system. - auto coredumpPid = fork(); - - if (coredumpPid == 0) { - return false; + auto* start = buf; + while (value > 0) { + *buf++ = static_cast('0' + (value % 10)); + value /= 10; } - auto* self = static_cast(context); + *buf = '\0'; + std::reverse(start, buf); + // NOLINTEND +} + +void signalHandler( + int sig, + siginfo_t* /*info*/, // NOLINT (misc-include-cleaner) + void* /*context*/ +) { + if (CrashInfo::INSTANCE.traceFd != -1) { + auto traceBuffer = std::array(); + auto frameCount = cpptrace::safe_generate_raw_trace(traceBuffer.data(), traceBuffer.size(), 1); + + for (size_t i = 0; i < static_cast(frameCount); i++) { + auto frame = cpptrace::safe_object_frame(); + cpptrace::get_safe_object_frame(traceBuffer[i], &frame); + + auto* wptr = reinterpret_cast(&frame); + auto* end = wptr + sizeof(cpptrace::safe_object_frame); // NOLINT + while (wptr != end) { + auto r = write(CrashInfo::INSTANCE.traceFd, &frame, sizeof(cpptrace::safe_object_frame)); + if (r < 0 && errno == EINTR) continue; + if (r <= 0) goto fail; + wptr += r; // NOLINT + } + } + + fail:; + } + + auto coredumpPid = fork(); + if (coredumpPid == 0) { + raise(sig); + _exit(-1); + } auto exe = std::array(); if (readlink("/proc/self/exe", exe.data(), exe.size() - 1) == -1) { @@ -116,17 +96,19 @@ bool CrashHandlerPrivate::minidumpCallback( auto env = std::array(); auto envi = 0; - auto infoFd = dup(self->infoFd); - auto infoFdStr = std::array(); - memcpy(infoFdStr.data(), "__QUICKSHELL_CRASH_INFO_FD=-1" /*\0*/, 30); - if (infoFd != -1) my_uitos(&infoFdStr[27], infoFd, 10); + // dup to remove CLOEXEC + auto infoFdStr = std::array(); + writeEnvInt(infoFdStr.data(), "__QUICKSHELL_CRASH_INFO_FD", dup(CrashInfo::INSTANCE.infoFd)); env[envi++] = infoFdStr.data(); - auto corePidStr = std::array(); - memcpy(corePidStr.data(), "__QUICKSHELL_CRASH_DUMP_PID=-1" /*\0*/, 31); - if (coredumpPid != -1) my_uitos(&corePidStr[28], coredumpPid, 10); + auto corePidStr = std::array(); + writeEnvInt(corePidStr.data(), "__QUICKSHELL_CRASH_DUMP_PID", coredumpPid); env[envi++] = corePidStr.data(); + auto sigStr = std::array(); + writeEnvInt(sigStr.data(), "__QUICKSHELL_CRASH_SIGNAL", sig); + env[envi++] = sigStr.data(); + auto populateEnv = [&]() { auto senvi = 0; while (envi != 4095) { @@ -138,30 +120,18 @@ bool CrashHandlerPrivate::minidumpCallback( env[envi] = nullptr; }; - sigset_t sigset; - sigemptyset(&sigset); // NOLINT (include) - sigprocmask(SIG_SETMASK, &sigset, nullptr); // NOLINT - auto pid = fork(); if (pid == -1) { perror("Failed to fork and launch crash reporter.\n"); - return false; + _exit(-1); } else if (pid == 0) { + // dup to remove CLOEXEC - // if already -1 will return -1 - auto dumpFd = dup(self->minidumpFd); - auto logFd = dup(CrashInfo::INSTANCE.logFd); - - // allow up to 10 digits, which should never happen - auto dumpFdStr = std::array(); - auto logFdStr = std::array(); - - memcpy(dumpFdStr.data(), "__QUICKSHELL_CRASH_DUMP_FD=-1" /*\0*/, 30); - memcpy(logFdStr.data(), "__QUICKSHELL_CRASH_LOG_FD=-1" /*\0*/, 29); - - if (dumpFd != -1) my_uitos(&dumpFdStr[27], dumpFd, 10); - if (logFd != -1) my_uitos(&logFdStr[26], logFd, 10); + auto dumpFdStr = std::array(); + auto logFdStr = std::array(); + writeEnvInt(dumpFdStr.data(), "__QUICKSHELL_CRASH_DUMP_FD", dup(CrashInfo::INSTANCE.traceFd)); + writeEnvInt(logFdStr.data(), "__QUICKSHELL_CRASH_LOG_FD", dup(CrashInfo::INSTANCE.logFd)); env[envi++] = dumpFdStr.data(); env[envi++] = logFdStr.data(); @@ -178,8 +148,82 @@ bool CrashHandlerPrivate::minidumpCallback( perror("Failed to relaunch quickshell.\n"); _exit(-1); } +} - return false; // should make sure it hits the system coredump handler +} // namespace + +void CrashHandler::init() { + qCDebug(logCrashHandler) << "Starting crash handler..."; + + CrashInfo::INSTANCE.traceFd = memfd_create("quickshell:trace", MFD_CLOEXEC); + + if (CrashInfo::INSTANCE.traceFd == -1) { + qCCritical(logCrashHandler) << "Failed to allocate trace memfd, stack traces will not be " + "available in crash reports."; + } else { + qCDebug(logCrashHandler) << "Created memfd" << CrashInfo::INSTANCE.traceFd + << "for holding possible stack traces."; + } + + { + // Preload anything dynamically linked to avoid malloc etc in the dynamic loader. + // See cpptrace documentation for more information. + auto buffer = std::array(); + cpptrace::safe_generate_raw_trace(buffer.data(), buffer.size()); + auto frame = cpptrace::safe_object_frame(); + cpptrace::get_safe_object_frame(buffer[0], &frame); + } + + // NOLINTBEGIN (misc-include-cleaner) + + // Set up alternate signal stack for stack overflow handling + auto ss = stack_t(); + ss.ss_sp = new char[SIGSTKSZ]; + ss.ss_size = SIGSTKSZ; + ss.ss_flags = 0; + sigaltstack(&ss, nullptr); + + // Install signal handlers + struct sigaction sa {}; + sa.sa_sigaction = &signalHandler; + sa.sa_flags = SA_SIGINFO | SA_ONSTACK | SA_RESETHAND; + sigemptyset(&sa.sa_mask); + + sigaction(SIGSEGV, &sa, nullptr); + sigaction(SIGABRT, &sa, nullptr); + sigaction(SIGFPE, &sa, nullptr); + sigaction(SIGILL, &sa, nullptr); + sigaction(SIGBUS, &sa, nullptr); + sigaction(SIGTRAP, &sa, nullptr); + + // NOLINTEND (misc-include-cleaner) + + qCInfo(logCrashHandler) << "Crash handler initialized."; +} + +void CrashHandler::setRelaunchInfo(const RelaunchInfo& info) { + CrashInfo::INSTANCE.infoFd = memfd_create("quickshell:instance_info", MFD_CLOEXEC); + + if (CrashInfo::INSTANCE.infoFd == -1) { + qCCritical( + logCrashHandler + ) << "Failed to allocate instance info memfd, crash recovery will not work."; + return; + } + + QFile file; + + if (!file.open(CrashInfo::INSTANCE.infoFd, QFile::ReadWrite)) { + qCCritical( + logCrashHandler + ) << "Failed to open instance info memfd, crash recovery will not work."; + } + + QDataStream ds(&file); + ds << info; + file.flush(); + + qCDebug(logCrashHandler) << "Stored instance info in memfd" << CrashInfo::INSTANCE.infoFd; } } // namespace qs::crash diff --git a/src/crash/handler.hpp b/src/crash/handler.hpp index 2a1d86f..9488d71 100644 --- a/src/crash/handler.hpp +++ b/src/crash/handler.hpp @@ -5,19 +5,10 @@ #include "../core/instanceinfo.hpp" namespace qs::crash { -struct CrashHandlerPrivate; - class CrashHandler { public: - explicit CrashHandler(); - ~CrashHandler(); - Q_DISABLE_COPY_MOVE(CrashHandler); - - void init(); - void setRelaunchInfo(const RelaunchInfo& info); - -private: - CrashHandlerPrivate* d; + static void init(); + static void setRelaunchInfo(const RelaunchInfo& info); }; } // namespace qs::crash diff --git a/src/crash/interface.cpp b/src/crash/interface.cpp index c633440..a3422d3 100644 --- a/src/crash/interface.cpp +++ b/src/crash/interface.cpp @@ -66,7 +66,8 @@ CrashReporterGui::CrashReporterGui(QString reportFolder, int pid) mainLayout->addSpacing(textHeight); if (qtVersionMatches) { - mainLayout->addWidget(new QLabel("Please open a bug report for this issue via github or email.") + mainLayout->addWidget( + new QLabel("Please open a bug report for this issue via github or email.") ); } else { mainLayout->addWidget(new QLabel( @@ -77,7 +78,7 @@ CrashReporterGui::CrashReporterGui(QString reportFolder, int pid) mainLayout->addWidget(new ReportLabel( "Github:", - "https://github.com/quickshell-mirror/quickshell/issues/new?template=crash.yml", + "https://github.com/quickshell-mirror/quickshell/issues/new?template=crash2.yml", this )); @@ -113,7 +114,7 @@ void CrashReporterGui::openFolder() { void CrashReporterGui::openReportUrl() { QDesktopServices::openUrl( - QUrl("https://github.com/outfoxxed/quickshell/issues/new?template=crash.yml") + QUrl("https://github.com/outfoxxed/quickshell/issues/new?template=crash2.yml") ); } diff --git a/src/crash/main.cpp b/src/crash/main.cpp index b9f0eab..c406ba6 100644 --- a/src/crash/main.cpp +++ b/src/crash/main.cpp @@ -1,7 +1,10 @@ #include "main.hpp" #include #include +#include +#include +#include #include #include #include @@ -13,13 +16,17 @@ #include #include #include +#include #include #include +#include #include "../core/instanceinfo.hpp" #include "../core/logcat.hpp" #include "../core/logging.hpp" +#include "../core/logging_p.hpp" #include "../core/paths.hpp" +#include "../core/ringbuf.hpp" #include "build.hpp" #include "interface.hpp" @@ -61,6 +68,76 @@ int tryDup(int fd, const QString& path) { return 0; } +QString readRecentLogs(int logFd, int maxLines, qint64 maxAgeSecs) { + QFile file; + if (!file.open(logFd, QFile::ReadOnly, QFile::AutoCloseHandle)) { + return QStringLiteral("(failed to open log fd)\n"); + } + + file.seek(0); + + qs::log::EncodedLogReader reader; + reader.setDevice(&file); + + bool readable = false; + quint8 logVersion = 0; + quint8 readerVersion = 0; + if (!reader.readHeader(&readable, &logVersion, &readerVersion) || !readable) { + return QStringLiteral("(failed to read log header)\n"); + } + + // Read all messages, keeping last maxLines in a ring buffer + auto tail = RingBuffer(maxLines); + qs::log::LogMessage message; + while (reader.read(&message)) { + tail.emplace(message); + } + + if (tail.size() == 0) { + return QStringLiteral("(no logs)\n"); + } + + // Filter to only messages within maxAgeSecs of the newest message + auto cutoff = tail.at(0).time.addSecs(-maxAgeSecs); + + QString result; + auto stream = QTextStream(&result); + for (auto i = tail.size() - 1; i != -1; i--) { + if (tail.at(i).time < cutoff) continue; + qs::log::LogMessage::formatMessage(stream, tail.at(i), false, true); + stream << '\n'; + } + + if (result.isEmpty()) { + return QStringLiteral("(no recent logs)\n"); + } + + return result; +} + +cpptrace::stacktrace resolveStacktrace(int dumpFd) { + QFile sourceFile; + if (!sourceFile.open(dumpFd, QFile::ReadOnly, QFile::AutoCloseHandle)) { + qCCritical(logCrashReporter) << "Failed to open trace memfd."; + return {}; + } + + sourceFile.seek(0); + auto data = sourceFile.readAll(); + + auto frameCount = static_cast(data.size()) / sizeof(cpptrace::safe_object_frame); + if (frameCount == 0) return {}; + + const auto* frames = reinterpret_cast(data.constData()); + + cpptrace::object_trace objectTrace; + for (size_t i = 0; i < frameCount; i++) { + objectTrace.frames.push_back(frames[i].resolve()); // NOLINT + } + + return objectTrace.resolve(); +} + void recordCrashInfo(const QDir& crashDir, const InstanceInfo& instance) { qCDebug(logCrashReporter) << "Recording crash information at" << crashDir.path(); @@ -71,32 +148,25 @@ void recordCrashInfo(const QDir& crashDir, const InstanceInfo& instance) { } auto crashProc = qEnvironmentVariable("__QUICKSHELL_CRASH_DUMP_PID").toInt(); + auto crashSignal = qEnvironmentVariable("__QUICKSHELL_CRASH_SIGNAL").toInt(); auto dumpFd = qEnvironmentVariable("__QUICKSHELL_CRASH_DUMP_FD").toInt(); auto logFd = qEnvironmentVariable("__QUICKSHELL_CRASH_LOG_FD").toInt(); - qCDebug(logCrashReporter) << "Saving minidump from fd" << dumpFd; - auto dumpDupStatus = tryDup(dumpFd, crashDir.filePath("minidump.dmp.log")); - if (dumpDupStatus != 0) { - qCCritical(logCrashReporter) << "Failed to write minidump:" << dumpDupStatus; - } + qCDebug(logCrashReporter) << "Resolving stacktrace from fd" << dumpFd; + auto stacktrace = resolveStacktrace(dumpFd); - qCDebug(logCrashReporter) << "Saving log from fd" << logFd; - auto logDupStatus = tryDup(logFd, crashDir.filePath("log.qslog.log")); + qCDebug(logCrashReporter) << "Reading recent log lines from fd" << logFd; + auto logDupFd = dup(logFd); + auto recentLogs = readRecentLogs(logFd, 100, 10); + + qCDebug(logCrashReporter) << "Saving log from fd" << logDupFd; + auto logDupStatus = tryDup(logDupFd, crashDir.filePath("log.qslog.log")); if (logDupStatus != 0) { qCCritical(logCrashReporter) << "Failed to save log:" << logDupStatus; } - auto copyBinStatus = 0; - if (!DISTRIBUTOR_DEBUGINFO_AVAILABLE) { - qCDebug(logCrashReporter) << "Copying binary to crash folder"; - if (!QFile(QCoreApplication::applicationFilePath()).copy(crashDir.filePath("executable.txt"))) { - copyBinStatus = 1; - qCCritical(logCrashReporter) << "Failed to copy binary."; - } - } - { - auto extraInfoFile = QFile(crashDir.filePath("info.txt")); + auto extraInfoFile = QFile(crashDir.filePath("report.txt")); if (!extraInfoFile.open(QFile::WriteOnly)) { qCCritical(logCrashReporter) << "Failed to open crash info file for writing."; } else { @@ -111,16 +181,12 @@ void recordCrashInfo(const QDir& crashDir, const InstanceInfo& instance) { stream << "\n===== Runtime Information =====\n"; stream << "Runtime Qt Version: " << qVersion() << '\n'; + stream << "Signal: " << strsignal(crashSignal) << " (" << crashSignal << ")\n"; // NOLINT stream << "Crashed process ID: " << crashProc << '\n'; stream << "Run ID: " << instance.instanceId << '\n'; stream << "Shell ID: " << instance.shellId << '\n'; stream << "Config Path: " << instance.configPath << '\n'; - stream << "\n===== Report Integrity =====\n"; - stream << "Minidump save status: " << dumpDupStatus << '\n'; - stream << "Log save status: " << logDupStatus << '\n'; - stream << "Binary copy status: " << copyBinStatus << '\n'; - stream << "\n===== System Information =====\n\n"; stream << "/etc/os-release:"; auto osReleaseFile = QFile("/etc/os-release"); @@ -140,6 +206,18 @@ void recordCrashInfo(const QDir& crashDir, const InstanceInfo& instance) { stream << "FAILED TO OPEN\n"; } + stream << "\n===== Stacktrace =====\n"; + if (stacktrace.empty()) { + stream << "(no trace available)\n"; + } else { + auto formatter = cpptrace::formatter().header(std::string()); + auto traceStr = formatter.format(stacktrace); + stream << QString::fromStdString(traceStr) << '\n'; + } + + stream << "\n===== Log Tail =====\n"; + stream << recentLogs; + extraInfoFile.close(); } } @@ -161,7 +239,10 @@ void qsCheckCrash(int argc, char** argv) { auto infoFd = qEnvironmentVariable("__QUICKSHELL_CRASH_INFO_FD").toInt(); QFile file; - file.open(infoFd, QFile::ReadOnly, QFile::AutoCloseHandle); + if (!file.open(infoFd, QFile::ReadOnly, QFile::AutoCloseHandle)) { + qFatal() << "Failed to open instance info fd."; + } + file.seek(0); auto ds = QDataStream(&file); diff --git a/src/dbus/dbusmenu/dbusmenu.cpp b/src/dbus/dbusmenu/dbusmenu.cpp index c0b4386..bcb354d 100644 --- a/src/dbus/dbusmenu/dbusmenu.cpp +++ b/src/dbus/dbusmenu/dbusmenu.cpp @@ -183,7 +183,7 @@ void DBusMenuItem::updateProperties(const QVariantMap& properties, const QString } } else if (removed.isEmpty() || removed.contains("icon-data")) { imageChanged = this->image.hasData(); - image.data.clear(); + this->image.data.clear(); } auto type = properties.value("type"); @@ -312,8 +312,8 @@ void DBusMenu::prepareToShow(qint32 item, qint32 depth) { auto responseCallback = [this, item, depth](QDBusPendingCallWatcher* call) { const QDBusPendingReply reply = *call; if (reply.isError()) { - qCWarning(logDbusMenu) << "Error in AboutToShow, but showing anyway for menu" << item << "of" - << this << reply.error(); + qCDebug(logDbusMenu) << "Error in AboutToShow, but showing anyway for menu" << item << "of" + << this << reply.error(); } this->updateLayout(item, depth); diff --git a/src/dbus/dbusmenu/dbusmenu.hpp b/src/dbus/dbusmenu/dbusmenu.hpp index 1192baa..06cbc34 100644 --- a/src/dbus/dbusmenu/dbusmenu.hpp +++ b/src/dbus/dbusmenu/dbusmenu.hpp @@ -36,7 +36,7 @@ class DBusMenuPngImage: public QsIndexedImageHandle { public: explicit DBusMenuPngImage(): QsIndexedImageHandle(QQuickImageProvider::Image) {} - [[nodiscard]] bool hasData() const { return !data.isEmpty(); } + [[nodiscard]] bool hasData() const { return !this->data.isEmpty(); } QImage requestImage(const QString& id, QSize* size, const QSize& requestedSize) override; QByteArray data; diff --git a/src/dbus/properties.cpp b/src/dbus/properties.cpp index 81f26d2..2c478ef 100644 --- a/src/dbus/properties.cpp +++ b/src/dbus/properties.cpp @@ -214,8 +214,10 @@ void DBusPropertyGroup::updatePropertySet(const QVariantMap& properties, bool co } } -void DBusPropertyGroup::tryUpdateProperty(DBusPropertyCore* property, const QVariant& variant) - const { +void DBusPropertyGroup::tryUpdateProperty( + DBusPropertyCore* property, + const QVariant& variant +) const { property->mExists = true; auto error = property->store(variant); @@ -246,8 +248,13 @@ void DBusPropertyGroup::requestPropertyUpdate(DBusPropertyCore* property) { const QDBusPendingReply reply = *call; if (reply.isError()) { - qCWarning(logDbusProperties).noquote() << "Error updating property" << propStr; - qCWarning(logDbusProperties) << reply.error(); + if (!property->isRequired() && reply.error().type() == QDBusError::InvalidArgs) { + qCDebug(logDbusProperties) << "Error updating non-required property" << propStr; + qCDebug(logDbusProperties) << reply.error(); + } else { + qCWarning(logDbusProperties).noquote() << "Error updating property" << propStr; + qCWarning(logDbusProperties) << reply.error(); + } } else { this->tryUpdateProperty(property, reply.value().variant()); } diff --git a/src/dbus/properties.hpp b/src/dbus/properties.hpp index a5fce98..1596cb7 100644 --- a/src/dbus/properties.hpp +++ b/src/dbus/properties.hpp @@ -168,9 +168,9 @@ class DBusBindableProperty: public DBusPropertyCore { public: explicit DBusBindableProperty() { this->group()->attachProperty(this); } - [[nodiscard]] QString name() const override { return Name; }; - [[nodiscard]] QStringView nameRef() const override { return Name; }; - [[nodiscard]] bool isRequired() const override { return required; }; + [[nodiscard]] QString name() const override { return Name; } + [[nodiscard]] QStringView nameRef() const override { return Name; } + [[nodiscard]] bool isRequired() const override { return required; } [[nodiscard]] QString valueString() override { QString str; @@ -217,7 +217,7 @@ protected: private: [[nodiscard]] constexpr Owner* owner() const { - auto* self = std::bit_cast(this); + auto* self = std::bit_cast(this); // NOLINT return std::bit_cast(self - offset()); // NOLINT } diff --git a/src/debug/lint.cpp b/src/debug/lint.cpp index dd65a28..5e12f76 100644 --- a/src/debug/lint.cpp +++ b/src/debug/lint.cpp @@ -9,7 +9,7 @@ #include #include #include -#include +#include #include "../core/logcat.hpp" diff --git a/src/io/CMakeLists.txt b/src/io/CMakeLists.txt index 8b5c20a..991beaa 100644 --- a/src/io/CMakeLists.txt +++ b/src/io/CMakeLists.txt @@ -21,9 +21,10 @@ qt_add_qml_module(quickshell-io FileView.qml ) +qs_add_module_deps_light(quickshell-io Quickshell) install_qml_module(quickshell-io) -target_link_libraries(quickshell-io PRIVATE Qt::Quick) +target_link_libraries(quickshell-io PRIVATE Qt::Quick quickshell-ipc) target_link_libraries(quickshell PRIVATE quickshell-ioplugin) qs_module_pch(quickshell-io) diff --git a/src/io/datastream.hpp b/src/io/datastream.hpp index d83e571..b91ec04 100644 --- a/src/io/datastream.hpp +++ b/src/io/datastream.hpp @@ -55,7 +55,7 @@ public: // the buffer will be sent in both slots if there is data remaining from a previous parser virtual void parseBytes(QByteArray& incoming, QByteArray& buffer) = 0; - virtual void streamEnded(QByteArray& /*buffer*/) {}; + virtual void streamEnded(QByteArray& /*buffer*/) {} signals: /// Emitted when data is read from the stream. @@ -63,7 +63,7 @@ signals: }; ///! DataStreamParser for delimited data streams. -/// DataStreamParser for delimited data streams. @@read() is emitted once per delimited chunk of the stream. +/// DataStreamParser for delimited data streams. @@DataStreamParser.read(s) is emitted once per delimited chunk of the stream. class SplitParser: public DataStreamParser { Q_OBJECT; /// The delimiter for parsed data. May be multiple characters. Defaults to `\n`. diff --git a/src/io/fileview.cpp b/src/io/fileview.cpp index 1585f26..04d77bd 100644 --- a/src/io/fileview.cpp +++ b/src/io/fileview.cpp @@ -93,7 +93,8 @@ void FileViewReader::run() { FileViewReader::read(this->owner, this->state, this->doStringConversion, this->shouldCancel); if (this->shouldCancel.loadAcquire()) { - qCDebug(logFileView) << "Read" << this << "of" << state.path << "canceled for" << this->owner; + qCDebug(logFileView) << "Read" << this << "of" << this->state.path << "canceled for" + << this->owner; } } @@ -206,7 +207,7 @@ void FileViewWriter::run() { FileViewWriter::write(this->owner, this->state, this->doAtomicWrite, this->shouldCancel); if (this->shouldCancel.loadAcquire()) { - qCDebug(logFileView) << "Write" << this << "of" << state.path << "canceled for" + qCDebug(logFileView) << "Write" << this << "of" << this->state.path << "canceled for" << this->owner; } } diff --git a/src/io/ipc.cpp b/src/io/ipc.cpp index 768299e..c381567 100644 --- a/src/io/ipc.cpp +++ b/src/io/ipc.cpp @@ -190,6 +190,14 @@ QString WirePropertyDefinition::toString() const { return "property " % this->name % ": " % this->type; } +QString WireSignalDefinition::toString() const { + if (this->rettype.isEmpty()) { + return "signal " % this->name % "()"; + } else { + return "signal " % this->name % "(" % this->retname % ": " % this->rettype % ')'; + } +} + QString WireTargetDefinition::toString() const { QString accum = "target " % this->name; @@ -201,6 +209,10 @@ QString WireTargetDefinition::toString() const { accum += "\n " % prop.toString(); } + for (const auto& sig: this->signalFunctions) { + accum += "\n " % sig.toString(); + } + return accum; } diff --git a/src/io/ipc.hpp b/src/io/ipc.hpp index d2b865a..32486d6 100644 --- a/src/io/ipc.hpp +++ b/src/io/ipc.hpp @@ -146,14 +146,31 @@ struct WirePropertyDefinition { DEFINE_SIMPLE_DATASTREAM_OPS(WirePropertyDefinition, data.name, data.type); -struct WireTargetDefinition { +struct WireSignalDefinition { QString name; - QVector functions; - QVector properties; + QString retname; + QString rettype; [[nodiscard]] QString toString() const; }; -DEFINE_SIMPLE_DATASTREAM_OPS(WireTargetDefinition, data.name, data.functions, data.properties); +DEFINE_SIMPLE_DATASTREAM_OPS(WireSignalDefinition, data.name, data.retname, data.rettype); + +struct WireTargetDefinition { + QString name; + QVector functions; + QVector properties; + QVector signalFunctions; + + [[nodiscard]] QString toString() const; +}; + +DEFINE_SIMPLE_DATASTREAM_OPS( + WireTargetDefinition, + data.name, + data.functions, + data.properties, + data.signalFunctions +); } // namespace qs::io::ipc diff --git a/src/io/ipccomm.cpp b/src/io/ipccomm.cpp index 7203a30..03b688a 100644 --- a/src/io/ipccomm.cpp +++ b/src/io/ipccomm.cpp @@ -1,10 +1,11 @@ #include "ipccomm.hpp" -#include +#include #include #include #include #include +#include #include #include @@ -19,10 +20,6 @@ using namespace qs::ipc; namespace qs::io::ipc::comm { -struct NoCurrentGeneration: std::monostate {}; -struct TargetNotFound: std::monostate {}; -struct EntryNotFound: std::monostate {}; - using QueryResponse = std::variant< std::monostate, NoCurrentGeneration, @@ -314,4 +311,106 @@ int getProperty(IpcClient* client, const QString& target, const QString& propert return -1; } +int listenToSignal(IpcClient* client, const QString& target, const QString& signal, bool once) { + if (target.isEmpty()) { + qCCritical(logBare) << "Target required to listen for signals."; + return -1; + } else if (signal.isEmpty()) { + qCCritical(logBare) << "Signal required to listen."; + return -1; + } + + client->sendMessage(IpcCommand(SignalListenCommand {.target = target, .signal = signal})); + + while (true) { + SignalListenResponse slot; + if (!client->waitForResponse(slot)) return -1; + + if (std::holds_alternative(slot)) { + auto& result = std::get(slot); + QTextStream(stdout) << result.response << Qt::endl; + if (once) return 0; + else continue; + } else if (std::holds_alternative(slot)) { + qCCritical(logBare) << "Target not found."; + } else if (std::holds_alternative(slot)) { + qCCritical(logBare) << "Signal not found."; + } else if (std::holds_alternative(slot)) { + qCCritical(logBare) << "Not ready to accept queries yet."; + } else { + qCCritical(logIpc) << "Received invalid IPC response from" << client; + } + break; + } + + return -1; +} + +void SignalListenCommand::exec(qs::ipc::IpcServerConnection* conn) { + auto resp = conn->responseStream(); + + if (auto* generation = EngineGeneration::currentGeneration()) { + auto* registry = IpcHandlerRegistry::forGeneration(generation); + + auto* handler = registry->findHandler(this->target); + if (!handler) { + resp << TargetNotFound(); + return; + } + + auto* signal = handler->findSignal(this->signal); + if (!signal) { + resp << EntryNotFound(); + return; + } + + new RemoteSignalListener(conn, *this); + } else { + conn->respond(SignalListenResponse(NoCurrentGeneration())); + } +} + +RemoteSignalListener::RemoteSignalListener( + qs::ipc::IpcServerConnection* conn, + SignalListenCommand command +) + : conn(conn) + , command(std::move(command)) { + conn->setParent(this); + + QObject::connect( + IpcSignalRemoteListener::instance(), + &IpcSignalRemoteListener::triggered, + this, + &RemoteSignalListener::onSignal + ); + + QObject::connect( + conn, + &qs::ipc::IpcServerConnection::destroyed, + this, + &RemoteSignalListener::onConnDestroyed + ); + + qCDebug(logIpc) << "Remote listener created for" << this->command.target << this->command.signal + << ":" << this; +} + +RemoteSignalListener::~RemoteSignalListener() { + qCDebug(logIpc) << "Destroying remote listener" << this; +} + +void RemoteSignalListener::onSignal( + const QString& target, + const QString& signal, + const QString& value +) { + if (target != this->command.target || signal != this->command.signal) return; + qCDebug(logIpc) << "Remote signal" << signal << "triggered on" << target << "with value" << value; + + this->conn->respond(SignalListenResponse(SignalResponse {.response = value})); +} + +void RemoteSignalListener::onConnDestroyed() { this->deleteLater(); } + } // namespace qs::io::ipc::comm diff --git a/src/io/ipccomm.hpp b/src/io/ipccomm.hpp index bc7dbf9..ac12979 100644 --- a/src/io/ipccomm.hpp +++ b/src/io/ipccomm.hpp @@ -2,6 +2,8 @@ #include #include +#include +#include #include #include "../ipc/ipc.hpp" @@ -48,4 +50,52 @@ DEFINE_SIMPLE_DATASTREAM_OPS(StringPropReadCommand, data.target, data.property); int getProperty(qs::ipc::IpcClient* client, const QString& target, const QString& property); +struct SignalListenCommand { + QString target; + QString signal; + + void exec(qs::ipc::IpcServerConnection* conn); +}; + +DEFINE_SIMPLE_DATASTREAM_OPS(SignalListenCommand, data.target, data.signal); + +int listenToSignal( + qs::ipc::IpcClient* client, + const QString& target, + const QString& signal, + bool once +); + +struct NoCurrentGeneration: std::monostate {}; +struct TargetNotFound: std::monostate {}; +struct EntryNotFound: std::monostate {}; + +struct SignalResponse { + QString response; +}; + +DEFINE_SIMPLE_DATASTREAM_OPS(SignalResponse, data.response); + +using SignalListenResponse = std:: + variant; + +class RemoteSignalListener: public QObject { + Q_OBJECT; + +public: + explicit RemoteSignalListener(qs::ipc::IpcServerConnection* conn, SignalListenCommand command); + + ~RemoteSignalListener() override; + + Q_DISABLE_COPY_MOVE(RemoteSignalListener); + +private slots: + void onSignal(const QString& target, const QString& signal, const QString& value); + void onConnDestroyed(); + +private: + qs::ipc::IpcServerConnection* conn; + SignalListenCommand command; +}; + } // namespace qs::io::ipc::comm diff --git a/src/io/ipchandler.cpp b/src/io/ipchandler.cpp index 5ffa0ad..e80cf4b 100644 --- a/src/io/ipchandler.cpp +++ b/src/io/ipchandler.cpp @@ -1,5 +1,7 @@ #include "ipchandler.hpp" #include +#include +#include #include #include @@ -139,6 +141,75 @@ WirePropertyDefinition IpcProperty::wireDef() const { return wire; } +WireSignalDefinition IpcSignal::wireDef() const { + WireSignalDefinition wire; + wire.name = this->signal.name(); + if (this->targetSlot != IpcSignalListener::SLOT_VOID) { + wire.retname = this->signal.parameterNames().value(0); + if (this->targetSlot == IpcSignalListener::SLOT_STRING) wire.rettype = "string"; + else if (this->targetSlot == IpcSignalListener::SLOT_INT) wire.rettype = "int"; + else if (this->targetSlot == IpcSignalListener::SLOT_BOOL) wire.rettype = "bool"; + else if (this->targetSlot == IpcSignalListener::SLOT_REAL) wire.rettype = "real"; + else if (this->targetSlot == IpcSignalListener::SLOT_COLOR) wire.rettype = "color"; + } + return wire; +} + +// NOLINTBEGIN (cppcoreguidelines-interfaces-global-init) +// clang-format off +const int IpcSignalListener::SLOT_VOID = IpcSignalListener::staticMetaObject.indexOfSlot("invokeVoid()"); +const int IpcSignalListener::SLOT_STRING = IpcSignalListener::staticMetaObject.indexOfSlot("invokeString(QString)"); +const int IpcSignalListener::SLOT_INT = IpcSignalListener::staticMetaObject.indexOfSlot("invokeInt(int)"); +const int IpcSignalListener::SLOT_BOOL = IpcSignalListener::staticMetaObject.indexOfSlot("invokeBool(bool)"); +const int IpcSignalListener::SLOT_REAL = IpcSignalListener::staticMetaObject.indexOfSlot("invokeReal(double)"); +const int IpcSignalListener::SLOT_COLOR = IpcSignalListener::staticMetaObject.indexOfSlot("invokeColor(QColor)"); +// clang-format on +// NOLINTEND + +bool IpcSignal::resolve(QString& error) { + if (this->signal.parameterCount() > 1) { + error = "Due to technical limitations, IPC signals can have at most one argument."; + return false; + } + + auto slot = IpcSignalListener::SLOT_VOID; + + if (this->signal.parameterCount() == 1) { + auto paramType = this->signal.parameterType(0); + if (paramType == QMetaType::QString) slot = IpcSignalListener::SLOT_STRING; + else if (paramType == QMetaType::Int) slot = IpcSignalListener::SLOT_INT; + else if (paramType == QMetaType::Bool) slot = IpcSignalListener::SLOT_BOOL; + else if (paramType == QMetaType::Double) slot = IpcSignalListener::SLOT_REAL; + else if (paramType == QMetaType::QColor) slot = IpcSignalListener::SLOT_COLOR; + else { + error = QString("Type of argument (%2: %3) cannot be used across IPC.") + .arg(this->signal.parameterNames().value(0)) + .arg(QMetaType(paramType).name()); + + return false; + } + } + + this->targetSlot = slot; + return true; +} + +void IpcSignal::connectListener(IpcHandler* handler) { + if (this->targetSlot == -1) { + qFatal() << "Tried to connect unresolved IPC signal"; + } + + this->listener = std::make_shared(this->signal.name()); + QMetaObject::connect(handler, this->signal.methodIndex(), this->listener.get(), this->targetSlot); + + QObject::connect( + this->listener.get(), + &IpcSignalListener::triggered, + handler, + &IpcHandler::onSignalTriggered + ); +} + IpcCallStorage::IpcCallStorage(const IpcFunction& function): returnSlot(function.returnType) { for (const auto& arg: function.argumentTypes) { this->argumentSlots.emplace_back(arg); @@ -172,16 +243,28 @@ void IpcHandler::onPostReload() { // which should handle inheritance on the qml side. for (auto i = smeta.methodCount(); i != meta->methodCount(); i++) { const auto& method = meta->method(i); - if (method.methodType() != QMetaMethod::Slot) continue; + if (method.methodType() == QMetaMethod::Slot) { + auto ipcFunc = IpcFunction(method); + QString error; - auto ipcFunc = IpcFunction(method); - QString error; + if (!ipcFunc.resolve(error)) { + qmlWarning(this).nospace().noquote() + << "Error parsing function \"" << method.name() << "\": " << error; + } else { + this->functionMap.insert(method.name(), ipcFunc); + } + } else if (method.methodType() == QMetaMethod::Signal) { + qmlDebug(this) << "Signal detected: " << method.name(); + auto ipcSig = IpcSignal(method); + QString error; - if (!ipcFunc.resolve(error)) { - qmlWarning(this).nospace().noquote() - << "Error parsing function \"" << method.name() << "\": " << error; - } else { - this->functionMap.insert(method.name(), ipcFunc); + if (!ipcSig.resolve(error)) { + qmlWarning(this).nospace().noquote() + << "Error parsing signal \"" << method.name() << "\": " << error; + } else { + ipcSig.connectListener(this); + this->signalMap.emplace(method.name(), std::move(ipcSig)); + } } } @@ -222,6 +305,11 @@ IpcHandlerRegistry* IpcHandlerRegistry::forGeneration(EngineGeneration* generati return dynamic_cast(ext); } +void IpcHandler::onSignalTriggered(const QString& signal, const QString& value) const { + emit IpcSignalRemoteListener::instance() + -> triggered(this->registeredState.target, signal, value); +} + void IpcHandler::updateRegistration(bool destroying) { if (!this->complete) return; @@ -324,6 +412,10 @@ WireTargetDefinition IpcHandler::wireDef() const { wire.properties += prop.wireDef(); } + for (const auto& sig: this->signalMap.values()) { + wire.signalFunctions += sig.wireDef(); + } + return wire; } @@ -368,6 +460,13 @@ IpcProperty* IpcHandler::findProperty(const QString& name) { else return &*itr; } +IpcSignal* IpcHandler::findSignal(const QString& name) { + auto itr = this->signalMap.find(name); + + if (itr == this->signalMap.end()) return nullptr; + else return &*itr; +} + IpcHandler* IpcHandlerRegistry::findHandler(const QString& target) { return this->handlers.value(target); } @@ -382,4 +481,9 @@ QVector IpcHandlerRegistry::wireTargets() const { return wire; } +IpcSignalRemoteListener* IpcSignalRemoteListener::instance() { + static auto* instance = new IpcSignalRemoteListener(); + return instance; +} + } // namespace qs::io::ipc diff --git a/src/io/ipchandler.hpp b/src/io/ipchandler.hpp index 1da3e71..eb274e3 100644 --- a/src/io/ipchandler.hpp +++ b/src/io/ipchandler.hpp @@ -1,8 +1,10 @@ #pragma once #include +#include #include +#include #include #include #include @@ -67,6 +69,54 @@ public: const IpcType* type = nullptr; }; +class IpcSignalListener: public QObject { + Q_OBJECT; + +public: + IpcSignalListener(QString signal): signal(std::move(signal)) {} + + static const int SLOT_VOID; + static const int SLOT_STRING; + static const int SLOT_INT; + static const int SLOT_BOOL; + static const int SLOT_REAL; + static const int SLOT_COLOR; + +signals: + void triggered(const QString& signal, const QString& value); + +private slots: + void invokeVoid() { this->triggered(this->signal, "void"); } + void invokeString(const QString& value) { this->triggered(this->signal, value); } + void invokeInt(int value) { this->triggered(this->signal, QString::number(value)); } + void invokeBool(bool value) { this->triggered(this->signal, value ? "true" : "false"); } + void invokeReal(double value) { this->triggered(this->signal, QString::number(value)); } + void invokeColor(QColor value) { this->triggered(this->signal, value.name(QColor::HexArgb)); } + +private: + QString signal; +}; + +class IpcHandler; + +class IpcSignal { +public: + explicit IpcSignal(QMetaMethod signal): signal(signal) {} + + bool resolve(QString& error); + + [[nodiscard]] WireSignalDefinition wireDef() const; + + QMetaMethod signal; + int targetSlot = -1; + + void connectListener(IpcHandler* handler); + +private: + void connectListener(QObject* handler, IpcSignalListener* listener) const; + std::shared_ptr listener; +}; + class IpcHandlerRegistry; ///! Handler for IPC message calls. @@ -100,6 +150,11 @@ class IpcHandlerRegistry; /// - `real` will be converted to a string and returned. /// - `color` will be converted to a hex string in the form `#AARRGGBB` and returned. /// +/// #### Signals +/// IPC handler signals can be observed remotely using `qs ipc wait` (one call) +/// and `qs ipc listen` (many calls). IPC signals may have zero or one argument, where +/// the argument is one of the types listed above, or no arguments for void. +/// /// #### Example /// The following example creates ipc functions to control and retrieve the appearance /// of a Rectangle. @@ -119,10 +174,18 @@ class IpcHandlerRegistry; /// /// function setColor(color: color): void { rect.color = color; } /// function getColor(): color { return rect.color; } +/// /// function setAngle(angle: real): void { rect.rotation = angle; } /// function getAngle(): real { return rect.rotation; } -/// function setRadius(radius: int): void { rect.radius = radius; } +/// +/// function setRadius(radius: int): void { +/// rect.radius = radius; +/// this.radiusChanged(radius); +/// } +/// /// function getRadius(): int { return rect.radius; } +/// +/// signal radiusChanged(newRadius: int); /// } /// } /// ``` @@ -136,6 +199,7 @@ class IpcHandlerRegistry; /// function getAngle(): real /// function setRadius(radius: int): void /// function getRadius(): int +/// signal radiusChanged(newRadius: int) /// ``` /// /// and then invoked using `qs ipc call`. @@ -164,7 +228,7 @@ class IpcHandler: public PostReloadHook { QML_ELEMENT; public: - explicit IpcHandler(QObject* parent = nullptr): PostReloadHook(parent) {}; + explicit IpcHandler(QObject* parent = nullptr): PostReloadHook(parent) {} ~IpcHandler() override; Q_DISABLE_COPY_MOVE(IpcHandler); @@ -179,14 +243,15 @@ public: QString listMembers(qsizetype indent); [[nodiscard]] IpcFunction* findFunction(const QString& name); [[nodiscard]] IpcProperty* findProperty(const QString& name); + [[nodiscard]] IpcSignal* findSignal(const QString& name); [[nodiscard]] WireTargetDefinition wireDef() const; signals: void enabledChanged(); void targetChanged(); -private slots: - //void handleIpcPropertyChange(); +public slots: + void onSignalTriggered(const QString& signal, const QString& value) const; private: void updateRegistration(bool destroying = false); @@ -204,6 +269,7 @@ private: QHash functionMap; QHash propertyMap; + QHash signalMap; friend class IpcHandlerRegistry; }; @@ -227,4 +293,14 @@ private: QHash> knownHandlers; }; +class IpcSignalRemoteListener: public QObject { + Q_OBJECT; + +public: + static IpcSignalRemoteListener* instance(); + +signals: + void triggered(const QString& target, const QString& signal, const QString& value); +}; + } // namespace qs::io::ipc diff --git a/src/io/jsonadapter.cpp b/src/io/jsonadapter.cpp index 80ac091..e80c6f2 100644 --- a/src/io/jsonadapter.cpp +++ b/src/io/jsonadapter.cpp @@ -44,7 +44,7 @@ void JsonAdapter::deserializeAdapter(const QByteArray& data) { this->deserializeRec(json.object(), this, &JsonAdapter::staticMetaObject); - for (auto* object: oldCreatedObjects) { + for (auto* object: this->oldCreatedObjects) { delete object; // FIXME: QMetaType::destroy? } @@ -56,7 +56,7 @@ void JsonAdapter::deserializeAdapter(const QByteArray& data) { void JsonAdapter::connectNotifiers() { auto notifySlot = JsonAdapter::staticMetaObject.indexOfSlot("onPropertyChanged()"); - connectNotifiersRec(notifySlot, this, &JsonAdapter::staticMetaObject); + this->connectNotifiersRec(notifySlot, this, &JsonAdapter::staticMetaObject); } void JsonAdapter::connectNotifiersRec(int notifySlot, QObject* obj, const QMetaObject* base) { @@ -71,7 +71,7 @@ void JsonAdapter::connectNotifiersRec(int notifySlot, QObject* obj, const QMetaO auto val = prop.read(obj); if (val.canView()) { auto* pobj = prop.read(obj).view(); - if (pobj) connectNotifiersRec(notifySlot, pobj, &JsonObject::staticMetaObject); + if (pobj) this->connectNotifiersRec(notifySlot, pobj, &JsonObject::staticMetaObject); } else if (val.canConvert>()) { auto listVal = val.value>(); @@ -79,7 +79,7 @@ void JsonAdapter::connectNotifiersRec(int notifySlot, QObject* obj, const QMetaO for (auto i = 0; i != len; i++) { auto* pobj = listVal.at(&listVal, i); - if (pobj) connectNotifiersRec(notifySlot, pobj, &JsonObject::staticMetaObject); + if (pobj) this->connectNotifiersRec(notifySlot, pobj, &JsonObject::staticMetaObject); } } } @@ -111,7 +111,7 @@ QJsonObject JsonAdapter::serializeRec(const QObject* obj, const QMetaObject* bas auto* pobj = val.view(); if (pobj) { - json.insert(prop.name(), serializeRec(pobj, &JsonObject::staticMetaObject)); + json.insert(prop.name(), this->serializeRec(pobj, &JsonObject::staticMetaObject)); } else { json.insert(prop.name(), QJsonValue::Null); } @@ -124,7 +124,7 @@ QJsonObject JsonAdapter::serializeRec(const QObject* obj, const QMetaObject* bas auto* pobj = listVal.at(&listVal, i); if (pobj) { - array.push_back(serializeRec(pobj, &JsonObject::staticMetaObject)); + array.push_back(this->serializeRec(pobj, &JsonObject::staticMetaObject)); } else { array.push_back(QJsonValue::Null); } @@ -178,8 +178,8 @@ void JsonAdapter::deserializeRec(const QJsonObject& json, QObject* obj, const QM currentValue->setParent(this); this->createdObjects.push_back(currentValue); - } else if (oldCreatedObjects.removeOne(currentValue)) { - createdObjects.push_back(currentValue); + } else if (this->oldCreatedObjects.removeOne(currentValue)) { + this->createdObjects.push_back(currentValue); } this->deserializeRec(jval.toObject(), currentValue, &JsonObject::staticMetaObject); @@ -212,8 +212,8 @@ void JsonAdapter::deserializeRec(const QJsonObject& json, QObject* obj, const QM if (jsonValue.isObject()) { if (isNew) { currentValue = lp.at(&lp, i); - if (oldCreatedObjects.removeOne(currentValue)) { - createdObjects.push_back(currentValue); + if (this->oldCreatedObjects.removeOne(currentValue)) { + this->createdObjects.push_back(currentValue); } } else { // FIXME: should be the type inside the QQmlListProperty but how can we get that? diff --git a/src/io/jsonadapter.hpp b/src/io/jsonadapter.hpp index a447c41..276d6a7 100644 --- a/src/io/jsonadapter.hpp +++ b/src/io/jsonadapter.hpp @@ -91,6 +91,7 @@ class JsonAdapter , public QQmlParserStatus { Q_OBJECT; QML_ELEMENT; + Q_INTERFACES(QQmlParserStatus); public: void classBegin() override {} diff --git a/src/io/process.hpp b/src/io/process.hpp index ab8763e..3c55745 100644 --- a/src/io/process.hpp +++ b/src/io/process.hpp @@ -102,7 +102,7 @@ class Process: public PostReloadHook { /// If the process is already running changing this property will affect the next /// started process. If the property has been changed after starting a process it will /// return the new value, not the one for the currently running process. - Q_PROPERTY(QHash environment READ environment WRITE setEnvironment NOTIFY environmentChanged); + Q_PROPERTY(QVariantHash environment READ environment WRITE setEnvironment NOTIFY environmentChanged); /// If the process's environment should be cleared prior to applying @@environment. /// Defaults to false. /// diff --git a/src/io/processcore.hpp b/src/io/processcore.hpp index 37ec409..8d566c9 100644 --- a/src/io/processcore.hpp +++ b/src/io/processcore.hpp @@ -13,7 +13,7 @@ namespace qs::io::process { class ProcessContext { Q_PROPERTY(QList command MEMBER command WRITE setCommand); - Q_PROPERTY(QHash environment MEMBER environment WRITE setEnvironment); + Q_PROPERTY(QVariantHash environment MEMBER environment WRITE setEnvironment); Q_PROPERTY(bool clearEnvironment MEMBER clearEnvironment WRITE setClearEnvironment); Q_PROPERTY(QString workingDirectory MEMBER workingDirectory WRITE setWorkingDirectory); Q_PROPERTY(bool unbindStdout MEMBER unbindStdout WRITE setUnbindStdout); diff --git a/src/ipc/ipc.cpp b/src/ipc/ipc.cpp index bf66801..4bfea4c 100644 --- a/src/ipc/ipc.cpp +++ b/src/ipc/ipc.cpp @@ -3,6 +3,7 @@ #include #include +#include #include #include #include @@ -36,7 +37,8 @@ void IpcServer::start() { auto path = run->filePath("ipc.sock"); new IpcServer(path); } else { - qCCritical(logIpc + qCCritical( + logIpc ) << "Could not start IPC server as the instance runtime path could not be created."; } } @@ -60,6 +62,7 @@ IpcServerConnection::IpcServerConnection(QLocalSocket* socket, IpcServer* server void IpcServerConnection::onDisconnected() { qCInfo(logIpc) << "IPC connection disconnected" << this; + this->deleteLater(); } void IpcServerConnection::onReadyRead() { @@ -83,6 +86,11 @@ void IpcServerConnection::onReadyRead() { ); if (!this->stream.commitTransaction()) return; + + // async connections reparent + if (dynamic_cast(this->parent()) != nullptr) { + this->deleteLater(); + } } IpcClient::IpcClient(const QString& path) { @@ -120,7 +128,9 @@ int IpcClient::connect(const QString& id, const std::functionquit(); + auto* generation = EngineGeneration::currentGeneration(); + if (generation) generation->quit(); + else QCoreApplication::exit(0); } } // namespace qs::ipc diff --git a/src/ipc/ipccommand.hpp b/src/ipc/ipccommand.hpp index b221b46..105ce1e 100644 --- a/src/ipc/ipccommand.hpp +++ b/src/ipc/ipccommand.hpp @@ -16,6 +16,7 @@ using IpcCommand = std::variant< IpcKillCommand, qs::io::ipc::comm::QueryMetadataCommand, qs::io::ipc::comm::StringCallCommand, + qs::io::ipc::comm::SignalListenCommand, qs::io::ipc::comm::StringPropReadCommand>; } // namespace qs::ipc diff --git a/src/launch/command.cpp b/src/launch/command.cpp index 64eb076..151fc24 100644 --- a/src/launch/command.cpp +++ b/src/launch/command.cpp @@ -1,7 +1,6 @@ #include #include #include -#include #include #include @@ -13,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -89,9 +89,9 @@ int locateConfigFile(CommandState& cmd, QString& path) { } if (!manifestPath.isEmpty()) { - qWarning( - ) << "Config manifests (manifest.conf) are deprecated and will be removed in a future " - "release."; + qWarning() + << "Config manifests (manifest.conf) are deprecated and will be removed in a future " + "release."; qWarning() << "Consider using symlinks to a subfolder of quickshell's XDG config dirs."; auto file = QFile(manifestPath); @@ -109,7 +109,7 @@ int locateConfigFile(CommandState& cmd, QString& path) { } if (split[0].trimmed() == *cmd.config.name) { - path = QDir(QFileInfo(file).canonicalPath()).filePath(split[1].trimmed()); + path = QDir(QFileInfo(file).absolutePath()).filePath(split[1].trimmed()); break; } } @@ -129,7 +129,8 @@ int locateConfigFile(CommandState& cmd, QString& path) { if (path.isEmpty()) { if (name == "default") { - qCCritical(logBare + qCCritical( + logBare ) << "Could not find \"default\" config directory or shell.qml in any valid config path."; } else { qCCritical(logBare) << "Could not find" << name @@ -139,8 +140,7 @@ int locateConfigFile(CommandState& cmd, QString& path) { return -1; } - path = QFileInfo(path).canonicalFilePath(); - return 0; + goto rpath; } } @@ -153,7 +153,8 @@ int locateConfigFile(CommandState& cmd, QString& path) { return -1; } - path = QFileInfo(path).canonicalFilePath(); +rpath: + path = QFileInfo(path).absoluteFilePath(); return 0; } @@ -178,7 +179,8 @@ int selectInstance(CommandState& cmd, InstanceLockInfo* instance, bool deadFallb } } else if (!cmd.instance.id->isEmpty()) { path = basePath->filePath("by-pid"); - auto [liveInstances, deadInstances] = QsPaths::collectInstances(path); + auto [liveInstances, deadInstances] = + QsPaths::collectInstances(path, cmd.config.anyDisplay ? "" : getDisplayConnection()); liveInstances.removeIf([&](const InstanceLockInfo& info) { return !info.instance.instanceId.startsWith(*cmd.instance.id); @@ -228,7 +230,8 @@ int selectInstance(CommandState& cmd, InstanceLockInfo* instance, bool deadFallb path = QDir(basePath->filePath("by-path")).filePath(pathId); - auto [liveInstances, deadInstances] = QsPaths::collectInstances(path); + auto [liveInstances, deadInstances] = + QsPaths::collectInstances(path, cmd.config.anyDisplay ? "" : getDisplayConnection()); auto instances = liveInstances; if (instances.isEmpty() && deadFallback) { @@ -311,7 +314,10 @@ int listInstances(CommandState& cmd) { path = QDir(basePath->filePath("by-path")).filePath(pathId); } - auto [liveInstances, deadInstances] = QsPaths::collectInstances(path); + auto [liveInstances, deadInstances] = QsPaths::collectInstances( + path, + cmd.config.anyDisplay || cmd.instance.all ? "" : getDisplayConnection() + ); sortInstances(liveInstances, cmd.config.newest); @@ -373,6 +379,7 @@ int listInstances(CommandState& cmd) { << " Process ID: " << instance.instance.pid << '\n' << " Shell ID: " << instance.instance.shellId << '\n' << " Config path: " << instance.instance.configPath << '\n' + << " Display connection: " << instance.instance.display << '\n' << " Launch time: " << launchTimeStr << (isDead ? "" : " (running for " + runtimeStr + ")") << '\n' << (gray ? "\033[0m" : ""); @@ -404,6 +411,10 @@ int ipcCommand(CommandState& cmd) { return qs::io::ipc::comm::queryMetadata(&client, *cmd.ipc.target, *cmd.ipc.name); } else if (*cmd.ipc.getprop) { return qs::io::ipc::comm::getProperty(&client, *cmd.ipc.target, *cmd.ipc.name); + } else if (*cmd.ipc.wait) { + return qs::io::ipc::comm::listenToSignal(&client, *cmd.ipc.target, *cmd.ipc.name, true); + } else if (*cmd.ipc.listen) { + return qs::io::ipc::comm::listenToSignal(&client, *cmd.ipc.target, *cmd.ipc.name, false); } else { QVector arguments; for (auto& arg: cmd.ipc.arguments) { @@ -453,7 +464,7 @@ int runCommand(int argc, char** argv, QCoreApplication* coreApplication) { QTextStream(stdout) << "\033[31mCOMPATIBILITY WARNING: Quickshell was built against Qt " << QT_VERSION_STR << " but the system has updated to Qt " << qVersion() << " without rebuilding the package. This is likely to cause crashes, so " - "you must rebuild the quickshell package.\n"; + "you must rebuild the quickshell package.\n\033[0m"; return 1; } @@ -508,8 +519,8 @@ int runCommand(int argc, char** argv, QCoreApplication* coreApplication) { } if (state.misc.printVersion) { - qCInfo(logBare).noquote().nospace() - << "quickshell 0.1.0, revision " << GIT_REVISION << ", distributed by: " << DISTRIBUTOR; + qCInfo(logBare).noquote().nospace() << "quickshell " << QS_VERSION << ", revision " + << GIT_REVISION << ", distributed by: " << DISTRIBUTOR; if (state.log.verbosity > 1) { qCInfo(logBare).noquote() << "\nBuildtime Qt Version:" << QT_VERSION_STR; @@ -545,4 +556,18 @@ int runCommand(int argc, char** argv, QCoreApplication* coreApplication) { return 0; } +QString getDisplayConnection() { + auto platform = qEnvironmentVariable("QT_QPA_PLATFORM"); + auto wlDisplay = qEnvironmentVariable("WAYLAND_DISPLAY"); + auto xDisplay = qEnvironmentVariable("DISPLAY"); + + if (platform == "wayland" || (platform.isEmpty() && !wlDisplay.isEmpty())) { + return "wayland," + wlDisplay; + } else if (platform == "xcb" || (platform.isEmpty() && !xDisplay.isEmpty())) { + return "x11," + xDisplay; + } else { + return "unk," + QGuiApplication::platformName(); + } +} + } // namespace qs::launch diff --git a/src/launch/launch.cpp b/src/launch/launch.cpp index 91e2e24..ee7ca64 100644 --- a/src/launch/launch.cpp +++ b/src/launch/launch.cpp @@ -27,7 +27,7 @@ #include "build.hpp" #include "launch_p.hpp" -#if CRASH_REPORTER +#if CRASH_HANDLER #include "../crash/handler.hpp" #endif @@ -73,10 +73,12 @@ int launch(const LaunchArgs& args, char** argv, QCoreApplication* coreApplicatio bool useQApplication = false; bool nativeTextRendering = false; bool desktopSettingsAware = true; + bool useSystemStyle = false; QString iconTheme = qEnvironmentVariable("QS_ICON_THEME"); QHash envOverrides; QString dataDir; QString stateDir; + QString cacheDir; } pragmas; auto stream = QTextStream(&file); @@ -88,6 +90,7 @@ int launch(const LaunchArgs& args, char** argv, QCoreApplication* coreApplicatio if (pragma == "UseQApplication") pragmas.useQApplication = true; else if (pragma == "NativeTextRendering") pragmas.nativeTextRendering = true; else if (pragma == "IgnoreSystemSettings") pragmas.desktopSettingsAware = false; + else if (pragma == "RespectSystemStyle") pragmas.useSystemStyle = true; else if (pragma.startsWith("IconTheme ")) pragmas.iconTheme = pragma.sliced(10); else if (pragma.startsWith("Env ")) { auto envPragma = pragma.sliced(4); @@ -107,6 +110,8 @@ int launch(const LaunchArgs& args, char** argv, QCoreApplication* coreApplicatio pragmas.dataDir = pragma.sliced(8).trimmed(); } else if (pragma.startsWith("StateDir ")) { pragmas.stateDir = pragma.sliced(9).trimmed(); + } else if (pragma.startsWith("CacheDir ")) { + pragmas.cacheDir = pragma.sliced(9).trimmed(); } else { qCritical() << "Unrecognized pragma" << pragma; return -1; @@ -129,15 +134,15 @@ int launch(const LaunchArgs& args, char** argv, QCoreApplication* coreApplicatio .shellId = shellId, .launchTime = qs::Common::LAUNCH_TIME, .pid = getpid(), + .display = getDisplayConnection(), }; -#if CRASH_REPORTER - auto crashHandler = crash::CrashHandler(); - crashHandler.init(); +#if CRASH_HANDLER + crash::CrashHandler::init(); { auto* log = LogManager::instance(); - crashHandler.setRelaunchInfo({ + crash::CrashHandler::setRelaunchInfo({ .instance = InstanceInfo::CURRENT, .noColor = !log->colorLogs, .timestamp = log->timestampLogs, @@ -148,13 +153,18 @@ int launch(const LaunchArgs& args, char** argv, QCoreApplication* coreApplicatio } #endif - QsPaths::init(shellId, pathId, pragmas.dataDir, pragmas.stateDir); + QsPaths::init(shellId, pathId, pragmas.dataDir, pragmas.stateDir, pragmas.cacheDir); QsPaths::instance()->linkRunDir(); QsPaths::instance()->linkPathDir(); LogManager::initFs(); Common::INITIAL_ENVIRONMENT = QProcessEnvironment::systemEnvironment(); + if (!pragmas.useSystemStyle) { + qunsetenv("QT_STYLE_OVERRIDE"); + qputenv("QT_QUICK_CONTROLS_STYLE", "Fusion"); + } + for (auto [var, val]: pragmas.envOverrides.asKeyValueRange()) { qputenv(var.toUtf8(), val.toUtf8()); } diff --git a/src/launch/launch_p.hpp b/src/launch/launch_p.hpp index 7b8fca6..f666e7a 100644 --- a/src/launch/launch_p.hpp +++ b/src/launch/launch_p.hpp @@ -50,6 +50,7 @@ struct CommandState { QStringOption manifest; QStringOption name; bool newest = false; + bool anyDisplay = false; } config; struct { @@ -73,6 +74,8 @@ struct CommandState { CLI::App* show = nullptr; CLI::App* call = nullptr; CLI::App* getprop = nullptr; + CLI::App* wait = nullptr; + CLI::App* listen = nullptr; bool showOld = false; QStringOption target; QStringOption name; @@ -106,6 +109,8 @@ void exitDaemon(int code); int parseCommand(int argc, char** argv, CommandState& state); int runCommand(int argc, char** argv, QCoreApplication* coreApplication); +QString getDisplayConnection(); + int launch(const LaunchArgs& args, char** argv, QCoreApplication* coreApplication); } // namespace qs::launch diff --git a/src/launch/main.cpp b/src/launch/main.cpp index 2bcbebd..a324e09 100644 --- a/src/launch/main.cpp +++ b/src/launch/main.cpp @@ -16,7 +16,7 @@ #include "build.hpp" #include "launch_p.hpp" -#if CRASH_REPORTER +#if CRASH_HANDLER #include "../crash/main.hpp" #endif @@ -25,14 +25,17 @@ namespace qs::launch { namespace { void checkCrashRelaunch(char** argv, QCoreApplication* coreApplication) { -#if CRASH_REPORTER +#if CRASH_HANDLER auto lastInfoFdStr = qEnvironmentVariable("__QUICKSHELL_CRASH_INFO_FD"); if (!lastInfoFdStr.isEmpty()) { auto lastInfoFd = lastInfoFdStr.toInt(); QFile file; - file.open(lastInfoFd, QFile::ReadOnly, QFile::AutoCloseHandle); + if (!file.open(lastInfoFd, QFile::ReadOnly, QFile::AutoCloseHandle)) { + qFatal() << "Failed to open crash info fd. Cannot restart."; + } + file.seek(0); auto ds = QDataStream(&file); @@ -101,7 +104,7 @@ void exitDaemon(int code) { int main(int argc, char** argv) { QCoreApplication::setApplicationName("quickshell"); -#if CRASH_REPORTER +#if CRASH_HANDLER qsCheckCrash(argc, argv); #endif diff --git a/src/launch/parsecommand.cpp b/src/launch/parsecommand.cpp index fc16086..fc43b6b 100644 --- a/src/launch/parsecommand.cpp +++ b/src/launch/parsecommand.cpp @@ -1,6 +1,7 @@ #include #include #include +#include #include #include // NOLINT: Need to include this for impls of some CLI11 classes @@ -16,7 +17,7 @@ int parseCommand(int argc, char** argv, CommandState& state) { .argv = argv, }; - auto addConfigSelection = [&](CLI::App* cmd, bool withNewestOption = false) { + auto addConfigSelection = [&](CLI::App* cmd, bool filtering = false) { auto* group = cmd->add_option_group("Config Selection") ->description( @@ -43,15 +44,23 @@ int parseCommand(int argc, char** argv, CommandState& state) { ->excludes(path); group->add_option("-m,--manifest", state.config.manifest) - ->description("[DEPRECATED] Path to a quickshell manifest.\n" - "If a manifest is specified, configs named by -c will point to its entries.\n" - "Defaults to $XDG_CONFIG_HOME/quickshell/manifest.conf") + ->description( + "[DEPRECATED] Path to a quickshell manifest.\n" + "If a manifest is specified, configs named by -c will point to its entries.\n" + "Defaults to $XDG_CONFIG_HOME/quickshell/manifest.conf" + ) ->envname("QS_MANIFEST") ->excludes(path); - if (withNewestOption) { + if (filtering) { group->add_flag("-n,--newest", state.config.newest) ->description("Operate on the most recently launched instance instead of the oldest"); + + group->add_flag("--any-display", state.config.anyDisplay) + ->description( + "If passed, instances will not be filtered by the display connection they " + "were launched on." + ); } return group; @@ -75,9 +84,11 @@ int parseCommand(int argc, char** argv, CommandState& state) { auto* group = noGroup ? cmd : cmd->add_option_group(noDisplay ? "" : "Logging"); group->add_flag("--no-color", state.log.noColor) - ->description("Disables colored logging.\n" - "Colored logging can also be disabled by specifying a non empty value " - "for the NO_COLOR environment variable."); + ->description( + "Disables colored logging.\n" + "Colored logging can also be disabled by specifying a non empty value " + "for the NO_COLOR environment variable." + ); group->add_flag("--log-times", state.log.timestamp) ->description("Log timestamps with each message."); @@ -86,9 +97,11 @@ int parseCommand(int argc, char** argv, CommandState& state) { ->description("Log rules to apply, in the format of QT_LOGGING_RULES."); group->add_flag("-v,--verbose", [&](size_t count) { state.log.verbosity = count; }) - ->description("Increases log verbosity.\n" - "-v will show INFO level internal logs.\n" - "-vv will show DEBUG level internal logs."); + ->description( + "Increases log verbosity.\n" + "-v will show INFO level internal logs.\n" + "-vv will show DEBUG level internal logs." + ); auto* hgroup = cmd->add_option_group(""); hgroup->add_flag("--no-detailed-logs", state.log.sparse); @@ -98,9 +111,11 @@ int parseCommand(int argc, char** argv, CommandState& state) { auto* group = cmd->add_option_group("Instance Selection"); group->add_option("-i,--id", state.instance.id) - ->description("The instance id to operate on.\n" - "You may also use a substring the id as long as it is unique, " - "for example \"abc\" will select \"abcdefg\"."); + ->description( + "The instance id to operate on.\n" + "You may also use a substring the id as long as it is unique, " + "for example \"abc\" will select \"abcdefg\"." + ); group->add_option("--pid", state.instance.pid) ->description("The process id of the instance to operate on."); @@ -157,9 +172,11 @@ int parseCommand(int argc, char** argv, CommandState& state) { auto* sub = cli->add_subcommand("list", "List running quickshell instances."); auto* all = sub->add_flag("-a,--all", state.instance.all) - ->description("List all instances.\n" - "If unspecified, only instances of" - "the selected config will be listed."); + ->description( + "List all instances.\n" + "If unspecified, only instances of" + "the selected config will be listed." + ); sub->add_flag("-j,--json", state.output.json, "Output the list as a json."); @@ -210,6 +227,16 @@ int parseCommand(int argc, char** argv, CommandState& state) { ->allow_extra_args(); } + auto signalCmd = [&](std::string cmd, std::string desc) { + auto* scmd = sub->add_subcommand(std::move(cmd), std::move(desc)); + scmd->add_option("target", state.ipc.target, "The target to listen on."); + scmd->add_option("signal", state.ipc.name, "The signal to listen for."); + return scmd; + }; + + state.ipc.wait = signalCmd("wait", "Wait for one IpcHandler signal."); + state.ipc.listen = signalCmd("listen", "Listen for IpcHandler signals."); + { auto* prop = sub->add_subcommand("prop", "Manipulate IpcHandler properties.")->require_subcommand(); @@ -235,8 +262,10 @@ int parseCommand(int argc, char** argv, CommandState& state) { ->allow_extra_args(); sub->add_flag("-s,--show", state.ipc.showOld) - ->description("Print information about a function or target if given, or all available " - "targets if not."); + ->description( + "Print information about a function or target if given, or all available " + "targets if not." + ); auto* instance = addInstanceSelection(sub); addConfigSelection(sub, true)->excludes(instance); diff --git a/src/network/CMakeLists.txt b/src/network/CMakeLists.txt new file mode 100644 index 0000000..6075040 --- /dev/null +++ b/src/network/CMakeLists.txt @@ -0,0 +1,24 @@ +add_subdirectory(nm) + +qt_add_library(quickshell-network STATIC + network.cpp + device.cpp + wifi.cpp +) + +target_include_directories(quickshell-network PRIVATE + ${CMAKE_CURRENT_BINARY_DIR} +) + +qt_add_qml_module(quickshell-network + URI Quickshell.Networking + VERSION 0.1 + DEPENDENCIES QtQml +) + +qs_add_module_deps_light(quickshell-network Quickshell) +install_qml_module(quickshell-network) +target_link_libraries(quickshell-network PRIVATE quickshell-network-nm Qt::Qml Qt::DBus) +qs_add_link_dependencies(quickshell-network quickshell-dbus) +target_link_libraries(quickshell PRIVATE quickshell-networkplugin) +qs_module_pch(quickshell-network SET dbus) diff --git a/src/network/device.cpp b/src/network/device.cpp new file mode 100644 index 0000000..22e3949 --- /dev/null +++ b/src/network/device.cpp @@ -0,0 +1,82 @@ +#include "device.hpp" + +#include +#include +#include +#include +#include +#include + +#include "../core/logcat.hpp" + +namespace qs::network { + +namespace { +QS_LOGGING_CATEGORY(logNetworkDevice, "quickshell.network.device", QtWarningMsg); +} // namespace + +QString DeviceConnectionState::toString(DeviceConnectionState::Enum state) { + switch (state) { + case Unknown: return QStringLiteral("Unknown"); + case Connecting: return QStringLiteral("Connecting"); + case Connected: return QStringLiteral("Connected"); + case Disconnecting: return QStringLiteral("Disconnecting"); + case Disconnected: return QStringLiteral("Disconnected"); + default: return QStringLiteral("Unknown"); + } +} + +QString DeviceType::toString(DeviceType::Enum type) { + switch (type) { + case None: return QStringLiteral("None"); + case Wifi: return QStringLiteral("Wifi"); + default: return QStringLiteral("Unknown"); + } +} + +QString NMDeviceState::toString(NMDeviceState::Enum state) { + switch (state) { + case Unknown: return QStringLiteral("Unknown"); + case Unmanaged: return QStringLiteral("Not managed by NetworkManager"); + case Unavailable: return QStringLiteral("Unavailable"); + case Disconnected: return QStringLiteral("Disconnected"); + case Prepare: return QStringLiteral("Preparing to connect"); + case Config: return QStringLiteral("Connecting to a network"); + case NeedAuth: return QStringLiteral("Waiting for authentication"); + case IPConfig: return QStringLiteral("Requesting IPv4 and/or IPv6 addresses from the network"); + case IPCheck: + return QStringLiteral("Checking if further action is required for the requested connection"); + case Secondaries: + return QStringLiteral("Waiting for a required secondary connection to activate"); + case Activated: return QStringLiteral("Connected"); + case Deactivating: return QStringLiteral("Disconnecting"); + case Failed: return QStringLiteral("Failed to connect"); + default: return QStringLiteral("Unknown"); + }; +} + +NetworkDevice::NetworkDevice(DeviceType::Enum type, QObject* parent): QObject(parent), mType(type) { + this->bindableConnected().setBinding([this]() { + return this->bState == DeviceConnectionState::Connected; + }); +}; + +void NetworkDevice::setAutoconnect(bool autoconnect) { + if (this->bAutoconnect == autoconnect) return; + emit this->requestSetAutoconnect(autoconnect); +} + +void NetworkDevice::disconnect() { + if (this->bState == DeviceConnectionState::Disconnected) { + qCCritical(logNetworkDevice) << "Device" << this << "is already disconnected"; + return; + } + if (this->bState == DeviceConnectionState::Disconnecting) { + qCCritical(logNetworkDevice) << "Device" << this << "is already disconnecting"; + return; + } + qCDebug(logNetworkDevice) << "Disconnecting from device" << this; + this->requestDisconnect(); +} + +} // namespace qs::network diff --git a/src/network/device.hpp b/src/network/device.hpp new file mode 100644 index 0000000..f3807c2 --- /dev/null +++ b/src/network/device.hpp @@ -0,0 +1,133 @@ +#pragma once + +#include +#include +#include +#include +#include + +namespace qs::network { + +///! Connection state of a NetworkDevice. +class DeviceConnectionState: public QObject { + Q_OBJECT; + QML_ELEMENT; + QML_SINGLETON; + +public: + enum Enum : quint8 { + Unknown = 0, + Connecting = 1, + Connected = 2, + Disconnecting = 3, + Disconnected = 4, + }; + Q_ENUM(Enum); + Q_INVOKABLE static QString toString(DeviceConnectionState::Enum state); +}; + +///! Type of network device. +class DeviceType: public QObject { + Q_OBJECT; + QML_ELEMENT; + QML_SINGLETON; + +public: + enum Enum : quint8 { + None = 0, + Wifi = 1, + }; + Q_ENUM(Enum); + Q_INVOKABLE static QString toString(DeviceType::Enum type); +}; + +///! NetworkManager-specific device state. +/// In sync with https://networkmanager.dev/docs/api/latest/nm-dbus-types.html#NMDeviceState. +class NMDeviceState: public QObject { + Q_OBJECT; + QML_ELEMENT; + QML_SINGLETON; + +public: + enum Enum : quint8 { + Unknown = 0, + Unmanaged = 10, + Unavailable = 20, + Disconnected = 30, + Prepare = 40, + Config = 50, + NeedAuth = 60, + IPConfig = 70, + IPCheck = 80, + Secondaries = 90, + Activated = 100, + Deactivating = 110, + Failed = 120, + }; + Q_ENUM(Enum); + Q_INVOKABLE static QString toString(NMDeviceState::Enum state); +}; + +///! A network device. +/// When @@type is `Wifi`, the device is a @@WifiDevice, which can be used to scan for and connect to access points. +class NetworkDevice: public QObject { + Q_OBJECT; + QML_ELEMENT; + QML_UNCREATABLE("Devices can only be acquired through Network"); + // clang-format off + /// The device type. + Q_PROPERTY(DeviceType::Enum type READ type CONSTANT); + /// The name of the device's control interface. + Q_PROPERTY(QString name READ name NOTIFY nameChanged BINDABLE bindableName); + /// The hardware address of the device in the XX:XX:XX:XX:XX:XX format. + Q_PROPERTY(QString address READ default NOTIFY addressChanged BINDABLE bindableAddress); + /// True if the device is connected. + Q_PROPERTY(bool connected READ default NOTIFY connectedChanged BINDABLE bindableConnected); + /// Connection state of the device. + Q_PROPERTY(qs::network::DeviceConnectionState::Enum state READ default NOTIFY stateChanged BINDABLE bindableState); + /// A more specific device state when the backend is NetworkManager. + Q_PROPERTY(qs::network::NMDeviceState::Enum nmState READ default NOTIFY nmStateChanged BINDABLE bindableNmState); + /// True if the device is allowed to autoconnect. + Q_PROPERTY(bool autoconnect READ autoconnect WRITE setAutoconnect NOTIFY autoconnectChanged); + // clang-format on + +public: + explicit NetworkDevice(DeviceType::Enum type, QObject* parent = nullptr); + + /// Disconnects the device and prevents it from automatically activating further connections. + Q_INVOKABLE void disconnect(); + + [[nodiscard]] DeviceType::Enum type() const { return this->mType; }; + QBindable bindableName() { return &this->bName; }; + [[nodiscard]] QString name() const { return this->bName; }; + QBindable bindableAddress() { return &this->bAddress; }; + QBindable bindableConnected() { return &this->bConnected; }; + QBindable bindableState() { return &this->bState; }; + QBindable bindableNmState() { return &this->bNmState; }; + [[nodiscard]] bool autoconnect() const { return this->bAutoconnect; }; + QBindable bindableAutoconnect() { return &this->bAutoconnect; }; + void setAutoconnect(bool autoconnect); + +signals: + void requestDisconnect(); + void requestSetAutoconnect(bool autoconnect); + void nameChanged(); + void addressChanged(); + void connectedChanged(); + void stateChanged(); + void nmStateChanged(); + void autoconnectChanged(); + +private: + DeviceType::Enum mType; + // clang-format off + Q_OBJECT_BINDABLE_PROPERTY(NetworkDevice, QString, bName, &NetworkDevice::nameChanged); + Q_OBJECT_BINDABLE_PROPERTY(NetworkDevice, QString, bAddress, &NetworkDevice::addressChanged); + Q_OBJECT_BINDABLE_PROPERTY(NetworkDevice, bool, bConnected, &NetworkDevice::connectedChanged); + Q_OBJECT_BINDABLE_PROPERTY(NetworkDevice, DeviceConnectionState::Enum, bState, &NetworkDevice::stateChanged); + Q_OBJECT_BINDABLE_PROPERTY(NetworkDevice, NMDeviceState::Enum, bNmState, &NetworkDevice::nmStateChanged); + Q_OBJECT_BINDABLE_PROPERTY(NetworkDevice, bool, bAutoconnect, &NetworkDevice::autoconnectChanged); + // clang-format on +}; + +} // namespace qs::network diff --git a/src/network/module.md b/src/network/module.md new file mode 100644 index 0000000..a0c8e64 --- /dev/null +++ b/src/network/module.md @@ -0,0 +1,13 @@ +name = "Quickshell.Networking" +description = "Network API" +headers = [ + "network.hpp", + "device.hpp", + "wifi.hpp", +] +----- +This module exposes Network management APIs provided by a supported network backend. +For now, the only backend available is the NetworkManager DBus interface. +Both DBus and NetworkManager must be running to use it. + +See the @@Quickshell.Networking.Networking singleton. diff --git a/src/network/network.cpp b/src/network/network.cpp new file mode 100644 index 0000000..e325b05 --- /dev/null +++ b/src/network/network.cpp @@ -0,0 +1,65 @@ +#include "network.hpp" +#include + +#include +#include +#include +#include +#include + +#include "../core/logcat.hpp" +#include "device.hpp" +#include "nm/backend.hpp" + +namespace qs::network { + +namespace { +QS_LOGGING_CATEGORY(logNetwork, "quickshell.network", QtWarningMsg); +} // namespace + +QString NetworkState::toString(NetworkState::Enum state) { + switch (state) { + case NetworkState::Connecting: return QStringLiteral("Connecting"); + case NetworkState::Connected: return QStringLiteral("Connected"); + case NetworkState::Disconnecting: return QStringLiteral("Disconnecting"); + case NetworkState::Disconnected: return QStringLiteral("Disconnected"); + default: return QStringLiteral("Unknown"); + } +} + +Networking::Networking(QObject* parent): QObject(parent) { + // Try to create the NetworkManager backend and bind to it. + auto* nm = new NetworkManager(this); + if (nm->isAvailable()) { + QObject::connect(nm, &NetworkManager::deviceAdded, this, &Networking::deviceAdded); + QObject::connect(nm, &NetworkManager::deviceRemoved, this, &Networking::deviceRemoved); + QObject::connect(this, &Networking::requestSetWifiEnabled, nm, &NetworkManager::setWifiEnabled); + this->bindableWifiEnabled().setBinding([nm]() { return nm->wifiEnabled(); }); + this->bindableWifiHardwareEnabled().setBinding([nm]() { return nm->wifiHardwareEnabled(); }); + + this->mBackend = nm; + this->mBackendType = NetworkBackendType::NetworkManager; + return; + } else { + delete nm; + } + + qCCritical(logNetwork) << "Network will not work. Could not find an available backend."; +} + +void Networking::deviceAdded(NetworkDevice* dev) { this->mDevices.insertObject(dev); } +void Networking::deviceRemoved(NetworkDevice* dev) { this->mDevices.removeObject(dev); } + +void Networking::setWifiEnabled(bool enabled) { + if (this->bWifiEnabled == enabled) return; + emit this->requestSetWifiEnabled(enabled); +} + +Network::Network(QString name, QObject* parent): QObject(parent), mName(std::move(name)) { + this->bStateChanging.setBinding([this] { + auto state = this->bState.value(); + return state == NetworkState::Connecting || state == NetworkState::Disconnecting; + }); +}; + +} // namespace qs::network diff --git a/src/network/network.hpp b/src/network/network.hpp new file mode 100644 index 0000000..8af7c9d --- /dev/null +++ b/src/network/network.hpp @@ -0,0 +1,142 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include "../core/model.hpp" +#include "device.hpp" + +namespace qs::network { + +///! The connection state of a Network. +class NetworkState: public QObject { + Q_OBJECT; + QML_ELEMENT; + QML_SINGLETON; + +public: + enum Enum : quint8 { + Unknown = 0, + Connecting = 1, + Connected = 2, + Disconnecting = 3, + Disconnected = 4, + }; + Q_ENUM(Enum); + Q_INVOKABLE static QString toString(NetworkState::Enum state); +}; + +///! The backend supplying the Network service. +class NetworkBackendType: public QObject { + Q_OBJECT; + QML_ELEMENT; + QML_SINGLETON; + +public: + enum Enum : quint8 { + None = 0, + NetworkManager = 1, + }; + Q_ENUM(Enum); +}; + +class NetworkBackend: public QObject { + Q_OBJECT; + +public: + [[nodiscard]] virtual bool isAvailable() const = 0; + +protected: + explicit NetworkBackend(QObject* parent = nullptr): QObject(parent) {}; +}; + +///! The Network service. +/// An interface to a network backend (currently only NetworkManager), +/// which can be used to view, configure, and connect to various networks. +class Networking: public QObject { + Q_OBJECT; + QML_SINGLETON; + QML_ELEMENT; + // clang-format off + /// A list of all network devices. + QSDOC_TYPE_OVERRIDE(ObjectModel*); + Q_PROPERTY(UntypedObjectModel* devices READ devices CONSTANT); + /// The backend being used to power the Network service. + Q_PROPERTY(qs::network::NetworkBackendType::Enum backend READ backend CONSTANT); + /// Switch for the rfkill software block of all wireless devices. + Q_PROPERTY(bool wifiEnabled READ wifiEnabled WRITE setWifiEnabled NOTIFY wifiEnabledChanged); + /// State of the rfkill hardware block of all wireless devices. + Q_PROPERTY(bool wifiHardwareEnabled READ default NOTIFY wifiHardwareEnabledChanged BINDABLE bindableWifiHardwareEnabled); + // clang-format on + +public: + explicit Networking(QObject* parent = nullptr); + + [[nodiscard]] ObjectModel* devices() { return &this->mDevices; }; + [[nodiscard]] NetworkBackendType::Enum backend() const { return this->mBackendType; }; + QBindable bindableWifiEnabled() { return &this->bWifiEnabled; }; + [[nodiscard]] bool wifiEnabled() const { return this->bWifiEnabled; }; + void setWifiEnabled(bool enabled); + QBindable bindableWifiHardwareEnabled() { return &this->bWifiHardwareEnabled; }; + +signals: + void requestSetWifiEnabled(bool enabled); + void wifiEnabledChanged(); + void wifiHardwareEnabledChanged(); + +private slots: + void deviceAdded(NetworkDevice* dev); + void deviceRemoved(NetworkDevice* dev); + +private: + ObjectModel mDevices {this}; + NetworkBackend* mBackend = nullptr; + NetworkBackendType::Enum mBackendType = NetworkBackendType::None; + // clang-format off + Q_OBJECT_BINDABLE_PROPERTY(Networking, bool, bWifiEnabled, &Networking::wifiEnabledChanged); + Q_OBJECT_BINDABLE_PROPERTY(Networking, bool, bWifiHardwareEnabled, &Networking::wifiHardwareEnabledChanged); + // clang-format on +}; + +///! A network. +class Network: public QObject { + Q_OBJECT; + QML_ELEMENT; + QML_UNCREATABLE("BaseNetwork can only be aqcuired through network devices"); + + // clang-format off + /// The name of the network. + Q_PROPERTY(QString name READ name CONSTANT); + /// True if the network is connected. + Q_PROPERTY(bool connected READ default NOTIFY connectedChanged BINDABLE bindableConnected); + /// The connectivity state of the network. + Q_PROPERTY(NetworkState::Enum state READ default NOTIFY stateChanged BINDABLE bindableState); + /// If the network is currently connecting or disconnecting. Shorthand for checking @@state. + Q_PROPERTY(bool stateChanging READ default NOTIFY stateChangingChanged BINDABLE bindableStateChanging); + // clang-format on + +public: + explicit Network(QString name, QObject* parent = nullptr); + + [[nodiscard]] QString name() const { return this->mName; }; + QBindable bindableConnected() { return &this->bConnected; } + QBindable bindableState() { return &this->bState; } + QBindable bindableStateChanging() { return &this->bStateChanging; } + +signals: + void connectedChanged(); + void stateChanged(); + void stateChangingChanged(); + +protected: + QString mName; + + Q_OBJECT_BINDABLE_PROPERTY(Network, bool, bConnected, &Network::connectedChanged); + Q_OBJECT_BINDABLE_PROPERTY(Network, NetworkState::Enum, bState, &Network::stateChanged); + Q_OBJECT_BINDABLE_PROPERTY(Network, bool, bStateChanging, &Network::stateChangingChanged); +}; + +} // namespace qs::network diff --git a/src/network/nm/CMakeLists.txt b/src/network/nm/CMakeLists.txt new file mode 100644 index 0000000..bb8635e --- /dev/null +++ b/src/network/nm/CMakeLists.txt @@ -0,0 +1,79 @@ +set_source_files_properties(org.freedesktop.NetworkManager.xml PROPERTIES + CLASSNAME DBusNetworkManagerProxy + NO_NAMESPACE TRUE + INCLUDE ${CMAKE_CURRENT_SOURCE_DIR}/dbus_types.hpp +) + +qt_add_dbus_interface(NM_DBUS_INTERFACES + org.freedesktop.NetworkManager.xml + dbus_nm_backend +) + +set_source_files_properties(org.freedesktop.NetworkManager.Device.xml PROPERTIES + CLASSNAME DBusNMDeviceProxy + NO_NAMESPACE TRUE +) + +qt_add_dbus_interface(NM_DBUS_INTERFACES + org.freedesktop.NetworkManager.Device.xml + dbus_nm_device +) + +set_source_files_properties(org.freedesktop.NetworkManager.Device.Wireless.xml PROPERTIES + CLASSNAME DBusNMWirelessProxy + NO_NAMESPACE TRUE +) + +qt_add_dbus_interface(NM_DBUS_INTERFACES + org.freedesktop.NetworkManager.Device.Wireless.xml + dbus_nm_wireless +) + +set_source_files_properties(org.freedesktop.NetworkManager.AccessPoint.xml PROPERTIES + CLASSNAME DBusNMAccessPointProxy + NO_NAMESPACE TRUE +) + +qt_add_dbus_interface(NM_DBUS_INTERFACES + org.freedesktop.NetworkManager.AccessPoint.xml + dbus_nm_accesspoint +) + +set_source_files_properties(org.freedesktop.NetworkManager.Settings.Connection.xml PROPERTIES + CLASSNAME DBusNMConnectionSettingsProxy + NO_NAMESPACE TRUE + INCLUDE ${CMAKE_CURRENT_SOURCE_DIR}/dbus_types.hpp +) + +qt_add_dbus_interface(NM_DBUS_INTERFACES + org.freedesktop.NetworkManager.Settings.Connection.xml + dbus_nm_connection_settings +) + +set_source_files_properties(org.freedesktop.NetworkManager.Connection.Active.xml PROPERTIES + CLASSNAME DBusNMActiveConnectionProxy + NO_NAMESPACE TRUE +) + +qt_add_dbus_interface(NM_DBUS_INTERFACES + org.freedesktop.NetworkManager.Connection.Active.xml + dbus_nm_active_connection +) + +qt_add_library(quickshell-network-nm STATIC + backend.cpp + device.cpp + connection.cpp + accesspoint.cpp + wireless.cpp + utils.cpp + enums.hpp + ${NM_DBUS_INTERFACES} +) + +target_include_directories(quickshell-network-nm PUBLIC + ${CMAKE_CURRENT_BINARY_DIR} +) + +target_link_libraries(quickshell-network-nm PRIVATE Qt::Qml Qt::DBus) +qs_add_link_dependencies(quickshell-network-nm quickshell-dbus) diff --git a/src/network/nm/accesspoint.cpp b/src/network/nm/accesspoint.cpp new file mode 100644 index 0000000..b6e3dfb --- /dev/null +++ b/src/network/nm/accesspoint.cpp @@ -0,0 +1,71 @@ +#include "accesspoint.hpp" + +#include +#include +#include +#include +#include +#include +#include + +#include "../../core/logcat.hpp" +#include "../../dbus/properties.hpp" +#include "dbus_nm_accesspoint.h" +#include "enums.hpp" + +namespace qs::network { +using namespace qs::dbus; + +namespace { +QS_LOGGING_CATEGORY(logNetworkManager, "quickshell.network.networkmanager", QtWarningMsg); +} + +NMAccessPoint::NMAccessPoint(const QString& path, QObject* parent): QObject(parent) { + this->proxy = new DBusNMAccessPointProxy( + "org.freedesktop.NetworkManager", + path, + QDBusConnection::systemBus(), + this + ); + + if (!this->proxy->isValid()) { + qCWarning(logNetworkManager) << "Cannot create DBus interface for access point at" << path; + return; + } + + QObject::connect( + &this->accessPointProperties, + &DBusPropertyGroup::getAllFinished, + this, + &NMAccessPoint::loaded, + Qt::SingleShotConnection + ); + + this->accessPointProperties.setInterface(this->proxy); + this->accessPointProperties.updateAllViaGetAll(); +} + +bool NMAccessPoint::isValid() const { return this->proxy && this->proxy->isValid(); } +QString NMAccessPoint::address() const { return this->proxy ? this->proxy->service() : QString(); } +QString NMAccessPoint::path() const { return this->proxy ? this->proxy->path() : QString(); } + +} // namespace qs::network + +namespace qs::dbus { + +DBusResult +DBusDataTransform::fromWire(quint32 wire) { + return DBusResult(static_cast(wire)); +} + +DBusResult +DBusDataTransform::fromWire(quint32 wire) { + return DBusResult(static_cast(wire)); +} + +DBusResult +DBusDataTransform::fromWire(quint32 wire) { + return DBusResult(static_cast(wire)); +} + +} // namespace qs::dbus diff --git a/src/network/nm/accesspoint.hpp b/src/network/nm/accesspoint.hpp new file mode 100644 index 0000000..8409089 --- /dev/null +++ b/src/network/nm/accesspoint.hpp @@ -0,0 +1,92 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include "../../dbus/properties.hpp" +#include "../wifi.hpp" +#include "dbus_nm_accesspoint.h" +#include "enums.hpp" + +namespace qs::dbus { + +template <> +struct DBusDataTransform { + using Wire = quint32; + using Data = qs::network::NM80211ApFlags::Enum; + static DBusResult fromWire(Wire wire); +}; + +template <> +struct DBusDataTransform { + using Wire = quint32; + using Data = qs::network::NM80211ApSecurityFlags::Enum; + static DBusResult fromWire(Wire wire); +}; + +template <> +struct DBusDataTransform { + using Wire = quint32; + using Data = qs::network::NM80211Mode::Enum; + static DBusResult fromWire(Wire wire); +}; + +} // namespace qs::dbus + +namespace qs::network { + +/// Proxy of a /org/freedesktop/NetworkManager/AccessPoint/* object. +class NMAccessPoint: public QObject { + Q_OBJECT; + +public: + explicit NMAccessPoint(const QString& path, QObject* parent = nullptr); + + [[nodiscard]] bool isValid() const; + [[nodiscard]] QString path() const; + [[nodiscard]] QString address() const; + [[nodiscard]] QByteArray ssid() const { return this->bSsid; }; + [[nodiscard]] quint8 signalStrength() const { return this->bSignalStrength; }; + [[nodiscard]] NM80211ApFlags::Enum flags() const { return this->bFlags; }; + [[nodiscard]] NM80211ApSecurityFlags::Enum wpaFlags() const { return this->bWpaFlags; }; + [[nodiscard]] NM80211ApSecurityFlags::Enum rsnFlags() const { return this->bRsnFlags; }; + [[nodiscard]] NM80211Mode::Enum mode() const { return this->bMode; }; + [[nodiscard]] QBindable bindableSecurity() { return &this->bSecurity; }; + [[nodiscard]] WifiSecurityType::Enum security() const { return this->bSecurity; }; + +signals: + void loaded(); + void ssidChanged(const QByteArray& ssid); + void signalStrengthChanged(quint8 signal); + void flagsChanged(NM80211ApFlags::Enum flags); + void wpaFlagsChanged(NM80211ApSecurityFlags::Enum wpaFlags); + void rsnFlagsChanged(NM80211ApSecurityFlags::Enum rsnFlags); + void modeChanged(NM80211Mode::Enum mode); + void securityChanged(WifiSecurityType::Enum security); + +private: + // clang-format off + Q_OBJECT_BINDABLE_PROPERTY(NMAccessPoint, QByteArray, bSsid, &NMAccessPoint::ssidChanged); + Q_OBJECT_BINDABLE_PROPERTY(NMAccessPoint, quint8, bSignalStrength, &NMAccessPoint::signalStrengthChanged); + Q_OBJECT_BINDABLE_PROPERTY(NMAccessPoint, NM80211ApFlags::Enum, bFlags, &NMAccessPoint::flagsChanged); + Q_OBJECT_BINDABLE_PROPERTY(NMAccessPoint, NM80211ApSecurityFlags::Enum, bWpaFlags, &NMAccessPoint::wpaFlagsChanged); + Q_OBJECT_BINDABLE_PROPERTY(NMAccessPoint, NM80211ApSecurityFlags::Enum, bRsnFlags, &NMAccessPoint::rsnFlagsChanged); + Q_OBJECT_BINDABLE_PROPERTY(NMAccessPoint, NM80211Mode::Enum, bMode, &NMAccessPoint::modeChanged); + Q_OBJECT_BINDABLE_PROPERTY(NMAccessPoint, WifiSecurityType::Enum, bSecurity, &NMAccessPoint::securityChanged); + + QS_DBUS_BINDABLE_PROPERTY_GROUP(NMAccessPointAdapter, accessPointProperties); + QS_DBUS_PROPERTY_BINDING(NMAccessPoint, pSsid, bSsid, accessPointProperties, "Ssid"); + QS_DBUS_PROPERTY_BINDING(NMAccessPoint, pSignalStrength, bSignalStrength, accessPointProperties, "Strength"); + QS_DBUS_PROPERTY_BINDING(NMAccessPoint, pFlags, bFlags, accessPointProperties, "Flags"); + QS_DBUS_PROPERTY_BINDING(NMAccessPoint, pWpaFlags, bWpaFlags, accessPointProperties, "WpaFlags"); + QS_DBUS_PROPERTY_BINDING(NMAccessPoint, pRsnFlags, bRsnFlags, accessPointProperties, "RsnFlags"); + QS_DBUS_PROPERTY_BINDING(NMAccessPoint, pMode, bMode, accessPointProperties, "Mode"); + // clang-format on + + DBusNMAccessPointProxy* proxy = nullptr; +}; + +} // namespace qs::network diff --git a/src/network/nm/backend.cpp b/src/network/nm/backend.cpp new file mode 100644 index 0000000..4b61e33 --- /dev/null +++ b/src/network/nm/backend.cpp @@ -0,0 +1,270 @@ +#include "backend.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../../core/logcat.hpp" +#include "../../dbus/properties.hpp" +#include "../device.hpp" +#include "../network.hpp" +#include "../wifi.hpp" +#include "dbus_nm_backend.h" +#include "dbus_nm_device.h" +#include "dbus_types.hpp" +#include "device.hpp" +#include "enums.hpp" +#include "wireless.hpp" + +namespace qs::network { + +namespace { +QS_LOGGING_CATEGORY(logNetworkManager, "quickshell.network.networkmanager", QtWarningMsg); +} + +NetworkManager::NetworkManager(QObject* parent): NetworkBackend(parent) { + qDBusRegisterMetaType(); + + auto bus = QDBusConnection::systemBus(); + if (!bus.isConnected()) { + qCWarning( + logNetworkManager + ) << "Could not connect to DBus. NetworkManager backend will not work."; + return; + } + + this->proxy = new DBusNetworkManagerProxy( + "org.freedesktop.NetworkManager", + "/org/freedesktop/NetworkManager", + bus, + this + ); + + if (!this->proxy->isValid()) { + qCDebug( + logNetworkManager + ) << "NetworkManager is not currently running. This network backend will not work"; + } else { + this->init(); + } +} + +void NetworkManager::init() { + // clang-format off + QObject::connect(this->proxy, &DBusNetworkManagerProxy::DeviceAdded, this, &NetworkManager::onDevicePathAdded); + QObject::connect(this->proxy, &DBusNetworkManagerProxy::DeviceRemoved, this, &NetworkManager::onDevicePathRemoved); + // clang-format on + + this->dbusProperties.setInterface(this->proxy); + this->dbusProperties.updateAllViaGetAll(); + + this->registerDevices(); +} + +void NetworkManager::registerDevices() { + auto pending = this->proxy->GetAllDevices(); + auto* call = new QDBusPendingCallWatcher(pending, this); + + auto responseCallback = [this](QDBusPendingCallWatcher* call) { + const QDBusPendingReply> reply = *call; + + if (reply.isError()) { + qCWarning(logNetworkManager) << "Failed to get devices: " << reply.error().message(); + } else { + for (const QDBusObjectPath& devicePath: reply.value()) { + this->registerDevice(devicePath.path()); + } + } + + delete call; + }; + + QObject::connect(call, &QDBusPendingCallWatcher::finished, this, responseCallback); +} + +void NetworkManager::registerDevice(const QString& path) { + if (this->mDevices.contains(path)) { + qCDebug(logNetworkManager) << "Skipping duplicate registration of device" << path; + return; + } + + auto* temp = new DBusNMDeviceProxy( + "org.freedesktop.NetworkManager", + path, + QDBusConnection::systemBus(), + this + ); + + auto callback = [this, path, temp](uint value, const QDBusError& error) { + if (error.isValid()) { + qCWarning(logNetworkManager) << "Failed to get device type:" << error; + } else { + auto type = static_cast(value); + NMDevice* dev = nullptr; + this->mDevices.insert(path, nullptr); + + switch (type) { + case NMDeviceType::Wifi: dev = new NMWirelessDevice(path); break; + default: break; + } + + if (dev) { + if (!dev->isValid()) { + qCWarning(logNetworkManager) << "Ignoring invalid registration of" << path; + delete dev; + } else { + this->mDevices[path] = dev; + // Only register a frontend device while it's managed by NM. + auto onManagedChanged = [this, dev, type](bool managed) { + managed ? this->registerFrontendDevice(type, dev) : this->removeFrontendDevice(dev); + }; + // clang-format off + QObject::connect(dev, &NMDevice::addAndActivateConnection, this, &NetworkManager::addAndActivateConnection); + QObject::connect(dev, &NMDevice::activateConnection, this, &NetworkManager::activateConnection); + QObject::connect(dev, &NMDevice::managedChanged, this, onManagedChanged); + // clang-format on + + if (dev->managed()) this->registerFrontendDevice(type, dev); + } + } + temp->deleteLater(); + } + }; + + qs::dbus::asyncReadProperty(*temp, "DeviceType", callback); +} + +void NetworkManager::registerFrontendDevice(NMDeviceType::Enum type, NMDevice* dev) { + NetworkDevice* frontendDev = nullptr; + switch (type) { + case NMDeviceType::Wifi: { + auto* frontendWifiDev = new WifiDevice(dev); + auto* wifiDev = qobject_cast(dev); + // Bind WifiDevice-specific properties + auto translateMode = [wifiDev]() { + switch (wifiDev->mode()) { + case NM80211Mode::Unknown: return WifiDeviceMode::Unknown; + case NM80211Mode::Adhoc: return WifiDeviceMode::AdHoc; + case NM80211Mode::Infra: return WifiDeviceMode::Station; + case NM80211Mode::Ap: return WifiDeviceMode::AccessPoint; + case NM80211Mode::Mesh: return WifiDeviceMode::Mesh; + } + }; + // clang-format off + frontendWifiDev->bindableMode().setBinding(translateMode); + wifiDev->bindableScanning().setBinding([frontendWifiDev]() { return frontendWifiDev->scannerEnabled(); }); + QObject::connect(wifiDev, &NMWirelessDevice::networkAdded, frontendWifiDev, &WifiDevice::networkAdded); + QObject::connect(wifiDev, &NMWirelessDevice::networkRemoved, frontendWifiDev, &WifiDevice::networkRemoved); + // clang-format on + frontendDev = frontendWifiDev; + break; + } + default: return; + } + + // Bind generic NetworkDevice properties + auto translateState = [dev]() { + switch (dev->state()) { + case 0 ... 20: return DeviceConnectionState::Unknown; + case 30: return DeviceConnectionState::Disconnected; + case 40 ... 90: return DeviceConnectionState::Connecting; + case 100: return DeviceConnectionState::Connected; + case 110 ... 120: return DeviceConnectionState::Disconnecting; + } + }; + // clang-format off + frontendDev->bindableName().setBinding([dev]() { return dev->interface(); }); + frontendDev->bindableAddress().setBinding([dev]() { return dev->hwAddress(); }); + frontendDev->bindableNmState().setBinding([dev]() { return dev->state(); }); + frontendDev->bindableState().setBinding(translateState); + frontendDev->bindableAutoconnect().setBinding([dev]() { return dev->autoconnect(); }); + QObject::connect(frontendDev, &WifiDevice::requestDisconnect, dev, &NMDevice::disconnect); + QObject::connect(frontendDev, &NetworkDevice::requestSetAutoconnect, dev, &NMDevice::setAutoconnect); + // clang-format on + + this->mFrontendDevices.insert(dev->path(), frontendDev); + emit this->deviceAdded(frontendDev); +} + +void NetworkManager::removeFrontendDevice(NMDevice* dev) { + auto* frontendDev = this->mFrontendDevices.take(dev->path()); + if (frontendDev) { + emit this->deviceRemoved(frontendDev); + frontendDev->deleteLater(); + } +} + +void NetworkManager::onDevicePathAdded(const QDBusObjectPath& path) { + this->registerDevice(path.path()); +} + +void NetworkManager::onDevicePathRemoved(const QDBusObjectPath& path) { + auto iter = this->mDevices.find(path.path()); + if (iter == this->mDevices.end()) { + qCWarning(logNetworkManager) << "Sent removal signal for" << path.path() + << "which is not registered."; + } else { + auto* dev = iter.value(); + this->mDevices.erase(iter); + if (dev) { + this->removeFrontendDevice(dev); + delete dev; + } + } +} + +void NetworkManager::activateConnection( + const QDBusObjectPath& connPath, + const QDBusObjectPath& devPath +) { + auto pending = this->proxy->ActivateConnection(connPath, devPath, QDBusObjectPath("/")); + auto* call = new QDBusPendingCallWatcher(pending, this); + + auto responseCallback = [](QDBusPendingCallWatcher* call) { + const QDBusPendingReply reply = *call; + + if (reply.isError()) { + qCWarning(logNetworkManager) << "Failed to activate connection:" << reply.error().message(); + } + delete call; + }; + QObject::connect(call, &QDBusPendingCallWatcher::finished, this, responseCallback); +} + +void NetworkManager::addAndActivateConnection( + const ConnectionSettingsMap& settings, + const QDBusObjectPath& devPath, + const QDBusObjectPath& specificObjectPath +) { + auto pending = this->proxy->AddAndActivateConnection(settings, devPath, specificObjectPath); + auto* call = new QDBusPendingCallWatcher(pending, this); + + auto responseCallback = [](QDBusPendingCallWatcher* call) { + const QDBusPendingReply reply = *call; + + if (reply.isError()) { + qCWarning(logNetworkManager) + << "Failed to add and activate connection:" << reply.error().message(); + } + delete call; + }; + QObject::connect(call, &QDBusPendingCallWatcher::finished, this, responseCallback); +} + +void NetworkManager::setWifiEnabled(bool enabled) { + if (enabled == this->bWifiEnabled) return; + this->bWifiEnabled = enabled; + this->pWifiEnabled.write(); +} + +bool NetworkManager::isAvailable() const { return this->proxy && this->proxy->isValid(); }; + +} // namespace qs::network diff --git a/src/network/nm/backend.hpp b/src/network/nm/backend.hpp new file mode 100644 index 0000000..471f57a --- /dev/null +++ b/src/network/nm/backend.hpp @@ -0,0 +1,67 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "../../dbus/properties.hpp" +#include "../network.hpp" +#include "dbus_nm_backend.h" +#include "device.hpp" + +namespace qs::network { + +class NetworkManager: public NetworkBackend { + Q_OBJECT; + +public: + explicit NetworkManager(QObject* parent = nullptr); + + [[nodiscard]] bool isAvailable() const override; + [[nodiscard]] bool wifiEnabled() const { return this->bWifiEnabled; }; + [[nodiscard]] bool wifiHardwareEnabled() const { return this->bWifiHardwareEnabled; }; + +signals: + void deviceAdded(NetworkDevice* device); + void deviceRemoved(NetworkDevice* device); + void wifiEnabledChanged(bool enabled); + void wifiHardwareEnabledChanged(bool enabled); + +public slots: + void setWifiEnabled(bool enabled); + +private slots: + void onDevicePathAdded(const QDBusObjectPath& path); + void onDevicePathRemoved(const QDBusObjectPath& path); + void activateConnection(const QDBusObjectPath& connPath, const QDBusObjectPath& devPath); + void addAndActivateConnection( + const ConnectionSettingsMap& settings, + const QDBusObjectPath& devPath, + const QDBusObjectPath& specificObjectPath + ); + +private: + void init(); + void registerDevices(); + void registerDevice(const QString& path); + void registerFrontendDevice(NMDeviceType::Enum type, NMDevice* dev); + void removeFrontendDevice(NMDevice* dev); + + QHash mDevices; + QHash mFrontendDevices; + + // clang-format off + Q_OBJECT_BINDABLE_PROPERTY(NetworkManager, bool, bWifiEnabled, &NetworkManager::wifiEnabledChanged); + Q_OBJECT_BINDABLE_PROPERTY(NetworkManager, bool, bWifiHardwareEnabled, &NetworkManager::wifiHardwareEnabledChanged); + + QS_DBUS_BINDABLE_PROPERTY_GROUP(NetworkManager, dbusProperties); + QS_DBUS_PROPERTY_BINDING(NetworkManager, pWifiEnabled, bWifiEnabled, dbusProperties, "WirelessEnabled"); + QS_DBUS_PROPERTY_BINDING(NetworkManager, pWifiHardwareEnabled, bWifiHardwareEnabled, dbusProperties, "WirelessHardwareEnabled"); + // clang-format on + DBusNetworkManagerProxy* proxy = nullptr; +}; + +} // namespace qs::network diff --git a/src/network/nm/connection.cpp b/src/network/nm/connection.cpp new file mode 100644 index 0000000..39b6f66 --- /dev/null +++ b/src/network/nm/connection.cpp @@ -0,0 +1,151 @@ +#include "connection.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../../core/logcat.hpp" +#include "../../dbus/properties.hpp" +#include "../wifi.hpp" +#include "dbus_nm_active_connection.h" +#include "dbus_nm_connection_settings.h" +#include "dbus_types.hpp" +#include "enums.hpp" +#include "utils.hpp" + +namespace qs::network { +using namespace qs::dbus; + +namespace { +QS_LOGGING_CATEGORY(logNetworkManager, "quickshell.network.networkmanager", QtWarningMsg); +} + +NMConnectionSettings::NMConnectionSettings(const QString& path, QObject* parent): QObject(parent) { + qDBusRegisterMetaType(); + + this->proxy = new DBusNMConnectionSettingsProxy( + "org.freedesktop.NetworkManager", + path, + QDBusConnection::systemBus(), + this + ); + + if (!this->proxy->isValid()) { + qCWarning(logNetworkManager) << "Cannot create DBus interface for connection at" << path; + return; + } + + QObject::connect( + this->proxy, + &DBusNMConnectionSettingsProxy::Updated, + this, + &NMConnectionSettings::updateSettings + ); + this->bSecurity.setBinding([this]() { return securityFromConnectionSettings(this->bSettings); }); + + this->connectionSettingsProperties.setInterface(this->proxy); + this->connectionSettingsProperties.updateAllViaGetAll(); + + this->updateSettings(); +} + +void NMConnectionSettings::updateSettings() { + auto pending = this->proxy->GetSettings(); + auto* call = new QDBusPendingCallWatcher(pending, this); + + auto responseCallback = [this](QDBusPendingCallWatcher* call) { + const QDBusPendingReply reply = *call; + + if (reply.isError()) { + qCWarning(logNetworkManager) + << "Failed to get" << this->path() << "settings:" << reply.error().message(); + } else { + this->bSettings = reply.value(); + } + + if (!this->mLoaded) { + emit this->loaded(); + this->mLoaded = true; + } + delete call; + }; + + QObject::connect(call, &QDBusPendingCallWatcher::finished, this, responseCallback); +} + +void NMConnectionSettings::forget() { + auto pending = this->proxy->Delete(); + auto* call = new QDBusPendingCallWatcher(pending, this); + + auto responseCallback = [this](QDBusPendingCallWatcher* call) { + const QDBusPendingReply<> reply = *call; + + if (reply.isError()) { + qCWarning(logNetworkManager) + << "Failed to forget" << this->path() << ":" << reply.error().message(); + } + delete call; + }; + + QObject::connect(call, &QDBusPendingCallWatcher::finished, this, responseCallback); +} + +bool NMConnectionSettings::isValid() const { return this->proxy && this->proxy->isValid(); } +QString NMConnectionSettings::address() const { + return this->proxy ? this->proxy->service() : QString(); +} +QString NMConnectionSettings::path() const { return this->proxy ? this->proxy->path() : QString(); } + +NMActiveConnection::NMActiveConnection(const QString& path, QObject* parent): QObject(parent) { + this->proxy = new DBusNMActiveConnectionProxy( + "org.freedesktop.NetworkManager", + path, + QDBusConnection::systemBus(), + this + ); + + if (!this->proxy->isValid()) { + qCWarning(logNetworkManager) << "Cannot create DBus interface for connection at" << path; + return; + } + + // clang-format off + QObject::connect(&this->activeConnectionProperties, &DBusPropertyGroup::getAllFinished, this, &NMActiveConnection::loaded, Qt::SingleShotConnection); + QObject::connect(this->proxy, &DBusNMActiveConnectionProxy::StateChanged, this, &NMActiveConnection::onStateChanged); + // clang-format on + + this->activeConnectionProperties.setInterface(this->proxy); + this->activeConnectionProperties.updateAllViaGetAll(); +} + +void NMActiveConnection::onStateChanged(quint32 /*state*/, quint32 reason) { + auto enumReason = static_cast(reason); + if (this->mStateReason == enumReason) return; + this->mStateReason = enumReason; + emit this->stateReasonChanged(enumReason); +} + +bool NMActiveConnection::isValid() const { return this->proxy && this->proxy->isValid(); } +QString NMActiveConnection::address() const { + return this->proxy ? this->proxy->service() : QString(); +} +QString NMActiveConnection::path() const { return this->proxy ? this->proxy->path() : QString(); } + +} // namespace qs::network + +namespace qs::dbus { + +DBusResult +DBusDataTransform::fromWire(quint32 wire) { + return DBusResult(static_cast(wire)); +} + +} // namespace qs::dbus diff --git a/src/network/nm/connection.hpp b/src/network/nm/connection.hpp new file mode 100644 index 0000000..4f126c8 --- /dev/null +++ b/src/network/nm/connection.hpp @@ -0,0 +1,105 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#include "../../dbus/properties.hpp" +#include "../wifi.hpp" +#include "dbus_nm_active_connection.h" +#include "dbus_nm_connection_settings.h" +#include "dbus_types.hpp" +#include "enums.hpp" + +namespace qs::dbus { + +template <> +struct DBusDataTransform { + using Wire = quint32; + using Data = qs::network::NMConnectionState::Enum; + static DBusResult fromWire(Wire wire); +}; + +} // namespace qs::dbus + +namespace qs::network { + +// Proxy of a /org/freedesktop/NetworkManager/Settings/Connection/* object. +class NMConnectionSettings: public QObject { + Q_OBJECT; + +public: + explicit NMConnectionSettings(const QString& path, QObject* parent = nullptr); + + void forget(); + + [[nodiscard]] bool isValid() const; + [[nodiscard]] QString path() const; + [[nodiscard]] QString address() const; + [[nodiscard]] ConnectionSettingsMap settings() const { return this->bSettings; }; + [[nodiscard]] WifiSecurityType::Enum security() const { return this->bSecurity; }; + [[nodiscard]] QBindable bindableSecurity() { return &this->bSecurity; }; + +signals: + void loaded(); + void settingsChanged(ConnectionSettingsMap settings); + void securityChanged(WifiSecurityType::Enum security); + void ssidChanged(QString ssid); + +private: + bool mLoaded = false; + void updateSettings(); + // clang-format off + Q_OBJECT_BINDABLE_PROPERTY(NMConnectionSettings, ConnectionSettingsMap, bSettings, &NMConnectionSettings::settingsChanged); + Q_OBJECT_BINDABLE_PROPERTY(NMConnectionSettings, WifiSecurityType::Enum, bSecurity, &NMConnectionSettings::securityChanged); + QS_DBUS_BINDABLE_PROPERTY_GROUP(NMConnectionSettings, connectionSettingsProperties); + // clang-format on + + DBusNMConnectionSettingsProxy* proxy = nullptr; +}; + +// Proxy of a /org/freedesktop/NetworkManager/ActiveConnection/* object. +class NMActiveConnection: public QObject { + Q_OBJECT; + +public: + explicit NMActiveConnection(const QString& path, QObject* parent = nullptr); + + [[nodiscard]] bool isValid() const; + [[nodiscard]] QString path() const; + [[nodiscard]] QString address() const; + [[nodiscard]] QDBusObjectPath connection() const { return this->bConnection; }; + [[nodiscard]] NMConnectionState::Enum state() const { return this->bState; }; + [[nodiscard]] NMConnectionStateReason::Enum stateReason() const { return this->mStateReason; }; + +signals: + void loaded(); + void connectionChanged(QDBusObjectPath path); + void stateChanged(NMConnectionState::Enum state); + void stateReasonChanged(NMConnectionStateReason::Enum reason); + void uuidChanged(const QString& uuid); + +private slots: + void onStateChanged(quint32 state, quint32 reason); + +private: + NMConnectionStateReason::Enum mStateReason = NMConnectionStateReason::Unknown; + + // clang-format off + Q_OBJECT_BINDABLE_PROPERTY(NMActiveConnection, QDBusObjectPath, bConnection, &NMActiveConnection::connectionChanged); + Q_OBJECT_BINDABLE_PROPERTY(NMActiveConnection, QString, bUuid, &NMActiveConnection::uuidChanged); + Q_OBJECT_BINDABLE_PROPERTY(NMActiveConnection, NMConnectionState::Enum, bState, &NMActiveConnection::stateChanged); + + QS_DBUS_BINDABLE_PROPERTY_GROUP(NMActiveConnection, activeConnectionProperties); + QS_DBUS_PROPERTY_BINDING(NMActiveConnection, pConnection, bConnection, activeConnectionProperties, "Connection"); + QS_DBUS_PROPERTY_BINDING(NMActiveConnection, pUuid, bUuid, activeConnectionProperties, "Uuid"); + QS_DBUS_PROPERTY_BINDING(NMActiveConnection, pState, bState, activeConnectionProperties, "State"); + // clang-format on + DBusNMActiveConnectionProxy* proxy = nullptr; +}; + +} // namespace qs::network diff --git a/src/network/nm/dbus_types.hpp b/src/network/nm/dbus_types.hpp new file mode 100644 index 0000000..dadbcf3 --- /dev/null +++ b/src/network/nm/dbus_types.hpp @@ -0,0 +1,9 @@ +#pragma once + +#include +#include +#include +#include + +using ConnectionSettingsMap = QMap; +Q_DECLARE_METATYPE(ConnectionSettingsMap); diff --git a/src/network/nm/device.cpp b/src/network/nm/device.cpp new file mode 100644 index 0000000..aad565d --- /dev/null +++ b/src/network/nm/device.cpp @@ -0,0 +1,143 @@ +#include "device.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../../core/logcat.hpp" +#include "../../dbus/properties.hpp" +#include "../device.hpp" +#include "connection.hpp" +#include "dbus_nm_device.h" + +namespace qs::network { +using namespace qs::dbus; + +namespace { +QS_LOGGING_CATEGORY(logNetworkManager, "quickshell.network.networkmanager", QtWarningMsg); +} + +NMDevice::NMDevice(const QString& path, QObject* parent): QObject(parent) { + this->deviceProxy = new DBusNMDeviceProxy( + "org.freedesktop.NetworkManager", + path, + QDBusConnection::systemBus(), + this + ); + + if (!this->deviceProxy->isValid()) { + qCWarning(logNetworkManager) << "Cannot create DBus interface for device at" << path; + return; + } + + // clang-format off + QObject::connect(this, &NMDevice::availableConnectionPathsChanged, this, &NMDevice::onAvailableConnectionPathsChanged); + QObject::connect(this, &NMDevice::activeConnectionPathChanged, this, &NMDevice::onActiveConnectionPathChanged); + // clang-format on + + this->deviceProperties.setInterface(this->deviceProxy); + this->deviceProperties.updateAllViaGetAll(); +} + +void NMDevice::onActiveConnectionPathChanged(const QDBusObjectPath& path) { + const QString stringPath = path.path(); + + // Remove old active connection + if (this->mActiveConnection) { + QObject::disconnect(this->mActiveConnection, nullptr, this, nullptr); + delete this->mActiveConnection; + this->mActiveConnection = nullptr; + } + + // Create new active connection + if (stringPath != "/") { + auto* active = new NMActiveConnection(stringPath, this); + if (!active->isValid()) { + qCWarning(logNetworkManager) << "Ignoring invalid registration of" << stringPath; + delete active; + } else { + this->mActiveConnection = active; + QObject::connect( + active, + &NMActiveConnection::loaded, + this, + [this, active]() { emit this->activeConnectionLoaded(active); }, + Qt::SingleShotConnection + ); + } + } +} + +void NMDevice::onAvailableConnectionPathsChanged(const QList& paths) { + QSet newPathSet; + for (const QDBusObjectPath& path: paths) { + newPathSet.insert(path.path()); + } + const auto existingPaths = this->mConnections.keys(); + const QSet existingPathSet(existingPaths.begin(), existingPaths.end()); + + const auto addedConnections = newPathSet - existingPathSet; + const auto removedConnections = existingPathSet - newPathSet; + + for (const QString& path: addedConnections) { + this->registerConnection(path); + } + for (const QString& path: removedConnections) { + auto* connection = this->mConnections.take(path); + if (!connection) { + qCDebug(logNetworkManager) << "Sent removal signal for" << path << "which is not registered."; + } else { + delete connection; + } + }; +} + +void NMDevice::registerConnection(const QString& path) { + auto* connection = new NMConnectionSettings(path, this); + if (!connection->isValid()) { + qCWarning(logNetworkManager) << "Ignoring invalid registration of" << path; + delete connection; + } else { + this->mConnections.insert(path, connection); + QObject::connect( + connection, + &NMConnectionSettings::loaded, + this, + [this, connection]() { emit this->connectionLoaded(connection); }, + Qt::SingleShotConnection + ); + } +} + +void NMDevice::disconnect() { this->deviceProxy->Disconnect(); } + +void NMDevice::setAutoconnect(bool autoconnect) { + if (autoconnect == this->bAutoconnect) return; + this->bAutoconnect = autoconnect; + this->pAutoconnect.write(); +} + +bool NMDevice::isValid() const { return this->deviceProxy && this->deviceProxy->isValid(); } +QString NMDevice::address() const { + return this->deviceProxy ? this->deviceProxy->service() : QString(); +} +QString NMDevice::path() const { return this->deviceProxy ? this->deviceProxy->path() : QString(); } + +} // namespace qs::network + +namespace qs::dbus { + +DBusResult +DBusDataTransform::fromWire(quint32 wire) { + return DBusResult(static_cast(wire)); +} + +} // namespace qs::dbus diff --git a/src/network/nm/device.hpp b/src/network/nm/device.hpp new file mode 100644 index 0000000..e3ff4b9 --- /dev/null +++ b/src/network/nm/device.hpp @@ -0,0 +1,100 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "../../dbus/properties.hpp" +#include "connection.hpp" +#include "dbus_nm_device.h" + +namespace qs::dbus { + +template <> +struct DBusDataTransform { + using Wire = quint32; + using Data = qs::network::NMDeviceState::Enum; + static DBusResult fromWire(Wire wire); +}; + +} // namespace qs::dbus + +namespace qs::network { + +// Proxy of a /org/freedesktop/NetworkManager/Device/* object. +// Only the members from the org.freedesktop.NetworkManager.Device interface. +// Owns the lifetime of NMActiveConnection(s) and NMConnectionSetting(s). +class NMDevice: public QObject { + Q_OBJECT; + +public: + explicit NMDevice(const QString& path, QObject* parent = nullptr); + + [[nodiscard]] virtual bool isValid() const; + [[nodiscard]] QString path() const; + [[nodiscard]] QString address() const; + [[nodiscard]] QString interface() const { return this->bInterface; }; + [[nodiscard]] QString hwAddress() const { return this->bHwAddress; }; + [[nodiscard]] bool managed() const { return this->bManaged; }; + [[nodiscard]] NMDeviceState::Enum state() const { return this->bState; }; + [[nodiscard]] bool autoconnect() const { return this->bAutoconnect; }; + [[nodiscard]] NMActiveConnection* activeConnection() const { return this->mActiveConnection; }; + +signals: + void activateConnection(const QDBusObjectPath& connPath, const QDBusObjectPath& devPath); + void addAndActivateConnection( + const ConnectionSettingsMap& settings, + const QDBusObjectPath& devPath, + const QDBusObjectPath& apPath + ); + void connectionLoaded(NMConnectionSettings* connection); + void connectionRemoved(NMConnectionSettings* connection); + void availableConnectionPathsChanged(QList paths); + void activeConnectionPathChanged(const QDBusObjectPath& connection); + void activeConnectionLoaded(NMActiveConnection* active); + void interfaceChanged(const QString& interface); + void hwAddressChanged(const QString& hwAddress); + void managedChanged(bool managed); + void stateChanged(NMDeviceState::Enum state); + void autoconnectChanged(bool autoconnect); + +public slots: + void disconnect(); + void setAutoconnect(bool autoconnect); + +private slots: + void onAvailableConnectionPathsChanged(const QList& paths); + void onActiveConnectionPathChanged(const QDBusObjectPath& path); + +private: + void registerConnection(const QString& path); + + QHash mConnections; + NMActiveConnection* mActiveConnection = nullptr; + + // clang-format off + Q_OBJECT_BINDABLE_PROPERTY(NMDevice, QString, bInterface, &NMDevice::interfaceChanged); + Q_OBJECT_BINDABLE_PROPERTY(NMDevice, QString, bHwAddress, &NMDevice::hwAddressChanged); + Q_OBJECT_BINDABLE_PROPERTY(NMDevice, bool, bManaged, &NMDevice::managedChanged); + Q_OBJECT_BINDABLE_PROPERTY(NMDevice, NMDeviceState::Enum, bState, &NMDevice::stateChanged); + Q_OBJECT_BINDABLE_PROPERTY(NMDevice, bool, bAutoconnect, &NMDevice::autoconnectChanged); + Q_OBJECT_BINDABLE_PROPERTY(NMDevice, QList, bAvailableConnections, &NMDevice::availableConnectionPathsChanged); + Q_OBJECT_BINDABLE_PROPERTY(NMDevice, QDBusObjectPath, bActiveConnection, &NMDevice::activeConnectionPathChanged); + + QS_DBUS_BINDABLE_PROPERTY_GROUP(NMDeviceAdapter, deviceProperties); + QS_DBUS_PROPERTY_BINDING(NMDevice, pName, bInterface, deviceProperties, "Interface"); + QS_DBUS_PROPERTY_BINDING(NMDevice, pAddress, bHwAddress, deviceProperties, "HwAddress"); + QS_DBUS_PROPERTY_BINDING(NMDevice, pManaged, bManaged, deviceProperties, "Managed"); + QS_DBUS_PROPERTY_BINDING(NMDevice, pState, bState, deviceProperties, "State"); + QS_DBUS_PROPERTY_BINDING(NMDevice, pAutoconnect, bAutoconnect, deviceProperties, "Autoconnect"); + QS_DBUS_PROPERTY_BINDING(NMDevice, pAvailableConnections, bAvailableConnections, deviceProperties, "AvailableConnections"); + QS_DBUS_PROPERTY_BINDING(NMDevice, pActiveConnection, bActiveConnection, deviceProperties, "ActiveConnection"); + // clang-format on + + DBusNMDeviceProxy* deviceProxy = nullptr; +}; + +} // namespace qs::network diff --git a/src/network/nm/enums.hpp b/src/network/nm/enums.hpp new file mode 100644 index 0000000..34e5b65 --- /dev/null +++ b/src/network/nm/enums.hpp @@ -0,0 +1,156 @@ +#pragma once + +#include +#include +#include +#include + +namespace qs::network { + +// Indicates the type of hardware represented by a device object. +class NMDeviceType: public QObject { + Q_OBJECT; + +public: + enum Enum : quint8 { + Unknown = 0, + Ethernet = 1, + Wifi = 2, + Unused1 = 3, + Unused2 = 4, + Bluetooth = 5, + OlpcMesh = 6, + Wimax = 7, + Modem = 8, + InfiniBand = 9, + Bond = 10, + Vlan = 11, + Adsl = 12, + Bridge = 13, + Generic = 14, + Team = 15, + Tun = 16, + IpTunnel = 17, + MacVlan = 18, + VxLan = 19, + Veth = 20, + MacSec = 21, + Dummy = 22, + Ppp = 23, + OvsInterface = 24, + OvsPort = 25, + OvsBridge = 26, + Wpan = 27, + Lowpan = 28, + Wireguard = 29, + WifiP2P = 30, + Vrf = 31, + Loopback = 32, + Hsr = 33, + IpVlan = 34, + }; + Q_ENUM(Enum); +}; + +// 802.11 specific device encryption and authentication capabilities. +// In sync with https://networkmanager.dev/docs/api/latest/nm-dbus-types.html#NMDeviceWifiCapabilities. +class NMWirelessCapabilities: public QObject { + Q_OBJECT; + +public: + enum Enum : quint16 { + None = 0, + CipherWep40 = 1, + CipherWep104 = 2, + CipherTkip = 4, + CipherCcmp = 8, + Wpa = 16, + Rsn = 32, + Ap = 64, + Adhoc = 128, + FreqValid = 256, + Freq2Ghz = 512, + Freq5Ghz = 1024, + Freq6Ghz = 2048, + Mesh = 4096, + IbssRsn = 8192, + }; + Q_ENUM(Enum); +}; + +// Indicates the 802.11 mode an access point is currently in. +// In sync with https://networkmanager.dev/docs/api/latest/nm-dbus-types.html#NM80211Mode. +class NM80211Mode: public QObject { + Q_OBJECT; + +public: + enum Enum : quint8 { + Unknown = 0, + Adhoc = 1, + Infra = 2, + Ap = 3, + Mesh = 4, + }; + Q_ENUM(Enum); +}; + +// 802.11 access point flags. +// In sync with https://networkmanager.dev/docs/api/latest/nm-dbus-types.html#NM80211ApSecurityFlags. +class NM80211ApFlags: public QObject { + Q_OBJECT; + +public: + enum Enum : quint8 { + None = 0, + Privacy = 1, + Wps = 2, + WpsPbc = 4, + WpsPin = 8, + }; + Q_ENUM(Enum); +}; + +// 802.11 access point security and authentication flags. +// These flags describe the current system requirements of an access point as determined from the access point's beacon. +// In sync with https://networkmanager.dev/docs/api/latest/nm-dbus-types.html#NM80211ApSecurityFlags. +class NM80211ApSecurityFlags: public QObject { + Q_OBJECT; + +public: + enum Enum : quint16 { + None = 0, + PairWep40 = 1, + PairWep104 = 2, + PairTkip = 4, + PairCcmp = 8, + GroupWep40 = 16, + GroupWep104 = 32, + GroupTkip = 64, + GroupCcmp = 128, + KeyMgmtPsk = 256, + KeyMgmt8021x = 512, + KeyMgmtSae = 1024, + KeyMgmtOwe = 2048, + KeyMgmtOweTm = 4096, + KeyMgmtEapSuiteB192 = 8192, + }; + Q_ENUM(Enum); +}; + +// Indicates the state of a connection to a specific network while it is starting, connected, or disconnected from that network. +// In sync with https://networkmanager.dev/docs/api/latest/nm-dbus-types.html#NMActiveConnectionState. +class NMConnectionState: public QObject { + Q_OBJECT; + +public: + enum Enum : quint8 { + Unknown = 0, + Activating = 1, + Activated = 2, + Deactivating = 3, + Deactivated = 4 + }; + Q_ENUM(Enum); +}; + +} // namespace qs::network diff --git a/src/network/nm/org.freedesktop.NetworkManager.AccessPoint.xml b/src/network/nm/org.freedesktop.NetworkManager.AccessPoint.xml new file mode 100644 index 0000000..c5e7737 --- /dev/null +++ b/src/network/nm/org.freedesktop.NetworkManager.AccessPoint.xml @@ -0,0 +1,4 @@ + + + + diff --git a/src/network/nm/org.freedesktop.NetworkManager.Connection.Active.xml b/src/network/nm/org.freedesktop.NetworkManager.Connection.Active.xml new file mode 100644 index 0000000..fa0e778 --- /dev/null +++ b/src/network/nm/org.freedesktop.NetworkManager.Connection.Active.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/network/nm/org.freedesktop.NetworkManager.Device.Wireless.xml b/src/network/nm/org.freedesktop.NetworkManager.Device.Wireless.xml new file mode 100644 index 0000000..ccfe333 --- /dev/null +++ b/src/network/nm/org.freedesktop.NetworkManager.Device.Wireless.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/src/network/nm/org.freedesktop.NetworkManager.Device.xml b/src/network/nm/org.freedesktop.NetworkManager.Device.xml new file mode 100644 index 0000000..322635f --- /dev/null +++ b/src/network/nm/org.freedesktop.NetworkManager.Device.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/src/network/nm/org.freedesktop.NetworkManager.Settings.Connection.xml b/src/network/nm/org.freedesktop.NetworkManager.Settings.Connection.xml new file mode 100644 index 0000000..0283847 --- /dev/null +++ b/src/network/nm/org.freedesktop.NetworkManager.Settings.Connection.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/src/network/nm/org.freedesktop.NetworkManager.xml b/src/network/nm/org.freedesktop.NetworkManager.xml new file mode 100644 index 0000000..d4470ea --- /dev/null +++ b/src/network/nm/org.freedesktop.NetworkManager.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/network/nm/utils.cpp b/src/network/nm/utils.cpp new file mode 100644 index 0000000..0be29e5 --- /dev/null +++ b/src/network/nm/utils.cpp @@ -0,0 +1,248 @@ +#include "utils.hpp" + +// We depend on non-std Linux extensions that ctime doesn't put in the global namespace +// NOLINTNEXTLINE(modernize-deprecated-headers) +#include + +#include +#include +#include +#include +#include +#include + +#include "../wifi.hpp" +#include "dbus_types.hpp" +#include "enums.hpp" + +namespace qs::network { + +WifiSecurityType::Enum securityFromConnectionSettings(const ConnectionSettingsMap& settings) { + const QVariantMap& security = settings.value("802-11-wireless-security"); + if (security.isEmpty()) { + return WifiSecurityType::Open; + }; + + const QString keyMgmt = security["key-mgmt"].toString(); + const QString authAlg = security["auth-alg"].toString(); + const QList proto = security["proto"].toList(); + + if (keyMgmt == "none") { + return WifiSecurityType::StaticWep; + } else if (keyMgmt == "ieee8021x") { + if (authAlg == "leap") { + return WifiSecurityType::Leap; + } else { + return WifiSecurityType::DynamicWep; + } + } else if (keyMgmt == "wpa-psk") { + if (proto.contains("wpa") && proto.contains("rsn")) return WifiSecurityType::WpaPsk; + return WifiSecurityType::Wpa2Psk; + } else if (keyMgmt == "wpa-eap") { + if (proto.contains("wpa") && proto.contains("rsn")) return WifiSecurityType::WpaEap; + return WifiSecurityType::Wpa2Eap; + } else if (keyMgmt == "sae") { + return WifiSecurityType::Sae; + } else if (keyMgmt == "wpa-eap-suite-b-192") { + return WifiSecurityType::Wpa3SuiteB192; + } + return WifiSecurityType::Open; +} + +bool deviceSupportsApCiphers( + NMWirelessCapabilities::Enum caps, + NM80211ApSecurityFlags::Enum apFlags, + WifiSecurityType::Enum type +) { + bool havePair = false; + bool haveGroup = false; + // Device needs to support at least one pairwise and one group cipher + + if (type == WifiSecurityType::StaticWep) { + // Static WEP only uses group ciphers + havePair = true; + } else { + if (caps & NMWirelessCapabilities::CipherWep40 && apFlags & NM80211ApSecurityFlags::PairWep40) { + havePair = true; + } + if (caps & NMWirelessCapabilities::CipherWep104 && apFlags & NM80211ApSecurityFlags::PairWep104) + { + havePair = true; + } + if (caps & NMWirelessCapabilities::CipherTkip && apFlags & NM80211ApSecurityFlags::PairTkip) { + havePair = true; + } + if (caps & NMWirelessCapabilities::CipherCcmp && apFlags & NM80211ApSecurityFlags::PairCcmp) { + havePair = true; + } + } + + if (caps & NMWirelessCapabilities::CipherWep40 && apFlags & NM80211ApSecurityFlags::GroupWep40) { + haveGroup = true; + } + if (caps & NMWirelessCapabilities::CipherWep104 && apFlags & NM80211ApSecurityFlags::GroupWep104) + { + haveGroup = true; + } + if (type != WifiSecurityType::StaticWep) { + if (caps & NMWirelessCapabilities::CipherTkip && apFlags & NM80211ApSecurityFlags::GroupTkip) { + haveGroup = true; + } + if (caps & NMWirelessCapabilities::CipherCcmp && apFlags & NM80211ApSecurityFlags::GroupCcmp) { + haveGroup = true; + } + } + + return (havePair && haveGroup); +} + +bool securityIsValid( + WifiSecurityType::Enum type, + NMWirelessCapabilities::Enum caps, + bool adhoc, + NM80211ApFlags::Enum apFlags, + NM80211ApSecurityFlags::Enum apWpa, + NM80211ApSecurityFlags::Enum apRsn +) { + switch (type) { + case WifiSecurityType::Open: + if (apFlags & NM80211ApFlags::Privacy) return false; + if (apWpa || apRsn) return false; + break; + case WifiSecurityType::Leap: + if (adhoc) return false; + case WifiSecurityType::StaticWep: + if (!(apFlags & NM80211ApFlags::Privacy)) return false; + if (apWpa || apRsn) { + if (!deviceSupportsApCiphers(caps, apWpa, WifiSecurityType::StaticWep)) { + if (!deviceSupportsApCiphers(caps, apRsn, WifiSecurityType::StaticWep)) return false; + } + } + break; + case WifiSecurityType::DynamicWep: + if (adhoc) return false; + if (apRsn || !(apFlags & NM80211ApFlags::Privacy)) return false; + if (apWpa) { + if (!(apWpa & NM80211ApSecurityFlags::KeyMgmt8021x)) return false; + if (!deviceSupportsApCiphers(caps, apWpa, WifiSecurityType::DynamicWep)) return false; + } + break; + case WifiSecurityType::WpaPsk: + if (adhoc) return false; + if (!(caps & NMWirelessCapabilities::Wpa)) return false; + if (apWpa & NM80211ApSecurityFlags::KeyMgmtPsk) { + if (apWpa & NM80211ApSecurityFlags::PairTkip && caps & NMWirelessCapabilities::CipherTkip) { + return true; + } + if (apWpa & NM80211ApSecurityFlags::PairCcmp && caps & NMWirelessCapabilities::CipherCcmp) { + return true; + } + } + return false; + case WifiSecurityType::Wpa2Psk: + if (!(caps & NMWirelessCapabilities::Rsn)) return false; + if (adhoc) { + if (!(caps & NMWirelessCapabilities::IbssRsn)) return false; + if (apRsn & NM80211ApSecurityFlags::PairCcmp && caps & NMWirelessCapabilities::CipherCcmp) { + return true; + } + } else { + if (apRsn & NM80211ApSecurityFlags::KeyMgmtPsk) { + if (apRsn & NM80211ApSecurityFlags::PairTkip && caps & NMWirelessCapabilities::CipherTkip) { + return true; + } + if (apRsn & NM80211ApSecurityFlags::PairCcmp && caps & NMWirelessCapabilities::CipherCcmp) { + return true; + } + } + } + return false; + case WifiSecurityType::WpaEap: + if (adhoc) return false; + if (!(caps & NMWirelessCapabilities::Wpa)) return false; + if (!(apWpa & NM80211ApSecurityFlags::KeyMgmt8021x)) return false; + if (!deviceSupportsApCiphers(caps, apWpa, WifiSecurityType::WpaEap)) return false; + break; + case WifiSecurityType::Wpa2Eap: + if (adhoc) return false; + if (!(caps & NMWirelessCapabilities::Rsn)) return false; + if (!(apRsn & NM80211ApSecurityFlags::KeyMgmt8021x)) return false; + if (!deviceSupportsApCiphers(caps, apRsn, WifiSecurityType::Wpa2Eap)) return false; + break; + case WifiSecurityType::Sae: + if (!(caps & NMWirelessCapabilities::Rsn)) return false; + if (adhoc) { + if (!(caps & NMWirelessCapabilities::IbssRsn)) return false; + if (apRsn & NM80211ApSecurityFlags::PairCcmp && caps & NMWirelessCapabilities::CipherCcmp) { + return true; + } + } else { + if (apRsn & NM80211ApSecurityFlags::KeyMgmtSae) { + if (apRsn & NM80211ApSecurityFlags::PairTkip && caps & NMWirelessCapabilities::CipherTkip) { + return true; + } + if (apRsn & NM80211ApSecurityFlags::PairCcmp && caps & NMWirelessCapabilities::CipherCcmp) { + return true; + } + } + } + return false; + case WifiSecurityType::Owe: + if (adhoc) return false; + if (!(caps & NMWirelessCapabilities::Rsn)) return false; + if (!(apRsn & NM80211ApSecurityFlags::KeyMgmtOwe) + && !(apRsn & NM80211ApSecurityFlags::KeyMgmtOweTm)) + { + return false; + } + break; + case WifiSecurityType::Wpa3SuiteB192: + if (adhoc) return false; + if (!(caps & NMWirelessCapabilities::Rsn)) return false; + if (!(apRsn & NM80211ApSecurityFlags::KeyMgmtEapSuiteB192)) return false; + break; + default: return false; + } + return true; +} + +WifiSecurityType::Enum findBestWirelessSecurity( + NMWirelessCapabilities::Enum caps, + bool adHoc, + NM80211ApFlags::Enum apFlags, + NM80211ApSecurityFlags::Enum apWpa, + NM80211ApSecurityFlags::Enum apRsn +) { + // Loop through security types from most to least secure since the enum + // values are sequential and in priority order (0-10, excluding Unknown=11) + for (int i = WifiSecurityType::Wpa3SuiteB192; i <= WifiSecurityType::Open; ++i) { + auto type = static_cast(i); + if (securityIsValid(type, caps, adHoc, apFlags, apWpa, apRsn)) { + return type; + } + } + return WifiSecurityType::Unknown; +} + +// NOLINTBEGIN +QDateTime clockBootTimeToDateTime(qint64 clockBootTime) { + clockid_t clkId = CLOCK_BOOTTIME; + struct timespec tp {}; + + const QDateTime now = QDateTime::currentDateTime(); + int r = clock_gettime(clkId, &tp); + if (r == -1 && errno == EINVAL) { + clkId = CLOCK_MONOTONIC; + r = clock_gettime(clkId, &tp); + } + + // Convert to milliseconds + const qint64 nowInMs = tp.tv_sec * 1000 + tp.tv_nsec / 1000000; + + // Return a QDateTime of the millisecond diff + const qint64 offset = clockBootTime - nowInMs; + return QDateTime::fromMSecsSinceEpoch(now.toMSecsSinceEpoch() + offset); +} +// NOLINTEND + +} // namespace qs::network diff --git a/src/network/nm/utils.hpp b/src/network/nm/utils.hpp new file mode 100644 index 0000000..ce8b784 --- /dev/null +++ b/src/network/nm/utils.hpp @@ -0,0 +1,45 @@ +#pragma once + +#include +#include +#include +#include + +#include "../wifi.hpp" +#include "dbus_types.hpp" +#include "enums.hpp" + +namespace qs::network { + +WifiSecurityType::Enum securityFromConnectionSettings(const ConnectionSettingsMap& settings); + +bool deviceSupportsApCiphers( + NMWirelessCapabilities::Enum caps, + NM80211ApSecurityFlags::Enum apFlags, + WifiSecurityType::Enum type +); + +// In sync with NetworkManager/libnm-core/nm-utils.c:nm_utils_security_valid() +// Given a set of device capabilities, and a desired security type to check +// against, determines whether the combination of device, desired security type, +// and AP capabilities intersect. +bool securityIsValid( + WifiSecurityType::Enum type, + NMWirelessCapabilities::Enum caps, + bool adhoc, + NM80211ApFlags::Enum apFlags, + NM80211ApSecurityFlags::Enum apWpa, + NM80211ApSecurityFlags::Enum apRsn +); + +WifiSecurityType::Enum findBestWirelessSecurity( + NMWirelessCapabilities::Enum caps, + bool adHoc, + NM80211ApFlags::Enum apFlags, + NM80211ApSecurityFlags::Enum apWpa, + NM80211ApSecurityFlags::Enum apRsn +); + +QDateTime clockBootTimeToDateTime(qint64 clockBootTime); + +} // namespace qs::network diff --git a/src/network/nm/wireless.cpp b/src/network/nm/wireless.cpp new file mode 100644 index 0000000..9dff14b --- /dev/null +++ b/src/network/nm/wireless.cpp @@ -0,0 +1,457 @@ +#include "wireless.hpp" +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../../core/logcat.hpp" +#include "../../dbus/properties.hpp" +#include "../network.hpp" +#include "../wifi.hpp" +#include "accesspoint.hpp" +#include "connection.hpp" +#include "dbus_nm_wireless.h" +#include "dbus_types.hpp" +#include "device.hpp" +#include "enums.hpp" +#include "utils.hpp" + +namespace qs::network { +using namespace qs::dbus; + +namespace { +QS_LOGGING_CATEGORY(logNetworkManager, "quickshell.network.networkmanager", QtWarningMsg); +} + +NMWirelessNetwork::NMWirelessNetwork(QString ssid, QObject* parent) + : QObject(parent) + , mSsid(std::move(ssid)) + , bKnown(false) + , bSecurity(WifiSecurityType::Unknown) + , bReason(NMConnectionStateReason::None) + , bState(NMConnectionState::Deactivated) {} + +void NMWirelessNetwork::updateReferenceConnection() { + // If the network has no connections, the reference is nullptr. + if (this->mConnections.isEmpty()) { + this->mReferenceConn = nullptr; + this->bSecurity = WifiSecurityType::Unknown; + // Set security back to reference AP. + if (this->mReferenceAp) { + this->bSecurity.setBinding([this]() { return this->mReferenceAp->security(); }); + } + return; + }; + + // If the network has an active connection, use it as the reference. + if (this->mActiveConnection) { + auto* conn = this->mConnections.value(this->mActiveConnection->connection().path()); + if (conn && conn != this->mReferenceConn) { + this->mReferenceConn = conn; + this->bSecurity.setBinding([conn]() { return conn->security(); }); + } + return; + } + + // Otherwise, choose the connection with the strongest security settings. + NMConnectionSettings* selectedConn = nullptr; + for (auto* conn: this->mConnections.values()) { + if (!selectedConn || conn->security() > selectedConn->security()) { + selectedConn = conn; + } + } + if (this->mReferenceConn != selectedConn) { + this->mReferenceConn = selectedConn; + this->bSecurity.setBinding([selectedConn]() { return selectedConn->security(); }); + } +} + +void NMWirelessNetwork::updateReferenceAp() { + // If the network has no APs, the reference is a nullptr. + if (this->mAccessPoints.isEmpty()) { + this->mReferenceAp = nullptr; + this->bSignalStrength = 0; + return; + } + + // Otherwise, choose the AP with the strongest signal. + NMAccessPoint* selectedAp = nullptr; + for (auto* ap: this->mAccessPoints.values()) { + // Always prefer the active AP. + if (ap->path() == this->bActiveApPath) { + selectedAp = ap; + break; + } + if (!selectedAp || ap->signalStrength() > selectedAp->signalStrength()) { + selectedAp = ap; + } + } + if (this->mReferenceAp != selectedAp) { + this->mReferenceAp = selectedAp; + this->bSignalStrength.setBinding([selectedAp]() { return selectedAp->signalStrength(); }); + // Reference AP is used for security when there's no connection settings. + if (!this->mReferenceConn) { + this->bSecurity.setBinding([selectedAp]() { return selectedAp->security(); }); + } + } +} + +void NMWirelessNetwork::addAccessPoint(NMAccessPoint* ap) { + if (this->mAccessPoints.contains(ap->path())) return; + this->mAccessPoints.insert(ap->path(), ap); + auto onDestroyed = [this, ap]() { + if (this->mAccessPoints.take(ap->path())) { + this->updateReferenceAp(); + if (this->mAccessPoints.isEmpty() && this->mConnections.isEmpty()) emit this->disappeared(); + } + }; + // clang-format off + QObject::connect(ap, &NMAccessPoint::signalStrengthChanged, this, &NMWirelessNetwork::updateReferenceAp); + QObject::connect(ap, &NMAccessPoint::destroyed, this, onDestroyed); + // clang-format on + this->updateReferenceAp(); +}; + +void NMWirelessNetwork::addConnection(NMConnectionSettings* conn) { + if (this->mConnections.contains(conn->path())) return; + this->mConnections.insert(conn->path(), conn); + auto onDestroyed = [this, conn]() { + if (this->mConnections.take(conn->path())) { + this->updateReferenceConnection(); + if (this->mConnections.isEmpty()) this->bKnown = false; + if (this->mAccessPoints.isEmpty() && this->mConnections.isEmpty()) emit this->disappeared(); + } + }; + // clang-format off + QObject::connect(conn, &NMConnectionSettings::securityChanged, this, &NMWirelessNetwork::updateReferenceConnection); + QObject::connect(conn, &NMConnectionSettings::destroyed, this, onDestroyed); + // clang-format on + this->bKnown = true; + this->updateReferenceConnection(); +}; + +void NMWirelessNetwork::addActiveConnection(NMActiveConnection* active) { + if (this->mActiveConnection) return; + this->mActiveConnection = active; + this->bState.setBinding([active]() { return active->state(); }); + this->bReason.setBinding([active]() { return active->stateReason(); }); + auto onDestroyed = [this, active]() { + if (this->mActiveConnection && this->mActiveConnection == active) { + this->mActiveConnection = nullptr; + this->updateReferenceConnection(); + this->bState = NMConnectionState::Deactivated; + this->bReason = NMConnectionStateReason::None; + } + }; + QObject::connect(active, &NMActiveConnection::destroyed, this, onDestroyed); + this->updateReferenceConnection(); +}; + +void NMWirelessNetwork::forget() { + if (this->mConnections.isEmpty()) return; + for (auto* conn: this->mConnections.values()) { + conn->forget(); + } +} + +NMWirelessDevice::NMWirelessDevice(const QString& path, QObject* parent) + : NMDevice(path, parent) + , mScanTimer(this) { + this->wirelessProxy = new DBusNMWirelessProxy( + "org.freedesktop.NetworkManager", + path, + QDBusConnection::systemBus(), + this + ); + + if (!this->wirelessProxy->isValid()) { + qCWarning(logNetworkManager) << "Cannot create DBus interface for wireless device at" << path; + return; + } + + QObject::connect( + &this->wirelessProperties, + &DBusPropertyGroup::getAllFinished, + this, + &NMWirelessDevice::initWireless, + Qt::SingleShotConnection + ); + + QObject::connect(&this->mScanTimer, &QTimer::timeout, this, &NMWirelessDevice::onScanTimeout); + this->mScanTimer.setSingleShot(true); + + this->wirelessProperties.setInterface(this->wirelessProxy); + this->wirelessProperties.updateAllViaGetAll(); +} + +void NMWirelessDevice::initWireless() { + // clang-format off + QObject::connect(this->wirelessProxy, &DBusNMWirelessProxy::AccessPointAdded, this, &NMWirelessDevice::onAccessPointAdded); + QObject::connect(this->wirelessProxy, &DBusNMWirelessProxy::AccessPointRemoved, this, &NMWirelessDevice::onAccessPointRemoved); + QObject::connect(this, &NMWirelessDevice::accessPointLoaded, this, &NMWirelessDevice::onAccessPointLoaded); + QObject::connect(this, &NMWirelessDevice::connectionLoaded, this, &NMWirelessDevice::onConnectionLoaded); + QObject::connect(this, &NMWirelessDevice::activeConnectionLoaded, this, &NMWirelessDevice::onActiveConnectionLoaded); + QObject::connect(this, &NMWirelessDevice::scanningChanged, this, &NMWirelessDevice::onScanningChanged); + // clang-format on + this->registerAccessPoints(); +} + +void NMWirelessDevice::onAccessPointAdded(const QDBusObjectPath& path) { + this->registerAccessPoint(path.path()); +} + +void NMWirelessDevice::onAccessPointRemoved(const QDBusObjectPath& path) { + auto* ap = this->mAccessPoints.take(path.path()); + if (!ap) { + qCDebug(logNetworkManager) << "Sent removal signal for" << path.path() + << "which is not registered."; + return; + } + delete ap; +} + +void NMWirelessDevice::onAccessPointLoaded(NMAccessPoint* ap) { + const QString ssid = ap->ssid(); + if (!ssid.isEmpty()) { + auto mode = ap->mode(); + if (mode == NM80211Mode::Infra) { + auto* net = this->mNetworks.value(ssid); + if (!net) net = this->registerNetwork(ssid); + net->addAccessPoint(ap); + } + } +} + +void NMWirelessDevice::onConnectionLoaded(NMConnectionSettings* conn) { + const ConnectionSettingsMap& settings = conn->settings(); + // Filter connections that aren't wireless or have missing settings + if (settings["connection"]["id"].toString().isEmpty() + || settings["connection"]["uuid"].toString().isEmpty() + || !settings.contains("802-11-wireless") + || settings["802-11-wireless"]["ssid"].toString().isEmpty()) + { + return; + } + + const auto ssid = settings["802-11-wireless"]["ssid"].toString(); + const auto mode = settings["802-11-wireless"]["mode"].toString(); + + if (mode == "infrastructure") { + auto* net = this->mNetworks.value(ssid); + if (!net) net = this->registerNetwork(ssid); + net->addConnection(conn); + + // Check for active connections that loaded before their respective connection settings + auto* active = this->activeConnection(); + if (active && conn->path() == active->connection().path()) { + net->addActiveConnection(active); + } + } + // TODO: Create hotspots when mode == "ap" +} + +void NMWirelessDevice::onActiveConnectionLoaded(NMActiveConnection* active) { + // Find an exisiting network with connection settings that matches the active + const QString activeConnPath = active->connection().path(); + for (const auto& net: this->mNetworks.values()) { + for (auto* conn: net->connections()) { + if (activeConnPath == conn->path()) { + net->addActiveConnection(active); + return; + } + } + } +} + +void NMWirelessDevice::onScanTimeout() { + const QDateTime now = QDateTime::currentDateTime(); + const QDateTime lastScan = this->bLastScan; + const QDateTime lastScanRequest = this->mLastScanRequest; + + if (lastScan.isValid() && lastScan.msecsTo(now) < this->mScanIntervalMs) { + // Rate limit if backend last scan property updated within the interval + auto diff = static_cast(this->mScanIntervalMs - lastScan.msecsTo(now)); + this->mScanTimer.start(diff); + } else if (lastScanRequest.isValid() && lastScanRequest.msecsTo(now) < this->mScanIntervalMs) { + // Rate limit if frontend changes scanner state within the interval + auto diff = static_cast(this->mScanIntervalMs - lastScanRequest.msecsTo(now)); + this->mScanTimer.start(diff); + } else { + this->wirelessProxy->RequestScan({}); + this->mLastScanRequest = now; + this->mScanTimer.start(this->mScanIntervalMs); + } +} + +void NMWirelessDevice::onScanningChanged(bool scanning) { + scanning ? this->onScanTimeout() : this->mScanTimer.stop(); +} + +void NMWirelessDevice::registerAccessPoints() { + auto pending = this->wirelessProxy->GetAllAccessPoints(); + auto* call = new QDBusPendingCallWatcher(pending, this); + + auto responseCallback = [this](QDBusPendingCallWatcher* call) { + const QDBusPendingReply> reply = *call; + + if (reply.isError()) { + qCWarning(logNetworkManager) + << "Failed to get all access points: " << reply.error().message(); + } else { + for (const QDBusObjectPath& devicePath: reply.value()) { + this->registerAccessPoint(devicePath.path()); + } + } + + delete call; + }; + + QObject::connect(call, &QDBusPendingCallWatcher::finished, this, responseCallback); +} + +void NMWirelessDevice::registerAccessPoint(const QString& path) { + if (this->mAccessPoints.contains(path)) { + qCDebug(logNetworkManager) << "Skipping duplicate registration of access point" << path; + return; + } + + auto* ap = new NMAccessPoint(path, this); + + if (!ap->isValid()) { + qCWarning(logNetworkManager) << "Ignoring invalid registration of" << path; + delete ap; + return; + } + + this->mAccessPoints.insert(path, ap); + QObject::connect( + ap, + &NMAccessPoint::loaded, + this, + [this, ap]() { emit this->accessPointLoaded(ap); }, + Qt::SingleShotConnection + ); + ap->bindableSecurity().setBinding([this, ap]() { + return findBestWirelessSecurity( + this->bCapabilities, + ap->mode() == NM80211Mode::Adhoc, + ap->flags(), + ap->wpaFlags(), + ap->rsnFlags() + ); + }); +} + +NMWirelessNetwork* NMWirelessDevice::registerNetwork(const QString& ssid) { + auto* net = new NMWirelessNetwork(ssid, this); + + // To avoid exposing outdated state to the frontend, filter the backend networks to only show + // the known or currently connected networks when the scanner is off. + auto visible = [this, net]() { + return this->bScanning || net->state() == NMConnectionState::Activated || net->known(); + }; + auto onVisibilityChanged = [this, net](bool visible) { + visible ? this->registerFrontendNetwork(net) : this->removeFrontendNetwork(net); + }; + + net->bindableVisible().setBinding(visible); + net->bindableActiveApPath().setBinding([this]() { return this->activeApPath().path(); }); + QObject::connect(net, &NMWirelessNetwork::disappeared, this, &NMWirelessDevice::removeNetwork); + QObject::connect(net, &NMWirelessNetwork::visibilityChanged, this, onVisibilityChanged); + + this->mNetworks.insert(ssid, net); + if (net->visible()) this->registerFrontendNetwork(net); + return net; +} + +void NMWirelessDevice::registerFrontendNetwork(NMWirelessNetwork* net) { + auto ssid = net->ssid(); + auto* frontendNet = new WifiNetwork(ssid, net); + + // Bind WifiNetwork to NMWirelessNetwork + auto translateSignal = [net]() { return net->signalStrength() / 100.0; }; + auto translateState = [net]() { return net->state() == NMConnectionState::Activated; }; + frontendNet->bindableSignalStrength().setBinding(translateSignal); + frontendNet->bindableConnected().setBinding(translateState); + frontendNet->bindableKnown().setBinding([net]() { return net->known(); }); + frontendNet->bindableNmReason().setBinding([net]() { return net->reason(); }); + frontendNet->bindableSecurity().setBinding([net]() { return net->security(); }); + frontendNet->bindableState().setBinding([net]() { + return static_cast(net->state()); + }); + + QObject::connect(frontendNet, &WifiNetwork::requestConnect, this, [this, net]() { + if (net->referenceConnection()) { + emit this->activateConnection( + QDBusObjectPath(net->referenceConnection()->path()), + QDBusObjectPath(this->path()) + ); + return; + } + if (net->referenceAp()) { + emit this->addAndActivateConnection( + ConnectionSettingsMap(), + QDBusObjectPath(this->path()), + QDBusObjectPath(net->referenceAp()->path()) + ); + } + }); + + QObject::connect( + frontendNet, + &WifiNetwork::requestDisconnect, + this, + &NMWirelessDevice::disconnect + ); + + QObject::connect(frontendNet, &WifiNetwork::requestForget, net, &NMWirelessNetwork::forget); + + this->mFrontendNetworks.insert(ssid, frontendNet); + emit this->networkAdded(frontendNet); +} + +void NMWirelessDevice::removeFrontendNetwork(NMWirelessNetwork* net) { + auto* frontendNet = this->mFrontendNetworks.take(net->ssid()); + if (frontendNet) { + emit this->networkRemoved(frontendNet); + frontendNet->deleteLater(); + } +} + +void NMWirelessDevice::removeNetwork() { + auto* net = qobject_cast(this->sender()); + if (this->mNetworks.take(net->ssid())) { + this->removeFrontendNetwork(net); + delete net; + }; +} + +bool NMWirelessDevice::isValid() const { + return this->NMDevice::isValid() && (this->wirelessProxy && this->wirelessProxy->isValid()); +} + +} // namespace qs::network + +namespace qs::dbus { + +DBusResult +DBusDataTransform::fromWire(quint32 wire) { + return DBusResult(static_cast(wire)); +} + +DBusResult DBusDataTransform::fromWire(qint64 wire) { + return DBusResult(qs::network::clockBootTimeToDateTime(wire)); +} + +} // namespace qs::dbus diff --git a/src/network/nm/wireless.hpp b/src/network/nm/wireless.hpp new file mode 100644 index 0000000..fe4010e --- /dev/null +++ b/src/network/nm/wireless.hpp @@ -0,0 +1,166 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "../wifi.hpp" +#include "accesspoint.hpp" +#include "connection.hpp" +#include "dbus_nm_wireless.h" +#include "device.hpp" +#include "enums.hpp" + +namespace qs::dbus { +template <> +struct DBusDataTransform { + using Wire = quint32; + using Data = qs::network::NMWirelessCapabilities::Enum; + static DBusResult fromWire(Wire wire); +}; + +template <> +struct DBusDataTransform { + using Wire = qint64; + using Data = QDateTime; + static DBusResult fromWire(Wire wire); +}; + +} // namespace qs::dbus +namespace qs::network { + +// NMWirelessNetwork aggregates all related NMActiveConnection, NMAccessPoint, and NMConnectionSetting objects. +class NMWirelessNetwork: public QObject { + Q_OBJECT; + +public: + explicit NMWirelessNetwork(QString ssid, QObject* parent = nullptr); + + void addAccessPoint(NMAccessPoint* ap); + void addConnection(NMConnectionSettings* conn); + void addActiveConnection(NMActiveConnection* active); + void forget(); + + [[nodiscard]] QString ssid() const { return this->mSsid; }; + [[nodiscard]] quint8 signalStrength() const { return this->bSignalStrength; }; + [[nodiscard]] WifiSecurityType::Enum security() const { return this->bSecurity; }; + [[nodiscard]] NMConnectionState::Enum state() const { return this->bState; }; + [[nodiscard]] bool known() const { return this->bKnown; }; + [[nodiscard]] NMConnectionStateReason::Enum reason() const { return this->bReason; }; + [[nodiscard]] NMAccessPoint* referenceAp() const { return this->mReferenceAp; }; + [[nodiscard]] NMConnectionSettings* referenceConnection() const { return this->mReferenceConn; }; + [[nodiscard]] QList accessPoints() const { return this->mAccessPoints.values(); }; + [[nodiscard]] QList connections() const { + return this->mConnections.values(); + } + [[nodiscard]] QBindable bindableActiveApPath() { return &this->bActiveApPath; }; + [[nodiscard]] QBindable bindableVisible() { return &this->bVisible; }; + [[nodiscard]] bool visible() const { return this->bVisible; }; + +signals: + void disappeared(); + void visibilityChanged(bool visible); + void signalStrengthChanged(quint8 signal); + void stateChanged(NMConnectionState::Enum state); + void knownChanged(bool known); + void securityChanged(WifiSecurityType::Enum security); + void reasonChanged(NMConnectionStateReason::Enum reason); + void capabilitiesChanged(NMWirelessCapabilities::Enum caps); + void activeApPathChanged(QString path); + +private: + void updateReferenceAp(); + void updateReferenceConnection(); + + QString mSsid; + QHash mAccessPoints; + QHash mConnections; + NMAccessPoint* mReferenceAp = nullptr; + NMConnectionSettings* mReferenceConn = nullptr; + NMActiveConnection* mActiveConnection = nullptr; + + // clang-format off + Q_OBJECT_BINDABLE_PROPERTY(NMWirelessNetwork, bool, bVisible, &NMWirelessNetwork::visibilityChanged); + Q_OBJECT_BINDABLE_PROPERTY(NMWirelessNetwork, bool, bKnown, &NMWirelessNetwork::knownChanged); + Q_OBJECT_BINDABLE_PROPERTY(NMWirelessNetwork, WifiSecurityType::Enum, bSecurity, &NMWirelessNetwork::securityChanged); + Q_OBJECT_BINDABLE_PROPERTY(NMWirelessNetwork, NMConnectionStateReason::Enum, bReason, &NMWirelessNetwork::reasonChanged); + Q_OBJECT_BINDABLE_PROPERTY(NMWirelessNetwork, NMConnectionState::Enum, bState, &NMWirelessNetwork::stateChanged); + Q_OBJECT_BINDABLE_PROPERTY(NMWirelessNetwork, quint8, bSignalStrength, &NMWirelessNetwork::signalStrengthChanged); + Q_OBJECT_BINDABLE_PROPERTY(NMWirelessNetwork, QString, bActiveApPath, &NMWirelessNetwork::activeApPathChanged); + // clang-format on +}; + +// Proxy of a /org/freedesktop/NetworkManager/Device/* object. +// Extends NMDevice to also include members from the org.freedesktop.NetworkManager.Device.Wireless interface +// Owns the lifetime of NMAccessPoints(s), NMWirelessNetwork(s), frontend WifiNetwork(s). +class NMWirelessDevice: public NMDevice { + Q_OBJECT; + +public: + explicit NMWirelessDevice(const QString& path, QObject* parent = nullptr); + + [[nodiscard]] bool isValid() const override; + [[nodiscard]] NMWirelessCapabilities::Enum capabilities() { return this->bCapabilities; }; + [[nodiscard]] const QDBusObjectPath& activeApPath() { return this->bActiveAccessPoint; }; + [[nodiscard]] NM80211Mode::Enum mode() { return this->bMode; }; + [[nodiscard]] QBindable bindableScanning() { return &this->bScanning; }; + +signals: + void accessPointLoaded(NMAccessPoint* ap); + void accessPointRemoved(NMAccessPoint* ap); + void networkAdded(WifiNetwork* net); + void networkRemoved(WifiNetwork* net); + void lastScanChanged(QDateTime lastScan); + void scanningChanged(bool scanning); + void capabilitiesChanged(NMWirelessCapabilities::Enum caps); + void activeAccessPointChanged(const QDBusObjectPath& path); + void modeChanged(NM80211Mode::Enum mode); + +private slots: + void onAccessPointAdded(const QDBusObjectPath& path); + void onAccessPointRemoved(const QDBusObjectPath& path); + void onAccessPointLoaded(NMAccessPoint* ap); + void onConnectionLoaded(NMConnectionSettings* conn); + void onActiveConnectionLoaded(NMActiveConnection* active); + void onScanTimeout(); + void onScanningChanged(bool scanning); + +private: + void registerAccessPoint(const QString& path); + void registerFrontendNetwork(NMWirelessNetwork* net); + void removeFrontendNetwork(NMWirelessNetwork* net); + void removeNetwork(); + bool checkVisibility(WifiNetwork* net); + void registerAccessPoints(); + void initWireless(); + NMWirelessNetwork* registerNetwork(const QString& ssid); + + QHash mAccessPoints; + QHash mNetworks; + QHash mFrontendNetworks; + + QDateTime mLastScanRequest; + QTimer mScanTimer; + qint32 mScanIntervalMs = 10001; + + // clang-format off + Q_OBJECT_BINDABLE_PROPERTY(NMWirelessDevice, bool, bScanning, &NMWirelessDevice::scanningChanged); + Q_OBJECT_BINDABLE_PROPERTY(NMWirelessDevice, QDateTime, bLastScan, &NMWirelessDevice::lastScanChanged); + Q_OBJECT_BINDABLE_PROPERTY(NMWirelessDevice, NMWirelessCapabilities::Enum, bCapabilities, &NMWirelessDevice::capabilitiesChanged); + Q_OBJECT_BINDABLE_PROPERTY(NMWirelessDevice, QDBusObjectPath, bActiveAccessPoint, &NMWirelessDevice::activeAccessPointChanged); + Q_OBJECT_BINDABLE_PROPERTY(NMWirelessDevice, NM80211Mode::Enum, bMode, &NMWirelessDevice::modeChanged); + + QS_DBUS_BINDABLE_PROPERTY_GROUP(NMWireless, wirelessProperties); + QS_DBUS_PROPERTY_BINDING(NMWirelessDevice, pLastScan, bLastScan, wirelessProperties, "LastScan"); + QS_DBUS_PROPERTY_BINDING(NMWirelessDevice, pCapabilities, bCapabilities, wirelessProperties, "WirelessCapabilities"); + QS_DBUS_PROPERTY_BINDING(NMWirelessDevice, pActiveAccessPoint, bActiveAccessPoint, wirelessProperties, "ActiveAccessPoint"); + QS_DBUS_PROPERTY_BINDING(NMWirelessDevice, pMode, bMode, wirelessProperties, "Mode"); + // clang-format on + + DBusNMWirelessProxy* wirelessProxy = nullptr; +}; + +} // namespace qs::network diff --git a/src/network/test/manual/network.qml b/src/network/test/manual/network.qml new file mode 100644 index 0000000..0fd0f72 --- /dev/null +++ b/src/network/test/manual/network.qml @@ -0,0 +1,155 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import Quickshell.Widgets +import Quickshell.Networking + +FloatingWindow { + color: contentItem.palette.window + + ColumnLayout { + anchors.fill: parent + anchors.margins: 5 + + Column { + Layout.fillWidth: true + RowLayout { + Label { + text: "WiFi" + font.bold: true + font.pointSize: 12 + } + CheckBox { + text: "Software" + checked: Networking.wifiEnabled + onClicked: Networking.wifiEnabled = !Networking.wifiEnabled + } + CheckBox { + enabled: false + text: "Hardware" + checked: Networking.wifiHardwareEnabled + } + } + } + + ListView { + clip: true + Layout.fillWidth: true + Layout.fillHeight: true + model: Networking.devices + + delegate: WrapperRectangle { + width: parent.width + color: "transparent" + border.color: palette.button + border.width: 1 + margin: 5 + + ColumnLayout { + RowLayout { + Label { text: modelData.name; font.bold: true } + Label { text: modelData.address } + Label { text: `(Type: ${DeviceType.toString(modelData.type)})` } + } + RowLayout { + Label { + text: DeviceConnectionState.toString(modelData.state) + color: modelData.connected ? palette.link : palette.placeholderText + } + Label { + visible: Networking.backend == NetworkBackendType.NetworkManager && (modelData.state == DeviceConnectionState.Connecting || modelData.state == DeviceConnectionState.Disconnecting) + text: `(${NMDeviceState.toString(modelData.nmState)})` + } + Button { + visible: modelData.state == DeviceConnectionState.Connected + text: "Disconnect" + onClicked: modelData.disconnect() + } + CheckBox { + text: "Autoconnect" + checked: modelData.autoconnect + onClicked: modelData.autoconnect = !modelData.autoconnect + } + Label { + text: `Mode: ${WifiDeviceMode.toString(modelData.mode)}` + visible: modelData.type == DeviceType.Wifi + } + CheckBox { + text: "Scanner" + checked: modelData.scannerEnabled + onClicked: modelData.scannerEnabled = !modelData.scannerEnabled + visible: modelData.type === DeviceType.Wifi + } + } + + Repeater { + Layout.fillWidth: true + model: { + if (modelData.type !== DeviceType.Wifi) return [] + return [...modelData.networks.values].sort((a, b) => { + if (a.connected !== b.connected) { + return b.connected - a.connected + } + return b.signalStrength - a.signalStrength + }) + } + + WrapperRectangle { + Layout.fillWidth: true + color: modelData.connected ? palette.highlight : palette.button + border.color: palette.mid + border.width: 1 + margin: 5 + + RowLayout { + ColumnLayout { + Layout.fillWidth: true + RowLayout { + Label { text: modelData.name; font.bold: true } + Label { + text: modelData.known ? "Known" : "" + color: palette.placeholderText + } + } + RowLayout { + Label { + text: `Security: ${WifiSecurityType.toString(modelData.security)}` + color: palette.placeholderText + } + Label { + text: `| Signal strength: ${Math.round(modelData.signalStrength*100)}%` + color: palette.placeholderText + } + } + Label { + visible: Networking.backend == NetworkBackendType.NetworkManager && (modelData.nmReason != NMConnectionStateReason.Unknown && modelData.nmReason != NMConnectionStateReason.None) + text: `Connection change reason: ${NMConnectionStateReason.toString(modelData.nmReason)}` + } + } + RowLayout { + Layout.alignment: Qt.AlignRight + Button { + text: "Connect" + onClicked: modelData.connect() + visible: !modelData.connected + } + Button { + text: "Disconnect" + onClicked: modelData.disconnect() + visible: modelData.connected + } + Button { + text: "Forget" + onClicked: modelData.forget() + visible: modelData.known + } + } + } + } + } + } + } + } + } +} diff --git a/src/network/wifi.cpp b/src/network/wifi.cpp new file mode 100644 index 0000000..57fb8ea --- /dev/null +++ b/src/network/wifi.cpp @@ -0,0 +1,138 @@ +#include "wifi.hpp" +#include + +#include +#include +#include +#include +#include + +#include "../core/logcat.hpp" +#include "device.hpp" +#include "network.hpp" + +namespace qs::network { + +namespace { +QS_LOGGING_CATEGORY(logWifi, "quickshell.network.wifi", QtWarningMsg); +} // namespace + +QString WifiSecurityType::toString(WifiSecurityType::Enum type) { + switch (type) { + case Unknown: return QStringLiteral("Unknown"); + case Wpa3SuiteB192: return QStringLiteral("WPA3 Suite B 192-bit"); + case Sae: return QStringLiteral("WPA3"); + case Wpa2Eap: return QStringLiteral("WPA2 Enterprise"); + case Wpa2Psk: return QStringLiteral("WPA2"); + case WpaEap: return QStringLiteral("WPA Enterprise"); + case WpaPsk: return QStringLiteral("WPA"); + case StaticWep: return QStringLiteral("WEP"); + case DynamicWep: return QStringLiteral("Dynamic WEP"); + case Leap: return QStringLiteral("LEAP"); + case Owe: return QStringLiteral("OWE"); + case Open: return QStringLiteral("Open"); + default: return QStringLiteral("Unknown"); + } +} + +QString WifiDeviceMode::toString(WifiDeviceMode::Enum mode) { + switch (mode) { + case Unknown: return QStringLiteral("Unknown"); + case AdHoc: return QStringLiteral("Ad-Hoc"); + case Station: return QStringLiteral("Station"); + case AccessPoint: return QStringLiteral("Access Point"); + case Mesh: return QStringLiteral("Mesh"); + default: return QStringLiteral("Unknown"); + }; +} + +QString NMConnectionStateReason::toString(NMConnectionStateReason::Enum reason) { + switch (reason) { + case Unknown: return QStringLiteral("Unknown"); + case None: return QStringLiteral("No reason"); + case UserDisconnected: return QStringLiteral("User disconnection"); + case DeviceDisconnected: + return QStringLiteral("The device the connection was using was disconnected."); + case ServiceStopped: + return QStringLiteral("The service providing the VPN connection was stopped."); + case IpConfigInvalid: + return QStringLiteral("The IP config of the active connection was invalid."); + case ConnectTimeout: + return QStringLiteral("The connection attempt to the VPN service timed out."); + case ServiceStartTimeout: + return QStringLiteral( + "A timeout occurred while starting the service providing the VPN connection." + ); + case ServiceStartFailed: + return QStringLiteral("Starting the service providing the VPN connection failed."); + case NoSecrets: return QStringLiteral("Necessary secrets for the connection were not provided."); + case LoginFailed: return QStringLiteral("Authentication to the server failed."); + case ConnectionRemoved: + return QStringLiteral("Necessary secrets for the connection were not provided."); + case DependencyFailed: + return QStringLiteral("Master connection of this connection failed to activate."); + case DeviceRealizeFailed: return QStringLiteral("Could not create the software device link."); + case DeviceRemoved: return QStringLiteral("The device this connection depended on disappeared."); + default: return QStringLiteral("Unknown"); + }; +}; + +WifiNetwork::WifiNetwork(QString ssid, QObject* parent): Network(std::move(ssid), parent) {}; + +void WifiNetwork::connect() { + if (this->bConnected) { + qCCritical(logWifi) << this << "is already connected."; + return; + } + + this->requestConnect(); +} + +void WifiNetwork::disconnect() { + if (!this->bConnected) { + qCCritical(logWifi) << this << "is not currently connected"; + return; + } + + this->requestDisconnect(); +} + +void WifiNetwork::forget() { this->requestForget(); } + +WifiDevice::WifiDevice(QObject* parent): NetworkDevice(DeviceType::Wifi, parent) {}; + +void WifiDevice::setScannerEnabled(bool enabled) { + if (this->bScannerEnabled == enabled) return; + this->bScannerEnabled = enabled; +} + +void WifiDevice::networkAdded(WifiNetwork* net) { this->mNetworks.insertObject(net); } +void WifiDevice::networkRemoved(WifiNetwork* net) { this->mNetworks.removeObject(net); } + +} // namespace qs::network + +QDebug operator<<(QDebug debug, const qs::network::WifiNetwork* network) { + auto saver = QDebugStateSaver(debug); + + if (network) { + debug.nospace() << "WifiNetwork(" << static_cast(network) + << ", name=" << network->name() << ")"; + } else { + debug << "WifiNetwork(nullptr)"; + } + + return debug; +} + +QDebug operator<<(QDebug debug, const qs::network::WifiDevice* device) { + auto saver = QDebugStateSaver(debug); + + if (device) { + debug.nospace() << "WifiDevice(" << static_cast(device) + << ", name=" << device->name() << ")"; + } else { + debug << "WifiDevice(nullptr)"; + } + + return debug; +} diff --git a/src/network/wifi.hpp b/src/network/wifi.hpp new file mode 100644 index 0000000..15b093d --- /dev/null +++ b/src/network/wifi.hpp @@ -0,0 +1,186 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include "../core/model.hpp" +#include "device.hpp" +#include "network.hpp" + +namespace qs::network { + +///! The security type of a wifi network. +class WifiSecurityType: public QObject { + Q_OBJECT; + QML_ELEMENT; + QML_SINGLETON; + +public: + enum Enum : quint8 { + Wpa3SuiteB192 = 0, + Sae = 1, + Wpa2Eap = 2, + Wpa2Psk = 3, + WpaEap = 4, + WpaPsk = 5, + StaticWep = 6, + DynamicWep = 7, + Leap = 8, + Owe = 9, + Open = 10, + Unknown = 11, + }; + Q_ENUM(Enum); + Q_INVOKABLE static QString toString(WifiSecurityType::Enum type); +}; + +///! The 802.11 mode of a wifi device. +class WifiDeviceMode: public QObject { + Q_OBJECT; + QML_ELEMENT; + QML_SINGLETON; + +public: + enum Enum : quint8 { + /// The device is part of an Ad-Hoc network without a central access point. + AdHoc = 0, + /// The device is a station that can connect to networks. + Station = 1, + /// The device is a local hotspot/access point. + AccessPoint = 2, + /// The device is an 802.11s mesh point. + Mesh = 3, + /// The device mode is unknown. + Unknown = 4, + }; + Q_ENUM(Enum); + Q_INVOKABLE static QString toString(WifiDeviceMode::Enum mode); +}; + +///! NetworkManager-specific reason for a WifiNetworks connection state. +/// In sync with https://networkmanager.dev/docs/api/latest/nm-dbus-types.html#NMActiveConnectionStateReason. +class NMConnectionStateReason: public QObject { + Q_OBJECT; + QML_ELEMENT; + QML_SINGLETON; + +public: + enum Enum : quint8 { + Unknown = 0, + None = 1, + UserDisconnected = 2, + DeviceDisconnected = 3, + ServiceStopped = 4, + IpConfigInvalid = 5, + ConnectTimeout = 6, + ServiceStartTimeout = 7, + ServiceStartFailed = 8, + NoSecrets = 9, + LoginFailed = 10, + ConnectionRemoved = 11, + DependencyFailed = 12, + DeviceRealizeFailed = 13, + DeviceRemoved = 14 + }; + Q_ENUM(Enum); + Q_INVOKABLE static QString toString(NMConnectionStateReason::Enum reason); +}; + +///! An available wifi network. +class WifiNetwork: public Network { + Q_OBJECT; + QML_ELEMENT; + QML_UNCREATABLE("WifiNetwork can only be acquired through WifiDevice"); + // clang-format off + /// The current signal strength of the network, from 0.0 to 1.0. + Q_PROPERTY(qreal signalStrength READ default NOTIFY signalStrengthChanged BINDABLE bindableSignalStrength); + /// True if the wifi network has known connection settings saved. + Q_PROPERTY(bool known READ default NOTIFY knownChanged BINDABLE bindableKnown); + /// The security type of the wifi network. + Q_PROPERTY(WifiSecurityType::Enum security READ default NOTIFY securityChanged BINDABLE bindableSecurity); + /// A specific reason for the connection state when the backend is NetworkManager. + Q_PROPERTY(NMConnectionStateReason::Enum nmReason READ default NOTIFY nmReasonChanged BINDABLE bindableNmReason); + // clang-format on + +public: + explicit WifiNetwork(QString ssid, QObject* parent = nullptr); + + /// Attempt to connect to the wifi network. + /// + /// > [!WARNING] Quickshell does not yet provide a NetworkManager authentication agent, + /// > meaning another agent will need to be active to enter passwords for unsaved networks. + Q_INVOKABLE void connect(); + /// Disconnect from the wifi network. + Q_INVOKABLE void disconnect(); + /// Forget all connection settings for this wifi network. + Q_INVOKABLE void forget(); + + QBindable bindableSignalStrength() { return &this->bSignalStrength; } + QBindable bindableKnown() { return &this->bKnown; } + QBindable bindableNmReason() { return &this->bNmReason; } + QBindable bindableSecurity() { return &this->bSecurity; } + +signals: + void requestConnect(); + void requestDisconnect(); + void requestForget(); + void signalStrengthChanged(); + void knownChanged(); + void securityChanged(); + void nmReasonChanged(); + +private: + // clang-format off + Q_OBJECT_BINDABLE_PROPERTY(WifiNetwork, qreal, bSignalStrength, &WifiNetwork::signalStrengthChanged); + Q_OBJECT_BINDABLE_PROPERTY(WifiNetwork, bool, bKnown, &WifiNetwork::knownChanged); + Q_OBJECT_BINDABLE_PROPERTY(WifiNetwork, NMConnectionStateReason::Enum, bNmReason, &WifiNetwork::nmReasonChanged); + Q_OBJECT_BINDABLE_PROPERTY(WifiNetwork, WifiSecurityType::Enum, bSecurity, &WifiNetwork::securityChanged); + // clang-format on +}; + +///! Wireless variant of a NetworkDevice. +class WifiDevice: public NetworkDevice { + Q_OBJECT; + QML_ELEMENT; + QML_UNCREATABLE(""); + + // clang-format off + /// A list of this available and connected wifi networks. + QSDOC_TYPE_OVERRIDE(ObjectModel*); + Q_PROPERTY(UntypedObjectModel* networks READ networks CONSTANT); + /// True when currently scanning for networks. + /// When enabled, the scanner populates the device with an active list of available wifi networks. + Q_PROPERTY(bool scannerEnabled READ scannerEnabled WRITE setScannerEnabled NOTIFY scannerEnabledChanged BINDABLE bindableScannerEnabled); + /// The 802.11 mode the device is in. + Q_PROPERTY(WifiDeviceMode::Enum mode READ default NOTIFY modeChanged BINDABLE bindableMode); + // clang-format on + +public: + explicit WifiDevice(QObject* parent = nullptr); + + void networkAdded(WifiNetwork* net); + void networkRemoved(WifiNetwork* net); + + [[nodiscard]] ObjectModel* networks() { return &this->mNetworks; }; + QBindable bindableScannerEnabled() { return &this->bScannerEnabled; }; + [[nodiscard]] bool scannerEnabled() const { return this->bScannerEnabled; }; + void setScannerEnabled(bool enabled); + QBindable bindableMode() { return &this->bMode; } + +signals: + void modeChanged(); + void scannerEnabledChanged(bool enabled); + +private: + ObjectModel mNetworks {this}; + Q_OBJECT_BINDABLE_PROPERTY(WifiDevice, bool, bScannerEnabled, &WifiDevice::scannerEnabledChanged); + Q_OBJECT_BINDABLE_PROPERTY(WifiDevice, WifiDeviceMode::Enum, bMode, &WifiDevice::modeChanged); +}; + +}; // namespace qs::network + +QDebug operator<<(QDebug debug, const qs::network::WifiNetwork* network); +QDebug operator<<(QDebug debug, const qs::network::WifiDevice* device); diff --git a/src/services/CMakeLists.txt b/src/services/CMakeLists.txt index 5ab5c55..f3912a9 100644 --- a/src/services/CMakeLists.txt +++ b/src/services/CMakeLists.txt @@ -14,6 +14,10 @@ if (SERVICE_PAM) add_subdirectory(pam) endif() +if (SERVICE_POLKIT) + add_subdirectory(polkit) +endif() + if (SERVICE_GREETD) add_subdirectory(greetd) endif() diff --git a/src/services/greetd/connection.cpp b/src/services/greetd/connection.cpp index bf0d1fd..7130870 100644 --- a/src/services/greetd/connection.cpp +++ b/src/services/greetd/connection.cpp @@ -199,7 +199,8 @@ void GreetdConnection::onSocketReady() { // Special case this error in case a session was already running. // This cancels and restarts the session. if (errorType == "error" && desc == "a session is already being configured") { - qCDebug(logGreetd + qCDebug( + logGreetd ) << "A session was already in progress, cancelling it and starting a new one."; this->setActive(false); this->setActive(true); @@ -225,6 +226,10 @@ void GreetdConnection::onSocketReady() { this->mResponseRequired = responseRequired; emit this->authMessage(message, error, responseRequired, echoResponse); + + if (!responseRequired) { + this->sendRequest({{"type", "post_auth_message_response"}}); + } } else goto unexpected; return; diff --git a/src/services/mpris/player.cpp b/src/services/mpris/player.cpp index 45d5cd4..fe8b349 100644 --- a/src/services/mpris/player.cpp +++ b/src/services/mpris/player.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -99,43 +100,12 @@ MprisPlayer::MprisPlayer(const QString& address, QObject* parent): QObject(paren } else return static_cast(-1); }); - this->bLengthSupported.setBinding([this]() { return this->bInternalLength != -1; }); - - this->bPlaybackState.setBinding([this]() { - const auto& status = this->bpPlaybackStatus.value(); - - if (status == "Playing") { - return MprisPlaybackState::Playing; - } else if (status == "Paused") { - this->pausedTime = QDateTime::currentDateTimeUtc(); - return MprisPlaybackState::Paused; - } else if (status == "Stopped") { - return MprisPlaybackState::Stopped; - } else { - qWarning() << "Received unexpected PlaybackStatus for" << this << status; - return MprisPlaybackState::Stopped; - } - }); + this->bLengthSupported.setBinding([this]() { return this->bInternalLength.value() != -1; }); this->bIsPlaying.setBinding([this]() { return this->bPlaybackState == MprisPlaybackState::Playing; }); - this->bLoopState.setBinding([this]() { - const auto& status = this->bpLoopStatus.value(); - - if (status == "None") { - return MprisLoopState::None; - } else if (status == "Track") { - return MprisLoopState::Track; - } else if (status == "Playlist") { - return MprisLoopState::Playlist; - } else { - qWarning() << "Received unexpected LoopStatus for" << this << status; - return MprisLoopState::None; - } - }); - // clang-format off QObject::connect(this->player, &DBusMprisPlayer::Seeked, this, &MprisPlayer::onSeek); QObject::connect(&this->playerProperties, &DBusPropertyGroup::getAllFinished, this, &MprisPlayer::onGetAllFinished); @@ -408,7 +378,7 @@ void MprisPlayer::onPlaybackStatusUpdated() { // For exceptionally bad players that update playback timestamps at an indeterminate time AFTER // updating playback state. (Youtube) - QTimer::singleShot(100, this, [&]() { this->pPosition.requestUpdate(); }); + QTimer::singleShot(100, this, [this]() { this->pPosition.requestUpdate(); }); // For exceptionally bad players that don't update length (or other metadata) until a new track actually // starts playing, and then don't trigger a metadata update when they do. (Jellyfin) @@ -432,18 +402,11 @@ void MprisPlayer::setLoopState(MprisLoopState::Enum loopState) { } if (loopState == this->bLoopState) return; - - QString loopStatusStr; - switch (loopState) { - case MprisLoopState::None: loopStatusStr = "None"; break; - case MprisLoopState::Track: loopStatusStr = "Track"; break; - case MprisLoopState::Playlist: loopStatusStr = "Playlist"; break; - default: + if (loopState < MprisLoopState::None || loopState > MprisLoopState::Playlist) { qWarning() << "Cannot set loopState of" << this << "to unknown value" << loopState; - return; } - this->bpLoopStatus = loopStatusStr; + this->bLoopState = loopState; this->pLoopStatus.write(); } @@ -496,3 +459,43 @@ void MprisPlayer::onGetAllFinished() { } } // namespace qs::service::mpris + +namespace qs::dbus { + +using namespace qs::service::mpris; + +DBusResult +DBusDataTransform::fromWire(const QString& wire) { + if (wire == "Playing") return MprisPlaybackState::Playing; + if (wire == "Paused") return MprisPlaybackState::Paused; + if (wire == "Stopped") return MprisPlaybackState::Stopped; + return QDBusError(QDBusError::InvalidArgs, QString("Invalid MprisPlaybackState: %1").arg(wire)); +} + +QString DBusDataTransform::toWire(MprisPlaybackState::Enum data) { + switch (data) { + case MprisPlaybackState::Playing: return "Playing"; + case MprisPlaybackState::Paused: return "Paused"; + case MprisPlaybackState::Stopped: return "Stopped"; + default: qFatal() << "Tried to convert an invalid MprisPlaybackState to String"; return QString(); + } +} + +DBusResult +DBusDataTransform::fromWire(const QString& wire) { + if (wire == "None") return MprisLoopState::None; + if (wire == "Track") return MprisLoopState::Track; + if (wire == "Playlist") return MprisLoopState::Playlist; + return QDBusError(QDBusError::InvalidArgs, QString("Invalid MprisLoopState: %1").arg(wire)); +} + +QString DBusDataTransform::toWire(MprisLoopState::Enum data) { + switch (data) { + case MprisLoopState::None: return "None"; + case MprisLoopState::Track: return "Track"; + case MprisLoopState::Playlist: return "Playlist"; + default: qFatal() << "Tried to convert an invalid MprisLoopState to String"; return QString(); + } +} + +} // namespace qs::dbus diff --git a/src/services/mpris/player.hpp b/src/services/mpris/player.hpp index 89bc27a..423453d 100644 --- a/src/services/mpris/player.hpp +++ b/src/services/mpris/player.hpp @@ -51,6 +51,30 @@ public: Q_INVOKABLE static QString toString(qs::service::mpris::MprisLoopState::Enum status); }; +}; // namespace qs::service::mpris + +namespace qs::dbus { + +template <> +struct DBusDataTransform { + using Wire = QString; + using Data = qs::service::mpris::MprisPlaybackState::Enum; + static DBusResult fromWire(const QString& wire); + static QString toWire(Data data); +}; + +template <> +struct DBusDataTransform { + using Wire = QString; + using Data = qs::service::mpris::MprisLoopState::Enum; + static DBusResult fromWire(const QString& wire); + static QString toWire(Data data); +}; + +}; // namespace qs::dbus + +namespace qs::service::mpris { + ///! A media player exposed over MPRIS. /// A media player exposed over MPRIS. /// @@ -250,23 +274,23 @@ public: [[nodiscard]] bool isValid() const; [[nodiscard]] QString address() const; - [[nodiscard]] QBindable bindableCanControl() const { return &this->bCanControl; }; - [[nodiscard]] QBindable bindableCanSeek() const { return &this->bCanSeek; }; - [[nodiscard]] QBindable bindableCanGoNext() const { return &this->bCanGoNext; }; - [[nodiscard]] QBindable bindableCanGoPrevious() const { return &this->bCanGoPrevious; }; - [[nodiscard]] QBindable bindableCanPlay() const { return &this->bCanPlay; }; - [[nodiscard]] QBindable bindableCanPause() const { return &this->bCanPause; }; + [[nodiscard]] QBindable bindableCanControl() const { return &this->bCanControl; } + [[nodiscard]] QBindable bindableCanSeek() const { return &this->bCanSeek; } + [[nodiscard]] QBindable bindableCanGoNext() const { return &this->bCanGoNext; } + [[nodiscard]] QBindable bindableCanGoPrevious() const { return &this->bCanGoPrevious; } + [[nodiscard]] QBindable bindableCanPlay() const { return &this->bCanPlay; } + [[nodiscard]] QBindable bindableCanPause() const { return &this->bCanPause; } [[nodiscard]] QBindable bindableCanTogglePlaying() const { return &this->bCanTogglePlaying; - }; - [[nodiscard]] QBindable bindableCanQuit() const { return &this->bCanQuit; }; - [[nodiscard]] QBindable bindableCanRaise() const { return &this->bCanRaise; }; + } + [[nodiscard]] QBindable bindableCanQuit() const { return &this->bCanQuit; } + [[nodiscard]] QBindable bindableCanRaise() const { return &this->bCanRaise; } [[nodiscard]] QBindable bindableCanSetFullscreen() const { return &this->bCanSetFullscreen; - }; + } - [[nodiscard]] QBindable bindableIdentity() const { return &this->bIdentity; }; - [[nodiscard]] QBindable bindableDesktopEntry() const { return &this->bDesktopEntry; }; + [[nodiscard]] QBindable bindableIdentity() const { return &this->bIdentity; } + [[nodiscard]] QBindable bindableDesktopEntry() const { return &this->bDesktopEntry; } [[nodiscard]] qlonglong positionMs() const; [[nodiscard]] qreal position() const; @@ -276,49 +300,49 @@ public: [[nodiscard]] qreal length() const; [[nodiscard]] QBindable bindableLengthSupported() const { return &this->bLengthSupported; } - [[nodiscard]] qreal volume() const { return this->bVolume; }; + [[nodiscard]] qreal volume() const { return this->bVolume; } [[nodiscard]] bool volumeSupported() const; void setVolume(qreal volume); - [[nodiscard]] QBindable bindableUniqueId() const { return &this->bUniqueId; }; - [[nodiscard]] QBindable bindableMetadata() const { return &this->bMetadata; }; - [[nodiscard]] QBindable bindableTrackTitle() const { return &this->bTrackTitle; }; - [[nodiscard]] QBindable bindableTrackAlbum() const { return &this->bTrackAlbum; }; + [[nodiscard]] QBindable bindableUniqueId() const { return &this->bUniqueId; } + [[nodiscard]] QBindable bindableMetadata() const { return &this->bMetadata; } + [[nodiscard]] QBindable bindableTrackTitle() const { return &this->bTrackTitle; } + [[nodiscard]] QBindable bindableTrackAlbum() const { return &this->bTrackAlbum; } [[nodiscard]] QBindable bindableTrackAlbumArtist() const { return &this->bTrackAlbumArtist; - }; - [[nodiscard]] QBindable bindableTrackArtist() const { return &this->bTrackArtist; }; - [[nodiscard]] QBindable bindableTrackArtUrl() const { return &this->bTrackArtUrl; }; + } + [[nodiscard]] QBindable bindableTrackArtist() const { return &this->bTrackArtist; } + [[nodiscard]] QBindable bindableTrackArtUrl() const { return &this->bTrackArtUrl; } - [[nodiscard]] MprisPlaybackState::Enum playbackState() const { return this->bPlaybackState; }; + [[nodiscard]] MprisPlaybackState::Enum playbackState() const { return this->bPlaybackState; } void setPlaybackState(MprisPlaybackState::Enum playbackState); - [[nodiscard]] bool isPlaying() const { return this->bIsPlaying; }; + [[nodiscard]] bool isPlaying() const { return this->bIsPlaying; } void setPlaying(bool playing); - [[nodiscard]] MprisLoopState::Enum loopState() const { return this->bLoopState; }; + [[nodiscard]] MprisLoopState::Enum loopState() const { return this->bLoopState; } [[nodiscard]] bool loopSupported() const; void setLoopState(MprisLoopState::Enum loopState); - [[nodiscard]] qreal rate() const { return this->bRate; }; - [[nodiscard]] QBindable bindableMinRate() const { return &this->bRate; }; - [[nodiscard]] QBindable bindableMaxRate() const { return &this->bRate; }; + [[nodiscard]] qreal rate() const { return this->bRate; } + [[nodiscard]] QBindable bindableMinRate() const { return &this->bRate; } + [[nodiscard]] QBindable bindableMaxRate() const { return &this->bRate; } void setRate(qreal rate); - [[nodiscard]] bool shuffle() const { return this->bShuffle; }; + [[nodiscard]] bool shuffle() const { return this->bShuffle; } [[nodiscard]] bool shuffleSupported() const; void setShuffle(bool shuffle); - [[nodiscard]] bool fullscreen() const { return this->bFullscreen; }; + [[nodiscard]] bool fullscreen() const { return this->bFullscreen; } void setFullscreen(bool fullscreen); [[nodiscard]] QBindable> bindableSupportedUriSchemes() const { return &this->bSupportedUriSchemes; - }; + } [[nodiscard]] QBindable> bindableSupportedMimeTypes() const { return &this->bSupportedMimeTypes; - }; + } signals: /// The track has changed. @@ -390,7 +414,7 @@ private: void onPlaybackStatusUpdated(); // call instead of setting bpPosition void setPosition(qlonglong position); - void requestPositionUpdate() { this->pPosition.requestUpdate(); }; + void requestPositionUpdate() { this->pPosition.requestUpdate(); } // clang-format off Q_OBJECT_BINDABLE_PROPERTY(MprisPlayer, QString, bIdentity, &MprisPlayer::identityChanged); @@ -404,13 +428,13 @@ private: QS_DBUS_BINDABLE_PROPERTY_GROUP(MprisPlayer, appProperties); QS_DBUS_PROPERTY_BINDING(MprisPlayer, pIdentity, bIdentity, appProperties, "Identity"); - QS_DBUS_PROPERTY_BINDING(MprisPlayer, pDesktopEntry, bDesktopEntry, appProperties, "DesktopEntry"); - QS_DBUS_PROPERTY_BINDING(MprisPlayer, pCanQuit, bCanQuit, appProperties, "CanQuit"); - QS_DBUS_PROPERTY_BINDING(MprisPlayer, pCanRaise, bCanRaise, appProperties, "CanRaise"); + QS_DBUS_PROPERTY_BINDING(MprisPlayer, pDesktopEntry, bDesktopEntry, appProperties, "DesktopEntry", false); + QS_DBUS_PROPERTY_BINDING(MprisPlayer, pCanQuit, bCanQuit, appProperties, "CanQuit", false); + QS_DBUS_PROPERTY_BINDING(MprisPlayer, pCanRaise, bCanRaise, appProperties, "CanRaise", false); QS_DBUS_PROPERTY_BINDING(MprisPlayer, pFullscreen, bFullscreen, appProperties, "Fullscreen", false); QS_DBUS_PROPERTY_BINDING(MprisPlayer, pCanSetFullscreen, bCanSetFullscreen, appProperties, "CanSetFullscreen", false); - QS_DBUS_PROPERTY_BINDING(MprisPlayer, pSupportedUriSchemes, bSupportedUriSchemes, appProperties, "SupportedUriSchemes"); - QS_DBUS_PROPERTY_BINDING(MprisPlayer, pSupportedMimeTypes, bSupportedMimeTypes, appProperties, "SupportedMimeTypes"); + QS_DBUS_PROPERTY_BINDING(MprisPlayer, pSupportedUriSchemes, bSupportedUriSchemes, appProperties, "SupportedUriSchemes", false); + QS_DBUS_PROPERTY_BINDING(MprisPlayer, pSupportedMimeTypes, bSupportedMimeTypes, appProperties, "SupportedMimeTypes", false); Q_OBJECT_BINDABLE_PROPERTY(MprisPlayer, bool, bpCanPlay); Q_OBJECT_BINDABLE_PROPERTY(MprisPlayer, bool, bpCanPause); @@ -420,8 +444,6 @@ private: Q_OBJECT_BINDABLE_PROPERTY(MprisPlayer, QVariantMap, bpMetadata); QS_BINDING_SUBSCRIBE_METHOD(MprisPlayer, bpMetadata, onMetadataChanged, onValueChanged); Q_OBJECT_BINDABLE_PROPERTY_WITH_ARGS(MprisPlayer, qlonglong, bpPosition, -1, &MprisPlayer::positionChanged); - Q_OBJECT_BINDABLE_PROPERTY(MprisPlayer, QString, bpPlaybackStatus); - Q_OBJECT_BINDABLE_PROPERTY(MprisPlayer, QString, bpLoopStatus); Q_OBJECT_BINDABLE_PROPERTY(MprisPlayer, bool, bCanControl, &MprisPlayer::canControlChanged); Q_OBJECT_BINDABLE_PROPERTY(MprisPlayer, bool, bCanPlay, &MprisPlayer::canPlayChanged); @@ -460,8 +482,8 @@ private: QS_DBUS_PROPERTY_BINDING(MprisPlayer, qlonglong, pPosition, bpPosition, onPositionUpdated, playerProperties, "Position", false); QS_DBUS_PROPERTY_BINDING(MprisPlayer, pVolume, bVolume, playerProperties, "Volume", false); QS_DBUS_PROPERTY_BINDING(MprisPlayer, pMetadata, bpMetadata, playerProperties, "Metadata"); - QS_DBUS_PROPERTY_BINDING(MprisPlayer, void, pPlaybackStatus, bpPlaybackStatus, onPlaybackStatusUpdated, playerProperties, "PlaybackStatus", true); - QS_DBUS_PROPERTY_BINDING(MprisPlayer, pLoopStatus, bpLoopStatus, playerProperties, "LoopStatus", false); + QS_DBUS_PROPERTY_BINDING(MprisPlayer, void, pPlaybackStatus, bPlaybackState, onPlaybackStatusUpdated, playerProperties, "PlaybackStatus", true); + QS_DBUS_PROPERTY_BINDING(MprisPlayer, pLoopStatus, bLoopState, playerProperties, "LoopStatus", false); QS_DBUS_PROPERTY_BINDING(MprisPlayer, pRate, bRate, playerProperties, "Rate", false); QS_DBUS_PROPERTY_BINDING(MprisPlayer, pMinRate, bMinRate, playerProperties, "MinimumRate", false); QS_DBUS_PROPERTY_BINDING(MprisPlayer, pMaxRate, bMaxRate, playerProperties, "MaximumRate", false); diff --git a/src/services/mpris/watcher.hpp b/src/services/mpris/watcher.hpp index bd922cd..cbe9669 100644 --- a/src/services/mpris/watcher.hpp +++ b/src/services/mpris/watcher.hpp @@ -51,7 +51,7 @@ class MprisQml: public QObject { Q_PROPERTY(UntypedObjectModel* players READ players CONSTANT); public: - explicit MprisQml(QObject* parent = nullptr): QObject(parent) {}; + explicit MprisQml(QObject* parent = nullptr): QObject(parent) {} [[nodiscard]] ObjectModel* players(); }; diff --git a/src/services/notifications/CMakeLists.txt b/src/services/notifications/CMakeLists.txt index 0cbb42e..58b6648 100644 --- a/src/services/notifications/CMakeLists.txt +++ b/src/services/notifications/CMakeLists.txt @@ -23,7 +23,6 @@ qt_add_qml_module(quickshell-service-notifications ) qs_add_module_deps_light(quickshell-service-notifications Quickshell) - install_qml_module(quickshell-service-notifications) target_link_libraries(quickshell-service-notifications PRIVATE Qt::Quick Qt::DBus) diff --git a/src/services/notifications/dbusimage.cpp b/src/services/notifications/dbusimage.cpp index e6c091b..469d08c 100644 --- a/src/services/notifications/dbusimage.cpp +++ b/src/services/notifications/dbusimage.cpp @@ -42,10 +42,9 @@ const QDBusArgument& operator>>(const QDBusArgument& argument, DBusNotificationI } else if (channels != (pixmap.hasAlpha ? 4 : 3)) { qCWarning(logNotifications) << "Unable to parse pixmap as channel count is incorrect." << "Got " << channels << "expected" << (pixmap.hasAlpha ? 4 : 3); - } else if (rowstride != pixmap.width * sampleBits * channels) { + } else if (rowstride != pixmap.width * channels) { qCWarning(logNotifications) << "Unable to parse pixmap as rowstride is incorrect. Got" - << rowstride << "expected" - << (pixmap.width * sampleBits * channels); + << rowstride << "expected" << (pixmap.width * channels); } return argument; @@ -55,7 +54,7 @@ const QDBusArgument& operator<<(QDBusArgument& argument, const DBusNotificationI argument.beginStructure(); argument << pixmap.width; argument << pixmap.height; - argument << pixmap.width * (pixmap.hasAlpha ? 4 : 3) * 8; + argument << pixmap.width * (pixmap.hasAlpha ? 4 : 3); argument << pixmap.hasAlpha; argument << 8; argument << (pixmap.hasAlpha ? 4 : 3); diff --git a/src/services/notifications/notification.cpp b/src/services/notifications/notification.cpp index 96a2ff0..d048bde 100644 --- a/src/services/notifications/notification.cpp +++ b/src/services/notifications/notification.cpp @@ -78,6 +78,29 @@ void Notification::close(NotificationCloseReason::Enum reason) { } } +void Notification::sendInlineReply(const QString& replyText) { + if (!NotificationServer::instance()->support.inlineReply) { + qCritical() << "Inline reply support disabled on server"; + return; + } + + if (!this->bHasInlineReply) { + qCritical() << "Cannot send reply to notification without inline-reply action"; + return; + } + + if (this->isRetained()) { + qCritical() << "Cannot send reply to destroyed notification" << this; + return; + } + + NotificationServer::instance()->NotificationReplied(this->id(), replyText); + + if (!this->bindableResident().value()) { + this->close(NotificationCloseReason::Dismissed); + } +} + void Notification::updateProperties( const QString& appName, QString appIcon, @@ -104,7 +127,7 @@ void Notification::updateProperties( if (appIcon.isEmpty() && !this->bDesktopEntry.value().isEmpty()) { if (auto* entry = DesktopEntryManager::instance()->byId(this->bDesktopEntry.value())) { - appIcon = entry->mIcon; + appIcon = entry->bIcon.value(); } } @@ -147,17 +170,27 @@ void Notification::updateProperties( this->bImage = imagePath; this->bHints = hints; - Qt::endPropertyUpdateGroup(); - bool actionsChanged = false; auto deletedActions = QVector(); if (actions.length() % 2 == 0) { int ai = 0; for (auto i = 0; i != actions.length(); i += 2) { - ai = i / 2; const auto& identifier = actions.at(i); const auto& text = actions.at(i + 1); + + if (identifier == "inline-reply" && NotificationServer::instance()->support.inlineReply) { + if (this->bHasInlineReply) { + qCWarning(logNotifications) << this << '(' << appName << ')' + << "sent an action set with duplicate inline-reply actions."; + } else { + this->bHasInlineReply = true; + this->bInlineReplyPlaceholder = text; + } + // skip inserting this action into action list + continue; + } + auto* action = ai < this->mActions.length() ? this->mActions.at(ai) : nullptr; if (action && identifier == action->identifier()) { @@ -188,6 +221,8 @@ void Notification::updateProperties( << "sent an action set of an invalid length."; } + Qt::endPropertyUpdateGroup(); + if (actionsChanged) emit this->actionsChanged(); for (auto* action: deletedActions) { diff --git a/src/services/notifications/notification.hpp b/src/services/notifications/notification.hpp index f0c65bb..7f5246c 100644 --- a/src/services/notifications/notification.hpp +++ b/src/services/notifications/notification.hpp @@ -107,6 +107,12 @@ class Notification /// /// This image is often something like a profile picture in instant messaging applications. Q_PROPERTY(QString image READ default NOTIFY imageChanged BINDABLE bindableImage); + /// If true, the notification has an inline reply action. + /// + /// A quick reply text field should be displayed and the reply can be sent using @@sendInlineReply(). + Q_PROPERTY(bool hasInlineReply READ default NOTIFY hasInlineReplyChanged BINDABLE bindableHasInlineReply); + /// The placeholder text/button caption for the inline reply. + Q_PROPERTY(QString inlineReplyPlaceholder READ default NOTIFY inlineReplyPlaceholderChanged BINDABLE bindableInlineReplyPlaceholder); /// All hints sent by the client application as a javascript object. /// Many common hints are exposed via other properties. Q_PROPERTY(QVariantMap hints READ default NOTIFY hintsChanged BINDABLE bindableHints); @@ -124,6 +130,12 @@ public: /// explicitly closed by the user. Q_INVOKABLE void dismiss(); + /// Send an inline reply to the notification with an inline reply action. + /// > [!WARNING] This method can only be called if + /// > @@hasInlineReply is true + /// > and the server has @@NotificationServer.inlineReplySupported set to true. + Q_INVOKABLE void sendInlineReply(const QString& replyText); + void updateProperties( const QString& appName, QString appIcon, @@ -142,23 +154,29 @@ public: [[nodiscard]] bool isLastGeneration() const; void setLastGeneration(); - [[nodiscard]] QBindable bindableExpireTimeout() const { return &this->bExpireTimeout; }; - [[nodiscard]] QBindable bindableAppName() const { return &this->bAppName; }; - [[nodiscard]] QBindable bindableAppIcon() const { return &this->bAppIcon; }; - [[nodiscard]] QBindable bindableSummary() const { return &this->bSummary; }; - [[nodiscard]] QBindable bindableBody() const { return &this->bBody; }; + [[nodiscard]] QBindable bindableExpireTimeout() const { return &this->bExpireTimeout; } + [[nodiscard]] QBindable bindableAppName() const { return &this->bAppName; } + [[nodiscard]] QBindable bindableAppIcon() const { return &this->bAppIcon; } + [[nodiscard]] QBindable bindableSummary() const { return &this->bSummary; } + [[nodiscard]] QBindable bindableBody() const { return &this->bBody; } [[nodiscard]] QBindable bindableUrgency() const { return &this->bUrgency; - }; + } [[nodiscard]] QList actions() const; - [[nodiscard]] QBindable bindableHasActionIcons() const { return &this->bHasActionIcons; }; - [[nodiscard]] QBindable bindableResident() const { return &this->bResident; }; - [[nodiscard]] QBindable bindableTransient() const { return &this->bTransient; }; - [[nodiscard]] QBindable bindableDesktopEntry() const { return &this->bDesktopEntry; }; - [[nodiscard]] QBindable bindableImage() const { return &this->bImage; }; - [[nodiscard]] QBindable bindableHints() const { return &this->bHints; }; + [[nodiscard]] QBindable bindableHasActionIcons() const { return &this->bHasActionIcons; } + [[nodiscard]] QBindable bindableResident() const { return &this->bResident; } + [[nodiscard]] QBindable bindableTransient() const { return &this->bTransient; } + [[nodiscard]] QBindable bindableDesktopEntry() const { return &this->bDesktopEntry; } + [[nodiscard]] QBindable bindableImage() const { return &this->bImage; } + [[nodiscard]] QBindable bindableHasInlineReply() const { return &this->bHasInlineReply; } + + [[nodiscard]] QBindable bindableInlineReplyPlaceholder() const { + return &this->bInlineReplyPlaceholder; + } + + [[nodiscard]] QBindable bindableHints() const { return &this->bHints; } [[nodiscard]] NotificationCloseReason::Enum closeReason() const; void setTracked(bool tracked); @@ -182,6 +200,8 @@ signals: void transientChanged(); void desktopEntryChanged(); void imageChanged(); + void hasInlineReplyChanged(); + void inlineReplyPlaceholderChanged(); void hintsChanged(); private: @@ -202,6 +222,8 @@ private: Q_OBJECT_BINDABLE_PROPERTY(Notification, bool, bTransient, &Notification::transientChanged); Q_OBJECT_BINDABLE_PROPERTY(Notification, QString, bDesktopEntry, &Notification::desktopEntryChanged); Q_OBJECT_BINDABLE_PROPERTY(Notification, QString, bImage, &Notification::imageChanged); + Q_OBJECT_BINDABLE_PROPERTY(Notification, bool, bHasInlineReply, &Notification::hasInlineReplyChanged); + Q_OBJECT_BINDABLE_PROPERTY(Notification, QString, bInlineReplyPlaceholder, &Notification::inlineReplyPlaceholderChanged); Q_OBJECT_BINDABLE_PROPERTY(Notification, QVariantMap, bHints, &Notification::hintsChanged); // clang-format on diff --git a/src/services/notifications/org.freedesktop.Notifications.xml b/src/services/notifications/org.freedesktop.Notifications.xml index 1a2001f..3d99db0 100644 --- a/src/services/notifications/org.freedesktop.Notifications.xml +++ b/src/services/notifications/org.freedesktop.Notifications.xml @@ -38,6 +38,11 @@ + + + + + diff --git a/src/services/notifications/qml.cpp b/src/services/notifications/qml.cpp index 9981821..42bb23a 100644 --- a/src/services/notifications/qml.cpp +++ b/src/services/notifications/qml.cpp @@ -115,6 +115,15 @@ void NotificationServerQml::setImageSupported(bool imageSupported) { emit this->imageSupportedChanged(); } +bool NotificationServerQml::inlineReplySupported() const { return this->support.inlineReply; } + +void NotificationServerQml::setInlineReplySupported(bool inlineReplySupported) { + if (inlineReplySupported == this->support.inlineReply) return; + this->support.inlineReply = inlineReplySupported; + this->updateSupported(); + emit this->inlineReplySupportedChanged(); +} + QVector NotificationServerQml::extraHints() const { return this->support.extraHints; } void NotificationServerQml::setExtraHints(QVector extraHints) { diff --git a/src/services/notifications/qml.hpp b/src/services/notifications/qml.hpp index feb33db..88132c7 100644 --- a/src/services/notifications/qml.hpp +++ b/src/services/notifications/qml.hpp @@ -65,6 +65,8 @@ class NotificationServerQml: public PostReloadHook { Q_PROPERTY(bool actionIconsSupported READ actionIconsSupported WRITE setActionIconsSupported NOTIFY actionIconsSupportedChanged); /// If the notification server should advertise that it supports images. Defaults to false. Q_PROPERTY(bool imageSupported READ imageSupported WRITE setImageSupported NOTIFY imageSupportedChanged); + /// If the notification server should advertise that it supports inline replies. Defaults to false. + Q_PROPERTY(bool inlineReplySupported READ inlineReplySupported WRITE setInlineReplySupported NOTIFY inlineReplySupportedChanged); /// All notifications currently tracked by the server. QSDOC_TYPE_OVERRIDE(ObjectModel*); Q_PROPERTY(UntypedObjectModel* trackedNotifications READ trackedNotifications NOTIFY trackedNotificationsChanged); @@ -103,6 +105,9 @@ public: [[nodiscard]] bool imageSupported() const; void setImageSupported(bool imageSupported); + [[nodiscard]] bool inlineReplySupported() const; + void setInlineReplySupported(bool inlineReplySupported); + [[nodiscard]] QVector extraHints() const; void setExtraHints(QVector extraHints); @@ -123,6 +128,7 @@ signals: void actionsSupportedChanged(); void actionIconsSupportedChanged(); void imageSupportedChanged(); + void inlineReplySupportedChanged(); void extraHintsChanged(); void trackedNotificationsChanged(); diff --git a/src/services/notifications/server.cpp b/src/services/notifications/server.cpp index 18a898a..d2b55d0 100644 --- a/src/services/notifications/server.cpp +++ b/src/services/notifications/server.cpp @@ -21,7 +21,7 @@ namespace qs::service::notifications { // NOLINTNEXTLINE(misc-use-internal-linkage) -QS_LOGGING_CATEGORY(logNotifications, "quickshell.service.notifications"); +QS_LOGGING_CATEGORY(logNotifications, "quickshell.service.notifications", QtWarningMsg); NotificationServer::NotificationServer() { qDBusRegisterMetaType(); @@ -117,10 +117,12 @@ void NotificationServer::tryRegister() { if (success) { qCInfo(logNotifications) << "Registered notification server with dbus."; } else { - qCWarning(logNotifications + qCWarning( + logNotifications ) << "Could not register notification server at org.freedesktop.Notifications, presumably " "because one is already registered."; - qCWarning(logNotifications + qCWarning( + logNotifications ) << "Registration will be attempted again if the active service is unregistered."; } } @@ -155,6 +157,7 @@ QStringList NotificationServer::GetCapabilities() const { } if (this->support.image) capabilities += "icon-static"; + if (this->support.inlineReply) capabilities += "inline-reply"; capabilities += this->support.extraHints; diff --git a/src/services/notifications/server.hpp b/src/services/notifications/server.hpp index 8c20943..8bd92a3 100644 --- a/src/services/notifications/server.hpp +++ b/src/services/notifications/server.hpp @@ -23,6 +23,7 @@ struct NotificationServerSupport { bool actions = false; bool actionIcons = false; bool image = false; + bool inlineReply = false; QVector extraHints; }; @@ -60,6 +61,7 @@ signals: // NOLINTBEGIN void NotificationClosed(quint32 id, quint32 reason); void ActionInvoked(quint32 id, QString action); + void NotificationReplied(quint32 id, QString replyText); // NOLINTEND private slots: diff --git a/src/services/pam/conversation.cpp b/src/services/pam/conversation.cpp index 6d27978..f8f5a09 100644 --- a/src/services/pam/conversation.cpp +++ b/src/services/pam/conversation.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include "../../core/logcat.hpp" diff --git a/src/services/pam/qml.hpp b/src/services/pam/qml.hpp index 805e04c..a36184e 100644 --- a/src/services/pam/qml.hpp +++ b/src/services/pam/qml.hpp @@ -6,7 +6,11 @@ #include #include #include +#ifdef __FreeBSD__ +#include +#else #include +#endif #include #include "conversation.hpp" @@ -17,6 +21,9 @@ class PamContext : public QObject , public QQmlParserStatus { Q_OBJECT; + QML_ELEMENT; + Q_INTERFACES(QQmlParserStatus); + // clang-format off /// If the pam context is actively performing an authentication. /// @@ -32,6 +39,8 @@ class PamContext /// /// The configuration directory is resolved relative to the current file if not an absolute path. /// + /// On FreeBSD this property is ignored as the pam configuration directory cannot be changed. + /// /// This property may not be set while @@active is true. Q_PROPERTY(QString configDirectory READ configDirectory WRITE setConfigDirectory NOTIFY configDirectoryChanged); /// The user to authenticate as. If unset the current user will be used. @@ -49,7 +58,6 @@ class PamContext /// If the user's response should be visible. Only valid when @@responseRequired is true. Q_PROPERTY(bool responseVisible READ isResponseVisible NOTIFY responseVisibleChanged); // clang-format on - QML_ELEMENT; public: explicit PamContext(QObject* parent = nullptr): QObject(parent) {} diff --git a/src/services/pam/subprocess.cpp b/src/services/pam/subprocess.cpp index f99b279..dc36228 100644 --- a/src/services/pam/subprocess.cpp +++ b/src/services/pam/subprocess.cpp @@ -7,7 +7,11 @@ #include #include #include +#ifdef __FreeBSD__ +#include +#else #include +#endif #include #include @@ -83,7 +87,11 @@ PamIpcExitCode PamSubprocess::exec(const char* configDir, const char* config, co logIf(this->log) << "Starting pam session for user \"" << user << "\" with config \"" << config << "\" in dir \"" << configDir << "\"" << std::endl; +#ifdef __FreeBSD__ + auto result = pam_start(config, user, &conv, &handle); +#else auto result = pam_start_confdir(config, user, &conv, configDir, &handle); +#endif if (result != PAM_SUCCESS) { logIf(true) << "Unable to start pam conversation with error \"" << pam_strerror(handle, result) diff --git a/src/services/pipewire/CMakeLists.txt b/src/services/pipewire/CMakeLists.txt index fddca6f..fe894c9 100644 --- a/src/services/pipewire/CMakeLists.txt +++ b/src/services/pipewire/CMakeLists.txt @@ -3,6 +3,7 @@ pkg_check_modules(pipewire REQUIRED IMPORTED_TARGET libpipewire-0.3) qt_add_library(quickshell-service-pipewire STATIC qml.cpp + peak.cpp core.cpp connection.cpp registry.cpp diff --git a/src/services/pipewire/connection.cpp b/src/services/pipewire/connection.cpp index ac4c5e6..c2f505f 100644 --- a/src/services/pipewire/connection.cpp +++ b/src/services/pipewire/connection.cpp @@ -1,15 +1,137 @@ #include "connection.hpp" +#include +#include +#include +#include +#include #include +#include +#include + +#include "../../core/logcat.hpp" +#include "core.hpp" namespace qs::service::pipewire { +namespace { +QS_LOGGING_CATEGORY(logConnection, "quickshell.service.pipewire.connection", QtWarningMsg); +} + PwConnection::PwConnection(QObject* parent): QObject(parent) { - if (this->core.isValid()) { - this->registry.init(this->core); + this->runtimeDir = PwConnection::resolveRuntimeDir(); + + QObject::connect(&this->core, &PwCore::fatalError, this, &PwConnection::queueFatalError); + + if (!this->tryConnect(false) + && qEnvironmentVariableIntValue("QS_PIPEWIRE_IMMEDIATE_RECONNECT") == 1) + { + this->beginReconnect(); } } +QString PwConnection::resolveRuntimeDir() { + auto runtimeDir = qEnvironmentVariable("PIPEWIRE_RUNTIME_DIR"); + if (runtimeDir.isEmpty()) { + runtimeDir = qEnvironmentVariable("XDG_RUNTIME_DIR"); + } + + if (runtimeDir.isEmpty()) { + runtimeDir = QString("/run/user/%1").arg(getuid()); + } + + return runtimeDir; +} + +void PwConnection::beginReconnect() { + if (this->core.isValid()) { + this->stopSocketWatcher(); + return; + } + + if (!qEnvironmentVariableIsEmpty("PIPEWIRE_REMOTE")) return; + + if (this->runtimeDir.isEmpty()) { + qCWarning( + logConnection + ) << "Cannot watch runtime dir for pipewire reconnects: runtime dir is empty."; + return; + } + + this->startSocketWatcher(); + this->tryConnect(true); +} + +bool PwConnection::tryConnect(bool retry) { + if (this->core.isValid()) return true; + + qCDebug(logConnection) << "Attempting reconnect..."; + if (!this->core.start(retry)) { + return false; + } + + qCInfo(logConnection) << "Connection established"; + this->stopSocketWatcher(); + + this->registry.init(this->core); + return true; +} + +void PwConnection::startSocketWatcher() { + if (this->socketWatcher != nullptr) return; + if (!qEnvironmentVariableIsEmpty("PIPEWIRE_REMOTE")) return; + + auto dir = QDir(this->runtimeDir); + if (!dir.exists()) { + qCWarning(logConnection) << "Cannot wait for a new pipewire socket, runtime dir does not exist:" + << this->runtimeDir; + return; + } + + this->socketWatcher = new QFileSystemWatcher(this); + this->socketWatcher->addPath(this->runtimeDir); + + QObject::connect( + this->socketWatcher, + &QFileSystemWatcher::directoryChanged, + this, + &PwConnection::onRuntimeDirChanged + ); +} + +void PwConnection::stopSocketWatcher() { + if (this->socketWatcher == nullptr) return; + + this->socketWatcher->deleteLater(); + this->socketWatcher = nullptr; +} + +void PwConnection::queueFatalError() { + if (this->fatalErrorQueued) return; + + this->fatalErrorQueued = true; + QMetaObject::invokeMethod(this, &PwConnection::onFatalError, Qt::QueuedConnection); +} + +void PwConnection::onFatalError() { + this->fatalErrorQueued = false; + + this->defaults.reset(); + this->registry.reset(); + this->core.shutdown(); + + this->beginReconnect(); +} + +void PwConnection::onRuntimeDirChanged(const QString& /*path*/) { + if (this->core.isValid()) { + this->stopSocketWatcher(); + return; + } + + this->tryConnect(true); +} + PwConnection* PwConnection::instance() { static PwConnection* instance = nullptr; // NOLINT diff --git a/src/services/pipewire/connection.hpp b/src/services/pipewire/connection.hpp index 2b3e860..d0374f8 100644 --- a/src/services/pipewire/connection.hpp +++ b/src/services/pipewire/connection.hpp @@ -1,9 +1,13 @@ #pragma once +#include + #include "core.hpp" #include "defaults.hpp" #include "registry.hpp" +class QFileSystemWatcher; + namespace qs::service::pipewire { class PwConnection: public QObject { @@ -18,6 +22,23 @@ public: static PwConnection* instance(); private: + static QString resolveRuntimeDir(); + + void beginReconnect(); + bool tryConnect(bool retry); + void startSocketWatcher(); + void stopSocketWatcher(); + +private slots: + void queueFatalError(); + void onFatalError(); + void onRuntimeDirChanged(const QString& path); + +private: + QString runtimeDir; + QFileSystemWatcher* socketWatcher = nullptr; + bool fatalErrorQueued = false; + // init/destroy order is important. do not rearrange. PwCore core; }; diff --git a/src/services/pipewire/core.cpp b/src/services/pipewire/core.cpp index 22445aa..5077abe 100644 --- a/src/services/pipewire/core.cpp +++ b/src/services/pipewire/core.cpp @@ -27,7 +27,7 @@ const pw_core_events PwCore::EVENTS = { .info = nullptr, .done = &PwCore::onSync, .ping = nullptr, - .error = nullptr, + .error = &PwCore::onError, .remove_id = nullptr, .bound_id = nullptr, .add_mem = nullptr, @@ -36,26 +36,46 @@ const pw_core_events PwCore::EVENTS = { }; PwCore::PwCore(QObject* parent): QObject(parent), notifier(QSocketNotifier::Read) { - qCInfo(logLoop) << "Creating pipewire event loop."; pw_init(nullptr, nullptr); +} + +bool PwCore::start(bool retry) { + if (this->core != nullptr) return true; + + qCInfo(logLoop) << "Creating pipewire event loop."; this->loop = pw_loop_new(nullptr); if (this->loop == nullptr) { - qCCritical(logLoop) << "Failed to create pipewire event loop."; - return; + if (retry) { + qCInfo(logLoop) << "Failed to create pipewire event loop."; + } else { + qCCritical(logLoop) << "Failed to create pipewire event loop."; + } + this->shutdown(); + return false; } this->context = pw_context_new(this->loop, nullptr, 0); if (this->context == nullptr) { - qCCritical(logLoop) << "Failed to create pipewire context."; - return; + if (retry) { + qCInfo(logLoop) << "Failed to create pipewire context."; + } else { + qCCritical(logLoop) << "Failed to create pipewire context."; + } + this->shutdown(); + return false; } qCInfo(logLoop) << "Connecting to pipewire server."; this->core = pw_context_connect(this->context, nullptr, 0); if (this->core == nullptr) { - qCCritical(logLoop) << "Failed to connect pipewire context. Errno:" << errno; - return; + if (retry) { + qCInfo(logLoop) << "Failed to connect pipewire context. Errno:" << errno; + } else { + qCCritical(logLoop) << "Failed to connect pipewire context. Errno:" << errno; + } + this->shutdown(); + return false; } pw_core_add_listener(this->core, &this->listener.hook, &PwCore::EVENTS, this); @@ -66,22 +86,34 @@ PwCore::PwCore(QObject* parent): QObject(parent), notifier(QSocketNotifier::Read this->notifier.setSocket(fd); QObject::connect(&this->notifier, &QSocketNotifier::activated, this, &PwCore::poll); this->notifier.setEnabled(true); + + return true; +} + +void PwCore::shutdown() { + if (this->core != nullptr) { + this->listener.remove(); + pw_core_disconnect(this->core); + this->core = nullptr; + } + + if (this->context != nullptr) { + pw_context_destroy(this->context); + this->context = nullptr; + } + + if (this->loop != nullptr) { + pw_loop_destroy(this->loop); + this->loop = nullptr; + } + + this->notifier.setEnabled(false); + QObject::disconnect(&this->notifier, nullptr, this, nullptr); } PwCore::~PwCore() { qCInfo(logLoop) << "Destroying PwCore."; - - if (this->loop != nullptr) { - if (this->context != nullptr) { - if (this->core != nullptr) { - pw_core_disconnect(this->core); - } - - pw_context_destroy(this->context); - } - - pw_loop_destroy(this->loop); - } + this->shutdown(); } bool PwCore::isValid() const { @@ -90,6 +122,7 @@ bool PwCore::isValid() const { } void PwCore::poll() { + if (this->loop == nullptr) return; qCDebug(logLoop) << "Pipewire event loop received new events, iterating."; // Spin pw event loop. pw_loop_iterate(this->loop, 0); @@ -107,6 +140,23 @@ void PwCore::onSync(void* data, quint32 id, qint32 seq) { emit self->synced(id, seq); } +void PwCore::onError(void* data, quint32 id, qint32 /*seq*/, qint32 res, const char* message) { + auto* self = static_cast(data); + + // Pipewire's documentation describes the error event as being fatal, however it isn't. + // We're not sure what causes these ENOENTs on device removal, presumably something in + // the teardown sequence, but they're harmless. Attempting to handle them as a fatal + // error causes unnecessary triggers for shells. + if (res == -ENOENT) { + qCDebug(logLoop) << "Pipewire ENOENT on object" << id << "with code" << res << message; + return; + } + + qCWarning(logLoop) << "Pipewire error on object" << id << "with code" << res << message; + + emit self->fatalError(); +} + SpaHook::SpaHook() { // NOLINT spa_zero(this->hook); } diff --git a/src/services/pipewire/core.hpp b/src/services/pipewire/core.hpp index 262e2d3..967efaf 100644 --- a/src/services/pipewire/core.hpp +++ b/src/services/pipewire/core.hpp @@ -30,6 +30,9 @@ public: ~PwCore() override; Q_DISABLE_COPY_MOVE(PwCore); + bool start(bool retry); + void shutdown(); + [[nodiscard]] bool isValid() const; [[nodiscard]] qint32 sync(quint32 id) const; @@ -40,6 +43,7 @@ public: signals: void polled(); void synced(quint32 id, qint32 seq); + void fatalError(); private slots: void poll(); @@ -48,6 +52,7 @@ private: static const pw_core_events EVENTS; static void onSync(void* data, quint32 id, qint32 seq); + static void onError(void* data, quint32 id, qint32 seq, qint32 res, const char* message); QSocketNotifier notifier; SpaHook listener; diff --git a/src/services/pipewire/defaults.cpp b/src/services/pipewire/defaults.cpp index b3d8bfc..7a24a65 100644 --- a/src/services/pipewire/defaults.cpp +++ b/src/services/pipewire/defaults.cpp @@ -12,7 +12,6 @@ #include #include "../../core/logcat.hpp" -#include "../../core/util.hpp" #include "metadata.hpp" #include "node.hpp" #include "registry.hpp" @@ -31,6 +30,22 @@ PwDefaultTracker::PwDefaultTracker(PwRegistry* registry): registry(registry) { QObject::connect(registry, &PwRegistry::nodeAdded, this, &PwDefaultTracker::onNodeAdded); } +void PwDefaultTracker::reset() { + if (auto* meta = this->defaultsMetadata.object()) { + QObject::disconnect(meta, nullptr, this, nullptr); + } + + this->defaultsMetadata.setObject(nullptr); + this->setDefaultSink(nullptr); + this->setDefaultSinkName(QString()); + this->setDefaultSource(nullptr); + this->setDefaultSourceName(QString()); + this->setDefaultConfiguredSink(nullptr); + this->setDefaultConfiguredSinkName(QString()); + this->setDefaultConfiguredSource(nullptr); + this->setDefaultConfiguredSourceName(QString()); +} + void PwDefaultTracker::onMetadataAdded(PwMetadata* metadata) { if (metadata->name() == "default") { qCDebug(logDefaults) << "Got new defaults metadata object" << metadata; @@ -122,32 +137,6 @@ void PwDefaultTracker::onNodeAdded(PwNode* node) { } } -void PwDefaultTracker::onNodeDestroyed(QObject* node) { - if (node == this->mDefaultSink) { - qCInfo(logDefaults) << "Default sink destroyed."; - this->mDefaultSink = nullptr; - emit this->defaultSinkChanged(); - } - - if (node == this->mDefaultSource) { - qCInfo(logDefaults) << "Default source destroyed."; - this->mDefaultSource = nullptr; - emit this->defaultSourceChanged(); - } - - if (node == this->mDefaultConfiguredSink) { - qCInfo(logDefaults) << "Default configured sink destroyed."; - this->mDefaultConfiguredSink = nullptr; - emit this->defaultConfiguredSinkChanged(); - } - - if (node == this->mDefaultConfiguredSource) { - qCInfo(logDefaults) << "Default configured source destroyed."; - this->mDefaultConfiguredSource = nullptr; - emit this->defaultConfiguredSourceChanged(); - } -} - void PwDefaultTracker::changeConfiguredSink(PwNode* node) { if (node != nullptr) { if (!node->type.testFlags(PwNodeType::AudioSink)) { @@ -201,7 +190,8 @@ bool PwDefaultTracker::setConfiguredDefault(const char* key, const QString& valu } if (!meta->hasSetPermission()) { - qCCritical(logDefaults + qCCritical( + logDefaults ) << "Cannot set default node as write+execute permissions are missing for" << meta; return false; @@ -223,10 +213,23 @@ void PwDefaultTracker::setDefaultSink(PwNode* node) { if (node == this->mDefaultSink) return; qCInfo(logDefaults) << "Default sink changed to" << node; - setSimpleObjectHandle< - &PwDefaultTracker::mDefaultSink, - &PwDefaultTracker::onNodeDestroyed, - &PwDefaultTracker::defaultSinkChanged>(this, node); + if (this->mDefaultSink != nullptr) { + QObject::disconnect(this->mDefaultSink, nullptr, this, nullptr); + } + + this->mDefaultSink = node; + + if (node != nullptr) { + QObject::connect(node, &QObject::destroyed, this, &PwDefaultTracker::onDefaultSinkDestroyed); + } + + emit this->defaultSinkChanged(); +} + +void PwDefaultTracker::onDefaultSinkDestroyed() { + qCInfo(logDefaults) << "Default sink destroyed."; + this->mDefaultSink = nullptr; + emit this->defaultSinkChanged(); } void PwDefaultTracker::setDefaultSinkName(const QString& name) { @@ -240,10 +243,23 @@ void PwDefaultTracker::setDefaultSource(PwNode* node) { if (node == this->mDefaultSource) return; qCInfo(logDefaults) << "Default source changed to" << node; - setSimpleObjectHandle< - &PwDefaultTracker::mDefaultSource, - &PwDefaultTracker::onNodeDestroyed, - &PwDefaultTracker::defaultSourceChanged>(this, node); + if (this->mDefaultSource != nullptr) { + QObject::disconnect(this->mDefaultSource, nullptr, this, nullptr); + } + + this->mDefaultSource = node; + + if (node != nullptr) { + QObject::connect(node, &QObject::destroyed, this, &PwDefaultTracker::onDefaultSourceDestroyed); + } + + emit this->defaultSourceChanged(); +} + +void PwDefaultTracker::onDefaultSourceDestroyed() { + qCInfo(logDefaults) << "Default source destroyed."; + this->mDefaultSource = nullptr; + emit this->defaultSourceChanged(); } void PwDefaultTracker::setDefaultSourceName(const QString& name) { @@ -257,10 +273,28 @@ void PwDefaultTracker::setDefaultConfiguredSink(PwNode* node) { if (node == this->mDefaultConfiguredSink) return; qCInfo(logDefaults) << "Default configured sink changed to" << node; - setSimpleObjectHandle< - &PwDefaultTracker::mDefaultConfiguredSink, - &PwDefaultTracker::onNodeDestroyed, - &PwDefaultTracker::defaultConfiguredSinkChanged>(this, node); + if (this->mDefaultConfiguredSink != nullptr) { + QObject::disconnect(this->mDefaultConfiguredSink, nullptr, this, nullptr); + } + + this->mDefaultConfiguredSink = node; + + if (node != nullptr) { + QObject::connect( + node, + &QObject::destroyed, + this, + &PwDefaultTracker::onDefaultConfiguredSinkDestroyed + ); + } + + emit this->defaultConfiguredSinkChanged(); +} + +void PwDefaultTracker::onDefaultConfiguredSinkDestroyed() { + qCInfo(logDefaults) << "Default configured sink destroyed."; + this->mDefaultConfiguredSink = nullptr; + emit this->defaultConfiguredSinkChanged(); } void PwDefaultTracker::setDefaultConfiguredSinkName(const QString& name) { @@ -274,10 +308,28 @@ void PwDefaultTracker::setDefaultConfiguredSource(PwNode* node) { if (node == this->mDefaultConfiguredSource) return; qCInfo(logDefaults) << "Default configured source changed to" << node; - setSimpleObjectHandle< - &PwDefaultTracker::mDefaultConfiguredSource, - &PwDefaultTracker::onNodeDestroyed, - &PwDefaultTracker::defaultConfiguredSourceChanged>(this, node); + if (this->mDefaultConfiguredSource != nullptr) { + QObject::disconnect(this->mDefaultConfiguredSource, nullptr, this, nullptr); + } + + this->mDefaultConfiguredSource = node; + + if (node != nullptr) { + QObject::connect( + node, + &QObject::destroyed, + this, + &PwDefaultTracker::onDefaultConfiguredSourceDestroyed + ); + } + + emit this->defaultConfiguredSourceChanged(); +} + +void PwDefaultTracker::onDefaultConfiguredSourceDestroyed() { + qCInfo(logDefaults) << "Default configured source destroyed."; + this->mDefaultConfiguredSource = nullptr; + emit this->defaultConfiguredSourceChanged(); } void PwDefaultTracker::setDefaultConfiguredSourceName(const QString& name) { diff --git a/src/services/pipewire/defaults.hpp b/src/services/pipewire/defaults.hpp index f3a8e3f..f31669e 100644 --- a/src/services/pipewire/defaults.hpp +++ b/src/services/pipewire/defaults.hpp @@ -12,6 +12,7 @@ class PwDefaultTracker: public QObject { public: explicit PwDefaultTracker(PwRegistry* registry); + void reset(); [[nodiscard]] PwNode* defaultSink() const; [[nodiscard]] PwNode* defaultSource() const; @@ -43,7 +44,10 @@ private slots: void onMetadataAdded(PwMetadata* metadata); void onMetadataProperty(const char* key, const char* type, const char* value); void onNodeAdded(PwNode* node); - void onNodeDestroyed(QObject* node); + void onDefaultSinkDestroyed(); + void onDefaultSourceDestroyed(); + void onDefaultConfiguredSinkDestroyed(); + void onDefaultConfiguredSourceDestroyed(); private: void setDefaultSink(PwNode* node); diff --git a/src/services/pipewire/device.cpp b/src/services/pipewire/device.cpp index 616e7d0..61079a1 100644 --- a/src/services/pipewire/device.cpp +++ b/src/services/pipewire/device.cpp @@ -107,7 +107,7 @@ void PwDevice::addDeviceIndexPairs(const spa_pod* param) { qint32 device = 0; qint32 index = 0; - spa_pod* props = nullptr; + const spa_pod* props = nullptr; // clang-format off quint32 id = SPA_PARAM_Route; @@ -125,18 +125,32 @@ void PwDevice::addDeviceIndexPairs(const spa_pod* param) { // Insert into the main map as well, staging's purpose is to remove old entries. this->routeDeviceIndexes.insert(device, index); + // Used for initial node volume if the device is bound before the node + // (e.g. multiple nodes pointing to the same device) + this->routeDeviceVolumes.insert(device, volumeProps); + qCDebug(logDevice).nospace() << "Registered device/index pair for " << this << ": [device: " << device << ", index: " << index << ']'; emit this->routeVolumesChanged(device, volumeProps); } +bool PwDevice::tryLoadVolumeProps(qint32 routeDevice, PwVolumeProps& volumeProps) { + if (!this->routeDeviceVolumes.contains(routeDevice)) return false; + volumeProps = this->routeDeviceVolumes.value(routeDevice); + return true; +} + +bool PwDevice::hasRouteDevice(qint32 routeDevice) const { + return this->routeDeviceIndexes.contains(routeDevice); +} + void PwDevice::polled() { // It is far more likely that the list content has not come in yet than it having no entries, // and there isn't a way to check in the case that there *aren't* actually any entries. if (!this->stagingIndexes.isEmpty()) { - this->routeDeviceIndexes.removeIf([&](const std::pair& entry) { - if (!stagingIndexes.contains(entry.first)) { + this->routeDeviceIndexes.removeIf([&, this](const std::pair& entry) { + if (!this->stagingIndexes.contains(entry.first)) { qCDebug(logDevice).nospace() << "Removed device/index pair [device: " << entry.first << ", index: " << entry.second << "] for" << this; return true; diff --git a/src/services/pipewire/device.hpp b/src/services/pipewire/device.hpp index 1a1f705..cd61709 100644 --- a/src/services/pipewire/device.hpp +++ b/src/services/pipewire/device.hpp @@ -12,13 +12,15 @@ #include #include "core.hpp" -#include "node.hpp" #include "registry.hpp" namespace qs::service::pipewire { class PwDevice; +// Forward declare to avoid circular dependency with node.hpp +struct PwVolumeProps; + class PwDevice: public PwBindable { Q_OBJECT; @@ -32,6 +34,9 @@ public: void waitForDevice(); [[nodiscard]] bool waitingForDevice() const; + [[nodiscard]] bool tryLoadVolumeProps(qint32 routeDevice, PwVolumeProps& volumeProps); + [[nodiscard]] bool hasRouteDevice(qint32 routeDevice) const; + signals: void deviceReady(); void routeVolumesChanged(qint32 routeDevice, const PwVolumeProps& volumeProps); @@ -46,6 +51,7 @@ private: onParam(void* data, qint32 seq, quint32 id, quint32 index, quint32 next, const spa_pod* param); QHash routeDeviceIndexes; + QHash routeDeviceVolumes; QList stagingIndexes; void addDeviceIndexPairs(const spa_pod* param); diff --git a/src/services/pipewire/module.md b/src/services/pipewire/module.md index d109f05..e34f77d 100644 --- a/src/services/pipewire/module.md +++ b/src/services/pipewire/module.md @@ -2,6 +2,7 @@ name = "Quickshell.Services.Pipewire" description = "Pipewire API" headers = [ "qml.hpp", + "peak.hpp", "link.hpp", "node.hpp", ] diff --git a/src/services/pipewire/node.cpp b/src/services/pipewire/node.cpp index 3e68149..075a7ec 100644 --- a/src/services/pipewire/node.cpp +++ b/src/services/pipewire/node.cpp @@ -11,7 +11,7 @@ #include #include #include -#include +#include #include #include #include @@ -23,6 +23,7 @@ #include #include #include +#include #include #include "../../core/logcat.hpp" @@ -160,6 +161,24 @@ void PwNode::initProps(const spa_dict* props) { this->nick = nodeNick; } + if (const auto* nodeCategory = spa_dict_lookup(props, PW_KEY_MEDIA_CATEGORY)) { + if (strcmp(nodeCategory, "Monitor") == 0 || strcmp(nodeCategory, "Manager") == 0) { + this->isMonitor = true; + } + } + + if (const auto* serial = spa_dict_lookup(props, PW_KEY_OBJECT_SERIAL)) { + auto ok = false; + auto value = QString::fromUtf8(serial).toULongLong(&ok); + if (!ok) { + qCWarning(logNode) << this + << "has an object.serial property but the value is not valid. Value:" + << serial; + } else { + this->objectSerial = value; + } + } + if (const auto* deviceId = spa_dict_lookup(props, PW_KEY_DEVICE_ID)) { auto ok = false; auto id = QString::fromUtf8(deviceId).toInt(&ok); @@ -171,7 +190,8 @@ void PwNode::initProps(const spa_dict* props) { this->device = this->registry->devices.value(id); if (this->device == nullptr) { - qCCritical(logNode + qCCritical( + logNode ) << this << "has a device.id property that does not corrospond to a device object. Id:" << id; } @@ -195,21 +215,36 @@ void PwNode::onInfo(void* data, const pw_node_info* info) { if ((info->change_mask & PW_NODE_CHANGE_MASK_PROPS) != 0) { auto properties = QMap(); + bool proAudio = false; + if (const auto* proAudioStr = spa_dict_lookup(info->props, "device.profile.pro")) { + proAudio = spa_atob(proAudioStr); + } + + if (proAudio != self->proAudio) { + qCDebug(logNode) << self << "pro audio state changed:" << proAudio; + self->proAudio = proAudio; + } + if (self->device) { if (const auto* routeDevice = spa_dict_lookup(info->props, "card.profile.device")) { auto ok = false; auto id = QString::fromUtf8(routeDevice).toInt(&ok); if (!ok) { - qCCritical(logNode + qCCritical( + logNode ) << self << "has a card.profile.device property but the value is not an integer. Value:" << id; } self->routeDevice = id; + if (self->boundData) self->boundData->onDeviceChanged(); } else { - qCCritical(logNode) << self << "has attached device" << self->device - << "but no card.profile.device property."; + qCDebug( + logNode + ) << self + << "has attached device" << self->device + << "but no card.profile.device property. Node volume control will be used."; } } @@ -266,6 +301,15 @@ PwNodeBoundAudio::PwNodeBoundAudio(PwNode* node): QObject(node), node(node) { } } +void PwNodeBoundAudio::onDeviceChanged() { + PwVolumeProps volumeProps; + if (this->node->device->tryLoadVolumeProps(this->node->routeDevice, volumeProps)) { + qCDebug(logNode) << "Initializing volume props for" << this->node + << "with known values from backing device."; + this->updateVolumeProps(volumeProps); + } +} + void PwNodeBoundAudio::onInfo(const pw_node_info* info) { if ((info->change_mask & PW_NODE_CHANGE_MASK_PARAMS) != 0) { for (quint32 i = 0; i < info->n_params; i++) { @@ -286,9 +330,10 @@ void PwNodeBoundAudio::onInfo(const pw_node_info* info) { void PwNodeBoundAudio::onSpaParam(quint32 id, quint32 index, const spa_pod* param) { if (id == SPA_PARAM_Props && index == 0) { - if (this->node->device) { + if (this->node->shouldUseDevice()) { qCDebug(logNode) << "Skipping node volume props update for" << this->node - << "in favor of device updates."; + << "in favor of device updates from routeDevice" << this->node->routeDevice + << "of" << this->node->device; return; } @@ -304,6 +349,8 @@ void PwNodeBoundAudio::updateVolumeProps(const PwVolumeProps& volumeProps) { return; } + this->volumeStep = volumeProps.volumeStep; + // It is important that the lengths of channels and volumes stay in sync whenever you read them. auto channelsChanged = false; auto volumesChanged = false; @@ -356,7 +403,7 @@ void PwNodeBoundAudio::setMuted(bool muted) { if (muted == this->mMuted) return; - if (this->node->device) { + if (this->node->shouldUseDevice()) { qCInfo(logNode) << "Changing muted state of" << this->node << "to" << muted << "via device"; if (!this->node->device->setMuted(this->node->routeDevice, muted)) { return; @@ -382,6 +429,10 @@ void PwNodeBoundAudio::setMuted(bool muted) { } float PwNodeBoundAudio::averageVolume() const { + if (this->mVolumes.isEmpty()) { + return 0.0f; + } + float total = 0; for (auto volume: this->mVolumes) { @@ -429,37 +480,41 @@ void PwNodeBoundAudio::setVolumes(const QVector& volumes) { return; } - if (this->node->device) { + if (this->node->shouldUseDevice()) { if (this->node->device->waitingForDevice()) { qCInfo(logNode) << "Waiting to change volumes of" << this->node << "to" << realVolumes << "via device"; this->waitingVolumes = realVolumes; } else { - auto significantChange = this->mServerVolumes.isEmpty(); - for (auto i = 0; i < this->mServerVolumes.length(); i++) { - auto serverVolume = this->mServerVolumes.value(i); - auto targetVolume = realVolumes.value(i); - if (targetVolume == 0 || abs(targetVolume - serverVolume) >= 0.0001) { - significantChange = true; - break; - } - } - - if (significantChange) { - qCInfo(logNode) << "Changing volumes of" << this->node << "to" << realVolumes - << "via device"; - if (!this->node->device->setVolumes(this->node->routeDevice, realVolumes)) { - return; + if (this->volumeStep != -1) { + auto significantChange = this->mServerVolumes.isEmpty(); + for (auto i = 0; i < this->mServerVolumes.length(); i++) { + auto serverVolume = this->mServerVolumes.value(i); + auto targetVolume = realVolumes.value(i); + if (targetVolume == 0 || abs(targetVolume - serverVolume) >= this->volumeStep) { + significantChange = true; + break; + } } - this->mDeviceVolumes = realVolumes; - this->node->device->waitForDevice(); - } else { - // Insignificant changes won't cause an info event on the device, leaving qs hung in the - // "waiting for acknowledgement" state forever. - qCInfo(logNode) << "Ignoring volume change for" << this->node << "to" << realVolumes - << "from" << this->mServerVolumes - << "as it is a device node and the change is too small."; + if (significantChange) { + qCInfo(logNode) << "Changing volumes of" << this->node << "to" << realVolumes + << "via device"; + if (!this->node->device->setVolumes(this->node->routeDevice, realVolumes)) { + return; + } + + this->mDeviceVolumes = realVolumes; + this->node->device->waitForDevice(); + } else { + // Insignificant changes won't cause an info event on the device, leaving qs hung in the + // "waiting for acknowledgement" state forever. + qCInfo(logNode).nospace() + << "Ignoring volume change for " << this->node << " to " << realVolumes << " from " + << this->mServerVolumes + << " as it is a device node and the change is too small (min step: " + << this->volumeStep << ")."; + } } } } else { @@ -505,7 +560,7 @@ void PwNodeBoundAudio::onDeviceVolumesChanged( qint32 routeDevice, const PwVolumeProps& volumeProps ) { - if (this->node->device && this->node->routeDevice == routeDevice) { + if (this->node->shouldUseDevice() && this->node->routeDevice == routeDevice) { qCDebug(logNode) << "Got updated device volume props for" << this->node << "via" << this->node->device; @@ -519,23 +574,36 @@ PwVolumeProps PwVolumeProps::parseSpaPod(const spa_pod* param) { const auto* volumesProp = spa_pod_find_prop(param, nullptr, SPA_PROP_channelVolumes); const auto* channelsProp = spa_pod_find_prop(param, nullptr, SPA_PROP_channelMap); const auto* muteProp = spa_pod_find_prop(param, nullptr, SPA_PROP_mute); + const auto* volumeStepProp = spa_pod_find_prop(param, nullptr, SPA_PROP_volumeStep); - const auto* volumes = reinterpret_cast(&volumesProp->value); - const auto* channels = reinterpret_cast(&channelsProp->value); - - spa_pod* iter = nullptr; - SPA_POD_ARRAY_FOREACH(volumes, iter) { - // Cubing behavior found in MPD source, and appears to corrospond to everyone else's measurements correctly. - auto linear = *reinterpret_cast(iter); - auto visual = std::cbrt(linear); - props.volumes.push_back(visual); + if (volumesProp) { + const auto* volumes = reinterpret_cast(&volumesProp->value); + spa_pod* iter = nullptr; + SPA_POD_ARRAY_FOREACH(volumes, iter) { + // Cubing behavior found in MPD source, and appears to corrospond to everyone else's measurements correctly. + auto linear = *reinterpret_cast(iter); + auto visual = std::cbrt(linear); + props.volumes.push_back(visual); + } } - SPA_POD_ARRAY_FOREACH(channels, iter) { - props.channels.push_back(*reinterpret_cast(iter)); + if (channelsProp) { + const auto* channels = reinterpret_cast(&channelsProp->value); + spa_pod* iter = nullptr; + SPA_POD_ARRAY_FOREACH(channels, iter) { + props.channels.push_back(*reinterpret_cast(iter)); + } } - spa_pod_get_bool(&muteProp->value, &props.mute); + if (muteProp) { + spa_pod_get_bool(&muteProp->value, &props.mute); + } + + if (volumeStepProp) { + spa_pod_get_float(&volumeStepProp->value, &props.volumeStep); + } else { + props.volumeStep = -1; + } return props; } diff --git a/src/services/pipewire/node.hpp b/src/services/pipewire/node.hpp index 0d4c92e..efc819c 100644 --- a/src/services/pipewire/node.hpp +++ b/src/services/pipewire/node.hpp @@ -15,6 +15,7 @@ #include #include "core.hpp" +#include "device.hpp" #include "registry.hpp" namespace qs::service::pipewire { @@ -158,6 +159,7 @@ struct PwVolumeProps { QVector channels; QVector volumes; bool mute = false; + float volumeStep = -1; static PwVolumeProps parseSpaPod(const spa_pod* param); }; @@ -168,6 +170,7 @@ public: virtual ~PwNodeBoundData() = default; Q_DISABLE_COPY_MOVE(PwNodeBoundData); + virtual void onDeviceChanged() {}; virtual void onInfo(const pw_node_info* /*info*/) {} virtual void onSpaParam(quint32 /*id*/, quint32 /*index*/, const spa_pod* /*param*/) {} virtual void onUnbind() {} @@ -181,6 +184,7 @@ class PwNodeBoundAudio public: explicit PwNodeBoundAudio(PwNode* node); + void onDeviceChanged() override; void onInfo(const pw_node_info* info) override; void onSpaParam(quint32 id, quint32 index, const spa_pod* param) override; void onUnbind() override; @@ -196,6 +200,8 @@ public: [[nodiscard]] QVector volumes() const; void setVolumes(const QVector& volumes); + [[nodiscard]] QVector server() const; + signals: void volumesChanged(); void channelsChanged(); @@ -214,6 +220,7 @@ private: QVector mServerVolumes; QVector mDeviceVolumes; QVector waitingVolumes; + float volumeStep = -1; PwNode* node; }; @@ -229,6 +236,8 @@ public: QString description; QString nick; QMap properties; + quint64 objectSerial = 0; + bool isMonitor = false; PwNodeType::Flags type = PwNodeType::Untracked; @@ -238,6 +247,13 @@ public: PwDevice* device = nullptr; qint32 routeDevice = -1; + bool proAudio = false; + + [[nodiscard]] bool shouldUseDevice() const { + if (!this->device || this->proAudio || this->routeDevice == -1) return false; + // Only use device control if the device actually has route indexes for this routeDevice + return this->device->hasRouteDevice(this->routeDevice); + } signals: void propertiesChanged(); diff --git a/src/services/pipewire/peak.cpp b/src/services/pipewire/peak.cpp new file mode 100644 index 0000000..64b5c42 --- /dev/null +++ b/src/services/pipewire/peak.cpp @@ -0,0 +1,404 @@ +#include "peak.hpp" +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../../core/logcat.hpp" +#include "connection.hpp" +#include "core.hpp" +#include "node.hpp" +#include "qml.hpp" + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wmissing-designated-field-initializers" + +namespace qs::service::pipewire { + +namespace { +QS_LOGGING_CATEGORY(logPeak, "quickshell.service.pipewire.peak", QtWarningMsg); +} + +class PwPeakStream { +public: + PwPeakStream(PwNodePeakMonitor* monitor, PwNode* node): monitor(monitor), node(node) {} + ~PwPeakStream() { this->destroy(); } + Q_DISABLE_COPY_MOVE(PwPeakStream); + + bool start(); + void destroy(); + +private: + static const pw_stream_events EVENTS; + static void onProcess(void* data); + static void onParamChanged(void* data, uint32_t id, const spa_pod* param); + static void + onStateChanged(void* data, pw_stream_state oldState, pw_stream_state state, const char* error); + static void onDestroy(void* data); + + void handleProcess(); + void handleParamChanged(uint32_t id, const spa_pod* param); + void handleStateChanged(pw_stream_state oldState, pw_stream_state state, const char* error); + void resetFormat(); + + PwNodePeakMonitor* monitor = nullptr; + PwNode* node = nullptr; + pw_stream* stream = nullptr; + SpaHook listener; + spa_audio_info_raw format = SPA_AUDIO_INFO_RAW_INIT(.format = SPA_AUDIO_FORMAT_UNKNOWN); + bool formatReady = false; + QVector channelPeaks; +}; + +const pw_stream_events PwPeakStream::EVENTS = { + .version = PW_VERSION_STREAM_EVENTS, + .destroy = &PwPeakStream::onDestroy, + .state_changed = &PwPeakStream::onStateChanged, + .param_changed = &PwPeakStream::onParamChanged, + .process = &PwPeakStream::onProcess, +}; + +bool PwPeakStream::start() { + auto* core = PwConnection::instance()->registry.core; + if (core == nullptr || !core->isValid()) { + qCWarning(logPeak) << "Cannot start peak monitor stream: pipewire core is not ready."; + return false; + } + + auto target = + QByteArray::number(this->node->objectSerial ? this->node->objectSerial : this->node->id); + + // clang-format off + auto* props = pw_properties_new( + PW_KEY_MEDIA_TYPE, "Audio", + PW_KEY_MEDIA_CATEGORY, "Monitor", + PW_KEY_MEDIA_NAME, "Peak detect", + PW_KEY_APP_NAME, "Quickshell Peak Detect", + PW_KEY_STREAM_MONITOR, "true", + PW_KEY_STREAM_CAPTURE_SINK, this->node->type.testFlags(PwNodeType::Sink) ? "true" : "false", + PW_KEY_TARGET_OBJECT, target.constData(), + nullptr + ); + // clang-format on + + if (props == nullptr) { + qCWarning(logPeak) << "Failed to create properties for peak monitor stream."; + return false; + } + + this->stream = pw_stream_new(core->core, "quickshell-peak-monitor", props); + if (this->stream == nullptr) { + qCWarning(logPeak) << "Failed to create peak monitor stream."; + return false; + } + + pw_stream_add_listener(this->stream, &this->listener.hook, &PwPeakStream::EVENTS, this); + + auto buffer = std::array {}; + auto builder = SPA_POD_BUILDER_INIT(buffer.data(), buffer.size()); // NOLINT + + auto params = std::array {}; + auto raw = SPA_AUDIO_INFO_RAW_INIT(.format = SPA_AUDIO_FORMAT_F32); + params[0] = spa_format_audio_raw_build(&builder, SPA_PARAM_EnumFormat, &raw); + + auto flags = + static_cast(PW_STREAM_FLAG_AUTOCONNECT | PW_STREAM_FLAG_MAP_BUFFERS); + auto res = + pw_stream_connect(this->stream, PW_DIRECTION_INPUT, PW_ID_ANY, flags, params.data(), 1); + + if (res < 0) { + qCWarning(logPeak) << "Failed to connect peak monitor stream:" << res; + this->destroy(); + return false; + } + + return true; +} + +void PwPeakStream::destroy() { + if (this->stream == nullptr) return; + this->listener.remove(); + pw_stream_destroy(this->stream); + this->stream = nullptr; + this->resetFormat(); +} + +void PwPeakStream::onProcess(void* data) { + static_cast(data)->handleProcess(); // NOLINT +} + +void PwPeakStream::onParamChanged(void* data, uint32_t id, const spa_pod* param) { + static_cast(data)->handleParamChanged(id, param); // NOLINT +} + +void PwPeakStream::onStateChanged( + void* data, + pw_stream_state oldState, + pw_stream_state state, + const char* error +) { + static_cast(data)->handleStateChanged(oldState, state, error); // NOLINT +} + +void PwPeakStream::onDestroy(void* data) { + auto* self = static_cast(data); // NOLINT + self->stream = nullptr; + self->listener.remove(); + self->resetFormat(); +} + +void PwPeakStream::handleStateChanged( + pw_stream_state oldState, + pw_stream_state state, + const char* error +) { + if (state == PW_STREAM_STATE_ERROR) { + if (error != nullptr) { + qCWarning(logPeak) << "Peak monitor stream error:" << error; + } else { + qCWarning(logPeak) << "Peak monitor stream error."; + } + } + + if (state == PW_STREAM_STATE_PAUSED && oldState != PW_STREAM_STATE_PAUSED) { + auto peakCount = this->monitor->mChannels.length(); + if (peakCount == 0) { + peakCount = this->monitor->mPeaks.length(); + } + if (peakCount == 0 && this->formatReady) { + peakCount = static_cast(this->format.channels); + } + + if (peakCount > 0) { + auto zeros = QVector(peakCount, 0.0f); + this->monitor->updatePeaks(zeros, 0.0f); + } + } +} + +void PwPeakStream::handleParamChanged(uint32_t id, const spa_pod* param) { + if (param == nullptr || id != SPA_PARAM_Format) return; + + auto info = spa_audio_info {}; + if (spa_format_parse(param, &info.media_type, &info.media_subtype) < 0) return; + + if (info.media_type != SPA_MEDIA_TYPE_audio || info.media_subtype != SPA_MEDIA_SUBTYPE_raw) + return; + + auto raw = SPA_AUDIO_INFO_RAW_INIT(.format = SPA_AUDIO_FORMAT_UNKNOWN); // NOLINT + if (spa_format_audio_raw_parse(param, &raw) < 0) return; + + if (raw.format != SPA_AUDIO_FORMAT_F32) { + qCWarning(logPeak) << "Unsupported peak monitor format for" << this->node << ":" << raw.format; + this->resetFormat(); + return; + } + + this->format = raw; + this->formatReady = raw.channels > 0; + + auto channels = QVector(); + channels.reserve(static_cast(raw.channels)); + + for (quint32 i = 0; i < raw.channels; i++) { + if ((raw.flags & SPA_AUDIO_FLAG_UNPOSITIONED) != 0) { + channels.push_back(PwAudioChannel::Unknown); + } else { + channels.push_back(static_cast(raw.position[i])); + } + } + + this->channelPeaks.fill(0.0f, channels.size()); + this->monitor->updateChannels(channels); + this->monitor->updatePeaks(this->channelPeaks, 0.0f); +} + +void PwPeakStream::resetFormat() { + this->format = SPA_AUDIO_INFO_RAW_INIT(.format = SPA_AUDIO_FORMAT_UNKNOWN); + this->formatReady = false; + this->channelPeaks.clear(); + this->monitor->clearPeaks(); +} + +void PwPeakStream::handleProcess() { + if (!this->formatReady || this->stream == nullptr) return; + + auto* buffer = pw_stream_dequeue_buffer(this->stream); + auto requeue = qScopeGuard([&, this] { pw_stream_queue_buffer(this->stream, buffer); }); + + if (buffer == nullptr) { + qCWarning(logPeak) << "Peak monitor ran out of buffers."; + return; + } + + auto* spaBuffer = buffer->buffer; + if (spaBuffer == nullptr || spaBuffer->n_datas < 1) { + return; + } + + auto* data = &spaBuffer->datas[0]; // NOLINT + if (data->data == nullptr || data->chunk == nullptr) { + return; + } + + auto channelCount = static_cast(this->format.channels); + if (channelCount <= 0) { + return; + } + + const auto* base = static_cast(data->data) + data->chunk->offset; // NOLINT + const auto* samples = reinterpret_cast(base); + auto sampleCount = static_cast(data->chunk->size / sizeof(float)); + + if (sampleCount < channelCount) { + return; + } + + QVector volumes; + if (auto* audioData = dynamic_cast(this->node->boundData)) { + if (!this->node->shouldUseDevice()) volumes = audioData->volumes(); + } + + this->channelPeaks.fill(0.0f, channelCount); + + auto maxPeak = 0.0f; + for (auto channel = 0; channel < channelCount; channel++) { + auto peak = 0.0f; + for (auto sample = channel; sample < sampleCount; sample += channelCount) { + peak = std::max(peak, std::abs(samples[sample])); // NOLINT + } + + auto visualPeak = std::cbrt(peak); + if (!volumes.isEmpty() && volumes[channel] != 0.0f) visualPeak *= 1.0f / volumes[channel]; + + this->channelPeaks[channel] = visualPeak; + maxPeak = std::max(maxPeak, visualPeak); + } + + this->monitor->updatePeaks(this->channelPeaks, maxPeak); +} + +PwNodePeakMonitor::PwNodePeakMonitor(QObject* parent): QObject(parent) {} + +PwNodePeakMonitor::~PwNodePeakMonitor() { + delete this->mStream; + this->mStream = nullptr; +} + +PwNodeIface* PwNodePeakMonitor::node() const { return this->mNode; } + +void PwNodePeakMonitor::setNode(PwNodeIface* node) { + if (node == this->mNode) return; + + if (this->mNode != nullptr) { + QObject::disconnect(this->mNode, nullptr, this, nullptr); + } + + if (node != nullptr) { + QObject::connect(node, &QObject::destroyed, this, &PwNodePeakMonitor::onNodeDestroyed); + } + + this->mNode = node; + this->mNodeRef.setObject(node != nullptr ? node->node() : nullptr); + this->rebuildStream(); + emit this->nodeChanged(); +} + +bool PwNodePeakMonitor::isEnabled() const { return this->mEnabled; } + +void PwNodePeakMonitor::setEnabled(bool enabled) { + if (enabled == this->mEnabled) return; + this->mEnabled = enabled; + this->rebuildStream(); + emit this->enabledChanged(); +} + +void PwNodePeakMonitor::onNodeDestroyed() { + this->mNode = nullptr; + this->mNodeRef.setObject(nullptr); + this->rebuildStream(); + emit this->nodeChanged(); +} + +void PwNodePeakMonitor::updatePeaks(const QVector& peaks, float peak) { + if (this->mPeaks != peaks) { + this->mPeaks = peaks; + emit this->peaksChanged(); + } + + if (this->mPeak != peak) { + this->mPeak = peak; + emit this->peakChanged(); + } +} + +void PwNodePeakMonitor::updateChannels(const QVector& channels) { + if (this->mChannels == channels) return; + this->mChannels = channels; + emit this->channelsChanged(); +} + +void PwNodePeakMonitor::clearPeaks() { + if (!this->mPeaks.isEmpty()) { + this->mPeaks.clear(); + emit this->peaksChanged(); + } + + if (!this->mChannels.isEmpty()) { + this->mChannels.clear(); + emit this->channelsChanged(); + } + + if (this->mPeak != 0.0f) { + this->mPeak = 0.0f; + emit this->peakChanged(); + } +} + +void PwNodePeakMonitor::rebuildStream() { + delete this->mStream; + this->mStream = nullptr; + + auto* node = this->mNodeRef.object(); + if (!this->mEnabled || node == nullptr) { + this->clearPeaks(); + return; + } + + if (node == nullptr || !node->type.testFlags(PwNodeType::Audio)) { + this->clearPeaks(); + return; + } + + this->mStream = new PwPeakStream(this, node); + if (!this->mStream->start()) { + delete this->mStream; + this->mStream = nullptr; + this->clearPeaks(); + } +} + +} // namespace qs::service::pipewire + +#pragma GCC diagnostic pop diff --git a/src/services/pipewire/peak.hpp b/src/services/pipewire/peak.hpp new file mode 100644 index 0000000..c4af3c2 --- /dev/null +++ b/src/services/pipewire/peak.hpp @@ -0,0 +1,87 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "node.hpp" + +namespace qs::service::pipewire { + +class PwNodeIface; +class PwPeakStream; + +} // namespace qs::service::pipewire + +Q_DECLARE_OPAQUE_POINTER(qs::service::pipewire::PwNodeIface*); + +namespace qs::service::pipewire { + +///! Monitors peak levels of an audio node. +/// Tracks volume peaks for a node across all its channels. +/// +/// The peak monitor binds nodes similarly to @@PwObjectTracker when enabled. +class PwNodePeakMonitor: public QObject { + Q_OBJECT; + // clang-format off + /// The node to monitor. Must be an audio node. + Q_PROPERTY(qs::service::pipewire::PwNodeIface* node READ node WRITE setNode NOTIFY nodeChanged); + /// If true, the monitor is actively capturing and computing peaks. Defaults to true. + Q_PROPERTY(bool enabled READ isEnabled WRITE setEnabled NOTIFY enabledChanged); + /// Per-channel peak noise levels (0.0-1.0). Length matches @@channels. + /// + /// The channel's volume does not affect this property. + Q_PROPERTY(QVector peaks READ peaks NOTIFY peaksChanged); + /// Maximum value of @@peaks. + Q_PROPERTY(float peak READ peak NOTIFY peakChanged); + /// Channel positions for the captured format. Length matches @@peaks. + Q_PROPERTY(QVector channels READ channels NOTIFY channelsChanged); + // clang-format on + QML_ELEMENT; + +public: + explicit PwNodePeakMonitor(QObject* parent = nullptr); + ~PwNodePeakMonitor() override; + Q_DISABLE_COPY_MOVE(PwNodePeakMonitor); + + [[nodiscard]] PwNodeIface* node() const; + void setNode(PwNodeIface* node); + + [[nodiscard]] bool isEnabled() const; + void setEnabled(bool enabled); + + [[nodiscard]] QVector peaks() const { return this->mPeaks; } + [[nodiscard]] float peak() const { return this->mPeak; } + [[nodiscard]] QVector channels() const { return this->mChannels; } + +signals: + void nodeChanged(); + void enabledChanged(); + void peaksChanged(); + void peakChanged(); + void channelsChanged(); + +private slots: + void onNodeDestroyed(); + +private: + friend class PwPeakStream; + + void updatePeaks(const QVector& peaks, float peak); + void updateChannels(const QVector& channels); + void clearPeaks(); + void rebuildStream(); + + PwNodeIface* mNode = nullptr; + PwBindableRef mNodeRef; + bool mEnabled = true; + QVector mPeaks; + float mPeak = 0.0f; + QVector mChannels; + PwPeakStream* mStream = nullptr; +}; + +} // namespace qs::service::pipewire diff --git a/src/services/pipewire/qml.cpp b/src/services/pipewire/qml.cpp index 5d8c45e..e4424c1 100644 --- a/src/services/pipewire/qml.cpp +++ b/src/services/pipewire/qml.cpp @@ -2,7 +2,6 @@ #include #include -#include #include #include #include @@ -18,6 +17,16 @@ namespace qs::service::pipewire { +PwObjectIface::PwObjectIface(PwBindableObject* object): QObject(object), object(object) { + // We want to destroy the interface before QObject::destroyed is fired, as handlers + // connected before PwObjectIface will run first and emit signals that hit user code, + // which can then try to reference the iface again after ~PwNode() has been called but + // before ~QObject() has finished. + QObject::connect(object, &PwBindableObject::destroying, this, &PwObjectIface::onObjectDestroying); +} + +void PwObjectIface::onObjectDestroying() { delete this; } + void PwObjectIface::ref() { this->refcount++; @@ -89,15 +98,8 @@ Pipewire::Pipewire(QObject* parent): QObject(parent) { &Pipewire::defaultConfiguredAudioSourceChanged ); - if (!connection->registry.isInitialized()) { - QObject::connect( - &connection->registry, - &PwRegistry::initialized, - this, - &Pipewire::readyChanged, - Qt::SingleShotConnection - ); - } + QObject::connect(&connection->registry, &PwRegistry::initialized, this, &Pipewire::readyChanged); + QObject::connect(&connection->registry, &PwRegistry::cleared, this, &Pipewire::readyChanged); } ObjectModel* Pipewire::nodes() { return &this->mNodes; } @@ -211,6 +213,7 @@ void PwNodeLinkTracker::updateLinks() { || (this->mNode->isSink() && link->inputNode() == this->mNode->id())) { auto* iface = PwLinkGroupIface::instance(link); + if (iface->target()->node()->isMonitor) return; // do not connect twice if (!this->mLinkGroups.contains(iface)) { @@ -229,7 +232,7 @@ void PwNodeLinkTracker::updateLinks() { for (auto* iface: this->mLinkGroups) { // only disconnect no longer used nodes - if (!newLinks.contains(iface)) { + if (!newLinks.contains(iface) || iface->target()->node()->isMonitor) { QObject::disconnect(iface, nullptr, this, nullptr); } } @@ -269,6 +272,8 @@ void PwNodeLinkTracker::onLinkGroupCreated(PwLinkGroup* linkGroup) { || (this->mNode->isSink() && linkGroup->inputNode() == this->mNode->id())) { auto* iface = PwLinkGroupIface::instance(linkGroup); + if (iface->target()->node()->isMonitor) return; + QObject::connect(iface, &QObject::destroyed, this, &PwNodeLinkTracker::onLinkGroupDestroyed); this->mLinkGroups.push_back(iface); emit this->linkGroupsChanged(); diff --git a/src/services/pipewire/qml.hpp b/src/services/pipewire/qml.hpp index 5bcc70d..a43ce19 100644 --- a/src/services/pipewire/qml.hpp +++ b/src/services/pipewire/qml.hpp @@ -36,7 +36,7 @@ class PwObjectIface Q_OBJECT; public: - explicit PwObjectIface(PwBindableObject* object): QObject(object), object(object) {}; + explicit PwObjectIface(PwBindableObject* object); // destructor should ONLY be called by the pw object destructor, making an unref unnecessary ~PwObjectIface() override = default; Q_DISABLE_COPY_MOVE(PwObjectIface); @@ -44,6 +44,9 @@ public: void ref() override; void unref() override; +private slots: + void onObjectDestroying(); + private: quint32 refcount = 0; PwBindableObject* object; @@ -168,13 +171,13 @@ private: ObjectModel mLinkGroups {this}; }; -///! Tracks all link connections to a given node. +///! Tracks non-monitor link connections to a given node. class PwNodeLinkTracker: public QObject { Q_OBJECT; // clang-format off /// The node to track connections to. Q_PROPERTY(qs::service::pipewire::PwNodeIface* node READ node WRITE setNode NOTIFY nodeChanged); - /// Link groups connected to the given node. + /// Link groups connected to the given node, excluding monitors. /// /// If the node is a sink, links which target the node will be tracked. /// If the node is a source, links which source the node will be tracked. diff --git a/src/services/pipewire/registry.cpp b/src/services/pipewire/registry.cpp index c08fc1d..4b670b1 100644 --- a/src/services/pipewire/registry.cpp +++ b/src/services/pipewire/registry.cpp @@ -134,6 +134,46 @@ void PwRegistry::init(PwCore& core) { this->coreSyncSeq = this->core->sync(PW_ID_CORE); } +void PwRegistry::reset() { + if (this->core != nullptr) { + QObject::disconnect(this->core, nullptr, this, nullptr); + } + + this->listener.remove(); + + if (this->object != nullptr) { + pw_proxy_destroy(reinterpret_cast(this->object)); + this->object = nullptr; + } + + for (auto* meta: this->metadata.values()) { + meta->safeDestroy(); + } + this->metadata.clear(); + + for (auto* link: this->links.values()) { + link->safeDestroy(); + } + this->links.clear(); + + for (auto* node: this->nodes.values()) { + node->safeDestroy(); + } + this->nodes.clear(); + + for (auto* device: this->devices.values()) { + device->safeDestroy(); + } + this->devices.clear(); + + this->linkGroups.clear(); + this->initState = InitState::SendingObjects; + this->coreSyncSeq = 0; + this->core = nullptr; + + emit this->cleared(); +} + void PwRegistry::onCoreSync(quint32 id, qint32 seq) { if (id != PW_ID_CORE || seq != this->coreSyncSeq) return; diff --git a/src/services/pipewire/registry.hpp b/src/services/pipewire/registry.hpp index 14ea405..bb2db8c 100644 --- a/src/services/pipewire/registry.hpp +++ b/src/services/pipewire/registry.hpp @@ -55,8 +55,8 @@ protected: void registryBind(const char* interface, quint32 version); virtual void bind(); void unbind(); - virtual void bindHooks() {}; - virtual void unbindHooks() {}; + virtual void bindHooks() {} + virtual void unbindHooks() {} quint32 refcount = 0; pw_proxy* object = nullptr; @@ -116,6 +116,7 @@ class PwRegistry public: void init(PwCore& core); + void reset(); [[nodiscard]] bool isInitialized() const { return this->initState == InitState::Done; } @@ -136,6 +137,7 @@ signals: void linkGroupAdded(PwLinkGroup* group); void metadataAdded(PwMetadata* metadata); void initialized(); + void cleared(); private slots: void onLinkGroupDestroyed(QObject* object); diff --git a/src/services/polkit/CMakeLists.txt b/src/services/polkit/CMakeLists.txt new file mode 100644 index 0000000..51791d8 --- /dev/null +++ b/src/services/polkit/CMakeLists.txt @@ -0,0 +1,35 @@ +find_package(PkgConfig REQUIRED) +pkg_check_modules(glib REQUIRED IMPORTED_TARGET glib-2.0>=2.36) +pkg_check_modules(gobject REQUIRED IMPORTED_TARGET gobject-2.0) +pkg_check_modules(polkit_agent REQUIRED IMPORTED_TARGET polkit-agent-1) +pkg_check_modules(polkit REQUIRED IMPORTED_TARGET polkit-gobject-1) + +qt_add_library(quickshell-service-polkit STATIC + agentimpl.cpp + flow.cpp + identity.cpp + listener.cpp + session.cpp + qml.cpp +) + +qt_add_qml_module(quickshell-service-polkit + URI Quickshell.Services.Polkit + VERSION 0.1 + DEPENDENCIES QtQml +) + +install_qml_module(quickshell-service-polkit) + +target_link_libraries(quickshell-service-polkit PRIVATE + Qt::Qml + Qt::Quick + PkgConfig::glib + PkgConfig::gobject + PkgConfig::polkit_agent + PkgConfig::polkit +) + +qs_module_pch(quickshell-service-polkit) + +target_link_libraries(quickshell PRIVATE quickshell-service-polkitplugin) diff --git a/src/services/polkit/agentimpl.cpp b/src/services/polkit/agentimpl.cpp new file mode 100644 index 0000000..85c62b7 --- /dev/null +++ b/src/services/polkit/agentimpl.cpp @@ -0,0 +1,180 @@ +#include "agentimpl.hpp" +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "../../core/generation.hpp" +#include "../../core/logcat.hpp" +#include "gobjectref.hpp" +#include "listener.hpp" +#include "qml.hpp" + +namespace { +QS_LOGGING_CATEGORY(logPolkit, "quickshell.service.polkit", QtWarningMsg); +} + +namespace qs::service::polkit { +PolkitAgentImpl* PolkitAgentImpl::instance = nullptr; + +PolkitAgentImpl::PolkitAgentImpl(PolkitAgent* agent) + : QObject(nullptr) + , listener(qs_polkit_agent_new(this), G_OBJECT_NO_REF) + , qmlAgent(agent) + , path(this->qmlAgent->path()) { + auto utf8Path = this->path.toUtf8(); + qs_polkit_agent_register(this->listener.get(), utf8Path.constData()); +} + +PolkitAgentImpl::~PolkitAgentImpl() { this->cancelAllRequests("PolkitAgent is being destroyed"); } + +void PolkitAgentImpl::cancelAllRequests(const QString& reason) { + for (; !this->queuedRequests.empty(); this->queuedRequests.pop_back()) { + AuthRequest* req = this->queuedRequests.back(); + qCDebug(logPolkit) << "destroying queued authentication request for action" << req->actionId; + req->cancel(reason); + delete req; + } + + auto* flow = this->bActiveFlow.value(); + if (flow) { + flow->cancelAuthenticationRequest(); + flow->deleteLater(); + } + + if (this->bIsRegistered.value()) qs_polkit_agent_unregister(this->listener.get()); +} + +PolkitAgentImpl* PolkitAgentImpl::tryGetOrCreate(PolkitAgent* agent) { + if (instance == nullptr) instance = new PolkitAgentImpl(agent); + if (instance->qmlAgent == agent) return instance; + return nullptr; +} + +PolkitAgentImpl* PolkitAgentImpl::tryGet(const PolkitAgent* agent) { + if (instance == nullptr) return nullptr; + if (instance->qmlAgent == agent) return instance; + return nullptr; +} + +PolkitAgentImpl* PolkitAgentImpl::tryTakeoverOrCreate(PolkitAgent* agent) { + if (auto* impl = tryGetOrCreate(agent); impl != nullptr) return impl; + + auto* prevGen = EngineGeneration::findObjectGeneration(instance->qmlAgent); + auto* myGen = EngineGeneration::findObjectGeneration(agent); + if (prevGen == myGen) return nullptr; + + qCDebug(logPolkit) << "taking over listener from previous generation"; + instance->qmlAgent = agent; + instance->setPath(agent->path()); + + return instance; +} + +void PolkitAgentImpl::onEndOfQmlAgent(PolkitAgent* agent) { + if (instance != nullptr && instance->qmlAgent == agent) { + delete instance; + instance = nullptr; + } +} + +void PolkitAgentImpl::setPath(const QString& path) { + if (this->path == path) return; + + this->path = path; + auto utf8Path = path.toUtf8(); + + this->cancelAllRequests("PolkitAgent path changed"); + qs_polkit_agent_unregister(this->listener.get()); + this->bIsRegistered = false; + + qs_polkit_agent_register(this->listener.get(), utf8Path.constData()); +} + +void PolkitAgentImpl::registerComplete(bool success) { + if (success) this->bIsRegistered = true; + else qCWarning(logPolkit) << "failed to register listener on path" << this->qmlAgent->path(); +} + +void PolkitAgentImpl::initiateAuthentication(AuthRequest* request) { + qCDebug(logPolkit) << "incoming authentication request for action" << request->actionId; + + this->queuedRequests.emplace_back(request); + + if (this->queuedRequests.size() == 1) { + this->activateAuthenticationRequest(); + } +} + +void PolkitAgentImpl::cancelAuthentication(AuthRequest* request) { + qCDebug(logPolkit) << "cancelling authentication request from agent"; + + auto* flow = this->bActiveFlow.value(); + if (flow && flow->authRequest() == request) { + flow->cancelFromAgent(); + } else if (auto it = std::ranges::find(this->queuedRequests, request); + it != this->queuedRequests.end()) + { + qCDebug(logPolkit) << "removing queued authentication request for action" << (*it)->actionId; + (*it)->cancel("Authentication request was cancelled"); + delete (*it); + this->queuedRequests.erase(it); + } else { + qCWarning(logPolkit) << "the cancelled request was not found in the queue."; + } +} + +void PolkitAgentImpl::activateAuthenticationRequest() { + if (this->queuedRequests.empty()) return; + + AuthRequest* req = this->queuedRequests.front(); + this->queuedRequests.pop_front(); + qCDebug(logPolkit) << "activating authentication request for action" << req->actionId + << ", cookie: " << req->cookie; + + QList identities; + for (auto& identity: req->identities) { + auto* obj = Identity::fromPolkitIdentity(identity); + if (obj) identities.append(obj); + } + if (identities.isEmpty()) { + qCWarning( + logPolkit + ) << "no supported identities available for authentication request, cancelling."; + req->cancel("Error requesting authentication: no supported identities available."); + delete req; + return; + } + + this->bActiveFlow = new AuthFlow(req, std::move(identities)); + + QObject::connect( + this->bActiveFlow.value(), + &AuthFlow::isCompletedChanged, + this, + &PolkitAgentImpl::finishAuthenticationRequest + ); + + emit this->qmlAgent->authenticationRequestStarted(); +} + +void PolkitAgentImpl::finishAuthenticationRequest() { + if (!this->bActiveFlow.value()) return; + + qCDebug(logPolkit) << "finishing authentication request for action" + << this->bActiveFlow.value()->actionId(); + + this->bActiveFlow.value()->deleteLater(); + + if (!this->queuedRequests.empty()) { + this->activateAuthenticationRequest(); + } else { + this->bActiveFlow = nullptr; + } +} +} // namespace qs::service::polkit diff --git a/src/services/polkit/agentimpl.hpp b/src/services/polkit/agentimpl.hpp new file mode 100644 index 0000000..65ae11a --- /dev/null +++ b/src/services/polkit/agentimpl.hpp @@ -0,0 +1,66 @@ +#pragma once + +#include + +#include +#include + +#include "flow.hpp" +#include "gobjectref.hpp" +#include "listener.hpp" + +namespace qs::service::polkit { +class PolkitAgent; + +class PolkitAgentImpl + : public QObject + , public ListenerCb { + Q_OBJECT; + Q_DISABLE_COPY_MOVE(PolkitAgentImpl); + +public: + ~PolkitAgentImpl() override; + + static PolkitAgentImpl* tryGetOrCreate(PolkitAgent* agent); + static PolkitAgentImpl* tryGet(const PolkitAgent* agent); + static PolkitAgentImpl* tryTakeoverOrCreate(PolkitAgent* agent); + static void onEndOfQmlAgent(PolkitAgent* agent); + + [[nodiscard]] QBindable activeFlow() { return &this->bActiveFlow; }; + [[nodiscard]] QBindable isRegistered() { return &this->bIsRegistered; }; + + [[nodiscard]] const QString& getPath() const { return this->path; } + void setPath(const QString& path); + + void initiateAuthentication(AuthRequest* request) override; + void cancelAuthentication(AuthRequest* request) override; + void registerComplete(bool success) override; + + void cancelAllRequests(const QString& reason); + +signals: + void activeFlowChanged(); + void isRegisteredChanged(); + +private: + PolkitAgentImpl(PolkitAgent* agent); + + static PolkitAgentImpl* instance; + + /// Start handling of the next authentication request in the queue. + void activateAuthenticationRequest(); + /// Finalize and remove the current authentication request. + void finishAuthenticationRequest(); + + GObjectRef listener; + PolkitAgent* qmlAgent = nullptr; + QString path; + + std::deque queuedRequests; + + // clang-format off + Q_OBJECT_BINDABLE_PROPERTY(PolkitAgentImpl, AuthFlow*, bActiveFlow, &PolkitAgentImpl::activeFlowChanged); + Q_OBJECT_BINDABLE_PROPERTY(PolkitAgentImpl, bool, bIsRegistered, &PolkitAgentImpl::isRegisteredChanged); + // clang-format on +}; +} // namespace qs::service::polkit diff --git a/src/services/polkit/flow.cpp b/src/services/polkit/flow.cpp new file mode 100644 index 0000000..2a709eb --- /dev/null +++ b/src/services/polkit/flow.cpp @@ -0,0 +1,163 @@ +#include "flow.hpp" +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "../../core/logcat.hpp" +#include "identity.hpp" +#include "qml.hpp" +#include "session.hpp" + +namespace { +QS_LOGGING_CATEGORY(logPolkitState, "quickshell.service.polkit.state", QtWarningMsg); +} + +namespace qs::service::polkit { +AuthFlow::AuthFlow(AuthRequest* request, QList&& identities, QObject* parent) + : QObject(parent) + , mRequest(request) + , mIdentities(std::move(identities)) + , bSelectedIdentity(this->mIdentities.isEmpty() ? nullptr : this->mIdentities.first()) { + // We reject auth requests with no identities before a flow is created. + // This should never happen. + if (!this->bSelectedIdentity.value()) + qCFatal(logPolkitState) << "AuthFlow created with no valid identities!"; + + for (auto* identity: this->mIdentities) { + identity->setParent(this); + } + + this->setupSession(); +} + +AuthFlow::~AuthFlow() { delete this->mRequest; }; + +void AuthFlow::setSelectedIdentity(Identity* identity) { + if (this->bSelectedIdentity.value() == identity) return; + if (!identity) { + qmlWarning(this) << "Cannot set selected identity to null."; + return; + } + this->bSelectedIdentity = identity; + this->currentSession->cancel(); + this->setupSession(); +} + +void AuthFlow::cancelFromAgent() { + if (!this->currentSession) return; + + qCDebug(logPolkitState) << "cancelling authentication request from agent"; + + // Session cancel can immediately call the cancel handler, which also + // performs property updates. + Qt::beginPropertyUpdateGroup(); + this->bIsCancelled = true; + this->currentSession->cancel(); + Qt::endPropertyUpdateGroup(); + + emit this->authenticationRequestCancelled(); + + this->mRequest->cancel("Authentication request cancelled by agent."); +} + +void AuthFlow::submit(const QString& value) { + if (!this->currentSession) return; + + qCDebug(logPolkitState) << "submitting response to authentication request"; + + this->currentSession->respond(value); + + Qt::beginPropertyUpdateGroup(); + this->bIsResponseRequired = false; + this->bInputPrompt = QString(); + this->bResponseVisible = false; + Qt::endPropertyUpdateGroup(); +} + +void AuthFlow::cancelAuthenticationRequest() { + if (!this->currentSession) return; + + qCDebug(logPolkitState) << "cancelling authentication request by user request"; + + // Session cancel can immediately call the cancel handler, which also + // performs property updates. + Qt::beginPropertyUpdateGroup(); + this->bIsCancelled = true; + this->currentSession->cancel(); + Qt::endPropertyUpdateGroup(); + + this->mRequest->cancel("Authentication request cancelled by user."); +} + +void AuthFlow::setupSession() { + delete this->currentSession; + + qCDebug(logPolkitState) << "setting up session for identity" + << this->bSelectedIdentity.value()->name(); + + this->currentSession = new Session( + this->bSelectedIdentity.value()->polkitIdentity.get(), + this->mRequest->cookie, + this + ); + QObject::connect(this->currentSession, &Session::request, this, &AuthFlow::request); + QObject::connect(this->currentSession, &Session::completed, this, &AuthFlow::completed); + QObject::connect(this->currentSession, &Session::showError, this, &AuthFlow::showError); + QObject::connect(this->currentSession, &Session::showInfo, this, &AuthFlow::showInfo); + this->currentSession->initiate(); +} + +void AuthFlow::request(const QString& message, bool echo) { + Qt::beginPropertyUpdateGroup(); + this->bIsResponseRequired = true; + this->bInputPrompt = message; + this->bResponseVisible = echo; + Qt::endPropertyUpdateGroup(); +} + +void AuthFlow::completed(bool gainedAuthorization) { + qCDebug(logPolkitState) << "authentication session completed, gainedAuthorization =" + << gainedAuthorization << ", isCancelled =" << this->bIsCancelled.value(); + + if (gainedAuthorization) { + Qt::beginPropertyUpdateGroup(); + this->bIsCompleted = true; + this->bIsSuccessful = true; + Qt::endPropertyUpdateGroup(); + + this->mRequest->complete(); + + emit this->authenticationSucceeded(); + } else if (this->bIsCancelled.value()) { + Qt::beginPropertyUpdateGroup(); + this->bIsCompleted = true; + this->bIsSuccessful = false; + Qt::endPropertyUpdateGroup(); + } else { + this->bFailed = true; + emit this->authenticationFailed(); + + this->setupSession(); + } +} + +void AuthFlow::showError(const QString& message) { + Qt::beginPropertyUpdateGroup(); + this->bSupplementaryMessage = message; + this->bSupplementaryIsError = true; + Qt::endPropertyUpdateGroup(); +} + +void AuthFlow::showInfo(const QString& message) { + Qt::beginPropertyUpdateGroup(); + this->bSupplementaryMessage = message; + this->bSupplementaryIsError = false; + Qt::endPropertyUpdateGroup(); +} +} // namespace qs::service::polkit diff --git a/src/services/polkit/flow.hpp b/src/services/polkit/flow.hpp new file mode 100644 index 0000000..0b7e845 --- /dev/null +++ b/src/services/polkit/flow.hpp @@ -0,0 +1,179 @@ +#pragma once + +#include +#include +#include + +#include "../../core/retainable.hpp" +#include "identity.hpp" +#include "listener.hpp" + +namespace qs::service::polkit { +class Session; + +class AuthFlow + : public QObject + , public Retainable { + Q_OBJECT; + QML_ELEMENT; + Q_DISABLE_COPY_MOVE(AuthFlow); + QML_UNCREATABLE("AuthFlow can only be obtained from PolkitAgent."); + + // clang-format off + /// The main message to present to the user. + Q_PROPERTY(QString message READ message CONSTANT); + + /// The icon to present to the user in association with the message. + /// + /// The icon name follows the [FreeDesktop icon naming specification](https://specifications.freedesktop.org/icon-naming-spec/icon-naming-spec-latest.html). + /// Use @@Quickshell.Quickshell.iconPath() to resolve the icon name to an + /// actual file path for display. + Q_PROPERTY(QString iconName READ iconName CONSTANT); + + /// The action ID represents the action that is being authorized. + /// + /// This is a machine-readable identifier. + Q_PROPERTY(QString actionId READ actionId CONSTANT); + + /// A cookie that identifies this authentication request. + /// + /// This is an internal identifier and not recommended to show to users. + Q_PROPERTY(QString cookie READ cookie CONSTANT); + + /// The list of identities that may be used to authenticate. + /// + /// Each identity may be a user or a group. You may select any of them to + /// authenticate by setting @@selectedIdentity. By default, the first identity + /// in the list is selected. + Q_PROPERTY(QList identities READ identities CONSTANT); + + /// The identity that will be used to authenticate. + /// + /// Changing this will abort any ongoing authentication conversations and start a new one. + Q_PROPERTY(Identity* selectedIdentity READ default WRITE setSelectedIdentity NOTIFY selectedIdentityChanged BINDABLE selectedIdentity); + + /// Indicates that a response from the user is required from the user, + /// typically a password. + Q_PROPERTY(bool isResponseRequired READ default NOTIFY isResponseRequiredChanged BINDABLE isResponseRequired); + + /// This message is used to prompt the user for required input. + Q_PROPERTY(QString inputPrompt READ default NOTIFY inputPromptChanged BINDABLE inputPrompt); + + /// Indicates whether the user's response should be visible. (e.g. for passwords this should be false) + Q_PROPERTY(bool responseVisible READ default NOTIFY responseVisibleChanged BINDABLE responseVisible); + + /// An additional message to present to the user. + /// + /// This may be used to show errors or supplementary information. + /// See @@supplementaryIsError to determine if this is an error message. + Q_PROPERTY(QString supplementaryMessage READ default NOTIFY supplementaryMessageChanged BINDABLE supplementaryMessage); + + /// Indicates whether the supplementary message is an error. + Q_PROPERTY(bool supplementaryIsError READ default NOTIFY supplementaryIsErrorChanged BINDABLE supplementaryIsError); + + /// Has the authentication request been completed. + Q_PROPERTY(bool isCompleted READ default NOTIFY isCompletedChanged BINDABLE isCompleted); + + /// Indicates whether the authentication request was successful. + Q_PROPERTY(bool isSuccessful READ default NOTIFY isSuccessfulChanged BINDABLE isSuccessful); + + /// Indicates whether the current authentication request was cancelled. + Q_PROPERTY(bool isCancelled READ default NOTIFY isCancelledChanged BINDABLE isCancelled); + + /// Indicates whether an authentication attempt has failed at least once during this authentication flow. + Q_PROPERTY(bool failed READ default NOTIFY failedChanged BINDABLE failed); + // clang-format on + +public: + explicit AuthFlow(AuthRequest* request, QList&& identities, QObject* parent = nullptr); + ~AuthFlow() override; + + /// Cancel the ongoing authentication request from the agent side. + void cancelFromAgent(); + + /// Submit a response to a request that was previously emitted. Typically the password. + Q_INVOKABLE void submit(const QString& value); + /// Cancel the ongoing authentication request from the user side. + Q_INVOKABLE void cancelAuthenticationRequest(); + + [[nodiscard]] const QString& message() const { return this->mRequest->message; }; + [[nodiscard]] const QString& iconName() const { return this->mRequest->iconName; }; + [[nodiscard]] const QString& actionId() const { return this->mRequest->actionId; }; + [[nodiscard]] const QString& cookie() const { return this->mRequest->cookie; }; + [[nodiscard]] const QList& identities() const { return this->mIdentities; }; + + [[nodiscard]] QBindable selectedIdentity() { return &this->bSelectedIdentity; }; + void setSelectedIdentity(Identity* identity); + + [[nodiscard]] QBindable isResponseRequired() { return &this->bIsResponseRequired; }; + [[nodiscard]] QBindable inputPrompt() { return &this->bInputPrompt; }; + [[nodiscard]] QBindable responseVisible() { return &this->bResponseVisible; }; + + [[nodiscard]] QBindable supplementaryMessage() { return &this->bSupplementaryMessage; }; + [[nodiscard]] QBindable supplementaryIsError() { return &this->bSupplementaryIsError; }; + + [[nodiscard]] QBindable isCompleted() { return &this->bIsCompleted; }; + [[nodiscard]] QBindable isSuccessful() { return &this->bIsSuccessful; }; + [[nodiscard]] QBindable isCancelled() { return &this->bIsCancelled; }; + [[nodiscard]] QBindable failed() { return &this->bFailed; }; + + [[nodiscard]] AuthRequest* authRequest() const { return this->mRequest; }; + +signals: + /// Emitted whenever an authentication request completes successfully. + void authenticationSucceeded(); + + /// Emitted whenever an authentication request completes unsuccessfully. + /// + /// This may be because the user entered the wrong password or otherwise + /// failed to authenticate. + /// This signal is not emmitted when the user canceled the request or it + /// was cancelled by the PolKit daemon. + /// + /// After this signal, a new session is automatically started for the same + /// identity. + void authenticationFailed(); + + /// Emmitted when on ongoing authentication request is cancelled by the PolKit daemon. + void authenticationRequestCancelled(); + + void selectedIdentityChanged(); + void isResponseRequiredChanged(); + void inputPromptChanged(); + void responseVisibleChanged(); + void supplementaryMessageChanged(); + void supplementaryIsErrorChanged(); + void isCompletedChanged(); + void isSuccessfulChanged(); + void isCancelledChanged(); + void failedChanged(); + +private slots: + // Signals received from session objects. + void request(const QString& message, bool echo); + void completed(bool gainedAuthorization); + void showError(const QString& message); + void showInfo(const QString& message); + +private: + /// Start a session for the currently selected identity and the current request. + void setupSession(); + + Session* currentSession = nullptr; + AuthRequest* mRequest = nullptr; + QList mIdentities; + + // clang-format off + Q_OBJECT_BINDABLE_PROPERTY(AuthFlow, Identity*, bSelectedIdentity, &AuthFlow::selectedIdentityChanged); + Q_OBJECT_BINDABLE_PROPERTY(AuthFlow, bool, bIsResponseRequired, &AuthFlow::isResponseRequiredChanged); + Q_OBJECT_BINDABLE_PROPERTY(AuthFlow, QString, bInputPrompt, &AuthFlow::inputPromptChanged); + Q_OBJECT_BINDABLE_PROPERTY(AuthFlow, bool, bResponseVisible, &AuthFlow::responseVisibleChanged); + Q_OBJECT_BINDABLE_PROPERTY(AuthFlow, QString, bSupplementaryMessage, &AuthFlow::supplementaryMessageChanged); + Q_OBJECT_BINDABLE_PROPERTY(AuthFlow, bool, bSupplementaryIsError, &AuthFlow::supplementaryIsErrorChanged); + Q_OBJECT_BINDABLE_PROPERTY(AuthFlow, bool, bIsCompleted, &AuthFlow::isCompletedChanged); + Q_OBJECT_BINDABLE_PROPERTY(AuthFlow, bool, bIsSuccessful, &AuthFlow::isSuccessfulChanged); + Q_OBJECT_BINDABLE_PROPERTY(AuthFlow, bool, bIsCancelled, &AuthFlow::isCancelledChanged); + Q_OBJECT_BINDABLE_PROPERTY(AuthFlow, bool, bFailed, &AuthFlow::failedChanged); + // clang-format on +}; +} // namespace qs::service::polkit diff --git a/src/services/polkit/gobjectref.hpp b/src/services/polkit/gobjectref.hpp new file mode 100644 index 0000000..cd29a9d --- /dev/null +++ b/src/services/polkit/gobjectref.hpp @@ -0,0 +1,65 @@ +#pragma once + +#include + +namespace qs::service::polkit { + +struct GObjectNoRefTag {}; +constexpr GObjectNoRefTag G_OBJECT_NO_REF; + +template +class GObjectRef { +public: + explicit GObjectRef(T* ptr = nullptr): ptr(ptr) { + if (this->ptr) { + g_object_ref(this->ptr); + } + } + + explicit GObjectRef(T* ptr, GObjectNoRefTag /*tag*/): ptr(ptr) {} + + ~GObjectRef() { + if (this->ptr) { + g_object_unref(this->ptr); + } + } + + // We do handle self-assignment in a more general case by checking the + // included pointers rather than the wrapper objects themselves. + // NOLINTBEGIN(bugprone-unhandled-self-assignment) + + GObjectRef(const GObjectRef& other): GObjectRef(other.ptr) {} + GObjectRef& operator=(const GObjectRef& other) { + if (*this == other) return *this; + if (this->ptr) { + g_object_unref(this->ptr); + } + this->ptr = other.ptr; + if (this->ptr) { + g_object_ref(this->ptr); + } + return *this; + } + + GObjectRef(GObjectRef&& other) noexcept: ptr(other.ptr) { other.ptr = nullptr; } + GObjectRef& operator=(GObjectRef&& other) noexcept { + if (*this == other) return *this; + if (this->ptr) { + g_object_unref(this->ptr); + } + this->ptr = other.ptr; + other.ptr = nullptr; + return *this; + } + + // NOLINTEND(bugprone-unhandled-self-assignment) + + [[nodiscard]] T* get() const { return this->ptr; } + T* operator->() const { return this->ptr; } + + bool operator==(const GObjectRef& other) const { return this->ptr == other.ptr; } + +private: + T* ptr; +}; +} // namespace qs::service::polkit \ No newline at end of file diff --git a/src/services/polkit/identity.cpp b/src/services/polkit/identity.cpp new file mode 100644 index 0000000..7be5f39 --- /dev/null +++ b/src/services/polkit/identity.cpp @@ -0,0 +1,84 @@ +#include "identity.hpp" +#include +#include +#include + +#include +#include +#include + +#define POLKIT_AGENT_I_KNOW_API_IS_SUBJECT_TO_CHANGE +// Workaround macro collision with glib 'signals' struct member. +#undef signals +#include +#define signals Q_SIGNALS +#include +#include +#include + +#include "gobjectref.hpp" + +namespace qs::service::polkit { +Identity::Identity( + id_t id, + QString name, + QString displayName, + bool isGroup, + GObjectRef polkitIdentity, + QObject* parent +) + : QObject(parent) + , polkitIdentity(std::move(polkitIdentity)) + , mId(id) + , mName(std::move(name)) + , mDisplayName(std::move(displayName)) + , mIsGroup(isGroup) {} + +Identity* Identity::fromPolkitIdentity(GObjectRef identity) { + if (POLKIT_IS_UNIX_USER(identity.get())) { + auto uid = polkit_unix_user_get_uid(POLKIT_UNIX_USER(identity.get())); + + auto bufSize = sysconf(_SC_GETPW_R_SIZE_MAX); + // The call can fail with -1, in this case choose a default that is + // big enough. + if (bufSize == -1) bufSize = 16384; + auto buffer = std::vector(bufSize); + + std::aligned_storage_t pwBuf; + passwd* pw = nullptr; + getpwuid_r(uid, reinterpret_cast(&pwBuf), buffer.data(), bufSize, &pw); + + auto name = + (pw && pw->pw_name && *pw->pw_name) ? QString::fromUtf8(pw->pw_name) : QString::number(uid); + + return new Identity( + uid, + name, + (pw && pw->pw_gecos && *pw->pw_gecos) ? QString::fromUtf8(pw->pw_gecos) : name, + false, + std::move(identity) + ); + } + + if (POLKIT_IS_UNIX_GROUP(identity.get())) { + auto gid = polkit_unix_group_get_gid(POLKIT_UNIX_GROUP(identity.get())); + + auto bufSize = sysconf(_SC_GETGR_R_SIZE_MAX); + // The call can fail with -1, in this case choose a default that is + // big enough. + if (bufSize == -1) bufSize = 16384; + auto buffer = std::vector(bufSize); + + std::aligned_storage_t grBuf; + group* gr = nullptr; + getgrgid_r(gid, reinterpret_cast(&grBuf), buffer.data(), bufSize, &gr); + + auto name = + (gr && gr->gr_name && *gr->gr_name) ? QString::fromUtf8(gr->gr_name) : QString::number(gid); + return new Identity(gid, name, name, true, std::move(identity)); + } + + // A different type of identity is netgroup. + return nullptr; +} +} // namespace qs::service::polkit diff --git a/src/services/polkit/identity.hpp b/src/services/polkit/identity.hpp new file mode 100644 index 0000000..27f3c1c --- /dev/null +++ b/src/services/polkit/identity.hpp @@ -0,0 +1,64 @@ +#pragma once + +#include +#include + +#include "gobjectref.hpp" + +// _PolkitIdentity is considered a reserved identifier, but I am specifically +// forward declaring this reserved name. +using PolkitIdentity = struct _PolkitIdentity; // NOLINT(bugprone-reserved-identifier) + +namespace qs::service::polkit { +//! Represents a user or group that can be used to authenticate. +class Identity: public QObject { + Q_OBJECT; + Q_DISABLE_COPY_MOVE(Identity); + + // clang-format off + /// The Id of the identity. If the identity is a user, this is the user's uid. See @@isGroup. + Q_PROPERTY(quint32 id READ id CONSTANT); + + /// The name of the user or group. + /// + /// If available, this is the actual username or group name, but may fallback to the ID. + Q_PROPERTY(QString string READ name CONSTANT); + + /// The full name of the user or group, if available. Otherwise the same as @@name. + Q_PROPERTY(QString displayName READ displayName CONSTANT); + + /// Indicates if this identity is a group or a user. + /// + /// If true, @@id is a gid, otherwise it is a uid. + Q_PROPERTY(bool isGroup READ isGroup CONSTANT); + + QML_UNCREATABLE("Identities cannot be created directly."); + // clang-format on + +public: + explicit Identity( + id_t id, + QString name, + QString displayName, + bool isGroup, + GObjectRef polkitIdentity, + QObject* parent = nullptr + ); + ~Identity() override = default; + + static Identity* fromPolkitIdentity(GObjectRef identity); + + [[nodiscard]] quint32 id() const { return static_cast(this->mId); }; + [[nodiscard]] const QString& name() const { return this->mName; }; + [[nodiscard]] const QString& displayName() const { return this->mDisplayName; }; + [[nodiscard]] bool isGroup() const { return this->mIsGroup; }; + + GObjectRef polkitIdentity; + +private: + id_t mId; + QString mName; + QString mDisplayName; + bool mIsGroup; +}; +} // namespace qs::service::polkit diff --git a/src/services/polkit/listener.cpp b/src/services/polkit/listener.cpp new file mode 100644 index 0000000..e4bca4c --- /dev/null +++ b/src/services/polkit/listener.cpp @@ -0,0 +1,232 @@ +#include "listener.hpp" +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../../core/logcat.hpp" +#include "gobjectref.hpp" +#include "qml.hpp" + +namespace { +QS_LOGGING_CATEGORY(logPolkitListener, "quickshell.service.polkit.listener", QtWarningMsg); +} + +using qs::service::polkit::GObjectRef; + +// This is mostly GObject code, we follow their naming conventions for improved +// clarity and to mark it as such. Additionally, many methods need to be static +// to conform with the expected declarations. +// NOLINTBEGIN(readability-identifier-naming,misc-use-anonymous-namespace) + +using QsPolkitAgent = struct _QsPolkitAgent { + PolkitAgentListener parent_instance; + + qs::service::polkit::ListenerCb* cb; + gpointer registration_handle; +}; + +G_DEFINE_TYPE(QsPolkitAgent, qs_polkit_agent, POLKIT_AGENT_TYPE_LISTENER) + +static void initiate_authentication( + PolkitAgentListener* listener, + const gchar* actionId, + const gchar* message, + const gchar* iconName, + PolkitDetails* details, + const gchar* cookie, + GList* identities, + GCancellable* cancellable, + GAsyncReadyCallback callback, + gpointer userData +); + +static gboolean +initiate_authentication_finish(PolkitAgentListener* listener, GAsyncResult* result, GError** error); + +static void qs_polkit_agent_init(QsPolkitAgent* self) { + self->cb = nullptr; + self->registration_handle = nullptr; +} + +static void qs_polkit_agent_finalize(GObject* object) { + if (G_OBJECT_CLASS(qs_polkit_agent_parent_class)) + G_OBJECT_CLASS(qs_polkit_agent_parent_class)->finalize(object); +} + +static void qs_polkit_agent_class_init(QsPolkitAgentClass* klass) { + GObjectClass* gobject_class = G_OBJECT_CLASS(klass); + gobject_class->finalize = qs_polkit_agent_finalize; + + PolkitAgentListenerClass* listener_class = POLKIT_AGENT_LISTENER_CLASS(klass); + listener_class->initiate_authentication = initiate_authentication; + listener_class->initiate_authentication_finish = initiate_authentication_finish; +} + +QsPolkitAgent* qs_polkit_agent_new(qs::service::polkit::ListenerCb* cb) { + QsPolkitAgent* self = QS_POLKIT_AGENT(g_object_new(QS_TYPE_POLKIT_AGENT, nullptr)); + self->cb = cb; + return self; +} + +struct RegisterCbData { + GObjectRef agent; + std::string path; +}; + +static void qs_polkit_agent_register_cb(GObject* /*unused*/, GAsyncResult* res, gpointer userData); +void qs_polkit_agent_register(QsPolkitAgent* agent, const char* path) { + if (path == nullptr || *path == '\0') { + qCWarning(logPolkitListener) << "cannot register listener without a path set."; + agent->cb->registerComplete(false); + return; + } + + auto* data = new RegisterCbData {.agent = GObjectRef(agent), .path = path}; + polkit_unix_session_new_for_process(getpid(), nullptr, &qs_polkit_agent_register_cb, data); +} + +static void qs_polkit_agent_register_cb(GObject* /*unused*/, GAsyncResult* res, gpointer userData) { + std::unique_ptr data(reinterpret_cast(userData)); + + GError* error = nullptr; + auto* subject = polkit_unix_session_new_for_process_finish(res, &error); + + if (subject == nullptr || error != nullptr) { + qCWarning(logPolkitListener) << "failed to create subject for listener:" + << (error ? error->message : ""); + g_clear_error(&error); + data->agent->cb->registerComplete(false); + return; + } + + data->agent->registration_handle = polkit_agent_listener_register( + POLKIT_AGENT_LISTENER(data->agent.get()), + POLKIT_AGENT_REGISTER_FLAGS_NONE, + subject, + data->path.c_str(), + nullptr, + &error + ); + + g_object_unref(subject); + + if (error != nullptr) { + qCWarning(logPolkitListener) << "failed to register listener:" << error->message; + g_clear_error(&error); + data->agent->cb->registerComplete(false); + return; + } + + data->agent->cb->registerComplete(true); +} + +void qs_polkit_agent_unregister(QsPolkitAgent* agent) { + if (agent->registration_handle != nullptr) { + polkit_agent_listener_unregister(agent->registration_handle); + agent->registration_handle = nullptr; + } +} + +static void authentication_cancelled_cb(GCancellable* /*unused*/, gpointer userData) { + auto* request = static_cast(userData); + request->cb->cancelAuthentication(request); +} + +static void initiate_authentication( + PolkitAgentListener* listener, + const gchar* actionId, + const gchar* message, + const gchar* iconName, + PolkitDetails* /*unused*/, + const gchar* cookie, + GList* identities, + GCancellable* cancellable, + GAsyncReadyCallback callback, + gpointer userData +) { + auto* self = QS_POLKIT_AGENT(listener); + + auto* asyncResult = g_task_new(reinterpret_cast(self), nullptr, callback, userData); + + // Identities may be duplicated, so we use the hash to filter them out. + std::unordered_set identitySet; + std::vector> identityVector; + for (auto* item = g_list_first(identities); item != nullptr; item = g_list_next(item)) { + auto* identity = static_cast(item->data); + if (identitySet.contains(polkit_identity_hash(identity))) continue; + + identitySet.insert(polkit_identity_hash(identity)); + // The caller unrefs all identities after we return, therefore we need to + // take our own reference for the identities we keep. Our wrapper does + // this automatically. + identityVector.emplace_back(identity); + } + + // The original strings are freed by the caller after we return, so we + // copy them into QStrings. + auto* request = new qs::service::polkit::AuthRequest { + .actionId = QString::fromUtf8(actionId), + .message = QString::fromUtf8(message), + .iconName = QString::fromUtf8(iconName), + .cookie = QString::fromUtf8(cookie), + .identities = std::move(identityVector), + + .task = asyncResult, + .cancellable = cancellable, + .handlerId = 0, + .cb = self->cb + }; + + if (cancellable != nullptr) { + request->handlerId = g_cancellable_connect( + cancellable, + reinterpret_cast(authentication_cancelled_cb), + request, + nullptr + ); + } + + self->cb->initiateAuthentication(request); +} + +static gboolean initiate_authentication_finish( + PolkitAgentListener* /*unused*/, + GAsyncResult* result, + GError** error +) { + return g_task_propagate_boolean(G_TASK(result), error); +} + +namespace qs::service::polkit { +// While these functions can be const since they do not modify member variables, +// they are logically non-const since they modify the state of the +// authentication request. Therefore, we do not mark them as const. +// NOLINTBEGIN(readability-make-member-function-const) +void AuthRequest::complete() { g_task_return_boolean(this->task, true); } + +void AuthRequest::cancel(const QString& reason) { + auto utf8Reason = reason.toUtf8(); + g_task_return_new_error( + this->task, + POLKIT_ERROR, + POLKIT_ERROR_CANCELLED, + "%s", + utf8Reason.constData() + ); +} +// NOLINTEND(readability-make-member-function-const) +} // namespace qs::service::polkit + +// NOLINTEND(readability-identifier-naming,misc-use-anonymous-namespace) diff --git a/src/services/polkit/listener.hpp b/src/services/polkit/listener.hpp new file mode 100644 index 0000000..996fa23 --- /dev/null +++ b/src/services/polkit/listener.hpp @@ -0,0 +1,75 @@ +#pragma once + +#include + +#define POLKIT_AGENT_I_KNOW_API_IS_SUBJECT_TO_CHANGE +// This causes a problem with variables of the name. +#undef signals + +#include +#include + +#define signals Q_SIGNALS + +#include "gobjectref.hpp" + +namespace qs::service::polkit { +class ListenerCb; +//! All state that comes in from PolKit about an authentication request. +struct AuthRequest { + //! The action ID that this session is for. + QString actionId; + //! Message to present to the user. + QString message; + //! Icon name according to the FreeDesktop specification. May be empty. + QString iconName; + // Details intentionally omitted because nothing seems to use them. + QString cookie; + //! List of users/groups that can be used for authentication. + std::vector> identities; + + //! Implementation detail to mark authentication done. + GTask* task; + //! Implementation detail for requests cancelled by agent. + GCancellable* cancellable; + //! Callback handler ID for the cancellable. + gulong handlerId; + //! Callbacks for the listener + ListenerCb* cb; + + void complete(); + void cancel(const QString& reason); +}; + +//! Callback interface for PolkitAgent listener events. +class ListenerCb { +public: + ListenerCb() = default; + virtual ~ListenerCb() = default; + Q_DISABLE_COPY_MOVE(ListenerCb); + + //! Called when the agent registration is complete. + virtual void registerComplete(bool success) = 0; + //! Called when an authentication request is initiated by PolKit. + virtual void initiateAuthentication(AuthRequest* request) = 0; + //! Called when an authentication request is cancelled by PolKit before completion. + virtual void cancelAuthentication(AuthRequest* request) = 0; +}; +} // namespace qs::service::polkit + +G_BEGIN_DECLS + +// This is GObject code. By using their naming conventions, we clearly mark it +// as such for the rest of the project. +// NOLINTBEGIN(readability-identifier-naming) + +#define QS_TYPE_POLKIT_AGENT (qs_polkit_agent_get_type()) +G_DECLARE_FINAL_TYPE(QsPolkitAgent, qs_polkit_agent, QS, POLKIT_AGENT, PolkitAgentListener) + +QsPolkitAgent* qs_polkit_agent_new(qs::service::polkit::ListenerCb* cb); +void qs_polkit_agent_register(QsPolkitAgent* agent, const char* path); +void qs_polkit_agent_unregister(QsPolkitAgent* agent); + +// NOLINTEND(readability-identifier-naming) + +G_END_DECLS diff --git a/src/services/polkit/module.md b/src/services/polkit/module.md new file mode 100644 index 0000000..b306ecb --- /dev/null +++ b/src/services/polkit/module.md @@ -0,0 +1,52 @@ +name = "Quickshell.Services.Polkit" +description = "Polkit Agent" +headers = [ + "agentimpl.hpp", + "flow.hpp", + "identity.hpp", + "listener.hpp", + "qml.hpp", + "session.hpp", +] +----- +## Purpose of a Polkit Agent + +PolKit is a system for privileged applications to query if a user is permitted to execute an action. +You have probably seen it in the form of a "Please enter your password to continue with X" dialog box before. +This dialog box is presented by your *PolKit agent*, it is a process running as your user that accepts authentication requests from the *daemon* and presents them to you to accept or deny. + +This service enables writing a PolKit agent in Quickshell. + +## Implementing a Polkit Agent + +The backend logic of communicating with the daemon is handled by the @@Quickshell.Services.Polkit.PolkitAgent object. +It exposes incoming requests via @@Quickshell.Services.Polkit.PolkitAgent.flow and provides appropriate signals. + +### Flow of an authentication request + +Incoming authentication requests are queued in the order that they arrive. +If none is queued, a request starts processing right away. +Otherwise, it will wait until prior requests are done. + +A request starts by emitting the @@Quickshell.Services.Polkit.PolkitAgent.authenticationRequestStarted signal. +At this point, information like the action to be performed and permitted users that can authenticate is available. + +An authentication *session* for the request is immediately started, which internally starts a PAM conversation that is likely to prompt for user input. +* Additional prompts may be shared with the user by way of the @@Quickshell.Services.Polkit.AuthFlow.supplementaryMessageChanged / @@Quickshell.Services.Polkit.AuthFlow.supplementaryIsErrorChanged signals and the @@Quickshell.Services.Polkit.AuthFlow.supplementaryMessage and @@Quickshell.Services.Polkit.AuthFlow.supplementaryIsError properties. A common message might be 'Please input your password'. +* An input request is forwarded via the @@Quickshell.Services.Polkit.AuthFlow.isResponseRequiredChanged / @@Quickshell.Services.Polkit.AuthFlow.inputPromptChanged / @@Quickshell.Services.Polkit.AuthFlow.responseVisibleChanged signals and the corresponding properties. Note that the request specifies whether the text box should show the typed input on screen or replace it with placeholders. + +User replies can be submitted via the @@Quickshell.Services.Polkit.AuthFlow.submit method. +A conversation can take multiple turns, for example if second factors are involved. + +If authentication fails, we automatically create a fresh session so the user can try again. +The @@Quickshell.Services.Polkit.AuthFlow.authenticationFailed signal is emitted in this case. + +If authentication is successful, you receive the @@Quickshell.Services.Polkit.AuthFlow.authenticationSucceeeded signal. At this point, the dialog can be closed. +If additional requests are queued, you will receive the @@Quickshell.Services.Polkit.PolkitAgent.authenticationRequestStarted signal again. + +#### Cancelled requests + +Requests may either be canceled by the user or the PolKit daemon. +In this case, we clean up any state and proceed to the next request, if any. + +If the request was cancelled by the daemon and not the user, you also receive the @@Quickshell.Services.Polkit.AuthFlow.authenticationRequestCancelled signal. diff --git a/src/services/polkit/qml.cpp b/src/services/polkit/qml.cpp new file mode 100644 index 0000000..9a08e5d --- /dev/null +++ b/src/services/polkit/qml.cpp @@ -0,0 +1,35 @@ +#include "qml.hpp" + +#include +#include +#include + +#include "../../core/logcat.hpp" +#include "agentimpl.hpp" + +namespace { +QS_LOGGING_CATEGORY(logPolkit, "quickshell.service.polkit", QtWarningMsg); +} + +namespace qs::service::polkit { +PolkitAgent::~PolkitAgent() { PolkitAgentImpl::onEndOfQmlAgent(this); }; + +void PolkitAgent::componentComplete() { + if (this->mPath.isEmpty()) this->mPath = "/org/quickshell/PolkitAgent"; + + auto* impl = PolkitAgentImpl::tryTakeoverOrCreate(this); + if (impl == nullptr) return; + + this->bFlow.setBinding([impl]() { return impl->activeFlow().value(); }); + this->bIsActive.setBinding([impl]() { return impl->activeFlow().value() != nullptr; }); + this->bIsRegistered.setBinding([impl]() { return impl->isRegistered().value(); }); +} + +void PolkitAgent::setPath(const QString& path) { + if (this->mPath.isEmpty()) { + this->mPath = path; + } else if (this->mPath != path) { + qCWarning(logPolkit) << "cannot change path after it has been set."; + } +} +} // namespace qs::service::polkit diff --git a/src/services/polkit/qml.hpp b/src/services/polkit/qml.hpp new file mode 100644 index 0000000..5343bcd --- /dev/null +++ b/src/services/polkit/qml.hpp @@ -0,0 +1,84 @@ +#pragma once + +#include + +#include +#include +#include +#include +#include + +#include "flow.hpp" + +// The reserved identifier is exactly the struct I mean. +using PolkitIdentity = struct _PolkitIdentity; // NOLINT(bugprone-reserved-identifier) +using QsPolkitAgent = struct _QsPolkitAgent; + +namespace qs::service::polkit { + +struct AuthRequest; +class Session; +class Identity; +class AuthFlow; + +//! Contains interface to instantiate a PolKit agent listener. +class PolkitAgent + : public QObject + , public QQmlParserStatus { + Q_OBJECT; + QML_ELEMENT; + Q_INTERFACES(QQmlParserStatus); + Q_DISABLE_COPY_MOVE(PolkitAgent); + + /// The D-Bus path that this agent listener will use. + /// + /// If not set, a default of /org/quickshell/Polkit will be used. + Q_PROPERTY(QString path READ path WRITE setPath); + + /// Indicates whether the agent registered successfully and is in use. + Q_PROPERTY(bool isRegistered READ default NOTIFY isRegisteredChanged BINDABLE isRegistered); + + /// Indicates an ongoing authentication request. + /// + /// If this is true, other properties such as @@message and @@iconName will + /// also be populated with relevant information. + Q_PROPERTY(bool isActive READ default NOTIFY isActiveChanged BINDABLE isActive); + + /// The current authentication state if an authentication request is active. + /// + /// Null when no authentication request is active. + Q_PROPERTY(AuthFlow* flow READ default NOTIFY flowChanged BINDABLE flow); + +public: + explicit PolkitAgent(QObject* parent = nullptr): QObject(parent) {}; + ~PolkitAgent() override; + + void classBegin() override {}; + void componentComplete() override; + + [[nodiscard]] QString path() const { return this->mPath; }; + void setPath(const QString& path); + + [[nodiscard]] QBindable flow() { return &this->bFlow; }; + [[nodiscard]] QBindable isActive() { return &this->bIsActive; }; + [[nodiscard]] QBindable isRegistered() { return &this->bIsRegistered; }; + +signals: + /// Emitted when an application makes a request that requires authentication. + /// + /// At this point, @@state will be populated with relevant information. + /// Note that signals for conversation outcome are emitted from the @@AuthFlow instance. + void authenticationRequestStarted(); + + void isRegisteredChanged(); + void isActiveChanged(); + void flowChanged(); + +private: + QString mPath = ""; + + Q_OBJECT_BINDABLE_PROPERTY(PolkitAgent, AuthFlow*, bFlow, &PolkitAgent::flowChanged); + Q_OBJECT_BINDABLE_PROPERTY(PolkitAgent, bool, bIsActive, &PolkitAgent::isActiveChanged); + Q_OBJECT_BINDABLE_PROPERTY(PolkitAgent, bool, bIsRegistered, &PolkitAgent::isRegisteredChanged); +}; +} // namespace qs::service::polkit diff --git a/src/services/polkit/session.cpp b/src/services/polkit/session.cpp new file mode 100644 index 0000000..71def68 --- /dev/null +++ b/src/services/polkit/session.cpp @@ -0,0 +1,68 @@ +#include "session.hpp" + +#include +#include +#include +#include + +#define POLKIT_AGENT_I_KNOW_API_IS_SUBJECT_TO_CHANGE +// This causes a problem with variables of the name. +#undef signals +#include +#define signals Q_SIGNALS + +namespace qs::service::polkit { + +namespace { +void completedCb(PolkitAgentSession* /*session*/, gboolean gainedAuthorization, gpointer userData) { + auto* self = static_cast(userData); + emit self->completed(gainedAuthorization); +} + +void requestCb( + PolkitAgentSession* /*session*/, + const char* message, + gboolean echo, + gpointer userData +) { + auto* self = static_cast(userData); + emit self->request(QString::fromUtf8(message), echo); +} + +void showErrorCb(PolkitAgentSession* /*session*/, const char* message, gpointer userData) { + auto* self = static_cast(userData); + emit self->showError(QString::fromUtf8(message)); +} + +void showInfoCb(PolkitAgentSession* /*session*/, const char* message, gpointer userData) { + auto* self = static_cast(userData); + emit self->showInfo(QString::fromUtf8(message)); +} +} // namespace + +Session::Session(PolkitIdentity* identity, const QString& cookie, QObject* parent) + : QObject(parent) { + this->session = polkit_agent_session_new(identity, cookie.toUtf8().constData()); + + g_signal_connect(G_OBJECT(this->session), "completed", G_CALLBACK(completedCb), this); + g_signal_connect(G_OBJECT(this->session), "request", G_CALLBACK(requestCb), this); + g_signal_connect(G_OBJECT(this->session), "show-error", G_CALLBACK(showErrorCb), this); + g_signal_connect(G_OBJECT(this->session), "show-info", G_CALLBACK(showInfoCb), this); +} + +Session::~Session() { + // Signals do not need to be disconnected explicitly. This happens during + // destruction of the gobject. Since we own the session object, we can be + // sure it is being destroyed after the unref. + g_object_unref(this->session); +} + +void Session::initiate() { polkit_agent_session_initiate(this->session); } + +void Session::cancel() { polkit_agent_session_cancel(this->session); } + +void Session::respond(const QString& response) { + polkit_agent_session_response(this->session, response.toUtf8().constData()); +} + +} // namespace qs::service::polkit diff --git a/src/services/polkit/session.hpp b/src/services/polkit/session.hpp new file mode 100644 index 0000000..29331b1 --- /dev/null +++ b/src/services/polkit/session.hpp @@ -0,0 +1,52 @@ +#pragma once + +#include + +// _PolkitIdentity and _PolkitAgentSession are considered reserved identifiers, +// but I am specifically forward declaring those reserved names. + +// NOLINTBEGIN(bugprone-reserved-identifier) +using PolkitIdentity = struct _PolkitIdentity; +using PolkitAgentSession = struct _PolkitAgentSession; +// NOLINTEND(bugprone-reserved-identifier) + +namespace qs::service::polkit { +//! Represents an authentication session for a specific identity. +class Session: public QObject { + Q_OBJECT; + Q_DISABLE_COPY_MOVE(Session); + +public: + explicit Session(PolkitIdentity* identity, const QString& cookie, QObject* parent = nullptr); + ~Session() override; + + /// Call this after connecting to the relevant signals. + void initiate(); + /// Call this to abort a running authentication session. + void cancel(); + /// Provide a response to an input request. + void respond(const QString& response); + +Q_SIGNALS: + /// Emitted when the session wants to request input from the user. + /// + /// The message is a prompt to present to the user. + /// If echo is false, the user's response should not be displayed (e.g. for passwords). + void request(const QString& message, bool echo); + + /// Emitted when the authentication session completes. + /// + /// If success is true, authentication was successful. + /// Otherwise it failed (e.g. wrong password). + void completed(bool success); + + /// Emitted when an error message should be shown to the user. + void showError(const QString& message); + + /// Emitted when an informational message should be shown to the user. + void showInfo(const QString& message); + +private: + PolkitAgentSession* session = nullptr; +}; +} // namespace qs::service::polkit diff --git a/src/services/polkit/test/manual/agent.qml b/src/services/polkit/test/manual/agent.qml new file mode 100644 index 0000000..4588e4b --- /dev/null +++ b/src/services/polkit/test/manual/agent.qml @@ -0,0 +1,97 @@ +import Quickshell +import Quickshell.Services.Polkit +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls + +Scope { + id: root + + FloatingWindow { + title: "Authentication Required" + + visible: polkitAgent.isActive + color: contentItem.palette.window + + ColumnLayout { + id: contentColumn + anchors.fill: parent + anchors.margins: 18 + spacing: 12 + + Item { Layout.fillHeight: true } + + Label { + Layout.fillWidth: true + text: polkitAgent.flow?.message || "" + wrapMode: Text.Wrap + font.bold: true + } + + Label { + Layout.fillWidth: true + text: polkitAgent.flow?.supplementaryMessage || "" + wrapMode: Text.Wrap + opacity: 0.8 + } + + Label { + Layout.fillWidth: true + text: polkitAgent.flow?.inputPrompt || "" + wrapMode: Text.Wrap + } + + Label { + text: "Authentication failed, try again" + color: "red" + visible: polkitAgent.flow?.failed + } + + TextField { + id: passwordInput + echoMode: polkitAgent.flow?.responseVisible + ? TextInput.Normal : TextInput.Password + selectByMouse: true + Layout.fillWidth: true + onAccepted: okButton.clicked() + } + + RowLayout { + spacing: 8 + Button { + id: okButton + text: "OK" + enabled: passwordInput.text.length > 0 || !!polkitAgent.flow?.isResponseRequired + onClicked: { + polkitAgent.flow.submit(passwordInput.text) + passwordInput.text = "" + passwordInput.forceActiveFocus() + } + } + Button { + text: "Cancel" + visible: polkitAgent.isActive + onClicked: { + polkitAgent.flow.cancelAuthenticationRequest() + passwordInput.text = "" + } + } + } + + Item { Layout.fillHeight: true } + } + + Connections { + target: polkitAgent.flow + function onIsResponseRequiredChanged() { + passwordInput.text = "" + if (polkitAgent.flow.isResponseRequired) + passwordInput.forceActiveFocus() + } + } + } + + PolkitAgent { + id: polkitAgent + } +} diff --git a/src/services/status_notifier/item.cpp b/src/services/status_notifier/item.cpp index 4632995..17404e1 100644 --- a/src/services/status_notifier/item.cpp +++ b/src/services/status_notifier/item.cpp @@ -16,7 +16,6 @@ #include #include #include -#include #include #include @@ -163,6 +162,10 @@ QPixmap StatusNotifierItem::createPixmap(const QSize& size) const { } else { const auto* icon = closestPixmap(size, this->bAttentionIconPixmaps.value()); + if (icon == nullptr) { + icon = closestPixmap(size, this->bIconPixmaps.value()); + } + if (icon != nullptr) { const auto image = icon->createImage().scaled(size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation); @@ -218,9 +221,14 @@ void StatusNotifierItem::activate() { const QDBusPendingReply<> reply = *call; if (reply.isError()) { - qCWarning(logStatusNotifierItem).noquote() - << "Error calling Activate method of StatusNotifierItem" << this->properties.toString(); - qCWarning(logStatusNotifierItem) << reply.error(); + if (reply.error().type() == QDBusError::UnknownMethod) { + qCDebug(logStatusNotifierItem) << "Tried to call Activate method of StatusNotifierItem" + << this->properties.toString() << "but it does not exist."; + } else { + qCWarning(logStatusNotifierItem).noquote() + << "Error calling Activate method of StatusNotifierItem" << this->properties.toString(); + qCWarning(logStatusNotifierItem) << reply.error(); + } } delete call; @@ -237,10 +245,16 @@ void StatusNotifierItem::secondaryActivate() { const QDBusPendingReply<> reply = *call; if (reply.isError()) { - qCWarning(logStatusNotifierItem).noquote() - << "Error calling SecondaryActivate method of StatusNotifierItem" - << this->properties.toString(); - qCWarning(logStatusNotifierItem) << reply.error(); + if (reply.error().type() == QDBusError::UnknownMethod) { + qCDebug(logStatusNotifierItem) + << "Tried to call SecondaryActivate method of StatusNotifierItem" + << this->properties.toString() << "but it does not exist."; + } else { + qCWarning(logStatusNotifierItem).noquote() + << "Error calling SecondaryActivate method of StatusNotifierItem" + << this->properties.toString(); + qCWarning(logStatusNotifierItem) << reply.error(); + } } delete call; diff --git a/src/services/status_notifier/item.hpp b/src/services/status_notifier/item.hpp index 60f3a98..2eff95d 100644 --- a/src/services/status_notifier/item.hpp +++ b/src/services/status_notifier/item.hpp @@ -126,13 +126,6 @@ class StatusNotifierItem: public QObject { public: explicit StatusNotifierItem(const QString& address, QObject* parent = nullptr); - [[nodiscard]] bool isValid() const; - [[nodiscard]] bool isReady() const; - [[nodiscard]] QBindable bindableIcon() const { return &this->bIcon; }; - [[nodiscard]] QPixmap createPixmap(const QSize& size) const; - - [[nodiscard]] dbus::dbusmenu::DBusMenuHandle* menuHandle(); - /// Primary activation action, generally triggered via a left click. Q_INVOKABLE void activate(); /// Secondary activation action, generally triggered via a middle click. @@ -142,14 +135,21 @@ public: /// Display a platform menu at the given location relative to the parent window. Q_INVOKABLE void display(QObject* parentWindow, qint32 relativeX, qint32 relativeY); - [[nodiscard]] QBindable bindableId() const { return &this->bId; }; - [[nodiscard]] QBindable bindableTitle() const { return &this->bTitle; }; - [[nodiscard]] QBindable bindableStatus() const { return &this->bStatus; }; - [[nodiscard]] QBindable bindableCategory() const { return &this->bCategory; }; - [[nodiscard]] QString tooltipTitle() const { return this->bTooltip.value().title; }; - [[nodiscard]] QString tooltipDescription() const { return this->bTooltip.value().description; }; - [[nodiscard]] QBindable bindableHasMenu() const { return &this->bHasMenu; }; - [[nodiscard]] QBindable bindableOnlyMenu() const { return &this->bIsMenu; }; + [[nodiscard]] bool isValid() const; + [[nodiscard]] bool isReady() const; + [[nodiscard]] QBindable bindableIcon() const { return &this->bIcon; } + [[nodiscard]] QPixmap createPixmap(const QSize& size) const; + + [[nodiscard]] dbus::dbusmenu::DBusMenuHandle* menuHandle(); + + [[nodiscard]] QBindable bindableId() const { return &this->bId; } + [[nodiscard]] QBindable bindableTitle() const { return &this->bTitle; } + [[nodiscard]] QBindable bindableStatus() const { return &this->bStatus; } + [[nodiscard]] QBindable bindableCategory() const { return &this->bCategory; } + [[nodiscard]] QString tooltipTitle() const { return this->bTooltip.value().title; } + [[nodiscard]] QString tooltipDescription() const { return this->bTooltip.value().description; } + [[nodiscard]] QBindable bindableHasMenu() const { return &this->bHasMenu; } + [[nodiscard]] QBindable bindableOnlyMenu() const { return &this->bIsMenu; } signals: void ready(); @@ -207,6 +207,8 @@ private: QS_BINDING_SUBSCRIBE_METHOD(StatusNotifierItem, bOverlayIconPixmaps, updatePixmapIndex, onValueChanged); QS_BINDING_SUBSCRIBE_METHOD(StatusNotifierItem, bAttentionIconPixmaps, updatePixmapIndex, onValueChanged); QS_BINDING_SUBSCRIBE_METHOD(StatusNotifierItem, bMenuPath, onMenuPathChanged, onValueChanged); + QS_BINDING_SUBSCRIBE_METHOD(StatusNotifierItem, bTooltip, tooltipTitleChanged, onValueChanged); + QS_BINDING_SUBSCRIBE_METHOD(StatusNotifierItem, bTooltip, tooltipDescriptionChanged, onValueChanged); Q_OBJECT_BINDABLE_PROPERTY(StatusNotifierItem, quint32, pixmapIndex); Q_OBJECT_BINDABLE_PROPERTY(StatusNotifierItem, QString, bIcon, &StatusNotifierItem::iconChanged); diff --git a/src/services/upower/core.hpp b/src/services/upower/core.hpp index e2ed4f7..62fca1d 100644 --- a/src/services/upower/core.hpp +++ b/src/services/upower/core.hpp @@ -22,7 +22,7 @@ class UPower: public QObject { public: [[nodiscard]] UPowerDevice* displayDevice(); [[nodiscard]] ObjectModel* devices(); - [[nodiscard]] QBindable bindableOnBattery() const { return &this->bOnBattery; }; + [[nodiscard]] QBindable bindableOnBattery() const { return &this->bOnBattery; } static UPower* instance(); diff --git a/src/services/upower/device.cpp b/src/services/upower/device.cpp index 2492b1f..63382ad 100644 --- a/src/services/upower/device.cpp +++ b/src/services/upower/device.cpp @@ -73,7 +73,7 @@ UPowerDevice::UPowerDevice(QObject* parent): QObject(parent) { return this->bType == UPowerDeviceType::Battery && this->bPowerSupply; }); - this->bHealthSupported.setBinding([this]() { return this->bHealthPercentage != 0; }); + this->bHealthSupported.setBinding([this]() { return this->bHealthPercentage.value() != 0; }); } void UPowerDevice::init(const QString& path) { @@ -101,7 +101,7 @@ QString UPowerDevice::address() const { return this->device ? this->device->serv QString UPowerDevice::path() const { return this->device ? this->device->path() : QString(); } void UPowerDevice::onGetAllFinished() { - qCDebug(logUPowerDevice) << "UPowerDevice" << device->path() << "ready."; + qCDebug(logUPowerDevice) << "UPowerDevice" << this->device->path() << "ready."; this->bReady = true; } @@ -126,8 +126,8 @@ DBusDataTransform::fromWire(quint32 wire) { ); } -DBusResult DBusDataTransform::fromWire(quint32 wire -) { +DBusResult +DBusDataTransform::fromWire(quint32 wire) { if (wire >= UPowerDeviceType::Unknown && wire <= UPowerDeviceType::BluetoothGeneric) { return DBusResult(static_cast(wire)); } diff --git a/src/services/upower/device.hpp b/src/services/upower/device.hpp index b2b5f02..a4fbe83 100644 --- a/src/services/upower/device.hpp +++ b/src/services/upower/device.hpp @@ -173,25 +173,25 @@ public: [[nodiscard]] QString address() const; [[nodiscard]] QString path() const; - [[nodiscard]] QBindable bindableType() const { return &this->bType; }; - [[nodiscard]] QBindable bindablePowerSupply() const { return &this->bPowerSupply; }; - [[nodiscard]] QBindable bindableEnergy() const { return &this->bEnergy; }; - [[nodiscard]] QBindable bindableEnergyCapacity() const { return &this->bEnergyCapacity; }; - [[nodiscard]] QBindable bindableChangeRate() const { return &this->bChangeRate; }; - [[nodiscard]] QBindable bindableTimeToEmpty() const { return &this->bTimeToEmpty; }; - [[nodiscard]] QBindable bindableTimeToFull() const { return &this->bTimeToFull; }; - [[nodiscard]] QBindable bindablePercentage() const { return &this->bPercentage; }; - [[nodiscard]] QBindable bindableIsPresent() const { return &this->bIsPresent; }; - [[nodiscard]] QBindable bindableState() const { return &this->bState; }; + [[nodiscard]] QBindable bindableType() const { return &this->bType; } + [[nodiscard]] QBindable bindablePowerSupply() const { return &this->bPowerSupply; } + [[nodiscard]] QBindable bindableEnergy() const { return &this->bEnergy; } + [[nodiscard]] QBindable bindableEnergyCapacity() const { return &this->bEnergyCapacity; } + [[nodiscard]] QBindable bindableChangeRate() const { return &this->bChangeRate; } + [[nodiscard]] QBindable bindableTimeToEmpty() const { return &this->bTimeToEmpty; } + [[nodiscard]] QBindable bindableTimeToFull() const { return &this->bTimeToFull; } + [[nodiscard]] QBindable bindablePercentage() const { return &this->bPercentage; } + [[nodiscard]] QBindable bindableIsPresent() const { return &this->bIsPresent; } + [[nodiscard]] QBindable bindableState() const { return &this->bState; } [[nodiscard]] QBindable bindableHealthPercentage() const { return &this->bHealthPercentage; - }; - [[nodiscard]] QBindable bindableHealthSupported() const { return &this->bHealthSupported; }; - [[nodiscard]] QBindable bindableIconName() const { return &this->bIconName; }; - [[nodiscard]] QBindable bindableIsLaptopBattery() const { return &this->bIsLaptopBattery; }; - [[nodiscard]] QBindable bindableNativePath() const { return &this->bNativePath; }; - [[nodiscard]] QBindable bindableModel() const { return &this->bModel; }; - [[nodiscard]] QBindable bindableReady() const { return &this->bReady; }; + } + [[nodiscard]] QBindable bindableHealthSupported() const { return &this->bHealthSupported; } + [[nodiscard]] QBindable bindableIconName() const { return &this->bIconName; } + [[nodiscard]] QBindable bindableIsLaptopBattery() const { return &this->bIsLaptopBattery; } + [[nodiscard]] QBindable bindableNativePath() const { return &this->bNativePath; } + [[nodiscard]] QBindable bindableModel() const { return &this->bModel; } + [[nodiscard]] QBindable bindableReady() const { return &this->bReady; } signals: QSDOC_HIDE void readyChanged(); diff --git a/src/services/upower/powerprofiles.cpp b/src/services/upower/powerprofiles.cpp index 4c40798..f59b871 100644 --- a/src/services/upower/powerprofiles.cpp +++ b/src/services/upower/powerprofiles.cpp @@ -10,7 +10,7 @@ #include #include #include -#include +#include #include "../../core/logcat.hpp" #include "../../dbus/bus.hpp" @@ -66,7 +66,8 @@ PowerProfiles::PowerProfiles() { auto bus = QDBusConnection::systemBus(); if (!bus.isConnected()) { - qCWarning(logPowerProfiles + qCWarning( + logPowerProfiles ) << "Could not connect to DBus. PowerProfiles services will not work."; } @@ -79,7 +80,8 @@ PowerProfiles::PowerProfiles() { ); if (!this->service->isValid()) { - qCDebug(logPowerProfiles + qCDebug( + logPowerProfiles ) << "PowerProfilesDaemon is not currently running, attempting to start it."; dbus::tryLaunchService(this, bus, "org.freedesktop.UPower.PowerProfiles", [this](bool success) { @@ -103,13 +105,15 @@ void PowerProfiles::init() { void PowerProfiles::setProfile(PowerProfile::Enum profile) { if (!this->properties.isConnected()) { - qCCritical(logPowerProfiles + qCCritical( + logPowerProfiles ) << "Cannot set power profile: power-profiles-daemon not accessible or not running"; return; } if (profile == PowerProfile::Performance && !this->bHasPerformanceProfile) { - qCCritical(logPowerProfiles + qCCritical( + logPowerProfiles ) << "Cannot request performance profile as it is not present for this device."; return; } else if (profile < PowerProfile::PowerSaver || profile > PowerProfile::Performance) { @@ -135,8 +139,9 @@ PowerProfilesQml::PowerProfilesQml(QObject* parent): QObject(parent) { return instance->bHasPerformanceProfile.value(); }); - this->bDegradationReason.setBinding([instance]() { return instance->bDegradationReason.value(); } - ); + this->bDegradationReason.setBinding([instance]() { + return instance->bDegradationReason.value(); + }); this->bHolds.setBinding([instance]() { return instance->bHolds.value(); }); } @@ -164,6 +169,7 @@ QString DBusDataTransform::toWire(Data data) { case PowerProfile::PowerSaver: return QStringLiteral("power-saver"); case PowerProfile::Balanced: return QStringLiteral("balanced"); case PowerProfile::Performance: return QStringLiteral("performance"); + default: qFatal() << "Attempted to convert invalid power profile" << data << "to wire format."; } } diff --git a/src/ui/reload_popup.cpp b/src/ui/reload_popup.cpp index 8e58dc9..f000374 100644 --- a/src/ui/reload_popup.cpp +++ b/src/ui/reload_popup.cpp @@ -25,7 +25,7 @@ ReloadPopup::ReloadPopup(QString instanceId, bool failed, QString errorString) this->popup = component.createWithInitialProperties({{"reloadInfo", QVariant::fromValue(this)}}); - if (!popup) { + if (!this->popup) { qCritical() << "Failed to open reload popup:" << component.errorString(); } diff --git a/src/wayland/CMakeLists.txt b/src/wayland/CMakeLists.txt index 1d6543e..db53f37 100644 --- a/src/wayland/CMakeLists.txt +++ b/src/wayland/CMakeLists.txt @@ -1,6 +1,6 @@ find_package(PkgConfig REQUIRED) find_package(WaylandScanner REQUIRED) -pkg_check_modules(wayland REQUIRED IMPORTED_TARGET wayland-client wayland-protocols) +pkg_check_modules(wayland REQUIRED IMPORTED_TARGET wayland-client wayland-protocols>=1.41) # wayland protocols @@ -12,13 +12,13 @@ if(NOT TARGET Qt6::qtwaylandscanner) message(FATAL_ERROR "qtwaylandscanner executable not found. Most likely there is an issue with your Qt installation.") endif() -execute_process( - COMMAND pkg-config --variable=pkgdatadir wayland-protocols - OUTPUT_VARIABLE WAYLAND_PROTOCOLS - OUTPUT_STRIP_TRAILING_WHITESPACE -) +pkg_get_variable(WAYLAND_PROTOCOLS wayland-protocols pkgdatadir) -message(STATUS "Found wayland-protocols at ${WAYLAND_PROTOCOLS_DIR}") +if(WAYLAND_PROTOCOLS) + message(STATUS "Found wayland protocols at ${WAYLAND_PROTOCOLS}") +else() + message(FATAL_ERROR "Could not find wayland protocols") +endif() qs_add_pchset(wayland-protocol DEPENDENCIES Qt::Core Qt::WaylandClient Qt::WaylandClientPrivate @@ -114,6 +114,17 @@ if (HYPRLAND) add_subdirectory(hyprland) endif() +add_subdirectory(idle_inhibit) +list(APPEND WAYLAND_MODULES Quickshell.Wayland._IdleInhibitor) + +add_subdirectory(idle_notify) +list(APPEND WAYLAND_MODULES Quickshell.Wayland._IdleNotify) + +add_subdirectory(shortcuts_inhibit) +list(APPEND WAYLAND_MODULES Quickshell.Wayland._ShortcutsInhibitor) + +add_subdirectory(windowmanager) + # widgets for qmenu target_link_libraries(quickshell-wayland PRIVATE Qt::Quick Qt::Widgets Qt::WaylandClient Qt::WaylandClientPrivate diff --git a/src/wayland/buffer/CMakeLists.txt b/src/wayland/buffer/CMakeLists.txt index f80c53a..15818fc 100644 --- a/src/wayland/buffer/CMakeLists.txt +++ b/src/wayland/buffer/CMakeLists.txt @@ -1,6 +1,8 @@ find_package(PkgConfig REQUIRED) pkg_check_modules(dmabuf-deps REQUIRED IMPORTED_TARGET libdrm gbm egl) +find_package(VulkanHeaders REQUIRED) + qt_add_library(quickshell-wayland-buffer STATIC manager.cpp dmabuf.cpp @@ -10,9 +12,10 @@ qt_add_library(quickshell-wayland-buffer STATIC wl_proto(wlp-linux-dmabuf linux-dmabuf-v1 "${WAYLAND_PROTOCOLS}/stable/linux-dmabuf") target_link_libraries(quickshell-wayland-buffer PRIVATE - Qt::Quick Qt::WaylandClient Qt::WaylandClientPrivate wayland-client + Qt::Quick Qt::QuickPrivate Qt::GuiPrivate Qt::WaylandClient Qt::WaylandClientPrivate wayland-client PkgConfig::dmabuf-deps wlp-linux-dmabuf + Vulkan::Headers ) qs_pch(quickshell-wayland-buffer SET large) diff --git a/src/wayland/buffer/dmabuf.cpp b/src/wayland/buffer/dmabuf.cpp index 4593389..ed9dbeb 100644 --- a/src/wayland/buffer/dmabuf.cpp +++ b/src/wayland/buffer/dmabuf.cpp @@ -1,7 +1,6 @@ #include "dmabuf.hpp" #include #include -#include #include #include #include @@ -15,6 +14,8 @@ #include #include #include +#include +#include #include #include #include @@ -25,12 +26,17 @@ #include #include #include +#include #include +#include +#include +#include #include #include #include #include #include +#include #include #include #include @@ -49,6 +55,36 @@ QS_LOGGING_CATEGORY(logDmabuf, "quickshell.wayland.buffer.dmabuf", QtWarningMsg) LinuxDmabufManager* MANAGER = nullptr; // NOLINT +VkFormat drmFormatToVkFormat(uint32_t drmFormat) { + // NOLINTBEGIN(bugprone-branch-clone): XRGB/ARGB intentionally map to the same VK format + switch (drmFormat) { + case DRM_FORMAT_ARGB8888: return VK_FORMAT_B8G8R8A8_UNORM; + case DRM_FORMAT_XRGB8888: return VK_FORMAT_B8G8R8A8_UNORM; + case DRM_FORMAT_ABGR8888: return VK_FORMAT_R8G8B8A8_UNORM; + case DRM_FORMAT_XBGR8888: return VK_FORMAT_R8G8B8A8_UNORM; + case DRM_FORMAT_ARGB2101010: return VK_FORMAT_A2R10G10B10_UNORM_PACK32; + case DRM_FORMAT_XRGB2101010: return VK_FORMAT_A2R10G10B10_UNORM_PACK32; + case DRM_FORMAT_ABGR2101010: return VK_FORMAT_A2B10G10R10_UNORM_PACK32; + case DRM_FORMAT_XBGR2101010: return VK_FORMAT_A2B10G10R10_UNORM_PACK32; + case DRM_FORMAT_ABGR16161616F: return VK_FORMAT_R16G16B16A16_SFLOAT; + case DRM_FORMAT_RGB565: return VK_FORMAT_R5G6B5_UNORM_PACK16; + case DRM_FORMAT_BGR565: return VK_FORMAT_B5G6R5_UNORM_PACK16; + default: return VK_FORMAT_UNDEFINED; + } + // NOLINTEND(bugprone-branch-clone) +} + +bool drmFormatHasAlpha(uint32_t drmFormat) { + switch (drmFormat) { + case DRM_FORMAT_ARGB8888: + case DRM_FORMAT_ABGR8888: + case DRM_FORMAT_ARGB2101010: + case DRM_FORMAT_ABGR2101010: + case DRM_FORMAT_ABGR16161616F: return true; + default: return false; + } +} + } // namespace QDebug& operator<<(QDebug& debug, const FourCCStr& fourcc) { @@ -77,30 +113,32 @@ QDebug& operator<<(QDebug& debug, const WlDmaBuffer* buffer) { } GbmDeviceHandle::~GbmDeviceHandle() { - if (device) { + if (this->device) { MANAGER->unrefGbmDevice(this->device); } } -// This will definitely backfire later +// Prefer ARGB over XRGB: XRGB has undefined alpha bytes which cause +// transparency artifacts on Vulkan (notably Intel GPUs) since Vulkan +// doesn't auto-fill alpha=1.0 for X formats like EGL does. void LinuxDmabufFormatSelection::ensureSorted() { if (this->sorted) return; auto beginIter = this->formats.begin(); - auto xrgbIter = std::ranges::find_if(this->formats, [](const auto& format) { - return format.first == DRM_FORMAT_XRGB8888; - }); - - if (xrgbIter != this->formats.end()) { - std::swap(*beginIter, *xrgbIter); - ++beginIter; - } - auto argbIter = std::ranges::find_if(this->formats, [](const auto& format) { return format.first == DRM_FORMAT_ARGB8888; }); - if (argbIter != this->formats.end()) std::swap(*beginIter, *argbIter); + if (argbIter != this->formats.end()) { + std::swap(*beginIter, *argbIter); + ++beginIter; + } + + auto xrgbIter = std::ranges::find_if(this->formats, [](const auto& format) { + return format.first == DRM_FORMAT_XRGB8888; + }); + + if (xrgbIter != this->formats.end()) std::swap(*beginIter, *xrgbIter); this->sorted = true; } @@ -414,7 +452,8 @@ WlBuffer* LinuxDmabufManager::createDmabuf( if (modifiers.modifiers.isEmpty()) { if (!modifiers.implicit) { - qCritical(logDmabuf + qCritical( + logDmabuf ) << "Failed to create gbm_bo: format supports no implicit OR explicit modifiers."; return nullptr; } @@ -522,7 +561,7 @@ WlDmaBuffer::~WlDmaBuffer() { bool WlDmaBuffer::isCompatible(const WlBufferRequest& request) const { if (request.width != this->width || request.height != this->height) return false; - auto matchingFormat = std::ranges::find_if(request.dmabuf.formats, [&](const auto& format) { + auto matchingFormat = std::ranges::find_if(request.dmabuf.formats, [this](const auto& format) { return format.format == this->format && (format.modifiers.isEmpty() || std::ranges::find(format.modifiers, this->modifier) != format.modifiers.end()); @@ -532,6 +571,15 @@ bool WlDmaBuffer::isCompatible(const WlBufferRequest& request) const { } WlBufferQSGTexture* WlDmaBuffer::createQsgTexture(QQuickWindow* window) const { + auto* ri = window->rendererInterface(); + if (ri && ri->graphicsApi() == QSGRendererInterface::Vulkan) { + return this->createQsgTextureVulkan(window); + } + + return this->createQsgTextureGl(window); +} + +WlBufferQSGTexture* WlDmaBuffer::createQsgTextureGl(QQuickWindow* window) const { static auto* glEGLImageTargetTexture2DOES = []() { auto* fn = reinterpret_cast( eglGetProcAddress("glEGLImageTargetTexture2DOES") @@ -662,6 +710,313 @@ WlBufferQSGTexture* WlDmaBuffer::createQsgTexture(QQuickWindow* window) const { return tex; } +WlBufferQSGTexture* WlDmaBuffer::createQsgTextureVulkan(QQuickWindow* window) const { + auto* ri = window->rendererInterface(); + auto* vkInst = window->vulkanInstance(); + + if (!vkInst) { + qCWarning(logDmabuf) << "Failed to create Vulkan QSG texture: no QVulkanInstance."; + return nullptr; + } + + auto* vkDevicePtr = + static_cast(ri->getResource(window, QSGRendererInterface::DeviceResource)); + auto* vkPhysDevicePtr = static_cast( + ri->getResource(window, QSGRendererInterface::PhysicalDeviceResource) + ); + + if (!vkDevicePtr || !vkPhysDevicePtr) { + qCWarning(logDmabuf) << "Failed to create Vulkan QSG texture: could not get Vulkan device."; + return nullptr; + } + + VkDevice device = *vkDevicePtr; + VkPhysicalDevice physDevice = *vkPhysDevicePtr; + + auto* devFuncs = vkInst->deviceFunctions(device); + auto* instFuncs = vkInst->functions(); + + if (!devFuncs || !instFuncs) { + qCWarning(logDmabuf) << "Failed to create Vulkan QSG texture: " + "could not get Vulkan functions."; + return nullptr; + } + + auto getMemoryFdPropertiesKHR = reinterpret_cast( + instFuncs->vkGetDeviceProcAddr(device, "vkGetMemoryFdPropertiesKHR") + ); + + if (!getMemoryFdPropertiesKHR) { + qCWarning(logDmabuf) << "Failed to create Vulkan QSG texture: " + "vkGetMemoryFdPropertiesKHR not available. " + "Missing VK_KHR_external_memory_fd extension."; + return nullptr; + } + + const VkFormat vkFormat = drmFormatToVkFormat(this->format); + if (vkFormat == VK_FORMAT_UNDEFINED) { + qCWarning(logDmabuf) << "Failed to create Vulkan QSG texture: unsupported DRM format" + << FourCCStr(this->format); + return nullptr; + } + + if (this->planeCount > 4) { + qCWarning(logDmabuf) << "Failed to create Vulkan QSG texture: too many planes" + << this->planeCount; + return nullptr; + } + + std::array planeLayouts = {}; + for (int i = 0; i < this->planeCount; ++i) { + planeLayouts[i].offset = this->planes[i].offset; // NOLINT + planeLayouts[i].rowPitch = this->planes[i].stride; // NOLINT + planeLayouts[i].size = 0; + planeLayouts[i].arrayPitch = 0; + planeLayouts[i].depthPitch = 0; + } + + const bool useModifier = this->modifier != DRM_FORMAT_MOD_INVALID; + + VkExternalMemoryImageCreateInfo externalInfo = {}; + externalInfo.sType = VK_STRUCTURE_TYPE_EXTERNAL_MEMORY_IMAGE_CREATE_INFO; + externalInfo.handleTypes = VK_EXTERNAL_MEMORY_HANDLE_TYPE_DMA_BUF_BIT_EXT; + + VkImageDrmFormatModifierExplicitCreateInfoEXT modifierInfo = {}; + modifierInfo.sType = VK_STRUCTURE_TYPE_IMAGE_DRM_FORMAT_MODIFIER_EXPLICIT_CREATE_INFO_EXT; + modifierInfo.drmFormatModifier = this->modifier; + modifierInfo.drmFormatModifierPlaneCount = static_cast(this->planeCount); + modifierInfo.pPlaneLayouts = planeLayouts.data(); + + if (useModifier) { + externalInfo.pNext = &modifierInfo; + } + + VkImageCreateInfo imageInfo = {}; + imageInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO; + imageInfo.pNext = &externalInfo; + imageInfo.imageType = VK_IMAGE_TYPE_2D; + imageInfo.format = vkFormat; + imageInfo.extent = {.width = this->width, .height = this->height, .depth = 1}; + imageInfo.mipLevels = 1; + imageInfo.arrayLayers = 1; + imageInfo.samples = VK_SAMPLE_COUNT_1_BIT; + imageInfo.tiling = useModifier ? VK_IMAGE_TILING_DRM_FORMAT_MODIFIER_EXT : VK_IMAGE_TILING_LINEAR; + imageInfo.usage = VK_IMAGE_USAGE_SAMPLED_BIT; + imageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE; + imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; + + VkImage image = VK_NULL_HANDLE; + VkResult result = devFuncs->vkCreateImage(device, &imageInfo, nullptr, &image); + if (result != VK_SUCCESS) { + qCWarning(logDmabuf) << "Failed to create VkImage for DMA-BUF import, result:" << result; + return nullptr; + } + + VkDeviceMemory memory = VK_NULL_HANDLE; + + // dup() is required because vkAllocateMemory with VK_EXTERNAL_MEMORY_HANDLE_TYPE_DMA_BUF_BIT_EXT + // takes ownership of the fd on succcess. Without dup, WlDmaBuffer would double-close. + const int dupFd = + dup(this->planes[0].fd); // NOLINT(cppcoreguidelines-pro-bounds-pointer-arithmetic) + if (dupFd < 0) { + qCWarning(logDmabuf) << "Failed to dup() fd for DMA-BUF import"; + goto cleanup_fail; // NOLINT + } + + { + VkMemoryRequirements memReqs = {}; + devFuncs->vkGetImageMemoryRequirements(device, image, &memReqs); + + VkMemoryFdPropertiesKHR fdProps = {}; + fdProps.sType = VK_STRUCTURE_TYPE_MEMORY_FD_PROPERTIES_KHR; + + result = getMemoryFdPropertiesKHR( + device, + VK_EXTERNAL_MEMORY_HANDLE_TYPE_DMA_BUF_BIT_EXT, + dupFd, + &fdProps + ); + + if (result != VK_SUCCESS) { + close(dupFd); + qCWarning(logDmabuf) << "vkGetMemoryFdPropertiesKHR failed, result:" << result; + goto cleanup_fail; // NOLINT + } + + const uint32_t memTypeBits = memReqs.memoryTypeBits & fdProps.memoryTypeBits; + + VkPhysicalDeviceMemoryProperties memProps = {}; + instFuncs->vkGetPhysicalDeviceMemoryProperties(physDevice, &memProps); + + uint32_t memTypeIndex = UINT32_MAX; + for (uint32_t j = 0; j < memProps.memoryTypeCount; ++j) { + if (memTypeBits & (1u << j)) { + memTypeIndex = j; + break; + } + } + + if (memTypeIndex == UINT32_MAX) { + close(dupFd); + qCWarning(logDmabuf) << "No compatible memory type for DMA-BUF import"; + goto cleanup_fail; // NOLINT + } + + VkImportMemoryFdInfoKHR importInfo = {}; + importInfo.sType = VK_STRUCTURE_TYPE_IMPORT_MEMORY_FD_INFO_KHR; + importInfo.handleType = VK_EXTERNAL_MEMORY_HANDLE_TYPE_DMA_BUF_BIT_EXT; + importInfo.fd = dupFd; + + VkMemoryDedicatedAllocateInfo dedicatedInfo = {}; + dedicatedInfo.sType = VK_STRUCTURE_TYPE_MEMORY_DEDICATED_ALLOCATE_INFO; + dedicatedInfo.image = image; + dedicatedInfo.pNext = &importInfo; + + VkMemoryAllocateInfo allocInfo = {}; + allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO; + allocInfo.pNext = &dedicatedInfo; + allocInfo.allocationSize = memReqs.size; + allocInfo.memoryTypeIndex = memTypeIndex; + + result = devFuncs->vkAllocateMemory(device, &allocInfo, nullptr, &memory); + if (result != VK_SUCCESS) { + close(dupFd); + qCWarning(logDmabuf) << "vkAllocateMemory failed, result:" << result; + goto cleanup_fail; // NOLINT + } + + result = devFuncs->vkBindImageMemory(device, image, memory, 0); + if (result != VK_SUCCESS) { + qCWarning(logDmabuf) << "vkBindImageMemory failed, result:" << result; + goto cleanup_fail; // NOLINT + } + } + + { + // acquire the DMA-BUF from the foreign (compositor) queue and transition + // to shader-read layout. oldLayout must be GENERAL (not UNDEFINED) to + // preserve the DMA-BUF contents written by the external producer. Hopefully. + window->beginExternalCommands(); + + auto* cmdBufPtr = static_cast( + ri->getResource(window, QSGRendererInterface::CommandListResource) + ); + + if (cmdBufPtr && *cmdBufPtr) { + VkCommandBuffer cmdBuf = *cmdBufPtr; + + // find the graphics queue family index for the ownrship transfer. + uint32_t graphicsQueueFamily = 0; + uint32_t queueFamilyCount = 0; + instFuncs->vkGetPhysicalDeviceQueueFamilyProperties(physDevice, &queueFamilyCount, nullptr); + std::vector queueFamilies(queueFamilyCount); + instFuncs->vkGetPhysicalDeviceQueueFamilyProperties( + physDevice, + &queueFamilyCount, + queueFamilies.data() + ); + for (uint32_t i = 0; i < queueFamilyCount; ++i) { + if (queueFamilies[i].queueFlags & VK_QUEUE_GRAPHICS_BIT) { + graphicsQueueFamily = i; + break; + } + } + + VkImageMemoryBarrier barrier = {}; + barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER; + barrier.oldLayout = VK_IMAGE_LAYOUT_GENERAL; + barrier.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; + barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_FOREIGN_EXT; + barrier.dstQueueFamilyIndex = graphicsQueueFamily; + barrier.image = image; + barrier.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + barrier.subresourceRange.baseMipLevel = 0; + barrier.subresourceRange.levelCount = 1; + barrier.subresourceRange.baseArrayLayer = 0; + barrier.subresourceRange.layerCount = 1; + barrier.srcAccessMask = 0; + barrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT; + + devFuncs->vkCmdPipelineBarrier( + cmdBuf, + VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, + VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, + 0, + 0, + nullptr, + 0, + nullptr, + 1, + &barrier + ); + } + + window->endExternalCommands(); + + auto* qsgTexture = QQuickWindowPrivate::get(window)->createTextureFromNativeTexture( + reinterpret_cast(image), + VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, + static_cast(vkFormat), + QSize(static_cast(this->width), static_cast(this->height)), + {} + ); + + // For opaque DRM formats (XRGB, XBGR, etc.), the alpha bytes are underfined. + // EGL silently forces alpha=1.0 for these, but Vulkan doesn't. Replace Qt's + // default identity-swizzle VkImageView with one that maps alpha to ONE. + if (!drmFormatHasAlpha(this->format)) { + auto* vkTexture = static_cast(qsgTexture->rhiTexture()); // NOLINT + + devFuncs->vkDestroyImageView(device, vkTexture->imageView, nullptr); + + VkImageViewCreateInfo viewInfo = {}; + viewInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO; + viewInfo.image = image; + viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D; + viewInfo.format = vkFormat; + viewInfo.components.r = VK_COMPONENT_SWIZZLE_IDENTITY; + viewInfo.components.g = VK_COMPONENT_SWIZZLE_IDENTITY; + viewInfo.components.b = VK_COMPONENT_SWIZZLE_IDENTITY; + viewInfo.components.a = VK_COMPONENT_SWIZZLE_ONE; + viewInfo.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + viewInfo.subresourceRange.levelCount = 1; + viewInfo.subresourceRange.layerCount = 1; + + result = devFuncs->vkCreateImageView(device, &viewInfo, nullptr, &vkTexture->imageView); + if (result != VK_SUCCESS) { + qCWarning(logDmabuf) << "Failed to create alpha-swizzled VkImageView, result:" << result; + } + } + + auto* tex = new WlDmaBufferVulkanQSGTexture(devFuncs, device, image, memory, qsgTexture); + qCDebug(logDmabuf) << "Created WlDmaBufferVulkanQSGTexture" << tex << "from" << this; + return tex; + } + +cleanup_fail: + if (image != VK_NULL_HANDLE) { + devFuncs->vkDestroyImage(device, image, nullptr); + } + if (memory != VK_NULL_HANDLE) { + devFuncs->vkFreeMemory(device, memory, nullptr); + } + return nullptr; +} + +WlDmaBufferVulkanQSGTexture::~WlDmaBufferVulkanQSGTexture() { + delete this->qsgTexture; + + if (this->image != VK_NULL_HANDLE) { + this->devFuncs->vkDestroyImage(this->device, this->image, nullptr); + } + + if (this->memory != VK_NULL_HANDLE) { + this->devFuncs->vkFreeMemory(this->device, this->memory, nullptr); + } + + qCDebug(logDmabuf) << "WlDmaBufferVulkanQSGTexture" << this << "destroyed."; +} + WlDmaBufferQSGTexture::~WlDmaBufferQSGTexture() { auto* context = QOpenGLContext::currentContext(); auto* display = context->nativeInterface()->display(); diff --git a/src/wayland/buffer/dmabuf.hpp b/src/wayland/buffer/dmabuf.hpp index a05e82a..ffe5d02 100644 --- a/src/wayland/buffer/dmabuf.hpp +++ b/src/wayland/buffer/dmabuf.hpp @@ -1,5 +1,6 @@ #pragma once +#include #include #include @@ -12,9 +13,11 @@ #include #include #include +#include #include #include #include +#include #include #include #include @@ -40,7 +43,7 @@ public: '\0'} ) { for (auto i = 3; i != 0; i--) { - if (chars[i] == ' ') chars[i] = '\0'; + if (this->chars[i] == ' ') this->chars[i] = '\0'; else break; } } @@ -114,6 +117,36 @@ private: friend class WlDmaBuffer; }; +class WlDmaBufferVulkanQSGTexture: public WlBufferQSGTexture { +public: + ~WlDmaBufferVulkanQSGTexture() override; + Q_DISABLE_COPY_MOVE(WlDmaBufferVulkanQSGTexture); + + [[nodiscard]] QSGTexture* texture() const override { return this->qsgTexture; } + +private: + WlDmaBufferVulkanQSGTexture( + QVulkanDeviceFunctions* devFuncs, + VkDevice device, + VkImage image, + VkDeviceMemory memory, + QSGTexture* qsgTexture + ) + : devFuncs(devFuncs) + , device(device) + , image(image) + , memory(memory) + , qsgTexture(qsgTexture) {} + + QVulkanDeviceFunctions* devFuncs = nullptr; + VkDevice device = VK_NULL_HANDLE; + VkImage image = VK_NULL_HANDLE; + VkDeviceMemory memory = VK_NULL_HANDLE; + QSGTexture* qsgTexture = nullptr; + + friend class WlDmaBuffer; +}; + class WlDmaBuffer: public WlBuffer { public: ~WlDmaBuffer() override; @@ -151,6 +184,9 @@ private: friend class LinuxDmabufManager; friend QDebug& operator<<(QDebug& debug, const WlDmaBuffer* buffer); + + [[nodiscard]] WlBufferQSGTexture* createQsgTextureGl(QQuickWindow* window) const; + [[nodiscard]] WlBufferQSGTexture* createQsgTextureVulkan(QQuickWindow* window) const; }; QDebug& operator<<(QDebug& debug, const WlDmaBuffer* buffer); diff --git a/src/wayland/buffer/manager.cpp b/src/wayland/buffer/manager.cpp index c7448df..713752a 100644 --- a/src/wayland/buffer/manager.cpp +++ b/src/wayland/buffer/manager.cpp @@ -22,6 +22,8 @@ namespace { QS_LOGGING_CATEGORY(logBuffer, "quickshell.wayland.buffer", QtWarningMsg); } +void WlBufferRequest::reset() { *this = WlBufferRequest(); } + WlBuffer* WlBufferSwapchain::createBackbuffer(const WlBufferRequest& request, bool* newBuffer) { auto& buffer = this->presentSecondBuffer ? this->buffer1 : this->buffer2; @@ -53,7 +55,8 @@ bool WlBufferManager::isReady() const { return this->p->mReady; } << " (disabled: " << dmabufDisabled << ')'; for (const auto& [format, modifiers]: request.dmabuf.formats) { - qCDebug(logBuffer) << " Format" << dmabuf::FourCCStr(format); + qCDebug(logBuffer).nospace() << " Format " << dmabuf::FourCCStr(format) + << (modifiers.length() == 0 ? " (No modifiers specified)" : ""); for (const auto& modifier: modifiers) { qCDebug(logBuffer) << " Explicit Modifier" << dmabuf::FourCCModStr(modifier); @@ -66,6 +69,11 @@ bool WlBufferManager::isReady() const { return this->p->mReady; } qCDebug(logBuffer) << " Format" << format; } + if (request.width == 0 || request.height == 0) { + qCWarning(logBuffer) << "Cannot create zero-sized buffer."; + return nullptr; + } + if (!dmabufDisabled) { if (auto* buf = this->p->dmabuf.createDmabuf(request)) return buf; qCWarning(logBuffer) << "DMA buffer creation failed, falling back to SHM."; diff --git a/src/wayland/buffer/manager.hpp b/src/wayland/buffer/manager.hpp index b521e89..8abc218 100644 --- a/src/wayland/buffer/manager.hpp +++ b/src/wayland/buffer/manager.hpp @@ -68,6 +68,8 @@ struct WlBufferRequest { dev_t device = 0; StackList formats; } dmabuf; + + void reset(); }; class WlBuffer { diff --git a/src/wayland/hyprland/CMakeLists.txt b/src/wayland/hyprland/CMakeLists.txt index 570cbe5..66b32b6 100644 --- a/src/wayland/hyprland/CMakeLists.txt +++ b/src/wayland/hyprland/CMakeLists.txt @@ -30,6 +30,7 @@ qt_add_qml_module(quickshell-hyprland IMPORTS ${HYPRLAND_MODULES} ) +qs_add_module_deps_light(quickshell-io Quickshell) install_qml_module(quickshell-hyprland) # intentionally no pch as the module is empty diff --git a/src/wayland/hyprland/focus_grab/qml.hpp b/src/wayland/hyprland/focus_grab/qml.hpp index 4ba7227..705b0d3 100644 --- a/src/wayland/hyprland/focus_grab/qml.hpp +++ b/src/wayland/hyprland/focus_grab/qml.hpp @@ -56,6 +56,8 @@ class HyprlandFocusGrab : public QObject , public QQmlParserStatus { Q_OBJECT; + QML_ELEMENT; + Q_INTERFACES(QQmlParserStatus); /// If the focus grab is active. Defaults to false. /// /// When set to true, an input grab will be created for the listed windows. @@ -66,7 +68,6 @@ class HyprlandFocusGrab Q_PROPERTY(bool active READ isActive WRITE setActive NOTIFY activeChanged); /// The list of windows to whitelist for input. Q_PROPERTY(QList windows READ windows WRITE setWindows NOTIFY windowsChanged); - QML_ELEMENT; public: explicit HyprlandFocusGrab(QObject* parent = nullptr): QObject(parent) {} diff --git a/src/wayland/hyprland/ipc/connection.cpp b/src/wayland/hyprland/ipc/connection.cpp index 067b922..ad091a6 100644 --- a/src/wayland/hyprland/ipc/connection.cpp +++ b/src/wayland/hyprland/ipc/connection.cpp @@ -442,8 +442,8 @@ void HyprlandIpc::onEvent(HyprlandIpcEvent* event) { if (!ok) return; auto workspaceName = QString::fromUtf8(args.at(1)); - auto windowTitle = QString::fromUtf8(args.at(2)); - auto windowClass = QString::fromUtf8(args.at(3)); + auto windowClass = QString::fromUtf8(args.at(2)); + auto windowTitle = QString::fromUtf8(args.at(3)); auto* workspace = this->findWorkspaceByName(workspaceName, false); if (!workspace) { @@ -484,6 +484,7 @@ void HyprlandIpc::onEvent(HyprlandIpcEvent* event) { } auto* toplevel = *toplevelIter; + if (toplevel == this->bActiveToplevel.value()) this->bActiveToplevel = nullptr; auto index = toplevelIter - mList.begin(); this->mToplevels.removeAt(index); diff --git a/src/wayland/hyprland/ipc/qml.cpp b/src/wayland/hyprland/ipc/qml.cpp index 89eec9e..eb5fdc6 100644 --- a/src/wayland/hyprland/ipc/qml.cpp +++ b/src/wayland/hyprland/ipc/qml.cpp @@ -24,9 +24,9 @@ HyprlandIpcQml::HyprlandIpcQml() { QObject::connect( instance, - &HyprlandIpc::focusedMonitorChanged, + &HyprlandIpc::focusedWorkspaceChanged, this, - &HyprlandIpcQml::focusedMonitorChanged + &HyprlandIpcQml::focusedWorkspaceChanged ); QObject::connect( diff --git a/src/wayland/hyprland/ipc/workspace.cpp b/src/wayland/hyprland/ipc/workspace.cpp index d16c821..c5d63b0 100644 --- a/src/wayland/hyprland/ipc/workspace.cpp +++ b/src/wayland/hyprland/ipc/workspace.cpp @@ -151,7 +151,7 @@ void HyprlandWorkspace::clearUrgent() { } void HyprlandWorkspace::activate() { - this->ipc->dispatch(QString("workspace %1").arg(this->bId.value())); + this->ipc->dispatch(QString("workspace %1").arg(this->bName.value())); } } // namespace qs::hyprland::ipc diff --git a/src/wayland/hyprland/ipc/workspace.hpp b/src/wayland/hyprland/ipc/workspace.hpp index 957639a..a0b09cf 100644 --- a/src/wayland/hyprland/ipc/workspace.hpp +++ b/src/wayland/hyprland/ipc/workspace.hpp @@ -54,7 +54,7 @@ public: /// /// > [!NOTE] This is equivalent to running /// > ```qml - /// > HyprlandIpc.dispatch(`workspace ${workspace.id}`); + /// > HyprlandIpc.dispatch(`workspace ${workspace.name}`); /// > ``` Q_INVOKABLE void activate(); diff --git a/src/wayland/hyprland/surface/qml.cpp b/src/wayland/hyprland/surface/qml.cpp index b00ee33..c4f7d67 100644 --- a/src/wayland/hyprland/surface/qml.cpp +++ b/src/wayland/hyprland/surface/qml.cpp @@ -65,7 +65,8 @@ void HyprlandWindow::setOpacity(qreal opacity) { if (opacity == this->mOpacity) return; if (opacity < 0.0 || opacity > 1.0) { - qmlWarning(this + qmlWarning( + this ) << "Cannot set HyprlandWindow.opacity to a value larger than 1.0 or smaller than 0.0"; return; } diff --git a/src/wayland/hyprland/surface/surface.cpp b/src/wayland/hyprland/surface/surface.cpp index f49ab8f..774acd0 100644 --- a/src/wayland/hyprland/surface/surface.cpp +++ b/src/wayland/hyprland/surface/surface.cpp @@ -1,5 +1,4 @@ #include "surface.hpp" -#include #include #include diff --git a/src/wayland/idle_inhibit/CMakeLists.txt b/src/wayland/idle_inhibit/CMakeLists.txt new file mode 100644 index 0000000..eb346d6 --- /dev/null +++ b/src/wayland/idle_inhibit/CMakeLists.txt @@ -0,0 +1,25 @@ +qt_add_library(quickshell-wayland-idle-inhibit STATIC + proto.cpp + inhibitor.cpp +) + +qt_add_qml_module(quickshell-wayland-idle-inhibit + URI Quickshell.Wayland._IdleInhibitor + VERSION 0.1 + DEPENDENCIES QtQuick +) + +install_qml_module(quickshell-wayland-idle-inhibit) + +qs_add_module_deps_light(quickshell-wayland-idle-inhibit Quickshell) + +wl_proto(wlp-idle-inhibit idle-inhibit-unstable-v1 "${WAYLAND_PROTOCOLS}/unstable/idle-inhibit") + +target_link_libraries(quickshell-wayland-idle-inhibit PRIVATE + Qt::Quick Qt::WaylandClient Qt::WaylandClientPrivate wayland-client + wlp-idle-inhibit +) + +qs_module_pch(quickshell-wayland-idle-inhibit SET large) + +target_link_libraries(quickshell PRIVATE quickshell-wayland-idle-inhibitplugin) diff --git a/src/wayland/idle_inhibit/inhibitor.cpp b/src/wayland/idle_inhibit/inhibitor.cpp new file mode 100644 index 0000000..efeeae1 --- /dev/null +++ b/src/wayland/idle_inhibit/inhibitor.cpp @@ -0,0 +1,140 @@ +#include "inhibitor.hpp" + +#include +#include +#include +#include + +#include "../../window/proxywindow.hpp" +#include "../../window/windowinterface.hpp" +#include "proto.hpp" + +namespace qs::wayland::idle_inhibit { +using QtWaylandClient::QWaylandWindow; + +IdleInhibitor::IdleInhibitor() { + this->bBoundWindow.setBinding([this] { + return this->bEnabled ? this->bWindowObject.value() : nullptr; + }); +} + +IdleInhibitor::~IdleInhibitor() { delete this->inhibitor; } + +QObject* IdleInhibitor::window() const { return this->bWindowObject; } + +void IdleInhibitor::setWindow(QObject* window) { + if (window == this->bWindowObject) return; + + auto* proxyWindow = qobject_cast(window); + + if (proxyWindow == nullptr) { + if (auto* iface = qobject_cast(window)) { + proxyWindow = iface->proxyWindow(); + } + } + + this->bWindowObject = proxyWindow ? window : nullptr; +} + +void IdleInhibitor::boundWindowChanged() { + auto* window = this->bBoundWindow.value(); + auto* proxyWindow = qobject_cast(window); + + if (proxyWindow == nullptr) { + if (auto* iface = qobject_cast(window)) { + proxyWindow = iface->proxyWindow(); + } + } + + if (proxyWindow == this->proxyWindow) return; + + if (this->mWaylandWindow) { + QObject::disconnect(this->mWaylandWindow, nullptr, this, nullptr); + this->mWaylandWindow = nullptr; + this->onWaylandSurfaceDestroyed(); + } + + if (this->proxyWindow) { + QObject::disconnect(this->proxyWindow, nullptr, this, nullptr); + this->proxyWindow = nullptr; + } + + if (proxyWindow) { + this->proxyWindow = proxyWindow; + + QObject::connect(proxyWindow, &QObject::destroyed, this, &IdleInhibitor::onWindowDestroyed); + + QObject::connect( + proxyWindow, + &ProxyWindowBase::backerVisibilityChanged, + this, + &IdleInhibitor::onWindowVisibilityChanged + ); + + this->onWindowVisibilityChanged(); + } + + emit this->windowChanged(); +} + +void IdleInhibitor::onWindowDestroyed() { + this->proxyWindow = nullptr; + this->onWaylandSurfaceDestroyed(); + this->bWindowObject = nullptr; +} + +void IdleInhibitor::onWindowVisibilityChanged() { + if (!this->proxyWindow->isVisibleDirect()) return; + + auto* window = this->proxyWindow->backingWindow(); + if (!window->handle()) window->create(); + + auto* waylandWindow = dynamic_cast(window->handle()); + if (waylandWindow == this->mWaylandWindow) return; + this->mWaylandWindow = waylandWindow; + + QObject::connect( + waylandWindow, + &QObject::destroyed, + this, + &IdleInhibitor::onWaylandWindowDestroyed + ); + + QObject::connect( + waylandWindow, + &QWaylandWindow::surfaceCreated, + this, + &IdleInhibitor::onWaylandSurfaceCreated + ); + + QObject::connect( + waylandWindow, + &QWaylandWindow::surfaceDestroyed, + this, + &IdleInhibitor::onWaylandSurfaceDestroyed + ); + + if (waylandWindow->surface()) this->onWaylandSurfaceCreated(); +} + +void IdleInhibitor::onWaylandWindowDestroyed() { this->mWaylandWindow = nullptr; } + +void IdleInhibitor::onWaylandSurfaceCreated() { + auto* manager = impl::IdleInhibitManager::instance(); + + if (!manager) { + qWarning() << "Cannot enable idle inhibitor as idle-inhibit-unstable-v1 is not supported by " + "the current compositor."; + return; + } + + delete this->inhibitor; + this->inhibitor = manager->createIdleInhibitor(this->mWaylandWindow); +} + +void IdleInhibitor::onWaylandSurfaceDestroyed() { + delete this->inhibitor; + this->inhibitor = nullptr; +} + +} // namespace qs::wayland::idle_inhibit diff --git a/src/wayland/idle_inhibit/inhibitor.hpp b/src/wayland/idle_inhibit/inhibitor.hpp new file mode 100644 index 0000000..c1a3e95 --- /dev/null +++ b/src/wayland/idle_inhibit/inhibitor.hpp @@ -0,0 +1,75 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include "../../window/proxywindow.hpp" +#include "proto.hpp" + +namespace qs::wayland::idle_inhibit { + +///! Prevents a wayland session from idling +/// If an idle daemon is running, it may perform actions such as locking the screen +/// or putting the computer to sleep. +/// +/// An idle inhibitor prevents a wayland session from being marked as idle, if compositor +/// defined heuristics determine the window the inhibitor is attached to is important. +/// +/// A compositor will usually consider a @@Quickshell.PanelWindow or +/// a focused @@Quickshell.FloatingWindow to be important. +/// +/// > [!NOTE] Using an idle inhibitor requires the compositor support the [idle-inhibit-unstable-v1] protocol. +/// +/// [idle-inhibit-unstable-v1]: https://wayland.app/protocols/idle-inhibit-unstable-v1 +class IdleInhibitor: public QObject { + Q_OBJECT; + QML_ELEMENT; + // clang-format off + /// If the idle inhibitor should be enabled. Defaults to false. + Q_PROPERTY(bool enabled READ default WRITE default NOTIFY enabledChanged BINDABLE bindableEnabled); + /// The window to associate the idle inhibitor with. This may be used by the compositor + /// to determine if the inhibitor should be respected. + /// + /// Must be set to a non null value to enable the inhibitor. + Q_PROPERTY(QObject* window READ window WRITE setWindow NOTIFY windowChanged); + // clang-format on + +public: + IdleInhibitor(); + ~IdleInhibitor() override; + Q_DISABLE_COPY_MOVE(IdleInhibitor); + + [[nodiscard]] QObject* window() const; + void setWindow(QObject* window); + + [[nodiscard]] QBindable bindableEnabled() { return &this->bEnabled; } + +signals: + void enabledChanged(); + void windowChanged(); + +private slots: + void onWindowDestroyed(); + void onWindowVisibilityChanged(); + void onWaylandWindowDestroyed(); + void onWaylandSurfaceCreated(); + void onWaylandSurfaceDestroyed(); + +private: + void boundWindowChanged(); + ProxyWindowBase* proxyWindow = nullptr; + QtWaylandClient::QWaylandWindow* mWaylandWindow = nullptr; + + impl::IdleInhibitor* inhibitor = nullptr; + + // clang-format off + Q_OBJECT_BINDABLE_PROPERTY(IdleInhibitor, bool, bEnabled, &IdleInhibitor::enabledChanged); + Q_OBJECT_BINDABLE_PROPERTY(IdleInhibitor, QObject*, bWindowObject, &IdleInhibitor::windowChanged); + Q_OBJECT_BINDABLE_PROPERTY(IdleInhibitor, QObject*, bBoundWindow, &IdleInhibitor::boundWindowChanged); + // clang-format on +}; + +} // namespace qs::wayland::idle_inhibit diff --git a/src/wayland/idle_inhibit/proto.cpp b/src/wayland/idle_inhibit/proto.cpp new file mode 100644 index 0000000..25701a7 --- /dev/null +++ b/src/wayland/idle_inhibit/proto.cpp @@ -0,0 +1,34 @@ +#include "proto.hpp" + +#include +#include +#include +#include + +#include "../../core/logcat.hpp" + +namespace qs::wayland::idle_inhibit::impl { + +namespace { +QS_LOGGING_CATEGORY(logIdleInhibit, "quickshell.wayland.idle_inhibit", QtWarningMsg); +} + +IdleInhibitManager::IdleInhibitManager(): QWaylandClientExtensionTemplate(1) { this->initialize(); } + +IdleInhibitManager* IdleInhibitManager::instance() { + static auto* instance = new IdleInhibitManager(); // NOLINT + return instance->isInitialized() ? instance : nullptr; +} + +IdleInhibitor* IdleInhibitManager::createIdleInhibitor(QtWaylandClient::QWaylandWindow* surface) { + auto* inhibitor = new IdleInhibitor(this->create_inhibitor(surface->surface())); + qCDebug(logIdleInhibit) << "Created inhibitor" << inhibitor; + return inhibitor; +} + +IdleInhibitor::~IdleInhibitor() { + qCDebug(logIdleInhibit) << "Destroyed inhibitor" << this; + if (this->isInitialized()) this->destroy(); +} + +} // namespace qs::wayland::idle_inhibit::impl diff --git a/src/wayland/idle_inhibit/proto.hpp b/src/wayland/idle_inhibit/proto.hpp new file mode 100644 index 0000000..c797c33 --- /dev/null +++ b/src/wayland/idle_inhibit/proto.hpp @@ -0,0 +1,34 @@ +#pragma once + +#include +#include +#include +#include + +#include "wayland-idle-inhibit-unstable-v1-client-protocol.h" + +namespace qs::wayland::idle_inhibit::impl { + +class IdleInhibitor; + +class IdleInhibitManager + : public QWaylandClientExtensionTemplate + , public QtWayland::zwp_idle_inhibit_manager_v1 { +public: + explicit IdleInhibitManager(); + + IdleInhibitor* createIdleInhibitor(QtWaylandClient::QWaylandWindow* surface); + + static IdleInhibitManager* instance(); +}; + +class IdleInhibitor: public QtWayland::zwp_idle_inhibitor_v1 { +public: + explicit IdleInhibitor(::zwp_idle_inhibitor_v1* inhibitor) + : QtWayland::zwp_idle_inhibitor_v1(inhibitor) {} + + ~IdleInhibitor() override; + Q_DISABLE_COPY_MOVE(IdleInhibitor); +}; + +} // namespace qs::wayland::idle_inhibit::impl diff --git a/src/wayland/idle_inhibit/test/manual/idle_inhibit.qml b/src/wayland/idle_inhibit/test/manual/idle_inhibit.qml new file mode 100644 index 0000000..f80e647 --- /dev/null +++ b/src/wayland/idle_inhibit/test/manual/idle_inhibit.qml @@ -0,0 +1,20 @@ +import QtQuick +import QtQuick.Controls +import Quickshell +import Quickshell.Wayland + +FloatingWindow { + id: root + color: contentItem.palette.window + + CheckBox { + id: enableCb + anchors.centerIn: parent + text: "Enable Inhibitor" + } + + IdleInhibitor { + window: root + enabled: enableCb.checked + } +} diff --git a/src/wayland/idle_notify/CMakeLists.txt b/src/wayland/idle_notify/CMakeLists.txt new file mode 100644 index 0000000..889c7ce --- /dev/null +++ b/src/wayland/idle_notify/CMakeLists.txt @@ -0,0 +1,25 @@ +qt_add_library(quickshell-wayland-idle-notify STATIC + proto.cpp + monitor.cpp +) + +qt_add_qml_module(quickshell-wayland-idle-notify + URI Quickshell.Wayland._IdleNotify + VERSION 0.1 + DEPENDENCIES QtQuick +) + +install_qml_module(quickshell-wayland-idle-notify) + +qs_add_module_deps_light(quickshell-wayland-idle-notify Quickshell) + +wl_proto(wlp-idle-notify ext-idle-notify-v1 "${WAYLAND_PROTOCOLS}/staging/ext-idle-notify") + +target_link_libraries(quickshell-wayland-idle-notify PRIVATE + Qt::Quick Qt::WaylandClient Qt::WaylandClientPrivate wayland-client + wlp-idle-notify +) + +qs_module_pch(quickshell-wayland-idle-notify SET large) + +target_link_libraries(quickshell PRIVATE quickshell-wayland-idle-notifyplugin) diff --git a/src/wayland/idle_notify/monitor.cpp b/src/wayland/idle_notify/monitor.cpp new file mode 100644 index 0000000..3f496e4 --- /dev/null +++ b/src/wayland/idle_notify/monitor.cpp @@ -0,0 +1,52 @@ +#include "monitor.hpp" +#include + +#include +#include +#include + +#include "proto.hpp" + +namespace qs::wayland::idle_notify { + +IdleMonitor::~IdleMonitor() { delete this->bNotification.value(); } + +void IdleMonitor::onPostReload() { + this->bParams.setBinding([this] { + return Params { + .enabled = this->bEnabled.value(), + .timeout = this->bTimeout.value(), + .respectInhibitors = this->bRespectInhibitors.value() + }; + }); + + this->bIsIdle.setBinding([this] { + auto* notification = this->bNotification.value(); + return notification ? notification->bIsIdle.value() : false; + }); +} + +void IdleMonitor::updateNotification() { + auto* notification = this->bNotification.value(); + delete notification; + notification = nullptr; + + auto guard = qScopeGuard([&, this] { this->bNotification = notification; }); + + auto params = this->bParams.value(); + + if (params.enabled) { + auto* manager = impl::IdleNotificationManager::instance(); + + if (!manager) { + qWarning() << "Cannot create idle monitor as ext-idle-notify-v1 is not supported by the " + "current compositor."; + return; + } + + auto timeout = static_cast(std::max(0, static_cast(params.timeout * 1000))); + notification = manager->createIdleNotification(timeout, params.respectInhibitors); + } +} + +} // namespace qs::wayland::idle_notify diff --git a/src/wayland/idle_notify/monitor.hpp b/src/wayland/idle_notify/monitor.hpp new file mode 100644 index 0000000..25bd5a6 --- /dev/null +++ b/src/wayland/idle_notify/monitor.hpp @@ -0,0 +1,78 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "../../core/reload.hpp" +#include "proto.hpp" + +namespace qs::wayland::idle_notify { + +///! Provides a notification when a wayland session is makred idle +/// An idle monitor detects when the user stops providing input for a period of time. +/// +/// > [!NOTE] Using an idle monitor requires the compositor support the [ext-idle-notify-v1] protocol. +/// +/// [ext-idle-notify-v1]: https://wayland.app/protocols/ext-idle-notify-v1 +class IdleMonitor: public PostReloadHook { + Q_OBJECT; + QML_ELEMENT; + // clang-format off + /// If the idle monitor should be enabled. Defaults to true. + Q_PROPERTY(bool enabled READ isEnabled WRITE setEnabled NOTIFY enabledChanged); + /// The amount of time in seconds the idle monitor should wait before reporting an idle state. + /// + /// Defaults to zero, which reports idle status immediately. + Q_PROPERTY(qreal timeout READ default WRITE default NOTIFY timeoutChanged BINDABLE bindableTimeout); + /// When set to true, @@isIdle will depend on both user interaction and active idle inhibitors. + /// When false, the value will depend solely on user interaction. Defaults to true. + Q_PROPERTY(bool respectInhibitors READ default WRITE default NOTIFY respectInhibitorsChanged BINDABLE bindableRespectInhibitors); + /// This property is true if the user has been idle for at least @@timeout. + /// What is considered to be idle is influenced by @@respectInhibitors. + Q_PROPERTY(bool isIdle READ default NOTIFY isIdleChanged BINDABLE bindableIsIdle); + // clang-format on + +public: + IdleMonitor() = default; + ~IdleMonitor() override; + Q_DISABLE_COPY_MOVE(IdleMonitor); + + void onPostReload() override; + + [[nodiscard]] bool isEnabled() const { return this->bNotification.value(); } + void setEnabled(bool enabled) { this->bEnabled = enabled; } + + [[nodiscard]] QBindable bindableTimeout() { return &this->bTimeout; } + [[nodiscard]] QBindable bindableRespectInhibitors() { return &this->bRespectInhibitors; } + [[nodiscard]] QBindable bindableIsIdle() const { return &this->bIsIdle; } + +signals: + void enabledChanged(); + void timeoutChanged(); + void respectInhibitorsChanged(); + void isIdleChanged(); + +private: + void updateNotification(); + + struct Params { + bool enabled; + qreal timeout; + bool respectInhibitors; + }; + + // clang-format off + Q_OBJECT_BINDABLE_PROPERTY_WITH_ARGS(IdleMonitor, bool, bEnabled, true, &IdleMonitor::enabledChanged); + Q_OBJECT_BINDABLE_PROPERTY(IdleMonitor, qreal, bTimeout, &IdleMonitor::timeoutChanged); + Q_OBJECT_BINDABLE_PROPERTY_WITH_ARGS(IdleMonitor, bool, bRespectInhibitors, true, &IdleMonitor::respectInhibitorsChanged); + Q_OBJECT_BINDABLE_PROPERTY(IdleMonitor, Params, bParams, &IdleMonitor::updateNotification); + Q_OBJECT_BINDABLE_PROPERTY(IdleMonitor, impl::IdleNotification*, bNotification); + Q_OBJECT_BINDABLE_PROPERTY(IdleMonitor, bool, bIsIdle, &IdleMonitor::isIdleChanged); + // clang-format on +}; + +} // namespace qs::wayland::idle_notify diff --git a/src/wayland/idle_notify/proto.cpp b/src/wayland/idle_notify/proto.cpp new file mode 100644 index 0000000..9b3fa81 --- /dev/null +++ b/src/wayland/idle_notify/proto.cpp @@ -0,0 +1,75 @@ +#include "proto.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../../core/logcat.hpp" + +namespace qs::wayland::idle_notify { +QS_LOGGING_CATEGORY(logIdleNotify, "quickshell.wayland.idle_notify", QtWarningMsg); +} + +namespace qs::wayland::idle_notify::impl { + +IdleNotificationManager::IdleNotificationManager(): QWaylandClientExtensionTemplate(2) { + this->initialize(); +} + +IdleNotificationManager* IdleNotificationManager::instance() { + static auto* instance = new IdleNotificationManager(); // NOLINT + return instance->isInitialized() ? instance : nullptr; +} + +IdleNotification* +IdleNotificationManager::createIdleNotification(quint32 timeout, bool respectInhibitors) { + if (!respectInhibitors + && this->QtWayland::ext_idle_notifier_v1::version() + < EXT_IDLE_NOTIFIER_V1_GET_INPUT_IDLE_NOTIFICATION_SINCE_VERSION) + { + qCWarning(logIdleNotify) << "Cannot ignore inhibitors for new idle notifier: Compositor does " + "not support protocol version 2."; + + respectInhibitors = true; + } + + auto* display = QtWaylandClient::QWaylandIntegration::instance()->display(); + auto* inputDevice = display->lastInputDevice(); + if (inputDevice == nullptr) inputDevice = display->defaultInputDevice(); + if (inputDevice == nullptr) { + qCCritical(logIdleNotify) << "Could not create idle notifier: No seat."; + return nullptr; + } + + ::ext_idle_notification_v1* notification = nullptr; + if (respectInhibitors) notification = this->get_idle_notification(timeout, inputDevice->object()); + else notification = this->get_input_idle_notification(timeout, inputDevice->object()); + + auto* wrapper = new IdleNotification(notification); + qCDebug(logIdleNotify) << "Created" << wrapper << "with timeout:" << timeout + << "respects inhibitors:" << respectInhibitors; + return wrapper; +} + +IdleNotification::~IdleNotification() { + qCDebug(logIdleNotify) << "Destroyed" << this; + if (this->isInitialized()) this->destroy(); +} + +void IdleNotification::ext_idle_notification_v1_idled() { + qCDebug(logIdleNotify) << this << "has been marked idle"; + this->bIsIdle = true; +} + +void IdleNotification::ext_idle_notification_v1_resumed() { + qCDebug(logIdleNotify) << this << "has been marked resumed"; + this->bIsIdle = false; +} + +} // namespace qs::wayland::idle_notify::impl diff --git a/src/wayland/idle_notify/proto.hpp b/src/wayland/idle_notify/proto.hpp new file mode 100644 index 0000000..11dbf29 --- /dev/null +++ b/src/wayland/idle_notify/proto.hpp @@ -0,0 +1,51 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../../core/logcat.hpp" + +namespace qs::wayland::idle_notify { +QS_DECLARE_LOGGING_CATEGORY(logIdleNotify); +} + +namespace qs::wayland::idle_notify::impl { + +class IdleNotification; + +class IdleNotificationManager + : public QWaylandClientExtensionTemplate + , public QtWayland::ext_idle_notifier_v1 { +public: + explicit IdleNotificationManager(); + IdleNotification* createIdleNotification(quint32 timeout, bool respectInhibitors); + + static IdleNotificationManager* instance(); +}; + +class IdleNotification + : public QObject + , public QtWayland::ext_idle_notification_v1 { + Q_OBJECT; + +public: + explicit IdleNotification(::ext_idle_notification_v1* notification) + : QtWayland::ext_idle_notification_v1(notification) {} + + ~IdleNotification() override; + Q_DISABLE_COPY_MOVE(IdleNotification); + + Q_OBJECT_BINDABLE_PROPERTY(IdleNotification, bool, bIsIdle); + +protected: + void ext_idle_notification_v1_idled() override; + void ext_idle_notification_v1_resumed() override; +}; + +} // namespace qs::wayland::idle_notify::impl diff --git a/src/wayland/idle_notify/test/manual/idle_notify.qml b/src/wayland/idle_notify/test/manual/idle_notify.qml new file mode 100644 index 0000000..3bf6cbd --- /dev/null +++ b/src/wayland/idle_notify/test/manual/idle_notify.qml @@ -0,0 +1,44 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import Quickshell.Wayland + +FloatingWindow { + color: contentItem.palette.window + + IdleMonitor { + id: monitor + enabled: enabledCb.checked + timeout: timeoutSb.value + respectInhibitors: respectInhibitorsCb.checked + } + + ColumnLayout { + Label { text: `Is idle? ${monitor.isIdle}` } + + CheckBox { + id: enabledCb + text: "Enabled" + checked: true + } + + CheckBox { + id: respectInhibitorsCb + text: "Respect Inhibitors" + checked: true + } + + RowLayout { + Label { text: "Timeout" } + + SpinBox { + id: timeoutSb + editable: true + from: 0 + to: 1000 + value: 5 + } + } + } +} diff --git a/src/wayland/module.md b/src/wayland/module.md index b9f8f59..9ad15ba 100644 --- a/src/wayland/module.md +++ b/src/wayland/module.md @@ -5,5 +5,8 @@ headers = [ "session_lock.hpp", "toplevel_management/qml.hpp", "screencopy/view.hpp", + "idle_inhibit/inhibitor.hpp", + "idle_notify/monitor.hpp", + "shortcuts_inhibit/inhibitor.hpp", ] ----- diff --git a/src/wayland/popupanchor.cpp b/src/wayland/popupanchor.cpp index cbbccae..14e1923 100644 --- a/src/wayland/popupanchor.cpp +++ b/src/wayland/popupanchor.cpp @@ -16,7 +16,6 @@ using XdgPositioner = QtWayland::xdg_positioner; using qs::wayland::xdg_shell::XdgWmBase; void WaylandPopupPositioner::reposition(PopupAnchor* anchor, QWindow* window, bool onlyIfDirty) { - auto* waylandWindow = dynamic_cast(window->handle()); auto* popupRole = waylandWindow ? waylandWindow->surfaceRole<::xdg_popup>() : nullptr; diff --git a/src/wayland/screencopy/hyprland_screencopy/hyprland_screencopy.cpp b/src/wayland/screencopy/hyprland_screencopy/hyprland_screencopy.cpp index b8aef96..6fc2955 100644 --- a/src/wayland/screencopy/hyprland_screencopy/hyprland_screencopy.cpp +++ b/src/wayland/screencopy/hyprland_screencopy/hyprland_screencopy.cpp @@ -64,6 +64,8 @@ void HyprlandScreencopyContext::onToplevelDestroyed() { void HyprlandScreencopyContext::captureFrame() { if (this->object()) return; + this->request.reset(); + this->init(this->manager->capture_toplevel_with_wlr_toplevel_handle( this->paintCursors ? 1 : 0, this->handle->object() @@ -101,6 +103,16 @@ void HyprlandScreencopyContext::hyprland_toplevel_export_frame_v1_flags(uint32_t void HyprlandScreencopyContext::hyprland_toplevel_export_frame_v1_buffer_done() { auto* backbuffer = this->mSwapchain.createBackbuffer(this->request); + + if (!backbuffer || !backbuffer->buffer()) { + qCWarning(logScreencopy) << "Backbuffer creation failed for screencopy. Skipping frame."; + + // Try again. This will be spammy if the compositor continuously sends bad frames. + this->destroy(); + this->captureFrame(); + return; + } + this->copy(backbuffer->buffer(), this->copiedFirstFrame ? 0 : 1); } diff --git a/src/wayland/screencopy/image_copy_capture/image_copy_capture.cpp b/src/wayland/screencopy/image_copy_capture/image_copy_capture.cpp index a307d1e..13d1bc6 100644 --- a/src/wayland/screencopy/image_copy_capture/image_copy_capture.cpp +++ b/src/wayland/screencopy/image_copy_capture/image_copy_capture.cpp @@ -117,6 +117,12 @@ void IccScreencopyContext::doCapture() { auto newBuffer = false; auto* backbuffer = this->mSwapchain.createBackbuffer(this->request, &newBuffer); + if (!backbuffer || !backbuffer->buffer()) { + qCWarning(logIcc) << "Backbuffer creation failed for screencopy. Waiting for updated buffer " + "creation parameters before trying again."; + return; + } + this->IccCaptureFrame::init(this->IccCaptureSession::create_frame()); this->IccCaptureFrame::attach_buffer(backbuffer->buffer()); diff --git a/src/wayland/screencopy/wlr_screencopy/wlr_screencopy.cpp b/src/wayland/screencopy/wlr_screencopy/wlr_screencopy.cpp index f4d8c48..927da8d 100644 --- a/src/wayland/screencopy/wlr_screencopy/wlr_screencopy.cpp +++ b/src/wayland/screencopy/wlr_screencopy/wlr_screencopy.cpp @@ -65,12 +65,14 @@ void WlrScreencopyContext::onScreenDestroyed() { void WlrScreencopyContext::captureFrame() { if (this->object()) return; + this->request.reset(); + if (this->region.isEmpty()) { - this->init(manager->capture_output(this->paintCursors ? 1 : 0, screen->output())); + this->init(this->manager->capture_output(this->paintCursors ? 1 : 0, this->screen->output())); } else { - this->init(manager->capture_output_region( + this->init(this->manager->capture_output_region( this->paintCursors ? 1 : 0, - screen->output(), + this->screen->output(), this->region.x(), this->region.y(), this->region.width(), @@ -109,6 +111,15 @@ void WlrScreencopyContext::zwlr_screencopy_frame_v1_flags(uint32_t flags) { void WlrScreencopyContext::zwlr_screencopy_frame_v1_buffer_done() { auto* backbuffer = this->mSwapchain.createBackbuffer(this->request); + if (!backbuffer || !backbuffer->buffer()) { + qCWarning(logScreencopy) << "Backbuffer creation failed for screencopy. Skipping frame."; + + // Try again. This will be spammy if the compositor continuously sends bad frames. + this->destroy(); + this->captureFrame(); + return; + } + if (this->copiedFirstFrame) { this->copy_with_damage(backbuffer->buffer()); } else { @@ -154,7 +165,8 @@ WlrScreencopyContext::OutputTransformQuery::~OutputTransformQuery() { if (this->isInitialized()) this->release(); } -void WlrScreencopyContext::OutputTransformQuery::setScreen(QtWaylandClient::QWaylandScreen* screen +void WlrScreencopyContext::OutputTransformQuery::setScreen( + QtWaylandClient::QWaylandScreen* screen ) { // cursed hack class QWaylandScreenReflector: public QtWaylandClient::QWaylandScreen { diff --git a/src/wayland/session_lock.cpp b/src/wayland/session_lock.cpp index 0ecf9ec..2ebe3fd 100644 --- a/src/wayland/session_lock.cpp +++ b/src/wayland/session_lock.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -79,8 +80,8 @@ void WlSessionLock::updateSurfaces(bool show, WlSessionLock* old) { auto* instance = qobject_cast(instanceObj); if (instance == nullptr) { - qWarning( - ) << "WlSessionLock.surface does not create a WlSessionLockSurface. Aborting lock."; + qWarning() + << "WlSessionLock.surface does not create a WlSessionLockSurface. Aborting lock."; if (instanceObj != nullptr) instanceObj->deleteLater(); this->unlock(); return; @@ -216,6 +217,15 @@ void WlSessionLockSurface::onReload(QObject* oldInstance) { if (this->window == nullptr) { this->window = new QQuickWindow(); + + // needed for vulkan dmabuf import, qt ignores these if not applicable + auto graphicsConfig = this->window->graphicsConfiguration(); + graphicsConfig.setDeviceExtensions({ + "VK_KHR_external_memory_fd", + "VK_EXT_external_memory_dma_buf", + "VK_EXT_image_drm_format_modifier", + }); + this->window->setGraphicsConfiguration(graphicsConfig); } this->mContentItem->setParentItem(this->window->contentItem()); diff --git a/src/wayland/session_lock/shell_integration.cpp b/src/wayland/session_lock/shell_integration.cpp index 2b5fdbf..207ef42 100644 --- a/src/wayland/session_lock/shell_integration.cpp +++ b/src/wayland/session_lock/shell_integration.cpp @@ -10,10 +10,11 @@ QtWaylandClient::QWaylandShellSurface* QSWaylandSessionLockIntegration::createShellSurface(QtWaylandClient::QWaylandWindow* window) { auto* lock = LockWindowExtension::get(window->window()); - if (lock == nullptr || lock->surface == nullptr || !lock->surface->isExposed()) { + if (lock == nullptr || lock->surface == nullptr) { qFatal() << "Visibility canary failed. A window with a LockWindowExtension MUST be set to " "visible via LockWindowExtension::setVisible"; } - return lock->surface; + QSWaylandSessionLockSurface* surface = lock->surface; // shut up the unused include linter + return surface; } diff --git a/src/wayland/session_lock/shell_integration.hpp b/src/wayland/session_lock/shell_integration.hpp index d6f9175..b2e2891 100644 --- a/src/wayland/session_lock/shell_integration.hpp +++ b/src/wayland/session_lock/shell_integration.hpp @@ -8,6 +8,6 @@ class QSWaylandSessionLockIntegration: public QtWaylandClient::QWaylandShellIntegration { public: bool initialize(QtWaylandClient::QWaylandDisplay* /* display */) override { return true; } - QtWaylandClient::QWaylandShellSurface* createShellSurface(QtWaylandClient::QWaylandWindow* window - ) override; + QtWaylandClient::QWaylandShellSurface* + createShellSurface(QtWaylandClient::QWaylandWindow* window) override; }; diff --git a/src/wayland/session_lock/surface.cpp b/src/wayland/session_lock/surface.cpp index bc0e75d..c73f459 100644 --- a/src/wayland/session_lock/surface.cpp +++ b/src/wayland/session_lock/surface.cpp @@ -28,7 +28,7 @@ QSWaylandSessionLockSurface::QSWaylandSessionLockSurface(QtWaylandClient::QWayla wl_output* output = nullptr; // NOLINT (include) auto* waylandScreen = dynamic_cast(qwindow->screen()->handle()); - if (waylandScreen != nullptr) { + if (waylandScreen != nullptr && !waylandScreen->isPlaceholder() && waylandScreen->output()) { output = waylandScreen->output(); } else { qFatal() << "Session lock screen does not corrospond to a real screen. Force closing window"; @@ -48,16 +48,6 @@ void QSWaylandSessionLockSurface::applyConfigure() { this->window()->resizeFromApplyConfigure(this->size); } -bool QSWaylandSessionLockSurface::handleExpose(const QRegion& region) { - if (this->initBuf != nullptr) { - // at this point qt's next commit to the surface will have a new buffer, and we can safely delete this one. - delete this->initBuf; - this->initBuf = nullptr; - } - - return this->QtWaylandClient::QWaylandShellSurface::handleExpose(region); -} - void QSWaylandSessionLockSurface::setExtension(LockWindowExtension* ext) { if (ext == nullptr) { if (this->window() != nullptr) this->window()->window()->close(); @@ -71,11 +61,6 @@ void QSWaylandSessionLockSurface::setExtension(LockWindowExtension* ext) { } } -void QSWaylandSessionLockSurface::setVisible() { - if (this->configured && !this->visible) this->initVisible(); - this->visible = true; -} - void QSWaylandSessionLockSurface::ext_session_lock_surface_v1_configure( quint32 serial, quint32 width, @@ -97,13 +82,41 @@ void QSWaylandSessionLockSurface::ext_session_lock_surface_v1_configure( #else this->window()->updateExposure(); #endif + +#if QT_VERSION < QT_VERSION_CHECK(6, 10, 0) if (this->visible) this->initVisible(); +#endif } else { // applyConfigureWhenPossible runs too late and causes a protocol error on reconfigure. this->window()->resizeFromApplyConfigure(this->size); } } +#if QT_VERSION >= QT_VERSION_CHECK(6, 10, 0) + +bool QSWaylandSessionLockSurface::commitSurfaceRole() const { return false; } + +void QSWaylandSessionLockSurface::setVisible() { this->window()->window()->setVisible(true); } + +#else + +bool QSWaylandSessionLockSurface::handleExpose(const QRegion& region) { + if (this->initBuf != nullptr) { + // at this point qt's next commit to the surface will have a new buffer, and we can safely delete this one. + delete this->initBuf; + this->initBuf = nullptr; + } + + return this->QtWaylandClient::QWaylandShellSurface::handleExpose(region); +} + +void QSWaylandSessionLockSurface::setVisible() { + if (this->configured && !this->visible) this->initVisible(); + this->visible = true; +} + +#endif + #if QT_VERSION < QT_VERSION_CHECK(6, 9, 0) #include @@ -123,7 +136,7 @@ void QSWaylandSessionLockSurface::initVisible() { this->window()->window()->setVisible(true); } -#else +#elif QT_VERSION < QT_VERSION_CHECK(6, 10, 0) #include diff --git a/src/wayland/session_lock/surface.hpp b/src/wayland/session_lock/surface.hpp index f7abc77..39be88d 100644 --- a/src/wayland/session_lock/surface.hpp +++ b/src/wayland/session_lock/surface.hpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include @@ -20,7 +21,12 @@ public: [[nodiscard]] bool isExposed() const override; void applyConfigure() override; + +#if QT_VERSION >= QT_VERSION_CHECK(6, 10, 0) + [[nodiscard]] bool commitSurfaceRole() const override; +#else bool handleExpose(const QRegion& region) override; +#endif void setExtension(LockWindowExtension* ext); void setVisible(); @@ -29,11 +35,13 @@ private: void ext_session_lock_surface_v1_configure(quint32 serial, quint32 width, quint32 height) override; +#if QT_VERSION < QT_VERSION_CHECK(6, 10, 0) void initVisible(); + bool visible = false; + QtWaylandClient::QWaylandShmBuffer* initBuf = nullptr; +#endif LockWindowExtension* ext = nullptr; QSize size; bool configured = false; - bool visible = false; - QtWaylandClient::QWaylandShmBuffer* initBuf = nullptr; }; diff --git a/src/wayland/shortcuts_inhibit/CMakeLists.txt b/src/wayland/shortcuts_inhibit/CMakeLists.txt new file mode 100644 index 0000000..8dedd3d --- /dev/null +++ b/src/wayland/shortcuts_inhibit/CMakeLists.txt @@ -0,0 +1,25 @@ +qt_add_library(quickshell-wayland-shortcuts-inhibit STATIC + proto.cpp + inhibitor.cpp +) + +qt_add_qml_module(quickshell-wayland-shortcuts-inhibit + URI Quickshell.Wayland._ShortcutsInhibitor + VERSION 0.1 + DEPENDENCIES QtQuick +) + +install_qml_module(quickshell-wayland-shortcuts-inhibit) + +qs_add_module_deps_light(quickshell-wayland-shortcuts-inhibit Quickshell) + +wl_proto(wlp-shortcuts-inhibit keyboard-shortcuts-inhibit-unstable-v1 "${WAYLAND_PROTOCOLS}/unstable/keyboard-shortcuts-inhibit") + +target_link_libraries(quickshell-wayland-shortcuts-inhibit PRIVATE + Qt::Quick Qt::WaylandClient Qt::WaylandClientPrivate wayland-client + wlp-shortcuts-inhibit +) + +qs_module_pch(quickshell-wayland-shortcuts-inhibit SET large) + +target_link_libraries(quickshell PRIVATE quickshell-wayland-shortcuts-inhibitplugin) \ No newline at end of file diff --git a/src/wayland/shortcuts_inhibit/inhibitor.cpp b/src/wayland/shortcuts_inhibit/inhibitor.cpp new file mode 100644 index 0000000..2fca9bc --- /dev/null +++ b/src/wayland/shortcuts_inhibit/inhibitor.cpp @@ -0,0 +1,187 @@ +#include "inhibitor.hpp" + +#include +#include +#include +#include +#include +#include +#include + +#include "../../window/proxywindow.hpp" +#include "../../window/windowinterface.hpp" +#include "proto.hpp" + +namespace qs::wayland::shortcuts_inhibit { +using QtWaylandClient::QWaylandWindow; + +ShortcutInhibitor::ShortcutInhibitor() { + this->bBoundWindow.setBinding([this] { + return this->bEnabled ? this->bWindowObject.value() : nullptr; + }); + + this->bActive.setBinding([this]() { + auto* inhibitor = this->bInhibitor.value(); + if (!inhibitor) return false; + if (!inhibitor->bindableActive().value()) return false; + return this->bWindow.value() == this->bFocusedWindow; + }); + + QObject::connect( + dynamic_cast(QGuiApplication::instance()), + &QGuiApplication::focusWindowChanged, + this, + &ShortcutInhibitor::onFocusedWindowChanged + ); + + this->onFocusedWindowChanged(QGuiApplication::focusWindow()); +} + +ShortcutInhibitor::~ShortcutInhibitor() { + if (!this->bInhibitor) return; + + auto* manager = impl::ShortcutsInhibitManager::instance(); + if (!manager) return; + + manager->unrefShortcutsInhibitor(this->bInhibitor); +} + +void ShortcutInhibitor::onBoundWindowChanged() { + auto* window = this->bBoundWindow.value(); + auto* proxyWindow = qobject_cast(window); + + if (!proxyWindow) { + if (auto* iface = qobject_cast(window)) { + proxyWindow = iface->proxyWindow(); + } + } + + if (proxyWindow == this->proxyWindow) return; + + if (this->proxyWindow) { + QObject::disconnect(this->proxyWindow, nullptr, this, nullptr); + this->proxyWindow = nullptr; + } + + if (this->mWaylandWindow) { + QObject::disconnect(this->mWaylandWindow, nullptr, this, nullptr); + this->mWaylandWindow = nullptr; + this->onWaylandSurfaceDestroyed(); + } + + if (proxyWindow) { + this->proxyWindow = proxyWindow; + + QObject::connect(proxyWindow, &QObject::destroyed, this, &ShortcutInhibitor::onWindowDestroyed); + + QObject::connect( + proxyWindow, + &ProxyWindowBase::backerVisibilityChanged, + this, + &ShortcutInhibitor::onWindowVisibilityChanged + ); + + this->onWindowVisibilityChanged(); + } +} + +void ShortcutInhibitor::onWindowDestroyed() { + this->proxyWindow = nullptr; + this->onWaylandSurfaceDestroyed(); +} + +void ShortcutInhibitor::onWindowVisibilityChanged() { + if (!this->proxyWindow->isVisibleDirect()) return; + + auto* window = this->proxyWindow->backingWindow(); + if (!window->handle()) window->create(); + + auto* waylandWindow = dynamic_cast(window->handle()); + if (!waylandWindow) { + qCCritical(impl::logShortcutsInhibit()) << "Window handle is not a QWaylandWindow"; + return; + } + if (waylandWindow == this->mWaylandWindow) return; + this->mWaylandWindow = waylandWindow; + this->bWindow = window; + + QObject::connect( + waylandWindow, + &QObject::destroyed, + this, + &ShortcutInhibitor::onWaylandWindowDestroyed + ); + + QObject::connect( + waylandWindow, + &QWaylandWindow::surfaceCreated, + this, + &ShortcutInhibitor::onWaylandSurfaceCreated + ); + + QObject::connect( + waylandWindow, + &QWaylandWindow::surfaceDestroyed, + this, + &ShortcutInhibitor::onWaylandSurfaceDestroyed + ); + + if (waylandWindow->surface()) this->onWaylandSurfaceCreated(); +} + +void ShortcutInhibitor::onWaylandWindowDestroyed() { this->mWaylandWindow = nullptr; } + +void ShortcutInhibitor::onWaylandSurfaceCreated() { + auto* manager = impl::ShortcutsInhibitManager::instance(); + + if (!manager) { + qWarning() << "Cannot enable shortcuts inhibitor as keyboard-shortcuts-inhibit-unstable-v1 is " + "not supported by " + "the current compositor."; + return; + } + + if (this->bInhibitor) { + qFatal("ShortcutsInhibitor: inhibitor already exists when creating surface"); + } + + this->bInhibitor = manager->createShortcutsInhibitor(this->mWaylandWindow); +} + +void ShortcutInhibitor::onWaylandSurfaceDestroyed() { + if (!this->bInhibitor) return; + + auto* manager = impl::ShortcutsInhibitManager::instance(); + if (!manager) return; + + manager->unrefShortcutsInhibitor(this->bInhibitor); + this->bInhibitor = nullptr; +} + +void ShortcutInhibitor::onInhibitorChanged() { + auto* inhibitor = this->bInhibitor.value(); + if (inhibitor) { + QObject::connect( + inhibitor, + &impl::ShortcutsInhibitor::activeChanged, + this, + &ShortcutInhibitor::onInhibitorActiveChanged + ); + } +} + +void ShortcutInhibitor::onInhibitorActiveChanged() { + auto* inhibitor = this->bInhibitor.value(); + if (inhibitor && !inhibitor->isActive()) { + // Compositor has deactivated the inhibitor, making it invalid. + // Set enabled to false so the user can enable it again to create a new inhibitor. + this->bEnabled = false; + emit this->cancelled(); + } +} + +void ShortcutInhibitor::onFocusedWindowChanged(QWindow* focusedWindow) { + this->bFocusedWindow = focusedWindow; +} + +} // namespace qs::wayland::shortcuts_inhibit diff --git a/src/wayland/shortcuts_inhibit/inhibitor.hpp b/src/wayland/shortcuts_inhibit/inhibitor.hpp new file mode 100644 index 0000000..2eed54f --- /dev/null +++ b/src/wayland/shortcuts_inhibit/inhibitor.hpp @@ -0,0 +1,89 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "../../window/proxywindow.hpp" +#include "proto.hpp" + +namespace qs::wayland::shortcuts_inhibit { + +///! Prevents compositor keyboard shortcuts from being triggered +/// A shortcuts inhibitor prevents the compositor from processing its own keyboard shortcuts +/// for the focused surface. This allows applications to receive key events for shortcuts +/// that would normally be handled by the compositor. +/// +/// The inhibitor only takes effect when the associated window is focused and the inhibitor +/// is enabled. The compositor may choose to ignore inhibitor requests based on its policy. +/// +/// > [!NOTE] Using a shortcuts inhibitor requires the compositor support the [keyboard-shortcuts-inhibit-unstable-v1] protocol. +/// +/// [keyboard-shortcuts-inhibit-unstable-v1]: https://wayland.app/protocols/keyboard-shortcuts-inhibit-unstable-v1 +class ShortcutInhibitor: public QObject { + Q_OBJECT; + QML_ELEMENT; + // clang-format off + /// If the shortcuts inhibitor should be enabled. Defaults to false. + Q_PROPERTY(bool enabled READ default WRITE default NOTIFY enabledChanged BINDABLE bindableEnabled); + /// The window to associate the shortcuts inhibitor with. + /// The inhibitor will only inhibit shortcuts pressed while this window has keyboard focus. + /// + /// Must be set to a non null value to enable the inhibitor. + Q_PROPERTY(QObject* window READ default WRITE default NOTIFY windowChanged BINDABLE bindableWindow); + /// Whether the inhibitor is currently active. The inhibitor is only active if @@enabled is true, + /// @@window has keyboard focus, and the compositor grants the inhibit request. + /// + /// The compositor may deactivate the inhibitor at any time (for example, if the user requests + /// normal shortcuts to be restored). When deactivated by the compositor, the inhibitor cannot be + /// programmatically reactivated. + Q_PROPERTY(bool active READ default NOTIFY activeChanged BINDABLE bindableActive); + // clang-format on + +public: + ShortcutInhibitor(); + ~ShortcutInhibitor() override; + Q_DISABLE_COPY_MOVE(ShortcutInhibitor); + + [[nodiscard]] QBindable bindableEnabled() { return &this->bEnabled; } + [[nodiscard]] QBindable bindableWindow() { return &this->bWindowObject; } + [[nodiscard]] QBindable bindableActive() const { return &this->bActive; } + +signals: + void enabledChanged(); + void windowChanged(); + void activeChanged(); + /// Sent if the compositor cancels the inhibitor while it is active. + void cancelled(); + +private slots: + void onWindowDestroyed(); + void onWindowVisibilityChanged(); + void onWaylandWindowDestroyed(); + void onWaylandSurfaceCreated(); + void onWaylandSurfaceDestroyed(); + void onInhibitorActiveChanged(); + +private: + void onBoundWindowChanged(); + void onInhibitorChanged(); + void onFocusedWindowChanged(QWindow* focusedWindow); + + ProxyWindowBase* proxyWindow = nullptr; + QtWaylandClient::QWaylandWindow* mWaylandWindow = nullptr; + + // clang-format off + Q_OBJECT_BINDABLE_PROPERTY(ShortcutInhibitor, bool, bEnabled, &ShortcutInhibitor::enabledChanged); + Q_OBJECT_BINDABLE_PROPERTY(ShortcutInhibitor, QObject*, bWindowObject, &ShortcutInhibitor::windowChanged); + Q_OBJECT_BINDABLE_PROPERTY(ShortcutInhibitor, QObject*, bBoundWindow, &ShortcutInhibitor::onBoundWindowChanged); + Q_OBJECT_BINDABLE_PROPERTY(ShortcutInhibitor, impl::ShortcutsInhibitor*, bInhibitor, &ShortcutInhibitor::onInhibitorChanged); + Q_OBJECT_BINDABLE_PROPERTY(ShortcutInhibitor, QWindow*, bWindow); + Q_OBJECT_BINDABLE_PROPERTY(ShortcutInhibitor, QWindow*, bFocusedWindow); + Q_OBJECT_BINDABLE_PROPERTY(ShortcutInhibitor, bool, bActive, &ShortcutInhibitor::activeChanged); + // clang-format on +}; + +} // namespace qs::wayland::shortcuts_inhibit diff --git a/src/wayland/shortcuts_inhibit/proto.cpp b/src/wayland/shortcuts_inhibit/proto.cpp new file mode 100644 index 0000000..8d35d5e --- /dev/null +++ b/src/wayland/shortcuts_inhibit/proto.cpp @@ -0,0 +1,88 @@ +#include "proto.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../../core/logcat.hpp" + +namespace qs::wayland::shortcuts_inhibit::impl { + +QS_LOGGING_CATEGORY(logShortcutsInhibit, "quickshell.wayland.shortcuts_inhibit", QtWarningMsg); + +ShortcutsInhibitManager::ShortcutsInhibitManager(): QWaylandClientExtensionTemplate(1) { + this->initialize(); +} + +ShortcutsInhibitManager* ShortcutsInhibitManager::instance() { + static auto* instance = new ShortcutsInhibitManager(); // NOLINT + return instance->isInitialized() ? instance : nullptr; +} + +ShortcutsInhibitor* +ShortcutsInhibitManager::createShortcutsInhibitor(QtWaylandClient::QWaylandWindow* surface) { + auto* display = QtWaylandClient::QWaylandIntegration::instance()->display(); + auto* inputDevice = display->lastInputDevice(); + if (inputDevice == nullptr) inputDevice = display->defaultInputDevice(); + + if (inputDevice == nullptr) { + qCCritical(logShortcutsInhibit) << "Could not create shortcuts inhibitor: No seat."; + return nullptr; + } + + auto* wlSurface = surface->surface(); + + if (this->inhibitors.contains(wlSurface)) { + auto& pair = this->inhibitors[wlSurface]; + pair.second++; + qCDebug(logShortcutsInhibit) << "Reusing existing inhibitor" << pair.first << "for surface" + << wlSurface << "- refcount:" << pair.second; + return pair.first; + } + + auto* inhibitor = + new ShortcutsInhibitor(this->inhibit_shortcuts(wlSurface, inputDevice->object()), wlSurface); + this->inhibitors.insert(wlSurface, qMakePair(inhibitor, 1)); + qCDebug(logShortcutsInhibit) << "Created inhibitor" << inhibitor << "for surface" << wlSurface; + return inhibitor; +} + +void ShortcutsInhibitManager::unrefShortcutsInhibitor(ShortcutsInhibitor* inhibitor) { + if (!inhibitor) return; + + auto* surface = inhibitor->surface(); + if (!this->inhibitors.contains(surface)) return; + + auto& pair = this->inhibitors[surface]; + pair.second--; + qCDebug(logShortcutsInhibit) << "Decremented refcount for inhibitor" << inhibitor + << "- refcount:" << pair.second; + + if (pair.second <= 0) { + qCDebug(logShortcutsInhibit) << "Refcount reached 0, destroying inhibitor" << inhibitor; + this->inhibitors.remove(surface); + delete inhibitor; + } +} + +ShortcutsInhibitor::~ShortcutsInhibitor() { + qCDebug(logShortcutsInhibit) << "Destroying inhibitor" << this << "for surface" << this->mSurface; + if (this->isInitialized()) this->destroy(); +} + +void ShortcutsInhibitor::zwp_keyboard_shortcuts_inhibitor_v1_active() { + qCDebug(logShortcutsInhibit) << "Inhibitor became active" << this; + this->bActive = true; +} + +void ShortcutsInhibitor::zwp_keyboard_shortcuts_inhibitor_v1_inactive() { + qCDebug(logShortcutsInhibit) << "Inhibitor became inactive" << this; + this->bActive = false; +} + +} // namespace qs::wayland::shortcuts_inhibit::impl \ No newline at end of file diff --git a/src/wayland/shortcuts_inhibit/proto.hpp b/src/wayland/shortcuts_inhibit/proto.hpp new file mode 100644 index 0000000..e79d5ca --- /dev/null +++ b/src/wayland/shortcuts_inhibit/proto.hpp @@ -0,0 +1,64 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "../../core/logcat.hpp" +#include "wayland-keyboard-shortcuts-inhibit-unstable-v1-client-protocol.h" + +namespace qs::wayland::shortcuts_inhibit::impl { + +QS_DECLARE_LOGGING_CATEGORY(logShortcutsInhibit); + +class ShortcutsInhibitor; + +class ShortcutsInhibitManager + : public QWaylandClientExtensionTemplate + , public QtWayland::zwp_keyboard_shortcuts_inhibit_manager_v1 { +public: + explicit ShortcutsInhibitManager(); + + ShortcutsInhibitor* createShortcutsInhibitor(QtWaylandClient::QWaylandWindow* surface); + void unrefShortcutsInhibitor(ShortcutsInhibitor* inhibitor); + + static ShortcutsInhibitManager* instance(); + +private: + QHash> inhibitors; +}; + +class ShortcutsInhibitor + : public QObject + , public QtWayland::zwp_keyboard_shortcuts_inhibitor_v1 { + Q_OBJECT; + +public: + explicit ShortcutsInhibitor(::zwp_keyboard_shortcuts_inhibitor_v1* inhibitor, wl_surface* surface) + : QtWayland::zwp_keyboard_shortcuts_inhibitor_v1(inhibitor) + , mSurface(surface) + , bActive(false) {} + + ~ShortcutsInhibitor() override; + Q_DISABLE_COPY_MOVE(ShortcutsInhibitor); + + [[nodiscard]] QBindable bindableActive() const { return &this->bActive; } + [[nodiscard]] bool isActive() const { return this->bActive; } + [[nodiscard]] wl_surface* surface() const { return this->mSurface; } + +signals: + void activeChanged(); + +protected: + void zwp_keyboard_shortcuts_inhibitor_v1_active() override; + void zwp_keyboard_shortcuts_inhibitor_v1_inactive() override; + +private: + wl_surface* mSurface; + Q_OBJECT_BINDABLE_PROPERTY(ShortcutsInhibitor, bool, bActive, &ShortcutsInhibitor::activeChanged); +}; + +} // namespace qs::wayland::shortcuts_inhibit::impl \ No newline at end of file diff --git a/src/wayland/shortcuts_inhibit/test/manual/test.qml b/src/wayland/shortcuts_inhibit/test/manual/test.qml new file mode 100644 index 0000000..1f64cbf --- /dev/null +++ b/src/wayland/shortcuts_inhibit/test/manual/test.qml @@ -0,0 +1,65 @@ +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls +import Quickshell +import Quickshell.Wayland + +Scope { + Timer { + id: toggleTimer + interval: 100 + onTriggered: windowLoader.active = true + } + + LazyLoader { + id: windowLoader + active: true + + property bool enabled: false + + FloatingWindow { + id: w + color: contentItem.palette.window + + ColumnLayout { + anchors.centerIn: parent + + CheckBox { + id: loadedCb + text: "Loaded" + checked: true + } + + CheckBox { + id: enabledCb + text: "Enabled" + checked: windowLoader.enabled + onCheckedChanged: windowLoader.enabled = checked + } + + Label { + text: `Active: ${inhibitorLoader.item?.active ?? false}` + } + + Button { + text: "Toggle Window" + onClicked: { + windowLoader.active = false; + toggleTimer.start(); + } + } + } + + LazyLoader { + id: inhibitorLoader + active: loadedCb.checked + + ShortcutInhibitor { + window: w + enabled: enabledCb.checked + onCancelled: enabledCb.checked = false + } + } + } + } +} diff --git a/src/wayland/toplevel_management/manager.hpp b/src/wayland/toplevel_management/manager.hpp index 4b906a5..83e3e09 100644 --- a/src/wayland/toplevel_management/manager.hpp +++ b/src/wayland/toplevel_management/manager.hpp @@ -33,8 +33,8 @@ signals: protected: explicit ToplevelManager(); - void zwlr_foreign_toplevel_manager_v1_toplevel(::zwlr_foreign_toplevel_handle_v1* toplevel - ) override; + void + zwlr_foreign_toplevel_manager_v1_toplevel(::zwlr_foreign_toplevel_handle_v1* toplevel) override; private slots: void onToplevelReady(); diff --git a/src/wayland/windowmanager/CMakeLists.txt b/src/wayland/windowmanager/CMakeLists.txt new file mode 100644 index 0000000..9e03b14 --- /dev/null +++ b/src/wayland/windowmanager/CMakeLists.txt @@ -0,0 +1,20 @@ +qt_add_library(quickshell-wayland-windowsystem STATIC + windowmanager.cpp + workspace.cpp + ext_workspace.cpp +) + +add_library(quickshell-wayland-windowsystem-init OBJECT init.cpp) +target_link_libraries(quickshell-wayland-windowsystem-init PRIVATE Qt::Quick) + +#wl_proto(wlp-ext-foreign-toplevel ext-foreign-toplevel-list-v1 "${WAYLAND_PROTOCOLS}/staging/ext-foreign-toplevel-list") +wl_proto(wlp-ext-workspace ext-workspace-v1 "${WAYLAND_PROTOCOLS}/staging/ext-workspace") + +target_link_libraries(quickshell-wayland-windowsystem PRIVATE + Qt::WaylandClient Qt::WaylandClientPrivate wayland-client + Qt::Quick # for pch? potentially, check w/ gcc + + wlp-ext-foreign-toplevel wlp-ext-workspace +) + +target_link_libraries(quickshell PRIVATE quickshell-wayland-windowsystem quickshell-wayland-windowsystem-init) diff --git a/src/wayland/windowmanager/ext_workspace.cpp b/src/wayland/windowmanager/ext_workspace.cpp new file mode 100644 index 0000000..3e4c099 --- /dev/null +++ b/src/wayland/windowmanager/ext_workspace.cpp @@ -0,0 +1,169 @@ +#include "ext_workspace.hpp" + +#include +#include +#include +#include +#include +#include + +namespace qs::wayland::workspace { + +Q_LOGGING_CATEGORY(logWorkspace, "quickshell.wm.wayland.workspace"); + +WorkspaceManager::WorkspaceManager(): QWaylandClientExtensionTemplate(1) { this->initialize(); } + +WorkspaceManager* WorkspaceManager::instance() { + static auto* instance = new WorkspaceManager(); + return instance; +} + +void WorkspaceManager::ext_workspace_manager_v1_workspace_group( + ::ext_workspace_group_handle_v1* handle +) { + auto* group = new WorkspaceGroup(handle); + qCDebug(logWorkspace) << "Created group" << group; + this->mGroups.insert(handle, group); + emit this->groupCreated(group); +} + +void WorkspaceManager::ext_workspace_manager_v1_workspace(::ext_workspace_handle_v1* handle) { + auto* workspace = new Workspace(handle); + qCDebug(logWorkspace) << "Created workspace" << workspace; + this->mWorkspaces.insert(handle, workspace); + emit this->workspaceCreated(workspace); +}; + +void WorkspaceManager::destroyWorkspace(Workspace* workspace) { + this->mWorkspaces.remove(workspace->object()); + this->destroyedWorkspaces.append(workspace); + emit this->workspaceDestroyed(workspace); +} + +void WorkspaceManager::destroyGroup(WorkspaceGroup* group) { + this->mGroups.remove(group->object()); + this->destroyedGroups.append(group); + emit this->groupDestroyed(group); +} + +void WorkspaceManager::ext_workspace_manager_v1_done() { + qCDebug(logWorkspace) << "Workspace changes done"; + emit this->serverCommit(); + + for (auto* workspace: this->destroyedWorkspaces) delete workspace; + for (auto* group: this->destroyedGroups) delete group; + this->destroyedWorkspaces.clear(); + this->destroyedGroups.clear(); +} + +void WorkspaceManager::ext_workspace_manager_v1_finished() { + qCWarning(logWorkspace) << "ext_workspace_manager_v1.finished() was received"; +} + +Workspace::~Workspace() { + if (this->isInitialized()) this->destroy(); +} + +void Workspace::ext_workspace_handle_v1_id(const QString& id) { + qCDebug(logWorkspace) << "Updated id for workspace" << this << "to" << id; + this->id = id; +} + +void Workspace::ext_workspace_handle_v1_name(const QString& name) { + qCDebug(logWorkspace) << "Updated name for workspace" << this << "to" << name; + this->name = name; +} + +void Workspace::ext_workspace_handle_v1_coordinates(wl_array* coordinates) { + this->coordinates.clear(); + + auto* data = static_cast(coordinates->data); + auto size = static_cast(coordinates->size / sizeof(qint32)); + + for (auto i = 0; i != size; ++i) { + this->coordinates.append(data[i]); // NOLINT + } + + qCDebug(logWorkspace) << "Updated coordinates for workspace" << this << "to" << this->coordinates; +} + +void Workspace::ext_workspace_handle_v1_state(quint32 state) { + this->active = state & ext_workspace_handle_v1::state_active; + this->urgent = state & ext_workspace_handle_v1::state_urgent; + this->hidden = state & ext_workspace_handle_v1::state_hidden; + + qCDebug(logWorkspace).nospace() << "Updated state for workspace " << this + << " to [active: " << this->active << ", urgent: " << this->urgent + << ", hidden: " << this->hidden << ']'; +} + +void Workspace::ext_workspace_handle_v1_capabilities(quint32 capabilities) { + this->canActivate = capabilities & ext_workspace_handle_v1::workspace_capabilities_activate; + this->canDeactivate = capabilities & ext_workspace_handle_v1::workspace_capabilities_deactivate; + this->canRemove = capabilities & ext_workspace_handle_v1::workspace_capabilities_remove; + this->canAssign = capabilities & ext_workspace_handle_v1::workspace_capabilities_assign; + + qCDebug(logWorkspace).nospace() << "Updated capabilities for workspace " << this + << " to [activate: " << this->canActivate + << ", deactivate: " << this->canDeactivate + << ", remove: " << this->canRemove + << ", assign: " << this->canAssign << ']'; +} + +void Workspace::ext_workspace_handle_v1_removed() { + qCDebug(logWorkspace) << "Destroyed workspace" << this; + WorkspaceManager::instance()->destroyWorkspace(this); + this->destroy(); +} + +void Workspace::enterGroup(WorkspaceGroup* group) { this->group = group; } + +void Workspace::leaveGroup(WorkspaceGroup* group) { + if (this->group == group) this->group = nullptr; +} + +WorkspaceGroup::~WorkspaceGroup() { + if (this->isInitialized()) this->destroy(); +} + +void WorkspaceGroup::ext_workspace_group_handle_v1_capabilities(uint32_t capabilities) { + this->canCreateWorkspace = + capabilities & ext_workspace_group_handle_v1::group_capabilities_create_workspace; + + qCDebug(logWorkspace).nospace() << "Updated capabilities for group " << this + << " to [create_workspace: " << this->canCreateWorkspace << ']'; +} + +void WorkspaceGroup::ext_workspace_group_handle_v1_output_enter(::wl_output* output) { + qCDebug(logWorkspace) << "Output" << output << "added to group" << this; + this->screens.addOutput(output); +} + +void WorkspaceGroup::ext_workspace_group_handle_v1_output_leave(::wl_output* output) { + qCDebug(logWorkspace) << "Output" << output << "removed from group" << this; + this->screens.removeOutput(output); +} + +void WorkspaceGroup::ext_workspace_group_handle_v1_workspace_enter(::ext_workspace_handle_v1* handle +) { + auto* workspace = WorkspaceManager::instance()->mWorkspaces.value(handle); + qCDebug(logWorkspace) << "Workspace" << workspace << "added to group" << this; + + if (workspace) workspace->enterGroup(this); +} + +void WorkspaceGroup::ext_workspace_group_handle_v1_workspace_leave(::ext_workspace_handle_v1* handle +) { + auto* workspace = WorkspaceManager::instance()->mWorkspaces.value(handle); + qCDebug(logWorkspace) << "Workspace" << workspace << "removed from group" << this; + + if (workspace) workspace->leaveGroup(this); +} + +void WorkspaceGroup::ext_workspace_group_handle_v1_removed() { + qCDebug(logWorkspace) << "Destroyed group" << this; + WorkspaceManager::instance()->destroyGroup(this); + this->destroy(); +} + +} // namespace qs::wayland::workspace diff --git a/src/wayland/windowmanager/ext_workspace.hpp b/src/wayland/windowmanager/ext_workspace.hpp new file mode 100644 index 0000000..9dfac1b --- /dev/null +++ b/src/wayland/windowmanager/ext_workspace.hpp @@ -0,0 +1,117 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../output_tracking.hpp" + +namespace qs::wayland::workspace { + +Q_DECLARE_LOGGING_CATEGORY(logWorkspace); + +class WorkspaceGroup; +class Workspace; + +class WorkspaceManager + : public QWaylandClientExtensionTemplate + , public QtWayland::ext_workspace_manager_v1 { + Q_OBJECT; + +public: + static WorkspaceManager* instance(); + + [[nodiscard]] QList workspaces() { return this->mWorkspaces.values(); } + +signals: + void serverCommit(); + void workspaceCreated(Workspace* workspace); + void workspaceDestroyed(Workspace* workspace); + void groupCreated(WorkspaceGroup* group); + void groupDestroyed(WorkspaceGroup* group); + +protected: + void ext_workspace_manager_v1_workspace_group(::ext_workspace_group_handle_v1* handle) override; + void ext_workspace_manager_v1_workspace(::ext_workspace_handle_v1* handle) override; + void ext_workspace_manager_v1_done() override; + void ext_workspace_manager_v1_finished() override; + +private: + WorkspaceManager(); + + void destroyGroup(WorkspaceGroup* group); + void destroyWorkspace(Workspace* workspace); + + QHash<::ext_workspace_handle_v1*, Workspace*> mWorkspaces; + QHash<::ext_workspace_group_handle_v1*, WorkspaceGroup*> mGroups; + QList destroyedGroups; + QList destroyedWorkspaces; + + friend class Workspace; + friend class WorkspaceGroup; +}; + +class Workspace: public QtWayland::ext_workspace_handle_v1 { +public: + Workspace(::ext_workspace_handle_v1* handle): QtWayland::ext_workspace_handle_v1(handle) {} + ~Workspace() override; + Q_DISABLE_COPY_MOVE(Workspace); + + QString id; + QString name; + QList coordinates; + WorkspaceGroup* group = nullptr; + + bool active : 1 = false; + bool urgent : 1 = false; + bool hidden : 1 = false; + + bool canActivate : 1 = false; + bool canDeactivate : 1 = false; + bool canRemove : 1 = false; + bool canAssign : 1 = false; + +protected: + void ext_workspace_handle_v1_id(const QString& id) override; + void ext_workspace_handle_v1_name(const QString& name) override; + void ext_workspace_handle_v1_coordinates(wl_array* coordinates) override; + void ext_workspace_handle_v1_state(uint32_t state) override; + void ext_workspace_handle_v1_capabilities(uint32_t capabilities) override; + void ext_workspace_handle_v1_removed() override; + +private: + void enterGroup(WorkspaceGroup* group); + void leaveGroup(WorkspaceGroup* group); + + friend class WorkspaceGroup; +}; + +class WorkspaceGroup: public QtWayland::ext_workspace_group_handle_v1 { +public: + WorkspaceGroup(::ext_workspace_group_handle_v1* handle) + : QtWayland::ext_workspace_group_handle_v1(handle) {} + + ~WorkspaceGroup() override; + Q_DISABLE_COPY_MOVE(WorkspaceGroup); + + WlOutputTracker screens; + bool canCreateWorkspace : 1 = false; + +protected: + void ext_workspace_group_handle_v1_capabilities(uint32_t capabilities) override; + void ext_workspace_group_handle_v1_output_enter(::wl_output* output) override; + void ext_workspace_group_handle_v1_output_leave(::wl_output* output) override; + void ext_workspace_group_handle_v1_workspace_enter(::ext_workspace_handle_v1* handle) override; + void ext_workspace_group_handle_v1_workspace_leave(::ext_workspace_handle_v1* handle) override; + void ext_workspace_group_handle_v1_removed() override; +}; + +} // namespace qs::wayland::workspace diff --git a/src/wayland/windowmanager/init.cpp b/src/wayland/windowmanager/init.cpp new file mode 100644 index 0000000..fa336d7 --- /dev/null +++ b/src/wayland/windowmanager/init.cpp @@ -0,0 +1,21 @@ +#include + +#include "../../core/plugin.hpp" + +namespace qs::wm::wayland { +void installWmProvider(); +} + +namespace { + +class WaylandWmPlugin: public QsEnginePlugin { + QList dependencies() override { return {"window"}; } + + bool applies() override { return QGuiApplication::platformName() == "wayland"; } + + void init() override { qs::wm::wayland::installWmProvider(); } +}; + +QS_REGISTER_PLUGIN(WaylandWmPlugin); + +} // namespace diff --git a/src/wayland/windowmanager/windowmanager.cpp b/src/wayland/windowmanager/windowmanager.cpp new file mode 100644 index 0000000..5f4a450 --- /dev/null +++ b/src/wayland/windowmanager/windowmanager.cpp @@ -0,0 +1,14 @@ +#include "windowmanager.hpp" + +namespace qs::wm::wayland { + +WaylandWindowManager* WaylandWindowManager::instance() { + static auto* instance = new WaylandWindowManager(); + return instance; +} + +void installWmProvider() { + qs::wm::WindowManager::setProvider([]() { return WaylandWindowManager::instance(); }); +} + +} // namespace qs::wm::wayland diff --git a/src/wayland/windowmanager/windowmanager.hpp b/src/wayland/windowmanager/windowmanager.hpp new file mode 100644 index 0000000..c732d6a --- /dev/null +++ b/src/wayland/windowmanager/windowmanager.hpp @@ -0,0 +1,25 @@ +#pragma once + +#include + +#include "../../windowmanager/windowmanager.hpp" +#include "workspace.hpp" + +namespace qs::wm::wayland { + +class WaylandWindowManager: public WindowManager { + Q_OBJECT; + +public: + static WaylandWindowManager* instance(); + + [[nodiscard]] UntypedObjectModel* workspaces() const override { + return &WorkspaceManager::instance()->mWorkspaces; + } + + [[nodiscard]] UntypedObjectModel* workspaceGroups() const override { + return &WorkspaceManager::instance()->mWorkspaceGroups; + } +}; + +} // namespace qs::wm::wayland diff --git a/src/wayland/windowmanager/workspace.cpp b/src/wayland/windowmanager/workspace.cpp new file mode 100644 index 0000000..07bf3da --- /dev/null +++ b/src/wayland/windowmanager/workspace.cpp @@ -0,0 +1,198 @@ +#include "workspace.hpp" + +#include +#include +#include +#include +#include + +#include "ext_workspace.hpp" + +namespace qs::wm::wayland { + +WorkspaceManager::WorkspaceManager() { + auto* impl = impl::WorkspaceManager::instance(); + + QObject::connect( + impl, + &impl::WorkspaceManager::serverCommit, + this, + &WorkspaceManager::onServerCommit + ); + + QObject::connect( + impl, + &impl::WorkspaceManager::workspaceCreated, + this, + &WorkspaceManager::onWorkspaceCreated + ); + + QObject::connect( + impl, + &impl::WorkspaceManager::workspaceDestroyed, + this, + &WorkspaceManager::onWorkspaceDestroyed + ); + + QObject::connect( + impl, + &impl::WorkspaceManager::groupCreated, + this, + &WorkspaceManager::onGroupCreated + ); + + QObject::connect( + impl, + &impl::WorkspaceManager::groupDestroyed, + this, + &WorkspaceManager::onGroupDestroyed + ); +} + +void WorkspaceManager::commit() { + qCDebug(impl::logWorkspace) << "Committing workspaces"; + impl::WorkspaceManager::instance()->commit(); +} + +void WorkspaceManager::onServerCommit() { + // Groups are created/destroyed around workspaces to avoid any nulls making it + // to the qml engine. + + for (auto* groupImpl: this->pendingGroupCreations) { + auto* group = new WlWorkspaceGroup(this, groupImpl); + this->groupsByImpl.insert(groupImpl, group); + this->mWorkspaceGroups.insertObject(group); + } + + for (auto* wsImpl: this->pendingWorkspaceCreations) { + auto* ws = new WlWorkspace(this, wsImpl); + this->workspaceByImpl.insert(wsImpl, ws); + this->mWorkspaces.insertObject(ws); + } + + for (auto* wsImpl: this->pendingWorkspaceDestructions) { + this->mWorkspaces.removeObject(this->workspaceByImpl.value(wsImpl)); + this->workspaceByImpl.remove(wsImpl); + } + + for (auto* groupImpl: this->pendingGroupDestructions) { + this->mWorkspaceGroups.removeObject(this->groupsByImpl.value(groupImpl)); + this->groupsByImpl.remove(groupImpl); + } + + for (auto* ws: this->mWorkspaces.valueList()) ws->commitImpl(); + for (auto* group: this->mWorkspaceGroups.valueList()) group->commitImpl(); + + this->pendingWorkspaceCreations.clear(); + this->pendingWorkspaceDestructions.clear(); + this->pendingGroupCreations.clear(); + this->pendingGroupDestructions.clear(); +} + +void WorkspaceManager::onWorkspaceCreated(impl::Workspace* workspace) { + this->pendingWorkspaceCreations.append(workspace); +} + +void WorkspaceManager::onWorkspaceDestroyed(impl::Workspace* workspace) { + if (!this->pendingWorkspaceCreations.removeOne(workspace)) { + this->pendingWorkspaceDestructions.append(workspace); + } +} + +void WorkspaceManager::onGroupCreated(impl::WorkspaceGroup* group) { + this->pendingGroupCreations.append(group); +} + +void WorkspaceManager::onGroupDestroyed(impl::WorkspaceGroup* group) { + if (!this->pendingGroupCreations.removeOne(group)) { + this->pendingGroupDestructions.append(group); + } +} + +WorkspaceManager* WorkspaceManager::instance() { + static auto* instance = new WorkspaceManager(); + return instance; +} + +WlWorkspace::WlWorkspace(WorkspaceManager* manager, impl::Workspace* impl) + : Workspace(manager) + , impl(impl) { + this->commitImpl(); +} + +void WlWorkspace::commitImpl() { + Qt::beginPropertyUpdateGroup(); + this->bId = this->impl->id; + this->bName = this->impl->name; + this->bActive = this->impl->active; + this->bShouldDisplay = !this->impl->hidden; + this->bUrgent = this->impl->urgent; + this->bCanActivate = this->impl->canActivate; + this->bCanDeactivate = this->impl->canDeactivate; + this->bCanSetGroup = this->impl->canAssign; + this->bGroup = this->manager()->groupsByImpl.value(this->impl->group); + Qt::endPropertyUpdateGroup(); +} + +void WlWorkspace::activate() { + if (!this->bCanActivate) { + qCritical(logWorkspace) << this << "cannot be activated"; + return; + } + + qCDebug(impl::logWorkspace) << "Calling activate() for" << this; + this->impl->activate(); + WorkspaceManager::commit(); +} + +void WlWorkspace::deactivate() { + if (!this->bCanDeactivate) { + qCritical(logWorkspace) << this << "cannot be deactivated"; + return; + } + + qCDebug(impl::logWorkspace) << "Calling deactivate() for" << this; + this->impl->deactivate(); + WorkspaceManager::commit(); +} + +void WlWorkspace::remove() { + if (!this->bCanRemove) { + qCritical(logWorkspace) << this << "cannot be removed"; + return; + } + + qCDebug(impl::logWorkspace) << "Calling remove() for" << this; + this->impl->remove(); + WorkspaceManager::commit(); +} + +void WlWorkspace::setGroup(WorkspaceGroup* group) { + if (!this->bCanSetGroup) { + qCritical(logWorkspace) << this << "cannot be assigned to a group"; + return; + } + + if (!group) { + qCritical(logWorkspace) << "Cannot set a workspace's group to null"; + return; + } + + qCDebug(impl::logWorkspace) << "Assigning" << this << "to" << group; + // NOLINTNEXTLINE: A WorkspaceGroup will always be a WlWorkspaceGroup under wayland. + this->impl->assign(static_cast(group)->impl->object()); + WorkspaceManager::commit(); +} + +WlWorkspaceGroup::WlWorkspaceGroup(WorkspaceManager* manager, impl::WorkspaceGroup* impl) + : WorkspaceGroup(manager) + , impl(impl) { + this->commitImpl(); +} + +void WlWorkspaceGroup::commitImpl() { + // TODO: will not commit the correct screens if missing qt repr at commit time + this->bScreens = this->impl->screens.screens(); +} + +} // namespace qs::wm::wayland diff --git a/src/wayland/windowmanager/workspace.hpp b/src/wayland/windowmanager/workspace.hpp new file mode 100644 index 0000000..82742a9 --- /dev/null +++ b/src/wayland/windowmanager/workspace.hpp @@ -0,0 +1,85 @@ +#pragma once + +#include +#include +#include +#include + +#include "../../core/model.hpp" +#include "../../windowmanager/workspace.hpp" +#include "ext_workspace.hpp" + +namespace qs::wm::wayland { +namespace impl = qs::wayland::workspace; + +class WlWorkspace; +class WlWorkspaceGroup; + +class WorkspaceManager: public QObject { + Q_OBJECT; + +public: + static WorkspaceManager* instance(); + + ObjectModel mWorkspaces {this}; + ObjectModel mWorkspaceGroups {this}; + + static void commit(); + +private slots: + void onServerCommit(); + void onWorkspaceCreated(impl::Workspace* workspace); + void onWorkspaceDestroyed(impl::Workspace* workspace); + void onGroupCreated(impl::WorkspaceGroup* group); + void onGroupDestroyed(impl::WorkspaceGroup* group); + +private: + WorkspaceManager(); + + QList pendingWorkspaceCreations; + QList pendingWorkspaceDestructions; + QHash workspaceByImpl; + + QList pendingGroupCreations; + QList pendingGroupDestructions; + QHash groupsByImpl; + + friend class WlWorkspace; +}; + +class WlWorkspace: public Workspace { +public: + WlWorkspace(WorkspaceManager* manager, impl::Workspace* impl); + + void commitImpl(); + + void activate() override; + void deactivate() override; + void remove() override; + void setGroup(WorkspaceGroup* group) override; + + [[nodiscard]] WorkspaceManager* manager() { + return static_cast(this->parent()); // NOLINT + } + +private: + impl::Workspace* impl = nullptr; +}; + +class WlWorkspaceGroup: public WorkspaceGroup { +public: + WlWorkspaceGroup(WorkspaceManager* manager, impl::WorkspaceGroup* impl); + + void commitImpl(); + + [[nodiscard]] WorkspaceManager* manager() { + return static_cast(this->parent()); // NOLINT + } + +private: + impl::WorkspaceGroup* impl = nullptr; + + friend class WlWorkspace; +}; + +} // namespace qs::wm::wayland diff --git a/src/wayland/wlr_layershell/shell_integration.hpp b/src/wayland/wlr_layershell/shell_integration.hpp index e92b7c6..93cda01 100644 --- a/src/wayland/wlr_layershell/shell_integration.hpp +++ b/src/wayland/wlr_layershell/shell_integration.hpp @@ -15,8 +15,8 @@ public: ~LayerShellIntegration() override; Q_DISABLE_COPY_MOVE(LayerShellIntegration); - QtWaylandClient::QWaylandShellSurface* createShellSurface(QtWaylandClient::QWaylandWindow* window - ) override; + QtWaylandClient::QWaylandShellSurface* + createShellSurface(QtWaylandClient::QWaylandWindow* window) override; }; } // namespace qs::wayland::layershell diff --git a/src/wayland/wlr_layershell/surface.cpp b/src/wayland/wlr_layershell/surface.cpp index 26d7558..4a5015e 100644 --- a/src/wayland/wlr_layershell/surface.cpp +++ b/src/wayland/wlr_layershell/surface.cpp @@ -30,8 +30,8 @@ namespace qs::wayland::layershell { namespace { -[[nodiscard]] QtWayland::zwlr_layer_shell_v1::layer toWaylandLayer(const WlrLayer::Enum& layer -) noexcept { +[[nodiscard]] QtWayland::zwlr_layer_shell_v1::layer +toWaylandLayer(const WlrLayer::Enum& layer) noexcept { switch (layer) { case WlrLayer::Background: return QtWayland::zwlr_layer_shell_v1::layer_background; case WlrLayer::Bottom: return QtWayland::zwlr_layer_shell_v1::layer_bottom; @@ -42,8 +42,8 @@ namespace { return QtWayland::zwlr_layer_shell_v1::layer_top; } -[[nodiscard]] QtWayland::zwlr_layer_surface_v1::anchor toWaylandAnchors(const Anchors& anchors -) noexcept { +[[nodiscard]] QtWayland::zwlr_layer_surface_v1::anchor +toWaylandAnchors(const Anchors& anchors) noexcept { quint32 wl = 0; if (anchors.mLeft) wl |= QtWayland::zwlr_layer_surface_v1::anchor_left; if (anchors.mRight) wl |= QtWayland::zwlr_layer_surface_v1::anchor_right; @@ -143,11 +143,11 @@ LayerSurface::LayerSurface(LayerShellIntegration* shell, QtWaylandClient::QWayla auto* waylandScreen = dynamic_cast(qwindow->screen()->handle()); - if (waylandScreen != nullptr) { + if (waylandScreen != nullptr && !waylandScreen->isPlaceholder() && waylandScreen->output()) { output = waylandScreen->output(); } else { - qWarning( - ) << "Layershell screen does not corrospond to a real screen. Letting the compositor pick."; + qWarning() + << "Layershell screen does not correspond to a real screen. Letting the compositor pick."; } } diff --git a/src/wayland/wlr_layershell/wlr_layershell.cpp b/src/wayland/wlr_layershell/wlr_layershell.cpp index e4726b5..947c51a 100644 --- a/src/wayland/wlr_layershell/wlr_layershell.cpp +++ b/src/wayland/wlr_layershell/wlr_layershell.cpp @@ -5,7 +5,6 @@ #include #include #include -#include #include #include "../../core/qmlscreen.hpp" @@ -27,11 +26,12 @@ WlrLayershell::WlrLayershell(QObject* parent): ProxyWindowBase(parent) { switch (this->bcExclusionEdge.value()) { case Qt::TopEdge: return this->bImplicitHeight + margins.bottom; case Qt::BottomEdge: return this->bImplicitHeight + margins.top; - case Qt::LeftEdge: return this->bImplicitHeight + margins.right; - case Qt::RightEdge: return this->bImplicitHeight + margins.left; - default: return 0; + case Qt::LeftEdge: return this->bImplicitWidth + margins.right; + case Qt::RightEdge: return this->bImplicitWidth + margins.left; } } + + return 0; }); this->bcExclusionEdge.setBinding([this] { return this->bAnchors.value().exclusionEdge(); }); @@ -173,23 +173,9 @@ WlrLayershell* WlrLayershell::qmlAttachedProperties(QObject* object) { WaylandPanelInterface::WaylandPanelInterface(QObject* parent) : PanelWindowInterface(parent) , layer(new WlrLayershell(this)) { + this->connectSignals(); // clang-format off - QObject::connect(this->layer, &ProxyWindowBase::windowConnected, this, &WaylandPanelInterface::windowConnected); - QObject::connect(this->layer, &ProxyWindowBase::visibleChanged, this, &WaylandPanelInterface::visibleChanged); - QObject::connect(this->layer, &ProxyWindowBase::backerVisibilityChanged, this, &WaylandPanelInterface::backingWindowVisibleChanged); - QObject::connect(this->layer, &ProxyWindowBase::implicitHeightChanged, this, &WaylandPanelInterface::implicitHeightChanged); - QObject::connect(this->layer, &ProxyWindowBase::implicitWidthChanged, this, &WaylandPanelInterface::implicitWidthChanged); - QObject::connect(this->layer, &ProxyWindowBase::heightChanged, this, &WaylandPanelInterface::heightChanged); - QObject::connect(this->layer, &ProxyWindowBase::widthChanged, this, &WaylandPanelInterface::widthChanged); - QObject::connect(this->layer, &ProxyWindowBase::devicePixelRatioChanged, this, &WaylandPanelInterface::devicePixelRatioChanged); - QObject::connect(this->layer, &ProxyWindowBase::screenChanged, this, &WaylandPanelInterface::screenChanged); - QObject::connect(this->layer, &ProxyWindowBase::windowTransformChanged, this, &WaylandPanelInterface::windowTransformChanged); - QObject::connect(this->layer, &ProxyWindowBase::colorChanged, this, &WaylandPanelInterface::colorChanged); - QObject::connect(this->layer, &ProxyWindowBase::maskChanged, this, &WaylandPanelInterface::maskChanged); - QObject::connect(this->layer, &ProxyWindowBase::surfaceFormatChanged, this, &WaylandPanelInterface::surfaceFormatChanged); - - // panel specific QObject::connect(this->layer, &WlrLayershell::anchorsChanged, this, &WaylandPanelInterface::anchorsChanged); QObject::connect(this->layer, &WlrLayershell::marginsChanged, this, &WaylandPanelInterface::marginsChanged); QObject::connect(this->layer, &WlrLayershell::exclusiveZoneChanged, this, &WaylandPanelInterface::exclusiveZoneChanged); @@ -206,32 +192,13 @@ void WaylandPanelInterface::onReload(QObject* oldInstance) { this->layer->reload(old != nullptr ? old->layer : nullptr); } -QQmlListProperty WaylandPanelInterface::data() { return this->layer->data(); } ProxyWindowBase* WaylandPanelInterface::proxyWindow() const { return this->layer; } -QQuickItem* WaylandPanelInterface::contentItem() const { return this->layer->contentItem(); } - -bool WaylandPanelInterface::isBackingWindowVisible() const { - return this->layer->isVisibleDirect(); -} - -qreal WaylandPanelInterface::devicePixelRatio() const { return this->layer->devicePixelRatio(); } // NOLINTBEGIN #define proxyPair(type, get, set) \ type WaylandPanelInterface::get() const { return this->layer->get(); } \ void WaylandPanelInterface::set(type value) { this->layer->set(value); } -proxyPair(bool, isVisible, setVisible); -proxyPair(qint32, implicitWidth, setImplicitWidth); -proxyPair(qint32, implicitHeight, setImplicitHeight); -proxyPair(qint32, width, setWidth); -proxyPair(qint32, height, setHeight); -proxyPair(QuickshellScreenInfo*, screen, setScreen); -proxyPair(QColor, color, setColor); -proxyPair(PendingRegion*, mask, setMask); -proxyPair(QsSurfaceFormat, surfaceFormat, setSurfaceFormat); - -// panel specific proxyPair(Anchors, anchors, setAnchors); proxyPair(Margins, margins, setMargins); proxyPair(qint32, exclusiveZone, setExclusiveZone); diff --git a/src/wayland/wlr_layershell/wlr_layershell.hpp b/src/wayland/wlr_layershell/wlr_layershell.hpp index 457a1f5..0f5617f 100644 --- a/src/wayland/wlr_layershell/wlr_layershell.hpp +++ b/src/wayland/wlr_layershell/wlr_layershell.hpp @@ -216,43 +216,8 @@ public: void onReload(QObject* oldInstance) override; [[nodiscard]] ProxyWindowBase* proxyWindow() const override; - [[nodiscard]] QQuickItem* contentItem() const override; // NOLINTBEGIN - [[nodiscard]] bool isVisible() const override; - [[nodiscard]] bool isBackingWindowVisible() const override; - void setVisible(bool visible) override; - - [[nodiscard]] qint32 implicitWidth() const override; - void setImplicitWidth(qint32 implicitWidth) override; - - [[nodiscard]] qint32 implicitHeight() const override; - void setImplicitHeight(qint32 implicitHeight) override; - - [[nodiscard]] qint32 width() const override; - void setWidth(qint32 width) override; - - [[nodiscard]] qint32 height() const override; - void setHeight(qint32 height) override; - - [[nodiscard]] virtual qreal devicePixelRatio() const override; - - [[nodiscard]] QuickshellScreenInfo* screen() const override; - void setScreen(QuickshellScreenInfo* screen) override; - - [[nodiscard]] QColor color() const override; - void setColor(QColor color) override; - - [[nodiscard]] PendingRegion* mask() const override; - void setMask(PendingRegion* mask) override; - - [[nodiscard]] QsSurfaceFormat surfaceFormat() const override; - void setSurfaceFormat(QsSurfaceFormat mask) override; - - [[nodiscard]] QQmlListProperty data() override; - - // panel specific - [[nodiscard]] Anchors anchors() const override; void setAnchors(Anchors anchors) override; diff --git a/src/widgets/ClippingRectangle.qml b/src/widgets/ClippingRectangle.qml index 86fe601..604f346 100644 --- a/src/widgets/ClippingRectangle.qml +++ b/src/widgets/ClippingRectangle.qml @@ -1,3 +1,5 @@ +pragma ComponentBehavior: Bound + import QtQuick ///! Rectangle capable of clipping content inside its border. @@ -72,6 +74,12 @@ Item { } } + ShaderEffectSource { + id: shaderSource + hideSource: true + sourceItem: contentItemContainer + } + ShaderEffect { id: shader anchors.fill: root @@ -79,10 +87,6 @@ Item { property Rectangle rect: rectangle property color backgroundColor: "white" property color borderColor: root.border.color - - property ShaderEffectSource content: ShaderEffectSource { - hideSource: true - sourceItem: contentItemContainer - } + property ShaderEffectSource content: shaderSource } } diff --git a/src/widgets/marginwrapper.cpp b/src/widgets/marginwrapper.cpp index 9960bba..b7d410c 100644 --- a/src/widgets/marginwrapper.cpp +++ b/src/widgets/marginwrapper.cpp @@ -12,8 +12,8 @@ namespace qs::widgets { MarginWrapperManager::MarginWrapperManager(QObject* parent): WrapperManager(parent) { this->bTopMargin.setBinding([this] { return this->bExtraMargin - + (this->bOverrides.value().testFlag(TopMargin) ? this->bTopMarginOverride : this->bMargin - ); + + (this->bOverrides.value().testFlag(TopMargin) ? this->bTopMarginOverride + : this->bMargin); }); this->bBottomMargin.setBinding([this] { diff --git a/src/widgets/wrapper.hpp b/src/widgets/wrapper.hpp index d506750..aca4172 100644 --- a/src/widgets/wrapper.hpp +++ b/src/widgets/wrapper.hpp @@ -85,6 +85,9 @@ class WrapperManager : public QObject , public QQmlParserStatus { Q_OBJECT; + QML_ELEMENT; + Q_INTERFACES(QQmlParserStatus); + // clang-format off /// The wrapper component's selected child. /// @@ -102,7 +105,6 @@ class WrapperManager /// This property may not be changed after Component.onCompleted. Q_PROPERTY(QQuickItem* wrapper READ wrapper WRITE setWrapper NOTIFY wrapperChanged FINAL); // clang-format on - QML_ELEMENT; public: explicit WrapperManager(QObject* parent = nullptr): QObject(parent) {} @@ -137,8 +139,8 @@ protected: void printChildCountWarning() const; void updateGeometry(); - virtual void disconnectChild() {}; - virtual void connectChild() {}; + virtual void disconnectChild() {} + virtual void connectChild() {} QQuickItem* mWrapper = nullptr; QQuickItem* mAssignedWrapper = nullptr; diff --git a/src/window/floatingwindow.cpp b/src/window/floatingwindow.cpp index 2f196fc..a0c9fdd 100644 --- a/src/window/floatingwindow.cpp +++ b/src/window/floatingwindow.cpp @@ -1,11 +1,12 @@ #include "floatingwindow.hpp" +#include #include #include #include -#include #include #include +#include #include "proxywindow.hpp" #include "windowinterface.hpp" @@ -50,24 +51,13 @@ void ProxyFloatingWindow::onMaximumSizeChanged() { FloatingWindowInterface::FloatingWindowInterface(QObject* parent) : WindowInterface(parent) , window(new ProxyFloatingWindow(this)) { - // clang-format off - QObject::connect(this->window, &ProxyWindowBase::windowConnected, this, &FloatingWindowInterface::windowConnected); - QObject::connect(this->window, &ProxyWindowBase::visibleChanged, this, &FloatingWindowInterface::visibleChanged); - QObject::connect(this->window, &ProxyWindowBase::backerVisibilityChanged, this, &FloatingWindowInterface::backingWindowVisibleChanged); - QObject::connect(this->window, &ProxyWindowBase::heightChanged, this, &FloatingWindowInterface::heightChanged); - QObject::connect(this->window, &ProxyWindowBase::widthChanged, this, &FloatingWindowInterface::widthChanged); - QObject::connect(this->window, &ProxyWindowBase::implicitHeightChanged, this, &FloatingWindowInterface::implicitHeightChanged); - QObject::connect(this->window, &ProxyWindowBase::implicitWidthChanged, this, &FloatingWindowInterface::implicitWidthChanged); - QObject::connect(this->window, &ProxyWindowBase::devicePixelRatioChanged, this, &FloatingWindowInterface::devicePixelRatioChanged); - QObject::connect(this->window, &ProxyWindowBase::screenChanged, this, &FloatingWindowInterface::screenChanged); - QObject::connect(this->window, &ProxyWindowBase::windowTransformChanged, this, &FloatingWindowInterface::windowTransformChanged); - QObject::connect(this->window, &ProxyWindowBase::colorChanged, this, &FloatingWindowInterface::colorChanged); - QObject::connect(this->window, &ProxyWindowBase::maskChanged, this, &FloatingWindowInterface::maskChanged); - QObject::connect(this->window, &ProxyWindowBase::surfaceFormatChanged, this, &FloatingWindowInterface::surfaceFormatChanged); + this->connectSignals(); + // clang-format off QObject::connect(this->window, &ProxyFloatingWindow::titleChanged, this, &FloatingWindowInterface::titleChanged); QObject::connect(this->window, &ProxyFloatingWindow::minimumSizeChanged, this, &FloatingWindowInterface::minimumSizeChanged); QObject::connect(this->window, &ProxyFloatingWindow::maximumSizeChanged, this, &FloatingWindowInterface::maximumSizeChanged); + QObject::connect(this->window, &ProxyWindowBase::windowConnected, this, &FloatingWindowInterface::onWindowConnected); // clang-format on } @@ -78,30 +68,104 @@ void FloatingWindowInterface::onReload(QObject* oldInstance) { this->window->reload(old != nullptr ? old->window : nullptr); } -QQmlListProperty FloatingWindowInterface::data() { return this->window->data(); } ProxyWindowBase* FloatingWindowInterface::proxyWindow() const { return this->window; } -QQuickItem* FloatingWindowInterface::contentItem() const { return this->window->contentItem(); } -bool FloatingWindowInterface::isBackingWindowVisible() const { - return this->window->isVisibleDirect(); +void FloatingWindowInterface::onWindowConnected() { + auto* qw = this->window->backingWindow(); + if (qw) { + QObject::connect( + qw, + &QWindow::windowStateChanged, + this, + &FloatingWindowInterface::onWindowStateChanged + ); + this->setMinimized(this->mMinimized); + this->setMaximized(this->mMaximized); + this->setFullscreen(this->mFullscreen); + this->onWindowStateChanged(); + } } -qreal FloatingWindowInterface::devicePixelRatio() const { return this->window->devicePixelRatio(); } +void FloatingWindowInterface::onWindowStateChanged() { + auto* qw = this->window->backingWindow(); + auto states = qw ? qw->windowStates() : Qt::WindowStates(); -// NOLINTBEGIN -#define proxyPair(type, get, set) \ - type FloatingWindowInterface::get() const { return this->window->get(); } \ - void FloatingWindowInterface::set(type value) { this->window->set(value); } + auto minimized = states.testFlag(Qt::WindowMinimized); + auto maximized = states.testFlag(Qt::WindowMaximized); + auto fullscreen = states.testFlag(Qt::WindowFullScreen); -proxyPair(bool, isVisible, setVisible); -proxyPair(qint32, implicitWidth, setImplicitWidth); -proxyPair(qint32, implicitHeight, setImplicitHeight); -proxyPair(qint32, width, setWidth); -proxyPair(qint32, height, setHeight); -proxyPair(QuickshellScreenInfo*, screen, setScreen); -proxyPair(QColor, color, setColor); -proxyPair(PendingRegion*, mask, setMask); -proxyPair(QsSurfaceFormat, surfaceFormat, setSurfaceFormat); + if (minimized != this->mWasMinimized) { + this->mWasMinimized = minimized; + emit this->minimizedChanged(); + } -#undef proxyPair -// NOLINTEND + if (maximized != this->mWasMaximized) { + this->mWasMaximized = maximized; + emit this->maximizedChanged(); + } + + if (fullscreen != this->mWasFullscreen) { + this->mWasFullscreen = fullscreen; + emit this->fullscreenChanged(); + } +} + +bool FloatingWindowInterface::isMinimized() const { + auto* qw = this->window->backingWindow(); + if (!qw) return this->mWasMinimized; + return qw->windowStates().testFlag(Qt::WindowMinimized); +} + +void FloatingWindowInterface::setMinimized(bool minimized) { + this->mMinimized = minimized; + + if (auto* qw = this->window->backingWindow()) { + auto states = qw->windowStates(); + states.setFlag(Qt::WindowMinimized, minimized); + qw->setWindowStates(states); + } +} + +bool FloatingWindowInterface::isMaximized() const { + auto* qw = this->window->backingWindow(); + if (!qw) return this->mWasMaximized; + return qw->windowStates().testFlag(Qt::WindowMaximized); +} + +void FloatingWindowInterface::setMaximized(bool maximized) { + this->mMaximized = maximized; + + if (auto* qw = this->window->backingWindow()) { + auto states = qw->windowStates(); + states.setFlag(Qt::WindowMaximized, maximized); + qw->setWindowStates(states); + } +} + +bool FloatingWindowInterface::isFullscreen() const { + auto* qw = this->window->backingWindow(); + if (!qw) return this->mWasFullscreen; + return qw->windowStates().testFlag(Qt::WindowFullScreen); +} + +void FloatingWindowInterface::setFullscreen(bool fullscreen) { + this->mFullscreen = fullscreen; + + if (auto* qw = this->window->backingWindow()) { + auto states = qw->windowStates(); + states.setFlag(Qt::WindowFullScreen, fullscreen); + qw->setWindowStates(states); + } +} + +bool FloatingWindowInterface::startSystemMove() const { + auto* qw = this->window->backingWindow(); + if (!qw) return false; + return qw->startSystemMove(); +} + +bool FloatingWindowInterface::startSystemResize(Qt::Edges edges) const { + auto* qw = this->window->backingWindow(); + if (!qw) return false; + return qw->startSystemResize(edges); +} diff --git a/src/window/floatingwindow.hpp b/src/window/floatingwindow.hpp index 48b7c13..06b5b9e 100644 --- a/src/window/floatingwindow.hpp +++ b/src/window/floatingwindow.hpp @@ -1,5 +1,6 @@ #pragma once +#include #include #include #include @@ -68,6 +69,12 @@ class FloatingWindowInterface: public WindowInterface { Q_PROPERTY(QSize minimumSize READ default WRITE default NOTIFY minimumSizeChanged BINDABLE bindableMinimumSize); /// Maximum window size given to the window system. Q_PROPERTY(QSize maximumSize READ default WRITE default NOTIFY maximumSizeChanged BINDABLE bindableMaximumSize); + /// Whether the window is currently minimized. + Q_PROPERTY(bool minimized READ isMinimized WRITE setMinimized NOTIFY minimizedChanged); + /// Whether the window is currently maximized. + Q_PROPERTY(bool maximized READ isMaximized WRITE setMaximized NOTIFY maximizedChanged); + /// Whether the window is currently fullscreen. + Q_PROPERTY(bool fullscreen READ isFullscreen WRITE setFullscreen NOTIFY fullscreenChanged); // clang-format on QML_NAMED_ELEMENT(FloatingWindow); @@ -77,51 +84,41 @@ public: void onReload(QObject* oldInstance) override; [[nodiscard]] ProxyWindowBase* proxyWindow() const override; - [[nodiscard]] QQuickItem* contentItem() const override; - // NOLINTBEGIN - [[nodiscard]] bool isVisible() const override; - [[nodiscard]] bool isBackingWindowVisible() const override; - void setVisible(bool visible) override; + [[nodiscard]] QBindable bindableMinimumSize() { return &this->window->bMinimumSize; } + [[nodiscard]] QBindable bindableMaximumSize() { return &this->window->bMaximumSize; } + [[nodiscard]] QBindable bindableTitle() { return &this->window->bTitle; } - [[nodiscard]] qint32 implicitWidth() const override; - void setImplicitWidth(qint32 implicitWidth) override; + [[nodiscard]] bool isMinimized() const; + void setMinimized(bool minimized); + [[nodiscard]] bool isMaximized() const; + void setMaximized(bool maximized); + [[nodiscard]] bool isFullscreen() const; + void setFullscreen(bool fullscreen); - [[nodiscard]] qint32 implicitHeight() const override; - void setImplicitHeight(qint32 implicitHeight) override; - - [[nodiscard]] qint32 width() const override; - void setWidth(qint32 width) override; - - [[nodiscard]] qint32 height() const override; - void setHeight(qint32 height) override; - - [[nodiscard]] virtual qreal devicePixelRatio() const override; - - [[nodiscard]] QuickshellScreenInfo* screen() const override; - void setScreen(QuickshellScreenInfo* screen) override; - - [[nodiscard]] QColor color() const override; - void setColor(QColor color) override; - - [[nodiscard]] PendingRegion* mask() const override; - void setMask(PendingRegion* mask) override; - - [[nodiscard]] QsSurfaceFormat surfaceFormat() const override; - void setSurfaceFormat(QsSurfaceFormat mask) override; - - [[nodiscard]] QQmlListProperty data() override; - // NOLINTEND - - QBindable bindableMinimumSize() { return &this->window->bMinimumSize; } - QBindable bindableMaximumSize() { return &this->window->bMaximumSize; } - QBindable bindableTitle() { return &this->window->bTitle; } + /// Start a system move operation. Must be called during a pointer press/drag. + Q_INVOKABLE [[nodiscard]] bool startSystemMove() const; + /// Start a system resize operation. Must be called during a pointer press/drag. + Q_INVOKABLE [[nodiscard]] bool startSystemResize(Qt::Edges edges) const; signals: void minimumSizeChanged(); void maximumSizeChanged(); void titleChanged(); + void minimizedChanged(); + void maximizedChanged(); + void fullscreenChanged(); + +private slots: + void onWindowConnected(); + void onWindowStateChanged(); private: ProxyFloatingWindow* window; + bool mMinimized = false; + bool mMaximized = false; + bool mFullscreen = false; + bool mWasMinimized = false; + bool mWasMaximized = false; + bool mWasFullscreen = false; }; diff --git a/src/window/popupwindow.cpp b/src/window/popupwindow.cpp index ec2be7e..bfe261e 100644 --- a/src/window/popupwindow.cpp +++ b/src/window/popupwindow.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include @@ -12,29 +13,74 @@ ProxyPopupWindow::ProxyPopupWindow(QObject* parent): ProxyWindowBase(parent) { this->mVisible = false; + // clang-format off - QObject::connect(&this->mAnchor, &PopupAnchor::windowChanged, this, &ProxyPopupWindow::parentWindowChanged); + QObject::connect(&this->mAnchor, &PopupAnchor::windowChanged, this, &ProxyPopupWindow::onParentWindowChanged); QObject::connect(&this->mAnchor, &PopupAnchor::windowRectChanged, this, &ProxyPopupWindow::reposition); QObject::connect(&this->mAnchor, &PopupAnchor::edgesChanged, this, &ProxyPopupWindow::reposition); QObject::connect(&this->mAnchor, &PopupAnchor::gravityChanged, this, &ProxyPopupWindow::reposition); QObject::connect(&this->mAnchor, &PopupAnchor::adjustmentChanged, this, &ProxyPopupWindow::reposition); - QObject::connect(&this->mAnchor, &PopupAnchor::backingWindowVisibilityChanged, this, &ProxyPopupWindow::onParentUpdated); // clang-format on + + this->bTargetVisible.setBinding([this] { + auto* window = this->mAnchor.bindableProxyWindow().value(); + + if (window == this) { + qmlWarning(this) << "Anchor assigned to current window"; + return false; + } + + if (!window) return false; + + if (!this->bWantsVisible) return false; + return window->bindableBackerVisibility().value(); + }); +} + +void ProxyPopupWindow::targetVisibleChanged() { + this->ProxyWindowBase::setVisible(this->bTargetVisible); } void ProxyPopupWindow::completeWindow() { this->ProxyWindowBase::completeWindow(); // clang-format off - QObject::connect(this->window, &QWindow::visibleChanged, this, &ProxyPopupWindow::onVisibleChanged); + QObject::connect(this, &ProxyWindowBase::closed, this, &ProxyPopupWindow::onClosed); QObject::connect(this->window, &QWindow::widthChanged, this, &ProxyPopupWindow::reposition); QObject::connect(this->window, &QWindow::heightChanged, this, &ProxyPopupWindow::reposition); // clang-format on - this->window->setFlag(Qt::ToolTip); + auto* bw = this->mAnchor.backingWindow(); + + if (bw && PopupPositioner::instance()->shouldRepositionOnMove()) { + QObject::connect(bw, &QWindow::xChanged, this, &ProxyPopupWindow::reposition); + QObject::connect(bw, &QWindow::yChanged, this, &ProxyPopupWindow::reposition); + QObject::connect(bw, &QWindow::widthChanged, this, &ProxyPopupWindow::reposition); + QObject::connect(bw, &QWindow::heightChanged, this, &ProxyPopupWindow::reposition); + } + + this->window->setTransientParent(bw); + this->window->setFlag(this->bWantsGrab ? Qt::Popup : Qt::ToolTip); + + this->mAnchor.markDirty(); + PopupPositioner::instance()->reposition(&this->mAnchor, this->window); } -void ProxyPopupWindow::postCompleteWindow() { this->updateTransientParent(); } +void ProxyPopupWindow::postCompleteWindow() { + this->ProxyWindowBase::setVisible(this->bTargetVisible); +} + +void ProxyPopupWindow::onClosed() { this->bWantsVisible = false; } + +void ProxyPopupWindow::onParentWindowChanged() { + // recreate for new parent + if (this->bTargetVisible && this->isVisibleDirect()) { + this->ProxyWindowBase::setVisibleDirect(false); + this->ProxyWindowBase::setVisibleDirect(true); + } + + emit this->parentWindowChanged(); +} void ProxyPopupWindow::setParentWindow(QObject* parent) { qmlWarning(this) << "PopupWindow.parentWindow is deprecated. Use PopupWindow.anchor.window."; @@ -43,59 +89,13 @@ void ProxyPopupWindow::setParentWindow(QObject* parent) { QObject* ProxyPopupWindow::parentWindow() const { return this->mAnchor.window(); } -void ProxyPopupWindow::updateTransientParent() { - auto* bw = this->mAnchor.backingWindow(); - - if (this->window != nullptr && bw != this->window->transientParent()) { - if (this->window->transientParent()) { - QObject::disconnect(this->window->transientParent(), nullptr, this, nullptr); - } - - if (bw && PopupPositioner::instance()->shouldRepositionOnMove()) { - QObject::connect(bw, &QWindow::xChanged, this, &ProxyPopupWindow::reposition); - QObject::connect(bw, &QWindow::yChanged, this, &ProxyPopupWindow::reposition); - QObject::connect(bw, &QWindow::widthChanged, this, &ProxyPopupWindow::reposition); - QObject::connect(bw, &QWindow::heightChanged, this, &ProxyPopupWindow::reposition); - } - - this->window->setTransientParent(bw); - } - - this->updateVisible(); -} - -void ProxyPopupWindow::onParentUpdated() { this->updateTransientParent(); } - void ProxyPopupWindow::setScreen(QuickshellScreenInfo* /*unused*/) { - qmlWarning(this + qmlWarning( + this ) << "Cannot set screen of popup window, as that is controlled by the parent window"; } -void ProxyPopupWindow::setVisible(bool visible) { - if (visible == this->wantsVisible) return; - this->wantsVisible = visible; - this->updateVisible(); -} - -void ProxyPopupWindow::updateVisible() { - auto target = this->wantsVisible && this->mAnchor.window() != nullptr - && this->mAnchor.proxyWindow()->isVisibleDirect(); - - if (target && this->window != nullptr && !this->window->isVisible()) { - PopupPositioner::instance()->reposition(&this->mAnchor, this->window); - } - - this->ProxyWindowBase::setVisible(target); -} - -void ProxyPopupWindow::onVisibleChanged() { - // If the window was made invisible without its parent becoming invisible - // the compositor probably destroyed it. Without this the window won't ever - // be able to become visible again. - if (this->window->transientParent() && this->window->transientParent()->isVisible()) { - this->wantsVisible = this->window->isVisible(); - } -} +void ProxyPopupWindow::setVisible(bool visible) { this->bWantsVisible = visible; } void ProxyPopupWindow::setRelativeX(qint32 x) { qmlWarning(this) << "PopupWindow.relativeX is deprecated. Use PopupWindow.anchor.rect.x."; @@ -143,3 +143,5 @@ void ProxyPopupWindow::onPolished() { } } } + +bool ProxyPopupWindow::deleteOnInvisible() const { return true; } diff --git a/src/window/popupwindow.hpp b/src/window/popupwindow.hpp index e00495c..d95eac0 100644 --- a/src/window/popupwindow.hpp +++ b/src/window/popupwindow.hpp @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include #include @@ -75,6 +76,15 @@ class ProxyPopupWindow: public ProxyWindowBase { /// /// The popup will not be shown until @@anchor is valid, regardless of this property. QSDOC_PROPERTY_OVERRIDE(bool visible READ isVisible WRITE setVisible NOTIFY visibleChanged); + /// If true, the popup window will be dismissed and @@visible will change to false + /// if the user clicks outside of the popup or it is otherwise closed. + /// + /// > [!WARNING] Changes to this property while the window is open will only take + /// > effect after the window is hidden and shown again. + /// + /// > [!NOTE] Under Hyprland, @@Quickshell.Hyprland.HyprlandFocusGrab provides more advanced + /// > functionality such as detecting clicks outside without closing the popup. + Q_PROPERTY(bool grabFocus READ default WRITE default NOTIFY grabFocusChanged BINDABLE bindableGrabFocus); /// The screen that the window currently occupies. /// /// This may be modified to move the window to the given screen. @@ -88,6 +98,7 @@ public: void completeWindow() override; void postCompleteWindow() override; void onPolished() override; + bool deleteOnInvisible() const override; void setScreen(QuickshellScreenInfo* screen) override; void setVisible(bool visible) override; @@ -101,24 +112,37 @@ public: [[nodiscard]] qint32 relativeY() const; void setRelativeY(qint32 y); + [[nodiscard]] QBindable bindableGrabFocus() { return &this->bWantsGrab; } + [[nodiscard]] PopupAnchor* anchor(); signals: void parentWindowChanged(); void relativeXChanged(); void relativeYChanged(); + void grabFocusChanged(); private slots: - void onVisibleChanged(); - void onParentUpdated(); + void onParentWindowChanged(); + void onClosed(); void reposition(); private: + void targetVisibleChanged(); + QQuickWindow* parentBackingWindow(); - void updateTransientParent(); - void updateVisible(); PopupAnchor mAnchor {this}; - bool wantsVisible = false; bool pendingReposition = false; + + Q_OBJECT_BINDABLE_PROPERTY(ProxyPopupWindow, bool, bWantsVisible); + + Q_OBJECT_BINDABLE_PROPERTY( + ProxyPopupWindow, + bool, + bTargetVisible, + &ProxyPopupWindow::targetVisibleChanged + ); + + Q_OBJECT_BINDABLE_PROPERTY(ProxyPopupWindow, bool, bWantsGrab); }; diff --git a/src/window/proxywindow.cpp b/src/window/proxywindow.cpp index 56d250c..62126bd 100644 --- a/src/window/proxywindow.cpp +++ b/src/window/proxywindow.cpp @@ -1,6 +1,7 @@ #include "proxywindow.hpp" #include +#include #include #include #include @@ -12,6 +13,7 @@ #include #include #include +#include #include #include #include @@ -56,9 +58,10 @@ ProxyWindowBase::ProxyWindowBase(QObject* parent) ProxyWindowBase::~ProxyWindowBase() { this->deleteWindow(true); } void ProxyWindowBase::onReload(QObject* oldInstance) { - this->window = this->retrieveWindow(oldInstance); + if (this->mVisible) this->window = this->retrieveWindow(oldInstance); auto wasVisible = this->window != nullptr && this->window->isVisible(); - this->ensureQWindow(); + + if (this->mVisible) this->ensureQWindow(); // The qml engine will leave the WindowInterface as owner of everything // nested in an item, so we have to make sure the interface's children @@ -75,17 +78,21 @@ void ProxyWindowBase::onReload(QObject* oldInstance) { Reloadable::reloadChildrenRecursive(this, oldInstance); - this->connectWindow(); - this->completeWindow(); + if (this->mVisible) { + this->connectWindow(); + this->completeWindow(); + } this->reloadComplete = true; - emit this->windowConnected(); - this->postCompleteWindow(); + if (this->mVisible) { + emit this->windowConnected(); + this->postCompleteWindow(); - if (wasVisible && this->isVisibleDirect()) { - emit this->backerVisibilityChanged(); - this->onExposed(); + if (wasVisible && this->isVisibleDirect()) { + this->bBackerVisibility = true; + this->onExposed(); + } } } @@ -112,6 +119,8 @@ void ProxyWindowBase::ensureQWindow() { auto opaque = this->qsSurfaceFormat.opaqueModified ? this->qsSurfaceFormat.opaque : this->mColor.alpha() >= 255; + format.setOption(QSurfaceFormat::ResetNotification); + if (opaque) format.setAlphaBufferSize(0); else format.setAlphaBufferSize(8); @@ -139,6 +148,15 @@ void ProxyWindowBase::ensureQWindow() { this->window = nullptr; // createQQuickWindow may indirectly reference this->window this->window = this->createQQuickWindow(); this->window->setFormat(format); + + // needed for vulkan dmabuf import, qt ignores these if not applicable + auto graphicsConfig = this->window->graphicsConfiguration(); + graphicsConfig.setDeviceExtensions({ + "VK_KHR_external_memory_fd", + "VK_EXT_external_memory_dma_buf", + "VK_EXT_image_drm_format_modifier", + }); + this->window->setGraphicsConfiguration(graphicsConfig); } void ProxyWindowBase::createWindow() { @@ -150,13 +168,7 @@ void ProxyWindowBase::createWindow() { void ProxyWindowBase::deleteWindow(bool keepItemOwnership) { if (this->window != nullptr) emit this->windowDestroyed(); - if (auto* window = this->disownWindow(keepItemOwnership)) { - if (auto* generation = EngineGeneration::findObjectGeneration(this)) { - generation->deregisterIncubationController(window->incubationController()); - } - - window->deleteLater(); - } + if (auto* window = this->disownWindow(keepItemOwnership)) window->deleteLater(); } ProxiedWindow* ProxyWindowBase::disownWindow(bool keepItemOwnership) { @@ -182,19 +194,20 @@ void ProxyWindowBase::connectWindow() { if (auto* generation = EngineGeneration::findObjectGeneration(this)) { // All windows have effectively the same incubation controller so it dosen't matter // which window it belongs to. We do want to replace the delay one though. - generation->registerIncubationController(this->window->incubationController()); + generation->trackWindowIncubationController(this->window); } this->window->setProxy(this); // clang-format off - QObject::connect(this->window, &QWindow::visibilityChanged, this, &ProxyWindowBase::visibleChanged); + QObject::connect(this->window, &QWindow::visibilityChanged, this, &ProxyWindowBase::onVisibleChanged); QObject::connect(this->window, &QWindow::xChanged, this, &ProxyWindowBase::xChanged); QObject::connect(this->window, &QWindow::yChanged, this, &ProxyWindowBase::yChanged); QObject::connect(this->window, &QWindow::widthChanged, this, &ProxyWindowBase::widthChanged); QObject::connect(this->window, &QWindow::heightChanged, this, &ProxyWindowBase::heightChanged); QObject::connect(this->window, &QWindow::screenChanged, this, &ProxyWindowBase::screenChanged); QObject::connect(this->window, &QQuickWindow::colorChanged, this, &ProxyWindowBase::colorChanged); + QObject::connect(this->window, &QQuickWindow::sceneGraphError, this, &ProxyWindowBase::onSceneGraphError); QObject::connect(this->window, &ProxiedWindow::exposed, this, &ProxyWindowBase::onExposed); QObject::connect(this->window, &ProxiedWindow::devicePixelRatioChanged, this, &ProxyWindowBase::devicePixelRatioChanged); // clang-format on @@ -210,6 +223,7 @@ void ProxyWindowBase::completeWindow() { this->trySetHeight(this->implicitHeight()); this->setColor(this->mColor); this->updateMask(); + QQuickWindowPrivate::get(this->window)->updatesEnabled = this->mUpdatesEnabled; // notify initial / post-connection geometry emit this->xChanged(); @@ -226,6 +240,32 @@ void ProxyWindowBase::completeWindow() { emit this->screenChanged(); } +void ProxyWindowBase::onSceneGraphError( + QQuickWindow::SceneGraphError error, + const QString& message +) { + if (error == QQuickWindow::ContextNotAvailable) { + qCritical().nospace() << "Failed to create graphics context for " << this << ": " << message; + } else { + qCritical().nospace() << "Scene graph error " << error << " occurred for " << this << ": " + << message; + } + + emit this->resourcesLost(); + this->mVisible = false; + this->setVisibleDirect(false); +} + +void ProxyWindowBase::onVisibleChanged() { + if (this->mVisible && !this->window->isVisible()) { + this->mVisible = false; + this->setVisibleDirect(false); + emit this->closed(); + } + + emit this->visibleChanged(); +} + bool ProxyWindowBase::deleteOnInvisible() const { return false; } QQuickWindow* ProxyWindowBase::backingWindow() const { return this->window; } @@ -248,24 +288,27 @@ void ProxyWindowBase::setVisible(bool visible) { void ProxyWindowBase::setVisibleDirect(bool visible) { if (this->deleteOnInvisible()) { - if (visible == this->isVisibleDirect()) return; - if (visible) { + if (visible == this->isVisibleDirect()) return; this->createWindow(); this->polishItems(); this->window->setVisible(true); - emit this->backerVisibilityChanged(); + this->bBackerVisibility = true; } else { - if (this->window != nullptr) { - this->window->setVisible(false); - emit this->backerVisibilityChanged(); - this->deleteWindow(); - } + if (this->window != nullptr) this->window->setVisible(false); + this->bBackerVisibility = false; + this->deleteWindow(); + } + } else { + if (visible && this->window == nullptr) { + this->createWindow(); + } + + if (this->window != nullptr) { + if (visible) this->polishItems(); + this->window->setVisible(visible); + this->bBackerVisibility = visible; } - } else if (this->window != nullptr) { - if (visible) this->polishItems(); - this->window->setVisible(visible); - emit this->backerVisibilityChanged(); } } @@ -437,6 +480,19 @@ void ProxyWindowBase::setSurfaceFormat(QsSurfaceFormat format) { emit this->surfaceFormatChanged(); } +bool ProxyWindowBase::updatesEnabled() const { return this->mUpdatesEnabled; } + +void ProxyWindowBase::setUpdatesEnabled(bool updatesEnabled) { + if (updatesEnabled == this->mUpdatesEnabled) return; + this->mUpdatesEnabled = updatesEnabled; + + if (this->window != nullptr) { + QQuickWindowPrivate::get(this->window)->updatesEnabled = updatesEnabled; + } + + emit this->updatesEnabledChanged(); +} + qreal ProxyWindowBase::devicePixelRatio() const { if (this->window != nullptr) return this->window->devicePixelRatio(); if (this->mScreen != nullptr) return this->mScreen->devicePixelRatio(); diff --git a/src/window/proxywindow.hpp b/src/window/proxywindow.hpp index 3fbc08e..aec821e 100644 --- a/src/window/proxywindow.hpp +++ b/src/window/proxywindow.hpp @@ -57,6 +57,7 @@ class ProxyWindowBase: public Reloadable { Q_PROPERTY(QObject* windowTransform READ windowTransform NOTIFY windowTransformChanged); Q_PROPERTY(bool backingWindowVisible READ isVisibleDirect NOTIFY backerVisibilityChanged); Q_PROPERTY(QsSurfaceFormat surfaceFormat READ surfaceFormat WRITE setSurfaceFormat NOTIFY surfaceFormatChanged); + Q_PROPERTY(bool updatesEnabled READ updatesEnabled WRITE setUpdatesEnabled NOTIFY updatesEnabledChanged); Q_PROPERTY(QQmlListProperty data READ data); // clang-format on Q_CLASSINFO("DefaultProperty", "data"); @@ -101,6 +102,10 @@ public: virtual void setVisible(bool visible); virtual void setVisibleDirect(bool visible); + [[nodiscard]] QBindable bindableBackerVisibility() const { + return &this->bBackerVisibility; + } + void schedulePolish(); [[nodiscard]] virtual qint32 x() const; @@ -136,11 +141,16 @@ public: [[nodiscard]] QsSurfaceFormat surfaceFormat() const { return this->qsSurfaceFormat; } void setSurfaceFormat(QsSurfaceFormat format); + [[nodiscard]] bool updatesEnabled() const; + void setUpdatesEnabled(bool updatesEnabled); + [[nodiscard]] QObject* windowTransform() const { return nullptr; } // NOLINT [[nodiscard]] QQmlListProperty data(); signals: + void closed(); + void resourcesLost(); void windowConnected(); void windowDestroyed(); void visibleChanged(); @@ -157,15 +167,20 @@ signals: void colorChanged(); void maskChanged(); void surfaceFormatChanged(); + void updatesEnabledChanged(); void polished(); protected slots: virtual void onWidthChanged(); virtual void onHeightChanged(); + virtual void onPolished(); + +private slots: + void onSceneGraphError(QQuickWindow::SceneGraphError error, const QString& message); + void onVisibleChanged(); void onMaskChanged(); void onMaskDestroyed(); void onScreenDestroyed(); - virtual void onPolished(); void onExposed(); protected: @@ -177,6 +192,7 @@ protected: ProxyWindowContentItem* mContentItem = nullptr; bool reloadComplete = false; bool ranLints = false; + bool mUpdatesEnabled = true; QsSurfaceFormat qsSurfaceFormat; QSurfaceFormat mSurfaceFormat; @@ -200,6 +216,13 @@ protected: &ProxyWindowBase::implicitHeightChanged ); + Q_OBJECT_BINDABLE_PROPERTY( + ProxyWindowBase, + bool, + bBackerVisibility, + &ProxyWindowBase::backerVisibilityChanged + ); + private: void polishItems(); void updateMask(); diff --git a/src/window/test/popupwindow.cpp b/src/window/test/popupwindow.cpp index 1262044..f9498d2 100644 --- a/src/window/test/popupwindow.cpp +++ b/src/window/test/popupwindow.cpp @@ -13,7 +13,7 @@ void TestPopupWindow::initiallyVisible() { // NOLINT auto parent = ProxyWindowBase(); auto popup = ProxyPopupWindow(); - popup.setParentWindow(&parent); + popup.anchor()->setWindow(&parent); popup.setVisible(true); parent.reload(); @@ -33,7 +33,7 @@ void TestPopupWindow::reloadReparent() { // NOLINT win2->setVisible(true); parent.setVisible(true); - popup.setParentWindow(&parent); + popup.anchor()->setWindow(&parent); popup.setVisible(true); parent.reload(); @@ -43,7 +43,7 @@ void TestPopupWindow::reloadReparent() { // NOLINT auto newParent = ProxyWindowBase(); auto newPopup = ProxyPopupWindow(); - newPopup.setParentWindow(&newParent); + newPopup.anchor()->setWindow(&newParent); newPopup.setVisible(true); auto* oldWindow = popup.backingWindow(); @@ -66,7 +66,7 @@ void TestPopupWindow::reloadUnparent() { // NOLINT auto parent = ProxyWindowBase(); auto popup = ProxyPopupWindow(); - popup.setParentWindow(&parent); + popup.anchor()->setWindow(&parent); popup.setVisible(true); parent.reload(); @@ -80,8 +80,7 @@ void TestPopupWindow::reloadUnparent() { // NOLINT newPopup.reload(&popup); QVERIFY(!newPopup.isVisible()); - QVERIFY(!newPopup.backingWindow()->isVisible()); - QCOMPARE(newPopup.backingWindow()->transientParent(), nullptr); + QVERIFY(!newPopup.backingWindow() || !newPopup.backingWindow()->isVisible()); } void TestPopupWindow::invisibleWithoutParent() { // NOLINT @@ -97,9 +96,11 @@ void TestPopupWindow::moveWithParent() { // NOLINT auto parent = ProxyWindowBase(); auto popup = ProxyPopupWindow(); - popup.setParentWindow(&parent); - popup.setRelativeX(10); - popup.setRelativeY(10); + popup.anchor()->setWindow(&parent); + auto rect = popup.anchor()->rect(); + rect.x = 10; + rect.y = 10; + popup.anchor()->setRect(rect); popup.setVisible(true); parent.reload(); @@ -126,7 +127,7 @@ void TestPopupWindow::attachParentLate() { // NOLINT QVERIFY(!popup.isVisible()); - popup.setParentWindow(&parent); + popup.anchor()->setWindow(&parent); QVERIFY(popup.isVisible()); QVERIFY(popup.backingWindow()->isVisible()); QCOMPARE(popup.backingWindow()->transientParent(), parent.backingWindow()); @@ -136,7 +137,7 @@ void TestPopupWindow::reparentLate() { // NOLINT auto parent = ProxyWindowBase(); auto popup = ProxyPopupWindow(); - popup.setParentWindow(&parent); + popup.anchor()->setWindow(&parent); popup.setVisible(true); parent.reload(); @@ -151,7 +152,7 @@ void TestPopupWindow::reparentLate() { // NOLINT parent2.backingWindow()->setX(10); parent2.backingWindow()->setY(10); - popup.setParentWindow(&parent2); + popup.anchor()->setWindow(&parent2); QVERIFY(popup.isVisible()); QVERIFY(popup.backingWindow()->isVisible()); QCOMPARE(popup.backingWindow()->transientParent(), parent2.backingWindow()); @@ -163,7 +164,7 @@ void TestPopupWindow::xMigrationFix() { // NOLINT auto parent = ProxyWindowBase(); auto popup = ProxyPopupWindow(); - popup.setParentWindow(&parent); + popup.anchor()->setWindow(&parent); popup.setVisible(true); parent.reload(); diff --git a/src/window/windowinterface.cpp b/src/window/windowinterface.cpp index 20057d6..e41afc2 100644 --- a/src/window/windowinterface.cpp +++ b/src/window/windowinterface.cpp @@ -2,9 +2,12 @@ #include #include +#include #include #include +#include "../core/qmlscreen.hpp" +#include "../core/region.hpp" #include "proxywindow.hpp" QPointF WindowInterface::itemPosition(QQuickItem* item) const { @@ -91,6 +94,67 @@ QsWindowAttached::mapFromItem(QQuickItem* item, qreal x, qreal y, qreal width, q } } +// clang-format off +QQuickItem* WindowInterface::contentItem() const { return this->proxyWindow()->contentItem(); } + +bool WindowInterface::isVisible() const { return this->proxyWindow()->isVisible(); }; +bool WindowInterface::isBackingWindowVisible() const { return this->proxyWindow()->isVisibleDirect(); }; +void WindowInterface::setVisible(bool visible) const { this->proxyWindow()->setVisible(visible); }; + +qint32 WindowInterface::implicitWidth() const { return this->proxyWindow()->implicitWidth(); }; +void WindowInterface::setImplicitWidth(qint32 implicitWidth) const { this->proxyWindow()->setImplicitWidth(implicitWidth); }; + +qint32 WindowInterface::implicitHeight() const { return this->proxyWindow()->implicitHeight(); }; +void WindowInterface::setImplicitHeight(qint32 implicitHeight) const { this->proxyWindow()->setImplicitHeight(implicitHeight); }; + +qint32 WindowInterface::width() const { return this->proxyWindow()->width(); }; +void WindowInterface::setWidth(qint32 width) const { this->proxyWindow()->setWidth(width); }; + +qint32 WindowInterface::height() const { return this->proxyWindow()->height(); }; +void WindowInterface::setHeight(qint32 height) const { this->proxyWindow()->setHeight(height); }; + +qreal WindowInterface::devicePixelRatio() const { return this->proxyWindow()->devicePixelRatio(); }; + +QuickshellScreenInfo* WindowInterface::screen() const { return this->proxyWindow()->screen(); }; +void WindowInterface::setScreen(QuickshellScreenInfo* screen) const { this->proxyWindow()->setScreen(screen); }; + +QColor WindowInterface::color() const { return this->proxyWindow()->color(); }; +void WindowInterface::setColor(QColor color) const { this->proxyWindow()->setColor(color); }; + +PendingRegion* WindowInterface::mask() const { return this->proxyWindow()->mask(); }; +void WindowInterface::setMask(PendingRegion* mask) const { this->proxyWindow()->setMask(mask); }; + +QsSurfaceFormat WindowInterface::surfaceFormat() const { return this->proxyWindow()->surfaceFormat(); }; +void WindowInterface::setSurfaceFormat(QsSurfaceFormat format) const { this->proxyWindow()->setSurfaceFormat(format); }; + +bool WindowInterface::updatesEnabled() const { return this->proxyWindow()->updatesEnabled(); }; +void WindowInterface::setUpdatesEnabled(bool updatesEnabled) const { this->proxyWindow()->setUpdatesEnabled(updatesEnabled); }; + +QQmlListProperty WindowInterface::data() const { return this->proxyWindow()->data(); }; +// clang-format on + +void WindowInterface::connectSignals() const { + auto* window = this->proxyWindow(); + // clang-format off + QObject::connect(window, &ProxyWindowBase::closed, this, &WindowInterface::closed); + QObject::connect(window, &ProxyWindowBase::resourcesLost, this, &WindowInterface::resourcesLost); + QObject::connect(window, &ProxyWindowBase::windowConnected, this, &WindowInterface::windowConnected); + QObject::connect(window, &ProxyWindowBase::visibleChanged, this, &WindowInterface::visibleChanged); + QObject::connect(window, &ProxyWindowBase::backerVisibilityChanged, this, &WindowInterface::backingWindowVisibleChanged); + QObject::connect(window, &ProxyWindowBase::implicitHeightChanged, this, &WindowInterface::implicitHeightChanged); + QObject::connect(window, &ProxyWindowBase::implicitWidthChanged, this, &WindowInterface::implicitWidthChanged); + QObject::connect(window, &ProxyWindowBase::heightChanged, this, &WindowInterface::heightChanged); + QObject::connect(window, &ProxyWindowBase::widthChanged, this, &WindowInterface::widthChanged); + QObject::connect(window, &ProxyWindowBase::devicePixelRatioChanged, this, &WindowInterface::devicePixelRatioChanged); + QObject::connect(window, &ProxyWindowBase::screenChanged, this, &WindowInterface::screenChanged); + QObject::connect(window, &ProxyWindowBase::windowTransformChanged, this, &WindowInterface::windowTransformChanged); + QObject::connect(window, &ProxyWindowBase::colorChanged, this, &WindowInterface::colorChanged); + QObject::connect(window, &ProxyWindowBase::maskChanged, this, &WindowInterface::maskChanged); + QObject::connect(window, &ProxyWindowBase::surfaceFormatChanged, this, &WindowInterface::surfaceFormatChanged); + QObject::connect(window, &ProxyWindowBase::updatesEnabledChanged, this, &WindowInterface::updatesEnabledChanged); + // clang-format on +} + QsWindowAttached* WindowInterface::qmlAttachedProperties(QObject* object) { while (object && !qobject_cast(object)) { object = object->parent(); diff --git a/src/window/windowinterface.hpp b/src/window/windowinterface.hpp index b8edff2..6f3db20 100644 --- a/src/window/windowinterface.hpp +++ b/src/window/windowinterface.hpp @@ -143,6 +143,12 @@ class WindowInterface: public Reloadable { /// /// > [!NOTE] The surface format cannot be changed after the window is created. Q_PROPERTY(QsSurfaceFormat surfaceFormat READ surfaceFormat WRITE setSurfaceFormat NOTIFY surfaceFormatChanged); + /// If the window should receive render updates. Defaults to true. + /// + /// When set to false, the window will not re-render in response to animations + /// or other visual updates from other windows. This is useful for static windows + /// such as wallpapers that do not need to update frequently, saving GPU cycles. + Q_PROPERTY(bool updatesEnabled READ updatesEnabled WRITE setUpdatesEnabled NOTIFY updatesEnabledChanged); Q_PROPERTY(QQmlListProperty data READ data); // clang-format on Q_CLASSINFO("DefaultProperty", "data"); @@ -197,45 +203,57 @@ public: // clang-format on [[nodiscard]] virtual ProxyWindowBase* proxyWindow() const = 0; - [[nodiscard]] virtual QQuickItem* contentItem() const = 0; + [[nodiscard]] QQuickItem* contentItem() const; - [[nodiscard]] virtual bool isVisible() const = 0; - [[nodiscard]] virtual bool isBackingWindowVisible() const = 0; - virtual void setVisible(bool visible) = 0; + [[nodiscard]] bool isVisible() const; + [[nodiscard]] bool isBackingWindowVisible() const; + void setVisible(bool visible) const; - [[nodiscard]] virtual qint32 implicitWidth() const = 0; - virtual void setImplicitWidth(qint32 implicitWidth) = 0; + [[nodiscard]] qint32 implicitWidth() const; + void setImplicitWidth(qint32 implicitWidth) const; - [[nodiscard]] virtual qint32 implicitHeight() const = 0; - virtual void setImplicitHeight(qint32 implicitHeight) = 0; + [[nodiscard]] qint32 implicitHeight() const; + void setImplicitHeight(qint32 implicitHeight) const; - [[nodiscard]] virtual qint32 width() const = 0; - virtual void setWidth(qint32 width) = 0; + [[nodiscard]] qint32 width() const; + void setWidth(qint32 width) const; - [[nodiscard]] virtual qint32 height() const = 0; - virtual void setHeight(qint32 height) = 0; + [[nodiscard]] qint32 height() const; + void setHeight(qint32 height) const; - [[nodiscard]] virtual qreal devicePixelRatio() const = 0; + [[nodiscard]] qreal devicePixelRatio() const; - [[nodiscard]] virtual QuickshellScreenInfo* screen() const = 0; - virtual void setScreen(QuickshellScreenInfo* screen) = 0; + [[nodiscard]] QuickshellScreenInfo* screen() const; + void setScreen(QuickshellScreenInfo* screen) const; [[nodiscard]] QObject* windowTransform() const { return nullptr; } // NOLINT - [[nodiscard]] virtual QColor color() const = 0; - virtual void setColor(QColor color) = 0; + [[nodiscard]] QColor color() const; + void setColor(QColor color) const; - [[nodiscard]] virtual PendingRegion* mask() const = 0; - virtual void setMask(PendingRegion* mask) = 0; + [[nodiscard]] PendingRegion* mask() const; + void setMask(PendingRegion* mask) const; - [[nodiscard]] virtual QsSurfaceFormat surfaceFormat() const = 0; - virtual void setSurfaceFormat(QsSurfaceFormat format) = 0; + [[nodiscard]] QsSurfaceFormat surfaceFormat() const; + void setSurfaceFormat(QsSurfaceFormat format) const; - [[nodiscard]] virtual QQmlListProperty data() = 0; + [[nodiscard]] bool updatesEnabled() const; + void setUpdatesEnabled(bool updatesEnabled) const; + + [[nodiscard]] QQmlListProperty data() const; static QsWindowAttached* qmlAttachedProperties(QObject* object); signals: + /// This signal is emitted when the window is closed by the user, the display server, + /// or an error. It is not emitted when @@visible is set to false. + void closed(); + /// This signal is emitted when resources a window depends on to display are lost, + /// or could not be acquired during window creation. The most common trigger for + /// this signal is a lack of VRAM when creating or resizing a window. + /// + /// Following this signal, @@closed(s) will be sent. + void resourcesLost(); void windowConnected(); void visibleChanged(); void backingWindowVisibleChanged(); @@ -249,6 +267,10 @@ signals: void colorChanged(); void maskChanged(); void surfaceFormatChanged(); + void updatesEnabledChanged(); + +protected: + void connectSignals() const; }; class QsWindowAttached: public QObject { diff --git a/src/windowmanager/CMakeLists.txt b/src/windowmanager/CMakeLists.txt new file mode 100644 index 0000000..365c43a --- /dev/null +++ b/src/windowmanager/CMakeLists.txt @@ -0,0 +1,18 @@ +qt_add_library(quickshell-windowmanager STATIC + windowmanager.cpp + workspace.cpp + workspacemodel.cpp +) + +qt_add_qml_module(quickshell-windowmanager + URI Quickshell.WindowManager + VERSION 0.1 + DEPENDENCIES QtQuick +) + +qs_add_module_deps_light(quickshell-windowmanager Quickshell) + +install_qml_module(quickshell-windowmanager) + +target_link_libraries(quickshell-windowmanager PRIVATE Qt::Quick) +target_link_libraries(quickshell PRIVATE quickshell-windowmanager) diff --git a/src/windowmanager/test/manual/workspaces.qml b/src/windowmanager/test/manual/workspaces.qml new file mode 100644 index 0000000..b379b47 --- /dev/null +++ b/src/windowmanager/test/manual/workspaces.qml @@ -0,0 +1,112 @@ +import QtQuick +import QtQuick.Controls.Fusion +import QtQuick.Layouts +import Quickshell +import Quickshell.Widgets +import Quickshell.WindowManager + +FloatingWindow { + ScrollView { + anchors.fill: parent + + ColumnLayout { + Repeater { + model: WindowManager.workspaceGroups + + WrapperRectangle { + id: delegate + required property WorkspaceGroup modelData + color: "slategray" + margin: 5 + + ColumnLayout { + Label { text: delegate.modelData.toString() } + Label { text: `Screens: ${delegate.modelData.screens.map(s => s.name)}` } + + Repeater { + model: ScriptModel { + values: [...WindowManager.workspaces.values].filter(w => w.group == delegate.modelData) + } + + WorkspaceDelegate {} + } + } + } + } + + Repeater { + model: ScriptModel { + values: WindowManager.workspaces.values.filter(w => w.group == null) + } + + WorkspaceDelegate {} + } + } + } + + component WorkspaceDelegate: WrapperRectangle { + id: delegate + required property Workspace modelData; + color: modelData.active ? "green" : "gray" + + ColumnLayout { + Label { text: delegate.modelData.toString() } + Label { text: `Id: ${delegate.modelData.id} Name: ${delegate.modelData.name}` } + + RowLayout { + Label { text: "Group:" } + ComboBox { + Layout.fillWidth: true + implicitContentWidthPolicy: ComboBox.WidestText + enabled: delegate.modelData.canSetGroup + model: [...WindowManager.workspaceGroups.values].map(w => w.toString()) + currentIndex: WindowManager.workspaceGroups.values.indexOf(delegate.modelData.group) + onActivated: i => delegate.modelData.setGroup(WindowManager.workspaceGroups.values[i]) + } + } + + RowLayout { + DisplayCheckBox { + text: "Active" + checked: delegate.modelData.active + } + + DisplayCheckBox { + text: "Urgent" + checked: delegate.modelData.urgent + } + + DisplayCheckBox { + text: "Should Display" + checked: delegate.modelData.shouldDisplay + } + } + + RowLayout { + Button { + text: "Activate" + enabled: delegate.modelData.canActivate + onClicked: delegate.modelData.activate() + } + + Button { + text: "Deactivate" + enabled: delegate.modelData.canDeactivate + onClicked: delegate.modelData.deactivate() + } + + Button { + text: "Remove" + enabled: delegate.modelData.canRemove + onClicked: delegate.modelData.remove() + } + } + } + } + + component DisplayCheckBox: CheckBox { + enabled: false + palette.disabled.text: parent.palette.active.text + palette.disabled.windowText: parent.palette.active.windowText + } +} diff --git a/src/windowmanager/windowmanager.cpp b/src/windowmanager/windowmanager.cpp new file mode 100644 index 0000000..7c6a6cc --- /dev/null +++ b/src/windowmanager/windowmanager.cpp @@ -0,0 +1,18 @@ +#include "windowmanager.hpp" +#include +#include + +namespace qs::wm { + +std::function WindowManager::provider; + +void WindowManager::setProvider(std::function provider) { + WindowManager::provider = std::move(provider); +} + +WindowManager* WindowManager::instance() { + static auto* instance = WindowManager::provider(); + return instance; +} + +} // namespace qs::wm diff --git a/src/windowmanager/windowmanager.hpp b/src/windowmanager/windowmanager.hpp new file mode 100644 index 0000000..03ebfc1 --- /dev/null +++ b/src/windowmanager/windowmanager.hpp @@ -0,0 +1,50 @@ +#pragma once + +#include + +#include +#include +#include + +#include "../core/model.hpp" +#include "workspace.hpp" + +namespace qs::wm { + +class WindowManager: public QObject { + Q_OBJECT; + +public: + static void setProvider(std::function provider); + static WindowManager* instance(); + + [[nodiscard]] virtual UntypedObjectModel* workspaces() const { + return UntypedObjectModel::emptyInstance(); + } + + [[nodiscard]] virtual UntypedObjectModel* workspaceGroups() const { + return UntypedObjectModel::emptyInstance(); + } + +private: + static std::function provider; +}; + +class WindowManagerQml: public QObject { + Q_OBJECT; + QML_NAMED_ELEMENT(WindowManager); + QML_SINGLETON; + Q_PROPERTY(UntypedObjectModel* workspaces READ workspaces CONSTANT); + Q_PROPERTY(UntypedObjectModel* workspaceGroups READ workspaceGroups CONSTANT); + +public: + [[nodiscard]] static UntypedObjectModel* workspaces() { + return WindowManager::instance()->workspaces(); + } + + [[nodiscard]] static UntypedObjectModel* workspaceGroups() { + return WindowManager::instance()->workspaceGroups(); + } +}; + +} // namespace qs::wm diff --git a/src/windowmanager/workspace.cpp b/src/windowmanager/workspace.cpp new file mode 100644 index 0000000..472234e --- /dev/null +++ b/src/windowmanager/workspace.cpp @@ -0,0 +1,31 @@ +#include "workspace.hpp" + +#include +#include +#include + +#include "../core/qmlglobal.hpp" + +namespace qs::wm { + +Q_LOGGING_CATEGORY(logWorkspace, "quickshell.wm.workspace", QtWarningMsg); + +void Workspace::activate() { qCCritical(logWorkspace) << this << "cannot be activated"; } +void Workspace::deactivate() { qCCritical(logWorkspace) << this << "cannot be deactivated"; } +void Workspace::remove() { qCCritical(logWorkspace) << this << "cannot be removed"; } + +void Workspace::setGroup(WorkspaceGroup* /*group*/) { + qCCritical(logWorkspace) << this << "cannot be assigned to a group"; +} + +void WorkspaceGroup::onScreensChanged() { + mCachedScreens.clear(); + + for (auto* screen: this->bScreens.value()) { + mCachedScreens.append(QuickshellTracked::instance()->screenInfo(screen)); + } + + emit this->screensChanged(); +} + +} // namespace qs::wm diff --git a/src/windowmanager/workspace.hpp b/src/windowmanager/workspace.hpp new file mode 100644 index 0000000..aa148a0 --- /dev/null +++ b/src/windowmanager/workspace.hpp @@ -0,0 +1,119 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +class QuickshellScreenInfo; + +namespace qs::wm { + +Q_DECLARE_LOGGING_CATEGORY(logWorkspace); + +class WorkspaceGroup; + +class Workspace: public QObject { + Q_OBJECT; + QML_ELEMENT; + QML_UNCREATABLE(""); + // clang-format off + // persistent id + Q_PROPERTY(QString id READ default BINDABLE bindableId NOTIFY idChanged); + Q_PROPERTY(QString name READ default BINDABLE bindableName NOTIFY nameChanged); + // currently visible + Q_PROPERTY(bool active READ default BINDABLE bindableActive NOTIFY activeChanged); + Q_PROPERTY(WorkspaceGroup* group READ default BINDABLE bindableGroup NOTIFY groupChanged); + // in workspace pickers + Q_PROPERTY(bool shouldDisplay READ default BINDABLE bindableShouldDisplay NOTIFY shouldDisplayChanged); + Q_PROPERTY(bool urgent READ default BINDABLE bindableUrgent NOTIFY urgentChanged); + Q_PROPERTY(bool canActivate READ default BINDABLE bindableCanActivate NOTIFY canActivateChanged); + Q_PROPERTY(bool canDeactivate READ default BINDABLE bindableCanDeactivate NOTIFY canDeactivateChanged); + Q_PROPERTY(bool canRemove READ default BINDABLE bindableCanRemove NOTIFY canRemoveChanged); + Q_PROPERTY(bool canSetGroup READ default BINDABLE bindableCanSetGroup NOTIFY canSetGroupChanged); + // clang-format on + +public: + explicit Workspace(QObject* parent): QObject(parent) {} + + Q_INVOKABLE virtual void activate(); + Q_INVOKABLE virtual void deactivate(); + Q_INVOKABLE virtual void remove(); + Q_INVOKABLE virtual void setGroup(WorkspaceGroup* group); + + [[nodiscard]] QBindable bindableId() const { return &this->bId; } + [[nodiscard]] QBindable bindableName() const { return &this->bName; } + [[nodiscard]] QBindable bindableActive() const { return &this->bActive; } + [[nodiscard]] QBindable bindableGroup() const { return &this->bGroup; } + [[nodiscard]] QBindable bindableShouldDisplay() const { return &this->bShouldDisplay; } + [[nodiscard]] QBindable bindableUrgent() const { return &this->bUrgent; } + [[nodiscard]] QBindable bindableCanActivate() const { return &this->bCanActivate; } + [[nodiscard]] QBindable bindableCanDeactivate() const { return &this->bCanDeactivate; } + [[nodiscard]] QBindable bindableCanRemove() const { return &this->bCanRemove; } + [[nodiscard]] QBindable bindableCanSetGroup() const { return &this->bCanSetGroup; } + +signals: + void idChanged(); + void nameChanged(); + void activeChanged(); + void groupChanged(); + void shouldDisplayChanged(); + void urgentChanged(); + void canActivateChanged(); + void canDeactivateChanged(); + void canRemoveChanged(); + void canSetGroupChanged(); + +protected: + Q_OBJECT_BINDABLE_PROPERTY(Workspace, QString, bId, &Workspace::idChanged); + Q_OBJECT_BINDABLE_PROPERTY(Workspace, QString, bName, &Workspace::nameChanged); + Q_OBJECT_BINDABLE_PROPERTY(Workspace, bool, bActive, &Workspace::activeChanged); + Q_OBJECT_BINDABLE_PROPERTY(Workspace, WorkspaceGroup*, bGroup, &Workspace::groupChanged); + Q_OBJECT_BINDABLE_PROPERTY(Workspace, bool, bShouldDisplay, &Workspace::shouldDisplayChanged); + Q_OBJECT_BINDABLE_PROPERTY(Workspace, bool, bUrgent, &Workspace::urgentChanged); + Q_OBJECT_BINDABLE_PROPERTY(Workspace, bool, bCanActivate, &Workspace::canActivateChanged); + Q_OBJECT_BINDABLE_PROPERTY(Workspace, bool, bCanDeactivate, &Workspace::canDeactivateChanged); + Q_OBJECT_BINDABLE_PROPERTY(Workspace, bool, bCanRemove, &Workspace::canRemoveChanged); + Q_OBJECT_BINDABLE_PROPERTY(Workspace, bool, bCanSetGroup, &Workspace::canSetGroupChanged); + //Q_OBJECT_BINDABLE_PROPERTY(Workspace, qint32, bIndex); +}; + +class WorkspaceGroup: public QObject { + Q_OBJECT; + QML_ELEMENT; + QML_UNCREATABLE(""); + /// Screens the workspace group is present on. + /// + /// > [!WARNING] This is not a model. Use @@Quickshell.ScriptModel if you need it to + /// > behave like one. + Q_PROPERTY(QList screens READ screens NOTIFY screensChanged); + +public: + explicit WorkspaceGroup(QObject* parent): QObject(parent) {} + + [[nodiscard]] const QList& screens() const { return this->mCachedScreens; } + +signals: + void screensChanged(); + +private slots: + void onScreensChanged(); + +protected: + Q_OBJECT_BINDABLE_PROPERTY( + WorkspaceGroup, + QList, + bScreens, + &WorkspaceGroup::onScreensChanged + ); + +private: + QList mCachedScreens; +}; + +} // namespace qs::wm diff --git a/src/windowmanager/workspacemodel.cpp b/src/windowmanager/workspacemodel.cpp new file mode 100644 index 0000000..f6941fb --- /dev/null +++ b/src/windowmanager/workspacemodel.cpp @@ -0,0 +1 @@ +#include "workspacemodel.hpp" diff --git a/src/windowmanager/workspacemodel.hpp b/src/windowmanager/workspacemodel.hpp new file mode 100644 index 0000000..aeb855f --- /dev/null +++ b/src/windowmanager/workspacemodel.hpp @@ -0,0 +1,39 @@ +#pragma once + +#include +#include +#include +#include +#include + +namespace qs::windowsystem { + +class WorkspaceModel: public QObject { + Q_OBJECT; + QML_ELEMENT; + +public: + enum ConflictStrategy : quint8 { + KeepFirst = 0, + ShowDuplicates, + }; + Q_ENUM(ConflictStrategy); + +signals: + void fromChanged(); + void toChanged(); + void screensChanged(); + void conflictStrategyChanged(); + +private: + Q_OBJECT_BINDABLE_PROPERTY(WorkspaceModel, qint32, bFrom, &WorkspaceModel::fromChanged); + Q_OBJECT_BINDABLE_PROPERTY(WorkspaceModel, qint32, bTo, &WorkspaceModel::toChanged); + Q_OBJECT_BINDABLE_PROPERTY( + WorkspaceModel, + ConflictStrategy, + bConflictStrategy, + &WorkspaceModel::conflictStrategyChanged + ); +}; + +} // namespace qs::windowsystem diff --git a/src/x11/i3/ipc/CMakeLists.txt b/src/x11/i3/ipc/CMakeLists.txt index 27a4484..c228ae3 100644 --- a/src/x11/i3/ipc/CMakeLists.txt +++ b/src/x11/i3/ipc/CMakeLists.txt @@ -3,6 +3,8 @@ qt_add_library(quickshell-i3-ipc STATIC qml.cpp workspace.cpp monitor.cpp + controller.cpp + listener.cpp ) qt_add_qml_module(quickshell-i3-ipc diff --git a/src/x11/i3/ipc/connection.cpp b/src/x11/i3/ipc/connection.cpp index ba010ed..b765ebc 100644 --- a/src/x11/i3/ipc/connection.cpp +++ b/src/x11/i3/ipc/connection.cpp @@ -1,4 +1,4 @@ -#include +#include "connection.hpp" #include #include #include @@ -23,11 +23,6 @@ #include #include "../../../core/logcat.hpp" -#include "../../../core/model.hpp" -#include "../../../core/qmlscreen.hpp" -#include "connection.hpp" -#include "monitor.hpp" -#include "workspace.hpp" namespace qs::i3::ipc { @@ -36,6 +31,69 @@ QS_LOGGING_CATEGORY(logI3Ipc, "quickshell.I3.ipc", QtWarningMsg); QS_LOGGING_CATEGORY(logI3IpcEvents, "quickshell.I3.ipc.events", QtWarningMsg); } // namespace +QString I3IpcEvent::type() const { return I3IpcEvent::eventToString(this->mCode); } +QString I3IpcEvent::data() const { return QString::fromUtf8(this->mData.toJson()); } + +EventCode I3IpcEvent::intToEvent(quint32 raw) { + if ((EventCode::Workspace <= raw && raw <= EventCode::Input) + || (EventCode::RunCommand <= raw && raw <= EventCode::GetTree)) + { + return static_cast(raw); + } else { + return EventCode::Unknown; + } +} + +QString I3IpcEvent::eventToString(EventCode event) { + switch (event) { + case EventCode::RunCommand: return "run_command"; break; + case EventCode::GetWorkspaces: return "get_workspaces"; break; + case EventCode::Subscribe: return "subscribe"; break; + case EventCode::GetOutputs: return "get_outputs"; break; + case EventCode::GetTree: return "get_tree"; break; + + case EventCode::Output: return "output"; break; + case EventCode::Workspace: return "workspace"; break; + case EventCode::Mode: return "mode"; break; + case EventCode::Window: return "window"; break; + case EventCode::BarconfigUpdate: return "barconfig_update"; break; + case EventCode::Binding: return "binding"; break; + case EventCode::Shutdown: return "shutdown"; break; + case EventCode::Tick: return "tick"; break; + case EventCode::BarStateUpdate: return "bar_state_update"; break; + case EventCode::Input: return "input"; break; + + default: return "unknown"; break; + } +} + +I3Ipc::I3Ipc(const QList& events): mEvents(events) { + auto sock = qEnvironmentVariable("I3SOCK"); + + if (sock.isEmpty()) { + qCWarning(logI3Ipc) << "$I3SOCK is unset. Trying $SWAYSOCK."; + + sock = qEnvironmentVariable("SWAYSOCK"); + + if (sock.isEmpty()) { + qCWarning(logI3Ipc) << "$SWAYSOCK and I3SOCK are unset. Cannot connect to socket."; + return; + } + } + + this->mSocketPath = sock; + + // clang-format off + QObject::connect(&this->liveEventSocket, &QLocalSocket::errorOccurred, this, &I3Ipc::eventSocketError); + QObject::connect(&this->liveEventSocket, &QLocalSocket::stateChanged, this, &I3Ipc::eventSocketStateChanged); + QObject::connect(&this->liveEventSocket, &QLocalSocket::readyRead, this, &I3Ipc::eventSocketReady); + QObject::connect(&this->liveEventSocket, &QLocalSocket::connected, this, &I3Ipc::subscribe); + // clang-format on + + this->liveEventSocketDs.setDevice(&this->liveEventSocket); + this->liveEventSocketDs.setByteOrder(static_cast(QSysInfo::ByteOrder)); +} + void I3Ipc::makeRequest(const QByteArray& request) { if (!this->valid) { qCWarning(logI3IpcEvents) << "IPC connection is not open, ignoring request."; @@ -60,50 +118,13 @@ QByteArray I3Ipc::buildRequestMessage(EventCode cmd, const QByteArray& payload) return MAGIC.data() + len + type + payload; } -I3Ipc::I3Ipc() { - auto sock = qEnvironmentVariable("I3SOCK"); - - if (sock.isEmpty()) { - qCWarning(logI3Ipc) << "$I3SOCK is unset. Trying $SWAYSOCK."; - - sock = qEnvironmentVariable("SWAYSOCK"); - - if (sock.isEmpty()) { - qCWarning(logI3Ipc) << "$SWAYSOCK and I3SOCK are unset. Cannot connect to socket."; - return; - } - } - - this->bFocusedWorkspace.setBinding([this]() -> I3Workspace* { - if (!this->bFocusedMonitor) return nullptr; - return this->bFocusedMonitor->bindableActiveWorkspace().value(); - }); - - this->mSocketPath = sock; - - // clang-format off - QObject::connect(&this->liveEventSocket, &QLocalSocket::errorOccurred, this, &I3Ipc::eventSocketError); - QObject::connect(&this->liveEventSocket, &QLocalSocket::stateChanged, this, &I3Ipc::eventSocketStateChanged); - QObject::connect(&this->liveEventSocket, &QLocalSocket::readyRead, this, &I3Ipc::eventSocketReady); - QObject::connect(&this->liveEventSocket, &QLocalSocket::connected, this, &I3Ipc::subscribe); - // clang-format on - - this->liveEventSocketDs.setDevice(&this->liveEventSocket); - this->liveEventSocketDs.setByteOrder(static_cast(QSysInfo::ByteOrder)); - - this->liveEventSocket.connectToServer(this->mSocketPath); -} - void I3Ipc::subscribe() { - auto payload = QByteArray(R"(["workspace","output"])"); + auto jsonArray = QJsonArray::fromStringList(this->mEvents); + auto jsonDoc = QJsonDocument(jsonArray); + auto payload = jsonDoc.toJson(QJsonDocument::Compact); auto message = I3Ipc::buildRequestMessage(EventCode::Subscribe, payload); this->makeRequest(message); - - // Workspaces must be refreshed before monitors or no focus will be - // detected on launch. - this->refreshWorkspaces(); - this->refreshMonitors(); } void I3Ipc::eventSocketReady() { @@ -111,15 +132,16 @@ void I3Ipc::eventSocketReady() { this->event.mCode = type; this->event.mData = data; - this->onEvent(&this->event); emit this->rawEvent(&this->event); } } +void I3Ipc::connect() { this->liveEventSocket.connectToServer(this->mSocketPath); } + void I3Ipc::reconnectIPC() { qCWarning(logI3Ipc) << "Fatal IPC error occured, recreating connection"; this->liveEventSocket.disconnectFromServer(); - this->liveEventSocket.connectToServer(this->mSocketPath); + this->connect(); } QVector I3Ipc::parseResponse() { @@ -193,347 +215,4 @@ void I3Ipc::eventSocketStateChanged(QLocalSocket::LocalSocketState state) { QString I3Ipc::socketPath() const { return this->mSocketPath; } -void I3Ipc::setFocusedMonitor(I3Monitor* monitor) { - auto* oldMonitor = this->bFocusedMonitor.value(); - if (monitor == oldMonitor) return; - - if (oldMonitor != nullptr) { - QObject::disconnect(oldMonitor, nullptr, this, nullptr); - } - - if (monitor != nullptr) { - QObject::connect(monitor, &QObject::destroyed, this, &I3Ipc::onFocusedMonitorDestroyed); - } - - this->bFocusedMonitor = monitor; -} - -void I3Ipc::onFocusedMonitorDestroyed() { this->bFocusedMonitor = nullptr; } - -I3Ipc* I3Ipc::instance() { - static I3Ipc* instance = nullptr; // NOLINT - - if (instance == nullptr) { - instance = new I3Ipc(); - } - - return instance; -} - -void I3Ipc::refreshWorkspaces() { - this->makeRequest(I3Ipc::buildRequestMessage(EventCode::GetWorkspaces)); -} - -void I3Ipc::handleGetWorkspacesEvent(I3IpcEvent* event) { - auto data = event->mData; - - auto workspaces = data.array(); - - const auto& mList = this->mWorkspaces.valueList(); - auto names = QVector(); - - qCDebug(logI3Ipc) << "There are" << workspaces.toVariantList().length() << "workspaces"; - for (auto entry: workspaces) { - auto object = entry.toObject().toVariantMap(); - auto name = object["name"].toString(); - - auto workspaceIter = std::ranges::find_if(mList, [name](I3Workspace* m) { - return m->bindableName().value() == name; - }); - - auto* workspace = workspaceIter == mList.end() ? nullptr : *workspaceIter; - auto existed = workspace != nullptr; - - if (workspace == nullptr) { - workspace = new I3Workspace(this); - } - - workspace->updateFromObject(object); - - if (!existed) { - this->mWorkspaces.insertObjectSorted(workspace, &I3Ipc::compareWorkspaces); - } - - if (!this->bFocusedWorkspace && object.value("focused").value()) { - this->bFocusedMonitor = workspace->bindableMonitor().value(); - } - - names.push_back(name); - } - - auto removedWorkspaces = QVector(); - - for (auto* workspace: mList) { - if (!names.contains(workspace->bindableName().value())) { - removedWorkspaces.push_back(workspace); - } - } - - qCDebug(logI3Ipc) << "Removing" << removedWorkspaces.length() << "deleted workspaces."; - - for (auto* workspace: removedWorkspaces) { - this->mWorkspaces.removeObject(workspace); - delete workspace; - } -} - -void I3Ipc::refreshMonitors() { - this->makeRequest(I3Ipc::buildRequestMessage(EventCode::GetOutputs)); -} - -void I3Ipc::handleGetOutputsEvent(I3IpcEvent* event) { - auto data = event->mData; - - auto monitors = data.array(); - const auto& mList = this->mMonitors.valueList(); - auto names = QVector(); - - qCDebug(logI3Ipc) << "There are" << monitors.toVariantList().length() << "monitors"; - - for (auto elem: monitors) { - auto object = elem.toObject().toVariantMap(); - auto name = object["name"].toString(); - - auto monitorIter = std::ranges::find_if(mList, [name](I3Monitor* m) { - return m->bindableName().value() == name; - }); - - auto* monitor = monitorIter == mList.end() ? nullptr : *monitorIter; - auto existed = monitor != nullptr; - - if (monitor == nullptr) { - monitor = new I3Monitor(this); - } - - monitor->updateFromObject(object); - - if (monitor->bindableFocused().value()) { - this->setFocusedMonitor(monitor); - } - - if (!existed) { - this->mMonitors.insertObject(monitor); - } - - names.push_back(name); - } - - auto removedMonitors = QVector(); - - for (auto* monitor: mList) { - if (!names.contains(monitor->bindableName().value())) { - removedMonitors.push_back(monitor); - } - } - - qCDebug(logI3Ipc) << "Removing" << removedMonitors.length() << "disconnected monitors."; - - for (auto* monitor: removedMonitors) { - this->mMonitors.removeObject(monitor); - delete monitor; - } -} - -void I3Ipc::onEvent(I3IpcEvent* event) { - switch (event->mCode) { - case EventCode::Workspace: this->handleWorkspaceEvent(event); return; - case EventCode::Output: - /// I3 only sends an "unspecified" event, so we have to query the data changes ourselves - qCInfo(logI3Ipc) << "Refreshing Monitors..."; - this->refreshMonitors(); - return; - case EventCode::Subscribe: qCInfo(logI3Ipc) << "Connected to IPC"; return; - case EventCode::GetOutputs: this->handleGetOutputsEvent(event); return; - case EventCode::GetWorkspaces: this->handleGetWorkspacesEvent(event); return; - case EventCode::RunCommand: I3Ipc::handleRunCommand(event); return; - case EventCode::Unknown: - qCWarning(logI3Ipc) << "Unknown event:" << event->type() << event->data(); - return; - default: qCWarning(logI3Ipc) << "Unhandled event:" << event->type(); - } -} - -void I3Ipc::handleRunCommand(I3IpcEvent* event) { - for (auto r: event->mData.array()) { - auto obj = r.toObject(); - const bool success = obj["success"].toBool(); - - if (!success) { - const QString error = obj["error"].toString(); - qCWarning(logI3Ipc) << "Error occured while running command:" << error; - } - } -} - -void I3Ipc::handleWorkspaceEvent(I3IpcEvent* event) { - // If a workspace doesn't exist, and is being switch to, no focus change event is emited, - // only the init one, which does not contain the previously focused workspace - auto change = event->mData["change"]; - - if (change == "init") { - qCInfo(logI3IpcEvents) << "New workspace has been created"; - - auto workspaceData = event->mData["current"]; - - auto* workspace = this->findWorkspaceByID(workspaceData["id"].toInt(-1)); - auto existed = workspace != nullptr; - - if (!existed) { - workspace = new I3Workspace(this); - } - - if (workspaceData.isObject()) { - workspace->updateFromObject(workspaceData.toObject().toVariantMap()); - } - - if (!existed) { - this->mWorkspaces.insertObjectSorted(workspace, &I3Ipc::compareWorkspaces); - qCInfo(logI3Ipc) << "Added workspace" << workspace->bindableName().value() << "to list"; - } - } else if (change == "focus") { - auto oldData = event->mData["old"]; - auto newData = event->mData["current"]; - auto oldName = oldData["name"].toString(); - auto newName = newData["name"].toString(); - - qCInfo(logI3IpcEvents) << "Focus changed: " << oldName << "->" << newName; - - if (auto* oldWorkspace = this->findWorkspaceByName(oldName)) { - oldWorkspace->updateFromObject(oldData.toObject().toVariantMap()); - } - - auto* newWorkspace = this->findWorkspaceByName(newName); - - if (newWorkspace == nullptr) { - newWorkspace = new I3Workspace(this); - } - - newWorkspace->updateFromObject(newData.toObject().toVariantMap()); - - if (newWorkspace->bindableMonitor().value()) { - auto* monitor = newWorkspace->bindableMonitor().value(); - monitor->setFocusedWorkspace(newWorkspace); - this->bFocusedMonitor = monitor; - } - } else if (change == "empty") { - auto name = event->mData["current"]["name"].toString(); - - auto* oldWorkspace = this->findWorkspaceByName(name); - - if (oldWorkspace != nullptr) { - qCInfo(logI3Ipc) << "Deleting" << oldWorkspace->bindableId().value() << name; - - if (this->bFocusedWorkspace == oldWorkspace) { - this->bFocusedMonitor->setFocusedWorkspace(nullptr); - } - - this->workspaces()->removeObject(oldWorkspace); - - delete oldWorkspace; - } else { - qCInfo(logI3Ipc) << "Workspace" << name << "has already been deleted"; - } - } else if (change == "move" || change == "rename" || change == "urgent") { - auto name = event->mData["current"]["name"].toString(); - - auto* workspace = this->findWorkspaceByName(name); - - if (workspace != nullptr) { - auto data = event->mData["current"].toObject().toVariantMap(); - - workspace->updateFromObject(data); - } else { - qCWarning(logI3Ipc) << "Workspace" << name << "doesn't exist"; - } - } else if (change == "reload") { - qCInfo(logI3Ipc) << "Refreshing Workspaces..."; - this->refreshWorkspaces(); - } -} - -I3Monitor* I3Ipc::monitorFor(QuickshellScreenInfo* screen) { - if (screen == nullptr) return nullptr; - - return this->findMonitorByName(screen->name()); -} - -I3Workspace* I3Ipc::findWorkspaceByID(qint32 id) { - auto list = this->mWorkspaces.valueList(); - auto workspaceIter = - std::ranges::find_if(list, [id](I3Workspace* m) { return m->bindableId().value() == id; }); - - return workspaceIter == list.end() ? nullptr : *workspaceIter; -} - -I3Workspace* I3Ipc::findWorkspaceByName(const QString& name) { - auto list = this->mWorkspaces.valueList(); - auto workspaceIter = std::ranges::find_if(list, [name](I3Workspace* m) { - return m->bindableName().value() == name; - }); - - return workspaceIter == list.end() ? nullptr : *workspaceIter; -} - -I3Monitor* I3Ipc::findMonitorByName(const QString& name, bool createIfMissing) { - auto list = this->mMonitors.valueList(); - auto monitorIter = std::ranges::find_if(list, [name](I3Monitor* m) { - return m->bindableName().value() == name; - }); - - if (monitorIter != list.end()) { - return *monitorIter; - } else if (createIfMissing) { - qCDebug(logI3Ipc) << "Monitor" << name << "requested before creation, performing early init"; - auto* monitor = new I3Monitor(this); - monitor->updateInitial(name); - this->mMonitors.insertObject(monitor); - return monitor; - } else { - return nullptr; - } -} - -ObjectModel* I3Ipc::monitors() { return &this->mMonitors; } -ObjectModel* I3Ipc::workspaces() { return &this->mWorkspaces; } - -bool I3Ipc::compareWorkspaces(I3Workspace* a, I3Workspace* b) { - return a->bindableNumber().value() > b->bindableNumber().value(); -} - -QString I3IpcEvent::type() const { return I3IpcEvent::eventToString(this->mCode); } -QString I3IpcEvent::data() const { return QString::fromUtf8(this->mData.toJson()); } - -EventCode I3IpcEvent::intToEvent(quint32 raw) { - if ((EventCode::Workspace <= raw && raw <= EventCode::Input) - || (EventCode::RunCommand <= raw && raw <= EventCode::GetTree)) - { - return static_cast(raw); - } else { - return EventCode::Unknown; - } -} - -QString I3IpcEvent::eventToString(EventCode event) { - switch (event) { - case EventCode::RunCommand: return "run_command"; break; - case EventCode::GetWorkspaces: return "get_workspaces"; break; - case EventCode::Subscribe: return "subscribe"; break; - case EventCode::GetOutputs: return "get_outputs"; break; - case EventCode::GetTree: return "get_tree"; break; - - case EventCode::Output: return "output"; break; - case EventCode::Workspace: return "workspace"; break; - case EventCode::Mode: return "mode"; break; - case EventCode::Window: return "window"; break; - case EventCode::BarconfigUpdate: return "barconfig_update"; break; - case EventCode::Binding: return "binding"; break; - case EventCode::Shutdown: return "shutdown"; break; - case EventCode::Tick: return "tick"; break; - case EventCode::BarStateUpdate: return "bar_state_update"; break; - case EventCode::Input: return "input"; break; - - case EventCode::Unknown: return "unknown"; break; - } -} - } // namespace qs::i3::ipc diff --git a/src/x11/i3/ipc/connection.hpp b/src/x11/i3/ipc/connection.hpp index af480c5..6100f7e 100644 --- a/src/x11/i3/ipc/connection.hpp +++ b/src/x11/i3/ipc/connection.hpp @@ -1,28 +1,14 @@ #pragma once #include +#include #include -#include #include #include -#include -#include #include #include #include -#include "../../../core/model.hpp" -#include "../../../core/qmlscreen.hpp" - -namespace qs::i3::ipc { - -class I3Workspace; -class I3Monitor; -} // namespace qs::i3::ipc - -Q_DECLARE_OPAQUE_POINTER(qs::i3::ipc::I3Workspace*); -Q_DECLARE_OPAQUE_POINTER(qs::i3::ipc::I3Monitor*); - namespace qs::i3::ipc { constexpr std::string MAGIC = "i3-ipc"; @@ -54,9 +40,7 @@ using Event = std::tuple; class I3IpcEvent: public QObject { Q_OBJECT; - /// The name of the event Q_PROPERTY(QString type READ type CONSTANT); - /// The payload of the event in JSON format. Q_PROPERTY(QString data READ data CONSTANT); QML_NAMED_ELEMENT(I3Event); @@ -75,90 +59,48 @@ public: static QString eventToString(EventCode event); }; +/// Base class that manages the IPC socket, subscriptions and event reception. class I3Ipc: public QObject { Q_OBJECT; public: - static I3Ipc* instance(); + explicit I3Ipc(const QList& events); [[nodiscard]] QString socketPath() const; void makeRequest(const QByteArray& request); void dispatch(const QString& payload); + void connect(); - static QByteArray buildRequestMessage(EventCode cmd, const QByteArray& payload = QByteArray()); - - I3Workspace* findWorkspaceByName(const QString& name); - I3Monitor* findMonitorByName(const QString& name, bool createIfMissing = false); - I3Workspace* findWorkspaceByID(qint32 id); - - void setFocusedMonitor(I3Monitor* monitor); - - void refreshWorkspaces(); - void refreshMonitors(); - - I3Monitor* monitorFor(QuickshellScreenInfo* screen); - - [[nodiscard]] QBindable bindableFocusedMonitor() const { - return &this->bFocusedMonitor; - }; - - [[nodiscard]] QBindable bindableFocusedWorkspace() const { - return &this->bFocusedWorkspace; - }; - - [[nodiscard]] ObjectModel* monitors(); - [[nodiscard]] ObjectModel* workspaces(); + [[nodiscard]] QByteArray static buildRequestMessage( + EventCode cmd, + const QByteArray& payload = QByteArray() + ); signals: void connected(); void rawEvent(I3IpcEvent* event); - void focusedWorkspaceChanged(); - void focusedMonitorChanged(); -private slots: +protected slots: void eventSocketError(QLocalSocket::LocalSocketError error) const; void eventSocketStateChanged(QLocalSocket::LocalSocketState state); void eventSocketReady(); void subscribe(); - void onFocusedMonitorDestroyed(); - -private: - explicit I3Ipc(); - - void onEvent(I3IpcEvent* event); - - void handleWorkspaceEvent(I3IpcEvent* event); - void handleGetWorkspacesEvent(I3IpcEvent* event); - void handleGetOutputsEvent(I3IpcEvent* event); - static void handleRunCommand(I3IpcEvent* event); - static bool compareWorkspaces(I3Workspace* a, I3Workspace* b); - +protected: void reconnectIPC(); - QVector> parseResponse(); QLocalSocket liveEventSocket; QDataStream liveEventSocketDs; QString mSocketPath; - bool valid = false; - ObjectModel mMonitors {this}; - ObjectModel mWorkspaces {this}; - I3IpcEvent event {this}; - Q_OBJECT_BINDABLE_PROPERTY(I3Ipc, I3Monitor*, bFocusedMonitor, &I3Ipc::focusedMonitorChanged); - - Q_OBJECT_BINDABLE_PROPERTY( - I3Ipc, - I3Workspace*, - bFocusedWorkspace, - &I3Ipc::focusedWorkspaceChanged - ); +private: + QList mEvents; }; } // namespace qs::i3::ipc diff --git a/src/x11/i3/ipc/controller.cpp b/src/x11/i3/ipc/controller.cpp new file mode 100644 index 0000000..1a08c63 --- /dev/null +++ b/src/x11/i3/ipc/controller.cpp @@ -0,0 +1,367 @@ +#include "controller.hpp" +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../../../core/logcat.hpp" +#include "../../../core/model.hpp" +#include "../../../core/qmlscreen.hpp" +#include "connection.hpp" +#include "monitor.hpp" +#include "workspace.hpp" + +namespace qs::i3::ipc { + +namespace { +QS_LOGGING_CATEGORY(logI3Ipc, "quickshell.I3.ipc", QtWarningMsg); +QS_LOGGING_CATEGORY(logI3IpcEvents, "quickshell.I3.ipc.events", QtWarningMsg); +} // namespace + +I3IpcController::I3IpcController(): I3Ipc({"workspace", "output"}) { + // bind focused workspace to focused monitor's active workspace + this->bFocusedWorkspace.setBinding([this]() -> I3Workspace* { + if (!this->bFocusedMonitor) return nullptr; + return this->bFocusedMonitor->bindableActiveWorkspace().value(); + }); + + // clang-format off + QObject::connect(this, &I3Ipc::rawEvent, this, &I3IpcController::onEvent); + QObject::connect(&this->liveEventSocket, &QLocalSocket::connected, this, &I3IpcController::onConnected); + // clang-format on +} + +void I3IpcController::onConnected() { + // Workspaces must be refreshed before monitors or no focus will be + // detected on launch. + this->refreshWorkspaces(); + this->refreshMonitors(); +} + +void I3IpcController::setFocusedMonitor(I3Monitor* monitor) { + auto* oldMonitor = this->bFocusedMonitor.value(); + if (monitor == oldMonitor) return; + + if (oldMonitor != nullptr) { + QObject::disconnect(oldMonitor, nullptr, this, nullptr); + } + + if (monitor != nullptr) { + QObject::connect( + monitor, + &QObject::destroyed, + this, + &I3IpcController::onFocusedMonitorDestroyed + ); + } + + this->bFocusedMonitor = monitor; +} + +void I3IpcController::onFocusedMonitorDestroyed() { this->bFocusedMonitor = nullptr; } + +I3IpcController* I3IpcController::instance() { + static I3IpcController* instance = nullptr; // NOLINT + + if (instance == nullptr) { + instance = new I3IpcController(); + instance->connect(); + } + + return instance; +} + +void I3IpcController::refreshWorkspaces() { + this->makeRequest(I3Ipc::buildRequestMessage(EventCode::GetWorkspaces)); +} + +void I3IpcController::handleGetWorkspacesEvent(I3IpcEvent* event) { + auto data = event->mData; + + auto workspaces = data.array(); + + const auto& mList = this->mWorkspaces.valueList(); + auto names = QVector(); + + qCDebug(logI3Ipc) << "There are" << workspaces.toVariantList().length() << "workspaces"; + for (auto entry: workspaces) { + auto object = entry.toObject().toVariantMap(); + auto name = object["name"].toString(); + + auto workspaceIter = std::ranges::find_if(mList, [name](I3Workspace* m) { + return m->bindableName().value() == name; + }); + + auto* workspace = workspaceIter == mList.end() ? nullptr : *workspaceIter; + auto existed = workspace != nullptr; + + if (workspace == nullptr) { + workspace = new I3Workspace(this); + } + + workspace->updateFromObject(object); + + if (!existed) { + this->mWorkspaces.insertObjectSorted(workspace, &I3IpcController::compareWorkspaces); + } + + if (!this->bFocusedWorkspace && object.value("focused").value()) { + this->bFocusedMonitor = workspace->bindableMonitor().value(); + } + + names.push_back(name); + } + + auto removedWorkspaces = QVector(); + + for (auto* workspace: mList) { + if (!names.contains(workspace->bindableName().value())) { + removedWorkspaces.push_back(workspace); + } + } + + qCDebug(logI3Ipc) << "Removing" << removedWorkspaces.length() << "deleted workspaces."; + + for (auto* workspace: removedWorkspaces) { + this->mWorkspaces.removeObject(workspace); + delete workspace; + } +} + +void I3IpcController::refreshMonitors() { + this->makeRequest(I3Ipc::buildRequestMessage(EventCode::GetOutputs)); +} + +void I3IpcController::handleGetOutputsEvent(I3IpcEvent* event) { + auto data = event->mData; + + auto monitors = data.array(); + const auto& mList = this->mMonitors.valueList(); + auto names = QVector(); + + qCDebug(logI3Ipc) << "There are" << monitors.toVariantList().length() << "monitors"; + + for (auto elem: monitors) { + auto object = elem.toObject().toVariantMap(); + auto name = object["name"].toString(); + + auto monitorIter = std::ranges::find_if(mList, [name](I3Monitor* m) { + return m->bindableName().value() == name; + }); + + auto* monitor = monitorIter == mList.end() ? nullptr : *monitorIter; + auto existed = monitor != nullptr; + + if (monitor == nullptr) { + monitor = new I3Monitor(this); + } + + monitor->updateFromObject(object); + + if (monitor->bindableFocused().value()) { + this->setFocusedMonitor(monitor); + } + + if (!existed) { + this->mMonitors.insertObject(monitor); + } + + names.push_back(name); + } + + auto removedMonitors = QVector(); + + for (auto* monitor: mList) { + if (!names.contains(monitor->bindableName().value())) { + removedMonitors.push_back(monitor); + } + } + + qCDebug(logI3Ipc) << "Removing" << removedMonitors.length() << "disconnected monitors."; + + for (auto* monitor: removedMonitors) { + this->mMonitors.removeObject(monitor); + delete monitor; + } +} + +void I3IpcController::onEvent(I3IpcEvent* event) { + switch (event->mCode) { + case EventCode::Workspace: this->handleWorkspaceEvent(event); return; + case EventCode::Output: + /// I3 only sends an "unspecified" event, so we have to query the data changes ourselves + qCInfo(logI3Ipc) << "Refreshing Monitors..."; + this->refreshMonitors(); + return; + case EventCode::Subscribe: qCInfo(logI3Ipc) << "Connected to IPC"; return; + case EventCode::GetOutputs: this->handleGetOutputsEvent(event); return; + case EventCode::GetWorkspaces: this->handleGetWorkspacesEvent(event); return; + case EventCode::RunCommand: I3IpcController::handleRunCommand(event); return; + case EventCode::Unknown: + qCWarning(logI3Ipc) << "Unknown event:" << event->type() << event->data(); + return; + default: qCWarning(logI3Ipc) << "Unhandled event:" << event->type(); + } +} + +void I3IpcController::handleRunCommand(I3IpcEvent* event) { + for (auto r: event->mData.array()) { + auto obj = r.toObject(); + const bool success = obj["success"].toBool(); + + if (!success) { + const QString error = obj["error"].toString(); + qCWarning(logI3Ipc) << "Error occured while running command:" << error; + } + } +} + +void I3IpcController::handleWorkspaceEvent(I3IpcEvent* event) { + // If a workspace doesn't exist, and is being switch to, no focus change event is emited, + // only the init one, which does not contain the previously focused workspace + auto change = event->mData["change"]; + + if (change == "init") { + qCInfo(logI3IpcEvents) << "New workspace has been created"; + + auto workspaceData = event->mData["current"]; + + auto* workspace = this->findWorkspaceByID(workspaceData["id"].toInt(-1)); + auto existed = workspace != nullptr; + + if (!existed) { + workspace = new I3Workspace(this); + } + + if (workspaceData.isObject()) { + workspace->updateFromObject(workspaceData.toObject().toVariantMap()); + } + + if (!existed) { + this->mWorkspaces.insertObjectSorted(workspace, &I3IpcController::compareWorkspaces); + qCInfo(logI3Ipc) << "Added workspace" << workspace->bindableName().value() << "to list"; + } + } else if (change == "focus") { + auto oldData = event->mData["old"]; + auto newData = event->mData["current"]; + auto oldName = oldData["name"].toString(); + auto newName = newData["name"].toString(); + + qCInfo(logI3IpcEvents) << "Focus changed: " << oldName << "->" << newName; + + if (auto* oldWorkspace = this->findWorkspaceByName(oldName)) { + oldWorkspace->updateFromObject(oldData.toObject().toVariantMap()); + } + + auto* newWorkspace = this->findWorkspaceByName(newName); + + if (newWorkspace == nullptr) { + newWorkspace = new I3Workspace(this); + } + + newWorkspace->updateFromObject(newData.toObject().toVariantMap()); + + if (newWorkspace->bindableMonitor().value()) { + auto* monitor = newWorkspace->bindableMonitor().value(); + monitor->setFocusedWorkspace(newWorkspace); + this->bFocusedMonitor = monitor; + } + } else if (change == "empty") { + auto name = event->mData["current"]["name"].toString(); + + auto* oldWorkspace = this->findWorkspaceByName(name); + + if (oldWorkspace != nullptr) { + qCInfo(logI3Ipc) << "Deleting" << oldWorkspace->bindableId().value() << name; + + if (this->bFocusedWorkspace == oldWorkspace) { + this->bFocusedMonitor->setFocusedWorkspace(nullptr); + } + + this->workspaces()->removeObject(oldWorkspace); + + delete oldWorkspace; + } else { + qCInfo(logI3Ipc) << "Workspace" << name << "has already been deleted"; + } + } else if (change == "move" || change == "rename" || change == "urgent") { + auto name = event->mData["current"]["name"].toString(); + + auto* workspace = this->findWorkspaceByName(name); + + if (workspace != nullptr) { + auto data = event->mData["current"].toObject().toVariantMap(); + + workspace->updateFromObject(data); + } else { + qCWarning(logI3Ipc) << "Workspace" << name << "doesn't exist"; + } + } else if (change == "reload") { + qCInfo(logI3Ipc) << "Refreshing Workspaces..."; + this->refreshWorkspaces(); + } +} + +I3Monitor* I3IpcController::monitorFor(QuickshellScreenInfo* screen) { + if (screen == nullptr) return nullptr; + + return this->findMonitorByName(screen->name()); +} + +I3Workspace* I3IpcController::findWorkspaceByID(qint32 id) { + auto list = this->mWorkspaces.valueList(); + auto workspaceIter = + std::ranges::find_if(list, [id](I3Workspace* m) { return m->bindableId().value() == id; }); + + return workspaceIter == list.end() ? nullptr : *workspaceIter; +} + +I3Workspace* I3IpcController::findWorkspaceByName(const QString& name) { + auto list = this->mWorkspaces.valueList(); + auto workspaceIter = std::ranges::find_if(list, [name](I3Workspace* m) { + return m->bindableName().value() == name; + }); + + return workspaceIter == list.end() ? nullptr : *workspaceIter; +} + +I3Monitor* I3IpcController::findMonitorByName(const QString& name, bool createIfMissing) { + auto list = this->mMonitors.valueList(); + auto monitorIter = std::ranges::find_if(list, [name](I3Monitor* m) { + return m->bindableName().value() == name; + }); + + if (monitorIter != list.end()) { + return *monitorIter; + } else if (createIfMissing) { + qCDebug(logI3Ipc) << "Monitor" << name << "requested before creation, performing early init"; + auto* monitor = new I3Monitor(this); + monitor->updateInitial(name); + this->mMonitors.insertObject(monitor); + return monitor; + } else { + return nullptr; + } +} + +ObjectModel* I3IpcController::monitors() { return &this->mMonitors; } +ObjectModel* I3IpcController::workspaces() { return &this->mWorkspaces; } + +bool I3IpcController::compareWorkspaces(I3Workspace* a, I3Workspace* b) { + return a->bindableNumber().value() > b->bindableNumber().value(); +} + +} // namespace qs::i3::ipc diff --git a/src/x11/i3/ipc/controller.hpp b/src/x11/i3/ipc/controller.hpp new file mode 100644 index 0000000..464f6f6 --- /dev/null +++ b/src/x11/i3/ipc/controller.hpp @@ -0,0 +1,94 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../../../core/model.hpp" +#include "../../../core/qmlscreen.hpp" +#include "connection.hpp" + +namespace qs::i3::ipc { + +class I3Workspace; +class I3Monitor; +} // namespace qs::i3::ipc + +Q_DECLARE_OPAQUE_POINTER(qs::i3::ipc::I3Workspace*); +Q_DECLARE_OPAQUE_POINTER(qs::i3::ipc::I3Monitor*); + +namespace qs::i3::ipc { + +/// I3/Sway IPC controller that manages workspaces and monitors +class I3IpcController: public I3Ipc { + Q_OBJECT; + +public: + static I3IpcController* instance(); + + I3Workspace* findWorkspaceByName(const QString& name); + I3Monitor* findMonitorByName(const QString& name, bool createIfMissing = false); + I3Workspace* findWorkspaceByID(qint32 id); + + void setFocusedMonitor(I3Monitor* monitor); + + void refreshWorkspaces(); + void refreshMonitors(); + + I3Monitor* monitorFor(QuickshellScreenInfo* screen); + + [[nodiscard]] QBindable bindableFocusedMonitor() const { + return &this->bFocusedMonitor; + }; + + [[nodiscard]] QBindable bindableFocusedWorkspace() const { + return &this->bFocusedWorkspace; + }; + + [[nodiscard]] ObjectModel* monitors(); + [[nodiscard]] ObjectModel* workspaces(); + +signals: + void focusedWorkspaceChanged(); + void focusedMonitorChanged(); + +private slots: + void onFocusedMonitorDestroyed(); + + void onEvent(I3IpcEvent* event); + void onConnected(); + +private: + explicit I3IpcController(); + + void handleWorkspaceEvent(I3IpcEvent* event); + void handleGetWorkspacesEvent(I3IpcEvent* event); + void handleGetOutputsEvent(I3IpcEvent* event); + static void handleRunCommand(I3IpcEvent* event); + static bool compareWorkspaces(I3Workspace* a, I3Workspace* b); + + ObjectModel mMonitors {this}; + ObjectModel mWorkspaces {this}; + + Q_OBJECT_BINDABLE_PROPERTY( + I3IpcController, + I3Monitor*, + bFocusedMonitor, + &I3IpcController::focusedMonitorChanged + ); + + Q_OBJECT_BINDABLE_PROPERTY( + I3IpcController, + I3Workspace*, + bFocusedWorkspace, + &I3IpcController::focusedWorkspaceChanged + ); +}; + +} // namespace qs::i3::ipc diff --git a/src/x11/i3/ipc/listener.cpp b/src/x11/i3/ipc/listener.cpp new file mode 100644 index 0000000..aa7719c --- /dev/null +++ b/src/x11/i3/ipc/listener.cpp @@ -0,0 +1,49 @@ +#include "listener.hpp" +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "connection.hpp" + +namespace qs::i3::ipc { + +I3IpcListener::~I3IpcListener() { this->freeI3Ipc(); } + +void I3IpcListener::onPostReload() { this->startListening(); } + +QList I3IpcListener::subscriptions() const { return this->mSubscriptions; } +void I3IpcListener::setSubscriptions(QList subscriptions) { + if (this->mSubscriptions == subscriptions) return; + this->mSubscriptions = std::move(subscriptions); + + emit this->subscriptionsChanged(); + this->startListening(); +} + +void I3IpcListener::startListening() { + this->freeI3Ipc(); + if (this->mSubscriptions.isEmpty()) return; + + this->i3Ipc = new I3Ipc(this->mSubscriptions); + + // clang-format off + QObject::connect(this->i3Ipc, &I3Ipc::rawEvent, this, &I3IpcListener::receiveEvent); + // clang-format on + + this->i3Ipc->connect(); +} + +void I3IpcListener::receiveEvent(I3IpcEvent* event) { emit this->ipcEvent(event); } + +void I3IpcListener::freeI3Ipc() { + delete this->i3Ipc; + this->i3Ipc = nullptr; +} + +} // namespace qs::i3::ipc diff --git a/src/x11/i3/ipc/listener.hpp b/src/x11/i3/ipc/listener.hpp new file mode 100644 index 0000000..9cb40bb --- /dev/null +++ b/src/x11/i3/ipc/listener.hpp @@ -0,0 +1,68 @@ +#pragma once + +#include // NOLINT +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../../../core/doc.hpp" +#include "../../../core/generation.hpp" +#include "../../../core/qmlglobal.hpp" +#include "../../../core/reload.hpp" +#include "connection.hpp" + +namespace qs::i3::ipc { + +///! I3/Sway IPC event listener +/// #### Example +/// ```qml +/// I3IpcListener { +/// subscriptions: ["input"] +/// onIpcEvent: function (event) { +/// handleInputEvent(event.data) +/// } +/// } +/// ``` +class I3IpcListener: public PostReloadHook { + Q_OBJECT; + // clang-format off + /// List of [I3/Sway events](https://man.archlinux.org/man/sway-ipc.7.en#EVENTS) to subscribe to. + Q_PROPERTY(QList subscriptions READ subscriptions WRITE setSubscriptions NOTIFY subscriptionsChanged); + // clang-format on + QML_ELEMENT; + +public: + explicit I3IpcListener(QObject* parent = nullptr): PostReloadHook(parent) {} + ~I3IpcListener() override; + Q_DISABLE_COPY_MOVE(I3IpcListener); + + void onPostReload() override; + + [[nodiscard]] QList subscriptions() const; + void setSubscriptions(QList subscriptions); + +signals: + void ipcEvent(I3IpcEvent* event); + void subscriptionsChanged(); + +private: + void startListening(); + void receiveEvent(I3IpcEvent* event); + + void freeI3Ipc(); + + QList mSubscriptions; + I3Ipc* i3Ipc = nullptr; +}; + +} // namespace qs::i3::ipc diff --git a/src/x11/i3/ipc/monitor.cpp b/src/x11/i3/ipc/monitor.cpp index 1bc593c..fb0ec86 100644 --- a/src/x11/i3/ipc/monitor.cpp +++ b/src/x11/i3/ipc/monitor.cpp @@ -7,12 +7,12 @@ #include #include -#include "connection.hpp" +#include "controller.hpp" #include "workspace.hpp" namespace qs::i3::ipc { -I3Monitor::I3Monitor(I3Ipc* ipc): QObject(ipc), ipc(ipc) { +I3Monitor::I3Monitor(I3IpcController* ipc): QObject(ipc), ipc(ipc) { // clang-format off this->bFocused.setBinding([this]() { return this->ipc->bindableFocusedMonitor().value() == this; }); // clang-format on diff --git a/src/x11/i3/ipc/monitor.hpp b/src/x11/i3/ipc/monitor.hpp index 00269a1..cd348b1 100644 --- a/src/x11/i3/ipc/monitor.hpp +++ b/src/x11/i3/ipc/monitor.hpp @@ -4,6 +4,7 @@ #include #include "connection.hpp" +#include "controller.hpp" namespace qs::i3::ipc { @@ -39,10 +40,10 @@ class I3Monitor: public QObject { Q_PROPERTY(QVariantMap lastIpcObject READ lastIpcObject NOTIFY lastIpcObjectChanged); // clang-format on QML_ELEMENT; - QML_UNCREATABLE("I3Monitors must be retrieved from the I3Ipc object."); + QML_UNCREATABLE("I3Monitors must be retrieved from the I3IpcController object."); public: - explicit I3Monitor(I3Ipc* ipc); + explicit I3Monitor(I3IpcController* ipc); [[nodiscard]] QBindable bindableId() { return &this->bId; } [[nodiscard]] QBindable bindableName() { return &this->bName; } @@ -79,7 +80,7 @@ signals: void focusedChanged(); private: - I3Ipc* ipc; + I3IpcController* ipc; QVariantMap mLastIpcObject; diff --git a/src/x11/i3/ipc/qml.cpp b/src/x11/i3/ipc/qml.cpp index 2804161..d835cbd 100644 --- a/src/x11/i3/ipc/qml.cpp +++ b/src/x11/i3/ipc/qml.cpp @@ -7,46 +7,49 @@ #include "../../../core/model.hpp" #include "../../../core/qmlscreen.hpp" #include "connection.hpp" +#include "controller.hpp" #include "workspace.hpp" namespace qs::i3::ipc { I3IpcQml::I3IpcQml() { - auto* instance = I3Ipc::instance(); + auto* instance = I3IpcController::instance(); // clang-format off QObject::connect(instance, &I3Ipc::rawEvent, this, &I3IpcQml::rawEvent); QObject::connect(instance, &I3Ipc::connected, this, &I3IpcQml::connected); - QObject::connect(instance, &I3Ipc::focusedWorkspaceChanged, this, &I3IpcQml::focusedWorkspaceChanged); - QObject::connect(instance, &I3Ipc::focusedMonitorChanged, this, &I3IpcQml::focusedMonitorChanged); + QObject::connect(instance, &I3IpcController::focusedWorkspaceChanged, this, &I3IpcQml::focusedWorkspaceChanged); + QObject::connect(instance, &I3IpcController::focusedMonitorChanged, this, &I3IpcQml::focusedMonitorChanged); // clang-format on } -void I3IpcQml::dispatch(const QString& request) { I3Ipc::instance()->dispatch(request); } -void I3IpcQml::refreshMonitors() { I3Ipc::instance()->refreshMonitors(); } -void I3IpcQml::refreshWorkspaces() { I3Ipc::instance()->refreshWorkspaces(); } -QString I3IpcQml::socketPath() { return I3Ipc::instance()->socketPath(); } -ObjectModel* I3IpcQml::monitors() { return I3Ipc::instance()->monitors(); } -ObjectModel* I3IpcQml::workspaces() { return I3Ipc::instance()->workspaces(); } +void I3IpcQml::dispatch(const QString& request) { I3IpcController::instance()->dispatch(request); } +void I3IpcQml::refreshMonitors() { I3IpcController::instance()->refreshMonitors(); } +void I3IpcQml::refreshWorkspaces() { I3IpcController::instance()->refreshWorkspaces(); } +QString I3IpcQml::socketPath() { return I3IpcController::instance()->socketPath(); } +ObjectModel* I3IpcQml::monitors() { return I3IpcController::instance()->monitors(); } +ObjectModel* I3IpcQml::workspaces() { + return I3IpcController::instance()->workspaces(); +} QBindable I3IpcQml::bindableFocusedWorkspace() { - return I3Ipc::instance()->bindableFocusedWorkspace(); + return I3IpcController::instance()->bindableFocusedWorkspace(); } QBindable I3IpcQml::bindableFocusedMonitor() { - return I3Ipc::instance()->bindableFocusedMonitor(); + return I3IpcController::instance()->bindableFocusedMonitor(); } I3Workspace* I3IpcQml::findWorkspaceByName(const QString& name) { - return I3Ipc::instance()->findWorkspaceByName(name); + return I3IpcController::instance()->findWorkspaceByName(name); } I3Monitor* I3IpcQml::findMonitorByName(const QString& name) { - return I3Ipc::instance()->findMonitorByName(name); + return I3IpcController::instance()->findMonitorByName(name); } I3Monitor* I3IpcQml::monitorFor(QuickshellScreenInfo* screen) { - return I3Ipc::instance()->monitorFor(screen); + return I3IpcController::instance()->monitorFor(screen); } } // namespace qs::i3::ipc diff --git a/src/x11/i3/ipc/qml.hpp b/src/x11/i3/ipc/qml.hpp index 804e42a..2e7c81c 100644 --- a/src/x11/i3/ipc/qml.hpp +++ b/src/x11/i3/ipc/qml.hpp @@ -7,6 +7,7 @@ #include "../../../core/doc.hpp" #include "../../../core/qmlscreen.hpp" #include "connection.hpp" +#include "controller.hpp" namespace qs::i3::ipc { diff --git a/src/x11/i3/ipc/workspace.cpp b/src/x11/i3/ipc/workspace.cpp index 7d0b730..03fadc2 100644 --- a/src/x11/i3/ipc/workspace.cpp +++ b/src/x11/i3/ipc/workspace.cpp @@ -7,12 +7,12 @@ #include #include -#include "connection.hpp" +#include "controller.hpp" #include "monitor.hpp" namespace qs::i3::ipc { -I3Workspace::I3Workspace(I3Ipc* ipc): QObject(ipc), ipc(ipc) { +I3Workspace::I3Workspace(I3IpcController* ipc): QObject(ipc), ipc(ipc) { Qt::beginPropertyUpdateGroup(); this->bActive.setBinding([this]() { diff --git a/src/x11/i3/ipc/workspace.hpp b/src/x11/i3/ipc/workspace.hpp index c9cd029..f540545 100644 --- a/src/x11/i3/ipc/workspace.hpp +++ b/src/x11/i3/ipc/workspace.hpp @@ -5,6 +5,7 @@ #include #include "connection.hpp" +#include "controller.hpp" namespace qs::i3::ipc { @@ -40,7 +41,7 @@ class I3Workspace: public QObject { QML_UNCREATABLE("I3Workspaces must be retrieved from the I3 object."); public: - I3Workspace(I3Ipc* ipc); + I3Workspace(I3IpcController* ipc); /// Activate the workspace. /// @@ -72,7 +73,7 @@ signals: void lastIpcObjectChanged(); private: - I3Ipc* ipc; + I3IpcController* ipc; QVariantMap mLastIpcObject; diff --git a/src/x11/i3/module.md b/src/x11/i3/module.md index 10afb98..1dc6180 100644 --- a/src/x11/i3/module.md +++ b/src/x11/i3/module.md @@ -2,8 +2,10 @@ name = "Quickshell.I3" description = "I3 specific Quickshell types" headers = [ "ipc/connection.hpp", + "ipc/controller.hpp", "ipc/qml.hpp", "ipc/workspace.hpp", "ipc/monitor.hpp", + "ipc/listener.hpp", ] ----- diff --git a/src/x11/panel_window.cpp b/src/x11/panel_window.cpp index adba0ab..c78b548 100644 --- a/src/x11/panel_window.cpp +++ b/src/x11/panel_window.cpp @@ -115,6 +115,8 @@ XPanelWindow::XPanelWindow(QObject* parent): ProxyWindowBase(parent) { return 0; } } + + return 0; }); this->bcExclusionEdge.setBinding([this] { return this->bAnchors.value().exclusionEdge(); }); @@ -414,23 +416,9 @@ void XPanelWindow::updateFocusable() { XPanelInterface::XPanelInterface(QObject* parent) : PanelWindowInterface(parent) , panel(new XPanelWindow(this)) { + this->connectSignals(); // clang-format off - QObject::connect(this->panel, &ProxyWindowBase::windowConnected, this, &XPanelInterface::windowConnected); - QObject::connect(this->panel, &ProxyWindowBase::visibleChanged, this, &XPanelInterface::visibleChanged); - QObject::connect(this->panel, &ProxyWindowBase::backerVisibilityChanged, this, &XPanelInterface::backingWindowVisibleChanged); - QObject::connect(this->panel, &ProxyWindowBase::implicitHeightChanged, this, &XPanelInterface::implicitHeightChanged); - QObject::connect(this->panel, &ProxyWindowBase::implicitWidthChanged, this, &XPanelInterface::implicitWidthChanged); - QObject::connect(this->panel, &ProxyWindowBase::heightChanged, this, &XPanelInterface::heightChanged); - QObject::connect(this->panel, &ProxyWindowBase::widthChanged, this, &XPanelInterface::widthChanged); - QObject::connect(this->panel, &ProxyWindowBase::devicePixelRatioChanged, this, &XPanelInterface::devicePixelRatioChanged); - QObject::connect(this->panel, &ProxyWindowBase::screenChanged, this, &XPanelInterface::screenChanged); - QObject::connect(this->panel, &ProxyWindowBase::windowTransformChanged, this, &XPanelInterface::windowTransformChanged); - QObject::connect(this->panel, &ProxyWindowBase::colorChanged, this, &XPanelInterface::colorChanged); - QObject::connect(this->panel, &ProxyWindowBase::maskChanged, this, &XPanelInterface::maskChanged); - QObject::connect(this->panel, &ProxyWindowBase::surfaceFormatChanged, this, &XPanelInterface::surfaceFormatChanged); - - // panel specific QObject::connect(this->panel, &XPanelWindow::anchorsChanged, this, &XPanelInterface::anchorsChanged); QObject::connect(this->panel, &XPanelWindow::marginsChanged, this, &XPanelInterface::marginsChanged); QObject::connect(this->panel, &XPanelWindow::exclusiveZoneChanged, this, &XPanelInterface::exclusiveZoneChanged); @@ -447,28 +435,13 @@ void XPanelInterface::onReload(QObject* oldInstance) { this->panel->reload(old != nullptr ? old->panel : nullptr); } -QQmlListProperty XPanelInterface::data() { return this->panel->data(); } ProxyWindowBase* XPanelInterface::proxyWindow() const { return this->panel; } -QQuickItem* XPanelInterface::contentItem() const { return this->panel->contentItem(); } -bool XPanelInterface::isBackingWindowVisible() const { return this->panel->isVisibleDirect(); } -qreal XPanelInterface::devicePixelRatio() const { return this->panel->devicePixelRatio(); } // NOLINTBEGIN #define proxyPair(type, get, set) \ type XPanelInterface::get() const { return this->panel->get(); } \ void XPanelInterface::set(type value) { this->panel->set(value); } -proxyPair(bool, isVisible, setVisible); -proxyPair(qint32, implicitWidth, setImplicitWidth); -proxyPair(qint32, implicitHeight, setImplicitHeight); -proxyPair(qint32, width, setWidth); -proxyPair(qint32, height, setHeight); -proxyPair(QuickshellScreenInfo*, screen, setScreen); -proxyPair(QColor, color, setColor); -proxyPair(PendingRegion*, mask, setMask); -proxyPair(QsSurfaceFormat, surfaceFormat, setSurfaceFormat); - -// panel specific proxyPair(Anchors, anchors, setAnchors); proxyPair(Margins, margins, setMargins); proxyPair(qint32, exclusiveZone, setExclusiveZone); diff --git a/src/x11/panel_window.hpp b/src/x11/panel_window.hpp index 02c05b1..ab36826 100644 --- a/src/x11/panel_window.hpp +++ b/src/x11/panel_window.hpp @@ -135,43 +135,8 @@ public: void onReload(QObject* oldInstance) override; [[nodiscard]] ProxyWindowBase* proxyWindow() const override; - [[nodiscard]] QQuickItem* contentItem() const override; // NOLINTBEGIN - [[nodiscard]] bool isVisible() const override; - [[nodiscard]] bool isBackingWindowVisible() const override; - void setVisible(bool visible) override; - - [[nodiscard]] qint32 implicitWidth() const override; - void setImplicitWidth(qint32 implicitWidth) override; - - [[nodiscard]] qint32 implicitHeight() const override; - void setImplicitHeight(qint32 implicitHeight) override; - - [[nodiscard]] qint32 width() const override; - void setWidth(qint32 width) override; - - [[nodiscard]] qint32 height() const override; - void setHeight(qint32 height) override; - - [[nodiscard]] virtual qreal devicePixelRatio() const override; - - [[nodiscard]] QuickshellScreenInfo* screen() const override; - void setScreen(QuickshellScreenInfo* screen) override; - - [[nodiscard]] QColor color() const override; - void setColor(QColor color) override; - - [[nodiscard]] PendingRegion* mask() const override; - void setMask(PendingRegion* mask) override; - - [[nodiscard]] QsSurfaceFormat surfaceFormat() const override; - void setSurfaceFormat(QsSurfaceFormat mask) override; - - [[nodiscard]] QQmlListProperty data() override; - - // panel specific - [[nodiscard]] Anchors anchors() const override; void setAnchors(Anchors anchors) override;