Compare commits

..

1 commit

Author SHA1 Message Date
6e9bb4183c
hyprland/focus_grab: add HyprlandFocusGrab 2024-05-05 04:12:39 -07:00
376 changed files with 2112 additions and 33400 deletions

View file

@ -5,9 +5,6 @@ Checks: >
-*,
bugprone-*,
-bugprone-easily-swappable-parameters,
-bugprone-forward-declararion-namespace,
-bugprone-forward-declararion-namespace,
-bugprone-return-const-ref-from-parameter,
concurrency-*,
cppcoreguidelines-*,
-cppcoreguidelines-owning-memory,
@ -16,10 +13,8 @@ Checks: >
-cppcoreguidelines-avoid-const-or-ref-data-members,
-cppcoreguidelines-non-private-member-variables-in-classes,
-cppcoreguidelines-avoid-goto,
-cppcoreguidelines-pro-bounds-array-to-pointer-decay,
-cppcoreguidelines-avoid-do-while,
-cppcoreguidelines-pro-type-reinterpret-cast,
-cppcoreguidelines-pro-type-vararg,
google-build-using-namespace.
google-explicit-constructor,
google-global-names-in-headers,
google-readability-casting,
google-runtime-int,
@ -31,7 +26,6 @@ Checks: >
-modernize-return-braced-init-list,
-modernize-use-trailing-return-type,
performance-*,
-performance-avoid-endl,
portability-std-allocator-const,
readability-*,
-readability-function-cognitive-complexity,
@ -42,10 +36,6 @@ Checks: >
-readability-braces-around-statements,
-readability-redundant-access-specifiers,
-readability-else-after-return,
-readability-container-data-pointer,
-readability-implicit-bool-conversion,
-readability-avoid-nested-conditional-operator,
-readability-math-missing-parentheses,
tidyfox-*,
CheckOptions:
performance-for-range-copy.WarnOnAllAutoCopies: true

View file

@ -9,7 +9,3 @@ indent_style = tab
[*.nix]
indent_style = space
indent_size = 2
[*.{yml,yaml}]
indent_style = space
indent_size = 2

View file

@ -1 +0,0 @@
blank_issues_enabled: true

View file

@ -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: "<details> <summary>General information</summary>
```
<Paste the contents of the file here inside of the triple backticks>
```
</details>"
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 <path-to-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 <pid>` 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.

View file

@ -1,56 +0,0 @@
name: Build
on: [push, pull_request, workflow_dispatch]
jobs:
nix:
name: Nix
strategy:
matrix:
qtver: [qt6.8.1, qt6.8.0, qt6.7.3, qt6.7.2, qt6.7.1, qt6.7.0, qt6.6.3, qt6.6.2, qt6.6.1, qt6.6.0]
compiler: [clang, gcc]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# Use cachix action over detsys for testing with act.
# - uses: cachix/install-nix-action@v27
- uses: DeterminateSystems/nix-installer-action@main
- name: Download Dependencies
run: nix-build --no-out-link --expr '((import ./ci/matrix.nix) { qtver = "${{ matrix.qtver }}"; compiler = "${{ matrix.compiler }}"; }).inputDerivation'
- name: Build
run: nix-build --no-out-link --expr '(import ./ci/matrix.nix) { qtver = "${{ matrix.qtver }}"; compiler = "${{ matrix.compiler }}"; }'
archlinux:
name: Archlinux
runs-on: ubuntu-latest
container: archlinux
steps:
- uses: actions/checkout@v4
- name: Download Dependencies
run: |
pacman --noconfirm --noprogressbar -Syyu
pacman --noconfirm --noprogressbar -Sy \
base-devel \
cmake \
ninja \
pkgconf \
qt6-base \
qt6-declarative \
qt6-svg \
qt6-wayland \
qt6-shadertools \
wayland-protocols \
wayland \
libdrm \
libxcb \
libpipewire \
cli11 \
jemalloc
- name: Build
# breakpad is annoying to build in ci due to makepkg not running as root
run: |
cmake -GNinja -B build -DCRASH_REPORTER=OFF
cmake --build build

View file

@ -1,25 +0,0 @@
name: Lint
on: [push, pull_request, workflow_dispatch]
jobs:
lint:
name: Lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# Use cachix action over detsys for testing with act.
# - uses: cachix/install-nix-action@v27
- uses: DeterminateSystems/nix-installer-action@main
- uses: nicknovitski/nix-develop@v1
- name: Check formatting
run: clang-format -Werror --dry-run src/**/*.{cpp,hpp}
# required for lint
- name: Build
run: |
just configure debug -DNO_PCH=ON -DBUILD_TESTING=ON
just build
- name: Run lints
run: just lint-ci

4
.gitignore vendored
View file

@ -1,7 +1,3 @@
# related repos
/docs
/examples
# build files
/result
/build/

6
.gitmodules vendored Normal file
View file

@ -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

243
BUILD.md
View file

@ -1,243 +0,0 @@
# Build instructions
Instructions for building from source and distro packagers. We highly recommend
distro packagers read through this page fully.
## Packaging
If you are packaging quickshell for official or unofficial distribution channels,
such as a distro package repository, user repository, or other shared build location,
please set the following CMake flags.
`-DDISTRIBUTOR="your distribution platform"`
Please make this descriptive enough to identify your specific package, for example:
- `Official Nix Flake`
- `AUR (quickshell-git)`
- `Nixpkgs`
- `Fedora COPR (errornointernet/quickshell)`
`-DDISTRIBUTOR_DEBUGINFO_AVAILABLE=YES/NO`
If we can retrieve binaries and debug information for the package without actually running your
distribution (e.g. from an website), and you would like to strip the binary, please set this to `YES`.
If we cannot retrieve debug information, please set this to `NO` and
**ensure you aren't distributing stripped (non debuggable) binaries**.
In both cases you should build with `-DCMAKE_BUILD_TYPE=RelWithDebInfo` (then split or keep the debuginfo).
### QML Module dir
Currently all QML modules are statically linked to quickshell, but this is where
tooling information will go.
`-DINSTALL_QML_PREFIX="path/to/qml"`
`-DINSTALL_QMLDIR="/full/path/to/qml"`
`INSTALL_QML_PREFIX` works the same as `INSTALL_QMLDIR`, except it prepends `CMAKE_INSTALL_PREFIX`. You usually want this.
## Dependencies
Quickshell has a set of base dependencies you will always need, names vary by distro:
- `cmake`
- `qt6base`
- `qt6declarative`
- `qtshadertools` (build-time only)
- `spirv-tools` (build-time only)
- `pkg-config` (build-time only)
- `cli11`
On some distros, private Qt headers are in separate packages which you may have to install.
We currently require private headers for the following libraries:
- `qt6declarative`
- `qt6wayland`
We recommend an implicit dependency on `qt6svg`. If it is not installed, svg images and
svg icons will not work, including system ones.
At least Qt 6.6 is required.
All features are enabled by default and some have their own dependencies.
### Crash Reporter
The crash reporter catches crashes, restarts quickshell when it crashes,
and collects useful crash information in one place. Leaving this enabled will
enable us to fix bugs far more easily.
To disable: `-DCRASH_REPORTER=OFF`
Dependencies: `google-breakpad`
### Jemalloc
We recommend leaving Jemalloc enabled as it will mask memory fragmentation caused
by the QML engine, which results in much lower memory usage. Without this you
will get a perceived memory leak.
To disable: `-DUSE_JEMALLOC=OFF`
Dependencies: `jemalloc`
### Unix Sockets
This feature allows interaction with unix sockets and creating socket servers
which is useful for IPC and has no additional dependencies.
WARNING: Disabling unix sockets will NOT make it safe to run arbitrary code using quickshell.
There are many vectors which mallicious code can use to escape into your system.
To disable: `-DSOCKETS=OFF`
### Wayland
This feature enables wayland support. Subfeatures exist for each particular wayland integration.
WARNING: Wayland integration relies on features that are not part of the public Qt API and which
may break in minor releases. Updating quickshell's dependencies without ensuring without ensuring
that the current Qt version is supported WILL result in quickshell failing to build or misbehaving
at runtime.
Currently supported Qt versions: `6.6`, `6.7`.
To disable: `-DWAYLAND=OFF`
Dependencies:
- `qt6wayland`
- `wayland` (libwayland-client)
- `wayland-scanner` (may be part of your distro's wayland package)
- `wayland-protocols`
#### Wlroots Layershell
Enables wlroots layershell integration through the [zwlr-layer-shell-v1] protocol,
enabling use cases such as bars overlays and backgrounds.
This feature has no extra dependencies.
To disable: `-DWAYLAND_WLR_LAYERSHELL=OFF`
[zwlr-layer-shell-v1]: https://wayland.app/protocols/wlr-layer-shell-unstable-v1
#### Session Lock
Enables session lock support through the [ext-session-lock-v1] protocol,
which allows quickshell to be used as a session lock under compatible wayland compositors.
To disable: `-DWAYLAND_SESSION_LOCK=OFF`
[ext-session-lock-v1]: https://wayland.app/protocols/ext-session-lock-v1
#### Foreign Toplevel Management
Enables management of windows of other clients through the [zwlr-foreign-toplevel-management-v1] protocol,
which allows quickshell to be used as a session lock under compatible wayland compositors.
[zwlr-foreign-toplevel-management-v1]: https://wayland.app/protocols/wlr-foreign-toplevel-management-unstable-v1
To disable: `-DWAYLAND_TOPLEVEL_MANAGEMENT=OFF`
#### Screencopy
Enables streaming video from monitors and toplevel windows through various protocols.
To disable: `-DSCREENCOPY=OFF`
Dependencies:
- `libdrm`
- `libgbm`
Specific protocols can also be disabled:
- `DSCREENCOPY_ICC=OFF` - Disable screencopy via [ext-image-copy-capture-v1]
- `DSCREENCOPY_WLR=OFF` - Disable screencopy via [zwlr-screencopy-v1]
- `DSCREENCOPY_HYPRLAND_TOPLEVEL=OFF` - Disable screencopy via [hyprland-toplevel-export-v1]
[ext-image-copy-capture-v1]:https://wayland.app/protocols/ext-image-copy-capture-v1
[zwlr-screencopy-v1]: https://wayland.app/protocols/wlr-screencopy-unstable-v1
[hyprland-toplevel-export-v1]: https://wayland.app/protocols/hyprland-toplevel-export-v1
### X11
This feature enables x11 support. Currently this implements panel windows for X11 similarly
to the wlroots layershell above.
To disable: `-DX11=OFF`
Dependencies: `libxcb`
### Pipewire
This features enables viewing and management of pipewire nodes.
To disable: `-DSERVICE_PIPEWIRE=OFF`
Dependencies: `libpipewire`
### StatusNotifier / System Tray
This feature enables system tray support using the status notifier dbus protocol.
To disable: `-DSERVICE_STATUS_NOTIFIER=OFF`
Dependencies: `qt6dbus` (usually part of qt6base)
### MPRIS
This feature enables access to MPRIS compatible media players using its dbus protocol.
To disable: `-DSERVICE_MPRIS=OFF`
Dependencies: `qt6dbus` (usually part of qt6base)
### PAM
This feature enables PAM integration for user authentication.
To disable: `-DSERVICE_PAM=OFF`
Dependencies: `pam`
### Hyprland
This feature enables hyprland specific integrations. It requires wayland support
but has no extra dependencies.
To disable: `-DHYPRLAND=OFF`
#### Hyprland Global Shortcuts
Enables creation of global shortcuts under hyprland through the [hyprland-global-shortcuts-v1]
protocol. Generally a much nicer alternative to using unix sockets to implement the same thing.
This feature has no extra dependencies.
To disable: `-DHYPRLAND_GLOBAL_SHORTCUTS=OFF`
[hyprland-global-shortcuts-v1]: https://github.com/hyprwm/hyprland-protocols/blob/main/protocols/hyprland-global-shortcuts-v1.xml
#### Hyprland Focus Grab
Enables windows to grab focus similarly to a context menu under hyprland through the
[hyprland-focus-grab-v1] protocol. This feature has no extra dependencies.
To disable: `-DHYPRLAND_FOCUS_GRAB=OFF`
[hyprland-focus-grab-v1]: https://github.com/hyprwm/hyprland-protocols/blob/main/protocols/hyprland-focus-grab-v1.xml
### i3/Sway
Enables i3 and Sway specific features, does not have any dependency on Wayland or x11.
To disable: `-DI3=OFF`
#### i3/Sway IPC
Enables interfacing with i3 and Sway's IPC.
To disable: `-DI3_IPC=OFF`
## Building
*For developers and prospective contributors: See [CONTRIBUTING.md](CONTRIBUTING.md).*
We highly recommend using `ninja` to run the build, but you can use makefiles if you must.
#### Configuring the build
```sh
$ cmake -GNinja -B build -DCMAKE_BUILD_TYPE=RelWithDebInfo [additional disable flags from above here]
```
Note that features you do not supply dependencies for MUST be disabled with their associated flags
or quickshell will fail to build.
Additionally, note that clang builds much faster than gcc if you care.
#### Building
```sh
$ cmake --build build
```
#### Installing
```sh
$ cmake --install build
```

View file

@ -5,79 +5,41 @@ set(QT_MIN_VERSION "6.6.0")
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(QS_BUILD_OPTIONS "")
option(BUILD_TESTING "Build tests" OFF)
option(ASAN "Enable ASAN" OFF)
option(FRAME_POINTERS "Always keep frame pointers" ${ASAN})
function(boption VAR NAME DEFAULT)
cmake_parse_arguments(PARSE_ARGV 3 arg "" "REQUIRES" "")
option(${VAR} ${NAME} ${DEFAULT})
set(STATUS "${VAR}_status")
set(EFFECTIVE "${VAR}_effective")
set(${STATUS} ${${VAR}})
set(${EFFECTIVE} ${${VAR}})
if (${${VAR}} AND DEFINED arg_REQUIRES)
set(REQUIRED_EFFECTIVE "${arg_REQUIRES}_effective")
if (NOT ${${REQUIRED_EFFECTIVE}})
set(${STATUS} "OFF (Requires ${arg_REQUIRES})")
set(${EFFECTIVE} OFF)
endif()
endif()
set(${EFFECTIVE} "${${EFFECTIVE}}" PARENT_SCOPE)
message(STATUS " ${NAME}: ${${STATUS}}")
string(APPEND QS_BUILD_OPTIONS "\\n ${NAME}: ${${STATUS}}")
set(QS_BUILD_OPTIONS "${QS_BUILD_OPTIONS}" PARENT_SCOPE)
endfunction()
set(DISTRIBUTOR "Unset" CACHE STRING "Distributor")
string(APPEND QS_BUILD_OPTIONS " Distributor: ${DISTRIBUTOR}")
option(NVIDIA_COMPAT "Workarounds for nvidia gpus" OFF)
option(SOCKETS "Enable unix socket support" ON)
option(WAYLAND "Enable wayland support" ON)
option(WAYLAND_WLR_LAYERSHELL "Support the zwlr_layer_shell_v1 wayland protocol" ON)
option(WAYLAND_SESSION_LOCK "Support the ext_session_lock_v1 wayland protocol" ON)
option(HYPRLAND "Support hyprland specific features" ON)
option(SERVICE_STATUS_NOTIFIER "StatusNotifierItem service" ON)
message(STATUS "Quickshell configuration")
message(STATUS " Distributor: ${DISTRIBUTOR}")
boption(DISTRIBUTOR_DEBUGINFO_AVAILABLE "Distributor provided debuginfo" NO)
boption(NO_PCH "Disable precompild headers (dev)" OFF)
boption(BUILD_TESTING "Build tests (dev)" OFF)
boption(ASAN "ASAN (dev)" OFF) # note: better output with gcc than clang
boption(FRAME_POINTERS "Keep Frame Pointers (dev)" ${ASAN})
message(STATUS " NVIDIA workarounds: ${NVIDIA_COMPAT}")
message(STATUS " Build tests: ${BUILD_TESTING}")
message(STATUS " Sockets: ${SOCKETS}")
message(STATUS " Wayland: ${WAYLAND}")
if (WAYLAND)
message(STATUS " Wlroots Layershell: ${WAYLAND_WLR_LAYERSHELL}")
message(STATUS " Session Lock: ${WAYLAND_SESSION_LOCK}")
endif ()
message(STATUS " Services")
message(STATUS " StatusNotifier: ${SERVICE_STATUS_NOTIFIER}")
message(STATUS " Hyprland: ${HYPRLAND}")
boption(CRASH_REPORTER "Crash Handling" ON)
boption(USE_JEMALLOC "Use jemalloc" ON)
boption(SOCKETS "Unix Sockets" ON)
boption(WAYLAND "Wayland" ON)
boption(WAYLAND_WLR_LAYERSHELL " Wlroots Layer-Shell" ON REQUIRES WAYLAND)
boption(WAYLAND_SESSION_LOCK " Session Lock" ON REQUIRES WAYLAND)
boption(WAYLAND_TOPLEVEL_MANAGEMENT " Foreign Toplevel Management" ON REQUIRES WAYLAND)
boption(HYPRLAND " Hyprland" ON REQUIRES WAYLAND)
boption(HYPRLAND_IPC " Hyprland IPC" ON REQUIRES HYPRLAND)
boption(HYPRLAND_GLOBAL_SHORTCUTS " Hyprland Global Shortcuts" ON REQUIRES HYPRLAND)
boption(HYPRLAND_FOCUS_GRAB " Hyprland Focus Grabbing" ON REQUIRES HYPRLAND)
boption(HYPRLAND_SURFACE_EXTENSIONS " Hyprland Surface Extensions" ON REQUIRES HYPRLAND)
boption(SCREENCOPY " Screencopy" ON REQUIRES WAYLAND)
boption(SCREENCOPY_ICC " Image Copy Capture" ON REQUIRES WAYLAND)
boption(SCREENCOPY_WLR " Wlroots Screencopy" ON REQUIRES WAYLAND)
boption(SCREENCOPY_HYPRLAND_TOPLEVEL " Hyprland Toplevel Export" ON REQUIRES WAYLAND)
boption(X11 "X11" ON)
boption(I3 "I3/Sway" ON)
boption(I3_IPC " I3/Sway IPC" ON REQUIRES I3)
boption(SERVICE_STATUS_NOTIFIER "System Tray" ON)
boption(SERVICE_PIPEWIRE "PipeWire" ON)
boption(SERVICE_MPRIS "Mpris" ON)
boption(SERVICE_PAM "Pam" ON)
boption(SERVICE_GREETD "Greetd" ON)
boption(SERVICE_UPOWER "UPower" ON)
boption(SERVICE_NOTIFICATIONS "Notifications" ON)
if (NOT DEFINED GIT_REVISION)
execute_process(
COMMAND git rev-parse HEAD
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
OUTPUT_VARIABLE GIT_REVISION
OUTPUT_STRIP_TRAILING_WHITESPACE
)
endif()
include(cmake/install-qml-module.cmake)
include(cmake/util.cmake)
add_compile_options(-Wall -Wextra -Wno-vla-cxx-extension)
# pipewire defines this, breaking PCH
add_compile_definitions(_REENTRANT)
add_compile_options(-Wall -Wextra)
if (FRAME_POINTERS)
add_compile_options(-fno-omit-frame-pointer)
@ -98,9 +60,8 @@ if (NOT CMAKE_BUILD_TYPE)
set(CMAKE_BUILD_TYPE Debug)
endif()
set(QT_FPDEPS Gui Qml Quick QuickControls2 Widgets ShaderTools)
include(cmake/pch.cmake)
set(QT_DEPS Qt6::Gui Qt6::Qml Qt6::Quick Qt6::QuickControls2 Qt6::Widgets)
set(QT_FPDEPS Gui Qml Quick QuickControls2 Widgets)
if (BUILD_TESTING)
enable_testing()
@ -109,39 +70,58 @@ if (BUILD_TESTING)
endif()
if (SOCKETS)
list(APPEND QT_DEPS Qt6::Network)
list(APPEND QT_FPDEPS Network)
endif()
if (WAYLAND)
list(APPEND QT_DEPS Qt6::WaylandClient Qt6::WaylandClientPrivate)
list(APPEND QT_FPDEPS WaylandClient)
endif()
if (SERVICE_STATUS_NOTIFIER OR SERVICE_MPRIS OR SERVICE_UPOWER OR SERVICE_NOTIFICATIONS)
if (SERVICE_STATUS_NOTIFIER)
set(DBUS ON)
endif()
if (DBUS)
list(APPEND QT_DEPS Qt6::DBus)
list(APPEND QT_FPDEPS DBus)
endif()
find_package(Qt6 REQUIRED COMPONENTS ${QT_FPDEPS})
set(CMAKE_AUTOUIC OFF)
qt_standard_project_setup(REQUIRES 6.6)
set(QT_QML_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/qml_modules)
add_subdirectory(src)
# pch breaks clang-tidy..... somehow
if (NOT NO_PCH)
file(GENERATE
OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/pchstub.cpp
CONTENT "// intentionally empty"
)
if (USE_JEMALLOC)
find_package(PkgConfig REQUIRED)
# IMPORTED_TARGET not working for some reason
pkg_check_modules(JEMALLOC REQUIRED jemalloc)
target_link_libraries(quickshell PRIVATE ${JEMALLOC_LIBRARIES})
add_library(qt-pch ${CMAKE_CURRENT_BINARY_DIR}/pchstub.cpp)
target_link_libraries(qt-pch PRIVATE ${QT_DEPS})
target_precompile_headers(qt-pch PUBLIC
<memory>
<qobject.h>
<qqmlengine.h>
<qlist.h>
<qcolor.h>
<qquickitem.h>
<qevent.h>
)
endif()
install(CODE "
execute_process(
COMMAND ${CMAKE_COMMAND} -E create_symlink \
${CMAKE_INSTALL_FULL_BINDIR}/quickshell \$ENV{DESTDIR}${CMAKE_INSTALL_FULL_BINDIR}/qs
)
")
function (qs_pch target)
if (NOT NO_PCH)
target_precompile_headers(${target} REUSE_FROM qt-pch)
target_link_libraries(${target} PRIVATE ${QT_DEPS}) # required for gcc to accept the pch on plugin targets
endif()
endfunction()
if (NVIDIA_COMPAT)
add_compile_definitions(NVIDIA_COMPAT)
endif()
add_subdirectory(src)

View file

@ -1,102 +0,0 @@
# Contributing / Development
Instructions for development setup and upstreaming patches.
If you just want to build or package quickshell see [BUILD.md](BUILD.md).
## Development
Install the dependencies listed in [BUILD.md](BUILD.md).
You probably want all of them even if you don't use all of them
to ensure tests work correctly and avoid passing a bunch of configure
flags when you need to wipe the build directory.
Quickshell also uses `just` for common development command aliases.
The dependencies are also available as a nix shell or nix flake which we recommend
using with nix-direnv.
Common aliases:
- `just configure [<debug|release> [extra cmake args]]` (note that you must specify debug/release to specify extra args)
- `just build` - runs the build, configuring if not configured already.
- `just run [args]` - runs quickshell with the given arguments
- `just clean` - clean up build artifacts. `just clean build` is somewhat common.
### Formatting
All contributions should be formatted similarly to what already exists.
Group related functionality together.
Run the formatter using `just fmt`.
If the results look stupid, fix the clang-format file if possible,
or disable clang-format in the affected area
using `// clang-format off` and `// clang-format on`.
### Linter
All contributions should pass the linter.
Note that running the linter requires disabling precompiled
headers and including the test codepaths:
```sh
$ just configure debug -DNO_PCH=ON -DBUILD_TESTING=ON
$ just lint
```
If the linter is complaining about something that you think it should not,
please disable the lint in your MR and explain your reasoning.
### Tests
If you feel like the feature you are working on is very complex or likely to break,
please write some tests. We will ask you to directly if you send in an MR for an
overly complex or breakable feature.
At least all tests that passed before your changes should still be passing
by the time your contribution is ready.
You can run the tests using `just test` but you must enable them first
using `-DBUILD_TESTING=ON`.
### Documentation
Most of quickshell's documentation is automatically generated from the source code.
You should annotate `Q_PROPERTY`s and `Q_INVOKABLE`s with doc comments. Note that the parser
cannot handle random line breaks and will usually require you to disable clang-format if the
lines are too long.
Before submitting an MR, if adding new features please make sure the documentation is generated
reasonably using the `quickshell-docs` repo. We recommend checking it out at `/docs` in this repo.
Doc comments take the form `///` or `///!` (summary) and work with markdown.
You can reference other types using the `@@[Module.][Type.][member]` shorthand
where all parts are optional. If module or type are not specified they will
be inferred as the current module. Member can be a `property`, `function()` or `signal(s)`.
Look at existing code for how it works.
Quickshell modules additionally have a `module.md` file which contains a summary, description,
and list of headers to scan for documentation.
## Contributing
### Commits
Please structure your commit messages as `scope[!]: commit` where
the scope is something like `core` or `service/mpris`. (pick what has been
used historically or what makes sense if new.) Add `!` for changes that break
existing APIs or functionality.
Commit descriptions should contain a summary of the changes if they are not
sufficiently addressed in the commit message.
Please squash/rebase additions or edits to previous changes and follow the
commit style to keep the history easily searchable at a glance.
Depending on the change, it is often reasonable to squash it into just
a single commit. (If you do not follow this we will squash your changes
for you.)
### Sending patches
You may contribute by submitting a pull request on github, asking for
an account on our git server, or emailing patches / git bundles
directly to `outfoxxed@outfoxxed.me`.
### Getting help
If you're getting stuck, you can come talk to us in the
[quickshell-development matrix room](https://matrix.to/#/#quickshell-development:outfoxxed.me)
for help on implementation, conventions, etc.
Feel free to ask for advice early in your implementation if you are
unsure.

View file

@ -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}} \

112
README.md
View file

@ -1,7 +1,7 @@
# quickshell
<a href="https://matrix.to/#/#quickshell:outfoxxed.me"><img src="https://img.shields.io/badge/Join%20the%20matrix%20room-%23quickshell:outfoxxed.me-0dbd8b?logo=matrix&style=flat-square"></a>
Flexbile QtQuick based desktop shell toolkit.
Simple and flexbile QtQuick based desktop shell toolkit.
Hosted on: [outfoxxed's gitea], [github]
@ -11,14 +11,21 @@ Hosted on: [outfoxxed's gitea], [github]
Documentation available at [quickshell.outfoxxed.me](https://quickshell.outfoxxed.me) or
can be built from the [quickshell-docs](https://git.outfoxxed.me/outfoxxed/quickshell-docs) repo.
Some fully working examples can be found in the [quickshell-examples](https://git.outfoxxed.me/outfoxxed/quickshell-examples)
Some fully working examples can be found in the [quickshell-examples](https://git.outfoxxed.me/outfoxxed/quickshell-examples)
repo.
# Breaking Changes
Quickshell is still in alpha and there will be breaking changes.
Both the documentation and examples are included as submodules with revisions that work with the current
version of quickshell.
Commits with breaking qml api changes will contain a `!` at the end of the scope
(`thing!: foo`) and the commit description will contain details about the broken api.
You can clone everything with
```
$ git clone --recursive https://git.outfoxxed.me/outfoxxed/quickshell.git
```
Or clone missing submodules later with
```
$ git submodule update --init --recursive
```
# Installation
@ -32,9 +39,6 @@ This repo has a nix flake you can use to install the package directly:
quickshell = {
url = "git+https://git.outfoxxed.me/outfoxxed/quickshell";
# THIS IS IMPORTANT
# Mismatched system dependencies will lead to crashes and other issues.
inputs.nixpkgs.follows = "nixpkgs";
};
};
@ -44,45 +48,75 @@ This repo has a nix flake you can use to install the package directly:
Quickshell's binary is available at `quickshell.packages.<system>.default` to be added to
lists such as `environment.systemPackages` or `home.packages`.
The package contains several features detailed in [BUILD.md](BUILD.md) which can be enabled
or disabled with overrides:
```nix
quickshell.packages.<system>.default.override {
withJemalloc = true;
withQtSvg = true;
withWayland = true;
withX11 = true;
withPipewire = true;
withPam = true;
withHyprland = true;
}
```
`quickshell.packages.<system>.nvidia` is also available for nvidia users which fixes some
common crashes.
Note: by default this package is built with clang as it is significantly faster.
## Arch (AUR)
Quickshell has a third party [AUR package] available under the same name.
It is not managed by us and should be looked over before use.
## Manual
[AUR package]: https://aur.archlinux.org/packages/quickshell
If not using nix, you'll have to build from source.
> [!CAUTION]
> The AUR provides no way to force the quickshell package to rebuild when the Qt version changes.
> If you experience crashes after updating Qt, please try rebuilding Quickshell against the
> current Qt version before opening an issue.
### Dependencies
To build quickshell at all, you will need the following packages (names may vary by distro)
## Fedora (COPR)
Quickshell has a third party [Fedora COPR package] available under the same name.
It is not managed by us and should be looked over before use.
- just
- cmake
- pkg-config
- ninja
- Qt6 [ QtBase, QtDeclarative ]
[Fedora COPR package]: https://copr.fedorainfracloud.org/coprs/errornointernet/quickshell
To build with wayland support you will additionally need:
- wayland
- wayland-scanner (may be part of wayland on some distros)
- wayland-protocols
- Qt6 [ QtWayland ]
## Anything else
See [BUILD.md](BUILD.md) for instructions on building and packaging quickshell.
### Building
# Contributing / Development
See [CONTRIBUTING.md](CONTRIBUTING.md) for details.
To make a release build of quickshell run:
```sh
$ just release
```
If running an nvidia GPU, instead run:
```sh
$ just configure release -DNVIDIA_COMPAT=ON
$ just build
```
(These commands are just aliases for cmake commands you can run directly,
see the Justfile for more information.)
If you have all the dependencies installed and they are in expected
locations this will build correctly.
To install to /usr/local/bin run as root (usually `sudo`) in the same folder:
```
$ just install
```
### Building (Nix)
You can build directly using the provided nix flake or nix package.
```
nix build
nix build -f package.nix # calls default.nix with a basic callPackage expression
```
# Development
For nix there is a devshell available from `shell.nix` and as a devShell
output from the flake.
The Justfile contains various useful aliases:
- `just configure [<debug|release> [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

View file

@ -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

View file

@ -1,63 +0,0 @@
let
byCommit = {
commit,
sha256,
}: import (builtins.fetchTarball {
name = "nixpkgs-${commit}";
url = "https://github.com/nixos/nixpkgs/archive/${commit}.tar.gz";
inherit sha256;
}) {};
in {
# For old qt versions, grab the commit before the version bump that has all the patches
# instead of the bumped version.
qt6_8_1 = byCommit {
commit = "3df3c47c19dc90fec35359e89ffb52b34d2b0e94";
sha256 = "1lhlm7czhwwys5ak6ngb5li6bxddilb9479k9nkss502kw8hwjyz";
};
qt6_8_0 = byCommit {
commit = "352f462ad9d2aa2cde75fdd8f1734e86402a3ff6";
sha256 = "02zfgkr9fpd6iwfh6dcr3m6fnx61jppm3v081f3brvkqwmmz7zq1";
};
qt6_7_3 = byCommit {
commit = "273673e839189c26130d48993d849a84199523e6";
sha256 = "0aca369hdxb8j0vx9791anyzy4m65zckx0lriicqhp95kv9q6m7z";
};
qt6_7_2 = byCommit {
commit = "841f166ff96fc2f3ecd1c0cc08072633033d41bf";
sha256 = "0d7p0cp7zjiadhpa6sdafxvrpw4lnmb1h673w17q615vm1yaasvy";
};
qt6_7_1 = byCommit {
commit = "69bee9866a4e2708b3153fdb61c1425e7857d6b8";
sha256 = "1an4sha4jsa29dvc4n9mqxbq8jjwg7frl0rhy085g73m7l1yx0lj";
};
qt6_7_0 = byCommit {
commit = "4fbbc17ccf11bc80002b19b31387c9c80276f076";
sha256 = "09lhgdqlx8j9a7vpdcf8sddlhbzjq0s208spfmxfjdn14fvx8k0j";
};
qt6_6_3 = byCommit {
commit = "8f1a3fbaa92f1d59b09f2d24af6a607b5a280071";
sha256 = "0322zwxvmg8v2wkm03xpk6mqmmbfjgrhc9prcx0zd36vjl6jmi18";
};
qt6_6_2 = byCommit {
commit = "0bb9cfbd69459488576a0ef3c0e0477bedc3a29e";
sha256 = "172ww486jm1mczk9id78s32p7ps9m9qgisml286flc8jffb6yad8";
};
qt6_6_1 = byCommit {
commit = "8eecc3342103c38eea666309a7c0d90d403a039a";
sha256 = "1lakc0immsgrpz3basaysdvd0sx01r0mcbyymx6id12fk0404z5r";
};
qt6_6_0 = byCommit {
commit = "1ded005f95a43953112ffc54b39593ea2f16409f";
sha256 = "1xvyd3lj81hak9j53mrhdsqx78x5v2ppv8m2s54qa2099anqgm0f";
};
}

View file

@ -1,7 +0,0 @@
{
clangStdenv,
gccStdenv,
}: {
clang = { buildStdenv = clangStdenv; };
gcc = { buildStdenv = gccStdenv; };
}

View file

@ -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()

View file

@ -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
<chrono>
<memory>
<vector>
<qdebug.h>
<qobject.h>
<qmetatype.h>
<qstring.h>
<qchar.h>
<qlist.h>
<qabstractitemmodel.h>
)
qs_add_pchset(common
DEPENDENCIES Qt::Quick
HEADERS ${COMMON_PCH_SET}
)
qs_add_pchset(large
DEPENDENCIES Qt::Quick
HEADERS
${COMMON_PCH_SET}
<qiodevice.h>
<qevent.h>
<qcoreapplication.h>
<qqmlengine.h>
<qquickitem.h>
<qquickwindow.h>
<qcolor.h>
<qdir.h>
<qtimer.h>
<qabstractitemmodel.h>
)
# including qplugin.h directly will cause required symbols to disappear
qs_add_pchset(plugin
DEPENDENCIES Qt::Qml
HEADERS
<qobject.h>
<qjsonobject.h>
<qpointer.h>
)

View file

@ -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()

View file

@ -3,22 +3,13 @@
nix-gitignore,
pkgs,
keepDebugInfo,
buildStdenv ? pkgs.clangStdenv,
buildStdenv ? pkgs.clang17Stdenv,
cmake,
ninja,
qt6,
spirv-tools,
cli11,
breakpad,
jemalloc,
wayland,
wayland-protocols,
libdrm,
libgbm ? null,
xorg,
pipewire,
pam,
gitRev ? (let
headExists = builtins.pathExists ./.git/HEAD;
@ -32,15 +23,9 @@
else "unknown"),
debug ? false,
withCrashReporter ? true,
withJemalloc ? true, # masks heap fragmentation
withQtSvg ? true,
withWayland ? true,
withX11 ? true,
withPipewire ? true,
withPam ? true,
withHyprland ? true,
withI3 ? true,
enableWayland ? true,
nvidiaCompat ? false,
svgSupport ? true, # you almost always want this
}: buildStdenv.mkDerivation {
pname = "quickshell${lib.optionalString debug "-debug"}";
version = "0.1.0";
@ -49,55 +34,43 @@
nativeBuildInputs = with pkgs; [
cmake
ninja
qt6.qtshadertools
spirv-tools
qt6.wrapQtAppsHook
] ++ (lib.optionals enableWayland [
pkg-config
] ++ (lib.optionals withWayland [
wayland-protocols
wayland-scanner
]);
buildInputs = [
buildInputs = with pkgs; [
qt6.qtbase
qt6.qtdeclarative
cli11
]
++ lib.optional withCrashReporter breakpad
++ lib.optional withJemalloc jemalloc
++ lib.optional withQtSvg qt6.qtsvg
++ lib.optionals withWayland ([ qt6.qtwayland wayland ] ++ (if libgbm != null then [ libdrm libgbm ] else []))
++ lib.optional withX11 xorg.libxcb
++ lib.optional withPam pam
++ lib.optional withPipewire pipewire;
++ (lib.optionals enableWayland [ qt6.qtwayland wayland ])
++ (lib.optionals svgSupport [ qt6.qtsvg ]);
cmakeBuildType = if debug then "Debug" else "RelWithDebInfo";
QTWAYLANDSCANNER = lib.optionalString enableWayland "${qt6.qtwayland}/libexec/qtwaylandscanner";
configurePhase = let
cmakeBuildType = if debug
then "Debug"
else "RelWithDebInfo";
in ''
cmakeBuildType=${cmakeBuildType} # qt6 setup hook resets this for some godforsaken reason
cmakeConfigurePhase
'';
cmakeFlags = [
(lib.cmakeFeature "DISTRIBUTOR" "Official-Nix-Flake")
(lib.cmakeFeature "INSTALL_QML_PREFIX" qt6.qtbase.qtQmlPrefix)
(lib.cmakeBool "DISTRIBUTOR_DEBUGINFO_AVAILABLE" true)
(lib.cmakeFeature "GIT_REVISION" gitRev)
(lib.cmakeBool "CRASH_REPORTER" withCrashReporter)
(lib.cmakeBool "USE_JEMALLOC" withJemalloc)
(lib.cmakeBool "WAYLAND" withWayland)
(lib.cmakeBool "SCREENCOPY" (libgbm != null))
(lib.cmakeBool "SERVICE_PIPEWIRE" withPipewire)
(lib.cmakeBool "SERVICE_PAM" withPam)
(lib.cmakeBool "HYPRLAND" withHyprland)
(lib.cmakeBool "I3" withI3)
];
"-DGIT_REVISION=${gitRev}"
] ++ lib.optional (!enableWayland) "-DWAYLAND=OFF"
++ lib.optional nvidiaCompat "-DNVIDIA_COMPAT=ON";
# How to get debuginfo in gdb from a release build:
# 1. build `quickshell.debug`
# 2. set NIX_DEBUG_INFO_DIRS="<quickshell.debug store path>/lib/debug"
# 3. launch gdb / coredumpctl and debuginfo will work
separateDebugInfo = !debug;
dontStrip = debug;
buildPhase = "ninjaBuildPhase";
enableParallelBuilding = true;
dontStrip = true;
meta = with lib; {
homepage = "https://git.outfoxxed.me/outfoxxed/quickshell";
description = "Flexbile QtQuick based desktop shell toolkit";
description = "Simple and flexbile QtQuick based desktop shell toolkit";
license = licenses.lgpl3Only;
platforms = platforms.linux;
};

1
docs Submodule

@ -0,0 +1 @@
Subproject commit 149b784a5a4c40ada67cb9f6af5a5350678ab6d4

1
examples Submodule

@ -0,0 +1 @@
Subproject commit b9e744b50673304dfddb68f3da2a2e906d028b96

6
flake.lock generated
View file

@ -2,11 +2,11 @@
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1736012469,
"narHash": "sha256-/qlNWm/IEVVH7GfgAIyP6EsVZI6zjAx1cV5zNyrs+rI=",
"lastModified": 1709237383,
"narHash": "sha256-cy6ArO4k5qTx+l5o+0mL9f5fa86tYUX3ozE1S+Txlds=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "8f3e1f807051e32d8c95cd12b9b421623850a34d",
"rev": "1536926ef5621b09bba54035ae2bb6d806d72ac8",
"type": "github"
},
"original": {

View file

@ -12,8 +12,10 @@
quickshell = pkgs.callPackage ./default.nix {
gitRev = self.rev or self.dirtyRev;
};
quickshell-nvidia = quickshell.override { nvidiaCompat = true; };
default = quickshell;
nvidia = quickshell-nvidia;
});
devShells = forEachSystem (system: pkgs: rec {

View file

@ -15,12 +15,13 @@ in pkgs.mkShell.override { stdenv = quickshell.stdenv; } {
nativeBuildInputs = with pkgs; [
just
clang-tools
clang-tools_17
parallel
makeWrapper
];
TIDYFOX = "${tidyfox}/lib/libtidyfox.so";
QTWAYLANDSCANNER = quickshell.QTWAYLANDSCANNER;
shellHook = ''
export CMAKE_BUILD_PARALLEL_LEVEL=$(nproc)

View file

@ -2,18 +2,8 @@ qt_add_executable(quickshell main.cpp)
install(TARGETS quickshell RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR})
add_subdirectory(build)
add_subdirectory(launch)
add_subdirectory(core)
add_subdirectory(debug)
add_subdirectory(ipc)
add_subdirectory(window)
add_subdirectory(io)
add_subdirectory(widgets)
if (CRASH_REPORTER)
add_subdirectory(crash)
endif()
if (DBUS)
add_subdirectory(dbus)
@ -21,10 +11,6 @@ endif()
if (WAYLAND)
add_subdirectory(wayland)
endif()
if (X11)
add_subdirectory(x11)
endif()
endif ()
add_subdirectory(services)

View file

@ -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})

View file

@ -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

View file

@ -1,14 +1,20 @@
qt_add_library(quickshell-core STATIC
main.cpp
plugin.cpp
shell.cpp
variants.cpp
rootwrapper.cpp
proxywindow.cpp
reload.cpp
rootwrapper.cpp
qmlglobal.cpp
qmlscreen.cpp
region.cpp
persistentprops.cpp
windowinterface.cpp
floatingwindow.cpp
panelinterface.cpp
popupwindow.cpp
singleton.cpp
generation.cpp
scan.cpp
@ -20,38 +26,13 @@ qt_add_library(quickshell-core STATIC
imageprovider.cpp
transformwatcher.cpp
boundcomponent.cpp
model.cpp
elapsedtimer.cpp
desktopentry.cpp
objectrepeater.cpp
platformmenu.cpp
qsmenu.cpp
retainable.cpp
popupanchor.cpp
types.cpp
qsmenuanchor.cpp
clock.cpp
logging.cpp
paths.cpp
instanceinfo.cpp
common.cpp
iconprovider.cpp
scriptmodel.cpp
)
qt_add_qml_module(quickshell-core
URI Quickshell
VERSION 0.1
DEPENDENCIES QtQuick
OPTIONAL_IMPORTS Quickshell._Window
DEFAULT_IMPORTS Quickshell._Window
)
set_source_files_properties(main.cpp PROPERTIES COMPILE_DEFINITIONS GIT_REVISION="${GIT_REVISION}")
qt_add_qml_module(quickshell-core URI Quickshell VERSION 0.1)
install_qml_module(quickshell-core)
target_link_libraries(quickshell-core PRIVATE Qt::Quick Qt::Widgets)
qs_module_pch(quickshell-core SET large)
target_link_libraries(quickshell-core PRIVATE ${QT_DEPS})
qs_pch(quickshell-core)
target_link_libraries(quickshell PRIVATE quickshell-coreplugin)

View file

@ -1,88 +0,0 @@
#include "clock.hpp"
#include <qdatetime.h>
#include <qobject.h>
#include <qtimer.h>
#include <qtmetamacros.h>
#include <qtypes.h>
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<qint32>(delay));
this->targetTime = nextTime;
}

View file

@ -1,91 +0,0 @@
#pragma once
#include <qdatetime.h>
#include <qobject.h>
#include <qqmlintegration.h>
#include <qtimer.h>
#include <qtmetamacros.h>
#include <qtypes.h>
///! 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);
};

View file

@ -1,9 +0,0 @@
#include "common.hpp"
#include <qdatetime.h>
namespace qs {
const QDateTime Common::LAUNCH_TIME = QDateTime::currentDateTime();
}

View file

@ -1,11 +0,0 @@
#pragma once
#include <qdatetime.h>
namespace qs {
struct Common {
static const QDateTime LAUNCH_TIME;
};
} // namespace qs

View file

@ -1,391 +0,0 @@
#include "desktopentry.hpp"
#include <qcontainerfwd.h>
#include <qdebug.h>
#include <qdir.h>
#include <qfileinfo.h>
#include <qhash.h>
#include <qlist.h>
#include <qlogging.h>
#include <qloggingcategory.h>
#include <qnamespace.h>
#include <qobject.h>
#include <qpair.h>
#include <qprocess.h>
#include <qstringview.h>
#include <qtenvironmentvariables.h>
#include <ranges>
#include "model.hpp"
namespace {
Q_LOGGING_CATEGORY(logDesktopEntry, "quickshell.desktopentry", QtWarningMsg);
}
struct Locale {
explicit Locale() = default;
explicit Locale(const QString& string) {
auto territoryIdx = string.indexOf('_');
auto codesetIdx = string.indexOf('.');
auto modifierIdx = string.indexOf('@');
auto parseEnd = string.length();
if (modifierIdx != -1) {
this->modifier = string.sliced(modifierIdx + 1, parseEnd - modifierIdx - 1);
parseEnd = modifierIdx;
}
if (codesetIdx != -1) {
parseEnd = codesetIdx;
}
if (territoryIdx != -1) {
this->territory = string.sliced(territoryIdx + 1, parseEnd - territoryIdx - 1);
parseEnd = territoryIdx;
}
this->language = string.sliced(0, parseEnd);
}
[[nodiscard]] bool isValid() const { return !this->language.isEmpty(); }
[[nodiscard]] int matchScore(const Locale& other) const {
if (this->language != other.language) return 0;
auto territoryMatches = !this->territory.isEmpty() && this->territory == other.territory;
auto modifierMatches = !this->modifier.isEmpty() && this->modifier == other.modifier;
auto score = 1;
if (territoryMatches) score += 2;
if (modifierMatches) score += 1;
return score;
}
static const Locale& system() {
static Locale* locale = nullptr; // NOLINT
if (locale == nullptr) {
auto lstr = qEnvironmentVariable("LC_MESSAGES");
if (lstr.isEmpty()) lstr = qEnvironmentVariable("LANG");
locale = new Locale(lstr);
}
return *locale;
}
QString language;
QString territory;
QString modifier;
};
// NOLINTNEXTLINE(misc-use-internal-linkage)
QDebug operator<<(QDebug debug, const Locale& locale) {
auto saver = QDebugStateSaver(debug);
debug.nospace() << "Locale(language=" << locale.language << ", territory=" << locale.territory
<< ", modifier" << locale.modifier << ')';
return debug;
}
void DesktopEntry::parseEntry(const QString& text) {
const auto& system = Locale::system();
auto groupName = QString();
auto entries = QHash<QString, QPair<Locale, QString>>();
auto finishCategory = [this, &groupName, &entries]() {
if (groupName == "Desktop Entry") {
if (entries["Type"].second != "Application") return;
if (entries.contains("Hidden") && entries["Hidden"].second == "true") return;
for (const auto& [key, pair]: entries.asKeyValueRange()) {
auto& [_, value] = pair;
this->mEntries.insert(key, value);
if (key == "Name") this->mName = value;
else if (key == "GenericName") this->mGenericName = value;
else if (key == "NoDisplay") this->mNoDisplay = value == "true";
else if (key == "Comment") this->mComment = value;
else if (key == "Icon") this->mIcon = value;
else if (key == "Exec") this->mExecString = value;
else if (key == "Path") this->mWorkingDirectory = value;
else if (key == "Terminal") this->mTerminal = value == "true";
else if (key == "Categories") this->mCategories = value.split(u';', Qt::SkipEmptyParts);
else if (key == "Keywords") this->mKeywords = value.split(u';', Qt::SkipEmptyParts);
}
} else if (groupName.startsWith("Desktop Action ")) {
auto actionName = groupName.sliced(16);
auto* action = new DesktopAction(actionName, this);
for (const auto& [key, pair]: entries.asKeyValueRange()) {
const auto& [_, value] = pair;
action->mEntries.insert(key, value);
if (key == "Name") action->mName = value;
else if (key == "Icon") action->mIcon = value;
else if (key == "Exec") action->mExecString = value;
}
this->mActions.insert(actionName, action);
}
entries.clear();
};
for (auto& line: text.split(u'\n', Qt::SkipEmptyParts)) {
if (line.startsWith(u'#')) continue;
if (line.startsWith(u'[') && line.endsWith(u']')) {
finishCategory();
groupName = line.sliced(1, line.length() - 2);
continue;
}
auto splitIdx = line.indexOf(u'=');
if (splitIdx == -1) {
qCWarning(logDesktopEntry) << "Encountered invalid line in desktop entry (no =)" << line;
continue;
}
auto key = line.sliced(0, splitIdx);
const auto& value = line.sliced(splitIdx + 1);
auto localeIdx = key.indexOf('[');
Locale locale;
if (localeIdx != -1 && localeIdx != key.length() - 1) {
locale = Locale(key.sliced(localeIdx + 1, key.length() - localeIdx - 2));
key = key.sliced(0, localeIdx);
}
if (entries.contains(key)) {
const auto& old = entries.value(key);
auto oldScore = system.matchScore(old.first);
auto newScore = system.matchScore(locale);
if (newScore > oldScore || (oldScore == 0 && !locale.isValid())) {
entries.insert(key, qMakePair(locale, value));
}
} else {
entries.insert(key, qMakePair(locale, value));
}
}
finishCategory();
}
void DesktopEntry::execute() const {
DesktopEntry::doExec(this->mExecString, this->mWorkingDirectory);
}
bool DesktopEntry::isValid() const { return !this->mName.isEmpty(); }
bool DesktopEntry::noDisplay() const { return this->mNoDisplay; }
QVector<DesktopAction*> DesktopEntry::actions() const { return this->mActions.values(); }
QVector<QString> DesktopEntry::parseExecString(const QString& execString) {
QVector<QString> arguments;
QString currentArgument;
auto parsingString = false;
auto escape = 0;
auto percent = false;
for (auto c: execString) {
if (escape == 0 && c == u'\\') {
escape = 1;
} else if (parsingString) {
if (c == '\\') {
escape++;
if (escape == 4) {
currentArgument += '\\';
escape = 0;
}
} else if (escape != 0) {
if (escape != 2) {
// Technically this is an illegal state, but the spec has a terrible double escape
// rule in strings for no discernable reason. Assuming someone might understandably
// misunderstand it, treat it as a normal escape and log it.
qCWarning(logDesktopEntry).noquote()
<< "Illegal escape sequence in desktop entry exec string:" << execString;
}
currentArgument += c;
escape = 0;
} else if (c == u'"' || c == u'\'') {
parsingString = false;
} else {
currentArgument += c;
}
} else if (escape != 0) {
currentArgument += c;
escape = 0;
} else if (percent) {
if (c == '%') {
currentArgument += '%';
} // else discard
percent = false;
} else if (c == '%') {
percent = true;
} else if (c == u'"' || c == u'\'') {
parsingString = true;
} else if (c == u' ') {
if (!currentArgument.isEmpty()) {
arguments.push_back(currentArgument);
currentArgument.clear();
}
} else {
currentArgument += c;
}
}
if (!currentArgument.isEmpty()) {
arguments.push_back(currentArgument);
currentArgument.clear();
}
return arguments;
}
void DesktopEntry::doExec(const QString& execString, const QString& workingDirectory) {
auto args = DesktopEntry::parseExecString(execString);
if (args.isEmpty()) {
qCWarning(logDesktopEntry) << "Tried to exec string" << execString << "which parsed as empty.";
return;
}
auto process = QProcess();
process.setProgram(args.at(0));
process.setArguments(args.sliced(1));
if (!workingDirectory.isEmpty()) process.setWorkingDirectory(workingDirectory);
process.startDetached();
}
void DesktopAction::execute() const {
DesktopEntry::doExec(this->mExecString, this->entry->mWorkingDirectory);
}
DesktopEntryManager::DesktopEntryManager() {
this->scanDesktopEntries();
this->populateApplications();
}
void DesktopEntryManager::scanDesktopEntries() {
QList<QString> dataPaths;
if (qEnvironmentVariableIsSet("XDG_DATA_DIRS")) {
auto var = qEnvironmentVariable("XDG_DATA_DIRS");
dataPaths = var.split(u':', Qt::SkipEmptyParts);
} else {
dataPaths.push_back("/usr/local/share");
dataPaths.push_back("/usr/share");
}
qCDebug(logDesktopEntry) << "Creating desktop entry scanners";
for (auto& path: std::ranges::reverse_view(dataPaths)) {
auto p = QDir(path).filePath("applications");
auto file = QFileInfo(p);
if (!file.isDir()) {
qCDebug(logDesktopEntry) << "Not scanning path" << p << "as it is not a directory";
continue;
}
qCDebug(logDesktopEntry) << "Scanning path" << p;
this->scanPath(p);
}
}
void DesktopEntryManager::populateApplications() {
for (auto& entry: this->desktopEntries.values()) {
if (!entry->noDisplay()) this->mApplications.insertObject(entry);
}
}
void DesktopEntryManager::scanPath(const QDir& dir, const QString& prefix) {
auto entries = dir.entryInfoList(QDir::Dirs | QDir::Files | QDir::NoDotAndDotDot);
for (auto& entry: entries) {
if (entry.isDir()) this->scanPath(entry.absoluteFilePath(), prefix + dir.dirName() + "-");
else if (entry.isFile()) {
auto path = entry.filePath();
if (!path.endsWith(".desktop")) {
qCDebug(logDesktopEntry) << "Skipping file" << path << "as it has no .desktop extension";
continue;
}
auto file = QFile(path);
if (!file.open(QFile::ReadOnly)) {
qCDebug(logDesktopEntry) << "Could not open file" << path;
continue;
}
auto id = prefix + entry.fileName().sliced(0, entry.fileName().length() - 8);
auto lowerId = id.toLower();
auto text = QString::fromUtf8(file.readAll());
auto* dentry = new DesktopEntry(id, this);
dentry->parseEntry(text);
if (!dentry->isValid()) {
qCDebug(logDesktopEntry) << "Skipping desktop entry" << path;
delete dentry;
continue;
}
qCDebug(logDesktopEntry) << "Found desktop entry" << id << "at" << path;
auto conflictingId = this->desktopEntries.contains(id);
if (conflictingId) {
qCDebug(logDesktopEntry) << "Replacing old entry for" << id;
delete this->desktopEntries.value(id);
this->desktopEntries.remove(id);
this->lowercaseDesktopEntries.remove(lowerId);
}
this->desktopEntries.insert(id, dentry);
if (this->lowercaseDesktopEntries.contains(lowerId)) {
qCInfo(logDesktopEntry).nospace()
<< "Multiple desktop entries have the same lowercased id " << lowerId
<< ". This can cause ambiguity when byId requests are not made with the correct case "
"already.";
this->lowercaseDesktopEntries.remove(lowerId);
}
this->lowercaseDesktopEntries.insert(lowerId, dentry);
}
}
}
DesktopEntryManager* DesktopEntryManager::instance() {
static auto* instance = new DesktopEntryManager(); // NOLINT
return instance;
}
DesktopEntry* DesktopEntryManager::byId(const QString& id) {
if (auto* entry = this->desktopEntries.value(id)) {
return entry;
} else if (auto* entry = this->lowercaseDesktopEntries.value(id.toLower())) {
return entry;
} else {
return nullptr;
}
}
ObjectModel<DesktopEntry>* DesktopEntryManager::applications() { return &this->mApplications; }
DesktopEntries::DesktopEntries() { DesktopEntryManager::instance(); }
DesktopEntry* DesktopEntries::byId(const QString& id) {
return DesktopEntryManager::instance()->byId(id);
}
ObjectModel<DesktopEntry>* DesktopEntries::applications() {
return DesktopEntryManager::instance()->applications();
}

View file

@ -1,155 +0,0 @@
#pragma once
#include <utility>
#include <qcontainerfwd.h>
#include <qdir.h>
#include <qhash.h>
#include <qobject.h>
#include <qqmlintegration.h>
#include <qtmetamacros.h>
#include "doc.hpp"
#include "model.hpp"
class DesktopAction;
/// A desktop entry. See @@DesktopEntries for details.
class DesktopEntry: public QObject {
Q_OBJECT;
Q_PROPERTY(QString id MEMBER mId CONSTANT);
/// Name of the specific application, such as "Firefox".
Q_PROPERTY(QString name MEMBER mName CONSTANT);
/// Short description of the application, such as "Web Browser". May be empty.
Q_PROPERTY(QString genericName MEMBER mGenericName CONSTANT);
/// If true, this application should not be displayed in menus and launchers.
Q_PROPERTY(bool noDisplay MEMBER mNoDisplay CONSTANT);
/// Long description of the application, such as "View websites on the internet". May be empty.
Q_PROPERTY(QString comment MEMBER mComment CONSTANT);
/// Name of the icon associated with this application. May be empty.
Q_PROPERTY(QString icon MEMBER mIcon CONSTANT);
/// The raw `Exec` string from the desktop entry. You probably want @@execute().
Q_PROPERTY(QString execString MEMBER mExecString CONSTANT);
/// The working directory to execute from.
Q_PROPERTY(QString workingDirectory MEMBER mWorkingDirectory CONSTANT);
/// If the application should run in a terminal.
Q_PROPERTY(bool runInTerminal MEMBER mTerminal CONSTANT);
Q_PROPERTY(QVector<QString> categories MEMBER mCategories CONSTANT);
Q_PROPERTY(QVector<QString> keywords MEMBER mKeywords CONSTANT);
Q_PROPERTY(QVector<DesktopAction*> actions READ actions CONSTANT);
QML_ELEMENT;
QML_UNCREATABLE("DesktopEntry instances must be retrieved from DesktopEntries");
public:
explicit DesktopEntry(QString id, QObject* parent): QObject(parent), mId(std::move(id)) {}
void parseEntry(const QString& text);
/// Run the application. Currently ignores @@runInTerminal and field codes.
Q_INVOKABLE void execute() const;
[[nodiscard]] bool isValid() const;
[[nodiscard]] bool noDisplay() const;
[[nodiscard]] QVector<DesktopAction*> actions() const;
// currently ignores all field codes.
static QVector<QString> parseExecString(const QString& execString);
static void doExec(const QString& execString, const QString& workingDirectory);
public:
QString mId;
QString mName;
QString mGenericName;
bool mNoDisplay = false;
QString mComment;
QString mIcon;
QString mExecString;
QString mWorkingDirectory;
bool mTerminal = false;
QVector<QString> mCategories;
QVector<QString> mKeywords;
private:
QHash<QString, QString> mEntries;
QHash<QString, DesktopAction*> mActions;
friend class DesktopAction;
};
/// An action of a @@DesktopEntry$.
class DesktopAction: public QObject {
Q_OBJECT;
Q_PROPERTY(QString id MEMBER mId CONSTANT);
Q_PROPERTY(QString name MEMBER mName CONSTANT);
Q_PROPERTY(QString icon MEMBER mIcon CONSTANT);
/// The raw `Exec` string from the desktop entry. You probably want @@execute().
Q_PROPERTY(QString execString MEMBER mExecString CONSTANT);
QML_ELEMENT;
QML_UNCREATABLE("DesktopAction instances must be retrieved from a DesktopEntry");
public:
explicit DesktopAction(QString id, DesktopEntry* entry)
: QObject(entry)
, entry(entry)
, mId(std::move(id)) {}
/// Run the application. Currently ignores @@DesktopEntry.runInTerminal and field codes.
Q_INVOKABLE void execute() const;
private:
DesktopEntry* entry;
QString mId;
QString mName;
QString mIcon;
QString mExecString;
QHash<QString, QString> mEntries;
friend class DesktopEntry;
};
class DesktopEntryManager: public QObject {
Q_OBJECT;
public:
void scanDesktopEntries();
[[nodiscard]] DesktopEntry* byId(const QString& id);
[[nodiscard]] ObjectModel<DesktopEntry>* applications();
static DesktopEntryManager* instance();
private:
explicit DesktopEntryManager();
void populateApplications();
void scanPath(const QDir& dir, const QString& prefix = QString());
QHash<QString, DesktopEntry*> desktopEntries;
QHash<QString, DesktopEntry*> lowercaseDesktopEntries;
ObjectModel<DesktopEntry> 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<DesktopEntry>*);
Q_PROPERTY(UntypedObjectModel* applications READ applications CONSTANT);
QML_ELEMENT;
QML_SINGLETON;
public:
explicit DesktopEntries();
/// Look up a desktop entry by name. Includes NoDisplay entries. May return null.
Q_INVOKABLE [[nodiscard]] static DesktopEntry* byId(const QString& id);
[[nodiscard]] static ObjectModel<DesktopEntry>* applications();
};

View file

@ -10,14 +10,5 @@
#define QSDOC_ELEMENT
#define QSDOC_NAMED_ELEMENT(name)
// unmark uncreatable (will be overlayed by other types)
#define QSDOC_CREATABLE
// change the cname used for this type
#define QSDOC_CNAME(name)
// overridden properties
#define QSDOC_PROPERTY_OVERRIDE(...)
// override types of properties for docs
#define QSDOC_TYPE_OVERRIDE(type)

View file

@ -1,22 +0,0 @@
#include "elapsedtimer.hpp"
#include <qtypes.h>
ElapsedTimer::ElapsedTimer() { this->timer.start(); }
qreal ElapsedTimer::elapsed() { return static_cast<qreal>(this->elapsedNs()) / 1000000000.0; }
qreal ElapsedTimer::restart() { return static_cast<qreal>(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();
}

View file

@ -1,45 +0,0 @@
#pragma once
#include <qelapsedtimer.h>
#include <qobject.h>
#include <qqmlintegration.h>
#include <qtmetamacros.h>
#include <qtypes.h>
///! 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;
};

View file

@ -32,12 +32,10 @@ FloatingWindowInterface::FloatingWindowInterface(QObject* parent)
QObject::connect(this->window, &ProxyWindowBase::backerVisibilityChanged, this, &FloatingWindowInterface::backingWindowVisibleChanged);
QObject::connect(this->window, &ProxyWindowBase::heightChanged, this, &FloatingWindowInterface::heightChanged);
QObject::connect(this->window, &ProxyWindowBase::widthChanged, this, &FloatingWindowInterface::widthChanged);
QObject::connect(this->window, &ProxyWindowBase::devicePixelRatioChanged, this, &FloatingWindowInterface::devicePixelRatioChanged);
QObject::connect(this->window, &ProxyWindowBase::screenChanged, this, &FloatingWindowInterface::screenChanged);
QObject::connect(this->window, &ProxyWindowBase::windowTransformChanged, this, &FloatingWindowInterface::windowTransformChanged);
QObject::connect(this->window, &ProxyWindowBase::colorChanged, this, &FloatingWindowInterface::colorChanged);
QObject::connect(this->window, &ProxyWindowBase::maskChanged, this, &FloatingWindowInterface::maskChanged);
QObject::connect(this->window, &ProxyWindowBase::surfaceFormatChanged, this, &FloatingWindowInterface::surfaceFormatChanged);
// clang-format on
}
@ -51,13 +49,10 @@ void FloatingWindowInterface::onReload(QObject* oldInstance) {
QQmlListProperty<QObject> FloatingWindowInterface::data() { return this->window->data(); }
ProxyWindowBase* FloatingWindowInterface::proxyWindow() const { return this->window; }
QQuickItem* FloatingWindowInterface::contentItem() const { return this->window->contentItem(); }
bool FloatingWindowInterface::isBackingWindowVisible() const {
return this->window->isVisibleDirect();
}
qreal FloatingWindowInterface::devicePixelRatio() const { return this->window->devicePixelRatio(); }
// NOLINTBEGIN
#define proxyPair(type, get, set) \
type FloatingWindowInterface::get() const { return this->window->get(); } \
@ -69,7 +64,6 @@ proxyPair(qint32, height, setHeight);
proxyPair(QuickshellScreenInfo*, screen, setScreen);
proxyPair(QColor, color, setColor);
proxyPair(PendingRegion*, mask, setMask);
proxyPair(QsSurfaceFormat, surfaceFormat, setSurfaceFormat);
#undef proxyPair
// NOLINTEND

View file

@ -4,7 +4,6 @@
#include <qtmetamacros.h>
#include "proxywindow.hpp"
#include "windowinterface.hpp"
class ProxyFloatingWindow: public ProxyWindowBase {
Q_OBJECT;
@ -18,7 +17,7 @@ public:
void setHeight(qint32 height) override;
};
///! Standard toplevel operating system window that looks like any other application.
///! Standard floating window.
class FloatingWindowInterface: public WindowInterface {
Q_OBJECT;
QML_NAMED_ELEMENT(FloatingWindow);
@ -42,8 +41,6 @@ public:
[[nodiscard]] qint32 height() const override;
void setHeight(qint32 height) override;
[[nodiscard]] virtual qreal devicePixelRatio() const override;
[[nodiscard]] QuickshellScreenInfo* screen() const override;
void setScreen(QuickshellScreenInfo* screen) override;
@ -53,9 +50,6 @@ public:
[[nodiscard]] PendingRegion* mask() const override;
void setMask(PendingRegion* mask) override;
[[nodiscard]] QsSurfaceFormat surfaceFormat() const override;
void setSurfaceFormat(QsSurfaceFormat mask) override;
[[nodiscard]] QQmlListProperty<QObject> data() override;
// NOLINTEND

View file

@ -4,8 +4,6 @@
#include <qcontainerfwd.h>
#include <qcoreapplication.h>
#include <qdebug.h>
#include <qdir.h>
#include <qfileinfo.h>
#include <qfilesystemwatcher.h>
#include <qhash.h>
#include <qlogging.h>
@ -14,6 +12,7 @@
#include <qqmlcontext.h>
#include <qqmlengine.h>
#include <qqmlincubator.h>
#include <qtimer.h>
#include <qtmetamacros.h>
#include "iconimageprovider.hpp"
@ -24,12 +23,10 @@
#include "reload.hpp"
#include "scan.hpp"
static QHash<const QQmlEngine*, EngineGeneration*> g_generations; // NOLINT
static QHash<QQmlEngine*, EngineGeneration*> g_generations; // NOLINT
EngineGeneration::EngineGeneration(const QDir& rootPath, QmlScanner scanner)
: rootPath(rootPath)
, scanner(std::move(scanner))
, urlInterceptor(this->rootPath)
EngineGeneration::EngineGeneration(QmlScanner scanner)
: scanner(std::move(scanner))
, interceptNetFactory(this->scanner.qmldirIntercepts)
, engine(new QQmlEngine()) {
g_generations.insert(this->engine, this);
@ -42,93 +39,56 @@ EngineGeneration::EngineGeneration(const QDir& rootPath, QmlScanner scanner)
this->engine->addImageProvider("qsimage", new QsImageProvider());
this->engine->addImageProvider("qspixmap", new QsPixmapProvider());
QsEnginePlugin::runConstructGeneration(*this);
QuickshellPlugin::runConstructGeneration(*this);
}
EngineGeneration::~EngineGeneration() {
if (this->engine != nullptr) {
qFatal() << this << "destroyed without calling destroy()";
}
g_generations.remove(this->engine);
delete this->engine;
}
void EngineGeneration::destroy() {
if (this->destroying) return;
this->destroying = true;
// Multiple generations can detect a reload at the same time.
delete this->watcher;
this->watcher = nullptr;
if (this->watcher != nullptr) {
// Multiple generations can detect a reload at the same time.
QObject::disconnect(this->watcher, nullptr, this, nullptr);
this->watcher->deleteLater();
this->watcher = nullptr;
}
for (auto* extension: this->extensions.values()) {
delete extension;
}
if (this->root != nullptr) {
// Yes all of this is actually necessary.
if (this->engine != nullptr && this->root != nullptr) {
QObject::connect(this->root, &QObject::destroyed, this, [this]() {
// prevent further js execution between garbage collection and engine destruction.
this->engine->setInterrupted(true);
// The timer seems to fix *one* of the possible qml item destructor crashes.
QTimer::singleShot(0, [this]() {
// Garbage is not collected during engine destruction.
this->engine->collectGarbage();
g_generations.remove(this->engine);
QObject::connect(this->engine, &QObject::destroyed, this, [this]() { delete this; });
// Garbage is not collected during engine destruction.
this->engine->collectGarbage();
delete this->engine;
this->engine = nullptr;
auto terminate = this->shouldTerminate;
auto code = this->exitCode;
delete this;
if (terminate) QCoreApplication::exit(code);
// Even after all of that there's still multiple failing assertions and segfaults.
// Pray you don't hit one.
// Note: it appeats *some* of the crashes are related to values owned by the generation.
// Test by commenting the connect() above.
this->engine->deleteLater();
this->engine = nullptr;
});
});
this->root->deleteLater();
this->root = nullptr;
} else {
g_generations.remove(this->engine);
// the engine has never been used, no need to clean up
delete this->engine;
this->engine = nullptr;
auto terminate = this->shouldTerminate;
auto code = this->exitCode;
delete this;
if (terminate) QCoreApplication::exit(code);
}
}
void EngineGeneration::shutdown() {
if (this->destroying) return;
delete this->root;
this->root = nullptr;
delete this->engine;
this->engine = nullptr;
delete this;
}
void EngineGeneration::onReload(EngineGeneration* old) {
if (old != nullptr) {
// if the old generation holds the window incubation controller as the
// new generation acquires it then incubators will hang intermittently
qCDebug(logIncubator) << "Locking incubation controllers of old generation" << old;
old->incubationControllersLocked = true;
old->incubationControllers.clear();
old->assignIncubationController();
}
QObject::connect(this->engine, &QQmlEngine::quit, this, &EngineGeneration::quit);
QObject::connect(this->engine, &QQmlEngine::exit, this, &EngineGeneration::exit);
if (auto* reloadable = qobject_cast<Reloadable*>(this->root)) {
reloadable->reload(old ? old->root : nullptr);
}
auto* app = QCoreApplication::instance();
QObject::connect(this->engine, &QQmlEngine::quit, app, &QCoreApplication::quit);
QObject::connect(this->engine, &QQmlEngine::exit, app, &QCoreApplication::exit);
this->root->reload(old == nullptr ? nullptr : old->root);
this->singletonRegistry.onReload(old == nullptr ? nullptr : &old->singletonRegistry);
this->reloadComplete = true;
emit this->reloadFinished();
@ -145,7 +105,7 @@ void EngineGeneration::postReload() {
// This can be called on a generation during its destruction.
if (this->engine == nullptr || this->root == nullptr) return;
QsEnginePlugin::runOnReload();
QuickshellPlugin::runOnReload();
PostReloadHook::postReloadTree(this->root);
this->singletonRegistry.onPostReload();
}
@ -157,21 +117,13 @@ void EngineGeneration::setWatchingFiles(bool watching) {
for (auto& file: this->scanner.scannedFiles) {
this->watcher->addPath(file);
this->watcher->addPath(QFileInfo(file).dir().absolutePath());
}
QObject::connect(
this->watcher,
&QFileSystemWatcher::fileChanged,
this,
&EngineGeneration::onFileChanged
);
QObject::connect(
this->watcher,
&QFileSystemWatcher::directoryChanged,
this,
&EngineGeneration::onDirectoryChanged
&EngineGeneration::filesChanged
);
}
} else {
@ -182,44 +134,28 @@ void EngineGeneration::setWatchingFiles(bool watching) {
}
}
void EngineGeneration::onFileChanged(const QString& name) {
if (!this->watcher->files().contains(name)) {
this->deletedWatchedFiles.push_back(name);
} else {
emit this->filesChanged();
}
}
void EngineGeneration::onDirectoryChanged() {
// try to find any files that were just deleted from a replace operation
for (auto& file: this->deletedWatchedFiles) {
if (QFileInfo(file).exists()) {
emit this->filesChanged();
break;
}
}
}
void EngineGeneration::registerIncubationController(QQmlIncubationController* controller) {
auto* obj = dynamic_cast<QObject*>(controller);
// We only want controllers that we can swap out if destroyed.
// This happens if the window owning the active controller dies.
if (auto* obj = dynamic_cast<QObject*>(controller)) {
QObject::connect(
obj,
&QObject::destroyed,
this,
&EngineGeneration::incubationControllerDestroyed
);
} else {
qCWarning(logIncubator) << "Could not register incubation controller as it is not a QObject"
<< controller;
if (obj == nullptr) {
qCDebug(logIncubator) << "Could not register incubation controller as it is not a QObject"
<< controller;
return;
}
this->incubationControllers.push_back(controller);
qCDebug(logIncubator) << "Registered incubation controller" << controller << "to generation"
<< this;
this->incubationControllers.push_back({controller, obj});
QObject::connect(
obj,
&QObject::destroyed,
this,
&EngineGeneration::incubationControllerDestroyed
);
qCDebug(logIncubator) << "Registered incubation controller" << controller;
// This function can run during destruction.
if (this->engine == nullptr) return;
@ -230,20 +166,22 @@ void EngineGeneration::registerIncubationController(QQmlIncubationController* co
}
void EngineGeneration::deregisterIncubationController(QQmlIncubationController* controller) {
if (auto* obj = dynamic_cast<QObject*>(controller)) {
QObject::disconnect(obj, nullptr, this, nullptr);
} else {
qCCritical(logIncubator) << "Deregistering incubation controller which is not a QObject, "
"however only QObject controllers should be registered.";
}
QObject* obj = nullptr;
this->incubationControllers.removeIf([&](QPair<QQmlIncubationController*, QObject*> other) {
if (controller == other.first) {
obj = other.second;
return true;
} else return false;
});
if (!this->incubationControllers.removeOne(controller)) {
qCCritical(logIncubator) << "Failed to deregister incubation controller" << controller << "from"
<< this << "as it was not registered to begin with";
qCCritical(logIncubator) << "Current registered incuabation controllers"
<< this->incubationControllers;
if (obj == nullptr) {
qCWarning(logIncubator) << "Failed to deregister incubation controller" << controller
<< "as it was not registered to begin with";
qCWarning(logIncubator) << "Current registered incuabation controllers"
<< this->incubationControllers;
} else {
qCDebug(logIncubator) << "Deregistered incubation controller" << controller << "from" << this;
QObject::disconnect(obj, nullptr, this, nullptr);
qCDebug(logIncubator) << "Deregistered incubation controller" << controller;
}
// This function can run during destruction.
@ -258,25 +196,22 @@ void EngineGeneration::deregisterIncubationController(QQmlIncubationController*
void EngineGeneration::incubationControllerDestroyed() {
auto* sender = this->sender();
auto* controller = dynamic_cast<QQmlIncubationController*>(sender);
QQmlIncubationController* controller = nullptr;
this->incubationControllers.removeIf([&](QPair<QQmlIncubationController*, QObject*> other) {
if (sender == other.second) {
controller = other.first;
return true;
} else return false;
});
if (controller == nullptr) {
qCCritical(logIncubator) << "Destroyed incubation controller" << sender << "is not known to"
<< this << ", this may cause memory corruption";
qCCritical(logIncubator) << "Destroyed incubation controller" << this->sender()
<< "could not be identified, this may cause memory corruption";
qCCritical(logIncubator) << "Current registered incuabation controllers"
<< this->incubationControllers;
return;
}
if (this->incubationControllers.removeOne(controller)) {
qCDebug(logIncubator) << "Destroyed incubation controller" << controller << "deregistered from"
<< this;
} else {
qCCritical(logIncubator) << "Destroyed incubation controller" << controller
<< "was not registered, but its destruction was observed by" << this;
return;
qCDebug(logIncubator) << "Destroyed incubation controller" << controller << "deregistered";
}
// This function can run during destruction.
@ -289,64 +224,23 @@ void EngineGeneration::incubationControllerDestroyed() {
}
}
void EngineGeneration::registerExtension(const void* key, EngineGenerationExt* extension) {
if (this->extensions.contains(key)) {
delete this->extensions.value(key);
}
this->extensions.insert(key, extension);
}
EngineGenerationExt* EngineGeneration::findExtension(const void* key) {
return this->extensions.value(key);
}
void EngineGeneration::quit() {
this->shouldTerminate = true;
this->destroy();
}
void EngineGeneration::exit(int code) {
this->shouldTerminate = true;
this->exitCode = code;
this->destroy();
}
void EngineGeneration::assignIncubationController() {
QQmlIncubationController* controller = nullptr;
if (this->incubationControllers.isEmpty()) controller = &this->delayedIncubationController;
else controller = this->incubationControllers.first().first;
if (this->incubationControllersLocked || this->incubationControllers.isEmpty()) {
controller = &this->delayedIncubationController;
} else {
controller = this->incubationControllers.first();
}
qCDebug(logIncubator) << "Assigning incubation controller" << controller << "to generation"
<< this
qCDebug(logIncubator) << "Assigning incubation controller to engine:" << controller
<< "fallback:" << (controller == &this->delayedIncubationController);
this->engine->setIncubationController(controller);
}
EngineGeneration* EngineGeneration::currentGeneration() {
if (g_generations.size() == 1) {
return *g_generations.begin();
} else return nullptr;
}
EngineGeneration* EngineGeneration::findEngineGeneration(const QQmlEngine* engine) {
return g_generations.value(engine);
}
EngineGeneration* EngineGeneration::findObjectGeneration(const QObject* object) {
// Objects can still attempt to find their generation after it has been destroyed.
// if (g_generations.size() == 1) return EngineGeneration::currentGeneration();
EngineGeneration* EngineGeneration::findObjectGeneration(QObject* object) {
while (object != nullptr) {
auto* context = QQmlEngine::contextForObject(object);
if (context != nullptr) {
if (auto* generation = EngineGeneration::findEngineGeneration(context->engine())) {
if (auto* generation = g_generations.value(context->engine())) {
return generation;
}
}

View file

@ -1,34 +1,25 @@
#pragma once
#include <qcontainerfwd.h>
#include <qdir.h>
#include <qfilesystemwatcher.h>
#include <qhash.h>
#include <qobject.h>
#include <qqmlengine.h>
#include <qpair.h>
#include <qqmlincubator.h>
#include <qtclasshelpermacros.h>
#include "incubator.hpp"
#include "qsintercept.hpp"
#include "scan.hpp"
#include "shell.hpp"
#include "singleton.hpp"
class RootWrapper;
class QuickshellGlobal;
class EngineGenerationExt {
public:
EngineGenerationExt() = default;
virtual ~EngineGenerationExt() = default;
Q_DISABLE_COPY_MOVE(EngineGenerationExt);
};
class EngineGeneration: public QObject {
Q_OBJECT;
public:
explicit EngineGeneration(const QDir& rootPath, QmlScanner scanner);
explicit EngineGeneration(QmlScanner scanner);
~EngineGeneration() override;
Q_DISABLE_COPY_MOVE(EngineGeneration);
@ -39,55 +30,30 @@ public:
void registerIncubationController(QQmlIncubationController* controller);
void deregisterIncubationController(QQmlIncubationController* controller);
// takes ownership
void registerExtension(const void* key, EngineGenerationExt* extension);
EngineGenerationExt* findExtension(const void* key);
static EngineGeneration* findEngineGeneration(const QQmlEngine* engine);
static EngineGeneration* findObjectGeneration(const QObject* object);
// Returns the current generation if there is only one generation,
// otherwise null.
static EngineGeneration* currentGeneration();
static EngineGeneration* findObjectGeneration(QObject* object);
RootWrapper* wrapper = nullptr;
QDir rootPath;
QmlScanner scanner;
QsUrlInterceptor urlInterceptor;
QsInterceptNetworkAccessManagerFactory interceptNetFactory;
QQmlEngine* engine = nullptr;
QObject* root = nullptr;
ShellRoot* root = nullptr;
SingletonRegistry singletonRegistry;
QFileSystemWatcher* watcher = nullptr;
QVector<QString> deletedWatchedFiles;
DelayedQmlIncubationController delayedIncubationController;
bool reloadComplete = false;
QuickshellGlobal* qsgInstance = nullptr;
void destroy();
void shutdown();
signals:
void filesChanged();
void reloadFinished();
public slots:
void quit();
void exit(int code);
private slots:
void onFileChanged(const QString& name);
void onDirectoryChanged();
void incubationControllerDestroyed();
private:
void postReload();
void assignIncubationController();
QVector<QQmlIncubationController*> incubationControllers;
bool incubationControllersLocked = false;
QHash<const void*, EngineGenerationExt*> extensions;
bool destroying = false;
bool shouldTerminate = false;
int exitCode = 0;
QVector<QPair<QQmlIncubationController*, QObject*>> incubationControllers;
};

View file

@ -1,5 +1,4 @@
#include "iconimageprovider.hpp"
#include <algorithm>
#include <qcolor.h>
#include <qicon.h>
@ -12,9 +11,7 @@
QPixmap
IconImageProvider::requestPixmap(const QString& id, QSize* size, const QSize& requestedSize) {
QString iconName;
QString fallbackName;
QString path;
auto splitIdx = id.indexOf("?path=");
if (splitIdx != -1) {
iconName = id.sliced(0, splitIdx);
@ -22,17 +19,10 @@ IconImageProvider::requestPixmap(const QString& id, QSize* size, const QSize& re
qWarning() << "Searching custom icon paths is not yet supported. Icon path will be ignored for"
<< id;
} else {
splitIdx = id.indexOf("?fallback=");
if (splitIdx != -1) {
iconName = id.sliced(0, splitIdx);
fallbackName = id.sliced(splitIdx + 10);
} else {
iconName = id;
}
iconName = id;
}
auto icon = QIcon::fromTheme(iconName);
if (icon.isNull()) icon = QIcon::fromTheme(fallbackName);
auto targetSize = requestedSize.isValid() ? requestedSize : QSize(100, 100);
if (targetSize.width() == 0 || targetSize.height() == 0) targetSize = QSize(2, 2);
@ -50,8 +40,8 @@ IconImageProvider::requestPixmap(const QString& id, QSize* size, const QSize& re
QPixmap IconImageProvider::missingPixmap(const QSize& size) {
auto width = size.width() % 2 == 0 ? size.width() : size.width() + 1;
auto height = size.height() % 2 == 0 ? size.height() : size.height() + 1;
width = std::max(width, 2);
height = std::max(height, 2);
if (width < 2) width = 2;
if (height < 2) height = 2;
auto pixmap = QPixmap(width, height);
pixmap.fill(QColorConstants::Black);
@ -65,20 +55,12 @@ QPixmap IconImageProvider::missingPixmap(const QSize& size) {
return pixmap;
}
QString IconImageProvider::requestString(
const QString& icon,
const QString& path,
const QString& fallback
) {
QString IconImageProvider::requestString(const QString& icon, const QString& path) {
auto req = "image://icon/" + icon;
if (!path.isEmpty()) {
req += "?path=" + path;
}
if (!fallback.isEmpty()) {
req += "?fallback=" + fallback;
}
return req;
}

View file

@ -10,10 +10,5 @@ public:
QPixmap requestPixmap(const QString& id, QSize* size, const QSize& requestedSize) override;
static QPixmap missingPixmap(const QSize& size);
static QString requestString(
const QString& icon,
const QString& path = QString(),
const QString& fallback = QString()
);
static QString requestString(const QString& icon, const QString& path);
};

View file

@ -1,105 +0,0 @@
#include "iconprovider.hpp"
#include <utility>
#include <qicon.h>
#include <qiconengine.h>
#include <qlogging.h>
#include <qobject.h>
#include <qpixmap.h>
#include <qqmlengine.h>
#include <qquickimageprovider.h>
#include <qrect.h>
#include <qsize.h>
#include <qstring.h>
#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<QQuickImageProvider*>(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);
}

View file

@ -1,8 +0,0 @@
#pragma once
#include <qicon.h>
#include <qqmlengine.h>
#include <qurl.h>
QIcon getEngineImageAsIcon(QQmlEngine* engine, const QUrl& url);
QIcon getCurrentEngineImageAsIcon(const QUrl& url);

View file

@ -1,6 +1,5 @@
#include "imageprovider.hpp"
#include <qcontainerfwd.h>
#include <qdebug.h>
#include <qimage.h>
#include <qlogging.h>
@ -8,30 +7,17 @@
#include <qobject.h>
#include <qpixmap.h>
#include <qqmlengine.h>
#include <qtypes.h>
namespace {
static QMap<QString, QsImageHandle*> liveImages; // NOLINT
namespace {
QMap<QString, QsImageHandle*> liveImages; // NOLINT
quint32 handleIndex = 0; // NOLINT
} // namespace
void parseReq(const QString& req, QString& target, QString& param) {
auto splitIdx = req.indexOf('/');
if (splitIdx != -1) {
target = req.sliced(0, splitIdx);
param = req.sliced(splitIdx + 1);
} else {
target = req;
QsImageHandle::QsImageHandle(QQmlImageProviderBase::ImageType type, QObject* parent)
: QObject(parent)
, type(type) {
{
auto dbg = QDebug(&this->id);
dbg.nospace() << static_cast<void*>(this);
}
}
} // namespace
QsImageHandle::QsImageHandle(QQmlImageProviderBase::ImageType type)
: type(type)
, id(QString::number(++handleIndex)) {
liveImages.insert(this->id, this);
}
@ -57,6 +43,16 @@ QPixmap QsImageHandle::
return QPixmap();
}
void parseReq(const QString& req, QString& target, QString& param) {
auto splitIdx = req.indexOf('/');
if (splitIdx != -1) {
target = req.sliced(0, splitIdx);
param = req.sliced(splitIdx + 1);
} else {
target = req;
}
}
QImage QsImageProvider::requestImage(const QString& id, QSize* size, const QSize& requestedSize) {
QString target;
QString param;
@ -85,9 +81,3 @@ QsPixmapProvider::requestPixmap(const QString& id, QSize* size, const QSize& req
return QPixmap();
}
}
QString QsIndexedImageHandle::url() const {
return this->QsImageHandle::url() % '/' % QString::number(this->changeIndex);
}
void QsIndexedImageHandle::imageChanged() { ++this->changeIndex; }

View file

@ -20,13 +20,15 @@ public:
QPixmap requestPixmap(const QString& id, QSize* size, const QSize& requestedSize) override;
};
class QsImageHandle {
class QsImageHandle: public QObject {
Q_OBJECT;
public:
explicit QsImageHandle(QQmlImageProviderBase::ImageType type);
virtual ~QsImageHandle();
explicit QsImageHandle(QQmlImageProviderBase::ImageType type, QObject* parent = nullptr);
~QsImageHandle() override;
Q_DISABLE_COPY_MOVE(QsImageHandle);
[[nodiscard]] virtual QString url() const;
[[nodiscard]] QString url() const;
virtual QImage requestImage(const QString& id, QSize* size, const QSize& requestedSize);
virtual QPixmap requestPixmap(const QString& id, QSize* size, const QSize& requestedSize);
@ -35,14 +37,3 @@ private:
QQmlImageProviderBase::ImageType type;
QString id;
};
class QsIndexedImageHandle: public QsImageHandle {
public:
explicit QsIndexedImageHandle(QQmlImageProviderBase::ImageType type): QsImageHandle(type) {}
[[nodiscard]] QString url() const override;
void imageChanged();
private:
quint32 changeIndex = 0;
};

View file

@ -1,35 +0,0 @@
#include "instanceinfo.hpp"
#include <qdatastream.h>
QDataStream& operator<<(QDataStream& stream, const InstanceInfo& info) {
stream << info.instanceId << info.configPath << info.shellId << info.launchTime;
return stream;
}
QDataStream& operator>>(QDataStream& stream, InstanceInfo& info) {
stream >> info.instanceId >> info.configPath >> info.shellId >> info.launchTime;
return stream;
}
QDataStream& operator<<(QDataStream& stream, const RelaunchInfo& info) {
stream << info.instance << info.noColor << info.timestamp << info.sparseLogsOnly
<< info.defaultLogLevel << info.logRules;
return stream;
}
QDataStream& operator>>(QDataStream& stream, RelaunchInfo& info) {
stream >> info.instance >> info.noColor >> info.timestamp >> info.sparseLogsOnly
>> info.defaultLogLevel >> info.logRules;
return stream;
}
InstanceInfo InstanceInfo::CURRENT = {}; // NOLINT
namespace qs::crash {
CrashInfo CrashInfo::INSTANCE = {}; // NOLINT
}

View file

@ -1,39 +0,0 @@
#pragma once
#include <qdatetime.h>
#include <qlogging.h>
#include <qstring.h>
struct InstanceInfo {
QString instanceId;
QString configPath;
QString shellId;
QDateTime launchTime;
static InstanceInfo CURRENT; // NOLINT
};
struct RelaunchInfo {
InstanceInfo instance;
bool noColor = false;
bool timestamp = false;
bool sparseLogsOnly = false;
QtMsgType defaultLogLevel = QtWarningMsg;
QString logRules;
};
QDataStream& operator<<(QDataStream& stream, const InstanceInfo& info);
QDataStream& operator>>(QDataStream& stream, InstanceInfo& info);
QDataStream& operator<<(QDataStream& stream, const RelaunchInfo& info);
QDataStream& operator>>(QDataStream& stream, RelaunchInfo& info);
namespace qs::crash {
struct CrashInfo {
int logFd = -1;
static CrashInfo INSTANCE; // NOLINT
};
} // namespace qs::crash

View file

@ -179,9 +179,7 @@ void LazyLoader::incubateIfReady(bool overrideReloadCheck) {
void LazyLoader::onIncubationCompleted() {
this->setItem(this->incubator->object());
// The incubator is not necessarily inert at the time of this callback,
// so deleteLater is required.
this->incubator->deleteLater();
delete this->incubator;
this->incubator = nullptr;
this->targetLoading = false;
emit this->loadingChanged();

View file

@ -79,7 +79,7 @@
/// > [!WARNING] Components that internally load other components must explicitly
/// > support asynchronous loading to avoid blocking.
/// >
/// > Notably, @@Variants does not corrently support asynchronous
/// > Notably, [Variants](../variants) does not corrently support asynchronous
/// > loading, meaning using it inside a LazyLoader will block similarly to not
/// > having a loader to start with.
///
@ -87,8 +87,8 @@
/// > meaning if you create all windows inside of lazy loaders, none of them will ever load.
class LazyLoader: public Reloadable {
Q_OBJECT;
/// The fully loaded item if the loader is @@loading or @@active, or `null`
/// if neither @@loading nor @@active.
/// The fully loaded item if the loader is `loading` or `active`, or `null`
/// if neither `loading` or `active`.
///
/// Note that the item is owned by the LazyLoader, and destroying the LazyLoader
/// will destroy the item.
@ -96,7 +96,7 @@ class LazyLoader: public Reloadable {
/// > [!WARNING] If you access the `item` of a loader that is currently loading,
/// > it will block as if you had set `active` to true immediately beforehand.
/// >
/// > You can instead set @@loading and listen to @@activeChanged(s) signal to
/// > You can instead set `loading` and listen to the `activeChanged` signal to
/// > ensure loading happens asynchronously.
Q_PROPERTY(QObject* item READ item NOTIFY itemChanged);
/// If the loader is actively loading.
@ -105,7 +105,7 @@ class LazyLoader: public Reloadable {
/// loading it asynchronously. If the component is already loaded, setting
/// this property has no effect.
///
/// See also: @@activeAsync.
/// See also: [activeAsync](#prop.activeAsync).
Q_PROPERTY(bool loading READ isLoading WRITE setLoading NOTIFY loadingChanged);
/// If the component is fully loaded.
///
@ -113,17 +113,17 @@ class LazyLoader: public Reloadable {
/// blocking the UI, and setting it to `false` will destroy the component, requiring
/// it to be loaded again.
///
/// See also: @@activeAsync.
/// See also: [activeAsync](#prop.activeAsync).
Q_PROPERTY(bool active READ isActive WRITE setActive NOTIFY activeChanged);
/// If the component is fully loaded.
///
/// Setting this property to true will asynchronously load the component similarly to
/// @@loading. Reading it or setting it to false will behanve
/// the same as @@active.
/// [loading](#prop.loading). Reading it or setting it to false will behanve
/// the same as [active](#prop.active).
Q_PROPERTY(bool activeAsync READ isActive WRITE setActiveAsync NOTIFY activeChanged);
/// The component to load. Mutually exclusive to @@source.
/// The component to load. Mutually exclusive to `source`.
Q_PROPERTY(QQmlComponent* component READ component WRITE setComponent NOTIFY componentChanged);
/// The URI to load the component from. Mutually exclusive to @@component.
/// The URI to load the component from. Mutually exclusive to `component`.
Q_PROPERTY(QString source READ source WRITE setSource NOTIFY sourceChanged);
Q_CLASSINFO("DefaultProperty", "component");
QML_ELEMENT;

View file

@ -1,937 +0,0 @@
#include "logging.hpp"
#include <array>
#include <cerrno>
#include <cstdio>
#include <fcntl.h>
#include <qbytearrayview.h>
#include <qcoreapplication.h>
#include <qdatetime.h>
#include <qendian.h>
#include <qfilesystemwatcher.h>
#include <qhash.h>
#include <qhashfunctions.h>
#include <qlist.h>
#include <qlogging.h>
#include <qloggingcategory.h>
#include <qnamespace.h>
#include <qobject.h>
#include <qobjectdefs.h>
#include <qpair.h>
#include <qstring.h>
#include <qstringview.h>
#include <qsysinfo.h>
#include <qtenvironmentvariables.h>
#include <qtextstream.h>
#include <qthread.h>
#include <qtmetamacros.h>
#include <qtypes.h>
#include <sys/mman.h>
#include <sys/sendfile.h>
#include "instanceinfo.hpp"
#include "logging_p.hpp"
#include "logging_qtprivate.cpp" // NOLINT
#include "paths.hpp"
#include "ringbuf.hpp"
Q_LOGGING_CATEGORY(logBare, "quickshell.bare");
namespace qs::log {
using namespace qt_logging_registry;
Q_LOGGING_CATEGORY(logLogging, "quickshell.logging", QtWarningMsg);
bool LogMessage::operator==(const LogMessage& other) const {
// note: not including time
return this->type == other.type && this->category == other.category && this->body == other.body;
}
size_t qHash(const LogMessage& message) {
return qHash(message.type) ^ qHash(message.category) ^ qHash(message.body);
}
void LogMessage::formatMessage(
QTextStream& stream,
const LogMessage& msg,
bool color,
bool timestamp,
const QString& prefix
) {
if (!prefix.isEmpty()) {
if (color) stream << "\033[90m";
stream << '[' << prefix << ']';
if (timestamp) stream << ' ';
if (color) stream << "\033[0m";
}
if (timestamp) {
if (color) stream << "\033[90m";
stream << msg.time.toString("yyyy-MM-dd hh:mm:ss.zzz");
}
if (msg.category == "quickshell.bare") {
if (!prefix.isEmpty()) stream << ' ';
stream << msg.body;
} else {
if (color) {
switch (msg.type) {
case QtDebugMsg: stream << "\033[34m DEBUG"; break;
case QtInfoMsg: stream << "\033[32m INFO"; break;
case QtWarningMsg: stream << "\033[33m WARN"; break;
case QtCriticalMsg: stream << "\033[31m ERROR"; break;
case QtFatalMsg: stream << "\033[31m FATAL"; break;
}
} else {
switch (msg.type) {
case QtDebugMsg: stream << " DEBUG"; break;
case QtInfoMsg: stream << " INFO"; break;
case QtWarningMsg: stream << " WARN"; break;
case QtCriticalMsg: stream << " ERROR"; break;
case QtFatalMsg: stream << " FATAL"; break;
}
}
const auto isDefault = msg.category == "default";
if (color && !isDefault && msg.type != QtFatalMsg) stream << "\033[97m";
if (!isDefault) {
stream << ' ' << msg.category;
}
if (color && msg.type != QtFatalMsg) stream << "\033[0m";
stream << ": " << msg.body;
if (color && msg.type == QtFatalMsg) stream << "\033[0m";
}
}
bool CategoryFilter::shouldDisplay(QtMsgType type) const {
switch (type) {
case QtDebugMsg: return this->debug;
case QtInfoMsg: return this->info;
case QtWarningMsg: return this->warn;
case QtCriticalMsg: return this->critical;
default: return true;
}
}
void CategoryFilter::apply(QLoggingCategory* category) const {
category->setEnabled(QtDebugMsg, this->debug);
category->setEnabled(QtInfoMsg, this->info);
category->setEnabled(QtWarningMsg, this->warn);
category->setEnabled(QtCriticalMsg, this->critical);
}
void CategoryFilter::applyRule(
QLatin1StringView category,
const qt_logging_registry::QLoggingRule& rule
) {
auto filterpass = rule.pass(category, QtDebugMsg);
if (filterpass != 0) this->debug = filterpass > 0;
filterpass = rule.pass(category, QtInfoMsg);
if (filterpass != 0) this->info = filterpass > 0;
filterpass = rule.pass(category, QtWarningMsg);
if (filterpass != 0) this->warn = filterpass > 0;
filterpass = rule.pass(category, QtCriticalMsg);
if (filterpass != 0) this->critical = filterpass > 0;
}
LogManager::LogManager(): stdoutStream(stdout) {}
void LogManager::messageHandler(
QtMsgType type,
const QMessageLogContext& context,
const QString& msg
) {
auto message = LogMessage(type, QLatin1StringView(context.category), msg.toUtf8());
auto* self = LogManager::instance();
auto display = true;
const auto* key = static_cast<const void*>(context.category);
if (self->sparseFilters.contains(key)) {
display = self->sparseFilters.value(key).shouldDisplay(type);
}
if (display) {
LogMessage::formatMessage(
self->stdoutStream,
message,
self->colorLogs,
self->timestampLogs,
self->prefix
);
self->stdoutStream << Qt::endl;
}
emit self->logMessage(message, display);
}
void LogManager::filterCategory(QLoggingCategory* category) {
auto* instance = LogManager::instance();
auto categoryName = QLatin1StringView(category->categoryName());
auto isQs = categoryName.startsWith(QLatin1StringView("quickshell."));
if (instance->lastCategoryFilter) {
instance->lastCategoryFilter(category);
}
auto filter = CategoryFilter(category);
if (isQs) {
filter.debug = filter.debug || instance->mDefaultLevel == QtDebugMsg;
filter.info = filter.debug || instance->mDefaultLevel == QtInfoMsg;
filter.warn = filter.info || instance->mDefaultLevel == QtWarningMsg;
filter.critical = filter.warn || instance->mDefaultLevel == QtCriticalMsg;
}
for (const auto& rule: *instance->rules) {
filter.applyRule(categoryName, rule);
}
if (isQs && !instance->sparse) {
// We assume the category name pointer will always be the same and be comparable in the message handler.
instance->sparseFilters.insert(static_cast<const void*>(category->categoryName()), filter);
// all enabled by default
CategoryFilter().apply(category);
} else {
filter.apply(category);
}
instance->allFilters.insert(categoryName, filter);
}
LogManager* LogManager::instance() {
static auto* instance = new LogManager(); // NOLINT
return instance;
}
void LogManager::init(
bool color,
bool timestamp,
bool sparseOnly,
QtMsgType defaultLevel,
const QString& rules,
const QString& prefix
) {
auto* instance = LogManager::instance();
instance->colorLogs = color;
instance->timestampLogs = timestamp;
instance->sparse = sparseOnly;
instance->prefix = prefix;
instance->mDefaultLevel = defaultLevel;
instance->mRulesString = rules;
{
QLoggingSettingsParser parser;
parser.setContent(rules);
instance->rules = new QList(parser.rules());
}
qInstallMessageHandler(&LogManager::messageHandler);
instance->lastCategoryFilter = QLoggingCategory::installFilter(&LogManager::filterCategory);
qCDebug(logLogging) << "Creating offthread logger...";
auto* thread = new QThread();
instance->threadProxy.moveToThread(thread);
thread->start();
QMetaObject::invokeMethod(
&instance->threadProxy,
&LoggingThreadProxy::initInThread,
Qt::BlockingQueuedConnection
);
qCDebug(logLogging) << "Logger initialized.";
}
void LogManager::initFs() {
QMetaObject::invokeMethod(
&LogManager::instance()->threadProxy,
"initFs",
Qt::BlockingQueuedConnection
);
}
QString LogManager::rulesString() const { return this->mRulesString; }
QtMsgType LogManager::defaultLevel() const { return this->mDefaultLevel; }
bool LogManager::isSparse() const { return this->sparse; }
CategoryFilter LogManager::getFilter(QLatin1StringView category) {
return this->allFilters.value(category);
}
void LoggingThreadProxy::initInThread() {
this->logging = new ThreadLogging(this);
this->logging->init();
}
void LoggingThreadProxy::initFs() { this->logging->initFs(); }
void ThreadLogging::init() {
auto logMfd = memfd_create("quickshell:logs", 0);
if (logMfd == -1) {
qCCritical(logLogging) << "Failed to create memfd for initial log storage"
<< qt_error_string(-1);
}
auto dlogMfd = memfd_create("quickshell:detailedlogs", 0);
if (dlogMfd == -1) {
qCCritical(logLogging) << "Failed to create memfd for initial detailed log storage"
<< qt_error_string(-1);
}
if (logMfd != -1) {
this->file = new QFile();
this->file->open(logMfd, QFile::ReadWrite, QFile::AutoCloseHandle);
this->fileStream.setDevice(this->file);
}
if (dlogMfd != -1) {
crash::CrashInfo::INSTANCE.logFd = dlogMfd;
this->detailedFile = new QFile();
// buffered by WriteBuffer
this->detailedFile->open(dlogMfd, QFile::ReadWrite | QFile::Unbuffered, QFile::AutoCloseHandle);
this->detailedWriter.setDevice(this->detailedFile);
if (!this->detailedWriter.writeHeader()) {
qCCritical(logLogging) << "Could not write header for detailed logs.";
this->detailedWriter.setDevice(nullptr);
delete this->detailedFile;
this->detailedFile = nullptr;
}
}
// This connection is direct so it works while the event loop is destroyed between
// QCoreApplication delete and Q(Gui)Application launch.
QObject::connect(
LogManager::instance(),
&LogManager::logMessage,
this,
&ThreadLogging::onMessage,
Qt::DirectConnection
);
qCDebug(logLogging) << "Created memfd" << logMfd << "for early logs.";
qCDebug(logLogging) << "Created memfd" << dlogMfd << "for early detailed logs.";
}
void ThreadLogging::initFs() {
qCDebug(logLogging) << "Starting filesystem logging...";
auto* runDir = QsPaths::instance()->instanceRunDir();
if (!runDir) {
qCCritical(logLogging
) << "Could not start filesystem logging as the runtime directory could not be created.";
return;
}
auto path = runDir->filePath("log.log");
auto detailedPath = runDir->filePath("log.qslog");
auto* file = new QFile(path);
auto* detailedFile = new QFile(detailedPath);
if (!file->open(QFile::ReadWrite | QFile::Truncate)) {
qCCritical(logLogging
) << "Could not start filesystem logger as the log file could not be created:"
<< path;
delete file;
file = nullptr;
} else {
qInfo() << "Saving logs to" << path;
}
// buffered by WriteBuffer
if (!detailedFile->open(QFile::ReadWrite | QFile::Truncate | QFile::Unbuffered)) {
qCCritical(logLogging
) << "Could not start detailed filesystem logger as the log file could not be created:"
<< detailedPath;
delete detailedFile;
detailedFile = nullptr;
} else {
auto lock = flock {
.l_type = F_WRLCK,
.l_whence = SEEK_SET,
.l_start = 0,
.l_len = 0,
.l_pid = 0,
};
if (fcntl(detailedFile->handle(), F_SETLK, &lock) != 0) { // NOLINT
qCWarning(logLogging) << "Unable to set lock marker on detailed log file. --follow from "
"other instances will not work.";
}
qCInfo(logLogging) << "Saving detailed logs to" << path;
}
qCDebug(logLogging) << "Copying memfd logs to log file...";
if (file) {
auto* oldFile = this->file;
if (oldFile) {
oldFile->seek(0);
sendfile(file->handle(), oldFile->handle(), nullptr, oldFile->size());
}
this->file = file;
this->fileStream.setDevice(file);
delete oldFile;
}
if (detailedFile) {
auto* oldFile = this->detailedFile;
if (oldFile) {
oldFile->seek(0);
sendfile(detailedFile->handle(), oldFile->handle(), nullptr, oldFile->size());
}
crash::CrashInfo::INSTANCE.logFd = detailedFile->handle();
this->detailedFile = detailedFile;
this->detailedWriter.setDevice(detailedFile);
if (!oldFile) {
if (!this->detailedWriter.writeHeader()) {
qCCritical(logLogging) << "Could not write header for detailed logs.";
this->detailedWriter.setDevice(nullptr);
delete this->detailedFile;
this->detailedFile = nullptr;
}
}
delete oldFile;
}
qCDebug(logLogging) << "Switched logging to disk logs.";
auto* logManager = LogManager::instance();
QObject::disconnect(logManager, &LogManager::logMessage, this, &ThreadLogging::onMessage);
QObject::connect(
logManager,
&LogManager::logMessage,
this,
&ThreadLogging::onMessage,
Qt::QueuedConnection
);
qCDebug(logLogging) << "Switched threaded logger to queued eventloop connection.";
}
void ThreadLogging::onMessage(const LogMessage& msg, bool showInSparse) {
if (showInSparse) {
if (this->fileStream.device() == nullptr) return;
LogMessage::formatMessage(this->fileStream, msg, false, true);
this->fileStream << Qt::endl;
}
if (this->detailedWriter.write(msg)) {
this->detailedFile->flush();
} else if (this->detailedFile != nullptr) {
qCCritical(logLogging) << "Detailed logger failed to write. Ending detailed logs.";
}
}
CompressedLogType compressedTypeOf(QtMsgType type) {
switch (type) {
case QtDebugMsg: return CompressedLogType::Debug;
case QtInfoMsg: return CompressedLogType::Info;
case QtWarningMsg: return CompressedLogType::Warn;
case QtCriticalMsg:
case QtFatalMsg: return CompressedLogType::Critical;
}
return CompressedLogType::Info; // unreachable under normal conditions
}
QtMsgType typeOfCompressed(CompressedLogType type) {
switch (type) {
case CompressedLogType::Debug: return QtDebugMsg;
case CompressedLogType::Info: return QtInfoMsg;
case CompressedLogType::Warn: return QtWarningMsg;
case CompressedLogType::Critical: return QtCriticalMsg;
}
return QtInfoMsg; // unreachable under normal conditions
}
void WriteBuffer::setDevice(QIODevice* device) { this->device = device; }
bool WriteBuffer::hasDevice() const { return this->device; }
bool WriteBuffer::flush() {
auto written = this->device->write(this->buffer);
auto success = written == this->buffer.length();
this->buffer.clear();
return success;
}
void WriteBuffer::writeBytes(const char* data, qsizetype length) {
this->buffer.append(data, length);
}
void WriteBuffer::writeU8(quint8 data) { this->writeBytes(reinterpret_cast<char*>(&data), 1); }
void WriteBuffer::writeU16(quint16 data) {
data = qToLittleEndian(data);
this->writeBytes(reinterpret_cast<char*>(&data), 2);
}
void WriteBuffer::writeU32(quint32 data) {
data = qToLittleEndian(data);
this->writeBytes(reinterpret_cast<char*>(&data), 4);
}
void WriteBuffer::writeU64(quint64 data) {
data = qToLittleEndian(data);
this->writeBytes(reinterpret_cast<char*>(&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<char*>(data), 1);
}
bool DeviceReader::readU16(quint16* data) {
return this->readBytes(reinterpret_cast<char*>(data), 2);
}
bool DeviceReader::readU32(quint32* data) {
return this->readBytes(reinterpret_cast<char*>(data), 4);
}
bool DeviceReader::readU64(quint64* data) {
return this->readBytes(reinterpret_cast<char*>(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<qint64>(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<CompressedLogType>(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<qint64>(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<quint8, 7>();
auto readLength = this->reader.peekBytes(reinterpret_cast<char*>(bytes.data()), 7);
if (bytes[0] != 0xff && readLength >= 1) {
auto n = *reinterpret_cast<quint8*>(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<quint16*>(bytes.data() + 1);
if (!this->reader.skip(3)) return false;
*slot = qFromLittleEndian(n);
} else if (readLength == 7) {
auto n = *reinterpret_cast<quint32*>(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<LogMessage>(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<QLoggingRule> 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

View file

@ -1,148 +0,0 @@
#pragma once
#include <utility>
#include <qcontainerfwd.h>
#include <qdatetime.h>
#include <qfile.h>
#include <qhash.h>
#include <qlatin1stringview.h>
#include <qlogging.h>
#include <qloggingcategory.h>
#include <qobject.h>
#include <qtmetamacros.h>
Q_DECLARE_LOGGING_CATEGORY(logBare);
namespace qs::log {
struct LogMessage {
explicit LogMessage() = default;
explicit LogMessage(
QtMsgType type,
QLatin1StringView category,
QByteArray body,
QDateTime time = QDateTime::currentDateTime()
)
: type(type)
, time(std::move(time))
, category(category)
, body(std::move(body)) {}
bool operator==(const LogMessage& other) const;
QtMsgType type = QtDebugMsg;
QDateTime time;
QLatin1StringView category;
QByteArray body;
quint16 readCategoryId = 0;
static void formatMessage(
QTextStream& stream,
const LogMessage& msg,
bool color,
bool timestamp,
const QString& prefix = ""
);
};
size_t qHash(const LogMessage& message);
class ThreadLogging;
class LoggingThreadProxy: public QObject {
Q_OBJECT;
public:
explicit LoggingThreadProxy() = default;
public slots:
void initInThread();
void initFs();
private:
ThreadLogging* logging = nullptr;
};
namespace qt_logging_registry {
class QLoggingRule;
}
struct CategoryFilter {
explicit CategoryFilter() = default;
explicit CategoryFilter(QLoggingCategory* category)
: debug(category->isDebugEnabled())
, info(category->isInfoEnabled())
, warn(category->isWarningEnabled())
, critical(category->isCriticalEnabled()) {}
[[nodiscard]] bool shouldDisplay(QtMsgType type) const;
void apply(QLoggingCategory* category) const;
void applyRule(QLatin1StringView category, const qt_logging_registry::QLoggingRule& rule);
bool debug = true;
bool info = true;
bool warn = true;
bool critical = true;
};
class LogManager: public QObject {
Q_OBJECT;
public:
static void init(
bool color,
bool timestamp,
bool sparseOnly,
QtMsgType defaultLevel,
const QString& rules,
const QString& prefix = ""
);
static void initFs();
static LogManager* instance();
bool colorLogs = true;
bool timestampLogs = false;
[[nodiscard]] QString rulesString() const;
[[nodiscard]] QtMsgType defaultLevel() const;
[[nodiscard]] bool isSparse() const;
[[nodiscard]] CategoryFilter getFilter(QLatin1StringView category);
signals:
void logMessage(LogMessage msg, bool showInSparse);
private:
explicit LogManager();
static void messageHandler(QtMsgType type, const QMessageLogContext& context, const QString& msg);
static void filterCategory(QLoggingCategory* category);
QLoggingCategory::CategoryFilter lastCategoryFilter = nullptr;
bool sparse = false;
QString prefix;
QString mRulesString;
QList<qt_logging_registry::QLoggingRule>* rules = nullptr;
QtMsgType mDefaultLevel = QtWarningMsg;
QHash<const void*, CategoryFilter> sparseFilters;
QHash<QLatin1StringView, CategoryFilter> allFilters;
QTextStream stdoutStream;
LoggingThreadProxy threadProxy;
};
bool readEncodedLogs(
QFile* file,
const QString& path,
bool timestamps,
int tail,
bool follow,
const QString& rulespec
);
} // namespace qs::log
using LogManager = qs::log::LogManager;

View file

@ -1,190 +0,0 @@
#pragma once
#include <utility>
#include <qbytearrayview.h>
#include <qcontainerfwd.h>
#include <qfile.h>
#include <qfilesystemwatcher.h>
#include <qlogging.h>
#include <qobject.h>
#include <qthread.h>
#include <qtmetamacros.h>
#include <qtypes.h>
#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<QLatin1StringView, quint16> categories;
quint16 nextCategory = EncodedLogOpcode::BeginCategories;
QDateTime lastMessageTime = QDateTime::fromSecsSinceEpoch(0);
HashBuffer<LogMessage> 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<QPair<QByteArray, CategoryFilter>> categories;
QDateTime lastMessageTime = QDateTime::fromSecsSinceEpoch(0);
RingBuffer<LogMessage> 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<qt_logging_registry::QLoggingRule> 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<quint16, CategoryFilter> filters;
QList<qt_logging_registry::QLoggingRule> 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

View file

@ -1,138 +0,0 @@
// The logging rule parser from qloggingregistry_p.h and qloggingregistry.cpp.
// Was unable to properly link the functions when directly using the headers (which we depend
// on anyway), so below is a slightly stripped down copy. Making the originals link would
// be preferable.
#include <utility>
#include <qbytearrayview.h>
#include <qchar.h>
#include <qflags.h>
#include <qlist.h>
#include <qlogging.h>
#include <qloggingcategory.h>
#include <qstringtokenizer.h>
#include <qstringview.h>
#include <qtypes.h>
#include "logging_qtprivate.hpp"
namespace qs::log {
Q_DECLARE_LOGGING_CATEGORY(logLogging);
namespace qt_logging_registry {
class QLoggingSettingsParser {
public:
void setContent(QStringView content);
[[nodiscard]] QList<QLoggingRule> rules() const { return this->mRules; }
private:
void parseNextLine(QStringView line);
private:
QList<QLoggingRule> 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

View file

@ -1,45 +0,0 @@
#pragma once
// The logging rule parser from qloggingregistry_p.h and qloggingregistry.cpp.
// Was unable to properly link the functions when directly using the headers (which we depend
// on anyway), so below is a slightly stripped down copy. Making the originals link would
// be preferable.
#include <qflags.h>
#include <qlogging.h>
#include <qloggingcategory.h>
#include <qstringview.h>
#include <qtypes.h>
namespace qs::log {
Q_DECLARE_LOGGING_CATEGORY(logLogging);
namespace qt_logging_registry {
class QLoggingRule {
public:
QLoggingRule();
QLoggingRule(QStringView pattern, bool enabled);
[[nodiscard]] int pass(QLatin1StringView categoryName, QtMsgType type) const;
enum PatternFlag : quint8 {
FullText = 0x1,
LeftFilter = 0x2,
RightFilter = 0x4,
MidFilter = LeftFilter | RightFilter
};
Q_DECLARE_FLAGS(PatternFlags, PatternFlag)
QString category;
int messageType;
PatternFlags flags;
bool enabled;
private:
void parse(QStringView pattern);
};
} // namespace qt_logging_registry
} // namespace qs::log

331
src/core/main.cpp Normal file
View file

@ -0,0 +1,331 @@
#include "main.hpp"
#include <iostream>
#include <qapplication.h>
#include <qcommandlineoption.h>
#include <qcommandlineparser.h>
#include <qcoreapplication.h>
#include <qdir.h>
#include <qfileinfo.h>
#include <qguiapplication.h>
#include <qhash.h>
#include <qlogging.h>
#include <qquickwindow.h>
#include <qstandardpaths.h>
#include <qstring.h>
#include <qtenvironmentvariables.h>
#include <qtextstream.h>
#include <qtpreprocessorsupport.h>
#include "plugin.hpp"
#include "rootwrapper.hpp"
int qs_main(int argc, char** argv) {
QString configFilePath;
QString workingDirectory;
auto useQApplication = false;
auto nativeTextRendering = false;
auto desktopSettingsAware = true;
QHash<QString, QString> envOverrides;
{
const auto app = QCoreApplication(argc, argv);
QCoreApplication::setApplicationName("quickshell");
QCoreApplication::setApplicationVersion("0.1.0 (" GIT_REVISION ")");
QCommandLineParser parser;
parser.addHelpOption();
parser.addVersionOption();
// clang-format off
auto currentOption = QCommandLineOption("current", "Print information about the manifest and defaults.");
auto manifestOption = QCommandLineOption({"m", "manifest"}, "Path to a configuration manifest.", "path");
auto configOption = QCommandLineOption({"c", "config"}, "Name of a configuration in the manifest.", "name");
auto pathOption = QCommandLineOption({"p", "path"}, "Path to a configuration file.", "path");
auto workdirOption = QCommandLineOption({"d", "workdir"}, "Initial working directory.", "path");
// clang-format on
parser.addOption(currentOption);
parser.addOption(manifestOption);
parser.addOption(configOption);
parser.addOption(pathOption);
parser.addOption(workdirOption);
parser.process(app);
{
auto printCurrent = parser.isSet(currentOption);
// NOLINTBEGIN
#define CHECK(rname, name, level, label, expr) \
QString name = expr; \
if (rname.isEmpty() && !name.isEmpty()) { \
rname = name; \
rname##Level = level; \
if (!printCurrent) goto label; \
}
#define OPTSTR(name) (name.isEmpty() ? "(unset)" : name.toStdString())
// NOLINTEND
QString basePath;
int basePathLevel = 0;
Q_UNUSED(basePathLevel);
{
// NOLINTBEGIN
// clang-format off
CHECK(basePath, envBasePath, 0, foundbase, qEnvironmentVariable("QS_BASE_PATH"));
CHECK(basePath, defaultBasePath, 0, foundbase, QDir(QStandardPaths::writableLocation(QStandardPaths::ConfigLocation)).filePath("quickshell"));
// clang-format on
// NOLINTEND
if (printCurrent) {
// clang-format off
std::cout << "Base path: " << OPTSTR(basePath) << "\n";
std::cout << " - Environment (QS_BASE_PATH): " << OPTSTR(envBasePath) << "\n";
std::cout << " - Default: " << OPTSTR(defaultBasePath) << "\n";
// clang-format on
}
}
foundbase:;
QString configPath;
int configPathLevel = 10;
{
// NOLINTBEGIN
CHECK(configPath, optionConfigPath, 0, foundpath, parser.value(pathOption));
CHECK(configPath, envConfigPath, 1, foundpath, qEnvironmentVariable("QS_CONFIG_PATH"));
// NOLINTEND
if (printCurrent) {
// clang-format off
std::cout << "\nConfig path: " << OPTSTR(configPath) << "\n";
std::cout << " - Option: " << OPTSTR(optionConfigPath) << "\n";
std::cout << " - Environment (QS_CONFIG_PATH): " << OPTSTR(envConfigPath) << "\n";
// clang-format on
}
}
foundpath:;
QString manifestPath;
int manifestPathLevel = 10;
{
// NOLINTBEGIN
// clang-format off
CHECK(manifestPath, optionManifestPath, 0, foundmf, parser.value(manifestOption));
CHECK(manifestPath, envManifestPath, 1, foundmf, qEnvironmentVariable("QS_MANIFEST"));
CHECK(manifestPath, defaultManifestPath, 2, foundmf, QDir(basePath).filePath("manifest.conf"));
// clang-format on
// NOLINTEND
if (printCurrent) {
// clang-format off
std::cout << "\nManifest path: " << OPTSTR(manifestPath) << "\n";
std::cout << " - Option: " << OPTSTR(optionManifestPath) << "\n";
std::cout << " - Environment (QS_MANIFEST): " << OPTSTR(envManifestPath) << "\n";
std::cout << " - Default: " << OPTSTR(defaultManifestPath) << "\n";
// clang-format on
}
}
foundmf:;
QString configName;
int configNameLevel = 10;
{
// NOLINTBEGIN
CHECK(configName, optionConfigName, 0, foundname, parser.value(configOption));
CHECK(configName, envConfigName, 1, foundname, qEnvironmentVariable("QS_CONFIG_NAME"));
// NOLINTEND
if (printCurrent) {
// clang-format off
std::cout << "\nConfig name: " << OPTSTR(configName) << "\n";
std::cout << " - Option: " << OPTSTR(optionConfigName) << "\n";
std::cout << " - Environment (QS_CONFIG_NAME): " << OPTSTR(envConfigName) << "\n\n";
// clang-format on
}
}
foundname:;
if (configPathLevel == 0 && configNameLevel == 0) {
qCritical() << "Pass only one of --path or --config";
return -1;
}
if (!configPath.isEmpty() && configPathLevel <= configNameLevel) {
configFilePath = configPath;
} else if (!configName.isEmpty()) {
if (!manifestPath.isEmpty()) {
auto file = QFile(manifestPath);
if (file.open(QIODevice::ReadOnly | QIODevice::Text)) {
auto stream = QTextStream(&file);
while (!stream.atEnd()) {
auto line = stream.readLine();
if (line.trimmed().startsWith("#")) continue;
if (line.trimmed().isEmpty()) continue;
auto split = line.split('=');
if (split.length() != 2) {
qCritical() << "manifest line not in expected format 'name = relativepath':"
<< line;
return -1;
}
if (split[0].trimmed() == configName) {
configFilePath = QDir(QFileInfo(file).canonicalPath()).filePath(split[1].trimmed());
goto haspath; // NOLINT
}
}
qCritical() << "configuration" << configName << "not found in manifest" << manifestPath;
return -1;
} else if (manifestPathLevel < 2) {
qCritical() << "cannot open config manifest at" << manifestPath;
return -1;
}
}
{
auto basePathInfo = QFileInfo(basePath);
if (!basePathInfo.exists()) {
qCritical() << "base path does not exist:" << basePath;
return -1;
} else if (!QFileInfo(basePathInfo.canonicalFilePath()).isDir()) {
qCritical() << "base path is not a directory" << basePath;
return -1;
}
auto dir = QDir(basePath);
for (auto& entry: dir.entryList(QDir::AllDirs | QDir::NoDotAndDotDot)) {
if (entry == configName) {
configFilePath = dir.filePath(entry);
goto haspath; // NOLINT
}
}
qCritical() << "no directory named " << configName << "found in base path" << basePath;
return -1;
}
haspath:;
} else {
configFilePath = basePath;
}
auto configFile = QFileInfo(configFilePath);
if (!configFile.exists()) {
qCritical() << "config path does not exist:" << configFilePath;
return -1;
}
if (configFile.isDir()) {
configFilePath = QDir(configFilePath).filePath("shell.qml");
}
configFile = QFileInfo(configFilePath);
if (!configFile.exists()) {
qCritical() << "no shell.qml found in config path:" << configFilePath;
return -1;
} else if (configFile.isDir()) {
qCritical() << "shell.qml is a directory:" << configFilePath;
return -1;
}
configFilePath = QFileInfo(configFilePath).canonicalFilePath();
configFile = QFileInfo(configFilePath);
if (!configFile.exists()) {
qCritical() << "config file does not exist:" << configFilePath;
return -1;
} else if (configFile.isDir()) {
qCritical() << "config file is a directory:" << configFilePath;
return -1;
}
#undef CHECK
#undef OPTSTR
qInfo() << "config file path:" << configFilePath;
if (printCurrent) return 0;
}
if (!QFile(configFilePath).exists()) {
qCritical() << "config file does not exist";
return -1;
}
if (parser.isSet(workdirOption)) {
workingDirectory = parser.value(workdirOption);
}
auto file = QFile(configFilePath);
if (!file.open(QFile::ReadOnly | QFile::Text)) {
qCritical() << "could not open config file";
return -1;
}
auto stream = QTextStream(&file);
while (!stream.atEnd()) {
auto line = stream.readLine().trimmed();
if (line.startsWith("//@ pragma ")) {
auto pragma = line.sliced(11).trimmed();
if (pragma == "UseQApplication") useQApplication = true;
else if (pragma == "NativeTextRendering") nativeTextRendering = true;
else if (pragma == "IgnoreSystemSettings") desktopSettingsAware = false;
else if (pragma.startsWith("Env ")) {
auto envPragma = pragma.sliced(4);
auto splitIdx = envPragma.indexOf('=');
if (splitIdx == -1) {
qCritical() << "Env pragma" << pragma << "not in the form 'VAR = VALUE'";
return -1;
}
auto var = envPragma.sliced(0, splitIdx).trimmed();
auto val = envPragma.sliced(splitIdx + 1).trimmed();
envOverrides.insert(var, val);
} else {
qCritical() << "Unrecognized pragma" << pragma;
return -1;
}
} else if (line.startsWith("import")) break;
}
file.close();
}
for (auto [var, val]: envOverrides.asKeyValueRange()) {
qputenv(var.toUtf8(), val.toUtf8());
}
QGuiApplication::setDesktopSettingsAware(desktopSettingsAware);
QGuiApplication* app = nullptr;
if (useQApplication) {
app = new QApplication(argc, argv);
} else {
app = new QGuiApplication(argc, argv);
}
if (!workingDirectory.isEmpty()) {
QDir::setCurrent(workingDirectory);
}
QuickshellPlugin::initPlugins();
// Base window transparency appears to be additive.
// Use a fully transparent window with a colored rect.
QQuickWindow::setDefaultAlphaBuffer(true);
if (nativeTextRendering) {
QQuickWindow::setTextRenderType(QQuickWindow::NativeTextRendering);
}
auto root = RootWrapper(configFilePath);
QGuiApplication::setQuitOnLastWindowClosed(false);
auto code = QGuiApplication::exec();
delete app;
return code;
}

3
src/core/main.hpp Normal file
View file

@ -0,0 +1,3 @@
#pragma once
int qs_main(int argc, char** argv); // NOLINT

View file

@ -1,98 +0,0 @@
#include "model.hpp"
#include <qabstractitemmodel.h>
#include <qhash.h>
#include <qnamespace.h>
#include <qobject.h>
#include <qqmllist.h>
#include <qtmetamacros.h>
#include <qtypes.h>
#include <qvariant.h>
qint32 UntypedObjectModel::rowCount(const QModelIndex& parent) const {
if (parent != QModelIndex()) return 0;
return static_cast<qint32>(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<int, QByteArray> UntypedObjectModel::roleNames() const {
return {{Qt::UserRole, "modelData"}};
}
QQmlListProperty<QObject> UntypedObjectModel::values() {
return QQmlListProperty<QObject>(
this,
nullptr,
&UntypedObjectModel::valuesCount,
&UntypedObjectModel::valueAt
);
}
qsizetype UntypedObjectModel::valuesCount(QQmlListProperty<QObject>* property) {
return static_cast<UntypedObjectModel*>(property->object)->valuesList.count(); // NOLINT
}
QObject* UntypedObjectModel::valueAt(QQmlListProperty<QObject>* property, qsizetype index) {
return static_cast<UntypedObjectModel*>(property->object)->valuesList.at(index); // NOLINT
}
void UntypedObjectModel::insertObject(QObject* object, qsizetype index) {
auto iindex = index == -1 ? this->valuesList.length() : index;
emit this->objectInsertedPre(object, iindex);
auto intIndex = static_cast<qint32>(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<qint32>(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<QObject*>& 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;
}

View file

@ -1,111 +0,0 @@
#pragma once
#include <bit>
#include <qabstractitemmodel.h>
#include <qcontainerfwd.h>
#include <qobject.h>
#include <qqmlintegration.h>
#include <qqmllist.h>
#include <qtmetamacros.h>
#include <qtypes.h>
#include <qvariant.h>
#include "doc.hpp"
///! View into a list of objets
/// Typed view into a list of objects.
///
/// An ObjectModel works as a QML [Data Model], allowing efficient interaction with
/// components that act on models. It has a single role named `modelData`, to match the
/// behavior of lists.
/// The same information contained in the list model is available as a normal list
/// via the `values` property.
///
/// #### Differences from a list
/// Unlike with a list, the following property binding will never be updated when `model[3]` changes.
/// ```qml
/// // will not update reactively
/// property var foo: model[3]
/// ```
///
/// You can work around this limitation using the @@values property of the model to view it as a list.
/// ```qml
/// // will update reactively
/// property var foo: model.values[3]
/// ```
///
/// [Data Model]: https://doc.qt.io/qt-6/qtquick-modelviewsdata-modelview.html#qml-data-models
class UntypedObjectModel: public QAbstractListModel {
QSDOC_CNAME(ObjectModel);
Q_OBJECT;
/// The content of the object model, as a QML list.
/// The values of this property will always be of the type of the model.
Q_PROPERTY(QQmlListProperty<QObject> 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<int, QByteArray> roleNames() const override;
[[nodiscard]] QQmlListProperty<QObject> values();
void removeAt(qsizetype index);
Q_INVOKABLE qsizetype indexOf(QObject* object);
static UntypedObjectModel* emptyInstance();
signals:
void valuesChanged();
/// Sent immediately before an object is inserted into the list.
void objectInsertedPre(QObject* object, qsizetype index);
/// Sent immediately after an object is inserted into the list.
void objectInsertedPost(QObject* object, qsizetype index);
/// Sent immediately before an object is removed from the list.
void objectRemovedPre(QObject* object, qsizetype index);
/// Sent immediately after an object is removed from the list.
void objectRemovedPost(QObject* object, qsizetype index);
protected:
void insertObject(QObject* object, qsizetype index = -1);
bool removeObject(const QObject* object);
// Assumes only one instance of a specific value
void diffUpdate(const QVector<QObject*>& newValues);
QVector<QObject*> valuesList;
private:
static qsizetype valuesCount(QQmlListProperty<QObject>* property);
static QObject* valueAt(QQmlListProperty<QObject>* property, qsizetype index);
};
template <typename T>
class ObjectModel: public UntypedObjectModel {
public:
explicit ObjectModel(QObject* parent): UntypedObjectModel(parent) {}
[[nodiscard]] QVector<T*>& valueList() { return *std::bit_cast<QVector<T*>*>(&this->valuesList); }
[[nodiscard]] const QVector<T*>& valueList() const {
return *std::bit_cast<const QVector<T*>*>(&this->valuesList);
}
void insertObject(T* object, qsizetype index = -1) {
this->UntypedObjectModel::insertObject(object, index);
}
void removeObject(const T* object) { this->UntypedObjectModel::removeObject(object); }
// Assumes only one instance of a specific value
void diffUpdate(const QVector<T*>& newValues) {
this->UntypedObjectModel::diffUpdate(*std::bit_cast<const QVector<QObject*>*>(&newValues));
}
static ObjectModel<T>* emptyInstance() {
return static_cast<ObjectModel<T>*>(UntypedObjectModel::emptyInstance());
}
};

View file

@ -7,27 +7,16 @@ headers = [
"shell.hpp",
"variants.hpp",
"region.hpp",
"../window/proxywindow.hpp",
"proxywindow.hpp",
"persistentprops.hpp",
"../window/windowinterface.hpp",
"../window/panelinterface.hpp",
"../window/floatingwindow.hpp",
"../window/popupwindow.hpp",
"windowinterface.hpp",
"panelinterface.hpp",
"floatingwindow.hpp",
"popupwindow.hpp",
"singleton.hpp",
"lazyloader.hpp",
"easingcurve.hpp",
"transformwatcher.hpp",
"boundcomponent.hpp",
"model.hpp",
"elapsedtimer.hpp",
"desktopentry.hpp",
"objectrepeater.hpp",
"qsmenu.hpp",
"retainable.hpp",
"popupanchor.hpp",
"types.hpp",
"qsmenuanchor.hpp",
"clock.hpp",
"scriptmodel.hpp",
]
-----

View file

@ -1,190 +0,0 @@
#include "objectrepeater.hpp"
#include <utility>
#include <qabstractitemmodel.h>
#include <qcontainerfwd.h>
#include <qhash.h>
#include <qlogging.h>
#include <qobject.h>
#include <qqmlcomponent.h>
#include <qqmlcontext.h>
#include <qqmlengine.h>
#include <qqmllist.h>
#include <qtmetamacros.h>
#include <qtypes.h>
#include <qvariant.h>
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<QAbstractItemModel*>()) {
auto* model = this->mModel.value<QAbstractItemModel*>();
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<QQmlListReference>()) {
auto values = this->mModel.value<QQmlListReference>();
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<QVector<QVariant>>()) {
auto values = this->mModel.value<QVector<QVariant>>();
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<QModelRoleData>();
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<int>(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;
}

View file

@ -1,85 +0,0 @@
#pragma once
#include <qabstractitemmodel.h>
#include <qobject.h>
#include <qqmlcomponent.h>
#include <qqmlintegration.h>
#include <qtmetamacros.h>
#include <qtypes.h>
#include <qvariant.h>
#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<QObject> {
Q_OBJECT;
/// The model providing data to the ObjectRepeater.
///
/// Currently accepted model types are `list<T>` 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<T>` 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;
};

View file

@ -2,9 +2,8 @@
#include <qqmlintegration.h>
#include <qtmetamacros.h>
#include <qtypes.h>
#include "../core/doc.hpp"
#include "doc.hpp"
#include "windowinterface.hpp"
class Anchors {
@ -13,8 +12,7 @@ class Anchors {
Q_PROPERTY(bool right MEMBER mRight);
Q_PROPERTY(bool top MEMBER mTop);
Q_PROPERTY(bool bottom MEMBER mBottom);
QML_VALUE_TYPE(panelAnchors);
QML_STRUCTURED_VALUE;
QML_VALUE_TYPE(anchors);
public:
[[nodiscard]] bool horizontalConstraint() const noexcept { return this->mLeft && this->mRight; }
@ -41,8 +39,7 @@ class Margins {
Q_PROPERTY(qint32 right MEMBER mRight);
Q_PROPERTY(qint32 top MEMBER mTop);
Q_PROPERTY(qint32 bottom MEMBER mBottom);
QML_VALUE_TYPE(panelMargins);
QML_STRUCTURED_VALUE;
QML_VALUE_TYPE(margins);
public:
[[nodiscard]] bool operator==(const Margins& other) const noexcept {
@ -60,13 +57,11 @@ public:
qint32 mBottom = 0;
};
///! Panel exclusion mode
/// See @@PanelWindow.exclusionMode.
namespace ExclusionMode { // NOLINT
Q_NAMESPACE;
QML_ELEMENT;
enum Enum : quint8 {
enum Enum {
/// Respect the exclusion zone of other shell layers and optionally set one
Normal = 0,
/// Ignore exclusion zones of other shell layers. You cannot set an exclusion zone in this mode.
@ -116,24 +111,14 @@ class PanelWindowInterface: public WindowInterface {
/// > [!INFO] Only applies to edges with anchors
Q_PROPERTY(Margins margins READ margins WRITE setMargins NOTIFY marginsChanged);
/// The amount of space reserved for the shell layer relative to its anchors.
/// Setting this property sets @@exclusionMode to `ExclusionMode.Normal`.
/// Setting this property sets `exclusionMode` to `Normal`.
///
/// > [!INFO] Either 1 or 3 anchors are required for the zone to take effect.
Q_PROPERTY(qint32 exclusiveZone READ exclusiveZone WRITE setExclusiveZone NOTIFY exclusiveZoneChanged);
/// Defaults to `ExclusionMode.Auto`.
Q_PROPERTY(ExclusionMode::Enum exclusionMode READ exclusionMode WRITE setExclusionMode NOTIFY exclusionModeChanged);
/// If the panel should render above standard windows. Defaults to true.
///
/// Note: On Wayland this property corrosponds to @@Quickshell.Wayland.WlrLayershell.layer.
Q_PROPERTY(bool aboveWindows READ aboveWindows WRITE setAboveWindows NOTIFY aboveWindowsChanged);
/// If the panel should accept keyboard focus. Defaults to false.
///
/// Note: On Wayland this property corrosponds to @@Quickshell.Wayland.WlrLayershell.keyboardFocus.
Q_PROPERTY(bool focusable READ focusable WRITE setFocusable NOTIFY focusableChanged);
// clang-format on
QML_NAMED_ELEMENT(PanelWindow);
QML_UNCREATABLE("No PanelWindow backend loaded.");
QSDOC_CREATABLE;
QSDOC_NAMED_ELEMENT(PanelWindow);
public:
explicit PanelWindowInterface(QObject* parent = nullptr): WindowInterface(parent) {}
@ -150,17 +135,9 @@ public:
[[nodiscard]] virtual ExclusionMode::Enum exclusionMode() const = 0;
virtual void setExclusionMode(ExclusionMode::Enum exclusionMode) = 0;
[[nodiscard]] virtual bool aboveWindows() const = 0;
virtual void setAboveWindows(bool aboveWindows) = 0;
[[nodiscard]] virtual bool focusable() const = 0;
virtual void setFocusable(bool focusable) = 0;
signals:
void anchorsChanged();
void marginsChanged();
void exclusiveZoneChanged();
void exclusionModeChanged();
void aboveWindowsChanged();
void focusableChanged();
};

View file

@ -1,310 +0,0 @@
#include "paths.hpp"
#include <cerrno>
#include <cstdio>
#include <utility>
#include <fcntl.h>
#include <qcontainerfwd.h>
#include <qdatastream.h>
#include <qdir.h>
#include <qlogging.h>
#include <qloggingcategory.h>
#include <qstandardpaths.h>
#include <qtenvironmentvariables.h>
#include <unistd.h>
#include "instanceinfo.hpp"
namespace {
Q_LOGGING_CATEGORY(logPaths, "quickshell.paths", QtWarningMsg);
}
QsPaths* QsPaths::instance() {
static auto* instance = new QsPaths(); // NOLINT
return instance;
}
void QsPaths::init(QString shellId, QString pathId) {
auto* instance = QsPaths::instance();
instance->shellId = std::move(shellId);
instance->pathId = std::move(pathId);
}
QDir QsPaths::crashDir(const QString& id) {
auto dir = QDir(QStandardPaths::writableLocation(QStandardPaths::CacheLocation));
dir = QDir(dir.filePath("crashes"));
dir = QDir(dir.filePath(id));
return dir;
}
QString QsPaths::basePath(const QString& id) {
auto path = QsPaths::instance()->baseRunDir()->filePath("by-id");
path = QDir(path).filePath(id);
return path;
}
QString QsPaths::ipcPath(const QString& id) {
return QDir(QsPaths::basePath(id)).filePath("ipc.sock");
}
QDir* QsPaths::cacheDir() {
if (this->cacheState == DirState::Unknown) {
auto dir = QDir(QStandardPaths::writableLocation(QStandardPaths::CacheLocation));
dir = QDir(dir.filePath(this->shellId));
this->mCacheDir = dir;
qCDebug(logPaths) << "Initialized cache path:" << dir.path();
if (!dir.mkpath(".")) {
qCCritical(logPaths) << "Could not create cache directory at" << dir.path();
this->cacheState = DirState::Failed;
} else {
this->cacheState = DirState::Ready;
}
}
if (this->cacheState == DirState::Failed) return nullptr;
else return &this->mCacheDir;
}
QDir* QsPaths::baseRunDir() {
if (this->baseRunState == DirState::Unknown) {
auto runtimeDir = qEnvironmentVariable("XDG_RUNTIME_DIR");
if (runtimeDir.isEmpty()) {
runtimeDir = QString("/run/user/$1").arg(getuid());
qCInfo(logPaths) << "XDG_RUNTIME_DIR was not set, defaulting to" << runtimeDir;
}
this->mBaseRunDir = QDir(runtimeDir);
this->mBaseRunDir = QDir(this->mBaseRunDir.filePath("quickshell"));
qCDebug(logPaths) << "Initialized base runtime path:" << this->mBaseRunDir.path();
if (!this->mBaseRunDir.mkpath(".")) {
qCCritical(logPaths) << "Could not create base runtime directory at"
<< this->mBaseRunDir.path();
this->baseRunState = DirState::Failed;
} else {
this->baseRunState = DirState::Ready;
}
}
if (this->baseRunState == DirState::Failed) return nullptr;
else return &this->mBaseRunDir;
}
QDir* QsPaths::shellRunDir() {
if (this->shellRunState == DirState::Unknown) {
if (auto* baseRunDir = this->baseRunDir()) {
this->mShellRunDir = QDir(baseRunDir->filePath("by-shell"));
this->mShellRunDir = QDir(this->mShellRunDir.filePath(this->shellId));
qCDebug(logPaths) << "Initialized runtime path:" << this->mShellRunDir.path();
if (!this->mShellRunDir.mkpath(".")) {
qCCritical(logPaths) << "Could not create runtime directory at"
<< this->mShellRunDir.path();
this->shellRunState = DirState::Failed;
} else {
this->shellRunState = DirState::Ready;
}
} else {
qCCritical(logPaths) << "Could not create shell runtime path as it was not possible to "
"create the base runtime path.";
this->shellRunState = DirState::Failed;
}
}
if (this->shellRunState == DirState::Failed) return nullptr;
else return &this->mShellRunDir;
}
QDir* QsPaths::instanceRunDir() {
if (this->instanceRunState == DirState::Unknown) {
auto* runDir = this->baseRunDir();
if (!runDir) {
qCCritical(logPaths) << "Cannot create instance runtime directory as main runtim directory "
"could not be created.";
this->instanceRunState = DirState::Failed;
} else {
auto byIdDir = QDir(runDir->filePath("by-id"));
this->mInstanceRunDir = byIdDir.filePath(InstanceInfo::CURRENT.instanceId);
qCDebug(logPaths) << "Initialized instance runtime path:" << this->mInstanceRunDir.path();
if (!this->mInstanceRunDir.mkpath(".")) {
qCCritical(logPaths) << "Could not create instance runtime directory at"
<< this->mInstanceRunDir.path();
this->instanceRunState = DirState::Failed;
} else {
this->instanceRunState = DirState::Ready;
}
}
}
if (this->shellRunState == DirState::Failed) return nullptr;
else return &this->mInstanceRunDir;
}
void QsPaths::linkRunDir() {
if (auto* runDir = this->instanceRunDir()) {
auto pidDir = QDir(this->baseRunDir()->filePath("by-pid"));
auto* shellDir = this->shellRunDir();
if (!shellDir) {
qCCritical(logPaths
) << "Could not create by-id symlink as the shell runtime path could not be created.";
} else {
auto shellPath = shellDir->filePath(runDir->dirName());
QFile::remove(shellPath);
auto r =
symlinkat(runDir->filesystemCanonicalPath().c_str(), 0, shellPath.toStdString().c_str());
if (r != 0) {
qCCritical(logPaths).nospace()
<< "Could not create id symlink to " << runDir->path() << " at " << shellPath
<< " with error code " << errno << ": " << qt_error_string();
} else {
qCDebug(logPaths) << "Created shellid symlink" << shellPath << "to instance runtime path"
<< runDir->path();
}
}
if (!pidDir.mkpath(".")) {
qCCritical(logPaths) << "Could not create PID symlink directory.";
} else {
auto pidPath = pidDir.filePath(QString::number(getpid()));
QFile::remove(pidPath);
auto r =
symlinkat(runDir->filesystemCanonicalPath().c_str(), 0, pidPath.toStdString().c_str());
if (r != 0) {
qCCritical(logPaths).nospace()
<< "Could not create PID symlink to " << runDir->path() << " at " << pidPath
<< " with error code " << errno << ": " << qt_error_string();
} else {
qCDebug(logPaths) << "Created PID symlink" << pidPath << "to instance runtime path"
<< runDir->path();
}
}
} else {
qCCritical(logPaths) << "Could not create PID symlink to runtime directory, as the runtime "
"directory could not be created.";
}
}
void QsPaths::linkPathDir() {
if (auto* runDir = this->shellRunDir()) {
auto pathDir = QDir(this->baseRunDir()->filePath("by-path"));
if (!pathDir.mkpath(".")) {
qCCritical(logPaths) << "Could not create path symlink directory.";
return;
}
auto linkPath = pathDir.filePath(this->pathId);
QFile::remove(linkPath);
auto r =
symlinkat(runDir->filesystemCanonicalPath().c_str(), 0, linkPath.toStdString().c_str());
if (r != 0) {
qCCritical(logPaths).nospace()
<< "Could not create path symlink to " << runDir->path() << " at " << linkPath
<< " with error code " << errno << ": " << qt_error_string();
} else {
qCDebug(logPaths) << "Created path symlink" << linkPath << "to shell runtime path"
<< runDir->path();
}
} else {
qCCritical(logPaths) << "Could not create path symlink to shell runtime directory, as the "
"shell runtime directory could not be created.";
}
}
void QsPaths::createLock() {
if (auto* runDir = this->instanceRunDir()) {
auto path = runDir->filePath("instance.lock");
auto* file = new QFile(path); // leaked
if (!file->open(QFile::ReadWrite | QFile::Truncate)) {
qCCritical(logPaths) << "Could not create instance lock at" << path;
return;
}
auto lock = flock {
.l_type = F_WRLCK,
.l_whence = SEEK_SET,
.l_start = 0,
.l_len = 0,
.l_pid = 0,
};
if (fcntl(file->handle(), F_SETLK, &lock) != 0) { // NOLINT
qCCritical(logPaths).nospace() << "Could not lock instance lock at " << path
<< " with error code " << errno << ": " << qt_error_string();
} else {
auto stream = QDataStream(file);
stream << InstanceInfo::CURRENT;
file->flush();
qCDebug(logPaths) << "Created instance lock at" << path;
}
} else {
qCCritical(logPaths
) << "Could not create instance lock, as the instance runtime directory could not be created.";
}
}
bool QsPaths::checkLock(const QString& path, InstanceLockInfo* info) {
auto file = QFile(QDir(path).filePath("instance.lock"));
if (!file.open(QFile::ReadOnly)) return false;
auto lock = flock {
.l_type = F_WRLCK,
.l_whence = SEEK_SET,
.l_start = 0,
.l_len = 0,
.l_pid = 0,
};
fcntl(file.handle(), F_GETLK, &lock); // NOLINT
if (lock.l_type == F_UNLCK) return false;
if (info) {
info->pid = lock.l_pid;
auto stream = QDataStream(&file);
stream >> info->instance;
}
return true;
}
QVector<InstanceLockInfo> QsPaths::collectInstances(const QString& path) {
qCDebug(logPaths) << "Collecting instances from" << path;
auto instances = QVector<InstanceLockInfo>();
auto dir = QDir(path);
InstanceLockInfo info;
for (auto& entry: dir.entryList(QDir::Dirs | QDir::NoDotAndDotDot)) {
auto path = dir.filePath(entry);
if (QsPaths::checkLock(path, &info)) {
qCDebug(logPaths).nospace() << "Found live instance " << info.instance.instanceId << " (pid "
<< info.pid << ") at " << path;
instances.push_back(info);
} else {
qCDebug(logPaths) << "Skipped dead instance at" << path;
}
}
return instances;
}

View file

@ -1,51 +0,0 @@
#pragma once
#include <qdatetime.h>
#include <qdir.h>
#include <qtypes.h>
#include "instanceinfo.hpp"
struct InstanceLockInfo {
pid_t pid = -1;
InstanceInfo instance;
};
QDataStream& operator<<(QDataStream& stream, const InstanceLockInfo& info);
QDataStream& operator>>(QDataStream& stream, InstanceLockInfo& info);
class QsPaths {
public:
static QsPaths* instance();
static void init(QString shellId, QString pathId);
static QDir crashDir(const QString& id);
static QString basePath(const QString& id);
static QString ipcPath(const QString& id);
static bool checkLock(const QString& path, InstanceLockInfo* info = nullptr);
static QVector<InstanceLockInfo> collectInstances(const QString& path);
QDir* cacheDir();
QDir* baseRunDir();
QDir* shellRunDir();
QDir* instanceRunDir();
void linkRunDir();
void linkPathDir();
void createLock();
private:
enum class DirState : quint8 {
Unknown = 0,
Ready = 1,
Failed = 2,
};
QString shellId;
QString pathId;
QDir mCacheDir;
QDir mBaseRunDir;
QDir mShellRunDir;
QDir mInstanceRunDir;
DirState cacheState = DirState::Unknown;
DirState baseRunState = DirState::Unknown;
DirState shellRunState = DirState::Unknown;
DirState instanceRunState = DirState::Unknown;
};

View file

@ -1,324 +0,0 @@
#include "platformmenu.hpp"
#include <functional>
#include <utility>
#include <qaction.h>
#include <qactiongroup.h>
#include <qapplication.h>
#include <qcontainerfwd.h>
#include <qcoreapplication.h>
#include <qicon.h>
#include <qlogging.h>
#include <qmenu.h>
#include <qnamespace.h>
#include <qobject.h>
#include <qpoint.h>
#include <qqmllist.h>
#include <qtmetamacros.h>
#include <qwindow.h>
#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<std::function<void(PlatformMenuQMenu*)>> 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<void(PlatformMenuQMenu*)> hook) {
CREATION_HOOKS.push_back(std::move(hook));
}
bool PlatformMenuEntry::display(QObject* parentWindow, int relativeX, int relativeY) {
QWindow* window = nullptr;
if (qobject_cast<QApplication*>(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<ProxyWindowBase*>(parentWindow)) {
window = proxy->backingWindow();
} else if (auto* interface = qobject_cast<WindowInterface*>(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<QApplication*>(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<QApplication*>(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<PlatformMenuEntry*>(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

View file

@ -1,63 +0,0 @@
#pragma once
#include <functional>
#include <qaction.h>
#include <qactiongroup.h>
#include <qcontainerfwd.h>
#include <qobject.h>
#include <qqmlintegration.h>
#include <qqmllist.h>
#include <qtclasshelpermacros.h>
#include <qtmetamacros.h>
#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<void(PlatformMenuQMenu*)> 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<PlatformMenuEntry*> childEntries;
};
} // namespace qs::menu::platform

View file

@ -1,19 +0,0 @@
#pragma once
#include <qmenu.h>
#include <qpoint.h>
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

View file

@ -5,34 +5,37 @@
#include "generation.hpp"
static QVector<QsEnginePlugin*> plugins; // NOLINT
static QVector<QuickshellPlugin*> plugins; // NOLINT
void QsEnginePlugin::registerPlugin(QsEnginePlugin& plugin) { plugins.push_back(&plugin); }
void QuickshellPlugin::registerPlugin(QuickshellPlugin& plugin) { plugins.push_back(&plugin); }
void QsEnginePlugin::initPlugins() {
plugins.removeIf([](QsEnginePlugin* plugin) { return !plugin->applies(); });
void QuickshellPlugin::initPlugins() {
plugins.erase(
std::remove_if(
plugins.begin(),
plugins.end(),
[](QuickshellPlugin* plugin) { return !plugin->applies(); }
),
plugins.end()
);
std::ranges::sort(plugins, [](QsEnginePlugin* a, QsEnginePlugin* b) {
return b->dependencies().contains(a->name());
});
for (QsEnginePlugin* plugin: plugins) {
for (QuickshellPlugin* plugin: plugins) {
plugin->init();
}
for (QsEnginePlugin* plugin: plugins) {
for (QuickshellPlugin* plugin: plugins) {
plugin->registerTypes();
}
}
void QsEnginePlugin::runConstructGeneration(EngineGeneration& generation) {
for (QsEnginePlugin* plugin: plugins) {
void QuickshellPlugin::runConstructGeneration(EngineGeneration& generation) {
for (QuickshellPlugin* plugin: plugins) {
plugin->constructGeneration(generation);
}
}
void QsEnginePlugin::runOnReload() {
for (QsEnginePlugin* plugin: plugins) {
void QuickshellPlugin::runOnReload() {
for (QuickshellPlugin* plugin: plugins) {
plugin->onReload();
}
}

View file

@ -2,28 +2,25 @@
#include <qcontainerfwd.h>
#include <qfunctionpointer.h>
#include <qlist.h>
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<QString> dependencies() { return {}; }
virtual bool applies() { return true; }
virtual void init() {}
virtual void registerTypes() {}
virtual void constructGeneration(EngineGeneration& /*unused*/) {} // NOLINT
virtual void onReload() {}
static void registerPlugin(QsEnginePlugin& plugin);
static void registerPlugin(QuickshellPlugin& plugin);
static void initPlugins();
static void runConstructGeneration(EngineGeneration& generation);
static void runOnReload();
@ -33,6 +30,6 @@ public:
#define QS_REGISTER_PLUGIN(clazz) \
[[gnu::constructor]] void qsInitPlugin() { \
static clazz plugin; \
QsEnginePlugin::registerPlugin(plugin); \
QuickshellPlugin::registerPlugin(plugin); \
}
// NOLINTEND

View file

@ -1,319 +0,0 @@
#include "popupanchor.hpp"
#include <algorithm>
#include <qcontainerfwd.h>
#include <qlogging.h>
#include <qobject.h>
#include <qsize.h>
#include <qtmetamacros.h>
#include <qwindow.h>
#include "../window/proxywindow.hpp"
#include "../window/windowinterface.hpp"
#include "types.hpp"
bool PopupAnchorState::operator==(const PopupAnchorState& other) const {
return this->rect == other.rect && this->edges == other.edges && this->gravity == other.gravity
&& this->adjustment == other.adjustment && this->anchorpoint == other.anchorpoint
&& this->size == other.size;
}
bool PopupAnchor::isDirty() const {
return !this->lastState.has_value() || this->state != this->lastState.value();
}
void PopupAnchor::markClean() { this->lastState = this->state; }
void PopupAnchor::markDirty() { this->lastState.reset(); }
QObject* PopupAnchor::window() const { return this->mWindow; }
ProxyWindowBase* PopupAnchor::proxyWindow() const { return this->mProxyWindow; }
QWindow* PopupAnchor::backingWindow() const {
return this->mProxyWindow ? this->mProxyWindow->backingWindow() : nullptr;
}
void PopupAnchor::setWindow(QObject* window) {
if (window == this->mWindow) return;
if (this->mWindow) {
QObject::disconnect(this->mWindow, nullptr, this, nullptr);
QObject::disconnect(this->mProxyWindow, nullptr, this, nullptr);
}
if (window) {
if (auto* proxy = qobject_cast<ProxyWindowBase*>(window)) {
this->mProxyWindow = proxy;
} else if (auto* interface = qobject_cast<WindowInterface*>(window)) {
this->mProxyWindow = interface->proxyWindow();
} else {
qWarning() << "Tried to set popup anchor window to" << window
<< "which is not a quickshell window.";
goto setnull;
}
this->mWindow = window;
QObject::connect(this->mWindow, &QObject::destroyed, this, &PopupAnchor::onWindowDestroyed);
QObject::connect(
this->mProxyWindow,
&ProxyWindowBase::backerVisibilityChanged,
this,
&PopupAnchor::backingWindowVisibilityChanged
);
emit this->windowChanged();
emit this->backingWindowVisibilityChanged();
return;
}
setnull:
if (this->mWindow) {
this->mWindow = nullptr;
this->mProxyWindow = nullptr;
emit this->windowChanged();
emit this->backingWindowVisibilityChanged();
}
}
void PopupAnchor::onWindowDestroyed() {
this->mWindow = nullptr;
this->mProxyWindow = nullptr;
emit this->windowChanged();
emit this->backingWindowVisibilityChanged();
}
Box PopupAnchor::rect() const { return this->state.rect; }
void PopupAnchor::setRect(Box rect) {
if (rect == this->state.rect) return;
if (rect.w <= 0) rect.w = 1;
if (rect.h <= 0) rect.h = 1;
this->state.rect = rect;
emit this->rectChanged();
}
Edges::Flags PopupAnchor::edges() const { return this->state.edges; }
void PopupAnchor::setEdges(Edges::Flags edges) {
if (edges == this->state.edges) return;
if (Edges::isOpposing(edges)) {
qWarning() << "Cannot set opposing edges for anchor edges. Tried to set" << edges;
return;
}
this->state.edges = edges;
emit this->edgesChanged();
}
Edges::Flags PopupAnchor::gravity() const { return this->state.gravity; }
void PopupAnchor::setGravity(Edges::Flags gravity) {
if (gravity == this->state.gravity) return;
if (Edges::isOpposing(gravity)) {
qWarning() << "Cannot set opposing edges for anchor gravity. Tried to set" << gravity;
return;
}
this->state.gravity = gravity;
emit this->gravityChanged();
}
PopupAdjustment::Flags PopupAnchor::adjustment() const { return this->state.adjustment; }
void PopupAnchor::setAdjustment(PopupAdjustment::Flags adjustment) {
if (adjustment == this->state.adjustment) return;
this->state.adjustment = adjustment;
emit this->adjustmentChanged();
}
void PopupAnchor::updatePlacement(const QPoint& anchorpoint, const QSize& size) {
this->state.anchorpoint = anchorpoint;
this->state.size = size;
}
static PopupPositioner* POSITIONER = nullptr; // NOLINT
void PopupPositioner::reposition(PopupAnchor* anchor, QWindow* window, bool onlyIfDirty) {
auto* parentWindow = window->transientParent();
if (!parentWindow) {
qFatal() << "Cannot reposition popup that does not have a transient parent.";
}
auto parentGeometry = parentWindow->geometry();
auto windowGeometry = window->geometry();
emit anchor->anchoring();
anchor->updatePlacement(parentGeometry.topLeft(), windowGeometry.size());
if (onlyIfDirty && !anchor->isDirty()) return;
anchor->markClean();
auto adjustment = anchor->adjustment();
auto screenGeometry = parentWindow->screen()->geometry();
auto anchorRectGeometry = anchor->rect().qrect().translated(parentGeometry.topLeft());
auto anchorEdges = anchor->edges();
auto anchorGravity = anchor->gravity();
auto width = windowGeometry.width();
auto height = windowGeometry.height();
auto anchorX = anchorEdges.testFlag(Edges::Left) ? anchorRectGeometry.left()
: anchorEdges.testFlag(Edges::Right) ? anchorRectGeometry.right()
: anchorRectGeometry.center().x();
auto anchorY = anchorEdges.testFlag(Edges::Top) ? anchorRectGeometry.top()
: anchorEdges.testFlag(Edges::Bottom) ? anchorRectGeometry.bottom()
: anchorRectGeometry.center().y();
auto calcEffectiveX = [&](Edges::Flags anchorGravity, int anchorX) {
auto ex = anchorGravity.testFlag(Edges::Left) ? anchorX - windowGeometry.width()
: anchorGravity.testFlag(Edges::Right) ? anchorX - 1
: anchorX - windowGeometry.width() / 2;
return ex + 1;
};
auto calcEffectiveY = [&](Edges::Flags anchorGravity, int anchorY) {
auto ey = anchorGravity.testFlag(Edges::Top) ? anchorY - windowGeometry.height()
: anchorGravity.testFlag(Edges::Bottom) ? anchorY - 1
: anchorY - windowGeometry.height() / 2;
return ey + 1;
};
auto calcRemainingWidth = [&](int effectiveX) {
auto width = windowGeometry.width();
if (effectiveX < screenGeometry.left()) {
auto diff = screenGeometry.left() - effectiveX;
effectiveX = screenGeometry.left();
width -= diff;
}
auto effectiveX2 = effectiveX + width;
if (effectiveX2 > screenGeometry.right()) {
width -= effectiveX2 - screenGeometry.right() - 1;
}
return QPair<int, int>(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<int, int>(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;
}

View file

@ -1,174 +0,0 @@
#pragma once
#include <optional>
#include <qflags.h>
#include <qnamespace.h>
#include <qobject.h>
#include <qpoint.h>
#include <qqmlintegration.h>
#include <qsize.h>
#include <qtclasshelpermacros.h>
#include <qtmetamacros.h>
#include <qtypes.h>
#include <qwindow.h>
#include "../window/proxywindow.hpp"
#include "doc.hpp"
#include "types.hpp"
///! Adjustment strategy for popups that do not fit on screen.
/// Adjustment strategy for popups. See @@PopupAnchor.adjustment.
///
/// Adjustment flags can be combined with the `|` operator.
///
/// `Flip` will be applied first, then `Slide`, then `Resize`.
namespace PopupAdjustment { // NOLINT
Q_NAMESPACE;
QML_ELEMENT;
enum Enum : quint8 {
None = 0,
/// If the X axis is constrained, the popup will slide along the X axis until it fits onscreen.
SlideX = 1,
/// If the Y axis is constrained, the popup will slide along the Y axis until it fits onscreen.
SlideY = 2,
/// Alias for `SlideX | SlideY`.
Slide = SlideX | SlideY,
/// If the X axis is constrained, the popup will invert its horizontal gravity if any.
FlipX = 4,
/// If the Y axis is constrained, the popup will invert its vertical gravity if any.
FlipY = 8,
/// Alias for `FlipX | FlipY`.
Flip = FlipX | FlipY,
/// If the X axis is constrained, the width of the popup will be reduced to fit on screen.
ResizeX = 16,
/// If the Y axis is constrained, the height of the popup will be reduced to fit on screen.
ResizeY = 32,
/// Alias for `ResizeX | ResizeY`
Resize = ResizeX | ResizeY,
/// Alias for `Flip | Slide | Resize`.
All = Slide | Flip | Resize,
};
Q_ENUM_NS(Enum);
Q_DECLARE_FLAGS(Flags, Enum);
} // namespace PopupAdjustment
Q_DECLARE_OPERATORS_FOR_FLAGS(PopupAdjustment::Flags);
struct PopupAnchorState {
bool operator==(const PopupAnchorState& other) const;
Box rect = {0, 0, 1, 1};
Edges::Flags edges = Edges::Top | Edges::Left;
Edges::Flags gravity = Edges::Bottom | Edges::Right;
PopupAdjustment::Flags adjustment = PopupAdjustment::Slide;
QPoint anchorpoint;
QSize size;
};
///! Anchorpoint or positioner for popup windows.
class PopupAnchor: public QObject {
Q_OBJECT;
// clang-format off
/// The window to anchor / attach the popup to.
Q_PROPERTY(QObject* window READ window WRITE setWindow NOTIFY windowChanged);
/// The anchorpoints the popup will attach to. Which anchors will be used is
/// determined by the @@edges, @@gravity, and @@adjustment.
///
/// If you leave @@edges, @@gravity and @@adjustment at their default values,
/// setting more than `x` and `y` does not matter. The anchor rect cannot
/// be smaller than 1x1 pixels.
///
/// > [!INFO] To position a popup relative to an item inside a window,
/// > you can use [coordinate mapping functions] (note the warning below).
///
/// > [!WARNING] Using [coordinate mapping functions] in a binding to
/// > this property will position the anchor incorrectly.
/// > If you want to use them, do so in @@anchoring(s), or use
/// > @@TransformWatcher if you need real-time updates to mapped coordinates.
///
/// [coordinate mapping functions]: https://doc.qt.io/qt-6/qml-qtquick-item.html#mapFromItem-method
Q_PROPERTY(Box rect READ rect WRITE setRect NOTIFY rectChanged);
/// The point on the anchor rectangle the popup should anchor to.
/// Opposing edges suchs as `Edges.Left | Edges.Right` are not allowed.
///
/// Defaults to `Edges.Top | Edges.Left`.
Q_PROPERTY(Edges::Flags edges READ edges WRITE setEdges NOTIFY edgesChanged);
/// The direction the popup should expand towards, relative to the anchorpoint.
/// Opposing edges suchs as `Edges.Left | Edges.Right` are not allowed.
///
/// Defaults to `Edges.Bottom | Edges.Right`.
Q_PROPERTY(Edges::Flags gravity READ gravity WRITE setGravity NOTIFY gravityChanged);
/// The strategy used to adjust the popup's position if it would otherwise not fit on screen,
/// based on the anchor @@rect, preferred @@edges, and @@gravity.
///
/// See the documentation for @@PopupAdjustment for details.
Q_PROPERTY(PopupAdjustment::Flags adjustment READ adjustment WRITE setAdjustment NOTIFY adjustmentChanged);
// clang-format on
QML_ELEMENT;
QML_UNCREATABLE("");
public:
explicit PopupAnchor(QObject* parent): QObject(parent) {}
[[nodiscard]] bool isDirty() const;
void markClean();
void markDirty();
[[nodiscard]] QObject* window() const;
[[nodiscard]] ProxyWindowBase* proxyWindow() const;
[[nodiscard]] QWindow* backingWindow() const;
void setWindow(QObject* window);
[[nodiscard]] Box rect() const;
void setRect(Box rect);
[[nodiscard]] Edges::Flags edges() const;
void setEdges(Edges::Flags edges);
[[nodiscard]] Edges::Flags gravity() const;
void setGravity(Edges::Flags gravity);
[[nodiscard]] PopupAdjustment::Flags adjustment() const;
void setAdjustment(PopupAdjustment::Flags adjustment);
void updatePlacement(const QPoint& anchorpoint, const QSize& size);
signals:
/// Emitted when this anchor is about to be used. Mostly useful for modifying
/// the anchor @@rect using [coordinate mapping functions], which are not reactive.
///
/// [coordinate mapping functions]: https://doc.qt.io/qt-6/qml-qtquick-item.html#mapFromItem-method
void anchoring();
void windowChanged();
QSDOC_HIDE void backingWindowVisibilityChanged();
void rectChanged();
void edgesChanged();
void gravityChanged();
void adjustmentChanged();
private slots:
void onWindowDestroyed();
private:
QObject* mWindow = nullptr;
ProxyWindowBase* mProxyWindow = nullptr;
PopupAnchorState state;
std::optional<PopupAnchorState> 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);
};

161
src/core/popupwindow.cpp Normal file
View file

@ -0,0 +1,161 @@
#include "popupwindow.hpp"
#include <qlogging.h>
#include <qnamespace.h>
#include <qobject.h>
#include <qquickwindow.h>
#include <qtmetamacros.h>
#include <qtypes.h>
#include "proxywindow.hpp"
#include "qmlscreen.hpp"
#include "windowinterface.hpp"
ProxyPopupWindow::ProxyPopupWindow(QObject* parent): ProxyWindowBase(parent) {
this->mVisible = false;
}
void ProxyPopupWindow::completeWindow() {
this->ProxyWindowBase::completeWindow();
this->window->setFlag(Qt::ToolTip);
this->updateTransientParent();
}
void ProxyPopupWindow::postCompleteWindow() { this->ProxyWindowBase::setVisible(this->mVisible); }
bool ProxyPopupWindow::deleteOnInvisible() const {
// Currently crashes in normal mode, do not have the time to debug it now.
return true;
}
qint32 ProxyPopupWindow::x() const {
// QTBUG-121550
auto basepos = this->mParentProxyWindow == nullptr ? 0 : this->mParentProxyWindow->x();
return basepos + this->mRelativeX;
}
void ProxyPopupWindow::setParentWindow(QObject* parent) {
if (parent == this->mParentWindow) return;
if (this->mParentWindow != nullptr) {
QObject::disconnect(this->mParentWindow, nullptr, this, nullptr);
QObject::disconnect(this->mParentProxyWindow, nullptr, this, nullptr);
}
if (parent == nullptr) {
this->mParentWindow = nullptr;
this->mParentProxyWindow = nullptr;
} else {
if (auto* proxy = qobject_cast<ProxyWindowBase*>(parent)) {
this->mParentProxyWindow = proxy;
} else if (auto* interface = qobject_cast<WindowInterface*>(parent)) {
this->mParentProxyWindow = interface->proxyWindow();
} else {
qWarning() << "Tried to set popup parent window to something that is not a quickshell window:"
<< parent;
this->mParentWindow = nullptr;
this->mParentProxyWindow = nullptr;
this->updateTransientParent();
return;
}
this->mParentWindow = parent;
// clang-format off
QObject::connect(this->mParentWindow, &QObject::destroyed, this, &ProxyPopupWindow::onParentDestroyed);
QObject::connect(this->mParentProxyWindow, &ProxyWindowBase::xChanged, this, &ProxyPopupWindow::updateX);
QObject::connect(this->mParentProxyWindow, &ProxyWindowBase::yChanged, this, &ProxyPopupWindow::updateY);
QObject::connect(this->mParentProxyWindow, &ProxyWindowBase::backerVisibilityChanged, this, &ProxyPopupWindow::onParentUpdated);
// clang-format on
}
this->updateTransientParent();
}
QObject* ProxyPopupWindow::parentWindow() const { return this->mParentWindow; }
void ProxyPopupWindow::updateTransientParent() {
this->updateX();
this->updateY();
if (this->window != nullptr) {
this->window->setTransientParent(
this->mParentProxyWindow == nullptr ? nullptr : this->mParentProxyWindow->backingWindow()
);
}
this->updateVisible();
}
void ProxyPopupWindow::onParentUpdated() { this->updateTransientParent(); }
void ProxyPopupWindow::onParentDestroyed() {
this->mParentWindow = nullptr;
this->mParentProxyWindow = nullptr;
this->updateVisible();
emit this->parentWindowChanged();
}
void ProxyPopupWindow::setScreen(QuickshellScreenInfo* /*unused*/) {
qWarning() << "Cannot set screen of popup window, as that is controlled by the parent window";
}
void ProxyPopupWindow::setVisible(bool visible) {
if (visible == this->wantsVisible) return;
this->wantsVisible = visible;
this->updateVisible();
}
void ProxyPopupWindow::updateVisible() {
auto target = this->wantsVisible && this->mParentWindow != nullptr
&& this->mParentProxyWindow->isVisibleDirect();
if (target && this->window != nullptr && !this->window->isVisible()) {
this->updateX(); // QTBUG-121550
}
this->ProxyWindowBase::setVisible(target);
}
void ProxyPopupWindow::setRelativeX(qint32 x) {
if (x == this->mRelativeX) return;
this->mRelativeX = x;
this->updateX();
}
qint32 ProxyPopupWindow::relativeX() const { return this->mRelativeX; }
void ProxyPopupWindow::setRelativeY(qint32 y) {
if (y == this->mRelativeY) return;
this->mRelativeY = y;
this->updateY();
}
qint32 ProxyPopupWindow::relativeY() const { return this->mRelativeY; }
void ProxyPopupWindow::updateX() {
if (this->mParentWindow == nullptr || this->window == nullptr) return;
auto target = this->x() - 1; // QTBUG-121550
auto reshow = this->isVisibleDirect() && (this->window->x() != target && this->x() != target);
if (reshow) this->setVisibleDirect(false);
if (this->window != nullptr) this->window->setX(target);
if (reshow && this->wantsVisible) this->setVisibleDirect(true);
}
void ProxyPopupWindow::updateY() {
if (this->mParentWindow == nullptr || this->window == nullptr) return;
auto target = this->mParentProxyWindow->y() + this->relativeY();
auto reshow = this->isVisibleDirect() && this->window->y() != target;
if (reshow) {
this->setVisibleDirect(false);
this->updateX(); // QTBUG-121550
}
if (this->window != nullptr) this->window->setY(target);
if (reshow && this->wantsVisible) this->setVisibleDirect(true);
}

View file

@ -6,10 +6,9 @@
#include <qtmetamacros.h>
#include <qtypes.h>
#include "../core/doc.hpp"
#include "../core/popupanchor.hpp"
#include "../core/qmlscreen.hpp"
#include "doc.hpp"
#include "proxywindow.hpp"
#include "qmlscreen.hpp"
#include "windowinterface.hpp"
///! Popup window.
@ -30,9 +29,9 @@
/// }
///
/// PopupWindow {
/// anchor.window: toplevel
/// anchor.rect.x: parentWindow.width / 2 - width / 2
/// anchor.rect.y: parentWindow.height
/// parentWindow: toplevel
/// relativeX: parentWindow.width / 2 - width / 2
/// relativeY: parentWindow.height
/// width: 500
/// height: 500
/// visible: true
@ -43,37 +42,15 @@ class ProxyPopupWindow: public ProxyWindowBase {
QSDOC_BASECLASS(WindowInterface);
Q_OBJECT;
// clang-format off
/// > [!ERROR] Deprecated in favor of `anchor.window`.
///
/// The parent window of this popup.
///
/// Changing this property reparents the popup.
Q_PROPERTY(QObject* parentWindow READ parentWindow WRITE setParentWindow NOTIFY parentWindowChanged);
/// > [!ERROR] Deprecated in favor of `anchor.rect.x`.
///
/// The X position of the popup relative to the parent window.
Q_PROPERTY(qint32 relativeX READ relativeX WRITE setRelativeX NOTIFY relativeXChanged);
/// > [!ERROR] Deprecated in favor of `anchor.rect.y`.
///
/// The Y position of the popup relative to the parent window.
Q_PROPERTY(qint32 relativeY READ relativeY WRITE setRelativeY NOTIFY relativeYChanged);
/// The popup's anchor / positioner relative to another window. The popup will not be
/// shown until it has a valid anchor relative to a window and @@visible is true.
///
/// You can set properties of the anchor like so:
/// ```qml
/// PopupWindow {
/// anchor.window: parentwindow
/// // or
/// anchor {
/// window: parentwindow
/// }
/// }
/// ```
Q_PROPERTY(PopupAnchor* anchor READ anchor CONSTANT);
/// If the window is shown or hidden. Defaults to false.
///
/// The popup will not be shown until @@anchor is valid, regardless of this property.
QSDOC_PROPERTY_OVERRIDE(bool visible READ isVisible WRITE setVisible NOTIFY visibleChanged);
/// The screen that the window currently occupies.
///
@ -87,10 +64,13 @@ public:
void completeWindow() override;
void postCompleteWindow() override;
[[nodiscard]] bool deleteOnInvisible() const override;
void setScreen(QuickshellScreenInfo* screen) override;
void setVisible(bool visible) override;
[[nodiscard]] qint32 x() const override;
[[nodiscard]] QObject* parentWindow() const;
void setParentWindow(QObject* parent);
@ -100,23 +80,25 @@ public:
[[nodiscard]] qint32 relativeY() const;
void setRelativeY(qint32 y);
[[nodiscard]] PopupAnchor* anchor();
signals:
void parentWindowChanged();
void relativeXChanged();
void relativeYChanged();
private slots:
void onVisibleChanged();
void onParentUpdated();
void reposition();
void onParentDestroyed();
void updateX();
void updateY();
private:
QQuickWindow* parentBackingWindow();
void updateTransientParent();
void updateVisible();
PopupAnchor mAnchor {this};
QObject* mParentWindow = nullptr;
ProxyWindowBase* mParentProxyWindow = nullptr;
qint32 mRelativeX = 0;
qint32 mRelativeY = 0;
bool wantsVisible = false;
};

View file

@ -1,45 +1,35 @@
#include "proxywindow.hpp"
#include <private/qquickwindow_p.h>
#include <qcoreevent.h>
#include <qevent.h>
#include <qguiapplication.h>
#include <qnamespace.h>
#include <qobject.h>
#include <qqmlcontext.h>
#include <qqmlengine.h>
#include <qqmlinfo.h>
#include <qqmllist.h>
#include <qquickitem.h>
#include <qquickwindow.h>
#include <qregion.h>
#include <qsurfaceformat.h>
#include <qtenvironmentvariables.h>
#include <qtmetamacros.h>
#include <qtypes.h>
#include <qvariant.h>
#include <qwindow.h>
#include "../core/generation.hpp"
#include "../core/qmlglobal.hpp"
#include "../core/qmlscreen.hpp"
#include "../core/region.hpp"
#include "../core/reload.hpp"
#include "../debug/lint.hpp"
#include "generation.hpp"
#include "qmlglobal.hpp"
#include "qmlscreen.hpp"
#include "region.hpp"
#include "reload.hpp"
#include "windowinterface.hpp"
ProxyWindowBase::ProxyWindowBase(QObject* parent)
: Reloadable(parent)
, mContentItem(new ProxyWindowContentItem()) {
, mContentItem(new QQuickItem()) {
QQmlEngine::setObjectOwnership(this->mContentItem, QQmlEngine::CppOwnership);
this->mContentItem->setParent(this);
// clang-format off
QObject::connect(this->mContentItem, &ProxyWindowContentItem::polished, this, &ProxyWindowBase::onPolished);
QObject::connect(this, &ProxyWindowBase::widthChanged, this, &ProxyWindowBase::onWidthChanged);
QObject::connect(this, &ProxyWindowBase::heightChanged, this, &ProxyWindowBase::onHeightChanged);
QObject::connect(this, &ProxyWindowBase::maskChanged, this, &ProxyWindowBase::onMaskChanged);
QObject::connect(this, &ProxyWindowBase::widthChanged, this, &ProxyWindowBase::onMaskChanged);
QObject::connect(this, &ProxyWindowBase::heightChanged, this, &ProxyWindowBase::onMaskChanged);
@ -51,12 +41,12 @@ ProxyWindowBase::ProxyWindowBase(QObject* parent)
// clang-format on
}
ProxyWindowBase::~ProxyWindowBase() { this->deleteWindow(true); }
ProxyWindowBase::~ProxyWindowBase() { this->deleteWindow(); }
void ProxyWindowBase::onReload(QObject* oldInstance) {
this->window = this->retrieveWindow(oldInstance);
auto wasVisible = this->window != nullptr && this->window->isVisible();
this->ensureQWindow();
if (this->window == nullptr) this->window = new QQuickWindow();
// The qml engine will leave the WindowInterface as owner of everything
// nested in an item, so we have to make sure the interface's children
@ -81,73 +71,25 @@ void ProxyWindowBase::onReload(QObject* oldInstance) {
emit this->windowConnected();
this->postCompleteWindow();
if (wasVisible && this->isVisibleDirect()) {
emit this->backerVisibilityChanged();
this->runLints();
}
if (wasVisible && this->isVisibleDirect()) emit this->backerVisibilityChanged();
}
void ProxyWindowBase::postCompleteWindow() { this->setVisible(this->mVisible); }
ProxiedWindow* ProxyWindowBase::createQQuickWindow() { return new ProxiedWindow(this); }
void ProxyWindowBase::ensureQWindow() {
auto format = QSurfaceFormat::defaultFormat();
{
// match QtQuick's default format, including env var controls
static const auto useDepth = qEnvironmentVariableIsEmpty("QSG_NO_DEPTH_BUFFER");
static const auto useStencil = qEnvironmentVariableIsEmpty("QSG_NO_STENCIL_BUFFER");
static const auto enableDebug = qEnvironmentVariableIsSet("QSG_OPENGL_DEBUG");
static const auto disableVSync = qEnvironmentVariableIsSet("QSG_NO_VSYNC");
if (useDepth && format.depthBufferSize() == -1) format.setDepthBufferSize(24);
else if (!useDepth) format.setDepthBufferSize(0);
if (useStencil && format.stencilBufferSize() == -1) format.setStencilBufferSize(8);
else if (!useStencil) format.setStencilBufferSize(0);
auto opaque = this->qsSurfaceFormat.opaqueModified ? this->qsSurfaceFormat.opaque
: this->mColor.alpha() >= 255;
if (opaque) format.setAlphaBufferSize(0);
else format.setAlphaBufferSize(8);
if (enableDebug) format.setOption(QSurfaceFormat::DebugContext);
if (disableVSync) format.setSwapInterval(0);
format.setSwapBehavior(QSurfaceFormat::DoubleBuffer);
format.setRedBufferSize(8);
format.setGreenBufferSize(8);
format.setBlueBufferSize(8);
}
this->mSurfaceFormat = format;
auto useOldWindow = this->window != nullptr;
if (useOldWindow) {
if (this->window->requestedFormat() != format) {
useOldWindow = false;
}
}
if (useOldWindow) return;
delete this->window;
this->window = this->createQQuickWindow();
this->window->setFormat(format);
}
QQuickWindow* ProxyWindowBase::createQQuickWindow() { return new QQuickWindow(); }
void ProxyWindowBase::createWindow() {
this->ensureQWindow();
if (this->window != nullptr) return;
this->window = this->createQQuickWindow();
this->connectWindow();
this->completeWindow();
emit this->windowConnected();
}
void ProxyWindowBase::deleteWindow(bool keepItemOwnership) {
void ProxyWindowBase::deleteWindow() {
if (this->window != nullptr) emit this->windowDestroyed();
if (auto* window = this->disownWindow(keepItemOwnership)) {
if (auto* window = this->disownWindow()) {
if (auto* generation = EngineGeneration::findObjectGeneration(this)) {
generation->deregisterIncubationController(window->incubationController());
}
@ -156,21 +98,19 @@ void ProxyWindowBase::deleteWindow(bool keepItemOwnership) {
}
}
ProxiedWindow* ProxyWindowBase::disownWindow(bool keepItemOwnership) {
QQuickWindow* ProxyWindowBase::disownWindow() {
if (this->window == nullptr) return nullptr;
QObject::disconnect(this->window, nullptr, this, nullptr);
if (!keepItemOwnership) {
this->mContentItem->setParentItem(nullptr);
}
this->mContentItem->setParentItem(nullptr);
auto* window = this->window;
this->window = nullptr;
return window;
}
ProxiedWindow* ProxyWindowBase::retrieveWindow(QObject* oldInstance) {
QQuickWindow* ProxyWindowBase::retrieveWindow(QObject* oldInstance) {
auto* old = qobject_cast<ProxyWindowBase*>(oldInstance);
return old == nullptr ? nullptr : old->disownWindow();
}
@ -182,8 +122,6 @@ void ProxyWindowBase::connectWindow() {
generation->registerIncubationController(this->window->incubationController());
}
this->window->setProxy(this);
// clang-format off
QObject::connect(this->window, &QWindow::visibilityChanged, this, &ProxyWindowBase::visibleChanged);
QObject::connect(this->window, &QWindow::xChanged, this, &ProxyWindowBase::xChanged);
@ -192,8 +130,6 @@ void ProxyWindowBase::connectWindow() {
QObject::connect(this->window, &QWindow::heightChanged, this, &ProxyWindowBase::heightChanged);
QObject::connect(this->window, &QWindow::screenChanged, this, &ProxyWindowBase::screenChanged);
QObject::connect(this->window, &QQuickWindow::colorChanged, this, &ProxyWindowBase::colorChanged);
QObject::connect(this->window, &ProxiedWindow::exposed, this, &ProxyWindowBase::runLints);
QObject::connect(this->window, &ProxiedWindow::devicePixelRatioChanged, this, &ProxyWindowBase::devicePixelRatioChanged);
// clang-format on
}
@ -208,12 +144,9 @@ void ProxyWindowBase::completeWindow() {
this->setColor(this->mColor);
this->updateMask();
// notify initial / post-connection geometry
// notify initial x and y positions
emit this->xChanged();
emit this->yChanged();
emit this->widthChanged();
emit this->heightChanged();
emit this->devicePixelRatioChanged();
this->mContentItem->setParentItem(this->window->contentItem());
this->mContentItem->setWidth(this->width());
@ -223,7 +156,16 @@ void ProxyWindowBase::completeWindow() {
emit this->screenChanged();
}
bool ProxyWindowBase::deleteOnInvisible() const { return false; }
bool ProxyWindowBase::deleteOnInvisible() const {
#ifdef NVIDIA_COMPAT
// Nvidia drivers and Qt do not play nice when hiding and showing a window
// so for nvidia compatibility we can never reuse windows if they have been
// hidden.
return true;
#else
return false;
#endif
}
QQuickWindow* ProxyWindowBase::backingWindow() const { return this->window; }
QQuickItem* ProxyWindowBase::contentItem() const { return this->mContentItem; }
@ -249,7 +191,6 @@ void ProxyWindowBase::setVisibleDirect(bool visible) {
if (visible) {
this->createWindow();
this->polishItems();
this->window->setVisible(true);
emit this->backerVisibilityChanged();
} else {
@ -260,35 +201,11 @@ void ProxyWindowBase::setVisibleDirect(bool visible) {
}
}
} else if (this->window != nullptr) {
if (visible) this->polishItems();
this->window->setVisible(visible);
emit this->backerVisibilityChanged();
}
}
void ProxyWindowBase::schedulePolish() {
if (this->isVisibleDirect()) {
this->mContentItem->polish();
}
}
void ProxyWindowBase::polishItems() {
// Due to QTBUG-126704, layouts in invisible windows don't update their dimensions.
// Usually this isn't an issue, but it is when the size of a window is based on the size
// of its content, and that content is in a layout.
//
// This hack manually polishes the item tree right before showing the window so it will
// always be created with the correct size.
QQuickWindowPrivate::get(this->window)->polishItems();
}
void ProxyWindowBase::runLints() {
if (!this->ranLints) {
qs::debug::lintItemTree(this->mContentItem);
this->ranLints = true;
}
}
qint32 ProxyWindowBase::x() const {
if (this->window == nullptr) return 0;
else return this->window->x();
@ -324,61 +241,52 @@ void ProxyWindowBase::setHeight(qint32 height) {
}
void ProxyWindowBase::setScreen(QuickshellScreenInfo* screen) {
auto* qscreen = screen == nullptr ? nullptr : screen->screen;
auto newMScreen = this->mScreen != qscreen;
if (this->mScreen && newMScreen) {
if (this->mScreen != nullptr) {
QObject::disconnect(this->mScreen, nullptr, this, nullptr);
}
if (this->qscreen() != qscreen) {
this->mScreen = qscreen;
if (this->window == nullptr) {
emit this->screenChanged();
} else if (qscreen) {
auto reshow = this->isVisibleDirect();
if (reshow) this->setVisibleDirect(false);
if (this->window != nullptr) this->window->setScreen(qscreen);
if (reshow) this->setVisibleDirect(true);
}
auto* qscreen = screen == nullptr ? nullptr : screen->screen;
if (qscreen == this->mScreen) return;
if (qscreen != nullptr) {
QObject::connect(qscreen, &QObject::destroyed, this, &ProxyWindowBase::onScreenDestroyed);
}
if (qscreen && newMScreen) {
QObject::connect(this->mScreen, &QObject::destroyed, this, &ProxyWindowBase::onScreenDestroyed);
if (this->window == nullptr) {
this->mScreen = qscreen;
emit this->screenChanged();
} else {
auto reshow = this->isVisibleDirect();
if (reshow) this->setVisibleDirect(false);
if (this->window != nullptr) this->window->setScreen(qscreen);
if (reshow) this->setVisibleDirect(true);
}
}
void ProxyWindowBase::onScreenDestroyed() { this->mScreen = nullptr; }
QScreen* ProxyWindowBase::qscreen() const {
if (this->window) return this->window->screen();
if (this->mScreen) return this->mScreen;
return QGuiApplication::primaryScreen();
}
QuickshellScreenInfo* ProxyWindowBase::screen() const {
return QuickshellTracked::instance()->screenInfo(this->qscreen());
}
QColor ProxyWindowBase::color() const { return this->mColor; }
void ProxyWindowBase::setColor(QColor color) {
this->mColor = color;
QScreen* qscreen = nullptr;
if (this->window == nullptr) {
if (color != this->mColor) emit this->colorChanged();
if (this->mScreen != nullptr) qscreen = this->mScreen;
} else {
auto premultiplied = QColor::fromRgbF(
color.redF() * color.alphaF(),
color.greenF() * color.alphaF(),
color.blueF() * color.alphaF(),
color.alphaF()
);
this->window->setColor(premultiplied);
// setColor also modifies the alpha buffer size of the surface format
this->window->setFormat(this->mSurfaceFormat);
qscreen = this->window->screen();
}
return QuickshellTracked::instance()->screenInfo(qscreen);
}
QColor ProxyWindowBase::color() const {
if (this->window == nullptr) return this->mColor;
else return this->window->color();
}
void ProxyWindowBase::setColor(QColor color) {
if (this->window == nullptr) {
this->mColor = color;
emit this->colorChanged();
} else this->window->setColor(color);
}
PendingRegion* ProxyWindowBase::mask() const { return this->mMask; }
@ -393,44 +301,37 @@ void ProxyWindowBase::setMask(PendingRegion* mask) {
this->mMask = mask;
if (mask != nullptr) {
mask->setParent(this);
QObject::connect(mask, &QObject::destroyed, this, &ProxyWindowBase::onMaskDestroyed);
QObject::connect(mask, &PendingRegion::changed, this, &ProxyWindowBase::onMaskChanged);
QObject::connect(mask, &PendingRegion::changed, this, &ProxyWindowBase::maskChanged);
}
this->onMaskChanged();
emit this->maskChanged();
}
void ProxyWindowBase::setSurfaceFormat(QsSurfaceFormat format) {
if (format == this->qsSurfaceFormat) return;
if (this->window != nullptr) {
qmlWarning(this) << "Cannot set window surface format.";
return;
}
this->qsSurfaceFormat = format;
emit this->surfaceFormatChanged();
}
qreal ProxyWindowBase::devicePixelRatio() const {
if (this->window != nullptr) return this->window->devicePixelRatio();
if (this->mScreen != nullptr) return this->mScreen->devicePixelRatio();
return 1.0;
}
void ProxyWindowBase::onMaskChanged() {
if (this->window != nullptr) this->updateMask();
}
void ProxyWindowBase::onMaskDestroyed() {
this->mMask = nullptr;
this->onMaskChanged();
emit this->maskChanged();
}
void ProxyWindowBase::updateMask() {
this->pendingPolish.inputMask = true;
this->schedulePolish();
QRegion mask;
if (this->mMask != nullptr) {
// if left as the default, dont combine it with the whole window area, leave it as is.
if (this->mMask->mIntersection == Intersection::Combine) {
mask = this->mMask->build();
} else {
auto windowRegion = QRegion(QRect(0, 0, this->width(), this->height()));
mask = this->mMask->applyTo(windowRegion);
}
}
this->window->setFlag(Qt::WindowTransparentForInput, this->mMask != nullptr && mask.isEmpty());
this->window->setMask(mask);
}
QQmlListProperty<QObject> ProxyWindowBase::data() {
@ -439,57 +340,3 @@ QQmlListProperty<QObject> ProxyWindowBase::data() {
void ProxyWindowBase::onWidthChanged() { this->mContentItem->setWidth(this->width()); }
void ProxyWindowBase::onHeightChanged() { this->mContentItem->setHeight(this->height()); }
ProxyWindowAttached::ProxyWindowAttached(QQuickItem* parent): QsWindowAttached(parent) {
this->updateWindow();
}
QObject* ProxyWindowAttached::window() const { return this->mWindow; }
QQuickItem* ProxyWindowAttached::contentItem() const { return this->mWindow->contentItem(); }
void ProxyWindowAttached::updateWindow() {
auto* window = static_cast<QQuickItem*>(this->parent())->window(); // NOLINT
if (auto* proxy = qobject_cast<ProxiedWindow*>(window)) {
this->setWindow(proxy->proxy());
} else {
this->setWindow(nullptr);
}
}
void ProxyWindowAttached::setWindow(ProxyWindowBase* window) {
if (window == this->mWindow) return;
this->mWindow = window;
emit this->windowChanged();
}
bool ProxiedWindow::event(QEvent* event) {
if (event->type() == QEvent::DevicePixelRatioChange) {
emit this->devicePixelRatioChanged();
}
return this->QQuickWindow::event(event);
}
void ProxiedWindow::exposeEvent(QExposeEvent* event) {
this->QQuickWindow::exposeEvent(event);
emit this->exposed();
}
void ProxyWindowContentItem::updatePolish() { emit this->polished(); }
void ProxyWindowBase::onPolished() {
if (this->pendingPolish.inputMask) {
QRegion mask;
if (this->mMask != nullptr) {
mask = this->mMask->applyTo(QRect(0, 0, this->width(), this->height()));
}
this->window->setFlag(Qt::WindowTransparentForInput, this->mMask != nullptr && mask.isEmpty());
this->window->setMask(mask);
this->pendingPolish.inputMask = false;
}
emit this->polished();
}

View file

@ -9,19 +9,15 @@
#include <qqmlparserstatus.h>
#include <qquickitem.h>
#include <qquickwindow.h>
#include <qsurfaceformat.h>
#include <qtmetamacros.h>
#include <qtypes.h>
#include <qwindow.h>
#include "../core/qmlscreen.hpp"
#include "../core/region.hpp"
#include "../core/reload.hpp"
#include "qmlglobal.hpp"
#include "qmlscreen.hpp"
#include "region.hpp"
#include "reload.hpp"
#include "windowinterface.hpp"
class ProxiedWindow;
class ProxyWindowContentItem;
// Proxy to an actual window exposing a limited property set with the ability to
// transfer it to a new window.
@ -31,7 +27,6 @@ class ProxyWindowContentItem;
/// [FloatingWindow]: ../floatingwindow
class ProxyWindowBase: public Reloadable {
Q_OBJECT;
// clang-format off
/// The QtQuick window backing this window.
///
/// > [!WARNING] Do not expect values set via this property to work correctly.
@ -44,15 +39,12 @@ class ProxyWindowBase: public Reloadable {
Q_PROPERTY(bool visible READ isVisible WRITE setVisible NOTIFY visibleChanged);
Q_PROPERTY(qint32 width READ width WRITE setWidth NOTIFY widthChanged);
Q_PROPERTY(qint32 height READ height WRITE setHeight NOTIFY heightChanged);
Q_PROPERTY(qreal devicePixelRatio READ devicePixelRatio NOTIFY devicePixelRatioChanged);
Q_PROPERTY(QuickshellScreenInfo* screen READ screen WRITE setScreen NOTIFY screenChanged);
Q_PROPERTY(QColor color READ color WRITE setColor NOTIFY colorChanged);
Q_PROPERTY(PendingRegion* mask READ mask WRITE setMask NOTIFY maskChanged);
Q_PROPERTY(QObject* windowTransform READ windowTransform NOTIFY windowTransformChanged);
Q_PROPERTY(bool backingWindowVisible READ isVisibleDirect NOTIFY backerVisibilityChanged);
Q_PROPERTY(QsSurfaceFormat surfaceFormat READ surfaceFormat WRITE setSurfaceFormat NOTIFY surfaceFormatChanged);
Q_PROPERTY(QQmlListProperty<QObject> data READ data);
// clang-format on
Q_CLASSINFO("DefaultProperty", "data");
public:
@ -65,15 +57,14 @@ public:
void operator=(ProxyWindowBase&&) = delete;
void onReload(QObject* oldInstance) override;
void ensureQWindow();
void createWindow();
void deleteWindow(bool keepItemOwnership = false);
void deleteWindow();
// Disown the backing window and delete all its children.
virtual ProxiedWindow* disownWindow(bool keepItemOwnership = false);
virtual QQuickWindow* disownWindow();
virtual ProxiedWindow* retrieveWindow(QObject* oldInstance);
virtual ProxiedWindow* createQQuickWindow();
virtual QQuickWindow* retrieveWindow(QObject* oldInstance);
virtual QQuickWindow* createQQuickWindow();
virtual void connectWindow();
virtual void completeWindow();
virtual void postCompleteWindow();
@ -87,8 +78,6 @@ public:
virtual void setVisible(bool visible);
virtual void setVisibleDirect(bool visible);
void schedulePolish();
[[nodiscard]] virtual qint32 x() const;
[[nodiscard]] virtual qint32 y() const;
@ -98,10 +87,7 @@ public:
[[nodiscard]] virtual qint32 height() const;
virtual void setHeight(qint32 height);
[[nodiscard]] qreal devicePixelRatio() const;
[[nodiscard]] QScreen* qscreen() const;
[[nodiscard]] QuickshellScreenInfo* screen() const;
[[nodiscard]] virtual QuickshellScreenInfo* screen() const;
virtual void setScreen(QuickshellScreenInfo* screen);
[[nodiscard]] QColor color() const;
@ -110,9 +96,6 @@ public:
[[nodiscard]] PendingRegion* mask() const;
virtual void setMask(PendingRegion* mask);
[[nodiscard]] QsSurfaceFormat surfaceFormat() const { return this->qsSurfaceFormat; }
void setSurfaceFormat(QsSurfaceFormat format);
[[nodiscard]] QObject* windowTransform() const { return nullptr; } // NOLINT
[[nodiscard]] QQmlListProperty<QObject> data();
@ -126,13 +109,10 @@ signals:
void yChanged();
void widthChanged();
void heightChanged();
void devicePixelRatioChanged();
void windowTransformChanged();
void screenChanged();
void colorChanged();
void maskChanged();
void surfaceFormatChanged();
void polished();
protected slots:
virtual void onWidthChanged();
@ -140,8 +120,6 @@ protected slots:
void onMaskChanged();
void onMaskDestroyed();
void onScreenDestroyed();
void onPolished();
void runLints();
protected:
bool mVisible = true;
@ -150,69 +128,10 @@ protected:
QScreen* mScreen = nullptr;
QColor mColor = Qt::white;
PendingRegion* mMask = nullptr;
ProxiedWindow* window = nullptr;
ProxyWindowContentItem* mContentItem = nullptr;
QQuickWindow* window = nullptr;
QQuickItem* mContentItem = nullptr;
bool reloadComplete = false;
bool ranLints = false;
QsSurfaceFormat qsSurfaceFormat;
QSurfaceFormat mSurfaceFormat;
struct {
bool inputMask : 1 = false;
} pendingPolish;
private:
void polishItems();
void updateMask();
};
class ProxyWindowAttached: public QsWindowAttached {
Q_OBJECT;
public:
explicit ProxyWindowAttached(QQuickItem* parent);
[[nodiscard]] QObject* window() const override;
[[nodiscard]] QQuickItem* contentItem() const override;
protected:
void updateWindow() override;
private:
ProxyWindowBase* mWindow = nullptr;
void setWindow(ProxyWindowBase* window);
};
class ProxiedWindow: public QQuickWindow {
Q_OBJECT;
public:
explicit ProxiedWindow(ProxyWindowBase* proxy, QWindow* parent = nullptr)
: QQuickWindow(parent)
, mProxy(proxy) {}
[[nodiscard]] ProxyWindowBase* proxy() const { return this->mProxy; }
void setProxy(ProxyWindowBase* proxy) { this->mProxy = proxy; }
signals:
void exposed();
void devicePixelRatioChanged();
protected:
bool event(QEvent* event) override;
void exposeEvent(QExposeEvent* event) override;
private:
ProxyWindowBase* mProxy;
};
class ProxyWindowContentItem: public QQuickItem {
Q_OBJECT;
signals:
void polished();
protected:
void updatePolish() override;
};

View file

@ -5,7 +5,6 @@
#include <qcoreapplication.h>
#include <qdir.h>
#include <qguiapplication.h>
#include <qicon.h>
#include <qjsengine.h>
#include <qlogging.h>
#include <qobject.h>
@ -20,7 +19,6 @@
#include <unistd.h>
#include "generation.hpp"
#include "iconimageprovider.hpp"
#include "qmlscreen.hpp"
#include "rootwrapper.hpp"
@ -167,12 +165,6 @@ void QuickshellGlobal::reload(bool hard) {
root->reloadGraph(hard);
}
QString QuickshellGlobal::shellRoot() const {
auto* generation = EngineGeneration::findObjectGeneration(this);
// already canonical
return generation->rootPath.path();
}
QString QuickshellGlobal::workingDirectory() const { // NOLINT
return QuickshellSettings::instance()->workingDirectory();
}
@ -195,27 +187,3 @@ QVariant QuickshellGlobal::env(const QString& variable) { // NOLINT
return qEnvironmentVariable(vstr.data());
}
QString QuickshellGlobal::iconPath(const QString& icon) {
return IconImageProvider::requestString(icon);
}
QString QuickshellGlobal::iconPath(const QString& icon, bool check) {
if (check && QIcon::fromTheme(icon).isNull()) return "";
return IconImageProvider::requestString(icon);
}
QString QuickshellGlobal::iconPath(const QString& icon, const QString& fallback) {
return IconImageProvider::requestString(icon, "", fallback);
}
QuickshellGlobal* QuickshellGlobal::create(QQmlEngine* engine, QJSEngine* /*unused*/) {
auto* qsg = new QuickshellGlobal();
auto* generation = EngineGeneration::findEngineGeneration(engine);
if (generation->qsgInstance == nullptr) {
generation->qsgInstance = qsg;
}
return qsg;
}

View file

@ -98,11 +98,6 @@ class QuickshellGlobal: public QObject {
/// This creates an instance of your window once on every screen.
/// As screens are added or removed your window will be created or destroyed on those screens.
Q_PROPERTY(QQmlListProperty<QuickshellScreenInfo> screens READ screens NOTIFY screensChanged);
/// The full path to the root directory of your shell.
///
/// The root directory is the folder containing the entrypoint to your shell, often referred
/// to as `shell.qml`.
Q_PROPERTY(QString shellRoot READ shellRoot CONSTANT);
/// Quickshell's working directory. Defaults to whereever quickshell was launched from.
Q_PROPERTY(QString workingDirectory READ workingDirectory WRITE setWorkingDirectory NOTIFY workingDirectoryChanged);
/// If true then the configuration will be reloaded whenever any files change.
@ -115,62 +110,40 @@ class QuickshellGlobal: public QObject {
public:
[[nodiscard]] qint32 processId() const;
QuickshellGlobal(QObject* parent = nullptr);
QQmlListProperty<QuickshellScreenInfo> screens();
/// Reload the shell.
/// Reload the shell from the [ShellRoot].
///
/// `hard` - perform a hard reload. If this is false, Quickshell will attempt to reuse windows
/// that already exist. If true windows will be recreated.
///
/// See @@Reloadable for more information on what can be reloaded and how.
/// See [Reloadable] for more information on what can be reloaded and how.
///
/// [Reloadable]: ../reloadable
Q_INVOKABLE void reload(bool hard);
/// Returns the string value of an environment variable or null if it is not set.
Q_INVOKABLE QVariant env(const QString& variable);
/// Returns a string usable for a @@QtQuick.Image.source for a given system icon.
///
/// > [!INFO] By default, icons are loaded from the theme selected by the qt platform theme,
/// > which means they should match with all other qt applications on your system.
/// >
/// > If you want to use a different icon theme, you can put `//@ pragma IconTheme <name>`
/// > at the top of your root config file or set the `QS_ICON_THEME` variable to the name
/// > of your icon theme.
Q_INVOKABLE static QString iconPath(const QString& icon);
/// Setting the `check` parameter of `iconPath` to true will return an empty string
/// if the icon does not exist, instead of an image showing a missing texture.
Q_INVOKABLE static QString iconPath(const QString& icon, bool check);
/// Setting the `fallback` parameter of `iconPath` will attempt to load the fallback
/// icon if the requested one could not be loaded.
Q_INVOKABLE static QString iconPath(const QString& icon, const QString& fallback);
[[nodiscard]] QString shellRoot() const;
[[nodiscard]] QString workingDirectory() const;
void setWorkingDirectory(QString workingDirectory);
[[nodiscard]] bool watchFiles() const;
void setWatchFiles(bool watchFiles);
static QuickshellGlobal* create(QQmlEngine* engine, QJSEngine* /*unused*/);
signals:
/// Sent when the last window is closed.
///
/// To make the application exit when the last window is closed run `Qt.quit()`.
void lastWindowClosed();
/// The reload sequence has completed successfully.
void reloadCompleted();
/// The reload sequence has failed.
void reloadFailed(QString errorString);
void screensChanged();
void workingDirectoryChanged();
void watchFilesChanged();
private:
QuickshellGlobal(QObject* parent = nullptr);
static qsizetype screensCount(QQmlListProperty<QuickshellScreenInfo>* prop);
static QuickshellScreenInfo* screenAt(QQmlListProperty<QuickshellScreenInfo>* prop, qsizetype i);
};

View file

@ -42,24 +42,6 @@ QString QuickshellScreenInfo::name() const {
return this->screen->name();
}
QString QuickshellScreenInfo::model() const {
if (this->screen == nullptr) {
this->warnDangling();
return "{ NULL SCREEN }";
}
return this->screen->model();
}
QString QuickshellScreenInfo::serialNumber() const {
if (this->screen == nullptr) {
this->warnDangling();
return "{ NULL SCREEN }";
}
return this->screen->serialNumber();
}
qint32 QuickshellScreenInfo::x() const {
if (this->screen == nullptr) {
this->warnDangling();

View file

@ -12,14 +12,17 @@
// unfortunately QQuickScreenInfo is private.
/// Monitor object useful for setting the monitor for a @@QsWindow
/// Monitor object useful for setting the monitor for a [ShellWindow]
/// or querying information about the monitor.
///
/// > [!WARNING] If the monitor is disconnected than any stored copies of its ShellMonitor will
/// > be marked as dangling and all properties will return default values.
/// > Reconnecting the monitor will not reconnect it to the ShellMonitor object.
///
/// Due to some technical limitations, it was not possible to reuse the native qml @@QtQuick.Screen type.
/// Due to some technical limitations, it was not possible to reuse the native qml [Screen] type.
///
/// [ShellWindow]: ../shellwindow
/// [Screen]: https://doc.qt.io/qt-6/qml-qtquick-screen.html
class QuickshellScreenInfo: public QObject {
Q_OBJECT;
QML_NAMED_ELEMENT(ShellScreen);
@ -29,10 +32,6 @@ class QuickshellScreenInfo: public QObject {
///
/// Usually something like `DP-1`, `HDMI-1`, `eDP-1`.
Q_PROPERTY(QString name READ name CONSTANT);
/// The model of the screen as seen by the operating system.
Q_PROPERTY(QString model READ model CONSTANT);
/// The serial number of the screen as seen by the operating system.
Q_PROPERTY(QString serialNumber READ serialNumber CONSTANT);
Q_PROPERTY(qint32 x READ x NOTIFY geometryChanged);
Q_PROPERTY(qint32 y READ y NOTIFY geometryChanged);
Q_PROPERTY(qint32 width READ width NOTIFY geometryChanged);
@ -44,7 +43,7 @@ class QuickshellScreenInfo: public QObject {
/// The ratio between physical pixels and device-independent (scaled) pixels.
Q_PROPERTY(qreal devicePixelRatio READ devicePixelRatio NOTIFY physicalPixelDensityChanged);
Q_PROPERTY(Qt::ScreenOrientation orientation READ orientation NOTIFY orientationChanged);
Q_PROPERTY(Qt::ScreenOrientation primaryOrientation READ primaryOrientation NOTIFY primaryOrientationChanged);
Q_PROPERTY(Qt::ScreenOrientation primatyOrientation READ primaryOrientation NOTIFY primaryOrientationChanged);
// clang-format on
public:
@ -53,8 +52,6 @@ public:
bool operator==(QuickshellScreenInfo& other) const;
[[nodiscard]] QString name() const;
[[nodiscard]] QString model() const;
[[nodiscard]] QString serialNumber() const;
[[nodiscard]] qint32 x() const;
[[nodiscard]] qint32 y() const;
[[nodiscard]] qint32 width() const;

View file

@ -16,22 +16,7 @@
Q_LOGGING_CATEGORY(logQsIntercept, "quickshell.interceptor", QtWarningMsg);
QUrl QsUrlInterceptor::intercept(
const QUrl& originalUrl,
QQmlAbstractUrlInterceptor::DataType type
) {
auto url = originalUrl;
if (url.scheme() == "root") {
url.setScheme("qsintercept");
auto path = url.path();
if (path.startsWith('/')) path = path.sliced(1);
url.setPath(this->configRoot.filePath(path));
qCDebug(logQsIntercept) << "Rewrote root intercept" << originalUrl << "to" << url;
}
QUrl QsUrlInterceptor::intercept(const QUrl& url, QQmlAbstractUrlInterceptor::DataType type) {
// Some types such as Image take into account where they are loading from, and force
// asynchronous loading over a network. qsintercept is considered to be over a network.
if (type == QQmlAbstractUrlInterceptor::DataType::UrlString && url.scheme() == "qsintercept") {

View file

@ -1,6 +1,5 @@
#pragma once
#include <qdir.h>
#include <qhash.h>
#include <qloggingcategory.h>
#include <qnetworkaccessmanager.h>
@ -14,12 +13,7 @@ Q_DECLARE_LOGGING_CATEGORY(logQsIntercept);
class QsUrlInterceptor: public QQmlAbstractUrlInterceptor {
public:
explicit QsUrlInterceptor(const QDir& configRoot): configRoot(configRoot) {}
QUrl intercept(const QUrl& originalUrl, QQmlAbstractUrlInterceptor::DataType type) override;
private:
QDir configRoot;
QUrl intercept(const QUrl& url, QQmlAbstractUrlInterceptor::DataType type) override;
};
class QsInterceptDataReply: public QNetworkReply {

View file

@ -1,110 +0,0 @@
#include "qsmenu.hpp"
#include <qcontainerfwd.h>
#include <qobject.h>
#include <qqmllist.h>
#include <qtmetamacros.h>
#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>* QsMenuEntry::children() {
return ObjectModel<QsMenuEntry>::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<QsMenuEntry>* QsMenuOpener::children() {
if (this->mMenu && this->mMenu->menu()) {
return this->mMenu->menu()->children();
} else {
return ObjectModel<QsMenuEntry>::emptyInstance();
}
}
} // namespace qs::menu

View file

@ -1,163 +0,0 @@
#pragma once
#include <qcontainerfwd.h>
#include <qnamespace.h>
#include <qobject.h>
#include <qqmlintegration.h>
#include <qqmllist.h>
#include <qtmetamacros.h>
#include <qtypes.h>
#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<QsMenuEntry>* 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<qs::menu::QsMenuEntry>*);
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<QsMenuEntry>* children();
signals:
void menuChanged();
void childrenChanged();
private slots:
void onMenuDestroyed();
private:
QsMenuHandle* mMenu = nullptr;
};
} // namespace qs::menu

View file

@ -1,125 +0,0 @@
#include "qsmenuanchor.hpp"
#include <qapplication.h>
#include <qcoreapplication.h>
#include <qlogging.h>
#include <qobject.h>
#include <qtmetamacros.h>
#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<QApplication*>(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

View file

@ -1,86 +0,0 @@
#pragma once
#include <qqmlintegration.h>
#include <qtclasshelpermacros.h>
#include <qtmetamacros.h>
#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

View file

@ -8,7 +8,6 @@
#include <qregion.h>
#include <qtmetamacros.h>
#include <qtypes.h>
#include <qvectornd.h>
PendingRegion::PendingRegion(QObject* parent): QObject(parent) {
QObject::connect(this, &PendingRegion::shapeChanged, this, &PendingRegion::changed);
@ -106,19 +105,8 @@ QRegion PendingRegion::applyTo(QRegion& region) const {
return region;
}
QRegion PendingRegion::applyTo(const QRect& rect) const {
// if left as the default, dont combine it with the whole rect area, leave it as is.
if (this->mIntersection == Intersection::Combine) {
return this->build();
} else {
auto baseRegion = QRegion(rect);
return this->applyTo(baseRegion);
}
}
void PendingRegion::regionsAppend(QQmlListProperty<PendingRegion>* prop, PendingRegion* region) {
auto* self = static_cast<PendingRegion*>(prop->object); // NOLINT
if (!region) return;
QObject::connect(region, &QObject::destroyed, self, &PendingRegion::onChildDestroyed);
QObject::connect(region, &PendingRegion::changed, self, &PendingRegion::childrenChanged);

View file

@ -9,13 +9,12 @@
#include <qtmetamacros.h>
#include <qtypes.h>
///! Shape of a Region.
/// See @@Region.shape.
/// Shape of a Region.
namespace RegionShape { // NOLINT
Q_NAMESPACE;
QML_ELEMENT;
enum Enum : quint8 {
enum Enum {
Rect = 0,
Ellipse = 1,
};
@ -24,12 +23,11 @@ Q_ENUM_NS(Enum);
} // namespace RegionShape
///! Intersection strategy for Regions.
/// See @@Region.intersection.
namespace Intersection { // NOLINT
Q_NAMESPACE;
QML_ELEMENT;
enum Enum : quint8 {
enum Enum {
/// Combine this region, leaving a union of this and the other region. (opposite of `Subtract`)
Combine = 0,
/// Subtract this region, cutting this region out of the other. (opposite of `Combine`)
@ -46,7 +44,6 @@ Q_ENUM_NS(Enum);
} // namespace Intersection
///! A composable region used as a mask.
/// See @@QsWindow.mask.
class PendingRegion: public QObject {
Q_OBJECT;
/// Defaults to `Rect`.
@ -55,16 +52,16 @@ class PendingRegion: public QObject {
Q_PROPERTY(Intersection::Enum intersection MEMBER mIntersection NOTIFY intersectionChanged);
/// The item that determines the geometry of the region.
/// `item` overrides @@x, @@y, @@width and @@height.
/// `item` overrides `x`, `y`, `width` and `height`.
Q_PROPERTY(QQuickItem* item MEMBER mItem WRITE setItem NOTIFY itemChanged);
/// Defaults to 0. Does nothing if @@item is set.
/// Defaults to 0. Does nothing if `item` is set.
Q_PROPERTY(qint32 x MEMBER mX NOTIFY xChanged);
/// Defaults to 0. Does nothing if @@item is set.
/// Defaults to 0. Does nothing if `item` is set.
Q_PROPERTY(qint32 y MEMBER mY NOTIFY yChanged);
/// Defaults to 0. Does nothing if @@item is set.
/// Defaults to 0. Does nothing if `item` is set.
Q_PROPERTY(qint32 width MEMBER mWidth NOTIFY widthChanged);
/// Defaults to 0. Does nothing if @@item is set.
/// Defaults to 0. Does nothing if `item` is set.
Q_PROPERTY(qint32 height MEMBER mHeight NOTIFY heightChanged);
/// Regions to apply on top of this region.
@ -96,7 +93,6 @@ public:
[[nodiscard]] bool empty() const;
[[nodiscard]] QRegion build() const;
[[nodiscard]] QRegion applyTo(QRegion& region) const;
[[nodiscard]] QRegion applyTo(const QRect& rect) const;
RegionShape::Enum mShape = RegionShape::Rect;
Intersection::Enum mIntersection = Intersection::Combine;
@ -110,11 +106,6 @@ signals:
void widthChanged();
void heightChanged();
void childrenChanged();
/// Triggered when the region's geometry changes.
///
/// In some cases the region does not update automatically.
/// In those cases you can emit this signal manually.
void changed();
private slots:

View file

@ -3,7 +3,6 @@
#include <qcontainerfwd.h>
#include <qobject.h>
#include <qqmllist.h>
#include <qtimer.h>
#include "generation.hpp"
@ -13,19 +12,8 @@ void Reloadable::componentComplete() {
if (this->engineGeneration != nullptr) {
// When called this way there is no chance a reload will have old data,
// but this will at least help prevent weird behaviors due to never getting a reload.
if (this->engineGeneration->reloadComplete) {
// Delayed due to Component.onCompleted running after QQmlParserStatus::componentComplete.
QTimer::singleShot(0, this, &Reloadable::onReloadFinished);
// This only matters for preventing the above timer from UAFing the generation,
// so it isn't connected anywhere else.
QObject::connect(
this->engineGeneration,
&QObject::destroyed,
this,
&Reloadable::onGenerationDestroyed
);
} else {
if (this->engineGeneration->reloadComplete) this->reload();
else {
QObject::connect(
this->engineGeneration,
&EngineGeneration::reloadFinished,
@ -52,7 +40,6 @@ void Reloadable::reload(QObject* oldInstance) {
}
void Reloadable::onReloadFinished() { this->reload(nullptr); }
void Reloadable::onGenerationDestroyed() { this->engineGeneration = nullptr; }
void ReloadPropagator::onReload(QObject* oldInstance) {
auto* old = qobject_cast<ReloadPropagator*>(oldInstance);
@ -99,7 +86,7 @@ void Reloadable::reloadRecursive(QObject* newObj, QObject* oldRoot) {
// pass handling to the child's onReload, which should call back into reloadRecursive,
// with its oldInstance becoming the new oldRoot.
reloadable->reload(oldInstance);
reloadable->onReload(oldInstance);
} else if (newObj != nullptr) {
Reloadable::reloadChildrenRecursive(newObj, oldRoot);
}

View file

@ -11,7 +11,10 @@ class EngineGeneration;
///! The base class of all types that can be reloaded.
/// Reloadables will attempt to take specific state from previous config revisions if possible.
/// Some examples are @@ProxyWindowBase and @@PersistentProperties
/// Some examples are [ProxyWindowBase] and [PersistentProperties]
///
/// [ProxyWindowBase]: ../proxywindowbase
/// [PersistentProperties]: ../persistentproperties
class Reloadable
: public QObject
, public QQmlParserStatus {
@ -25,7 +28,7 @@ class Reloadable
/// this object in the current revision, and facilitate smoother reloading.
///
/// Note that identifiers are scoped, and will try to do the right thing in context.
/// For example if you have a @@Variants wrapping an object with an identified element inside,
/// For example if you have a `Variants` wrapping an object with an identified element inside,
/// a scope is created at the variant level.
///
/// ```qml
@ -71,7 +74,6 @@ public:
private slots:
void onReloadFinished();
void onGenerationDestroyed();
protected:
// Called unconditionally in the reload phase, with nullptr if no source could be determined.
@ -84,9 +86,10 @@ private:
};
///! Scope that propagates reloads to child items in order.
/// Convenience type equivalent to setting @@Reloadable.reloadableId for all children.
/// Convenience type equivalent to setting `reloadableId` on properties in a
/// QtObject instance.
///
/// Note that this does not work for visible @@QtQuick.Item$s (all widgets).
/// Note that this does not work for visible `Item`s (all widgets).
///
/// ```qml
/// ShellRoot {

View file

@ -1,163 +0,0 @@
#include "retainable.hpp"
#include <qlogging.h>
#include <qobject.h>
#include <qtmetamacros.h>
#include <qvariant.h>
RetainableHook* RetainableHook::getHook(QObject* object, bool create) {
auto v = object->property("__qs_retainable");
if (v.canConvert<RetainableHook*>()) {
return v.value<RetainableHook*>();
} else if (create) {
auto* retainable = dynamic_cast<Retainable*>(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<QObject*>(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<QObject*>(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(); }

View file

@ -1,162 +0,0 @@
#pragma once
#include <qobject.h>
#include <qqmlintegration.h>
#include <qtclasshelpermacros.h>
#include <qtmetamacros.h>
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;
};

View file

@ -1,169 +0,0 @@
#pragma once
#include <new>
#include <tuple>
#include <utility>
#include <qcontainerfwd.h>
#include <qhashfunctions.h>
#include <qtclasshelpermacros.h>
#include <qtypes.h>
// NOLINTBEGIN(cppcoreguidelines-pro-bounds-pointer-arithmetic)
// capacity 0 buffer cannot be inserted into, only replaced with =
// this is NOT exception safe for constructors
template <typename T>
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 <typename... Args>
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>(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<RingBuffer<T>*>(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<T*>(::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 <typename T>
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<HashBuffer<T>*>(this)->indexOf(value, slot); // NOLINT
}
template <typename... Args>
T& emplace(Args&&... args) {
auto& entry = this->ring.emplace(
std::piecewise_construct,
std::forward_as_tuple(0),
std::forward_as_tuple(std::forward<Args>(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<std::pair<size_t, T>> ring;
};
// NOLINTEND(cppcoreguidelines-pro-bounds-pointer-arithmetic)

View file

@ -8,19 +8,16 @@
#include <qobject.h>
#include <qqmlcomponent.h>
#include <qqmlengine.h>
#include <qquickitem.h>
#include <qtmetamacros.h>
#include <qurl.h>
#include "../window/floatingwindow.hpp"
#include "generation.hpp"
#include "qmlglobal.hpp"
#include "scan.hpp"
#include "shell.hpp"
RootWrapper::RootWrapper(QString rootPath, QString shellId)
RootWrapper::RootWrapper(QString rootPath)
: QObject(nullptr)
, rootPath(std::move(rootPath))
, shellId(std::move(shellId))
, originalWorkingDirectory(QDir::current().absolutePath()) {
// clang-format off
QObject::connect(QuickshellSettings::instance(), &QuickshellSettings::watchFilesChanged, this, &RootWrapper::onWatchFilesChanged);
@ -37,16 +34,18 @@ RootWrapper::RootWrapper(QString rootPath, QString shellId)
RootWrapper::~RootWrapper() {
// event loop may no longer be running so deleteLater is not an option
if (this->generation != nullptr) {
this->generation->shutdown();
delete this->generation->root;
this->generation->root = nullptr;
}
delete this->generation;
}
void RootWrapper::reloadGraph(bool hard) {
auto rootPath = QFileInfo(this->rootPath).dir();
auto scanner = QmlScanner(rootPath);
auto scanner = QmlScanner();
scanner.scanQmlFile(this->rootPath);
auto* generation = new EngineGeneration(rootPath, std::move(scanner));
auto* generation = new EngineGeneration(std::move(scanner));
generation->wrapper = this;
// todo: move into EngineGeneration
@ -61,49 +60,33 @@ void RootWrapper::reloadGraph(bool hard) {
url.setScheme("qsintercept");
auto component = QQmlComponent(generation->engine, url);
auto* newRoot = component.beginCreate(generation->engine->rootContext());
if (newRoot == nullptr) {
const QString error = "failed to create root component\n" + component.errorString();
qWarning().noquote() << error;
generation->destroy();
if (this->generation != nullptr && this->generation->qsgInstance != nullptr) {
emit this->generation->qsgInstance->reloadFailed(error);
}
auto* obj = component.beginCreate(generation->engine->rootContext());
if (obj == nullptr) {
qWarning() << component.errorString().toStdString().c_str();
qWarning() << "failed to create root component";
delete generation;
return;
}
if (auto* item = qobject_cast<QQuickItem*>(newRoot)) {
auto* window = new FloatingWindowInterface();
item->setParent(window);
item->setParentItem(window->contentItem());
window->setWidth(static_cast<int>(item->width()));
window->setHeight(static_cast<int>(item->height()));
newRoot = window;
auto* newRoot = qobject_cast<ShellRoot*>(obj);
if (newRoot == nullptr) {
qWarning() << "root component was not a Quickshell.ShellRoot";
delete obj;
delete generation;
return;
}
generation->root = newRoot;
component.completeCreate();
if (this->generation) {
QObject::disconnect(this->generation, nullptr, this, nullptr);
}
auto isReload = this->generation != nullptr;
generation->onReload(hard ? nullptr : this->generation);
if (hard && this->generation) {
this->generation->destroy();
}
if (hard) delete this->generation;
this->generation = generation;
qInfo() << "Configuration Loaded";
QObject::connect(this->generation, &QObject::destroyed, this, &RootWrapper::generationDestroyed);
QObject::connect(
this->generation,
&EngineGeneration::filesChanged,
@ -112,14 +95,8 @@ void RootWrapper::reloadGraph(bool hard) {
);
this->onWatchFilesChanged();
if (isReload && this->generation->qsgInstance != nullptr) {
emit this->generation->qsgInstance->reloadCompleted();
}
}
void RootWrapper::generationDestroyed() { this->generation = nullptr; }
void RootWrapper::onWatchFilesChanged() {
auto watchFiles = QuickshellSettings::instance()->watchFiles();
if (this->generation != nullptr) {

View file

@ -12,20 +12,18 @@ class RootWrapper: public QObject {
Q_OBJECT;
public:
explicit RootWrapper(QString rootPath, QString shellId);
explicit RootWrapper(QString rootPath);
~RootWrapper() override;
Q_DISABLE_COPY_MOVE(RootWrapper);
void reloadGraph(bool hard);
private slots:
void generationDestroyed();
void onWatchFilesChanged();
void onWatchedFilesChanged();
private:
QString rootPath;
QString shellId;
EngineGeneration* generation = nullptr;
QString originalWorkingDirectory;
};

View file

@ -103,15 +103,7 @@ bool QmlScanner::scanQmlFile(const QString& path) {
this->scanDir(currentdir.path());
for (auto& import: imports) {
QString ipath;
if (import.startsWith("root:")) {
auto path = import.sliced(5);
if (path.startsWith('/')) path = path.sliced(1);
ipath = this->rootPath.filePath(path);
} else {
ipath = currentdir.filePath(import);
}
auto ipath = currentdir.filePath(import);
auto cpath = QFileInfo(ipath).canonicalFilePath();
if (cpath.isEmpty()) {

View file

@ -1,7 +1,6 @@
#pragma once
#include <qcontainerfwd.h>
#include <qdir.h>
#include <qhash.h>
#include <qloggingcategory.h>
#include <qvector.h>
@ -11,8 +10,6 @@ Q_DECLARE_LOGGING_CATEGORY(logQmlScanner);
// expects canonical paths
class QmlScanner {
public:
QmlScanner(const QDir& rootPath): rootPath(rootPath) {}
void scanDir(const QString& path);
// returns if the file has a singleton
bool scanQmlFile(const QString& path);
@ -20,7 +17,4 @@ public:
QVector<QString> scannedDirs;
QVector<QString> scannedFiles;
QHash<QString, QString> qmldirIntercepts;
private:
QDir rootPath;
};

Some files were not shown because too many files have changed in this diff Show more