diff --git a/.clang-tidy b/.clang-tidy index 002c444d..ff820f6e 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -5,9 +5,6 @@ Checks: > -*, bugprone-*, -bugprone-easily-swappable-parameters, - -bugprone-forward-declararion-namespace, - -bugprone-forward-declararion-namespace, - -bugprone-return-const-ref-from-parameter, concurrency-*, cppcoreguidelines-*, -cppcoreguidelines-owning-memory, @@ -16,10 +13,8 @@ Checks: > -cppcoreguidelines-avoid-const-or-ref-data-members, -cppcoreguidelines-non-private-member-variables-in-classes, -cppcoreguidelines-avoid-goto, - -cppcoreguidelines-pro-bounds-array-to-pointer-decay, - -cppcoreguidelines-avoid-do-while, - -cppcoreguidelines-pro-type-reinterpret-cast, - -cppcoreguidelines-pro-type-vararg, + google-build-using-namespace. + google-explicit-constructor, google-global-names-in-headers, google-readability-casting, google-runtime-int, @@ -31,7 +26,6 @@ Checks: > -modernize-return-braced-init-list, -modernize-use-trailing-return-type, performance-*, - -performance-avoid-endl, portability-std-allocator-const, readability-*, -readability-function-cognitive-complexity, @@ -42,10 +36,6 @@ Checks: > -readability-braces-around-statements, -readability-redundant-access-specifiers, -readability-else-after-return, - -readability-container-data-pointer, - -readability-implicit-bool-conversion, - -readability-avoid-nested-conditional-operator, - -readability-math-missing-parentheses, tidyfox-*, CheckOptions: performance-for-range-copy.WarnOnAllAutoCopies: true diff --git a/.editorconfig b/.editorconfig index 439ba6b7..6b1b58df 100644 --- a/.editorconfig +++ b/.editorconfig @@ -9,7 +9,3 @@ indent_style = tab [*.nix] indent_style = space indent_size = 2 - -[*.{yml,yaml}] -indent_style = space -indent_size = 2 diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml deleted file mode 100644 index 0086358d..00000000 --- a/.github/ISSUE_TEMPLATE/config.yml +++ /dev/null @@ -1 +0,0 @@ -blank_issues_enabled: true diff --git a/.github/ISSUE_TEMPLATE/crash.yml b/.github/ISSUE_TEMPLATE/crash.yml deleted file mode 100644 index c8b4804e..00000000 --- a/.github/ISSUE_TEMPLATE/crash.yml +++ /dev/null @@ -1,82 +0,0 @@ -name: Crash Report -description: Quickshell has crashed -labels: ["bug", "crash"] -body: - - type: textarea - id: crashinfo - attributes: - label: General crash information - description: | - Paste the contents of the `info.txt` file in your crash folder here. - value: "
General information - - - ``` - - - - ``` - - -
" - validations: - required: true - - 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: dump - attributes: - label: Minidump - description: | - Attach `minidump.dmp.log` here. If it is too big to upload, compress it. - - You may skip this step if quickshell crashed while processing a password - or other sensitive information. If you skipped it write why instead. - 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: | - If you have gdb installed and use systemd, or otherwise know how to get a backtrace, - we would appreciate one. (You may have gdb installed without knowing it) - - 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. - - type: textarea - id: exe - attributes: - label: Executable - description: | - If the crash folder contains a executable.txt file, upload it here. If not you can ignore this field. - If it is too big to upload, compress it. - - Note: executable.txt is the quickshell binary. It has a .txt extension due to github's limitations on - filetypes. diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml deleted file mode 100644 index a67e5f43..00000000 --- a/.github/workflows/build.yml +++ /dev/null @@ -1,56 +0,0 @@ -name: Build -on: [push, pull_request, workflow_dispatch] - -jobs: - nix: - name: Nix - strategy: - matrix: - qtver: [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 - 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 - - - name: Download Dependencies - run: nix-build --no-out-link --expr '((import ./ci/matrix.nix) { qtver = "${{ matrix.qtver }}"; compiler = "${{ matrix.compiler }}"; }).inputDerivation' - - - name: Build - run: nix-build --no-out-link --expr '(import ./ci/matrix.nix) { qtver = "${{ matrix.qtver }}"; compiler = "${{ matrix.compiler }}"; }' - - archlinux: - name: Archlinux - runs-on: ubuntu-latest - container: archlinux - steps: - - uses: actions/checkout@v4 - - - name: Download Dependencies - run: | - pacman --noconfirm --noprogressbar -Syyu - pacman --noconfirm --noprogressbar -Sy \ - base-devel \ - cmake \ - ninja \ - pkgconf \ - qt6-base \ - qt6-declarative \ - qt6-svg \ - qt6-wayland \ - qt6-shadertools \ - wayland-protocols \ - wayland \ - libdrm \ - libxcb \ - libpipewire \ - cli11 \ - jemalloc - - - 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 --build build diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml deleted file mode 100644 index a53221cb..00000000 --- a/.github/workflows/lint.yml +++ /dev/null @@ -1,25 +0,0 @@ -name: Lint -on: [push, pull_request, workflow_dispatch] - -jobs: - lint: - name: Lint - runs-on: ubuntu-latest - 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: nicknovitski/nix-develop@v1 - - - name: Check formatting - run: clang-format -Werror --dry-run src/**/*.{cpp,hpp} - - # required for lint - - name: Build - run: | - just configure debug -DNO_PCH=ON -DBUILD_TESTING=ON - just build - - - name: Run lints - run: just lint-ci diff --git a/.gitignore b/.gitignore index dcdefe39..1933837e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,3 @@ -# related repos -/docs -/examples - # build files /result /build/ diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000..74013769 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,6 @@ +[submodule "docs"] + path = docs + url = https://git.outfoxxed.me/outfoxxed/quickshell-docs +[submodule "examples"] + path = examples + url = https://git.outfoxxed.me/outfoxxed/quickshell-examples diff --git a/BUILD.md b/BUILD.md deleted file mode 100644 index 3172dbe3..00000000 --- a/BUILD.md +++ /dev/null @@ -1,243 +0,0 @@ -# Build instructions -Instructions for building from source and distro packagers. We highly recommend -distro packagers read through this page fully. - -## Packaging -If you are packaging quickshell for official or unofficial distribution channels, -such as a distro package repository, user repository, or other shared build location, -please set the following CMake flags. - -`-DDISTRIBUTOR="your distribution platform"` - -Please make this descriptive enough to identify your specific package, for example: -- `Official Nix Flake` -- `AUR (quickshell-git)` -- `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). - -### QML Module dir -Currently all QML modules are statically linked to quickshell, but this is where -tooling information will go. - -`-DINSTALL_QML_PREFIX="path/to/qml"` - -`-DINSTALL_QMLDIR="/full/path/to/qml"` - -`INSTALL_QML_PREFIX` works the same as `INSTALL_QMLDIR`, except it prepends `CMAKE_INSTALL_PREFIX`. You usually want this. - -## Dependencies -Quickshell has a set of base dependencies you will always need, names vary by distro: - -- `cmake` -- `qt6base` -- `qt6declarative` -- `qtshadertools` (build-time only) -- `spirv-tools` (build-time only) -- `pkg-config` (build-time only) -- `cli11` - -On some distros, private Qt headers are in separate packages which you may have to install. -We currently require private headers for the following libraries: - -- `qt6declarative` -- `qt6wayland` - -We recommend an implicit dependency on `qt6svg`. If it is not installed, svg images and -svg icons will not work, including system ones. - -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, -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` - -Dependencies: `google-breakpad` - -### Jemalloc -We recommend leaving Jemalloc enabled as it will mask memory fragmentation caused -by the QML engine, which results in much lower memory usage. Without this you -will get a perceived memory leak. - -To disable: `-DUSE_JEMALLOC=OFF` - -Dependencies: `jemalloc` - -### Unix Sockets -This feature allows interaction with unix sockets and creating socket servers -which is useful for IPC and has no additional dependencies. - -WARNING: Disabling unix sockets will NOT make it safe to run arbitrary code using quickshell. -There are many vectors which mallicious code can use to escape into your system. - -To disable: `-DSOCKETS=OFF` - -### Wayland -This feature enables wayland support. Subfeatures exist for each particular wayland integration. - -WARNING: Wayland integration relies on features that are not part of the public Qt API and which -may break in minor releases. Updating quickshell's dependencies without ensuring without ensuring -that the current Qt version is supported WILL result in quickshell failing to build or misbehaving -at runtime. - -Currently supported Qt versions: `6.6`, `6.7`. - -To disable: `-DWAYLAND=OFF` - -Dependencies: - - `qt6wayland` - - `wayland` (libwayland-client) - - `wayland-scanner` (may be part of your distro's wayland package) - - `wayland-protocols` - -#### Wlroots Layershell -Enables wlroots layershell integration through the [zwlr-layer-shell-v1] protocol, -enabling use cases such as bars overlays and backgrounds. -This feature has no extra dependencies. - -To disable: `-DWAYLAND_WLR_LAYERSHELL=OFF` - -[zwlr-layer-shell-v1]: https://wayland.app/protocols/wlr-layer-shell-unstable-v1 - -#### Session Lock -Enables session lock support through the [ext-session-lock-v1] protocol, -which allows quickshell to be used as a session lock under compatible wayland compositors. - -To disable: `-DWAYLAND_SESSION_LOCK=OFF` - -[ext-session-lock-v1]: https://wayland.app/protocols/ext-session-lock-v1 - - -#### Foreign Toplevel Management -Enables management of windows of other clients through the [zwlr-foreign-toplevel-management-v1] protocol, -which allows quickshell to be used as a session lock under compatible wayland compositors. - -[zwlr-foreign-toplevel-management-v1]: https://wayland.app/protocols/wlr-foreign-toplevel-management-unstable-v1 - -To disable: `-DWAYLAND_TOPLEVEL_MANAGEMENT=OFF` - -#### Screencopy -Enables streaming video from monitors and toplevel windows through various protocols. - -To disable: `-DSCREENCOPY=OFF` - -Dependencies: -- `libdrm` -- `libgbm` - -Specific protocols can also be disabled: -- `DSCREENCOPY_ICC=OFF` - Disable screencopy via [ext-image-copy-capture-v1] -- `DSCREENCOPY_WLR=OFF` - Disable screencopy via [zwlr-screencopy-v1] -- `DSCREENCOPY_HYPRLAND_TOPLEVEL=OFF` - Disable screencopy via [hyprland-toplevel-export-v1] - -[ext-image-copy-capture-v1]:https://wayland.app/protocols/ext-image-copy-capture-v1 -[zwlr-screencopy-v1]: https://wayland.app/protocols/wlr-screencopy-unstable-v1 -[hyprland-toplevel-export-v1]: https://wayland.app/protocols/hyprland-toplevel-export-v1 - -### X11 -This feature enables x11 support. Currently this implements panel windows for X11 similarly -to the wlroots layershell above. - -To disable: `-DX11=OFF` - -Dependencies: `libxcb` - -### Pipewire -This features enables viewing and management of pipewire nodes. - -To disable: `-DSERVICE_PIPEWIRE=OFF` - -Dependencies: `libpipewire` - -### StatusNotifier / System Tray -This feature enables system tray support using the status notifier dbus protocol. - -To disable: `-DSERVICE_STATUS_NOTIFIER=OFF` - -Dependencies: `qt6dbus` (usually part of qt6base) - -### MPRIS -This feature enables access to MPRIS compatible media players using its dbus protocol. - -To disable: `-DSERVICE_MPRIS=OFF` - -Dependencies: `qt6dbus` (usually part of qt6base) - -### PAM -This feature enables PAM integration for user authentication. - -To disable: `-DSERVICE_PAM=OFF` - -Dependencies: `pam` - -### Hyprland -This feature enables hyprland specific integrations. It requires wayland support -but has no extra dependencies. - -To disable: `-DHYPRLAND=OFF` - -#### Hyprland Global Shortcuts -Enables creation of global shortcuts under hyprland through the [hyprland-global-shortcuts-v1] -protocol. Generally a much nicer alternative to using unix sockets to implement the same thing. -This feature has no extra dependencies. - -To disable: `-DHYPRLAND_GLOBAL_SHORTCUTS=OFF` - -[hyprland-global-shortcuts-v1]: https://github.com/hyprwm/hyprland-protocols/blob/main/protocols/hyprland-global-shortcuts-v1.xml - -#### Hyprland Focus Grab -Enables windows to grab focus similarly to a context menu under hyprland through the -[hyprland-focus-grab-v1] protocol. This feature has no extra dependencies. - -To disable: `-DHYPRLAND_FOCUS_GRAB=OFF` - -[hyprland-focus-grab-v1]: https://github.com/hyprwm/hyprland-protocols/blob/main/protocols/hyprland-focus-grab-v1.xml - -### i3/Sway -Enables i3 and Sway specific features, does not have any dependency on Wayland or x11. - -To disable: `-DI3=OFF` - -#### i3/Sway IPC -Enables interfacing with i3 and Sway's IPC. - -To disable: `-DI3_IPC=OFF` - -## Building -*For developers and prospective contributors: See [CONTRIBUTING.md](CONTRIBUTING.md).* - -We highly recommend using `ninja` to run the build, but you can use makefiles if you must. - -#### Configuring the build -```sh -$ cmake -GNinja -B build -DCMAKE_BUILD_TYPE=RelWithDebInfo [additional disable flags from above here] -``` - -Note that features you do not supply dependencies for MUST be disabled with their associated flags -or quickshell will fail to build. - -Additionally, note that clang builds much faster than gcc if you care. - -#### Building -```sh -$ cmake --build build -``` - -#### Installing -```sh -$ cmake --install build -``` diff --git a/CMakeLists.txt b/CMakeLists.txt index 846a280c..e5f2042f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -5,79 +5,41 @@ set(QT_MIN_VERSION "6.6.0") set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) -set(QS_BUILD_OPTIONS "") +option(BUILD_TESTING "Build tests" OFF) +option(ASAN "Enable ASAN" OFF) +option(FRAME_POINTERS "Always keep frame pointers" ${ASAN}) -function(boption VAR NAME DEFAULT) - cmake_parse_arguments(PARSE_ARGV 3 arg "" "REQUIRES" "") - - option(${VAR} ${NAME} ${DEFAULT}) - - set(STATUS "${VAR}_status") - set(EFFECTIVE "${VAR}_effective") - set(${STATUS} ${${VAR}}) - set(${EFFECTIVE} ${${VAR}}) - - if (${${VAR}} AND DEFINED arg_REQUIRES) - set(REQUIRED_EFFECTIVE "${arg_REQUIRES}_effective") - if (NOT ${${REQUIRED_EFFECTIVE}}) - set(${STATUS} "OFF (Requires ${arg_REQUIRES})") - set(${EFFECTIVE} OFF) - endif() - endif() - - set(${EFFECTIVE} "${${EFFECTIVE}}" PARENT_SCOPE) - - message(STATUS " ${NAME}: ${${STATUS}}") - - string(APPEND QS_BUILD_OPTIONS "\\n ${NAME}: ${${STATUS}}") - set(QS_BUILD_OPTIONS "${QS_BUILD_OPTIONS}" PARENT_SCOPE) -endfunction() - -set(DISTRIBUTOR "Unset" CACHE STRING "Distributor") -string(APPEND QS_BUILD_OPTIONS " Distributor: ${DISTRIBUTOR}") +option(NVIDIA_COMPAT "Workarounds for nvidia gpus" OFF) +option(SOCKETS "Enable unix socket support" ON) +option(WAYLAND "Enable wayland support" ON) +option(WAYLAND_WLR_LAYERSHELL "Support the zwlr_layer_shell_v1 wayland protocol" ON) +option(WAYLAND_SESSION_LOCK "Support the ext_session_lock_v1 wayland protocol" ON) +option(HYPRLAND "Support hyprland specific features" ON) +option(SERVICE_STATUS_NOTIFIER "StatusNotifierItem service" ON) 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}) +message(STATUS " NVIDIA workarounds: ${NVIDIA_COMPAT}") +message(STATUS " Build tests: ${BUILD_TESTING}") +message(STATUS " Sockets: ${SOCKETS}") +message(STATUS " Wayland: ${WAYLAND}") +if (WAYLAND) + message(STATUS " Wlroots Layershell: ${WAYLAND_WLR_LAYERSHELL}") + message(STATUS " Session Lock: ${WAYLAND_SESSION_LOCK}") +endif () +message(STATUS " Services") +message(STATUS " StatusNotifier: ${SERVICE_STATUS_NOTIFIER}") +message(STATUS " Hyprland: ${HYPRLAND}") -boption(CRASH_REPORTER "Crash Handling" ON) -boption(USE_JEMALLOC "Use jemalloc" ON) -boption(SOCKETS "Unix Sockets" ON) -boption(WAYLAND "Wayland" ON) -boption(WAYLAND_WLR_LAYERSHELL " Wlroots Layer-Shell" ON REQUIRES WAYLAND) -boption(WAYLAND_SESSION_LOCK " Session Lock" ON REQUIRES WAYLAND) -boption(WAYLAND_TOPLEVEL_MANAGEMENT " Foreign Toplevel Management" ON REQUIRES WAYLAND) -boption(HYPRLAND " Hyprland" ON REQUIRES WAYLAND) -boption(HYPRLAND_IPC " Hyprland IPC" ON REQUIRES HYPRLAND) -boption(HYPRLAND_GLOBAL_SHORTCUTS " Hyprland Global Shortcuts" ON REQUIRES HYPRLAND) -boption(HYPRLAND_FOCUS_GRAB " Hyprland Focus Grabbing" ON REQUIRES HYPRLAND) -boption(HYPRLAND_SURFACE_EXTENSIONS " Hyprland Surface Extensions" ON REQUIRES HYPRLAND) -boption(SCREENCOPY " Screencopy" ON REQUIRES WAYLAND) -boption(SCREENCOPY_ICC " Image Copy Capture" ON REQUIRES WAYLAND) -boption(SCREENCOPY_WLR " Wlroots Screencopy" ON REQUIRES WAYLAND) -boption(SCREENCOPY_HYPRLAND_TOPLEVEL " Hyprland Toplevel Export" ON REQUIRES WAYLAND) -boption(X11 "X11" ON) -boption(I3 "I3/Sway" ON) -boption(I3_IPC " I3/Sway IPC" ON REQUIRES I3) -boption(SERVICE_STATUS_NOTIFIER "System Tray" ON) -boption(SERVICE_PIPEWIRE "PipeWire" ON) -boption(SERVICE_MPRIS "Mpris" ON) -boption(SERVICE_PAM "Pam" ON) -boption(SERVICE_GREETD "Greetd" ON) -boption(SERVICE_UPOWER "UPower" ON) -boption(SERVICE_NOTIFICATIONS "Notifications" ON) +if (NOT DEFINED GIT_REVISION) + execute_process( + COMMAND git rev-parse HEAD + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} + OUTPUT_VARIABLE GIT_REVISION + OUTPUT_STRIP_TRAILING_WHITESPACE + ) +endif() -include(cmake/install-qml-module.cmake) -include(cmake/util.cmake) - -add_compile_options(-Wall -Wextra -Wno-vla-cxx-extension) - -# pipewire defines this, breaking PCH -add_compile_definitions(_REENTRANT) +add_compile_options(-Wall -Wextra) if (FRAME_POINTERS) add_compile_options(-fno-omit-frame-pointer) @@ -98,9 +60,8 @@ if (NOT CMAKE_BUILD_TYPE) set(CMAKE_BUILD_TYPE Debug) endif() -set(QT_FPDEPS Gui Qml Quick QuickControls2 Widgets ShaderTools) - -include(cmake/pch.cmake) +set(QT_DEPS Qt6::Gui Qt6::Qml Qt6::Quick Qt6::QuickControls2 Qt6::Widgets) +set(QT_FPDEPS Gui Qml Quick QuickControls2 Widgets) if (BUILD_TESTING) enable_testing() @@ -109,39 +70,58 @@ if (BUILD_TESTING) endif() if (SOCKETS) + list(APPEND QT_DEPS Qt6::Network) list(APPEND QT_FPDEPS Network) endif() if (WAYLAND) + list(APPEND QT_DEPS Qt6::WaylandClient Qt6::WaylandClientPrivate) list(APPEND QT_FPDEPS WaylandClient) endif() -if (SERVICE_STATUS_NOTIFIER OR SERVICE_MPRIS OR SERVICE_UPOWER OR SERVICE_NOTIFICATIONS) +if (SERVICE_STATUS_NOTIFIER) set(DBUS ON) endif() if (DBUS) + list(APPEND QT_DEPS Qt6::DBus) list(APPEND QT_FPDEPS DBus) endif() find_package(Qt6 REQUIRED COMPONENTS ${QT_FPDEPS}) -set(CMAKE_AUTOUIC OFF) qt_standard_project_setup(REQUIRES 6.6) set(QT_QML_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/qml_modules) -add_subdirectory(src) +# pch breaks clang-tidy..... somehow +if (NOT NO_PCH) + file(GENERATE + OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/pchstub.cpp + CONTENT "// intentionally empty" + ) -if (USE_JEMALLOC) - find_package(PkgConfig REQUIRED) - # IMPORTED_TARGET not working for some reason - pkg_check_modules(JEMALLOC REQUIRED jemalloc) - target_link_libraries(quickshell PRIVATE ${JEMALLOC_LIBRARIES}) + add_library(qt-pch ${CMAKE_CURRENT_BINARY_DIR}/pchstub.cpp) + target_link_libraries(qt-pch PRIVATE ${QT_DEPS}) + target_precompile_headers(qt-pch PUBLIC + + + + + + + + ) endif() -install(CODE " - execute_process( - COMMAND ${CMAKE_COMMAND} -E create_symlink \ - ${CMAKE_INSTALL_FULL_BINDIR}/quickshell \$ENV{DESTDIR}${CMAKE_INSTALL_FULL_BINDIR}/qs - ) -") +function (qs_pch target) + if (NOT NO_PCH) + target_precompile_headers(${target} REUSE_FROM qt-pch) + target_link_libraries(${target} PRIVATE ${QT_DEPS}) # required for gcc to accept the pch on plugin targets + endif() +endfunction() + +if (NVIDIA_COMPAT) + add_compile_definitions(NVIDIA_COMPAT) +endif() + +add_subdirectory(src) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index feeb746b..00000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,102 +0,0 @@ -# Contributing / Development -Instructions for development setup and upstreaming patches. - -If you just want to build or package quickshell see [BUILD.md](BUILD.md). - -## Development - -Install the dependencies listed in [BUILD.md](BUILD.md). -You probably want all of them even if you don't use all of them -to ensure tests work correctly and avoid passing a bunch of configure -flags when you need to wipe the build directory. - -Quickshell also uses `just` for common development command aliases. - -The dependencies are also available as a nix shell or nix flake which we recommend -using with nix-direnv. - -Common aliases: -- `just configure [ [extra cmake args]]` (note that you must specify debug/release to specify extra args) -- `just build` - runs the build, configuring if not configured already. -- `just run [args]` - runs quickshell with the given arguments -- `just clean` - clean up build artifacts. `just clean build` is somewhat common. - -### Formatting -All contributions should be formatted similarly to what already exists. -Group related functionality together. - -Run the formatter using `just fmt`. -If the results look stupid, fix the clang-format file if possible, -or disable clang-format in the affected area -using `// clang-format off` and `// clang-format on`. - -### Linter -All contributions should pass the linter. - -Note that running the linter requires disabling precompiled -headers and including the test codepaths: -```sh -$ just configure debug -DNO_PCH=ON -DBUILD_TESTING=ON -$ just lint -``` - -If the linter is complaining about something that you think it should not, -please disable the lint in your MR and explain your reasoning. - -### Tests -If you feel like the feature you are working on is very complex or likely to break, -please write some tests. We will ask you to directly if you send in an MR for an -overly complex or breakable feature. - -At least all tests that passed before your changes should still be passing -by the time your contribution is ready. - -You can run the tests using `just test` but you must enable them first -using `-DBUILD_TESTING=ON`. - -### Documentation -Most of quickshell's documentation is automatically generated from the source code. -You should annotate `Q_PROPERTY`s and `Q_INVOKABLE`s with doc comments. Note that the parser -cannot handle random line breaks and will usually require you to disable clang-format if the -lines are too long. - -Before submitting an MR, if adding new features please make sure the documentation is generated -reasonably using the `quickshell-docs` repo. We recommend checking it out at `/docs` in this repo. - -Doc comments take the form `///` or `///!` (summary) and work with markdown. -You can reference other types using the `@@[Module.][Type.][member]` shorthand -where all parts are optional. If module or type are not specified they will -be inferred as the current module. Member can be a `property`, `function()` or `signal(s)`. -Look at existing code for how it works. - -Quickshell modules additionally have a `module.md` file which contains a summary, description, -and list of headers to scan for documentation. - -## Contributing - -### Commits -Please structure your commit messages as `scope[!]: commit` where -the scope is something like `core` or `service/mpris`. (pick what has been -used historically or what makes sense if new.) Add `!` for changes that break -existing APIs or functionality. - -Commit descriptions should contain a summary of the changes if they are not -sufficiently addressed in the commit message. - -Please squash/rebase additions or edits to previous changes and follow the -commit style to keep the history easily searchable at a glance. -Depending on the change, it is often reasonable to squash it into just -a single commit. (If you do not follow this we will squash your changes -for you.) - -### Sending patches -You may contribute by submitting a pull request on github, asking for -an account on our git server, or emailing patches / git bundles -directly to `outfoxxed@outfoxxed.me`. - -### Getting help -If you're getting stuck, you can come talk to us in the -[quickshell-development matrix room](https://matrix.to/#/#quickshell-development:outfoxxed.me) -for help on implementation, conventions, etc. -Feel free to ask for advice early in your implementation if you are -unsure. diff --git a/Justfile b/Justfile index f60771aa..69fdff70 100644 --- a/Justfile +++ b/Justfile @@ -4,13 +4,7 @@ fmt: find src -type f \( -name "*.cpp" -o -name "*.hpp" \) -print0 | xargs -0 clang-format -i lint: - find src -type f -name "*.cpp" -print0 | parallel -j$(nproc) -q0 --no-notice --will-cite --tty --bar clang-tidy --load={{ env_var("TIDYFOX") }} - -lint-ci: - find src -type f -name "*.cpp" -print0 | parallel -j$(nproc) -q0 --no-notice --will-cite --tty clang-tidy --load={{ env_var("TIDYFOX") }} - -lint-changed: - git diff --name-only HEAD | grep "^.*\.cpp\$" | parallel -j$(nproc) --no-notice --will-cite --tty --bar clang-tidy --load={{ env_var("TIDYFOX") }} + find src -type f -name "*.cpp" -print0 | parallel -q0 --eta clang-tidy --load={{ env_var("TIDYFOX") }} configure target='debug' *FLAGS='': cmake -GNinja -B {{builddir}} \ diff --git a/README.md b/README.md index 82f912fd..d05e3347 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # quickshell -Flexbile QtQuick based desktop shell toolkit. +Simple and flexbile QtQuick based desktop shell toolkit. Hosted on: [outfoxxed's gitea], [github] @@ -11,14 +11,21 @@ Hosted on: [outfoxxed's gitea], [github] Documentation available at [quickshell.outfoxxed.me](https://quickshell.outfoxxed.me) or can be built from the [quickshell-docs](https://git.outfoxxed.me/outfoxxed/quickshell-docs) repo. -Some fully working examples can be found in the [quickshell-examples](https://git.outfoxxed.me/outfoxxed/quickshell-examples) +Some fully working examples can be found in the [quickshell-examples](https://git.outfoxxed.me/outfoxxed/quickshell-examples) repo. -# Breaking Changes -Quickshell is still in alpha and there will be breaking changes. +Both the documentation and examples are included as submodules with revisions that work with the current +version of quickshell. -Commits with breaking qml api changes will contain a `!` at the end of the scope -(`thing!: foo`) and the commit description will contain details about the broken api. +You can clone everything with +``` +$ git clone --recursive https://git.outfoxxed.me/outfoxxed/quickshell.git +``` + +Or clone missing submodules later with +``` +$ git submodule update --init --recursive +``` # Installation @@ -32,9 +39,6 @@ This repo has a nix flake you can use to install the package directly: quickshell = { url = "git+https://git.outfoxxed.me/outfoxxed/quickshell"; - - # THIS IS IMPORTANT - # Mismatched system dependencies will lead to crashes and other issues. inputs.nixpkgs.follows = "nixpkgs"; }; }; @@ -44,45 +48,75 @@ This repo has a nix flake you can use to install the package directly: Quickshell's binary is available at `quickshell.packages..default` to be added to lists such as `environment.systemPackages` or `home.packages`. -The package contains several features detailed in [BUILD.md](BUILD.md) which can be enabled -or disabled with overrides: - -```nix -quickshell.packages..default.override { - withJemalloc = true; - withQtSvg = true; - withWayland = true; - withX11 = true; - withPipewire = true; - withPam = true; - withHyprland = true; -} -``` +`quickshell.packages..nvidia` is also available for nvidia users which fixes some +common crashes. Note: by default this package is built with clang as it is significantly faster. -## Arch (AUR) -Quickshell has a third party [AUR package] available under the same name. -It is not managed by us and should be looked over before use. +## Manual -[AUR package]: https://aur.archlinux.org/packages/quickshell +If not using nix, you'll have to build from source. -> [!CAUTION] -> The AUR provides no way to force the quickshell package to rebuild when the Qt version changes. -> If you experience crashes after updating Qt, please try rebuilding Quickshell against the -> current Qt version before opening an issue. +### Dependencies +To build quickshell at all, you will need the following packages (names may vary by distro) -## Fedora (COPR) -Quickshell has a third party [Fedora COPR package] available under the same name. -It is not managed by us and should be looked over before use. +- just +- cmake +- pkg-config +- ninja +- Qt6 [ QtBase, QtDeclarative ] -[Fedora COPR package]: https://copr.fedorainfracloud.org/coprs/errornointernet/quickshell +To build with wayland support you will additionally need: +- wayland +- wayland-scanner (may be part of wayland on some distros) +- wayland-protocols +- Qt6 [ QtWayland ] -## Anything else -See [BUILD.md](BUILD.md) for instructions on building and packaging quickshell. +### Building -# Contributing / Development -See [CONTRIBUTING.md](CONTRIBUTING.md) for details. +To make a release build of quickshell run: +```sh +$ just release +``` + +If running an nvidia GPU, instead run: +```sh +$ just configure release -DNVIDIA_COMPAT=ON +$ just build +``` + +(These commands are just aliases for cmake commands you can run directly, +see the Justfile for more information.) + +If you have all the dependencies installed and they are in expected +locations this will build correctly. + +To install to /usr/local/bin run as root (usually `sudo`) in the same folder: +``` +$ just install +``` + +### Building (Nix) + +You can build directly using the provided nix flake or nix package. +``` +nix build +nix build -f package.nix # calls default.nix with a basic callPackage expression +``` + +# Development + +For nix there is a devshell available from `shell.nix` and as a devShell +output from the flake. + +The Justfile contains various useful aliases: +- `just configure [ [extra cmake args]]` +- `just build` (runs configure for debug mode) +- `just run [args]` +- `just clean` +- `just test [args]` (configure with `-DBUILD_TESTING=ON` first) +- `just fmt` +- `just lint` #### License diff --git a/ci/matrix.nix b/ci/matrix.nix deleted file mode 100644 index be2da616..00000000 --- a/ci/matrix.nix +++ /dev/null @@ -1,8 +0,0 @@ -{ - qtver, - compiler, -}: let - nixpkgs = (import ./nix-checkouts.nix).${builtins.replaceStrings ["."] ["_"] qtver}; - compilerOverride = (nixpkgs.callPackage ./variations.nix {}).${compiler}; - pkg = (nixpkgs.callPackage ../default.nix {}).override compilerOverride; -in pkg diff --git a/ci/nix-checkouts.nix b/ci/nix-checkouts.nix deleted file mode 100644 index f38d5fa4..00000000 --- a/ci/nix-checkouts.nix +++ /dev/null @@ -1,63 +0,0 @@ -let - byCommit = { - commit, - sha256, - }: import (builtins.fetchTarball { - name = "nixpkgs-${commit}"; - 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. - - qt6_8_1 = byCommit { - commit = "3df3c47c19dc90fec35359e89ffb52b34d2b0e94"; - sha256 = "1lhlm7czhwwys5ak6ngb5li6bxddilb9479k9nkss502kw8hwjyz"; - }; - - qt6_8_0 = byCommit { - commit = "352f462ad9d2aa2cde75fdd8f1734e86402a3ff6"; - sha256 = "02zfgkr9fpd6iwfh6dcr3m6fnx61jppm3v081f3brvkqwmmz7zq1"; - }; - - qt6_7_3 = byCommit { - commit = "273673e839189c26130d48993d849a84199523e6"; - sha256 = "0aca369hdxb8j0vx9791anyzy4m65zckx0lriicqhp95kv9q6m7z"; - }; - - qt6_7_2 = byCommit { - commit = "841f166ff96fc2f3ecd1c0cc08072633033d41bf"; - sha256 = "0d7p0cp7zjiadhpa6sdafxvrpw4lnmb1h673w17q615vm1yaasvy"; - }; - - qt6_7_1 = byCommit { - commit = "69bee9866a4e2708b3153fdb61c1425e7857d6b8"; - sha256 = "1an4sha4jsa29dvc4n9mqxbq8jjwg7frl0rhy085g73m7l1yx0lj"; - }; - - qt6_7_0 = byCommit { - commit = "4fbbc17ccf11bc80002b19b31387c9c80276f076"; - sha256 = "09lhgdqlx8j9a7vpdcf8sddlhbzjq0s208spfmxfjdn14fvx8k0j"; - }; - - qt6_6_3 = byCommit { - commit = "8f1a3fbaa92f1d59b09f2d24af6a607b5a280071"; - sha256 = "0322zwxvmg8v2wkm03xpk6mqmmbfjgrhc9prcx0zd36vjl6jmi18"; - }; - - qt6_6_2 = byCommit { - commit = "0bb9cfbd69459488576a0ef3c0e0477bedc3a29e"; - sha256 = "172ww486jm1mczk9id78s32p7ps9m9qgisml286flc8jffb6yad8"; - }; - - qt6_6_1 = byCommit { - commit = "8eecc3342103c38eea666309a7c0d90d403a039a"; - sha256 = "1lakc0immsgrpz3basaysdvd0sx01r0mcbyymx6id12fk0404z5r"; - }; - - qt6_6_0 = byCommit { - commit = "1ded005f95a43953112ffc54b39593ea2f16409f"; - sha256 = "1xvyd3lj81hak9j53mrhdsqx78x5v2ppv8m2s54qa2099anqgm0f"; - }; -} diff --git a/ci/variations.nix b/ci/variations.nix deleted file mode 100644 index b0889be6..00000000 --- a/ci/variations.nix +++ /dev/null @@ -1,7 +0,0 @@ -{ - clangStdenv, - gccStdenv, -}: { - clang = { buildStdenv = clangStdenv; }; - gcc = { buildStdenv = gccStdenv; }; -} diff --git a/cmake/install-qml-module.cmake b/cmake/install-qml-module.cmake deleted file mode 100644 index 5c95531c..00000000 --- a/cmake/install-qml-module.cmake +++ /dev/null @@ -1,89 +0,0 @@ -set(INSTALL_QMLDIR "" CACHE STRING "QML install dir") -set(INSTALL_QML_PREFIX "" CACHE STRING "QML install prefix") - -# There doesn't seem to be a standard cross-distro qml install path. -if ("${INSTALL_QMLDIR}" STREQUAL "" AND "${INSTALL_QML_PREFIX}" STREQUAL "") - message(WARNING "Neither INSTALL_QMLDIR nor INSTALL_QML_PREFIX is set. QML modules will not be installed.") -else() - if ("${INSTALL_QMLDIR}" STREQUAL "") - set(QML_FULL_INSTALLDIR "${CMAKE_INSTALL_PREFIX}/${INSTALL_QML_PREFIX}") - else() - set(QML_FULL_INSTALLDIR "${INSTALL_QMLDIR}") - endif() - - message(STATUS "QML install dir: ${QML_FULL_INSTALLDIR}") -endif() - -# Install a given target as a QML module. This is mostly pulled from ECM, as there does not seem -# to be an official way to do it. -# see https://github.com/KDE/extra-cmake-modules/blob/fe0f606bf7f222e36f7560fd7a2c33ef993e23bb/modules/ECMQmlModule6.cmake#L160 -function(install_qml_module arg_TARGET) - if (NOT DEFINED QML_FULL_INSTALLDIR) - return() - endif() - - qt_query_qml_module(${arg_TARGET} - URI module_uri - VERSION module_version - PLUGIN_TARGET module_plugin_target - TARGET_PATH module_target_path - QMLDIR module_qmldir - TYPEINFO module_typeinfo - QML_FILES module_qml_files - RESOURCES module_resources - ) - - set(module_dir "${QML_FULL_INSTALLDIR}/${module_target_path}") - - if (NOT TARGET "${module_plugin_target}") - message(FATAL_ERROR "install_qml_modules called for a target without a plugin") - endif() - - get_target_property(target_type "${arg_TARGET}" TYPE) - if (NOT "${target_type}" STREQUAL "STATIC_LIBRARY") - install( - TARGETS "${arg_TARGET}" - LIBRARY DESTINATION "${module_dir}" - RUNTIME DESTINATION "${module_dir}" - ) - - install( - TARGETS "${module_plugin_target}" - LIBRARY DESTINATION "${module_dir}" - RUNTIME DESTINATION "${module_dir}" - ) - endif() - - install(FILES "${module_qmldir}" DESTINATION "${module_dir}") - install(FILES "${module_typeinfo}" DESTINATION "${module_dir}") - - # Install QML files - list(LENGTH module_qml_files num_files) - if (NOT "${module_qml_files}" MATCHES "NOTFOUND" AND ${num_files} GREATER 0) - qt_query_qml_module(${arg_TARGET} QML_FILES_DEPLOY_PATHS qml_files_deploy_paths) - - math(EXPR last_index "${num_files} - 1") - foreach(i RANGE 0 ${last_index}) - list(GET module_qml_files ${i} src_file) - list(GET qml_files_deploy_paths ${i} deploy_path) - get_filename_component(dst_name "${deploy_path}" NAME) - get_filename_component(dest_dir "${deploy_path}" DIRECTORY) - install(FILES "${src_file}" DESTINATION "${module_dir}/${dest_dir}" RENAME "${dst_name}") - endforeach() - endif() - - # Install resources - list(LENGTH module_resources num_files) - if (NOT "${module_resources}" MATCHES "NOTFOUND" AND ${num_files} GREATER 0) - qt_query_qml_module(${arg_TARGET} RESOURCES_DEPLOY_PATHS resources_deploy_paths) - - math(EXPR last_index "${num_files} - 1") - foreach(i RANGE 0 ${last_index}) - list(GET module_resources ${i} src_file) - list(GET resources_deploy_paths ${i} deploy_path) - get_filename_component(dst_name "${deploy_path}" NAME) - get_filename_component(dest_dir "${deploy_path}" DIRECTORY) - install(FILES "${src_file}" DESTINATION "${module_dir}/${dest_dir}" RENAME "${dst_name}") - endforeach() - endif() -endfunction() diff --git a/cmake/pch.cmake b/cmake/pch.cmake deleted file mode 100644 index e136015e..00000000 --- a/cmake/pch.cmake +++ /dev/null @@ -1,85 +0,0 @@ -# pch breaks clang-tidy..... somehow -if (NOT NO_PCH) - file(GENERATE - OUTPUT ${CMAKE_BINARY_DIR}/pchstub.cpp - CONTENT "// intentionally empty" - ) -endif() - -function (qs_pch target) - if (NO_PCH) - return() - endif() - - cmake_parse_arguments(PARSE_ARGV 1 arg "" "SET" "") - - if ("${arg_SET}" STREQUAL "") - set(arg_SET "common") - endif() - - target_precompile_headers(${target} REUSE_FROM "qs-pchset-${arg_SET}") -endfunction() - -function (qs_module_pch target) - qs_pch(${target} ${ARGN}) - qs_pch("${target}plugin" SET plugin) - qs_pch("${target}plugin_init" SET plugin) -endfunction() - -function (qs_add_pchset SETNAME) - if (NO_PCH) - return() - endif() - - cmake_parse_arguments(PARSE_ARGV 1 arg "" "" "HEADERS;DEPENDENCIES") - - set(LIBNAME "qs-pchset-${SETNAME}") - - add_library(${LIBNAME} ${CMAKE_BINARY_DIR}/pchstub.cpp) - target_link_libraries(${LIBNAME} ${arg_DEPENDENCIES}) - target_precompile_headers(${LIBNAME} PUBLIC ${arg_HEADERS}) -endfunction() - -set(COMMON_PCH_SET - - - - - - - - - - -) - -qs_add_pchset(common - DEPENDENCIES Qt::Quick - HEADERS ${COMMON_PCH_SET} -) - -qs_add_pchset(large - DEPENDENCIES Qt::Quick - HEADERS - ${COMMON_PCH_SET} - - - - - - - - - - -) - - -# including qplugin.h directly will cause required symbols to disappear -qs_add_pchset(plugin - DEPENDENCIES Qt::Qml - HEADERS - - - -) diff --git a/cmake/util.cmake b/cmake/util.cmake deleted file mode 100644 index 14fa7c2d..00000000 --- a/cmake/util.cmake +++ /dev/null @@ -1,29 +0,0 @@ -# Adds a dependency hint to the link order, but does not block build on the dependency. -function (qs_add_link_dependencies target) - set_property( - TARGET ${target} - APPEND PROPERTY INTERFACE_LINK_LIBRARIES - ${ARGN} - ) -endfunction() - -function (qs_append_qmldir target text) - get_property(qmldir_content TARGET ${target} PROPERTY _qt_internal_qmldir_content) - - if ("${qmldir_content}" STREQUAL "") - message(WARNING "qs_append_qmldir depends on private Qt cmake code, which has broken.") - return() - endif() - - set_property(TARGET ${target} APPEND_STRING PROPERTY _qt_internal_qmldir_content ${text}) -endfunction() - -# DEPENDENCIES introduces a cmake dependency which we don't need with static modules. -# This greatly improves comp speed by not introducing those dependencies. -function (qs_add_module_deps_light target) - foreach (dep IN LISTS ARGN) - string(APPEND qmldir_extra "depends ${dep}\n") - endforeach() - - qs_append_qmldir(${target} "${qmldir_extra}") -endfunction() diff --git a/default.nix b/default.nix index 79c9b7a4..3f2029b5 100644 --- a/default.nix +++ b/default.nix @@ -3,22 +3,13 @@ nix-gitignore, pkgs, keepDebugInfo, - buildStdenv ? pkgs.clangStdenv, + buildStdenv ? pkgs.clang17Stdenv, cmake, ninja, qt6, - spirv-tools, - cli11, - breakpad, - jemalloc, wayland, wayland-protocols, - libdrm, - libgbm ? null, - xorg, - pipewire, - pam, gitRev ? (let headExists = builtins.pathExists ./.git/HEAD; @@ -32,15 +23,9 @@ else "unknown"), debug ? false, - withCrashReporter ? true, - withJemalloc ? true, # masks heap fragmentation - withQtSvg ? true, - withWayland ? true, - withX11 ? true, - withPipewire ? true, - withPam ? true, - withHyprland ? true, - withI3 ? true, + enableWayland ? true, + nvidiaCompat ? false, + svgSupport ? true, # you almost always want this }: buildStdenv.mkDerivation { pname = "quickshell${lib.optionalString debug "-debug"}"; version = "0.1.0"; @@ -49,55 +34,43 @@ nativeBuildInputs = with pkgs; [ cmake ninja - qt6.qtshadertools - spirv-tools qt6.wrapQtAppsHook + ] ++ (lib.optionals enableWayland [ pkg-config - ] ++ (lib.optionals withWayland [ wayland-protocols wayland-scanner ]); - buildInputs = [ + buildInputs = with pkgs; [ qt6.qtbase qt6.qtdeclarative - cli11 ] - ++ lib.optional withCrashReporter breakpad - ++ lib.optional withJemalloc jemalloc - ++ lib.optional withQtSvg qt6.qtsvg - ++ lib.optionals withWayland ([ qt6.qtwayland wayland ] ++ (if libgbm != null then [ libdrm libgbm ] else [])) - ++ lib.optional withX11 xorg.libxcb - ++ lib.optional withPam pam - ++ lib.optional withPipewire pipewire; + ++ (lib.optionals enableWayland [ qt6.qtwayland wayland ]) + ++ (lib.optionals svgSupport [ qt6.qtsvg ]); - cmakeBuildType = if debug then "Debug" else "RelWithDebInfo"; + QTWAYLANDSCANNER = lib.optionalString enableWayland "${qt6.qtwayland}/libexec/qtwaylandscanner"; + + configurePhase = let + cmakeBuildType = if debug + then "Debug" + else "RelWithDebInfo"; + in '' + cmakeBuildType=${cmakeBuildType} # qt6 setup hook resets this for some godforsaken reason + cmakeConfigurePhase + ''; 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) - ]; + "-DGIT_REVISION=${gitRev}" + ] ++ lib.optional (!enableWayland) "-DWAYLAND=OFF" + ++ lib.optional nvidiaCompat "-DNVIDIA_COMPAT=ON"; - # 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; + buildPhase = "ninjaBuildPhase"; + enableParallelBuilding = true; + dontStrip = true; meta = with lib; { homepage = "https://git.outfoxxed.me/outfoxxed/quickshell"; - description = "Flexbile QtQuick based desktop shell toolkit"; + description = "Simple and flexbile QtQuick based desktop shell toolkit"; license = licenses.lgpl3Only; platforms = platforms.linux; }; diff --git a/docs b/docs new file mode 160000 index 00000000..149b784a --- /dev/null +++ b/docs @@ -0,0 +1 @@ +Subproject commit 149b784a5a4c40ada67cb9f6af5a5350678ab6d4 diff --git a/examples b/examples new file mode 160000 index 00000000..b9e744b5 --- /dev/null +++ b/examples @@ -0,0 +1 @@ +Subproject commit b9e744b50673304dfddb68f3da2a2e906d028b96 diff --git a/flake.lock b/flake.lock index df5aa3f9..1527f635 100644 --- a/flake.lock +++ b/flake.lock @@ -2,11 +2,11 @@ "nodes": { "nixpkgs": { "locked": { - "lastModified": 1736012469, - "narHash": "sha256-/qlNWm/IEVVH7GfgAIyP6EsVZI6zjAx1cV5zNyrs+rI=", + "lastModified": 1709237383, + "narHash": "sha256-cy6ArO4k5qTx+l5o+0mL9f5fa86tYUX3ozE1S+Txlds=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "8f3e1f807051e32d8c95cd12b9b421623850a34d", + "rev": "1536926ef5621b09bba54035ae2bb6d806d72ac8", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index a0bc18d4..5bb5069e 100644 --- a/flake.nix +++ b/flake.nix @@ -12,8 +12,10 @@ quickshell = pkgs.callPackage ./default.nix { gitRev = self.rev or self.dirtyRev; }; + quickshell-nvidia = quickshell.override { nvidiaCompat = true; }; default = quickshell; + nvidia = quickshell-nvidia; }); devShells = forEachSystem (system: pkgs: rec { diff --git a/shell.nix b/shell.nix index 82382f90..07b5b57d 100644 --- a/shell.nix +++ b/shell.nix @@ -15,12 +15,13 @@ in pkgs.mkShell.override { stdenv = quickshell.stdenv; } { nativeBuildInputs = with pkgs; [ just - clang-tools + clang-tools_17 parallel makeWrapper ]; TIDYFOX = "${tidyfox}/lib/libtidyfox.so"; + QTWAYLANDSCANNER = quickshell.QTWAYLANDSCANNER; shellHook = '' export CMAKE_BUILD_PARALLEL_LEVEL=$(nproc) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 882d2bae..8fe9c651 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -2,18 +2,8 @@ qt_add_executable(quickshell main.cpp) install(TARGETS quickshell RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}) -add_subdirectory(build) -add_subdirectory(launch) add_subdirectory(core) -add_subdirectory(debug) -add_subdirectory(ipc) -add_subdirectory(window) add_subdirectory(io) -add_subdirectory(widgets) - -if (CRASH_REPORTER) - add_subdirectory(crash) -endif() if (DBUS) add_subdirectory(dbus) @@ -21,10 +11,6 @@ endif() if (WAYLAND) add_subdirectory(wayland) -endif() - -if (X11) - add_subdirectory(x11) -endif() +endif () add_subdirectory(services) diff --git a/src/build/CMakeLists.txt b/src/build/CMakeLists.txt deleted file mode 100644 index bb35da99..00000000 --- a/src/build/CMakeLists.txt +++ /dev/null @@ -1,26 +0,0 @@ -add_library(quickshell-build INTERFACE) - -if (NOT DEFINED GIT_REVISION) - execute_process( - COMMAND git rev-parse HEAD - WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} - OUTPUT_VARIABLE GIT_REVISION - OUTPUT_STRIP_TRAILING_WHITESPACE - ) -endif() - -if (CRASH_REPORTER) - set(CRASH_REPORTER_DEF 1) -else() - set(CRASH_REPORTER_DEF 0) -endif() - -if (DISTRIBUTOR_DEBUGINFO_AVAILABLE) - set(DEBUGINFO_AVAILABLE 1) -else() - set(DEBUGINFO_AVAILABLE 0) -endif() - -configure_file(build.hpp.in build.hpp @ONLY ESCAPE_QUOTES) - -target_include_directories(quickshell-build INTERFACE ${CMAKE_CURRENT_BINARY_DIR}) diff --git a/src/build/build.hpp.in b/src/build/build.hpp.in deleted file mode 100644 index 075abd17..00000000 --- a/src/build/build.hpp.in +++ /dev/null @@ -1,12 +0,0 @@ -#pragma once - -// NOLINTBEGIN -#define GIT_REVISION "@GIT_REVISION@" -#define DISTRIBUTOR "@DISTRIBUTOR@" -#define DISTRIBUTOR_DEBUGINFO_AVAILABLE @DEBUGINFO_AVAILABLE@ -#define CRASH_REPORTER @CRASH_REPORTER_DEF@ -#define BUILD_TYPE "@CMAKE_BUILD_TYPE@" -#define COMPILER "@CMAKE_CXX_COMPILER_ID@ (@CMAKE_CXX_COMPILER_VERSION@)" -#define COMPILE_FLAGS "@CMAKE_CXX_FLAGS@" -#define BUILD_CONFIGURATION "@QS_BUILD_OPTIONS@" -// NOLINTEND diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 6778e984..b40b807f 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -1,14 +1,20 @@ qt_add_library(quickshell-core STATIC + main.cpp plugin.cpp shell.cpp variants.cpp rootwrapper.cpp + proxywindow.cpp reload.cpp rootwrapper.cpp qmlglobal.cpp qmlscreen.cpp region.cpp persistentprops.cpp + windowinterface.cpp + floatingwindow.cpp + panelinterface.cpp + popupwindow.cpp singleton.cpp generation.cpp scan.cpp @@ -20,38 +26,13 @@ qt_add_library(quickshell-core STATIC imageprovider.cpp transformwatcher.cpp boundcomponent.cpp - model.cpp - elapsedtimer.cpp - desktopentry.cpp - objectrepeater.cpp - platformmenu.cpp - qsmenu.cpp - retainable.cpp - popupanchor.cpp - types.cpp - qsmenuanchor.cpp - clock.cpp - logging.cpp - paths.cpp - instanceinfo.cpp - common.cpp - iconprovider.cpp - scriptmodel.cpp ) -qt_add_qml_module(quickshell-core - URI Quickshell - VERSION 0.1 - DEPENDENCIES QtQuick - OPTIONAL_IMPORTS Quickshell._Window - DEFAULT_IMPORTS Quickshell._Window -) +set_source_files_properties(main.cpp PROPERTIES COMPILE_DEFINITIONS GIT_REVISION="${GIT_REVISION}") +qt_add_qml_module(quickshell-core URI Quickshell VERSION 0.1) -install_qml_module(quickshell-core) - -target_link_libraries(quickshell-core PRIVATE Qt::Quick Qt::Widgets) - -qs_module_pch(quickshell-core SET large) +target_link_libraries(quickshell-core PRIVATE ${QT_DEPS}) +qs_pch(quickshell-core) target_link_libraries(quickshell PRIVATE quickshell-coreplugin) diff --git a/src/core/clock.cpp b/src/core/clock.cpp deleted file mode 100644 index 90938d21..00000000 --- a/src/core/clock.cpp +++ /dev/null @@ -1,88 +0,0 @@ -#include "clock.hpp" - -#include -#include -#include -#include -#include - -SystemClock::SystemClock(QObject* parent): QObject(parent) { - QObject::connect(&this->timer, &QTimer::timeout, this, &SystemClock::onTimeout); - this->update(); -} - -bool SystemClock::enabled() const { return this->mEnabled; } - -void SystemClock::setEnabled(bool enabled) { - if (enabled == this->mEnabled) return; - this->mEnabled = enabled; - emit this->enabledChanged(); - this->update(); -} - -SystemClock::Enum SystemClock::precision() const { return this->mPrecision; } - -void SystemClock::setPrecision(SystemClock::Enum precision) { - if (precision == this->mPrecision) return; - this->mPrecision = precision; - emit this->precisionChanged(); - this->update(); -} - -void SystemClock::onTimeout() { - this->setTime(this->targetTime); - this->schedule(this->targetTime); -} - -void SystemClock::update() { - if (this->mEnabled) { - this->setTime(QDateTime::fromMSecsSinceEpoch(0)); - this->schedule(QDateTime::fromMSecsSinceEpoch(0)); - } else { - this->timer.stop(); - } -} - -void SystemClock::setTime(const QDateTime& targetTime) { - auto currentTime = QDateTime::currentDateTime(); - auto offset = currentTime.msecsTo(targetTime); - this->currentTime = offset > -500 && offset < 500 ? targetTime : currentTime; - - auto time = this->currentTime.time(); - this->currentTime.setTime(QTime( - this->mPrecision >= SystemClock::Hours ? time.hour() : 0, - this->mPrecision >= SystemClock::Minutes ? time.minute() : 0, - this->mPrecision >= SystemClock::Seconds ? time.second() : 0 - )); - - emit this->dateChanged(); -} - -void SystemClock::schedule(const QDateTime& targetTime) { - auto secondPrecision = this->mPrecision >= SystemClock::Seconds; - auto minutePrecision = this->mPrecision >= SystemClock::Minutes; - auto hourPrecision = this->mPrecision >= SystemClock::Hours; - - auto currentTime = QDateTime::currentDateTime(); - - auto offset = currentTime.msecsTo(targetTime); - - // timer skew - auto nextTime = offset > 0 && offset < 500 ? targetTime : currentTime; - - auto baseTimeT = nextTime.time(); - nextTime.setTime(QTime( - hourPrecision ? baseTimeT.hour() : 0, - minutePrecision ? baseTimeT.minute() : 0, - secondPrecision ? baseTimeT.second() : 0 - )); - - if (secondPrecision) nextTime = nextTime.addSecs(1); - else if (minutePrecision) nextTime = nextTime.addSecs(60); - else if (hourPrecision) nextTime = nextTime.addSecs(3600); - - auto delay = currentTime.msecsTo(nextTime); - - this->timer.start(static_cast(delay)); - this->targetTime = nextTime; -} diff --git a/src/core/clock.hpp b/src/core/clock.hpp deleted file mode 100644 index 67461911..00000000 --- a/src/core/clock.hpp +++ /dev/null @@ -1,91 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include -#include - -///! System clock accessor. -/// SystemClock is a view into the system's clock. -/// It updates at hour, minute, or second intervals depending on @@precision. -/// -/// # Examples -/// ```qml -/// SystemClock { -/// id: clock -/// precision: SystemClock.Seconds -/// } -/// -/// @@QtQuick.Text { -/// text: Qt.formatDateTime(clock.date, "hh:mm:ss - yyyy-MM-dd") -/// } -/// ``` -/// -/// > [!WARNING] Clock updates will trigger within 50ms of the system clock changing, -/// > however this can be either before or after the clock changes (+-50ms). If you -/// > need a date object, use @@date instead of constructing a new one, or the time -/// > of the constructed object could be off by up to a second. -class SystemClock: public QObject { - Q_OBJECT; - /// If the clock should update. Defaults to true. - /// - /// Setting enabled to false pauses the clock. - Q_PROPERTY(bool enabled READ enabled WRITE setEnabled NOTIFY enabledChanged); - /// The precision the clock should measure at. Defaults to `SystemClock.Seconds`. - Q_PROPERTY(SystemClock::Enum precision READ precision WRITE setPrecision NOTIFY precisionChanged); - /// The current date and time. - /// - /// > [!TIP] You can use @@QtQml.Qt.formatDateTime() to get the time as a string in - /// > your format of choice. - Q_PROPERTY(QDateTime date READ date NOTIFY dateChanged); - /// The current hour. - Q_PROPERTY(quint32 hours READ hours NOTIFY dateChanged); - /// The current minute, or 0 if @@precision is `SystemClock.Hours`. - Q_PROPERTY(quint32 minutes READ minutes NOTIFY dateChanged); - /// The current second, or 0 if @@precision is `SystemClock.Hours` or `SystemClock.Minutes`. - Q_PROPERTY(quint32 seconds READ seconds NOTIFY dateChanged); - QML_ELEMENT; - -public: - // must be named enum until docgen is ready to handle member enums better - enum Enum : quint8 { - Hours = 1, - Minutes = 2, - Seconds = 3, - }; - Q_ENUM(Enum); - - explicit SystemClock(QObject* parent = nullptr); - - [[nodiscard]] bool enabled() const; - void setEnabled(bool enabled); - - [[nodiscard]] SystemClock::Enum precision() const; - void setPrecision(SystemClock::Enum precision); - - [[nodiscard]] QDateTime date() const { return this->currentTime; } - [[nodiscard]] quint32 hours() const { return this->currentTime.time().hour(); } - [[nodiscard]] quint32 minutes() const { return this->currentTime.time().minute(); } - [[nodiscard]] quint32 seconds() const { return this->currentTime.time().second(); } - -signals: - void enabledChanged(); - void precisionChanged(); - void dateChanged(); - -private slots: - void onTimeout(); - -private: - bool mEnabled = true; - SystemClock::Enum mPrecision = SystemClock::Seconds; - QTimer timer; - QDateTime currentTime; - QDateTime targetTime; - - void update(); - void setTime(const QDateTime& targetTime); - void schedule(const QDateTime& targetTime); -}; diff --git a/src/core/common.cpp b/src/core/common.cpp deleted file mode 100644 index d09661f1..00000000 --- a/src/core/common.cpp +++ /dev/null @@ -1,9 +0,0 @@ -#include "common.hpp" - -#include - -namespace qs { - -const QDateTime Common::LAUNCH_TIME = QDateTime::currentDateTime(); - -} diff --git a/src/core/common.hpp b/src/core/common.hpp deleted file mode 100644 index 36094f89..00000000 --- a/src/core/common.hpp +++ /dev/null @@ -1,11 +0,0 @@ -#pragma once - -#include - -namespace qs { - -struct Common { - static const QDateTime LAUNCH_TIME; -}; - -} // namespace qs diff --git a/src/core/desktopentry.cpp b/src/core/desktopentry.cpp deleted file mode 100644 index 75a088d9..00000000 --- a/src/core/desktopentry.cpp +++ /dev/null @@ -1,391 +0,0 @@ -#include "desktopentry.hpp" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "model.hpp" - -namespace { -Q_LOGGING_CATEGORY(logDesktopEntry, "quickshell.desktopentry", QtWarningMsg); -} - -struct Locale { - explicit Locale() = default; - - explicit Locale(const QString& string) { - auto territoryIdx = string.indexOf('_'); - auto codesetIdx = string.indexOf('.'); - auto modifierIdx = string.indexOf('@'); - - auto parseEnd = string.length(); - - if (modifierIdx != -1) { - this->modifier = string.sliced(modifierIdx + 1, parseEnd - modifierIdx - 1); - parseEnd = modifierIdx; - } - - if (codesetIdx != -1) { - parseEnd = codesetIdx; - } - - if (territoryIdx != -1) { - this->territory = string.sliced(territoryIdx + 1, parseEnd - territoryIdx - 1); - parseEnd = territoryIdx; - } - - this->language = string.sliced(0, parseEnd); - } - - [[nodiscard]] bool isValid() const { return !this->language.isEmpty(); } - - [[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; - - auto score = 1; - if (territoryMatches) score += 2; - if (modifierMatches) score += 1; - - return score; - } - - static const Locale& system() { - static Locale* locale = nullptr; // NOLINT - - if (locale == nullptr) { - auto lstr = qEnvironmentVariable("LC_MESSAGES"); - if (lstr.isEmpty()) lstr = qEnvironmentVariable("LANG"); - locale = new Locale(lstr); - } - - return *locale; - } - - QString language; - QString territory; - QString modifier; -}; - -// NOLINTNEXTLINE(misc-use-internal-linkage) -QDebug operator<<(QDebug debug, const Locale& locale) { - auto saver = QDebugStateSaver(debug); - debug.nospace() << "Locale(language=" << locale.language << ", territory=" << locale.territory - << ", modifier" << locale.modifier << ')'; - - return debug; -} - -void DesktopEntry::parseEntry(const QString& text) { - const auto& system = Locale::system(); - - auto groupName = QString(); - auto entries = QHash>(); - - auto finishCategory = [this, &groupName, &entries]() { - if (groupName == "Desktop Entry") { - if (entries["Type"].second != "Application") return; - if (entries.contains("Hidden") && entries["Hidden"].second == "true") return; - - for (const auto& [key, pair]: entries.asKeyValueRange()) { - auto& [_, value] = pair; - this->mEntries.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; - else if (key == "Exec") this->mExecString = 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); - } - } else if (groupName.startsWith("Desktop Action ")) { - auto actionName = groupName.sliced(16); - auto* action = new DesktopAction(actionName, this); - - for (const auto& [key, pair]: entries.asKeyValueRange()) { - const auto& [_, value] = pair; - action->mEntries.insert(key, value); - - if (key == "Name") action->mName = value; - else if (key == "Icon") action->mIcon = value; - else if (key == "Exec") action->mExecString = value; - } - - this->mActions.insert(actionName, action); - } - - entries.clear(); - }; - - for (auto& line: text.split(u'\n', Qt::SkipEmptyParts)) { - if (line.startsWith(u'#')) continue; - - if (line.startsWith(u'[') && line.endsWith(u']')) { - finishCategory(); - groupName = line.sliced(1, line.length() - 2); - continue; - } - - auto splitIdx = line.indexOf(u'='); - if (splitIdx == -1) { - qCWarning(logDesktopEntry) << "Encountered invalid line in desktop entry (no =)" << line; - continue; - } - - auto key = line.sliced(0, splitIdx); - const auto& value = line.sliced(splitIdx + 1); - - auto localeIdx = key.indexOf('['); - Locale locale; - if (localeIdx != -1 && localeIdx != key.length() - 1) { - locale = Locale(key.sliced(localeIdx + 1, key.length() - localeIdx - 2)); - key = key.sliced(0, localeIdx); - } - - if (entries.contains(key)) { - const auto& old = entries.value(key); - - auto oldScore = system.matchScore(old.first); - auto newScore = system.matchScore(locale); - - if (newScore > oldScore || (oldScore == 0 && !locale.isValid())) { - entries.insert(key, qMakePair(locale, value)); - } - } else { - entries.insert(key, qMakePair(locale, value)); - } - } - - finishCategory(); -} - -void DesktopEntry::execute() const { - DesktopEntry::doExec(this->mExecString, this->mWorkingDirectory); -} - -bool DesktopEntry::isValid() const { return !this->mName.isEmpty(); } -bool DesktopEntry::noDisplay() const { return this->mNoDisplay; } - -QVector DesktopEntry::actions() const { return this->mActions.values(); } - -QVector DesktopEntry::parseExecString(const QString& execString) { - QVector arguments; - QString currentArgument; - auto parsingString = false; - auto escape = 0; - auto percent = false; - - for (auto c: execString) { - if (escape == 0 && c == u'\\') { - escape = 1; - } else if (parsingString) { - if (c == '\\') { - escape++; - if (escape == 4) { - currentArgument += '\\'; - 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. - qCWarning(logDesktopEntry).noquote() - << "Illegal escape sequence in desktop entry exec string:" << execString; - } - - currentArgument += c; - escape = 0; - } else if (c == u'"' || c == u'\'') { - parsingString = false; - } else { - currentArgument += c; - } - } else if (escape != 0) { - currentArgument += c; - escape = 0; - } else if (percent) { - if (c == '%') { - currentArgument += '%'; - } // else discard - - percent = false; - } else if (c == '%') { - percent = true; - } else if (c == u'"' || c == u'\'') { - parsingString = true; - } else if (c == u' ') { - if (!currentArgument.isEmpty()) { - arguments.push_back(currentArgument); - currentArgument.clear(); - } - } else { - currentArgument += c; - } - } - - if (!currentArgument.isEmpty()) { - arguments.push_back(currentArgument); - currentArgument.clear(); - } - - return arguments; -} - -void DesktopEntry::doExec(const QString& execString, const QString& workingDirectory) { - auto args = DesktopEntry::parseExecString(execString); - if (args.isEmpty()) { - qCWarning(logDesktopEntry) << "Tried to exec string" << execString << "which parsed as empty."; - return; - } - - auto process = QProcess(); - process.setProgram(args.at(0)); - process.setArguments(args.sliced(1)); - if (!workingDirectory.isEmpty()) process.setWorkingDirectory(workingDirectory); - process.startDetached(); -} - -void DesktopAction::execute() const { - DesktopEntry::doExec(this->mExecString, this->entry->mWorkingDirectory); -} - -DesktopEntryManager::DesktopEntryManager() { - this->scanDesktopEntries(); - this->populateApplications(); -} - -void DesktopEntryManager::scanDesktopEntries() { - QList dataPaths; - - 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); - } -} - -void DesktopEntryManager::populateApplications() { - for (auto& entry: this->desktopEntries.values()) { - if (!entry->noDisplay()) this->mApplications.insertObject(entry); - } -} - -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()) { - auto path = entry.filePath(); - if (!path.endsWith(".desktop")) { - qCDebug(logDesktopEntry) << "Skipping file" << path << "as it has no .desktop extension"; - continue; - } - - auto file = QFile(path); - if (!file.open(QFile::ReadOnly)) { - qCDebug(logDesktopEntry) << "Could not open file" << path; - continue; - } - - auto id = prefix + entry.fileName().sliced(0, entry.fileName().length() - 8); - auto lowerId = id.toLower(); - - 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); - } - } -} - -DesktopEntryManager* DesktopEntryManager::instance() { - static auto* instance = new DesktopEntryManager(); // NOLINT - return instance; -} - -DesktopEntry* DesktopEntryManager::byId(const QString& id) { - if (auto* entry = this->desktopEntries.value(id)) { - return entry; - } else if (auto* entry = this->lowercaseDesktopEntries.value(id.toLower())) { - return entry; - } else { - return nullptr; - } -} - -ObjectModel* DesktopEntryManager::applications() { return &this->mApplications; } - -DesktopEntries::DesktopEntries() { DesktopEntryManager::instance(); } - -DesktopEntry* DesktopEntries::byId(const QString& id) { - return DesktopEntryManager::instance()->byId(id); -} - -ObjectModel* DesktopEntries::applications() { - return DesktopEntryManager::instance()->applications(); -} diff --git a/src/core/desktopentry.hpp b/src/core/desktopentry.hpp deleted file mode 100644 index 3871181b..00000000 --- a/src/core/desktopentry.hpp +++ /dev/null @@ -1,155 +0,0 @@ -#pragma once - -#include - -#include -#include -#include -#include -#include -#include - -#include "doc.hpp" -#include "model.hpp" - -class DesktopAction; - -/// 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); - /// Short description of the application, such as "Web Browser". May be empty. - Q_PROPERTY(QString genericName MEMBER mGenericName CONSTANT); - /// If true, this application should not be displayed in menus and launchers. - Q_PROPERTY(bool noDisplay MEMBER mNoDisplay CONSTANT); - /// Long description of the application, such as "View websites on the internet". May be empty. - Q_PROPERTY(QString comment MEMBER mComment CONSTANT); - /// Name of the icon associated with this application. May be empty. - Q_PROPERTY(QString icon MEMBER mIcon CONSTANT); - /// The raw `Exec` string from the desktop entry. You probably want @@execute(). - Q_PROPERTY(QString execString MEMBER mExecString CONSTANT); - /// The working directory to execute from. - Q_PROPERTY(QString workingDirectory MEMBER mWorkingDirectory CONSTANT); - /// 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(QVector actions READ actions CONSTANT); - QML_ELEMENT; - QML_UNCREATABLE("DesktopEntry instances must be retrieved from DesktopEntries"); - -public: - explicit DesktopEntry(QString id, QObject* parent): QObject(parent), mId(std::move(id)) {} - - void parseEntry(const QString& text); - - /// Run the application. Currently ignores @@runInTerminal and field codes. - Q_INVOKABLE void execute() const; - - [[nodiscard]] bool isValid() const; - [[nodiscard]] bool noDisplay() const; - [[nodiscard]] QVector actions() const; - - // currently ignores all field codes. - static QVector parseExecString(const QString& execString); - static void doExec(const QString& execString, const QString& workingDirectory); - -public: - QString mId; - QString mName; - QString mGenericName; - bool mNoDisplay = false; - QString mComment; - QString mIcon; - QString mExecString; - QString mWorkingDirectory; - bool mTerminal = false; - QVector mCategories; - QVector mKeywords; - -private: - QHash mEntries; - QHash mActions; - - friend class DesktopAction; -}; - -/// An action of a @@DesktopEntry$. -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); - /// The raw `Exec` string from the desktop entry. You probably want @@execute(). - Q_PROPERTY(QString execString MEMBER mExecString CONSTANT); - QML_ELEMENT; - QML_UNCREATABLE("DesktopAction instances must be retrieved from a DesktopEntry"); - -public: - explicit DesktopAction(QString id, DesktopEntry* entry) - : QObject(entry) - , entry(entry) - , mId(std::move(id)) {} - - /// Run the application. Currently ignores @@DesktopEntry.runInTerminal and field codes. - Q_INVOKABLE void execute() const; - -private: - DesktopEntry* entry; - QString mId; - QString mName; - QString mIcon; - QString mExecString; - QHash mEntries; - - friend class DesktopEntry; -}; - -class DesktopEntryManager: public QObject { - Q_OBJECT; - -public: - void scanDesktopEntries(); - - [[nodiscard]] DesktopEntry* byId(const QString& id); - - [[nodiscard]] ObjectModel* applications(); - - static DesktopEntryManager* instance(); - -private: - explicit DesktopEntryManager(); - - void populateApplications(); - void scanPath(const QDir& dir, const QString& prefix = QString()); - - QHash desktopEntries; - QHash lowercaseDesktopEntries; - ObjectModel mApplications {this}; -}; - -///! Desktop entry index. -/// Index of desktop entries according to the [desktop entry specification]. -/// -/// Primarily useful for looking up icons and metadata from an id, as there is -/// currently no mechanism for usage based sorting of entries and other launcher niceties. -/// -/// [desktop entry specification]: https://specifications.freedesktop.org/desktop-entry-spec/latest/ -class DesktopEntries: public QObject { - Q_OBJECT; - /// All desktop entries of type Application that are not Hidden or NoDisplay. - QSDOC_TYPE_OVERRIDE(ObjectModel*); - Q_PROPERTY(UntypedObjectModel* applications READ applications CONSTANT); - QML_ELEMENT; - QML_SINGLETON; - -public: - explicit DesktopEntries(); - - /// Look up a desktop entry by name. Includes NoDisplay entries. May return null. - Q_INVOKABLE [[nodiscard]] static DesktopEntry* byId(const QString& id); - - [[nodiscard]] static ObjectModel* applications(); -}; diff --git a/src/core/doc.hpp b/src/core/doc.hpp index fbb21400..b619b0a6 100644 --- a/src/core/doc.hpp +++ b/src/core/doc.hpp @@ -10,14 +10,5 @@ #define QSDOC_ELEMENT #define QSDOC_NAMED_ELEMENT(name) -// unmark uncreatable (will be overlayed by other types) -#define QSDOC_CREATABLE - -// change the cname used for this type -#define QSDOC_CNAME(name) - // overridden properties #define QSDOC_PROPERTY_OVERRIDE(...) - -// override types of properties for docs -#define QSDOC_TYPE_OVERRIDE(type) diff --git a/src/core/elapsedtimer.cpp b/src/core/elapsedtimer.cpp deleted file mode 100644 index 91321122..00000000 --- a/src/core/elapsedtimer.cpp +++ /dev/null @@ -1,22 +0,0 @@ -#include "elapsedtimer.hpp" - -#include - -ElapsedTimer::ElapsedTimer() { this->timer.start(); } - -qreal ElapsedTimer::elapsed() { return static_cast(this->elapsedNs()) / 1000000000.0; } - -qreal ElapsedTimer::restart() { return static_cast(this->restartNs()) / 1000000000.0; } - -qint64 ElapsedTimer::elapsedMs() { return this->timer.elapsed(); } - -qint64 ElapsedTimer::restartMs() { return this->timer.restart(); } - -qint64 ElapsedTimer::elapsedNs() { return this->timer.nsecsElapsed(); } - -qint64 ElapsedTimer::restartNs() { - // see qelapsedtimer.cpp - auto old = this->timer; - this->timer.start(); - return old.durationTo(this->timer).count(); -} diff --git a/src/core/elapsedtimer.hpp b/src/core/elapsedtimer.hpp deleted file mode 100644 index 85850963..00000000 --- a/src/core/elapsedtimer.hpp +++ /dev/null @@ -1,45 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include - -///! Measures time between events -/// The ElapsedTimer measures time since its last restart, and is useful -/// for determining the time between events that don't supply it. -class ElapsedTimer: public QObject { - Q_OBJECT; - QML_ELEMENT; - -public: - explicit ElapsedTimer(); - - /// Return the number of seconds since the timer was last - /// started or restarted, with nanosecond precision. - Q_INVOKABLE qreal elapsed(); - - /// Restart the timer, returning the number of seconds since - /// the timer was last started or restarted, with nanosecond precision. - Q_INVOKABLE qreal restart(); - - /// Return the number of milliseconds since the timer was last - /// started or restarted. - Q_INVOKABLE qint64 elapsedMs(); - - /// Restart the timer, returning the number of milliseconds since - /// the timer was last started or restarted. - Q_INVOKABLE qint64 restartMs(); - - /// Return the number of nanoseconds since the timer was last - /// started or restarted. - Q_INVOKABLE qint64 elapsedNs(); - - /// Restart the timer, returning the number of nanoseconds since - /// the timer was last started or restarted. - Q_INVOKABLE qint64 restartNs(); - -private: - QElapsedTimer timer; -}; diff --git a/src/window/floatingwindow.cpp b/src/core/floatingwindow.cpp similarity index 87% rename from src/window/floatingwindow.cpp rename to src/core/floatingwindow.cpp index 761bc2d4..a1d23f54 100644 --- a/src/window/floatingwindow.cpp +++ b/src/core/floatingwindow.cpp @@ -32,12 +32,10 @@ FloatingWindowInterface::FloatingWindowInterface(QObject* parent) 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::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); // clang-format on } @@ -51,13 +49,10 @@ void FloatingWindowInterface::onReload(QObject* oldInstance) { 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(); } -qreal FloatingWindowInterface::devicePixelRatio() const { return this->window->devicePixelRatio(); } - // NOLINTBEGIN #define proxyPair(type, get, set) \ type FloatingWindowInterface::get() const { return this->window->get(); } \ @@ -69,7 +64,6 @@ proxyPair(qint32, height, setHeight); proxyPair(QuickshellScreenInfo*, screen, setScreen); proxyPair(QColor, color, setColor); proxyPair(PendingRegion*, mask, setMask); -proxyPair(QsSurfaceFormat, surfaceFormat, setSurfaceFormat); #undef proxyPair // NOLINTEND diff --git a/src/window/floatingwindow.hpp b/src/core/floatingwindow.hpp similarity index 84% rename from src/window/floatingwindow.hpp rename to src/core/floatingwindow.hpp index 7dd0d4ed..93b5723d 100644 --- a/src/window/floatingwindow.hpp +++ b/src/core/floatingwindow.hpp @@ -4,7 +4,6 @@ #include #include "proxywindow.hpp" -#include "windowinterface.hpp" class ProxyFloatingWindow: public ProxyWindowBase { Q_OBJECT; @@ -18,7 +17,7 @@ public: void setHeight(qint32 height) override; }; -///! Standard toplevel operating system window that looks like any other application. +///! Standard floating window. class FloatingWindowInterface: public WindowInterface { Q_OBJECT; QML_NAMED_ELEMENT(FloatingWindow); @@ -42,8 +41,6 @@ public: [[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; @@ -53,9 +50,6 @@ public: [[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 diff --git a/src/core/generation.cpp b/src/core/generation.cpp index ef4449b3..77e4a9cb 100644 --- a/src/core/generation.cpp +++ b/src/core/generation.cpp @@ -4,8 +4,6 @@ #include #include #include -#include -#include #include #include #include @@ -14,6 +12,7 @@ #include #include #include +#include #include #include "iconimageprovider.hpp" @@ -24,12 +23,10 @@ #include "reload.hpp" #include "scan.hpp" -static QHash g_generations; // NOLINT +static QHash g_generations; // NOLINT -EngineGeneration::EngineGeneration(const QDir& rootPath, QmlScanner scanner) - : rootPath(rootPath) - , scanner(std::move(scanner)) - , urlInterceptor(this->rootPath) +EngineGeneration::EngineGeneration(QmlScanner scanner) + : scanner(std::move(scanner)) , interceptNetFactory(this->scanner.qmldirIntercepts) , engine(new QQmlEngine()) { g_generations.insert(this->engine, this); @@ -42,93 +39,56 @@ EngineGeneration::EngineGeneration(const QDir& rootPath, QmlScanner scanner) this->engine->addImageProvider("qsimage", new QsImageProvider()); this->engine->addImageProvider("qspixmap", new QsPixmapProvider()); - QsEnginePlugin::runConstructGeneration(*this); + QuickshellPlugin::runConstructGeneration(*this); } EngineGeneration::~EngineGeneration() { - if (this->engine != nullptr) { - qFatal() << this << "destroyed without calling destroy()"; - } + g_generations.remove(this->engine); + delete this->engine; } void EngineGeneration::destroy() { - if (this->destroying) return; - this->destroying = true; + // Multiple generations can detect a reload at the same time. + delete this->watcher; + this->watcher = nullptr; - if (this->watcher != nullptr) { - // Multiple generations can detect a reload at the same time. - QObject::disconnect(this->watcher, nullptr, this, nullptr); - this->watcher->deleteLater(); - this->watcher = nullptr; - } - - for (auto* extension: this->extensions.values()) { - delete extension; - } - - if (this->root != nullptr) { + // Yes all of this is actually necessary. + if (this->engine != nullptr && this->root != nullptr) { QObject::connect(this->root, &QObject::destroyed, this, [this]() { - // prevent further js execution between garbage collection and engine destruction. - this->engine->setInterrupted(true); + // The timer seems to fix *one* of the possible qml item destructor crashes. + QTimer::singleShot(0, [this]() { + // Garbage is not collected during engine destruction. + this->engine->collectGarbage(); - g_generations.remove(this->engine); + QObject::connect(this->engine, &QObject::destroyed, this, [this]() { delete this; }); - // Garbage is not collected during engine destruction. - this->engine->collectGarbage(); - - delete this->engine; - this->engine = nullptr; - - auto terminate = this->shouldTerminate; - auto code = this->exitCode; - delete this; - - if (terminate) QCoreApplication::exit(code); + // Even after all of that there's still multiple failing assertions and segfaults. + // Pray you don't hit one. + // Note: it appeats *some* of the crashes are related to values owned by the generation. + // Test by commenting the connect() above. + this->engine->deleteLater(); + this->engine = nullptr; + }); }); this->root->deleteLater(); this->root = nullptr; - } else { - g_generations.remove(this->engine); - - // the engine has never been used, no need to clean up - delete this->engine; - this->engine = nullptr; - - auto terminate = this->shouldTerminate; - auto code = this->exitCode; - delete this; - - if (terminate) QCoreApplication::exit(code); } } -void EngineGeneration::shutdown() { - if (this->destroying) return; - - delete this->root; - this->root = nullptr; - delete this->engine; - this->engine = nullptr; - delete this; -} - void EngineGeneration::onReload(EngineGeneration* old) { if (old != nullptr) { // if the old generation holds the window incubation controller as the // new generation acquires it then incubators will hang intermittently - qCDebug(logIncubator) << "Locking incubation controllers of old generation" << old; - old->incubationControllersLocked = true; + old->incubationControllers.clear(); old->assignIncubationController(); } - QObject::connect(this->engine, &QQmlEngine::quit, this, &EngineGeneration::quit); - QObject::connect(this->engine, &QQmlEngine::exit, this, &EngineGeneration::exit); - - if (auto* reloadable = qobject_cast(this->root)) { - reloadable->reload(old ? old->root : nullptr); - } + auto* app = QCoreApplication::instance(); + QObject::connect(this->engine, &QQmlEngine::quit, app, &QCoreApplication::quit); + QObject::connect(this->engine, &QQmlEngine::exit, app, &QCoreApplication::exit); + this->root->reload(old == nullptr ? nullptr : old->root); this->singletonRegistry.onReload(old == nullptr ? nullptr : &old->singletonRegistry); this->reloadComplete = true; emit this->reloadFinished(); @@ -145,7 +105,7 @@ void EngineGeneration::postReload() { // This can be called on a generation during its destruction. if (this->engine == nullptr || this->root == nullptr) return; - QsEnginePlugin::runOnReload(); + QuickshellPlugin::runOnReload(); PostReloadHook::postReloadTree(this->root); this->singletonRegistry.onPostReload(); } @@ -157,21 +117,13 @@ void EngineGeneration::setWatchingFiles(bool watching) { for (auto& file: this->scanner.scannedFiles) { this->watcher->addPath(file); - this->watcher->addPath(QFileInfo(file).dir().absolutePath()); } QObject::connect( this->watcher, &QFileSystemWatcher::fileChanged, this, - &EngineGeneration::onFileChanged - ); - - QObject::connect( - this->watcher, - &QFileSystemWatcher::directoryChanged, - this, - &EngineGeneration::onDirectoryChanged + &EngineGeneration::filesChanged ); } } else { @@ -182,44 +134,28 @@ void EngineGeneration::setWatchingFiles(bool watching) { } } -void EngineGeneration::onFileChanged(const QString& name) { - if (!this->watcher->files().contains(name)) { - this->deletedWatchedFiles.push_back(name); - } else { - emit this->filesChanged(); - } -} - -void EngineGeneration::onDirectoryChanged() { - // try to find any files that were just deleted from a replace operation - for (auto& file: this->deletedWatchedFiles) { - if (QFileInfo(file).exists()) { - emit this->filesChanged(); - break; - } - } -} - void EngineGeneration::registerIncubationController(QQmlIncubationController* controller) { + auto* obj = dynamic_cast(controller); + // We only want controllers that we can swap out if destroyed. // This happens if the window owning the active controller dies. - if (auto* obj = dynamic_cast(controller)) { - QObject::connect( - obj, - &QObject::destroyed, - this, - &EngineGeneration::incubationControllerDestroyed - ); - } else { - qCWarning(logIncubator) << "Could not register incubation controller as it is not a QObject" - << controller; + if (obj == nullptr) { + qCDebug(logIncubator) << "Could not register incubation controller as it is not a QObject" + << controller; return; } - this->incubationControllers.push_back(controller); - qCDebug(logIncubator) << "Registered incubation controller" << controller << "to generation" - << this; + this->incubationControllers.push_back({controller, obj}); + + QObject::connect( + obj, + &QObject::destroyed, + this, + &EngineGeneration::incubationControllerDestroyed + ); + + qCDebug(logIncubator) << "Registered incubation controller" << controller; // This function can run during destruction. if (this->engine == nullptr) return; @@ -230,20 +166,22 @@ void EngineGeneration::registerIncubationController(QQmlIncubationController* co } void EngineGeneration::deregisterIncubationController(QQmlIncubationController* controller) { - if (auto* obj = dynamic_cast(controller)) { - QObject::disconnect(obj, nullptr, this, nullptr); - } else { - qCCritical(logIncubator) << "Deregistering incubation controller which is not a QObject, " - "however only QObject controllers should be registered."; - } + QObject* obj = nullptr; + this->incubationControllers.removeIf([&](QPair other) { + if (controller == other.first) { + obj = other.second; + return true; + } else return false; + }); - if (!this->incubationControllers.removeOne(controller)) { - qCCritical(logIncubator) << "Failed to deregister incubation controller" << controller << "from" - << this << "as it was not registered to begin with"; - qCCritical(logIncubator) << "Current registered incuabation controllers" - << this->incubationControllers; + if (obj == nullptr) { + qCWarning(logIncubator) << "Failed to deregister incubation controller" << controller + << "as it was not registered to begin with"; + qCWarning(logIncubator) << "Current registered incuabation controllers" + << this->incubationControllers; } else { - qCDebug(logIncubator) << "Deregistered incubation controller" << controller << "from" << this; + QObject::disconnect(obj, nullptr, this, nullptr); + qCDebug(logIncubator) << "Deregistered incubation controller" << controller; } // This function can run during destruction. @@ -258,25 +196,22 @@ void EngineGeneration::deregisterIncubationController(QQmlIncubationController* void EngineGeneration::incubationControllerDestroyed() { auto* sender = this->sender(); - auto* controller = dynamic_cast(sender); + QQmlIncubationController* controller = nullptr; + + this->incubationControllers.removeIf([&](QPair other) { + if (sender == other.second) { + controller = other.first; + return true; + } else return false; + }); if (controller == nullptr) { - qCCritical(logIncubator) << "Destroyed incubation controller" << sender << "is not known to" - << this << ", this may cause memory corruption"; + qCCritical(logIncubator) << "Destroyed incubation controller" << this->sender() + << "could not be identified, this may cause memory corruption"; qCCritical(logIncubator) << "Current registered incuabation controllers" << this->incubationControllers; - - return; - } - - if (this->incubationControllers.removeOne(controller)) { - qCDebug(logIncubator) << "Destroyed incubation controller" << controller << "deregistered from" - << this; } else { - qCCritical(logIncubator) << "Destroyed incubation controller" << controller - << "was not registered, but its destruction was observed by" << this; - - return; + qCDebug(logIncubator) << "Destroyed incubation controller" << controller << "deregistered"; } // This function can run during destruction. @@ -289,64 +224,23 @@ void EngineGeneration::incubationControllerDestroyed() { } } -void EngineGeneration::registerExtension(const void* key, EngineGenerationExt* extension) { - if (this->extensions.contains(key)) { - delete this->extensions.value(key); - } - - this->extensions.insert(key, extension); -} - -EngineGenerationExt* EngineGeneration::findExtension(const void* key) { - return this->extensions.value(key); -} - -void EngineGeneration::quit() { - this->shouldTerminate = true; - this->destroy(); -} - -void EngineGeneration::exit(int code) { - this->shouldTerminate = true; - this->exitCode = code; - this->destroy(); -} - void EngineGeneration::assignIncubationController() { QQmlIncubationController* controller = nullptr; + if (this->incubationControllers.isEmpty()) controller = &this->delayedIncubationController; + else controller = this->incubationControllers.first().first; - if (this->incubationControllersLocked || this->incubationControllers.isEmpty()) { - controller = &this->delayedIncubationController; - } else { - controller = this->incubationControllers.first(); - } - - qCDebug(logIncubator) << "Assigning incubation controller" << controller << "to generation" - << this + qCDebug(logIncubator) << "Assigning incubation controller to engine:" << controller << "fallback:" << (controller == &this->delayedIncubationController); this->engine->setIncubationController(controller); } -EngineGeneration* EngineGeneration::currentGeneration() { - if (g_generations.size() == 1) { - return *g_generations.begin(); - } else return nullptr; -} - -EngineGeneration* EngineGeneration::findEngineGeneration(const QQmlEngine* engine) { - return g_generations.value(engine); -} - -EngineGeneration* EngineGeneration::findObjectGeneration(const QObject* object) { - // Objects can still attempt to find their generation after it has been destroyed. - // if (g_generations.size() == 1) return EngineGeneration::currentGeneration(); - +EngineGeneration* EngineGeneration::findObjectGeneration(QObject* object) { while (object != nullptr) { auto* context = QQmlEngine::contextForObject(object); if (context != nullptr) { - if (auto* generation = EngineGeneration::findEngineGeneration(context->engine())) { + if (auto* generation = g_generations.value(context->engine())) { return generation; } } diff --git a/src/core/generation.hpp b/src/core/generation.hpp index 632bd8a5..11ebf0be 100644 --- a/src/core/generation.hpp +++ b/src/core/generation.hpp @@ -1,34 +1,25 @@ #pragma once #include -#include #include -#include #include -#include +#include #include #include #include "incubator.hpp" #include "qsintercept.hpp" #include "scan.hpp" +#include "shell.hpp" #include "singleton.hpp" class RootWrapper; -class QuickshellGlobal; - -class EngineGenerationExt { -public: - EngineGenerationExt() = default; - virtual ~EngineGenerationExt() = default; - Q_DISABLE_COPY_MOVE(EngineGenerationExt); -}; class EngineGeneration: public QObject { Q_OBJECT; public: - explicit EngineGeneration(const QDir& rootPath, QmlScanner scanner); + explicit EngineGeneration(QmlScanner scanner); ~EngineGeneration() override; Q_DISABLE_COPY_MOVE(EngineGeneration); @@ -39,55 +30,30 @@ public: void registerIncubationController(QQmlIncubationController* controller); void deregisterIncubationController(QQmlIncubationController* controller); - // takes ownership - void registerExtension(const void* key, EngineGenerationExt* extension); - EngineGenerationExt* findExtension(const void* key); - - static EngineGeneration* findEngineGeneration(const QQmlEngine* engine); - static EngineGeneration* findObjectGeneration(const QObject* object); - - // Returns the current generation if there is only one generation, - // otherwise null. - static EngineGeneration* currentGeneration(); + static EngineGeneration* findObjectGeneration(QObject* object); RootWrapper* wrapper = nullptr; - QDir rootPath; QmlScanner scanner; QsUrlInterceptor urlInterceptor; QsInterceptNetworkAccessManagerFactory interceptNetFactory; QQmlEngine* engine = nullptr; - QObject* root = nullptr; + ShellRoot* root = nullptr; SingletonRegistry singletonRegistry; QFileSystemWatcher* watcher = nullptr; - QVector deletedWatchedFiles; DelayedQmlIncubationController delayedIncubationController; bool reloadComplete = false; - QuickshellGlobal* qsgInstance = nullptr; void destroy(); - void shutdown(); signals: void filesChanged(); void reloadFinished(); -public slots: - void quit(); - void exit(int code); - private slots: - void onFileChanged(const QString& name); - void onDirectoryChanged(); void incubationControllerDestroyed(); private: void postReload(); void assignIncubationController(); - QVector incubationControllers; - bool incubationControllersLocked = false; - QHash extensions; - - bool destroying = false; - bool shouldTerminate = false; - int exitCode = 0; + QVector> incubationControllers; }; diff --git a/src/core/iconimageprovider.cpp b/src/core/iconimageprovider.cpp index 43e00fd8..f4710fb8 100644 --- a/src/core/iconimageprovider.cpp +++ b/src/core/iconimageprovider.cpp @@ -1,5 +1,4 @@ #include "iconimageprovider.hpp" -#include #include #include @@ -12,9 +11,7 @@ QPixmap IconImageProvider::requestPixmap(const QString& id, QSize* size, const QSize& requestedSize) { QString iconName; - QString fallbackName; QString path; - auto splitIdx = id.indexOf("?path="); if (splitIdx != -1) { iconName = id.sliced(0, splitIdx); @@ -22,17 +19,10 @@ IconImageProvider::requestPixmap(const QString& id, QSize* size, const QSize& re qWarning() << "Searching custom icon paths is not yet supported. Icon path will be ignored for" << id; } else { - splitIdx = id.indexOf("?fallback="); - if (splitIdx != -1) { - iconName = id.sliced(0, splitIdx); - fallbackName = id.sliced(splitIdx + 10); - } else { - iconName = id; - } + iconName = id; } auto icon = QIcon::fromTheme(iconName); - if (icon.isNull()) icon = QIcon::fromTheme(fallbackName); auto targetSize = requestedSize.isValid() ? requestedSize : QSize(100, 100); if (targetSize.width() == 0 || targetSize.height() == 0) targetSize = QSize(2, 2); @@ -50,8 +40,8 @@ IconImageProvider::requestPixmap(const QString& id, QSize* size, const QSize& re QPixmap IconImageProvider::missingPixmap(const QSize& size) { auto width = size.width() % 2 == 0 ? size.width() : size.width() + 1; auto height = size.height() % 2 == 0 ? size.height() : size.height() + 1; - width = std::max(width, 2); - height = std::max(height, 2); + if (width < 2) width = 2; + if (height < 2) height = 2; auto pixmap = QPixmap(width, height); pixmap.fill(QColorConstants::Black); @@ -65,20 +55,12 @@ QPixmap IconImageProvider::missingPixmap(const QSize& size) { return pixmap; } -QString IconImageProvider::requestString( - const QString& icon, - const QString& path, - const QString& fallback -) { +QString IconImageProvider::requestString(const QString& icon, const QString& path) { auto req = "image://icon/" + icon; if (!path.isEmpty()) { req += "?path=" + path; } - if (!fallback.isEmpty()) { - req += "?fallback=" + fallback; - } - return req; } diff --git a/src/core/iconimageprovider.hpp b/src/core/iconimageprovider.hpp index 57e26049..167d93bd 100644 --- a/src/core/iconimageprovider.hpp +++ b/src/core/iconimageprovider.hpp @@ -10,10 +10,5 @@ public: QPixmap requestPixmap(const QString& id, QSize* size, const QSize& requestedSize) override; static QPixmap missingPixmap(const QSize& size); - - static QString requestString( - const QString& icon, - const QString& path = QString(), - const QString& fallback = QString() - ); + static QString requestString(const QString& icon, const QString& path); }; diff --git a/src/core/iconprovider.cpp b/src/core/iconprovider.cpp deleted file mode 100644 index 99b423ed..00000000 --- a/src/core/iconprovider.cpp +++ /dev/null @@ -1,105 +0,0 @@ -#include "iconprovider.hpp" -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "generation.hpp" - -// QMenu re-calls pixmap() every time the mouse moves so its important to cache it. -class PixmapCacheIconEngine: public QIconEngine { - void paint( - QPainter* /*unused*/, - const QRect& /*unused*/, - QIcon::Mode /*unused*/, - QIcon::State /*unused*/ - ) override { - 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 { - if (this->lastPixmap.isNull() || size != this->lastSize) { - this->lastPixmap = this->createPixmap(size); - this->lastSize = size; - } - - return this->lastPixmap; - } - - virtual QPixmap createPixmap(const QSize& size) = 0; - -private: - QSize lastSize; - QPixmap lastPixmap; -}; - -class ImageProviderIconEngine: public PixmapCacheIconEngine { -public: - explicit ImageProviderIconEngine(QQuickImageProvider* provider, QString id) - : provider(provider) - , id(std::move(id)) {} - - QPixmap createPixmap(const QSize& size) override { - if (this->provider->imageType() == QQmlImageProviderBase::Pixmap) { - return this->provider->requestPixmap(this->id, nullptr, size); - } else if (this->provider->imageType() == QQmlImageProviderBase::Image) { - auto image = this->provider->requestImage(this->id, nullptr, size); - return QPixmap::fromImage(image); - } else { - qFatal() << "Unexpected ImageProviderIconEngine image type" << this->provider->imageType(); - return QPixmap(); // never reached, satisfies lint - } - } - - [[nodiscard]] QIconEngine* clone() const override { - return new ImageProviderIconEngine(this->provider, this->id); - } - -private: - QQuickImageProvider* provider; - QString id; -}; - -QIcon getEngineImageAsIcon(QQmlEngine* engine, const QUrl& url) { - if (!engine || url.isEmpty()) return QIcon(); - - auto scheme = url.scheme(); - if (scheme == "image") { - auto providerName = url.authority(); - auto path = url.path(); - if (!path.isEmpty()) path = path.sliced(1); - - auto* provider = qobject_cast(engine->imageProvider(providerName)); - - if (provider == nullptr) { - qWarning() << "iconByUrl failed: no provider found for" << url; - return QIcon(); - } - - if (provider->imageType() == QQmlImageProviderBase::Pixmap - || provider->imageType() == QQmlImageProviderBase::Image) - { - return QIcon(new ImageProviderIconEngine(provider, path)); - } - - } else { - qWarning() << "iconByUrl failed: unsupported scheme" << scheme << "in path" << url; - } - - return QIcon(); -} - -QIcon getCurrentEngineImageAsIcon(const QUrl& url) { - auto* generation = EngineGeneration::currentGeneration(); - if (!generation) return QIcon(); - return getEngineImageAsIcon(generation->engine, url); -} diff --git a/src/core/iconprovider.hpp b/src/core/iconprovider.hpp deleted file mode 100644 index 173d20e6..00000000 --- a/src/core/iconprovider.hpp +++ /dev/null @@ -1,8 +0,0 @@ -#pragma once - -#include -#include -#include - -QIcon getEngineImageAsIcon(QQmlEngine* engine, const QUrl& url); -QIcon getCurrentEngineImageAsIcon(const QUrl& url); diff --git a/src/core/imageprovider.cpp b/src/core/imageprovider.cpp index 47f284c7..cc81c47f 100644 --- a/src/core/imageprovider.cpp +++ b/src/core/imageprovider.cpp @@ -1,6 +1,5 @@ #include "imageprovider.hpp" -#include #include #include #include @@ -8,30 +7,17 @@ #include #include #include -#include -namespace { +static QMap liveImages; // NOLINT -namespace { -QMap liveImages; // NOLINT -quint32 handleIndex = 0; // NOLINT -} // namespace - -void parseReq(const QString& req, QString& target, QString& param) { - auto splitIdx = req.indexOf('/'); - if (splitIdx != -1) { - target = req.sliced(0, splitIdx); - param = req.sliced(splitIdx + 1); - } else { - target = req; +QsImageHandle::QsImageHandle(QQmlImageProviderBase::ImageType type, QObject* parent) + : QObject(parent) + , type(type) { + { + auto dbg = QDebug(&this->id); + dbg.nospace() << static_cast(this); } -} -} // namespace - -QsImageHandle::QsImageHandle(QQmlImageProviderBase::ImageType type) - : type(type) - , id(QString::number(++handleIndex)) { liveImages.insert(this->id, this); } @@ -57,6 +43,16 @@ QPixmap QsImageHandle:: return QPixmap(); } +void parseReq(const QString& req, QString& target, QString& param) { + auto splitIdx = req.indexOf('/'); + if (splitIdx != -1) { + target = req.sliced(0, splitIdx); + param = req.sliced(splitIdx + 1); + } else { + target = req; + } +} + QImage QsImageProvider::requestImage(const QString& id, QSize* size, const QSize& requestedSize) { QString target; QString param; @@ -85,9 +81,3 @@ QsPixmapProvider::requestPixmap(const QString& id, QSize* size, const QSize& req return QPixmap(); } } - -QString QsIndexedImageHandle::url() const { - return this->QsImageHandle::url() % '/' % QString::number(this->changeIndex); -} - -void QsIndexedImageHandle::imageChanged() { ++this->changeIndex; } diff --git a/src/core/imageprovider.hpp b/src/core/imageprovider.hpp index 8568d4f7..5ea7843d 100644 --- a/src/core/imageprovider.hpp +++ b/src/core/imageprovider.hpp @@ -20,13 +20,15 @@ public: QPixmap requestPixmap(const QString& id, QSize* size, const QSize& requestedSize) override; }; -class QsImageHandle { +class QsImageHandle: public QObject { + Q_OBJECT; + public: - explicit QsImageHandle(QQmlImageProviderBase::ImageType type); - virtual ~QsImageHandle(); + explicit QsImageHandle(QQmlImageProviderBase::ImageType type, QObject* parent = nullptr); + ~QsImageHandle() override; Q_DISABLE_COPY_MOVE(QsImageHandle); - [[nodiscard]] virtual QString url() const; + [[nodiscard]] QString url() const; virtual QImage requestImage(const QString& id, QSize* size, const QSize& requestedSize); virtual QPixmap requestPixmap(const QString& id, QSize* size, const QSize& requestedSize); @@ -35,14 +37,3 @@ private: QQmlImageProviderBase::ImageType type; QString id; }; - -class QsIndexedImageHandle: public QsImageHandle { -public: - explicit QsIndexedImageHandle(QQmlImageProviderBase::ImageType type): QsImageHandle(type) {} - - [[nodiscard]] QString url() const override; - void imageChanged(); - -private: - quint32 changeIndex = 0; -}; diff --git a/src/core/instanceinfo.cpp b/src/core/instanceinfo.cpp deleted file mode 100644 index 96097c76..00000000 --- a/src/core/instanceinfo.cpp +++ /dev/null @@ -1,35 +0,0 @@ -#include "instanceinfo.hpp" - -#include - -QDataStream& operator<<(QDataStream& stream, const InstanceInfo& info) { - stream << info.instanceId << info.configPath << info.shellId << info.launchTime; - return stream; -} - -QDataStream& operator>>(QDataStream& stream, InstanceInfo& info) { - stream >> info.instanceId >> info.configPath >> info.shellId >> info.launchTime; - return stream; -} - -QDataStream& operator<<(QDataStream& stream, const RelaunchInfo& info) { - stream << info.instance << info.noColor << info.timestamp << info.sparseLogsOnly - << info.defaultLogLevel << info.logRules; - - return stream; -} - -QDataStream& operator>>(QDataStream& stream, RelaunchInfo& info) { - stream >> info.instance >> info.noColor >> info.timestamp >> info.sparseLogsOnly - >> info.defaultLogLevel >> info.logRules; - - return stream; -} - -InstanceInfo InstanceInfo::CURRENT = {}; // NOLINT - -namespace qs::crash { - -CrashInfo CrashInfo::INSTANCE = {}; // NOLINT - -} diff --git a/src/core/instanceinfo.hpp b/src/core/instanceinfo.hpp deleted file mode 100644 index f0fc02a0..00000000 --- a/src/core/instanceinfo.hpp +++ /dev/null @@ -1,39 +0,0 @@ -#pragma once - -#include -#include -#include - -struct InstanceInfo { - QString instanceId; - QString configPath; - QString shellId; - QDateTime launchTime; - - static InstanceInfo CURRENT; // NOLINT -}; - -struct RelaunchInfo { - InstanceInfo instance; - bool noColor = false; - bool timestamp = false; - bool sparseLogsOnly = false; - QtMsgType defaultLogLevel = QtWarningMsg; - QString logRules; -}; - -QDataStream& operator<<(QDataStream& stream, const InstanceInfo& info); -QDataStream& operator>>(QDataStream& stream, InstanceInfo& info); - -QDataStream& operator<<(QDataStream& stream, const RelaunchInfo& info); -QDataStream& operator>>(QDataStream& stream, RelaunchInfo& info); - -namespace qs::crash { - -struct CrashInfo { - int logFd = -1; - - static CrashInfo INSTANCE; // NOLINT -}; - -} // namespace qs::crash diff --git a/src/core/lazyloader.cpp b/src/core/lazyloader.cpp index be0eb78b..76317223 100644 --- a/src/core/lazyloader.cpp +++ b/src/core/lazyloader.cpp @@ -179,9 +179,7 @@ void LazyLoader::incubateIfReady(bool overrideReloadCheck) { void LazyLoader::onIncubationCompleted() { this->setItem(this->incubator->object()); - // The incubator is not necessarily inert at the time of this callback, - // so deleteLater is required. - this->incubator->deleteLater(); + delete this->incubator; this->incubator = nullptr; this->targetLoading = false; emit this->loadingChanged(); diff --git a/src/core/lazyloader.hpp b/src/core/lazyloader.hpp index dbaad4b5..8ef935f6 100644 --- a/src/core/lazyloader.hpp +++ b/src/core/lazyloader.hpp @@ -79,7 +79,7 @@ /// > [!WARNING] Components that internally load other components must explicitly /// > support asynchronous loading to avoid blocking. /// > -/// > Notably, @@Variants does not corrently support asynchronous +/// > Notably, [Variants](../variants) does not corrently support asynchronous /// > loading, meaning using it inside a LazyLoader will block similarly to not /// > having a loader to start with. /// @@ -87,8 +87,8 @@ /// > 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` - /// if neither @@loading nor @@active. + /// The fully loaded item if the loader is `loading` or `active`, or `null` + /// if neither `loading` or `active`. /// /// Note that the item is owned by the LazyLoader, and destroying the LazyLoader /// will destroy the item. @@ -96,7 +96,7 @@ class LazyLoader: public Reloadable { /// > [!WARNING] If you access the `item` of a loader that is currently loading, /// > it will block as if you had set `active` to true immediately beforehand. /// > - /// > You can instead set @@loading and listen to @@activeChanged(s) signal to + /// > You can instead set `loading` and listen to the `activeChanged` signal to /// > ensure loading happens asynchronously. Q_PROPERTY(QObject* item READ item NOTIFY itemChanged); /// If the loader is actively loading. @@ -105,7 +105,7 @@ class LazyLoader: public Reloadable { /// loading it asynchronously. If the component is already loaded, setting /// this property has no effect. /// - /// See also: @@activeAsync. + /// See also: [activeAsync](#prop.activeAsync). Q_PROPERTY(bool loading READ isLoading WRITE setLoading NOTIFY loadingChanged); /// If the component is fully loaded. /// @@ -113,17 +113,17 @@ class LazyLoader: public Reloadable { /// blocking the UI, and setting it to `false` will destroy the component, requiring /// it to be loaded again. /// - /// See also: @@activeAsync. + /// See also: [activeAsync](#prop.activeAsync). Q_PROPERTY(bool active READ isActive WRITE setActive NOTIFY activeChanged); /// If the component is fully loaded. /// /// Setting this property to true will asynchronously load the component similarly to - /// @@loading. Reading it or setting it to false will behanve - /// the same as @@active. + /// [loading](#prop.loading). Reading it or setting it to false will behanve + /// the same as [active](#prop.active). Q_PROPERTY(bool activeAsync READ isActive WRITE setActiveAsync NOTIFY activeChanged); - /// The component to load. Mutually exclusive to @@source. + /// The component to load. Mutually exclusive to `source`. Q_PROPERTY(QQmlComponent* component READ component WRITE setComponent NOTIFY componentChanged); - /// The URI to load the component from. Mutually exclusive to @@component. + /// The URI to load the component from. Mutually exclusive to `component`. Q_PROPERTY(QString source READ source WRITE setSource NOTIFY sourceChanged); Q_CLASSINFO("DefaultProperty", "component"); QML_ELEMENT; diff --git a/src/core/logging.cpp b/src/core/logging.cpp deleted file mode 100644 index 57b63e18..00000000 --- a/src/core/logging.cpp +++ /dev/null @@ -1,937 +0,0 @@ -#include "logging.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 -#include -#include -#include - -#include "instanceinfo.hpp" -#include "logging_p.hpp" -#include "logging_qtprivate.cpp" // NOLINT -#include "paths.hpp" -#include "ringbuf.hpp" - -Q_LOGGING_CATEGORY(logBare, "quickshell.bare"); - -namespace qs::log { -using namespace qt_logging_registry; - -Q_LOGGING_CATEGORY(logLogging, "quickshell.logging", QtWarningMsg); - -bool LogMessage::operator==(const LogMessage& other) const { - // note: not including time - return this->type == other.type && this->category == other.category && this->body == other.body; -} - -size_t qHash(const LogMessage& message) { - return qHash(message.type) ^ qHash(message.category) ^ qHash(message.body); -} - -void LogMessage::formatMessage( - QTextStream& stream, - const LogMessage& msg, - bool color, - bool timestamp, - const QString& prefix -) { - if (!prefix.isEmpty()) { - if (color) stream << "\033[90m"; - stream << '[' << prefix << ']'; - if (timestamp) stream << ' '; - if (color) stream << "\033[0m"; - } - - if (timestamp) { - if (color) stream << "\033[90m"; - stream << msg.time.toString("yyyy-MM-dd hh:mm:ss.zzz"); - } - - if (msg.category == "quickshell.bare") { - if (!prefix.isEmpty()) stream << ' '; - stream << msg.body; - } else { - if (color) { - switch (msg.type) { - case QtDebugMsg: stream << "\033[34m DEBUG"; break; - case QtInfoMsg: stream << "\033[32m INFO"; break; - case QtWarningMsg: stream << "\033[33m WARN"; break; - case QtCriticalMsg: stream << "\033[31m ERROR"; break; - case QtFatalMsg: stream << "\033[31m FATAL"; break; - } - } else { - switch (msg.type) { - case QtDebugMsg: stream << " DEBUG"; break; - case QtInfoMsg: stream << " INFO"; break; - case QtWarningMsg: stream << " WARN"; break; - case QtCriticalMsg: stream << " ERROR"; break; - case QtFatalMsg: stream << " FATAL"; break; - } - } - - const auto isDefault = msg.category == "default"; - - if (color && !isDefault && msg.type != QtFatalMsg) stream << "\033[97m"; - - if (!isDefault) { - stream << ' ' << msg.category; - } - - if (color && msg.type != QtFatalMsg) stream << "\033[0m"; - - stream << ": " << msg.body; - - if (color && msg.type == QtFatalMsg) stream << "\033[0m"; - } -} - -bool CategoryFilter::shouldDisplay(QtMsgType type) const { - switch (type) { - case QtDebugMsg: return this->debug; - case QtInfoMsg: return this->info; - case QtWarningMsg: return this->warn; - case QtCriticalMsg: return this->critical; - default: return true; - } -} - -void CategoryFilter::apply(QLoggingCategory* category) const { - category->setEnabled(QtDebugMsg, this->debug); - category->setEnabled(QtInfoMsg, this->info); - category->setEnabled(QtWarningMsg, this->warn); - category->setEnabled(QtCriticalMsg, this->critical); -} - -void CategoryFilter::applyRule( - QLatin1StringView category, - const qt_logging_registry::QLoggingRule& rule -) { - auto filterpass = rule.pass(category, QtDebugMsg); - if (filterpass != 0) this->debug = filterpass > 0; - - filterpass = rule.pass(category, QtInfoMsg); - if (filterpass != 0) this->info = filterpass > 0; - - filterpass = rule.pass(category, QtWarningMsg); - if (filterpass != 0) this->warn = filterpass > 0; - - filterpass = rule.pass(category, QtCriticalMsg); - if (filterpass != 0) this->critical = filterpass > 0; -} - -LogManager::LogManager(): stdoutStream(stdout) {} - -void LogManager::messageHandler( - QtMsgType type, - const QMessageLogContext& context, - const QString& msg -) { - auto message = LogMessage(type, QLatin1StringView(context.category), msg.toUtf8()); - - auto* self = LogManager::instance(); - - auto display = true; - - const auto* key = static_cast(context.category); - - if (self->sparseFilters.contains(key)) { - display = self->sparseFilters.value(key).shouldDisplay(type); - } - - if (display) { - LogMessage::formatMessage( - self->stdoutStream, - message, - self->colorLogs, - self->timestampLogs, - self->prefix - ); - - self->stdoutStream << Qt::endl; - } - - emit self->logMessage(message, display); -} - -void LogManager::filterCategory(QLoggingCategory* category) { - auto* instance = LogManager::instance(); - - auto categoryName = QLatin1StringView(category->categoryName()); - auto isQs = categoryName.startsWith(QLatin1StringView("quickshell.")); - - if (instance->lastCategoryFilter) { - instance->lastCategoryFilter(category); - } - - auto filter = CategoryFilter(category); - - if (isQs) { - filter.debug = filter.debug || instance->mDefaultLevel == QtDebugMsg; - filter.info = filter.debug || instance->mDefaultLevel == QtInfoMsg; - filter.warn = filter.info || instance->mDefaultLevel == QtWarningMsg; - filter.critical = filter.warn || instance->mDefaultLevel == QtCriticalMsg; - } - - for (const auto& rule: *instance->rules) { - filter.applyRule(categoryName, rule); - } - - if (isQs && !instance->sparse) { - // We assume the category name pointer will always be the same and be comparable in the message handler. - instance->sparseFilters.insert(static_cast(category->categoryName()), filter); - - // all enabled by default - CategoryFilter().apply(category); - } else { - filter.apply(category); - } - - instance->allFilters.insert(categoryName, filter); -} - -LogManager* LogManager::instance() { - static auto* instance = new LogManager(); // NOLINT - return instance; -} - -void LogManager::init( - bool color, - bool timestamp, - bool sparseOnly, - QtMsgType defaultLevel, - const QString& rules, - const QString& prefix -) { - auto* instance = LogManager::instance(); - instance->colorLogs = color; - instance->timestampLogs = timestamp; - instance->sparse = sparseOnly; - instance->prefix = prefix; - instance->mDefaultLevel = defaultLevel; - instance->mRulesString = rules; - - { - QLoggingSettingsParser parser; - parser.setContent(rules); - instance->rules = new QList(parser.rules()); - } - - qInstallMessageHandler(&LogManager::messageHandler); - - instance->lastCategoryFilter = QLoggingCategory::installFilter(&LogManager::filterCategory); - - qCDebug(logLogging) << "Creating offthread logger..."; - auto* thread = new QThread(); - instance->threadProxy.moveToThread(thread); - thread->start(); - - QMetaObject::invokeMethod( - &instance->threadProxy, - &LoggingThreadProxy::initInThread, - Qt::BlockingQueuedConnection - ); - - qCDebug(logLogging) << "Logger initialized."; -} - -void LogManager::initFs() { - QMetaObject::invokeMethod( - &LogManager::instance()->threadProxy, - "initFs", - Qt::BlockingQueuedConnection - ); -} - -QString LogManager::rulesString() const { return this->mRulesString; } -QtMsgType LogManager::defaultLevel() const { return this->mDefaultLevel; } -bool LogManager::isSparse() const { return this->sparse; } - -CategoryFilter LogManager::getFilter(QLatin1StringView category) { - return this->allFilters.value(category); -} - -void LoggingThreadProxy::initInThread() { - this->logging = new ThreadLogging(this); - this->logging->init(); -} - -void LoggingThreadProxy::initFs() { this->logging->initFs(); } - -void ThreadLogging::init() { - auto logMfd = memfd_create("quickshell:logs", 0); - - if (logMfd == -1) { - qCCritical(logLogging) << "Failed to create memfd for initial log storage" - << qt_error_string(-1); - } - - auto dlogMfd = memfd_create("quickshell:detailedlogs", 0); - - if (dlogMfd == -1) { - qCCritical(logLogging) << "Failed to create memfd for initial detailed log storage" - << qt_error_string(-1); - } - - if (logMfd != -1) { - this->file = new QFile(); - this->file->open(logMfd, QFile::ReadWrite, QFile::AutoCloseHandle); - this->fileStream.setDevice(this->file); - } - - if (dlogMfd != -1) { - crash::CrashInfo::INSTANCE.logFd = dlogMfd; - - this->detailedFile = new QFile(); - // buffered by WriteBuffer - 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; - } - } - - // This connection is direct so it works while the event loop is destroyed between - // QCoreApplication delete and Q(Gui)Application launch. - QObject::connect( - LogManager::instance(), - &LogManager::logMessage, - this, - &ThreadLogging::onMessage, - Qt::DirectConnection - ); - - qCDebug(logLogging) << "Created memfd" << logMfd << "for early logs."; - qCDebug(logLogging) << "Created memfd" << dlogMfd << "for early detailed logs."; -} - -void ThreadLogging::initFs() { - qCDebug(logLogging) << "Starting filesystem logging..."; - auto* runDir = QsPaths::instance()->instanceRunDir(); - - if (!runDir) { - qCCritical(logLogging - ) << "Could not start filesystem logging as the runtime directory could not be created."; - return; - } - - auto path = runDir->filePath("log.log"); - auto detailedPath = runDir->filePath("log.qslog"); - auto* file = new QFile(path); - auto* detailedFile = new QFile(detailedPath); - - if (!file->open(QFile::ReadWrite | QFile::Truncate)) { - qCCritical(logLogging - ) << "Could not start filesystem logger as the log file could not be created:" - << path; - delete file; - file = nullptr; - } else { - qInfo() << "Saving logs to" << path; - } - - // buffered by WriteBuffer - if (!detailedFile->open(QFile::ReadWrite | QFile::Truncate | QFile::Unbuffered)) { - 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 { - .l_type = F_WRLCK, - .l_whence = SEEK_SET, - .l_start = 0, - .l_len = 0, - .l_pid = 0, - }; - - if (fcntl(detailedFile->handle(), F_SETLK, &lock) != 0) { // NOLINT - qCWarning(logLogging) << "Unable to set lock marker on detailed log file. --follow from " - "other instances will not work."; - } - - qCInfo(logLogging) << "Saving detailed logs to" << path; - } - - qCDebug(logLogging) << "Copying memfd logs to log file..."; - - if (file) { - auto* oldFile = this->file; - if (oldFile) { - oldFile->seek(0); - sendfile(file->handle(), oldFile->handle(), nullptr, oldFile->size()); - } - - this->file = file; - this->fileStream.setDevice(file); - delete oldFile; - } - - if (detailedFile) { - auto* oldFile = this->detailedFile; - if (oldFile) { - oldFile->seek(0); - sendfile(detailedFile->handle(), oldFile->handle(), nullptr, oldFile->size()); - } - - crash::CrashInfo::INSTANCE.logFd = detailedFile->handle(); - - this->detailedFile = detailedFile; - this->detailedWriter.setDevice(detailedFile); - - if (!oldFile) { - if (!this->detailedWriter.writeHeader()) { - qCCritical(logLogging) << "Could not write header for detailed logs."; - this->detailedWriter.setDevice(nullptr); - delete this->detailedFile; - this->detailedFile = nullptr; - } - } - - delete oldFile; - } - - qCDebug(logLogging) << "Switched logging to disk logs."; - - auto* logManager = LogManager::instance(); - QObject::disconnect(logManager, &LogManager::logMessage, this, &ThreadLogging::onMessage); - - QObject::connect( - logManager, - &LogManager::logMessage, - this, - &ThreadLogging::onMessage, - Qt::QueuedConnection - ); - - qCDebug(logLogging) << "Switched threaded logger to queued eventloop connection."; -} - -void ThreadLogging::onMessage(const LogMessage& msg, bool showInSparse) { - if (showInSparse) { - if (this->fileStream.device() == nullptr) return; - LogMessage::formatMessage(this->fileStream, msg, false, true); - 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."; - } -} - -CompressedLogType compressedTypeOf(QtMsgType type) { - switch (type) { - case QtDebugMsg: return CompressedLogType::Debug; - case QtInfoMsg: return CompressedLogType::Info; - case QtWarningMsg: return CompressedLogType::Warn; - case QtCriticalMsg: - case QtFatalMsg: return CompressedLogType::Critical; - } - - return CompressedLogType::Info; // unreachable under normal conditions -} - -QtMsgType typeOfCompressed(CompressedLogType type) { - switch (type) { - case CompressedLogType::Debug: return QtDebugMsg; - case CompressedLogType::Info: return QtInfoMsg; - case CompressedLogType::Warn: return QtWarningMsg; - case CompressedLogType::Critical: return QtCriticalMsg; - } - - return QtInfoMsg; // unreachable under normal conditions -} - -void WriteBuffer::setDevice(QIODevice* device) { this->device = device; } -bool WriteBuffer::hasDevice() const { return this->device; } - -bool WriteBuffer::flush() { - auto written = this->device->write(this->buffer); - auto success = written == this->buffer.length(); - this->buffer.clear(); - return success; -} - -void WriteBuffer::writeBytes(const char* data, qsizetype length) { - this->buffer.append(data, length); -} - -void WriteBuffer::writeU8(quint8 data) { this->writeBytes(reinterpret_cast(&data), 1); } - -void WriteBuffer::writeU16(quint16 data) { - data = qToLittleEndian(data); - this->writeBytes(reinterpret_cast(&data), 2); -} - -void WriteBuffer::writeU32(quint32 data) { - data = qToLittleEndian(data); - this->writeBytes(reinterpret_cast(&data), 4); -} - -void WriteBuffer::writeU64(quint64 data) { - data = qToLittleEndian(data); - this->writeBytes(reinterpret_cast(&data), 8); -} - -void DeviceReader::setDevice(QIODevice* device) { this->device = device; } -bool DeviceReader::hasDevice() const { return this->device; } - -bool DeviceReader::readBytes(char* data, qsizetype length) { - return this->device->read(data, length) == length; -} - -qsizetype DeviceReader::peekBytes(char* data, qsizetype length) { - return this->device->peek(data, length); -} - -bool DeviceReader::skip(qsizetype length) { return this->device->skip(length) == length; } - -bool DeviceReader::readU8(quint8* data) { - return this->readBytes(reinterpret_cast(data), 1); -} - -bool DeviceReader::readU16(quint16* data) { - return this->readBytes(reinterpret_cast(data), 2); -} - -bool DeviceReader::readU32(quint32* data) { - return this->readBytes(reinterpret_cast(data), 4); -} - -bool DeviceReader::readU64(quint64* data) { - return this->readBytes(reinterpret_cast(data), 8); -} - -void EncodedLogWriter::setDevice(QIODevice* target) { this->buffer.setDevice(target); } -void EncodedLogReader::setDevice(QIODevice* source) { this->reader.setDevice(source); } - -constexpr quint8 LOG_VERSION = 2; - -bool EncodedLogWriter::writeHeader() { - this->buffer.writeU8(LOG_VERSION); - return this->buffer.flush(); -} - -bool EncodedLogReader::readHeader(bool* success, quint8* version, quint8* readerVersion) { - if (!this->reader.readU8(version)) return false; - *success = *version == LOG_VERSION; - *readerVersion = LOG_VERSION; - return true; -} - -bool EncodedLogWriter::write(const LogMessage& message) { - if (!this->buffer.hasDevice()) return false; - - LogMessage* prevMessage = nullptr; - auto index = this->recentMessages.indexOf(message, &prevMessage); - - // If its a dupe, save memory by reusing the buffer of the first message and letting - // the new one be deallocated. - auto body = prevMessage ? prevMessage->body : message.body; - this->recentMessages.emplace(message.type, message.category, body, message.time); - - if (index != -1) { - auto secondDelta = this->lastMessageTime.secsTo(message.time); - - if (secondDelta < 16 && index < 16) { - this->writeOp(EncodedLogOpcode::RecentMessageShort); - this->buffer.writeU8(index | (secondDelta << 4)); - } else { - this->writeOp(EncodedLogOpcode::RecentMessageLong); - this->buffer.writeU8(index); - this->writeVarInt(secondDelta); - } - - goto finish; - } else { - auto categoryId = this->getOrCreateCategory(message.category); - this->writeVarInt(categoryId); - - auto writeFullTimestamp = [this, &message]() { - this->buffer.writeU64(message.time.toSecsSinceEpoch()); - }; - - if (message.type == QtFatalMsg) { - this->buffer.writeU8(0xff); - writeFullTimestamp(); - } else { - quint8 field = compressedTypeOf(message.type); - - auto secondDelta = this->lastMessageTime.secsTo(message.time); - if (secondDelta >= 0x1d) { - // 0x1d = followed by delta int - // 0x1e = followed by epoch delta int - field |= (secondDelta < 0xffff ? 0x1d : 0x1e) << 3; - } else { - field |= secondDelta << 3; - } - - this->buffer.writeU8(field); - - if (secondDelta >= 0x1d) { - if (secondDelta > 0xffff) { - writeFullTimestamp(); - } else { - this->writeVarInt(secondDelta); - } - } - } - - this->writeString(message.body); - } - -finish: - // copy with second precision - this->lastMessageTime = QDateTime::fromSecsSinceEpoch(message.time.toSecsSinceEpoch()); - return this->buffer.flush(); -} - -bool EncodedLogReader::read(LogMessage* slot) { -start: - quint32 next = 0; - if (!this->readVarInt(&next)) return false; - - if (next < EncodedLogOpcode::BeginCategories) { - if (next == EncodedLogOpcode::RegisterCategory) { - if (!this->registerCategory()) return false; - goto start; - } else if (next == EncodedLogOpcode::RecentMessageShort - || next == EncodedLogOpcode::RecentMessageLong) - { - quint8 index = 0; - quint32 secondDelta = 0; - - if (next == EncodedLogOpcode::RecentMessageShort) { - quint8 field = 0; - if (!this->reader.readU8(&field)) return false; - index = field & 0xf; - secondDelta = field >> 4; - } else { - if (!this->reader.readU8(&index)) return false; - if (!this->readVarInt(&secondDelta)) return false; - } - - if (index >= this->recentMessages.size()) return false; - *slot = this->recentMessages.at(index); - this->lastMessageTime = this->lastMessageTime.addSecs(static_cast(secondDelta)); - slot->time = this->lastMessageTime; - } - } else { - auto categoryId = next - EncodedLogOpcode::BeginCategories; - auto category = this->categories.value(categoryId); - - quint8 field = 0; - if (!this->reader.readU8(&field)) return false; - - auto msgType = QtDebugMsg; - quint64 secondDelta = 0; - auto needsTimeRead = false; - - if (field == 0xff) { - msgType = QtFatalMsg; - needsTimeRead = true; - } else { - msgType = typeOfCompressed(static_cast(field & 0x07)); - secondDelta = field >> 3; - - if (secondDelta == 0x1d) { - quint32 slot = 0; - if (!this->readVarInt(&slot)) return false; - secondDelta = slot; - } else if (secondDelta == 0x1e) { - needsTimeRead = true; - } - } - - if (needsTimeRead) { - if (!this->reader.readU64(&secondDelta)) return false; - } - - this->lastMessageTime = this->lastMessageTime.addSecs(static_cast(secondDelta)); - - QByteArray body; - if (!this->readString(&body)) return false; - - *slot = LogMessage(msgType, QLatin1StringView(category.first), body, this->lastMessageTime); - slot->readCategoryId = categoryId; - } - - this->recentMessages.emplace(*slot); - return true; -} - -CategoryFilter EncodedLogReader::categoryFilterById(quint16 id) { - return this->categories.value(id).second; -} - -void EncodedLogWriter::writeOp(EncodedLogOpcode opcode) { this->buffer.writeU8(opcode); } - -void EncodedLogWriter::writeVarInt(quint32 n) { - if (n < 0xff) { - this->buffer.writeU8(n); - } else if (n < 0xffff) { - this->buffer.writeU8(0xff); - this->buffer.writeU16(n); - } else { - this->buffer.writeU8(0xff); - this->buffer.writeU16(0xffff); - this->buffer.writeU32(n); - } -} - -bool EncodedLogReader::readVarInt(quint32* slot) { - auto bytes = std::array(); - auto readLength = this->reader.peekBytes(reinterpret_cast(bytes.data()), 7); - - if (bytes[0] != 0xff && readLength >= 1) { - auto n = *reinterpret_cast(bytes.data()); - 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); - if (!this->reader.skip(3)) return false; - *slot = qFromLittleEndian(n); - } else if (readLength == 7) { - auto n = *reinterpret_cast(bytes.data() + 3); - if (!this->reader.skip(7)) return false; - *slot = qFromLittleEndian(n); - } else return false; - - return true; -} - -void EncodedLogWriter::writeString(QByteArrayView bytes) { - this->writeVarInt(bytes.length()); - this->buffer.writeBytes(bytes.constData(), bytes.length()); -} - -bool EncodedLogReader::readString(QByteArray* slot) { - quint32 length = 0; - if (!this->readVarInt(&length)) return false; - - *slot = QByteArray(length, Qt::Uninitialized); - auto r = this->reader.readBytes(slot->data(), slot->size()); - return r; -} - -quint16 EncodedLogWriter::getOrCreateCategory(QLatin1StringView category) { - if (this->categories.contains(category)) { - return this->categories.value(category); - } else { - this->writeOp(EncodedLogOpcode::RegisterCategory); - // id is implicitly the next available id - this->writeString(category); - - auto id = this->nextCategory++; - this->categories.insert(category, id); - - auto filter = LogManager::instance()->getFilter(category); - quint8 flags = 0; - flags |= filter.debug << 0; - flags |= filter.info << 1; - flags |= filter.warn << 2; - flags |= filter.critical << 3; - - this->buffer.writeU8(flags); - return id; - } -} - -bool EncodedLogReader::registerCategory() { - QByteArray name; - quint8 flags = 0; - if (!this->readString(&name)) return false; - if (!this->reader.readU8(&flags)) return false; - - CategoryFilter filter; - filter.debug = (flags >> 0) & 1; - filter.info = (flags >> 1) & 1; - filter.warn = (flags >> 2) & 1; - filter.critical = (flags >> 3) & 1; - - this->categories.append(qMakePair(name, filter)); - return true; -} - -bool LogReader::initialize() { - this->reader.setDevice(this->file); - - bool readable = false; - quint8 logVersion = 0; - quint8 readerVersion = 0; - if (!this->reader.readHeader(&readable, &logVersion, &readerVersion)) { - qCritical() << "Failed to read log header."; - return false; - } - - if (!readable) { - qCritical() << "This log was encoded with version" << logVersion - << "of the quickshell log encoder, which cannot be decoded by the current " - "version of quickshell, with log version" - << readerVersion; - return false; - } - - return true; -} - -bool LogReader::continueReading() { - auto color = LogManager::instance()->colorLogs; - auto tailRing = RingBuffer(this->remainingTail); - - LogMessage message; - auto stream = QTextStream(stdout); - auto readCursor = this->file->pos(); - while (this->reader.read(&message)) { - readCursor = this->file->pos(); - - CategoryFilter filter; - if (this->filters.contains(message.readCategoryId)) { - filter = this->filters.value(message.readCategoryId); - } else { - filter = this->reader.categoryFilterById(message.readCategoryId); - - for (const auto& rule: this->rules) { - filter.applyRule(message.category, rule); - } - - this->filters.insert(message.readCategoryId, filter); - } - - if (filter.shouldDisplay(message.type)) { - if (this->remainingTail == 0) { - LogMessage::formatMessage(stream, message, color, this->timestamps); - stream << '\n'; - } else { - tailRing.emplace(message); - } - } - } - - if (this->remainingTail != 0) { - for (auto i = tailRing.size() - 1; i != -1; i--) { - auto& message = tailRing.at(i); - LogMessage::formatMessage(stream, message, color, this->timestamps); - stream << '\n'; - } - } - - stream << Qt::flush; - - if (this->file->pos() != readCursor) { - qCritical() << "An error occurred parsing the end of this log file."; - qCritical() << "Remaining data:" << this->file->readAll(); - return false; - } - - return true; -} - -void LogFollower::FcntlWaitThread::run() { - auto lock = flock { - .l_type = F_RDLCK, // won't block other read locks when we take it - .l_whence = SEEK_SET, - .l_start = 0, - .l_len = 0, - .l_pid = 0, - }; - - auto r = fcntl(this->follower->reader->file->handle(), F_SETLKW, &lock); // NOLINT - - if (r != 0) { - qCWarning(logLogging).nospace() - << "Failed to wait for write locks to be removed from log file with error code " << errno - << ": " << qt_error_string(); - } -} - -bool LogFollower::follow() { - QObject::connect(&this->waitThread, &QThread::finished, this, &LogFollower::onFileLocked); - - QObject::connect( - &this->fileWatcher, - &QFileSystemWatcher::fileChanged, - this, - &LogFollower::onFileChanged - ); - - this->fileWatcher.addPath(this->path); - this->waitThread.start(); - - auto r = QCoreApplication::exec(); - return r == 0; -} - -void LogFollower::onFileChanged() { - if (!this->reader->continueReading()) { - QCoreApplication::exit(1); - } -} - -void LogFollower::onFileLocked() { - if (!this->reader->continueReading()) { - QCoreApplication::exit(1); - } else { - QCoreApplication::exit(0); - } -} - -bool readEncodedLogs( - QFile* file, - const QString& path, - bool timestamps, - int tail, - bool follow, - const QString& rulespec -) { - QList rules; - - { - QLoggingSettingsParser parser; - parser.setContent(rulespec); - rules = parser.rules(); - } - - auto reader = LogReader(file, timestamps, tail, rules); - - if (!reader.initialize()) return false; - if (!reader.continueReading()) return false; - - if (follow) { - auto follower = LogFollower(&reader, path); - return follower.follow(); - } - - return true; -} - -} // namespace qs::log diff --git a/src/core/logging.hpp b/src/core/logging.hpp deleted file mode 100644 index 7ff1b5e0..00000000 --- a/src/core/logging.hpp +++ /dev/null @@ -1,148 +0,0 @@ -#pragma once - -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -Q_DECLARE_LOGGING_CATEGORY(logBare); - -namespace qs::log { - -struct LogMessage { - explicit LogMessage() = default; - - explicit LogMessage( - QtMsgType type, - QLatin1StringView category, - QByteArray body, - QDateTime time = QDateTime::currentDateTime() - ) - : type(type) - , time(std::move(time)) - , category(category) - , body(std::move(body)) {} - - bool operator==(const LogMessage& other) const; - - QtMsgType type = QtDebugMsg; - QDateTime time; - QLatin1StringView category; - QByteArray body; - quint16 readCategoryId = 0; - - static void formatMessage( - QTextStream& stream, - const LogMessage& msg, - bool color, - bool timestamp, - const QString& prefix = "" - ); -}; - -size_t qHash(const LogMessage& message); - -class ThreadLogging; - -class LoggingThreadProxy: public QObject { - Q_OBJECT; - -public: - explicit LoggingThreadProxy() = default; - -public slots: - void initInThread(); - void initFs(); - -private: - ThreadLogging* logging = nullptr; -}; - -namespace qt_logging_registry { -class QLoggingRule; -} - -struct CategoryFilter { - explicit CategoryFilter() = default; - explicit CategoryFilter(QLoggingCategory* category) - : debug(category->isDebugEnabled()) - , info(category->isInfoEnabled()) - , warn(category->isWarningEnabled()) - , critical(category->isCriticalEnabled()) {} - - [[nodiscard]] bool shouldDisplay(QtMsgType type) const; - void apply(QLoggingCategory* category) const; - void applyRule(QLatin1StringView category, const qt_logging_registry::QLoggingRule& rule); - - bool debug = true; - bool info = true; - bool warn = true; - bool critical = true; -}; - -class LogManager: public QObject { - Q_OBJECT; - -public: - static void init( - bool color, - bool timestamp, - bool sparseOnly, - QtMsgType defaultLevel, - const QString& rules, - const QString& prefix = "" - ); - - static void initFs(); - static LogManager* instance(); - - bool colorLogs = true; - bool timestampLogs = false; - - [[nodiscard]] QString rulesString() const; - [[nodiscard]] QtMsgType defaultLevel() const; - [[nodiscard]] bool isSparse() const; - - [[nodiscard]] CategoryFilter getFilter(QLatin1StringView category); - -signals: - void logMessage(LogMessage msg, bool showInSparse); - -private: - explicit LogManager(); - static void messageHandler(QtMsgType type, const QMessageLogContext& context, const QString& msg); - - static void filterCategory(QLoggingCategory* category); - - QLoggingCategory::CategoryFilter lastCategoryFilter = nullptr; - bool sparse = false; - QString prefix; - QString mRulesString; - QList* rules = nullptr; - QtMsgType mDefaultLevel = QtWarningMsg; - QHash sparseFilters; - QHash allFilters; - - QTextStream stdoutStream; - LoggingThreadProxy threadProxy; -}; - -bool readEncodedLogs( - QFile* file, - const QString& path, - bool timestamps, - int tail, - bool follow, - const QString& rulespec -); - -} // namespace qs::log - -using LogManager = qs::log::LogManager; diff --git a/src/core/logging_p.hpp b/src/core/logging_p.hpp deleted file mode 100644 index 3297ea1b..00000000 --- a/src/core/logging_p.hpp +++ /dev/null @@ -1,190 +0,0 @@ -#pragma once -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "logging.hpp" -#include "logging_qtprivate.hpp" -#include "ringbuf.hpp" - -namespace qs::log { - -enum EncodedLogOpcode : quint8 { - RegisterCategory = 0, - RecentMessageShort, - RecentMessageLong, - BeginCategories, -}; - -enum CompressedLogType : quint8 { - Debug = 0, - Info = 1, - Warn = 2, - Critical = 3, -}; - -CompressedLogType compressedTypeOf(QtMsgType type); -QtMsgType typeOfCompressed(CompressedLogType type); - -class WriteBuffer { -public: - void setDevice(QIODevice* device); - [[nodiscard]] bool hasDevice() const; - [[nodiscard]] bool flush(); - void writeBytes(const char* data, qsizetype length); - void writeU8(quint8 data); - void writeU16(quint16 data); - void writeU32(quint32 data); - void writeU64(quint64 data); - -private: - QIODevice* device = nullptr; - QByteArray buffer; -}; - -class DeviceReader { -public: - void setDevice(QIODevice* device); - [[nodiscard]] bool hasDevice() const; - [[nodiscard]] bool readBytes(char* data, qsizetype length); - // peek UP TO length - [[nodiscard]] qsizetype peekBytes(char* data, qsizetype length); - [[nodiscard]] bool skip(qsizetype length); - [[nodiscard]] bool readU8(quint8* data); - [[nodiscard]] bool readU16(quint16* data); - [[nodiscard]] bool readU32(quint32* data); - [[nodiscard]] bool readU64(quint64* data); - -private: - QIODevice* device = nullptr; -}; - -class EncodedLogWriter { -public: - void setDevice(QIODevice* target); - [[nodiscard]] bool writeHeader(); - [[nodiscard]] bool write(const LogMessage& message); - -private: - void writeOp(EncodedLogOpcode opcode); - void writeVarInt(quint32 n); - void writeString(QByteArrayView bytes); - quint16 getOrCreateCategory(QLatin1StringView category); - - WriteBuffer buffer; - - QHash categories; - quint16 nextCategory = EncodedLogOpcode::BeginCategories; - - QDateTime lastMessageTime = QDateTime::fromSecsSinceEpoch(0); - HashBuffer recentMessages {256}; -}; - -class EncodedLogReader { -public: - void setDevice(QIODevice* source); - [[nodiscard]] bool readHeader(bool* success, quint8* logVersion, quint8* readerVersion); - // WARNING: log messages written to the given slot are invalidated when the log reader is destroyed. - [[nodiscard]] bool read(LogMessage* slot); - [[nodiscard]] CategoryFilter categoryFilterById(quint16 id); - -private: - [[nodiscard]] bool readVarInt(quint32* slot); - [[nodiscard]] bool readString(QByteArray* slot); - [[nodiscard]] bool registerCategory(); - - DeviceReader reader; - QVector> categories; - QDateTime lastMessageTime = QDateTime::fromSecsSinceEpoch(0); - RingBuffer recentMessages {256}; -}; - -class ThreadLogging: public QObject { - Q_OBJECT; - -public: - explicit ThreadLogging(QObject* parent): QObject(parent) {} - - void init(); - void initFs(); - void setupFileLogging(); - -private slots: - void onMessage(const LogMessage& msg, bool showInSparse); - -private: - QFile* file = nullptr; - QTextStream fileStream; - QFile* detailedFile = nullptr; - EncodedLogWriter detailedWriter; -}; - -class LogFollower; - -class LogReader { -public: - explicit LogReader( - QFile* file, - bool timestamps, - int tail, - QList rules - ) - : file(file) - , timestamps(timestamps) - , remainingTail(tail) - , rules(std::move(rules)) {} - - bool initialize(); - bool continueReading(); - -private: - QFile* file; - EncodedLogReader reader; - bool timestamps; - int remainingTail; - QHash filters; - QList rules; - - friend class LogFollower; -}; - -class LogFollower: public QObject { - Q_OBJECT; - -public: - explicit LogFollower(LogReader* reader, QString path): reader(reader), path(std::move(path)) {} - - bool follow(); - -private slots: - void onFileChanged(); - void onFileLocked(); - -private: - LogReader* reader; - QString path; - QFileSystemWatcher fileWatcher; - - class FcntlWaitThread: public QThread { - public: - explicit FcntlWaitThread(LogFollower* follower): follower(follower) {} - - protected: - void run() override; - - private: - LogFollower* follower; - }; - - FcntlWaitThread waitThread {this}; -}; - -} // namespace qs::log diff --git a/src/core/logging_qtprivate.cpp b/src/core/logging_qtprivate.cpp deleted file mode 100644 index 5078eeb4..00000000 --- a/src/core/logging_qtprivate.cpp +++ /dev/null @@ -1,138 +0,0 @@ -// The logging rule parser from qloggingregistry_p.h and qloggingregistry.cpp. - -// Was unable to properly link the functions when directly using the headers (which we depend -// on anyway), so below is a slightly stripped down copy. Making the originals link would -// be preferable. - -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "logging_qtprivate.hpp" - -namespace qs::log { -Q_DECLARE_LOGGING_CATEGORY(logLogging); - -namespace qt_logging_registry { - -class QLoggingSettingsParser { -public: - void setContent(QStringView content); - - [[nodiscard]] QList rules() const { return this->mRules; } - -private: - void parseNextLine(QStringView line); - -private: - QList mRules; -}; - -void QLoggingSettingsParser::setContent(QStringView content) { - this->mRules.clear(); - for (auto line: qTokenize(content, u';')) this->parseNextLine(line); -} - -void QLoggingSettingsParser::parseNextLine(QStringView line) { - // Remove whitespace at start and end of line: - line = line.trimmed(); - - const qsizetype equalPos = line.indexOf(u'='); - if (equalPos != -1) { - if (line.lastIndexOf(u'=') == equalPos) { - const auto key = line.left(equalPos).trimmed(); - const QStringView pattern = key; - const auto valueStr = line.mid(equalPos + 1).trimmed(); - int value = -1; - if (valueStr == QString("true")) value = 1; - else if (valueStr == QString("false")) value = 0; - QLoggingRule rule(pattern, (value == 1)); - if (rule.flags != 0 && (value != -1)) this->mRules.append(std::move(rule)); - else - qCWarning(logLogging, "Ignoring malformed logging rule: '%s'", line.toUtf8().constData()); - } else { - qCWarning(logLogging, "Ignoring malformed logging rule: '%s'", line.toUtf8().constData()); - } - } -} - -QLoggingRule::QLoggingRule(QStringView pattern, bool enabled): messageType(-1), enabled(enabled) { - this->parse(pattern); -} - -void QLoggingRule::parse(QStringView pattern) { - QStringView p; - - // strip trailing ".messagetype" - if (pattern.endsWith(QString(".debug"))) { - p = pattern.chopped(6); // strlen(".debug") - this->messageType = QtDebugMsg; - } else if (pattern.endsWith(QString(".info"))) { - p = pattern.chopped(5); // strlen(".info") - this->messageType = QtInfoMsg; - } else if (pattern.endsWith(QString(".warning"))) { - p = pattern.chopped(8); // strlen(".warning") - this->messageType = QtWarningMsg; - } else if (pattern.endsWith(QString(".critical"))) { - p = pattern.chopped(9); // strlen(".critical") - this->messageType = QtCriticalMsg; - } else { - p = pattern; - } - - const QChar asterisk = u'*'; - if (!p.contains(asterisk)) { - this->flags = FullText; - } else { - if (p.endsWith(asterisk)) { - this->flags |= LeftFilter; - p = p.chopped(1); - } - if (p.startsWith(asterisk)) { - this->flags |= RightFilter; - p = p.mid(1); - } - if (p.contains(asterisk)) // '*' only supported at start/end - this->flags = PatternFlags(); - } - - this->category = p.toString(); -} - -int QLoggingRule::pass(QLatin1StringView cat, QtMsgType msgType) const { - // check message type - if (this->messageType > -1 && this->messageType != msgType) return 0; - - if (this->flags == FullText) { - // full match - if (this->category == cat) return (this->enabled ? 1 : -1); - else return 0; - } - - const qsizetype idx = cat.indexOf(this->category); - if (idx >= 0) { - if (this->flags == MidFilter) { - // matches somewhere - return (this->enabled ? 1 : -1); - } else if (this->flags == LeftFilter) { - // matches left - if (idx == 0) return (this->enabled ? 1 : -1); - } else if (this->flags == RightFilter) { - // matches right - if (idx == (cat.size() - this->category.size())) return (this->enabled ? 1 : -1); - } - } - return 0; -} - -} // namespace qt_logging_registry - -} // namespace qs::log diff --git a/src/core/logging_qtprivate.hpp b/src/core/logging_qtprivate.hpp deleted file mode 100644 index 83c82585..00000000 --- a/src/core/logging_qtprivate.hpp +++ /dev/null @@ -1,45 +0,0 @@ -#pragma once - -// The logging rule parser from qloggingregistry_p.h and qloggingregistry.cpp. - -// Was unable to properly link the functions when directly using the headers (which we depend -// on anyway), so below is a slightly stripped down copy. Making the originals link would -// be preferable. - -#include -#include -#include -#include -#include - -namespace qs::log { -Q_DECLARE_LOGGING_CATEGORY(logLogging); - -namespace qt_logging_registry { - -class QLoggingRule { -public: - QLoggingRule(); - QLoggingRule(QStringView pattern, bool enabled); - [[nodiscard]] int pass(QLatin1StringView categoryName, QtMsgType type) const; - - enum PatternFlag : quint8 { - FullText = 0x1, - LeftFilter = 0x2, - RightFilter = 0x4, - MidFilter = LeftFilter | RightFilter - }; - Q_DECLARE_FLAGS(PatternFlags, PatternFlag) - - QString category; - int messageType; - PatternFlags flags; - bool enabled; - -private: - void parse(QStringView pattern); -}; - -} // namespace qt_logging_registry - -} // namespace qs::log diff --git a/src/core/main.cpp b/src/core/main.cpp new file mode 100644 index 00000000..2cfd4d9c --- /dev/null +++ b/src/core/main.cpp @@ -0,0 +1,331 @@ +#include "main.hpp" +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "plugin.hpp" +#include "rootwrapper.hpp" + +int qs_main(int argc, char** argv) { + QString configFilePath; + QString workingDirectory; + + auto useQApplication = false; + auto nativeTextRendering = false; + auto desktopSettingsAware = true; + QHash envOverrides; + + { + const auto app = QCoreApplication(argc, argv); + QCoreApplication::setApplicationName("quickshell"); + QCoreApplication::setApplicationVersion("0.1.0 (" GIT_REVISION ")"); + + QCommandLineParser parser; + parser.addHelpOption(); + parser.addVersionOption(); + + // clang-format off + auto currentOption = QCommandLineOption("current", "Print information about the manifest and defaults."); + auto manifestOption = QCommandLineOption({"m", "manifest"}, "Path to a configuration manifest.", "path"); + auto configOption = QCommandLineOption({"c", "config"}, "Name of a configuration in the manifest.", "name"); + auto pathOption = QCommandLineOption({"p", "path"}, "Path to a configuration file.", "path"); + auto workdirOption = QCommandLineOption({"d", "workdir"}, "Initial working directory.", "path"); + // clang-format on + + parser.addOption(currentOption); + parser.addOption(manifestOption); + parser.addOption(configOption); + parser.addOption(pathOption); + parser.addOption(workdirOption); + parser.process(app); + + { + auto printCurrent = parser.isSet(currentOption); + + // NOLINTBEGIN +#define CHECK(rname, name, level, label, expr) \ + QString name = expr; \ + if (rname.isEmpty() && !name.isEmpty()) { \ + rname = name; \ + rname##Level = level; \ + if (!printCurrent) goto label; \ + } + +#define OPTSTR(name) (name.isEmpty() ? "(unset)" : name.toStdString()) + // NOLINTEND + + QString basePath; + int basePathLevel = 0; + Q_UNUSED(basePathLevel); + { + // NOLINTBEGIN + // clang-format off + CHECK(basePath, envBasePath, 0, foundbase, qEnvironmentVariable("QS_BASE_PATH")); + CHECK(basePath, defaultBasePath, 0, foundbase, QDir(QStandardPaths::writableLocation(QStandardPaths::ConfigLocation)).filePath("quickshell")); + // clang-format on + // NOLINTEND + + if (printCurrent) { + // clang-format off + std::cout << "Base path: " << OPTSTR(basePath) << "\n"; + std::cout << " - Environment (QS_BASE_PATH): " << OPTSTR(envBasePath) << "\n"; + std::cout << " - Default: " << OPTSTR(defaultBasePath) << "\n"; + // clang-format on + } + } + foundbase:; + + QString configPath; + int configPathLevel = 10; + { + // NOLINTBEGIN + CHECK(configPath, optionConfigPath, 0, foundpath, parser.value(pathOption)); + CHECK(configPath, envConfigPath, 1, foundpath, qEnvironmentVariable("QS_CONFIG_PATH")); + // NOLINTEND + + if (printCurrent) { + // clang-format off + std::cout << "\nConfig path: " << OPTSTR(configPath) << "\n"; + std::cout << " - Option: " << OPTSTR(optionConfigPath) << "\n"; + std::cout << " - Environment (QS_CONFIG_PATH): " << OPTSTR(envConfigPath) << "\n"; + // clang-format on + } + } + foundpath:; + + QString manifestPath; + int manifestPathLevel = 10; + { + // NOLINTBEGIN + // clang-format off + CHECK(manifestPath, optionManifestPath, 0, foundmf, parser.value(manifestOption)); + CHECK(manifestPath, envManifestPath, 1, foundmf, qEnvironmentVariable("QS_MANIFEST")); + CHECK(manifestPath, defaultManifestPath, 2, foundmf, QDir(basePath).filePath("manifest.conf")); + // clang-format on + // NOLINTEND + + if (printCurrent) { + // clang-format off + std::cout << "\nManifest path: " << OPTSTR(manifestPath) << "\n"; + std::cout << " - Option: " << OPTSTR(optionManifestPath) << "\n"; + std::cout << " - Environment (QS_MANIFEST): " << OPTSTR(envManifestPath) << "\n"; + std::cout << " - Default: " << OPTSTR(defaultManifestPath) << "\n"; + // clang-format on + } + } + foundmf:; + + QString configName; + int configNameLevel = 10; + { + // NOLINTBEGIN + CHECK(configName, optionConfigName, 0, foundname, parser.value(configOption)); + CHECK(configName, envConfigName, 1, foundname, qEnvironmentVariable("QS_CONFIG_NAME")); + // NOLINTEND + + if (printCurrent) { + // clang-format off + std::cout << "\nConfig name: " << OPTSTR(configName) << "\n"; + std::cout << " - Option: " << OPTSTR(optionConfigName) << "\n"; + std::cout << " - Environment (QS_CONFIG_NAME): " << OPTSTR(envConfigName) << "\n\n"; + // clang-format on + } + } + foundname:; + + if (configPathLevel == 0 && configNameLevel == 0) { + qCritical() << "Pass only one of --path or --config"; + return -1; + } + + if (!configPath.isEmpty() && configPathLevel <= configNameLevel) { + configFilePath = configPath; + } else if (!configName.isEmpty()) { + if (!manifestPath.isEmpty()) { + auto file = QFile(manifestPath); + if (file.open(QIODevice::ReadOnly | QIODevice::Text)) { + auto stream = QTextStream(&file); + while (!stream.atEnd()) { + auto line = stream.readLine(); + if (line.trimmed().startsWith("#")) continue; + if (line.trimmed().isEmpty()) continue; + + auto split = line.split('='); + if (split.length() != 2) { + qCritical() << "manifest line not in expected format 'name = relativepath':" + << line; + return -1; + } + + if (split[0].trimmed() == configName) { + configFilePath = QDir(QFileInfo(file).canonicalPath()).filePath(split[1].trimmed()); + goto haspath; // NOLINT + } + } + + qCritical() << "configuration" << configName << "not found in manifest" << manifestPath; + return -1; + } else if (manifestPathLevel < 2) { + qCritical() << "cannot open config manifest at" << manifestPath; + return -1; + } + } + + { + auto basePathInfo = QFileInfo(basePath); + if (!basePathInfo.exists()) { + qCritical() << "base path does not exist:" << basePath; + return -1; + } else if (!QFileInfo(basePathInfo.canonicalFilePath()).isDir()) { + qCritical() << "base path is not a directory" << basePath; + return -1; + } + + auto dir = QDir(basePath); + for (auto& entry: dir.entryList(QDir::AllDirs | QDir::NoDotAndDotDot)) { + if (entry == configName) { + configFilePath = dir.filePath(entry); + goto haspath; // NOLINT + } + } + + qCritical() << "no directory named " << configName << "found in base path" << basePath; + return -1; + } + haspath:; + } else { + configFilePath = basePath; + } + + auto configFile = QFileInfo(configFilePath); + if (!configFile.exists()) { + qCritical() << "config path does not exist:" << configFilePath; + return -1; + } + + if (configFile.isDir()) { + configFilePath = QDir(configFilePath).filePath("shell.qml"); + } + + configFile = QFileInfo(configFilePath); + if (!configFile.exists()) { + qCritical() << "no shell.qml found in config path:" << configFilePath; + return -1; + } else if (configFile.isDir()) { + qCritical() << "shell.qml is a directory:" << configFilePath; + return -1; + } + + configFilePath = QFileInfo(configFilePath).canonicalFilePath(); + configFile = QFileInfo(configFilePath); + if (!configFile.exists()) { + qCritical() << "config file does not exist:" << configFilePath; + return -1; + } else if (configFile.isDir()) { + qCritical() << "config file is a directory:" << configFilePath; + return -1; + } + +#undef CHECK +#undef OPTSTR + + qInfo() << "config file path:" << configFilePath; + + if (printCurrent) return 0; + } + + if (!QFile(configFilePath).exists()) { + qCritical() << "config file does not exist"; + return -1; + } + + if (parser.isSet(workdirOption)) { + workingDirectory = parser.value(workdirOption); + } + + auto file = QFile(configFilePath); + if (!file.open(QFile::ReadOnly | QFile::Text)) { + qCritical() << "could not open config file"; + return -1; + } + + auto stream = QTextStream(&file); + while (!stream.atEnd()) { + auto line = stream.readLine().trimmed(); + if (line.startsWith("//@ pragma ")) { + auto pragma = line.sliced(11).trimmed(); + + if (pragma == "UseQApplication") useQApplication = true; + else if (pragma == "NativeTextRendering") nativeTextRendering = true; + else if (pragma == "IgnoreSystemSettings") desktopSettingsAware = false; + else if (pragma.startsWith("Env ")) { + auto envPragma = pragma.sliced(4); + auto splitIdx = envPragma.indexOf('='); + + if (splitIdx == -1) { + qCritical() << "Env pragma" << pragma << "not in the form 'VAR = VALUE'"; + return -1; + } + + auto var = envPragma.sliced(0, splitIdx).trimmed(); + auto val = envPragma.sliced(splitIdx + 1).trimmed(); + envOverrides.insert(var, val); + } else { + qCritical() << "Unrecognized pragma" << pragma; + return -1; + } + } else if (line.startsWith("import")) break; + } + + file.close(); + } + + for (auto [var, val]: envOverrides.asKeyValueRange()) { + qputenv(var.toUtf8(), val.toUtf8()); + } + + QGuiApplication::setDesktopSettingsAware(desktopSettingsAware); + + QGuiApplication* app = nullptr; + + if (useQApplication) { + app = new QApplication(argc, argv); + } else { + app = new QGuiApplication(argc, argv); + } + + if (!workingDirectory.isEmpty()) { + QDir::setCurrent(workingDirectory); + } + + QuickshellPlugin::initPlugins(); + + // Base window transparency appears to be additive. + // Use a fully transparent window with a colored rect. + QQuickWindow::setDefaultAlphaBuffer(true); + + if (nativeTextRendering) { + QQuickWindow::setTextRenderType(QQuickWindow::NativeTextRendering); + } + + auto root = RootWrapper(configFilePath); + QGuiApplication::setQuitOnLastWindowClosed(false); + + auto code = QGuiApplication::exec(); + delete app; + return code; +} diff --git a/src/core/main.hpp b/src/core/main.hpp new file mode 100644 index 00000000..33921b40 --- /dev/null +++ b/src/core/main.hpp @@ -0,0 +1,3 @@ +#pragma once + +int qs_main(int argc, char** argv); // NOLINT diff --git a/src/core/model.cpp b/src/core/model.cpp deleted file mode 100644 index 2aba1846..00000000 --- a/src/core/model.cpp +++ /dev/null @@ -1,98 +0,0 @@ -#include "model.hpp" - -#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"}}; -} - -QQmlListProperty UntypedObjectModel::values() { - return QQmlListProperty( - this, - nullptr, - &UntypedObjectModel::valuesCount, - &UntypedObjectModel::valueAt - ); -} - -qsizetype UntypedObjectModel::valuesCount(QQmlListProperty* property) { - return static_cast(property->object)->valuesList.count(); // NOLINT -} - -QObject* UntypedObjectModel::valueAt(QQmlListProperty* property, qsizetype index) { - return static_cast(property->object)->valuesList.at(index); // NOLINT -} - -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 - return instance; -} diff --git a/src/core/model.hpp b/src/core/model.hpp deleted file mode 100644 index 56297bfa..00000000 --- a/src/core/model.hpp +++ /dev/null @@ -1,111 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "doc.hpp" - -///! View into a list of objets -/// Typed view into a list of objects. -/// -/// An ObjectModel works as a QML [Data Model], allowing efficient interaction with -/// components that act on models. It has a single role named `modelData`, to match the -/// behavior of lists. -/// The same information contained in the list model is available as a normal list -/// via the `values` property. -/// -/// #### Differences from a list -/// Unlike with a list, the following property binding will never be updated when `model[3]` changes. -/// ```qml -/// // will not update reactively -/// property var foo: model[3] -/// ``` -/// -/// You can work around this limitation using the @@values property of the model to view it as a list. -/// ```qml -/// // will update reactively -/// property var foo: model.values[3] -/// ``` -/// -/// [Data Model]: https://doc.qt.io/qt-6/qtquick-modelviewsdata-modelview.html#qml-data-models -class UntypedObjectModel: public QAbstractListModel { - QSDOC_CNAME(ObjectModel); - Q_OBJECT; - /// The content of the object model, as a QML list. - /// The values of this property will always be of the type of the model. - Q_PROPERTY(QQmlListProperty values READ values NOTIFY valuesChanged); - QML_NAMED_ELEMENT(ObjectModel); - QML_UNCREATABLE("ObjectModels cannot be created directly."); - -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]] QQmlListProperty values(); - void removeAt(qsizetype index); - - Q_INVOKABLE qsizetype indexOf(QObject* object); - - static UntypedObjectModel* emptyInstance(); - -signals: - void valuesChanged(); - /// Sent immediately before an object is inserted into the list. - void objectInsertedPre(QObject* object, qsizetype index); - /// Sent immediately after an object is inserted into the list. - void objectInsertedPost(QObject* object, qsizetype index); - /// Sent immediately before an object is removed from the list. - void objectRemovedPre(QObject* object, qsizetype index); - /// 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); -}; - -template -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); - } - - void insertObject(T* object, qsizetype index = -1) { - this->UntypedObjectModel::insertObject(object, index); - } - - void removeObject(const T* object) { this->UntypedObjectModel::removeObject(object); } - - // Assumes only one instance of a specific value - void diffUpdate(const QVector& newValues) { - this->UntypedObjectModel::diffUpdate(*std::bit_cast*>(&newValues)); - } - - static ObjectModel* emptyInstance() { - return static_cast*>(UntypedObjectModel::emptyInstance()); - } -}; diff --git a/src/core/module.md b/src/core/module.md index c8b17ab9..8eb9b638 100644 --- a/src/core/module.md +++ b/src/core/module.md @@ -7,27 +7,16 @@ headers = [ "shell.hpp", "variants.hpp", "region.hpp", - "../window/proxywindow.hpp", + "proxywindow.hpp", "persistentprops.hpp", - "../window/windowinterface.hpp", - "../window/panelinterface.hpp", - "../window/floatingwindow.hpp", - "../window/popupwindow.hpp", + "windowinterface.hpp", + "panelinterface.hpp", + "floatingwindow.hpp", + "popupwindow.hpp", "singleton.hpp", "lazyloader.hpp", "easingcurve.hpp", "transformwatcher.hpp", "boundcomponent.hpp", - "model.hpp", - "elapsedtimer.hpp", - "desktopentry.hpp", - "objectrepeater.hpp", - "qsmenu.hpp", - "retainable.hpp", - "popupanchor.hpp", - "types.hpp", - "qsmenuanchor.hpp", - "clock.hpp", - "scriptmodel.hpp", ] ----- diff --git a/src/core/objectrepeater.cpp b/src/core/objectrepeater.cpp deleted file mode 100644 index 7971952c..00000000 --- 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 409b12dc..00000000 --- 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/window/panelinterface.cpp b/src/core/panelinterface.cpp similarity index 100% rename from src/window/panelinterface.cpp rename to src/core/panelinterface.cpp diff --git a/src/window/panelinterface.hpp b/src/core/panelinterface.hpp similarity index 77% rename from src/window/panelinterface.hpp rename to src/core/panelinterface.hpp index b9664ff9..b46c25ca 100644 --- a/src/window/panelinterface.hpp +++ b/src/core/panelinterface.hpp @@ -2,9 +2,8 @@ #include #include -#include -#include "../core/doc.hpp" +#include "doc.hpp" #include "windowinterface.hpp" class Anchors { @@ -13,8 +12,7 @@ class Anchors { Q_PROPERTY(bool right MEMBER mRight); Q_PROPERTY(bool top MEMBER mTop); Q_PROPERTY(bool bottom MEMBER mBottom); - QML_VALUE_TYPE(panelAnchors); - QML_STRUCTURED_VALUE; + QML_VALUE_TYPE(anchors); public: [[nodiscard]] bool horizontalConstraint() const noexcept { return this->mLeft && this->mRight; } @@ -41,8 +39,7 @@ class Margins { Q_PROPERTY(qint32 right MEMBER mRight); Q_PROPERTY(qint32 top MEMBER mTop); Q_PROPERTY(qint32 bottom MEMBER mBottom); - QML_VALUE_TYPE(panelMargins); - QML_STRUCTURED_VALUE; + QML_VALUE_TYPE(margins); public: [[nodiscard]] bool operator==(const Margins& other) const noexcept { @@ -60,13 +57,11 @@ public: qint32 mBottom = 0; }; -///! Panel exclusion mode -/// See @@PanelWindow.exclusionMode. namespace ExclusionMode { // NOLINT Q_NAMESPACE; QML_ELEMENT; -enum Enum : quint8 { +enum Enum { /// Respect the exclusion zone of other shell layers and optionally set one Normal = 0, /// Ignore exclusion zones of other shell layers. You cannot set an exclusion zone in this mode. @@ -116,24 +111,14 @@ class PanelWindowInterface: public WindowInterface { /// > [!INFO] Only applies to edges with anchors Q_PROPERTY(Margins margins READ margins WRITE setMargins NOTIFY marginsChanged); /// The amount of space reserved for the shell layer relative to its anchors. - /// Setting this property sets @@exclusionMode to `ExclusionMode.Normal`. + /// Setting this property sets `exclusionMode` to `Normal`. /// /// > [!INFO] Either 1 or 3 anchors are required for the zone to take effect. Q_PROPERTY(qint32 exclusiveZone READ exclusiveZone WRITE setExclusiveZone NOTIFY exclusiveZoneChanged); /// Defaults to `ExclusionMode.Auto`. Q_PROPERTY(ExclusionMode::Enum exclusionMode READ exclusionMode WRITE setExclusionMode NOTIFY exclusionModeChanged); - /// If the panel should render above standard windows. Defaults to true. - /// - /// Note: On Wayland this property corrosponds to @@Quickshell.Wayland.WlrLayershell.layer. - Q_PROPERTY(bool aboveWindows READ aboveWindows WRITE setAboveWindows NOTIFY aboveWindowsChanged); - /// If the panel should accept keyboard focus. Defaults to false. - /// - /// Note: On Wayland this property corrosponds to @@Quickshell.Wayland.WlrLayershell.keyboardFocus. - Q_PROPERTY(bool focusable READ focusable WRITE setFocusable NOTIFY focusableChanged); // clang-format on - QML_NAMED_ELEMENT(PanelWindow); - QML_UNCREATABLE("No PanelWindow backend loaded."); - QSDOC_CREATABLE; + QSDOC_NAMED_ELEMENT(PanelWindow); public: explicit PanelWindowInterface(QObject* parent = nullptr): WindowInterface(parent) {} @@ -150,17 +135,9 @@ public: [[nodiscard]] virtual ExclusionMode::Enum exclusionMode() const = 0; virtual void setExclusionMode(ExclusionMode::Enum exclusionMode) = 0; - [[nodiscard]] virtual bool aboveWindows() const = 0; - virtual void setAboveWindows(bool aboveWindows) = 0; - - [[nodiscard]] virtual bool focusable() const = 0; - virtual void setFocusable(bool focusable) = 0; - signals: void anchorsChanged(); void marginsChanged(); void exclusiveZoneChanged(); void exclusionModeChanged(); - void aboveWindowsChanged(); - void focusableChanged(); }; diff --git a/src/core/paths.cpp b/src/core/paths.cpp deleted file mode 100644 index e49a9d41..00000000 --- a/src/core/paths.cpp +++ /dev/null @@ -1,310 +0,0 @@ -#include "paths.hpp" -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "instanceinfo.hpp" - -namespace { -Q_LOGGING_CATEGORY(logPaths, "quickshell.paths", QtWarningMsg); -} - -QsPaths* QsPaths::instance() { - static auto* instance = new QsPaths(); // NOLINT - return instance; -} - -void QsPaths::init(QString shellId, QString pathId) { - auto* instance = QsPaths::instance(); - instance->shellId = std::move(shellId); - instance->pathId = std::move(pathId); -} - -QDir QsPaths::crashDir(const QString& id) { - auto dir = QDir(QStandardPaths::writableLocation(QStandardPaths::CacheLocation)); - dir = QDir(dir.filePath("crashes")); - dir = QDir(dir.filePath(id)); - - return dir; -} - -QString QsPaths::basePath(const QString& id) { - auto path = QsPaths::instance()->baseRunDir()->filePath("by-id"); - path = QDir(path).filePath(id); - return path; -} - -QString QsPaths::ipcPath(const QString& id) { - return QDir(QsPaths::basePath(id)).filePath("ipc.sock"); -} - -QDir* QsPaths::cacheDir() { - if (this->cacheState == DirState::Unknown) { - auto dir = QDir(QStandardPaths::writableLocation(QStandardPaths::CacheLocation)); - dir = QDir(dir.filePath(this->shellId)); - this->mCacheDir = dir; - - qCDebug(logPaths) << "Initialized cache path:" << dir.path(); - - if (!dir.mkpath(".")) { - qCCritical(logPaths) << "Could not create cache directory at" << dir.path(); - - this->cacheState = DirState::Failed; - } else { - this->cacheState = DirState::Ready; - } - } - - if (this->cacheState == DirState::Failed) return nullptr; - else return &this->mCacheDir; -} - -QDir* QsPaths::baseRunDir() { - if (this->baseRunState == DirState::Unknown) { - auto runtimeDir = qEnvironmentVariable("XDG_RUNTIME_DIR"); - if (runtimeDir.isEmpty()) { - runtimeDir = QString("/run/user/$1").arg(getuid()); - qCInfo(logPaths) << "XDG_RUNTIME_DIR was not set, defaulting to" << runtimeDir; - } - - this->mBaseRunDir = QDir(runtimeDir); - this->mBaseRunDir = QDir(this->mBaseRunDir.filePath("quickshell")); - qCDebug(logPaths) << "Initialized base runtime path:" << this->mBaseRunDir.path(); - - if (!this->mBaseRunDir.mkpath(".")) { - qCCritical(logPaths) << "Could not create base runtime directory at" - << this->mBaseRunDir.path(); - - this->baseRunState = DirState::Failed; - } else { - this->baseRunState = DirState::Ready; - } - } - - if (this->baseRunState == DirState::Failed) return nullptr; - else return &this->mBaseRunDir; -} - -QDir* QsPaths::shellRunDir() { - if (this->shellRunState == DirState::Unknown) { - if (auto* baseRunDir = this->baseRunDir()) { - this->mShellRunDir = QDir(baseRunDir->filePath("by-shell")); - this->mShellRunDir = QDir(this->mShellRunDir.filePath(this->shellId)); - - qCDebug(logPaths) << "Initialized runtime path:" << this->mShellRunDir.path(); - - if (!this->mShellRunDir.mkpath(".")) { - qCCritical(logPaths) << "Could not create runtime directory at" - << this->mShellRunDir.path(); - this->shellRunState = DirState::Failed; - } else { - this->shellRunState = DirState::Ready; - } - } else { - qCCritical(logPaths) << "Could not create shell runtime path as it was not possible to " - "create the base runtime path."; - - this->shellRunState = DirState::Failed; - } - } - - if (this->shellRunState == DirState::Failed) return nullptr; - else return &this->mShellRunDir; -} - -QDir* QsPaths::instanceRunDir() { - if (this->instanceRunState == DirState::Unknown) { - auto* runDir = this->baseRunDir(); - - if (!runDir) { - qCCritical(logPaths) << "Cannot create instance runtime directory as main runtim directory " - "could not be created."; - this->instanceRunState = DirState::Failed; - } else { - auto byIdDir = QDir(runDir->filePath("by-id")); - - this->mInstanceRunDir = byIdDir.filePath(InstanceInfo::CURRENT.instanceId); - - qCDebug(logPaths) << "Initialized instance runtime path:" << this->mInstanceRunDir.path(); - - if (!this->mInstanceRunDir.mkpath(".")) { - qCCritical(logPaths) << "Could not create instance runtime directory at" - << this->mInstanceRunDir.path(); - this->instanceRunState = DirState::Failed; - } else { - this->instanceRunState = DirState::Ready; - } - } - } - - if (this->shellRunState == DirState::Failed) return nullptr; - else return &this->mInstanceRunDir; -} - -void QsPaths::linkRunDir() { - if (auto* runDir = this->instanceRunDir()) { - auto pidDir = QDir(this->baseRunDir()->filePath("by-pid")); - auto* shellDir = this->shellRunDir(); - - if (!shellDir) { - qCCritical(logPaths - ) << "Could not create by-id symlink as the shell runtime path could not be created."; - } else { - auto shellPath = shellDir->filePath(runDir->dirName()); - - QFile::remove(shellPath); - auto r = - symlinkat(runDir->filesystemCanonicalPath().c_str(), 0, shellPath.toStdString().c_str()); - - if (r != 0) { - qCCritical(logPaths).nospace() - << "Could not create id symlink to " << runDir->path() << " at " << shellPath - << " with error code " << errno << ": " << qt_error_string(); - } else { - qCDebug(logPaths) << "Created shellid symlink" << shellPath << "to instance runtime path" - << runDir->path(); - } - } - - if (!pidDir.mkpath(".")) { - qCCritical(logPaths) << "Could not create PID symlink directory."; - } else { - auto pidPath = pidDir.filePath(QString::number(getpid())); - - QFile::remove(pidPath); - auto r = - symlinkat(runDir->filesystemCanonicalPath().c_str(), 0, pidPath.toStdString().c_str()); - - if (r != 0) { - qCCritical(logPaths).nospace() - << "Could not create PID symlink to " << runDir->path() << " at " << pidPath - << " with error code " << errno << ": " << qt_error_string(); - } else { - qCDebug(logPaths) << "Created PID symlink" << pidPath << "to instance runtime path" - << runDir->path(); - } - } - } else { - qCCritical(logPaths) << "Could not create PID symlink to runtime directory, as the runtime " - "directory could not be created."; - } -} - -void QsPaths::linkPathDir() { - if (auto* runDir = this->shellRunDir()) { - auto pathDir = QDir(this->baseRunDir()->filePath("by-path")); - - if (!pathDir.mkpath(".")) { - qCCritical(logPaths) << "Could not create path symlink directory."; - return; - } - - auto linkPath = pathDir.filePath(this->pathId); - - QFile::remove(linkPath); - auto r = - symlinkat(runDir->filesystemCanonicalPath().c_str(), 0, linkPath.toStdString().c_str()); - - if (r != 0) { - qCCritical(logPaths).nospace() - << "Could not create path symlink to " << runDir->path() << " at " << linkPath - << " with error code " << errno << ": " << qt_error_string(); - } else { - qCDebug(logPaths) << "Created path symlink" << linkPath << "to shell runtime path" - << runDir->path(); - } - } else { - qCCritical(logPaths) << "Could not create path symlink to shell runtime directory, as the " - "shell runtime directory could not be created."; - } -} - -void QsPaths::createLock() { - if (auto* runDir = this->instanceRunDir()) { - auto path = runDir->filePath("instance.lock"); - auto* file = new QFile(path); // leaked - - if (!file->open(QFile::ReadWrite | QFile::Truncate)) { - qCCritical(logPaths) << "Could not create instance lock at" << path; - return; - } - - auto lock = flock { - .l_type = F_WRLCK, - .l_whence = SEEK_SET, - .l_start = 0, - .l_len = 0, - .l_pid = 0, - }; - - if (fcntl(file->handle(), F_SETLK, &lock) != 0) { // NOLINT - qCCritical(logPaths).nospace() << "Could not lock instance lock at " << path - << " with error code " << errno << ": " << qt_error_string(); - } else { - auto stream = QDataStream(file); - stream << InstanceInfo::CURRENT; - file->flush(); - qCDebug(logPaths) << "Created instance lock at" << path; - } - } else { - qCCritical(logPaths - ) << "Could not create instance lock, as the instance runtime directory could not be created."; - } -} - -bool QsPaths::checkLock(const QString& path, InstanceLockInfo* info) { - auto file = QFile(QDir(path).filePath("instance.lock")); - if (!file.open(QFile::ReadOnly)) return false; - - auto lock = flock { - .l_type = F_WRLCK, - .l_whence = SEEK_SET, - .l_start = 0, - .l_len = 0, - .l_pid = 0, - }; - - fcntl(file.handle(), F_GETLK, &lock); // NOLINT - if (lock.l_type == F_UNLCK) return false; - - if (info) { - info->pid = lock.l_pid; - - auto stream = QDataStream(&file); - stream >> info->instance; - } - - return true; -} - -QVector QsPaths::collectInstances(const QString& path) { - qCDebug(logPaths) << "Collecting instances from" << path; - auto instances = QVector(); - auto dir = QDir(path); - - InstanceLockInfo info; - for (auto& entry: dir.entryList(QDir::Dirs | QDir::NoDotAndDotDot)) { - auto path = dir.filePath(entry); - - if (QsPaths::checkLock(path, &info)) { - qCDebug(logPaths).nospace() << "Found live instance " << info.instance.instanceId << " (pid " - << info.pid << ") at " << path; - - instances.push_back(info); - } else { - qCDebug(logPaths) << "Skipped dead instance at" << path; - } - } - - return instances; -} diff --git a/src/core/paths.hpp b/src/core/paths.hpp deleted file mode 100644 index b20d4087..00000000 --- a/src/core/paths.hpp +++ /dev/null @@ -1,51 +0,0 @@ -#pragma once -#include -#include -#include - -#include "instanceinfo.hpp" - -struct InstanceLockInfo { - pid_t pid = -1; - InstanceInfo instance; -}; - -QDataStream& operator<<(QDataStream& stream, const InstanceLockInfo& info); -QDataStream& operator>>(QDataStream& stream, InstanceLockInfo& info); - -class QsPaths { -public: - static QsPaths* instance(); - static void init(QString shellId, QString pathId); - 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); - static QVector collectInstances(const QString& path); - - QDir* cacheDir(); - QDir* baseRunDir(); - QDir* shellRunDir(); - QDir* instanceRunDir(); - void linkRunDir(); - void linkPathDir(); - void createLock(); - -private: - enum class DirState : quint8 { - Unknown = 0, - Ready = 1, - Failed = 2, - }; - - QString shellId; - QString pathId; - QDir mCacheDir; - QDir mBaseRunDir; - QDir mShellRunDir; - QDir mInstanceRunDir; - DirState cacheState = DirState::Unknown; - DirState baseRunState = DirState::Unknown; - DirState shellRunState = DirState::Unknown; - DirState instanceRunState = DirState::Unknown; -}; diff --git a/src/core/platformmenu.cpp b/src/core/platformmenu.cpp deleted file mode 100644 index 427dde08..00000000 --- a/src/core/platformmenu.cpp +++ /dev/null @@ -1,324 +0,0 @@ -#include "platformmenu.hpp" -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "../window/proxywindow.hpp" -#include "../window/windowinterface.hpp" -#include "iconprovider.hpp" -#include "model.hpp" -#include "platformmenu_p.hpp" -#include "popupanchor.hpp" -#include "qsmenu.hpp" - -namespace qs::menu::platform { - -namespace { -QVector> CREATION_HOOKS; // NOLINT -PlatformMenuQMenu* ACTIVE_MENU = nullptr; // NOLINT -} // namespace - -PlatformMenuQMenu::~PlatformMenuQMenu() { - if (this == ACTIVE_MENU) { - ACTIVE_MENU = nullptr; - } -} - -void PlatformMenuQMenu::setVisible(bool visible) { - if (visible) { - for (auto& hook: CREATION_HOOKS) { - hook(this); - } - } else { - if (this == ACTIVE_MENU) { - ACTIVE_MENU = nullptr; - } - } - - this->QMenu::setVisible(visible); -} - -PlatformMenuEntry::PlatformMenuEntry(QsMenuEntry* menu): QObject(menu), menu(menu) { - this->relayout(); - - // clang-format off - QObject::connect(menu, &QsMenuEntry::enabledChanged, this, &PlatformMenuEntry::onEnabledChanged); - QObject::connect(menu, &QsMenuEntry::textChanged, this, &PlatformMenuEntry::onTextChanged); - QObject::connect(menu, &QsMenuEntry::iconChanged, this, &PlatformMenuEntry::onIconChanged); - QObject::connect(menu, &QsMenuEntry::buttonTypeChanged, this, &PlatformMenuEntry::onButtonTypeChanged); - QObject::connect(menu, &QsMenuEntry::checkStateChanged, this, &PlatformMenuEntry::onCheckStateChanged); - QObject::connect(menu, &QsMenuEntry::hasChildrenChanged, this, &PlatformMenuEntry::relayoutParent); - QObject::connect(menu->children(), &UntypedObjectModel::valuesChanged, this, &PlatformMenuEntry::relayout); - // clang-format on -} - -PlatformMenuEntry::~PlatformMenuEntry() { - this->clearChildren(); - delete this->qaction; - delete this->qmenu; -} - -void PlatformMenuEntry::registerCreationHook(std::function hook) { - CREATION_HOOKS.push_back(std::move(hook)); -} - -bool PlatformMenuEntry::display(QObject* parentWindow, int relativeX, int relativeY) { - QWindow* window = nullptr; - - if (qobject_cast(QCoreApplication::instance()) == nullptr) { - qCritical() << "Cannot display PlatformMenuEntry as quickshell was not started in " - "QApplication mode."; - qCritical() << "To use platform menus, add `//@ pragma UseQApplication` to the top of your " - "root QML file and restart quickshell."; - return false; - } else if (this->qmenu == nullptr) { - qCritical() << "Cannot display PlatformMenuEntry as it is not a menu."; - return false; - } else if (parentWindow == nullptr) { - qCritical() << "Cannot display PlatformMenuEntry with null parent window."; - return false; - } else if (auto* proxy = qobject_cast(parentWindow)) { - window = proxy->backingWindow(); - } else if (auto* interface = qobject_cast(parentWindow)) { - window = interface->proxyWindow()->backingWindow(); - } else { - qCritical() << "PlatformMenuEntry.display() must be called with a window."; - return false; - } - - if (window == nullptr) { - qCritical() << "Cannot display PlatformMenuEntry from a parent window that is not visible."; - return false; - } - - if (ACTIVE_MENU && this->qmenu != ACTIVE_MENU) { - ACTIVE_MENU->close(); - } - - ACTIVE_MENU = this->qmenu; - - auto point = window->mapToGlobal(QPoint(relativeX, relativeY)); - - this->qmenu->createWinId(); - this->qmenu->windowHandle()->setTransientParent(window); - - // Skips screen edge repositioning so it can be left to the compositor on wayland. - this->qmenu->targetPosition = point; - this->qmenu->popup(point); - - return true; -} - -bool PlatformMenuEntry::display(PopupAnchor* anchor) { - if (qobject_cast(QCoreApplication::instance()) == nullptr) { - qCritical() << "Cannot display PlatformMenuEntry as quickshell was not started in " - "QApplication mode."; - qCritical() << "To use platform menus, add `//@ pragma UseQApplication` to the top of your " - "root QML file and restart quickshell."; - return false; - } else if (!anchor->backingWindow() || !anchor->backingWindow()->isVisible()) { - qCritical() << "Cannot display PlatformMenuEntry on anchor without visible window."; - return false; - } - - if (ACTIVE_MENU && this->qmenu != ACTIVE_MENU) { - ACTIVE_MENU->close(); - } - - ACTIVE_MENU = this->qmenu; - - this->qmenu->createWinId(); - this->qmenu->windowHandle()->setTransientParent(anchor->backingWindow()); - - // Update the window geometry to the menu's actual dimensions so reposition - // can accurately adjust it if applicable for the current platform. - this->qmenu->windowHandle()->setGeometry({{0, 0}, this->qmenu->sizeHint()}); - - PopupPositioner::instance()->reposition(anchor, this->qmenu->windowHandle(), false); - - // Open the menu at the position determined by the popup positioner. - this->qmenu->popup(this->qmenu->windowHandle()->position()); - - return true; -} - -void PlatformMenuEntry::relayout() { - if (qobject_cast(QCoreApplication::instance()) == nullptr) { - return; - } - - if (this->menu->hasChildren()) { - delete this->qaction; - this->qaction = nullptr; - - if (this->qmenu == nullptr) { - this->qmenu = new PlatformMenuQMenu(); - QObject::connect(this->qmenu, &QMenu::aboutToShow, this, &PlatformMenuEntry::onAboutToShow); - QObject::connect(this->qmenu, &QMenu::aboutToHide, this, &PlatformMenuEntry::onAboutToHide); - } else { - this->clearChildren(); - } - - this->qmenu->setTitle(this->menu->text()); - - auto icon = this->menu->icon(); - if (!icon.isEmpty()) { - this->qmenu->setIcon(getCurrentEngineImageAsIcon(icon)); - } - - const auto& children = this->menu->children()->valueList(); - auto len = children.count(); - for (auto i = 0; i < len; i++) { - auto* child = children.at(i); - - auto* instance = new PlatformMenuEntry(child); - QObject::connect(instance, &QObject::destroyed, this, &PlatformMenuEntry::onChildDestroyed); - - QObject::connect( - instance, - &PlatformMenuEntry::relayoutParent, - this, - &PlatformMenuEntry::relayout - ); - - this->childEntries.push_back(instance); - instance->addToQMenu(this->qmenu); - } - } else if (!this->menu->isSeparator()) { - this->clearChildren(); - delete this->qmenu; - this->qmenu = nullptr; - - if (this->qaction == nullptr) { - this->qaction = new QAction(this); - - QObject::connect( - this->qaction, - &QAction::triggered, - this, - &PlatformMenuEntry::onActionTriggered - ); - } - - this->qaction->setText(this->menu->text()); - - auto icon = this->menu->icon(); - if (!icon.isEmpty()) { - this->qaction->setIcon(getCurrentEngineImageAsIcon(icon)); - } - - this->qaction->setEnabled(this->menu->enabled()); - this->qaction->setCheckable(this->menu->buttonType() != QsMenuButtonType::None); - - if (this->menu->buttonType() == QsMenuButtonType::RadioButton) { - if (!this->qactiongroup) this->qactiongroup = new QActionGroup(this); - this->qaction->setActionGroup(this->qactiongroup); - } - - this->qaction->setChecked(this->menu->checkState() != Qt::Unchecked); - } else { - delete this->qmenu; - delete this->qaction; - this->qmenu = nullptr; - this->qaction = nullptr; - } -} - -void PlatformMenuEntry::onAboutToShow() { this->menu->ref(); } - -void PlatformMenuEntry::onAboutToHide() { - this->menu->unref(); - emit this->closed(); -} - -void PlatformMenuEntry::onActionTriggered() { - auto* action = qobject_cast(this->sender()->parent()); - emit action->menu->triggered(); -} - -void PlatformMenuEntry::onChildDestroyed() { this->childEntries.removeOne(this->sender()); } - -void PlatformMenuEntry::onEnabledChanged() { - if (this->qaction != nullptr) { - this->qaction->setEnabled(this->menu->enabled()); - } -} - -void PlatformMenuEntry::onTextChanged() { - if (this->qmenu != nullptr) { - this->qmenu->setTitle(this->menu->text()); - } else if (this->qaction != nullptr) { - this->qaction->setText(this->menu->text()); - } -} - -void PlatformMenuEntry::onIconChanged() { - if (this->qmenu == nullptr && this->qaction == nullptr) return; - - auto iconName = this->menu->icon(); - QIcon icon; - - if (!iconName.isEmpty()) { - icon = getCurrentEngineImageAsIcon(iconName); - } - - if (this->qmenu != nullptr) { - this->qmenu->setIcon(icon); - } else if (this->qaction != nullptr) { - this->qaction->setIcon(icon); - } -} - -void PlatformMenuEntry::onButtonTypeChanged() { - if (this->qaction != nullptr) { - QActionGroup* group = nullptr; - - if (this->menu->buttonType() == QsMenuButtonType::RadioButton) { - if (!this->qactiongroup) this->qactiongroup = new QActionGroup(this); - group = this->qactiongroup; - } - - this->qaction->setActionGroup(group); - } -} - -void PlatformMenuEntry::onCheckStateChanged() { - if (this->qaction != nullptr) { - this->qaction->setChecked(this->menu->checkState() != Qt::Unchecked); - } -} - -void PlatformMenuEntry::clearChildren() { - for (auto* child: this->childEntries) { - delete child; - } - - this->childEntries.clear(); -} - -void PlatformMenuEntry::addToQMenu(PlatformMenuQMenu* menu) { - if (this->qmenu != nullptr) { - menu->addMenu(this->qmenu); - this->qmenu->containingMenu = menu; - } else if (this->qaction != nullptr) { - menu->addAction(this->qaction); - } else { - menu->addSeparator(); - } -} - -} // namespace qs::menu::platform diff --git a/src/core/platformmenu.hpp b/src/core/platformmenu.hpp deleted file mode 100644 index 5979f90e..00000000 --- a/src/core/platformmenu.hpp +++ /dev/null @@ -1,63 +0,0 @@ -#pragma once - -#include - -#include -#include -#include -#include -#include -#include -#include -#include - -#include "../core/popupanchor.hpp" -#include "qsmenu.hpp" - -namespace qs::menu::platform { - -class PlatformMenuQMenu; - -class PlatformMenuEntry: public QObject { - Q_OBJECT; - -public: - explicit PlatformMenuEntry(QsMenuEntry* menu); - ~PlatformMenuEntry() override; - Q_DISABLE_COPY_MOVE(PlatformMenuEntry); - - bool display(QObject* parentWindow, int relativeX, int relativeY); - bool display(PopupAnchor* anchor); - - static void registerCreationHook(std::function hook); - -signals: - void closed(); - void relayoutParent(); - -public slots: - void relayout(); - -private slots: - void onAboutToShow(); - void onAboutToHide(); - void onActionTriggered(); - void onChildDestroyed(); - void onEnabledChanged(); - void onTextChanged(); - void onIconChanged(); - void onButtonTypeChanged(); - void onCheckStateChanged(); - -private: - void clearChildren(); - void addToQMenu(PlatformMenuQMenu* menu); - - QsMenuEntry* menu; - PlatformMenuQMenu* qmenu = nullptr; - QAction* qaction = nullptr; - QActionGroup* qactiongroup = nullptr; - QVector childEntries; -}; - -} // namespace qs::menu::platform diff --git a/src/core/platformmenu_p.hpp b/src/core/platformmenu_p.hpp deleted file mode 100644 index 9109959d..00000000 --- a/src/core/platformmenu_p.hpp +++ /dev/null @@ -1,19 +0,0 @@ -#pragma once -#include -#include - -namespace qs::menu::platform { - -class PlatformMenuQMenu: public QMenu { -public: - explicit PlatformMenuQMenu() = default; - ~PlatformMenuQMenu() override; - Q_DISABLE_COPY_MOVE(PlatformMenuQMenu); - - void setVisible(bool visible) override; - - PlatformMenuQMenu* containingMenu = nullptr; - QPoint targetPosition; -}; - -} // namespace qs::menu::platform diff --git a/src/core/plugin.cpp b/src/core/plugin.cpp index 0eb9a067..8f1d0e96 100644 --- a/src/core/plugin.cpp +++ b/src/core/plugin.cpp @@ -5,34 +5,37 @@ #include "generation.hpp" -static QVector plugins; // NOLINT +static QVector plugins; // NOLINT -void QsEnginePlugin::registerPlugin(QsEnginePlugin& plugin) { plugins.push_back(&plugin); } +void QuickshellPlugin::registerPlugin(QuickshellPlugin& plugin) { plugins.push_back(&plugin); } -void QsEnginePlugin::initPlugins() { - plugins.removeIf([](QsEnginePlugin* plugin) { return !plugin->applies(); }); +void QuickshellPlugin::initPlugins() { + plugins.erase( + std::remove_if( + plugins.begin(), + plugins.end(), + [](QuickshellPlugin* plugin) { return !plugin->applies(); } + ), + plugins.end() + ); - std::ranges::sort(plugins, [](QsEnginePlugin* a, QsEnginePlugin* b) { - return b->dependencies().contains(a->name()); - }); - - for (QsEnginePlugin* plugin: plugins) { + for (QuickshellPlugin* plugin: plugins) { plugin->init(); } - for (QsEnginePlugin* plugin: plugins) { + for (QuickshellPlugin* plugin: plugins) { plugin->registerTypes(); } } -void QsEnginePlugin::runConstructGeneration(EngineGeneration& generation) { - for (QsEnginePlugin* plugin: plugins) { +void QuickshellPlugin::runConstructGeneration(EngineGeneration& generation) { + for (QuickshellPlugin* plugin: plugins) { plugin->constructGeneration(generation); } } -void QsEnginePlugin::runOnReload() { - for (QsEnginePlugin* plugin: plugins) { +void QuickshellPlugin::runOnReload() { + for (QuickshellPlugin* plugin: plugins) { plugin->onReload(); } } diff --git a/src/core/plugin.hpp b/src/core/plugin.hpp index f0c14dce..38c9ddc2 100644 --- a/src/core/plugin.hpp +++ b/src/core/plugin.hpp @@ -2,28 +2,25 @@ #include #include -#include class EngineGeneration; -class QsEnginePlugin { +class QuickshellPlugin { public: - QsEnginePlugin() = default; - virtual ~QsEnginePlugin() = default; - QsEnginePlugin(QsEnginePlugin&&) = delete; - QsEnginePlugin(const QsEnginePlugin&) = delete; - void operator=(QsEnginePlugin&&) = delete; - void operator=(const QsEnginePlugin&) = delete; + QuickshellPlugin() = default; + virtual ~QuickshellPlugin() = default; + QuickshellPlugin(QuickshellPlugin&&) = delete; + QuickshellPlugin(const QuickshellPlugin&) = delete; + void operator=(QuickshellPlugin&&) = delete; + void operator=(const QuickshellPlugin&) = delete; - virtual QString name() { return QString(); } - virtual QList dependencies() { return {}; } virtual bool applies() { return true; } virtual void init() {} virtual void registerTypes() {} virtual void constructGeneration(EngineGeneration& /*unused*/) {} // NOLINT virtual void onReload() {} - static void registerPlugin(QsEnginePlugin& plugin); + static void registerPlugin(QuickshellPlugin& plugin); static void initPlugins(); static void runConstructGeneration(EngineGeneration& generation); static void runOnReload(); @@ -33,6 +30,6 @@ public: #define QS_REGISTER_PLUGIN(clazz) \ [[gnu::constructor]] void qsInitPlugin() { \ static clazz plugin; \ - QsEnginePlugin::registerPlugin(plugin); \ + QuickshellPlugin::registerPlugin(plugin); \ } // NOLINTEND diff --git a/src/core/popupanchor.cpp b/src/core/popupanchor.cpp deleted file mode 100644 index f72d5c59..00000000 --- a/src/core/popupanchor.cpp +++ /dev/null @@ -1,319 +0,0 @@ -#include "popupanchor.hpp" -#include - -#include -#include -#include -#include -#include -#include - -#include "../window/proxywindow.hpp" -#include "../window/windowinterface.hpp" -#include "types.hpp" - -bool PopupAnchorState::operator==(const PopupAnchorState& other) const { - return this->rect == other.rect && this->edges == other.edges && this->gravity == other.gravity - && this->adjustment == other.adjustment && this->anchorpoint == other.anchorpoint - && this->size == other.size; -} - -bool PopupAnchor::isDirty() const { - return !this->lastState.has_value() || this->state != this->lastState.value(); -} - -void PopupAnchor::markClean() { this->lastState = this->state; } -void PopupAnchor::markDirty() { this->lastState.reset(); } - -QObject* PopupAnchor::window() const { return this->mWindow; } -ProxyWindowBase* PopupAnchor::proxyWindow() const { return this->mProxyWindow; } - -QWindow* PopupAnchor::backingWindow() const { - return this->mProxyWindow ? this->mProxyWindow->backingWindow() : nullptr; -} - -void PopupAnchor::setWindow(QObject* window) { - if (window == this->mWindow) return; - - if (this->mWindow) { - QObject::disconnect(this->mWindow, nullptr, this, nullptr); - QObject::disconnect(this->mProxyWindow, nullptr, this, nullptr); - } - - if (window) { - if (auto* proxy = qobject_cast(window)) { - this->mProxyWindow = proxy; - } else if (auto* interface = qobject_cast(window)) { - this->mProxyWindow = interface->proxyWindow(); - } else { - qWarning() << "Tried to set popup anchor window to" << window - << "which is not a quickshell window."; - goto setnull; - } - - this->mWindow = window; - - QObject::connect(this->mWindow, &QObject::destroyed, this, &PopupAnchor::onWindowDestroyed); - - QObject::connect( - this->mProxyWindow, - &ProxyWindowBase::backerVisibilityChanged, - this, - &PopupAnchor::backingWindowVisibilityChanged - ); - - emit this->windowChanged(); - emit this->backingWindowVisibilityChanged(); - - return; - } - -setnull: - if (this->mWindow) { - this->mWindow = nullptr; - this->mProxyWindow = nullptr; - - emit this->windowChanged(); - emit this->backingWindowVisibilityChanged(); - } -} - -void PopupAnchor::onWindowDestroyed() { - this->mWindow = nullptr; - this->mProxyWindow = nullptr; - emit this->windowChanged(); - emit this->backingWindowVisibilityChanged(); -} - -Box PopupAnchor::rect() const { return this->state.rect; } - -void PopupAnchor::setRect(Box rect) { - if (rect == this->state.rect) return; - if (rect.w <= 0) rect.w = 1; - if (rect.h <= 0) rect.h = 1; - - this->state.rect = rect; - emit this->rectChanged(); -} - -Edges::Flags PopupAnchor::edges() const { return this->state.edges; } - -void PopupAnchor::setEdges(Edges::Flags edges) { - if (edges == this->state.edges) return; - - if (Edges::isOpposing(edges)) { - qWarning() << "Cannot set opposing edges for anchor edges. Tried to set" << edges; - return; - } - - this->state.edges = edges; - emit this->edgesChanged(); -} - -Edges::Flags PopupAnchor::gravity() const { return this->state.gravity; } - -void PopupAnchor::setGravity(Edges::Flags gravity) { - if (gravity == this->state.gravity) return; - - if (Edges::isOpposing(gravity)) { - qWarning() << "Cannot set opposing edges for anchor gravity. Tried to set" << gravity; - return; - } - - this->state.gravity = gravity; - emit this->gravityChanged(); -} - -PopupAdjustment::Flags PopupAnchor::adjustment() const { return this->state.adjustment; } - -void PopupAnchor::setAdjustment(PopupAdjustment::Flags adjustment) { - if (adjustment == this->state.adjustment) return; - this->state.adjustment = adjustment; - emit this->adjustmentChanged(); -} - -void PopupAnchor::updatePlacement(const QPoint& anchorpoint, const QSize& size) { - this->state.anchorpoint = anchorpoint; - this->state.size = size; -} - -static PopupPositioner* POSITIONER = nullptr; // NOLINT - -void PopupPositioner::reposition(PopupAnchor* anchor, QWindow* window, bool onlyIfDirty) { - auto* parentWindow = window->transientParent(); - if (!parentWindow) { - qFatal() << "Cannot reposition popup that does not have a transient parent."; - } - - auto parentGeometry = parentWindow->geometry(); - auto windowGeometry = window->geometry(); - - emit anchor->anchoring(); - anchor->updatePlacement(parentGeometry.topLeft(), windowGeometry.size()); - - if (onlyIfDirty && !anchor->isDirty()) return; - anchor->markClean(); - - auto adjustment = anchor->adjustment(); - auto screenGeometry = parentWindow->screen()->geometry(); - auto anchorRectGeometry = anchor->rect().qrect().translated(parentGeometry.topLeft()); - - auto anchorEdges = anchor->edges(); - auto anchorGravity = anchor->gravity(); - - auto width = windowGeometry.width(); - auto height = windowGeometry.height(); - - auto anchorX = anchorEdges.testFlag(Edges::Left) ? anchorRectGeometry.left() - : anchorEdges.testFlag(Edges::Right) ? anchorRectGeometry.right() - : anchorRectGeometry.center().x(); - - auto anchorY = anchorEdges.testFlag(Edges::Top) ? anchorRectGeometry.top() - : anchorEdges.testFlag(Edges::Bottom) ? anchorRectGeometry.bottom() - : anchorRectGeometry.center().y(); - - auto calcEffectiveX = [&](Edges::Flags anchorGravity, int anchorX) { - auto ex = anchorGravity.testFlag(Edges::Left) ? anchorX - windowGeometry.width() - : anchorGravity.testFlag(Edges::Right) ? anchorX - 1 - : anchorX - windowGeometry.width() / 2; - - return ex + 1; - }; - - auto calcEffectiveY = [&](Edges::Flags anchorGravity, int anchorY) { - auto ey = anchorGravity.testFlag(Edges::Top) ? anchorY - windowGeometry.height() - : anchorGravity.testFlag(Edges::Bottom) ? anchorY - 1 - : anchorY - windowGeometry.height() / 2; - - return ey + 1; - }; - - auto calcRemainingWidth = [&](int effectiveX) { - auto width = windowGeometry.width(); - if (effectiveX < screenGeometry.left()) { - auto diff = screenGeometry.left() - effectiveX; - effectiveX = screenGeometry.left(); - width -= diff; - } - - auto effectiveX2 = effectiveX + width; - if (effectiveX2 > screenGeometry.right()) { - width -= effectiveX2 - screenGeometry.right() - 1; - } - - return QPair(effectiveX, width); - }; - - auto calcRemainingHeight = [&](int effectiveY) { - auto height = windowGeometry.height(); - if (effectiveY < screenGeometry.left()) { - auto diff = screenGeometry.top() - effectiveY; - effectiveY = screenGeometry.top(); - height -= diff; - } - - auto effectiveY2 = effectiveY + height; - if (effectiveY2 > screenGeometry.bottom()) { - height -= effectiveY2 - screenGeometry.bottom() - 1; - } - - return QPair(effectiveY, height); - }; - - auto effectiveX = calcEffectiveX(anchorGravity, anchorX); - auto effectiveY = calcEffectiveY(anchorGravity, anchorY); - - if (adjustment.testFlag(PopupAdjustment::FlipX)) { - const bool flip = (anchorGravity.testFlag(Edges::Left) && effectiveX < screenGeometry.left()) - || (anchorGravity.testFlag(Edges::Right) - && effectiveX + windowGeometry.width() > screenGeometry.right()); - - if (flip) { - auto newAnchorGravity = anchorGravity ^ (Edges::Left | Edges::Right); - - auto newAnchorX = anchorEdges.testFlags(Edges::Left) ? anchorRectGeometry.right() - : anchorEdges.testFlags(Edges::Right) ? anchorRectGeometry.left() - : anchorX; - - auto newEffectiveX = calcEffectiveX(newAnchorGravity, newAnchorX); - - // TODO IN HL: pick constraint monitor based on anchor rect position in window - - // if the available width when flipped is more than the available width without flipping then flip - if (calcRemainingWidth(newEffectiveX).second > calcRemainingWidth(effectiveX).second) { - anchorGravity = newAnchorGravity; - anchorX = newAnchorX; - effectiveX = newEffectiveX; - } - } - } - - if (adjustment.testFlag(PopupAdjustment::FlipY)) { - const bool flip = (anchorGravity.testFlag(Edges::Top) && effectiveY < screenGeometry.top()) - || (anchorGravity.testFlag(Edges::Bottom) - && effectiveY + windowGeometry.height() > screenGeometry.bottom()); - - if (flip) { - auto newAnchorGravity = anchorGravity ^ (Edges::Top | Edges::Bottom); - - auto newAnchorY = anchorEdges.testFlags(Edges::Top) ? anchorRectGeometry.bottom() - : anchorEdges.testFlags(Edges::Bottom) ? anchorRectGeometry.top() - : anchorY; - - auto newEffectiveY = calcEffectiveY(newAnchorGravity, newAnchorY); - - // if the available width when flipped is more than the available width without flipping then flip - if (calcRemainingHeight(newEffectiveY).second > calcRemainingHeight(effectiveY).second) { - anchorGravity = newAnchorGravity; - anchorY = newAnchorY; - effectiveY = newEffectiveY; - } - } - } - - // Slide order is important for the case where the window is too large to fit on screen. - if (adjustment.testFlag(PopupAdjustment::SlideX)) { - if (effectiveX + windowGeometry.width() > screenGeometry.right()) { - effectiveX = screenGeometry.right() - windowGeometry.width() + 1; - } - - effectiveX = std::max(effectiveX, screenGeometry.left()); - } - - if (adjustment.testFlag(PopupAdjustment::SlideY)) { - if (effectiveY + windowGeometry.height() > screenGeometry.bottom()) { - effectiveY = screenGeometry.bottom() - windowGeometry.height() + 1; - } - - effectiveY = std::max(effectiveY, screenGeometry.top()); - } - - if (adjustment.testFlag(PopupAdjustment::ResizeX)) { - auto [newX, newWidth] = calcRemainingWidth(effectiveX); - effectiveX = newX; - width = newWidth; - } - - if (adjustment.testFlag(PopupAdjustment::ResizeY)) { - auto [newY, newHeight] = calcRemainingHeight(effectiveY); - effectiveY = newY; - height = newHeight; - } - - window->setGeometry({effectiveX, effectiveY, width, height}); -} - -bool PopupPositioner::shouldRepositionOnMove() const { return true; } - -PopupPositioner* PopupPositioner::instance() { - if (POSITIONER == nullptr) { - POSITIONER = new PopupPositioner(); - } - - return POSITIONER; -} - -void PopupPositioner::setInstance(PopupPositioner* instance) { - delete POSITIONER; - POSITIONER = instance; -} diff --git a/src/core/popupanchor.hpp b/src/core/popupanchor.hpp deleted file mode 100644 index 90ba697f..00000000 --- a/src/core/popupanchor.hpp +++ /dev/null @@ -1,174 +0,0 @@ -#pragma once - -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "../window/proxywindow.hpp" -#include "doc.hpp" -#include "types.hpp" - -///! Adjustment strategy for popups that do not fit on screen. -/// Adjustment strategy for popups. See @@PopupAnchor.adjustment. -/// -/// Adjustment flags can be combined with the `|` operator. -/// -/// `Flip` will be applied first, then `Slide`, then `Resize`. -namespace PopupAdjustment { // NOLINT -Q_NAMESPACE; -QML_ELEMENT; - -enum Enum : quint8 { - None = 0, - /// If the X axis is constrained, the popup will slide along the X axis until it fits onscreen. - SlideX = 1, - /// If the Y axis is constrained, the popup will slide along the Y axis until it fits onscreen. - SlideY = 2, - /// Alias for `SlideX | SlideY`. - Slide = SlideX | SlideY, - /// If the X axis is constrained, the popup will invert its horizontal gravity if any. - FlipX = 4, - /// If the Y axis is constrained, the popup will invert its vertical gravity if any. - FlipY = 8, - /// Alias for `FlipX | FlipY`. - Flip = FlipX | FlipY, - /// If the X axis is constrained, the width of the popup will be reduced to fit on screen. - ResizeX = 16, - /// If the Y axis is constrained, the height of the popup will be reduced to fit on screen. - ResizeY = 32, - /// Alias for `ResizeX | ResizeY` - Resize = ResizeX | ResizeY, - /// Alias for `Flip | Slide | Resize`. - All = Slide | Flip | Resize, -}; -Q_ENUM_NS(Enum); -Q_DECLARE_FLAGS(Flags, Enum); - -} // namespace PopupAdjustment - -Q_DECLARE_OPERATORS_FOR_FLAGS(PopupAdjustment::Flags); - -struct PopupAnchorState { - bool operator==(const PopupAnchorState& other) const; - - Box rect = {0, 0, 1, 1}; - Edges::Flags edges = Edges::Top | Edges::Left; - Edges::Flags gravity = Edges::Bottom | Edges::Right; - PopupAdjustment::Flags adjustment = PopupAdjustment::Slide; - QPoint anchorpoint; - QSize size; -}; - -///! Anchorpoint or positioner for popup windows. -class PopupAnchor: public QObject { - Q_OBJECT; - // clang-format off - /// The window to anchor / attach the popup to. - Q_PROPERTY(QObject* window READ window WRITE setWindow NOTIFY windowChanged); - /// The anchorpoints the popup will attach to. Which anchors will be used is - /// determined by the @@edges, @@gravity, and @@adjustment. - /// - /// If you leave @@edges, @@gravity and @@adjustment at their default values, - /// setting more than `x` and `y` does not matter. The anchor rect cannot - /// be smaller than 1x1 pixels. - /// - /// > [!INFO] To position a popup relative to an item inside a window, - /// > you can use [coordinate mapping functions] (note the warning below). - /// - /// > [!WARNING] Using [coordinate mapping functions] in a binding to - /// > this property will position the anchor incorrectly. - /// > If you want to use them, do so in @@anchoring(s), or use - /// > @@TransformWatcher if you need real-time updates to mapped coordinates. - /// - /// [coordinate mapping functions]: https://doc.qt.io/qt-6/qml-qtquick-item.html#mapFromItem-method - Q_PROPERTY(Box rect READ rect WRITE setRect NOTIFY rectChanged); - /// The point on the anchor rectangle the popup should anchor to. - /// Opposing edges suchs as `Edges.Left | Edges.Right` are not allowed. - /// - /// Defaults to `Edges.Top | Edges.Left`. - Q_PROPERTY(Edges::Flags edges READ edges WRITE setEdges NOTIFY edgesChanged); - /// The direction the popup should expand towards, relative to the anchorpoint. - /// Opposing edges suchs as `Edges.Left | Edges.Right` are not allowed. - /// - /// Defaults to `Edges.Bottom | Edges.Right`. - Q_PROPERTY(Edges::Flags gravity READ gravity WRITE setGravity NOTIFY gravityChanged); - /// The strategy used to adjust the popup's position if it would otherwise not fit on screen, - /// based on the anchor @@rect, preferred @@edges, and @@gravity. - /// - /// See the documentation for @@PopupAdjustment for details. - Q_PROPERTY(PopupAdjustment::Flags adjustment READ adjustment WRITE setAdjustment NOTIFY adjustmentChanged); - // clang-format on - QML_ELEMENT; - QML_UNCREATABLE(""); - -public: - explicit PopupAnchor(QObject* parent): QObject(parent) {} - - [[nodiscard]] bool isDirty() const; - void markClean(); - void markDirty(); - - [[nodiscard]] QObject* window() const; - [[nodiscard]] ProxyWindowBase* proxyWindow() const; - [[nodiscard]] QWindow* backingWindow() const; - void setWindow(QObject* window); - - [[nodiscard]] Box rect() const; - void setRect(Box rect); - - [[nodiscard]] Edges::Flags edges() const; - void setEdges(Edges::Flags edges); - - [[nodiscard]] Edges::Flags gravity() const; - void setGravity(Edges::Flags gravity); - - [[nodiscard]] PopupAdjustment::Flags adjustment() const; - void setAdjustment(PopupAdjustment::Flags adjustment); - - void updatePlacement(const QPoint& anchorpoint, const QSize& size); - -signals: - /// Emitted when this anchor is about to be used. Mostly useful for modifying - /// the anchor @@rect using [coordinate mapping functions], which are not reactive. - /// - /// [coordinate mapping functions]: https://doc.qt.io/qt-6/qml-qtquick-item.html#mapFromItem-method - void anchoring(); - - void windowChanged(); - QSDOC_HIDE void backingWindowVisibilityChanged(); - void rectChanged(); - void edgesChanged(); - void gravityChanged(); - void adjustmentChanged(); - -private slots: - void onWindowDestroyed(); - -private: - QObject* mWindow = nullptr; - ProxyWindowBase* mProxyWindow = nullptr; - PopupAnchorState state; - std::optional lastState; -}; - -class PopupPositioner { -public: - explicit PopupPositioner() = default; - virtual ~PopupPositioner() = default; - Q_DISABLE_COPY_MOVE(PopupPositioner); - - virtual void reposition(PopupAnchor* anchor, QWindow* window, bool onlyIfDirty = true); - [[nodiscard]] virtual bool shouldRepositionOnMove() const; - - static PopupPositioner* instance(); - static void setInstance(PopupPositioner* instance); -}; diff --git a/src/core/popupwindow.cpp b/src/core/popupwindow.cpp new file mode 100644 index 00000000..547bbe36 --- /dev/null +++ b/src/core/popupwindow.cpp @@ -0,0 +1,161 @@ +#include "popupwindow.hpp" + +#include +#include +#include +#include +#include +#include + +#include "proxywindow.hpp" +#include "qmlscreen.hpp" +#include "windowinterface.hpp" + +ProxyPopupWindow::ProxyPopupWindow(QObject* parent): ProxyWindowBase(parent) { + this->mVisible = false; +} + +void ProxyPopupWindow::completeWindow() { + this->ProxyWindowBase::completeWindow(); + + this->window->setFlag(Qt::ToolTip); + this->updateTransientParent(); +} + +void ProxyPopupWindow::postCompleteWindow() { this->ProxyWindowBase::setVisible(this->mVisible); } + +bool ProxyPopupWindow::deleteOnInvisible() const { + // Currently crashes in normal mode, do not have the time to debug it now. + return true; +} + +qint32 ProxyPopupWindow::x() const { + // QTBUG-121550 + auto basepos = this->mParentProxyWindow == nullptr ? 0 : this->mParentProxyWindow->x(); + return basepos + this->mRelativeX; +} + +void ProxyPopupWindow::setParentWindow(QObject* parent) { + if (parent == this->mParentWindow) return; + + if (this->mParentWindow != nullptr) { + QObject::disconnect(this->mParentWindow, nullptr, this, nullptr); + QObject::disconnect(this->mParentProxyWindow, nullptr, this, nullptr); + } + + if (parent == nullptr) { + this->mParentWindow = nullptr; + this->mParentProxyWindow = nullptr; + } else { + if (auto* proxy = qobject_cast(parent)) { + this->mParentProxyWindow = proxy; + } else if (auto* interface = qobject_cast(parent)) { + this->mParentProxyWindow = interface->proxyWindow(); + } else { + qWarning() << "Tried to set popup parent window to something that is not a quickshell window:" + << parent; + this->mParentWindow = nullptr; + this->mParentProxyWindow = nullptr; + this->updateTransientParent(); + return; + } + + this->mParentWindow = parent; + + // clang-format off + QObject::connect(this->mParentWindow, &QObject::destroyed, this, &ProxyPopupWindow::onParentDestroyed); + + QObject::connect(this->mParentProxyWindow, &ProxyWindowBase::xChanged, this, &ProxyPopupWindow::updateX); + QObject::connect(this->mParentProxyWindow, &ProxyWindowBase::yChanged, this, &ProxyPopupWindow::updateY); + QObject::connect(this->mParentProxyWindow, &ProxyWindowBase::backerVisibilityChanged, this, &ProxyPopupWindow::onParentUpdated); + // clang-format on + } + + this->updateTransientParent(); +} + +QObject* ProxyPopupWindow::parentWindow() const { return this->mParentWindow; } + +void ProxyPopupWindow::updateTransientParent() { + this->updateX(); + this->updateY(); + + if (this->window != nullptr) { + this->window->setTransientParent( + this->mParentProxyWindow == nullptr ? nullptr : this->mParentProxyWindow->backingWindow() + ); + } + + this->updateVisible(); +} + +void ProxyPopupWindow::onParentUpdated() { this->updateTransientParent(); } + +void ProxyPopupWindow::onParentDestroyed() { + this->mParentWindow = nullptr; + this->mParentProxyWindow = nullptr; + this->updateVisible(); + emit this->parentWindowChanged(); +} + +void ProxyPopupWindow::setScreen(QuickshellScreenInfo* /*unused*/) { + qWarning() << "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->mParentWindow != nullptr + && this->mParentProxyWindow->isVisibleDirect(); + + if (target && this->window != nullptr && !this->window->isVisible()) { + this->updateX(); // QTBUG-121550 + } + + this->ProxyWindowBase::setVisible(target); +} + +void ProxyPopupWindow::setRelativeX(qint32 x) { + if (x == this->mRelativeX) return; + this->mRelativeX = x; + this->updateX(); +} + +qint32 ProxyPopupWindow::relativeX() const { return this->mRelativeX; } + +void ProxyPopupWindow::setRelativeY(qint32 y) { + if (y == this->mRelativeY) return; + this->mRelativeY = y; + this->updateY(); +} + +qint32 ProxyPopupWindow::relativeY() const { return this->mRelativeY; } + +void ProxyPopupWindow::updateX() { + if (this->mParentWindow == nullptr || this->window == nullptr) return; + + auto target = this->x() - 1; // QTBUG-121550 + + auto reshow = this->isVisibleDirect() && (this->window->x() != target && this->x() != target); + if (reshow) this->setVisibleDirect(false); + if (this->window != nullptr) this->window->setX(target); + if (reshow && this->wantsVisible) this->setVisibleDirect(true); +} + +void ProxyPopupWindow::updateY() { + if (this->mParentWindow == nullptr || this->window == nullptr) return; + + auto target = this->mParentProxyWindow->y() + this->relativeY(); + + auto reshow = this->isVisibleDirect() && this->window->y() != target; + if (reshow) { + this->setVisibleDirect(false); + this->updateX(); // QTBUG-121550 + } + if (this->window != nullptr) this->window->setY(target); + if (reshow && this->wantsVisible) this->setVisibleDirect(true); +} diff --git a/src/window/popupwindow.hpp b/src/core/popupwindow.hpp similarity index 69% rename from src/window/popupwindow.hpp rename to src/core/popupwindow.hpp index a1e535f7..7815d400 100644 --- a/src/window/popupwindow.hpp +++ b/src/core/popupwindow.hpp @@ -6,10 +6,9 @@ #include #include -#include "../core/doc.hpp" -#include "../core/popupanchor.hpp" -#include "../core/qmlscreen.hpp" +#include "doc.hpp" #include "proxywindow.hpp" +#include "qmlscreen.hpp" #include "windowinterface.hpp" ///! Popup window. @@ -30,9 +29,9 @@ /// } /// /// PopupWindow { -/// anchor.window: toplevel -/// anchor.rect.x: parentWindow.width / 2 - width / 2 -/// anchor.rect.y: parentWindow.height +/// parentWindow: toplevel +/// relativeX: parentWindow.width / 2 - width / 2 +/// relativeY: parentWindow.height /// width: 500 /// height: 500 /// visible: true @@ -43,37 +42,15 @@ class ProxyPopupWindow: public ProxyWindowBase { QSDOC_BASECLASS(WindowInterface); Q_OBJECT; // clang-format off - /// > [!ERROR] Deprecated in favor of `anchor.window`. - /// /// The parent window of this popup. /// /// Changing this property reparents the popup. Q_PROPERTY(QObject* parentWindow READ parentWindow WRITE setParentWindow NOTIFY parentWindowChanged); - /// > [!ERROR] Deprecated in favor of `anchor.rect.x`. - /// /// The X position of the popup relative to the parent window. Q_PROPERTY(qint32 relativeX READ relativeX WRITE setRelativeX NOTIFY relativeXChanged); - /// > [!ERROR] Deprecated in favor of `anchor.rect.y`. - /// /// The Y position of the popup relative to the parent window. Q_PROPERTY(qint32 relativeY READ relativeY WRITE setRelativeY NOTIFY relativeYChanged); - /// The popup's anchor / positioner relative to another window. The popup will not be - /// shown until it has a valid anchor relative to a window and @@visible is true. - /// - /// You can set properties of the anchor like so: - /// ```qml - /// PopupWindow { - /// anchor.window: parentwindow - /// // or - /// anchor { - /// window: parentwindow - /// } - /// } - /// ``` - Q_PROPERTY(PopupAnchor* anchor READ anchor CONSTANT); /// If the window is shown or hidden. Defaults to false. - /// - /// 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); /// The screen that the window currently occupies. /// @@ -87,10 +64,13 @@ public: void completeWindow() override; void postCompleteWindow() override; + [[nodiscard]] bool deleteOnInvisible() const override; void setScreen(QuickshellScreenInfo* screen) override; void setVisible(bool visible) override; + [[nodiscard]] qint32 x() const override; + [[nodiscard]] QObject* parentWindow() const; void setParentWindow(QObject* parent); @@ -100,23 +80,25 @@ public: [[nodiscard]] qint32 relativeY() const; void setRelativeY(qint32 y); - [[nodiscard]] PopupAnchor* anchor(); - signals: void parentWindowChanged(); void relativeXChanged(); void relativeYChanged(); private slots: - void onVisibleChanged(); void onParentUpdated(); - void reposition(); + void onParentDestroyed(); + void updateX(); + void updateY(); private: QQuickWindow* parentBackingWindow(); void updateTransientParent(); void updateVisible(); - PopupAnchor mAnchor {this}; + QObject* mParentWindow = nullptr; + ProxyWindowBase* mParentProxyWindow = nullptr; + qint32 mRelativeX = 0; + qint32 mRelativeY = 0; bool wantsVisible = false; }; diff --git a/src/window/proxywindow.cpp b/src/core/proxywindow.cpp similarity index 53% rename from src/window/proxywindow.cpp rename to src/core/proxywindow.cpp index 8ce80c20..e2a80a54 100644 --- a/src/window/proxywindow.cpp +++ b/src/core/proxywindow.cpp @@ -1,45 +1,35 @@ #include "proxywindow.hpp" -#include -#include -#include -#include #include #include #include #include -#include #include #include #include #include -#include -#include #include #include -#include #include -#include "../core/generation.hpp" -#include "../core/qmlglobal.hpp" -#include "../core/qmlscreen.hpp" -#include "../core/region.hpp" -#include "../core/reload.hpp" -#include "../debug/lint.hpp" +#include "generation.hpp" +#include "qmlglobal.hpp" +#include "qmlscreen.hpp" +#include "region.hpp" +#include "reload.hpp" #include "windowinterface.hpp" ProxyWindowBase::ProxyWindowBase(QObject* parent) : Reloadable(parent) - , mContentItem(new ProxyWindowContentItem()) { + , mContentItem(new QQuickItem()) { QQmlEngine::setObjectOwnership(this->mContentItem, QQmlEngine::CppOwnership); this->mContentItem->setParent(this); // clang-format off - QObject::connect(this->mContentItem, &ProxyWindowContentItem::polished, this, &ProxyWindowBase::onPolished); - QObject::connect(this, &ProxyWindowBase::widthChanged, this, &ProxyWindowBase::onWidthChanged); QObject::connect(this, &ProxyWindowBase::heightChanged, this, &ProxyWindowBase::onHeightChanged); + QObject::connect(this, &ProxyWindowBase::maskChanged, this, &ProxyWindowBase::onMaskChanged); QObject::connect(this, &ProxyWindowBase::widthChanged, this, &ProxyWindowBase::onMaskChanged); QObject::connect(this, &ProxyWindowBase::heightChanged, this, &ProxyWindowBase::onMaskChanged); @@ -51,12 +41,12 @@ ProxyWindowBase::ProxyWindowBase(QObject* parent) // clang-format on } -ProxyWindowBase::~ProxyWindowBase() { this->deleteWindow(true); } +ProxyWindowBase::~ProxyWindowBase() { this->deleteWindow(); } void ProxyWindowBase::onReload(QObject* oldInstance) { this->window = this->retrieveWindow(oldInstance); auto wasVisible = this->window != nullptr && this->window->isVisible(); - this->ensureQWindow(); + if (this->window == nullptr) this->window = new QQuickWindow(); // 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 @@ -81,73 +71,25 @@ void ProxyWindowBase::onReload(QObject* oldInstance) { emit this->windowConnected(); this->postCompleteWindow(); - if (wasVisible && this->isVisibleDirect()) { - emit this->backerVisibilityChanged(); - this->runLints(); - } + if (wasVisible && this->isVisibleDirect()) emit this->backerVisibilityChanged(); } void ProxyWindowBase::postCompleteWindow() { this->setVisible(this->mVisible); } -ProxiedWindow* ProxyWindowBase::createQQuickWindow() { return new ProxiedWindow(this); } - -void ProxyWindowBase::ensureQWindow() { - auto format = QSurfaceFormat::defaultFormat(); - - { - // match QtQuick's default format, including env var controls - static const auto useDepth = qEnvironmentVariableIsEmpty("QSG_NO_DEPTH_BUFFER"); - static const auto useStencil = qEnvironmentVariableIsEmpty("QSG_NO_STENCIL_BUFFER"); - static const auto enableDebug = qEnvironmentVariableIsSet("QSG_OPENGL_DEBUG"); - static const auto disableVSync = qEnvironmentVariableIsSet("QSG_NO_VSYNC"); - - if (useDepth && format.depthBufferSize() == -1) format.setDepthBufferSize(24); - else if (!useDepth) format.setDepthBufferSize(0); - - if (useStencil && format.stencilBufferSize() == -1) format.setStencilBufferSize(8); - else if (!useStencil) format.setStencilBufferSize(0); - - auto opaque = this->qsSurfaceFormat.opaqueModified ? this->qsSurfaceFormat.opaque - : this->mColor.alpha() >= 255; - - if (opaque) format.setAlphaBufferSize(0); - else format.setAlphaBufferSize(8); - - if (enableDebug) format.setOption(QSurfaceFormat::DebugContext); - if (disableVSync) format.setSwapInterval(0); - - format.setSwapBehavior(QSurfaceFormat::DoubleBuffer); - format.setRedBufferSize(8); - format.setGreenBufferSize(8); - format.setBlueBufferSize(8); - } - - this->mSurfaceFormat = format; - - auto useOldWindow = this->window != nullptr; - - if (useOldWindow) { - if (this->window->requestedFormat() != format) { - useOldWindow = false; - } - } - - if (useOldWindow) return; - delete this->window; - this->window = this->createQQuickWindow(); - this->window->setFormat(format); -} +QQuickWindow* ProxyWindowBase::createQQuickWindow() { return new QQuickWindow(); } void ProxyWindowBase::createWindow() { - this->ensureQWindow(); + if (this->window != nullptr) return; + this->window = this->createQQuickWindow(); + this->connectWindow(); this->completeWindow(); emit this->windowConnected(); } -void ProxyWindowBase::deleteWindow(bool keepItemOwnership) { +void ProxyWindowBase::deleteWindow() { if (this->window != nullptr) emit this->windowDestroyed(); - if (auto* window = this->disownWindow(keepItemOwnership)) { + if (auto* window = this->disownWindow()) { if (auto* generation = EngineGeneration::findObjectGeneration(this)) { generation->deregisterIncubationController(window->incubationController()); } @@ -156,21 +98,19 @@ void ProxyWindowBase::deleteWindow(bool keepItemOwnership) { } } -ProxiedWindow* ProxyWindowBase::disownWindow(bool keepItemOwnership) { +QQuickWindow* ProxyWindowBase::disownWindow() { if (this->window == nullptr) return nullptr; QObject::disconnect(this->window, nullptr, this, nullptr); - if (!keepItemOwnership) { - this->mContentItem->setParentItem(nullptr); - } + this->mContentItem->setParentItem(nullptr); auto* window = this->window; this->window = nullptr; return window; } -ProxiedWindow* ProxyWindowBase::retrieveWindow(QObject* oldInstance) { +QQuickWindow* ProxyWindowBase::retrieveWindow(QObject* oldInstance) { auto* old = qobject_cast(oldInstance); return old == nullptr ? nullptr : old->disownWindow(); } @@ -182,8 +122,6 @@ void ProxyWindowBase::connectWindow() { generation->registerIncubationController(this->window->incubationController()); } - this->window->setProxy(this); - // clang-format off QObject::connect(this->window, &QWindow::visibilityChanged, this, &ProxyWindowBase::visibleChanged); QObject::connect(this->window, &QWindow::xChanged, this, &ProxyWindowBase::xChanged); @@ -192,8 +130,6 @@ void ProxyWindowBase::connectWindow() { 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, &ProxiedWindow::exposed, this, &ProxyWindowBase::runLints); - QObject::connect(this->window, &ProxiedWindow::devicePixelRatioChanged, this, &ProxyWindowBase::devicePixelRatioChanged); // clang-format on } @@ -208,12 +144,9 @@ void ProxyWindowBase::completeWindow() { this->setColor(this->mColor); this->updateMask(); - // notify initial / post-connection geometry + // notify initial x and y positions emit this->xChanged(); emit this->yChanged(); - emit this->widthChanged(); - emit this->heightChanged(); - emit this->devicePixelRatioChanged(); this->mContentItem->setParentItem(this->window->contentItem()); this->mContentItem->setWidth(this->width()); @@ -223,7 +156,16 @@ void ProxyWindowBase::completeWindow() { emit this->screenChanged(); } -bool ProxyWindowBase::deleteOnInvisible() const { return false; } +bool ProxyWindowBase::deleteOnInvisible() const { +#ifdef NVIDIA_COMPAT + // Nvidia drivers and Qt do not play nice when hiding and showing a window + // so for nvidia compatibility we can never reuse windows if they have been + // hidden. + return true; +#else + return false; +#endif +} QQuickWindow* ProxyWindowBase::backingWindow() const { return this->window; } QQuickItem* ProxyWindowBase::contentItem() const { return this->mContentItem; } @@ -249,7 +191,6 @@ void ProxyWindowBase::setVisibleDirect(bool visible) { if (visible) { this->createWindow(); - this->polishItems(); this->window->setVisible(true); emit this->backerVisibilityChanged(); } else { @@ -260,35 +201,11 @@ void ProxyWindowBase::setVisibleDirect(bool visible) { } } } else if (this->window != nullptr) { - if (visible) this->polishItems(); this->window->setVisible(visible); emit this->backerVisibilityChanged(); } } -void ProxyWindowBase::schedulePolish() { - if (this->isVisibleDirect()) { - this->mContentItem->polish(); - } -} - -void ProxyWindowBase::polishItems() { - // Due to QTBUG-126704, layouts in invisible windows don't update their dimensions. - // Usually this isn't an issue, but it is when the size of a window is based on the size - // of its content, and that content is in a layout. - // - // This hack manually polishes the item tree right before showing the window so it will - // always be created with the correct size. - QQuickWindowPrivate::get(this->window)->polishItems(); -} - -void ProxyWindowBase::runLints() { - if (!this->ranLints) { - qs::debug::lintItemTree(this->mContentItem); - this->ranLints = true; - } -} - qint32 ProxyWindowBase::x() const { if (this->window == nullptr) return 0; else return this->window->x(); @@ -324,61 +241,52 @@ void ProxyWindowBase::setHeight(qint32 height) { } void ProxyWindowBase::setScreen(QuickshellScreenInfo* screen) { - auto* qscreen = screen == nullptr ? nullptr : screen->screen; - auto newMScreen = this->mScreen != qscreen; - - if (this->mScreen && newMScreen) { + if (this->mScreen != nullptr) { QObject::disconnect(this->mScreen, nullptr, this, nullptr); } - if (this->qscreen() != qscreen) { - this->mScreen = qscreen; - if (this->window == nullptr) { - emit this->screenChanged(); - } else if (qscreen) { - auto reshow = this->isVisibleDirect(); - if (reshow) this->setVisibleDirect(false); - if (this->window != nullptr) this->window->setScreen(qscreen); - if (reshow) this->setVisibleDirect(true); - } + auto* qscreen = screen == nullptr ? nullptr : screen->screen; + if (qscreen == this->mScreen) return; + + if (qscreen != nullptr) { + QObject::connect(qscreen, &QObject::destroyed, this, &ProxyWindowBase::onScreenDestroyed); } - if (qscreen && newMScreen) { - QObject::connect(this->mScreen, &QObject::destroyed, this, &ProxyWindowBase::onScreenDestroyed); + if (this->window == nullptr) { + this->mScreen = qscreen; + emit this->screenChanged(); + } else { + auto reshow = this->isVisibleDirect(); + if (reshow) this->setVisibleDirect(false); + if (this->window != nullptr) this->window->setScreen(qscreen); + if (reshow) this->setVisibleDirect(true); } } void ProxyWindowBase::onScreenDestroyed() { this->mScreen = nullptr; } -QScreen* ProxyWindowBase::qscreen() const { - if (this->window) return this->window->screen(); - if (this->mScreen) return this->mScreen; - return QGuiApplication::primaryScreen(); -} - QuickshellScreenInfo* ProxyWindowBase::screen() const { - return QuickshellTracked::instance()->screenInfo(this->qscreen()); -} - -QColor ProxyWindowBase::color() const { return this->mColor; } - -void ProxyWindowBase::setColor(QColor color) { - this->mColor = color; + QScreen* qscreen = nullptr; if (this->window == nullptr) { - if (color != this->mColor) emit this->colorChanged(); + if (this->mScreen != nullptr) qscreen = this->mScreen; } else { - auto premultiplied = QColor::fromRgbF( - color.redF() * color.alphaF(), - color.greenF() * color.alphaF(), - color.blueF() * color.alphaF(), - color.alphaF() - ); - - this->window->setColor(premultiplied); - // setColor also modifies the alpha buffer size of the surface format - this->window->setFormat(this->mSurfaceFormat); + qscreen = this->window->screen(); } + + return QuickshellTracked::instance()->screenInfo(qscreen); +} + +QColor ProxyWindowBase::color() const { + if (this->window == nullptr) return this->mColor; + else return this->window->color(); +} + +void ProxyWindowBase::setColor(QColor color) { + if (this->window == nullptr) { + this->mColor = color; + emit this->colorChanged(); + } else this->window->setColor(color); } PendingRegion* ProxyWindowBase::mask() const { return this->mMask; } @@ -393,44 +301,37 @@ void ProxyWindowBase::setMask(PendingRegion* mask) { this->mMask = mask; if (mask != nullptr) { + mask->setParent(this); QObject::connect(mask, &QObject::destroyed, this, &ProxyWindowBase::onMaskDestroyed); - QObject::connect(mask, &PendingRegion::changed, this, &ProxyWindowBase::onMaskChanged); + QObject::connect(mask, &PendingRegion::changed, this, &ProxyWindowBase::maskChanged); } - this->onMaskChanged(); emit this->maskChanged(); } -void ProxyWindowBase::setSurfaceFormat(QsSurfaceFormat format) { - if (format == this->qsSurfaceFormat) return; - if (this->window != nullptr) { - qmlWarning(this) << "Cannot set window surface format."; - return; - } - - this->qsSurfaceFormat = format; - emit this->surfaceFormatChanged(); -} - -qreal ProxyWindowBase::devicePixelRatio() const { - if (this->window != nullptr) return this->window->devicePixelRatio(); - if (this->mScreen != nullptr) return this->mScreen->devicePixelRatio(); - return 1.0; -} - void ProxyWindowBase::onMaskChanged() { if (this->window != nullptr) this->updateMask(); } void ProxyWindowBase::onMaskDestroyed() { this->mMask = nullptr; - this->onMaskChanged(); emit this->maskChanged(); } void ProxyWindowBase::updateMask() { - this->pendingPolish.inputMask = true; - this->schedulePolish(); + QRegion mask; + if (this->mMask != nullptr) { + // if left as the default, dont combine it with the whole window area, leave it as is. + if (this->mMask->mIntersection == Intersection::Combine) { + mask = this->mMask->build(); + } else { + auto windowRegion = QRegion(QRect(0, 0, this->width(), this->height())); + mask = this->mMask->applyTo(windowRegion); + } + } + + this->window->setFlag(Qt::WindowTransparentForInput, this->mMask != nullptr && mask.isEmpty()); + this->window->setMask(mask); } QQmlListProperty ProxyWindowBase::data() { @@ -439,57 +340,3 @@ QQmlListProperty ProxyWindowBase::data() { void ProxyWindowBase::onWidthChanged() { this->mContentItem->setWidth(this->width()); } void ProxyWindowBase::onHeightChanged() { this->mContentItem->setHeight(this->height()); } - -ProxyWindowAttached::ProxyWindowAttached(QQuickItem* parent): QsWindowAttached(parent) { - this->updateWindow(); -} - -QObject* ProxyWindowAttached::window() const { return this->mWindow; } -QQuickItem* ProxyWindowAttached::contentItem() const { return this->mWindow->contentItem(); } - -void ProxyWindowAttached::updateWindow() { - auto* window = static_cast(this->parent())->window(); // NOLINT - - if (auto* proxy = qobject_cast(window)) { - this->setWindow(proxy->proxy()); - } else { - this->setWindow(nullptr); - } -} - -void ProxyWindowAttached::setWindow(ProxyWindowBase* window) { - if (window == this->mWindow) return; - this->mWindow = window; - emit this->windowChanged(); -} - -bool ProxiedWindow::event(QEvent* event) { - if (event->type() == QEvent::DevicePixelRatioChange) { - emit this->devicePixelRatioChanged(); - } - - return this->QQuickWindow::event(event); -} - -void ProxiedWindow::exposeEvent(QExposeEvent* event) { - this->QQuickWindow::exposeEvent(event); - emit this->exposed(); -} - -void ProxyWindowContentItem::updatePolish() { emit this->polished(); } - -void ProxyWindowBase::onPolished() { - if (this->pendingPolish.inputMask) { - QRegion mask; - if (this->mMask != nullptr) { - mask = this->mMask->applyTo(QRect(0, 0, this->width(), this->height())); - } - - this->window->setFlag(Qt::WindowTransparentForInput, this->mMask != nullptr && mask.isEmpty()); - this->window->setMask(mask); - - this->pendingPolish.inputMask = false; - } - - emit this->polished(); -} diff --git a/src/window/proxywindow.hpp b/src/core/proxywindow.hpp similarity index 61% rename from src/window/proxywindow.hpp rename to src/core/proxywindow.hpp index 92a85f4f..40f14c4a 100644 --- a/src/window/proxywindow.hpp +++ b/src/core/proxywindow.hpp @@ -9,19 +9,15 @@ #include #include #include -#include #include #include -#include -#include "../core/qmlscreen.hpp" -#include "../core/region.hpp" -#include "../core/reload.hpp" +#include "qmlglobal.hpp" +#include "qmlscreen.hpp" +#include "region.hpp" +#include "reload.hpp" #include "windowinterface.hpp" -class ProxiedWindow; -class ProxyWindowContentItem; - // Proxy to an actual window exposing a limited property set with the ability to // transfer it to a new window. @@ -31,7 +27,6 @@ class ProxyWindowContentItem; /// [FloatingWindow]: ../floatingwindow class ProxyWindowBase: public Reloadable { Q_OBJECT; - // clang-format off /// The QtQuick window backing this window. /// /// > [!WARNING] Do not expect values set via this property to work correctly. @@ -44,15 +39,12 @@ class ProxyWindowBase: public Reloadable { Q_PROPERTY(bool visible READ isVisible WRITE setVisible NOTIFY visibleChanged); Q_PROPERTY(qint32 width READ width WRITE setWidth NOTIFY widthChanged); Q_PROPERTY(qint32 height READ height WRITE setHeight NOTIFY heightChanged); - Q_PROPERTY(qreal devicePixelRatio READ devicePixelRatio NOTIFY devicePixelRatioChanged); Q_PROPERTY(QuickshellScreenInfo* screen READ screen WRITE setScreen NOTIFY screenChanged); Q_PROPERTY(QColor color READ color WRITE setColor NOTIFY colorChanged); Q_PROPERTY(PendingRegion* mask READ mask WRITE setMask NOTIFY maskChanged); 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(QQmlListProperty data READ data); - // clang-format on Q_CLASSINFO("DefaultProperty", "data"); public: @@ -65,15 +57,14 @@ public: void operator=(ProxyWindowBase&&) = delete; void onReload(QObject* oldInstance) override; - void ensureQWindow(); void createWindow(); - void deleteWindow(bool keepItemOwnership = false); + void deleteWindow(); // Disown the backing window and delete all its children. - virtual ProxiedWindow* disownWindow(bool keepItemOwnership = false); + virtual QQuickWindow* disownWindow(); - virtual ProxiedWindow* retrieveWindow(QObject* oldInstance); - virtual ProxiedWindow* createQQuickWindow(); + virtual QQuickWindow* retrieveWindow(QObject* oldInstance); + virtual QQuickWindow* createQQuickWindow(); virtual void connectWindow(); virtual void completeWindow(); virtual void postCompleteWindow(); @@ -87,8 +78,6 @@ public: virtual void setVisible(bool visible); virtual void setVisibleDirect(bool visible); - void schedulePolish(); - [[nodiscard]] virtual qint32 x() const; [[nodiscard]] virtual qint32 y() const; @@ -98,10 +87,7 @@ public: [[nodiscard]] virtual qint32 height() const; virtual void setHeight(qint32 height); - [[nodiscard]] qreal devicePixelRatio() const; - - [[nodiscard]] QScreen* qscreen() const; - [[nodiscard]] QuickshellScreenInfo* screen() const; + [[nodiscard]] virtual QuickshellScreenInfo* screen() const; virtual void setScreen(QuickshellScreenInfo* screen); [[nodiscard]] QColor color() const; @@ -110,9 +96,6 @@ public: [[nodiscard]] PendingRegion* mask() const; virtual void setMask(PendingRegion* mask); - [[nodiscard]] QsSurfaceFormat surfaceFormat() const { return this->qsSurfaceFormat; } - void setSurfaceFormat(QsSurfaceFormat format); - [[nodiscard]] QObject* windowTransform() const { return nullptr; } // NOLINT [[nodiscard]] QQmlListProperty data(); @@ -126,13 +109,10 @@ signals: void yChanged(); void widthChanged(); void heightChanged(); - void devicePixelRatioChanged(); void windowTransformChanged(); void screenChanged(); void colorChanged(); void maskChanged(); - void surfaceFormatChanged(); - void polished(); protected slots: virtual void onWidthChanged(); @@ -140,8 +120,6 @@ protected slots: void onMaskChanged(); void onMaskDestroyed(); void onScreenDestroyed(); - void onPolished(); - void runLints(); protected: bool mVisible = true; @@ -150,69 +128,10 @@ protected: QScreen* mScreen = nullptr; QColor mColor = Qt::white; PendingRegion* mMask = nullptr; - ProxiedWindow* window = nullptr; - ProxyWindowContentItem* mContentItem = nullptr; + QQuickWindow* window = nullptr; + QQuickItem* mContentItem = nullptr; bool reloadComplete = false; - bool ranLints = false; - QsSurfaceFormat qsSurfaceFormat; - QSurfaceFormat mSurfaceFormat; - - struct { - bool inputMask : 1 = false; - } pendingPolish; private: - void polishItems(); void updateMask(); }; - -class ProxyWindowAttached: public QsWindowAttached { - Q_OBJECT; - -public: - explicit ProxyWindowAttached(QQuickItem* parent); - - [[nodiscard]] QObject* window() const override; - [[nodiscard]] QQuickItem* contentItem() const override; - -protected: - void updateWindow() override; - -private: - ProxyWindowBase* mWindow = nullptr; - - void setWindow(ProxyWindowBase* window); -}; - -class ProxiedWindow: public QQuickWindow { - Q_OBJECT; - -public: - explicit ProxiedWindow(ProxyWindowBase* proxy, QWindow* parent = nullptr) - : QQuickWindow(parent) - , mProxy(proxy) {} - - [[nodiscard]] ProxyWindowBase* proxy() const { return this->mProxy; } - void setProxy(ProxyWindowBase* proxy) { this->mProxy = proxy; } - -signals: - void exposed(); - void devicePixelRatioChanged(); - -protected: - bool event(QEvent* event) override; - void exposeEvent(QExposeEvent* event) override; - -private: - ProxyWindowBase* mProxy; -}; - -class ProxyWindowContentItem: public QQuickItem { - Q_OBJECT; - -signals: - void polished(); - -protected: - void updatePolish() override; -}; diff --git a/src/core/qmlglobal.cpp b/src/core/qmlglobal.cpp index 23f238da..70d7b416 100644 --- a/src/core/qmlglobal.cpp +++ b/src/core/qmlglobal.cpp @@ -5,7 +5,6 @@ #include #include #include -#include #include #include #include @@ -20,7 +19,6 @@ #include #include "generation.hpp" -#include "iconimageprovider.hpp" #include "qmlscreen.hpp" #include "rootwrapper.hpp" @@ -167,12 +165,6 @@ void QuickshellGlobal::reload(bool hard) { root->reloadGraph(hard); } -QString QuickshellGlobal::shellRoot() const { - auto* generation = EngineGeneration::findObjectGeneration(this); - // already canonical - return generation->rootPath.path(); -} - QString QuickshellGlobal::workingDirectory() const { // NOLINT return QuickshellSettings::instance()->workingDirectory(); } @@ -195,27 +187,3 @@ QVariant QuickshellGlobal::env(const QString& variable) { // NOLINT return qEnvironmentVariable(vstr.data()); } - -QString QuickshellGlobal::iconPath(const QString& icon) { - return IconImageProvider::requestString(icon); -} - -QString QuickshellGlobal::iconPath(const QString& icon, bool check) { - if (check && QIcon::fromTheme(icon).isNull()) return ""; - return IconImageProvider::requestString(icon); -} - -QString QuickshellGlobal::iconPath(const QString& icon, const QString& fallback) { - return IconImageProvider::requestString(icon, "", fallback); -} - -QuickshellGlobal* QuickshellGlobal::create(QQmlEngine* engine, QJSEngine* /*unused*/) { - auto* qsg = new QuickshellGlobal(); - auto* generation = EngineGeneration::findEngineGeneration(engine); - - if (generation->qsgInstance == nullptr) { - generation->qsgInstance = qsg; - } - - return qsg; -} diff --git a/src/core/qmlglobal.hpp b/src/core/qmlglobal.hpp index 4f46d238..83ef68d4 100644 --- a/src/core/qmlglobal.hpp +++ b/src/core/qmlglobal.hpp @@ -98,11 +98,6 @@ class QuickshellGlobal: public QObject { /// This creates an instance of your window once on every screen. /// As screens are added or removed your window will be created or destroyed on those screens. Q_PROPERTY(QQmlListProperty screens READ screens NOTIFY screensChanged); - /// The full path to the root directory of your shell. - /// - /// The root directory is the folder containing the entrypoint to your shell, often referred - /// to as `shell.qml`. - 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. @@ -115,62 +110,40 @@ class QuickshellGlobal: public QObject { public: [[nodiscard]] qint32 processId() const; + QuickshellGlobal(QObject* parent = nullptr); + QQmlListProperty screens(); - /// Reload the shell. + /// Reload the shell from the [ShellRoot]. /// /// `hard` - perform a hard reload. If this is false, Quickshell will attempt to reuse windows /// that already exist. If true windows will be recreated. /// - /// See @@Reloadable for more information on what can be reloaded and how. + /// See [Reloadable] for more information on what can be reloaded and how. + /// + /// [Reloadable]: ../reloadable Q_INVOKABLE void reload(bool hard); /// Returns the string value of an environment variable or null if it is not set. Q_INVOKABLE QVariant env(const QString& variable); - /// Returns a string usable for a @@QtQuick.Image.source for a given system icon. - /// - /// > [!INFO] By default, icons are loaded from the theme selected by the qt platform theme, - /// > which means they should match with all other qt applications on your system. - /// > - /// > If you want to use a different icon theme, you can put `//@ pragma IconTheme ` - /// > at the top of your root config file or set the `QS_ICON_THEME` variable to the name - /// > of your icon theme. - Q_INVOKABLE static QString iconPath(const QString& icon); - /// Setting the `check` parameter of `iconPath` to true will return an empty string - /// if the icon does not exist, instead of an image showing a missing texture. - Q_INVOKABLE static QString iconPath(const QString& icon, bool check); - /// 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); - - [[nodiscard]] QString shellRoot() const; - [[nodiscard]] QString workingDirectory() const; void setWorkingDirectory(QString workingDirectory); [[nodiscard]] bool watchFiles() const; void setWatchFiles(bool watchFiles); - static QuickshellGlobal* create(QQmlEngine* engine, QJSEngine* /*unused*/); - signals: /// Sent when the last window is closed. /// /// To make the application exit when the last window is closed run `Qt.quit()`. void lastWindowClosed(); - /// The reload sequence has completed successfully. - void reloadCompleted(); - /// The reload sequence has failed. - void reloadFailed(QString errorString); void screensChanged(); void workingDirectoryChanged(); void watchFilesChanged(); private: - QuickshellGlobal(QObject* parent = nullptr); - static qsizetype screensCount(QQmlListProperty* prop); static QuickshellScreenInfo* screenAt(QQmlListProperty* prop, qsizetype i); }; diff --git a/src/core/qmlscreen.cpp b/src/core/qmlscreen.cpp index 105b4f01..34588b77 100644 --- a/src/core/qmlscreen.cpp +++ b/src/core/qmlscreen.cpp @@ -42,24 +42,6 @@ QString QuickshellScreenInfo::name() const { return this->screen->name(); } -QString QuickshellScreenInfo::model() const { - if (this->screen == nullptr) { - this->warnDangling(); - return "{ NULL SCREEN }"; - } - - return this->screen->model(); -} - -QString QuickshellScreenInfo::serialNumber() const { - if (this->screen == nullptr) { - this->warnDangling(); - return "{ NULL SCREEN }"; - } - - return this->screen->serialNumber(); -} - qint32 QuickshellScreenInfo::x() const { if (this->screen == nullptr) { this->warnDangling(); diff --git a/src/core/qmlscreen.hpp b/src/core/qmlscreen.hpp index 5e978bc0..dfebf331 100644 --- a/src/core/qmlscreen.hpp +++ b/src/core/qmlscreen.hpp @@ -12,14 +12,17 @@ // unfortunately QQuickScreenInfo is private. -/// Monitor object useful for setting the monitor for a @@QsWindow +/// Monitor object useful for setting the monitor for a [ShellWindow] /// or querying information about the monitor. /// /// > [!WARNING] If the monitor is disconnected than any stored copies of its ShellMonitor will /// > be marked as dangling and all properties will return default values. /// > Reconnecting the monitor will not reconnect it to the ShellMonitor object. /// -/// Due to some technical limitations, it was not possible to reuse the native qml @@QtQuick.Screen type. +/// Due to some technical limitations, it was not possible to reuse the native qml [Screen] type. +/// +/// [ShellWindow]: ../shellwindow +/// [Screen]: https://doc.qt.io/qt-6/qml-qtquick-screen.html class QuickshellScreenInfo: public QObject { Q_OBJECT; QML_NAMED_ELEMENT(ShellScreen); @@ -29,10 +32,6 @@ class QuickshellScreenInfo: public QObject { /// /// Usually something like `DP-1`, `HDMI-1`, `eDP-1`. Q_PROPERTY(QString name READ name CONSTANT); - /// The model of the screen as seen by the operating system. - Q_PROPERTY(QString model READ model CONSTANT); - /// The serial number of the screen as seen by the operating system. - Q_PROPERTY(QString serialNumber READ serialNumber CONSTANT); Q_PROPERTY(qint32 x READ x NOTIFY geometryChanged); Q_PROPERTY(qint32 y READ y NOTIFY geometryChanged); Q_PROPERTY(qint32 width READ width NOTIFY geometryChanged); @@ -44,7 +43,7 @@ class QuickshellScreenInfo: public QObject { /// The ratio between physical pixels and device-independent (scaled) pixels. Q_PROPERTY(qreal devicePixelRatio READ devicePixelRatio NOTIFY physicalPixelDensityChanged); Q_PROPERTY(Qt::ScreenOrientation orientation READ orientation NOTIFY orientationChanged); - Q_PROPERTY(Qt::ScreenOrientation primaryOrientation READ primaryOrientation NOTIFY primaryOrientationChanged); + Q_PROPERTY(Qt::ScreenOrientation primatyOrientation READ primaryOrientation NOTIFY primaryOrientationChanged); // clang-format on public: @@ -53,8 +52,6 @@ public: bool operator==(QuickshellScreenInfo& other) const; [[nodiscard]] QString name() const; - [[nodiscard]] QString model() const; - [[nodiscard]] QString serialNumber() const; [[nodiscard]] qint32 x() const; [[nodiscard]] qint32 y() const; [[nodiscard]] qint32 width() const; diff --git a/src/core/qsintercept.cpp b/src/core/qsintercept.cpp index ba46ab7b..2eaf498e 100644 --- a/src/core/qsintercept.cpp +++ b/src/core/qsintercept.cpp @@ -16,22 +16,7 @@ Q_LOGGING_CATEGORY(logQsIntercept, "quickshell.interceptor", QtWarningMsg); -QUrl QsUrlInterceptor::intercept( - const QUrl& originalUrl, - QQmlAbstractUrlInterceptor::DataType type -) { - auto url = originalUrl; - - if (url.scheme() == "root") { - url.setScheme("qsintercept"); - - auto path = url.path(); - if (path.startsWith('/')) path = path.sliced(1); - url.setPath(this->configRoot.filePath(path)); - - qCDebug(logQsIntercept) << "Rewrote root intercept" << originalUrl << "to" << url; - } - +QUrl QsUrlInterceptor::intercept(const QUrl& url, QQmlAbstractUrlInterceptor::DataType type) { // Some types such as Image take into account where they are loading from, and force // asynchronous loading over a network. qsintercept is considered to be over a network. if (type == QQmlAbstractUrlInterceptor::DataType::UrlString && url.scheme() == "qsintercept") { diff --git a/src/core/qsintercept.hpp b/src/core/qsintercept.hpp index 57923568..d51b78e6 100644 --- a/src/core/qsintercept.hpp +++ b/src/core/qsintercept.hpp @@ -1,6 +1,5 @@ #pragma once -#include #include #include #include @@ -14,12 +13,7 @@ Q_DECLARE_LOGGING_CATEGORY(logQsIntercept); class QsUrlInterceptor: public QQmlAbstractUrlInterceptor { public: - explicit QsUrlInterceptor(const QDir& configRoot): configRoot(configRoot) {} - - QUrl intercept(const QUrl& originalUrl, QQmlAbstractUrlInterceptor::DataType type) override; - -private: - QDir configRoot; + QUrl intercept(const QUrl& url, QQmlAbstractUrlInterceptor::DataType type) override; }; class QsInterceptDataReply: public QNetworkReply { diff --git a/src/core/qsmenu.cpp b/src/core/qsmenu.cpp deleted file mode 100644 index 760f7e76..00000000 --- a/src/core/qsmenu.cpp +++ /dev/null @@ -1,110 +0,0 @@ -#include "qsmenu.hpp" - -#include -#include -#include -#include - -#include "model.hpp" -#include "platformmenu.hpp" - -using namespace qs::menu::platform; - -namespace qs::menu { - -QString QsMenuButtonType::toString(QsMenuButtonType::Enum value) { - switch (value) { - case QsMenuButtonType::None: return "None"; - case QsMenuButtonType::CheckBox: return "CheckBox"; - case QsMenuButtonType::RadioButton: return "RadioButton"; - default: return "Invalid button type"; - } -} - -QsMenuEntry* QsMenuEntry::menu() { return this; } - -void QsMenuEntry::display(QObject* parentWindow, int relativeX, int relativeY) { - auto* platform = new PlatformMenuEntry(this); - - QObject::connect(platform, &PlatformMenuEntry::closed, platform, [=]() { - platform->deleteLater(); - }); - - auto success = platform->display(parentWindow, relativeX, relativeY); - if (!success) delete platform; -} - -void QsMenuEntry::ref() { - this->refcount++; - if (this->refcount == 1) emit this->opened(); -} - -void QsMenuEntry::unref() { - this->refcount--; - if (this->refcount == 0) emit this->closed(); -} - -ObjectModel* QsMenuEntry::children() { - return ObjectModel::emptyInstance(); -} - -QsMenuOpener::~QsMenuOpener() { - if (this->mMenu) { - if (this->mMenu->menu()) this->mMenu->menu()->unref(); - this->mMenu->unrefHandle(); - } -} - -QsMenuHandle* QsMenuOpener::menu() const { return this->mMenu; } - -void QsMenuOpener::setMenu(QsMenuHandle* menu) { - if (menu == this->mMenu) return; - - if (this->mMenu != nullptr) { - QObject::disconnect(this->mMenu, nullptr, this, nullptr); - - if (this->mMenu->menu()) { - QObject::disconnect(this->mMenu->menu(), nullptr, this, nullptr); - this->mMenu->menu()->unref(); - } - - this->mMenu->unrefHandle(); - } - - this->mMenu = menu; - - if (menu != nullptr) { - auto onMenuChanged = [this, menu]() { - if (menu->menu()) { - menu->menu()->ref(); - } - - emit this->childrenChanged(); - }; - - QObject::connect(menu, &QObject::destroyed, this, &QsMenuOpener::onMenuDestroyed); - QObject::connect(menu, &QsMenuHandle::menuChanged, this, onMenuChanged); - - if (menu->menu()) onMenuChanged(); - menu->refHandle(); - } - - emit this->menuChanged(); - emit this->childrenChanged(); -} - -void QsMenuOpener::onMenuDestroyed() { - this->mMenu = nullptr; - emit this->menuChanged(); - emit this->childrenChanged(); -} - -ObjectModel* QsMenuOpener::children() { - if (this->mMenu && this->mMenu->menu()) { - return this->mMenu->menu()->children(); - } else { - return ObjectModel::emptyInstance(); - } -} - -} // namespace qs::menu diff --git a/src/core/qsmenu.hpp b/src/core/qsmenu.hpp deleted file mode 100644 index 6684c686..00000000 --- a/src/core/qsmenu.hpp +++ /dev/null @@ -1,163 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include -#include -#include - -#include "doc.hpp" -#include "model.hpp" - -namespace qs::menu { - -///! Button type associated with a QsMenuEntry. -/// See @@QsMenuEntry.buttonType. -class QsMenuButtonType: public QObject { - Q_OBJECT; - QML_ELEMENT; - QML_SINGLETON; - -public: - enum Enum : quint8 { - /// This menu item does not have a checkbox or a radiobutton associated with it. - None = 0, - /// This menu item should draw a checkbox. - CheckBox = 1, - /// This menu item should draw a radiobutton. - RadioButton = 2, - }; - Q_ENUM(Enum); - - Q_INVOKABLE static QString toString(qs::menu::QsMenuButtonType::Enum value); -}; - -class QsMenuEntry; - -///! Menu handle for QsMenuOpener -/// See @@QsMenuOpener. -class QsMenuHandle: public QObject { - Q_OBJECT; - QML_ELEMENT; - QML_UNCREATABLE(""); - -public: - explicit QsMenuHandle(QObject* parent): QObject(parent) {} - - virtual void refHandle() {}; - virtual void unrefHandle() {}; - - [[nodiscard]] virtual QsMenuEntry* menu() = 0; - -signals: - void menuChanged(); -}; - -class QsMenuEntry: public QsMenuHandle { - Q_OBJECT; - /// If this menu item should be rendered as a separator between other items. - /// - /// No other properties have a meaningful value when @@isSeparator is true. - Q_PROPERTY(bool isSeparator READ isSeparator NOTIFY isSeparatorChanged); - Q_PROPERTY(bool enabled READ enabled NOTIFY enabledChanged); - /// Text of the menu item. - Q_PROPERTY(QString text READ text NOTIFY textChanged); - /// Url of the menu item's icon or `""` if it doesn't have one. - /// - /// This can be passed to [Image.source](https://doc.qt.io/qt-6/qml-qtquick-image.html#source-prop) - /// as shown below. - /// - /// ```qml - /// Image { - /// source: menuItem.icon - /// // To get the best image quality, set the image source size to the same size - /// // as the rendered image. - /// sourceSize.width: width - /// sourceSize.height: height - /// } - /// ``` - Q_PROPERTY(QString icon READ icon NOTIFY iconChanged); - /// If this menu item has an associated checkbox or radiobutton. - Q_PROPERTY(qs::menu::QsMenuButtonType::Enum buttonType READ buttonType NOTIFY buttonTypeChanged); - /// The check state of the checkbox or radiobutton if applicable, as a - /// [Qt.CheckState](https://doc.qt.io/qt-6/qt.html#CheckState-enum). - Q_PROPERTY(Qt::CheckState checkState READ checkState NOTIFY checkStateChanged); - /// If this menu item has children that can be accessed through a @@QsMenuOpener$. - Q_PROPERTY(bool hasChildren READ hasChildren NOTIFY hasChildrenChanged); - QML_ELEMENT; - QML_UNCREATABLE("QsMenuEntry cannot be directly created"); - -public: - explicit QsMenuEntry(QObject* parent): QsMenuHandle(parent) {} - - [[nodiscard]] QsMenuEntry* menu() override; - - /// Display a platform menu at the given location relative to the parent window. - Q_INVOKABLE void display(QObject* parentWindow, qint32 relativeX, qint32 relativeY); - - [[nodiscard]] virtual bool isSeparator() const { return false; } - [[nodiscard]] virtual bool enabled() const { return true; } - [[nodiscard]] virtual QString text() const { return ""; } - [[nodiscard]] virtual QString icon() const { return ""; } - [[nodiscard]] virtual QsMenuButtonType::Enum buttonType() const { return QsMenuButtonType::None; } - [[nodiscard]] virtual Qt::CheckState checkState() const { return Qt::Unchecked; } - [[nodiscard]] virtual bool hasChildren() const { return false; } - - void ref(); - void unref(); - - [[nodiscard]] virtual ObjectModel* children(); - -signals: - /// Send a trigger/click signal to the menu entry. - void triggered(); - - QSDOC_HIDE void opened(); - QSDOC_HIDE void closed(); - - void isSeparatorChanged(); - void enabledChanged(); - void textChanged(); - void iconChanged(); - void buttonTypeChanged(); - void checkStateChanged(); - void hasChildrenChanged(); - -private: - qsizetype refcount = 0; -}; - -///! Provides access to children of a QsMenuEntry -class QsMenuOpener: public QObject { - Q_OBJECT; - /// The menu to retrieve children from. - Q_PROPERTY(qs::menu::QsMenuHandle* menu READ menu WRITE setMenu NOTIFY menuChanged); - /// The children of the given menu. - QSDOC_TYPE_OVERRIDE(ObjectModel*); - Q_PROPERTY(UntypedObjectModel* children READ children NOTIFY childrenChanged); - QML_ELEMENT; - -public: - explicit QsMenuOpener(QObject* parent = nullptr): QObject(parent) {} - ~QsMenuOpener() override; - Q_DISABLE_COPY_MOVE(QsMenuOpener); - - [[nodiscard]] QsMenuHandle* menu() const; - void setMenu(QsMenuHandle* menu); - - [[nodiscard]] ObjectModel* children(); - -signals: - void menuChanged(); - void childrenChanged(); - -private slots: - void onMenuDestroyed(); - -private: - QsMenuHandle* mMenu = nullptr; -}; - -} // namespace qs::menu diff --git a/src/core/qsmenuanchor.cpp b/src/core/qsmenuanchor.cpp deleted file mode 100644 index ded8ad05..00000000 --- a/src/core/qsmenuanchor.cpp +++ /dev/null @@ -1,125 +0,0 @@ -#include "qsmenuanchor.hpp" - -#include -#include -#include -#include -#include - -#include "platformmenu.hpp" -#include "popupanchor.hpp" -#include "qsmenu.hpp" - -using qs::menu::platform::PlatformMenuEntry; - -namespace qs::menu { - -QsMenuAnchor::~QsMenuAnchor() { this->onClosed(); } - -void QsMenuAnchor::open() { - if (qobject_cast(QCoreApplication::instance()) == nullptr) { - qCritical() << "Cannot call QsMenuAnchor.open() as quickshell was not started in " - "QApplication mode."; - qCritical() << "To use platform menus, add `//@ pragma UseQApplication` to the top of your " - "root QML file and restart quickshell."; - return; - } - - if (this->mOpen) { - qCritical() << "Cannot call QsMenuAnchor.open() as it is already open."; - return; - } - - if (!this->mMenu) { - qCritical() << "Cannot open QsMenuAnchor with no menu attached."; - return; - } - - this->mOpen = true; - - if (this->mMenu->menu()) this->onMenuChanged(); - QObject::connect(this->mMenu, &QsMenuHandle::menuChanged, this, &QsMenuAnchor::onMenuChanged); - this->mMenu->refHandle(); - - emit this->visibleChanged(); -} - -void QsMenuAnchor::onMenuChanged() { - // close menu if the path changes - if (this->platformMenu || !this->mMenu->menu()) { - this->onClosed(); - return; - } - - this->platformMenu = new PlatformMenuEntry(this->mMenu->menu()); - QObject::connect(this->platformMenu, &PlatformMenuEntry::closed, this, &QsMenuAnchor::onClosed); - - auto success = this->platformMenu->display(&this->mAnchor); - if (!success) this->onClosed(); - else emit this->opened(); -} - -void QsMenuAnchor::close() { - if (!this->mOpen) { - qCritical() << "Cannot close QsMenuAnchor as it isn't open."; - return; - } - - this->onClosed(); -} - -void QsMenuAnchor::onClosed() { - if (!this->mOpen) return; - - this->mOpen = false; - - if (this->platformMenu) { - this->platformMenu->deleteLater(); - this->platformMenu = nullptr; - } - - if (this->mMenu) { - QObject::disconnect( - this->mMenu, - &QsMenuHandle::menuChanged, - this, - &QsMenuAnchor::onMenuChanged - ); - - this->mMenu->unrefHandle(); - } - - emit this->closed(); - emit this->visibleChanged(); -} - -PopupAnchor* QsMenuAnchor::anchor() { return &this->mAnchor; } - -QsMenuHandle* QsMenuAnchor::menu() const { return this->mMenu; } - -void QsMenuAnchor::setMenu(QsMenuHandle* menu) { - if (menu == this->mMenu) return; - - if (this->mMenu != nullptr) { - if (this->platformMenu != nullptr) this->platformMenu->deleteLater(); - QObject::disconnect(this->mMenu, nullptr, this, nullptr); - } - - this->mMenu = menu; - - if (menu != nullptr) { - QObject::connect(menu, &QObject::destroyed, this, &QsMenuAnchor::onMenuDestroyed); - } - - emit this->menuChanged(); -} - -bool QsMenuAnchor::isVisible() const { return this->mOpen; } - -void QsMenuAnchor::onMenuDestroyed() { - this->mMenu = nullptr; - this->onClosed(); - emit this->menuChanged(); -} - -} // namespace qs::menu diff --git a/src/core/qsmenuanchor.hpp b/src/core/qsmenuanchor.hpp deleted file mode 100644 index 14e06c63..00000000 --- a/src/core/qsmenuanchor.hpp +++ /dev/null @@ -1,86 +0,0 @@ -#pragma once - -#include -#include -#include - -#include "platformmenu.hpp" -#include "popupanchor.hpp" -#include "qsmenu.hpp" - -namespace qs::menu { - -///! Display anchor for platform menus. -class QsMenuAnchor: public QObject { - Q_OBJECT; - /// The menu's anchor / positioner relative to another window. The menu will not be - /// shown until it has a valid anchor. - /// - /// > [!INFO] *The following is subject to change and NOT a guarantee of future behavior.* - /// > - /// > A snapshot of the anchor at the time @@opened(s) is emitted will be - /// > used to position the menu. Additional changes to the anchor after this point - /// > will not affect the placement of the menu. - /// - /// You can set properties of the anchor like so: - /// ```qml - /// QsMenuAnchor { - /// anchor.window: parentwindow - /// // or - /// anchor { - /// window: parentwindow - /// } - /// } - /// ``` - Q_PROPERTY(PopupAnchor* anchor READ anchor CONSTANT); - /// The menu that should be displayed on this anchor. - /// - /// See also: @@Quickshell.Services.SystemTray.SystemTrayItem.menu. - Q_PROPERTY(qs::menu::QsMenuHandle* menu READ menu WRITE setMenu NOTIFY menuChanged); - /// If the menu is currently open and visible. - /// - /// See also: @@open(), @@close(). - Q_PROPERTY(bool visible READ isVisible NOTIFY visibleChanged); - QML_ELEMENT; - -public: - explicit QsMenuAnchor(QObject* parent = nullptr): QObject(parent) {} - ~QsMenuAnchor() override; - Q_DISABLE_COPY_MOVE(QsMenuAnchor); - - /// Open the given menu on this menu Requires that @@anchor is valid. - Q_INVOKABLE void open(); - /// Close the open menu. - Q_INVOKABLE void close(); - - [[nodiscard]] PopupAnchor* anchor(); - - [[nodiscard]] QsMenuHandle* menu() const; - void setMenu(QsMenuHandle* menu); - - [[nodiscard]] bool isVisible() const; - -signals: - /// Sent when the menu is displayed onscreen which may be after @@visible - /// becomes true. - void opened(); - /// Sent when the menu is closed. - void closed(); - - void menuChanged(); - void visibleChanged(); - -private slots: - void onMenuChanged(); - void onMenuDestroyed(); - -private: - void onClosed(); - - PopupAnchor mAnchor {this}; - QsMenuHandle* mMenu = nullptr; - bool mOpen = false; - platform::PlatformMenuEntry* platformMenu = nullptr; -}; - -} // namespace qs::menu diff --git a/src/core/region.cpp b/src/core/region.cpp index 439cfbd2..47f15d4a 100644 --- a/src/core/region.cpp +++ b/src/core/region.cpp @@ -8,7 +8,6 @@ #include #include #include -#include PendingRegion::PendingRegion(QObject* parent): QObject(parent) { QObject::connect(this, &PendingRegion::shapeChanged, this, &PendingRegion::changed); @@ -106,19 +105,8 @@ QRegion PendingRegion::applyTo(QRegion& region) const { return region; } -QRegion PendingRegion::applyTo(const QRect& rect) const { - // if left as the default, dont combine it with the whole rect area, leave it as is. - if (this->mIntersection == Intersection::Combine) { - return this->build(); - } else { - auto baseRegion = QRegion(rect); - 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); diff --git a/src/core/region.hpp b/src/core/region.hpp index 6637d7bd..35f2736c 100644 --- a/src/core/region.hpp +++ b/src/core/region.hpp @@ -9,13 +9,12 @@ #include #include -///! Shape of a Region. -/// See @@Region.shape. +/// Shape of a Region. namespace RegionShape { // NOLINT Q_NAMESPACE; QML_ELEMENT; -enum Enum : quint8 { +enum Enum { Rect = 0, Ellipse = 1, }; @@ -24,12 +23,11 @@ Q_ENUM_NS(Enum); } // namespace RegionShape ///! Intersection strategy for Regions. -/// See @@Region.intersection. namespace Intersection { // NOLINT Q_NAMESPACE; QML_ELEMENT; -enum Enum : quint8 { +enum Enum { /// Combine this region, leaving a union of this and the other region. (opposite of `Subtract`) Combine = 0, /// Subtract this region, cutting this region out of the other. (opposite of `Combine`) @@ -46,7 +44,6 @@ Q_ENUM_NS(Enum); } // namespace Intersection ///! A composable region used as a mask. -/// See @@QsWindow.mask. class PendingRegion: public QObject { Q_OBJECT; /// Defaults to `Rect`. @@ -55,16 +52,16 @@ class PendingRegion: public QObject { Q_PROPERTY(Intersection::Enum intersection MEMBER mIntersection NOTIFY intersectionChanged); /// The item that determines the geometry of the region. - /// `item` overrides @@x, @@y, @@width and @@height. + /// `item` overrides `x`, `y`, `width` and `height`. Q_PROPERTY(QQuickItem* item MEMBER mItem WRITE setItem NOTIFY itemChanged); - /// Defaults to 0. Does nothing if @@item is set. + /// Defaults to 0. Does nothing if `item` is set. Q_PROPERTY(qint32 x MEMBER mX NOTIFY xChanged); - /// Defaults to 0. Does nothing if @@item is set. + /// Defaults to 0. Does nothing if `item` is set. Q_PROPERTY(qint32 y MEMBER mY NOTIFY yChanged); - /// Defaults to 0. Does nothing if @@item is set. + /// Defaults to 0. Does nothing if `item` is set. Q_PROPERTY(qint32 width MEMBER mWidth NOTIFY widthChanged); - /// Defaults to 0. Does nothing if @@item is set. + /// Defaults to 0. Does nothing if `item` is set. Q_PROPERTY(qint32 height MEMBER mHeight NOTIFY heightChanged); /// Regions to apply on top of this region. @@ -96,7 +93,6 @@ public: [[nodiscard]] bool empty() const; [[nodiscard]] QRegion build() const; [[nodiscard]] QRegion applyTo(QRegion& region) const; - [[nodiscard]] QRegion applyTo(const QRect& rect) const; RegionShape::Enum mShape = RegionShape::Rect; Intersection::Enum mIntersection = Intersection::Combine; @@ -110,11 +106,6 @@ signals: void widthChanged(); void heightChanged(); void childrenChanged(); - - /// Triggered when the region's geometry changes. - /// - /// In some cases the region does not update automatically. - /// In those cases you can emit this signal manually. void changed(); private slots: diff --git a/src/core/reload.cpp b/src/core/reload.cpp index 25ab33f3..4940ddc8 100644 --- a/src/core/reload.cpp +++ b/src/core/reload.cpp @@ -3,7 +3,6 @@ #include #include #include -#include #include "generation.hpp" @@ -13,19 +12,8 @@ void Reloadable::componentComplete() { if (this->engineGeneration != nullptr) { // When called this way there is no chance a reload will have old data, // but this will at least help prevent weird behaviors due to never getting a reload. - if (this->engineGeneration->reloadComplete) { - // Delayed due to Component.onCompleted running after QQmlParserStatus::componentComplete. - QTimer::singleShot(0, this, &Reloadable::onReloadFinished); - - // This only matters for preventing the above timer from UAFing the generation, - // so it isn't connected anywhere else. - QObject::connect( - this->engineGeneration, - &QObject::destroyed, - this, - &Reloadable::onGenerationDestroyed - ); - } else { + if (this->engineGeneration->reloadComplete) this->reload(); + else { QObject::connect( this->engineGeneration, &EngineGeneration::reloadFinished, @@ -52,7 +40,6 @@ void Reloadable::reload(QObject* oldInstance) { } void Reloadable::onReloadFinished() { this->reload(nullptr); } -void Reloadable::onGenerationDestroyed() { this->engineGeneration = nullptr; } void ReloadPropagator::onReload(QObject* oldInstance) { auto* old = qobject_cast(oldInstance); @@ -99,7 +86,7 @@ void Reloadable::reloadRecursive(QObject* newObj, QObject* oldRoot) { // pass handling to the child's onReload, which should call back into reloadRecursive, // with its oldInstance becoming the new oldRoot. - reloadable->reload(oldInstance); + reloadable->onReload(oldInstance); } else if (newObj != nullptr) { Reloadable::reloadChildrenRecursive(newObj, oldRoot); } diff --git a/src/core/reload.hpp b/src/core/reload.hpp index 560c8bd0..0d33e2b6 100644 --- a/src/core/reload.hpp +++ b/src/core/reload.hpp @@ -11,7 +11,10 @@ class EngineGeneration; ///! The base class of all types that can be reloaded. /// Reloadables will attempt to take specific state from previous config revisions if possible. -/// Some examples are @@ProxyWindowBase and @@PersistentProperties +/// Some examples are [ProxyWindowBase] and [PersistentProperties] +/// +/// [ProxyWindowBase]: ../proxywindowbase +/// [PersistentProperties]: ../persistentproperties class Reloadable : public QObject , public QQmlParserStatus { @@ -25,7 +28,7 @@ class Reloadable /// this object in the current revision, and facilitate smoother reloading. /// /// Note that identifiers are scoped, and will try to do the right thing in context. - /// For example if you have a @@Variants wrapping an object with an identified element inside, + /// For example if you have a `Variants` wrapping an object with an identified element inside, /// a scope is created at the variant level. /// /// ```qml @@ -71,7 +74,6 @@ public: private slots: void onReloadFinished(); - void onGenerationDestroyed(); protected: // Called unconditionally in the reload phase, with nullptr if no source could be determined. @@ -84,9 +86,10 @@ private: }; ///! Scope that propagates reloads to child items in order. -/// Convenience type equivalent to setting @@Reloadable.reloadableId for all children. +/// Convenience type equivalent to setting `reloadableId` on properties in a +/// QtObject instance. /// -/// Note that this does not work for visible @@QtQuick.Item$s (all widgets). +/// Note that this does not work for visible `Item`s (all widgets). /// /// ```qml /// ShellRoot { diff --git a/src/core/retainable.cpp b/src/core/retainable.cpp deleted file mode 100644 index 4e77e051..00000000 --- a/src/core/retainable.cpp +++ /dev/null @@ -1,163 +0,0 @@ -#include "retainable.hpp" - -#include -#include -#include -#include - -RetainableHook* RetainableHook::getHook(QObject* object, bool create) { - auto v = object->property("__qs_retainable"); - - if (v.canConvert()) { - return v.value(); - } else if (create) { - auto* retainable = dynamic_cast(object); - if (!retainable) return nullptr; - - auto* hook = new RetainableHook(object); - hook->retainableFacet = retainable; - retainable->hook = hook; - - object->setProperty("__qs_retainable", QVariant::fromValue(hook)); - - return hook; - } else return nullptr; -} - -RetainableHook* RetainableHook::qmlAttachedProperties(QObject* object) { - return RetainableHook::getHook(object, true); -} - -void RetainableHook::ref() { this->refcount++; } - -void RetainableHook::unref() { - this->refcount--; - if (this->refcount == 0) this->unlocked(); -} - -void RetainableHook::lock() { - this->explicitRefcount++; - this->ref(); -} - -void RetainableHook::unlock() { - if (this->explicitRefcount < 1) { - qWarning() << "Retainable object" << this->parent() - << "unlocked more times than it was locked!"; - } else { - this->explicitRefcount--; - this->unref(); - } -} - -void RetainableHook::forceUnlock() { this->unlocked(); } - -bool RetainableHook::isRetained() const { return !this->inactive; } - -void RetainableHook::unlocked() { - if (this->inactive) return; - - emit this->aboutToDestroy(); - this->retainableFacet->retainFinished(); -} - -void Retainable::retainedDestroy() { - this->retaining = true; - - auto* hook = RetainableHook::getHook(dynamic_cast(this), false); - - if (hook) { - // let all signal handlers run before acting on changes - emit hook->dropped(); - hook->inactive = false; - - if (hook->refcount == 0) hook->unlocked(); - else emit hook->retainedChanged(); - } else { - this->retainFinished(); - } -} - -bool Retainable::isRetained() const { return this->retaining; } - -void Retainable::retainFinished() { - // a normal delete tends to cause deref errors in a listview. - dynamic_cast(this)->deleteLater(); -} - -RetainableLock::~RetainableLock() { - if (this->mEnabled && this->mObject) { - this->hook->unref(); - } -} - -QObject* RetainableLock::object() const { return this->mObject; } - -void RetainableLock::setObject(QObject* object) { - if (object == this->mObject) return; - - if (this->mObject) { - QObject::disconnect(this->mObject, nullptr, this, nullptr); - if (this->hook->isRetained()) emit this->retainedChanged(); - this->hook->unref(); - } - - this->mObject = nullptr; - this->hook = nullptr; - - if (object) { - if (auto* hook = RetainableHook::getHook(object, true)) { - this->mObject = object; - this->hook = hook; - - QObject::connect(object, &QObject::destroyed, this, &RetainableLock::onObjectDestroyed); - QObject::connect(hook, &RetainableHook::dropped, this, &RetainableLock::dropped); - QObject::connect( - hook, - &RetainableHook::aboutToDestroy, - this, - &RetainableLock::aboutToDestroy - ); - QObject::connect( - hook, - &RetainableHook::retainedChanged, - this, - &RetainableLock::retainedChanged - ); - if (hook->isRetained()) emit this->retainedChanged(); - - hook->ref(); - } else { - qCritical() << "Tried to set non retainable object" << object << "as the target of" << this; - } - } - - emit this->objectChanged(); -} - -void RetainableLock::onObjectDestroyed() { - this->mObject = nullptr; - this->hook = nullptr; - - emit this->objectChanged(); -} - -bool RetainableLock::locked() const { return this->mEnabled; } - -void RetainableLock::setLocked(bool locked) { - if (locked == this->mEnabled) return; - - this->mEnabled = locked; - - if (this->mObject) { - if (locked) this->hook->ref(); - else { - if (this->hook->isRetained()) emit this->retainedChanged(); - this->hook->unref(); - } - } - - emit this->lockedChanged(); -} - -bool RetainableLock::isRetained() const { return this->mObject && this->hook->isRetained(); } diff --git a/src/core/retainable.hpp b/src/core/retainable.hpp deleted file mode 100644 index dfe2e794..00000000 --- a/src/core/retainable.hpp +++ /dev/null @@ -1,162 +0,0 @@ -#pragma once - -#include -#include -#include -#include - -class Retainable; - -///! Attached object for types that can have delayed destruction. -/// Retainable works as an attached property that allows objects to be -/// kept around (retained) after they would normally be destroyed, which -/// is especially useful for things like exit transitions. -/// -/// An object that is retainable will have @@Retainable as an attached property. -/// All retainable objects will say that they are retainable on their respective -/// typeinfo pages. -/// -/// > [!INFO] Working directly with @@Retainable is often overly complicated and -/// > error prone. For this reason @@RetainableLock should -/// > usually be used instead. -class RetainableHook: public QObject { - Q_OBJECT; - /// If the object is currently in a retained state. - Q_PROPERTY(bool retained READ isRetained NOTIFY retainedChanged); - QML_ATTACHED(RetainableHook); - QML_NAMED_ELEMENT(Retainable); - QML_UNCREATABLE("Retainable can only be used as an attached object."); - -public: - static RetainableHook* getHook(QObject* object, bool create = false); - - void destroyOnRelease(); - - void ref(); - void unref(); - - /// Hold a lock on the object so it cannot be destroyed. - /// - /// A counter is used to ensure you can lock the object from multiple places - /// and it will not be unlocked until the same number of unlocks as locks have occurred. - /// - /// > [!WARNING] It is easy to forget to unlock a locked object. - /// > Doing so will create what is effectively a memory leak. - /// > - /// > Using @@RetainableLock is recommended as it will help - /// > avoid this scenario and make misuse more obvious. - Q_INVOKABLE void lock(); - /// Remove a lock on the object. See @@lock() for more information. - Q_INVOKABLE void unlock(); - /// Forcibly remove all locks, destroying the object. - /// - /// @@unlock() should usually be preferred. - Q_INVOKABLE void forceUnlock(); - - [[nodiscard]] bool isRetained() const; - - static RetainableHook* qmlAttachedProperties(QObject* object); - -signals: - /// This signal is sent when the object would normally be destroyed. - /// - /// If all signal handlers return and no locks are in place, the object will be destroyed. - /// If at least one lock is present the object will be retained until all are removed. - void dropped(); - /// This signal is sent immediately before the object is destroyed. - /// At this point destruction cannot be interrupted. - void aboutToDestroy(); - - void retainedChanged(); - -private: - explicit RetainableHook(QObject* parent): QObject(parent) {} - - void unlocked(); - - uint refcount = 0; - // tracked separately so a warning can be given when unlock is called too many times, - // without affecting other lock sources such as RetainableLock. - uint explicitRefcount = 0; - Retainable* retainableFacet = nullptr; - bool inactive = true; - - friend class Retainable; -}; - -class Retainable { -public: - Retainable() = default; - virtual ~Retainable() = default; - Q_DISABLE_COPY_MOVE(Retainable); - - void retainedDestroy(); - [[nodiscard]] bool isRetained() const; - -protected: - virtual void retainFinished(); - -private: - RetainableHook* hook = nullptr; - bool retaining = false; - - friend class RetainableHook; -}; - -///! A helper for easily using Retainable. -/// A RetainableLock provides extra safety and ease of use for locking -/// @@Retainable objects. A retainable object can be locked by multiple -/// locks at once, and each lock re-exposes relevant properties -/// of the retained objects. -/// -/// #### Example -/// The code below will keep a retainable object alive for as long as the -/// RetainableLock exists. -/// -/// ```qml -/// RetainableLock { -/// object: aRetainableObject -/// locked: true -/// } -/// ``` -class RetainableLock: public QObject { - Q_OBJECT; - /// The object to lock. Must be @@Retainable. - Q_PROPERTY(QObject* object READ object WRITE setObject NOTIFY objectChanged); - /// If the object should be locked. - Q_PROPERTY(bool locked READ locked WRITE setLocked NOTIFY lockedChanged); - /// If the object is currently in a retained state. - Q_PROPERTY(bool retained READ isRetained NOTIFY retainedChanged); - QML_ELEMENT; - -public: - explicit RetainableLock(QObject* parent = nullptr): QObject(parent) {} - ~RetainableLock() override; - Q_DISABLE_COPY_MOVE(RetainableLock); - - [[nodiscard]] QObject* object() const; - void setObject(QObject* object); - - [[nodiscard]] bool locked() const; - void setLocked(bool locked); - - [[nodiscard]] bool isRetained() const; - -signals: - /// Rebroadcast of the object's @@Retainable.dropped(s). - void dropped(); - /// Rebroadcast of the object's @@Retainable.aboutToDestroy(s). - void aboutToDestroy(); - void retainedChanged(); - - void objectChanged(); - void lockedChanged(); - -private slots: - void onObjectDestroyed(); - -private: - QObject* mObject = nullptr; - RetainableHook* hook = nullptr; - bool mEnabled = false; -}; diff --git a/src/core/ringbuf.hpp b/src/core/ringbuf.hpp deleted file mode 100644 index a50a56da..00000000 --- a/src/core/ringbuf.hpp +++ /dev/null @@ -1,169 +0,0 @@ -#pragma once - -#include -#include -#include - -#include -#include -#include -#include - -// NOLINTBEGIN(cppcoreguidelines-pro-bounds-pointer-arithmetic) - -// capacity 0 buffer cannot be inserted into, only replaced with = -// this is NOT exception safe for constructors -template -class RingBuffer { -public: - explicit RingBuffer() = default; - explicit RingBuffer(qsizetype capacity): mCapacity(capacity) { - if (capacity > 0) this->createData(); - } - - ~RingBuffer() { this->deleteData(); } - - Q_DISABLE_COPY(RingBuffer); - - explicit RingBuffer(RingBuffer&& other) noexcept { *this = std::move(other); } - - RingBuffer& operator=(RingBuffer&& other) noexcept { - this->deleteData(); - this->data = other.data; - this->head = other.head; - this->mSize = other.mSize; - this->mCapacity = other.mCapacity; - other.data = nullptr; - other.head = -1; - return *this; - } - - // undefined if capacity is 0 - template - T& emplace(Args&&... args) { - auto i = (this->head + 1) % this->mCapacity; - - if (this->indexIsAllocated(i)) { - this->data[i].~T(); - } - - auto* slot = &this->data[i]; - new (&this->data[i]) T(std::forward(args)...); - - this->head = i; - if (this->mSize != this->mCapacity) this->mSize = i + 1; - - return *slot; - } - - void clear() { - if (this->head == -1) return; - - auto i = this->head; - - do { - i = (i + 1) % this->mSize; - this->data[i].~T(); - } while (i != this->head); - - this->mSize = 0; - this->head = -1; - } - - // negative indexes and >size indexes are undefined - [[nodiscard]] T& at(qsizetype i) { - auto bufferI = (this->head - i) % this->mCapacity; - if (bufferI < 0) bufferI += this->mCapacity; - return this->data[bufferI]; - } - - [[nodiscard]] const T& at(qsizetype i) const { - return const_cast*>(this)->at(i); // NOLINT - } - - [[nodiscard]] qsizetype size() const { return this->mSize; } - [[nodiscard]] qsizetype capacity() const { return this->mCapacity; } - -private: - void createData() { - if (this->data != nullptr) return; - this->data = - static_cast(::operator new(this->mCapacity * sizeof(T), std::align_val_t {alignof(T)})); - } - - void deleteData() { - this->clear(); - ::operator delete(this->data, std::align_val_t {alignof(T)}); - this->data = nullptr; - } - - bool indexIsAllocated(qsizetype index) { - return this->mSize == this->mCapacity || index <= this->head; - } - - T* data = nullptr; - qsizetype mCapacity = 0; - qsizetype head = -1; - qsizetype mSize = 0; -}; - -// ring buffer with the ability to look up elements by hash (single bucket) -template -class HashBuffer { -public: - explicit HashBuffer() = default; - explicit HashBuffer(qsizetype capacity): ring(capacity) {} - ~HashBuffer() = default; - - Q_DISABLE_COPY(HashBuffer); - explicit HashBuffer(HashBuffer&& other) noexcept: ring(other.ring) {} - - HashBuffer& operator=(HashBuffer&& other) noexcept { - this->ring = other.ring; - return *this; - } - - // returns the index of the given value or -1 if missing - [[nodiscard]] qsizetype indexOf(const T& value, T** slot = nullptr) { - auto hash = qHash(value); - - for (auto i = 0; i < this->size(); i++) { - auto& v = this->ring.at(i); - if (hash == v.first && value == v.second) { - if (slot != nullptr) *slot = &v.second; - return i; - } - } - - return -1; - } - - [[nodiscard]] qsizetype indexOf(const T& value, T const** slot = nullptr) const { - return const_cast*>(this)->indexOf(value, slot); // NOLINT - } - - template - T& emplace(Args&&... args) { - auto& entry = this->ring.emplace( - std::piecewise_construct, - std::forward_as_tuple(0), - std::forward_as_tuple(std::forward(args)...) - ); - - entry.first = qHash(entry.second); - return entry.second; - } - - void clear() { this->ring.clear(); } - - // negative indexes and >size indexes are undefined - [[nodiscard]] T& at(qsizetype i) { return this->ring.at(i).second; } - [[nodiscard]] const T& at(qsizetype i) const { return this->ring.at(i).second; } - [[nodiscard]] qsizetype size() const { return this->ring.size(); } - [[nodiscard]] qsizetype capacity() const { return this->ring.capacity(); } - -private: - RingBuffer> ring; -}; - -// NOLINTEND(cppcoreguidelines-pro-bounds-pointer-arithmetic) diff --git a/src/core/rootwrapper.cpp b/src/core/rootwrapper.cpp index b394af58..ed2ef4b7 100644 --- a/src/core/rootwrapper.cpp +++ b/src/core/rootwrapper.cpp @@ -8,19 +8,16 @@ #include #include #include -#include -#include #include -#include "../window/floatingwindow.hpp" #include "generation.hpp" #include "qmlglobal.hpp" #include "scan.hpp" +#include "shell.hpp" -RootWrapper::RootWrapper(QString rootPath, QString shellId) +RootWrapper::RootWrapper(QString rootPath) : 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); @@ -37,16 +34,18 @@ RootWrapper::RootWrapper(QString rootPath, QString shellId) RootWrapper::~RootWrapper() { // event loop may no longer be running so deleteLater is not an option if (this->generation != nullptr) { - this->generation->shutdown(); + delete this->generation->root; + this->generation->root = nullptr; } + + delete this->generation; } void RootWrapper::reloadGraph(bool hard) { - auto rootPath = QFileInfo(this->rootPath).dir(); - auto scanner = QmlScanner(rootPath); + auto scanner = QmlScanner(); scanner.scanQmlFile(this->rootPath); - auto* generation = new EngineGeneration(rootPath, std::move(scanner)); + auto* generation = new EngineGeneration(std::move(scanner)); generation->wrapper = this; // todo: move into EngineGeneration @@ -61,49 +60,33 @@ void RootWrapper::reloadGraph(bool hard) { url.setScheme("qsintercept"); auto component = QQmlComponent(generation->engine, url); - auto* newRoot = component.beginCreate(generation->engine->rootContext()); - - if (newRoot == nullptr) { - const QString error = "failed to create root component\n" + component.errorString(); - qWarning().noquote() << error; - generation->destroy(); - - if (this->generation != nullptr && this->generation->qsgInstance != nullptr) { - emit this->generation->qsgInstance->reloadFailed(error); - } + auto* obj = component.beginCreate(generation->engine->rootContext()); + if (obj == nullptr) { + qWarning() << component.errorString().toStdString().c_str(); + qWarning() << "failed to create root component"; + delete generation; return; } - if (auto* item = qobject_cast(newRoot)) { - auto* window = new FloatingWindowInterface(); - item->setParent(window); - item->setParentItem(window->contentItem()); - window->setWidth(static_cast(item->width())); - window->setHeight(static_cast(item->height())); - newRoot = window; + auto* newRoot = qobject_cast(obj); + if (newRoot == nullptr) { + qWarning() << "root component was not a Quickshell.ShellRoot"; + delete obj; + delete generation; + return; } generation->root = newRoot; component.completeCreate(); - if (this->generation) { - QObject::disconnect(this->generation, nullptr, this, nullptr); - } - - auto isReload = this->generation != nullptr; generation->onReload(hard ? nullptr : this->generation); - - if (hard && this->generation) { - this->generation->destroy(); - } - + if (hard) delete this->generation; this->generation = generation; qInfo() << "Configuration Loaded"; - QObject::connect(this->generation, &QObject::destroyed, this, &RootWrapper::generationDestroyed); QObject::connect( this->generation, &EngineGeneration::filesChanged, @@ -112,14 +95,8 @@ void RootWrapper::reloadGraph(bool hard) { ); this->onWatchFilesChanged(); - - if (isReload && this->generation->qsgInstance != nullptr) { - emit this->generation->qsgInstance->reloadCompleted(); - } } -void RootWrapper::generationDestroyed() { this->generation = nullptr; } - void RootWrapper::onWatchFilesChanged() { auto watchFiles = QuickshellSettings::instance()->watchFiles(); if (this->generation != nullptr) { diff --git a/src/core/rootwrapper.hpp b/src/core/rootwrapper.hpp index 02d7a143..7958ee5c 100644 --- a/src/core/rootwrapper.hpp +++ b/src/core/rootwrapper.hpp @@ -12,20 +12,18 @@ class RootWrapper: public QObject { Q_OBJECT; public: - explicit RootWrapper(QString rootPath, QString shellId); + explicit RootWrapper(QString rootPath); ~RootWrapper() override; Q_DISABLE_COPY_MOVE(RootWrapper); void reloadGraph(bool hard); private slots: - void generationDestroyed(); void onWatchFilesChanged(); void onWatchedFilesChanged(); private: QString rootPath; - QString shellId; EngineGeneration* generation = nullptr; QString originalWorkingDirectory; }; diff --git a/src/core/scan.cpp b/src/core/scan.cpp index 59ec05b6..f5f078aa 100644 --- a/src/core/scan.cpp +++ b/src/core/scan.cpp @@ -103,15 +103,7 @@ bool QmlScanner::scanQmlFile(const QString& path) { this->scanDir(currentdir.path()); for (auto& import: imports) { - QString ipath; - if (import.startsWith("root:")) { - auto path = import.sliced(5); - if (path.startsWith('/')) path = path.sliced(1); - ipath = this->rootPath.filePath(path); - } else { - ipath = currentdir.filePath(import); - } - + auto ipath = currentdir.filePath(import); auto cpath = QFileInfo(ipath).canonicalFilePath(); if (cpath.isEmpty()) { diff --git a/src/core/scan.hpp b/src/core/scan.hpp index e3071a88..32a6166d 100644 --- a/src/core/scan.hpp +++ b/src/core/scan.hpp @@ -1,7 +1,6 @@ #pragma once #include -#include #include #include #include @@ -11,8 +10,6 @@ Q_DECLARE_LOGGING_CATEGORY(logQmlScanner); // expects canonical paths class QmlScanner { public: - QmlScanner(const QDir& rootPath): rootPath(rootPath) {} - void scanDir(const QString& path); // returns if the file has a singleton bool scanQmlFile(const QString& path); @@ -20,7 +17,4 @@ public: QVector scannedDirs; QVector scannedFiles; QHash qmldirIntercepts; - -private: - QDir rootPath; }; diff --git a/src/core/scriptmodel.cpp b/src/core/scriptmodel.cpp deleted file mode 100644 index 259587c1..00000000 --- a/src/core/scriptmodel.cpp +++ /dev/null @@ -1,133 +0,0 @@ -#include "scriptmodel.hpp" -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include - -void ScriptModel::updateValuesUnique(const QVariantList& newValues) { - this->mValues.reserve(newValues.size()); - - auto iter = this->mValues.begin(); - auto newIter = newValues.begin(); - - while (true) { - if (newIter == newValues.end()) { - if (iter == this->mValues.end()) break; - - auto startIndex = static_cast(newValues.length()); - auto endIndex = static_cast(this->mValues.length() - 1); - - this->beginRemoveRows(QModelIndex(), startIndex, endIndex); - this->mValues.erase(iter, this->mValues.end()); - this->endRemoveRows(); - - break; - } else if (iter == this->mValues.end()) { - // Prior branch ensures length is at least 1. - auto startIndex = static_cast(this->mValues.length()); - auto endIndex = static_cast(newValues.length() - 1); - - this->beginInsertRows(QModelIndex(), startIndex, endIndex); - this->mValues.append(newValues.sliced(startIndex)); - this->endInsertRows(); - - break; - } else if (*newIter != *iter) { - auto oldIter = std::find(iter, this->mValues.end(), *newIter); - - if (oldIter != this->mValues.end()) { - if (std::find(newIter, newValues.end(), *iter) == newValues.end()) { - // Remove any entries we would otherwise move around that aren't in the new list. - auto startIter = iter; - - do { - ++iter; - } while (iter != this->mValues.end() - && std::find(newIter, newValues.end(), *iter) == newValues.end()); - - auto index = static_cast(std::distance(this->mValues.begin(), iter)); - auto startIndex = static_cast(std::distance(this->mValues.begin(), startIter)); - - this->beginRemoveRows(QModelIndex(), startIndex, index - 1); - iter = this->mValues.erase(startIter, iter); - this->endRemoveRows(); - } else { - // Advance iters to capture a whole move sequence as a single operation if possible. - auto oldStartIter = oldIter; - do { - ++oldIter; - ++newIter; - } while (oldIter != this->mValues.end() && newIter != newValues.end() - && *oldIter == *newIter); - - auto index = static_cast(std::distance(this->mValues.begin(), iter)); - auto oldStartIndex = - static_cast(std::distance(this->mValues.begin(), oldStartIter)); - auto oldIndex = static_cast(std::distance(this->mValues.begin(), oldIter)); - auto len = oldIndex - oldStartIndex; - - this->beginMoveRows(QModelIndex(), oldStartIndex, oldIndex - 1, QModelIndex(), index); - - // While it is possible to optimize this further, it is currently not worth the time. - for (auto i = 0; i != len; i++) { - this->mValues.move(oldStartIndex + i, index + i); - } - - iter = this->mValues.begin() + (index + len); - this->endMoveRows(); - } - } else { - auto startNewIter = newIter; - - do { - newIter++; - } while (newIter != newValues.end() - && std::find(iter, this->mValues.end(), *newIter) == this->mValues.end()); - - auto index = static_cast(std::distance(this->mValues.begin(), iter)); - auto newIndex = static_cast(std::distance(newValues.begin(), newIter)); - auto startNewIndex = static_cast(std::distance(newValues.begin(), startNewIter)); - auto len = newIndex - startNewIndex; - - this->beginInsertRows(QModelIndex(), index, index + len - 1); -#if QT_VERSION <= QT_VERSION_CHECK(6, 8, 0) - this->mValues.resize(this->mValues.length() + len); -#else - this->mValues.resizeForOverwrite(this->mValues.length() + len); -#endif - iter = this->mValues.begin() + index; // invalidated - std::move_backward(iter, this->mValues.end() - len, this->mValues.end()); - iter = std::copy(startNewIter, newIter, iter); - this->endInsertRows(); - } - } else { - ++iter; - ++newIter; - } - } -} - -void ScriptModel::setValues(const QVariantList& newValues) { - if (newValues == this->mValues) return; - this->updateValuesUnique(newValues); - emit this->valuesChanged(); -} - -qint32 ScriptModel::rowCount(const QModelIndex& parent) const { - if (parent != QModelIndex()) return 0; - return static_cast(this->mValues.length()); -} - -QVariant ScriptModel::data(const QModelIndex& index, qint32 role) const { - if (role != Qt::UserRole) return QVariant(); - return this->mValues.at(index.row()); -} - -QHash ScriptModel::roleNames() const { return {{Qt::UserRole, "modelData"}}; } diff --git a/src/core/scriptmodel.hpp b/src/core/scriptmodel.hpp deleted file mode 100644 index 10a42d6b..00000000 --- a/src/core/scriptmodel.hpp +++ /dev/null @@ -1,87 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include - -///! QML model reflecting a javascript expression -/// ScriptModel is a QML [Data Model] that generates model operations based on changes -/// to a javascript expression attached to @@values. -/// -/// ### When should I use this -/// ScriptModel should be used when you would otherwise use a javascript expression as a model, -/// [QAbstractItemModel] is accepted, and the data is likely to change over the lifetime of the program. -/// -/// When directly using a javascript expression as a model, types like @@QtQuick.Repeater or @@QtQuick.ListView -/// will destroy all created delegates, and re-create the entire list. In the case of @@QtQuick.ListView this -/// will also prevent animations from working. If you wrap your expression with ScriptModel, only new items -/// will be created, and ListView animations will work as expected. -/// -/// ### Example -/// ```qml -/// // Will cause all delegates to be re-created every time filterText changes. -/// @@QtQuick.Repeater { -/// model: myList.filter(entry => entry.name.startsWith(filterText)) -/// delegate: // ... -/// } -/// -/// // Will add and remove delegates only when required. -/// @@QtQuick.Repeater { -/// model: ScriptModel { -/// values: myList.filter(entry => entry.name.startsWith(filterText)) -/// } -/// -/// delegate: // ... -/// } -/// ``` -/// [QAbstractItemModel]: https://doc.qt.io/qt-6/qabstractitemmodel.html -/// [Data Model]: https://doc.qt.io/qt-6/qtquick-modelviewsdata-modelview.html#qml-data-models -class ScriptModel: public QAbstractListModel { - Q_OBJECT; - /// The list of values to reflect in the model. - /// > [!WARNING] ScriptModel currently only works with lists of *unique* values. - /// > There must not be any duplicates in the given list, or behavior of the model is undefined. - /// - /// > [!TIP] @@ObjectModel$s supplied by Quickshell types will only contain unique values, - /// > and can be used like so: - /// > - /// > ```qml - /// > ScriptModel { - /// > values: DesktopEntries.applications.values.filter(...) - /// > } - /// > ``` - /// > - /// > Note that we are using @@ObjectModel.values because it will cause @@ScriptModel.values - /// > to receive an update on change. - /// - /// > [!TIP] Most lists exposed by Quickshell are read-only. Some operations like `sort()` - /// > act on a list in-place and cannot be used directly on a list exposed by Quickshell. - /// > You can copy a list using spread syntax: `[...variable]` instead of `variable`. - /// > - /// > For example: - /// > ```qml - /// > ScriptModel { - /// > values: [...DesktopEntries.applications.values].sort(...) - /// > } - /// > ``` - Q_PROPERTY(QVariantList values READ values WRITE setValues NOTIFY valuesChanged); - QML_ELEMENT; - -public: - [[nodiscard]] const QVariantList& values() const { return this->mValues; } - void setValues(const QVariantList& newValues); - - [[nodiscard]] qint32 rowCount(const QModelIndex& parent) const override; - [[nodiscard]] QVariant data(const QModelIndex& index, qint32 role) const override; - [[nodiscard]] QHash roleNames() const override; - -signals: - void valuesChanged(); - -private: - QVariantList mValues; - - void updateValuesUnique(const QVariantList& newValues); -}; diff --git a/src/core/shell.hpp b/src/core/shell.hpp index 66b6ef6a..807f0275 100644 --- a/src/core/shell.hpp +++ b/src/core/shell.hpp @@ -8,7 +8,7 @@ #include "qmlglobal.hpp" #include "reload.hpp" -///! Optional root config element, allowing some settings to be specified inline. +///! Root config element class ShellRoot: public ReloadPropagator { Q_OBJECT; Q_PROPERTY(QuickshellSettings* settings READ settings CONSTANT); diff --git a/src/core/stacklist.hpp b/src/core/stacklist.hpp deleted file mode 100644 index 41dc58ee..00000000 --- a/src/core/stacklist.hpp +++ /dev/null @@ -1,173 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include -#include - -#include -#include - -template -class StackList { -public: - T& operator[](size_t i) { - if (i < N) { - return this->array[i]; - } else { - return this->vec[i - N]; - } - } - - const T& operator[](size_t i) const { - return const_cast*>(this)->operator[](i); // NOLINT - } - - void push(const T& value) { - if (this->size < N) { - this->array[this->size] = value; - } else { - this->vec.push_back(value); - } - - ++this->size; - } - - [[nodiscard]] size_t length() const { return this->size; } - [[nodiscard]] bool isEmpty() const { return this->size == 0; } - - [[nodiscard]] bool operator==(const StackList& other) const { - if (other.size != this->size) return false; - - for (size_t i = 0; i != this->size; ++i) { - if (this->operator[](i) != other[i]) return false; - } - - return true; - } - - [[nodiscard]] QList toList() const { - QList list; - list.reserve(this->size); - - for (const auto& entry: *this) { - list.push_back(entry); - } - - return list; - } - - template - struct BaseIterator { - using iterator_category = std::bidirectional_iterator_tag; - using difference_type = int64_t; - using value_type = IT; - using pointer = IT*; - using reference = IT&; - - BaseIterator() = default; - explicit BaseIterator(ListPtr list, size_t i): list(list), i(i) {} - - reference operator*() const { return this->list->operator[](this->i); } - pointer operator->() const { return &**this; } - - Self& operator++() { - ++this->i; - return *static_cast(this); - } - - Self& operator--() { - --this->i; - return *static_cast(this); - } - - Self operator++(int) { - auto v = *this; - this->operator++(); - return v; - } - - Self operator--(int) { - auto v = *this; - this->operator--(); - return v; - } - - difference_type operator-(const Self& other) { - return static_cast(this->i) - static_cast(other.i); - } - - Self& operator+(difference_type offset) { - return Self(this->list, static_cast(this->i) + offset); - } - - [[nodiscard]] bool operator==(const Self& other) const { - return this->list == other.list && this->i == other.i; - } - - [[nodiscard]] bool operator!=(const Self& other) const { return !(*this == other); } - - private: - ListPtr list = nullptr; - size_t i = 0; - }; - - struct Iterator: public BaseIterator*, T> { - Iterator() = default; - Iterator(StackList* list, size_t i) - : BaseIterator*, T>(list, i) {} - }; - - struct ConstIterator: public BaseIterator*, const T> { - ConstIterator() = default; - ConstIterator(const StackList* list, size_t i) - : BaseIterator*, const T>(list, i) {} - }; - - [[nodiscard]] Iterator begin() { return Iterator(this, 0); } - [[nodiscard]] Iterator end() { return Iterator(this, this->size); } - - [[nodiscard]] ConstIterator begin() const { return ConstIterator(this, 0); } - [[nodiscard]] ConstIterator end() const { return ConstIterator(this, this->size); } - - [[nodiscard]] bool isContiguous() const { return this->vec.empty(); } - [[nodiscard]] const T* pArray() const { return this->array.data(); } - [[nodiscard]] size_t dataLength() const { return this->size * sizeof(T); } - - const T* populateAlloc(void* alloc) const { - auto arraylen = std::min(this->size, N) * sizeof(T); - memcpy(alloc, this->array.data(), arraylen); - - if (!this->vec.empty()) { - memcpy( - static_cast(alloc) + arraylen, // NOLINT - this->vec.data(), - this->vec.size() * sizeof(T) - ); - } - - return static_cast(alloc); - } - -private: - std::array array {}; - std::vector vec; - size_t size = 0; -}; - -// might be incorrectly aligned depending on type -// #define STACKLIST_ALLOCA_VIEW(list) ((list).isContiguous() ? (list).pArray() : (list).populateAlloc(alloca((list).dataLength()))) - -// NOLINTBEGIN -#define STACKLIST_VLA_VIEW(type, list, var) \ - const type* var; \ - type var##Data[(list).length()]; \ - if ((list).isContiguous()) { \ - (var) = (list).pArray(); \ - } else { \ - (list).populateAlloc(var##Data); \ - (var) = var##Data; \ - } -// NOLINTEND diff --git a/src/core/test/CMakeLists.txt b/src/core/test/CMakeLists.txt index bb49192d..d0191ee9 100644 --- a/src/core/test/CMakeLists.txt +++ b/src/core/test/CMakeLists.txt @@ -1,10 +1,8 @@ function (qs_test name) add_executable(${name} ${ARGN}) - target_link_libraries(${name} PRIVATE Qt::Quick Qt::Test quickshell-core quickshell-window) + target_link_libraries(${name} PRIVATE ${QT_DEPS} Qt6::Test quickshell-core) add_test(NAME ${name} WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}" COMMAND $) endfunction() +qs_test(popupwindow popupwindow.cpp) qs_test(transformwatcher transformwatcher.cpp) -qs_test(ringbuffer ringbuf.cpp) -qs_test(scriptmodel scriptmodel.cpp) -qs_test(stacklist stacklist.cpp) diff --git a/src/window/test/popupwindow.cpp b/src/core/test/popupwindow.cpp similarity index 100% rename from src/window/test/popupwindow.cpp rename to src/core/test/popupwindow.cpp diff --git a/src/core/test/popupwindow.hpp b/src/core/test/popupwindow.hpp index e69de29b..bebc5154 100644 --- a/src/core/test/popupwindow.hpp +++ b/src/core/test/popupwindow.hpp @@ -0,0 +1,18 @@ +#pragma once + +#include +#include + +class TestPopupWindow: public QObject { + Q_OBJECT; + +private slots: + void initiallyVisible(); + void reloadReparent(); + void reloadUnparent(); + void invisibleWithoutParent(); + void moveWithParent(); + void attachParentLate(); + void reparentLate(); + void xMigrationFix(); +}; diff --git a/src/core/test/ringbuf.cpp b/src/core/test/ringbuf.cpp deleted file mode 100644 index 4f114796..00000000 --- a/src/core/test/ringbuf.cpp +++ /dev/null @@ -1,125 +0,0 @@ -#include "ringbuf.hpp" -#include - -#include -#include -#include -#include - -#include "../ringbuf.hpp" - -TestObject::TestObject(quint32* count): count(count) { - (*this->count)++; - qDebug() << "Created TestObject" << this << "- count is now" << *this->count; -} - -TestObject::~TestObject() { - (*this->count)--; - qDebug() << "Destroyed TestObject" << this << "- count is now" << *this->count; -} - -void TestRingBuffer::fill() { - quint32 counter = 0; - auto rb = RingBuffer(3); - QCOMPARE(rb.capacity(), 3); - - qInfo() << "adding test objects"; - auto* n1 = &rb.emplace(&counter); - auto* n2 = &rb.emplace(&counter); - auto* n3 = &rb.emplace(&counter); - QCOMPARE(counter, 3); - QCOMPARE(rb.size(), 3); - QCOMPARE(&rb.at(0), n3); - QCOMPARE(&rb.at(1), n2); - QCOMPARE(&rb.at(2), n1); - - qInfo() << "replacing last object with new one"; - auto* n4 = &rb.emplace(&counter); - QCOMPARE(counter, 3); - QCOMPARE(rb.size(), 3); - QCOMPARE(&rb.at(0), n4); - QCOMPARE(&rb.at(1), n3); - QCOMPARE(&rb.at(2), n2); - - qInfo() << "replacing the rest"; - auto* n5 = &rb.emplace(&counter); - auto* n6 = &rb.emplace(&counter); - QCOMPARE(counter, 3); - QCOMPARE(rb.size(), 3); - QCOMPARE(&rb.at(0), n6); - QCOMPARE(&rb.at(1), n5); - QCOMPARE(&rb.at(2), n4); - - qInfo() << "clearing buffer"; - rb.clear(); - QCOMPARE(counter, 0); - QCOMPARE(rb.size(), 0); -} - -void TestRingBuffer::clearPartial() { - quint32 counter = 0; - auto rb = RingBuffer(2); - - qInfo() << "adding object to buffer"; - auto* n1 = &rb.emplace(&counter); - QCOMPARE(counter, 1); - QCOMPARE(rb.size(), 1); - QCOMPARE(&rb.at(0), n1); - - qInfo() << "clearing buffer"; - rb.clear(); - QCOMPARE(counter, 0); - QCOMPARE(rb.size(), 0); -} - -void TestRingBuffer::move() { - quint32 counter = 0; - - { - auto rb1 = RingBuffer(1); - - qInfo() << "adding object to first buffer"; - auto* n1 = &rb1.emplace(&counter); - QCOMPARE(counter, 1); - QCOMPARE(rb1.size(), 1); - QCOMPARE(&rb1.at(0), n1); - - qInfo() << "move constructing new buffer"; - auto rb2 = RingBuffer(std::move(rb1)); - QCOMPARE(counter, 1); - QCOMPARE(rb2.size(), 1); - QCOMPARE(&rb2.at(0), n1); - - qInfo() << "move assigning new buffer"; - auto rb3 = RingBuffer(); - rb3 = std::move(rb2); - QCOMPARE(counter, 1); - QCOMPARE(rb3.size(), 1); - QCOMPARE(&rb3.at(0), n1); - } - - QCOMPARE(counter, 0); -} - -void TestRingBuffer::hashLookup() { - auto hb = HashBuffer(3); - - qInfo() << "inserting 1,2,3 into HashBuffer"; - hb.emplace(1); - hb.emplace(2); - hb.emplace(3); - - qInfo() << "checking lookups"; - QCOMPARE(hb.indexOf(3), 0); - QCOMPARE(hb.indexOf(2), 1); - QCOMPARE(hb.indexOf(1), 2); - - qInfo() << "adding 4"; - hb.emplace(4); - QCOMPARE(hb.indexOf(4), 0); - QCOMPARE(hb.indexOf(3), 1); - QCOMPARE(hb.indexOf(2), 2); - QCOMPARE(hb.indexOf(1), -1); -} - -QTEST_MAIN(TestRingBuffer); diff --git a/src/core/test/ringbuf.hpp b/src/core/test/ringbuf.hpp deleted file mode 100644 index 1413031c..00000000 --- a/src/core/test/ringbuf.hpp +++ /dev/null @@ -1,27 +0,0 @@ -#pragma once - -#include -#include -#include -#include - -class TestObject { -public: - explicit TestObject(quint32* count); - ~TestObject(); - Q_DISABLE_COPY_MOVE(TestObject); - -private: - quint32* count; -}; - -class TestRingBuffer: public QObject { - Q_OBJECT; - -private slots: - static void fill(); - static void clearPartial(); - static void move(); - - static void hashLookup(); -}; diff --git a/src/core/test/scriptmodel.cpp b/src/core/test/scriptmodel.cpp deleted file mode 100644 index 66746832..00000000 --- a/src/core/test/scriptmodel.cpp +++ /dev/null @@ -1,181 +0,0 @@ -#include "scriptmodel.hpp" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "../scriptmodel.hpp" - -using OpList = QList; - -bool ModelOperation::operator==(const ModelOperation& other) const { - return other.operation == this->operation && other.index == this->index - && other.length == this->length && other.destIndex == this->destIndex; -} - -// NOLINTNEXTLINE(misc-use-internal-linkage) -QDebug& operator<<(QDebug& debug, const ModelOperation& op) { - auto saver = QDebugStateSaver(debug); - debug.nospace(); - - switch (op.operation) { - case ModelOperation::Insert: debug << "Insert"; break; - case ModelOperation::Remove: debug << "Remove"; break; - case ModelOperation::Move: debug << "Move"; break; - } - - debug << "(i: " << op.index << ", l: " << op.length; - - if (op.destIndex != -1) { - debug << ", d: " << op.destIndex; - } - - debug << ')'; - - return debug; -} - -// NOLINTNEXTLINE(misc-use-internal-linkage) -QDebug& operator<<(QDebug& debug, const QVariantList& list) { - auto str = QString(); - - for (const auto& var: list) { - if (var.canConvert()) { - str += var.value(); - } else { - qFatal() << "QVariantList debug overridden in test"; - } - } - - debug << str; - return debug; -} - -void TestScriptModel::unique_data() { - QTest::addColumn("oldstr"); - QTest::addColumn("newstr"); - QTest::addColumn("operations"); - - QTest::addRow("append") << "ABCD" << "ABCDEFG" << OpList({{ModelOperation::Insert, 4, 3}}); - - QTest::addRow("prepend") << "EFG" << "ABCDEFG" << OpList({{ModelOperation::Insert, 0, 4}}); - - QTest::addRow("insert") << "ABFG" << "ABCDEFG" << OpList({{ModelOperation::Insert, 2, 3}}); - - QTest::addRow("chop") << "ABCDEFG" << "ABCD" << OpList({{ModelOperation::Remove, 4, 3}}); - - QTest::addRow("slice") << "ABCDEFG" << "DEFG" << OpList({{ModelOperation::Remove, 0, 3}}); - - QTest::addRow("remove_mid") << "ABCDEFG" << "ABFG" << OpList({{ModelOperation::Remove, 2, 3}}); - - QTest::addRow("move_single") << "ABCDEFG" << "AFBCDEG" - << OpList({{ModelOperation::Move, 5, 1, 1}}); - - QTest::addRow("move_range") << "ABCDEFG" << "ADEFBCG" - << OpList({{ModelOperation::Move, 3, 3, 1}}); - - // beginning to end is the same operation - QTest::addRow("move_end_to_beginning") - << "ABCDEFG" << "EFGABCD" << OpList({{ModelOperation::Move, 4, 3, 0}}); - - QTest::addRow("move_overlapping") - << "ABCDEFG" << "ABDEFCG" << OpList({{ModelOperation::Move, 3, 3, 2}}); - - // Ensure iterators arent skipping anything at the end of operations by performing - // multiple back to back. - - QTest::addRow("insert_state_ok") << "ABCDEFG" << "ABXXEFG" - << OpList({ - {ModelOperation::Insert, 2, 2}, // ABXXCDEFG - {ModelOperation::Remove, 4, 2}, // ABXXEFG - }); - - QTest::addRow("remove_state_ok") << "ABCDEFG" << "ABFGE" - << OpList({ - {ModelOperation::Remove, 2, 2}, // ABEFG - {ModelOperation::Move, 3, 2, 2}, // ABFGE - }); - - QTest::addRow("move_state_ok") << "ABCDEFG" << "ABEFXYCDG" - << OpList({ - {ModelOperation::Move, 4, 2, 2}, // ABEFCDG - {ModelOperation::Insert, 4, 2}, // ABEFXYCDG - }); -} - -void TestScriptModel::unique() { - QFETCH(const QString, oldstr); - QFETCH(const QString, newstr); - QFETCH(OpList, operations); - - auto strToVariantList = [](const QString& str) -> QVariantList { - QVariantList list; - - for (auto c: str) { - list.emplace_back(c); - } - - return list; - }; - - auto oldlist = strToVariantList(oldstr); - auto newlist = strToVariantList(newstr); - - auto model = ScriptModel(); - auto modelTester = QAbstractItemModelTester(&model); - - OpList actualOperations; - - auto onInsert = [&](const QModelIndex& parent, int first, int last) { - QCOMPARE(parent, QModelIndex()); - actualOperations << ModelOperation(ModelOperation::Insert, first, last - first + 1); - }; - - auto onRemove = [&](const QModelIndex& parent, int first, int last) { - QCOMPARE(parent, QModelIndex()); - actualOperations << ModelOperation(ModelOperation::Remove, first, last - first + 1); - }; - - auto onMove = [&](const QModelIndex& sourceParent, - int sourceStart, - int sourceEnd, - const QModelIndex& destParent, - int destStart) { - QCOMPARE(sourceParent, QModelIndex()); - QCOMPARE(destParent, QModelIndex()); - actualOperations << ModelOperation( - ModelOperation::Move, - sourceStart, - sourceEnd - sourceStart + 1, - destStart - ); - }; - - QObject::connect(&model, &QAbstractItemModel::rowsInserted, &model, onInsert); - QObject::connect(&model, &QAbstractItemModel::rowsRemoved, &model, onRemove); - QObject::connect(&model, &QAbstractItemModel::rowsMoved, &model, onMove); - - model.setValues(oldlist); - QCOMPARE_EQ(model.values(), oldlist); - QCOMPARE_EQ( - actualOperations, - OpList({{ModelOperation::Insert, 0, static_cast(oldlist.length())}}) - ); - - actualOperations.clear(); - - model.setValues(newlist); - QCOMPARE_EQ(model.values(), newlist); - QCOMPARE_EQ(actualOperations, operations); -} - -QTEST_MAIN(TestScriptModel); diff --git a/src/core/test/scriptmodel.hpp b/src/core/test/scriptmodel.hpp deleted file mode 100644 index 3b50b328..00000000 --- a/src/core/test/scriptmodel.hpp +++ /dev/null @@ -1,37 +0,0 @@ -#pragma once - -#include -#include -#include -#include - -struct ModelOperation { - enum Enum : quint8 { - Insert, - Remove, - Move, - }; - - ModelOperation(Enum operation, qint32 index, qint32 length, qint32 destIndex = -1) - : operation(operation) - , index(index) - , length(length) - , destIndex(destIndex) {} - - Enum operation; - qint32 index = 0; - qint32 length = 0; - qint32 destIndex = -1; - - [[nodiscard]] bool operator==(const ModelOperation& other) const; -}; - -QDebug& operator<<(QDebug& debug, const ModelOperation& op); - -class TestScriptModel: public QObject { - Q_OBJECT; - -private slots: - static void unique_data(); // NOLINT - static void unique(); -}; diff --git a/src/core/test/stacklist.cpp b/src/core/test/stacklist.cpp deleted file mode 100644 index 9b981729..00000000 --- a/src/core/test/stacklist.cpp +++ /dev/null @@ -1,92 +0,0 @@ -#include "stacklist.hpp" -#include - -#include -#include -#include - -#include "../stacklist.hpp" - -void TestStackList::push() { - StackList list; - - list.push(1); - list.push(2); - - QCOMPARE_EQ(list.toList(), QList({1, 2})); - QCOMPARE_EQ(list.length(), 2); -} - -void TestStackList::pushAndGrow() { - StackList list; - - list.push(1); - list.push(2); - list.push(3); - list.push(4); - - QCOMPARE_EQ(list.toList(), QList({1, 2, 3, 4})); - QCOMPARE_EQ(list.length(), 4); -} - -void TestStackList::copy() { - StackList list; - - list.push(1); - list.push(2); - list.push(3); - list.push(4); - - QCOMPARE_EQ(list.toList(), QList({1, 2, 3, 4})); - QCOMPARE_EQ(list.length(), 4); - - auto list2 = list; - - QCOMPARE_EQ(list2.toList(), QList({1, 2, 3, 4})); - QCOMPARE_EQ(list2.length(), 4); - QCOMPARE_EQ(list2, list); -} - -void TestStackList::viewVla() { - StackList list; - - list.push(1); - list.push(2); - - QCOMPARE_EQ(list.toList(), QList({1, 2})); - QCOMPARE_EQ(list.length(), 2); - - STACKLIST_VLA_VIEW(int, list, listView); - - QList ql; - - for (size_t i = 0; i != list.length(); ++i) { - ql.push_back(listView[i]); // NOLINT - } - - QCOMPARE_EQ(ql, list.toList()); -} - -void TestStackList::viewVlaGrown() { - StackList list; - - list.push(1); - list.push(2); - list.push(3); - list.push(4); - - QCOMPARE_EQ(list.toList(), QList({1, 2, 3, 4})); - QCOMPARE_EQ(list.length(), 4); - - STACKLIST_VLA_VIEW(int, list, listView); - - QList ql; - - for (size_t i = 0; i != list.length(); ++i) { - ql.push_back(listView[i]); // NOLINT - } - - QCOMPARE_EQ(ql, list.toList()); -} - -QTEST_MAIN(TestStackList); diff --git a/src/core/test/stacklist.hpp b/src/core/test/stacklist.hpp deleted file mode 100644 index f582761d..00000000 --- a/src/core/test/stacklist.hpp +++ /dev/null @@ -1,15 +0,0 @@ -#pragma once - -#include -#include - -class TestStackList: public QObject { - Q_OBJECT; - -private slots: - static void push(); - static void pushAndGrow(); - static void copy(); - static void viewVla(); - static void viewVlaGrown(); -}; diff --git a/src/core/transformwatcher.cpp b/src/core/transformwatcher.cpp index 6fc7c34a..697dfc56 100644 --- a/src/core/transformwatcher.cpp +++ b/src/core/transformwatcher.cpp @@ -7,7 +7,6 @@ #include #include #include -#include void TransformWatcher::resolveChains(QQuickItem* a, QQuickItem* b, QQuickItem* commonParent) { if (a == nullptr || b == nullptr) return; @@ -83,10 +82,7 @@ void TransformWatcher::linkItem(QQuickItem* item) const { QObject::connect(item, &QQuickItem::parentChanged, this, &TransformWatcher::recalcChains); QObject::connect(item, &QQuickItem::windowChanged, this, &TransformWatcher::recalcChains); - - if (item != this->mA && item != this->mB) { - QObject::connect(item, &QObject::destroyed, this, &TransformWatcher::itemDestroyed); - } + QObject::connect(item, &QObject::destroyed, this, &TransformWatcher::recalcChains); } void TransformWatcher::linkChains() { @@ -107,18 +103,6 @@ void TransformWatcher::unlinkChains() { for (auto* item: this->childChain) { QObject::disconnect(item, nullptr, this, nullptr); } - - // relink a and b destruction notifications - if (this->mA != nullptr) { - QObject::connect(this->mA, &QObject::destroyed, this, &TransformWatcher::aDestroyed); - } - - if (this->mB != nullptr) { - QObject::connect(this->mB, &QObject::destroyed, this, &TransformWatcher::bDestroyed); - } - - this->parentChain.clear(); - this->childChain.clear(); } void TransformWatcher::recalcChains() { @@ -127,57 +111,26 @@ void TransformWatcher::recalcChains() { this->linkChains(); } -void TransformWatcher::itemDestroyed() { - auto destroyed = - this->parentChain.removeOne(this->sender()) || this->childChain.removeOne(this->sender()); - - if (destroyed) this->recalcChains(); -} - QQuickItem* TransformWatcher::a() const { return this->mA; } void TransformWatcher::setA(QQuickItem* a) { if (this->mA == a) return; - if (this->mA != nullptr) QObject::disconnect(this->mA, nullptr, this, nullptr); this->mA = a; - - if (this->mA != nullptr) { - QObject::connect(this->mA, &QObject::destroyed, this, &TransformWatcher::aDestroyed); - } - this->recalcChains(); } -void TransformWatcher::aDestroyed() { - this->mA = nullptr; - this->unlinkChains(); - emit this->aChanged(); -} - QQuickItem* TransformWatcher::b() const { return this->mB; } void TransformWatcher::setB(QQuickItem* b) { if (this->mB == b) return; - if (this->mB != nullptr) QObject::disconnect(this->mB, nullptr, this, nullptr); this->mB = b; - - if (this->mB != nullptr) { - QObject::connect(this->mB, &QObject::destroyed, this, &TransformWatcher::bDestroyed); - } - this->recalcChains(); } -void TransformWatcher::bDestroyed() { - this->mB = nullptr; - this->unlinkChains(); - emit this->bChanged(); -} - QQuickItem* TransformWatcher::commonParent() const { return this->mCommonParent; } void TransformWatcher::setCommonParent(QQuickItem* commonParent) { if (this->mCommonParent == commonParent) return; this->mCommonParent = commonParent; - this->recalcChains(); + this->resolveChains(); } diff --git a/src/core/transformwatcher.hpp b/src/core/transformwatcher.hpp index 8efa9399..d7174e4c 100644 --- a/src/core/transformwatcher.hpp +++ b/src/core/transformwatcher.hpp @@ -13,7 +13,7 @@ class TestTransformWatcher; ///! Monitor of all geometry changes between two objects. /// The TransformWatcher monitors all properties that affect the geometry -/// of two @@QtQuick.Item$s relative to eachother. +/// of two `Item`s relative to eachother. /// /// > [!INFO] The algorithm responsible for determining the relationship /// > between `a` and `b` is biased towards `a` being a parent of `b`, @@ -60,9 +60,6 @@ signals: private slots: void recalcChains(); - void itemDestroyed(); - void aDestroyed(); - void bDestroyed(); private: void resolveChains(QQuickItem* a, QQuickItem* b, QQuickItem* commonParent); diff --git a/src/core/types.cpp b/src/core/types.cpp deleted file mode 100644 index 5ed63a02..00000000 --- a/src/core/types.cpp +++ /dev/null @@ -1,23 +0,0 @@ -#include "types.hpp" - -#include -#include -#include - -QRect Box::qrect() const { return {this->x, this->y, this->w, this->h}; } - -bool Box::operator==(const Box& other) const { - return this->x == other.x && this->y == other.y && this->w == other.w && this->h == other.h; -} - -QDebug operator<<(QDebug debug, const Box& box) { - auto saver = QDebugStateSaver(debug); - debug.nospace() << "Box(" << box.x << ',' << box.y << ' ' << box.w << 'x' << box.h << ')'; - return debug; -} - -Qt::Edges Edges::toQt(Edges::Flags edges) { return Qt::Edges(edges.toInt()); } - -bool Edges::isOpposing(Edges::Flags edges) { - return edges.testFlags(Edges::Top | Edges::Bottom) || edges.testFlags(Edges::Left | Edges::Right); -} diff --git a/src/core/types.hpp b/src/core/types.hpp deleted file mode 100644 index 0adc85c0..00000000 --- a/src/core/types.hpp +++ /dev/null @@ -1,72 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include -#include -#include - -class Box { - Q_GADGET; - Q_PROPERTY(qint32 x MEMBER x); - Q_PROPERTY(qint32 y MEMBER y); - Q_PROPERTY(qint32 w MEMBER w); - Q_PROPERTY(qint32 h MEMBER h); - Q_PROPERTY(qint32 width MEMBER w); - Q_PROPERTY(qint32 height MEMBER h); - QML_CONSTRUCTIBLE_VALUE; - QML_VALUE_TYPE(box); - -public: - explicit Box() = default; - Box(qint32 x, qint32 y, qint32 w, qint32 h): x(x), y(y), w(w), h(h) {} - - Q_INVOKABLE Box(const QRect& rect): x(rect.x()), y(rect.y()), w(rect.width()), h(rect.height()) {} - Q_INVOKABLE Box(const QPoint& rect): x(rect.x()), y(rect.y()) {} - - Q_INVOKABLE Box(const QRectF& rect) - : x(static_cast(rect.x())) - , y(static_cast(rect.y())) - , w(static_cast(rect.width())) - , h(static_cast(rect.height())) {} - - Q_INVOKABLE Box(const QPointF& rect) - : x(static_cast(rect.x())) - , y(static_cast(rect.y())) {} - - bool operator==(const Box& other) const; - - qint32 x = 0; - qint32 y = 0; - qint32 w = 0; - qint32 h = 0; - - [[nodiscard]] QRect qrect() const; -}; - -QDebug operator<<(QDebug debug, const Box& box); - -///! Top Left Right Bottom flags. -/// Edge flags can be combined with the `|` operator. -namespace Edges { // NOLINT -Q_NAMESPACE; -QML_NAMED_ELEMENT(Edges); - -enum Enum : quint8 { - None = 0, - Top = Qt::TopEdge, - Left = Qt::LeftEdge, - Right = Qt::RightEdge, - Bottom = Qt::BottomEdge, -}; -Q_ENUM_NS(Enum); -Q_DECLARE_FLAGS(Flags, Enum); - -Qt::Edges toQt(Flags edges); -bool isOpposing(Flags edges); - -}; // namespace Edges - -Q_DECLARE_OPERATORS_FOR_FLAGS(Edges::Flags); diff --git a/src/core/util.hpp b/src/core/util.hpp deleted file mode 100644 index 719c9201..00000000 --- a/src/core/util.hpp +++ /dev/null @@ -1,313 +0,0 @@ -#pragma once -#include -#include - -#include -#include -#include -#include -#include -#include -#include - -template -struct StringLiteral { - constexpr StringLiteral(const char (&str)[length]) { // NOLINT - std::copy_n(str, length, this->value); - } - - constexpr operator const char*() const noexcept { return this->value; } - operator QLatin1StringView() const { return QLatin1String(this->value, length); } - - char value[length]; // NOLINT -}; - -template -struct StringLiteral16 { - constexpr StringLiteral16(const char16_t (&str)[length]) { // NOLINT - std::copy_n(str, length, this->value); - } - - [[nodiscard]] constexpr const QChar* qCharPtr() const noexcept { - return std::bit_cast(&this->value); - } - - [[nodiscard]] Q_ALWAYS_INLINE operator QString() const noexcept { - return QString::fromRawData(this->qCharPtr(), static_cast(length - 1)); - } - - [[nodiscard]] Q_ALWAYS_INLINE operator QStringView() const noexcept { - return QStringView(this->qCharPtr(), static_cast(length - 1)); - } - - char16_t value[length]; // NOLINT -}; - -// NOLINTBEGIN -#define DROP_EMIT(object, func) \ - DropEmitter(object, static_cast([](typeof(object) o) { o->func(); })) - -#define DROP_EMIT_IF(cond, object, func) (cond) ? DROP_EMIT(object, func) : DropEmitter() - -#define DEFINE_DROP_EMIT_IF(cond, object, func) DropEmitter func = DROP_EMIT_IF(cond, object, func) - -#define DROP_EMIT_SET(object, local, member, signal) \ - auto signal = DropEmitter(); \ - if (local == object->member) { \ - object->member = local; \ - signal = DROP_EMIT(object, signal); \ - } -// NOLINTEND - -class DropEmitter { -public: - Q_DISABLE_COPY(DropEmitter); - template - DropEmitter(O* object, void (*signal)(O*)) - : object(object) - , signal(*reinterpret_cast(signal)) {} - - DropEmitter() = default; - - DropEmitter(DropEmitter&& other) noexcept: object(other.object), signal(other.signal) { - other.object = nullptr; - } - - ~DropEmitter() { this->call(); } - - DropEmitter& operator=(DropEmitter&& other) noexcept { - this->object = other.object; - this->signal = other.signal; - other.object = nullptr; - return *this; - } - - explicit operator bool() const noexcept { return this->object; } - - void call() { - if (!this->object) return; - this->signal(this->object); - this->object = nullptr; - } - - // orders calls for multiple emitters (instead of reverse definition order) - template - static void call(Args&... args) { - (args.call(), ...); - } - -private: - void* object = nullptr; - void (*signal)(void*) = nullptr; -}; - -// NOLINTBEGIN -#define DECLARE_MEMBER(class, name, member, signal) \ - using M_##name = MemberMetadata<&class ::member, &class ::signal> - -#define DECLARE_MEMBER_NS(class, name, member) using M_##name = MemberMetadata<&class ::member> - -#define DECLARE_MEMBER_GET(name) [[nodiscard]] M_##name::Ref name() const -#define DECLARE_MEMBER_SET(name, setter) M_##name::Ret setter(M_##name::Ref value) - -#define DECLARE_MEMBER_GETSET(name, setter) \ - DECLARE_MEMBER_GET(name); \ - DECLARE_MEMBER_SET(name, setter) - -#define DECLARE_MEMBER_SETONLY(class, name, setter, member, signal) DECLARE_MEMBER(cl - -#define DECLARE_MEMBER_FULL(class, name, setter, member, signal) \ - DECLARE_MEMBER(class, name, member, signal); \ - DECLARE_MEMBER_GETSET(name, setter) - -#define DECLARE_MEMBER_WITH_GET(class, name, member, signal) \ - DECLARE_MEMBER(class, name, member, signal); \ - \ -public: \ - DECLARE_MEMBER_GET(name); \ - \ -private: - -#define DECLARE_PRIVATE_MEMBER(class, name, setter, member, signal) \ - DECLARE_MEMBER_WITH_GET(class, name, member, signal); \ - DECLARE_MEMBER_SET(name, setter); - -#define DECLARE_PMEMBER(type, name) using M_##name = PseudomemberMetadata; -#define DECLARE_PMEMBER_NS(type, name) using M_##name = PseudomemberMetadata; - -#define DECLARE_PMEMBER_FULL(type, name, setter) \ - DECLARE_PMEMBER(type, name); \ - DECLARE_MEMBER_GETSET(name, setter) - -#define DECLARE_PMEMBER_WITH_GET(type, name) \ - DECLARE_PMEMBER(type, name); \ - \ -public: \ - DECLARE_MEMBER_GET(name); \ - \ -private: - -#define DECLARE_PRIVATE_PMEMBER(type, name, setter) \ - DECLARE_PMEMBER_WITH_GET(type, name); \ - DECLARE_MEMBER_SET(name, setter); - -#define DEFINE_PMEMBER_GET_M(Class, Member, name) Member::Ref Class::name() const -#define DEFINE_PMEMBER_GET(Class, name) DEFINE_PMEMBER_GET_M(Class, Class::M_##name, name) - -#define DEFINE_MEMBER_GET_M(Class, Member, name) \ - DEFINE_PMEMBER_GET_M(Class, Member, name) { return Member::get(this); } - -#define DEFINE_MEMBER_GET(Class, name) DEFINE_MEMBER_GET_M(Class, Class::M_##name, name) - -#define DEFINE_MEMBER_SET_M(Class, Member, setter) \ - Member::Ret Class::setter(Member::Ref value) { return Member::set(this, value); } - -#define DEFINE_MEMBER_SET(Class, name, setter) DEFINE_MEMBER_SET_M(Class, Class::M_##name, setter) - -#define DEFINE_MEMBER_GETSET(Class, name, setter) \ - DEFINE_MEMBER_GET(Class, name) \ - DEFINE_MEMBER_SET(Class, name, setter) - -#define MEMBER_EMIT(name) std::remove_reference_t::M_##name::emitter(this) -// NOLINTEND - -template -struct MemberPointerTraits; - -template -struct MemberPointerTraits { - using Class = C; - using Type = T; -}; - -template -class MemberMetadata { - using Traits = MemberPointerTraits; - using Class = Traits::Class; - -public: - using Type = Traits::Type; - using Ref = const Type&; - using Ret = std::conditional_t, void, DropEmitter>; - - static Ref get(const Class* obj) { return obj->*member; } - - static Ret set(Class* obj, Ref value) { - if constexpr (signal == nullptr) { - if (MemberMetadata::get(obj) == value) return; - obj->*member = value; - } else { - if (MemberMetadata::get(obj) == value) return DropEmitter(); - obj->*member = value; - return MemberMetadata::emitter(obj); - } - } - - static Ret emitter(Class* obj) { - if constexpr (signal != nullptr) { - return DropEmitter(obj, &MemberMetadata::emitForObject); - } - } - -private: - static void emitForObject(Class* obj) { (obj->*signal)(); } -}; - -// allows use of member macros without an actual field backing them -template -class PseudomemberMetadata { -public: - using Type = T; - using Ref = const Type&; - using Ret = std::conditional_t; -}; - -class GuardedEmitBlocker { -public: - explicit GuardedEmitBlocker(bool* var): var(var) { *this->var = true; } - ~GuardedEmitBlocker() { *this->var = false; } - Q_DISABLE_COPY_MOVE(GuardedEmitBlocker); - -private: - bool* var; -}; - -template -class GuardedEmitter { - using Traits = MemberPointerTraits; - using Class = Traits::Class; - - bool blocked = false; - -public: - GuardedEmitter() = default; - ~GuardedEmitter() = default; - Q_DISABLE_COPY_MOVE(GuardedEmitter); - - void call(Class* obj) { - if (!this->blocked) (obj->*signal)(); - } - - 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); -} - -// NOLINTBEGIN -#define QS_TRIVIAL_GETTER(Type, member, getter) \ - [[nodiscard]] Type getter() { return this->member; } - -#define QS_BINDABLE_GETTER(Type, member, getter, bindable) \ - [[nodiscard]] Type getter() { return this->member.value(); } \ - [[nodiscard]] QBindable bindable() { return &this->member; } -// NOLINTEND - -template -class MethodFunctor { - using PtrMeta = MemberPointerTraits; - using Class = PtrMeta::Class; - -public: - MethodFunctor(Class* obj): obj(obj) {} - - void operator()() { (this->obj->*methodPtr)(); } - -private: - Class* obj; -}; - -// NOLINTBEGIN -#define QS_BINDING_SUBSCRIBE_METHOD(Class, bindable, method, strategy) \ - QPropertyChangeHandler> \ - _qs_method_subscribe_##bindable##_##method = \ - (bindable).strategy(MethodFunctor<&Class::method>(this)); -// NOLINTEND diff --git a/src/core/variants.cpp b/src/core/variants.cpp index a190e36d..6c8713b6 100644 --- a/src/core/variants.cpp +++ b/src/core/variants.cpp @@ -196,7 +196,7 @@ V* AwfulMap::get(const K& key) { } template -void AwfulMap::insert(const K& key, V value) { +void AwfulMap::insert(K key, V value) { this->values.push_back(QPair(key, value)); } diff --git a/src/core/variants.hpp b/src/core/variants.hpp index fa0333d3..a9071cf8 100644 --- a/src/core/variants.hpp +++ b/src/core/variants.hpp @@ -20,31 +20,34 @@ class AwfulMap { public: [[nodiscard]] bool contains(const K& key) const; [[nodiscard]] V* get(const K& key); - void insert(const K& key, V value); // assumes no duplicates - bool remove(const K& key); // returns true if anything was removed + void insert(K key, V value); // assumes no duplicates + bool remove(const K& key); // returns true if anything was removed QList> values; }; ///! Creates instances of a component based on a given model. /// Creates and destroys instances of the given component when the given property changes. /// -/// `Variants` is similar to @@QtQuick.Repeater except it is for *non @@QtQuick.Item$* objects, and acts as +/// `Variants` is similar to [Repeater] except it is for *non Item* objects, and acts as /// a reload scope. /// -/// Each non duplicate value passed to @@model will create a new instance of -/// @@delegate with a `modelData` property set to that value. +/// Each non duplicate value passed to [model](#prop.model) will create a new instance of +/// [delegate](#prop.delegate) with its `modelData` property set to that value. /// -/// See @@Quickshell.screens for an example of using `Variants` to create copies of a window per +/// See [Quickshell.screens] for an example of using `Variants` to create copies of a window per /// screen. /// /// > [!WARNING] BUG: Variants currently fails to reload children if the variant set is changed as /// > it is instantiated. (usually due to a mutation during variant creation) +/// +/// [Repeater]: https://doc.qt.io/qt-6/qml-qtquick-repeater.html +/// [Quickshell.screens]: ../quickshell#prop.screens class Variants: public Reloadable { Q_OBJECT; /// The component to create instances of. /// /// The delegate should define a `modelData` property that will be popuplated with a value - /// from the @@model. + /// from the [model](#prop.model). Q_PROPERTY(QQmlComponent* delegate MEMBER mDelegate); /// The list of sets of properties to create instances with. /// Each set creates an instance of the component, which are updated when the input sets update. diff --git a/src/core/windowinterface.cpp b/src/core/windowinterface.cpp new file mode 100644 index 00000000..a29bd599 --- /dev/null +++ b/src/core/windowinterface.cpp @@ -0,0 +1 @@ +#include "windowinterface.hpp" // NOLINT diff --git a/src/window/windowinterface.hpp b/src/core/windowinterface.hpp similarity index 60% rename from src/window/windowinterface.hpp rename to src/core/windowinterface.hpp index 2aca7a76..ec50dfd8 100644 --- a/src/window/windowinterface.hpp +++ b/src/core/windowinterface.hpp @@ -8,40 +8,12 @@ #include #include -#include "../core/qmlscreen.hpp" -#include "../core/region.hpp" -#include "../core/reload.hpp" +#include "qmlscreen.hpp" +#include "region.hpp" +#include "reload.hpp" class ProxyWindowBase; -class QsWindowAttached; -class QsSurfaceFormat { - Q_GADGET; - QML_VALUE_TYPE(surfaceFormat); - QML_STRUCTURED_VALUE; - Q_PROPERTY(bool opaque MEMBER opaque WRITE setOpaque); - -public: - bool opaque = false; - bool opaqueModified = false; - - void setOpaque(bool opaque) { - this->opaque = opaque; - this->opaqueModified = true; - } - - [[nodiscard]] bool operator==(const QsSurfaceFormat& other) const { - return other.opaqueModified == this->opaqueModified && other.opaque == this->opaque; - } -}; - -///! Base class of Quickshell windows -/// Base class of Quickshell windows -/// ### Attached properties -/// `QSWindow` can be used as an attached object of anything that subclasses @@QtQuick.Item$. -/// It provides the following properties -/// - `window` - the `QSWindow` object. -/// - `contentItem` - the `contentItem` property of the window. class WindowInterface: public Reloadable { Q_OBJECT; // clang-format off @@ -55,12 +27,6 @@ class WindowInterface: public Reloadable { Q_PROPERTY(bool backingWindowVisible READ isBackingWindowVisible NOTIFY backingWindowVisibleChanged); Q_PROPERTY(qint32 width READ width WRITE setWidth NOTIFY widthChanged); Q_PROPERTY(qint32 height READ height WRITE setHeight NOTIFY heightChanged); - /// The ratio between logical pixels and monitor pixels. - /// - /// Qt's coordinate system works in logical pixels, which equal N monitor pixels - /// depending on scale factor. This property returns the amount of monitor pixels - /// in a logical pixel for the current window. - Q_PROPERTY(qreal devicePixelRatio READ devicePixelRatio NOTIFY devicePixelRatioChanged); /// The screen that the window currently occupies. /// /// This may be modified to move the window to the given screen. @@ -73,9 +39,19 @@ class WindowInterface: public Reloadable { Q_PROPERTY(QObject* windowTransform READ windowTransform NOTIFY windowTransformChanged); /// The background color of the window. Defaults to white. /// - /// > [!WARNING] If the window color is opaque before it is made visible, - /// > it will not be able to become transparent later unless @@surfaceFormat$.opaque - /// > is false. + /// > [!WARNING] This seems to behave weirdly when using transparent colors on some systems. + /// > Using a colored content item over a transparent window is the recommended way to work around this: + /// > ```qml + /// > ProxyWindow { + /// > color: "transparent" + /// > Rectangle { + /// > anchors.fill: parent + /// > color: "#20ffffff" + /// > + /// > // your content here + /// > } + /// > } + /// > ``` Q_PROPERTY(QColor color READ color WRITE setColor NOTIFY colorChanged); /// The clickthrough mask. Defaults to null. /// @@ -120,22 +96,11 @@ class WindowInterface: public Reloadable { /// } /// ``` Q_PROPERTY(PendingRegion* mask READ mask WRITE setMask NOTIFY maskChanged); - /// Set the surface format to request from the system. - /// - /// - `opaque` - If the requested surface should be opaque. Opaque windows allow - /// the operating system to avoid drawing things behind them, or blending the window - /// with those behind it, saving power and GPU load. If unset, this property defaults to - /// true if @@color is opaque, or false if not. *You should not need to modify this - /// property unless you create a surface that starts opaque and later becomes transparent.* - /// - /// > [!NOTE] The surface format cannot be changed after the window is created. - Q_PROPERTY(QsSurfaceFormat surfaceFormat READ surfaceFormat WRITE setSurfaceFormat NOTIFY surfaceFormatChanged); Q_PROPERTY(QQmlListProperty data READ data); // clang-format on Q_CLASSINFO("DefaultProperty", "data"); - QML_NAMED_ELEMENT(QsWindow); + QML_NAMED_ELEMENT(QSWindow); QML_UNCREATABLE("uncreatable base class"); - QML_ATTACHED(QsWindowAttached); public: explicit WindowInterface(QObject* parent = nullptr): Reloadable(parent) {} @@ -153,8 +118,6 @@ public: [[nodiscard]] virtual qint32 height() const = 0; virtual void setHeight(qint32 height) = 0; - [[nodiscard]] virtual qreal devicePixelRatio() const = 0; - [[nodiscard]] virtual QuickshellScreenInfo* screen() const = 0; virtual void setScreen(QuickshellScreenInfo* screen) = 0; @@ -166,43 +129,16 @@ public: [[nodiscard]] virtual PendingRegion* mask() const = 0; virtual void setMask(PendingRegion* mask) = 0; - [[nodiscard]] virtual QsSurfaceFormat surfaceFormat() const = 0; - virtual void setSurfaceFormat(QsSurfaceFormat format) = 0; - [[nodiscard]] virtual QQmlListProperty data() = 0; - static QsWindowAttached* qmlAttachedProperties(QObject* object); - signals: void windowConnected(); void visibleChanged(); void backingWindowVisibleChanged(); void widthChanged(); void heightChanged(); - void devicePixelRatioChanged(); void screenChanged(); void windowTransformChanged(); void colorChanged(); void maskChanged(); - void surfaceFormatChanged(); -}; - -class QsWindowAttached: public QObject { - Q_OBJECT; - Q_PROPERTY(QObject* window READ window NOTIFY windowChanged); - Q_PROPERTY(QQuickItem* contentItem READ contentItem NOTIFY windowChanged); - QML_ANONYMOUS; - -public: - [[nodiscard]] virtual QObject* window() const = 0; - [[nodiscard]] virtual QQuickItem* contentItem() const = 0; - -signals: - void windowChanged(); - -protected slots: - virtual void updateWindow() = 0; - -protected: - explicit QsWindowAttached(QQuickItem* parent); }; diff --git a/src/crash/CMakeLists.txt b/src/crash/CMakeLists.txt deleted file mode 100644 index 7fdd8305..00000000 --- a/src/crash/CMakeLists.txt +++ /dev/null @@ -1,17 +0,0 @@ -qt_add_library(quickshell-crash STATIC - main.cpp - interface.cpp - handler.cpp -) - -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) - -# quick linked for pch compat -target_link_libraries(quickshell-crash PRIVATE quickshell-build Qt::Quick Qt::Widgets) - -target_link_libraries(quickshell PRIVATE quickshell-crash) diff --git a/src/crash/handler.cpp b/src/crash/handler.cpp deleted file mode 100644 index 8d9a8a71..00000000 --- a/src/crash/handler.cpp +++ /dev/null @@ -1,184 +0,0 @@ -#include "handler.hpp" -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "../core/instanceinfo.hpp" - -extern char** environ; // NOLINT - -using namespace google_breakpad; - -namespace qs::crash { - -namespace { -Q_LOGGING_CATEGORY(logCrashHandler, "quickshell.crashhandler", QtWarningMsg); -} - -struct CrashHandlerPrivate { - ExceptionHandler* exceptionHandler = nullptr; - int minidumpFd = -1; - int infoFd = -1; - - 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)); - } - - 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."; - 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* self = static_cast(context); - - auto exe = std::array(); - if (readlink("/proc/self/exe", exe.data(), exe.size() - 1) == -1) { - perror("Failed to find crash reporter executable.\n"); - _exit(-1); - } - - auto arg = std::array {exe.data(), nullptr}; - - 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); - 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); - env[envi++] = corePidStr.data(); - - auto populateEnv = [&]() { - auto senvi = 0; - while (envi != 4095) { - auto var = environ[senvi++]; // NOLINT - if (var == nullptr) break; - env[envi++] = var; - } - - 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; - } 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); - - env[envi++] = dumpFdStr.data(); - env[envi++] = logFdStr.data(); - - populateEnv(); - execve(exe.data(), arg.data(), env.data()); - - perror("Failed to launch crash reporter.\n"); - _exit(-1); - } else { - populateEnv(); - execve(exe.data(), arg.data(), env.data()); - - perror("Failed to relaunch quickshell.\n"); - _exit(-1); - } - - return false; // should make sure it hits the system coredump handler -} - -} // namespace qs::crash diff --git a/src/crash/handler.hpp b/src/crash/handler.hpp deleted file mode 100644 index 2a1d86fa..00000000 --- a/src/crash/handler.hpp +++ /dev/null @@ -1,23 +0,0 @@ -#pragma once - -#include - -#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; -}; - -} // namespace qs::crash diff --git a/src/crash/interface.cpp b/src/crash/interface.cpp deleted file mode 100644 index c6334401..00000000 --- a/src/crash/interface.cpp +++ /dev/null @@ -1,120 +0,0 @@ -#include "interface.hpp" -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "build.hpp" - -class ReportLabel: public QWidget { -public: - ReportLabel(const QString& label, const QString& content, QWidget* parent): QWidget(parent) { - auto* layout = new QHBoxLayout(this); - layout->setContentsMargins(0, 0, 0, 0); - layout->addWidget(new QLabel(label, this)); - - auto* cl = new QLabel(content, this); - cl->setTextInteractionFlags(Qt::TextSelectableByMouse); - layout->addWidget(cl); - - layout->addStretch(); - } -}; - -CrashReporterGui::CrashReporterGui(QString reportFolder, int pid) - : reportFolder(std::move(reportFolder)) { - this->setWindowFlags(Qt::Window); - - auto textHeight = QFontInfo(QFont()).pixelSize(); - - auto* mainLayout = new QVBoxLayout(this); - - auto qtVersionMatches = strcmp(qVersion(), QT_VERSION_STR) == 0; - if (qtVersionMatches) { - mainLayout->addWidget(new QLabel( - "Quickshell has crashed. Please submit a bug report to help us fix it.", - this - )); - } else { - mainLayout->addWidget( - new QLabel("Quickshell has crashed, likely due to a Qt version mismatch.", this) - ); - } - - mainLayout->addSpacing(textHeight); - - mainLayout->addWidget(new QLabel("General information", this)); - mainLayout->addWidget(new ReportLabel("Git Revision:", GIT_REVISION, this)); - mainLayout->addWidget(new QLabel( - QString::fromLatin1("Runtime Qt version: ") % qVersion() % ", Buildtime Qt version: " - % QT_VERSION_STR, - this - )); - mainLayout->addWidget(new ReportLabel("Crashed process ID:", QString::number(pid), this)); - mainLayout->addWidget(new ReportLabel("Crash report folder:", this->reportFolder, this)); - mainLayout->addSpacing(textHeight); - - if (qtVersionMatches) { - mainLayout->addWidget(new QLabel("Please open a bug report for this issue via github or email.") - ); - } else { - mainLayout->addWidget(new QLabel( - "Please rebuild Quickshell against the current Qt version.\n" - "If this does not solve the problem, please open a bug report via github or email." - )); - } - - mainLayout->addWidget(new ReportLabel( - "Github:", - "https://github.com/quickshell-mirror/quickshell/issues/new?template=crash.yml", - this - )); - - mainLayout->addWidget(new ReportLabel("Email:", "quickshell-bugs@outfoxxed.me", this)); - - auto* buttons = new QWidget(this); - buttons->setMinimumWidth(900); - auto* buttonLayout = new QHBoxLayout(buttons); - buttonLayout->setContentsMargins(0, 0, 0, 0); - - auto* reportButton = new QPushButton("Open report page", buttons); - reportButton->setDefault(true); - QObject::connect(reportButton, &QPushButton::clicked, this, &CrashReporterGui::openReportUrl); - buttonLayout->addWidget(reportButton); - - auto* openFolderButton = new QPushButton("Open crash folder", buttons); - QObject::connect(openFolderButton, &QPushButton::clicked, this, &CrashReporterGui::openFolder); - buttonLayout->addWidget(openFolderButton); - - auto* cancelButton = new QPushButton("Exit", buttons); - QObject::connect(cancelButton, &QPushButton::clicked, this, &CrashReporterGui::cancel); - buttonLayout->addWidget(cancelButton); - - mainLayout->addWidget(buttons); - - mainLayout->addStretch(); - this->setFixedSize(this->sizeHint()); -} - -void CrashReporterGui::openFolder() { - QDesktopServices::openUrl(QUrl::fromLocalFile(this->reportFolder)); -} - -void CrashReporterGui::openReportUrl() { - QDesktopServices::openUrl( - QUrl("https://github.com/outfoxxed/quickshell/issues/new?template=crash.yml") - ); -} - -void CrashReporterGui::cancel() { QApplication::quit(); } diff --git a/src/crash/interface.hpp b/src/crash/interface.hpp deleted file mode 100644 index d7800435..00000000 --- a/src/crash/interface.hpp +++ /dev/null @@ -1,17 +0,0 @@ -#pragma once - -#include - -class CrashReporterGui: public QWidget { -public: - CrashReporterGui(QString reportFolder, int pid); - -private slots: - void openFolder(); - - static void openReportUrl(); - static void cancel(); - -private: - QString reportFolder; -}; diff --git a/src/crash/main.cpp b/src/crash/main.cpp deleted file mode 100644 index 7c3bad73..00000000 --- a/src/crash/main.cpp +++ /dev/null @@ -1,188 +0,0 @@ -#include "main.hpp" -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "../core/instanceinfo.hpp" -#include "../core/logging.hpp" -#include "../core/paths.hpp" -#include "build.hpp" -#include "interface.hpp" - -namespace { - -Q_LOGGING_CATEGORY(logCrashReporter, "quickshell.crashreporter", QtWarningMsg); - -int tryDup(int fd, const QString& path) { - QFile sourceFile; - if (!sourceFile.open(fd, QFile::ReadOnly, QFile::AutoCloseHandle)) { - qCCritical(logCrashReporter) << "Failed to open source fd for duplication."; - return 1; - } - - auto destFile = QFile(path); - if (!destFile.open(QFile::WriteOnly)) { - qCCritical(logCrashReporter) << "Failed to open dest file for duplication."; - return 2; - } - - auto size = sourceFile.size(); - off_t offset = 0; - ssize_t count = 0; - - sourceFile.seek(0); - - while (count != size) { - auto r = sendfile(destFile.handle(), sourceFile.handle(), &offset, sourceFile.size()); - if (r == -1) { - qCCritical(logCrashReporter).nospace() - << "Failed to duplicate fd " << fd << " with error code " << errno - << ". Error: " << qt_error_string(); - return 3; - } else { - count += r; - } - } - - return 0; -} - -void recordCrashInfo(const QDir& crashDir, const InstanceInfo& instance) { - qCDebug(logCrashReporter) << "Recording crash information at" << crashDir.path(); - - if (!crashDir.mkpath(".")) { - qCCritical(logCrashReporter) << "Failed to create folder" << crashDir - << "to save crash information."; - return; - } - - auto crashProc = qEnvironmentVariable("__QUICKSHELL_CRASH_DUMP_PID").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) << "Saving log from fd" << logFd; - auto logDupStatus = tryDup(logFd, 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")); - if (!extraInfoFile.open(QFile::WriteOnly)) { - qCCritical(logCrashReporter) << "Failed to open crash info file for writing."; - } else { - auto stream = QTextStream(&extraInfoFile); - stream << "===== Build Information =====\n"; - stream << "Git Revision: " << GIT_REVISION << '\n'; - stream << "Buildtime Qt Version: " << QT_VERSION_STR << "\n"; - stream << "Build Type: " << BUILD_TYPE << '\n'; - stream << "Compiler: " << COMPILER << '\n'; - stream << "Complie Flags: " << COMPILE_FLAGS << "\n\n"; - stream << "Build configuration:\n" << BUILD_CONFIGURATION << "\n"; - - stream << "\n===== Runtime Information =====\n"; - stream << "Runtime Qt Version: " << qVersion() << '\n'; - 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"); - if (osReleaseFile.open(QFile::ReadOnly)) { - stream << '\n' << osReleaseFile.readAll() << '\n'; - osReleaseFile.close(); - } else { - stream << "FAILED TO OPEN\n"; - } - - stream << "/etc/lsb-release:"; - auto lsbReleaseFile = QFile("/etc/lsb-release"); - if (lsbReleaseFile.open(QFile::ReadOnly)) { - stream << '\n' << lsbReleaseFile.readAll(); - lsbReleaseFile.close(); - } else { - stream << "FAILED TO OPEN\n"; - } - - extraInfoFile.close(); - } - } - - qCDebug(logCrashReporter) << "Recorded crash information."; -} - -} // namespace - -void qsCheckCrash(int argc, char** argv) { - auto fd = qEnvironmentVariable("__QUICKSHELL_CRASH_DUMP_FD"); - if (fd.isEmpty()) return; - auto app = QApplication(argc, argv); - - RelaunchInfo info; - - auto crashProc = qEnvironmentVariable("__QUICKSHELL_CRASH_DUMP_PID").toInt(); - - { - auto infoFd = qEnvironmentVariable("__QUICKSHELL_CRASH_INFO_FD").toInt(); - - QFile file; - file.open(infoFd, QFile::ReadOnly, QFile::AutoCloseHandle); - file.seek(0); - - auto ds = QDataStream(&file); - ds >> info; - } - - LogManager::init( - !info.noColor, - info.timestamp, - info.sparseLogsOnly, - info.defaultLogLevel, - info.logRules - ); - - auto crashDir = QsPaths::crashDir(info.instance.instanceId); - - qCInfo(logCrashReporter) << "Starting crash reporter..."; - - recordCrashInfo(crashDir, info.instance); - - auto gui = CrashReporterGui(crashDir.path(), crashProc); - gui.show(); - exit(QApplication::exec()); // NOLINT -} diff --git a/src/crash/main.hpp b/src/crash/main.hpp deleted file mode 100644 index b6a282cf..00000000 --- a/src/crash/main.hpp +++ /dev/null @@ -1,3 +0,0 @@ -#pragma once - -void qsCheckCrash(int argc, char** argv); diff --git a/src/dbus/CMakeLists.txt b/src/dbus/CMakeLists.txt index 9948ea74..ee6df30a 100644 --- a/src/dbus/CMakeLists.txt +++ b/src/dbus/CMakeLists.txt @@ -9,25 +9,14 @@ qt_add_dbus_interface(DBUS_INTERFACES qt_add_library(quickshell-dbus STATIC properties.cpp - bus.cpp ${DBUS_INTERFACES} ) # dbus headers target_include_directories(quickshell-dbus PRIVATE ${CMAKE_CURRENT_BINARY_DIR}) -target_link_libraries(quickshell-dbus PRIVATE Qt::Core Qt::DBus) -# todo: link dbus to quickshell here instead of in modules that use it directly -# linker does not like this as is +target_link_libraries(quickshell-dbus PRIVATE ${QT_DEPS}) -qs_add_pchset(dbus - DEPENDENCIES Qt::DBus - HEADERS - - - -) - -qs_pch(quickshell-dbus SET dbus) +qs_pch(quickshell-dbus) add_subdirectory(dbusmenu) diff --git a/src/dbus/bus.cpp b/src/dbus/bus.cpp deleted file mode 100644 index dc6d21bf..00000000 --- a/src/dbus/bus.cpp +++ /dev/null @@ -1,59 +0,0 @@ -#include "bus.hpp" // NOLINT -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace qs::dbus { - -namespace { -Q_LOGGING_CATEGORY(logDbus, "quickshell.dbus", QtWarningMsg); -} - -void tryLaunchService( - QObject* parent, - QDBusConnection& connection, - const QString& serviceName, - const std::function& callback -) { - qCDebug(logDbus) << "Attempting to launch service" << serviceName; - - auto message = QDBusMessage::createMethodCall( - "org.freedesktop.DBus", - "/org/freedesktop/DBus", - "org.freedesktop.DBus", - "StartServiceByName" - ); - - message << serviceName << 0u; - auto pendingCall = connection.asyncCall(message); - - auto* call = new QDBusPendingCallWatcher(pendingCall, parent); - - auto responseCallback = [callback, serviceName](QDBusPendingCallWatcher* call) { - const QDBusPendingReply reply = *call; - - if (reply.isError()) { - qCWarning(logDbus).noquote().nospace() - << "Could not launch service " << serviceName << ": " << reply.error(); - callback(false); - } else { - qCDebug(logDbus) << "Service launch successful for" << serviceName; - callback(true); - } - - delete call; - }; - - QObject::connect(call, &QDBusPendingCallWatcher::finished, parent, responseCallback); -} - -} // namespace qs::dbus diff --git a/src/dbus/bus.hpp b/src/dbus/bus.hpp deleted file mode 100644 index 1c4c71e4..00000000 --- a/src/dbus/bus.hpp +++ /dev/null @@ -1,18 +0,0 @@ -#pragma once - -#include - -#include -#include -#include - -namespace qs::dbus { - -void tryLaunchService( - QObject* parent, - QDBusConnection& connection, - const QString& serviceName, - const std::function& callback -); - -} diff --git a/src/dbus/dbusmenu/CMakeLists.txt b/src/dbus/dbusmenu/CMakeLists.txt index 61cee42c..ab222e5a 100644 --- a/src/dbus/dbusmenu/CMakeLists.txt +++ b/src/dbus/dbusmenu/CMakeLists.txt @@ -14,22 +14,12 @@ qt_add_library(quickshell-dbusmenu STATIC ${DBUS_INTERFACES} ) -qt_add_qml_module(quickshell-dbusmenu - URI Quickshell.DBusMenu - VERSION 0.1 - DEPENDENCIES QtQml -) - -qs_add_module_deps_light(quickshell-dbusmenu Quickshell) - -install_qml_module(quickshell-dbusmenu) +qt_add_qml_module(quickshell-dbusmenu URI Quickshell.DBusMenu VERSION 0.1) # dbus headers target_include_directories(quickshell-dbusmenu PRIVATE ${CMAKE_CURRENT_BINARY_DIR}) -target_link_libraries(quickshell-dbusmenu PRIVATE Qt::Quick Qt::DBus) -qs_add_link_dependencies(quickshell-dbusmenu quickshell-dbus) +target_link_libraries(quickshell-dbusmenu PRIVATE ${QT_DEPS}) -qs_module_pch(quickshell-dbusmenu SET dbus) - -target_link_libraries(quickshell PRIVATE quickshell-dbusmenuplugin) +qs_pch(quickshell-dbusmenu) +qs_pch(quickshell-dbusmenuplugin) diff --git a/src/dbus/dbusmenu/dbusmenu.cpp b/src/dbus/dbusmenu/dbusmenu.cpp index 2b633b76..7484d849 100644 --- a/src/dbus/dbusmenu/dbusmenu.cpp +++ b/src/dbus/dbusmenu/dbusmenu.cpp @@ -21,63 +21,70 @@ #include #include "../../core/iconimageprovider.hpp" -#include "../../core/model.hpp" -#include "../../core/qsmenu.hpp" #include "../../dbus/properties.hpp" #include "dbus_menu.h" #include "dbus_menu_types.hpp" Q_LOGGING_CATEGORY(logDbusMenu, "quickshell.dbus.dbusmenu", QtWarningMsg); -using namespace qs::menu; - namespace qs::dbus::dbusmenu { DBusMenuItem::DBusMenuItem(qint32 id, DBusMenu* menu, DBusMenuItem* parentMenu) - : QsMenuEntry(menu) + : QObject(menu) , id(id) , menu(menu) , parentMenu(parentMenu) { - QObject::connect(this, &QsMenuEntry::opened, this, &DBusMenuItem::sendOpened); - QObject::connect(this, &QsMenuEntry::closed, this, &DBusMenuItem::sendClosed); - QObject::connect(this, &QsMenuEntry::triggered, this, &DBusMenuItem::sendTriggered); - - QObject::connect(this->menu, &DBusMenu::iconThemePathChanged, this, &DBusMenuItem::iconChanged); + QObject::connect( + &this->menu->iconThemePath, + &AbstractDBusProperty::changed, + this, + &DBusMenuItem::iconChanged + ); } -void DBusMenuItem::sendOpened() const { this->menu->sendEvent(this->id, "opened"); } -void DBusMenuItem::sendClosed() const { this->menu->sendEvent(this->id, "closed"); } -void DBusMenuItem::sendTriggered() const { this->menu->sendEvent(this->id, "clicked"); } +void DBusMenuItem::click() { + if (this->displayChildren) { + this->setShowChildren(!this->mShowChildren); + } else { + this->menu->sendEvent(this->id, "clicked"); + } +} + +void DBusMenuItem::hover() const { this->menu->sendEvent(this->id, "hovered"); } DBusMenu* DBusMenuItem::menuHandle() const { return this->menu; } +QString DBusMenuItem::label() const { return this->mLabel; } +QString DBusMenuItem::cleanLabel() const { return this->mCleanLabel; } bool DBusMenuItem::enabled() const { return this->mEnabled; } -QString DBusMenuItem::text() const { return this->mCleanLabel; } QString DBusMenuItem::icon() const { if (!this->iconName.isEmpty()) { return IconImageProvider::requestString( this->iconName, - this->menu->iconThemePath.value().join(':') + this->menu->iconThemePath.get().join(':') ); - } else if (this->image.hasData()) { - return this->image.url(); + } else if (this->image != nullptr) { + return this->image->url(); } else return nullptr; } -QsMenuButtonType::Enum DBusMenuItem::buttonType() const { return this->mButtonType; }; +ToggleButtonType::Enum DBusMenuItem::toggleType() const { return this->mToggleType; }; Qt::CheckState DBusMenuItem::checkState() const { return this->mCheckState; } bool DBusMenuItem::isSeparator() const { return this->mSeparator; } bool DBusMenuItem::isShowingChildren() const { return this->mShowChildren && this->childrenLoaded; } -void DBusMenuItem::setShowChildrenRecursive(bool showChildren) { +void DBusMenuItem::setShowChildren(bool showChildren) { if (showChildren == this->mShowChildren) return; this->mShowChildren = showChildren; this->childrenLoaded = false; if (showChildren) { - this->menu->prepareToShow(this->id, -1); + this->menu->prepareToShow(this->id, true); } else { + this->menu->sendEvent(this->id, "closed"); + emit this->showingChildrenChanged(); + if (!this->mChildren.isEmpty()) { for (auto child: this->mChildren) { this->menu->removeRecursive(child); @@ -89,15 +96,24 @@ void DBusMenuItem::setShowChildrenRecursive(bool showChildren) { } } -void DBusMenuItem::updateLayout() const { - if (!this->isShowingChildren()) return; - this->menu->updateLayout(this->id, -1); +bool DBusMenuItem::hasChildren() const { return this->displayChildren; } + +QQmlListProperty DBusMenuItem::children() { + return QQmlListProperty( + this, + nullptr, + &DBusMenuItem::childrenCount, + &DBusMenuItem::childAt + ); } -bool DBusMenuItem::hasChildren() const { return this->displayChildren || this->id == 0; } +qsizetype DBusMenuItem::childrenCount(QQmlListProperty* property) { + return reinterpret_cast(property->object)->enabledChildren.count(); // NOLINT +} -ObjectModel* DBusMenuItem::children() { - return reinterpret_cast*>(&this->enabledChildren); +DBusMenuItem* DBusMenuItem::childAt(QQmlListProperty* property, qsizetype index) { + auto* item = reinterpret_cast(property->object); // NOLINT + return item->menu->items.value(item->enabledChildren.at(index)); } void DBusMenuItem::updateProperties(const QVariantMap& properties, const QStringList& removed) { @@ -108,30 +124,30 @@ void DBusMenuItem::updateProperties(const QVariantMap& properties, const QString return; } - auto originalText = this->mText; + auto originalLabel = this->mLabel; //auto originalMnemonic = this->mnemonic; auto originalEnabled = this->mEnabled; auto originalVisible = this->visible; auto originalIconName = this->iconName; - auto imageChanged = false; + auto* originalImage = this->image; auto originalIsSeparator = this->mSeparator; - auto originalButtonType = this->mButtonType; + auto originalToggleType = this->mToggleType; auto originalToggleState = this->mCheckState; auto originalDisplayChildren = this->displayChildren; auto label = properties.value("label"); if (label.canConvert()) { auto text = label.value(); - this->mText = text; + this->mLabel = text; this->mCleanLabel = text; //this->mnemonic = QChar(); - for (auto i = 0; i < this->mText.length() - 1;) { - if (this->mText.at(i) == '_') { + for (auto i = 0; i < this->mLabel.length() - 1;) { + if (this->mLabel.at(i) == '_') { //if (this->mnemonic == QChar()) this->mnemonic = this->mLabel.at(i + 1); - this->mText.remove(i, 1); - this->mText.insert(i + 1, ""); - this->mText.insert(i, ""); + this->mLabel.remove(i, 1); + this->mLabel.insert(i + 1, ""); + this->mLabel.insert(i, ""); i += 8; } else { i++; @@ -144,7 +160,7 @@ void DBusMenuItem::updateProperties(const QVariantMap& properties, const QString } } } else if (removed.isEmpty() || removed.contains("label")) { - this->mText = ""; + this->mLabel = ""; //this->mnemonic = QChar(); } @@ -173,16 +189,12 @@ void DBusMenuItem::updateProperties(const QVariantMap& properties, const QString if (iconData.canConvert()) { auto data = iconData.value(); if (data.isEmpty()) { - imageChanged = this->image.hasData(); - this->image.data.clear(); - } else if (!this->image.hasData() || this->image.data != data) { - imageChanged = true; - this->image.data = data; - this->image.imageChanged(); + this->image = nullptr; + } else if (this->image == nullptr || this->image->data != data) { + this->image = new DBusMenuPngImage(data, this); } } else if (removed.isEmpty() || removed.contains("icon-data")) { - imageChanged = this->image.hasData(); - image.data.clear(); + this->image = nullptr; } auto type = properties.value("type"); @@ -196,15 +208,15 @@ void DBusMenuItem::updateProperties(const QVariantMap& properties, const QString if (toggleType.canConvert()) { auto toggleTypeStr = toggleType.value(); - if (toggleTypeStr == "") this->mButtonType = QsMenuButtonType::None; - else if (toggleTypeStr == "checkmark") this->mButtonType = QsMenuButtonType::CheckBox; - else if (toggleTypeStr == "radio") this->mButtonType = QsMenuButtonType::RadioButton; + if (toggleTypeStr == "") this->mToggleType = ToggleButtonType::None; + else if (toggleTypeStr == "checkmark") this->mToggleType = ToggleButtonType::CheckBox; + else if (toggleTypeStr == "radio") this->mToggleType = ToggleButtonType::RadioButton; else { qCWarning(logDbusMenu) << "Unrecognized toggle type" << toggleTypeStr << "for" << this; - this->mButtonType = QsMenuButtonType::None; + this->mToggleType = ToggleButtonType::None; } } else if (removed.isEmpty() || removed.contains("toggle-type")) { - this->mButtonType = QsMenuButtonType::None; + this->mToggleType = ToggleButtonType::None; } auto toggleState = properties.value("toggle-state"); @@ -215,7 +227,7 @@ void DBusMenuItem::updateProperties(const QVariantMap& properties, const QString else if (toggleStateInt == 1) this->mCheckState = Qt::Checked; else this->mCheckState = Qt::PartiallyChecked; } else if (removed.isEmpty() || removed.contains("toggle-state")) { - this->mCheckState = Qt::Unchecked; + this->mCheckState = Qt::PartiallyChecked; } auto childrenDisplay = properties.value("children-display"); @@ -233,37 +245,42 @@ void DBusMenuItem::updateProperties(const QVariantMap& properties, const QString this->displayChildren = false; } - if (this->mText != originalText) emit this->textChanged(); + if (this->mLabel != originalLabel) emit this->labelChanged(); //if (this->mnemonic != originalMnemonic) emit this->labelChanged(); if (this->mEnabled != originalEnabled) emit this->enabledChanged(); if (this->visible != originalVisible && this->parentMenu != nullptr) this->parentMenu->onChildrenUpdated(); - if (this->mButtonType != originalButtonType) emit this->buttonTypeChanged(); + if (this->mToggleType != originalToggleType) emit this->toggleTypeChanged(); if (this->mCheckState != originalToggleState) emit this->checkStateChanged(); - if (this->mSeparator != originalIsSeparator) emit this->isSeparatorChanged(); + if (this->mSeparator != originalIsSeparator) emit this->separatorChanged(); if (this->displayChildren != originalDisplayChildren) emit this->hasChildrenChanged(); - if (this->iconName != originalIconName || imageChanged) { + if (this->iconName != originalIconName || this->image != originalImage) { + if (this->image != originalImage) { + delete originalImage; + } + emit this->iconChanged(); } - qCDebug(logDbusMenu).nospace() << "Updated properties of " << this << " { label=" << this->mText + qCDebug(logDbusMenu).nospace() << "Updated properties of " << this << " { label=" << this->mLabel << ", enabled=" << this->mEnabled << ", visible=" << this->visible - << ", iconName=" << this->iconName << ", iconData=" << &this->image + << ", iconName=" << this->iconName << ", iconData=" << this->image << ", separator=" << this->mSeparator - << ", toggleType=" << this->mButtonType + << ", toggleType=" << this->mToggleType << ", toggleState=" << this->mCheckState << ", displayChildren=" << this->displayChildren << " }"; } void DBusMenuItem::onChildrenUpdated() { - QVector children; + this->enabledChildren.clear(); + for (auto child: this->mChildren) { auto* item = this->menu->items.value(child); - if (item->visible) children.append(item); + if (item->visible) this->enabledChildren.push_back(child); } - this->enabledChildren.diffUpdate(children); + emit this->childrenChanged(); } QDebug operator<<(QDebug debug, DBusMenuItem* item) { @@ -274,7 +291,20 @@ QDebug operator<<(QDebug debug, DBusMenuItem* item) { auto saver = QDebugStateSaver(debug); debug.nospace() << "DBusMenuItem(" << static_cast(item) << ", id=" << item->id - << ", label=" << item->mText << ", menu=" << item->menu << ")"; + << ", label=" << item->mLabel << ", menu=" << item->menu << ")"; + return debug; +} + +QDebug operator<<(QDebug debug, const ToggleButtonType::Enum& toggleType) { + auto saver = QDebugStateSaver(debug); + debug.nospace() << "ToggleType::"; + + switch (toggleType) { + case ToggleButtonType::None: debug << "None"; break; + case ToggleButtonType::CheckBox: debug << "Checkbox"; break; + case ToggleButtonType::RadioButton: debug << "Radiobutton"; break; + } + return debug; } @@ -304,18 +334,19 @@ DBusMenu::DBusMenu(const QString& service, const QString& path, QObject* parent) this->properties.updateAllViaGetAll(); } -void DBusMenu::prepareToShow(qint32 item, qint32 depth) { +void DBusMenu::prepareToShow(qint32 item, bool sendOpened) { auto pending = this->interface->AboutToShow(item); auto* call = new QDBusPendingCallWatcher(pending, this); - auto responseCallback = [this, item, depth](QDBusPendingCallWatcher* call) { + auto responseCallback = [this, item, sendOpened](QDBusPendingCallWatcher* call) { const QDBusPendingReply reply = *call; if (reply.isError()) { qCWarning(logDbusMenu) << "Error in AboutToShow, but showing anyway for menu" << item << "of" << this << reply.error(); } - this->updateLayout(item, depth); + this->updateLayout(item, 1); + if (sendOpened) this->sendEvent(item, "opened"); delete call; }; @@ -354,7 +385,6 @@ void DBusMenu::updateLayoutRecursive( // there is an actual nullptr in the map and not no entry if (this->items.contains(layout.id)) { item = new DBusMenuItem(layout.id, this, parent); - item->mShowChildren = parent != nullptr && parent->mShowChildren; this->items.insert(layout.id, item); } } @@ -368,11 +398,13 @@ void DBusMenu::updateLayoutRecursive( auto childrenChanged = false; auto iter = item->mChildren.begin(); while (iter != item->mChildren.end()) { - auto existing = std::ranges::find_if(layout.children, [&](const DBusMenuLayout& layout) { - return layout.id == *iter; - }); + auto existing = std::find_if( + layout.children.begin(), + layout.children.end(), + [&](const DBusMenuLayout& layout) { return layout.id == *iter; } + ); - if (!item->mShowChildren || existing == layout.children.end()) { + if (existing == layout.children.end()) { qCDebug(logDbusMenu) << "Removing missing layout item" << this->items.value(*iter) << "from" << item; this->removeRecursive(*iter); @@ -386,7 +418,7 @@ void DBusMenu::updateLayoutRecursive( for (const auto& child: layout.children) { if (item->mShowChildren && !item->mChildren.contains(child.id)) { qCDebug(logDbusMenu) << "Creating new layout item" << child.id << "in" << item; - // item->mChildren.push_back(child.id); + item->mChildren.push_back(child.id); this->items.insert(child.id, nullptr); childrenChanged = true; } @@ -394,22 +426,13 @@ void DBusMenu::updateLayoutRecursive( this->updateLayoutRecursive(child, item, depth - 1); } - if (childrenChanged) { - // reset to preserve order - item->mChildren.clear(); - for (const auto& child: layout.children) { - item->mChildren.push_back(child.id); - } - - item->onChildrenUpdated(); - } + if (childrenChanged) item->onChildrenUpdated(); } if (item->mShowChildren && !item->childrenLoaded) { item->childrenLoaded = true; + emit item->showingChildrenChanged(); } - - emit item->layoutUpdated(); } void DBusMenu::removeRecursive(qint32 id) { @@ -499,74 +522,4 @@ DBusMenuPngImage::requestImage(const QString& /*unused*/, QSize* size, const QSi return image; } -void DBusMenuHandle::setAddress(const QString& service, const QString& path) { - if (service == this->service && path == this->path) return; - this->service = service; - this->path = path; - this->onMenuPathChanged(); -} - -void DBusMenuHandle::refHandle() { - this->refcount++; - qCDebug(logDbusMenu) << this << "gained a reference. Refcount is now" << this->refcount; - - if (this->refcount == 1 || !this->mMenu) { - this->onMenuPathChanged(); - } else { - // Refresh the layout when opening a menu in case a bad client isn't updating it - // and another ref is open somewhere. - this->mMenu->rootItem.updateLayout(); - } -} - -void DBusMenuHandle::unrefHandle() { - this->refcount--; - qCDebug(logDbusMenu) << this << "lost a reference. Refcount is now" << this->refcount; - - if (this->refcount == 0) { - this->onMenuPathChanged(); - } -} - -void DBusMenuHandle::onMenuPathChanged() { - qCDebug(logDbusMenu) << "Updating" << this << "with refcount" << this->refcount; - - if (this->mMenu) { - // Without this, layout updated can be sent after mMenu is set to null, - // leaving loaded = true while mMenu = nullptr. - QObject::disconnect(&this->mMenu->rootItem, nullptr, this, nullptr); - this->mMenu->deleteLater(); - this->mMenu = nullptr; - this->loaded = false; - emit this->menuChanged(); - } - - if (this->refcount > 0 && !this->service.isEmpty() && !this->path.isEmpty()) { - this->mMenu = new DBusMenu(this->service, this->path); - this->mMenu->setParent(this); - - QObject::connect(&this->mMenu->rootItem, &DBusMenuItem::layoutUpdated, this, [this]() { - QObject::disconnect(&this->mMenu->rootItem, &DBusMenuItem::layoutUpdated, this, nullptr); - this->loaded = true; - emit this->menuChanged(); - }); - - this->mMenu->rootItem.setShowChildrenRecursive(true); - } -} - -QsMenuEntry* DBusMenuHandle::menu() { return this->loaded ? &this->mMenu->rootItem : nullptr; } - -QDebug operator<<(QDebug debug, const DBusMenuHandle* handle) { - if (handle) { - auto saver = QDebugStateSaver(debug); - debug.nospace() << "DBusMenuHandle(" << static_cast(handle) - << ", service=" << handle->service << ", path=" << handle->path << ')'; - } else { - debug << "DBusMenuHandle(nullptr)"; - } - - return debug; -} - } // namespace qs::dbus::dbusmenu diff --git a/src/dbus/dbusmenu/dbusmenu.hpp b/src/dbus/dbusmenu/dbusmenu.hpp index 1a8b399e..b07919ad 100644 --- a/src/dbus/dbusmenu/dbusmenu.hpp +++ b/src/dbus/dbusmenu/dbusmenu.hpp @@ -13,88 +13,147 @@ #include #include -#include "../../core/doc.hpp" #include "../../core/imageprovider.hpp" -#include "../../core/model.hpp" -#include "../../core/qsmenu.hpp" #include "../properties.hpp" #include "dbus_menu_types.hpp" Q_DECLARE_LOGGING_CATEGORY(logDbusMenu); +namespace ToggleButtonType { // NOLINT +Q_NAMESPACE; +QML_ELEMENT; + +enum Enum { + /// This menu item does not have a checkbox or a radiobutton associated with it. + None = 0, + /// This menu item should draw a checkbox. + CheckBox = 1, + /// This menu item should draw a radiobutton. + RadioButton = 2, +}; +Q_ENUM_NS(Enum); + +} // namespace ToggleButtonType + class DBusMenuInterface; namespace qs::dbus::dbusmenu { -// hack because docgen can't take namespaces in superclasses -using menu::QsMenuEntry; +QDebug operator<<(QDebug debug, const ToggleButtonType::Enum& toggleType); class DBusMenu; -class DBusMenuItem; - -class DBusMenuPngImage: public QsIndexedImageHandle { -public: - explicit DBusMenuPngImage(): QsIndexedImageHandle(QQuickImageProvider::Image) {} - - [[nodiscard]] bool hasData() const { return !data.isEmpty(); } - QImage requestImage(const QString& id, QSize* size, const QSize& requestedSize) override; - - QByteArray data; -}; +class DBusMenuPngImage; ///! Menu item shared by an external program. /// Menu item shared by an external program via the /// [DBusMenu specification](https://github.com/AyatanaIndicators/libdbusmenu/blob/master/libdbusmenu-glib/dbus-menu.xml). -class DBusMenuItem: public QsMenuEntry { +class DBusMenuItem: public QObject { Q_OBJECT; + // clang-format off /// Handle to the root of this menu. - Q_PROPERTY(qs::dbus::dbusmenu::DBusMenu* menuHandle READ menuHandle CONSTANT); + Q_PROPERTY(DBusMenu* menuHandle READ menuHandle CONSTANT); + /// Text of the menu item, including hotkey markup. + Q_PROPERTY(QString label READ label NOTIFY labelChanged); + /// Text of the menu item without hotkey markup. + Q_PROPERTY(QString cleanLabel READ cleanLabel NOTIFY labelChanged); + /// If the menu item should be shown as enabled. + /// + /// > [!INFO] Disabled menu items are often used as headers in addition + /// > to actual disabled entries. + Q_PROPERTY(bool enabled READ enabled NOTIFY enabledChanged); + /// Url of the menu item's icon or `""` if it doesn't have one. + /// + /// This can be passed to [Image.source](https://doc.qt.io/qt-6/qml-qtquick-image.html#source-prop) + /// as shown below. + /// + /// ```qml + /// Image { + /// source: menuItem.icon + /// // To get the best image quality, set the image source size to the same size + /// // as the rendered image. + /// sourceSize.width: width + /// sourceSize.height: height + /// } + /// ``` + Q_PROPERTY(QString icon READ icon NOTIFY iconChanged); + /// If this menu item has an associated checkbox or radiobutton. + /// + /// > [!INFO] It is the responsibility of the remote application to update the state of + /// > checkboxes and radiobuttons via [checkState](#prop.checkState). + Q_PROPERTY(ToggleButtonType::Enum toggleType READ toggleType NOTIFY toggleTypeChanged); + /// The check state of the checkbox or radiobutton if applicable, as a + /// [Qt.CheckState](https://doc.qt.io/qt-6/qt.html#CheckState-enum). + Q_PROPERTY(Qt::CheckState checkState READ checkState NOTIFY checkStateChanged); + /// If this menu item should be rendered as a separator between other items. + /// + /// No other properties have a meaningful value when `isSeparator` is true. + Q_PROPERTY(bool isSeparator READ isSeparator NOTIFY separatorChanged); + /// If this menu item reveals a submenu containing more items. + /// + /// Any submenu items must be requested by setting [showChildren](#prop.showChildren). + Q_PROPERTY(bool hasChildren READ hasChildren NOTIFY hasChildrenChanged); + /// If submenu entries of this item should be shown. + /// + /// When true, children of this menu item will be exposed via [children](#prop.children). + /// Setting this property will additionally send the `opened` and `closed` events to the + /// process that provided the menu. + Q_PROPERTY(bool showChildren READ isShowingChildren WRITE setShowChildren NOTIFY showingChildrenChanged); + /// Children of this menu item. Only populated when [showChildren](#prop.showChildren) is true. + /// + /// > [!INFO] Use [hasChildren](#prop.hasChildren) to check if this item should reveal a submenu + /// > instead of checking if `children` is empty. + Q_PROPERTY(QQmlListProperty children READ children NOTIFY childrenChanged); + // clang-format on QML_ELEMENT; QML_UNCREATABLE("DBusMenus can only be acquired from a DBusMenuHandle"); public: explicit DBusMenuItem(qint32 id, DBusMenu* menu, DBusMenuItem* parentMenu); - /// Refreshes the menu contents. + /// Send a `clicked` event to the remote application for this menu item. + Q_INVOKABLE void click(); + + /// Send a `hovered` event to the remote application for this menu item. /// - /// Usually you shouldn't need to call this manually but some applications providing - /// menus do not update them correctly. Call this if menus don't update their state. - /// - /// The @@layoutUpdated(s) signal will be sent when a response is received. - Q_INVOKABLE void updateLayout() const; + /// Note: we are not aware of any programs that use this in any meaningful way. + Q_INVOKABLE void hover() const; [[nodiscard]] DBusMenu* menuHandle() const; - - [[nodiscard]] bool isSeparator() const override; - [[nodiscard]] bool enabled() const override; - [[nodiscard]] QString text() const override; - [[nodiscard]] QString icon() const override; - [[nodiscard]] menu::QsMenuButtonType::Enum buttonType() const override; - [[nodiscard]] Qt::CheckState checkState() const override; - [[nodiscard]] bool hasChildren() const override; + [[nodiscard]] QString label() const; + [[nodiscard]] QString cleanLabel() const; + [[nodiscard]] bool enabled() const; + [[nodiscard]] QString icon() const; + [[nodiscard]] ToggleButtonType::Enum toggleType() const; + [[nodiscard]] Qt::CheckState checkState() const; + [[nodiscard]] bool isSeparator() const; + [[nodiscard]] bool hasChildren() const; [[nodiscard]] bool isShowingChildren() const; - void setShowChildrenRecursive(bool showChildren); + void setShowChildren(bool showChildren); - [[nodiscard]] ObjectModel* children() override; + [[nodiscard]] QQmlListProperty children(); void updateProperties(const QVariantMap& properties, const QStringList& removed = {}); void onChildrenUpdated(); qint32 id = 0; - QString mText; + QString mLabel; QVector mChildren; bool mShowChildren = false; bool childrenLoaded = false; DBusMenu* menu = nullptr; signals: - void layoutUpdated(); - -private slots: - void sendOpened() const; - void sendClosed() const; - void sendTriggered() const; + void labelChanged(); + //void mnemonicChanged(); + void enabledChanged(); + void iconChanged(); + void separatorChanged(); + void toggleTypeChanged(); + void checkStateChanged(); + void hasChildrenChanged(); + void showingChildrenChanged(); + void childrenChanged(); private: QString mCleanLabel; @@ -103,12 +162,15 @@ private: bool visible = true; bool mSeparator = false; QString iconName; - DBusMenuPngImage image; - menu::QsMenuButtonType::Enum mButtonType = menu::QsMenuButtonType::None; - Qt::CheckState mCheckState = Qt::Unchecked; + DBusMenuPngImage* image = nullptr; + ToggleButtonType::Enum mToggleType = ToggleButtonType::None; + Qt::CheckState mCheckState = Qt::Checked; bool displayChildren = false; - ObjectModel enabledChildren {this}; + QVector enabledChildren; DBusMenuItem* parentMenu = nullptr; + + static qsizetype childrenCount(QQmlListProperty* property); + static DBusMenuItem* childAt(QQmlListProperty* property, qsizetype index); }; QDebug operator<<(QDebug debug, DBusMenuItem* item); @@ -117,22 +179,20 @@ QDebug operator<<(QDebug debug, DBusMenuItem* item); /// Handle to a menu tree provided by a remote process. class DBusMenu: public QObject { Q_OBJECT; - Q_PROPERTY(qs::dbus::dbusmenu::DBusMenuItem* menu READ menu CONSTANT); + Q_PROPERTY(DBusMenuItem* menu READ menu CONSTANT); QML_NAMED_ELEMENT(DBusMenuHandle); QML_UNCREATABLE("Menu handles cannot be directly created"); public: explicit DBusMenu(const QString& service, const QString& path, QObject* parent = nullptr); - QS_DBUS_BINDABLE_PROPERTY_GROUP(DBusMenu, properties); + dbus::DBusPropertyGroup properties; + dbus::DBusProperty version {this->properties, "Version"}; + dbus::DBusProperty textDirection {this->properties, "TextDirection"}; + dbus::DBusProperty status {this->properties, "Status"}; + dbus::DBusProperty iconThemePath {this->properties, "IconThemePath"}; -signals: - QSDOC_HIDE void iconThemePathChanged(); - -public: - Q_OBJECT_BINDABLE_PROPERTY(DBusMenu, QStringList, iconThemePath, &DBusMenu::iconThemePathChanged); - - void prepareToShow(qint32 item, qint32 depth); + void prepareToShow(qint32 item, bool sendOpened); void updateLayout(qint32 parent, qint32 depth); void removeRecursive(qint32 id); void sendEvent(qint32 item, const QString& event); @@ -152,45 +212,20 @@ private slots: private: void updateLayoutRecursive(const DBusMenuLayout& layout, DBusMenuItem* parent, qint32 depth); - QS_DBUS_PROPERTY_BINDING( - DBusMenu, - pIconThemePath, - iconThemePath, - properties, - "IconThemePath", - false - ); - DBusMenuInterface* interface = nullptr; }; QDebug operator<<(QDebug debug, DBusMenu* menu); -class DBusMenuHandle; - -QDebug operator<<(QDebug debug, const DBusMenuHandle* handle); - -class DBusMenuHandle: public menu::QsMenuHandle { +class DBusMenuPngImage: public QsImageHandle { public: - explicit DBusMenuHandle(QObject* parent): menu::QsMenuHandle(parent) {} + explicit DBusMenuPngImage(QByteArray data, DBusMenuItem* parent) + : QsImageHandle(QQuickImageProvider::Image, parent) + , data(std::move(data)) {} - void setAddress(const QString& service, const QString& path); + QImage requestImage(const QString& id, QSize* size, const QSize& requestedSize) override; - void refHandle() override; - void unrefHandle() override; - - [[nodiscard]] QsMenuEntry* menu() override; - -private: - void onMenuPathChanged(); - - QString service; - QString path; - DBusMenu* mMenu = nullptr; - bool loaded = false; - quint32 refcount = 0; - - friend QDebug operator<<(QDebug debug, const DBusMenuHandle* handle); + QByteArray data; }; } // namespace qs::dbus::dbusmenu diff --git a/src/dbus/properties.cpp b/src/dbus/properties.cpp index 52f50060..1e5e0bd1 100644 --- a/src/dbus/properties.cpp +++ b/src/dbus/properties.cpp @@ -16,6 +16,7 @@ #include #include #include +#include #include #include @@ -37,7 +38,7 @@ QDBusError demarshallVariant(const QVariant& variant, const QMetaType& type, voi if (variant.metaType() == type) { if (type.id() == QMetaType::QVariant) { - *reinterpret_cast(slot) = variant; + *reinterpret_cast(slot) = variant; // NOLINT } else { type.destruct(slot); type.construct(slot, variant.constData()); @@ -110,7 +111,60 @@ void asyncReadPropertyInternal( QObject::connect(call, &QDBusPendingCallWatcher::finished, &interface, responseCallback); } -DBusPropertyGroup::DBusPropertyGroup(QVector properties, QObject* parent) +void AbstractDBusProperty::tryUpdate(const QVariant& variant) { + auto error = this->read(variant); + if (error.isValid()) { + qCWarning(logDbusProperties).noquote() + << "Error demarshalling property update for" << this->toString(); + qCWarning(logDbusProperties) << error; + } else { + qCDebug(logDbusProperties).noquote() + << "Updated property" << this->toString() << "to" << this->valueString(); + } +} + +void AbstractDBusProperty::update() { + if (this->group == nullptr) { + qFatal(logDbusProperties) << "Tried to update dbus property" << this->name + << "which is not attached to a group"; + } else { + const QString propStr = this->toString(); + + if (this->group->interface == nullptr) { + qFatal(logDbusProperties).noquote() + << "Tried to update property" << propStr << "of a disconnected interface"; + } + + qCDebug(logDbusProperties).noquote() << "Updating property" << propStr; + + auto pendingCall = + this->group->propertyInterface->Get(this->group->interface->interface(), this->name); + + auto* call = new QDBusPendingCallWatcher(pendingCall, this); + + auto responseCallback = [this, propStr](QDBusPendingCallWatcher* call) { + const QDBusPendingReply reply = *call; + + if (reply.isError()) { + qCWarning(logDbusProperties).noquote() << "Error updating property" << propStr; + qCWarning(logDbusProperties) << reply.error(); + } else { + this->tryUpdate(reply.value().variant()); + } + + delete call; + }; + + QObject::connect(call, &QDBusPendingCallWatcher::finished, this, responseCallback); + } +} + +QString AbstractDBusProperty::toString() const { + const QString group = this->group == nullptr ? "{ NO GROUP }" : this->group->toString(); + return group + ':' + this->name; +} + +DBusPropertyGroup::DBusPropertyGroup(QVector properties, QObject* parent) : QObject(parent) , properties(std::move(properties)) {} @@ -139,8 +193,9 @@ void DBusPropertyGroup::setInterface(QDBusAbstractInterface* interface) { } } -void DBusPropertyGroup::attachProperty(DBusPropertyCore* property) { +void DBusPropertyGroup::attachProperty(AbstractDBusProperty* property) { this->properties.append(property); + property->group = this; } void DBusPropertyGroup::updateAllDirect() { @@ -152,7 +207,7 @@ void DBusPropertyGroup::updateAllDirect() { } for (auto* property: this->properties) { - this->requestPropertyUpdate(property); + property->update(); } } @@ -174,117 +229,33 @@ void DBusPropertyGroup::updateAllViaGetAll() { qCWarning(logDbusProperties).noquote() << "Error updating properties of" << this->toString() << "via GetAll"; qCWarning(logDbusProperties) << reply.error(); - emit this->getAllFailed(reply.error()); } else { qCDebug(logDbusProperties).noquote() << "Received GetAll property set for" << this->toString(); - this->updatePropertySet(reply.value(), true); - emit this->getAllFinished(); + this->updatePropertySet(reply.value()); } delete call; + emit this->getAllFinished(); }; QObject::connect(call, &QDBusPendingCallWatcher::finished, this, responseCallback); } -void DBusPropertyGroup::updatePropertySet(const QVariantMap& properties, bool complainMissing) { +void DBusPropertyGroup::updatePropertySet(const QVariantMap& properties) { for (const auto [name, value]: properties.asKeyValueRange()) { - auto prop = std::ranges::find_if(this->properties, [&name](DBusPropertyCore* prop) { - return prop->nameRef() == name; - }); + auto prop = std::find_if( + this->properties.begin(), + this->properties.end(), + [&name](AbstractDBusProperty* prop) { return prop->name == name; } + ); if (prop == this->properties.end()) { - qCDebug(logDbusProperties) << "Ignoring untracked property update" << name << "for" - << this->toString(); + qCDebug(logDbusProperties) << "Ignoring untracked property update" << name << "for" << this; } else { - this->tryUpdateProperty(*prop, value); + (*prop)->tryUpdate(value); } } - - if (complainMissing) { - for (const auto* prop: this->properties) { - if (prop->isRequired() && !properties.contains(prop->name())) { - qCWarning(logDbusProperties) - << prop->nameRef() << "missing from property set for" << this->toString(); - } - } - } -} - -void DBusPropertyGroup::tryUpdateProperty(DBusPropertyCore* property, const QVariant& variant) - const { - property->mExists = true; - - auto error = property->store(variant); - if (error.isValid()) { - qCWarning(logDbusProperties).noquote() - << "Error demarshalling property update for" << this->propertyString(property); - qCWarning(logDbusProperties) << error; - } else { - qCDebug(logDbusProperties).noquote() - << "Updated property" << this->propertyString(property) << "to" << property->valueString(); - } -} - -void DBusPropertyGroup::requestPropertyUpdate(DBusPropertyCore* property) { - const QString propStr = this->propertyString(property); - - if (this->interface == nullptr) { - qFatal(logDbusProperties).noquote() - << "Tried to update property" << propStr << "of a disconnected interface"; - } - - qCDebug(logDbusProperties).noquote() << "Updating property" << propStr; - - auto pendingCall = this->propertyInterface->Get(this->interface->interface(), property->name()); - auto* call = new QDBusPendingCallWatcher(pendingCall, this); - - auto responseCallback = [this, propStr, property](QDBusPendingCallWatcher* call) { - const QDBusPendingReply reply = *call; - - if (reply.isError()) { - qCWarning(logDbusProperties).noquote() << "Error updating property" << propStr; - qCWarning(logDbusProperties) << reply.error(); - } else { - this->tryUpdateProperty(property, reply.value().variant()); - } - - delete call; - }; - - QObject::connect(call, &QDBusPendingCallWatcher::finished, this, responseCallback); -} - -void DBusPropertyGroup::pushPropertyUpdate(DBusPropertyCore* property) { - const QString propStr = this->propertyString(property); - - if (this->interface == nullptr) { - qFatal(logDbusProperties).noquote() - << "Tried to write property" << propStr << "of a disconnected interface"; - } - - qCDebug(logDbusProperties).noquote() << "Writing property" << propStr; - - auto pendingCall = this->propertyInterface->Set( - this->interface->interface(), - property->name(), - QDBusVariant(property->serialize()) - ); - - auto* call = new QDBusPendingCallWatcher(pendingCall, this); - - auto responseCallback = [propStr](QDBusPendingCallWatcher* call) { - const QDBusPendingReply<> reply = *call; - - if (reply.isError()) { - qCWarning(logDbusProperties).noquote() << "Error writing property" << propStr; - qCWarning(logDbusProperties) << reply.error(); - } - delete call; - }; - - QObject::connect(call, &QDBusPendingCallWatcher::finished, this, responseCallback); } QString DBusPropertyGroup::toString() const { @@ -296,10 +267,6 @@ QString DBusPropertyGroup::toString() const { } } -QString DBusPropertyGroup::propertyString(const DBusPropertyCore* property) const { - return this->toString() % ':' % property->nameRef(); -} - void DBusPropertyGroup::onPropertiesChanged( const QString& interfaceName, const QVariantMap& changedProperties, @@ -310,19 +277,21 @@ void DBusPropertyGroup::onPropertiesChanged( << "Received property change set and invalidations for" << this->toString(); for (const auto& name: invalidatedProperties) { - auto prop = std::ranges::find_if(this->properties, [&name](DBusPropertyCore* prop) { - return prop->nameRef() == name; - }); + auto prop = std::find_if( + this->properties.begin(), + this->properties.end(), + [&name](AbstractDBusProperty* prop) { return prop->name == name; } + ); if (prop == this->properties.end()) { qCDebug(logDbusProperties) << "Ignoring untracked property invalidation" << name << "for" << this; } else { - this->requestPropertyUpdate(*prop); + (*prop)->update(); } } - this->updatePropertySet(changedProperties, false); + this->updatePropertySet(changedProperties); } } // namespace qs::dbus diff --git a/src/dbus/properties.hpp b/src/dbus/properties.hpp index 6feaa43d..3aac07f6 100644 --- a/src/dbus/properties.hpp +++ b/src/dbus/properties.hpp @@ -1,10 +1,8 @@ #pragma once #include -#include #include -#include #include #include #include @@ -16,14 +14,9 @@ #include #include #include -#include -#include -#include #include #include -#include "../core/util.hpp" - class DBusPropertiesInterface; Q_DECLARE_LOGGING_CATEGORY(logDbusProperties); @@ -36,15 +29,15 @@ template class DBusResult { public: explicit DBusResult() = default; - DBusResult(T value): value(std::move(value)) {} - DBusResult(QDBusError error): error(std::move(error)) {} + explicit DBusResult(T value): value(std::move(value)) {} + explicit DBusResult(QDBusError error): error(std::move(error)) {} explicit DBusResult(T value, QDBusError error) : value(std::move(value)) , error(std::move(error)) {} bool isValid() { return !this->error.isValid(); } - T value {}; + T value; QDBusError error; }; @@ -66,7 +59,7 @@ template void asyncReadProperty( QDBusAbstractInterface& interface, const QString& property, - const std::function& callback + std::function callback ) { asyncReadPropertyInternal( QMetaType::fromType(), @@ -82,166 +75,55 @@ void asyncReadProperty( class DBusPropertyGroup; -class DBusPropertyCore { -public: - DBusPropertyCore() = default; - virtual ~DBusPropertyCore() = default; - Q_DISABLE_COPY_MOVE(DBusPropertyCore); +class AbstractDBusProperty: public QObject { + Q_OBJECT; - [[nodiscard]] virtual QString name() const = 0; - [[nodiscard]] virtual QStringView nameRef() const = 0; +public: + explicit AbstractDBusProperty(QString name, const QMetaType& type, QObject* parent = nullptr) + : QObject(parent) + , name(std::move(name)) + , type(type) {} + + [[nodiscard]] QString toString() const; [[nodiscard]] virtual QString valueString() = 0; - [[nodiscard]] virtual bool isRequired() const = 0; - [[nodiscard]] bool exists() const { return this->mExists; } + +public slots: + void update(); + +signals: + void changed(); protected: - virtual QDBusError store(const QVariant& variant) = 0; - [[nodiscard]] virtual QVariant serialize() = 0; + virtual QDBusError read(const QVariant& variant) = 0; private: - bool mExists : 1 = false; + void tryUpdate(const QVariant& variant); + + DBusPropertyGroup* group = nullptr; + + QString name; + QMetaType type; friend class DBusPropertyGroup; }; -// Default implementation with no transformation -template -struct DBusDataTransform { - using Wire = T; - using Data = T; -}; - -namespace bindable_p { - -template -struct BindableParams; - -template