diff --git a/.clang-tidy b/.clang-tidy index 002c444d..6642fa76 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, @@ -15,11 +12,8 @@ Checks: > -cppcoreguidelines-pro-bounds-constant-array-index, -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 +25,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 +35,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 9de26e09..6b1b58df 100644 --- a/.editorconfig +++ b/.editorconfig @@ -9,10 +9,3 @@ indent_style = tab [*.nix] indent_style = space indent_size = 2 - -[*.{yml,yaml}] -indent_style = space -indent_size = 2 - -[*.scm] -indent_style = space \ No newline at end of file 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 93b84585..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.9.0, qt6.8.3, qt6.8.2, qt6.8.1, qt6.8.0, qt6.7.3, qt6.7.2, qt6.7.1, qt6.7.0, qt6.6.3, qt6.6.2, qt6.6.1, qt6.6.0] - compiler: [clang, gcc] - runs-on: ubuntu-latest - 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 da329cc2..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: LC_ALL=en_US.UTF-8 LC_CTYPE=en_US.UTF-8 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 aa7c98ae..00000000 --- a/BUILD.md +++ /dev/null @@ -1,251 +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) -- `spirv-tools` (build-time) -- `pkg-config` (build-time) -- `cli11` (static library) - -Build time dependencies and static libraries don't have to exist at runtime, -however build time dependencies must be compiled for the architecture of -the builder, while static libraries must be compiled for the architecture -of the target. - -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` (static library) - -### 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` (build time) - - `wayland-protocols` (static library) - -Note that one or both of `wayland-scanner` and `wayland-protocols` may be bundled -with you distro's wayland package. - -#### 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).* - -Only `ninja` builds are tested, but makefiles may work. - -#### 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 55b5e5d5..67f8a1fe 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,93 +1,35 @@ cmake_minimum_required(VERSION 3.20) -project(quickshell VERSION "0.2.0" LANGUAGES CXX C) +project(quickshell VERSION "0.1.0") set(QT_MIN_VERSION "6.6.0") set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) -set(QS_BUILD_OPTIONS "") +option(TESTS "Build tests" OFF) -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(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) 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 " 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 () -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) -boption(BLUETOOTH "Bluetooth" ON) - -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) - -if (FRAME_POINTERS) - add_compile_options(-fno-omit-frame-pointer) +if (NOT DEFINED GIT_REVISION) + execute_process( + COMMAND git rev-parse HEAD + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} + OUTPUT_VARIABLE GIT_REVISION + ) endif() -if (ASAN) - add_compile_options(-fsanitize=address) - add_link_options(-fsanitize=address) -endif() +add_compile_options(-Wall -Wextra) # nix workaround if (CMAKE_EXPORT_COMPILE_COMMANDS) @@ -99,61 +41,34 @@ 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) +set(QT_FPDEPS Gui Qml Quick QuickControls2) if (BUILD_TESTING) enable_testing() - add_definitions(-DQS_TEST) list(APPEND QT_FPDEPS Test) 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 OR BLUETOOTH) - set(DBUS ON) -endif() - -if (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) +add_subdirectory(src/core) +add_subdirectory(src/io) -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}) -endif() +if (WAYLAND) + add_subdirectory(src/wayland) +endif () -install(CODE " - execute_process( - COMMAND ${CMAKE_COMMAND} -E create_symlink \ - ${CMAKE_INSTALL_FULL_BINDIR}/quickshell \$ENV{DESTDIR}${CMAKE_INSTALL_FULL_BINDIR}/qs - ) -") - -install( - FILES ${CMAKE_SOURCE_DIR}/assets/org.quickshell.desktop - DESTINATION ${CMAKE_INSTALL_DATADIR}/applications -) - -install( - FILES ${CMAKE_SOURCE_DIR}/assets/quickshell.svg - DESTINATION ${CMAKE_INSTALL_DATADIR}/icons/hicolor/scalable/apps - RENAME org.quickshell.svg -) +install(TARGETS quickshell RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index 39fab13e..00000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,235 +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`. - -#### Style preferences not caught by clang-format -These are flexible. You can ignore them if it looks or works better to -for one reason or another. - -Use `auto` if the type of a variable can be deduced automatically, instead of -redeclaring the returned value's type. Additionally, auto should be used when a -constructor takes arguments. - -```cpp -auto x = ; // ok -auto x = QString::number(3); // ok -QString x; // ok -QString x = "foo"; // ok -auto x = QString("foo"); // ok - -auto x = QString(); // avoid -QString x(); // avoid -QString x("foo"); // avoid -``` - -Put newlines around logical units of code, and after closing braces. If the -most reasonable logical unit of code takes only a single line, it should be -merged into the next single line logical unit if applicable. -```cpp -// multiple units -auto x = ; // unit 1 -auto y = ; // unit 2 - -auto x = ; // unit 1 -emit this->y(); // unit 2 - -auto x1 = ; // unit 1 -auto x2 = ; // unit 1 -auto x3 = ; // unit 1 - -auto y1 = ; // unit 2 -auto y2 = ; // unit 2 -auto y3 = ; // unit 2 - -// one unit -auto x = ; -if (x...) { - // ... -} - -// if more than one variable needs to be used then add a newline -auto x = ; -auto y = ; - -if (x && y) { - // ... -} -``` - -Class formatting: -```cpp -//! Doc comment summary -/// Doc comment body -class Foo: public QObject { - // The Q_OBJECT macro comes first. Macros are ; terminated. - Q_OBJECT; - QML_ELEMENT; - QML_CLASSINFO(...); - // Properties must stay on a single line or the doc generator won't be able to pick them up - Q_PROPERTY(...); - /// Doc comment - Q_PROPERTY(...); - /// Doc comment - Q_PROPERTY(...); - -public: - // Classes should have explicit constructors if they aren't intended to - // implicitly cast. The constructor can be inline in the header if it has no body. - explicit Foo(QObject* parent = nullptr): QObject(parent) {} - - // Instance functions if applicable. - static Foo* instance(); - - // Member functions unrelated to properties come next - void function(); - void function(); - void function(); - - // Then Q_INVOKABLEs - Q_INVOKABLE function(); - /// Doc comment - Q_INVOKABLE function(); - /// Doc comment - Q_INVOKABLE function(); - - // Then property related functions, in the order (bindable, getter, setter). - // Related functions may be included here as well. Function bodies may be inline - // if they are a single expression. There should be a newline between each - // property's methods. - [[nodiscard]] QBindable bindableFoo() { return &this->bFoo; } - [[nodiscard]] T foo() const { return this->foo; } - void setFoo(); - - [[nodiscard]] T bar() const { return this->foo; } - void setBar(); - -signals: - // Signals that are not property change related go first. - // Property change signals go in property definition order. - void asd(); - void asd2(); - void fooChanged(); - void barChanged(); - -public slots: - // generally Q_INVOKABLEs are preferred to public slots. - void slot(); - -private slots: - // ... - -private: - // statics, then functions, then fields - static const foo BAR; - static void foo(); - - void foo(); - void bar(); - - // property related members are prefixed with `m`. - QString mFoo; - QString bar; - - // Bindables go last and should be prefixed with `b`. - Q_OBJECT_BINDABLE_PROPERTY(Foo, QString, bFoo, &Foo::fooChanged); -}; -``` - -### 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-changed -``` - -If the linter is complaining about something that you think it should not, -please disable the lint in your MR and explain your reasoning if it isn't obvious. - -### 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..314bcdd5 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}} \ @@ -32,7 +26,7 @@ clean: rm -rf {{builddir}} run *ARGS='': build - {{builddir}}/src/quickshell {{ARGS}} + {{builddir}}/src/core/quickshell {{ARGS}} test *ARGS='': build ctest --test-dir {{builddir}} --output-on-failure {{ARGS}} diff --git a/README.md b/README.md index 4491d24b..e075a322 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,107 @@ -# Quickshell -See the [website](https://quickshell.outfoxxed.me) for more information -and installation instructions. +# quickshell -This repo is hosted at: -- https://git.outfoxxed.me/quickshell/quickshell -- https://github.com/quickshell-mirror/quickshell +Simple and flexbile QtQuick based desktop shell toolkit. -# Contributing / Development -See [CONTRIBUTING.md](CONTRIBUTING.md) for details. +Hosts: [outfoxxed's gitea], [github] + +[outfoxxed's gitea]: https://git.outfoxxed.me/outfoxxed/quickshell +[github]: https://github.com/outfoxxed/quickshell + +Documentation can be built from the [quickshell-docs](https://git.outfoxxed.me/outfoxxed/quickshell-docs) repo, +though is currently pretty lacking. + +Some fully working examples can be found in the [quickshell-examples](https://git.outfoxxed.me/outfoxxed/quickshell-examples) +repo. + +Both the documentation and examples are included as submodules with revisions that work with the current +version of quickshell. + +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 + +## Nix +This repo has a nix flake you can use to install the package directly: + +```nix +{ + inputs = { + nixpkgs.url = "nixpkgs/nixos-unstable"; + + quickshell = { + url = "git+https://git.outfoxxed.me/outfoxxed/quickshell"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + }; +} +``` + +Quickshell's binary is available at `quickshell.packages..default` to be added to +lists such as `environment.systemPackages` or `home.packages`. + +## Manual + +If not using nix, you'll have to build from source. + +### Dependencies +To build quickshell at all, you will need the following packages (names may vary by distro) + +- just +- cmake +- pkg-config +- ninja +- Qt6 [ QtBase, QtDeclarative ] + +To build with wayland support you will additionally need: +- wayland +- wayland-scanner (may be part of wayland on some distros) +- wayland-protocols +- Qt6 [ QtWayland ] + +### Building + +To make a release build of quickshell run: +```sh +$ just release +``` + +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/assets/org.quickshell.desktop b/assets/org.quickshell.desktop deleted file mode 100644 index 63f65fd9..00000000 --- a/assets/org.quickshell.desktop +++ /dev/null @@ -1,7 +0,0 @@ -[Desktop Entry] -Version=1.5 -Type=Application -NoDisplay=true - -Name=Quickshell -Icon=org.quickshell diff --git a/assets/quickshell.svg b/assets/quickshell.svg deleted file mode 100644 index 7d0f9481..00000000 --- a/assets/quickshell.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/changelog/v0.1.0.md b/changelog/v0.1.0.md deleted file mode 100644 index f8a032f2..00000000 --- a/changelog/v0.1.0.md +++ /dev/null @@ -1 +0,0 @@ -Initial release diff --git a/changelog/v0.2.0.md b/changelog/v0.2.0.md deleted file mode 100644 index 2fbf74d7..00000000 --- a/changelog/v0.2.0.md +++ /dev/null @@ -1,84 +0,0 @@ -## Breaking Changes - -- Files outside of the shell directory can no longer be referenced with relative paths, e.g. '../../foo.png'. -- PanelWindow's Automatic exclusion mode now adds an exclusion zone for panels with a single anchor. -- `QT_QUICK_CONTROLS_STYLE` and `QT_STYLE_OVERRIDE` are ignored unless `//@ pragma RespectSystemStyle` is set. - -## New Features - -### Root-Relative Imports - -Quickshell 0.2 comes with a new method to import QML modules which is supported by QMLLS. -This replaces "root:/" imports for QML modules. - -The new syntax is `import qs.path.to.module`, where `path/to/module` is the path to -a module/subdirectory relative to the config root (`qs`). - -### Better LSP support - -LSP support for Singletons and Root-Relative imports can be enabled by creating a file named -`.qmlls.ini` in the shell root directory. Quickshell will detect this file and automatically -populate it with an LSP configuration. This file should be gitignored in your configuration, -as it is system dependent. - -The generated configuration also includes QML import paths available to Quickshell, meaning -QMLLS no longer requires the `-E` flag. - -### Bluetooth Module - -Quickshell can now manage your bluetooth devices through BlueZ. While authenticated pairing -has not landed in 0.2, support for connecting and disconnecting devices, basic device information, -and non-authenticated pairing are now supported. - -### Other Features - -- Added `HyprlandToplevel` and related toplevel/window management APIs in the Hyprland module. -- Added `Quickshell.execDetached()`, which spawns a detached process without a `Process` object. -- Added `Process.exec()` for easier reconfiguration of process commands when starting them. -- Added `FloatingWindow.title`, which allows changing the title of a floating window. -- Added `signal QsWindow.closed()`, fired when a window is closed externally. -- Added support for inline replies in notifications, when supported by applications. -- Added `DesktopEntry.startupWmClass` and `DesktopEntry.heuristicLookup()` to better identify toplevels. -- Added `DesktopEntry.command` which can be run as an alternative to `DesktopEntry.execute()`. -- Added `//@ pragma Internal`, which makes a QML component impossible to import outside of its module. -- Added dead instance selection for some subcommands, such as `qs log` and `qs list`. - -## Other Changes - -- `Quickshell.shellRoot` has been renamed to `Quickshell.shellDir`. -- PanelWindow margins opposite the window's anchorpoint are now added to exclusion zone. -- stdout/stderr or detached processes and executed desktop entries are now hidden by default. -- Various warnings caused by other applications Quickshell communicates with over D-BUS have been hidden in logs. -- Quickshell's new logo is now shown in any floating windows. - -## Bug Fixes - -- Fixed pipewire device volume and mute states not updating before the device has been used. -- Fixed a crash when changing the volume of any pipewire device on a sound card another removed device was using. -- Fixed a crash when accessing a removed previous default pipewire node from the default sink/source changed signals. -- Fixed session locks crashing if all monitors are disconnected. -- Fixed session locks crashing if unsupported by the compositor. -- Fixed a crash when creating a session lock and destroying it before acknowledged by the compositor. -- Fixed window input masks not updating after a reload. -- Fixed PanelWindows being unconfigurable unless `screen` was set under X11. -- Fixed a crash when anchoring a popup to a zero sized `Item`. -- Fixed `FileView` crashing if `watchChanges` was used. -- Fixed `SocketServer` sockets disappearing after a reload. -- Fixed `ScreencopyView` having incorrect rotation when displaying a rotated monitor. -- Fixed `MarginWrapperManager` breaking pixel alignment of child items when centering. -- Fixed `IpcHandler`, `NotificationServer` and `GlobalShortcut` not activating with certain QML structures. -- Fixed tracking of QML incubator destruction and deregistration, which occasionally caused crashes. -- Fixed FloatingWindows being constrained to the smallest window manager supported size unless max size was set. -- Fixed `MprisPlayer.lengthSupported` not updating reactively. -- Fixed normal tray icon being ignored when status is `NeedsAttention` and no attention icon is provided. -- Fixed `HyprlandWorkspace.activate()` sending invalid commands to Hyprland for named or special workspaces. -- Fixed file watcher occasionally breaking when using VSCode to edit QML files. -- Fixed crashes when screencopy buffer creation fails. -- Fixed a crash when wayland layer surfaces are recreated for the same window. -- Fixed the `QsWindow` attached object not working when using `WlrLayershell` directly. -- Fixed a crash when attempting to create a window without available VRAM. -- Fixed OOM crash when failing to write to detailed log file. -- Prevented distro logging configurations for Qt from interfering with Quickshell commands. -- Removed the "QProcess destroyed for running process" warning when destroying `Process` objects. -- Fixed `ColorQuantizer` printing a pointer to an error message instead of an error message. -- Fixed notification pixmap rowstride warning showing for correct rowstrides. diff --git a/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 73c24156..00000000 --- a/ci/nix-checkouts.nix +++ /dev/null @@ -1,78 +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_9_0 = byCommit { - commit = "546c545bd0594809a28ab7e869b5f80dd7243ef6"; - sha256 = "0562lbi67a9brfwzpqs4n3l0i8zvgla368aakcy5mghr7ps80567"; - }; - - qt6_8_3 = byCommit { - commit = "374e6bcc403e02a35e07b650463c01a52b13a7c8"; - sha256 = "1ck2d7q1f6k58qg47bc07036h9gmc2mqmqlgrv67k3frgplfhfga"; - }; - - qt6_8_2 = byCommit { - commit = "97be9fbfc7a8a794bb51bd5dfcbfad5fad860512"; - sha256 = "1sqh6kb8yg9yw6brkkb3n4y3vpbx8fnx45skyikqdqj2xs76v559"; - }; - - qt6_8_1 = byCommit { - commit = "4a66c00fcb3f85ddad658b8cfa2e870063ce60b5"; - sha256 = "1fcvr67s7366bk8czzwhr12zsq60izl5iq4znqbm44pzyq9pf8rq"; - }; - - 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 71c949e3..4fa9326f 100644 --- a/default.nix +++ b/default.nix @@ -3,24 +3,13 @@ nix-gitignore, pkgs, keepDebugInfo, - buildStdenv ? pkgs.clangStdenv, + stdenv ? (keepDebugInfo pkgs.stdenv), - pkg-config, cmake, ninja, - spirv-tools, qt6, - breakpad, - jemalloc, - cli11, wayland, wayland-protocols, - wayland-scanner, - xorg, - libdrm, - libgbm ? null, - pipewire, - pam, gitRev ? (let headExists = builtins.pathExists ./.git/HEAD; @@ -32,101 +21,51 @@ then builtins.readFile ./.git/refs/heads/${builtins.elemAt matches 0} else headContent) 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, -}: let - unwrapped = buildStdenv.mkDerivation { - pname = "quickshell${lib.optionalString debug "-debug"}"; - version = "0.2.0"; - src = nix-gitignore.gitignoreSource "/default.nix\n" ./.; + enableWayland ? true, +}: stdenv.mkDerivation { + pname = "quickshell${lib.optionalString debug "-debug"}"; + version = "0.1.0"; + src = nix-gitignore.gitignoreSource [] ./.; - dontWrapQtApps = true; # see wrappers + nativeBuildInputs = with pkgs; [ + cmake + ninja + qt6.wrapQtAppsHook + ] ++ (lib.optionals enableWayland [ + pkg-config + wayland-protocols + wayland-scanner + ]); - nativeBuildInputs = [ - cmake - ninja - qt6.qtshadertools - spirv-tools - pkg-config - ] - ++ lib.optional withWayland wayland-scanner; + buildInputs = with pkgs; [ + qt6.qtbase + qt6.qtdeclarative + ] ++ (lib.optionals enableWayland [ qt6.qtwayland wayland ]); - buildInputs = [ - qt6.qtbase - qt6.qtdeclarative - cli11 - ] - ++ lib.optional withQtSvg qt6.qtsvg - ++ lib.optional withCrashReporter breakpad - ++ lib.optional withJemalloc jemalloc - ++ lib.optionals withWayland [ qt6.qtwayland wayland wayland-protocols ] - ++ lib.optionals (withWayland && libgbm != null) [ libdrm libgbm ] - ++ lib.optional withX11 xorg.libxcb - ++ lib.optional withPam pam - ++ lib.optional withPipewire pipewire; + QTWAYLANDSCANNER = lib.optionalString enableWayland "${qt6.qtwayland}/libexec/qtwaylandscanner"; - cmakeBuildType = if debug then "Debug" else "RelWithDebInfo"; + 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) - ]; + cmakeFlags = [ + "-DGIT_REVISION=${gitRev}" + ] ++ lib.optional (!enableWayland) "-DWAYLAND=OFF"; - # 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://quickshell.org"; - description = "Flexbile QtQuick based desktop shell toolkit"; - license = licenses.lgpl3Only; - platforms = platforms.linux; - mainProgram = "quickshell"; - }; + meta = with lib; { + homepage = "https://git.outfoxxed.me/outfoxxed/quickshell"; + description = "Simple and flexbile QtQuick based desktop shell toolkit"; + license = licenses.lgpl3Only; + platforms = platforms.linux; }; - - wrapper = unwrapped.stdenv.mkDerivation { - inherit (unwrapped) version meta buildInputs; - pname = "${unwrapped.pname}-wrapped"; - - nativeBuildInputs = unwrapped.nativeBuildInputs ++ [ qt6.wrapQtAppsHook ]; - - dontUnpack = true; - dontConfigure = true; - dontBuild = true; - - installPhase = '' - mkdir -p $out - cp -r ${unwrapped}/* $out - ''; - - passthru = { - unwrapped = unwrapped; - withModules = modules: wrapper.overrideAttrs (prev: { - buildInputs = prev.buildInputs ++ modules; - }); - }; - }; -in wrapper +} diff --git a/docs b/docs new file mode 160000 index 00000000..70989dc6 --- /dev/null +++ b/docs @@ -0,0 +1 @@ +Subproject commit 70989dc619bcdc29dc4880b4ff5257d6ad188a18 diff --git a/examples b/examples new file mode 160000 index 00000000..9c83cc24 --- /dev/null +++ b/examples @@ -0,0 +1 @@ +Subproject commit 9c83cc248c968b18a827b4fa4c616a8d362176e1 diff --git a/flake.lock b/flake.lock index 7c25aa23..1527f635 100644 --- a/flake.lock +++ b/flake.lock @@ -2,11 +2,11 @@ "nodes": { "nixpkgs": { "locked": { - "lastModified": 1749285348, - "narHash": "sha256-frdhQvPbmDYaScPFiCnfdh3B/Vh81Uuoo0w5TkWmmjU=", + "lastModified": 1709237383, + "narHash": "sha256-cy6ArO4k5qTx+l5o+0mL9f5fa86tYUX3ozE1S+Txlds=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "3e3afe5174c561dee0df6f2c2b2236990146329f", + "rev": "1536926ef5621b09bba54035ae2bb6d806d72ac8", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index 5de9c96a..b9ba8d1f 100644 --- a/flake.nix +++ b/flake.nix @@ -4,16 +4,12 @@ }; outputs = { self, nixpkgs }: let - forEachSystem = fn: - nixpkgs.lib.genAttrs - nixpkgs.lib.platforms.linux - (system: fn system nixpkgs.legacyPackages.${system}); + forEachSystem = fn: nixpkgs.lib.genAttrs + [ "x86_64-linux" "aarch64-linux" ] + (system: fn system nixpkgs.legacyPackages.${system}); in { packages = forEachSystem (system: pkgs: rec { - quickshell = pkgs.callPackage ./default.nix { - gitRev = self.rev or self.dirtyRev; - }; - + quickshell = import ./package.nix { inherit pkgs; }; default = quickshell; }); diff --git a/package.nix b/package.nix new file mode 100644 index 00000000..a3e6249e --- /dev/null +++ b/package.nix @@ -0,0 +1 @@ +{ pkgs ? import {}, ... }: pkgs.callPackage ./default.nix {} diff --git a/quickshell.scm b/quickshell.scm deleted file mode 100644 index 26abdc0b..00000000 --- a/quickshell.scm +++ /dev/null @@ -1,77 +0,0 @@ -(define-module (quickshell) - #:use-module ((guix licenses) #:prefix license:) - #:use-module (gnu packages cpp) - #:use-module (gnu packages freedesktop) - #:use-module (gnu packages gcc) - #:use-module (gnu packages gl) - #:use-module (gnu packages jemalloc) - #:use-module (gnu packages linux) - #:use-module (gnu packages ninja) - #:use-module (gnu packages pkg-config) - #:use-module (gnu packages qt) - #:use-module (gnu packages vulkan) - #:use-module (gnu packages xdisorg) - #:use-module (gnu packages xorg) - #:use-module (guix build-system cmake) - #:use-module (guix download) - #:use-module (guix gexp) - #:use-module (guix git-download) - #:use-module (guix packages) - #:use-module (guix packages) - #:use-module (guix utils)) - -(define-public quickshell-git - (package - (name "quickshell") - (version "git") - (source (local-file "." "quickshell-checkout" - #:recursive? #t - #:select? (or (git-predicate (current-source-directory)) - (const #t)))) - (build-system cmake-build-system) - (propagated-inputs (list qtbase qtdeclarative qtsvg)) - (native-inputs (list ninja - gcc-14 - pkg-config - qtshadertools - spirv-tools - wayland-protocols - cli11)) - (inputs (list jemalloc - libdrm - libxcb - libxkbcommon - linux-pam - mesa - pipewire - qtbase - qtdeclarative - qtwayland - vulkan-headers - wayland)) - (arguments - (list #:tests? #f - #:configure-flags - #~(list "-GNinja" - "-DDISTRIBUTOR=\"In-tree Guix channel\"" - "-DDISTRIBUTOR_DEBUGINFO_AVAILABLE=NO" - ;; Breakpad is not currently packaged for Guix. - "-DCRASH_REPORTER=OFF") - #:phases - #~(modify-phases %standard-phases - (replace 'build (lambda _ (invoke "cmake" "--build" "."))) - (replace 'install (lambda _ (invoke "cmake" "--install" "."))) - (add-after 'install 'wrap-program - (lambda* (#:key inputs #:allow-other-keys) - (wrap-program (string-append #$output "/bin/quickshell") - `("QML_IMPORT_PATH" ":" - = (,(getenv "QML_IMPORT_PATH"))))))))) - (home-page "https://quickshell.outfoxxed.me") - (synopsis "QtQuick-based desktop shell toolkit") - (description - "Quickshell is a flexible QtQuick-based toolkit for creating and -customizing toolbars, notification centers, and other desktop -environment tools in a live programming environment.") - (license license:lgpl3))) - -quickshell-git diff --git a/shell.nix b/shell.nix index 82382f90..484bf70a 100644 --- a/shell.nix +++ b/shell.nix @@ -10,17 +10,18 @@ rev = "1f062cc198d1112d13e5128fa1f2ee3dbffe613b"; sha256 = "kbt0Zc1qHE5fhqBkKz8iue+B+ZANjF1AR/RdgmX1r0I="; }) { inherit pkgs; }; -in pkgs.mkShell.override { stdenv = quickshell.stdenv; } { +in pkgs.mkShell { inputsFrom = [ quickshell ]; 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 deleted file mode 100644 index 52db00a5..00000000 --- a/src/CMakeLists.txt +++ /dev/null @@ -1,35 +0,0 @@ -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) -add_subdirectory(ui) - -if (CRASH_REPORTER) - add_subdirectory(crash) -endif() - -if (DBUS) - add_subdirectory(dbus) -endif() - -if (WAYLAND) - add_subdirectory(wayland) -endif() - -if (X11) - add_subdirectory(x11) -endif() - -add_subdirectory(services) - -if (BLUETOOTH) - add_subdirectory(bluetooth) -endif() diff --git a/src/bluetooth/CMakeLists.txt b/src/bluetooth/CMakeLists.txt deleted file mode 100644 index 806ff04d..00000000 --- a/src/bluetooth/CMakeLists.txt +++ /dev/null @@ -1,42 +0,0 @@ -set_source_files_properties(org.bluez.Adapter.xml PROPERTIES - CLASSNAME DBusBluezAdapterInterface -) - -set_source_files_properties(org.bluez.Device.xml PROPERTIES - CLASSNAME DBusBluezDeviceInterface -) - -qt_add_dbus_interface(DBUS_INTERFACES - org.bluez.Adapter.xml - dbus_adapter -) - -qt_add_dbus_interface(DBUS_INTERFACES - org.bluez.Device.xml - dbus_device -) - -qt_add_library(quickshell-bluetooth STATIC - adapter.cpp - bluez.cpp - device.cpp - ${DBUS_INTERFACES} -) - -qt_add_qml_module(quickshell-bluetooth - URI Quickshell.Bluetooth - VERSION 0.1 - DEPENDENCIES QtQml -) - -install_qml_module(quickshell-bluetooth) - -# dbus headers -target_include_directories(quickshell-bluetooth PRIVATE ${CMAKE_CURRENT_BINARY_DIR}) - -target_link_libraries(quickshell-bluetooth PRIVATE Qt::Qml Qt::DBus) -qs_add_link_dependencies(quickshell-bluetooth quickshell-dbus) - -qs_module_pch(quickshell-bluetooth SET dbus) - -target_link_libraries(quickshell PRIVATE quickshell-bluetoothplugin) diff --git a/src/bluetooth/adapter.cpp b/src/bluetooth/adapter.cpp deleted file mode 100644 index 0d8a3192..00000000 --- a/src/bluetooth/adapter.cpp +++ /dev/null @@ -1,224 +0,0 @@ -#include "adapter.hpp" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "../core/logcat.hpp" -#include "../dbus/properties.hpp" -#include "dbus_adapter.h" - -namespace qs::bluetooth { - -namespace { -QS_LOGGING_CATEGORY(logAdapter, "quickshell.bluetooth.adapter", QtWarningMsg); -} - -QString BluetoothAdapterState::toString(BluetoothAdapterState::Enum state) { - switch (state) { - case BluetoothAdapterState::Disabled: return QStringLiteral("Disabled"); - case BluetoothAdapterState::Enabled: return QStringLiteral("Enabled"); - case BluetoothAdapterState::Enabling: return QStringLiteral("Enabling"); - case BluetoothAdapterState::Disabling: return QStringLiteral("Disabling"); - case BluetoothAdapterState::Blocked: return QStringLiteral("Blocked"); - default: return QStringLiteral("Unknown"); - } -} - -BluetoothAdapter::BluetoothAdapter(const QString& path, QObject* parent): QObject(parent) { - this->mInterface = - new DBusBluezAdapterInterface("org.bluez", path, QDBusConnection::systemBus(), this); - - if (!this->mInterface->isValid()) { - qCWarning(logAdapter) << "Could not create DBus interface for adapter at" << path; - this->mInterface = nullptr; - return; - } - - this->properties.setInterface(this->mInterface); -} - -QString BluetoothAdapter::adapterId() const { - auto path = this->path(); - return path.sliced(path.lastIndexOf('/') + 1); -} - -void BluetoothAdapter::setEnabled(bool enabled) { - if (enabled == this->bEnabled) return; - - if (enabled && this->bState == BluetoothAdapterState::Blocked) { - qCCritical(logAdapter) << "Cannot enable adapter because it is blocked by rfkill."; - return; - } - - this->bEnabled = enabled; - this->pEnabled.write(); -} - -void BluetoothAdapter::setDiscoverable(bool discoverable) { - if (discoverable == this->bDiscoverable) return; - this->bDiscoverable = discoverable; - this->pDiscoverable.write(); -} - -void BluetoothAdapter::setDiscovering(bool discovering) { - if (discovering) { - this->startDiscovery(); - } else { - this->stopDiscovery(); - } -} - -void BluetoothAdapter::setDiscoverableTimeout(quint32 timeout) { - if (timeout == this->bDiscoverableTimeout) return; - this->bDiscoverableTimeout = timeout; - this->pDiscoverableTimeout.write(); -} - -void BluetoothAdapter::setPairable(bool pairable) { - if (pairable == this->bPairable) return; - this->bPairable = pairable; - this->pPairable.write(); -} - -void BluetoothAdapter::setPairableTimeout(quint32 timeout) { - if (timeout == this->bPairableTimeout) return; - this->bPairableTimeout = timeout; - this->pPairableTimeout.write(); -} - -void BluetoothAdapter::addInterface(const QString& interface, const QVariantMap& properties) { - if (interface == "org.bluez.Adapter1") { - this->properties.updatePropertySet(properties, false); - qCDebug(logAdapter) << "Updated Adapter properties for" << this; - } -} - -void BluetoothAdapter::removeDevice(const QString& devicePath) { - qCDebug(logAdapter) << "Removing device" << devicePath << "from adapter" << this; - - auto reply = this->mInterface->RemoveDevice(QDBusObjectPath(devicePath)); - - auto* watcher = new QDBusPendingCallWatcher(reply, this); - - QObject::connect( - watcher, - &QDBusPendingCallWatcher::finished, - this, - [this, devicePath](QDBusPendingCallWatcher* watcher) { - const QDBusPendingReply<> reply = *watcher; - - if (reply.isError()) { - qCWarning(logAdapter).nospace() - << "Failed to remove device " << devicePath << " from adapter" << this << ": " - << reply.error().message(); - } else { - qCDebug(logAdapter) << "Successfully removed device" << devicePath << "from adapter" - << this; - } - - delete watcher; - } - ); -} - -void BluetoothAdapter::startDiscovery() { - if (this->bDiscovering) return; - qCDebug(logAdapter) << "Starting discovery for adapter" << this; - - auto reply = this->mInterface->StartDiscovery(); - auto* watcher = new QDBusPendingCallWatcher(reply, this); - - QObject::connect( - watcher, - &QDBusPendingCallWatcher::finished, - this, - [this](QDBusPendingCallWatcher* watcher) { - const QDBusPendingReply<> reply = *watcher; - - if (reply.isError()) { - qCWarning(logAdapter).nospace() - << "Failed to start discovery on adapter" << this << ": " << reply.error().message(); - } else { - qCDebug(logAdapter) << "Successfully started discovery on adapter" << this; - } - - delete watcher; - } - ); -} - -void BluetoothAdapter::stopDiscovery() { - if (!this->bDiscovering) return; - qCDebug(logAdapter) << "Stopping discovery for adapter" << this; - - auto reply = this->mInterface->StopDiscovery(); - auto* watcher = new QDBusPendingCallWatcher(reply, this); - - QObject::connect( - watcher, - &QDBusPendingCallWatcher::finished, - this, - [this](QDBusPendingCallWatcher* watcher) { - const QDBusPendingReply<> reply = *watcher; - - if (reply.isError()) { - qCWarning(logAdapter).nospace() - << "Failed to stop discovery on adapter " << this << ": " << reply.error().message(); - } else { - qCDebug(logAdapter) << "Successfully stopped discovery on adapter" << this; - } - - delete watcher; - } - ); -} - -} // namespace qs::bluetooth - -namespace qs::dbus { - -using namespace qs::bluetooth; - -DBusResult -DBusDataTransform::fromWire(const Wire& wire) { - if (wire == QStringLiteral("off")) { - return BluetoothAdapterState::Disabled; - } else if (wire == QStringLiteral("on")) { - return BluetoothAdapterState::Enabled; - } else if (wire == QStringLiteral("off-enabling")) { - return BluetoothAdapterState::Enabling; - } else if (wire == QStringLiteral("on-disabling")) { - return BluetoothAdapterState::Disabling; - } else if (wire == QStringLiteral("off-blocked")) { - return BluetoothAdapterState::Blocked; - } else { - return QDBusError( - QDBusError::InvalidArgs, - QString("Invalid BluetoothAdapterState: %1").arg(wire) - ); - } -} - -} // namespace qs::dbus - -QDebug operator<<(QDebug debug, const qs::bluetooth::BluetoothAdapter* adapter) { - auto saver = QDebugStateSaver(debug); - - if (adapter) { - debug.nospace() << "BluetoothAdapter(" << static_cast(adapter) - << ", path=" << adapter->path() << ")"; - } else { - debug << "BluetoothAdapter(nullptr)"; - } - - return debug; -} diff --git a/src/bluetooth/adapter.hpp b/src/bluetooth/adapter.hpp deleted file mode 100644 index d7f21d7e..00000000 --- a/src/bluetooth/adapter.hpp +++ /dev/null @@ -1,173 +0,0 @@ -#pragma once - -#include -#include -#include - -#include "../core/doc.hpp" -#include "../core/model.hpp" -#include "../dbus/properties.hpp" -#include "dbus_adapter.h" - -namespace qs::bluetooth { - -///! Power state of a Bluetooth adapter. -class BluetoothAdapterState: public QObject { - Q_OBJECT; - QML_ELEMENT; - QML_SINGLETON; - -public: - enum Enum : quint8 { - /// The adapter is powered off. - Disabled = 0, - /// The adapter is powered on. - Enabled = 1, - /// The adapter is transitioning from off to on. - Enabling = 2, - /// The adapter is transitioning from on to off. - Disabling = 3, - /// The adapter is blocked by rfkill. - Blocked = 4, - }; - Q_ENUM(Enum); - - Q_INVOKABLE static QString toString(BluetoothAdapterState::Enum state); -}; - -} // namespace qs::bluetooth - -namespace qs::dbus { - -template <> -struct DBusDataTransform { - using Wire = QString; - using Data = qs::bluetooth::BluetoothAdapterState::Enum; - static DBusResult fromWire(const Wire& wire); -}; - -} // namespace qs::dbus - -namespace qs::bluetooth { - -class BluetoothAdapter; -class BluetoothDevice; - -///! A Bluetooth adapter -class BluetoothAdapter: public QObject { - Q_OBJECT; - QML_ELEMENT; - QML_UNCREATABLE(""); - // clang-format off - /// System provided name of the adapter. See @@adapterId for the internal identifier. - Q_PROPERTY(QString name READ default NOTIFY nameChanged BINDABLE bindableName); - /// True if the adapter is currently enabled. More detailed state is available from @@state. - Q_PROPERTY(bool enabled READ enabled WRITE setEnabled NOTIFY enabledChanged); - /// Detailed power state of the adapter. - Q_PROPERTY(BluetoothAdapterState::Enum state READ default NOTIFY stateChanged BINDABLE bindableState); - /// True if the adapter can be discovered by other bluetooth devices. - Q_PROPERTY(bool discoverable READ discoverable WRITE setDiscoverable NOTIFY discoverableChanged); - /// Timeout in seconds for how long the adapter stays discoverable after @@discoverable is set to true. - /// A value of 0 means the adapter stays discoverable forever. - Q_PROPERTY(quint32 discoverableTimeout READ discoverableTimeout WRITE setDiscoverableTimeout NOTIFY discoverableTimeoutChanged); - /// True if the adapter is scanning for new devices. - Q_PROPERTY(bool discovering READ discovering WRITE setDiscovering NOTIFY discoveringChanged); - /// True if the adapter is accepting incoming pairing requests. - /// - /// This only affects incoming pairing requests and should typically only be changed - /// by system settings applications. Defaults to true. - Q_PROPERTY(bool pairable READ pairable WRITE setPairable NOTIFY pairableChanged); - /// Timeout in seconds for how long the adapter stays pairable after @@pairable is set to true. - /// A value of 0 means the adapter stays pairable forever. Defaults to 0. - Q_PROPERTY(quint32 pairableTimeout READ pairableTimeout WRITE setPairableTimeout NOTIFY pairableTimeoutChanged); - /// Bluetooth devices connected to this adapter. - QSDOC_TYPE_OVERRIDE(ObjectModel*); - Q_PROPERTY(UntypedObjectModel* devices READ devices CONSTANT); - /// The internal ID of the adapter (e.g., "hci0"). - Q_PROPERTY(QString adapterId READ adapterId CONSTANT); - /// DBus path of the adapter under the `org.bluez` system service. - Q_PROPERTY(QString dbusPath READ path CONSTANT); - // clang-format on - -public: - explicit BluetoothAdapter(const QString& path, QObject* parent = nullptr); - - [[nodiscard]] bool isValid() const { return this->mInterface->isValid(); } - [[nodiscard]] QString path() const { return this->mInterface->path(); } - [[nodiscard]] QString adapterId() const; - - [[nodiscard]] bool enabled() const { return this->bEnabled; } - void setEnabled(bool enabled); - - [[nodiscard]] bool discoverable() const { return this->bDiscoverable; } - void setDiscoverable(bool discoverable); - - [[nodiscard]] bool discovering() const { return this->bDiscovering; } - void setDiscovering(bool discovering); - - [[nodiscard]] quint32 discoverableTimeout() const { return this->bDiscoverableTimeout; } - void setDiscoverableTimeout(quint32 timeout); - - [[nodiscard]] bool pairable() const { return this->bPairable; } - void setPairable(bool pairable); - - [[nodiscard]] quint32 pairableTimeout() const { return this->bPairableTimeout; } - void setPairableTimeout(quint32 timeout); - - [[nodiscard]] QBindable bindableName() { return &this->bName; } - [[nodiscard]] QBindable bindableEnabled() { return &this->bEnabled; } - [[nodiscard]] QBindable bindableState() { return &this->bState; } - [[nodiscard]] QBindable bindableDiscoverable() { return &this->bDiscoverable; } - [[nodiscard]] QBindable bindableDiscoverableTimeout() { - return &this->bDiscoverableTimeout; - } - [[nodiscard]] QBindable bindableDiscovering() { return &this->bDiscovering; } - [[nodiscard]] QBindable bindablePairable() { return &this->bPairable; } - [[nodiscard]] QBindable bindablePairableTimeout() { return &this->bPairableTimeout; } - [[nodiscard]] ObjectModel* devices() { return &this->mDevices; } - - void addInterface(const QString& interface, const QVariantMap& properties); - void removeDevice(const QString& devicePath); - - void startDiscovery(); - void stopDiscovery(); - -signals: - void nameChanged(); - void enabledChanged(); - void stateChanged(); - void discoverableChanged(); - void discoverableTimeoutChanged(); - void discoveringChanged(); - void pairableChanged(); - void pairableTimeoutChanged(); - -private: - DBusBluezAdapterInterface* mInterface = nullptr; - ObjectModel mDevices {this}; - - // clang-format off - Q_OBJECT_BINDABLE_PROPERTY(BluetoothAdapter, QString, bName, &BluetoothAdapter::nameChanged); - Q_OBJECT_BINDABLE_PROPERTY(BluetoothAdapter, bool, bEnabled, &BluetoothAdapter::enabledChanged); - Q_OBJECT_BINDABLE_PROPERTY(BluetoothAdapter, BluetoothAdapterState::Enum, bState, &BluetoothAdapter::stateChanged); - Q_OBJECT_BINDABLE_PROPERTY(BluetoothAdapter, bool, bDiscoverable, &BluetoothAdapter::discoverableChanged); - Q_OBJECT_BINDABLE_PROPERTY(BluetoothAdapter, quint32, bDiscoverableTimeout, &BluetoothAdapter::discoverableTimeoutChanged); - Q_OBJECT_BINDABLE_PROPERTY(BluetoothAdapter, bool, bDiscovering, &BluetoothAdapter::discoveringChanged); - Q_OBJECT_BINDABLE_PROPERTY(BluetoothAdapter, bool, bPairable, &BluetoothAdapter::pairableChanged); - Q_OBJECT_BINDABLE_PROPERTY(BluetoothAdapter, quint32, bPairableTimeout, &BluetoothAdapter::pairableTimeoutChanged); - - QS_DBUS_BINDABLE_PROPERTY_GROUP(BluetoothAdapter, properties); - QS_DBUS_PROPERTY_BINDING(BluetoothAdapter, pName, bName, properties, "Alias"); - QS_DBUS_PROPERTY_BINDING(BluetoothAdapter, pEnabled, bEnabled, properties, "Powered"); - QS_DBUS_PROPERTY_BINDING(BluetoothAdapter, pState, bState, properties, "PowerState"); - QS_DBUS_PROPERTY_BINDING(BluetoothAdapter, pDiscoverable, bDiscoverable, properties, "Discoverable"); - QS_DBUS_PROPERTY_BINDING(BluetoothAdapter, pDiscoverableTimeout, bDiscoverableTimeout, properties, "DiscoverableTimeout"); - QS_DBUS_PROPERTY_BINDING(BluetoothAdapter, pDiscovering, bDiscovering, properties, "Discovering"); - QS_DBUS_PROPERTY_BINDING(BluetoothAdapter, pPairable, bPairable, properties, "Pairable"); - QS_DBUS_PROPERTY_BINDING(BluetoothAdapter, pPairableTimeout, bPairableTimeout, properties, "PairableTimeout"); - // clang-format on -}; - -} // namespace qs::bluetooth - -QDebug operator<<(QDebug debug, const qs::bluetooth::BluetoothAdapter* adapter); diff --git a/src/bluetooth/bluez.cpp b/src/bluetooth/bluez.cpp deleted file mode 100644 index f2c4300d..00000000 --- a/src/bluetooth/bluez.cpp +++ /dev/null @@ -1,168 +0,0 @@ -#include "bluez.hpp" - -#include -#include -#include -#include -#include -#include -#include - -#include "../core/logcat.hpp" -#include "../dbus/dbus_objectmanager_types.hpp" -#include "../dbus/objectmanager.hpp" -#include "adapter.hpp" -#include "device.hpp" - -namespace qs::bluetooth { - -namespace { -QS_LOGGING_CATEGORY(logBluetooth, "quickshell.bluetooth", QtWarningMsg); -} - -Bluez* Bluez::instance() { - static auto* instance = new Bluez(); - return instance; -} - -Bluez::Bluez() { this->init(); } - -void Bluez::updateDefaultAdapter() { - const auto& adapters = this->mAdapters.valueList(); - this->bDefaultAdapter = adapters.empty() ? nullptr : adapters.first(); -} - -void Bluez::init() { - qCDebug(logBluetooth) << "Connecting to BlueZ"; - - auto bus = QDBusConnection::systemBus(); - - if (!bus.isConnected()) { - qCWarning(logBluetooth) << "Could not connect to DBus. Bluetooth integration is not available."; - return; - } - - this->objectManager = new qs::dbus::DBusObjectManager(this); - - QObject::connect( - this->objectManager, - &qs::dbus::DBusObjectManager::interfacesAdded, - this, - &Bluez::onInterfacesAdded - ); - - QObject::connect( - this->objectManager, - &qs::dbus::DBusObjectManager::interfacesRemoved, - this, - &Bluez::onInterfacesRemoved - ); - - if (!this->objectManager->setInterface("org.bluez", "/", bus)) { - qCDebug(logBluetooth) << "BlueZ is not running. Bluetooth integration will not work."; - return; - } -} - -void Bluez::onInterfacesAdded( - const QDBusObjectPath& path, - const DBusObjectManagerInterfaces& interfaces -) { - if (auto* adapter = this->mAdapterMap.value(path.path())) { - for (const auto& [interface, properties]: interfaces.asKeyValueRange()) { - adapter->addInterface(interface, properties); - } - } else if (auto* device = this->mDeviceMap.value(path.path())) { - for (const auto& [interface, properties]: interfaces.asKeyValueRange()) { - device->addInterface(interface, properties); - } - } else if (interfaces.contains("org.bluez.Adapter1")) { - auto* adapter = new BluetoothAdapter(path.path(), this); - - if (!adapter->isValid()) { - qCWarning(logBluetooth) << "Adapter path is not valid, cannot track: " << device; - delete adapter; - return; - } - - qCDebug(logBluetooth) << "Tracked new adapter" << adapter; - - for (const auto& [interface, properties]: interfaces.asKeyValueRange()) { - adapter->addInterface(interface, properties); - } - - for (auto* device: this->mDevices.valueList()) { - if (device->adapterPath() == path) { - adapter->devices()->insertObject(device); - qCDebug(logBluetooth) << "Added tracked device" << device << "to new adapter" << adapter; - emit device->adapterChanged(); - } - } - - this->mAdapterMap.insert(path.path(), adapter); - this->mAdapters.insertObject(adapter); - this->updateDefaultAdapter(); - } else if (interfaces.contains("org.bluez.Device1")) { - auto* device = new BluetoothDevice(path.path(), this); - - if (!device->isValid()) { - qCWarning(logBluetooth) << "Device path is not valid, cannot track: " << device; - delete device; - return; - } - - qCDebug(logBluetooth) << "Tracked new device" << device; - - for (const auto& [interface, properties]: interfaces.asKeyValueRange()) { - device->addInterface(interface, properties); - } - - if (auto* adapter = device->adapter()) { - adapter->devices()->insertObject(device); - qCDebug(logBluetooth) << "Added device" << device << "to adapter" << adapter; - } - - this->mDeviceMap.insert(path.path(), device); - this->mDevices.insertObject(device); - } -} - -void Bluez::onInterfacesRemoved(const QDBusObjectPath& path, const QStringList& interfaces) { - if (auto* adapter = this->mAdapterMap.value(path.path())) { - if (interfaces.contains("org.bluez.Adapter1")) { - qCDebug(logBluetooth) << "Adapter removed:" << adapter; - - this->mAdapterMap.remove(path.path()); - this->mAdapters.removeObject(adapter); - this->updateDefaultAdapter(); - delete adapter; - } - } else if (auto* device = this->mDeviceMap.value(path.path())) { - if (interfaces.contains("org.bluez.Device1")) { - qCDebug(logBluetooth) << "Device removed:" << device; - - if (auto* adapter = device->adapter()) { - adapter->devices()->removeObject(device); - } - - this->mDeviceMap.remove(path.path()); - this->mDevices.removeObject(device); - delete device; - } else { - for (const auto& interface: interfaces) { - device->removeInterface(interface); - } - } - } -} - -BluezQml::BluezQml() { - QObject::connect( - Bluez::instance(), - &Bluez::defaultAdapterChanged, - this, - &BluezQml::defaultAdapterChanged - ); -} - -} // namespace qs::bluetooth diff --git a/src/bluetooth/bluez.hpp b/src/bluetooth/bluez.hpp deleted file mode 100644 index 9d7c93ca..00000000 --- a/src/bluetooth/bluez.hpp +++ /dev/null @@ -1,98 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include -#include - -#include "../core/doc.hpp" -#include "../core/model.hpp" -#include "../dbus/dbus_objectmanager_types.hpp" -#include "../dbus/objectmanager.hpp" - -namespace qs::bluetooth { - -class BluetoothAdapter; -class BluetoothDevice; - -class Bluez: public QObject { - Q_OBJECT; - -public: - [[nodiscard]] ObjectModel* adapters() { return &this->mAdapters; } - [[nodiscard]] ObjectModel* devices() { return &this->mDevices; } - - [[nodiscard]] BluetoothAdapter* adapter(const QString& path) { - return this->mAdapterMap.value(path); - } - - static Bluez* instance(); - -signals: - void defaultAdapterChanged(); - -private slots: - void - onInterfacesAdded(const QDBusObjectPath& path, const DBusObjectManagerInterfaces& interfaces); - void onInterfacesRemoved(const QDBusObjectPath& path, const QStringList& interfaces); - void updateDefaultAdapter(); - -private: - explicit Bluez(); - void init(); - - qs::dbus::DBusObjectManager* objectManager = nullptr; - QHash mAdapterMap; - QHash mDeviceMap; - ObjectModel mAdapters {this}; - ObjectModel mDevices {this}; - -public: - Q_OBJECT_BINDABLE_PROPERTY( - Bluez, - BluetoothAdapter*, - bDefaultAdapter, - &Bluez::defaultAdapterChanged - ); -}; - -///! Bluetooth manager -/// Provides access to bluetooth devices and adapters. -class BluezQml: public QObject { - Q_OBJECT; - QML_NAMED_ELEMENT(Bluetooth); - QML_SINGLETON; - // clang-format off - /// The default bluetooth adapter. Usually there is only one. - Q_PROPERTY(BluetoothAdapter* defaultAdapter READ default NOTIFY defaultAdapterChanged BINDABLE bindableDefaultAdapter); - QSDOC_TYPE_OVERRIDE(ObjectModel*); - /// A list of all bluetooth adapters. See @@defaultAdapter for the default. - Q_PROPERTY(UntypedObjectModel* adapters READ adapters CONSTANT); - QSDOC_TYPE_OVERRIDE(ObjectModel*); - /// A list of all connected bluetooth devices across all adapters. - /// See @@BluetoothAdapter.devices for the devices connected to a single adapter. - Q_PROPERTY(UntypedObjectModel* devices READ devices CONSTANT); - // clang-format on - -signals: - void defaultAdapterChanged(); - -public: - explicit BluezQml(); - - [[nodiscard]] static ObjectModel* adapters() { - return Bluez::instance()->adapters(); - } - - [[nodiscard]] static ObjectModel* devices() { - return Bluez::instance()->devices(); - } - - [[nodiscard]] static QBindable bindableDefaultAdapter() { - return &Bluez::instance()->bDefaultAdapter; - } -}; - -} // namespace qs::bluetooth diff --git a/src/bluetooth/device.cpp b/src/bluetooth/device.cpp deleted file mode 100644 index 7265b241..00000000 --- a/src/bluetooth/device.cpp +++ /dev/null @@ -1,319 +0,0 @@ -#include "device.hpp" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "../core/logcat.hpp" -#include "../dbus/properties.hpp" -#include "adapter.hpp" -#include "bluez.hpp" -#include "dbus_device.h" - -namespace qs::bluetooth { - -namespace { -QS_LOGGING_CATEGORY(logDevice, "quickshell.bluetooth.device", QtWarningMsg); -} - -QString BluetoothDeviceState::toString(BluetoothDeviceState::Enum state) { - switch (state) { - case BluetoothDeviceState::Disconnected: return QStringLiteral("Disconnected"); - case BluetoothDeviceState::Connected: return QStringLiteral("Connected"); - case BluetoothDeviceState::Disconnecting: return QStringLiteral("Disconnecting"); - case BluetoothDeviceState::Connecting: return QStringLiteral("Connecting"); - default: return QStringLiteral("Unknown"); - } -} - -BluetoothDevice::BluetoothDevice(const QString& path, QObject* parent): QObject(parent) { - this->mInterface = - new DBusBluezDeviceInterface("org.bluez", path, QDBusConnection::systemBus(), this); - - if (!this->mInterface->isValid()) { - qCWarning(logDevice) << "Could not create DBus interface for device at" << path; - delete this->mInterface; - this->mInterface = nullptr; - return; - } - - this->properties.setInterface(this->mInterface); -} - -BluetoothAdapter* BluetoothDevice::adapter() const { - return Bluez::instance()->adapter(this->bAdapterPath.value().path()); -} - -void BluetoothDevice::setConnected(bool connected) { - if (connected == this->bConnected) return; - - if (connected) { - this->connect(); - } else { - this->disconnect(); - } -} - -void BluetoothDevice::setTrusted(bool trusted) { - if (trusted == this->bTrusted) return; - this->bTrusted = trusted; - this->pTrusted.write(); -} - -void BluetoothDevice::setBlocked(bool blocked) { - if (blocked == this->bBlocked) return; - this->bBlocked = blocked; - this->pBlocked.write(); -} - -void BluetoothDevice::setName(const QString& name) { - if (name == this->bName) return; - this->bName = name; - this->pName.write(); -} - -void BluetoothDevice::setWakeAllowed(bool wakeAllowed) { - if (wakeAllowed == this->bWakeAllowed) return; - this->bWakeAllowed = wakeAllowed; - this->pWakeAllowed.write(); -} - -void BluetoothDevice::connect() { - if (this->bConnected) { - qCCritical(logDevice) << "Device" << this << "is already connected"; - return; - } - - if (this->bState == BluetoothDeviceState::Connecting) { - qCCritical(logDevice) << "Device" << this << "is already connecting"; - return; - } - - qCDebug(logDevice) << "Connecting to device" << this; - this->bState = BluetoothDeviceState::Connecting; - - auto reply = this->mInterface->Connect(); - auto* watcher = new QDBusPendingCallWatcher(reply, this); - - QObject::connect( - watcher, - &QDBusPendingCallWatcher::finished, - this, - [this](QDBusPendingCallWatcher* watcher) { - const QDBusPendingReply<> reply = *watcher; - - if (reply.isError()) { - qCWarning(logDevice).nospace() - << "Failed to connect to device " << this << ": " << reply.error().message(); - - this->bState = this->bConnected ? BluetoothDeviceState::Connected - : BluetoothDeviceState::Disconnected; - } else { - qCDebug(logDevice) << "Successfully connected to to device" << this; - } - - delete watcher; - } - ); -} - -void BluetoothDevice::disconnect() { - if (!this->bConnected) { - qCCritical(logDevice) << "Device" << this << "is already disconnected"; - return; - } - - if (this->bState == BluetoothDeviceState::Disconnecting) { - qCCritical(logDevice) << "Device" << this << "is already disconnecting"; - return; - } - - qCDebug(logDevice) << "Disconnecting from device" << this; - this->bState = BluetoothDeviceState::Disconnecting; - - auto reply = this->mInterface->Disconnect(); - auto* watcher = new QDBusPendingCallWatcher(reply, this); - - QObject::connect( - watcher, - &QDBusPendingCallWatcher::finished, - this, - [this](QDBusPendingCallWatcher* watcher) { - const QDBusPendingReply<> reply = *watcher; - - if (reply.isError()) { - qCWarning(logDevice).nospace() - << "Failed to disconnect from device " << this << ": " << reply.error().message(); - - this->bState = this->bConnected ? BluetoothDeviceState::Connected - : BluetoothDeviceState::Disconnected; - } else { - qCDebug(logDevice) << "Successfully disconnected from from device" << this; - } - - delete watcher; - } - ); -} - -void BluetoothDevice::pair() { - if (this->bPaired) { - qCCritical(logDevice) << "Device" << this << "is already paired"; - return; - } - - if (this->bPairing) { - qCCritical(logDevice) << "Device" << this << "is already pairing"; - return; - } - - qCDebug(logDevice) << "Pairing with device" << this; - this->bPairing = true; - - auto reply = this->mInterface->Pair(); - auto* watcher = new QDBusPendingCallWatcher(reply, this); - - QObject::connect( - watcher, - &QDBusPendingCallWatcher::finished, - this, - [this](QDBusPendingCallWatcher* watcher) { - const QDBusPendingReply<> reply = *watcher; - if (reply.isError()) { - qCWarning(logDevice).nospace() - << "Failed to pair with device " << this << ": " << reply.error().message(); - } else { - qCDebug(logDevice) << "Successfully initiated pairing with device" << this; - } - - this->bPairing = false; - delete watcher; - } - ); -} - -void BluetoothDevice::cancelPair() { - if (!this->bPairing) { - qCCritical(logDevice) << "Device" << this << "is not currently pairing"; - return; - } - - qCDebug(logDevice) << "Cancelling pairing with device" << this; - - auto reply = this->mInterface->CancelPairing(); - auto* watcher = new QDBusPendingCallWatcher(reply, this); - - QObject::connect( - watcher, - &QDBusPendingCallWatcher::finished, - this, - [this](QDBusPendingCallWatcher* watcher) { - const QDBusPendingReply<> reply = *watcher; - if (reply.isError()) { - qCWarning(logDevice) << "Failed to cancel pairing with device" << this << ":" - << reply.error().message(); - } else { - qCDebug(logDevice) << "Successfully cancelled pairing with device" << this; - } - - this->bPairing = false; - delete watcher; - } - ); -} - -void BluetoothDevice::forget() { - if (!this->mInterface || !this->mInterface->isValid()) { - qCCritical(logDevice) << "Cannot forget - device interface is invalid"; - return; - } - - if (auto* adapter = Bluez::instance()->adapter(this->bAdapterPath.value().path())) { - qCDebug(logDevice) << "Forgetting device" << this << "via adapter" << adapter; - adapter->removeDevice(this->path()); - } else { - qCCritical(logDevice) << "Could not find adapter for path" << this->bAdapterPath.value().path() - << "to forget from"; - } -} - -void BluetoothDevice::addInterface(const QString& interface, const QVariantMap& properties) { - if (interface == "org.bluez.Device1") { - this->properties.updatePropertySet(properties, false); - qCDebug(logDevice) << "Updated Device properties for" << this; - } else if (interface == "org.bluez.Battery1") { - if (!this->mBatteryInterface) { - this->mBatteryInterface = new QDBusInterface( - "org.bluez", - this->path(), - "org.bluez.Battery1", - QDBusConnection::systemBus(), - this - ); - - if (!this->mBatteryInterface->isValid()) { - qCWarning(logDevice) << "Could not create Battery interface for device at" << this; - delete this->mBatteryInterface; - this->mBatteryInterface = nullptr; - return; - } - } - - this->batteryProperties.setInterface(this->mBatteryInterface); - this->batteryProperties.updatePropertySet(properties, false); - - emit this->batteryAvailableChanged(); - qCDebug(logDevice) << "Updated Battery properties for" << this; - } -} - -void BluetoothDevice::removeInterface(const QString& interface) { - if (interface == "org.bluez.Battery1" && this->mBatteryInterface) { - this->batteryProperties.setInterface(nullptr); - delete this->mBatteryInterface; - this->mBatteryInterface = nullptr; - this->bBattery = 0; - - emit this->batteryAvailableChanged(); - qCDebug(logDevice) << "Battery interface removed from device" << this; - } -} - -void BluetoothDevice::onConnectedChanged() { - this->bState = - this->bConnected ? BluetoothDeviceState::Connected : BluetoothDeviceState::Disconnected; - emit this->connectedChanged(); -} - -} // namespace qs::bluetooth - -namespace qs::dbus { - -using namespace qs::bluetooth; - -DBusResult DBusDataTransform::fromWire(quint8 percentage) { - return DBusResult(percentage * 0.01); -} - -} // namespace qs::dbus - -QDebug operator<<(QDebug debug, const qs::bluetooth::BluetoothDevice* device) { - auto saver = QDebugStateSaver(debug); - - if (device) { - debug.nospace() << "BluetoothDevice(" << static_cast(device) - << ", path=" << device->path() << ")"; - } else { - debug << "BluetoothDevice(nullptr)"; - } - - return debug; -} diff --git a/src/bluetooth/device.hpp b/src/bluetooth/device.hpp deleted file mode 100644 index 23f230f5..00000000 --- a/src/bluetooth/device.hpp +++ /dev/null @@ -1,225 +0,0 @@ -#pragma once - -#include -#include -#include -#include - -#include "../dbus/properties.hpp" -#include "dbus_device.h" - -namespace qs::bluetooth { - -///! Connection state of a Bluetooth device. -class BluetoothDeviceState: public QObject { - Q_OBJECT; - QML_ELEMENT; - QML_SINGLETON; - -public: - enum Enum : quint8 { - /// The device is not connected. - Disconnected = 0, - /// The device is connected. - Connected = 1, - /// The device is disconnecting. - Disconnecting = 2, - /// The device is connecting. - Connecting = 3, - }; - Q_ENUM(Enum); - - Q_INVOKABLE static QString toString(BluetoothDeviceState::Enum state); -}; - -struct BatteryPercentage {}; - -} // namespace qs::bluetooth - -namespace qs::dbus { - -template <> -struct DBusDataTransform { - using Wire = quint8; - using Data = qreal; - static DBusResult fromWire(Wire percentage); -}; - -} // namespace qs::dbus - -namespace qs::bluetooth { - -class BluetoothAdapter; - -///! A tracked Bluetooth device. -class BluetoothDevice: public QObject { - Q_OBJECT; - QML_ELEMENT; - QML_UNCREATABLE(""); - // clang-format off - /// MAC address of the device. - Q_PROPERTY(QString address READ default NOTIFY addressChanged BINDABLE bindableAddress); - /// The name of the Bluetooth device. This property may be written to create an alias, or set to - /// an empty string to fall back to the device provided name. - /// - /// See @@deviceName for the name provided by the device. - Q_PROPERTY(QString name READ name WRITE setName NOTIFY nameChanged); - /// The name of the Bluetooth device, ignoring user provided aliases. See also @@name - /// which returns a user provided alias if set. - Q_PROPERTY(QString deviceName READ default NOTIFY deviceNameChanged BINDABLE bindableDeviceName); - /// System icon representing the device type. Use @@Quickshell.Quickshell.iconPath() to display this in an image. - Q_PROPERTY(QString icon READ default NOTIFY iconChanged BINDABLE bindableIcon); - /// Connection state of the device. - Q_PROPERTY(BluetoothDeviceState::Enum state READ default NOTIFY stateChanged BINDABLE bindableState); - /// True if the device is currently connected to the computer. - /// - /// Setting this property is equivalent to calling @@connect() and @@disconnect(). - /// - /// > [!NOTE] @@state provides more detailed information if required. - Q_PROPERTY(bool connected READ connected WRITE setConnected NOTIFY connectedChanged); - /// True if the device is paired to the computer. - /// - /// > [!NOTE] @@pair() can be used to pair a device, however you must @@forget() the device to unpair it. - Q_PROPERTY(bool paired READ default NOTIFY pairedChanged BINDABLE bindablePaired); - /// True if pairing information is stored for future connections. - Q_PROPERTY(bool bonded READ default NOTIFY bondedChanged BINDABLE bindableBonded); - /// True if the device is currently being paired. - /// - /// > [!NOTE] @@cancelPair() can be used to cancel the pairing process. - Q_PROPERTY(bool pairing READ pairing NOTIFY pairingChanged); - /// True if the device is considered to be trusted by the system. - /// Trusted devices are allowed to reconnect themselves to the system without intervention. - Q_PROPERTY(bool trusted READ trusted WRITE setTrusted NOTIFY trustedChanged); - /// True if the device is blocked from connecting. - /// If a device is blocked, any connection attempts will be immediately rejected by the system. - Q_PROPERTY(bool blocked READ blocked WRITE setBlocked NOTIFY blockedChanged); - /// True if the device is allowed to wake up the host system from suspend. - Q_PROPERTY(bool wakeAllowed READ wakeAllowed WRITE setWakeAllowed NOTIFY wakeAllowedChanged); - /// True if the connected device reports its battery level. Battery level can be accessed via @@battery. - Q_PROPERTY(bool batteryAvailable READ batteryAvailable NOTIFY batteryAvailableChanged); - /// Battery level of the connected device, from `0.0` to `1.0`. Only valid if @@batteryAvailable is true. - Q_PROPERTY(qreal battery READ default NOTIFY batteryChanged BINDABLE bindableBattery); - /// The Bluetooth adapter this device belongs to. - Q_PROPERTY(BluetoothAdapter* adapter READ adapter NOTIFY adapterChanged); - /// DBus path of the device under the `org.bluez` system service. - Q_PROPERTY(QString dbusPath READ path CONSTANT); - // clang-format on - -public: - explicit BluetoothDevice(const QString& path, QObject* parent = nullptr); - - /// Attempt to connect to the device. - Q_INVOKABLE void connect(); - /// Disconnect from the device. - Q_INVOKABLE void disconnect(); - /// Attempt to pair the device. - /// - /// > [!NOTE] @@paired and @@pairing return the current pairing status of the device. - Q_INVOKABLE void pair(); - /// Cancel an active pairing attempt. - Q_INVOKABLE void cancelPair(); - /// Forget the device. - Q_INVOKABLE void forget(); - - [[nodiscard]] bool isValid() const { return this->mInterface && this->mInterface->isValid(); } - [[nodiscard]] QString path() const { - return this->mInterface ? this->mInterface->path() : QString(); - } - - [[nodiscard]] bool batteryAvailable() const { return this->mBatteryInterface != nullptr; } - [[nodiscard]] BluetoothAdapter* adapter() const; - [[nodiscard]] QDBusObjectPath adapterPath() const { return this->bAdapterPath.value(); } - - [[nodiscard]] bool connected() const { return this->bConnected; } - void setConnected(bool connected); - - [[nodiscard]] bool trusted() const { return this->bTrusted; } - void setTrusted(bool trusted); - - [[nodiscard]] bool blocked() const { return this->bBlocked; } - void setBlocked(bool blocked); - - [[nodiscard]] QString name() const { return this->bName; } - void setName(const QString& name); - - [[nodiscard]] bool wakeAllowed() const { return this->bWakeAllowed; } - void setWakeAllowed(bool wakeAllowed); - - [[nodiscard]] bool pairing() const { return this->bPairing; } - - [[nodiscard]] QBindable bindableAddress() { return &this->bAddress; } - [[nodiscard]] QBindable bindableDeviceName() { return &this->bDeviceName; } - [[nodiscard]] QBindable bindableName() { return &this->bName; } - [[nodiscard]] QBindable bindableConnected() { return &this->bConnected; } - [[nodiscard]] QBindable bindablePaired() { return &this->bPaired; } - [[nodiscard]] QBindable bindableBonded() { return &this->bBonded; } - [[nodiscard]] QBindable bindableTrusted() { return &this->bTrusted; } - [[nodiscard]] QBindable bindableBlocked() { return &this->bBlocked; } - [[nodiscard]] QBindable bindableWakeAllowed() { return &this->bWakeAllowed; } - [[nodiscard]] QBindable bindableIcon() { return &this->bIcon; } - [[nodiscard]] QBindable bindableBattery() { return &this->bBattery; } - [[nodiscard]] QBindable bindableState() { return &this->bState; } - - void addInterface(const QString& interface, const QVariantMap& properties); - void removeInterface(const QString& interface); - -signals: - void addressChanged(); - void deviceNameChanged(); - void nameChanged(); - void connectedChanged(); - void stateChanged(); - void pairedChanged(); - void bondedChanged(); - void pairingChanged(); - void trustedChanged(); - void blockedChanged(); - void wakeAllowedChanged(); - void iconChanged(); - void batteryAvailableChanged(); - void batteryChanged(); - void adapterChanged(); - -private: - void onConnectedChanged(); - - DBusBluezDeviceInterface* mInterface = nullptr; - QDBusInterface* mBatteryInterface = nullptr; - - // clang-format off - Q_OBJECT_BINDABLE_PROPERTY(BluetoothDevice, QString, bAddress, &BluetoothDevice::addressChanged); - Q_OBJECT_BINDABLE_PROPERTY(BluetoothDevice, QString, bDeviceName, &BluetoothDevice::deviceNameChanged); - Q_OBJECT_BINDABLE_PROPERTY(BluetoothDevice, QString, bName, &BluetoothDevice::nameChanged); - Q_OBJECT_BINDABLE_PROPERTY(BluetoothDevice, bool, bConnected, &BluetoothDevice::onConnectedChanged); - Q_OBJECT_BINDABLE_PROPERTY(BluetoothDevice, bool, bPaired, &BluetoothDevice::pairedChanged); - Q_OBJECT_BINDABLE_PROPERTY(BluetoothDevice, bool, bBonded, &BluetoothDevice::bondedChanged); - Q_OBJECT_BINDABLE_PROPERTY(BluetoothDevice, bool, bTrusted, &BluetoothDevice::trustedChanged); - Q_OBJECT_BINDABLE_PROPERTY(BluetoothDevice, bool, bBlocked, &BluetoothDevice::blockedChanged); - Q_OBJECT_BINDABLE_PROPERTY(BluetoothDevice, bool, bWakeAllowed, &BluetoothDevice::wakeAllowedChanged); - Q_OBJECT_BINDABLE_PROPERTY(BluetoothDevice, QString, bIcon, &BluetoothDevice::iconChanged); - Q_OBJECT_BINDABLE_PROPERTY(BluetoothDevice, QDBusObjectPath, bAdapterPath); - Q_OBJECT_BINDABLE_PROPERTY(BluetoothDevice, qreal, bBattery, &BluetoothDevice::batteryChanged); - Q_OBJECT_BINDABLE_PROPERTY(BluetoothDevice, BluetoothDeviceState::Enum, bState, &BluetoothDevice::stateChanged); - Q_OBJECT_BINDABLE_PROPERTY(BluetoothDevice, bool, bPairing, &BluetoothDevice::pairingChanged); - - QS_DBUS_BINDABLE_PROPERTY_GROUP(BluetoothDevice, properties); - QS_DBUS_PROPERTY_BINDING(BluetoothDevice, pAddress, bAddress, properties, "Address"); - QS_DBUS_PROPERTY_BINDING(BluetoothDevice, pDeviceName, bDeviceName, properties, "Name"); - QS_DBUS_PROPERTY_BINDING(BluetoothDevice, pName, bName, properties, "Alias"); - QS_DBUS_PROPERTY_BINDING(BluetoothDevice, pConnected, bConnected, properties, "Connected"); - QS_DBUS_PROPERTY_BINDING(BluetoothDevice, pPaired, bPaired, properties, "Paired"); - QS_DBUS_PROPERTY_BINDING(BluetoothDevice, pBonded, bBonded, properties, "Bonded"); - QS_DBUS_PROPERTY_BINDING(BluetoothDevice, pTrusted, bTrusted, properties, "Trusted"); - QS_DBUS_PROPERTY_BINDING(BluetoothDevice, pBlocked, bBlocked, properties, "Blocked"); - QS_DBUS_PROPERTY_BINDING(BluetoothDevice, pWakeAllowed, bWakeAllowed, properties, "WakeAllowed"); - QS_DBUS_PROPERTY_BINDING(BluetoothDevice, pIcon, bIcon, properties, "Icon"); - QS_DBUS_PROPERTY_BINDING(BluetoothDevice, pAdapterPath, bAdapterPath, properties, "Adapter"); - - QS_DBUS_BINDABLE_PROPERTY_GROUP(BluetoothDevice, batteryProperties); - QS_DBUS_PROPERTY_BINDING(BluetoothDevice, BatteryPercentage, pBattery, bBattery, batteryProperties, "Percentage", true); - // clang-format on -}; - -} // namespace qs::bluetooth - -QDebug operator<<(QDebug debug, const qs::bluetooth::BluetoothDevice* device); diff --git a/src/bluetooth/module.md b/src/bluetooth/module.md deleted file mode 100644 index eb797d93..00000000 --- a/src/bluetooth/module.md +++ /dev/null @@ -1,12 +0,0 @@ -name = "Quickshell.Bluetooth" -description = "Bluetooth API" -headers = [ - "bluez.hpp", - "adapter.hpp", - "device.hpp", -] ------ -This module exposes Bluetooth management APIs provided by the BlueZ DBus interface. -Both DBus and BlueZ must be running to use it. - -See the @@Quickshell.Bluetooth.Bluetooth singleton. diff --git a/src/bluetooth/org.bluez.Adapter.xml b/src/bluetooth/org.bluez.Adapter.xml deleted file mode 100644 index 286991e1..00000000 --- a/src/bluetooth/org.bluez.Adapter.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/src/bluetooth/org.bluez.Device.xml b/src/bluetooth/org.bluez.Device.xml deleted file mode 100644 index 274e9fde..00000000 --- a/src/bluetooth/org.bluez.Device.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - diff --git a/src/bluetooth/test/manual/test.qml b/src/bluetooth/test/manual/test.qml deleted file mode 100644 index 21c53b1d..00000000 --- a/src/bluetooth/test/manual/test.qml +++ /dev/null @@ -1,200 +0,0 @@ -import QtQuick -import QtQuick.Controls -import QtQuick.Layouts -import Quickshell -import Quickshell.Widgets -import Quickshell.Bluetooth - -FloatingWindow { - color: contentItem.palette.window - - ListView { - anchors.fill: parent - anchors.margins: 5 - model: Bluetooth.adapters - - delegate: WrapperRectangle { - width: parent.width - color: "transparent" - border.color: palette.button - border.width: 1 - margin: 5 - - ColumnLayout { - Label { text: `Adapter: ${modelData.name} (${modelData.adapterId})` } - - RowLayout { - Layout.fillWidth: true - - CheckBox { - text: "Enable" - checked: modelData.enabled - onToggled: modelData.enabled = checked - } - - Label { - color: modelData.state === BluetoothAdapterState.Blocked ? palette.errorText : palette.placeholderText - text: BluetoothAdapterState.toString(modelData.state) - } - - CheckBox { - text: "Discoverable" - checked: modelData.discoverable - onToggled: modelData.discoverable = checked - } - - CheckBox { - text: "Discovering" - checked: modelData.discovering - onToggled: modelData.discovering = checked - } - - CheckBox { - text: "Pairable" - checked: modelData.pairable - onToggled: modelData.pairable = checked - } - } - - RowLayout { - Layout.fillWidth: true - - Label { text: "Discoverable timeout:" } - - SpinBox { - from: 0 - to: 3600 - value: modelData.discoverableTimeout - onValueModified: modelData.discoverableTimeout = value - textFromValue: time => time === 0 ? "∞" : time + "s" - } - - Label { text: "Pairable timeout:" } - - SpinBox { - from: 0 - to: 3600 - value: modelData.pairableTimeout - onValueModified: modelData.pairableTimeout = value - textFromValue: time => time === 0 ? "∞" : time + "s" - } - } - - Repeater { - model: modelData.devices - - WrapperRectangle { - Layout.fillWidth: true - color: palette.button - border.color: palette.mid - border.width: 1 - margin: 5 - - RowLayout { - ColumnLayout { - Layout.fillWidth: true - - RowLayout { - IconImage { - Layout.fillHeight: true - implicitWidth: height - source: Quickshell.iconPath(modelData.icon) - } - - TextField { - text: modelData.name - font.bold: true - background: null - readOnly: false - selectByMouse: true - onEditingFinished: modelData.name = text - } - - Label { - visible: modelData.name && modelData.name !== modelData.deviceName - text: `(${modelData.deviceName})` - color: palette.placeholderText - } - } - - RowLayout { - Label { - text: modelData.address - color: palette.placeholderText - } - - Label { - visible: modelData.batteryAvailable - text: `| Battery: ${Math.round(modelData.battery * 100)}%` - color: palette.placeholderText - } - } - - RowLayout { - Label { - text: BluetoothDeviceState.toString(modelData.state) - - color: modelData.connected ? palette.link : palette.placeholderText - } - - Label { - text: modelData.pairing ? "Pairing" : (modelData.paired ? "Paired" : "Not Paired") - color: modelData.paired || modelData.pairing ? palette.link : palette.placeholderText - } - - Label { - visible: modelData.bonded - text: "| Bonded" - color: palette.link - } - - CheckBox { - text: "Trusted" - checked: modelData.trusted - onToggled: modelData.trusted = checked - } - - CheckBox { - text: "Blocked" - checked: modelData.blocked - onToggled: modelData.blocked = checked - } - - CheckBox { - text: "Wake Allowed" - checked: modelData.wakeAllowed - onToggled: modelData.wakeAllowed = checked - } - } - } - - ColumnLayout { - Layout.alignment: Qt.AlignRight - - Button { - Layout.alignment: Qt.AlignRight - text: modelData.connected ? "Disconnect" : "Connect" - onClicked: modelData.connected = !modelData.connected - } - - Button { - Layout.alignment: Qt.AlignRight - text: modelData.pairing ? "Cancel" : (modelData.paired ? "Forget" : "Pair") - onClicked: { - if (modelData.pairing) { - modelData.cancelPair(); - } else if (modelData.paired) { - modelData.forget(); - } else { - modelData.pair(); - } - } - } - } - } - } - } - } - } - } -} 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 7cef987a..c1fdee8e 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -1,62 +1,23 @@ -qt_add_library(quickshell-core STATIC +qt_add_executable(quickshell + main.cpp plugin.cpp shell.cpp variants.cpp rootwrapper.cpp + proxywindow.cpp reload.cpp rootwrapper.cpp qmlglobal.cpp qmlscreen.cpp + watcher.cpp region.cpp persistentprops.cpp - singleton.cpp - generation.cpp - scan.cpp - qsintercept.cpp - incubator.cpp - lazyloader.cpp - easingcurve.cpp - iconimageprovider.cpp - 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 - colorquantizer.cpp - toolsupport.cpp + windowinterface.cpp + floatingwindow.cpp + panelinterface.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 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 PRIVATE quickshell-coreplugin) - -if (BUILD_TESTING) - add_subdirectory(test) -endif() +target_link_libraries(quickshell PRIVATE ${QT_DEPS}) diff --git a/src/core/boundcomponent.cpp b/src/core/boundcomponent.cpp deleted file mode 100644 index 8b1c8284..00000000 --- a/src/core/boundcomponent.cpp +++ /dev/null @@ -1,258 +0,0 @@ -#include "boundcomponent.hpp" -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "incubator.hpp" - -QObject* BoundComponent::item() const { return this->object; } -QQmlComponent* BoundComponent::sourceComponent() const { return this->mComponent; } - -void BoundComponent::setSourceComponent(QQmlComponent* component) { - if (component == this->mComponent) return; - - if (this->componentCompleted) { - qWarning() << "BoundComponent.component cannot be set after creation"; - return; - } - this->disconnectComponent(); - - this->ownsComponent = false; - this->mComponent = component; - if (component != nullptr) { - QObject::connect(component, &QObject::destroyed, this, &BoundComponent::onComponentDestroyed); - } - - emit this->sourceComponentChanged(); -} - -void BoundComponent::disconnectComponent() { - if (this->mComponent == nullptr) return; - - if (this->ownsComponent) { - delete this->mComponent; - } else { - QObject::disconnect(this->mComponent, nullptr, this, nullptr); - } - - this->mComponent = nullptr; -} - -void BoundComponent::onComponentDestroyed() { this->mComponent = nullptr; } -QString BoundComponent::source() const { return this->mSource; } - -void BoundComponent::setSource(QString source) { - if (source == this->mSource) return; - - if (this->componentCompleted) { - qWarning() << "BoundComponent.url cannot be set after creation"; - return; - } - - auto* context = QQmlEngine::contextForObject(this); - auto* component = new QQmlComponent(context->engine(), context->resolvedUrl(source), this); - - if (component->isError()) { - qWarning() << component->errorString().toStdString().c_str(); - delete component; - } else { - this->disconnectComponent(); - this->ownsComponent = true; - this->mSource = std::move(source); - this->mComponent = component; - - emit this->sourceChanged(); - emit this->sourceComponentChanged(); - } -} - -bool BoundComponent::bindValues() const { return this->mBindValues; } - -void BoundComponent::setBindValues(bool bindValues) { - if (this->componentCompleted) { - qWarning() << "BoundComponent.bindValues cannot be set after creation"; - return; - } - - this->mBindValues = bindValues; - emit this->bindValuesChanged(); -} - -void BoundComponent::componentComplete() { - this->QQuickItem::componentComplete(); - this->componentCompleted = true; - this->tryCreate(); -} - -void BoundComponent::tryCreate() { - if (this->mComponent == nullptr) { - qWarning() << "BoundComponent has no component"; - return; - } - - auto initialProperties = QVariantMap(); - - const auto* metaObject = this->metaObject(); - for (auto i = metaObject->propertyOffset(); i < metaObject->propertyCount(); i++) { - const auto prop = metaObject->property(i); - - if (prop.isReadable()) { - initialProperties.insert(prop.name(), prop.read(this)); - } - } - - this->incubator = new QsQmlIncubator(QsQmlIncubator::AsynchronousIfNested, this); - this->incubator->setInitialProperties(initialProperties); - - // clang-format off - QObject::connect(this->incubator, &QsQmlIncubator::completed, this, &BoundComponent::onIncubationCompleted); - QObject::connect(this->incubator, &QsQmlIncubator::failed, this, &BoundComponent::onIncubationFailed); - // clang-format on - - this->mComponent->create(*this->incubator, QQmlEngine::contextForObject(this)); -} - -void BoundComponent::onIncubationCompleted() { - this->object = this->incubator->object(); - delete this->incubator; - this->disconnectComponent(); - - this->object->setParent(this); - this->mItem = qobject_cast(this->object); - - const auto* metaObject = this->metaObject(); - const auto* objectMetaObject = this->object->metaObject(); - - if (this->mBindValues) { - for (auto i = metaObject->propertyOffset(); i < metaObject->propertyCount(); i++) { - const auto prop = metaObject->property(i); - - if (prop.isReadable() && prop.hasNotifySignal()) { - const auto objectPropIndex = objectMetaObject->indexOfProperty(prop.name()); - - if (objectPropIndex == -1) { - qWarning() << "property" << prop.name() - << "defined on BoundComponent but not on its contained object."; - continue; - } - - const auto objectProp = objectMetaObject->property(objectPropIndex); - if (objectProp.isWritable()) { - auto* proxy = new BoundComponentPropertyProxy(this, this->object, prop, objectProp); - proxy->onNotified(); // any changes that might've happened before connection - } else { - qWarning() << "property" << prop.name() - << "defined on BoundComponent is not writable for its contained object."; - } - } - } - } - - for (auto i = metaObject->methodOffset(); i < metaObject->methodCount(); i++) { - const auto method = metaObject->method(i); - - if (method.name().startsWith("on") && method.name().length() > 2) { - auto sig = QString(method.methodSignature()).sliced(2); - if (!sig[0].isUpper()) continue; - sig[0] = sig[0].toLower(); - auto name = sig.sliced(0, sig.indexOf('(')); - - auto mostViableSignal = QMetaMethod(); - for (auto i = 0; i < objectMetaObject->methodCount(); i++) { - const auto method = objectMetaObject->method(i); - if (method.methodSignature() == sig) { - mostViableSignal = method; - break; - } - - if (method.name() == name) { - if (mostViableSignal.isValid()) { - qWarning() << "Multiple candidates, so none will be attached for signal" << name; - goto next; - } - - mostViableSignal = method; - } - } - - if (!mostViableSignal.isValid()) { - qWarning() << "Function" << method.name() << "appears to be a signal handler for" << name - << "but it does not match any signals on the target object"; - goto next; - } - - QMetaObject::connect( - this->object, - mostViableSignal.methodIndex(), - this, - method.methodIndex() - ); - } - - next:; - } - - if (this->mItem != nullptr) { - this->mItem->setParentItem(this); - - // clang-format off - QObject::connect(this, &QQuickItem::widthChanged, this, &BoundComponent::updateSize); - QObject::connect(this, &QQuickItem::heightChanged, this, &BoundComponent::updateSize); - QObject::connect(this->mItem, &QQuickItem::implicitWidthChanged, this, &BoundComponent::updateImplicitSize); - QObject::connect(this->mItem, &QQuickItem::implicitHeightChanged, this, &BoundComponent::updateImplicitSize); - // clang-format on - - this->updateImplicitSize(); - this->updateSize(); - } - - emit this->loaded(); -} - -void BoundComponent::onIncubationFailed() { - qWarning() << "Failed to create BoundComponent"; - - for (auto& error: this->incubator->errors()) { - qWarning() << error; - } - - delete this->incubator; - this->disconnectComponent(); -} - -void BoundComponent::updateSize() { this->mItem->setSize(this->size()); } - -void BoundComponent::updateImplicitSize() { - this->setImplicitWidth(this->mItem->implicitWidth()); - this->setImplicitHeight(this->mItem->implicitHeight()); -} - -BoundComponentPropertyProxy::BoundComponentPropertyProxy( - QObject* from, - QObject* to, - QMetaProperty fromProperty, - QMetaProperty toProperty -) - : QObject(from) - , from(from) - , to(to) - , fromProperty(fromProperty) - , toProperty(toProperty) { - const auto* metaObject = this->metaObject(); - auto method = metaObject->indexOfSlot("onNotified()"); - QMetaObject::connect(from, fromProperty.notifySignal().methodIndex(), this, method); -} - -void BoundComponentPropertyProxy::onNotified() { - this->toProperty.write(this->to, this->fromProperty.read(this->from)); -} diff --git a/src/core/boundcomponent.hpp b/src/core/boundcomponent.hpp deleted file mode 100644 index d47121df..00000000 --- a/src/core/boundcomponent.hpp +++ /dev/null @@ -1,125 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include -#include -#include - -#include "incubator.hpp" - -///! Component loader that allows setting initial properties. -/// Component loader that allows setting initial properties, primarily useful for -/// escaping cyclic dependency errors. -/// -/// Properties defined on the BoundComponent will be applied to its loaded component, -/// including required properties, and will remain reactive. Functions created with -/// the names of signal handlers will also be attached to signals of the loaded component. -/// -/// ```qml {filename="MyComponent.qml"} -/// MouseArea { -/// required property color color; -/// width: 100 -/// height: 100 -/// -/// Rectangle { -/// anchors.fill: parent -/// color: parent.color -/// } -/// } -/// ``` -/// -/// ```qml -/// BoundComponent { -/// source: "MyComponent.qml" -/// -/// // this is the same as assigning to `color` on MyComponent if loaded normally. -/// property color color: "red"; -/// -/// // this will be triggered when the `clicked` signal from the MouseArea is sent. -/// function onClicked() { -/// color = "blue"; -/// } -/// } -/// ``` -class BoundComponent: public QQuickItem { - Q_OBJECT; - // clang-format off - /// The loaded component. Will be null until it has finished loading. - Q_PROPERTY(QObject* item READ item NOTIFY loaded); - /// The source to load, as a Component. - Q_PROPERTY(QQmlComponent* sourceComponent READ sourceComponent WRITE setSourceComponent NOTIFY sourceComponentChanged); - /// The source to load, as a Url. - Q_PROPERTY(QString source READ source WRITE setSource NOTIFY sourceChanged); - /// If property values should be bound after they are initially set. Defaults to `true`. - Q_PROPERTY(bool bindValues READ bindValues WRITE setBindValues NOTIFY bindValuesChanged); - Q_PROPERTY(qreal implicitWidth READ implicitWidth NOTIFY implicitWidthChanged); - Q_PROPERTY(qreal implicitHeight READ implicitHeight NOTIFY implicitHeightChanged); - // clang-format on - QML_ELEMENT; - -public: - explicit BoundComponent(QQuickItem* parent = nullptr): QQuickItem(parent) {} - - void componentComplete() override; - - [[nodiscard]] QObject* item() const; - - [[nodiscard]] QQmlComponent* sourceComponent() const; - void setSourceComponent(QQmlComponent* sourceComponent); - - [[nodiscard]] QString source() const; - void setSource(QString source); - - [[nodiscard]] bool bindValues() const; - void setBindValues(bool bindValues); - -signals: - void loaded(); - void sourceComponentChanged(); - void sourceChanged(); - void bindValuesChanged(); - -private slots: - void onComponentDestroyed(); - void onIncubationCompleted(); - void onIncubationFailed(); - void updateSize(); - void updateImplicitSize(); - -private: - void disconnectComponent(); - void tryCreate(); - - QString mSource; - bool mBindValues = true; - QQmlComponent* mComponent = nullptr; - bool ownsComponent = false; - QsQmlIncubator* incubator = nullptr; - QObject* object = nullptr; - QQuickItem* mItem = nullptr; - bool componentCompleted = false; -}; - -class BoundComponentPropertyProxy: public QObject { - Q_OBJECT; - -public: - BoundComponentPropertyProxy( - QObject* from, - QObject* to, - QMetaProperty fromProperty, - QMetaProperty toProperty - ); - -public slots: - void onNotified(); - -private: - QObject* from; - QObject* to; - QMetaProperty fromProperty; - QMetaProperty toProperty; -}; 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/colorquantizer.cpp b/src/core/colorquantizer.cpp deleted file mode 100644 index 6cfb05db..00000000 --- a/src/core/colorquantizer.cpp +++ /dev/null @@ -1,242 +0,0 @@ -#include "colorquantizer.hpp" -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "logcat.hpp" - -namespace { -QS_LOGGING_CATEGORY(logColorQuantizer, "quickshell.colorquantizer", QtWarningMsg); -} - -ColorQuantizerOperation::ColorQuantizerOperation(QUrl* source, qreal depth, qreal rescaleSize) - : source(source) - , maxDepth(depth) - , rescaleSize(rescaleSize) { - setAutoDelete(false); -} - -void ColorQuantizerOperation::quantizeImage(const QAtomicInteger& shouldCancel) { - if (shouldCancel.loadAcquire() || source->isEmpty()) return; - - colors.clear(); - - auto image = QImage(source->toLocalFile()); - if ((image.width() > rescaleSize || image.height() > rescaleSize) && rescaleSize > 0) { - image = image.scaled( - static_cast(rescaleSize), - static_cast(rescaleSize), - Qt::KeepAspectRatio, - Qt::SmoothTransformation - ); - } - - if (image.isNull()) { - qCWarning(logColorQuantizer) << "Failed to load image from" << source->toString(); - return; - } - - QList pixels; - for (int y = 0; y != image.height(); ++y) { - for (int x = 0; x != image.width(); ++x) { - auto pixel = image.pixel(x, y); - if (qAlpha(pixel) == 0) continue; - - pixels.append(QColor::fromRgb(pixel)); - } - } - - auto startTime = QDateTime::currentDateTime(); - - colors = quantization(pixels, 0); - - auto endTime = QDateTime::currentDateTime(); - auto milliseconds = startTime.msecsTo(endTime); - qCDebug(logColorQuantizer) << "Color Quantization took: " << milliseconds << "ms"; -} - -QList ColorQuantizerOperation::quantization( - QList& rgbValues, - qreal depth, - const QAtomicInteger& shouldCancel -) { - if (shouldCancel.loadAcquire()) return QList(); - - if (depth >= maxDepth || rgbValues.isEmpty()) { - if (rgbValues.isEmpty()) return QList(); - - auto totalR = 0; - auto totalG = 0; - auto totalB = 0; - - for (const auto& color: rgbValues) { - if (shouldCancel.loadAcquire()) return QList(); - - totalR += color.red(); - totalG += color.green(); - totalB += color.blue(); - } - - auto avgColor = QColor( - qRound(totalR / static_cast(rgbValues.size())), - qRound(totalG / static_cast(rgbValues.size())), - qRound(totalB / static_cast(rgbValues.size())) - ); - - return QList() << avgColor; - } - - auto dominantChannel = findBiggestColorRange(rgbValues); - std::ranges::sort(rgbValues, [dominantChannel](const auto& a, const auto& b) { - if (dominantChannel == 'r') return a.red() < b.red(); - else if (dominantChannel == 'g') return a.green() < b.green(); - return a.blue() < b.blue(); - }); - - auto mid = rgbValues.size() / 2; - - auto leftHalf = rgbValues.mid(0, mid); - auto rightHalf = rgbValues.mid(mid); - - QList result; - result.append(quantization(leftHalf, depth + 1)); - result.append(quantization(rightHalf, depth + 1)); - - return result; -} - -char ColorQuantizerOperation::findBiggestColorRange(const QList& rgbValues) { - if (rgbValues.isEmpty()) return 'r'; - - auto rMin = 255; - auto gMin = 255; - auto bMin = 255; - auto rMax = 0; - auto gMax = 0; - auto bMax = 0; - - for (const auto& color: rgbValues) { - rMin = qMin(rMin, color.red()); - gMin = qMin(gMin, color.green()); - bMin = qMin(bMin, color.blue()); - - rMax = qMax(rMax, color.red()); - gMax = qMax(gMax, color.green()); - bMax = qMax(bMax, color.blue()); - } - - auto rRange = rMax - rMin; - auto gRange = gMax - gMin; - auto bRange = bMax - bMin; - - auto biggestRange = qMax(rRange, qMax(gRange, bRange)); - if (biggestRange == rRange) { - return 'r'; - } else if (biggestRange == gRange) { - return 'g'; - } else { - return 'b'; - } -} - -void ColorQuantizerOperation::finishRun() { - QMetaObject::invokeMethod(this, &ColorQuantizerOperation::finished, Qt::QueuedConnection); -} - -void ColorQuantizerOperation::finished() { - emit this->done(colors); - delete this; -} - -void ColorQuantizerOperation::run() { - if (!this->shouldCancel) { - this->quantizeImage(); - - if (this->shouldCancel.loadAcquire()) { - qCDebug(logColorQuantizer) << "Color quantization" << this << "cancelled"; - } - } - - this->finishRun(); -} - -void ColorQuantizerOperation::tryCancel() { this->shouldCancel.storeRelease(true); } - -void ColorQuantizer::componentComplete() { - componentCompleted = true; - if (!mSource.isEmpty()) quantizeAsync(); -} - -void ColorQuantizer::setSource(const QUrl& source) { - if (mSource != source) { - mSource = source; - emit this->sourceChanged(); - - if (this->componentCompleted && !mSource.isEmpty()) quantizeAsync(); - } -} - -void ColorQuantizer::setDepth(qreal depth) { - if (mDepth != depth) { - mDepth = depth; - emit this->depthChanged(); - - if (this->componentCompleted) quantizeAsync(); - } -} - -void ColorQuantizer::setRescaleSize(int rescaleSize) { - if (mRescaleSize != rescaleSize) { - mRescaleSize = rescaleSize; - emit this->rescaleSizeChanged(); - - if (this->componentCompleted) quantizeAsync(); - } -} - -void ColorQuantizer::operationFinished(const QList& result) { - bColors = result; - this->liveOperation = nullptr; - emit this->colorsChanged(); -} - -void ColorQuantizer::quantizeAsync() { - if (this->liveOperation) this->cancelAsync(); - - qCDebug(logColorQuantizer) << "Starting color quantization asynchronously"; - this->liveOperation = new ColorQuantizerOperation(&mSource, mDepth, mRescaleSize); - - QObject::connect( - this->liveOperation, - &ColorQuantizerOperation::done, - this, - &ColorQuantizer::operationFinished - ); - - QThreadPool::globalInstance()->start(this->liveOperation); -} - -void ColorQuantizer::cancelAsync() { - if (!this->liveOperation) return; - - this->liveOperation->tryCancel(); - QThreadPool::globalInstance()->waitForDone(); - - QObject::disconnect(this->liveOperation, nullptr, this, nullptr); - this->liveOperation = nullptr; -} diff --git a/src/core/colorquantizer.hpp b/src/core/colorquantizer.hpp deleted file mode 100644 index d35a15ac..00000000 --- a/src/core/colorquantizer.hpp +++ /dev/null @@ -1,128 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -class ColorQuantizerOperation - : public QObject - , public QRunnable { - Q_OBJECT; - -public: - explicit ColorQuantizerOperation(QUrl* source, qreal depth, qreal rescaleSize); - - void run() override; - void tryCancel(); - -signals: - void done(QList colors); - -private slots: - void finished(); - -private: - static char findBiggestColorRange(const QList& rgbValues); - - void quantizeImage(const QAtomicInteger& shouldCancel = false); - - QList quantization( - QList& rgbValues, - qreal depth, - const QAtomicInteger& shouldCancel = false - ); - - void finishRun(); - - QAtomicInteger shouldCancel = false; - QList colors; - QUrl* source; - qreal maxDepth; - qreal rescaleSize; -}; - -///! Color Quantization Utility -/// A color quantization utility used for getting prevalent colors in an image, by -/// averaging out the image's color data recursively. -/// -/// #### Example -/// ```qml -/// ColorQuantizer { -/// id: colorQuantizer -/// source: Qt.resolvedUrl("./yourImage.png") -/// depth: 3 // Will produce 8 colors (2³) -/// rescaleSize: 64 // Rescale to 64x64 for faster processing -/// } -/// ``` -class ColorQuantizer - : public QObject - , public QQmlParserStatus { - Q_OBJECT; - QML_ELEMENT; - Q_INTERFACES(QQmlParserStatus); - /// Access the colors resulting from the color quantization performed. - /// > [!NOTE] The amount of colors returned from the quantization is determined by - /// > the property depth, specifically 2ⁿ where n is the depth. - Q_PROPERTY(QList colors READ default NOTIFY colorsChanged BINDABLE bindableColors); - - /// Path to the image you'd like to run the color quantization on. - Q_PROPERTY(QUrl source READ source WRITE setSource NOTIFY sourceChanged); - - /// Max depth for the color quantization. Each level of depth represents another - /// binary split of the color space - Q_PROPERTY(qreal depth READ depth WRITE setDepth NOTIFY depthChanged); - - /// The size to rescale the image to, when rescaleSize is 0 then no scaling will be done. - /// > [!NOTE] Results from color quantization doesn't suffer much when rescaling, it's - /// > reccommended to rescale, otherwise the quantization process will take much longer. - Q_PROPERTY(qreal rescaleSize READ rescaleSize WRITE setRescaleSize NOTIFY rescaleSizeChanged); - -public: - explicit ColorQuantizer(QObject* parent = nullptr): QObject(parent) {} - - void componentComplete() override; - void classBegin() override {} - - [[nodiscard]] QBindable> bindableColors() { return &this->bColors; } - - [[nodiscard]] QUrl source() const { return mSource; } - void setSource(const QUrl& source); - - [[nodiscard]] qreal depth() const { return mDepth; } - void setDepth(qreal depth); - - [[nodiscard]] qreal rescaleSize() const { return mRescaleSize; } - void setRescaleSize(int rescaleSize); - -signals: - void colorsChanged(); - void sourceChanged(); - void depthChanged(); - void rescaleSizeChanged(); - -public slots: - void operationFinished(const QList& result); - -private: - void quantizeAsync(); - void cancelAsync(); - - bool componentCompleted = false; - ColorQuantizerOperation* liveOperation = nullptr; - QUrl mSource; - qreal mDepth = 0; - qreal mRescaleSize = 0; - - Q_OBJECT_BINDABLE_PROPERTY( - ColorQuantizer, - QList, - bColors, - &ColorQuantizer::colorsChanged - ); -}; diff --git a/src/core/common.cpp b/src/core/common.cpp deleted file mode 100644 index 080019ab..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(); - -} // namespace qs diff --git a/src/core/common.hpp b/src/core/common.hpp deleted file mode 100644 index ab8edb80..00000000 --- a/src/core/common.hpp +++ /dev/null @@ -1,13 +0,0 @@ -#pragma once - -#include -#include - -namespace qs { - -struct Common { - static const QDateTime LAUNCH_TIME; - static inline QProcessEnvironment INITIAL_ENVIRONMENT = {}; // NOLINT -}; - -} // namespace qs diff --git a/src/core/desktopentry.cpp b/src/core/desktopentry.cpp deleted file mode 100644 index 95fcb89e..00000000 --- a/src/core/desktopentry.cpp +++ /dev/null @@ -1,422 +0,0 @@ -#include "desktopentry.hpp" -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "../io/processcore.hpp" -#include "logcat.hpp" -#include "model.hpp" -#include "qmlglobal.hpp" - -namespace { -QS_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 == "StartupWMClass") this->mStartupClass = 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; - this->mCommand = DesktopEntry::parseExecString(value); - } else if (key == "Path") this->mWorkingDirectory = value; - else if (key == "Terminal") this->mTerminal = value == "true"; - else if (key == "Categories") this->mCategories = value.split(u';', Qt::SkipEmptyParts); - else if (key == "Keywords") this->mKeywords = value.split(u';', Qt::SkipEmptyParts); - } - } 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; - action->mCommand = DesktopEntry::parseExecString(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->mCommand, 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 QList& execString, const QString& workingDirectory) { - qs::io::process::ProcessContext ctx; - ctx.setCommand(execString); - ctx.setWorkingDirectory(workingDirectory); - QuickshellGlobal::execDetached(ctx); -} - -void DesktopAction::execute() const { - DesktopEntry::doExec(this->mCommand, this->entry->mWorkingDirectory); -} - -DesktopEntryManager::DesktopEntryManager() { - this->scanDesktopEntries(); - this->populateApplications(); -} - -void DesktopEntryManager::scanDesktopEntries() { - QList dataPaths; - - if (qEnvironmentVariableIsSet("XDG_DATA_HOME")) { - dataPaths.push_back(qEnvironmentVariable("XDG_DATA_HOME")); - } else if (qEnvironmentVariableIsSet("HOME")) { - dataPaths.push_back(qEnvironmentVariable("HOME") + "/.local/share"); - } - - 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; - } -} - -DesktopEntry* DesktopEntryManager::heuristicLookup(const QString& name) { - if (auto* entry = this->byId(name)) return entry; - - auto list = this->desktopEntries.values(); - - auto iter = std::ranges::find_if(list, [&](const DesktopEntry* entry) { - return name == entry->mStartupClass; - }); - - if (iter != list.end()) return *iter; - - iter = std::ranges::find_if(list, [&](const DesktopEntry* entry) { - return name.toLower() == entry->mStartupClass.toLower(); - }); - - if (iter != list.end()) return *iter; - return nullptr; -} - -ObjectModel* DesktopEntryManager::applications() { return &this->mApplications; } - -DesktopEntries::DesktopEntries() { DesktopEntryManager::instance(); } - -DesktopEntry* DesktopEntries::byId(const QString& id) { - return DesktopEntryManager::instance()->byId(id); -} - -DesktopEntry* DesktopEntries::heuristicLookup(const QString& name) { - return DesktopEntryManager::instance()->heuristicLookup(name); -} - -ObjectModel* DesktopEntries::applications() { - return DesktopEntryManager::instance()->applications(); -} diff --git a/src/core/desktopentry.hpp b/src/core/desktopentry.hpp deleted file mode 100644 index 827a6187..00000000 --- a/src/core/desktopentry.hpp +++ /dev/null @@ -1,204 +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); - /// Initial class or app id the app intends to use. May be useful for matching running apps - /// to desktop entries. - Q_PROPERTY(QString startupClass MEMBER mStartupClass 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. - /// - /// > [!WARNING] This cannot be reliably run as a command. See @@command for one you can run. - Q_PROPERTY(QString execString MEMBER mExecString CONSTANT); - /// The parsed `Exec` command in the desktop entry. - /// - /// The entry can be run with @@execute(), or by using this command in - /// @@Quickshell.Quickshell.execDetached() or @@Quickshell.Io.Process. - /// If used in `execDetached` or a `Process`, @@workingDirectory should also be passed to - /// the invoked process. See @@execute() for details. - /// - /// > [!NOTE] The provided command does not invoke a terminal even if @@runInTerminal is true. - Q_PROPERTY(QVector command MEMBER mCommand CONSTANT); - /// 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. - /// - /// This is equivalent to calling @@Quickshell.Quickshell.execDetached() with @@command - /// and @@DesktopEntry.workingDirectory as shown below: - /// - /// ```qml - /// Quickshell.execDetached({ - /// command: desktopEntry.command, - /// workingDirectory: desktopEntry.workingDirectory, - /// }); - /// ``` - 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 QList& execString, const QString& workingDirectory); - -public: - QString mId; - QString mName; - QString mGenericName; - QString mStartupClass; - bool mNoDisplay = false; - QString mComment; - QString mIcon; - QString mExecString; - QVector mCommand; - 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 action. - /// - /// > [!WARNING] This cannot be reliably run as a command. See @@command for one you can run. - Q_PROPERTY(QString execString MEMBER mExecString CONSTANT); - /// The parsed `Exec` command in the action. - /// - /// The entry can be run with @@execute(), or by using this command in - /// @@Quickshell.Quickshell.execDetached() or @@Quickshell.Io.Process. - /// If used in `execDetached` or a `Process`, @@DesktopEntry.workingDirectory should also be passed to - /// the invoked process. - /// - /// > [!NOTE] The provided command does not invoke a terminal even if @@runInTerminal is true. - Q_PROPERTY(QVector command MEMBER mCommand 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. - /// - /// This is equivalent to calling @@Quickshell.Quickshell.execDetached() with @@command - /// and @@DesktopEntry.workingDirectory. - Q_INVOKABLE void execute() const; - -private: - DesktopEntry* entry; - QString mId; - QString mName; - QString mIcon; - QString mExecString; - QVector mCommand; - QHash mEntries; - - friend class DesktopEntry; -}; - -class DesktopEntryManager: public QObject { - Q_OBJECT; - -public: - void scanDesktopEntries(); - - [[nodiscard]] DesktopEntry* byId(const QString& id); - [[nodiscard]] DesktopEntry* heuristicLookup(const QString& name); - - [[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. - /// - /// While this function requires an exact match, @@heuristicLookup() will correctly - /// find an entry more often and is generally more useful. - Q_INVOKABLE [[nodiscard]] static DesktopEntry* byId(const QString& id); - /// Look up a desktop entry by name using heuristics. Unlike @@byId(), - /// if no exact matches are found this function will try to guess - potentially incorrectly. - /// May return null. - Q_INVOKABLE [[nodiscard]] static DesktopEntry* heuristicLookup(const QString& name); - - [[nodiscard]] static ObjectModel* applications(); -}; diff --git a/src/core/doc.hpp b/src/core/doc.hpp index fbb21400..e4c907aa 100644 --- a/src/core/doc.hpp +++ b/src/core/doc.hpp @@ -9,15 +9,3 @@ // make the type visible in the docs even if not a QML_ELEMENT #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/easingcurve.cpp b/src/core/easingcurve.cpp deleted file mode 100644 index a758bd3c..00000000 --- a/src/core/easingcurve.cpp +++ /dev/null @@ -1,33 +0,0 @@ -#include "easingcurve.hpp" -#include - -#include -#include -#include -#include -#include - -qreal EasingCurve::valueAt(qreal x) const { return this->mCurve.valueForProgress(x); } - -qreal EasingCurve::interpolate(qreal x, qreal a, qreal b) const { - return a + (b - a) * this->valueAt(x); -} - -QPointF EasingCurve::interpolate(qreal x, const QPointF& a, const QPointF& b) const { - return QPointF(this->interpolate(x, a.x(), b.x()), this->interpolate(x, a.y(), b.y())); -} - -QRectF EasingCurve::interpolate(qreal x, const QRectF& a, const QRectF& b) const { - return QRectF( - this->interpolate(x, a.topLeft(), b.topLeft()), - this->interpolate(x, a.bottomRight(), b.bottomRight()) - ); -} - -QEasingCurve EasingCurve::curve() const { return this->mCurve; } - -void EasingCurve::setCurve(QEasingCurve curve) { - if (this->mCurve == curve) return; - this->mCurve = std::move(curve); - emit this->curveChanged(); -} diff --git a/src/core/easingcurve.hpp b/src/core/easingcurve.hpp deleted file mode 100644 index ef210383..00000000 --- a/src/core/easingcurve.hpp +++ /dev/null @@ -1,40 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include -#include - -///! Easing curve. -/// Directly accessible easing curve as used in property animations. -class EasingCurve: public QObject { - Q_OBJECT; - /// Easing curve settings. Works exactly the same as - /// [PropertyAnimation.easing](https://doc.qt.io/qt-6/qml-qtquick-propertyanimation.html#easing-prop). - Q_PROPERTY(QEasingCurve curve READ curve WRITE setCurve NOTIFY curveChanged); - QML_ELEMENT; - -public: - EasingCurve(QObject* parent = nullptr): QObject(parent) {} - - /// Returns the Y value for the given X value on the curve - /// from 0.0 to 1.0. - Q_INVOKABLE [[nodiscard]] qreal valueAt(qreal x) const; - /// Interpolates between two values using the given X coordinate. - Q_INVOKABLE [[nodiscard]] qreal interpolate(qreal x, qreal a, qreal b) const; - /// Interpolates between two points using the given X coordinate. - Q_INVOKABLE [[nodiscard]] QPointF interpolate(qreal x, const QPointF& a, const QPointF& b) const; - /// Interpolates two rects using the given X coordinate. - Q_INVOKABLE [[nodiscard]] QRectF interpolate(qreal x, const QRectF& a, const QRectF& b) const; - - [[nodiscard]] QEasingCurve curve() const; - void setCurve(QEasingCurve curve); - -signals: - void curveChanged(); - -private: - QEasingCurve mCurve; -}; 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/core/enginecontext.hpp b/src/core/enginecontext.hpp deleted file mode 100644 index 6675fc30..00000000 --- a/src/core/enginecontext.hpp +++ /dev/null @@ -1,16 +0,0 @@ -#pragma once - -#include "qsintercept.hpp" -#include "scan.hpp" -#include "singleton.hpp" - -class EngineContext { -public: - explicit EngineContext(const QmlScanner& scanner); - -private: - const QmlScanner& scanner; - QQmlEngine engine; - QsInterceptNetworkAccessManagerFactory interceptFactory; - SingletonRegistry singletonRegistry; -}; diff --git a/src/core/floatingwindow.cpp b/src/core/floatingwindow.cpp new file mode 100644 index 00000000..0f909c23 --- /dev/null +++ b/src/core/floatingwindow.cpp @@ -0,0 +1,62 @@ +#include "floatingwindow.hpp" + +#include +#include +#include +#include + +#include "proxywindow.hpp" +#include "windowinterface.hpp" + +void ProxyFloatingWindow::setWidth(qint32 width) { + if (this->window == nullptr || !this->window->isVisible()) { + this->ProxyWindowBase::setWidth(width); + } +} + +void ProxyFloatingWindow::setHeight(qint32 height) { + if (this->window == nullptr || !this->window->isVisible()) { + this->ProxyWindowBase::setHeight(height); + } +} + +// FloatingWindowInterface + +FloatingWindowInterface::FloatingWindowInterface(QObject* parent) + : WindowInterface(parent) + , window(new ProxyFloatingWindow(this)) { + // clang-format off + QObject::connect(this->window, &ProxyWindowBase::windowConnected, this, &FloatingWindowInterface::windowConnected); + QObject::connect(this->window, &ProxyWindowBase::visibleChanged, this, &FloatingWindowInterface::visibleChanged); + QObject::connect(this->window, &ProxyWindowBase::heightChanged, this, &FloatingWindowInterface::heightChanged); + QObject::connect(this->window, &ProxyWindowBase::widthChanged, this, &FloatingWindowInterface::widthChanged); + QObject::connect(this->window, &ProxyWindowBase::screenChanged, this, &FloatingWindowInterface::screenChanged); + QObject::connect(this->window, &ProxyWindowBase::colorChanged, this, &FloatingWindowInterface::colorChanged); + QObject::connect(this->window, &ProxyWindowBase::maskChanged, this, &FloatingWindowInterface::maskChanged); + // clang-format on +} + +void FloatingWindowInterface::onReload(QObject* oldInstance) { + auto* old = qobject_cast(oldInstance); + this->window->onReload(old != nullptr ? old->window : nullptr); +} + +QQmlListProperty FloatingWindowInterface::data() { return this->window->data(); } +QQuickItem* FloatingWindowInterface::contentItem() const { return this->window->contentItem(); } + +// NOLINTBEGIN +#define proxyPair(type, get, set) \ + type FloatingWindowInterface::get() const { return this->window->get(); } \ + void FloatingWindowInterface::set(type value) { this->window->set(value); } + +proxyPair(bool, isVisible, setVisible); +proxyPair(qint32, width, setWidth); +proxyPair(qint32, height, setHeight); +proxyPair(QuickshellScreenInfo*, screen, setScreen); +proxyPair(QColor, color, setColor); +proxyPair(PendingRegion*, mask, setMask); + +#undef proxyPair +#undef proxySet +#undef proxyGet +// NOLINTEND diff --git a/src/core/floatingwindow.hpp b/src/core/floatingwindow.hpp new file mode 100644 index 00000000..408b1e9f --- /dev/null +++ b/src/core/floatingwindow.hpp @@ -0,0 +1,56 @@ +#pragma once + +#include +#include + +#include "proxywindow.hpp" + +class ProxyFloatingWindow: public ProxyWindowBase { + Q_OBJECT; + +public: + explicit ProxyFloatingWindow(QObject* parent = nullptr): ProxyWindowBase(parent) {} + + // Setting geometry while the window is visible makes the content item shrinks but not the window + // which is awful so we disable it for floating windows. + void setWidth(qint32 width) override; + void setHeight(qint32 height) override; +}; + +///! Standard floating window. +class FloatingWindowInterface: public WindowInterface { + Q_OBJECT; + QML_NAMED_ELEMENT(FloatingWindow); + +public: + explicit FloatingWindowInterface(QObject* parent = nullptr); + + void onReload(QObject* oldInstance) override; + + [[nodiscard]] QQuickItem* contentItem() const override; + + // NOLINTBEGIN + [[nodiscard]] bool isVisible() const override; + void setVisible(bool visible) override; + + [[nodiscard]] qint32 width() const override; + void setWidth(qint32 width) override; + + [[nodiscard]] qint32 height() const override; + void setHeight(qint32 height) override; + + [[nodiscard]] QuickshellScreenInfo* screen() const override; + void setScreen(QuickshellScreenInfo* screen) override; + + [[nodiscard]] QColor color() const override; + void setColor(QColor color) override; + + [[nodiscard]] PendingRegion* mask() const override; + void setMask(PendingRegion* mask) override; + + [[nodiscard]] QQmlListProperty data() override; + // NOLINTEND + +private: + ProxyFloatingWindow* window; +}; diff --git a/src/core/generation.cpp b/src/core/generation.cpp deleted file mode 100644 index fee94416..00000000 --- a/src/core/generation.cpp +++ /dev/null @@ -1,413 +0,0 @@ -#include "generation.hpp" -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "iconimageprovider.hpp" -#include "imageprovider.hpp" -#include "incubator.hpp" -#include "logcat.hpp" -#include "plugin.hpp" -#include "qsintercept.hpp" -#include "reload.hpp" -#include "scan.hpp" - -namespace { -QS_LOGGING_CATEGORY(logScene, "scene"); -} - -static QHash g_generations; // NOLINT - -EngineGeneration::EngineGeneration(const QDir& rootPath, QmlScanner scanner) - : rootPath(rootPath) - , scanner(std::move(scanner)) - , urlInterceptor(this->rootPath) - , interceptNetFactory(this->rootPath, this->scanner.fileIntercepts) - , engine(new QQmlEngine()) { - g_generations.insert(this->engine, this); - - this->engine->setOutputWarningsToStandardError(false); - QObject::connect(this->engine, &QQmlEngine::warnings, this, &EngineGeneration::onEngineWarnings); - - this->engine->addUrlInterceptor(&this->urlInterceptor); - this->engine->addImportPath("qs:@/"); - - this->engine->setNetworkAccessManagerFactory(&this->interceptNetFactory); - this->engine->setIncubationController(&this->delayedIncubationController); - - this->engine->addImageProvider("icon", new IconImageProvider()); - this->engine->addImageProvider("qsimage", new QsImageProvider()); - this->engine->addImageProvider("qspixmap", new QsPixmapProvider()); - - QsEnginePlugin::runConstructGeneration(*this); -} - -EngineGeneration::EngineGeneration(): EngineGeneration(QDir(), QmlScanner()) {} - -EngineGeneration::~EngineGeneration() { - if (this->engine != nullptr) { - qFatal() << this << "destroyed without calling destroy()"; - } -} - -void EngineGeneration::destroy() { - if (this->destroying) return; - this->destroying = true; - - 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) { - QObject::connect(this->root, &QObject::destroyed, this, [this]() { - // prevent further js execution between garbage collection and engine destruction. - this->engine->setInterrupted(true); - - g_generations.remove(this->engine); - - // 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); - }); - - 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->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); - } - - this->singletonRegistry.onReload(old == nullptr ? nullptr : &old->singletonRegistry); - this->reloadComplete = true; - emit this->reloadFinished(); - - if (old != nullptr) { - QObject::connect(old, &QObject::destroyed, this, [this]() { this->postReload(); }); - old->destroy(); - } else { - this->postReload(); - } -} - -void EngineGeneration::postReload() { - // This can be called on a generation during its destruction. - if (this->engine == nullptr || this->root == nullptr) return; - - QsEnginePlugin::runOnReload(); - - emit this->firePostReload(); - QObject::disconnect(this, &EngineGeneration::firePostReload, nullptr, nullptr); -} - -void EngineGeneration::setWatchingFiles(bool watching) { - if (watching) { - if (this->watcher == nullptr) { - this->watcher = new QFileSystemWatcher(); - - for (auto& file: this->scanner.scannedFiles) { - this->watcher->addPath(file); - this->watcher->addPath(QFileInfo(file).dir().absolutePath()); - } - - for (auto& file: this->extraWatchedFiles) { - 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 - ); - } - } else { - if (this->watcher != nullptr) { - this->watcher->deleteLater(); - this->watcher = nullptr; - } - } -} - -bool EngineGeneration::setExtraWatchedFiles(const QVector& files) { - this->extraWatchedFiles.clear(); - for (const auto& file: files) { - if (!this->scanner.scannedFiles.contains(file)) { - this->extraWatchedFiles.append(file); - } - } - - if (this->watcher) { - this->setWatchingFiles(false); - this->setWatchingFiles(true); - } - - return !this->extraWatchedFiles.isEmpty(); -} - -void EngineGeneration::onFileChanged(const QString& name) { - if (!this->watcher->files().contains(name)) { - this->deletedWatchedFiles.push_back(name); - } else { - // some editors (e.g vscode) perform file saving in two steps: truncate + write - // ignore the first event (truncate) with size 0 to prevent incorrect live reloading - auto fileInfo = QFileInfo(name); - if (fileInfo.isFile() && fileInfo.size() == 0) return; - - emit this->filesChanged(); - } -} - -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) { - // We only want controllers that we can swap out if destroyed. - // This happens if the window owning the active controller dies. - auto* obj = dynamic_cast(controller); - if (!obj) { - qCWarning(logIncubator) << "Could not register incubation controller as it is not a QObject" - << controller; - - return; - } - - QObject::connect( - obj, - &QObject::destroyed, - this, - &EngineGeneration::incubationControllerDestroyed, - Qt::UniqueConnection - ); - - this->incubationControllers.push_back(obj); - qCDebug(logIncubator) << "Registered incubation controller" << obj << "to generation" << this; - - // This function can run during destruction. - if (this->engine == nullptr) return; - - if (this->engine->incubationController() == &this->delayedIncubationController) { - this->assignIncubationController(); - } -} - -// Multiple controllers may be destroyed at once. Dynamic casts must be performed before working -// with any controllers. The QQmlIncubationController destructor will already have run by the -// point QObject::destroyed is called, so we can't cast to that. -void EngineGeneration::deregisterIncubationController(QQmlIncubationController* controller) { - auto* obj = dynamic_cast(controller); - if (!obj) { - qCCritical(logIncubator) << "Deregistering incubation controller which is not a QObject, " - "however only QObject controllers should be registered."; - } - - QObject::disconnect(obj, nullptr, this, nullptr); - - if (this->incubationControllers.removeOne(obj)) { - qCDebug(logIncubator) << "Deregistered incubation controller" << obj << "from" << this; - } else { - qCCritical(logIncubator) << "Failed to deregister incubation controller" << obj << "from" - << this << "as it was not registered to begin with"; - qCCritical(logIncubator) << "Current registered incuabation controllers" - << this->incubationControllers; - } - - // This function can run during destruction. - if (this->engine == nullptr) return; - - if (this->engine->incubationController() == controller) { - qCDebug(logIncubator - ) << "Destroyed incubation controller was currently active, reassigning from pool"; - this->assignIncubationController(); - } -} - -void EngineGeneration::incubationControllerDestroyed() { - auto* sender = this->sender(); - - if (this->incubationControllers.removeAll(sender) != 0) { - qCDebug(logIncubator) << "Destroyed incubation controller" << sender << "deregistered from" - << this; - } else { - qCCritical(logIncubator) << "Destroyed incubation controller" << sender - << "was not registered, but its destruction was observed by" << this; - - return; - } - - // This function can run during destruction. - if (this->engine == nullptr) return; - - if (dynamic_cast(this->engine->incubationController()) == sender) { - qCDebug(logIncubator - ) << "Destroyed incubation controller was currently active, reassigning from pool"; - this->assignIncubationController(); - } -} - -void EngineGeneration::onEngineWarnings(const QList& warnings) { - for (const auto& error: warnings) { - const auto& url = error.url(); - auto rel = url.scheme() == "qs" && url.path().startsWith("@/qs/") ? "@" % url.path().sliced(5) - : url.toString(); - - QString objectName; - auto desc = error.description(); - if (auto i = desc.indexOf(": "); i != -1 && desc.startsWith("QML ")) { - objectName = desc.first(i) + " at "; - desc = desc.sliced(i + 2); - } - - qCWarning(logScene).noquote().nospace() - << objectName << rel << '[' << error.line() << ':' << error.column() << "]: " << desc; - } -} - -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->incubationControllersLocked || this->incubationControllers.isEmpty()) { - controller = &this->delayedIncubationController; - } else { - controller = dynamic_cast(this->incubationControllers.first()); - } - - qCDebug(logIncubator) << "Assigning incubation controller" << controller << "to generation" - << this - << "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(); - - while (object != nullptr) { - auto* context = QQmlEngine::contextForObject(object); - - if (context != nullptr) { - if (auto* generation = EngineGeneration::findEngineGeneration(context->engine())) { - return generation; - } - } - - object = object->parent(); - } - - return nullptr; -} diff --git a/src/core/generation.hpp b/src/core/generation.hpp deleted file mode 100644 index 3c0c4ae5..00000000 --- a/src/core/generation.hpp +++ /dev/null @@ -1,100 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "incubator.hpp" -#include "qsintercept.hpp" -#include "scan.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(); - explicit EngineGeneration(const QDir& rootPath, QmlScanner scanner); - ~EngineGeneration() override; - Q_DISABLE_COPY_MOVE(EngineGeneration); - - // assumes root has been initialized, consumes old generation - void onReload(EngineGeneration* old); - void setWatchingFiles(bool watching); - bool setExtraWatchedFiles(const QVector& files); - - 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(); - - RootWrapper* wrapper = nullptr; - QDir rootPath; - QmlScanner scanner; - QsUrlInterceptor urlInterceptor; - QsInterceptNetworkAccessManagerFactory interceptNetFactory; - QQmlEngine* engine = nullptr; - QObject* root = nullptr; - SingletonRegistry singletonRegistry; - QFileSystemWatcher* watcher = nullptr; - QVector deletedWatchedFiles; - QVector extraWatchedFiles; - DelayedQmlIncubationController delayedIncubationController; - bool reloadComplete = false; - QuickshellGlobal* qsgInstance = nullptr; - - void destroy(); - void shutdown(); - -signals: - void filesChanged(); - void reloadFinished(); - void firePostReload(); - -public slots: - void quit(); - void exit(int code); - -private slots: - void onFileChanged(const QString& name); - void onDirectoryChanged(); - void incubationControllerDestroyed(); - static void onEngineWarnings(const QList& warnings); - -private: - void postReload(); - void assignIncubationController(); - QVector incubationControllers; - bool incubationControllersLocked = false; - QHash extensions; - - bool destroying = false; - bool shouldTerminate = false; - int exitCode = 0; -}; diff --git a/src/core/iconimageprovider.cpp b/src/core/iconimageprovider.cpp deleted file mode 100644 index 43e00fd8..00000000 --- a/src/core/iconimageprovider.cpp +++ /dev/null @@ -1,84 +0,0 @@ -#include "iconimageprovider.hpp" -#include - -#include -#include -#include -#include -#include -#include -#include - -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); - path = id.sliced(splitIdx + 6); - 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; - } - } - - 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); - auto pixmap = icon.pixmap(targetSize.width(), targetSize.height()); - - if (pixmap.isNull()) { - qWarning() << "Could not load icon" << id << "at size" << targetSize << "from request"; - pixmap = IconImageProvider::missingPixmap(targetSize); - } - - if (size != nullptr) *size = pixmap.size(); - return pixmap; -} - -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); - - auto pixmap = QPixmap(width, height); - pixmap.fill(QColorConstants::Black); - auto painter = QPainter(&pixmap); - - auto halfWidth = width / 2; - auto halfHeight = height / 2; - auto purple = QColor(0xd900d8); - painter.fillRect(halfWidth, 0, halfWidth, halfHeight, purple); - painter.fillRect(0, halfHeight, halfWidth, halfHeight, purple); - return pixmap; -} - -QString IconImageProvider::requestString( - const QString& icon, - const QString& path, - const QString& fallback -) { - 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 deleted file mode 100644 index 57e26049..00000000 --- a/src/core/iconimageprovider.hpp +++ /dev/null @@ -1,19 +0,0 @@ -#pragma once - -#include -#include - -class IconImageProvider: public QQuickImageProvider { -public: - explicit IconImageProvider(): QQuickImageProvider(QQuickImageProvider::Pixmap) {} - - 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() - ); -}; 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 deleted file mode 100644 index 47f284c7..00000000 --- a/src/core/imageprovider.cpp +++ /dev/null @@ -1,93 +0,0 @@ -#include "imageprovider.hpp" - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace { - -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; - } -} - -} // namespace - -QsImageHandle::QsImageHandle(QQmlImageProviderBase::ImageType type) - : type(type) - , id(QString::number(++handleIndex)) { - liveImages.insert(this->id, this); -} - -QsImageHandle::~QsImageHandle() { liveImages.remove(this->id); } - -QString QsImageHandle::url() const { - QString url = "image://"; - if (this->type == QQmlImageProviderBase::Image) url += "qsimage"; - else if (this->type == QQmlImageProviderBase::Pixmap) url += "qspixmap"; - url += "/" + this->id; - return url; -} - -QImage -QsImageHandle::requestImage(const QString& /*unused*/, QSize* /*unused*/, const QSize& /*unused*/) { - qWarning() << "Image handle" << this << "does not provide QImages"; - return QImage(); -} - -QPixmap QsImageHandle:: - requestPixmap(const QString& /*unused*/, QSize* /*unused*/, const QSize& /*unused*/) { - qWarning() << "Image handle" << this << "does not provide QPixmaps"; - return QPixmap(); -} - -QImage QsImageProvider::requestImage(const QString& id, QSize* size, const QSize& requestedSize) { - QString target; - QString param; - parseReq(id, target, param); - - auto* handle = liveImages.value(target); - if (handle != nullptr) { - return handle->requestImage(param, size, requestedSize); - } else { - qWarning() << "Requested image from unknown handle" << id; - return QImage(); - } -} - -QPixmap -QsPixmapProvider::requestPixmap(const QString& id, QSize* size, const QSize& requestedSize) { - QString target; - QString param; - parseReq(id, target, param); - - auto* handle = liveImages.value(target); - if (handle != nullptr) { - return handle->requestPixmap(param, size, requestedSize); - } else { - qWarning() << "Requested image from unknown handle" << id; - 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 deleted file mode 100644 index 8568d4f7..00000000 --- a/src/core/imageprovider.hpp +++ /dev/null @@ -1,48 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include -#include -#include - -class QsImageProvider: public QQuickImageProvider { -public: - explicit QsImageProvider(): QQuickImageProvider(QQuickImageProvider::Image) {} - QImage requestImage(const QString& id, QSize* size, const QSize& requestedSize) override; -}; - -class QsPixmapProvider: public QQuickImageProvider { -public: - explicit QsPixmapProvider(): QQuickImageProvider(QQuickImageProvider::Pixmap) {} - QPixmap requestPixmap(const QString& id, QSize* size, const QSize& requestedSize) override; -}; - -class QsImageHandle { -public: - explicit QsImageHandle(QQmlImageProviderBase::ImageType type); - virtual ~QsImageHandle(); - Q_DISABLE_COPY_MOVE(QsImageHandle); - - [[nodiscard]] virtual 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); - -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/incubator.cpp b/src/core/incubator.cpp deleted file mode 100644 index c9d149a8..00000000 --- a/src/core/incubator.cpp +++ /dev/null @@ -1,17 +0,0 @@ -#include "incubator.hpp" - -#include -#include -#include - -#include "logcat.hpp" - -QS_LOGGING_CATEGORY(logIncubator, "quickshell.incubator", QtWarningMsg); - -void QsQmlIncubator::statusChanged(QQmlIncubator::Status status) { - switch (status) { - case QQmlIncubator::Ready: emit this->completed(); break; - case QQmlIncubator::Error: emit this->failed(); break; - default: break; - } -} diff --git a/src/core/incubator.hpp b/src/core/incubator.hpp deleted file mode 100644 index 5ebb9a0e..00000000 --- a/src/core/incubator.hpp +++ /dev/null @@ -1,31 +0,0 @@ -#pragma once - -#include -#include -#include - -#include "logcat.hpp" - -QS_DECLARE_LOGGING_CATEGORY(logIncubator); - -class QsQmlIncubator - : public QObject - , public QQmlIncubator { - Q_OBJECT; - -public: - explicit QsQmlIncubator(QsQmlIncubator::IncubationMode mode, QObject* parent = nullptr) - : QObject(parent) - , QQmlIncubator(mode) {} - - void statusChanged(QQmlIncubator::Status status) override; - -signals: - void completed(); - void failed(); -}; - -class DelayedQmlIncubationController: public QQmlIncubationController { - // Do nothing. - // This ensures lazy loaders don't start blocking before onReload creates windows. -}; diff --git a/src/core/instanceinfo.cpp b/src/core/instanceinfo.cpp deleted file mode 100644 index 7f0132be..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 << info.pid; - return stream; -} - -QDataStream& operator>>(QDataStream& stream, InstanceInfo& info) { - stream >> info.instanceId >> info.configPath >> info.shellId >> info.launchTime >> info.pid; - 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 98ce614f..00000000 --- a/src/core/instanceinfo.hpp +++ /dev/null @@ -1,41 +0,0 @@ -#pragma once - -#include -#include -#include -#include - -struct InstanceInfo { - QString instanceId; - QString configPath; - QString shellId; - QDateTime launchTime; - pid_t pid = -1; - - 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 deleted file mode 100644 index be0eb78b..00000000 --- a/src/core/lazyloader.cpp +++ /dev/null @@ -1,200 +0,0 @@ -#include "lazyloader.hpp" -#include - -#include -#include -#include -#include -#include -#include -#include - -#include "incubator.hpp" -#include "reload.hpp" - -void LazyLoader::onReload(QObject* oldInstance) { - auto* old = qobject_cast(oldInstance); - - this->incubateIfReady(true); - - if (old != nullptr && old->mItem != nullptr && this->incubator != nullptr) { - this->incubator->forceCompletion(); - } - - if (this->mItem != nullptr) { - if (auto* reloadable = qobject_cast(this->mItem)) { - reloadable->reload(old == nullptr ? nullptr : old->mItem); - } else { - Reloadable::reloadRecursive(this->mItem, old); - } - } -} - -QObject* LazyLoader::item() { - if (this->isLoading()) this->setActive(true); - return this->mItem; -} - -void LazyLoader::setItem(QObject* item) { - if (item == this->mItem) return; - - if (this->mItem != nullptr) { - this->mItem->deleteLater(); - } - - this->mItem = item; - - if (item != nullptr) { - item->setParent(this); - } - - this->targetActive = this->isActive(); - - emit this->itemChanged(); - emit this->activeChanged(); -} - -bool LazyLoader::isLoading() const { return this->incubator != nullptr; } - -void LazyLoader::setLoading(bool loading) { - if (loading == this->targetLoading || this->isActive()) return; - this->targetLoading = loading; - - if (loading) { - this->incubateIfReady(); - } else if (this->mItem != nullptr) { - this->mItem->deleteLater(); - this->mItem = nullptr; - } else if (this->incubator != nullptr) { - delete this->incubator; - this->incubator = nullptr; - } -} - -bool LazyLoader::isActive() const { return this->mItem != nullptr; } - -void LazyLoader::setActive(bool active) { - if (active == this->targetActive) return; - this->targetActive = active; - - if (active) { - if (this->isLoading()) { - this->incubator->forceCompletion(); - } else if (!this->isActive()) { - this->incubateIfReady(); - } - } else if (this->isActive()) { - this->setItem(nullptr); - } -} - -void LazyLoader::setActiveAsync(bool active) { - if (active == (this->targetActive || this->targetLoading)) return; - if (active) this->setLoading(true); - else this->setActive(false); -} - -QQmlComponent* LazyLoader::component() const { - return this->cleanupComponent ? nullptr : this->mComponent; -} - -void LazyLoader::setComponent(QQmlComponent* component) { - if (this->cleanupComponent) this->setSource(nullptr); - if (component == this->mComponent) return; - this->cleanupComponent = false; - - if (this->mComponent != nullptr) { - QObject::disconnect(this->mComponent, nullptr, this, nullptr); - } - - this->mComponent = component; - - if (component != nullptr) { - QObject::connect( - this->mComponent, - &QObject::destroyed, - this, - &LazyLoader::onComponentDestroyed - ); - } - - emit this->componentChanged(); -} - -void LazyLoader::onComponentDestroyed() { - this->mComponent = nullptr; - // todo: figure out what happens to the incubator -} - -QString LazyLoader::source() const { return this->mSource; } - -void LazyLoader::setSource(QString source) { - if (!this->cleanupComponent) this->setComponent(nullptr); - if (source == this->mSource) return; - this->cleanupComponent = true; - - this->mSource = std::move(source); - delete this->mComponent; - - if (!this->mSource.isEmpty()) { - auto* context = QQmlEngine::contextForObject(this); - this->mComponent = new QQmlComponent( - context == nullptr ? nullptr : context->engine(), - context == nullptr ? this->mSource : context->resolvedUrl(this->mSource) - ); - - if (this->mComponent->isError()) { - qWarning() << this->mComponent->errorString().toStdString().c_str(); - delete this->mComponent; - this->mComponent = nullptr; - } - } else { - this->mComponent = nullptr; - } - - emit this->sourceChanged(); -} - -void LazyLoader::incubateIfReady(bool overrideReloadCheck) { - if (!(this->reloadComplete || overrideReloadCheck) || !(this->targetLoading || this->targetActive) - || this->mComponent == nullptr || this->incubator != nullptr) - { - return; - } - - this->incubator = new QsQmlIncubator( - this->targetActive ? QQmlIncubator::Synchronous : QQmlIncubator::Asynchronous, - this - ); - - // clang-format off - QObject::connect(this->incubator, &QsQmlIncubator::completed, this, &LazyLoader::onIncubationCompleted); - QObject::connect(this->incubator, &QsQmlIncubator::failed, this, &LazyLoader::onIncubationFailed); - // clang-format on - - emit this->loadingChanged(); - - this->mComponent->create(*this->incubator, QQmlEngine::contextForObject(this->mComponent)); -} - -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(); - this->incubator = nullptr; - this->targetLoading = false; - emit this->loadingChanged(); -} - -void LazyLoader::onIncubationFailed() { - qWarning() << "Failed to create LazyLoader component"; - - for (auto& error: this->incubator->errors()) { - qWarning() << error; - } - - delete this->incubator; - this->targetLoading = false; - emit this->loadingChanged(); -} diff --git a/src/core/lazyloader.hpp b/src/core/lazyloader.hpp deleted file mode 100644 index dbaad4b5..00000000 --- a/src/core/lazyloader.hpp +++ /dev/null @@ -1,173 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include - -#include "incubator.hpp" -#include "reload.hpp" - -///! Asynchronous component loader. -/// The LazyLoader can be used to prepare components that don't need to be -/// created immediately, such as windows that aren't visible until triggered -/// by another action. It works on creating the component in the gaps between -/// frame rendering to prevent blocking the interface thread. -/// It can also be used to preserve memory by loading components only -/// when you need them and unloading them afterward. -/// -/// Note that when reloading the UI due to changes, lazy loaders will always -/// load synchronously so windows can be reused. -/// -/// #### Example -/// The following example creates a PopupWindow asynchronously as the bar loads. -/// This means the bar can be shown onscreen before the popup is ready, however -/// trying to show the popup before it has finished loading in the background -/// will cause the UI thread to block. -/// -/// ```qml -/// import QtQuick -/// import QtQuick.Controls -/// import Quickshell -/// -/// ShellRoot { -/// PanelWindow { -/// id: window -/// height: 50 -/// -/// anchors { -/// bottom: true -/// left: true -/// right: true -/// } -/// -/// LazyLoader { -/// id: popupLoader -/// -/// // start loading immediately -/// loading: true -/// -/// // this window will be loaded in the background during spare -/// // frame time unless active is set to true, where it will be -/// // loaded in the foreground -/// PopupWindow { -/// // position the popup above the button -/// parentWindow: window -/// relativeX: window.width / 2 - width / 2 -/// relativeY: -height -/// -/// // some heavy component here -/// -/// width: 200 -/// height: 200 -/// } -/// } -/// -/// Button { -/// anchors.centerIn: parent -/// text: "show popup" -/// -/// // accessing popupLoader.item will force the loader to -/// // finish loading on the UI thread if it isn't finished yet. -/// onClicked: popupLoader.item.visible = !popupLoader.item.visible -/// } -/// } -/// } -/// ``` -/// -/// > [!WARNING] Components that internally load other components must explicitly -/// > support asynchronous loading to avoid blocking. -/// > -/// > Notably, @@Variants does not corrently support asynchronous -/// > loading, meaning using it inside a LazyLoader will block similarly to not -/// > having a loader to start with. -/// -/// > [!WARNING] LazyLoaders do not start loading before the first window is created, -/// > meaning if you create all windows inside of lazy loaders, none of them will ever load. -class LazyLoader: public Reloadable { - Q_OBJECT; - /// The fully loaded item if the loader is @@loading or @@active, or `null` - /// if neither @@loading nor @@active. - /// - /// Note that the item is owned by the LazyLoader, and destroying the LazyLoader - /// will destroy the item. - /// - /// > [!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 - /// > ensure loading happens asynchronously. - Q_PROPERTY(QObject* item READ item NOTIFY itemChanged); - /// If the loader is actively loading. - /// - /// If the component is not loaded, setting this property to true will start - /// loading it asynchronously. If the component is already loaded, setting - /// this property has no effect. - /// - /// See also: @@activeAsync. - Q_PROPERTY(bool loading READ isLoading WRITE setLoading NOTIFY loadingChanged); - /// If the component is fully loaded. - /// - /// Setting this property to `true` will force the component to load to completion, - /// blocking the UI, and setting it to `false` will destroy the component, requiring - /// it to be loaded again. - /// - /// See also: @@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. - Q_PROPERTY(bool activeAsync READ isActive WRITE setActiveAsync NOTIFY activeChanged); - /// 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. - Q_PROPERTY(QString source READ source WRITE setSource NOTIFY sourceChanged); - Q_CLASSINFO("DefaultProperty", "component"); - QML_ELEMENT; - -public: - void onReload(QObject* oldInstance) override; - - [[nodiscard]] bool isActive() const; - void setActive(bool active); - void setActiveAsync(bool active); - - [[nodiscard]] bool isLoading() const; - void setLoading(bool loading); - - [[nodiscard]] QObject* item(); - void setItem(QObject* item); - - [[nodiscard]] QQmlComponent* component() const; - void setComponent(QQmlComponent* component); - - [[nodiscard]] QString source() const; - void setSource(QString source); - -signals: - void activeChanged(); - void loadingChanged(); - void itemChanged(); - void sourceChanged(); - void componentChanged(); - -private slots: - void onIncubationCompleted(); - void onIncubationFailed(); - void onComponentDestroyed(); - -private: - void incubateIfReady(bool overrideReloadCheck = false); - void waitForObjectCreation(); - - bool targetLoading = false; - bool targetActive = false; - QObject* mItem = nullptr; - QString mSource; - QQmlComponent* mComponent = nullptr; - QsQmlIncubator* incubator = nullptr; - bool cleanupComponent = false; -}; diff --git a/src/core/logcat.hpp b/src/core/logcat.hpp deleted file mode 100644 index 9650ddbf..00000000 --- a/src/core/logcat.hpp +++ /dev/null @@ -1,28 +0,0 @@ -#pragma once - -#include -#include - -namespace qs::log { -void initLogCategoryLevel(const char* name, QtMsgType defaultLevel = QtDebugMsg); -} - -// NOLINTNEXTLINE(cppcoreguidelines-macro-usage) -#define QS_DECLARE_LOGGING_CATEGORY(name) \ - namespace qslogcat { \ - Q_DECLARE_LOGGING_CATEGORY(name); \ - } \ - const QLoggingCategory& name() - -// NOLINTNEXTLINE(cppcoreguidelines-macro-usage) -#define QS_LOGGING_CATEGORY(name, category, ...) \ - namespace qslogcat { \ - Q_LOGGING_CATEGORY(name, category __VA_OPT__(, __VA_ARGS__)); \ - } \ - const QLoggingCategory& name() { \ - static auto* init = []() { \ - qs::log::initLogCategoryLevel(category __VA_OPT__(, __VA_ARGS__)); \ - return &qslogcat::name; \ - }(); \ - return (init) (); \ - } diff --git a/src/core/logging.cpp b/src/core/logging.cpp deleted file mode 100644 index cb3a2142..00000000 --- a/src/core/logging.cpp +++ /dev/null @@ -1,957 +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 "logcat.hpp" -#include "logging_p.hpp" -#include "logging_qtprivate.cpp" // NOLINT -#include "paths.hpp" -#include "ringbuf.hpp" - -QS_LOGGING_CATEGORY(logBare, "quickshell.bare"); - -namespace qs::log { -using namespace qt_logging_registry; - -QS_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.")); - - CategoryFilter filter; - - // We don't respect log filters for qs logs because some distros like to ship - // default configs that hide everything. QT_LOGGING_RULES is considered via the filter list. - if (isQs) { - // QtDebugMsg == 0, so default - auto defaultLevel = instance->defaultLevels.value(categoryName); - - filter = CategoryFilter(); - // clang-format off - filter.debug = instance->mDefaultLevel == QtDebugMsg || defaultLevel == QtDebugMsg; - filter.info = filter.debug || instance->mDefaultLevel == QtInfoMsg || defaultLevel == QtInfoMsg; - filter.warn = filter.info || instance->mDefaultLevel == QtWarningMsg || defaultLevel == QtWarningMsg; - filter.critical = filter.warn || instance->mDefaultLevel == QtCriticalMsg || defaultLevel == QtCriticalMsg; - // clang-format on - } else if (instance->lastCategoryFilter) { - instance->lastCategoryFilter(category); - filter = CategoryFilter(category); - } - - 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; - // Load QT_LOGGING_RULES because we ignore the last category filter for QS messages - // due to disk config files. - parser.setContent(qEnvironmentVariable("QT_LOGGING_RULES")); - instance->rules = new QList(parser.rules()); - parser.setContent(rules); - instance->rules->append(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 initLogCategoryLevel(const char* name, QtMsgType defaultLevel) { - LogManager::instance()->defaultLevels.insert(QLatin1StringView(name), defaultLevel); -} - -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" << detailedPath; - } - - // 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 && !this->detailedFile->flush())) { - if (this->detailedFile) { - qCCritical(logLogging) << "Detailed logger failed to write. Ending detailed logs."; - } - - this->detailedWriter.setDevice(nullptr); - this->detailedFile->close(); - this->detailedFile = nullptr; - } -} - -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 bf811333..00000000 --- a/src/core/logging.hpp +++ /dev/null @@ -1,154 +0,0 @@ -#pragma once - -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "logcat.hpp" - -QS_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 defaultLevels; - QHash sparseFilters; - QHash allFilters; - - QTextStream stdoutStream; - LoggingThreadProxy threadProxy; - - friend void initLogCategoryLevel(const char* name, QtMsgType defaultLevel); -}; - -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 48f74dee..00000000 --- a/src/core/logging_qtprivate.cpp +++ /dev/null @@ -1,139 +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 "logcat.hpp" -#include "logging_qtprivate.hpp" - -namespace qs::log { -QS_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 61d3a7cf..00000000 --- a/src/core/logging_qtprivate.hpp +++ /dev/null @@ -1,47 +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 - -#include "logcat.hpp" - -namespace qs::log { -QS_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..7dd7af30 --- /dev/null +++ b/src/core/main.cpp @@ -0,0 +1,257 @@ +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "plugin.hpp" +#include "rootwrapper.hpp" + +int main(int argc, char** argv) { + const auto app = QGuiApplication(argc, argv); + QGuiApplication::setApplicationName("quickshell"); + QGuiApplication::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); + + QString configFilePath; + { + 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) { + qFatal() << "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) { + qFatal() << "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 + } + } + + qFatal() << "configuration" << configName << "not found in manifest" << manifestPath; + return -1; + } else if (manifestPathLevel < 2) { + qFatal() << "cannot open config manifest at" << manifestPath; + return -1; + } + } + + { + auto basePathInfo = QFileInfo(basePath); + if (!basePathInfo.exists()) { + qFatal() << "base path does not exist:" << basePath; + return -1; + } else if (!QFileInfo(basePathInfo.canonicalFilePath()).isDir()) { + qFatal() << "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 + } + } + + qFatal() << "no directory named " << configName << "found in base path" << basePath; + return -1; + } + haspath:; + } else { + configFilePath = basePath; + } + + auto configFile = QFileInfo(configFilePath); + if (!configFile.exists()) { + qFatal() << "config path does not exist:" << configFilePath; + return -1; + } + + if (configFile.isDir()) { + configFilePath = QDir(configFilePath).filePath("shell.qml"); + } + + configFile = QFileInfo(configFilePath); + if (!configFile.exists()) { + qFatal() << "no shell.qml found in config path:" << configFilePath; + return -1; + } else if (configFile.isDir()) { + qFatal() << "shell.qml is a directory:" << configFilePath; + return -1; + } + + configFilePath = QFileInfo(configFilePath).canonicalFilePath(); + configFile = QFileInfo(configFilePath); + if (!configFile.exists()) { + qFatal() << "config file does not exist:" << configFilePath; + return -1; + } else if (configFile.isDir()) { + qFatal() << "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)) { + QDir::setCurrent(parser.value(workdirOption)); + } + + QuickshellPlugin::initPlugins(); + + // Base window transparency appears to be additive. + // Use a fully transparent window with a colored rect. + QQuickWindow::setDefaultAlphaBuffer(true); + + auto root = RootWrapper(configFilePath); + + return QGuiApplication::exec(); +} diff --git a/src/core/model.cpp b/src/core/model.cpp deleted file mode 100644 index 165c6066..00000000 --- a/src/core/model.cpp +++ /dev/null @@ -1,81 +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"}}; -} - -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 3c5822a4..00000000 --- a/src/core/model.hpp +++ /dev/null @@ -1,126 +0,0 @@ -#pragma once - -#include - -#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(QList 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]] QList values() const { return this->valuesList; } - 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 insertObjectSorted(T* object, const std::function& compare) { - auto& list = this->valueList(); - auto iter = list.begin(); - - while (iter != list.end()) { - if (!compare(object, *iter)) break; - ++iter; - } - - auto idx = iter - list.begin(); - this->UntypedObjectModel::insertObject(object, idx); - } - - 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 b9404ea9..70b2c8c8 100644 --- a/src/core/module.md +++ b/src/core/module.md @@ -7,28 +7,10 @@ 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", - "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", - "colorquantizer.hpp", + "windowinterface.hpp", + "panelinterface.hpp", + "floatingwindow.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 69% rename from src/window/panelinterface.hpp rename to src/core/panelinterface.hpp index 64dff503..b46c25ca 100644 --- a/src/window/panelinterface.hpp +++ b/src/core/panelinterface.hpp @@ -1,12 +1,9 @@ #pragma once -#include #include #include -#include -#include "../core/doc.hpp" -#include "../core/types.hpp" +#include "doc.hpp" #include "windowinterface.hpp" class Anchors { @@ -15,28 +12,12 @@ 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; } [[nodiscard]] bool verticalConstraint() const noexcept { return this->mTop && this->mBottom; } - [[nodiscard]] Qt::Edge exclusionEdge() const noexcept { - auto hasHEdge = this->mLeft ^ this->mRight; - auto hasVEdge = this->mTop ^ this->mBottom; - - if (hasVEdge && !hasHEdge) { - if (this->mTop) return Qt::TopEdge; - if (this->mBottom) return Qt::BottomEdge; - } else if (hasHEdge && !hasVEdge) { - if (this->mLeft) return Qt::LeftEdge; - if (this->mRight) return Qt::RightEdge; - } - - return static_cast(0); - } - [[nodiscard]] bool operator==(const Anchors& other) const noexcept { // clang-format off return this->mLeft == other.mLeft @@ -52,13 +33,35 @@ public: bool mBottom = false; }; -///! Panel exclusion mode -/// See @@PanelWindow.exclusionMode. +class Margins { + Q_GADGET; + Q_PROPERTY(qint32 left MEMBER mLeft); + Q_PROPERTY(qint32 right MEMBER mRight); + Q_PROPERTY(qint32 top MEMBER mTop); + Q_PROPERTY(qint32 bottom MEMBER mBottom); + QML_VALUE_TYPE(margins); + +public: + [[nodiscard]] bool operator==(const Margins& other) const noexcept { + // clang-format off + return this->mLeft == other.mLeft + && this->mRight == other.mRight + && this->mTop == other.mTop + && this->mBottom == other.mBottom; + // clang-format on + } + + qint32 mLeft = 0; + qint32 mRight = 0; + qint32 mTop = 0; + qint32 mBottom = 0; +}; + 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. @@ -108,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) {} @@ -142,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 e17c3bcf..00000000 --- a/src/core/paths.cpp +++ /dev/null @@ -1,425 +0,0 @@ -#include "paths.hpp" -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "instanceinfo.hpp" -#include "logcat.hpp" - -namespace { -QS_LOGGING_CATEGORY(logPaths, "quickshell.paths", QtWarningMsg); -} - -QsPaths* QsPaths::instance() { - static auto* instance = new QsPaths(); // NOLINT - return instance; -} - -void QsPaths::init(QString shellId, QString pathId, QString dataOverride, QString stateOverride) { - auto* instance = QsPaths::instance(); - instance->shellId = std::move(shellId); - instance->pathId = std::move(pathId); - instance->shellDataOverride = std::move(dataOverride); - instance->shellStateOverride = std::move(stateOverride); -} - -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::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; -} - -QDir* QsPaths::shellVfsDir() { - if (this->shellVfsState == DirState::Unknown) { - if (auto* baseRunDir = this->baseRunDir()) { - this->mShellVfsDir = QDir(baseRunDir->filePath("vfs")); - this->mShellVfsDir = QDir(this->mShellVfsDir.filePath(this->shellId)); - - qCDebug(logPaths) << "Initialized runtime vfs path:" << this->mShellVfsDir.path(); - - if (!this->mShellVfsDir.mkpath(".")) { - qCCritical(logPaths) << "Could not create runtime vfs directory at" - << this->mShellVfsDir.path(); - this->shellVfsState = DirState::Failed; - } else { - this->shellVfsState = DirState::Ready; - } - } else { - qCCritical(logPaths) << "Could not create shell runtime vfs path as it was not possible to " - "create the base runtime path."; - - this->shellVfsState = DirState::Failed; - } - } - - if (this->shellVfsState == DirState::Failed) return nullptr; - else return &this->mShellVfsDir; -} - -void QsPaths::linkRunDir() { - if (auto* runDir = this->instanceRunDir()) { - auto pidDir = QDir(this->baseRunDir()->filePath("by-pid")); - auto* shellDir = this->shellRunDir(); - - if (!shellDir) { - qCCritical(logPaths - ) << "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."; - } -} - -QDir QsPaths::shellDataDir() { - if (this->shellDataState == DirState::Unknown) { - QDir dir; - if (this->shellDataOverride.isEmpty()) { - dir = QDir(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation)); - dir = QDir(dir.filePath("by-shell")); - dir = QDir(dir.filePath(this->shellId)); - } else { - auto basedir = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation); - dir = QDir(this->shellDataOverride.replace("$BASE", basedir)); - } - - this->mShellDataDir = dir; - - qCDebug(logPaths) << "Initialized data path:" << dir.path(); - - if (!dir.mkpath(".")) { - qCCritical(logPaths) << "Could not create data directory at" << dir.path(); - - this->shellDataState = DirState::Failed; - } else { - this->shellDataState = DirState::Ready; - } - } - - // Returning no path on fail might result in files being written in unintended locations. - return this->mShellDataDir; -} - -QDir QsPaths::shellStateDir() { - if (this->shellStateState == DirState::Unknown) { -#if QT_VERSION < QT_VERSION_CHECK(6, 7, 0) - QDir dir; - if (qEnvironmentVariableIsSet("XDG_STATE_HOME")) { - dir = QDir(qEnvironmentVariable("XDG_STATE_HOME")); - } else { - auto home = QDir(QStandardPaths::writableLocation(QStandardPaths::HomeLocation)); - dir = QDir(home.filePath(".local/state")); - } - - if (this->shellStateOverride.isEmpty()) { - dir = QDir(dir.filePath("quickshell/by-shell")); - dir = QDir(dir.filePath(this->shellId)); - } else { - dir = QDir(this->shellStateOverride.replace("$BASE", dir.path())); - } -#else - QDir dir; - if (this->shellStateOverride.isEmpty()) { - dir = QDir(QStandardPaths::writableLocation(QStandardPaths::StateLocation)); - dir = QDir(dir.filePath("by-shell")); - dir = QDir(dir.filePath(this->shellId)); - } else { - auto basedir = QStandardPaths::writableLocation(QStandardPaths::GenericStateLocation); - dir = QDir(this->shellStateOverride.replace("$BASE", basedir)); - } -#endif - this->mShellStateDir = dir; - - qCDebug(logPaths) << "Initialized state path:" << dir.path(); - - if (!dir.mkpath(".")) { - qCCritical(logPaths) << "Could not create state directory at" << dir.path(); - - this->shellStateState = DirState::Failed; - } else { - this->shellStateState = DirState::Ready; - } - } - - // Returning no path on fail might result in files being written in unintended locations. - return this->mShellStateDir; -} - -QDir QsPaths::shellCacheDir() { - if (this->shellCacheState == DirState::Unknown) { - auto dir = QDir(QStandardPaths::writableLocation(QStandardPaths::CacheLocation)); - dir = QDir(dir.filePath("by-shell")); - dir = QDir(dir.filePath(this->shellId)); - this->mShellCacheDir = dir; - - qCDebug(logPaths) << "Initialized cache path:" << dir.path(); - - if (!dir.mkpath(".")) { - qCCritical(logPaths) << "Could not create cache directory at" << dir.path(); - - this->shellCacheState = DirState::Failed; - } else { - this->shellCacheState = DirState::Ready; - } - } - - // Returning no path on fail might result in files being written in unintended locations. - return this->mShellCacheDir; -} - -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, bool allowDead) { - 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 - auto isLocked = lock.l_type != F_UNLCK; - - if (!isLocked && !allowDead) return false; - - if (info) { - info->pid = isLocked ? lock.l_pid : -1; - - auto stream = QDataStream(&file); - stream >> info->instance; - } - - return true; -} - -QPair, QVector> -QsPaths::collectInstances(const QString& path) { - qCDebug(logPaths) << "Collecting instances from" << path; - auto liveInstances = QVector(); - auto deadInstances = 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, true)) { - qCDebug(logPaths).nospace() << "Found instance " << info.instance.instanceId << " (pid " - << info.pid << ") at " << path; - - if (info.pid == -1) { - deadInstances.push_back(info); - } else { - liveInstances.push_back(info); - } - } else { - qCDebug(logPaths) << "Skipped potential instance at" << path; - } - } - - return qMakePair(liveInstances, deadInstances); -} diff --git a/src/core/paths.hpp b/src/core/paths.hpp deleted file mode 100644 index 178bcda1..00000000 --- a/src/core/paths.hpp +++ /dev/null @@ -1,68 +0,0 @@ -#pragma once -#include -#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, QString dataOverride, QString stateOverride); - static QDir crashDir(const QString& id); - static QString basePath(const QString& id); - static QString ipcPath(const QString& id); - static bool - checkLock(const QString& path, InstanceLockInfo* info = nullptr, bool allowDead = false); - static QPair, QVector> - collectInstances(const QString& path); - - QDir* baseRunDir(); - QDir* shellRunDir(); - QDir* shellVfsDir(); - QDir* instanceRunDir(); - void linkRunDir(); - void linkPathDir(); - void createLock(); - - QDir shellDataDir(); - QDir shellStateDir(); - QDir shellCacheDir(); - -private: - enum class DirState : quint8 { - Unknown = 0, - Ready = 1, - Failed = 2, - }; - - QString shellId; - QString pathId; - QDir mBaseRunDir; - QDir mShellRunDir; - QDir mShellVfsDir; - QDir mInstanceRunDir; - DirState baseRunState = DirState::Unknown; - DirState shellRunState = DirState::Unknown; - DirState shellVfsState = DirState::Unknown; - DirState instanceRunState = DirState::Unknown; - - QDir mShellDataDir; - QDir mShellStateDir; - QDir mShellCacheDir; - DirState shellDataState = DirState::Unknown; - DirState shellStateState = DirState::Unknown; - DirState shellCacheState = DirState::Unknown; - - QString shellDataOverride; - QString shellStateOverride; -}; 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..21312de1 100644 --- a/src/core/plugin.cpp +++ b/src/core/plugin.cpp @@ -3,36 +3,31 @@ #include // NOLINT (what??) -#include "generation.hpp" +static QVector plugins; // NOLINT -static QVector plugins; // NOLINT +void QuickshellPlugin::registerPlugin(QuickshellPlugin& plugin) { plugins.push_back(&plugin); } -void QsEnginePlugin::registerPlugin(QsEnginePlugin& plugin) { plugins.push_back(&plugin); } +void QuickshellPlugin::initPlugins() { + plugins.erase( + std::remove_if( + plugins.begin(), + plugins.end(), + [](QuickshellPlugin* plugin) { return !plugin->applies(); } + ), + plugins.end() + ); -void QsEnginePlugin::initPlugins() { - plugins.removeIf([](QsEnginePlugin* plugin) { return !plugin->applies(); }); - - 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) { - 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..8a3719b1 100644 --- a/src/core/plugin.hpp +++ b/src/core/plugin.hpp @@ -2,30 +2,23 @@ #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 +26,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 bbcc3a5f..00000000 --- a/src/core/popupanchor.cpp +++ /dev/null @@ -1,386 +0,0 @@ -#include "popupanchor.hpp" -#include - -#include -#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(); } - -QWindow* PopupAnchor::backingWindow() const { - return this->mProxyWindow ? this->mProxyWindow->backingWindow() : nullptr; -} - -void PopupAnchor::setWindowInternal(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::setWindow(QObject* window) { - this->setItem(nullptr); - this->setWindowInternal(window); -} - -void PopupAnchor::setItem(QQuickItem* item) { - if (item == this->mItem) return; - - if (this->mItem) { - QObject::disconnect(this->mItem, nullptr, this, nullptr); - } - - this->mItem = item; - this->onItemWindowChanged(); - - if (item) { - QObject::connect(item, &QObject::destroyed, this, &PopupAnchor::onItemDestroyed); - QObject::connect(item, &QQuickItem::windowChanged, this, &PopupAnchor::onItemWindowChanged); - } -} - -void PopupAnchor::onWindowDestroyed() { - this->mWindow = nullptr; - this->mProxyWindow = nullptr; - emit this->windowChanged(); - emit this->backingWindowVisibilityChanged(); -} - -void PopupAnchor::onItemDestroyed() { - this->mItem = nullptr; - emit this->itemChanged(); - this->setWindowInternal(nullptr); -} - -void PopupAnchor::onItemWindowChanged() { - if (auto* window = qobject_cast(this->mItem->window())) { - this->setWindowInternal(window->proxy()); - } else { - this->setWindowInternal(nullptr); - } -} - -void PopupAnchor::setRect(Box rect) { - if (rect.w <= 0) rect.w = 1; - if (rect.h <= 0) rect.h = 1; - if (rect == this->mUserRect) return; - - this->mUserRect = rect; - emit this->rectChanged(); - - this->setWindowRect(rect.qrect().marginsRemoved(this->mMargins.qmargins())); -} - -void PopupAnchor::setMargins(Margins margins) { - if (margins == this->mMargins) return; - - this->mMargins = margins; - emit this->marginsChanged(); - - this->setWindowRect(this->mUserRect.qrect().marginsRemoved(margins.qmargins())); -} - -void PopupAnchor::setWindowRect(QRect rect) { - if (rect.width() <= 0) rect.setWidth(1); - if (rect.height() <= 0) rect.setHeight(1); - if (rect == this->state.rect) return; - - this->state.rect = rect; - emit this->windowRectChanged(); -} - -void PopupAnchor::resetRect() { this->mUserRect = Box(); } - -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(); -} - -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(); -} - -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; -} - -void PopupAnchor::updateAnchor() { - if (this->mItem && this->mProxyWindow) { - auto baseRect = - this->mUserRect.isEmpty() ? this->mItem->boundingRect() : this->mUserRect.qrect(); - - auto rect = this->mProxyWindow->contentItem()->mapFromItem( - this->mItem, - baseRect.marginsRemoved(this->mMargins.qmargins()) - ); - - if (rect.width() < 1) rect.setWidth(1); - if (rect.height() < 1) rect.setHeight(1); - - this->setWindowRect(rect.toRect()); - } - - emit this->anchoring(); -} - -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(); - - anchor->updateAnchor(); - 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->windowRect().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 a9b121ed..00000000 --- a/src/core/popupanchor.hpp +++ /dev/null @@ -1,214 +0,0 @@ -#pragma once - -#include - -#include -#include -#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; - - QRect 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. Setting this property unsets @@item. - Q_PROPERTY(QObject* window READ window WRITE setWindow NOTIFY windowChanged); - /// The item to anchor / attach the popup to. Setting this property unsets @@window. - /// - /// The popup's position relative to its parent window is only calculated when it is - /// initially shown (directly before @@anchoring(s) is emitted), meaning its anchor - /// rectangle will be set relative to the item's position in the window at that time. - /// @@updateAnchor() can be called to update the anchor rectangle if the item's position - /// has changed. - /// - /// > [!NOTE] If a more flexible way to position a popup relative to an item is needed, - /// > set @@window to the item's parent window, and handle the @@anchoring signal to - /// > position the popup relative to the window's contentItem. - Q_PROPERTY(QQuickItem* item READ item WRITE setItem NOTIFY itemChanged); - /// The anchorpoints the popup will attach to, relative to @@item or @@window. - /// Which anchors will be used is determined by the @@edges, @@gravity, and @@adjustment. - /// - /// If using @@item, the default anchor rectangle matches the dimensions of the item. - /// - /// 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. - /// - /// [coordinate mapping functions]: https://doc.qt.io/qt-6/qml-qtquick-item.html#mapFromItem-method - Q_PROPERTY(Box rect READ rect WRITE setRect RESET resetRect NOTIFY rectChanged); - /// A margin applied to the anchor rect. - /// - /// This is most useful when @@item is used and @@rect is left at its default - /// value (matching the Item's dimensions). - Q_PROPERTY(Margins margins READ margins WRITE setMargins NOTIFY marginsChanged); - /// 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) {} - - /// Update the popup's anchor rect relative to its parent window. - /// - /// If anchored to an item, popups anchors will not automatically follow - /// the item if its position changes. This function can be called to - /// recalculate the anchors. - Q_INVOKABLE void updateAnchor(); - - [[nodiscard]] bool isDirty() const; - void markClean(); - void markDirty(); - - [[nodiscard]] QObject* window() const { return this->mWindow; } - [[nodiscard]] ProxyWindowBase* proxyWindow() const { return this->mProxyWindow; } - [[nodiscard]] QWindow* backingWindow() const; - void setWindowInternal(QObject* window); - void setWindow(QObject* window); - - [[nodiscard]] QQuickItem* item() const { return this->mItem; } - void setItem(QQuickItem* item); - - [[nodiscard]] QRect windowRect() const { return this->state.rect; } - void setWindowRect(QRect rect); - - [[nodiscard]] Box rect() const { return this->mUserRect; } - void setRect(Box rect); - void resetRect(); - - [[nodiscard]] Margins margins() const { return this->mMargins; } - void setMargins(Margins margins); - - [[nodiscard]] Edges::Flags edges() const { return this->state.edges; } - void setEdges(Edges::Flags edges); - - [[nodiscard]] Edges::Flags gravity() const { return this->state.gravity; } - void setGravity(Edges::Flags gravity); - - [[nodiscard]] PopupAdjustment::Flags adjustment() const { return this->state.adjustment; } - 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(); - void itemChanged(); - QSDOC_HIDE void backingWindowVisibilityChanged(); - QSDOC_HIDE void windowRectChanged(); - void rectChanged(); - void marginsChanged(); - void edgesChanged(); - void gravityChanged(); - void adjustmentChanged(); - -private slots: - void onWindowDestroyed(); - void onItemDestroyed(); - void onItemWindowChanged(); - -private: - QObject* mWindow = nullptr; - QQuickItem* mItem = nullptr; - ProxyWindowBase* mProxyWindow = nullptr; - PopupAnchorState state; - Box mUserRect; - Margins mMargins; - 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/proxywindow.cpp b/src/core/proxywindow.cpp new file mode 100644 index 00000000..1c1796ad --- /dev/null +++ b/src/core/proxywindow.cpp @@ -0,0 +1,223 @@ +#include "proxywindow.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "qmlscreen.hpp" +#include "region.hpp" +#include "reload.hpp" + +ProxyWindowBase::ProxyWindowBase(QObject* parent) + : Reloadable(parent) + , mContentItem(new QQuickItem()) { + QQmlEngine::setObjectOwnership(this->mContentItem, QQmlEngine::CppOwnership); + this->mContentItem->setParent(this); + + QObject::connect(this, &ProxyWindowBase::widthChanged, this, &ProxyWindowBase::onWidthChanged); + QObject::connect(this, &ProxyWindowBase::heightChanged, this, &ProxyWindowBase::onHeightChanged); +} + +ProxyWindowBase::~ProxyWindowBase() { + if (this->window != nullptr) { + this->window->deleteLater(); + } +} + +void ProxyWindowBase::onReload(QObject* oldInstance) { + this->window = this->createWindow(oldInstance); + this->setupWindow(); + + Reloadable::reloadRecursive(this->mContentItem, oldInstance); + + this->mContentItem->setParentItem(this->window->contentItem()); + this->mContentItem->setWidth(this->width()); + this->mContentItem->setHeight(this->height()); + + // without this the dangling screen pointer wont be updated to a real screen + emit this->screenChanged(); + + emit this->windowConnected(); + this->window->setVisible(this->mVisible); +} + +QQuickWindow* ProxyWindowBase::createWindow(QObject* oldInstance) { + auto* old = qobject_cast(oldInstance); + + if (old == nullptr || old->window == nullptr) { + return new QQuickWindow(); + } else { + return old->disownWindow(); + } +} + +void ProxyWindowBase::setupWindow() { + // clang-format off + QObject::connect(this->window, &QWindow::visibilityChanged, this, &ProxyWindowBase::visibleChanged); + QObject::connect(this->window, &QWindow::widthChanged, this, &ProxyWindowBase::widthChanged); + QObject::connect(this->window, &QWindow::heightChanged, this, &ProxyWindowBase::heightChanged); + QObject::connect(this->window, &QWindow::screenChanged, this, &ProxyWindowBase::screenChanged); + QObject::connect(this->window, &QQuickWindow::colorChanged, this, &ProxyWindowBase::colorChanged); + + QObject::connect(this, &ProxyWindowBase::maskChanged, this, &ProxyWindowBase::onMaskChanged); + QObject::connect(this, &ProxyWindowBase::widthChanged, this, &ProxyWindowBase::onMaskChanged); + QObject::connect(this, &ProxyWindowBase::heightChanged, this, &ProxyWindowBase::onMaskChanged); + // clang-format on + + this->window->setScreen(this->mScreen); + this->setWidth(this->mWidth); + this->setHeight(this->mHeight); + this->setColor(this->mColor); + this->updateMask(); +} + +QQuickWindow* ProxyWindowBase::disownWindow() { + QObject::disconnect(this->window, nullptr, this, nullptr); + + this->mContentItem->setParentItem(nullptr); + + auto* window = this->window; + this->window = nullptr; + return window; +} + +QQuickWindow* ProxyWindowBase::backingWindow() const { return this->window; } +QQuickItem* ProxyWindowBase::contentItem() const { return this->mContentItem; } + +bool ProxyWindowBase::isVisible() const { + if (this->window == nullptr) return this->mVisible; + else return this->window->isVisible(); +} + +void ProxyWindowBase::setVisible(bool visible) { + if (this->window == nullptr) { + this->mVisible = visible; + emit this->visibleChanged(); + } else this->window->setVisible(visible); +} + +qint32 ProxyWindowBase::width() const { + if (this->window == nullptr) return this->mWidth; + else return this->window->width(); +} + +void ProxyWindowBase::setWidth(qint32 width) { + if (this->window == nullptr) { + this->mWidth = width; + emit this->widthChanged(); + } else this->window->setWidth(width); +} + +qint32 ProxyWindowBase::height() const { + if (this->window == nullptr) return this->mHeight; + else return this->window->height(); +} + +void ProxyWindowBase::setHeight(qint32 height) { + if (this->window == nullptr) { + this->mHeight = height; + emit this->heightChanged(); + } else this->window->setHeight(height); +} + +void ProxyWindowBase::setScreen(QuickshellScreenInfo* screen) { + if (this->mScreen != nullptr) { + QObject::disconnect(this->mScreen, nullptr, this, nullptr); + } + + auto* qscreen = screen == nullptr ? nullptr : screen->screen; + if (qscreen != nullptr) { + QObject::connect(qscreen, &QObject::destroyed, this, &ProxyWindowBase::onScreenDestroyed); + } + + if (this->window == nullptr) { + this->mScreen = qscreen; + emit this->screenChanged(); + } else this->window->setScreen(qscreen); +} + +void ProxyWindowBase::onScreenDestroyed() { this->mScreen = nullptr; } + +QuickshellScreenInfo* ProxyWindowBase::screen() const { + QScreen* qscreen = nullptr; + + if (this->window == nullptr) { + if (this->mScreen != nullptr) qscreen = this->mScreen; + } else { + qscreen = this->window->screen(); + } + + return new QuickshellScreenInfo( + const_cast(this), // NOLINT + 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; } + +void ProxyWindowBase::setMask(PendingRegion* mask) { + if (mask == this->mMask) return; + + if (this->mMask != nullptr) { + QObject::disconnect(this->mMask, nullptr, this, nullptr); + } + + this->mMask = mask; + + if (mask != nullptr) { + mask->setParent(this); + QObject::connect(mask, &QObject::destroyed, this, &ProxyWindowBase::onMaskDestroyed); + QObject::connect(mask, &PendingRegion::changed, this, &ProxyWindowBase::maskChanged); + } + + emit this->maskChanged(); +} + +void ProxyWindowBase::onMaskChanged() { + if (this->window != nullptr) this->updateMask(); +} + +void ProxyWindowBase::onMaskDestroyed() { + this->mMask = nullptr; + emit this->maskChanged(); +} + +void ProxyWindowBase::updateMask() { + 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->setMask(mask); +} + +QQmlListProperty ProxyWindowBase::data() { + return this->mContentItem->property("data").value>(); +} + +void ProxyWindowBase::onWidthChanged() { this->mContentItem->setWidth(this->width()); } +void ProxyWindowBase::onHeightChanged() { this->mContentItem->setHeight(this->height()); } diff --git a/src/core/proxywindow.hpp b/src/core/proxywindow.hpp new file mode 100644 index 00000000..accd434e --- /dev/null +++ b/src/core/proxywindow.hpp @@ -0,0 +1,115 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "qmlscreen.hpp" +#include "region.hpp" +#include "reload.hpp" +#include "windowinterface.hpp" + +// Proxy to an actual window exposing a limited property set with the ability to +// transfer it to a new window. + +///! Base class for reloadable windows +/// +/// [ShellWindow]: ../shellwindow +/// [FloatingWindow]: ../floatingwindow +class ProxyWindowBase: public Reloadable { + Q_OBJECT; + /// The QtQuick window backing this window. + /// + /// > [!WARNING] Do not expect values set via this property to work correctly. + /// > Values set this way will almost certainly misbehave across a reload, possibly + /// > even without one. + /// > + /// > Use **only** if you know what you are doing. + Q_PROPERTY(QQuickWindow* _backingWindow READ backingWindow); + Q_PROPERTY(QQuickItem* contentItem READ contentItem); + 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(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(QQmlListProperty data READ data); + Q_CLASSINFO("DefaultProperty", "data"); + +public: + explicit ProxyWindowBase(QObject* parent = nullptr); + ~ProxyWindowBase() override; + + ProxyWindowBase(ProxyWindowBase&) = delete; + ProxyWindowBase(ProxyWindowBase&&) = delete; + void operator=(ProxyWindowBase&) = delete; + void operator=(ProxyWindowBase&&) = delete; + + void onReload(QObject* oldInstance) override; + + virtual QQuickWindow* createWindow(QObject* oldInstance); + virtual void setupWindow(); + + // Disown the backing window and delete all its children. + virtual QQuickWindow* disownWindow(); + + [[nodiscard]] QQuickWindow* backingWindow() const; + [[nodiscard]] QQuickItem* contentItem() const; + + [[nodiscard]] virtual bool isVisible() const; + virtual void setVisible(bool visible); + + [[nodiscard]] virtual qint32 width() const; + virtual void setWidth(qint32 width); + + [[nodiscard]] virtual qint32 height() const; + virtual void setHeight(qint32 height); + + [[nodiscard]] virtual QuickshellScreenInfo* screen() const; + virtual void setScreen(QuickshellScreenInfo* screen); + + [[nodiscard]] QColor color() const; + virtual void setColor(QColor color); + + [[nodiscard]] PendingRegion* mask() const; + virtual void setMask(PendingRegion* mask); + + [[nodiscard]] QQmlListProperty data(); + +signals: + void windowConnected(); + void visibleChanged(); + void widthChanged(); + void heightChanged(); + void screenChanged(); + void colorChanged(); + void maskChanged(); + +protected slots: + virtual void onWidthChanged(); + virtual void onHeightChanged(); + void onMaskChanged(); + void onMaskDestroyed(); + void onScreenDestroyed(); + +protected: + bool mVisible = true; + qint32 mWidth = 100; + qint32 mHeight = 100; + QScreen* mScreen = nullptr; + QColor mColor = Qt::white; + PendingRegion* mMask = nullptr; + QQuickWindow* window = nullptr; + QQuickItem* mContentItem = nullptr; + +private: + void updateMask(); +}; diff --git a/src/core/qmlglobal.cpp b/src/core/qmlglobal.cpp index 07238f61..99f25631 100644 --- a/src/core/qmlglobal.cpp +++ b/src/core/qmlglobal.cpp @@ -1,44 +1,25 @@ #include "qmlglobal.hpp" #include -#include #include #include #include #include -#include #include -#include #include #include -#include #include #include #include -#include #include #include #include #include -#include #include -#include "../io/processcore.hpp" -#include "generation.hpp" -#include "iconimageprovider.hpp" -#include "paths.hpp" #include "qmlscreen.hpp" #include "rootwrapper.hpp" -QuickshellSettings::QuickshellSettings() { - QObject::connect( - static_cast(QGuiApplication::instance()), // NOLINT - &QGuiApplication::lastWindowClosed, - this, - &QuickshellSettings::lastWindowClosed - ); -} - QuickshellSettings* QuickshellSettings::instance() { static QuickshellSettings* instance = nullptr; // NOLINT if (instance == nullptr) { @@ -67,96 +48,37 @@ void QuickshellSettings::setWatchFiles(bool watchFiles) { emit this->watchFilesChanged(); } -QuickshellTracked::QuickshellTracked() { +QuickshellGlobal::QuickshellGlobal(QObject* parent): QObject(parent) { + // clang-format off + QObject::connect(QuickshellSettings::instance(), &QuickshellSettings::workingDirectoryChanged, this, &QuickshellGlobal::workingDirectoryChanged); + QObject::connect(QuickshellSettings::instance(), &QuickshellSettings::watchFilesChanged, this, &QuickshellGlobal::watchFilesChanged); + // clang-format on + auto* app = QCoreApplication::instance(); auto* guiApp = qobject_cast(app); if (guiApp != nullptr) { // clang-format off - QObject::connect(guiApp, &QGuiApplication::primaryScreenChanged, this, &QuickshellTracked::updateScreens); - QObject::connect(guiApp, &QGuiApplication::screenAdded, this, &QuickshellTracked::updateScreens); - QObject::connect(guiApp, &QGuiApplication::screenRemoved, this, &QuickshellTracked::updateScreens); + QObject::connect(guiApp, &QGuiApplication::primaryScreenChanged, this, &QuickshellGlobal::updateScreens); + QObject::connect(guiApp, &QGuiApplication::screenAdded, this, &QuickshellGlobal::updateScreens); + QObject::connect(guiApp, &QGuiApplication::screenRemoved, this, &QuickshellGlobal::updateScreens); // clang-format on this->updateScreens(); } } -QuickshellScreenInfo* QuickshellTracked::screenInfo(QScreen* screen) const { - for (auto* info: this->screens) { - if (info->screen == screen) return info; - } - - return nullptr; -} - -QuickshellTracked* QuickshellTracked::instance() { - static QuickshellTracked* instance = nullptr; // NOLINT - if (instance == nullptr) { - QJSEngine::setObjectOwnership(instance, QJSEngine::CppOwnership); - instance = new QuickshellTracked(); - } - return instance; -} - -void QuickshellTracked::updateScreens() { - auto screens = QGuiApplication::screens(); - auto newScreens = QList(); - - for (auto* newScreen: screens) { - for (auto i = 0; i < this->screens.length(); i++) { - auto* oldScreen = this->screens[i]; - if (newScreen == oldScreen->screen) { - newScreens.push_back(oldScreen); - this->screens.remove(i); - goto next; - } - } - - { - auto* si = new QuickshellScreenInfo(this, newScreen); - QQmlEngine::setObjectOwnership(si, QQmlEngine::CppOwnership); - newScreens.push_back(si); - } - next:; - } - - for (auto* oldScreen: this->screens) { - oldScreen->deleteLater(); - } - - this->screens = newScreens; - emit this->screensChanged(); -} - -QuickshellGlobal::QuickshellGlobal(QObject* parent): QObject(parent) { - // clang-format off - QObject::connect(QuickshellSettings::instance(), &QuickshellSettings::workingDirectoryChanged, this, &QuickshellGlobal::workingDirectoryChanged); - QObject::connect(QuickshellSettings::instance(), &QuickshellSettings::watchFilesChanged, this, &QuickshellGlobal::watchFilesChanged); - QObject::connect(QuickshellSettings::instance(), &QuickshellSettings::lastWindowClosed, this, &QuickshellGlobal::lastWindowClosed); - - QObject::connect(QuickshellTracked::instance(), &QuickshellTracked::screensChanged, this, &QuickshellGlobal::screensChanged); - // clang-format on - - QObject::connect( - static_cast(QGuiApplication::instance())->clipboard(), // NOLINT - &QClipboard::changed, - this, - &QuickshellGlobal::onClipboardChanged - ); -} - qint32 QuickshellGlobal::processId() const { // NOLINT return getpid(); } -qsizetype QuickshellGlobal::screensCount(QQmlListProperty* /*unused*/) { - return QuickshellTracked::instance()->screens.size(); +qsizetype QuickshellGlobal::screensCount(QQmlListProperty* prop) { + return static_cast(prop->object)->mScreens.size(); // NOLINT } QuickshellScreenInfo* -QuickshellGlobal::screenAt(QQmlListProperty* /*unused*/, qsizetype i) { - return QuickshellTracked::instance()->screens.at(i); +QuickshellGlobal::screenAt(QQmlListProperty* prop, qsizetype i) { + return static_cast(prop->object)->mScreens.at(i); // NOLINT } QQmlListProperty QuickshellGlobal::screens() { @@ -169,8 +91,8 @@ QQmlListProperty QuickshellGlobal::screens() { } void QuickshellGlobal::reload(bool hard) { - auto* generation = EngineGeneration::findObjectGeneration(this); - auto* root = generation == nullptr ? nullptr : generation->wrapper; + auto* rootobj = QQmlEngine::contextForObject(this)->engine()->parent(); + auto* root = qobject_cast(rootobj); if (root == nullptr) { qWarning() << "cannot find RootWrapper for reload, ignoring request"; @@ -196,68 +118,20 @@ void QuickshellGlobal::setWatchFiles(bool watchFiles) { // NOLINT QuickshellSettings::instance()->setWatchFiles(watchFiles); } -QString QuickshellGlobal::clipboardText() { - return static_cast(QGuiApplication::instance())->clipboard()->text(); // NOLINT -} +void QuickshellGlobal::updateScreens() { + auto screens = QGuiApplication::screens(); + this->mScreens.resize(screens.size()); -void QuickshellGlobal::setClipboardText(const QString& text) { - return static_cast(QGuiApplication::instance()) // NOLINT - ->clipboard() - ->setText(text); -} + for (auto i = 0; i < screens.size(); i++) { + if (this->mScreens[i] != nullptr) { + this->mScreens[i]->screen = nullptr; + this->mScreens[i]->setParent(nullptr); // delete if not owned by the js engine + } -void QuickshellGlobal::onClipboardChanged(QClipboard::Mode mode) { - if (mode == QClipboard::Clipboard) emit this->clipboardTextChanged(); -} + this->mScreens[i] = new QuickshellScreenInfo(this, screens[i]); + } -QString QuickshellGlobal::shellDir() const { - return EngineGeneration::findObjectGeneration(this)->rootPath.path(); -} - -QString QuickshellGlobal::configDir() const { - qWarning() << "Quickshell.configDir is deprecated and may be removed in a future release. Use " - "Quickshell.shellDir."; - return this->shellDir(); -} - -QString QuickshellGlobal::shellRoot() const { - qWarning() << "Quickshell.shellRoot is deprecated and may be removed in a future release. Use " - "Quickshell.shellDir."; - return this->shellDir(); -} - -QString QuickshellGlobal::dataDir() const { // NOLINT - return QsPaths::instance()->shellDataDir().path(); -} - -QString QuickshellGlobal::stateDir() const { // NOLINT - return QsPaths::instance()->shellStateDir().path(); -} - -QString QuickshellGlobal::cacheDir() const { // NOLINT - return QsPaths::instance()->shellCacheDir().path(); -} - -QString QuickshellGlobal::shellPath(const QString& path) const { - return this->shellDir() % '/' % path; -} - -QString QuickshellGlobal::configPath(const QString& path) const { - qWarning() << "Quickshell.configPath() is deprecated and may be removed in a future release. Use " - "Quickshell.shellPath()."; - return this->shellPath(path); -} - -QString QuickshellGlobal::dataPath(const QString& path) const { - return this->dataDir() % '/' % path; -} - -QString QuickshellGlobal::statePath(const QString& path) const { - return this->stateDir() % '/' % path; -} - -QString QuickshellGlobal::cachePath(const QString& path) const { - return this->cacheDir() % '/' % path; + emit this->screensChanged(); } QVariant QuickshellGlobal::env(const QString& variable) { // NOLINT @@ -266,60 +140,3 @@ QVariant QuickshellGlobal::env(const QString& variable) { // NOLINT return qEnvironmentVariable(vstr.data()); } - -void QuickshellGlobal::execDetached(QList command) { - QuickshellGlobal::execDetached(qs::io::process::ProcessContext(std::move(command))); -} - -void QuickshellGlobal::execDetached(const qs::io::process::ProcessContext& context) { - if (context.command.isEmpty()) { - qWarning() << "Cannot start process as command is empty."; - return; - } - - const auto& cmd = context.command.first(); - auto args = context.command.sliced(1); - - QProcess process; - qs::io::process::setupProcessEnvironment(&process, context.clearEnvironment, context.environment); - - if (!context.workingDirectory.isEmpty()) { - process.setWorkingDirectory(context.workingDirectory); - } - - process.setProgram(cmd); - process.setArguments(args); - - process.setStandardInputFile(QProcess::nullDevice()); - - if (context.unbindStdout) { - process.setStandardOutputFile(QProcess::nullDevice()); - process.setStandardErrorFile(QProcess::nullDevice()); - } - - process.startDetached(); -} - -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 9d88591e..8dceea14 100644 --- a/src/core/qmlglobal.hpp +++ b/src/core/qmlglobal.hpp @@ -1,22 +1,15 @@ #pragma once -#include #include -#include #include -#include #include #include #include #include -#include #include #include #include -#include -#include "../io/processcore.hpp" -#include "doc.hpp" #include "qmlscreen.hpp" ///! Accessor for some options under the Quickshell type. @@ -33,26 +26,16 @@ class QuickshellSettings: public QObject { QML_UNCREATABLE("singleton"); public: - QuickshellSettings(); - [[nodiscard]] QString workingDirectory() const; void setWorkingDirectory(QString workingDirectory); [[nodiscard]] bool watchFiles() const; void setWatchFiles(bool watchFiles); - [[nodiscard]] bool quitOnLastClosed() const; - void setQuitOnLastClosed(bool exitOnLastClosed); - static QuickshellSettings* instance(); static void reset(); signals: - /// Sent when the last window is closed. - /// - /// To make the application exit when the last window is closed run `Qt.quit()`. - void lastWindowClosed(); - void workingDirectoryChanged(); void watchFilesChanged(); @@ -60,24 +43,6 @@ private: bool mWatchFiles = true; }; -class QuickshellTracked: public QObject { - Q_OBJECT; - -public: - QuickshellTracked(); - - QVector screens; - QuickshellScreenInfo* screenInfo(QScreen* screen) const; - - static QuickshellTracked* instance(); - -private slots: - void updateScreens(); - -signals: - void screensChanged(); -}; - class QuickshellGlobal: public QObject { Q_OBJECT; // clang-format off @@ -91,12 +56,12 @@ class QuickshellGlobal: public QObject { /// ```qml /// ShellRoot { /// Variants { - /// // see Variants for details - /// variants: Quickshell.screens - /// PanelWindow { - /// property var modelData - /// screen: modelData + /// ShellWindow { + /// // ... /// } + /// + /// // see Variants for details + /// variants: Quickshell.screens.map(screen => ({ screen })) /// } /// } /// ``` @@ -104,42 +69,11 @@ 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 shellDir READ shellDir CONSTANT); - /// > [!WARNING] Deprecated: Renamed to @@shellDir for clarity. - Q_PROPERTY(QString configDir READ configDir CONSTANT); - /// > [!WARNING] Deprecated: Renamed to @@shellDir for consistency. - Q_PROPERTY(QString shellRoot READ shellRoot CONSTANT); /// Quickshell's working directory. Defaults to whereever quickshell was launched from. Q_PROPERTY(QString workingDirectory READ workingDirectory WRITE setWorkingDirectory NOTIFY workingDirectoryChanged); /// If true then the configuration will be reloaded whenever any files change. /// Defaults to true. Q_PROPERTY(bool watchFiles READ watchFiles WRITE setWatchFiles NOTIFY watchFilesChanged); - /// The system clipboard. - /// - /// > [!WARNING] Under wayland the clipboard will be empty unless a quickshell window is focused. - Q_PROPERTY(QString clipboardText READ clipboardText WRITE setClipboardText NOTIFY clipboardTextChanged); - /// The per-shell data directory. - /// - /// Usually `~/.local/share/quickshell/by-shell/` - /// - /// Can be overridden using `//@ pragma DataDir $BASE/path` in the root qml file, where `$BASE` - /// corrosponds to `$XDG_DATA_HOME` (usually `~/.local/share`). - Q_PROPERTY(QString dataDir READ dataDir CONSTANT); - /// The per-shell state directory. - /// - /// Usually `~/.local/state/quickshell/by-shell/` - /// - /// Can be overridden using `//@ pragma StateDir $BASE/path` in the root qml file, where `$BASE` - /// corrosponds to `$XDG_STATE_HOME` (usually `~/.local/state`). - Q_PROPERTY(QString stateDir READ stateDir CONSTANT); - /// The per-shell cache directory. - /// - /// Usually `~/.cache/quickshell/by-shell/` - Q_PROPERTY(QString cacheDir READ cacheDir CONSTANT); // clang-format on QML_SINGLETON; QML_NAMED_ELEMENT(Quickshell); @@ -147,119 +81,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); - // MUST be before execDetached(ctx) or the other will be called with a default constructed obj. - QSDOC_HIDE Q_INVOKABLE static void execDetached(QList command); - /// Launch a process detached from Quickshell. - /// - /// The context parameter can either be a list of command arguments or a JS object with the following fields: - /// - `command`: A list containing the command and all its arguments. See @@Quickshell.Io.Process.command. - /// - `environment`: Changes to make to the process environment. See @@Quickshell.Io.Process.environment. - /// - `clearEnvironment`: Removes all variables from the environment if true. - /// - `workingDirectory`: The working directory the command should run in. - /// - /// > [!WARNING] This does not run command in a shell. All arguments to the command - /// > must be in separate values in the list, e.g. `["echo", "hello"]` - /// > and not `["echo hello"]`. - /// > - /// > Additionally, shell scripts must be run by your shell, - /// > e.g. `["sh", "script.sh"]` instead of `["script.sh"]` unless the script - /// > has a shebang. - /// - /// > [!INFO] You can use `["sh", "-c", ]` to execute your command with - /// > the system shell. - /// - /// This function is equivalent to @@Quickshell.Io.Process.startDetached(). - Q_INVOKABLE static void execDetached(const qs::io::process::ProcessContext& context); - - /// 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); - /// Equivalent to `${Quickshell.configDir}/${path}` - Q_INVOKABLE [[nodiscard]] QString shellPath(const QString& path) const; - /// > [!WARNING] Deprecated: Renamed to @@shellPath() for clarity. - Q_INVOKABLE [[nodiscard]] QString configPath(const QString& path) const; - /// Equivalent to `${Quickshell.dataDir}/${path}` - Q_INVOKABLE [[nodiscard]] QString dataPath(const QString& path) const; - /// Equivalent to `${Quickshell.stateDir}/${path}` - Q_INVOKABLE [[nodiscard]] QString statePath(const QString& path) const; - /// Equivalent to `${Quickshell.cacheDir}/${path}` - Q_INVOKABLE [[nodiscard]] QString cachePath(const QString& path) const; - /// When called from @@reloadCompleted() or @@reloadFailed(), prevents the - /// default reload popup from displaying. - /// - /// The popup can also be blocked by setting `QS_NO_RELOAD_POPUP=1`. - Q_INVOKABLE void inhibitReloadPopup() { this->mInhibitReloadPopup = true; } - - void clearReloadPopupInhibit() { this->mInhibitReloadPopup = false; } - [[nodiscard]] bool isReloadPopupInhibited() const { return this->mInhibitReloadPopup; } - - [[nodiscard]] QString shellDir() const; - [[nodiscard]] QString configDir() const; - [[nodiscard]] QString shellRoot() const; - [[nodiscard]] QString workingDirectory() const; void setWorkingDirectory(QString workingDirectory); [[nodiscard]] bool watchFiles() const; void setWatchFiles(bool watchFiles); - [[nodiscard]] static QString clipboardText(); - static void setClipboardText(const QString& text); - - [[nodiscard]] QString dataDir() const; - [[nodiscard]] QString stateDir() const; - [[nodiscard]] QString cacheDir() const; - - 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(); - void clipboardTextChanged(); -private slots: - void onClipboardChanged(QClipboard::Mode mode); +public slots: + void updateScreens(); private: - QuickshellGlobal(QObject* parent = nullptr); - - bool mInhibitReloadPopup = false; - static qsizetype screensCount(QQmlListProperty* prop); static QuickshellScreenInfo* screenAt(QQmlListProperty* prop, qsizetype i); + + QVector mScreens; }; diff --git a/src/core/qmlscreen.cpp b/src/core/qmlscreen.cpp index 105b4f01..05e01185 100644 --- a/src/core/qmlscreen.cpp +++ b/src/core/qmlscreen.cpp @@ -42,42 +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(); - return 0; - } - - return this->screen->geometry().x(); -} - -qint32 QuickshellScreenInfo::y() const { - if (this->screen == nullptr) { - this->warnDangling(); - return 0; - } - - return this->screen->geometry().y(); -} - qint32 QuickshellScreenInfo::width() const { if (this->screen == nullptr) { this->warnDangling(); @@ -145,21 +109,3 @@ void QuickshellScreenInfo::screenDestroyed() { this->screen = nullptr; this->dangling = true; } - -QDebug operator<<(QDebug debug, const QuickshellScreenInfo* screen) { - if (screen == nullptr) { - debug.nospace() << "QuickshellScreenInfo(nullptr)"; - return debug; - } - - debug.nospace() << screen->metaObject()->className() << '(' << static_cast(screen) - << ", screen=" << screen->screen << ')'; - - return debug; -} - -QString QuickshellScreenInfo::toString() const { - QString str; - QDebug(&str) << this; - return str; -} diff --git a/src/core/qmlscreen.hpp b/src/core/qmlscreen.hpp index 5e978bc0..e499dfae 100644 --- a/src/core/qmlscreen.hpp +++ b/src/core/qmlscreen.hpp @@ -1,25 +1,25 @@ #pragma once -#include #include #include -#include #include #include -#include #include #include // 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,12 +29,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); Q_PROPERTY(qint32 height READ height NOTIFY geometryChanged); /// The number of physical pixels per millimeter. @@ -44,7 +38,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,10 +47,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; [[nodiscard]] qint32 height() const; [[nodiscard]] qreal physicalPixelDensity() const; @@ -65,8 +55,6 @@ public: [[nodiscard]] Qt::ScreenOrientation orientation() const; [[nodiscard]] Qt::ScreenOrientation primaryOrientation() const; - [[nodiscard]] Q_INVOKABLE QString toString() const; - QScreen* screen; private: @@ -83,5 +71,3 @@ signals: private slots: void screenDestroyed(); }; - -QDebug operator<<(QDebug debug, const QuickshellScreenInfo* screen); diff --git a/src/core/qsintercept.cpp b/src/core/qsintercept.cpp deleted file mode 100644 index 6687681b..00000000 --- a/src/core/qsintercept.cpp +++ /dev/null @@ -1,131 +0,0 @@ -#include "qsintercept.hpp" -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "logcat.hpp" - -QS_LOGGING_CATEGORY(logQsIntercept, "quickshell.interceptor", QtWarningMsg); - -QUrl QsUrlInterceptor::intercept( - const QUrl& originalUrl, - QQmlAbstractUrlInterceptor::DataType type -) { - auto url = originalUrl; - - if (url.scheme() == "root") { - url.setScheme("qs"); - - auto path = url.path(); - if (path.startsWith('/')) path = path.sliced(1); - url.setPath("@/qs/" % path); - - qCDebug(logQsIntercept) << "Rewrote root intercept" << originalUrl << "to" << url; - } - - if (url.scheme() == "qs") { - auto path = url.path(); - - // Our import path is on "qs:@/". - // We want to blackhole any import resolution outside of the config folder as it breaks Qt - // but NOT file lookups that might be on "qs:/" due to a missing "file:/" prefix. - if (path.startsWith("@/qs/")) { - path = this->configRoot.filePath(path.sliced(5)); - } else if (!path.startsWith("/")) { - qCDebug(logQsIntercept) << "Blackholed import URL" << url; - return QUrl("qrc:/qs-blackhole"); - } - - // Some types such as Image take into account where they are loading from, and force - // asynchronous loading over a network. qs: is considered to be over a network. - // In those cases we want to return a file:// url so asynchronous loading is not forced. - if (type == QQmlAbstractUrlInterceptor::DataType::UrlString) { - // Qt.resolvedUrl and context->resolvedUrl can use this on qml files, in which - // case we want to keep the intercept, otherwise objects created from those paths - // will not be able to use singletons. - if (path.endsWith(".qml")) return url; - - auto newUrl = url; - newUrl.setScheme("file"); - // above check asserts path starts with /qs/ - newUrl.setPath(path); - qCDebug(logQsIntercept) << "Rewrote intercept" << url << "to" << newUrl; - return newUrl; - } - } - - return url; -} - -QsInterceptDataReply::QsInterceptDataReply(const QString& data, QObject* parent) - : QNetworkReply(parent) - , content(data.toUtf8()) { - this->setOpenMode(QIODevice::ReadOnly); - this->setFinished(true); -} - -qint64 QsInterceptDataReply::readData(char* data, qint64 maxSize) { - auto size = qMin(maxSize, this->content.length() - this->offset); - if (size == 0) return -1; - memcpy(data, this->content.constData() + this->offset, size); // NOLINT - this->offset += size; - return size; -} - -QsInterceptNetworkAccessManager::QsInterceptNetworkAccessManager( - const QDir& configRoot, - const QHash& fileIntercepts, - QObject* parent -) - : QNetworkAccessManager(parent) - , configRoot(configRoot) - , fileIntercepts(fileIntercepts) {} - -QNetworkReply* QsInterceptNetworkAccessManager::createRequest( - QNetworkAccessManager::Operation op, - const QNetworkRequest& req, - QIODevice* outgoingData -) { - auto url = req.url(); - - if (url.scheme() == "qs") { - auto path = url.path(); - - if (path.startsWith("@/qs/")) path = this->configRoot.filePath(path.sliced(5)); - // otherwise pass through to fs - - qCDebug(logQsIntercept) << "Got intercept for" << path << "contains" - << this->fileIntercepts.value(path); - - if (auto data = this->fileIntercepts.value(path); !data.isEmpty()) { - return new QsInterceptDataReply(data, this); - } - - auto fileReq = req; - auto fileUrl = req.url(); - fileUrl.setScheme("file"); - fileUrl.setPath(path); - qCDebug(logQsIntercept) << "Passing through intercept" << url << "to" << fileUrl; - - fileReq.setUrl(fileUrl); - return this->QNetworkAccessManager::createRequest(op, fileReq, outgoingData); - } - - return this->QNetworkAccessManager::createRequest(op, req, outgoingData); -} - -QNetworkAccessManager* QsInterceptNetworkAccessManagerFactory::create(QObject* parent) { - return new QsInterceptNetworkAccessManager(this->configRoot, this->fileIntercepts, parent); -} diff --git a/src/core/qsintercept.hpp b/src/core/qsintercept.hpp deleted file mode 100644 index c3d8b552..00000000 --- a/src/core/qsintercept.hpp +++ /dev/null @@ -1,78 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "logcat.hpp" - -QS_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; -}; - -class QsInterceptDataReply: public QNetworkReply { - Q_OBJECT; - -public: - QsInterceptDataReply(const QString& data, QObject* parent = nullptr); - - qint64 readData(char* data, qint64 maxSize) override; - -private slots: - void abort() override {} - -private: - qint64 offset = 0; - QByteArray content; -}; - -class QsInterceptNetworkAccessManager: public QNetworkAccessManager { - Q_OBJECT; - -public: - QsInterceptNetworkAccessManager( - const QDir& configRoot, - const QHash& fileIntercepts, - QObject* parent = nullptr - ); - -protected: - QNetworkReply* createRequest( - QNetworkAccessManager::Operation op, - const QNetworkRequest& req, - QIODevice* outgoingData = nullptr - ) override; - -private: - QDir configRoot; - const QHash& fileIntercepts; -}; - -class QsInterceptNetworkAccessManagerFactory: public QQmlNetworkAccessManagerFactory { -public: - QsInterceptNetworkAccessManagerFactory( - const QDir& configRoot, - const QHash& fileIntercepts - ) - : configRoot(configRoot) - , fileIntercepts(fileIntercepts) {} - QNetworkAccessManager* create(QObject* parent) override; - -private: - QDir configRoot; - const QHash& fileIntercepts; -}; 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 90df8b9a..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 11892d6d..9826dbd5 100644 --- a/src/core/region.cpp +++ b/src/core/region.cpp @@ -7,8 +7,6 @@ #include #include #include -#include -#include PendingRegion::PendingRegion(QObject* parent): QObject(parent) { QObject::connect(this, &PendingRegion::shapeChanged, this, &PendingRegion::changed); @@ -22,39 +20,30 @@ PendingRegion::PendingRegion(QObject* parent): QObject(parent) { } void PendingRegion::setItem(QQuickItem* item) { - if (item == this->mItem) return; - if (this->mItem != nullptr) { QObject::disconnect(this->mItem, nullptr, this, nullptr); } this->mItem = item; - if (item != nullptr) { - QObject::connect(this->mItem, &QObject::destroyed, this, &PendingRegion::onItemDestroyed); - QObject::connect(this->mItem, &QQuickItem::xChanged, this, &PendingRegion::itemChanged); - QObject::connect(this->mItem, &QQuickItem::yChanged, this, &PendingRegion::itemChanged); - QObject::connect(this->mItem, &QQuickItem::widthChanged, this, &PendingRegion::itemChanged); - QObject::connect(this->mItem, &QQuickItem::heightChanged, this, &PendingRegion::itemChanged); - } - - emit this->itemChanged(); + QObject::connect(this->mItem, &QQuickItem::xChanged, this, &PendingRegion::itemChanged); + QObject::connect(this->mItem, &QQuickItem::yChanged, this, &PendingRegion::itemChanged); + QObject::connect(this->mItem, &QQuickItem::widthChanged, this, &PendingRegion::itemChanged); + QObject::connect(this->mItem, &QQuickItem::heightChanged, this, &PendingRegion::itemChanged); } void PendingRegion::onItemDestroyed() { this->mItem = nullptr; } -void PendingRegion::onChildDestroyed() { this->mRegions.removeAll(this->sender()); } - QQmlListProperty PendingRegion::regions() { return QQmlListProperty( this, nullptr, - &PendingRegion::regionsAppend, - &PendingRegion::regionsCount, - &PendingRegion::regionAt, - &PendingRegion::regionsClear, - &PendingRegion::regionsReplace, - &PendingRegion::regionsRemoveLast + PendingRegion::regionsAppend, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr ); } @@ -108,67 +97,11 @@ 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); - + region->setParent(self); self->mRegions.append(region); - emit self->childrenChanged(); -} - -PendingRegion* PendingRegion::regionAt(QQmlListProperty* prop, qsizetype i) { - return static_cast(prop->object)->mRegions.at(i); // NOLINT -} - -void PendingRegion::regionsClear(QQmlListProperty* prop) { - auto* self = static_cast(prop->object); // NOLINT - - for (auto* region: self->mRegions) { - QObject::disconnect(region, nullptr, self, nullptr); - } - - self->mRegions.clear(); // NOLINT - emit self->childrenChanged(); -} - -qsizetype PendingRegion::regionsCount(QQmlListProperty* prop) { - return static_cast(prop->object)->mRegions.length(); // NOLINT -} - -void PendingRegion::regionsRemoveLast(QQmlListProperty* prop) { - auto* self = static_cast(prop->object); // NOLINT - - auto* last = self->mRegions.last(); - if (last != nullptr) QObject::disconnect(last, nullptr, self, nullptr); - - self->mRegions.removeLast(); - emit self->childrenChanged(); -} - -void PendingRegion::regionsReplace( - QQmlListProperty* prop, - qsizetype i, - PendingRegion* region -) { - auto* self = static_cast(prop->object); // NOLINT - - auto* old = self->mRegions.at(i); - if (old != nullptr) QObject::disconnect(old, nullptr, self, nullptr); - - self->mRegions.replace(i, region); + QObject::connect(region, &PendingRegion::changed, self, &PendingRegion::childrenChanged); emit self->childrenChanged(); } diff --git a/src/core/region.hpp b/src/core/region.hpp index 6637d7bd..06654ca9 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,25 +106,13 @@ 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: void onItemDestroyed(); - void onChildDestroyed(); private: static void regionsAppend(QQmlListProperty* prop, PendingRegion* region); - static PendingRegion* regionAt(QQmlListProperty* prop, qsizetype i); - static void regionsClear(QQmlListProperty* prop); - static qsizetype regionsCount(QQmlListProperty* prop); - static void regionsRemoveLast(QQmlListProperty* prop); - static void - regionsReplace(QQmlListProperty* prop, qsizetype i, PendingRegion* region); QQuickItem* mItem = nullptr; diff --git a/src/core/reload.cpp b/src/core/reload.cpp index ea2abbf6..8768dc74 100644 --- a/src/core/reload.cpp +++ b/src/core/reload.cpp @@ -3,56 +3,6 @@ #include #include #include -#include - -#include "generation.hpp" - -void Reloadable::componentComplete() { - this->engineGeneration = EngineGeneration::findObjectGeneration(this); - - 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 { - QObject::connect( - this->engineGeneration, - &EngineGeneration::reloadFinished, - this, - &Reloadable::onReloadFinished - ); - } - } -} - -void Reloadable::reload(QObject* oldInstance) { - if (this->reloadComplete) return; - this->onReload(oldInstance); - this->reloadComplete = true; - - if (this->engineGeneration != nullptr) { - QObject::disconnect( - this->engineGeneration, - &EngineGeneration::reloadFinished, - this, - &Reloadable::onReloadFinished - ); - } -} - -void Reloadable::onReloadFinished() { this->reload(nullptr); } -void Reloadable::onGenerationDestroyed() { this->engineGeneration = nullptr; } void ReloadPropagator::onReload(QObject* oldInstance) { auto* old = qobject_cast(oldInstance); @@ -63,7 +13,7 @@ void ReloadPropagator::onReload(QObject* oldInstance) { auto* oldChild = old == nullptr || old->mChildren.length() <= i ? nullptr : qobject_cast(old->mChildren.at(i)); - newChild->reload(oldChild); + newChild->onReload(oldChild); } else { Reloadable::reloadRecursive(newChild, oldInstance); } @@ -99,7 +49,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); } @@ -126,21 +76,12 @@ QObject* Reloadable::getChildByReloadId(QObject* parent, const QString& reloadId return nullptr; } -void PostReloadHook::componentComplete() { - auto* engineGeneration = EngineGeneration::findObjectGeneration(this); - if (!engineGeneration || engineGeneration->reloadComplete) this->postReload(); - else { - // disconnected by EngineGeneration::postReload - QObject::connect( - engineGeneration, - &EngineGeneration::firePostReload, - this, - &PostReloadHook::postReload - ); +void PostReloadHook::postReloadTree(QObject* root) { + for (auto* child: root->children()) { + PostReloadHook::postReloadTree(child); + } + + if (auto* self = dynamic_cast(root)) { + self->onPostReload(); } } - -void PostReloadHook::postReload() { - this->isPostReload = true; - this->onPostReload(); -} diff --git a/src/core/reload.hpp b/src/core/reload.hpp index ae5d7c92..2ae459a3 100644 --- a/src/core/reload.hpp +++ b/src/core/reload.hpp @@ -7,11 +7,12 @@ #include #include -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 +26,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 @@ -55,10 +56,14 @@ class Reloadable public: explicit Reloadable(QObject* parent = nullptr): QObject(parent) {} - void reload(QObject* oldInstance = nullptr); + // Called unconditionally in the reload phase, with nullptr if no source could be determined. + // If non null the old instance may or may not be of the same type, and should be checked + // by `onReload`. + virtual void onReload(QObject* oldInstance) = 0; + // TODO: onReload runs after initialization for reloadable objects created late void classBegin() override {} - void componentComplete() override; + void componentComplete() override {} // Reload objects in the parent->child graph recursively. static void reloadRecursive(QObject* newObj, QObject* oldRoot); @@ -66,27 +71,16 @@ public: static void reloadChildrenRecursive(QObject* newRoot, QObject* oldRoot); QString mReloadableId; - bool reloadComplete = false; - EngineGeneration* engineGeneration = nullptr; - -private slots: - void onReloadFinished(); - void onGenerationDestroyed(); - -protected: - // Called unconditionally in the reload phase, with nullptr if no source could be determined. - // If non null the old instance may or may not be of the same type, and should be checked - // by `onReload`. - virtual void onReload(QObject* oldInstance) = 0; private: static QObject* getChildByReloadId(QObject* parent, const QString& reloadId); }; ///! 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 { @@ -119,23 +113,16 @@ private: }; /// Hook that runs after the old widget tree is dropped during a reload. -class PostReloadHook - : public QObject - , public QQmlParserStatus { - Q_OBJECT; - QML_ANONYMOUS; - Q_INTERFACES(QQmlParserStatus); - +class PostReloadHook { public: - PostReloadHook(QObject* parent = nullptr): QObject(parent) {} - void classBegin() override {} - void componentComplete() override; + PostReloadHook() = default; + virtual ~PostReloadHook() = default; + PostReloadHook(PostReloadHook&&) = default; + PostReloadHook(const PostReloadHook&) = default; + PostReloadHook& operator=(PostReloadHook&&) = default; + PostReloadHook& operator=(const PostReloadHook&) = default; virtual void onPostReload() = 0; -public slots: - void postReload(); - -protected: - bool isPostReload = false; + static void postReloadTree(QObject* root); }; 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 25c46ccd..16941f5a 100644 --- a/src/core/rootwrapper.cpp +++ b/src/core/rootwrapper.cpp @@ -2,190 +2,114 @@ #include #include +#include #include #include -#include #include #include #include #include -#include -#include +#include #include -#include "../ui/reload_popup.hpp" -#include "../window/floatingwindow.hpp" -#include "generation.hpp" -#include "instanceinfo.hpp" +#include "plugin.hpp" #include "qmlglobal.hpp" -#include "scan.hpp" -#include "toolsupport.hpp" +#include "reload.hpp" +#include "shell.hpp" +#include "watcher.hpp" -RootWrapper::RootWrapper(QString rootPath, QString shellId) +RootWrapper::RootWrapper(QString rootPath) : QObject(nullptr) , rootPath(std::move(rootPath)) - , shellId(std::move(shellId)) + , engine(this) , originalWorkingDirectory(QDir::current().absolutePath()) { - QObject::connect( - QuickshellSettings::instance(), - &QuickshellSettings::watchFilesChanged, - this, - &RootWrapper::onWatchFilesChanged - ); + auto* app = QCoreApplication::instance(); + QObject::connect(&this->engine, &QQmlEngine::quit, app, &QCoreApplication::quit); + QObject::connect(&this->engine, &QQmlEngine::exit, app, &QCoreApplication::exit); - QObject::connect( - &this->configDirWatcher, - &QFileSystemWatcher::directoryChanged, - this, - &RootWrapper::updateTooling - ); + // clang-format off + QObject::connect(QuickshellSettings::instance(), &QuickshellSettings::watchFilesChanged, this, &RootWrapper::onWatchFilesChanged); + // clang-format on this->reloadGraph(true); - if (this->generation == nullptr) { + if (this->root == nullptr) { + qCritical() << "could not create scene graph, exiting"; exit(-1); // NOLINT } } RootWrapper::~RootWrapper() { // event loop may no longer be running so deleteLater is not an option - if (this->generation != nullptr) { - this->generation->shutdown(); - } + delete this->root; } void RootWrapper::reloadGraph(bool hard) { - auto rootFile = QFileInfo(this->rootPath); - auto rootPath = rootFile.dir(); - auto scanner = QmlScanner(rootPath); - scanner.scanQmlRoot(this->rootPath); - - qs::core::QmlToolingSupport::updateTooling(rootPath, scanner); - this->configDirWatcher.addPath(rootPath.path()); - - auto* generation = new EngineGeneration(rootPath, std::move(scanner)); - generation->wrapper = this; - - // todo: move into EngineGeneration - if (this->generation != nullptr) { - qInfo() << "Reloading configuration..."; + if (this->root != nullptr) { QuickshellSettings::reset(); + this->engine.clearComponentCache(); } QDir::setCurrent(this->originalWorkingDirectory); - QUrl url; - url.setScheme("qs"); - url.setPath("@/qs/" % rootFile.fileName()); - auto component = QQmlComponent(generation->engine, url); + auto component = QQmlComponent(&this->engine, QUrl::fromLocalFile(this->rootPath)); - if (!component.isReady()) { - qCritical() << "Failed to load configuration"; - QString errorString = "Failed to load configuration"; - - auto errors = component.errors(); - for (auto& error: errors) { - const auto& url = error.url(); - auto rel = url.scheme() == "qs" && url.path().startsWith("@/qs/") ? "@" % url.path().sliced(5) - : url.toString(); - auto msg = " caused by " % rel % '[' % QString::number(error.line()) % ':' - % QString::number(error.column()) % "]: " % error.description(); - errorString += '\n' % msg; - qCritical().noquote() << msg; - } - - auto newFiles = generation->scanner.scannedFiles; - generation->destroy(); - - if (this->generation != nullptr) { - if (this->generation->setExtraWatchedFiles(newFiles)) { - qInfo() << "Watching additional files picked up in reload for changes..."; - } - - auto showPopup = true; - if (this->generation->qsgInstance != nullptr) { - this->generation->qsgInstance->clearReloadPopupInhibit(); - emit this->generation->qsgInstance->reloadFailed(errorString); - showPopup = !this->generation->qsgInstance->isReloadPopupInhibited(); - } - - if (showPopup) - qs::ui::ReloadPopup::spawnPopup(InstanceInfo::CURRENT.instanceId, true, errorString); - } - - if (this->generation != nullptr && this->generation->qsgInstance != nullptr) { - emit this->generation->qsgInstance->reloadFailed(errorString); - } + auto* obj = component.beginCreate(this->engine.rootContext()); + if (obj == nullptr) { + qWarning() << component.errorString().toStdString().c_str(); + qWarning() << "failed to create root component"; return; } - auto* newRoot = component.beginCreate(generation->engine->rootContext()); - - 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; + return; } - generation->root = newRoot; - component.completeCreate(); - if (this->generation) { - QObject::disconnect(this->generation, nullptr, this, nullptr); + auto* oldRoot = this->root; + this->root = newRoot; + + this->root->onReload(hard ? nullptr : oldRoot); + + if (oldRoot != nullptr) { + oldRoot->deleteLater(); + + QTimer::singleShot(0, [this, newRoot]() { + if (this->root == newRoot) { + QuickshellPlugin::runOnReload(); + PostReloadHook::postReloadTree(this->root); + } + }); + } else { + PostReloadHook::postReloadTree(newRoot); + QuickshellPlugin::runOnReload(); } - auto isReload = this->generation != nullptr; - generation->onReload(hard ? nullptr : this->generation); - - if (hard && this->generation) { - this->generation->destroy(); - } - - this->generation = generation; - - qInfo() << "Configuration Loaded"; - - QObject::connect(this->generation, &QObject::destroyed, this, &RootWrapper::generationDestroyed); - QObject::connect( - this->generation, - &EngineGeneration::filesChanged, - this, - &RootWrapper::onWatchedFilesChanged - ); - this->onWatchFilesChanged(); - - if (isReload) { - auto showPopup = true; - - if (this->generation->qsgInstance != nullptr) { - this->generation->qsgInstance->clearReloadPopupInhibit(); - emit this->generation->qsgInstance->reloadCompleted(); - showPopup = !this->generation->qsgInstance->isReloadPopupInhibited(); - } - - if (showPopup) qs::ui::ReloadPopup::spawnPopup(InstanceInfo::CURRENT.instanceId, false, ""); - } } -void RootWrapper::generationDestroyed() { this->generation = nullptr; } - void RootWrapper::onWatchFilesChanged() { auto watchFiles = QuickshellSettings::instance()->watchFiles(); - if (this->generation != nullptr) { - this->generation->setWatchingFiles(watchFiles); + + if (watchFiles && this->configWatcher == nullptr) { + this->configWatcher = new FiletreeWatcher(); + this->configWatcher->addPath(QFileInfo(this->rootPath).dir().path()); + + QObject::connect( + this->configWatcher, + &FiletreeWatcher::fileChanged, + this, + &RootWrapper::onWatchedFilesChanged + ); + } else if (!watchFiles && this->configWatcher != nullptr) { + this->configWatcher->deleteLater(); + this->configWatcher = nullptr; } } void RootWrapper::onWatchedFilesChanged() { this->reloadGraph(false); } - -void RootWrapper::updateTooling() { - if (!this->generation) return; - auto configDir = QFileInfo(this->rootPath).dir(); - qs::core::QmlToolingSupport::updateTooling(configDir, this->generation->scanner); -} diff --git a/src/core/rootwrapper.hpp b/src/core/rootwrapper.hpp index 1425d177..6174c7b1 100644 --- a/src/core/rootwrapper.hpp +++ b/src/core/rootwrapper.hpp @@ -1,34 +1,32 @@ #pragma once -#include #include #include #include #include #include -#include "generation.hpp" +#include "shell.hpp" +#include "watcher.hpp" 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(); - void updateTooling(); private: QString rootPath; - QString shellId; - EngineGeneration* generation = nullptr; + QQmlEngine engine; + ShellRoot* root = nullptr; + FiletreeWatcher* configWatcher = nullptr; QString originalWorkingDirectory; - QFileSystemWatcher configDirWatcher; }; diff --git a/src/core/scan.cpp b/src/core/scan.cpp deleted file mode 100644 index 4306de73..00000000 --- a/src/core/scan.cpp +++ /dev/null @@ -1,287 +0,0 @@ -#include "scan.hpp" -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "logcat.hpp" - -QS_LOGGING_CATEGORY(logQmlScanner, "quickshell.qmlscanner", QtWarningMsg); - -void QmlScanner::scanDir(const QString& path) { - if (this->scannedDirs.contains(path)) return; - this->scannedDirs.push_back(path); - - qCDebug(logQmlScanner) << "Scanning directory" << path; - auto dir = QDir(path); - - struct Entry { - QString name; - bool singleton = false; - bool internal = false; - }; - - bool seenQmldir = false; - auto entries = QVector(); - - for (auto& name: dir.entryList(QDir::Files | QDir::NoDotAndDotDot)) { - if (name == "qmldir") { - qCDebug(logQmlScanner - ) << "Found qmldir file, qmldir synthesization will be disabled for directory" - << path; - seenQmldir = true; - } else if (name.at(0).isUpper() && name.endsWith(".qml")) { - auto& entry = entries.emplaceBack(); - - if (this->scanQmlFile(dir.filePath(name), entry.singleton, entry.internal)) { - entry.name = name; - } else { - entries.pop_back(); - } - } else if (name.at(0).isUpper() && name.endsWith(".qml.json")) { - if (this->scanQmlJson(dir.filePath(name))) { - entries.push_back({ - .name = name.first(name.length() - 5), - .singleton = true, - }); - } - } - } - - if (!seenQmldir) { - qCDebug(logQmlScanner) << "Synthesizing qmldir for directory" << path; - - QString qmldir; - auto stream = QTextStream(&qmldir); - - // cant derive a module name if not in shell path - if (path.startsWith(this->rootPath.path())) { - auto end = path.sliced(this->rootPath.path().length()); - - // verify we have a valid module name. - for (auto& c: end) { - if (c == '/') c = '.'; - else if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') - || c == '_') - { - } else { - qCWarning(logQmlScanner) << "Module path contains invalid characters for a module name: " - << path.sliced(this->rootPath.path().length()); - goto skipadd; - } - } - - stream << "module qs" << end << '\n'; - skipadd:; - } else { - qCWarning(logQmlScanner) << "Module path" << path << "is outside of the config folder."; - } - - for (const auto& entry: entries) { - if (entry.internal) stream << "internal "; - if (entry.singleton) stream << "singleton "; - stream << entry.name.sliced(0, entry.name.length() - 4) << " 1.0 " << entry.name << '\n'; - } - - qCDebug(logQmlScanner) << "Synthesized qmldir for" << path << qPrintable("\n" + qmldir); - this->fileIntercepts.insert(QDir(path).filePath("qmldir"), qmldir); - } -} - -bool QmlScanner::scanQmlFile(const QString& path, bool& singleton, bool& internal) { - if (this->scannedFiles.contains(path)) return false; - this->scannedFiles.push_back(path); - - qCDebug(logQmlScanner) << "Scanning qml file" << path; - - auto file = QFile(path); - if (!file.open(QFile::ReadOnly | QFile::Text)) { - qCWarning(logQmlScanner) << "Failed to open file" << path; - return false; - } - - auto stream = QTextStream(&file); - auto imports = QVector(); - - while (!stream.atEnd()) { - auto line = stream.readLine().trimmed(); - if (!singleton && line == "pragma Singleton") { - singleton = true; - } else if (!internal && line == "//@ pragma Internal") { - internal = true; - } else if (line.startsWith("import")) { - // we dont care about "import qs" as we always load the root folder - if (auto importCursor = line.indexOf(" qs."); importCursor != -1) { - importCursor += 4; - QString path; - - while (importCursor != line.length()) { - auto c = line.at(importCursor); - if (c == '.') c = '/'; - else if (c == ' ') break; - else if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') - || c == '_') - { - } else { - qCWarning(logQmlScanner) << "Import line contains invalid characters: " << line; - goto next; - } - - path.append(c); - importCursor += 1; - } - - imports.append(this->rootPath.filePath(path)); - } else if (auto startQuot = line.indexOf('"'); - startQuot != -1 && line.length() >= startQuot + 3) - { - auto endQuot = line.indexOf('"', startQuot + 1); - if (endQuot == -1) continue; - - auto name = line.sliced(startQuot + 1, endQuot - startQuot - 1); - imports.push_back(name); - } - } else if (line.contains('{')) break; - - next:; - } - - file.close(); - - if (logQmlScanner().isDebugEnabled() && !imports.isEmpty()) { - qCDebug(logQmlScanner) << "Found imports" << imports; - } - - auto currentdir = QDir(QFileInfo(path).canonicalPath()); - - // the root can never be a singleton so it dosent matter if we skip it - 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 pathInfo = QFileInfo(ipath); - auto cpath = pathInfo.canonicalFilePath(); - - if (cpath.isEmpty()) { - qCWarning(logQmlScanner) << "Ignoring unresolvable import" << ipath << "from" << path; - continue; - } - - if (!pathInfo.isDir()) { - qCDebug(logQmlScanner) << "Ignoring non-directory import" << ipath << "from" << path; - continue; - } - - if (import.endsWith(".js")) this->scannedFiles.push_back(cpath); - else this->scanDir(cpath); - } - - return true; -} - -void QmlScanner::scanQmlRoot(const QString& path) { - bool singleton = false; - bool internal = false; - this->scanQmlFile(path, singleton, internal); -} - -bool QmlScanner::scanQmlJson(const QString& path) { - qCDebug(logQmlScanner) << "Scanning qml.json file" << path; - - auto file = QFile(path); - if (!file.open(QFile::ReadOnly | QFile::Text)) { - qCWarning(logQmlScanner) << "Failed to open file" << path; - return false; - } - - auto data = file.readAll(); - - // Importing this makes CI builds fail for some reason. - QJsonParseError error; // NOLINT (misc-include-cleaner) - auto json = QJsonDocument::fromJson(data, &error); - - if (error.error != QJsonParseError::NoError) { - qCCritical(logQmlScanner).nospace() - << "Failed to parse qml.json file at " << path << ": " << error.errorString(); - return false; - } - - const QString body = - "pragma Singleton\nimport QtQuick as Q\n\n" % QmlScanner::jsonToQml(json.object()).second; - - qCDebug(logQmlScanner) << "Synthesized qml file for" << path << qPrintable("\n" + body); - - this->fileIntercepts.insert(path.first(path.length() - 5), body); - this->scannedFiles.push_back(path); - return true; -} - -QPair QmlScanner::jsonToQml(const QJsonValue& value, int indent) { - if (value.isObject()) { - const auto& object = value.toObject(); - - auto valIter = object.constBegin(); - - QString accum = "Q.QtObject {\n"; - for (const auto& key: object.keys()) { - const auto& val = *valIter++; - auto [type, repr] = QmlScanner::jsonToQml(val, indent + 2); - accum += QString(' ').repeated(indent + 2) % "readonly property " % type % ' ' % key % ": " - % repr % ";\n"; - } - - accum += QString(' ').repeated(indent) % '}'; - return qMakePair(QStringLiteral("Q.QtObject"), accum); - } else if (value.isArray()) { - return qMakePair( - QStringLiteral("var"), - QJsonDocument(value.toArray()).toJson(QJsonDocument::Compact) - ); - } else if (value.isString()) { - const auto& str = value.toString(); - - if (str.startsWith('#') && (str.length() == 4 || str.length() == 7 || str.length() == 9)) { - for (auto c: str.sliced(1)) { - if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'))) { - goto noncolor; - } - } - - return qMakePair(QStringLiteral("Q.color"), '"' % str % '"'); - } - - noncolor: - return qMakePair(QStringLiteral("string"), '"' % QString(str).replace("\"", "\\\"") % '"'); - } else if (value.isDouble()) { - auto num = value.toDouble(); - double whole = 0; - if (std::modf(num, &whole) == 0.0) { - return qMakePair(QStringLiteral("int"), QString::number(static_cast(whole))); - } else { - return qMakePair(QStringLiteral("real"), QString::number(num)); - } - } else if (value.isBool()) { - return qMakePair(QStringLiteral("bool"), value.toBool() ? "true" : "false"); - } else { - return qMakePair(QStringLiteral("var"), "null"); - } -} diff --git a/src/core/scan.hpp b/src/core/scan.hpp deleted file mode 100644 index 1d3be850..00000000 --- a/src/core/scan.hpp +++ /dev/null @@ -1,34 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include - -#include "logcat.hpp" - -QS_DECLARE_LOGGING_CATEGORY(logQmlScanner); - -// expects canonical paths -class QmlScanner { -public: - QmlScanner() = default; - QmlScanner(const QDir& rootPath): rootPath(rootPath) {} - - // path must be canonical - void scanDir(const QString& path); - - void scanQmlRoot(const QString& path); - - QVector scannedDirs; - QVector scannedFiles; - QHash fileIntercepts; - -private: - QDir rootPath; - - bool scanQmlFile(const QString& path, bool& singleton, bool& internal); - bool scanQmlJson(const QString& path); - [[nodiscard]] static QPair jsonToQml(const QJsonValue& value, int indent = 0); -}; diff --git a/src/core/scriptmodel.cpp b/src/core/scriptmodel.cpp deleted file mode 100644 index 6837c4ab..00000000 --- a/src/core/scriptmodel.cpp +++ /dev/null @@ -1,182 +0,0 @@ -#include "scriptmodel.hpp" -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include - -void ScriptModel::updateValuesUnique(const QVariantList& newValues) { - this->hasActiveIterators = true; - this->mValues.reserve(newValues.size()); - - auto iter = this->mValues.begin(); - auto newIter = newValues.begin(); - - // TODO: cache this - auto getCmpKey = [&](const QVariant& v) { - if (v.canConvert()) { - auto vMap = v.value(); - if (vMap.contains(this->cmpKey)) { - return vMap.value(this->cmpKey); - } - } - - return v; - }; - - auto variantCmp = [&](const QVariant& a, const QVariant& b) { - if (!this->cmpKey.isEmpty()) return getCmpKey(a) == getCmpKey(b); - else return a == b; - }; - - auto eqPredicate = [&](const QVariant& b) { - return [&](const QVariant& a) { return variantCmp(a, b); }; - }; - - 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 (!variantCmp(*newIter, *iter)) { - auto oldIter = std::find_if(iter, this->mValues.end(), eqPredicate(*newIter)); - - if (oldIter != this->mValues.end()) { - if (std::find_if(newIter, newValues.end(), eqPredicate(*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_if(newIter, newValues.end(), eqPredicate(*iter)) == newValues.end() - ); - - auto index = static_cast(std::distance(this->mValues.begin(), iter)); - auto startIndex = static_cast(std::distance(this->mValues.begin(), startIter)); - - 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() - && variantCmp(*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_if(iter, this->mValues.end(), eqPredicate(*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 if (*newIter != *iter) { - auto first = static_cast(std::distance(this->mValues.begin(), iter)); - auto index = first; - - do { - this->mValues.replace(index, *newIter); - ++iter; - ++newIter; - ++index; - } while (iter != this->mValues.end() && newIter != newValues.end() && *newIter != *iter); - - this->dataChanged( - this->index(first, 0, QModelIndex()), - this->index(index - 1, 0, QModelIndex()), - {Qt::UserRole} - ); - } else { - ++iter; - ++newIter; - } - } - - this->hasActiveIterators = false; -} - -void ScriptModel::setValues(const QVariantList& newValues) { - if (newValues == this->mValues) return; - this->updateValuesUnique(newValues); - emit this->valuesChanged(); -} - -void ScriptModel::setObjectProp(const QString& objectProp) { - if (objectProp == this->cmpKey) return; - this->cmpKey = objectProp; - this->updateValuesUnique(this->mValues); - emit this->objectPropChanged(); -} - -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 10916f6c..00000000 --- a/src/core/scriptmodel.hpp +++ /dev/null @@ -1,106 +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); - /// The property that javascript objects passed into the model will be compared with. - /// - /// For example, if `objectProp` is `"myprop"` then `{ myprop: "a", other: "y" }` and - /// `{ myprop: "a", other: "z" }` will be considered equal. - /// - /// Defaults to `""`, meaning no key. - Q_PROPERTY(QString objectProp READ objectProp WRITE setObjectProp NOTIFY objectPropChanged); - QML_ELEMENT; - -public: - [[nodiscard]] QVariantList values() const { - auto values = this->mValues; - // If not detached, the QML engine will invalidate iterators in updateValuesUnique. - if (this->hasActiveIterators) values.detach(); - return values; - } - - void setValues(const QVariantList& newValues); - - [[nodiscard]] QString objectProp() const { return this->cmpKey; } - void setObjectProp(const QString& objectProp); - - [[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(); - void objectPropChanged(); - -private: - QVariantList mValues; - QString cmpKey; - bool hasActiveIterators = false; - - 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/singleton.cpp b/src/core/singleton.cpp deleted file mode 100644 index 15668c98..00000000 --- a/src/core/singleton.cpp +++ /dev/null @@ -1,53 +0,0 @@ -#include "singleton.hpp" - -#include -#include -#include -#include -#include - -#include "generation.hpp" -#include "reload.hpp" - -void Singleton::componentComplete() { - auto* context = QQmlEngine::contextForObject(this); - - if (context == nullptr) { - qWarning() << "Not registering singleton not created in the qml context:" << this; - return; - } - - auto url = context->baseUrl(); - - if (this->parent() != nullptr || context->contextObject() != this) { - qWarning() << "Tried to register singleton" << this - << "which is not the root component of its file" << url; - return; - } - - auto* generation = EngineGeneration::findObjectGeneration(this); - - if (generation == nullptr) { - qWarning() << "Tried to register singleton" << this - << "which has no associated engine generation" << url; - return; - } - - generation->singletonRegistry.registerSingleton(url, this); - this->ReloadPropagator::componentComplete(); -} - -void SingletonRegistry::registerSingleton(const QUrl& url, Singleton* singleton) { - if (this->registry.contains(url)) { - qWarning() << "Tried to register singleton twice for the same file" << url; - return; - } - - this->registry.insert(url, singleton); -} - -void SingletonRegistry::onReload(SingletonRegistry* old) { - for (auto [url, singleton]: this->registry.asKeyValueRange()) { - singleton->reload(old == nullptr ? nullptr : old->registry.value(url)); - } -} diff --git a/src/core/singleton.hpp b/src/core/singleton.hpp deleted file mode 100644 index 200c97f1..00000000 --- a/src/core/singleton.hpp +++ /dev/null @@ -1,32 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include -#include -#include - -#include "reload.hpp" - -///! The root component for reloadable singletons. -/// All singletons should inherit from this type. -class Singleton: public ReloadPropagator { - Q_OBJECT; - QML_ELEMENT; - -public: - void componentComplete() override; -}; - -class SingletonRegistry { -public: - SingletonRegistry() = default; - - void registerSingleton(const QUrl& url, Singleton* singleton); - void onReload(SingletonRegistry* old); - -private: - QHash registry; -}; 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 deleted file mode 100644 index 4e66c627..00000000 --- a/src/core/test/CMakeLists.txt +++ /dev/null @@ -1,10 +0,0 @@ -function (qs_test name) - add_executable(${name} ${ARGN}) - target_link_libraries(${name} PRIVATE Qt::Quick Qt::Test quickshell-core quickshell-window quickshell-ui quickshell-io) - add_test(NAME ${name} WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}" COMMAND $) -endfunction() - -qs_test(transformwatcher transformwatcher.cpp) -qs_test(ringbuffer ringbuf.cpp) -qs_test(scriptmodel scriptmodel.cpp) -qs_test(stacklist stacklist.cpp) diff --git a/src/core/test/popupwindow.hpp b/src/core/test/popupwindow.hpp deleted file mode 100644 index e69de29b..00000000 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 0abfdbf3..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(const 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/test/transformwatcher.cpp b/src/core/test/transformwatcher.cpp deleted file mode 100644 index ac10cfb0..00000000 --- a/src/core/test/transformwatcher.cpp +++ /dev/null @@ -1,105 +0,0 @@ -#include "transformwatcher.hpp" - -#include -#include -#include -#include -#include - -#include "../transformwatcher.hpp" - -void TestTransformWatcher::aParentOfB() { // NOLINT - auto a = QQuickItem(); - a.setObjectName("a"); - auto b = QQuickItem(); - b.setObjectName("b"); - b.setParentItem(&a); - - auto watcher = TransformWatcher(); - watcher.setA(&a); - watcher.setB(&b); - - QCOMPARE(watcher.parentChain, {&a}); - QCOMPARE(watcher.childChain, {&b}); -} - -void TestTransformWatcher::bParentOfA() { // NOLINT - auto a = QQuickItem(); - a.setObjectName("a"); - auto b = QQuickItem(); - b.setObjectName("b"); - a.setParentItem(&b); - - auto watcher = TransformWatcher(); - watcher.setA(&a); - watcher.setB(&b); - - QCOMPARE(watcher.parentChain, (QList {&a, &b})); - QCOMPARE(watcher.childChain, {}); -} - -// a -// p1 b -// p2 c1 -// p3 -void TestTransformWatcher::aParentChainB() { // NOLINT - auto a = QQuickItem(); - a.setObjectName("a"); - auto b = QQuickItem(); - b.setObjectName("b"); - - auto p1 = QQuickItem(); - p1.setObjectName("p1"); - auto p2 = QQuickItem(); - p2.setObjectName("p2"); - auto p3 = QQuickItem(); - p3.setObjectName("p3"); - auto c1 = QQuickItem(); - c1.setObjectName("c1"); - - a.setParentItem(&p1); - p1.setParentItem(&p2); - p2.setParentItem(&p3); - - b.setParentItem(&c1); - c1.setParentItem(&p3); - - auto watcher = TransformWatcher(); - watcher.setA(&a); - watcher.setB(&b); - - QCOMPARE(watcher.parentChain, (QList {&a, &p1, &p2, &p3})); - QCOMPARE(watcher.childChain, (QList {&b, &c1})); -} - -void TestTransformWatcher::multiWindow() { // NOLINT - auto a = QQuickItem(); - a.setObjectName("a"); - auto b = QQuickItem(); - b.setObjectName("b"); - - auto p = QQuickItem(); - p.setObjectName("p"); - auto c = QQuickItem(); - c.setObjectName("c"); - - a.setParentItem(&p); - b.setParentItem(&c); - - auto aW = QQuickWindow(); - auto bW = QQuickWindow(); - - p.setParentItem(aW.contentItem()); - c.setParentItem(bW.contentItem()); - - auto watcher = TransformWatcher(); - watcher.setA(&a); - watcher.setB(&b); - - QCOMPARE(watcher.parentChain, (QList {&a, &p, aW.contentItem()})); - QCOMPARE(watcher.childChain, (QList {&b, &c, bW.contentItem()})); - QCOMPARE(watcher.parentWindow, &aW); - QCOMPARE(watcher.childWindow, &bW); -} - -QTEST_MAIN(TestTransformWatcher); diff --git a/src/core/test/transformwatcher.hpp b/src/core/test/transformwatcher.hpp deleted file mode 100644 index e2bdfda8..00000000 --- a/src/core/test/transformwatcher.hpp +++ /dev/null @@ -1,14 +0,0 @@ -#pragma once - -#include -#include - -class TestTransformWatcher: public QObject { - Q_OBJECT; - -private slots: - void aParentOfB(); - void bParentOfA(); - void aParentChainB(); - void multiWindow(); -}; diff --git a/src/core/toolsupport.cpp b/src/core/toolsupport.cpp deleted file mode 100644 index afce008b..00000000 --- a/src/core/toolsupport.cpp +++ /dev/null @@ -1,241 +0,0 @@ -#include "toolsupport.hpp" -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "logcat.hpp" -#include "paths.hpp" -#include "scan.hpp" - -namespace qs::core { - -namespace { -QS_LOGGING_CATEGORY(logTooling, "quickshell.tooling", QtWarningMsg); -} - -bool QmlToolingSupport::updateTooling(const QDir& configRoot, QmlScanner& scanner) { - auto* vfs = QsPaths::instance()->shellVfsDir(); - - if (!vfs) { - qCCritical(logTooling) << "Tooling dir could not be created"; - return false; - } - - if (!QmlToolingSupport::lockTooling()) { - return false; - } - - if (!QmlToolingSupport::updateQmllsConfig(configRoot, false)) { - QDir(vfs->filePath("qs")).removeRecursively(); - return false; - } - - QmlToolingSupport::updateToolingFs(scanner, configRoot, vfs->filePath("qs")); - return true; -} - -bool QmlToolingSupport::lockTooling() { - if (QmlToolingSupport::toolingLock) return true; - - auto lockPath = QsPaths::instance()->shellVfsDir()->filePath("tooling.lock"); - auto* file = new QFile(lockPath); - - if (!file->open(QFile::WriteOnly)) { - qCCritical(logTooling) << "Could not open tooling lock for write"; - return false; - } - - auto lock = flock { - .l_type = F_WRLCK, - .l_whence = SEEK_SET, // NOLINT (fcntl.h??) - .l_start = 0, - .l_len = 0, - .l_pid = 0, - }; - - if (fcntl(file->handle(), F_SETLK, &lock) == 0) { - qCInfo(logTooling) << "Acquired tooling support lock"; - QmlToolingSupport::toolingLock = file; - return true; - } else if (errno == EACCES || errno == EAGAIN) { - qCInfo(logTooling) << "Tooling support locked by another instance"; - return false; - } else { - qCCritical(logTooling).nospace() << "Could not create tooling lock at " << lockPath - << " with error code " << errno << ": " << qt_error_string(); - return false; - } -} - -QString QmlToolingSupport::getQmllsConfig() { - static auto config = []() { - // We can't replicate the algorithm used to create the import path list as it can have distro - // specific patches, e.g. nixos. - auto importPaths = QQmlEngine().importPathList(); - importPaths.removeIf([](const QString& path) { return path.startsWith("qrc:"); }); - - auto vfsPath = QsPaths::instance()->shellVfsDir()->path(); - auto importPathsStr = importPaths.join(u':'); - - QString qmllsConfig; - auto print = QDebug(&qmllsConfig).nospace(); - print << "[General]\nno-cmake-calls=true\nbuildDir=" << vfsPath - << "\nimportPaths=" << importPathsStr << '\n'; - - return qmllsConfig; - }(); - - return config; -} - -bool QmlToolingSupport::updateQmllsConfig(const QDir& configRoot, bool create) { - auto shellConfigPath = configRoot.filePath(".qmlls.ini"); - auto vfsConfigPath = QsPaths::instance()->shellVfsDir()->filePath(".qmlls.ini"); - - auto shellFileInfo = QFileInfo(shellConfigPath); - if (!create && !shellFileInfo.exists() && !shellFileInfo.isSymLink()) { - if (QmlToolingSupport::toolingEnabled) { - qInfo() << "QML tooling support disabled"; - QmlToolingSupport::toolingEnabled = false; - } else { - qCInfo(logTooling) << "Not enabling QML tooling support, qmlls.ini is missing at path" - << shellConfigPath; - } - - QFile::remove(vfsConfigPath); - return false; - } - - auto vfsFile = QFile(vfsConfigPath); - - if (!vfsFile.open(QFile::ReadWrite | QFile::Text)) { - qCCritical(logTooling) << "Failed to create qmlls config in vfs"; - return false; - } - - auto config = QmlToolingSupport::getQmllsConfig(); - - if (vfsFile.readAll() != config) { - if (!vfsFile.resize(0) || !vfsFile.write(config.toUtf8())) { - qCCritical(logTooling) << "Failed to write qmlls config in vfs"; - return false; - } - - qCDebug(logTooling) << "Wrote qmlls config in vfs"; - } - - if (!shellFileInfo.isSymLink() || shellFileInfo.symLinkTarget() != vfsConfigPath) { - QFile::remove(shellConfigPath); - - if (!QFile::link(vfsConfigPath, shellConfigPath)) { - qCCritical(logTooling) << "Failed to create qmlls config symlink"; - return false; - } - - qCDebug(logTooling) << "Created qmlls config symlink"; - } - - if (!QmlToolingSupport::toolingEnabled) { - qInfo() << "QML tooling support enabled"; - QmlToolingSupport::toolingEnabled = true; - } - - return true; -} - -void QmlToolingSupport::updateToolingFs( - QmlScanner& scanner, - const QDir& scanDir, - const QDir& linkDir -) { - QList files; - QSet subdirs; - - auto scanPath = scanDir.path(); - - linkDir.mkpath("."); - - for (auto& path: scanner.scannedFiles) { - if (path.length() < scanPath.length() + 1 || !path.startsWith(scanPath)) continue; - auto name = path.sliced(scanPath.length() + 1); - - if (name.contains('/')) { - auto dirname = name.first(name.indexOf('/')); - subdirs.insert(dirname); - continue; - } - - auto fileInfo = QFileInfo(path); - if (!fileInfo.isFile()) continue; - - auto spath = linkDir.filePath(name); - auto sFileInfo = QFileInfo(spath); - - if (!sFileInfo.isSymLink() || sFileInfo.symLinkTarget() != path) { - QFile::remove(spath); - - if (QFile::link(path, spath)) { - qCDebug(logTooling) << "Created symlink to" << path << "at" << spath; - files.append(spath); - } else { - qCCritical(logTooling) << "Could not create symlink to" << path << "at" << spath; - } - } else { - files.append(spath); - } - } - - for (auto [path, text]: scanner.fileIntercepts.asKeyValueRange()) { - if (path.length() < scanPath.length() + 1 || !path.startsWith(scanPath)) continue; - auto name = path.sliced(scanPath.length() + 1); - - if (name.contains('/')) { - auto dirname = name.first(name.indexOf('/')); - subdirs.insert(dirname); - continue; - } - - auto spath = linkDir.filePath(name); - auto file = QFile(spath); - if (!file.open(QFile::ReadWrite | QFile::Text)) { - qCCritical(logTooling) << "Failed to open injected file" << spath; - continue; - } - - if (file.readAll() == text) { - files.append(spath); - continue; - } - - if (file.resize(0) && file.write(text.toUtf8())) { - files.append(spath); - qCDebug(logTooling) << "Wrote injected file" << spath; - } else { - qCCritical(logTooling) << "Failed to write injected file" << spath; - } - } - - for (auto& name: linkDir.entryList(QDir::Files | QDir::System)) { // System = broken symlinks - auto path = linkDir.filePath(name); - - if (!files.contains(path)) { - if (QFile::remove(path)) qCDebug(logTooling) << "Removed old file at" << path; - else qCWarning(logTooling) << "Failed to remove old file at" << path; - } - } - - for (const auto& subdir: subdirs) { - QmlToolingSupport::updateToolingFs(scanner, scanDir.filePath(subdir), linkDir.filePath(subdir)); - } -} - -} // namespace qs::core diff --git a/src/core/toolsupport.hpp b/src/core/toolsupport.hpp deleted file mode 100644 index 9fb79216..00000000 --- a/src/core/toolsupport.hpp +++ /dev/null @@ -1,22 +0,0 @@ -#pragma once - -#include - -#include "scan.hpp" - -namespace qs::core { - -class QmlToolingSupport { -public: - static bool updateTooling(const QDir& configRoot, QmlScanner& scanner); - -private: - static QString getQmllsConfig(); - static bool lockTooling(); - static bool updateQmllsConfig(const QDir& configRoot, bool create); - static void updateToolingFs(QmlScanner& scanner, const QDir& scanDir, const QDir& linkDir); - static inline bool toolingEnabled = false; - static inline QFile* toolingLock = nullptr; -}; - -} // namespace qs::core diff --git a/src/core/transformwatcher.cpp b/src/core/transformwatcher.cpp deleted file mode 100644 index 6fc7c34a..00000000 --- a/src/core/transformwatcher.cpp +++ /dev/null @@ -1,183 +0,0 @@ -#include "transformwatcher.hpp" - -#include -#include -#include -#include -#include -#include -#include -#include - -void TransformWatcher::resolveChains(QQuickItem* a, QQuickItem* b, QQuickItem* commonParent) { - if (a == nullptr || b == nullptr) return; - - auto aChain = QVector(); - auto bChain = QVector(); - - auto* aParent = a; - auto* bParent = b; - - // resolve the parent chain of b. if a is in the chain break early - while (bParent != nullptr) { - bChain.push_back(bParent); - - if (bParent == a) { - aChain.push_back(a); - goto chainResolved; - } - - if (bParent == commonParent) break; - bParent = bParent->parentItem(); - } - - // resolve the parent chain of a, breaking as soon as b is found - while (aParent != nullptr) { - aChain.push_back(aParent); - - for (auto bParent = bChain.begin(); bParent != bChain.end(); bParent++) { - if (*bParent == aParent) { - bParent++; - bChain.erase(bParent, bChain.end()); - goto chainResolved; - } - } - - if (aParent == commonParent) break; - aParent = aParent->parentItem(); - } - - if (commonParent != nullptr && aParent == commonParent) { - qWarning() << this << "failed to find a common parent between" << a << "and" << b - << "due to incorrectly set commonParent" << commonParent; - - return; - } - -chainResolved: - - this->parentChain = aChain; - if (bChain.last() == aChain.last()) bChain.removeLast(); - this->childChain = bChain; - - if (a->window() != b->window()) { - this->parentWindow = a->window(); - this->childWindow = b->window(); - } else { - this->parentWindow = nullptr; - this->childWindow = nullptr; - } -} - -void TransformWatcher::resolveChains() { - this->resolveChains(this->mA, this->mB, this->mCommonParent); -} - -void TransformWatcher::linkItem(QQuickItem* item) const { - QObject::connect(item, &QQuickItem::xChanged, this, &TransformWatcher::transformChanged); - QObject::connect(item, &QQuickItem::yChanged, this, &TransformWatcher::transformChanged); - QObject::connect(item, &QQuickItem::widthChanged, this, &TransformWatcher::transformChanged); - QObject::connect(item, &QQuickItem::heightChanged, this, &TransformWatcher::transformChanged); - QObject::connect(item, &QQuickItem::scaleChanged, this, &TransformWatcher::transformChanged); - QObject::connect(item, &QQuickItem::rotationChanged, this, &TransformWatcher::transformChanged); - - 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); - } -} - -void TransformWatcher::linkChains() { - for (auto* item: this->parentChain) { - this->linkItem(item); - } - - for (auto* item: this->childChain) { - this->linkItem(item); - } -} - -void TransformWatcher::unlinkChains() { - for (auto* item: this->parentChain) { - QObject::disconnect(item, nullptr, this, nullptr); - } - - 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() { - this->unlinkChains(); - this->resolveChains(); - 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(); -} diff --git a/src/core/transformwatcher.hpp b/src/core/transformwatcher.hpp deleted file mode 100644 index 8efa9399..00000000 --- a/src/core/transformwatcher.hpp +++ /dev/null @@ -1,87 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include -#include - -#ifdef QS_TEST -class TestTransformWatcher; -#endif - -///! 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. -/// -/// > [!INFO] The algorithm responsible for determining the relationship -/// > between `a` and `b` is biased towards `a` being a parent of `b`, -/// > or `a` being closer to the common parent of `a` and `b` than `b`. -class TransformWatcher: public QObject { - Q_OBJECT; - // clang-format off - Q_PROPERTY(QQuickItem* a READ a WRITE setA NOTIFY aChanged); - Q_PROPERTY(QQuickItem* b READ b WRITE setB NOTIFY bChanged); - /// Known common parent of both `a` and `b`. Defaults to `null`. - /// - /// This property can be used to optimize the algorithm that figures out - /// the relationship between `a` and `b`. Setting it to something that is not - /// a common parent of both `a` and `b` will prevent the path from being determined - /// correctly, and setting it to `null` will disable the optimization. - Q_PROPERTY(QQuickItem* commonParent READ commonParent WRITE setCommonParent NOTIFY commonParentChanged); - /// This property is updated whenever the geometry of any item in the path from `a` to `b` changes. - /// - /// Its value is undefined, and is intended to trigger an expression update. - Q_PROPERTY(QObject* transform READ transform NOTIFY transformChanged); - // clang-format on - QML_ELEMENT; - -public: - explicit TransformWatcher(QObject* parent = nullptr): QObject(parent) {} - - [[nodiscard]] QQuickItem* a() const; - void setA(QQuickItem* a); - - [[nodiscard]] QQuickItem* b() const; - void setB(QQuickItem* b); - - [[nodiscard]] QQuickItem* commonParent() const; - void setCommonParent(QQuickItem* commonParent); - - [[nodiscard]] QObject* transform() const { return nullptr; } // NOLINT - -signals: - void transformChanged(); - - void aChanged(); - void bChanged(); - void commonParentChanged(); - -private slots: - void recalcChains(); - void itemDestroyed(); - void aDestroyed(); - void bDestroyed(); - -private: - void resolveChains(QQuickItem* a, QQuickItem* b, QQuickItem* commonParent); - void resolveChains(); - void linkItem(QQuickItem* item) const; - void linkChains(); - void unlinkChains(); - - QQuickItem* mA = nullptr; - QQuickItem* mB = nullptr; - QQuickItem* mCommonParent = nullptr; - - // a -> traverse parent chain -> parent window -> global scope -> child window -> traverse child chain -> b - QList parentChain; - QList childChain; - QQuickWindow* parentWindow = nullptr; - QQuickWindow* childWindow = nullptr; - -#ifdef QS_TEST - friend class TestTransformWatcher; -#endif -}; diff --git a/src/core/types.cpp b/src/core/types.cpp deleted file mode 100644 index 81c1d010..00000000 --- a/src/core/types.cpp +++ /dev/null @@ -1,26 +0,0 @@ -#include "types.hpp" - -#include -#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); -} - -QMargins Margins::qmargins() const { return {this->left, this->top, this->right, this->bottom}; } diff --git a/src/core/types.hpp b/src/core/types.hpp deleted file mode 100644 index b6cb2598..00000000 --- a/src/core/types.hpp +++ /dev/null @@ -1,101 +0,0 @@ -#pragma once - -#include -#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; - [[nodiscard]] bool isEmpty() const { return this->w == 0 && this->h == 0; } -}; - -QDebug operator<<(QDebug debug, const Box& box); - -class Margins { - Q_GADGET; - Q_PROPERTY(qint32 left MEMBER left); - Q_PROPERTY(qint32 right MEMBER right); - Q_PROPERTY(qint32 top MEMBER top); - Q_PROPERTY(qint32 bottom MEMBER bottom); - QML_CONSTRUCTIBLE_VALUE; - QML_VALUE_TYPE(margins); - -public: - [[nodiscard]] bool operator==(const Margins& other) const noexcept { - // clang-format off - return this->left == other.left - && this->right == other.right - && this->top == other.top - && this->bottom == other.bottom; - // clang-format on - } - - qint32 left = 0; - qint32 right = 0; - qint32 top = 0; - qint32 bottom = 0; - - [[nodiscard]] QMargins qmargins() const; -}; - -///! 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 88583d0c..00000000 --- a/src/core/util.hpp +++ /dev/null @@ -1,304 +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); -} - -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..e564e1bf 100644 --- a/src/core/variants.cpp +++ b/src/core/variants.cpp @@ -6,110 +6,51 @@ #include #include #include -#include -#include -#include -#include #include "reload.hpp" void Variants::onReload(QObject* oldInstance) { auto* old = qobject_cast(oldInstance); - for (auto& [variant, instanceObj]: this->mInstances.values) { + for (auto& [variant, instanceObj]: this->instances.values) { QObject* oldInstance = nullptr; if (old != nullptr) { - auto& values = old->mInstances.values; + auto& values = old->instances.values; - if (variant.canConvert()) { - auto variantMap = variant.value(); - - int matchcount = 0; - int matchi = 0; - int i = 0; - for (auto& [value, _]: values) { - if (!value.canConvert()) continue; - auto valueSet = value.value(); - - int count = 0; - for (auto [k, v]: variantMap.asKeyValueRange()) { - if (valueSet.contains(k) && valueSet.value(k) == v) { - count++; - } + int matchcount = 0; + int matchi = 0; + int i = 0; + for (auto& [valueSet, _]: values) { + int count = 0; + for (auto& [k, v]: variant.toStdMap()) { + if (valueSet.contains(k) && valueSet.value(k) == v) { + count++; } - - if (count > matchcount) { - matchcount = count; - matchi = i; - } - - i++; } - if (matchcount > 0) { - oldInstance = values.takeAt(matchi).second; + if (count > matchcount) { + matchcount = count; + matchi = i; } - } else { - int i = 0; - for (auto& [value, _]: values) { - if (variant == value) { - oldInstance = values.takeAt(i).second; - break; - } - i++; - } + i++; + } + + if (matchcount > 0) { + oldInstance = values.takeAt(matchi).second; } } auto* instance = qobject_cast(instanceObj); - if (instance != nullptr) instance->reload(oldInstance); + if (instance != nullptr) instance->onReload(oldInstance); else Reloadable::reloadChildrenRecursive(instanceObj, oldInstance); } - - this->loaded = true; } -QVariant Variants::model() const { return QVariant::fromValue(this->mModel); } - -void Variants::setModel(const QVariant& model) { - if (model.canConvert()) { - this->mModel = model.value(); - } else if (model.canConvert()) { - auto list = model.value(); - if (!list.isReadable()) { - qWarning() << "Non readable list" << model << "assigned to Variants.model, Ignoring."; - return; - } - - QVariantList model; - auto size = list.count(); - for (auto i = 0; i < size; i++) { - model.push_back(QVariant::fromValue(list.at(i))); - } - - this->mModel = std::move(model); - } else { - qWarning() << "Non list data" << model << "assigned to Variants.model, Ignoring."; - return; - } - +void Variants::setVariants(QVariantList variants) { + this->mVariants = std::move(variants); this->updateVariants(); - emit this->modelChanged(); - emit this->instancesChanged(); -} - -QQmlListProperty Variants::instances() { - return QQmlListProperty(this, nullptr, &Variants::instanceCount, &Variants::instanceAt); -} - -qsizetype Variants::instanceCount(QQmlListProperty* prop) { - return static_cast(prop->object)->mInstances.values.length(); // NOLINT -} - -QObject* Variants::instanceAt(QQmlListProperty* prop, qsizetype i) { - return static_cast(prop->object)->mInstances.values.at(i).second; // NOLINT } void Variants::componentComplete() { @@ -118,46 +59,47 @@ void Variants::componentComplete() { } void Variants::updateVariants() { - if (this->mDelegate == nullptr) { + if (this->mComponent == nullptr) { qWarning() << "Variants instance does not have a component specified"; return; } // clean up removed entries - for (auto iter = this->mInstances.values.begin(); iter < this->mInstances.values.end();) { - if (this->mModel.contains(iter->first)) { + for (auto iter = this->instances.values.begin(); iter < this->instances.values.end();) { + if (this->mVariants.contains(iter->first)) { iter++; } else { iter->second->deleteLater(); - iter = this->mInstances.values.erase(iter); + iter = this->instances.values.erase(iter); } } - for (auto iter = this->mModel.begin(); iter < this->mModel.end(); iter++) { - auto& variant = *iter; - for (auto iter2 = this->mModel.begin(); iter2 < iter; iter2++) { - if (*iter2 == variant) { - qWarning() << "same value specified twice in Variants, duplicates will be ignored:" - << variant; - goto outer; - } - } + for (auto iter = this->mVariants.begin(); iter < this->mVariants.end(); iter++) { + auto& variantObj = *iter; + if (!variantObj.canConvert()) { + qWarning() << "value passed to Variants is not an object and will be ignored:" << variantObj; + } else { + auto variant = variantObj.value(); - { - if (this->mInstances.contains(variant)) { + for (auto iter2 = this->mVariants.begin(); iter2 < iter; iter2++) { + if (*iter2 == variantObj) { + qWarning() << "same value specified twice in Variants, duplicates will be ignored:" + << variantObj; + goto outer; + } + } + + if (this->instances.contains(variant)) { continue; // we dont need to recreate this one } - auto variantMap = QVariantMap(); - variantMap.insert("modelData", variant); - - auto* instance = this->mDelegate->createWithInitialProperties( - variantMap, - QQmlEngine::contextForObject(this->mDelegate) + auto* instance = this->mComponent->createWithInitialProperties( + variant, + QQmlEngine::contextForObject(this->mComponent) ); if (instance == nullptr) { - qWarning() << this->mDelegate->errorString().toStdString().c_str(); + qWarning() << this->mComponent->errorString().toStdString().c_str(); qWarning() << "failed to create variant with object" << variant; continue; } @@ -165,12 +107,7 @@ void Variants::updateVariants() { QQmlEngine::setObjectOwnership(instance, QQmlEngine::CppOwnership); instance->setParent(this); - this->mInstances.insert(variant, instance); - - if (this->loaded) { - if (auto* reloadable = qobject_cast(instance)) reloadable->reload(nullptr); - else Reloadable::reloadChildrenRecursive(instance, nullptr); - } + this->instances.insert(variant, instance); } outer:; @@ -196,7 +133,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..79224e24 100644 --- a/src/core/variants.hpp +++ b/src/core/variants.hpp @@ -6,12 +6,9 @@ #include #include #include -#include #include #include -#include -#include "doc.hpp" #include "reload.hpp" // extremely inefficient map @@ -20,64 +17,40 @@ 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 instances of a component based on a given set of variants. /// 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 -/// 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. -/// -/// 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) +/// [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. - Q_PROPERTY(QQmlComponent* delegate MEMBER mDelegate); + /// The component to create instances of + Q_PROPERTY(QQmlComponent* component MEMBER mComponent); /// 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. - QSDOC_PROPERTY_OVERRIDE(QList model READ model WRITE setModel NOTIFY modelChanged); - QSDOC_HIDE Q_PROPERTY(QVariant model READ model WRITE setModel NOTIFY modelChanged); - /// Current instances of the delegate. - Q_PROPERTY(QQmlListProperty instances READ instances NOTIFY instancesChanged); - Q_CLASSINFO("DefaultProperty", "delegate"); + Q_PROPERTY(QList variants MEMBER mVariants WRITE setVariants); + Q_CLASSINFO("DefaultProperty", "component"); QML_ELEMENT; public: explicit Variants(QObject* parent = nullptr): Reloadable(parent) {} void onReload(QObject* oldInstance) override; + void componentComplete() override; - [[nodiscard]] QVariant model() const; - void setModel(const QVariant& model); - - QQmlListProperty instances(); - -signals: - void modelChanged(); - void instancesChanged(); - private: - static qsizetype instanceCount(QQmlListProperty* prop); - static QObject* instanceAt(QQmlListProperty* prop, qsizetype i); - + void setVariants(QVariantList variants); void updateVariants(); - QQmlComponent* mDelegate = nullptr; - QVariantList mModel; - AwfulMap mInstances; - bool loaded = false; + QQmlComponent* mComponent = nullptr; + QVariantList mVariants; + AwfulMap instances; }; diff --git a/src/core/watcher.cpp b/src/core/watcher.cpp new file mode 100644 index 00000000..6b06d584 --- /dev/null +++ b/src/core/watcher.cpp @@ -0,0 +1,38 @@ +#include "watcher.hpp" + +#include +#include +#include +#include +#include + +FiletreeWatcher::FiletreeWatcher(QObject* parent): QObject(parent) { + QObject::connect( + &this->watcher, + &QFileSystemWatcher::fileChanged, + this, + &FiletreeWatcher::onFileChanged + ); + + QObject::connect( + &this->watcher, + &QFileSystemWatcher::directoryChanged, + this, + &FiletreeWatcher::onDirectoryChanged + ); +} +void FiletreeWatcher::addPath(const QString& path) { + this->watcher.addPath(path); + + if (QFileInfo(path).isDir()) { + auto dir = QDir(path); + + for (auto& entry: dir.entryList(QDir::AllEntries | QDir::NoDotAndDotDot)) { + this->addPath(dir.filePath(entry)); + } + } +} + +void FiletreeWatcher::onDirectoryChanged(const QString& path) { this->addPath(path); } + +void FiletreeWatcher::onFileChanged(const QString& path) { emit this->fileChanged(path); } diff --git a/src/core/watcher.hpp b/src/core/watcher.hpp new file mode 100644 index 00000000..a729f03c --- /dev/null +++ b/src/core/watcher.hpp @@ -0,0 +1,24 @@ +#pragma once + +#include +#include +#include + +class FiletreeWatcher: public QObject { + Q_OBJECT; + +public: + explicit FiletreeWatcher(QObject* parent = nullptr); + + void addPath(const QString& path); + +signals: + void fileChanged(const QString& path); + +private slots: + void onDirectoryChanged(const QString& path); + void onFileChanged(const QString& path); + +private: + QFileSystemWatcher watcher; +}; 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/core/windowinterface.hpp b/src/core/windowinterface.hpp new file mode 100644 index 00000000..4f20d9c0 --- /dev/null +++ b/src/core/windowinterface.hpp @@ -0,0 +1,125 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#include "qmlscreen.hpp" +#include "region.hpp" +#include "reload.hpp" + +class WindowInterface: public Reloadable { + Q_OBJECT; + // clang-format off + Q_PROPERTY(QQuickItem* contentItem READ contentItem); + /// If the window is shown or hidden. Defaults to true. + 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); + /// The screen that the window currently occupies. + /// + /// > [!INFO] This cannot be changed after windowConnected. + Q_PROPERTY(QuickshellScreenInfo* screen READ screen WRITE setScreen NOTIFY screenChanged); + /// The background color of the window. Defaults to white. + /// + /// > [!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. + /// + /// If non null then the clickable areas of the window will be determined by the provided region. + /// + /// ```qml + /// ShellWindow { + /// // The mask region is set to `rect`, meaning only `rect` is clickable. + /// // All other clicks pass through the window to ones behind it. + /// mask: Region { item: rect } + /// + /// Rectangle { + /// id: rect + /// + /// anchors.centerIn: parent + /// width: 100 + /// height: 100 + /// } + /// } + /// ``` + /// + /// If the provided region's intersection mode is `Combine` (the default), + /// then the region will be used as is. Otherwise it will be applied on top of the window region. + /// + /// For example, setting the intersection mode to `Xor` will invert the mask and make everything in + /// the mask region not clickable and pass through clicks inside it through the window. + /// + /// ```qml + /// ShellWindow { + /// // The mask region is set to `rect`, but the intersection mode is set to `Xor`. + /// // This inverts the mask causing all clicks inside `rect` to be passed to the window + /// // behind this one. + /// mask: Region { item: rect; intersection: Intersection.Xor } + /// + /// Rectangle { + /// id: rect + /// + /// anchors.centerIn: parent + /// width: 100 + /// height: 100 + /// } + /// } + /// ``` + Q_PROPERTY(PendingRegion* mask READ mask WRITE setMask NOTIFY maskChanged); + Q_PROPERTY(QQmlListProperty data READ data); + // clang-format on + Q_CLASSINFO("DefaultProperty", "data"); + QML_NAMED_ELEMENT(QSWindow); + QML_UNCREATABLE("uncreatable base class"); + +public: + explicit WindowInterface(QObject* parent = nullptr): Reloadable(parent) {} + + [[nodiscard]] virtual QQuickItem* contentItem() const = 0; + + [[nodiscard]] virtual bool isVisible() const = 0; + virtual void setVisible(bool visible) = 0; + + [[nodiscard]] virtual qint32 width() const = 0; + virtual void setWidth(qint32 width) = 0; + + [[nodiscard]] virtual qint32 height() const = 0; + virtual void setHeight(qint32 height) = 0; + + [[nodiscard]] virtual QuickshellScreenInfo* screen() const = 0; + virtual void setScreen(QuickshellScreenInfo* screen) = 0; + + [[nodiscard]] virtual QColor color() const = 0; + virtual void setColor(QColor color) = 0; + + [[nodiscard]] virtual PendingRegion* mask() const = 0; + virtual void setMask(PendingRegion* mask) = 0; + + [[nodiscard]] virtual QQmlListProperty data() = 0; + +signals: + void windowConnected(); + void visibleChanged(); + void widthChanged(); + void heightChanged(); + void screenChanged(); + void colorChanged(); + void maskChanged(); +}; 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 1433a879..00000000 --- a/src/crash/handler.cpp +++ /dev/null @@ -1,185 +0,0 @@ -#include "handler.hpp" -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "../core/instanceinfo.hpp" -#include "../core/logcat.hpp" - -extern char** environ; // NOLINT - -using namespace google_breakpad; - -namespace qs::crash { - -namespace { -QS_LOGGING_CATEGORY(logCrashHandler, "quickshell.crashhandler", QtWarningMsg); -} - -struct CrashHandlerPrivate { - ExceptionHandler* exceptionHandler = nullptr; - int minidumpFd = -1; - int infoFd = -1; - - 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 b9f0eabe..00000000 --- a/src/crash/main.cpp +++ /dev/null @@ -1,191 +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/logcat.hpp" -#include "../core/logging.hpp" -#include "../core/paths.hpp" -#include "build.hpp" -#include "interface.hpp" - -namespace { - -QS_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; - - 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 app = QApplication(argc, argv); - QApplication::setDesktopFileName("org.quickshell"); - - 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 deleted file mode 100644 index fc004f3d..00000000 --- a/src/dbus/CMakeLists.txt +++ /dev/null @@ -1,44 +0,0 @@ -set_source_files_properties(org.freedesktop.DBus.Properties.xml PROPERTIES - CLASSNAME DBusPropertiesInterface -) - -set_source_files_properties(org.freedesktop.DBus.ObjectManager.xml PROPERTIES - CLASSNAME DBusObjectManagerInterface - INCLUDE ${CMAKE_CURRENT_SOURCE_DIR}/dbus_objectmanager_types.hpp -) - -qt_add_dbus_interface(DBUS_INTERFACES - org.freedesktop.DBus.Properties.xml - dbus_properties -) - -qt_add_dbus_interface(DBUS_INTERFACES - org.freedesktop.DBus.ObjectManager.xml - dbus_objectmanager -) - -qt_add_library(quickshell-dbus STATIC - properties.cpp - objectmanager.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 - -qs_add_pchset(dbus - DEPENDENCIES Qt::DBus - HEADERS - - - -) - -qs_pch(quickshell-dbus SET dbus) - -add_subdirectory(dbusmenu) diff --git a/src/dbus/bus.cpp b/src/dbus/bus.cpp deleted file mode 100644 index d53c4c6b..00000000 --- a/src/dbus/bus.cpp +++ /dev/null @@ -1,61 +0,0 @@ -#include "bus.hpp" // NOLINT -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "../core/logcat.hpp" - -namespace qs::dbus { - -namespace { -QS_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/dbus_objectmanager_types.hpp b/src/dbus/dbus_objectmanager_types.hpp deleted file mode 100644 index 5e0869c1..00000000 --- a/src/dbus/dbus_objectmanager_types.hpp +++ /dev/null @@ -1,10 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include - -using DBusObjectManagerInterfaces = QHash; -using DBusObjectManagerObjects = QHash; diff --git a/src/dbus/dbusmenu/CMakeLists.txt b/src/dbus/dbusmenu/CMakeLists.txt deleted file mode 100644 index 61cee42c..00000000 --- a/src/dbus/dbusmenu/CMakeLists.txt +++ /dev/null @@ -1,35 +0,0 @@ -set_source_files_properties(com.canonical.dbusmenu.xml PROPERTIES - CLASSNAME DBusMenuInterface - INCLUDE ${CMAKE_CURRENT_SOURCE_DIR}/dbus_menu_types.hpp -) - -qt_add_dbus_interface(DBUS_INTERFACES - com.canonical.dbusmenu.xml - dbus_menu -) - -qt_add_library(quickshell-dbusmenu STATIC - dbus_menu_types.cpp - dbusmenu.cpp - ${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) - -# 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) - -qs_module_pch(quickshell-dbusmenu SET dbus) - -target_link_libraries(quickshell PRIVATE quickshell-dbusmenuplugin) diff --git a/src/dbus/dbusmenu/com.canonical.dbusmenu.xml b/src/dbus/dbusmenu/com.canonical.dbusmenu.xml deleted file mode 100644 index 12f021bc..00000000 --- a/src/dbus/dbusmenu/com.canonical.dbusmenu.xml +++ /dev/null @@ -1,68 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/dbus/dbusmenu/dbus_menu_types.cpp b/src/dbus/dbusmenu/dbus_menu_types.cpp deleted file mode 100644 index 36ae41fe..00000000 --- a/src/dbus/dbusmenu/dbus_menu_types.cpp +++ /dev/null @@ -1,92 +0,0 @@ -#include "dbus_menu_types.hpp" - -#include -#include -#include -#include -#include - -const QDBusArgument& operator>>(const QDBusArgument& argument, DBusMenuLayout& layout) { - layout.children.clear(); - - argument.beginStructure(); - argument >> layout.id; - argument >> layout.properties; - - argument.beginArray(); - while (!argument.atEnd()) { - auto childArgument = qdbus_cast(argument).variant().value(); - auto child = qdbus_cast(childArgument); - layout.children.append(child); - } - argument.endArray(); - - argument.endStructure(); - return argument; -} - -const QDBusArgument& operator<<(QDBusArgument& argument, const DBusMenuLayout& layout) { - argument.beginStructure(); - argument << layout.id; - argument << layout.properties; - - argument.beginArray(qMetaTypeId()); - for (const auto& child: layout.children) { - argument << QDBusVariant(QVariant::fromValue(child)); - } - argument.endArray(); - - argument.endStructure(); - return argument; -} - -const QDBusArgument& operator>>(const QDBusArgument& argument, DBusMenuItemProperties& item) { - argument.beginStructure(); - argument >> item.id; - argument >> item.properties; - argument.endStructure(); - return argument; -} - -const QDBusArgument& operator<<(QDBusArgument& argument, const DBusMenuItemProperties& item) { - argument.beginStructure(); - argument << item.id; - argument << item.properties; - argument.endStructure(); - return argument; -} - -const QDBusArgument& operator>>(const QDBusArgument& argument, DBusMenuItemPropertyNames& names) { - argument.beginStructure(); - argument >> names.id; - argument >> names.properties; - argument.endStructure(); - return argument; -} - -const QDBusArgument& operator<<(QDBusArgument& argument, const DBusMenuItemPropertyNames& names) { - argument.beginStructure(); - argument << names.id; - argument << names.properties; - argument.endStructure(); - return argument; -} - -QDebug operator<<(QDebug debug, const DBusMenuLayout& layout) { - debug.nospace() << "DBusMenuLayout(id=" << layout.id << ", properties=" << layout.properties - << ", children=" << layout.children << ")"; - - return debug; -} - -QDebug operator<<(QDebug debug, const DBusMenuItemProperties& item) { - debug.nospace() << "DBusMenuItemProperties(id=" << item.id << ", properties=" << item.properties - << ")"; - return debug; -} - -QDebug operator<<(QDebug debug, const DBusMenuItemPropertyNames& names) { - debug.nospace() << "DBusMenuItemPropertyNames(id=" << names.id - << ", properties=" << names.properties << ")"; - return debug; -} diff --git a/src/dbus/dbusmenu/dbus_menu_types.hpp b/src/dbus/dbusmenu/dbus_menu_types.hpp deleted file mode 100644 index 29659497..00000000 --- a/src/dbus/dbusmenu/dbus_menu_types.hpp +++ /dev/null @@ -1,47 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include - -struct DBusMenuLayout { - qint32 id = 0; - QVariantMap properties; - QList children; -}; - -using DBusMenuIdList = QList; - -struct DBusMenuItemProperties { - qint32 id = 0; - QVariantMap properties; -}; - -using DBusMenuItemPropertiesList = QList; - -struct DBusMenuItemPropertyNames { - qint32 id = 0; - QStringList properties; -}; - -using DBusMenuItemPropertyNamesList = QList; - -const QDBusArgument& operator>>(const QDBusArgument& argument, DBusMenuLayout& layout); -const QDBusArgument& operator<<(QDBusArgument& argument, const DBusMenuLayout& layout); -const QDBusArgument& operator>>(const QDBusArgument& argument, DBusMenuItemProperties& item); -const QDBusArgument& operator<<(QDBusArgument& argument, const DBusMenuItemProperties& item); -const QDBusArgument& operator>>(const QDBusArgument& argument, DBusMenuItemPropertyNames& names); -const QDBusArgument& operator<<(QDBusArgument& argument, const DBusMenuItemPropertyNames& names); - -QDebug operator<<(QDebug debug, const DBusMenuLayout& layout); -QDebug operator<<(QDebug debug, const DBusMenuItemProperties& item); -QDebug operator<<(QDebug debug, const DBusMenuItemPropertyNames& names); - -Q_DECLARE_METATYPE(DBusMenuLayout); -Q_DECLARE_METATYPE(DBusMenuIdList); -Q_DECLARE_METATYPE(DBusMenuItemProperties); -Q_DECLARE_METATYPE(DBusMenuItemPropertiesList); -Q_DECLARE_METATYPE(DBusMenuItemPropertyNames); -Q_DECLARE_METATYPE(DBusMenuItemPropertyNamesList); diff --git a/src/dbus/dbusmenu/dbusmenu.cpp b/src/dbus/dbusmenu/dbusmenu.cpp deleted file mode 100644 index 186b1330..00000000 --- a/src/dbus/dbusmenu/dbusmenu.cpp +++ /dev/null @@ -1,573 +0,0 @@ -#include "dbusmenu.hpp" -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "../../core/iconimageprovider.hpp" -#include "../../core/logcat.hpp" -#include "../../core/model.hpp" -#include "../../core/qsmenu.hpp" -#include "../../dbus/properties.hpp" -#include "dbus_menu.h" -#include "dbus_menu_types.hpp" - -QS_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) - , 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); -} - -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"); } - -DBusMenu* DBusMenuItem::menuHandle() const { return this->menu; } -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(':') - ); - } else if (this->image.hasData()) { - return this->image.url(); - } else return nullptr; -} - -QsMenuButtonType::Enum DBusMenuItem::buttonType() const { return this->mButtonType; }; -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) { - if (showChildren == this->mShowChildren) return; - this->mShowChildren = showChildren; - this->childrenLoaded = false; - - if (showChildren) { - this->menu->prepareToShow(this->id, -1); - } else { - if (!this->mChildren.isEmpty()) { - for (auto child: this->mChildren) { - this->menu->removeRecursive(child); - } - - this->mChildren.clear(); - this->onChildrenUpdated(); - } - } -} - -void DBusMenuItem::updateLayout() const { - if (!this->isShowingChildren()) return; - this->menu->updateLayout(this->id, -1); -} - -bool DBusMenuItem::hasChildren() const { return this->displayChildren || this->id == 0; } - -ObjectModel* DBusMenuItem::children() { - return reinterpret_cast*>(&this->enabledChildren); -} - -void DBusMenuItem::updateProperties(const QVariantMap& properties, const QStringList& removed) { - // Some programs appear to think sending an empty map does not mean "reset everything" - // and instead means "do nothing". oh well... - if (properties.isEmpty() && removed.isEmpty()) { - qCDebug(logDbusMenu) << "Ignoring empty property update for" << this; - return; - } - - auto originalText = this->mText; - //auto originalMnemonic = this->mnemonic; - auto originalEnabled = this->mEnabled; - auto originalVisible = this->visible; - auto originalIconName = this->iconName; - auto imageChanged = false; - auto originalIsSeparator = this->mSeparator; - auto originalButtonType = this->mButtonType; - auto originalToggleState = this->mCheckState; - auto originalDisplayChildren = this->displayChildren; - - auto label = properties.value("label"); - if (label.canConvert()) { - auto text = label.value(); - this->mText = text; - this->mCleanLabel = text; - //this->mnemonic = QChar(); - - for (auto i = 0; i < this->mText.length() - 1;) { - if (this->mText.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, ""); - i += 8; - } else { - i++; - } - } - - for (auto i = 0; i < this->mCleanLabel.length() - 1; i++) { - if (this->mCleanLabel.at(i) == '_') { - this->mCleanLabel.remove(i, 1); - } - } - } else if (removed.isEmpty() || removed.contains("label")) { - this->mText = ""; - //this->mnemonic = QChar(); - } - - auto enabled = properties.value("enabled"); - if (enabled.canConvert()) { - this->mEnabled = enabled.value(); - } else if (removed.isEmpty() || removed.contains("enabled")) { - this->mEnabled = true; - } - - auto visible = properties.value("visible"); - if (visible.canConvert()) { - this->visible = visible.value(); - } else if (removed.isEmpty() || removed.contains("visible")) { - this->visible = true; - } - - auto iconName = properties.value("icon-name"); - if (iconName.canConvert()) { - this->iconName = iconName.value(); - } else if (removed.isEmpty() || removed.contains("icon-name")) { - this->iconName = ""; - } - - auto iconData = properties.value("icon-data"); - 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(); - } - } else if (removed.isEmpty() || removed.contains("icon-data")) { - imageChanged = this->image.hasData(); - image.data.clear(); - } - - auto type = properties.value("type"); - if (type.canConvert()) { - this->mSeparator = type.value() == "separator"; - } else if (removed.isEmpty() || removed.contains("type")) { - this->mSeparator = false; - } - - auto toggleType = properties.value("toggle-type"); - 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; - else { - qCWarning(logDbusMenu) << "Unrecognized toggle type" << toggleTypeStr << "for" << this; - this->mButtonType = QsMenuButtonType::None; - } - } else if (removed.isEmpty() || removed.contains("toggle-type")) { - this->mButtonType = QsMenuButtonType::None; - } - - auto toggleState = properties.value("toggle-state"); - if (toggleState.canConvert()) { - auto toggleStateInt = toggleState.value(); - - if (toggleStateInt == 0) this->mCheckState = Qt::Unchecked; - 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; - } - - auto childrenDisplay = properties.value("children-display"); - if (childrenDisplay.canConvert()) { - auto childrenDisplayStr = childrenDisplay.value(); - - if (childrenDisplayStr == "") this->displayChildren = false; - else if (childrenDisplayStr == "submenu") this->displayChildren = true; - else { - qCWarning(logDbusMenu) << "Unrecognized children-display mode" << childrenDisplayStr << "for" - << this; - this->displayChildren = false; - } - } else if (removed.isEmpty() || removed.contains("children-display")) { - this->displayChildren = false; - } - - if (this->mText != originalText) emit this->textChanged(); - //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->mCheckState != originalToggleState) emit this->checkStateChanged(); - if (this->mSeparator != originalIsSeparator) emit this->isSeparatorChanged(); - if (this->displayChildren != originalDisplayChildren) emit this->hasChildrenChanged(); - - if (this->iconName != originalIconName || imageChanged) { - emit this->iconChanged(); - } - - qCDebug(logDbusMenu).nospace() << "Updated properties of " << this << " { label=" << this->mText - << ", enabled=" << this->mEnabled << ", visible=" << this->visible - << ", iconName=" << this->iconName << ", iconData=" << &this->image - << ", separator=" << this->mSeparator - << ", toggleType=" << this->mButtonType - << ", toggleState=" << this->mCheckState - << ", displayChildren=" << this->displayChildren << " }"; -} - -void DBusMenuItem::onChildrenUpdated() { - QVector children; - for (auto child: this->mChildren) { - auto* item = this->menu->items.value(child); - if (item->visible) children.append(item); - } - - this->enabledChildren.diffUpdate(children); -} - -QDebug operator<<(QDebug debug, DBusMenuItem* item) { - if (item == nullptr) { - debug << "DBusMenuItem(nullptr)"; - return debug; - } - - auto saver = QDebugStateSaver(debug); - debug.nospace() << "DBusMenuItem(" << static_cast(item) << ", id=" << item->id - << ", label=" << item->mText << ", menu=" << item->menu << ")"; - return debug; -} - -DBusMenu::DBusMenu(const QString& service, const QString& path, QObject* parent): QObject(parent) { - qDBusRegisterMetaType(); - qDBusRegisterMetaType(); - qDBusRegisterMetaType(); - qDBusRegisterMetaType(); - qDBusRegisterMetaType(); - qDBusRegisterMetaType(); - - this->interface = new DBusMenuInterface(service, path, QDBusConnection::sessionBus(), this); - - if (!this->interface->isValid()) { - qCWarning(logDbusMenu).noquote() << "Cannot create DBusMenu for" << service << "at" << path; - return; - } - - QObject::connect( - this->interface, - &DBusMenuInterface::LayoutUpdated, - this, - &DBusMenu::onLayoutUpdated - ); - - this->properties.setInterface(this->interface); - this->properties.updateAllViaGetAll(); -} - -void DBusMenu::prepareToShow(qint32 item, qint32 depth) { - auto pending = this->interface->AboutToShow(item); - auto* call = new QDBusPendingCallWatcher(pending, this); - - auto responseCallback = [this, item, depth](QDBusPendingCallWatcher* call) { - const QDBusPendingReply reply = *call; - if (reply.isError()) { - qCDebug(logDbusMenu) << "Error in AboutToShow, but showing anyway for menu" << item << "of" - << this << reply.error(); - } - - this->updateLayout(item, depth); - - delete call; - }; - - QObject::connect(call, &QDBusPendingCallWatcher::finished, this, responseCallback); -} - -void DBusMenu::updateLayout(qint32 parent, qint32 depth) { - auto pending = this->interface->GetLayout(parent, depth, QStringList()); - auto* call = new QDBusPendingCallWatcher(pending, this); - - auto responseCallback = [this, parent, depth](QDBusPendingCallWatcher* call) { - const QDBusPendingReply reply = *call; - - if (reply.isError()) { - qCWarning(logDbusMenu) << "Error updating layout for menu" << parent << "of" << this - << reply.error(); - } else { - auto layout = reply.argumentAt<1>(); - this->updateLayoutRecursive(layout, this->items.value(parent), depth); - } - - delete call; - }; - - QObject::connect(call, &QDBusPendingCallWatcher::finished, this, responseCallback); -} - -void DBusMenu::updateLayoutRecursive( - const DBusMenuLayout& layout, - DBusMenuItem* parent, - qint32 depth -) { - auto* item = this->items.value(layout.id); - if (item == nullptr) { - // 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); - } - } - - if (item == nullptr) return; - - qCDebug(logDbusMenu) << "Updating layout recursively for" << this << "menu" << layout.id; - item->updateProperties(layout.properties); - - if (depth != 0) { - 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; - }); - - if (!item->mShowChildren || existing == layout.children.end()) { - qCDebug(logDbusMenu) << "Removing missing layout item" << this->items.value(*iter) << "from" - << item; - this->removeRecursive(*iter); - iter = item->mChildren.erase(iter); - childrenChanged = true; - } else { - iter++; - } - } - - 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); - this->items.insert(child.id, nullptr); - childrenChanged = true; - } - - 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 (item->mShowChildren && !item->childrenLoaded) { - item->childrenLoaded = true; - } - - emit item->layoutUpdated(); -} - -void DBusMenu::removeRecursive(qint32 id) { - auto* item = this->items.value(id); - - if (item != nullptr) { - for (auto child: item->mChildren) { - this->removeRecursive(child); - } - } - - this->items.remove(id); - - if (item != nullptr) { - item->deleteLater(); - } -} - -void DBusMenu::sendEvent(qint32 item, const QString& event) { - qCDebug(logDbusMenu) << "Sending event" << event << "to menu" << item << "of" << this; - - auto pending = - this->interface->Event(item, event, QDBusVariant(0), QDateTime::currentSecsSinceEpoch()); - auto* call = new QDBusPendingCallWatcher(pending, this); - - auto responseCallback = [this, item, event](QDBusPendingCallWatcher* call) { - const QDBusPendingReply<> reply = *call; - - if (reply.isError()) { - qCWarning(logDbusMenu) << "Error sending event" << event << "to" << item << "of" << this - << reply.error(); - } - - delete call; - }; - - QObject::connect(call, &QDBusPendingCallWatcher::finished, this, responseCallback); -} - -DBusMenuItem* DBusMenu::menu() { return &this->rootItem; } - -void DBusMenu::onLayoutUpdated(quint32 /*unused*/, qint32 parent) { - // note: spec says this is recursive - this->updateLayout(parent, -1); -} - -void DBusMenu::onItemPropertiesUpdated( // NOLINT - const DBusMenuItemPropertiesList& updatedProps, - const DBusMenuItemPropertyNamesList& removedProps -) { - for (const auto& propset: updatedProps) { - auto* item = this->items.value(propset.id); - if (item != nullptr) { - item->updateProperties(propset.properties); - } - } - - for (const auto& propset: removedProps) { - auto* item = this->items.value(propset.id); - if (item != nullptr) { - item->updateProperties({}, propset.properties); - } - } -} - -QDebug operator<<(QDebug debug, DBusMenu* menu) { - if (menu == nullptr) { - debug << "DBusMenu(nullptr)"; - return debug; - } - - auto saver = QDebugStateSaver(debug); - debug.nospace() << "DBusMenu(" << static_cast(menu) << ", " << menu->properties.toString() - << ")"; - return debug; -} - -QImage -DBusMenuPngImage::requestImage(const QString& /*unused*/, QSize* size, const QSize& /*unused*/) { - auto image = QImage(); - - if (!image.loadFromData(this->data, "PNG")) { - qCWarning(logDbusMenu) << "Failed to load dbusmenu item png"; - } - - if (size != nullptr) *size = image.size(); - 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 deleted file mode 100644 index 1192baaa..00000000 --- a/src/dbus/dbusmenu/dbusmenu.hpp +++ /dev/null @@ -1,196 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#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" - -QS_DECLARE_LOGGING_CATEGORY(logDbusMenu); - -class DBusMenuInterface; - -namespace qs::dbus::dbusmenu { - -// hack because docgen can't take namespaces in superclasses -using menu::QsMenuEntry; - -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; -}; - -///! 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 { - Q_OBJECT; - /// Handle to the root of this menu. - Q_PROPERTY(qs::dbus::dbusmenu::DBusMenu* menuHandle READ menuHandle CONSTANT); - 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. - /// - /// 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; - - [[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]] bool isShowingChildren() const; - void setShowChildrenRecursive(bool showChildren); - - [[nodiscard]] ObjectModel* children() override; - - void updateProperties(const QVariantMap& properties, const QStringList& removed = {}); - void onChildrenUpdated(); - - qint32 id = 0; - QString mText; - 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; - -private: - QString mCleanLabel; - //QChar mnemonic; - bool mEnabled = true; - bool visible = true; - bool mSeparator = false; - QString iconName; - DBusMenuPngImage image; - menu::QsMenuButtonType::Enum mButtonType = menu::QsMenuButtonType::None; - Qt::CheckState mCheckState = Qt::Unchecked; - bool displayChildren = false; - ObjectModel enabledChildren {this}; - DBusMenuItem* parentMenu = nullptr; -}; - -QDebug operator<<(QDebug debug, DBusMenuItem* item); - -///! Handle to a DBusMenu tree. -/// 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); - 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); - -signals: - QSDOC_HIDE void iconThemePathChanged(); - -public: - Q_OBJECT_BINDABLE_PROPERTY(DBusMenu, QStringList, iconThemePath, &DBusMenu::iconThemePathChanged); - - void prepareToShow(qint32 item, qint32 depth); - void updateLayout(qint32 parent, qint32 depth); - void removeRecursive(qint32 id); - void sendEvent(qint32 item, const QString& event); - - DBusMenuItem rootItem {0, this, nullptr}; - QHash items {std::make_pair(0, &this->rootItem)}; - - [[nodiscard]] DBusMenuItem* menu(); - -private slots: - void onLayoutUpdated(quint32 revision, qint32 parent); - void onItemPropertiesUpdated( - const DBusMenuItemPropertiesList& updatedProps, - const DBusMenuItemPropertyNamesList& removedProps - ); - -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 { -public: - explicit DBusMenuHandle(QObject* parent): menu::QsMenuHandle(parent) {} - - void setAddress(const QString& service, const QString& path); - - 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); -}; - -} // namespace qs::dbus::dbusmenu diff --git a/src/dbus/dbusmenu/module.md b/src/dbus/dbusmenu/module.md deleted file mode 100644 index 810393e4..00000000 --- a/src/dbus/dbusmenu/module.md +++ /dev/null @@ -1,4 +0,0 @@ -name = "Quickshell.DBusMenu" -description = "Types related to DBusMenu (used in system tray)" -headers = [ "dbusmenu.hpp" ] ------ diff --git a/src/dbus/objectmanager.cpp b/src/dbus/objectmanager.cpp deleted file mode 100644 index 258f6fe8..00000000 --- a/src/dbus/objectmanager.cpp +++ /dev/null @@ -1,87 +0,0 @@ -#include "objectmanager.hpp" - -#include -#include -#include -#include -#include -#include -#include -#include - -#include "../core/logcat.hpp" -#include "dbus_objectmanager.h" -#include "dbus_objectmanager_types.hpp" - -namespace { -QS_LOGGING_CATEGORY(logDbusObjectManager, "quickshell.dbus.objectmanager", QtWarningMsg); -} - -namespace qs::dbus { - -DBusObjectManager::DBusObjectManager(QObject* parent): QObject(parent) { - qDBusRegisterMetaType(); - qDBusRegisterMetaType(); -} - -bool DBusObjectManager::setInterface( - const QString& service, - const QString& path, - const QDBusConnection& connection -) { - delete this->mInterface; - this->mInterface = new DBusObjectManagerInterface(service, path, connection, this); - - if (!this->mInterface->isValid()) { - qCWarning(logDbusObjectManager) << "Failed to create DBusObjectManagerInterface for" << service - << path << ":" << this->mInterface->lastError(); - delete this->mInterface; - this->mInterface = nullptr; - return false; - } - - QObject::connect( - this->mInterface, - &DBusObjectManagerInterface::InterfacesAdded, - this, - &DBusObjectManager::interfacesAdded - ); - - QObject::connect( - this->mInterface, - &DBusObjectManagerInterface::InterfacesRemoved, - this, - &DBusObjectManager::interfacesRemoved - ); - - this->fetchInitialObjects(); - return true; -} - -void DBusObjectManager::fetchInitialObjects() { - if (!this->mInterface) return; - - auto reply = this->mInterface->GetManagedObjects(); - auto* watcher = new QDBusPendingCallWatcher(reply, this); - - QObject::connect( - watcher, - &QDBusPendingCallWatcher::finished, - this, - [this](QDBusPendingCallWatcher* watcher) { - const QDBusPendingReply reply = *watcher; - watcher->deleteLater(); - - if (reply.isError()) { - qCWarning(logDbusObjectManager) << "Failed to get managed objects:" << reply.error(); - return; - } - - for (const auto& [path, interfaces]: reply.value().asKeyValueRange()) { - emit this->interfacesAdded(path, interfaces); - } - } - ); -} - -} // namespace qs::dbus diff --git a/src/dbus/objectmanager.hpp b/src/dbus/objectmanager.hpp deleted file mode 100644 index 4246ea28..00000000 --- a/src/dbus/objectmanager.hpp +++ /dev/null @@ -1,37 +0,0 @@ -#pragma once - -#include -#include -#include -#include - -#include "dbus_objectmanager_types.hpp" - -class DBusObjectManagerInterface; - -namespace qs::dbus { - -class DBusObjectManager: public QObject { - Q_OBJECT; - -public: - explicit DBusObjectManager(QObject* parent = nullptr); - - bool setInterface( - const QString& service, - const QString& path, - const QDBusConnection& connection = QDBusConnection::sessionBus() - ); - -signals: - void - interfacesAdded(const QDBusObjectPath& objectPath, const DBusObjectManagerInterfaces& interfaces); - void interfacesRemoved(const QDBusObjectPath& objectPath, const QStringList& interfaces); - -private: - void fetchInitialObjects(); - - DBusObjectManagerInterface* mInterface = nullptr; -}; - -} // namespace qs::dbus \ No newline at end of file diff --git a/src/dbus/org.freedesktop.DBus.ObjectManager.xml b/src/dbus/org.freedesktop.DBus.ObjectManager.xml deleted file mode 100644 index 24749f22..00000000 --- a/src/dbus/org.freedesktop.DBus.ObjectManager.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - - - - - - - - - - - diff --git a/src/dbus/org.freedesktop.DBus.Properties.xml b/src/dbus/org.freedesktop.DBus.Properties.xml deleted file mode 100644 index 021123ab..00000000 --- a/src/dbus/org.freedesktop.DBus.Properties.xml +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/dbus/properties.cpp b/src/dbus/properties.cpp deleted file mode 100644 index d0f65d99..00000000 --- a/src/dbus/properties.cpp +++ /dev/null @@ -1,342 +0,0 @@ -#include "properties.hpp" -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "../core/logcat.hpp" -#include "dbus_properties.h" - -QS_LOGGING_CATEGORY(logDbusProperties, "quickshell.dbus.properties", QtWarningMsg); - -namespace qs::dbus { - -QDBusError demarshallVariant(const QVariant& variant, const QMetaType& type, void* slot) { - const char* expectedSignature = "v"; - - if (type.id() != QMetaType::QVariant) { - expectedSignature = QDBusMetaType::typeToSignature(type); - if (expectedSignature == nullptr) { - qFatal() << "failed to demarshall unregistered dbus meta-type" << type << "with" << variant; - } - } - - if (variant.metaType() == type) { - if (type.id() == QMetaType::QVariant) { - *reinterpret_cast(slot) = variant; - } else { - type.destruct(slot); - type.construct(slot, variant.constData()); - } - } else if (variant.metaType() == QMetaType::fromType()) { - auto arg = qvariant_cast(variant); - auto signature = arg.currentSignature(); - - if (signature == expectedSignature) { - if (!QDBusMetaType::demarshall(arg, type, slot)) { - QString error; - QDebug(&error) << "failed to deserialize dbus value" << variant << "into" << type; - return QDBusError(QDBusError::InvalidArgs, error); - } - } else { - QString error; - QDebug(&error) << "mismatched signature while trying to demarshall" << variant << "into" - << type << "expected" << expectedSignature << "got" << signature; - return QDBusError(QDBusError::InvalidArgs, error); - } - } else { - QString error; - QDebug(&error) << "failed to deserialize variant" << variant << "into" << type; - return QDBusError(QDBusError::InvalidArgs, error); - } - - return QDBusError(); -} - -void asyncReadPropertyInternal( - const QMetaType& type, - QDBusAbstractInterface& interface, - const QString& property, - std::function)> callback // NOLINT -) { - if (type.id() != QMetaType::QVariant) { - const char* expectedSignature = QDBusMetaType::typeToSignature(type); - if (expectedSignature == nullptr) { - qFatal() << "qs::dbus::asyncReadPropertyInternal called with unregistered dbus meta-type" - << type; - } - } - - auto callMessage = QDBusMessage::createMethodCall( - interface.service(), - interface.path(), - "org.freedesktop.DBus.Properties", - "Get" - ); - - callMessage << interface.interface() << property; - auto pendingCall = interface.connection().asyncCall(callMessage); - - auto* call = new QDBusPendingCallWatcher(pendingCall, &interface); - - auto responseCallback = [type, callback](QDBusPendingCallWatcher* call) { - QDBusPendingReply reply = *call; - - callback([&](void* slot) { - if (reply.isError()) { - return reply.error(); - } else { - return demarshallVariant(reply.value().variant(), type, slot); - } - }); - - delete call; - }; - - QObject::connect(call, &QDBusPendingCallWatcher::finished, &interface, responseCallback); -} - -DBusPropertyGroup::DBusPropertyGroup(QVector properties, QObject* parent) - : QObject(parent) - , properties(std::move(properties)) {} - -void DBusPropertyGroup::setInterface(QDBusAbstractInterface* interface) { - if (this->interface != nullptr) { - delete this->propertyInterface; - this->propertyInterface = nullptr; - } - - if (interface != nullptr) { - this->interface = interface; - - this->propertyInterface = new DBusPropertiesInterface( - interface->service(), - interface->path(), - interface->connection(), - this - ); - - QObject::connect( - this->propertyInterface, - &DBusPropertiesInterface::PropertiesChanged, - this, - &DBusPropertyGroup::onPropertiesChanged - ); - } -} - -void DBusPropertyGroup::attachProperty(DBusPropertyCore* property) { - this->properties.append(property); -} - -void DBusPropertyGroup::updateAllDirect() { - qCDebug(logDbusProperties).noquote() - << "Updating all properties of" << this->toString() << "via individual queries"; - - if (this->interface == nullptr) { - qFatal() << "Attempted to update properties of disconnected property group"; - } - - for (auto* property: this->properties) { - this->requestPropertyUpdate(property); - } -} - -void DBusPropertyGroup::updateAllViaGetAll() { - qCDebug(logDbusProperties).noquote() - << "Updating all properties of" << this->toString() << "via GetAll"; - - if (this->interface == nullptr) { - qFatal() << "Attempted to update properties of disconnected property group"; - } - - auto pendingCall = this->propertyInterface->GetAll(this->interface->interface()); - auto* call = new QDBusPendingCallWatcher(pendingCall, this); - - auto responseCallback = [this](QDBusPendingCallWatcher* call) { - const QDBusPendingReply reply = *call; - - if (reply.isError()) { - 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(); - } - - delete call; - }; - - QObject::connect(call, &QDBusPendingCallWatcher::finished, this, responseCallback); -} - -void DBusPropertyGroup::updatePropertySet(const QVariantMap& properties, bool complainMissing) { - for (const auto [name, value]: properties.asKeyValueRange()) { - auto prop = std::ranges::find_if(this->properties, [&name](DBusPropertyCore* prop) { - return prop->nameRef() == name; - }); - - if (prop == this->properties.end()) { - qCDebug(logDbusProperties) << "Ignoring untracked property update" << name << "for" - << this->toString(); - } else { - this->tryUpdateProperty(*prop, 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()) { - if (!property->isRequired() && reply.error().type() == QDBusError::InvalidArgs) { - qCDebug(logDbusProperties) << "Error updating non-required property" << propStr; - qCDebug(logDbusProperties) << reply.error(); - } else { - qCWarning(logDbusProperties).noquote() << "Error updating property" << propStr; - qCWarning(logDbusProperties) << reply.error(); - } - } else { - this->tryUpdateProperty(property, reply.value().variant()); - } - - 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 { - if (this->interface == nullptr) { - return "{ DISCONNECTED }"; - } else { - return this->interface->service() + this->interface->path() + "/" - + this->interface->interface(); - } -} - -QString DBusPropertyGroup::propertyString(const DBusPropertyCore* property) const { - return this->toString() % ':' % property->nameRef(); -} - -void DBusPropertyGroup::onPropertiesChanged( - const QString& interfaceName, - const QVariantMap& changedProperties, - const QStringList& invalidatedProperties -) { - if (interfaceName != this->interface->interface()) return; - qCDebug(logDbusProperties).noquote() - << "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; - }); - - if (prop == this->properties.end()) { - qCDebug(logDbusProperties) << "Ignoring untracked property invalidation" << name << "for" - << this; - } else { - this->requestPropertyUpdate(*prop); - } - } - - this->updatePropertySet(changedProperties, false); -} - -} // namespace qs::dbus - -#if QT_VERSION < QT_VERSION_CHECK(6, 8, 0) -QDebug operator<<(QDebug debug, const QDBusObjectPath& path) { - debug.nospace() << "QDBusObjectPath(" << path.path() << ")"; - return debug; -} -#endif diff --git a/src/dbus/properties.hpp b/src/dbus/properties.hpp deleted file mode 100644 index f6a63300..00000000 --- a/src/dbus/properties.hpp +++ /dev/null @@ -1,337 +0,0 @@ -#pragma once - -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "../core/logcat.hpp" -#include "../core/util.hpp" - -class DBusPropertiesInterface; - -QS_DECLARE_LOGGING_CATEGORY(logDbusProperties); - -namespace qs::dbus { - -QDBusError demarshallVariant(const QVariant& variant, const QMetaType& type, void* slot); - -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, QDBusError error) - : value(std::move(value)) - , error(std::move(error)) {} - - bool isValid() { return !this->error.isValid(); } - - T value {}; - QDBusError error; -}; - -template -DBusResult demarshallVariant(const QVariant& variant) { - T value; - auto error = demarshallVariant(variant, QMetaType::fromType(), &value); - return DBusResult(value, error); -} - -void asyncReadPropertyInternal( - const QMetaType& type, - QDBusAbstractInterface& interface, - const QString& property, - std::function)> callback -); - -template -void asyncReadProperty( - QDBusAbstractInterface& interface, - const QString& property, - const std::function& callback -) { - asyncReadPropertyInternal( - QMetaType::fromType(), - interface, - property, - [callback](std::function internalCallback) { // NOLINT - T slot; - auto error = internalCallback(static_cast(&slot)); - callback(slot, error); - } - ); -} - -class DBusPropertyGroup; - -class DBusPropertyCore { -public: - DBusPropertyCore() = default; - virtual ~DBusPropertyCore() = default; - Q_DISABLE_COPY_MOVE(DBusPropertyCore); - - [[nodiscard]] virtual QString name() const = 0; - [[nodiscard]] virtual QStringView nameRef() const = 0; - [[nodiscard]] virtual QString valueString() = 0; - [[nodiscard]] virtual bool isRequired() const = 0; - [[nodiscard]] bool exists() const { return this->mExists; } - -protected: - virtual QDBusError store(const QVariant& variant) = 0; - [[nodiscard]] virtual QVariant serialize() = 0; - -private: - bool mExists : 1 = false; - - friend class DBusPropertyGroup; -}; - -// Default implementation with no transformation -template -struct DBusDataTransform { - using Wire = T; - using Data = T; -}; - -namespace bindable_p { - -template -struct BindableParams; - -template