mirror of
https://git.outfoxxed.me/quickshell/quickshell.git
synced 2026-04-10 06:11:54 +10:00
Compare commits
No commits in common. "master" and "v0.1.0" have entirely different histories.
385 changed files with 3591 additions and 20711 deletions
|
|
@ -1,6 +1,6 @@
|
||||||
AlignArrayOfStructures: None
|
AlignArrayOfStructures: None
|
||||||
AlignAfterOpenBracket: BlockIndent
|
AlignAfterOpenBracket: BlockIndent
|
||||||
AllowShortBlocksOnASingleLine: Empty
|
AllowShortBlocksOnASingleLine: Always
|
||||||
AllowShortCaseLabelsOnASingleLine: true
|
AllowShortCaseLabelsOnASingleLine: true
|
||||||
AllowShortEnumsOnASingleLine: true
|
AllowShortEnumsOnASingleLine: true
|
||||||
AllowShortFunctionsOnASingleLine: All
|
AllowShortFunctionsOnASingleLine: All
|
||||||
|
|
|
||||||
|
|
@ -20,8 +20,6 @@ Checks: >
|
||||||
-cppcoreguidelines-avoid-do-while,
|
-cppcoreguidelines-avoid-do-while,
|
||||||
-cppcoreguidelines-pro-type-reinterpret-cast,
|
-cppcoreguidelines-pro-type-reinterpret-cast,
|
||||||
-cppcoreguidelines-pro-type-vararg,
|
-cppcoreguidelines-pro-type-vararg,
|
||||||
-cppcoreguidelines-pro-type-union-access,
|
|
||||||
-cppcoreguidelines-use-enum-class,
|
|
||||||
google-global-names-in-headers,
|
google-global-names-in-headers,
|
||||||
google-readability-casting,
|
google-readability-casting,
|
||||||
google-runtime-int,
|
google-runtime-int,
|
||||||
|
|
@ -65,8 +63,6 @@ CheckOptions:
|
||||||
readability-identifier-naming.ParameterCase: camelBack
|
readability-identifier-naming.ParameterCase: camelBack
|
||||||
readability-identifier-naming.VariableCase: camelBack
|
readability-identifier-naming.VariableCase: camelBack
|
||||||
|
|
||||||
misc-const-correctness.WarnPointersAsPointers: false
|
|
||||||
|
|
||||||
# does not appear to work
|
# does not appear to work
|
||||||
readability-operators-representation.BinaryOperators: '&&;&=;&;|;~;!;!=;||;|=;^;^='
|
readability-operators-representation.BinaryOperators: '&&;&=;&;|;~;!;!=;||;|=;^;^='
|
||||||
readability-operators-representation.OverloadedOperators: '&&;&=;&;|;~;!;!=;||;|=;^;^='
|
readability-operators-representation.OverloadedOperators: '&&;&=;&;|;~;!;!=;||;|=;^;^='
|
||||||
|
|
|
||||||
93
.github/ISSUE_TEMPLATE/crash.yml
vendored
93
.github/ISSUE_TEMPLATE/crash.yml
vendored
|
|
@ -1,17 +1,82 @@
|
||||||
name: Crash Report (v1)
|
name: Crash Report
|
||||||
description: Quickshell has crashed (old)
|
description: Quickshell has crashed
|
||||||
labels: ["unactionable"]
|
labels: ["bug", "crash"]
|
||||||
body:
|
body:
|
||||||
- type: markdown
|
- type: textarea
|
||||||
|
id: crashinfo
|
||||||
attributes:
|
attributes:
|
||||||
value: |
|
label: General crash information
|
||||||
Thank you for taking the time to click the report button.
|
description: |
|
||||||
At this point most of the worst issues in 0.2.1 and before have been fixed and we are
|
Paste the contents of the `info.txt` file in your crash folder here.
|
||||||
preparing for a new release. Please do not submit this report.
|
value: "<details> <summary>General information</summary>
|
||||||
- type: checkboxes
|
|
||||||
id: donotcheck
|
|
||||||
|
```
|
||||||
|
|
||||||
|
<Paste the contents of the file here inside of the triple backticks>
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
</details>"
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
id: userinfo
|
||||||
attributes:
|
attributes:
|
||||||
label: Read the text above. Do not submit the report.
|
label: What caused the crash
|
||||||
options:
|
description: |
|
||||||
- label: Yes I want this report to be deleted.
|
Any information likely to help debug the crash. What were you doing when the crash occurred,
|
||||||
required: true
|
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.
|
||||||
|
|
|
||||||
49
.github/ISSUE_TEMPLATE/crash2.yml
vendored
49
.github/ISSUE_TEMPLATE/crash2.yml
vendored
|
|
@ -1,49 +0,0 @@
|
||||||
name: Crash Report (v2)
|
|
||||||
description: Quickshell has crashed
|
|
||||||
labels: ["bug", "crash"]
|
|
||||||
body:
|
|
||||||
- type: textarea
|
|
||||||
id: userinfo
|
|
||||||
attributes:
|
|
||||||
label: What caused the crash
|
|
||||||
description: |
|
|
||||||
Any information likely to help debug the crash. What were you doing when the crash occurred,
|
|
||||||
what changes did you make, can you get it to happen again?
|
|
||||||
- type: upload
|
|
||||||
id: report
|
|
||||||
attributes:
|
|
||||||
label: Report file
|
|
||||||
description: Attach `report.txt` here.
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: upload
|
|
||||||
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 `qs log <path-to-log> -r '*=true'`.
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: textarea
|
|
||||||
id: config
|
|
||||||
attributes:
|
|
||||||
label: Configuration
|
|
||||||
description: |
|
|
||||||
Attach or link your configuration here, preferrably in full (not just one file).
|
|
||||||
Compress it into a zip, tar, etc.
|
|
||||||
|
|
||||||
This will help us reproduce the crash ourselves.
|
|
||||||
- type: textarea
|
|
||||||
id: bt
|
|
||||||
attributes:
|
|
||||||
label: Backtrace
|
|
||||||
description: |
|
|
||||||
GDB usually produces better stacktraces than quickshell can. Consider attaching a gdb backtrace
|
|
||||||
following the instructions below.
|
|
||||||
|
|
||||||
1. Run `coredumpctl debug <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.
|
|
||||||
19
.github/workflows/build.yml
vendored
19
.github/workflows/build.yml
vendored
|
|
@ -6,23 +6,17 @@ jobs:
|
||||||
name: Nix
|
name: Nix
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
qtver: [qt6.10.1, qt6.10.0, qt6.9.2, qt6.9.1, qt6.9.0, qt6.8.3, qt6.8.2, qt6.8.1, qt6.8.0, qt6.7.3, qt6.7.2, qt6.7.1, qt6.7.0, qt6.6.3, qt6.6.2, qt6.6.1, qt6.6.0]
|
qtver: [qt6.9.0, qt6.8.3, qt6.8.2, qt6.8.1, qt6.8.0, qt6.7.3, qt6.7.2, qt6.7.1, qt6.7.0, qt6.6.3, qt6.6.2, qt6.6.1, qt6.6.0]
|
||||||
compiler: [clang, gcc]
|
compiler: [clang, gcc]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
id-token: write
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
# Use cachix action over detsys for testing with act.
|
# Use cachix action over detsys for testing with act.
|
||||||
# - uses: cachix/install-nix-action@v27
|
# - uses: cachix/install-nix-action@v27
|
||||||
- uses: DeterminateSystems/nix-installer-action@main
|
- uses: DeterminateSystems/nix-installer-action@main
|
||||||
- uses: DeterminateSystems/magic-nix-cache-action@main
|
|
||||||
with:
|
|
||||||
use-flakehub: false
|
|
||||||
|
|
||||||
- name: Download Dependencies
|
- name: Download Dependencies
|
||||||
run: nix-build --no-out-link --expr '((import ./ci/matrix.nix) { qtver = "${{ matrix.qtver }}"; compiler = "${{ matrix.compiler }}"; }).unwrapped.inputDerivation'
|
run: nix-build --no-out-link --expr '((import ./ci/matrix.nix) { qtver = "${{ matrix.qtver }}"; compiler = "${{ matrix.compiler }}"; }).inputDerivation'
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: nix-build --no-out-link --expr '(import ./ci/matrix.nix) { qtver = "${{ matrix.qtver }}"; compiler = "${{ matrix.compiler }}"; }'
|
run: nix-build --no-out-link --expr '(import ./ci/matrix.nix) { qtver = "${{ matrix.qtver }}"; compiler = "${{ matrix.compiler }}"; }'
|
||||||
|
|
@ -50,16 +44,13 @@ jobs:
|
||||||
wayland-protocols \
|
wayland-protocols \
|
||||||
wayland \
|
wayland \
|
||||||
libdrm \
|
libdrm \
|
||||||
vulkan-headers \
|
|
||||||
libxcb \
|
libxcb \
|
||||||
libpipewire \
|
libpipewire \
|
||||||
cli11 \
|
cli11 \
|
||||||
polkit \
|
jemalloc
|
||||||
jemalloc \
|
|
||||||
libunwind \
|
|
||||||
git # for cpptrace clone
|
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
|
# breakpad is annoying to build in ci due to makepkg not running as root
|
||||||
run: |
|
run: |
|
||||||
cmake -GNinja -B build -DVENDOR_CPPTRACE=ON
|
cmake -GNinja -B build -DCRASH_REPORTER=OFF
|
||||||
cmake --build build
|
cmake --build build
|
||||||
|
|
|
||||||
6
.github/workflows/lint.yml
vendored
6
.github/workflows/lint.yml
vendored
|
|
@ -5,17 +5,11 @@ jobs:
|
||||||
lint:
|
lint:
|
||||||
name: Lint
|
name: Lint
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
id-token: write
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
# Use cachix action over detsys for testing with act.
|
# Use cachix action over detsys for testing with act.
|
||||||
# - uses: cachix/install-nix-action@v27
|
# - uses: cachix/install-nix-action@v27
|
||||||
- uses: DeterminateSystems/nix-installer-action@main
|
- uses: DeterminateSystems/nix-installer-action@main
|
||||||
- uses: DeterminateSystems/magic-nix-cache-action@main
|
|
||||||
with:
|
|
||||||
use-flakehub: false
|
|
||||||
- uses: nicknovitski/nix-develop@v1
|
- uses: nicknovitski/nix-develop@v1
|
||||||
|
|
||||||
- name: Check formatting
|
- name: Check formatting
|
||||||
|
|
|
||||||
66
BUILD.md
66
BUILD.md
|
|
@ -15,7 +15,15 @@ Please make this descriptive enough to identify your specific package, for examp
|
||||||
- `Nixpkgs`
|
- `Nixpkgs`
|
||||||
- `Fedora COPR (errornointernet/quickshell)`
|
- `Fedora COPR (errornointernet/quickshell)`
|
||||||
|
|
||||||
If you are forking quickshell, please change `CRASHREPORT_URL` to your own issue tracker.
|
`-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
|
### QML Module dir
|
||||||
Currently all QML modules are statically linked to quickshell, but this is where
|
Currently all QML modules are statically linked to quickshell, but this is where
|
||||||
|
|
@ -33,22 +41,16 @@ Quickshell has a set of base dependencies you will always need, names vary by di
|
||||||
- `cmake`
|
- `cmake`
|
||||||
- `qt6base`
|
- `qt6base`
|
||||||
- `qt6declarative`
|
- `qt6declarative`
|
||||||
- `libdrm`
|
- `qtshadertools` (build-time only)
|
||||||
- `qtshadertools` (build-time)
|
- `spirv-tools` (build-time only)
|
||||||
- `spirv-tools` (build-time)
|
- `pkg-config` (build-time only)
|
||||||
- `pkg-config` (build-time)
|
- `cli11`
|
||||||
- `cli11` (static library)
|
|
||||||
|
|
||||||
Build time dependencies and static libraries don't have to exist at runtime,
|
|
||||||
however build time dependencies must be compiled for the architecture of
|
|
||||||
the builder, while static libraries must be compiled for the architecture
|
|
||||||
of the target.
|
|
||||||
|
|
||||||
On some distros, private Qt headers are in separate packages which you may have to install.
|
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:
|
We currently require private headers for the following libraries:
|
||||||
|
|
||||||
- `qt6declarative`
|
- `qt6declarative`
|
||||||
- `qt6wayland` (for Qt versions prior to 6.10)
|
- `qt6wayland`
|
||||||
|
|
||||||
We recommend an implicit dependency on `qt6svg`. If it is not installed, svg images and
|
We recommend an implicit dependency on `qt6svg`. If it is not installed, svg images and
|
||||||
svg icons will not work, including system ones.
|
svg icons will not work, including system ones.
|
||||||
|
|
@ -57,24 +59,14 @@ At least Qt 6.6 is required.
|
||||||
|
|
||||||
All features are enabled by default and some have their own dependencies.
|
All features are enabled by default and some have their own dependencies.
|
||||||
|
|
||||||
### Crash Handler
|
### Crash Reporter
|
||||||
The crash reporter catches crashes, restarts Quickshell when it crashes,
|
The crash reporter catches crashes, restarts quickshell when it crashes,
|
||||||
and collects useful crash information in one place. Leaving this enabled will
|
and collects useful crash information in one place. Leaving this enabled will
|
||||||
enable us to fix bugs far more easily.
|
enable us to fix bugs far more easily.
|
||||||
|
|
||||||
To disable: `-DCRASH_HANDLER=OFF`
|
To disable: `-DCRASH_REPORTER=OFF`
|
||||||
|
|
||||||
Dependencies: `cpptrace`
|
Dependencies: `google-breakpad`
|
||||||
|
|
||||||
Note: `-DVENDOR_CPPTRACE=ON` can be set to vendor cpptrace using FetchContent.
|
|
||||||
|
|
||||||
When using FetchContent, `libunwind` is required, and `libdwarf` can be provided by the
|
|
||||||
package manager or fetched with FetchContent.
|
|
||||||
|
|
||||||
*Please ensure binaries have usable symbols.* We do not necessarily need full debuginfo, but
|
|
||||||
leaving symbols in the binary is extremely helpful. You can check if symbols are useful
|
|
||||||
by sending a SIGSEGV to the process and ensuring symbols for the quickshell binary are present
|
|
||||||
in the trace.
|
|
||||||
|
|
||||||
### Jemalloc
|
### Jemalloc
|
||||||
We recommend leaving Jemalloc enabled as it will mask memory fragmentation caused
|
We recommend leaving Jemalloc enabled as it will mask memory fragmentation caused
|
||||||
|
|
@ -107,13 +99,10 @@ Currently supported Qt versions: `6.6`, `6.7`.
|
||||||
To disable: `-DWAYLAND=OFF`
|
To disable: `-DWAYLAND=OFF`
|
||||||
|
|
||||||
Dependencies:
|
Dependencies:
|
||||||
- `qt6wayland` (for Qt versions prior to 6.10)
|
- `qt6wayland`
|
||||||
- `wayland` (libwayland-client)
|
- `wayland` (libwayland-client)
|
||||||
- `wayland-scanner` (build time)
|
- `wayland-scanner` (may be part of your distro's wayland package)
|
||||||
- `wayland-protocols` (static library)
|
- `wayland-protocols`
|
||||||
|
|
||||||
Note that one or both of `wayland-scanner` and `wayland-protocols` may be bundled
|
|
||||||
with you distro's wayland package.
|
|
||||||
|
|
||||||
#### Wlroots Layershell
|
#### Wlroots Layershell
|
||||||
Enables wlroots layershell integration through the [zwlr-layer-shell-v1] protocol,
|
Enables wlroots layershell integration through the [zwlr-layer-shell-v1] protocol,
|
||||||
|
|
@ -147,8 +136,8 @@ Enables streaming video from monitors and toplevel windows through various proto
|
||||||
To disable: `-DSCREENCOPY=OFF`
|
To disable: `-DSCREENCOPY=OFF`
|
||||||
|
|
||||||
Dependencies:
|
Dependencies:
|
||||||
|
- `libdrm`
|
||||||
- `libgbm`
|
- `libgbm`
|
||||||
- `vulkan-headers` (build-time)
|
|
||||||
|
|
||||||
Specific protocols can also be disabled:
|
Specific protocols can also be disabled:
|
||||||
- `DSCREENCOPY_ICC=OFF` - Disable screencopy via [ext-image-copy-capture-v1]
|
- `DSCREENCOPY_ICC=OFF` - Disable screencopy via [ext-image-copy-capture-v1]
|
||||||
|
|
@ -195,13 +184,6 @@ To disable: `-DSERVICE_PAM=OFF`
|
||||||
|
|
||||||
Dependencies: `pam`
|
Dependencies: `pam`
|
||||||
|
|
||||||
### Polkit
|
|
||||||
This feature enables creating Polkit agents that can prompt user for authentication.
|
|
||||||
|
|
||||||
To disable: `-DSERVICE_POLKIT=OFF`
|
|
||||||
|
|
||||||
Dependencies: `polkit`, `glib`
|
|
||||||
|
|
||||||
### Hyprland
|
### Hyprland
|
||||||
This feature enables hyprland specific integrations. It requires wayland support
|
This feature enables hyprland specific integrations. It requires wayland support
|
||||||
but has no extra dependencies.
|
but has no extra dependencies.
|
||||||
|
|
@ -238,11 +220,11 @@ To disable: `-DI3_IPC=OFF`
|
||||||
## Building
|
## Building
|
||||||
*For developers and prospective contributors: See [CONTRIBUTING.md](CONTRIBUTING.md).*
|
*For developers and prospective contributors: See [CONTRIBUTING.md](CONTRIBUTING.md).*
|
||||||
|
|
||||||
Only `ninja` builds are tested, but makefiles may work.
|
We highly recommend using `ninja` to run the build, but you can use makefiles if you must.
|
||||||
|
|
||||||
#### Configuring the build
|
#### Configuring the build
|
||||||
```sh
|
```sh
|
||||||
$ cmake -GNinja -B build -DCMAKE_BUILD_TYPE=Release [additional disable flags from above here]
|
$ 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
|
Note that features you do not supply dependencies for MUST be disabled with their associated flags
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,5 @@
|
||||||
cmake_minimum_required(VERSION 3.20)
|
cmake_minimum_required(VERSION 3.20)
|
||||||
project(quickshell VERSION "0.2.1" LANGUAGES CXX C)
|
project(quickshell VERSION "0.1.0" LANGUAGES CXX C)
|
||||||
|
|
||||||
set(UNRELEASED_FEATURES
|
|
||||||
"network.2"
|
|
||||||
"colorquant-imagerect"
|
|
||||||
"window-parent"
|
|
||||||
)
|
|
||||||
|
|
||||||
set(QT_MIN_VERSION "6.6.0")
|
set(QT_MIN_VERSION "6.6.0")
|
||||||
set(CMAKE_CXX_STANDARD 20)
|
set(CMAKE_CXX_STANDARD 20)
|
||||||
|
|
@ -13,9 +7,6 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||||
|
|
||||||
set(QS_BUILD_OPTIONS "")
|
set(QS_BUILD_OPTIONS "")
|
||||||
|
|
||||||
# should be changed for forks
|
|
||||||
set(CRASHREPORT_URL "https://github.com/outfoxxed/quickshell/issues/new?template=crash2.yml" CACHE STRING "Bugreport URL")
|
|
||||||
|
|
||||||
function(boption VAR NAME DEFAULT)
|
function(boption VAR NAME DEFAULT)
|
||||||
cmake_parse_arguments(PARSE_ARGV 3 arg "" "REQUIRES" "")
|
cmake_parse_arguments(PARSE_ARGV 3 arg "" "REQUIRES" "")
|
||||||
|
|
||||||
|
|
@ -47,17 +38,14 @@ string(APPEND QS_BUILD_OPTIONS " Distributor: ${DISTRIBUTOR}")
|
||||||
|
|
||||||
message(STATUS "Quickshell configuration")
|
message(STATUS "Quickshell configuration")
|
||||||
message(STATUS " Distributor: ${DISTRIBUTOR}")
|
message(STATUS " Distributor: ${DISTRIBUTOR}")
|
||||||
|
boption(DISTRIBUTOR_DEBUGINFO_AVAILABLE "Distributor provided debuginfo" NO)
|
||||||
boption(NO_PCH "Disable precompild headers (dev)" OFF)
|
boption(NO_PCH "Disable precompild headers (dev)" OFF)
|
||||||
boption(BUILD_TESTING "Build tests (dev)" OFF)
|
boption(BUILD_TESTING "Build tests (dev)" OFF)
|
||||||
boption(ASAN "ASAN (dev)" OFF) # note: better output with gcc than clang
|
boption(ASAN "ASAN (dev)" OFF) # note: better output with gcc than clang
|
||||||
boption(FRAME_POINTERS "Keep Frame Pointers (dev)" ${ASAN})
|
boption(FRAME_POINTERS "Keep Frame Pointers (dev)" ${ASAN})
|
||||||
|
|
||||||
if (CMAKE_SYSTEM_NAME STREQUAL "FreeBSD")
|
boption(CRASH_REPORTER "Crash Handling" ON)
|
||||||
boption(USE_JEMALLOC "Use jemalloc" OFF)
|
boption(USE_JEMALLOC "Use jemalloc" ON)
|
||||||
else()
|
|
||||||
boption(USE_JEMALLOC "Use jemalloc" ON)
|
|
||||||
endif()
|
|
||||||
boption(CRASH_HANDLER "Crash Handling" ON)
|
|
||||||
boption(SOCKETS "Unix Sockets" ON)
|
boption(SOCKETS "Unix Sockets" ON)
|
||||||
boption(WAYLAND "Wayland" ON)
|
boption(WAYLAND "Wayland" ON)
|
||||||
boption(WAYLAND_WLR_LAYERSHELL " Wlroots Layer-Shell" ON REQUIRES WAYLAND)
|
boption(WAYLAND_WLR_LAYERSHELL " Wlroots Layer-Shell" ON REQUIRES WAYLAND)
|
||||||
|
|
@ -79,21 +67,17 @@ boption(SERVICE_STATUS_NOTIFIER "System Tray" ON)
|
||||||
boption(SERVICE_PIPEWIRE "PipeWire" ON)
|
boption(SERVICE_PIPEWIRE "PipeWire" ON)
|
||||||
boption(SERVICE_MPRIS "Mpris" ON)
|
boption(SERVICE_MPRIS "Mpris" ON)
|
||||||
boption(SERVICE_PAM "Pam" ON)
|
boption(SERVICE_PAM "Pam" ON)
|
||||||
boption(SERVICE_POLKIT "Polkit" ON)
|
|
||||||
boption(SERVICE_GREETD "Greetd" ON)
|
boption(SERVICE_GREETD "Greetd" ON)
|
||||||
boption(SERVICE_UPOWER "UPower" ON)
|
boption(SERVICE_UPOWER "UPower" ON)
|
||||||
boption(SERVICE_NOTIFICATIONS "Notifications" ON)
|
boption(SERVICE_NOTIFICATIONS "Notifications" ON)
|
||||||
boption(BLUETOOTH "Bluetooth" ON)
|
|
||||||
boption(NETWORK "Network" ON)
|
|
||||||
|
|
||||||
include(cmake/install-qml-module.cmake)
|
include(cmake/install-qml-module.cmake)
|
||||||
include(cmake/util.cmake)
|
include(cmake/util.cmake)
|
||||||
|
|
||||||
add_compile_options(-Wall -Wextra -Wno-vla-cxx-extension)
|
add_compile_options(-Wall -Wextra -Wno-vla-cxx-extension)
|
||||||
|
|
||||||
# pipewire defines these, breaking PCH
|
# pipewire defines this, breaking PCH
|
||||||
add_compile_definitions(_REENTRANT)
|
add_compile_definitions(_REENTRANT)
|
||||||
add_compile_options(-fno-strict-overflow)
|
|
||||||
|
|
||||||
if (FRAME_POINTERS)
|
if (FRAME_POINTERS)
|
||||||
add_compile_options(-fno-omit-frame-pointer)
|
add_compile_options(-fno-omit-frame-pointer)
|
||||||
|
|
@ -115,7 +99,6 @@ if (NOT CMAKE_BUILD_TYPE)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
set(QT_FPDEPS Gui Qml Quick QuickControls2 Widgets ShaderTools)
|
set(QT_FPDEPS Gui Qml Quick QuickControls2 Widgets ShaderTools)
|
||||||
set(QT_PRIVDEPS QuickPrivate)
|
|
||||||
|
|
||||||
include(cmake/pch.cmake)
|
include(cmake/pch.cmake)
|
||||||
|
|
||||||
|
|
@ -131,10 +114,9 @@ endif()
|
||||||
|
|
||||||
if (WAYLAND)
|
if (WAYLAND)
|
||||||
list(APPEND QT_FPDEPS WaylandClient)
|
list(APPEND QT_FPDEPS WaylandClient)
|
||||||
list(APPEND QT_PRIVDEPS WaylandClientPrivate)
|
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
if (SERVICE_STATUS_NOTIFIER OR SERVICE_MPRIS OR SERVICE_UPOWER OR SERVICE_NOTIFICATIONS OR BLUETOOTH OR NETWORK)
|
if (SERVICE_STATUS_NOTIFIER OR SERVICE_MPRIS OR SERVICE_UPOWER OR SERVICE_NOTIFICATIONS)
|
||||||
set(DBUS ON)
|
set(DBUS ON)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
|
@ -144,13 +126,6 @@ endif()
|
||||||
|
|
||||||
find_package(Qt6 REQUIRED COMPONENTS ${QT_FPDEPS})
|
find_package(Qt6 REQUIRED COMPONENTS ${QT_FPDEPS})
|
||||||
|
|
||||||
# In Qt 6.10, private dependencies are required to be explicit,
|
|
||||||
# but they could not be explicitly depended on prior to 6.9.
|
|
||||||
if (Qt6_VERSION VERSION_GREATER_EQUAL "6.9.0")
|
|
||||||
set(QT_NO_PRIVATE_MODULE_WARNING ON)
|
|
||||||
find_package(Qt6 REQUIRED COMPONENTS ${QT_PRIVDEPS})
|
|
||||||
endif()
|
|
||||||
|
|
||||||
set(CMAKE_AUTOUIC OFF)
|
set(CMAKE_AUTOUIC OFF)
|
||||||
qt_standard_project_setup(REQUIRES 6.6)
|
qt_standard_project_setup(REQUIRES 6.6)
|
||||||
set(QT_QML_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/qml_modules)
|
set(QT_QML_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/qml_modules)
|
||||||
|
|
@ -170,14 +145,3 @@ install(CODE "
|
||||||
${CMAKE_INSTALL_FULL_BINDIR}/quickshell \$ENV{DESTDIR}${CMAKE_INSTALL_FULL_BINDIR}/qs
|
${CMAKE_INSTALL_FULL_BINDIR}/quickshell \$ENV{DESTDIR}${CMAKE_INSTALL_FULL_BINDIR}/qs
|
||||||
)
|
)
|
||||||
")
|
")
|
||||||
|
|
||||||
install(
|
|
||||||
FILES ${CMAKE_SOURCE_DIR}/assets/org.quickshell.desktop
|
|
||||||
DESTINATION ${CMAKE_INSTALL_DATADIR}/applications
|
|
||||||
)
|
|
||||||
|
|
||||||
install(
|
|
||||||
FILES ${CMAKE_SOURCE_DIR}/assets/quickshell.svg
|
|
||||||
DESTINATION ${CMAKE_INSTALL_DATADIR}/icons/hicolor/scalable/apps
|
|
||||||
RENAME org.quickshell.svg
|
|
||||||
)
|
|
||||||
|
|
|
||||||
247
CONTRIBUTING.md
247
CONTRIBUTING.md
|
|
@ -1,40 +1,235 @@
|
||||||
# Contributing
|
# Contributing / Development
|
||||||
|
Instructions for development setup and upstreaming patches.
|
||||||
|
|
||||||
Thank you for taking the time to contribute.
|
If you just want to build or package quickshell see [BUILD.md](BUILD.md).
|
||||||
To ensure nobody's time is wasted, please follow the rules below.
|
|
||||||
|
|
||||||
## Acceptable Code Contributions
|
## Development
|
||||||
|
|
||||||
- All changes submitted MUST be **fully understood by the submitter**. If you do not know why or how
|
Install the dependencies listed in [BUILD.md](BUILD.md).
|
||||||
your change works, do not submit it to be merged. You must be able to explain your reasoning
|
You probably want all of them even if you don't use all of them
|
||||||
for every change.
|
to ensure tests work correctly and avoid passing a bunch of configure
|
||||||
|
flags when you need to wipe the build directory.
|
||||||
|
|
||||||
- Changes MUST be submitted by a human who will be responsible for them. Changes submitted without
|
Quickshell also uses `just` for common development command aliases.
|
||||||
a human in the loop such as automated tooling and AI Agents are **strictly disallowed**. Accounts
|
|
||||||
responsible for such contribution attempts **will be banned**.
|
|
||||||
|
|
||||||
- Changes MUST respect Quickshell's license and the license of any source works. Changes including
|
The dependencies are also available as a nix shell or nix flake which we recommend
|
||||||
code from any other works must disclose the source of the code, explain why it was used, and
|
using with nix-direnv.
|
||||||
ensure the license is compatible.
|
|
||||||
|
|
||||||
- Changes must follow the guidelines outlined in [HACKING.md](HACKING.md) for style and substance.
|
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.
|
||||||
|
|
||||||
- Changes must stand on their own as a unit. Do not make multiple unrelated changes in one PR.
|
### Formatting
|
||||||
Changes depending on prior merges should be marked as a draft.
|
All contributions should be formatted similarly to what already exists.
|
||||||
|
Group related functionality together.
|
||||||
|
|
||||||
## Acceptable Non-code Contributions
|
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`.
|
||||||
|
|
||||||
- Bug and crash reports. You must follow the instructions in the issue templates and provide the
|
#### Style preferences not caught by clang-format
|
||||||
information requested.
|
These are flexible. You can ignore them if it looks or works better to
|
||||||
|
for one reason or another.
|
||||||
|
|
||||||
- Feature requests can be made via Issues. Please check to ensure nobody else has requested the same feature.
|
Use `auto` if the type of a variable can be deduced automatically, instead of
|
||||||
|
redeclaring the returned value's type. Additionally, auto should be used when a
|
||||||
|
constructor takes arguments.
|
||||||
|
|
||||||
- Do not make insubstantial or pointless changes.
|
```cpp
|
||||||
|
auto x = <expr>; // ok
|
||||||
|
auto x = QString::number(3); // ok
|
||||||
|
QString x; // ok
|
||||||
|
QString x = "foo"; // ok
|
||||||
|
auto x = QString("foo"); // ok
|
||||||
|
|
||||||
- Changes to project rules / policy / governance will not be entertained, except from significant
|
auto x = QString(); // avoid
|
||||||
long-term contributors. These changes should not be addressed through contribution channels.
|
QString x(); // avoid
|
||||||
|
QString x("foo"); // avoid
|
||||||
|
```
|
||||||
|
|
||||||
## Merge timelines
|
Put newlines around logical units of code, and after closing braces. If the
|
||||||
|
most reasonable logical unit of code takes only a single line, it should be
|
||||||
|
merged into the next single line logical unit if applicable.
|
||||||
|
```cpp
|
||||||
|
// multiple units
|
||||||
|
auto x = <expr>; // unit 1
|
||||||
|
auto y = <expr>; // unit 2
|
||||||
|
|
||||||
We handle work for the most part on a push basis. If your PR has been ignored for a while
|
auto x = <expr>; // unit 1
|
||||||
and is still relevant please bump it.
|
emit this->y(); // unit 2
|
||||||
|
|
||||||
|
auto x1 = <expr>; // unit 1
|
||||||
|
auto x2 = <expr>; // unit 1
|
||||||
|
auto x3 = <expr>; // unit 1
|
||||||
|
|
||||||
|
auto y1 = <expr>; // unit 2
|
||||||
|
auto y2 = <expr>; // unit 2
|
||||||
|
auto y3 = <expr>; // unit 2
|
||||||
|
|
||||||
|
// one unit
|
||||||
|
auto x = <expr>;
|
||||||
|
if (x...) {
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
|
||||||
|
// if more than one variable needs to be used then add a newline
|
||||||
|
auto x = <expr>;
|
||||||
|
auto y = <expr>;
|
||||||
|
|
||||||
|
if (x && y) {
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Class formatting:
|
||||||
|
```cpp
|
||||||
|
//! Doc comment summary
|
||||||
|
/// Doc comment body
|
||||||
|
class Foo: public QObject {
|
||||||
|
// The Q_OBJECT macro comes first. Macros are ; terminated.
|
||||||
|
Q_OBJECT;
|
||||||
|
QML_ELEMENT;
|
||||||
|
QML_CLASSINFO(...);
|
||||||
|
// Properties must stay on a single line or the doc generator won't be able to pick them up
|
||||||
|
Q_PROPERTY(...);
|
||||||
|
/// Doc comment
|
||||||
|
Q_PROPERTY(...);
|
||||||
|
/// Doc comment
|
||||||
|
Q_PROPERTY(...);
|
||||||
|
|
||||||
|
public:
|
||||||
|
// Classes should have explicit constructors if they aren't intended to
|
||||||
|
// implicitly cast. The constructor can be inline in the header if it has no body.
|
||||||
|
explicit Foo(QObject* parent = nullptr): QObject(parent) {}
|
||||||
|
|
||||||
|
// Instance functions if applicable.
|
||||||
|
static Foo* instance();
|
||||||
|
|
||||||
|
// Member functions unrelated to properties come next
|
||||||
|
void function();
|
||||||
|
void function();
|
||||||
|
void function();
|
||||||
|
|
||||||
|
// Then Q_INVOKABLEs
|
||||||
|
Q_INVOKABLE function();
|
||||||
|
/// Doc comment
|
||||||
|
Q_INVOKABLE function();
|
||||||
|
/// Doc comment
|
||||||
|
Q_INVOKABLE function();
|
||||||
|
|
||||||
|
// Then property related functions, in the order (bindable, getter, setter).
|
||||||
|
// Related functions may be included here as well. Function bodies may be inline
|
||||||
|
// if they are a single expression. There should be a newline between each
|
||||||
|
// property's methods.
|
||||||
|
[[nodiscard]] QBindable<T> bindableFoo() { return &this->bFoo; }
|
||||||
|
[[nodiscard]] T foo() const { return this->foo; }
|
||||||
|
void setFoo();
|
||||||
|
|
||||||
|
[[nodiscard]] T bar() const { return this->foo; }
|
||||||
|
void setBar();
|
||||||
|
|
||||||
|
signals:
|
||||||
|
// Signals that are not property change related go first.
|
||||||
|
// Property change signals go in property definition order.
|
||||||
|
void asd();
|
||||||
|
void asd2();
|
||||||
|
void fooChanged();
|
||||||
|
void barChanged();
|
||||||
|
|
||||||
|
public slots:
|
||||||
|
// generally Q_INVOKABLEs are preferred to public slots.
|
||||||
|
void slot();
|
||||||
|
|
||||||
|
private slots:
|
||||||
|
// ...
|
||||||
|
|
||||||
|
private:
|
||||||
|
// statics, then functions, then fields
|
||||||
|
static const foo BAR;
|
||||||
|
static void foo();
|
||||||
|
|
||||||
|
void foo();
|
||||||
|
void bar();
|
||||||
|
|
||||||
|
// property related members are prefixed with `m`.
|
||||||
|
QString mFoo;
|
||||||
|
QString bar;
|
||||||
|
|
||||||
|
// Bindables go last and should be prefixed with `b`.
|
||||||
|
Q_OBJECT_BINDABLE_PROPERTY(Foo, QString, bFoo, &Foo::fooChanged);
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### Linter
|
||||||
|
All contributions should pass the linter.
|
||||||
|
|
||||||
|
Note that running the linter requires disabling precompiled
|
||||||
|
headers and including the test codepaths:
|
||||||
|
```sh
|
||||||
|
$ just configure debug -DNO_PCH=ON -DBUILD_TESTING=ON
|
||||||
|
$ just lint-changed
|
||||||
|
```
|
||||||
|
|
||||||
|
If the linter is complaining about something that you think it should not,
|
||||||
|
please disable the lint in your MR and explain your reasoning if it isn't obvious.
|
||||||
|
|
||||||
|
### Tests
|
||||||
|
If you feel like the feature you are working on is very complex or likely to break,
|
||||||
|
please write some tests. We will ask you to directly if you send in an MR for an
|
||||||
|
overly complex or breakable feature.
|
||||||
|
|
||||||
|
At least all tests that passed before your changes should still be passing
|
||||||
|
by the time your contribution is ready.
|
||||||
|
|
||||||
|
You can run the tests using `just test` but you must enable them first
|
||||||
|
using `-DBUILD_TESTING=ON`.
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
Most of quickshell's documentation is automatically generated from the source code.
|
||||||
|
You should annotate `Q_PROPERTY`s and `Q_INVOKABLE`s with doc comments. Note that the parser
|
||||||
|
cannot handle random line breaks and will usually require you to disable clang-format if the
|
||||||
|
lines are too long.
|
||||||
|
|
||||||
|
Before submitting an MR, if adding new features please make sure the documentation is generated
|
||||||
|
reasonably using the `quickshell-docs` repo. We recommend checking it out at `/docs` in this repo.
|
||||||
|
|
||||||
|
Doc comments take the form `///` or `///!` (summary) and work with markdown.
|
||||||
|
You can reference other types using the `@@[Module.][Type.][member]` shorthand
|
||||||
|
where all parts are optional. If module or type are not specified they will
|
||||||
|
be inferred as the current module. Member can be a `property`, `function()` or `signal(s)`.
|
||||||
|
Look at existing code for how it works.
|
||||||
|
|
||||||
|
Quickshell modules additionally have a `module.md` file which contains a summary, description,
|
||||||
|
and list of headers to scan for documentation.
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
### Commits
|
||||||
|
Please structure your commit messages as `scope[!]: commit` where
|
||||||
|
the scope is something like `core` or `service/mpris`. (pick what has been
|
||||||
|
used historically or what makes sense if new). Add `!` for changes that break
|
||||||
|
existing APIs or functionality.
|
||||||
|
|
||||||
|
Commit descriptions should contain a summary of the changes if they are not
|
||||||
|
sufficiently addressed in the commit message.
|
||||||
|
|
||||||
|
Please squash/rebase additions or edits to previous changes and follow the
|
||||||
|
commit style to keep the history easily searchable at a glance.
|
||||||
|
Depending on the change, it is often reasonable to squash it into just
|
||||||
|
a single commit. (If you do not follow this we will squash your changes
|
||||||
|
for you.)
|
||||||
|
|
||||||
|
### Sending patches
|
||||||
|
You may contribute by submitting a pull request on github, asking for
|
||||||
|
an account on our git server, or emailing patches / git bundles
|
||||||
|
directly to `outfoxxed@outfoxxed.me`.
|
||||||
|
|
||||||
|
### Getting help
|
||||||
|
If you're getting stuck, you can come talk to us in the
|
||||||
|
[quickshell-development matrix room](https://matrix.to/#/#quickshell-development:outfoxxed.me)
|
||||||
|
for help on implementation, conventions, etc.
|
||||||
|
Feel free to ask for advice early in your implementation if you are
|
||||||
|
unsure.
|
||||||
|
|
|
||||||
226
HACKING.md
226
HACKING.md
|
|
@ -1,226 +0,0 @@
|
||||||
## 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.
|
|
||||||
|
|
||||||
The dependencies are also available as a nix shell or nix flake which we recommend
|
|
||||||
using with nix-direnv.
|
|
||||||
|
|
||||||
Quickshell uses `just` for common development command aliases.
|
|
||||||
|
|
||||||
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`.
|
|
||||||
|
|
||||||
#### Style preferences not caught by clang-format
|
|
||||||
These are flexible. You can ignore them if it looks or works better to
|
|
||||||
for one reason or another.
|
|
||||||
|
|
||||||
Use `auto` if the type of a variable can be deduced automatically, instead of
|
|
||||||
redeclaring the returned value's type. Additionally, auto should be used when a
|
|
||||||
constructor takes arguments.
|
|
||||||
|
|
||||||
```cpp
|
|
||||||
auto x = <expr>; // ok
|
|
||||||
auto x = QString::number(3); // ok
|
|
||||||
QString x; // ok
|
|
||||||
QString x = "foo"; // ok
|
|
||||||
auto x = QString("foo"); // ok
|
|
||||||
|
|
||||||
auto x = QString(); // avoid
|
|
||||||
QString x(); // avoid
|
|
||||||
QString x("foo"); // avoid
|
|
||||||
```
|
|
||||||
|
|
||||||
Put newlines around logical units of code, and after closing braces. If the
|
|
||||||
most reasonable logical unit of code takes only a single line, it should be
|
|
||||||
merged into the next single line logical unit if applicable.
|
|
||||||
```cpp
|
|
||||||
// multiple units
|
|
||||||
auto x = <expr>; // unit 1
|
|
||||||
auto y = <expr>; // unit 2
|
|
||||||
|
|
||||||
auto x = <expr>; // unit 1
|
|
||||||
emit this->y(); // unit 2
|
|
||||||
|
|
||||||
auto x1 = <expr>; // unit 1
|
|
||||||
auto x2 = <expr>; // unit 1
|
|
||||||
auto x3 = <expr>; // unit 1
|
|
||||||
|
|
||||||
auto y1 = <expr>; // unit 2
|
|
||||||
auto y2 = <expr>; // unit 2
|
|
||||||
auto y3 = <expr>; // unit 2
|
|
||||||
|
|
||||||
// one unit
|
|
||||||
auto x = <expr>;
|
|
||||||
if (x...) {
|
|
||||||
// ...
|
|
||||||
}
|
|
||||||
|
|
||||||
// if more than one variable needs to be used then add a newline
|
|
||||||
auto x = <expr>;
|
|
||||||
auto y = <expr>;
|
|
||||||
|
|
||||||
if (x && y) {
|
|
||||||
// ...
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Class formatting:
|
|
||||||
```cpp
|
|
||||||
//! Doc comment summary
|
|
||||||
/// Doc comment body
|
|
||||||
class Foo: public QObject {
|
|
||||||
// The Q_OBJECT macro comes first. Macros are ; terminated.
|
|
||||||
Q_OBJECT;
|
|
||||||
QML_ELEMENT;
|
|
||||||
QML_CLASSINFO(...);
|
|
||||||
// Properties must stay on a single line or the doc generator won't be able to pick them up
|
|
||||||
Q_PROPERTY(...);
|
|
||||||
/// Doc comment
|
|
||||||
Q_PROPERTY(...);
|
|
||||||
/// Doc comment
|
|
||||||
Q_PROPERTY(...);
|
|
||||||
|
|
||||||
public:
|
|
||||||
// Classes should have explicit constructors if they aren't intended to
|
|
||||||
// implicitly cast. The constructor can be inline in the header if it has no body.
|
|
||||||
explicit Foo(QObject* parent = nullptr): QObject(parent) {}
|
|
||||||
|
|
||||||
// Instance functions if applicable.
|
|
||||||
static Foo* instance();
|
|
||||||
|
|
||||||
// Member functions unrelated to properties come next
|
|
||||||
void function();
|
|
||||||
void function();
|
|
||||||
void function();
|
|
||||||
|
|
||||||
// Then Q_INVOKABLEs
|
|
||||||
Q_INVOKABLE function();
|
|
||||||
/// Doc comment
|
|
||||||
Q_INVOKABLE function();
|
|
||||||
/// Doc comment
|
|
||||||
Q_INVOKABLE function();
|
|
||||||
|
|
||||||
// Then property related functions, in the order (bindable, getter, setter).
|
|
||||||
// Related functions may be included here as well. Function bodies may be inline
|
|
||||||
// if they are a single expression. There should be a newline between each
|
|
||||||
// property's methods.
|
|
||||||
[[nodiscard]] QBindable<T> bindableFoo() { return &this->bFoo; }
|
|
||||||
[[nodiscard]] T foo() const { return this->foo; }
|
|
||||||
void setFoo();
|
|
||||||
|
|
||||||
[[nodiscard]] T bar() const { return this->foo; }
|
|
||||||
void setBar();
|
|
||||||
|
|
||||||
signals:
|
|
||||||
// Signals that are not property change related go first.
|
|
||||||
// Property change signals go in property definition order.
|
|
||||||
void asd();
|
|
||||||
void asd2();
|
|
||||||
void fooChanged();
|
|
||||||
void barChanged();
|
|
||||||
|
|
||||||
public slots:
|
|
||||||
// generally Q_INVOKABLEs are preferred to public slots.
|
|
||||||
void slot();
|
|
||||||
|
|
||||||
private slots:
|
|
||||||
// ...
|
|
||||||
|
|
||||||
private:
|
|
||||||
// statics, then functions, then fields
|
|
||||||
static const foo BAR;
|
|
||||||
static void foo();
|
|
||||||
|
|
||||||
void foo();
|
|
||||||
void bar();
|
|
||||||
|
|
||||||
// property related members are prefixed with `m`.
|
|
||||||
QString mFoo;
|
|
||||||
QString bar;
|
|
||||||
|
|
||||||
// Bindables go last and should be prefixed with `b`.
|
|
||||||
Q_OBJECT_BINDABLE_PROPERTY(Foo, QString, bFoo, &Foo::fooChanged);
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
Use lowercase .h suffixed Qt headers, e.g. `<qwindow.h>` over `<QWindow>`.
|
|
||||||
|
|
||||||
### Linter
|
|
||||||
All contributions should pass the linter.
|
|
||||||
|
|
||||||
Note that running the linter requires disabling precompiled
|
|
||||||
headers and including the test codepaths:
|
|
||||||
```sh
|
|
||||||
$ just configure debug -DNO_PCH=ON -DBUILD_TESTING=ON
|
|
||||||
$ just lint-changed
|
|
||||||
```
|
|
||||||
|
|
||||||
If the linter is complaining about something that you think it should not,
|
|
||||||
please disable the lint in your MR and explain your reasoning if it isn't obvious.
|
|
||||||
|
|
||||||
### Tests
|
|
||||||
If you feel like the feature you are working on is very complex or likely to break,
|
|
||||||
please write some tests. We will ask you to directly if you send in an MR for an
|
|
||||||
overly complex or breakable feature.
|
|
||||||
|
|
||||||
At least all tests that passed before your changes should still be passing
|
|
||||||
by the time your contribution is ready.
|
|
||||||
|
|
||||||
You can run the tests using `just test` but you must enable them first
|
|
||||||
using `-DBUILD_TESTING=ON`.
|
|
||||||
|
|
||||||
### Documentation
|
|
||||||
Most of quickshell's documentation is automatically generated from the source code.
|
|
||||||
You should annotate `Q_PROPERTY`s and `Q_INVOKABLE`s with doc comments. Note that the parser
|
|
||||||
cannot handle random line breaks and will usually require you to disable clang-format if the
|
|
||||||
lines are too long.
|
|
||||||
|
|
||||||
Make sure new files containing doc comments are added to a `module.md` file.
|
|
||||||
See existing module files for reference.
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
If you have made a user visible change since the last tagged release, describe it in
|
|
||||||
[changelog/next.md](changelog/next.md).
|
|
||||||
|
|
||||||
## 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).
|
|
||||||
|
|
||||||
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.)
|
|
||||||
|
|
||||||
### 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. There is also a bridged [discord server](https://discord.gg/UtZeT3xNyT).
|
|
||||||
Feel free to ask for advice early in your implementation if you are
|
|
||||||
unsure.
|
|
||||||
3
Justfile
3
Justfile
|
|
@ -12,9 +12,6 @@ lint-ci:
|
||||||
lint-changed:
|
lint-changed:
|
||||||
git diff --name-only HEAD | grep "^.*\.cpp\$" | parallel -j$(nproc) --no-notice --will-cite --tty --bar clang-tidy --load={{ env_var("TIDYFOX") }}
|
git diff --name-only HEAD | grep "^.*\.cpp\$" | parallel -j$(nproc) --no-notice --will-cite --tty --bar clang-tidy --load={{ env_var("TIDYFOX") }}
|
||||||
|
|
||||||
lint-staged:
|
|
||||||
git diff --staged --name-only --diff-filter=d HEAD | grep "^.*\.cpp\$" | parallel -j$(nproc) --no-notice --will-cite --tty --bar clang-tidy --load={{ env_var("TIDYFOX") }}
|
|
||||||
|
|
||||||
configure target='debug' *FLAGS='':
|
configure target='debug' *FLAGS='':
|
||||||
cmake -GNinja -B {{builddir}} \
|
cmake -GNinja -B {{builddir}} \
|
||||||
-DCMAKE_BUILD_TYPE={{ if target == "debug" { "Debug" } else { "RelWithDebInfo" } }} \
|
-DCMAKE_BUILD_TYPE={{ if target == "debug" { "Debug" } else { "RelWithDebInfo" } }} \
|
||||||
|
|
|
||||||
|
|
@ -2,14 +2,8 @@
|
||||||
See the [website](https://quickshell.outfoxxed.me) for more information
|
See the [website](https://quickshell.outfoxxed.me) for more information
|
||||||
and installation instructions.
|
and installation instructions.
|
||||||
|
|
||||||
This repo is hosted at:
|
|
||||||
- https://git.outfoxxed.me/quickshell/quickshell
|
|
||||||
- https://github.com/quickshell-mirror/quickshell
|
|
||||||
|
|
||||||
# Contributing / Development
|
# Contributing / Development
|
||||||
- [HACKING.md](HACKING.md) - Development instructions and policy.
|
See [CONTRIBUTING.md](CONTRIBUTING.md) for details.
|
||||||
- [CONTRIBUTING.md](CONTRIBUTING.md) - Contribution policy.
|
|
||||||
- [BUILD.md](BUILD.md) - Packaging and build instructions.
|
|
||||||
|
|
||||||
#### License
|
#### License
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
[Desktop Entry]
|
|
||||||
Version=1.5
|
|
||||||
Type=Application
|
|
||||||
NoDisplay=true
|
|
||||||
|
|
||||||
Name=Quickshell
|
|
||||||
Icon=org.quickshell
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="724.635" height="724.635"><path fill="#359757" stroke="#359757" stroke-linecap="square" stroke-linejoin="round" stroke-width="74.755" d="m37.378 160.237 122.859-122.86h527.02v527.02l-122.86 122.86H37.378Z"/><path fill="#fff" stroke="#fff" stroke-linecap="round" stroke-linejoin="round" stroke-width="12.201" d="M323.051 96.412a268.74 268.74 0 0 0-3.51.542c-4.052 14.481-7.815 29.941-14.904 42.692a230.02 230.02 0 0 0-59.406 24.679c-14.036-3.974-27.647-12.214-40.766-19.562a268.788 268.788 0 0 0-60.035 60.16c7.376 13.105 15.645 26.698 19.648 40.726a230.02 230.02 0 0 0-24.554 59.458c-12.735 7.115-28.186 10.913-42.66 14.994a268.789 268.789 0 0 0 .09 84.992c14.48 4.05 29.941 7.814 42.691 14.903a230.02 230.02 0 0 0 24.68 59.406c-3.974 14.037-12.215 27.647-19.563 40.766a268.788 268.788 0 0 0 60.161 60.035c13.104-7.376 26.696-15.645 40.725-19.648a230.02 230.02 0 0 0 59.457 24.555c7.116 12.735 10.913 28.186 14.995 42.659a268.788 268.788 0 0 0 84.99-.09c4.052-14.482 7.817-29.941 14.906-42.691a230.02 230.02 0 0 0 59.405-24.68c14.037 3.974 33.069 17.638 46.188 24.986a268.788 268.788 0 0 0 60.035-60.161c-7.376-13.105-21.068-32.12-25.071-46.149a230.02 230.02 0 0 0 24.555-59.457c12.735-7.116 28.186-10.912 42.659-14.993a268.788 268.788 0 0 0-.089-84.993c-14.482-4.051-29.942-7.814-42.692-14.904a230.02 230.02 0 0 0-24.68-59.405c3.974-14.037 12.216-27.647 19.565-40.767a268.788 268.788 0 0 0-60.161-60.035c-13.105 7.376-26.698 15.645-40.726 19.649a230.02 230.02 0 0 0-59.458-24.555c-7.115-12.735-10.913-28.187-14.994-42.66a268.788 268.788 0 0 0-81.481-.452zm15.778 106.85c58.282-8.328 116.455 15.865 151.643 63.065 35.19 47.2 41.766 109.86 17.144 163.337l-41.728-22.688s-38.558 31.44-57.344 63.012l23.893 36.326a160.78 160.78 0 0 1-46.633 15.058c-87.854 12.99-169.6-47.708-182.573-135.564-12.974-87.855 47.74-169.59 135.598-182.546Z"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.8 KiB |
|
|
@ -1,84 +0,0 @@
|
||||||
## Breaking Changes
|
|
||||||
|
|
||||||
### Config paths are no longer canonicalized
|
|
||||||
|
|
||||||
This fixes nix configs changing shell-ids on rebuild as the shell id is now derived from
|
|
||||||
the symlink path. Configs with a symlink in their path will have a different shell id.
|
|
||||||
|
|
||||||
Shell ids are used to derive the default config / state / cache folders, so those files
|
|
||||||
will need to be manually moved if using a config behind a symlinked path without an explicitly
|
|
||||||
set shell id.
|
|
||||||
|
|
||||||
## New Features
|
|
||||||
|
|
||||||
- Added support for creating Polkit agents.
|
|
||||||
- Added support for creating wayland idle inhibitors.
|
|
||||||
- Added support for wayland idle timeouts.
|
|
||||||
- Added support for inhibiting wayland compositor shortcuts for focused windows.
|
|
||||||
- Added the ability to override Quickshell.cacheDir with a custom path.
|
|
||||||
- Added minimized, maximized, and fullscreen properties to FloatingWindow.
|
|
||||||
- Added the ability to handle move and resize events to FloatingWindow.
|
|
||||||
- Pipewire service now reconnects if pipewire dies or a protocol error occurs.
|
|
||||||
- Added pipewire audio peak detection.
|
|
||||||
- Added network management support.
|
|
||||||
- Added support for grabbing focus from popup windows.
|
|
||||||
- Added support for IPC signal listeners.
|
|
||||||
- Added Quickshell version checking and version gated preprocessing.
|
|
||||||
- Added a way to detect if an icon is from the system icon theme or not.
|
|
||||||
- Added vulkan support to screencopy.
|
|
||||||
- Added generic WindowManager interface implementing ext-workspace.
|
|
||||||
- Added ext-background-effect window blur support.
|
|
||||||
- Added per-corner radius support to Region.
|
|
||||||
- Added ColorQuantizer region selection.
|
|
||||||
- Added dialog window support to FloatingWindow.
|
|
||||||
|
|
||||||
## Other Changes
|
|
||||||
|
|
||||||
- FreeBSD is now partially supported.
|
|
||||||
- IPC operations filter available instances to the current display connection by default.
|
|
||||||
- PwNodeLinkTracker ignores sound level monitoring programs.
|
|
||||||
- Replaced breakpad with cpptrace.
|
|
||||||
- Reloads are prevented if no file content has changed.
|
|
||||||
- Added `QS_DISABLE_FILE_WATCHER` environment variable to disable file watching.
|
|
||||||
- Added `QS_DISABLE_CRASH_HANDLER` environment variable to disable crash handling.
|
|
||||||
- Added `QS_CRASHREPORT_URL` environment variable to allow overriding the crash reporter link.
|
|
||||||
- Added `AppId` pragma and `QS_APP_ID` environment variable to allow overriding the desktop application ID.
|
|
||||||
- Added `DropExpensiveFonts` pragma and `QS_DROP_EXPENSIVE_FONTS` environment variable which avoids loading fonts which may cause lag and excessive memory usage if many variants are used.
|
|
||||||
- Unrecognized pragmas are no longer a hard error for future backward compatibility.
|
|
||||||
|
|
||||||
## Bug Fixes
|
|
||||||
|
|
||||||
- Fixed volume control breaking with pipewire pro audio mode.
|
|
||||||
- Fixed volume control breaking with bluez streams and potentially others.
|
|
||||||
- Fixed volume control breaking for devices without route definitions.
|
|
||||||
- Fixed escape sequence handling in desktop entries.
|
|
||||||
- Fixed volumes not initializing if a pipewire device was already loaded before its node.
|
|
||||||
- Fixed hyprland active toplevel not resetting after window closes.
|
|
||||||
- Fixed hyprland ipc window names and titles being reversed.
|
|
||||||
- Fixed a hyprland ipc crash when refreshing toplevels before workspaces.
|
|
||||||
- Fixed missing signals for system tray item title and description updates.
|
|
||||||
- Fixed asynchronous loaders not working after reload.
|
|
||||||
- Fixed asynchronous loaders not working before window creation.
|
|
||||||
- Fixed memory leak in IPC handlers.
|
|
||||||
- Fixed ClippingRectangle related crashes.
|
|
||||||
- Fixed crashes when monitors are unplugged.
|
|
||||||
- Fixed crashes when default pipewire devices are lost.
|
|
||||||
- Fixed ToplevelManager not clearing activeToplevel on deactivation.
|
|
||||||
- Desktop action order is now preserved.
|
|
||||||
- Fixed partial socket reads in greetd and hyprland on slow machines.
|
|
||||||
- Worked around Qt bug causing crashes when plugging and unplugging monitors.
|
|
||||||
- Fixed HyprlandFocusGrab crashing if windows were destroyed after being passed to it.
|
|
||||||
- Fixed ScreencopyView pixelation when scaled.
|
|
||||||
- Fixed JsonAdapter crashing and providing bad data on read when using JsonObject.
|
|
||||||
- Fixed JsonAdapter sending unnecessary property changes for primitive values.
|
|
||||||
- Fixed JsonAdapter serialization for lists.
|
|
||||||
- Fixed pipewire crashes after hotplugging devices and changing default outputs.
|
|
||||||
- Fixed launches failing for `--daemonize` on some systems.
|
|
||||||
|
|
||||||
## Packaging Changes
|
|
||||||
|
|
||||||
- `glib` and `polkit` have been added as dependencies when compiling with polkit agent support.
|
|
||||||
- `vulkan-headers` has been added as a build-time dependency for screencopy (Vulkan backend support).
|
|
||||||
- `breakpad` has been replaced by `cpptrace`, which is far easier to package, and the `CRASH_REPORTER` cmake variable has been replaced with `CRASH_HANDLER` to stop this from being easy to ignore.
|
|
||||||
- `DISTRIBUTOR_DEBUGINFO_AVAILABLE` was removed as it is no longer important without breakpad.
|
|
||||||
- `libdrm` is now unconditionally required as a direct dependency.
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
Initial release
|
|
||||||
|
|
@ -1,84 +0,0 @@
|
||||||
## Breaking Changes
|
|
||||||
|
|
||||||
- Files outside of the shell directory can no longer be referenced with relative paths, e.g. '../../foo.png'.
|
|
||||||
- PanelWindow's Automatic exclusion mode now adds an exclusion zone for panels with a single anchor.
|
|
||||||
- `QT_QUICK_CONTROLS_STYLE` and `QT_STYLE_OVERRIDE` are ignored unless `//@ pragma RespectSystemStyle` is set.
|
|
||||||
|
|
||||||
## New Features
|
|
||||||
|
|
||||||
### Root-Relative Imports
|
|
||||||
|
|
||||||
Quickshell 0.2 comes with a new method to import QML modules which is supported by QMLLS.
|
|
||||||
This replaces "root:/" imports for QML modules.
|
|
||||||
|
|
||||||
The new syntax is `import qs.path.to.module`, where `path/to/module` is the path to
|
|
||||||
a module/subdirectory relative to the config root (`qs`).
|
|
||||||
|
|
||||||
### Better LSP support
|
|
||||||
|
|
||||||
LSP support for Singletons and Root-Relative imports can be enabled by creating a file named
|
|
||||||
`.qmlls.ini` in the shell root directory. Quickshell will detect this file and automatically
|
|
||||||
populate it with an LSP configuration. This file should be gitignored in your configuration,
|
|
||||||
as it is system dependent.
|
|
||||||
|
|
||||||
The generated configuration also includes QML import paths available to Quickshell, meaning
|
|
||||||
QMLLS no longer requires the `-E` flag.
|
|
||||||
|
|
||||||
### Bluetooth Module
|
|
||||||
|
|
||||||
Quickshell can now manage your bluetooth devices through BlueZ. While authenticated pairing
|
|
||||||
has not landed in 0.2, support for connecting and disconnecting devices, basic device information,
|
|
||||||
and non-authenticated pairing are now supported.
|
|
||||||
|
|
||||||
### Other Features
|
|
||||||
|
|
||||||
- Added `HyprlandToplevel` and related toplevel/window management APIs in the Hyprland module.
|
|
||||||
- Added `Quickshell.execDetached()`, which spawns a detached process without a `Process` object.
|
|
||||||
- Added `Process.exec()` for easier reconfiguration of process commands when starting them.
|
|
||||||
- Added `FloatingWindow.title`, which allows changing the title of a floating window.
|
|
||||||
- Added `signal QsWindow.closed()`, fired when a window is closed externally.
|
|
||||||
- Added support for inline replies in notifications, when supported by applications.
|
|
||||||
- Added `DesktopEntry.startupWmClass` and `DesktopEntry.heuristicLookup()` to better identify toplevels.
|
|
||||||
- Added `DesktopEntry.command` which can be run as an alternative to `DesktopEntry.execute()`.
|
|
||||||
- Added `//@ pragma Internal`, which makes a QML component impossible to import outside of its module.
|
|
||||||
- Added dead instance selection for some subcommands, such as `qs log` and `qs list`.
|
|
||||||
|
|
||||||
## Other Changes
|
|
||||||
|
|
||||||
- `Quickshell.shellRoot` has been renamed to `Quickshell.shellDir`.
|
|
||||||
- PanelWindow margins opposite the window's anchorpoint are now added to exclusion zone.
|
|
||||||
- stdout/stderr or detached processes and executed desktop entries are now hidden by default.
|
|
||||||
- Various warnings caused by other applications Quickshell communicates with over D-BUS have been hidden in logs.
|
|
||||||
- Quickshell's new logo is now shown in any floating windows.
|
|
||||||
|
|
||||||
## Bug Fixes
|
|
||||||
|
|
||||||
- Fixed pipewire device volume and mute states not updating before the device has been used.
|
|
||||||
- Fixed a crash when changing the volume of any pipewire device on a sound card another removed device was using.
|
|
||||||
- Fixed a crash when accessing a removed previous default pipewire node from the default sink/source changed signals.
|
|
||||||
- Fixed session locks crashing if all monitors are disconnected.
|
|
||||||
- Fixed session locks crashing if unsupported by the compositor.
|
|
||||||
- Fixed a crash when creating a session lock and destroying it before acknowledged by the compositor.
|
|
||||||
- Fixed window input masks not updating after a reload.
|
|
||||||
- Fixed PanelWindows being unconfigurable unless `screen` was set under X11.
|
|
||||||
- Fixed a crash when anchoring a popup to a zero sized `Item`.
|
|
||||||
- Fixed `FileView` crashing if `watchChanges` was used.
|
|
||||||
- Fixed `SocketServer` sockets disappearing after a reload.
|
|
||||||
- Fixed `ScreencopyView` having incorrect rotation when displaying a rotated monitor.
|
|
||||||
- Fixed `MarginWrapperManager` breaking pixel alignment of child items when centering.
|
|
||||||
- Fixed `IpcHandler`, `NotificationServer` and `GlobalShortcut` not activating with certain QML structures.
|
|
||||||
- Fixed tracking of QML incubator destruction and deregistration, which occasionally caused crashes.
|
|
||||||
- Fixed FloatingWindows being constrained to the smallest window manager supported size unless max size was set.
|
|
||||||
- Fixed `MprisPlayer.lengthSupported` not updating reactively.
|
|
||||||
- Fixed normal tray icon being ignored when status is `NeedsAttention` and no attention icon is provided.
|
|
||||||
- Fixed `HyprlandWorkspace.activate()` sending invalid commands to Hyprland for named or special workspaces.
|
|
||||||
- Fixed file watcher occasionally breaking when using VSCode to edit QML files.
|
|
||||||
- Fixed crashes when screencopy buffer creation fails.
|
|
||||||
- Fixed a crash when wayland layer surfaces are recreated for the same window.
|
|
||||||
- Fixed the `QsWindow` attached object not working when using `WlrLayershell` directly.
|
|
||||||
- Fixed a crash when attempting to create a window without available VRAM.
|
|
||||||
- Fixed OOM crash when failing to write to detailed log file.
|
|
||||||
- Prevented distro logging configurations for Qt from interfering with Quickshell commands.
|
|
||||||
- Removed the "QProcess destroyed for running process" warning when destroying `Process` objects.
|
|
||||||
- Fixed `ColorQuantizer` printing a pointer to an error message instead of an error message.
|
|
||||||
- Fixed notification pixmap rowstride warning showing for correct rowstrides.
|
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
## New Features
|
|
||||||
|
|
||||||
- Changes to desktop entries are now tracked in real time.
|
|
||||||
|
|
||||||
## Other Changes
|
|
||||||
|
|
||||||
- Added support for Qt 6.10
|
|
||||||
|
|
||||||
## Bug Fixes
|
|
||||||
|
|
||||||
- Fixed volumes getting stuck on change for pipewire devices with few volume steps.
|
|
||||||
- Fixed a crash when running out of disk space to write log files.
|
|
||||||
- Fixed a rare crash when disconnecting a monitor.
|
|
||||||
- Fixed build issues preventing cross compilation from working.
|
|
||||||
- Fixed dekstop entries with lower priority than a hidden entry not being hidden.
|
|
||||||
- Fixed desktop entry keys with mismatched modifier or country not being discarded.
|
|
||||||
- Fixed greetd hanging when authenticating with a fingerprint.
|
|
||||||
|
|
@ -2,10 +2,7 @@
|
||||||
qtver,
|
qtver,
|
||||||
compiler,
|
compiler,
|
||||||
}: let
|
}: let
|
||||||
checkouts = import ./nix-checkouts.nix;
|
nixpkgs = (import ./nix-checkouts.nix).${builtins.replaceStrings ["."] ["_"] qtver};
|
||||||
nixpkgs = checkouts.${builtins.replaceStrings ["."] ["_"] qtver};
|
|
||||||
compilerOverride = (nixpkgs.callPackage ./variations.nix {}).${compiler};
|
compilerOverride = (nixpkgs.callPackage ./variations.nix {}).${compiler};
|
||||||
pkg = (nixpkgs.callPackage ../default.nix {}).override (compilerOverride // {
|
pkg = (nixpkgs.callPackage ../default.nix {}).override compilerOverride;
|
||||||
wayland-protocols = checkouts.latest.wayland-protocols;
|
|
||||||
});
|
|
||||||
in pkg
|
in pkg
|
||||||
|
|
|
||||||
|
|
@ -7,28 +7,9 @@ let
|
||||||
url = "https://github.com/nixos/nixpkgs/archive/${commit}.tar.gz";
|
url = "https://github.com/nixos/nixpkgs/archive/${commit}.tar.gz";
|
||||||
inherit sha256;
|
inherit sha256;
|
||||||
}) {};
|
}) {};
|
||||||
in rec {
|
in {
|
||||||
latest = qt6_10_0;
|
# For old qt versions, grab the commit before the version bump that has all the patches
|
||||||
|
# instead of the bumped version.
|
||||||
qt6_10_1 = byCommit {
|
|
||||||
commit = "ffbc9f8cbaacfb331b6017d5a5abb21a492c9a38";
|
|
||||||
sha256 = "0fvbizl7j5rv2rf8j76yw0xb3d9l06hahkjys2a7k1yraznvnafm";
|
|
||||||
};
|
|
||||||
|
|
||||||
qt6_10_0 = byCommit {
|
|
||||||
commit = "c5ae371f1a6a7fd27823bc500d9390b38c05fa55";
|
|
||||||
sha256 = "18g0f8cb9m8mxnz9cf48sks0hib79b282iajl2nysyszph993yp0";
|
|
||||||
};
|
|
||||||
|
|
||||||
qt6_9_2 = byCommit {
|
|
||||||
commit = "e9f00bd893984bc8ce46c895c3bf7cac95331127";
|
|
||||||
sha256 = "0s2mhbrgzxlgkg2yxb0q0hpk8lby1a7w67dxvfmaz4gsmc0bnvfj";
|
|
||||||
};
|
|
||||||
|
|
||||||
qt6_9_1 = byCommit {
|
|
||||||
commit = "4c202d26483c5ccf3cb95e0053163facde9f047e";
|
|
||||||
sha256 = "06l2w4bcgfw7dfanpzpjcf25ydf84in240yplqsss82qx405y9di";
|
|
||||||
};
|
|
||||||
|
|
||||||
qt6_9_0 = byCommit {
|
qt6_9_0 = byCommit {
|
||||||
commit = "546c545bd0594809a28ab7e869b5f80dd7243ef6";
|
commit = "546c545bd0594809a28ab7e869b5f80dd7243ef6";
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,6 @@
|
||||||
clangStdenv,
|
clangStdenv,
|
||||||
gccStdenv,
|
gccStdenv,
|
||||||
}: {
|
}: {
|
||||||
clang = { stdenv = clangStdenv; };
|
clang = { buildStdenv = clangStdenv; };
|
||||||
gcc = { stdenv = gccStdenv; };
|
gcc = { buildStdenv = gccStdenv; };
|
||||||
}
|
}
|
||||||
|
|
|
||||||
169
default.nix
169
default.nix
|
|
@ -2,31 +2,23 @@
|
||||||
lib,
|
lib,
|
||||||
nix-gitignore,
|
nix-gitignore,
|
||||||
pkgs,
|
pkgs,
|
||||||
stdenv,
|
|
||||||
keepDebugInfo,
|
keepDebugInfo,
|
||||||
|
buildStdenv ? pkgs.clangStdenv,
|
||||||
|
|
||||||
pkg-config,
|
|
||||||
cmake,
|
cmake,
|
||||||
ninja,
|
ninja,
|
||||||
spirv-tools,
|
|
||||||
qt6,
|
qt6,
|
||||||
cpptrace ? null,
|
spirv-tools,
|
||||||
libunwind,
|
|
||||||
libdwarf,
|
|
||||||
jemalloc,
|
|
||||||
cli11,
|
cli11,
|
||||||
|
breakpad,
|
||||||
|
jemalloc,
|
||||||
wayland,
|
wayland,
|
||||||
wayland-protocols,
|
wayland-protocols,
|
||||||
wayland-scanner,
|
|
||||||
xorg,
|
|
||||||
libxcb ? xorg.libxcb,
|
|
||||||
libdrm,
|
libdrm,
|
||||||
libgbm ? null,
|
libgbm ? null,
|
||||||
vulkan-headers,
|
xorg,
|
||||||
pipewire,
|
pipewire,
|
||||||
pam,
|
pam,
|
||||||
polkit,
|
|
||||||
glib,
|
|
||||||
|
|
||||||
gitRev ? (let
|
gitRev ? (let
|
||||||
headExists = builtins.pathExists ./.git/HEAD;
|
headExists = builtins.pathExists ./.git/HEAD;
|
||||||
|
|
@ -49,107 +41,64 @@
|
||||||
withPam ? true,
|
withPam ? true,
|
||||||
withHyprland ? true,
|
withHyprland ? true,
|
||||||
withI3 ? true,
|
withI3 ? true,
|
||||||
withPolkit ? true,
|
}: buildStdenv.mkDerivation {
|
||||||
withNetworkManager ? true,
|
pname = "quickshell${lib.optionalString debug "-debug"}";
|
||||||
}: let
|
version = "0.1.0";
|
||||||
withCrashHandler = withCrashReporter && cpptrace != null && lib.strings.compareVersions cpptrace.version "0.7.2" >= 0;
|
src = nix-gitignore.gitignoreSource "/docs\n/examples\n" ./.;
|
||||||
|
|
||||||
unwrapped = stdenv.mkDerivation {
|
nativeBuildInputs = with pkgs; [
|
||||||
pname = "quickshell${lib.optionalString debug "-debug"}";
|
cmake
|
||||||
version = "0.2.1";
|
ninja
|
||||||
src = nix-gitignore.gitignoreSource "/default.nix\n" ./.;
|
qt6.qtshadertools
|
||||||
|
spirv-tools
|
||||||
|
qt6.wrapQtAppsHook
|
||||||
|
pkg-config
|
||||||
|
] ++ (lib.optionals withWayland [
|
||||||
|
wayland-protocols
|
||||||
|
wayland-scanner
|
||||||
|
]);
|
||||||
|
|
||||||
dontWrapQtApps = true; # see wrappers
|
buildInputs = [
|
||||||
|
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;
|
||||||
|
|
||||||
nativeBuildInputs = [
|
cmakeBuildType = if debug then "Debug" else "RelWithDebInfo";
|
||||||
cmake
|
|
||||||
ninja
|
|
||||||
spirv-tools
|
|
||||||
pkg-config
|
|
||||||
]
|
|
||||||
++ lib.optional (withWayland && lib.strings.compareVersions qt6.qtbase.version "6.10.0" == -1) qt6.qtwayland
|
|
||||||
++ lib.optionals withWayland [
|
|
||||||
qt6.qtwayland # qtwaylandscanner required at build time
|
|
||||||
wayland-scanner
|
|
||||||
];
|
|
||||||
|
|
||||||
buildInputs = [
|
cmakeFlags = [
|
||||||
qt6.qtbase
|
(lib.cmakeFeature "DISTRIBUTOR" "Official-Nix-Flake")
|
||||||
qt6.qtdeclarative
|
(lib.cmakeFeature "INSTALL_QML_PREFIX" qt6.qtbase.qtQmlPrefix)
|
||||||
libdrm
|
(lib.cmakeBool "DISTRIBUTOR_DEBUGINFO_AVAILABLE" true)
|
||||||
cli11
|
(lib.cmakeFeature "GIT_REVISION" gitRev)
|
||||||
]
|
(lib.cmakeBool "CRASH_REPORTER" withCrashReporter)
|
||||||
++ lib.optional withQtSvg qt6.qtsvg
|
(lib.cmakeBool "USE_JEMALLOC" withJemalloc)
|
||||||
++ lib.optional withCrashHandler (cpptrace.overrideAttrs (prev: {
|
(lib.cmakeBool "WAYLAND" withWayland)
|
||||||
cmakeFlags = prev.cmakeFlags ++ [
|
(lib.cmakeBool "SCREENCOPY" (libgbm != null))
|
||||||
"-DCPPTRACE_UNWIND_WITH_LIBUNWIND=TRUE"
|
(lib.cmakeBool "SERVICE_PIPEWIRE" withPipewire)
|
||||||
];
|
(lib.cmakeBool "SERVICE_PAM" withPam)
|
||||||
buildInputs = prev.buildInputs ++ [ libunwind ];
|
(lib.cmakeBool "HYPRLAND" withHyprland)
|
||||||
}))
|
(lib.cmakeBool "I3" withI3)
|
||||||
++ lib.optional withJemalloc jemalloc
|
];
|
||||||
++ lib.optional (withWayland && lib.strings.compareVersions qt6.qtbase.version "6.10.0" == -1) qt6.qtwayland
|
|
||||||
++ lib.optionals withWayland [ wayland wayland-protocols ]
|
|
||||||
++ lib.optionals (withWayland && libgbm != null) [ libgbm vulkan-headers ]
|
|
||||||
++ lib.optional withX11 libxcb
|
|
||||||
++ lib.optional withPam pam
|
|
||||||
++ lib.optional withPipewire pipewire
|
|
||||||
++ lib.optionals withPolkit [ polkit glib ];
|
|
||||||
|
|
||||||
cmakeBuildType = if debug then "Debug" else "RelWithDebInfo";
|
# 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;
|
||||||
|
|
||||||
cmakeFlags = [
|
meta = with lib; {
|
||||||
(lib.cmakeFeature "DISTRIBUTOR" "Official-Nix-Flake")
|
homepage = "https://git.outfoxxed.me/outfoxxed/quickshell";
|
||||||
(lib.cmakeFeature "INSTALL_QML_PREFIX" qt6.qtbase.qtQmlPrefix)
|
description = "Flexbile QtQuick based desktop shell toolkit";
|
||||||
(lib.cmakeBool "DISTRIBUTOR_DEBUGINFO_AVAILABLE" true)
|
license = licenses.lgpl3Only;
|
||||||
(lib.cmakeFeature "GIT_REVISION" gitRev)
|
platforms = platforms.linux;
|
||||||
(lib.cmakeBool "CRASH_HANDLER" withCrashHandler)
|
|
||||||
(lib.cmakeBool "USE_JEMALLOC" withJemalloc)
|
|
||||||
(lib.cmakeBool "WAYLAND" withWayland)
|
|
||||||
(lib.cmakeBool "SCREENCOPY" (libgbm != null))
|
|
||||||
(lib.cmakeBool "SERVICE_PIPEWIRE" withPipewire)
|
|
||||||
(lib.cmakeBool "SERVICE_PAM" withPam)
|
|
||||||
(lib.cmakeBool "SERVICE_NETWORKMANAGER" withNetworkManager)
|
|
||||||
(lib.cmakeBool "SERVICE_POLKIT" withPolkit)
|
|
||||||
(lib.cmakeBool "HYPRLAND" withHyprland)
|
|
||||||
(lib.cmakeBool "I3" withI3)
|
|
||||||
];
|
|
||||||
|
|
||||||
# How to get debuginfo in gdb from a release build:
|
|
||||||
# 1. build `quickshell.debug`
|
|
||||||
# 2. set NIX_DEBUG_INFO_DIRS="<quickshell.debug store path>/lib/debug"
|
|
||||||
# 3. launch gdb / coredumpctl and debuginfo will work
|
|
||||||
separateDebugInfo = !debug;
|
|
||||||
dontStrip = debug;
|
|
||||||
|
|
||||||
meta = with lib; {
|
|
||||||
homepage = "https://quickshell.org";
|
|
||||||
description = "Flexbile QtQuick based desktop shell toolkit";
|
|
||||||
license = licenses.lgpl3Only;
|
|
||||||
platforms = platforms.linux;
|
|
||||||
mainProgram = "quickshell";
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
}
|
||||||
wrapper = unwrapped.stdenv.mkDerivation {
|
|
||||||
inherit (unwrapped) version meta buildInputs;
|
|
||||||
pname = "${unwrapped.pname}-wrapped";
|
|
||||||
|
|
||||||
nativeBuildInputs = unwrapped.nativeBuildInputs ++ [ qt6.wrapQtAppsHook ];
|
|
||||||
|
|
||||||
dontUnpack = true;
|
|
||||||
dontConfigure = true;
|
|
||||||
dontBuild = true;
|
|
||||||
|
|
||||||
installPhase = ''
|
|
||||||
mkdir -p $out
|
|
||||||
cp -r ${unwrapped}/* $out
|
|
||||||
'';
|
|
||||||
|
|
||||||
passthru = {
|
|
||||||
unwrapped = unwrapped;
|
|
||||||
withModules = modules: wrapper.overrideAttrs (prev: {
|
|
||||||
buildInputs = prev.buildInputs ++ modules;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
};
|
|
||||||
in wrapper
|
|
||||||
|
|
|
||||||
6
flake.lock
generated
6
flake.lock
generated
|
|
@ -2,11 +2,11 @@
|
||||||
"nodes": {
|
"nodes": {
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1768127708,
|
"lastModified": 1736012469,
|
||||||
"narHash": "sha256-1Sm77VfZh3mU0F5OqKABNLWxOuDeHIlcFjsXeeiPazs=",
|
"narHash": "sha256-/qlNWm/IEVVH7GfgAIyP6EsVZI6zjAx1cV5zNyrs+rI=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "ffbc9f8cbaacfb331b6017d5a5abb21a492c9a38",
|
"rev": "8f3e1f807051e32d8c95cd12b9b421623850a34d",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|
|
||||||
22
flake.nix
22
flake.nix
|
|
@ -4,28 +4,22 @@
|
||||||
};
|
};
|
||||||
|
|
||||||
outputs = { self, nixpkgs }: let
|
outputs = { self, nixpkgs }: let
|
||||||
overlayPkgs = p: p.appendOverlays [ self.overlays.default ];
|
forEachSystem = fn: nixpkgs.lib.genAttrs
|
||||||
|
[ "x86_64-linux" "aarch64-linux" ]
|
||||||
forEachSystem = fn:
|
(system: fn system nixpkgs.legacyPackages.${system});
|
||||||
nixpkgs.lib.genAttrs
|
|
||||||
nixpkgs.lib.platforms.linux
|
|
||||||
(system: fn system (overlayPkgs nixpkgs.legacyPackages.${system}));
|
|
||||||
in {
|
in {
|
||||||
overlays.default = import ./overlay.nix {
|
|
||||||
rev = self.rev or self.dirtyRev;
|
|
||||||
};
|
|
||||||
|
|
||||||
packages = forEachSystem (system: pkgs: rec {
|
packages = forEachSystem (system: pkgs: rec {
|
||||||
quickshell = pkgs.quickshell;
|
quickshell = pkgs.callPackage ./default.nix {
|
||||||
|
gitRev = self.rev or self.dirtyRev;
|
||||||
|
};
|
||||||
|
|
||||||
default = quickshell;
|
default = quickshell;
|
||||||
});
|
});
|
||||||
|
|
||||||
devShells = forEachSystem (system: pkgs: rec {
|
devShells = forEachSystem (system: pkgs: rec {
|
||||||
default = import ./shell.nix {
|
default = import ./shell.nix {
|
||||||
inherit pkgs;
|
inherit pkgs;
|
||||||
quickshell = self.packages.${system}.quickshell.override {
|
inherit (self.packages.${system}) quickshell;
|
||||||
stdenv = pkgs.clangStdenv;
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
{ rev ? null }: (final: prev: {
|
|
||||||
quickshell = final.callPackage ./default.nix {
|
|
||||||
gitRev = rev;
|
|
||||||
};
|
|
||||||
})
|
|
||||||
|
|
@ -35,14 +35,13 @@
|
||||||
pkg-config
|
pkg-config
|
||||||
qtshadertools
|
qtshadertools
|
||||||
spirv-tools
|
spirv-tools
|
||||||
wayland-protocols
|
wayland-protocols))
|
||||||
cli11))
|
(inputs (list cli11
|
||||||
(inputs (list jemalloc
|
jemalloc
|
||||||
libdrm
|
libdrm
|
||||||
libxcb
|
libxcb
|
||||||
libxkbcommon
|
libxkbcommon
|
||||||
linux-pam
|
linux-pam
|
||||||
polkit
|
|
||||||
mesa
|
mesa
|
||||||
pipewire
|
pipewire
|
||||||
qtbase
|
qtbase
|
||||||
|
|
@ -56,7 +55,8 @@
|
||||||
#~(list "-GNinja"
|
#~(list "-GNinja"
|
||||||
"-DDISTRIBUTOR=\"In-tree Guix channel\""
|
"-DDISTRIBUTOR=\"In-tree Guix channel\""
|
||||||
"-DDISTRIBUTOR_DEBUGINFO_AVAILABLE=NO"
|
"-DDISTRIBUTOR_DEBUGINFO_AVAILABLE=NO"
|
||||||
"-DCRASH_HANDLER=OFF")
|
;; Breakpad is not currently packaged for Guix.
|
||||||
|
"-DCRASH_REPORTER=OFF")
|
||||||
#:phases
|
#:phases
|
||||||
#~(modify-phases %standard-phases
|
#~(modify-phases %standard-phases
|
||||||
(replace 'build (lambda _ (invoke "cmake" "--build" ".")))
|
(replace 'build (lambda _ (invoke "cmake" "--build" ".")))
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,14 @@
|
||||||
{
|
{
|
||||||
pkgs ? import <nixpkgs> {},
|
pkgs ? import <nixpkgs> {},
|
||||||
stdenv ? pkgs.clangStdenv, # faster compiles than gcc
|
quickshell ? pkgs.callPackage ./default.nix {},
|
||||||
quickshell ? pkgs.callPackage ./default.nix { inherit stdenv; },
|
|
||||||
...
|
...
|
||||||
}: let
|
}: let
|
||||||
tidyfox = import (pkgs.fetchFromGitea {
|
tidyfox = import (pkgs.fetchFromGitea {
|
||||||
domain = "git.outfoxxed.me";
|
domain = "git.outfoxxed.me";
|
||||||
owner = "outfoxxed";
|
owner = "outfoxxed";
|
||||||
repo = "tidyfox";
|
repo = "tidyfox";
|
||||||
rev = "9d85d7e7dea2602aa74ec3168955fee69967e92f";
|
rev = "1f062cc198d1112d13e5128fa1f2ee3dbffe613b";
|
||||||
hash = "sha256-77ERiweF6lumonp2c/124rAoVG6/o9J+Aajhttwtu0w=";
|
sha256 = "kbt0Zc1qHE5fhqBkKz8iue+B+ZANjF1AR/RdgmX1r0I=";
|
||||||
}) { inherit pkgs; };
|
}) { inherit pkgs; };
|
||||||
in pkgs.mkShell.override { stdenv = quickshell.stdenv; } {
|
in pkgs.mkShell.override { stdenv = quickshell.stdenv; } {
|
||||||
inputsFrom = [ quickshell ];
|
inputsFrom = [ quickshell ];
|
||||||
|
|
|
||||||
|
|
@ -11,9 +11,8 @@ add_subdirectory(window)
|
||||||
add_subdirectory(io)
|
add_subdirectory(io)
|
||||||
add_subdirectory(widgets)
|
add_subdirectory(widgets)
|
||||||
add_subdirectory(ui)
|
add_subdirectory(ui)
|
||||||
add_subdirectory(windowmanager)
|
|
||||||
|
|
||||||
if (CRASH_HANDLER)
|
if (CRASH_REPORTER)
|
||||||
add_subdirectory(crash)
|
add_subdirectory(crash)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
|
@ -30,11 +29,3 @@ if (X11)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
add_subdirectory(services)
|
add_subdirectory(services)
|
||||||
|
|
||||||
if (BLUETOOTH)
|
|
||||||
add_subdirectory(bluetooth)
|
|
||||||
endif()
|
|
||||||
|
|
||||||
if (NETWORK)
|
|
||||||
add_subdirectory(network)
|
|
||||||
endif()
|
|
||||||
|
|
|
||||||
|
|
@ -1,42 +0,0 @@
|
||||||
set_source_files_properties(org.bluez.Adapter.xml PROPERTIES
|
|
||||||
CLASSNAME DBusBluezAdapterInterface
|
|
||||||
)
|
|
||||||
|
|
||||||
set_source_files_properties(org.bluez.Device.xml PROPERTIES
|
|
||||||
CLASSNAME DBusBluezDeviceInterface
|
|
||||||
)
|
|
||||||
|
|
||||||
qt_add_dbus_interface(DBUS_INTERFACES
|
|
||||||
org.bluez.Adapter.xml
|
|
||||||
dbus_adapter
|
|
||||||
)
|
|
||||||
|
|
||||||
qt_add_dbus_interface(DBUS_INTERFACES
|
|
||||||
org.bluez.Device.xml
|
|
||||||
dbus_device
|
|
||||||
)
|
|
||||||
|
|
||||||
qt_add_library(quickshell-bluetooth STATIC
|
|
||||||
adapter.cpp
|
|
||||||
bluez.cpp
|
|
||||||
device.cpp
|
|
||||||
${DBUS_INTERFACES}
|
|
||||||
)
|
|
||||||
|
|
||||||
qt_add_qml_module(quickshell-bluetooth
|
|
||||||
URI Quickshell.Bluetooth
|
|
||||||
VERSION 0.1
|
|
||||||
DEPENDENCIES QtQml
|
|
||||||
)
|
|
||||||
|
|
||||||
install_qml_module(quickshell-bluetooth)
|
|
||||||
|
|
||||||
# dbus headers
|
|
||||||
target_include_directories(quickshell-bluetooth PRIVATE ${CMAKE_CURRENT_BINARY_DIR})
|
|
||||||
|
|
||||||
target_link_libraries(quickshell-bluetooth PRIVATE Qt::Qml Qt::DBus)
|
|
||||||
qs_add_link_dependencies(quickshell-bluetooth quickshell-dbus)
|
|
||||||
|
|
||||||
qs_module_pch(quickshell-bluetooth SET dbus)
|
|
||||||
|
|
||||||
target_link_libraries(quickshell PRIVATE quickshell-bluetoothplugin)
|
|
||||||
|
|
@ -1,223 +0,0 @@
|
||||||
#include "adapter.hpp"
|
|
||||||
|
|
||||||
#include <qcontainerfwd.h>
|
|
||||||
#include <qdbusconnection.h>
|
|
||||||
#include <qdbusextratypes.h>
|
|
||||||
#include <qdbuspendingcall.h>
|
|
||||||
#include <qdbuspendingreply.h>
|
|
||||||
#include <qdebug.h>
|
|
||||||
#include <qlogging.h>
|
|
||||||
#include <qloggingcategory.h>
|
|
||||||
#include <qstring.h>
|
|
||||||
#include <qtypes.h>
|
|
||||||
|
|
||||||
#include "../core/logcat.hpp"
|
|
||||||
#include "../dbus/properties.hpp"
|
|
||||||
#include "dbus_adapter.h"
|
|
||||||
|
|
||||||
namespace qs::bluetooth {
|
|
||||||
|
|
||||||
namespace {
|
|
||||||
QS_LOGGING_CATEGORY(logAdapter, "quickshell.bluetooth.adapter", QtWarningMsg);
|
|
||||||
}
|
|
||||||
|
|
||||||
QString BluetoothAdapterState::toString(BluetoothAdapterState::Enum state) {
|
|
||||||
switch (state) {
|
|
||||||
case BluetoothAdapterState::Disabled: return QStringLiteral("Disabled");
|
|
||||||
case BluetoothAdapterState::Enabled: return QStringLiteral("Enabled");
|
|
||||||
case BluetoothAdapterState::Enabling: return QStringLiteral("Enabling");
|
|
||||||
case BluetoothAdapterState::Disabling: return QStringLiteral("Disabling");
|
|
||||||
case BluetoothAdapterState::Blocked: return QStringLiteral("Blocked");
|
|
||||||
default: return QStringLiteral("Unknown");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
BluetoothAdapter::BluetoothAdapter(const QString& path, QObject* parent): QObject(parent) {
|
|
||||||
this->mInterface =
|
|
||||||
new DBusBluezAdapterInterface("org.bluez", path, QDBusConnection::systemBus(), this);
|
|
||||||
|
|
||||||
if (!this->mInterface->isValid()) {
|
|
||||||
qCWarning(logAdapter) << "Could not create DBus interface for adapter at" << path;
|
|
||||||
this->mInterface = nullptr;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this->properties.setInterface(this->mInterface);
|
|
||||||
}
|
|
||||||
|
|
||||||
QString BluetoothAdapter::adapterId() const {
|
|
||||||
auto path = this->path();
|
|
||||||
return path.sliced(path.lastIndexOf('/') + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
void BluetoothAdapter::setEnabled(bool enabled) {
|
|
||||||
if (enabled == this->bEnabled) return;
|
|
||||||
|
|
||||||
if (enabled && this->bState == BluetoothAdapterState::Blocked) {
|
|
||||||
qCCritical(logAdapter) << "Cannot enable adapter because it is blocked by rfkill.";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this->bEnabled = enabled;
|
|
||||||
this->pEnabled.write();
|
|
||||||
}
|
|
||||||
|
|
||||||
void BluetoothAdapter::setDiscoverable(bool discoverable) {
|
|
||||||
if (discoverable == this->bDiscoverable) return;
|
|
||||||
this->bDiscoverable = discoverable;
|
|
||||||
this->pDiscoverable.write();
|
|
||||||
}
|
|
||||||
|
|
||||||
void BluetoothAdapter::setDiscovering(bool discovering) {
|
|
||||||
if (discovering) {
|
|
||||||
this->startDiscovery();
|
|
||||||
} else {
|
|
||||||
this->stopDiscovery();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void BluetoothAdapter::setDiscoverableTimeout(quint32 timeout) {
|
|
||||||
if (timeout == this->bDiscoverableTimeout) return;
|
|
||||||
this->bDiscoverableTimeout = timeout;
|
|
||||||
this->pDiscoverableTimeout.write();
|
|
||||||
}
|
|
||||||
|
|
||||||
void BluetoothAdapter::setPairable(bool pairable) {
|
|
||||||
if (pairable == this->bPairable) return;
|
|
||||||
this->bPairable = pairable;
|
|
||||||
this->pPairable.write();
|
|
||||||
}
|
|
||||||
|
|
||||||
void BluetoothAdapter::setPairableTimeout(quint32 timeout) {
|
|
||||||
if (timeout == this->bPairableTimeout) return;
|
|
||||||
this->bPairableTimeout = timeout;
|
|
||||||
this->pPairableTimeout.write();
|
|
||||||
}
|
|
||||||
|
|
||||||
void BluetoothAdapter::addInterface(const QString& interface, const QVariantMap& properties) {
|
|
||||||
if (interface == "org.bluez.Adapter1") {
|
|
||||||
this->properties.updatePropertySet(properties, false);
|
|
||||||
qCDebug(logAdapter) << "Updated Adapter properties for" << this;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void BluetoothAdapter::removeDevice(const QString& devicePath) {
|
|
||||||
qCDebug(logAdapter) << "Removing device" << devicePath << "from adapter" << this;
|
|
||||||
|
|
||||||
auto reply = this->mInterface->RemoveDevice(QDBusObjectPath(devicePath));
|
|
||||||
|
|
||||||
auto* watcher = new QDBusPendingCallWatcher(reply, this);
|
|
||||||
|
|
||||||
QObject::connect(
|
|
||||||
watcher,
|
|
||||||
&QDBusPendingCallWatcher::finished,
|
|
||||||
this,
|
|
||||||
[this, devicePath](QDBusPendingCallWatcher* watcher) {
|
|
||||||
const QDBusPendingReply<> reply = *watcher;
|
|
||||||
|
|
||||||
if (reply.isError()) {
|
|
||||||
qCWarning(logAdapter).nospace()
|
|
||||||
<< "Failed to remove device " << devicePath << " from adapter" << this << ": "
|
|
||||||
<< reply.error().message();
|
|
||||||
} else {
|
|
||||||
qCDebug(logAdapter) << "Successfully removed device" << devicePath << "from adapter"
|
|
||||||
<< this;
|
|
||||||
}
|
|
||||||
|
|
||||||
delete watcher;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void BluetoothAdapter::startDiscovery() {
|
|
||||||
if (this->bDiscovering) return;
|
|
||||||
qCDebug(logAdapter) << "Starting discovery for adapter" << this;
|
|
||||||
|
|
||||||
auto reply = this->mInterface->StartDiscovery();
|
|
||||||
auto* watcher = new QDBusPendingCallWatcher(reply, this);
|
|
||||||
|
|
||||||
QObject::connect(
|
|
||||||
watcher,
|
|
||||||
&QDBusPendingCallWatcher::finished,
|
|
||||||
this,
|
|
||||||
[this](QDBusPendingCallWatcher* watcher) {
|
|
||||||
const QDBusPendingReply<> reply = *watcher;
|
|
||||||
|
|
||||||
if (reply.isError()) {
|
|
||||||
qCWarning(logAdapter).nospace()
|
|
||||||
<< "Failed to start discovery on adapter" << this << ": " << reply.error().message();
|
|
||||||
} else {
|
|
||||||
qCDebug(logAdapter) << "Successfully started discovery on adapter" << this;
|
|
||||||
}
|
|
||||||
|
|
||||||
delete watcher;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void BluetoothAdapter::stopDiscovery() {
|
|
||||||
if (!this->bDiscovering) return;
|
|
||||||
qCDebug(logAdapter) << "Stopping discovery for adapter" << this;
|
|
||||||
|
|
||||||
auto reply = this->mInterface->StopDiscovery();
|
|
||||||
auto* watcher = new QDBusPendingCallWatcher(reply, this);
|
|
||||||
|
|
||||||
QObject::connect(
|
|
||||||
watcher,
|
|
||||||
&QDBusPendingCallWatcher::finished,
|
|
||||||
this,
|
|
||||||
[this](QDBusPendingCallWatcher* watcher) {
|
|
||||||
const QDBusPendingReply<> reply = *watcher;
|
|
||||||
|
|
||||||
if (reply.isError()) {
|
|
||||||
qCWarning(logAdapter).nospace()
|
|
||||||
<< "Failed to stop discovery on adapter " << this << ": " << reply.error().message();
|
|
||||||
} else {
|
|
||||||
qCDebug(logAdapter) << "Successfully stopped discovery on adapter" << this;
|
|
||||||
}
|
|
||||||
|
|
||||||
delete watcher;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace qs::bluetooth
|
|
||||||
|
|
||||||
namespace qs::dbus {
|
|
||||||
|
|
||||||
using namespace qs::bluetooth;
|
|
||||||
|
|
||||||
DBusResult<BluetoothAdapterState::Enum>
|
|
||||||
DBusDataTransform<BluetoothAdapterState::Enum>::fromWire(const Wire& wire) {
|
|
||||||
if (wire == QStringLiteral("off")) {
|
|
||||||
return BluetoothAdapterState::Disabled;
|
|
||||||
} else if (wire == QStringLiteral("on")) {
|
|
||||||
return BluetoothAdapterState::Enabled;
|
|
||||||
} else if (wire == QStringLiteral("off-enabling")) {
|
|
||||||
return BluetoothAdapterState::Enabling;
|
|
||||||
} else if (wire == QStringLiteral("on-disabling")) {
|
|
||||||
return BluetoothAdapterState::Disabling;
|
|
||||||
} else if (wire == QStringLiteral("off-blocked")) {
|
|
||||||
return BluetoothAdapterState::Blocked;
|
|
||||||
} else {
|
|
||||||
return QDBusError(
|
|
||||||
QDBusError::InvalidArgs,
|
|
||||||
QString("Invalid BluetoothAdapterState: %1").arg(wire)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace qs::dbus
|
|
||||||
|
|
||||||
QDebug operator<<(QDebug debug, const qs::bluetooth::BluetoothAdapter* adapter) {
|
|
||||||
auto saver = QDebugStateSaver(debug);
|
|
||||||
|
|
||||||
if (adapter) {
|
|
||||||
debug.nospace() << "BluetoothAdapter(" << static_cast<const void*>(adapter)
|
|
||||||
<< ", path=" << adapter->path() << ")";
|
|
||||||
} else {
|
|
||||||
debug << "BluetoothAdapter(nullptr)";
|
|
||||||
}
|
|
||||||
|
|
||||||
return debug;
|
|
||||||
}
|
|
||||||
|
|
@ -1,173 +0,0 @@
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include <qobject.h>
|
|
||||||
#include <qqmlintegration.h>
|
|
||||||
#include <qtmetamacros.h>
|
|
||||||
|
|
||||||
#include "../core/doc.hpp"
|
|
||||||
#include "../core/model.hpp"
|
|
||||||
#include "../dbus/properties.hpp"
|
|
||||||
#include "dbus_adapter.h"
|
|
||||||
|
|
||||||
namespace qs::bluetooth {
|
|
||||||
|
|
||||||
///! Power state of a Bluetooth adapter.
|
|
||||||
class BluetoothAdapterState: public QObject {
|
|
||||||
Q_OBJECT;
|
|
||||||
QML_ELEMENT;
|
|
||||||
QML_SINGLETON;
|
|
||||||
|
|
||||||
public:
|
|
||||||
enum Enum : quint8 {
|
|
||||||
/// The adapter is powered off.
|
|
||||||
Disabled = 0,
|
|
||||||
/// The adapter is powered on.
|
|
||||||
Enabled = 1,
|
|
||||||
/// The adapter is transitioning from off to on.
|
|
||||||
Enabling = 2,
|
|
||||||
/// The adapter is transitioning from on to off.
|
|
||||||
Disabling = 3,
|
|
||||||
/// The adapter is blocked by rfkill.
|
|
||||||
Blocked = 4,
|
|
||||||
};
|
|
||||||
Q_ENUM(Enum);
|
|
||||||
|
|
||||||
Q_INVOKABLE static QString toString(BluetoothAdapterState::Enum state);
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace qs::bluetooth
|
|
||||||
|
|
||||||
namespace qs::dbus {
|
|
||||||
|
|
||||||
template <>
|
|
||||||
struct DBusDataTransform<qs::bluetooth::BluetoothAdapterState::Enum> {
|
|
||||||
using Wire = QString;
|
|
||||||
using Data = qs::bluetooth::BluetoothAdapterState::Enum;
|
|
||||||
static DBusResult<Data> fromWire(const Wire& wire);
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace qs::dbus
|
|
||||||
|
|
||||||
namespace qs::bluetooth {
|
|
||||||
|
|
||||||
class BluetoothAdapter;
|
|
||||||
class BluetoothDevice;
|
|
||||||
|
|
||||||
///! A Bluetooth adapter
|
|
||||||
class BluetoothAdapter: public QObject {
|
|
||||||
Q_OBJECT;
|
|
||||||
QML_ELEMENT;
|
|
||||||
QML_UNCREATABLE("");
|
|
||||||
// clang-format off
|
|
||||||
/// System provided name of the adapter. See @@adapterId for the internal identifier.
|
|
||||||
Q_PROPERTY(QString name READ default NOTIFY nameChanged BINDABLE bindableName);
|
|
||||||
/// True if the adapter is currently enabled. More detailed state is available from @@state.
|
|
||||||
Q_PROPERTY(bool enabled READ enabled WRITE setEnabled NOTIFY enabledChanged);
|
|
||||||
/// Detailed power state of the adapter.
|
|
||||||
Q_PROPERTY(BluetoothAdapterState::Enum state READ default NOTIFY stateChanged BINDABLE bindableState);
|
|
||||||
/// True if the adapter can be discovered by other bluetooth devices.
|
|
||||||
Q_PROPERTY(bool discoverable READ discoverable WRITE setDiscoverable NOTIFY discoverableChanged);
|
|
||||||
/// Timeout in seconds for how long the adapter stays discoverable after @@discoverable is set to true.
|
|
||||||
/// A value of 0 means the adapter stays discoverable forever.
|
|
||||||
Q_PROPERTY(quint32 discoverableTimeout READ discoverableTimeout WRITE setDiscoverableTimeout NOTIFY discoverableTimeoutChanged);
|
|
||||||
/// True if the adapter is scanning for new devices.
|
|
||||||
Q_PROPERTY(bool discovering READ discovering WRITE setDiscovering NOTIFY discoveringChanged);
|
|
||||||
/// True if the adapter is accepting incoming pairing requests.
|
|
||||||
///
|
|
||||||
/// This only affects incoming pairing requests and should typically only be changed
|
|
||||||
/// by system settings applications. Defaults to true.
|
|
||||||
Q_PROPERTY(bool pairable READ pairable WRITE setPairable NOTIFY pairableChanged);
|
|
||||||
/// Timeout in seconds for how long the adapter stays pairable after @@pairable is set to true.
|
|
||||||
/// A value of 0 means the adapter stays pairable forever. Defaults to 0.
|
|
||||||
Q_PROPERTY(quint32 pairableTimeout READ pairableTimeout WRITE setPairableTimeout NOTIFY pairableTimeoutChanged);
|
|
||||||
/// Bluetooth devices connected to this adapter.
|
|
||||||
QSDOC_TYPE_OVERRIDE(ObjectModel<qs::bluetooth::BluetoothDevice>*);
|
|
||||||
Q_PROPERTY(UntypedObjectModel* devices READ devices CONSTANT);
|
|
||||||
/// The internal ID of the adapter (e.g., "hci0").
|
|
||||||
Q_PROPERTY(QString adapterId READ adapterId CONSTANT);
|
|
||||||
/// DBus path of the adapter under the `org.bluez` system service.
|
|
||||||
Q_PROPERTY(QString dbusPath READ path CONSTANT);
|
|
||||||
// clang-format on
|
|
||||||
|
|
||||||
public:
|
|
||||||
explicit BluetoothAdapter(const QString& path, QObject* parent = nullptr);
|
|
||||||
|
|
||||||
[[nodiscard]] bool isValid() const { return this->mInterface->isValid(); }
|
|
||||||
[[nodiscard]] QString path() const { return this->mInterface->path(); }
|
|
||||||
[[nodiscard]] QString adapterId() const;
|
|
||||||
|
|
||||||
[[nodiscard]] bool enabled() const { return this->bEnabled; }
|
|
||||||
void setEnabled(bool enabled);
|
|
||||||
|
|
||||||
[[nodiscard]] bool discoverable() const { return this->bDiscoverable; }
|
|
||||||
void setDiscoverable(bool discoverable);
|
|
||||||
|
|
||||||
[[nodiscard]] bool discovering() const { return this->bDiscovering; }
|
|
||||||
void setDiscovering(bool discovering);
|
|
||||||
|
|
||||||
[[nodiscard]] quint32 discoverableTimeout() const { return this->bDiscoverableTimeout; }
|
|
||||||
void setDiscoverableTimeout(quint32 timeout);
|
|
||||||
|
|
||||||
[[nodiscard]] bool pairable() const { return this->bPairable; }
|
|
||||||
void setPairable(bool pairable);
|
|
||||||
|
|
||||||
[[nodiscard]] quint32 pairableTimeout() const { return this->bPairableTimeout; }
|
|
||||||
void setPairableTimeout(quint32 timeout);
|
|
||||||
|
|
||||||
[[nodiscard]] QBindable<QString> bindableName() { return &this->bName; }
|
|
||||||
[[nodiscard]] QBindable<bool> bindableEnabled() { return &this->bEnabled; }
|
|
||||||
[[nodiscard]] QBindable<BluetoothAdapterState::Enum> bindableState() { return &this->bState; }
|
|
||||||
[[nodiscard]] QBindable<bool> bindableDiscoverable() { return &this->bDiscoverable; }
|
|
||||||
[[nodiscard]] QBindable<quint32> bindableDiscoverableTimeout() {
|
|
||||||
return &this->bDiscoverableTimeout;
|
|
||||||
}
|
|
||||||
[[nodiscard]] QBindable<bool> bindableDiscovering() { return &this->bDiscovering; }
|
|
||||||
[[nodiscard]] QBindable<bool> bindablePairable() { return &this->bPairable; }
|
|
||||||
[[nodiscard]] QBindable<quint32> bindablePairableTimeout() { return &this->bPairableTimeout; }
|
|
||||||
[[nodiscard]] ObjectModel<BluetoothDevice>* devices() { return &this->mDevices; }
|
|
||||||
|
|
||||||
void addInterface(const QString& interface, const QVariantMap& properties);
|
|
||||||
void removeDevice(const QString& devicePath);
|
|
||||||
|
|
||||||
void startDiscovery();
|
|
||||||
void stopDiscovery();
|
|
||||||
|
|
||||||
signals:
|
|
||||||
void nameChanged();
|
|
||||||
void enabledChanged();
|
|
||||||
void stateChanged();
|
|
||||||
void discoverableChanged();
|
|
||||||
void discoverableTimeoutChanged();
|
|
||||||
void discoveringChanged();
|
|
||||||
void pairableChanged();
|
|
||||||
void pairableTimeoutChanged();
|
|
||||||
|
|
||||||
private:
|
|
||||||
DBusBluezAdapterInterface* mInterface = nullptr;
|
|
||||||
ObjectModel<BluetoothDevice> mDevices {this};
|
|
||||||
|
|
||||||
// clang-format off
|
|
||||||
Q_OBJECT_BINDABLE_PROPERTY(BluetoothAdapter, QString, bName, &BluetoothAdapter::nameChanged);
|
|
||||||
Q_OBJECT_BINDABLE_PROPERTY(BluetoothAdapter, bool, bEnabled, &BluetoothAdapter::enabledChanged);
|
|
||||||
Q_OBJECT_BINDABLE_PROPERTY(BluetoothAdapter, BluetoothAdapterState::Enum, bState, &BluetoothAdapter::stateChanged);
|
|
||||||
Q_OBJECT_BINDABLE_PROPERTY(BluetoothAdapter, bool, bDiscoverable, &BluetoothAdapter::discoverableChanged);
|
|
||||||
Q_OBJECT_BINDABLE_PROPERTY(BluetoothAdapter, quint32, bDiscoverableTimeout, &BluetoothAdapter::discoverableTimeoutChanged);
|
|
||||||
Q_OBJECT_BINDABLE_PROPERTY(BluetoothAdapter, bool, bDiscovering, &BluetoothAdapter::discoveringChanged);
|
|
||||||
Q_OBJECT_BINDABLE_PROPERTY(BluetoothAdapter, bool, bPairable, &BluetoothAdapter::pairableChanged);
|
|
||||||
Q_OBJECT_BINDABLE_PROPERTY(BluetoothAdapter, quint32, bPairableTimeout, &BluetoothAdapter::pairableTimeoutChanged);
|
|
||||||
|
|
||||||
QS_DBUS_BINDABLE_PROPERTY_GROUP(BluetoothAdapter, properties);
|
|
||||||
QS_DBUS_PROPERTY_BINDING(BluetoothAdapter, pName, bName, properties, "Alias");
|
|
||||||
QS_DBUS_PROPERTY_BINDING(BluetoothAdapter, pEnabled, bEnabled, properties, "Powered");
|
|
||||||
QS_DBUS_PROPERTY_BINDING(BluetoothAdapter, pState, bState, properties, "PowerState");
|
|
||||||
QS_DBUS_PROPERTY_BINDING(BluetoothAdapter, pDiscoverable, bDiscoverable, properties, "Discoverable");
|
|
||||||
QS_DBUS_PROPERTY_BINDING(BluetoothAdapter, pDiscoverableTimeout, bDiscoverableTimeout, properties, "DiscoverableTimeout");
|
|
||||||
QS_DBUS_PROPERTY_BINDING(BluetoothAdapter, pDiscovering, bDiscovering, properties, "Discovering");
|
|
||||||
QS_DBUS_PROPERTY_BINDING(BluetoothAdapter, pPairable, bPairable, properties, "Pairable");
|
|
||||||
QS_DBUS_PROPERTY_BINDING(BluetoothAdapter, pPairableTimeout, bPairableTimeout, properties, "PairableTimeout");
|
|
||||||
// clang-format on
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace qs::bluetooth
|
|
||||||
|
|
||||||
QDebug operator<<(QDebug debug, const qs::bluetooth::BluetoothAdapter* adapter);
|
|
||||||
|
|
@ -1,168 +0,0 @@
|
||||||
#include "bluez.hpp"
|
|
||||||
|
|
||||||
#include <qcontainerfwd.h>
|
|
||||||
#include <qdbusconnection.h>
|
|
||||||
#include <qdbusextratypes.h>
|
|
||||||
#include <qlogging.h>
|
|
||||||
#include <qloggingcategory.h>
|
|
||||||
#include <qobject.h>
|
|
||||||
#include <qtmetamacros.h>
|
|
||||||
|
|
||||||
#include "../core/logcat.hpp"
|
|
||||||
#include "../dbus/dbus_objectmanager_types.hpp"
|
|
||||||
#include "../dbus/objectmanager.hpp"
|
|
||||||
#include "adapter.hpp"
|
|
||||||
#include "device.hpp"
|
|
||||||
|
|
||||||
namespace qs::bluetooth {
|
|
||||||
|
|
||||||
namespace {
|
|
||||||
QS_LOGGING_CATEGORY(logBluetooth, "quickshell.bluetooth", QtWarningMsg);
|
|
||||||
}
|
|
||||||
|
|
||||||
Bluez* Bluez::instance() {
|
|
||||||
static auto* instance = new Bluez();
|
|
||||||
return instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
Bluez::Bluez() { this->init(); }
|
|
||||||
|
|
||||||
void Bluez::updateDefaultAdapter() {
|
|
||||||
const auto& adapters = this->mAdapters.valueList();
|
|
||||||
this->bDefaultAdapter = adapters.empty() ? nullptr : adapters.first();
|
|
||||||
}
|
|
||||||
|
|
||||||
void Bluez::init() {
|
|
||||||
qCDebug(logBluetooth) << "Connecting to BlueZ";
|
|
||||||
|
|
||||||
auto bus = QDBusConnection::systemBus();
|
|
||||||
|
|
||||||
if (!bus.isConnected()) {
|
|
||||||
qCWarning(logBluetooth) << "Could not connect to DBus. Bluetooth integration is not available.";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this->objectManager = new qs::dbus::DBusObjectManager(this);
|
|
||||||
|
|
||||||
QObject::connect(
|
|
||||||
this->objectManager,
|
|
||||||
&qs::dbus::DBusObjectManager::interfacesAdded,
|
|
||||||
this,
|
|
||||||
&Bluez::onInterfacesAdded
|
|
||||||
);
|
|
||||||
|
|
||||||
QObject::connect(
|
|
||||||
this->objectManager,
|
|
||||||
&qs::dbus::DBusObjectManager::interfacesRemoved,
|
|
||||||
this,
|
|
||||||
&Bluez::onInterfacesRemoved
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!this->objectManager->setInterface("org.bluez", "/", bus)) {
|
|
||||||
qCDebug(logBluetooth) << "BlueZ is not running. Bluetooth integration will not work.";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void Bluez::onInterfacesAdded(
|
|
||||||
const QDBusObjectPath& path,
|
|
||||||
const DBusObjectManagerInterfaces& interfaces
|
|
||||||
) {
|
|
||||||
if (auto* adapter = this->mAdapterMap.value(path.path())) {
|
|
||||||
for (const auto& [interface, properties]: interfaces.asKeyValueRange()) {
|
|
||||||
adapter->addInterface(interface, properties);
|
|
||||||
}
|
|
||||||
} else if (auto* device = this->mDeviceMap.value(path.path())) {
|
|
||||||
for (const auto& [interface, properties]: interfaces.asKeyValueRange()) {
|
|
||||||
device->addInterface(interface, properties);
|
|
||||||
}
|
|
||||||
} else if (interfaces.contains("org.bluez.Adapter1")) {
|
|
||||||
auto* adapter = new BluetoothAdapter(path.path(), this);
|
|
||||||
|
|
||||||
if (!adapter->isValid()) {
|
|
||||||
qCWarning(logBluetooth) << "Adapter path is not valid, cannot track: " << device;
|
|
||||||
delete adapter;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
qCDebug(logBluetooth) << "Tracked new adapter" << adapter;
|
|
||||||
|
|
||||||
for (const auto& [interface, properties]: interfaces.asKeyValueRange()) {
|
|
||||||
adapter->addInterface(interface, properties);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (auto* device: this->mDevices.valueList()) {
|
|
||||||
if (device->adapterPath() == path) {
|
|
||||||
adapter->devices()->insertObject(device);
|
|
||||||
qCDebug(logBluetooth) << "Added tracked device" << device << "to new adapter" << adapter;
|
|
||||||
emit device->adapterChanged();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this->mAdapterMap.insert(path.path(), adapter);
|
|
||||||
this->mAdapters.insertObject(adapter);
|
|
||||||
this->updateDefaultAdapter();
|
|
||||||
} else if (interfaces.contains("org.bluez.Device1")) {
|
|
||||||
auto* device = new BluetoothDevice(path.path(), this);
|
|
||||||
|
|
||||||
if (!device->isValid()) {
|
|
||||||
qCWarning(logBluetooth) << "Device path is not valid, cannot track: " << device;
|
|
||||||
delete device;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
qCDebug(logBluetooth) << "Tracked new device" << device;
|
|
||||||
|
|
||||||
for (const auto& [interface, properties]: interfaces.asKeyValueRange()) {
|
|
||||||
device->addInterface(interface, properties);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (auto* adapter = device->adapter()) {
|
|
||||||
adapter->devices()->insertObject(device);
|
|
||||||
qCDebug(logBluetooth) << "Added device" << device << "to adapter" << adapter;
|
|
||||||
}
|
|
||||||
|
|
||||||
this->mDeviceMap.insert(path.path(), device);
|
|
||||||
this->mDevices.insertObject(device);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void Bluez::onInterfacesRemoved(const QDBusObjectPath& path, const QStringList& interfaces) {
|
|
||||||
if (auto* adapter = this->mAdapterMap.value(path.path())) {
|
|
||||||
if (interfaces.contains("org.bluez.Adapter1")) {
|
|
||||||
qCDebug(logBluetooth) << "Adapter removed:" << adapter;
|
|
||||||
|
|
||||||
this->mAdapterMap.remove(path.path());
|
|
||||||
this->mAdapters.removeObject(adapter);
|
|
||||||
this->updateDefaultAdapter();
|
|
||||||
delete adapter;
|
|
||||||
}
|
|
||||||
} else if (auto* device = this->mDeviceMap.value(path.path())) {
|
|
||||||
if (interfaces.contains("org.bluez.Device1")) {
|
|
||||||
qCDebug(logBluetooth) << "Device removed:" << device;
|
|
||||||
|
|
||||||
if (auto* adapter = device->adapter()) {
|
|
||||||
adapter->devices()->removeObject(device);
|
|
||||||
}
|
|
||||||
|
|
||||||
this->mDeviceMap.remove(path.path());
|
|
||||||
this->mDevices.removeObject(device);
|
|
||||||
delete device;
|
|
||||||
} else {
|
|
||||||
for (const auto& interface: interfaces) {
|
|
||||||
device->removeInterface(interface);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
BluezQml::BluezQml() {
|
|
||||||
QObject::connect(
|
|
||||||
Bluez::instance(),
|
|
||||||
&Bluez::defaultAdapterChanged,
|
|
||||||
this,
|
|
||||||
&BluezQml::defaultAdapterChanged
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace qs::bluetooth
|
|
||||||
|
|
@ -1,98 +0,0 @@
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include <qcontainerfwd.h>
|
|
||||||
#include <qhash.h>
|
|
||||||
#include <qobject.h>
|
|
||||||
#include <qproperty.h>
|
|
||||||
#include <qqmlintegration.h>
|
|
||||||
#include <qtmetamacros.h>
|
|
||||||
|
|
||||||
#include "../core/doc.hpp"
|
|
||||||
#include "../core/model.hpp"
|
|
||||||
#include "../dbus/dbus_objectmanager_types.hpp"
|
|
||||||
#include "../dbus/objectmanager.hpp"
|
|
||||||
|
|
||||||
namespace qs::bluetooth {
|
|
||||||
|
|
||||||
class BluetoothAdapter;
|
|
||||||
class BluetoothDevice;
|
|
||||||
|
|
||||||
class Bluez: public QObject {
|
|
||||||
Q_OBJECT;
|
|
||||||
|
|
||||||
public:
|
|
||||||
[[nodiscard]] ObjectModel<BluetoothAdapter>* adapters() { return &this->mAdapters; }
|
|
||||||
[[nodiscard]] ObjectModel<BluetoothDevice>* devices() { return &this->mDevices; }
|
|
||||||
|
|
||||||
[[nodiscard]] BluetoothAdapter* adapter(const QString& path) {
|
|
||||||
return this->mAdapterMap.value(path);
|
|
||||||
}
|
|
||||||
|
|
||||||
static Bluez* instance();
|
|
||||||
|
|
||||||
signals:
|
|
||||||
void defaultAdapterChanged();
|
|
||||||
|
|
||||||
private slots:
|
|
||||||
void
|
|
||||||
onInterfacesAdded(const QDBusObjectPath& path, const DBusObjectManagerInterfaces& interfaces);
|
|
||||||
void onInterfacesRemoved(const QDBusObjectPath& path, const QStringList& interfaces);
|
|
||||||
void updateDefaultAdapter();
|
|
||||||
|
|
||||||
private:
|
|
||||||
explicit Bluez();
|
|
||||||
void init();
|
|
||||||
|
|
||||||
qs::dbus::DBusObjectManager* objectManager = nullptr;
|
|
||||||
QHash<QString, BluetoothAdapter*> mAdapterMap;
|
|
||||||
QHash<QString, BluetoothDevice*> mDeviceMap;
|
|
||||||
ObjectModel<BluetoothAdapter> mAdapters {this};
|
|
||||||
ObjectModel<BluetoothDevice> mDevices {this};
|
|
||||||
|
|
||||||
public:
|
|
||||||
Q_OBJECT_BINDABLE_PROPERTY(
|
|
||||||
Bluez,
|
|
||||||
BluetoothAdapter*,
|
|
||||||
bDefaultAdapter,
|
|
||||||
&Bluez::defaultAdapterChanged
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
///! Bluetooth manager
|
|
||||||
/// Provides access to bluetooth devices and adapters.
|
|
||||||
class BluezQml: public QObject {
|
|
||||||
Q_OBJECT;
|
|
||||||
QML_NAMED_ELEMENT(Bluetooth);
|
|
||||||
QML_SINGLETON;
|
|
||||||
// clang-format off
|
|
||||||
/// The default bluetooth adapter. Usually there is only one.
|
|
||||||
Q_PROPERTY(BluetoothAdapter* defaultAdapter READ default NOTIFY defaultAdapterChanged BINDABLE bindableDefaultAdapter);
|
|
||||||
QSDOC_TYPE_OVERRIDE(ObjectModel<qs::bluetooth::BluetoothAdapter>*);
|
|
||||||
/// A list of all bluetooth adapters. See @@defaultAdapter for the default.
|
|
||||||
Q_PROPERTY(UntypedObjectModel* adapters READ adapters CONSTANT);
|
|
||||||
QSDOC_TYPE_OVERRIDE(ObjectModel<qs::bluetooth::BluetoothDevice>*);
|
|
||||||
/// A list of all connected bluetooth devices across all adapters.
|
|
||||||
/// See @@BluetoothAdapter.devices for the devices connected to a single adapter.
|
|
||||||
Q_PROPERTY(UntypedObjectModel* devices READ devices CONSTANT);
|
|
||||||
// clang-format on
|
|
||||||
|
|
||||||
signals:
|
|
||||||
void defaultAdapterChanged();
|
|
||||||
|
|
||||||
public:
|
|
||||||
explicit BluezQml();
|
|
||||||
|
|
||||||
[[nodiscard]] static ObjectModel<BluetoothAdapter>* adapters() {
|
|
||||||
return Bluez::instance()->adapters();
|
|
||||||
}
|
|
||||||
|
|
||||||
[[nodiscard]] static ObjectModel<BluetoothDevice>* devices() {
|
|
||||||
return Bluez::instance()->devices();
|
|
||||||
}
|
|
||||||
|
|
||||||
[[nodiscard]] static QBindable<BluetoothAdapter*> bindableDefaultAdapter() {
|
|
||||||
return &Bluez::instance()->bDefaultAdapter;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace qs::bluetooth
|
|
||||||
|
|
@ -1,318 +0,0 @@
|
||||||
#include "device.hpp"
|
|
||||||
|
|
||||||
#include <qcontainerfwd.h>
|
|
||||||
#include <qdbusconnection.h>
|
|
||||||
#include <qdbuspendingcall.h>
|
|
||||||
#include <qdbuspendingreply.h>
|
|
||||||
#include <qdebug.h>
|
|
||||||
#include <qlogging.h>
|
|
||||||
#include <qloggingcategory.h>
|
|
||||||
#include <qstring.h>
|
|
||||||
#include <qtmetamacros.h>
|
|
||||||
#include <qtypes.h>
|
|
||||||
|
|
||||||
#include "../core/logcat.hpp"
|
|
||||||
#include "../dbus/properties.hpp"
|
|
||||||
#include "adapter.hpp"
|
|
||||||
#include "bluez.hpp"
|
|
||||||
#include "dbus_device.h"
|
|
||||||
|
|
||||||
namespace qs::bluetooth {
|
|
||||||
|
|
||||||
namespace {
|
|
||||||
QS_LOGGING_CATEGORY(logDevice, "quickshell.bluetooth.device", QtWarningMsg);
|
|
||||||
}
|
|
||||||
|
|
||||||
QString BluetoothDeviceState::toString(BluetoothDeviceState::Enum state) {
|
|
||||||
switch (state) {
|
|
||||||
case BluetoothDeviceState::Disconnected: return QStringLiteral("Disconnected");
|
|
||||||
case BluetoothDeviceState::Connected: return QStringLiteral("Connected");
|
|
||||||
case BluetoothDeviceState::Disconnecting: return QStringLiteral("Disconnecting");
|
|
||||||
case BluetoothDeviceState::Connecting: return QStringLiteral("Connecting");
|
|
||||||
default: return QStringLiteral("Unknown");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
BluetoothDevice::BluetoothDevice(const QString& path, QObject* parent): QObject(parent) {
|
|
||||||
this->mInterface =
|
|
||||||
new DBusBluezDeviceInterface("org.bluez", path, QDBusConnection::systemBus(), this);
|
|
||||||
|
|
||||||
if (!this->mInterface->isValid()) {
|
|
||||||
qCWarning(logDevice) << "Could not create DBus interface for device at" << path;
|
|
||||||
delete this->mInterface;
|
|
||||||
this->mInterface = nullptr;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this->properties.setInterface(this->mInterface);
|
|
||||||
}
|
|
||||||
|
|
||||||
BluetoothAdapter* BluetoothDevice::adapter() const {
|
|
||||||
return Bluez::instance()->adapter(this->bAdapterPath.value().path());
|
|
||||||
}
|
|
||||||
|
|
||||||
void BluetoothDevice::setConnected(bool connected) {
|
|
||||||
if (connected == this->bConnected) return;
|
|
||||||
|
|
||||||
if (connected) {
|
|
||||||
this->connect();
|
|
||||||
} else {
|
|
||||||
this->disconnect();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void BluetoothDevice::setTrusted(bool trusted) {
|
|
||||||
if (trusted == this->bTrusted) return;
|
|
||||||
this->bTrusted = trusted;
|
|
||||||
this->pTrusted.write();
|
|
||||||
}
|
|
||||||
|
|
||||||
void BluetoothDevice::setBlocked(bool blocked) {
|
|
||||||
if (blocked == this->bBlocked) return;
|
|
||||||
this->bBlocked = blocked;
|
|
||||||
this->pBlocked.write();
|
|
||||||
}
|
|
||||||
|
|
||||||
void BluetoothDevice::setName(const QString& name) {
|
|
||||||
if (name == this->bName) return;
|
|
||||||
this->bName = name;
|
|
||||||
this->pName.write();
|
|
||||||
}
|
|
||||||
|
|
||||||
void BluetoothDevice::setWakeAllowed(bool wakeAllowed) {
|
|
||||||
if (wakeAllowed == this->bWakeAllowed) return;
|
|
||||||
this->bWakeAllowed = wakeAllowed;
|
|
||||||
this->pWakeAllowed.write();
|
|
||||||
}
|
|
||||||
|
|
||||||
void BluetoothDevice::connect() {
|
|
||||||
if (this->bConnected) {
|
|
||||||
qCCritical(logDevice) << "Device" << this << "is already connected";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this->bState == BluetoothDeviceState::Connecting) {
|
|
||||||
qCCritical(logDevice) << "Device" << this << "is already connecting";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
qCDebug(logDevice) << "Connecting to device" << this;
|
|
||||||
this->bState = BluetoothDeviceState::Connecting;
|
|
||||||
|
|
||||||
auto reply = this->mInterface->Connect();
|
|
||||||
auto* watcher = new QDBusPendingCallWatcher(reply, this);
|
|
||||||
|
|
||||||
QObject::connect(
|
|
||||||
watcher,
|
|
||||||
&QDBusPendingCallWatcher::finished,
|
|
||||||
this,
|
|
||||||
[this](QDBusPendingCallWatcher* watcher) {
|
|
||||||
const QDBusPendingReply<> reply = *watcher;
|
|
||||||
|
|
||||||
if (reply.isError()) {
|
|
||||||
qCWarning(logDevice).nospace()
|
|
||||||
<< "Failed to connect to device " << this << ": " << reply.error().message();
|
|
||||||
|
|
||||||
this->bState = this->bConnected ? BluetoothDeviceState::Connected
|
|
||||||
: BluetoothDeviceState::Disconnected;
|
|
||||||
} else {
|
|
||||||
qCDebug(logDevice) << "Successfully connected to to device" << this;
|
|
||||||
}
|
|
||||||
|
|
||||||
delete watcher;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void BluetoothDevice::disconnect() {
|
|
||||||
if (!this->bConnected) {
|
|
||||||
qCCritical(logDevice) << "Device" << this << "is already disconnected";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this->bState == BluetoothDeviceState::Disconnecting) {
|
|
||||||
qCCritical(logDevice) << "Device" << this << "is already disconnecting";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
qCDebug(logDevice) << "Disconnecting from device" << this;
|
|
||||||
this->bState = BluetoothDeviceState::Disconnecting;
|
|
||||||
|
|
||||||
auto reply = this->mInterface->Disconnect();
|
|
||||||
auto* watcher = new QDBusPendingCallWatcher(reply, this);
|
|
||||||
|
|
||||||
QObject::connect(
|
|
||||||
watcher,
|
|
||||||
&QDBusPendingCallWatcher::finished,
|
|
||||||
this,
|
|
||||||
[this](QDBusPendingCallWatcher* watcher) {
|
|
||||||
const QDBusPendingReply<> reply = *watcher;
|
|
||||||
|
|
||||||
if (reply.isError()) {
|
|
||||||
qCWarning(logDevice).nospace()
|
|
||||||
<< "Failed to disconnect from device " << this << ": " << reply.error().message();
|
|
||||||
|
|
||||||
this->bState = this->bConnected ? BluetoothDeviceState::Connected
|
|
||||||
: BluetoothDeviceState::Disconnected;
|
|
||||||
} else {
|
|
||||||
qCDebug(logDevice) << "Successfully disconnected from from device" << this;
|
|
||||||
}
|
|
||||||
|
|
||||||
delete watcher;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void BluetoothDevice::pair() {
|
|
||||||
if (this->bPaired) {
|
|
||||||
qCCritical(logDevice) << "Device" << this << "is already paired";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this->bPairing) {
|
|
||||||
qCCritical(logDevice) << "Device" << this << "is already pairing";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
qCDebug(logDevice) << "Pairing with device" << this;
|
|
||||||
this->bPairing = true;
|
|
||||||
|
|
||||||
auto reply = this->mInterface->Pair();
|
|
||||||
auto* watcher = new QDBusPendingCallWatcher(reply, this);
|
|
||||||
|
|
||||||
QObject::connect(
|
|
||||||
watcher,
|
|
||||||
&QDBusPendingCallWatcher::finished,
|
|
||||||
this,
|
|
||||||
[this](QDBusPendingCallWatcher* watcher) {
|
|
||||||
const QDBusPendingReply<> reply = *watcher;
|
|
||||||
if (reply.isError()) {
|
|
||||||
qCWarning(logDevice).nospace()
|
|
||||||
<< "Failed to pair with device " << this << ": " << reply.error().message();
|
|
||||||
} else {
|
|
||||||
qCDebug(logDevice) << "Successfully initiated pairing with device" << this;
|
|
||||||
}
|
|
||||||
|
|
||||||
this->bPairing = false;
|
|
||||||
delete watcher;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void BluetoothDevice::cancelPair() {
|
|
||||||
if (!this->bPairing) {
|
|
||||||
qCCritical(logDevice) << "Device" << this << "is not currently pairing";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
qCDebug(logDevice) << "Cancelling pairing with device" << this;
|
|
||||||
|
|
||||||
auto reply = this->mInterface->CancelPairing();
|
|
||||||
auto* watcher = new QDBusPendingCallWatcher(reply, this);
|
|
||||||
|
|
||||||
QObject::connect(
|
|
||||||
watcher,
|
|
||||||
&QDBusPendingCallWatcher::finished,
|
|
||||||
this,
|
|
||||||
[this](QDBusPendingCallWatcher* watcher) {
|
|
||||||
const QDBusPendingReply<> reply = *watcher;
|
|
||||||
if (reply.isError()) {
|
|
||||||
qCWarning(logDevice) << "Failed to cancel pairing with device" << this << ":"
|
|
||||||
<< reply.error().message();
|
|
||||||
} else {
|
|
||||||
qCDebug(logDevice) << "Successfully cancelled pairing with device" << this;
|
|
||||||
}
|
|
||||||
|
|
||||||
this->bPairing = false;
|
|
||||||
delete watcher;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void BluetoothDevice::forget() {
|
|
||||||
if (!this->mInterface || !this->mInterface->isValid()) {
|
|
||||||
qCCritical(logDevice) << "Cannot forget - device interface is invalid";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (auto* adapter = Bluez::instance()->adapter(this->bAdapterPath.value().path())) {
|
|
||||||
qCDebug(logDevice) << "Forgetting device" << this << "via adapter" << adapter;
|
|
||||||
adapter->removeDevice(this->path());
|
|
||||||
} else {
|
|
||||||
qCCritical(logDevice) << "Could not find adapter for path" << this->bAdapterPath.value().path()
|
|
||||||
<< "to forget from";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void BluetoothDevice::addInterface(const QString& interface, const QVariantMap& properties) {
|
|
||||||
if (interface == "org.bluez.Device1") {
|
|
||||||
this->properties.updatePropertySet(properties, false);
|
|
||||||
qCDebug(logDevice) << "Updated Device properties for" << this;
|
|
||||||
} else if (interface == "org.bluez.Battery1") {
|
|
||||||
if (!this->mBatteryInterface) {
|
|
||||||
this->mBatteryInterface = new QDBusInterface(
|
|
||||||
"org.bluez",
|
|
||||||
this->path(),
|
|
||||||
"org.bluez.Battery1",
|
|
||||||
QDBusConnection::systemBus(),
|
|
||||||
this
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!this->mBatteryInterface->isValid()) {
|
|
||||||
qCWarning(logDevice) << "Could not create Battery interface for device at" << this;
|
|
||||||
delete this->mBatteryInterface;
|
|
||||||
this->mBatteryInterface = nullptr;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this->batteryProperties.setInterface(this->mBatteryInterface);
|
|
||||||
this->batteryProperties.updatePropertySet(properties, false);
|
|
||||||
|
|
||||||
emit this->batteryAvailableChanged();
|
|
||||||
qCDebug(logDevice) << "Updated Battery properties for" << this;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void BluetoothDevice::removeInterface(const QString& interface) {
|
|
||||||
if (interface == "org.bluez.Battery1" && this->mBatteryInterface) {
|
|
||||||
this->batteryProperties.setInterface(nullptr);
|
|
||||||
delete this->mBatteryInterface;
|
|
||||||
this->mBatteryInterface = nullptr;
|
|
||||||
this->bBattery = 0;
|
|
||||||
|
|
||||||
emit this->batteryAvailableChanged();
|
|
||||||
qCDebug(logDevice) << "Battery interface removed from device" << this;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void BluetoothDevice::onConnectedChanged() {
|
|
||||||
this->bState =
|
|
||||||
this->bConnected ? BluetoothDeviceState::Connected : BluetoothDeviceState::Disconnected;
|
|
||||||
emit this->connectedChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace qs::bluetooth
|
|
||||||
|
|
||||||
namespace qs::dbus {
|
|
||||||
|
|
||||||
using namespace qs::bluetooth;
|
|
||||||
|
|
||||||
DBusResult<qreal> DBusDataTransform<BatteryPercentage>::fromWire(quint8 percentage) {
|
|
||||||
return DBusResult(percentage * 0.01);
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace qs::dbus
|
|
||||||
|
|
||||||
QDebug operator<<(QDebug debug, const qs::bluetooth::BluetoothDevice* device) {
|
|
||||||
auto saver = QDebugStateSaver(debug);
|
|
||||||
|
|
||||||
if (device) {
|
|
||||||
debug.nospace() << "BluetoothDevice(" << static_cast<const void*>(device)
|
|
||||||
<< ", path=" << device->path() << ")";
|
|
||||||
} else {
|
|
||||||
debug << "BluetoothDevice(nullptr)";
|
|
||||||
}
|
|
||||||
|
|
||||||
return debug;
|
|
||||||
}
|
|
||||||
|
|
@ -1,225 +0,0 @@
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include <qobject.h>
|
|
||||||
#include <qqmlintegration.h>
|
|
||||||
#include <qtmetamacros.h>
|
|
||||||
#include <qtypes.h>
|
|
||||||
|
|
||||||
#include "../dbus/properties.hpp"
|
|
||||||
#include "dbus_device.h"
|
|
||||||
|
|
||||||
namespace qs::bluetooth {
|
|
||||||
|
|
||||||
///! Connection state of a Bluetooth device.
|
|
||||||
class BluetoothDeviceState: public QObject {
|
|
||||||
Q_OBJECT;
|
|
||||||
QML_ELEMENT;
|
|
||||||
QML_SINGLETON;
|
|
||||||
|
|
||||||
public:
|
|
||||||
enum Enum : quint8 {
|
|
||||||
/// The device is not connected.
|
|
||||||
Disconnected = 0,
|
|
||||||
/// The device is connected.
|
|
||||||
Connected = 1,
|
|
||||||
/// The device is disconnecting.
|
|
||||||
Disconnecting = 2,
|
|
||||||
/// The device is connecting.
|
|
||||||
Connecting = 3,
|
|
||||||
};
|
|
||||||
Q_ENUM(Enum);
|
|
||||||
|
|
||||||
Q_INVOKABLE static QString toString(BluetoothDeviceState::Enum state);
|
|
||||||
};
|
|
||||||
|
|
||||||
struct BatteryPercentage {};
|
|
||||||
|
|
||||||
} // namespace qs::bluetooth
|
|
||||||
|
|
||||||
namespace qs::dbus {
|
|
||||||
|
|
||||||
template <>
|
|
||||||
struct DBusDataTransform<qs::bluetooth::BatteryPercentage> {
|
|
||||||
using Wire = quint8;
|
|
||||||
using Data = qreal;
|
|
||||||
static DBusResult<Data> fromWire(Wire percentage);
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace qs::dbus
|
|
||||||
|
|
||||||
namespace qs::bluetooth {
|
|
||||||
|
|
||||||
class BluetoothAdapter;
|
|
||||||
|
|
||||||
///! A tracked Bluetooth device.
|
|
||||||
class BluetoothDevice: public QObject {
|
|
||||||
Q_OBJECT;
|
|
||||||
QML_ELEMENT;
|
|
||||||
QML_UNCREATABLE("");
|
|
||||||
// clang-format off
|
|
||||||
/// MAC address of the device.
|
|
||||||
Q_PROPERTY(QString address READ default NOTIFY addressChanged BINDABLE bindableAddress);
|
|
||||||
/// The name of the Bluetooth device. This property may be written to create an alias, or set to
|
|
||||||
/// an empty string to fall back to the device provided name.
|
|
||||||
///
|
|
||||||
/// See @@deviceName for the name provided by the device.
|
|
||||||
Q_PROPERTY(QString name READ name WRITE setName NOTIFY nameChanged);
|
|
||||||
/// The name of the Bluetooth device, ignoring user provided aliases. See also @@name
|
|
||||||
/// which returns a user provided alias if set.
|
|
||||||
Q_PROPERTY(QString deviceName READ default NOTIFY deviceNameChanged BINDABLE bindableDeviceName);
|
|
||||||
/// System icon representing the device type. Use @@Quickshell.Quickshell.iconPath() to display this in an image.
|
|
||||||
Q_PROPERTY(QString icon READ default NOTIFY iconChanged BINDABLE bindableIcon);
|
|
||||||
/// Connection state of the device.
|
|
||||||
Q_PROPERTY(BluetoothDeviceState::Enum state READ default NOTIFY stateChanged BINDABLE bindableState);
|
|
||||||
/// True if the device is currently connected to the computer.
|
|
||||||
///
|
|
||||||
/// Setting this property is equivalent to calling @@connect() and @@disconnect().
|
|
||||||
///
|
|
||||||
/// > [!NOTE] @@state provides more detailed information if required.
|
|
||||||
Q_PROPERTY(bool connected READ connected WRITE setConnected NOTIFY connectedChanged);
|
|
||||||
/// True if the device is paired to the computer.
|
|
||||||
///
|
|
||||||
/// > [!NOTE] @@pair() can be used to pair a device, however you must @@forget() the device to unpair it.
|
|
||||||
Q_PROPERTY(bool paired READ default NOTIFY pairedChanged BINDABLE bindablePaired);
|
|
||||||
/// True if pairing information is stored for future connections.
|
|
||||||
Q_PROPERTY(bool bonded READ default NOTIFY bondedChanged BINDABLE bindableBonded);
|
|
||||||
/// True if the device is currently being paired.
|
|
||||||
///
|
|
||||||
/// > [!NOTE] @@cancelPair() can be used to cancel the pairing process.
|
|
||||||
Q_PROPERTY(bool pairing READ pairing NOTIFY pairingChanged);
|
|
||||||
/// True if the device is considered to be trusted by the system.
|
|
||||||
/// Trusted devices are allowed to reconnect themselves to the system without intervention.
|
|
||||||
Q_PROPERTY(bool trusted READ trusted WRITE setTrusted NOTIFY trustedChanged);
|
|
||||||
/// True if the device is blocked from connecting.
|
|
||||||
/// If a device is blocked, any connection attempts will be immediately rejected by the system.
|
|
||||||
Q_PROPERTY(bool blocked READ blocked WRITE setBlocked NOTIFY blockedChanged);
|
|
||||||
/// True if the device is allowed to wake up the host system from suspend.
|
|
||||||
Q_PROPERTY(bool wakeAllowed READ wakeAllowed WRITE setWakeAllowed NOTIFY wakeAllowedChanged);
|
|
||||||
/// True if the connected device reports its battery level. Battery level can be accessed via @@battery.
|
|
||||||
Q_PROPERTY(bool batteryAvailable READ batteryAvailable NOTIFY batteryAvailableChanged);
|
|
||||||
/// Battery level of the connected device, from `0.0` to `1.0`. Only valid if @@batteryAvailable is true.
|
|
||||||
Q_PROPERTY(qreal battery READ default NOTIFY batteryChanged BINDABLE bindableBattery);
|
|
||||||
/// The Bluetooth adapter this device belongs to.
|
|
||||||
Q_PROPERTY(BluetoothAdapter* adapter READ adapter NOTIFY adapterChanged);
|
|
||||||
/// DBus path of the device under the `org.bluez` system service.
|
|
||||||
Q_PROPERTY(QString dbusPath READ path CONSTANT);
|
|
||||||
// clang-format on
|
|
||||||
|
|
||||||
public:
|
|
||||||
explicit BluetoothDevice(const QString& path, QObject* parent = nullptr);
|
|
||||||
|
|
||||||
/// Attempt to connect to the device.
|
|
||||||
Q_INVOKABLE void connect();
|
|
||||||
/// Disconnect from the device.
|
|
||||||
Q_INVOKABLE void disconnect();
|
|
||||||
/// Attempt to pair the device.
|
|
||||||
///
|
|
||||||
/// > [!NOTE] @@paired and @@pairing return the current pairing status of the device.
|
|
||||||
Q_INVOKABLE void pair();
|
|
||||||
/// Cancel an active pairing attempt.
|
|
||||||
Q_INVOKABLE void cancelPair();
|
|
||||||
/// Forget the device.
|
|
||||||
Q_INVOKABLE void forget();
|
|
||||||
|
|
||||||
[[nodiscard]] bool isValid() const { return this->mInterface && this->mInterface->isValid(); }
|
|
||||||
[[nodiscard]] QString path() const {
|
|
||||||
return this->mInterface ? this->mInterface->path() : QString();
|
|
||||||
}
|
|
||||||
|
|
||||||
[[nodiscard]] bool batteryAvailable() const { return this->mBatteryInterface != nullptr; }
|
|
||||||
[[nodiscard]] BluetoothAdapter* adapter() const;
|
|
||||||
[[nodiscard]] QDBusObjectPath adapterPath() const { return this->bAdapterPath.value(); }
|
|
||||||
|
|
||||||
[[nodiscard]] bool connected() const { return this->bConnected; }
|
|
||||||
void setConnected(bool connected);
|
|
||||||
|
|
||||||
[[nodiscard]] bool trusted() const { return this->bTrusted; }
|
|
||||||
void setTrusted(bool trusted);
|
|
||||||
|
|
||||||
[[nodiscard]] bool blocked() const { return this->bBlocked; }
|
|
||||||
void setBlocked(bool blocked);
|
|
||||||
|
|
||||||
[[nodiscard]] QString name() const { return this->bName; }
|
|
||||||
void setName(const QString& name);
|
|
||||||
|
|
||||||
[[nodiscard]] bool wakeAllowed() const { return this->bWakeAllowed; }
|
|
||||||
void setWakeAllowed(bool wakeAllowed);
|
|
||||||
|
|
||||||
[[nodiscard]] bool pairing() const { return this->bPairing; }
|
|
||||||
|
|
||||||
[[nodiscard]] QBindable<QString> bindableAddress() { return &this->bAddress; }
|
|
||||||
[[nodiscard]] QBindable<QString> bindableDeviceName() { return &this->bDeviceName; }
|
|
||||||
[[nodiscard]] QBindable<QString> bindableName() { return &this->bName; }
|
|
||||||
[[nodiscard]] QBindable<bool> bindableConnected() { return &this->bConnected; }
|
|
||||||
[[nodiscard]] QBindable<bool> bindablePaired() { return &this->bPaired; }
|
|
||||||
[[nodiscard]] QBindable<bool> bindableBonded() { return &this->bBonded; }
|
|
||||||
[[nodiscard]] QBindable<bool> bindableTrusted() { return &this->bTrusted; }
|
|
||||||
[[nodiscard]] QBindable<bool> bindableBlocked() { return &this->bBlocked; }
|
|
||||||
[[nodiscard]] QBindable<bool> bindableWakeAllowed() { return &this->bWakeAllowed; }
|
|
||||||
[[nodiscard]] QBindable<QString> bindableIcon() { return &this->bIcon; }
|
|
||||||
[[nodiscard]] QBindable<qreal> bindableBattery() { return &this->bBattery; }
|
|
||||||
[[nodiscard]] QBindable<BluetoothDeviceState::Enum> bindableState() { return &this->bState; }
|
|
||||||
|
|
||||||
void addInterface(const QString& interface, const QVariantMap& properties);
|
|
||||||
void removeInterface(const QString& interface);
|
|
||||||
|
|
||||||
signals:
|
|
||||||
void addressChanged();
|
|
||||||
void deviceNameChanged();
|
|
||||||
void nameChanged();
|
|
||||||
void connectedChanged();
|
|
||||||
void stateChanged();
|
|
||||||
void pairedChanged();
|
|
||||||
void bondedChanged();
|
|
||||||
void pairingChanged();
|
|
||||||
void trustedChanged();
|
|
||||||
void blockedChanged();
|
|
||||||
void wakeAllowedChanged();
|
|
||||||
void iconChanged();
|
|
||||||
void batteryAvailableChanged();
|
|
||||||
void batteryChanged();
|
|
||||||
void adapterChanged();
|
|
||||||
|
|
||||||
private:
|
|
||||||
void onConnectedChanged();
|
|
||||||
|
|
||||||
DBusBluezDeviceInterface* mInterface = nullptr;
|
|
||||||
QDBusInterface* mBatteryInterface = nullptr;
|
|
||||||
|
|
||||||
// clang-format off
|
|
||||||
Q_OBJECT_BINDABLE_PROPERTY(BluetoothDevice, QString, bAddress, &BluetoothDevice::addressChanged);
|
|
||||||
Q_OBJECT_BINDABLE_PROPERTY(BluetoothDevice, QString, bDeviceName, &BluetoothDevice::deviceNameChanged);
|
|
||||||
Q_OBJECT_BINDABLE_PROPERTY(BluetoothDevice, QString, bName, &BluetoothDevice::nameChanged);
|
|
||||||
Q_OBJECT_BINDABLE_PROPERTY(BluetoothDevice, bool, bConnected, &BluetoothDevice::onConnectedChanged);
|
|
||||||
Q_OBJECT_BINDABLE_PROPERTY(BluetoothDevice, bool, bPaired, &BluetoothDevice::pairedChanged);
|
|
||||||
Q_OBJECT_BINDABLE_PROPERTY(BluetoothDevice, bool, bBonded, &BluetoothDevice::bondedChanged);
|
|
||||||
Q_OBJECT_BINDABLE_PROPERTY(BluetoothDevice, bool, bTrusted, &BluetoothDevice::trustedChanged);
|
|
||||||
Q_OBJECT_BINDABLE_PROPERTY(BluetoothDevice, bool, bBlocked, &BluetoothDevice::blockedChanged);
|
|
||||||
Q_OBJECT_BINDABLE_PROPERTY(BluetoothDevice, bool, bWakeAllowed, &BluetoothDevice::wakeAllowedChanged);
|
|
||||||
Q_OBJECT_BINDABLE_PROPERTY(BluetoothDevice, QString, bIcon, &BluetoothDevice::iconChanged);
|
|
||||||
Q_OBJECT_BINDABLE_PROPERTY(BluetoothDevice, QDBusObjectPath, bAdapterPath);
|
|
||||||
Q_OBJECT_BINDABLE_PROPERTY(BluetoothDevice, qreal, bBattery, &BluetoothDevice::batteryChanged);
|
|
||||||
Q_OBJECT_BINDABLE_PROPERTY(BluetoothDevice, BluetoothDeviceState::Enum, bState, &BluetoothDevice::stateChanged);
|
|
||||||
Q_OBJECT_BINDABLE_PROPERTY(BluetoothDevice, bool, bPairing, &BluetoothDevice::pairingChanged);
|
|
||||||
|
|
||||||
QS_DBUS_BINDABLE_PROPERTY_GROUP(BluetoothDevice, properties);
|
|
||||||
QS_DBUS_PROPERTY_BINDING(BluetoothDevice, pAddress, bAddress, properties, "Address");
|
|
||||||
QS_DBUS_PROPERTY_BINDING(BluetoothDevice, pDeviceName, bDeviceName, properties, "Name");
|
|
||||||
QS_DBUS_PROPERTY_BINDING(BluetoothDevice, pName, bName, properties, "Alias");
|
|
||||||
QS_DBUS_PROPERTY_BINDING(BluetoothDevice, pConnected, bConnected, properties, "Connected");
|
|
||||||
QS_DBUS_PROPERTY_BINDING(BluetoothDevice, pPaired, bPaired, properties, "Paired");
|
|
||||||
QS_DBUS_PROPERTY_BINDING(BluetoothDevice, pBonded, bBonded, properties, "Bonded");
|
|
||||||
QS_DBUS_PROPERTY_BINDING(BluetoothDevice, pTrusted, bTrusted, properties, "Trusted");
|
|
||||||
QS_DBUS_PROPERTY_BINDING(BluetoothDevice, pBlocked, bBlocked, properties, "Blocked");
|
|
||||||
QS_DBUS_PROPERTY_BINDING(BluetoothDevice, pWakeAllowed, bWakeAllowed, properties, "WakeAllowed");
|
|
||||||
QS_DBUS_PROPERTY_BINDING(BluetoothDevice, pIcon, bIcon, properties, "Icon");
|
|
||||||
QS_DBUS_PROPERTY_BINDING(BluetoothDevice, pAdapterPath, bAdapterPath, properties, "Adapter");
|
|
||||||
|
|
||||||
QS_DBUS_BINDABLE_PROPERTY_GROUP(BluetoothDevice, batteryProperties);
|
|
||||||
QS_DBUS_PROPERTY_BINDING(BluetoothDevice, BatteryPercentage, pBattery, bBattery, batteryProperties, "Percentage", true);
|
|
||||||
// clang-format on
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace qs::bluetooth
|
|
||||||
|
|
||||||
QDebug operator<<(QDebug debug, const qs::bluetooth::BluetoothDevice* device);
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
name = "Quickshell.Bluetooth"
|
|
||||||
description = "Bluetooth API"
|
|
||||||
headers = [
|
|
||||||
"bluez.hpp",
|
|
||||||
"adapter.hpp",
|
|
||||||
"device.hpp",
|
|
||||||
]
|
|
||||||
-----
|
|
||||||
This module exposes Bluetooth management APIs provided by the BlueZ DBus interface.
|
|
||||||
Both DBus and BlueZ must be running to use it.
|
|
||||||
|
|
||||||
See the @@Quickshell.Bluetooth.Bluetooth singleton.
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
<node>
|
|
||||||
<interface name="org.bluez.Adapter1">
|
|
||||||
<method name="StartDiscovery"/>
|
|
||||||
<method name="StopDiscovery"/>
|
|
||||||
<method name="RemoveDevice">
|
|
||||||
<arg name="device" type="o"/>
|
|
||||||
</method>
|
|
||||||
</interface>
|
|
||||||
</node>
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
<node>
|
|
||||||
<interface name="org.bluez.Device1">
|
|
||||||
<method name="Connect"/>
|
|
||||||
<method name="Disconnect"/>
|
|
||||||
<method name="Pair"/>
|
|
||||||
<method name="CancelPairing"/>
|
|
||||||
</interface>
|
|
||||||
</node>
|
|
||||||
|
|
@ -1,200 +0,0 @@
|
||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import QtQuick.Layouts
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Widgets
|
|
||||||
import Quickshell.Bluetooth
|
|
||||||
|
|
||||||
FloatingWindow {
|
|
||||||
color: contentItem.palette.window
|
|
||||||
|
|
||||||
ListView {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: 5
|
|
||||||
model: Bluetooth.adapters
|
|
||||||
|
|
||||||
delegate: WrapperRectangle {
|
|
||||||
width: parent.width
|
|
||||||
color: "transparent"
|
|
||||||
border.color: palette.button
|
|
||||||
border.width: 1
|
|
||||||
margin: 5
|
|
||||||
|
|
||||||
ColumnLayout {
|
|
||||||
Label { text: `Adapter: ${modelData.name} (${modelData.adapterId})` }
|
|
||||||
|
|
||||||
RowLayout {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
|
|
||||||
CheckBox {
|
|
||||||
text: "Enable"
|
|
||||||
checked: modelData.enabled
|
|
||||||
onToggled: modelData.enabled = checked
|
|
||||||
}
|
|
||||||
|
|
||||||
Label {
|
|
||||||
color: modelData.state === BluetoothAdapterState.Blocked ? palette.errorText : palette.placeholderText
|
|
||||||
text: BluetoothAdapterState.toString(modelData.state)
|
|
||||||
}
|
|
||||||
|
|
||||||
CheckBox {
|
|
||||||
text: "Discoverable"
|
|
||||||
checked: modelData.discoverable
|
|
||||||
onToggled: modelData.discoverable = checked
|
|
||||||
}
|
|
||||||
|
|
||||||
CheckBox {
|
|
||||||
text: "Discovering"
|
|
||||||
checked: modelData.discovering
|
|
||||||
onToggled: modelData.discovering = checked
|
|
||||||
}
|
|
||||||
|
|
||||||
CheckBox {
|
|
||||||
text: "Pairable"
|
|
||||||
checked: modelData.pairable
|
|
||||||
onToggled: modelData.pairable = checked
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
RowLayout {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
|
|
||||||
Label { text: "Discoverable timeout:" }
|
|
||||||
|
|
||||||
SpinBox {
|
|
||||||
from: 0
|
|
||||||
to: 3600
|
|
||||||
value: modelData.discoverableTimeout
|
|
||||||
onValueModified: modelData.discoverableTimeout = value
|
|
||||||
textFromValue: time => time === 0 ? "∞" : time + "s"
|
|
||||||
}
|
|
||||||
|
|
||||||
Label { text: "Pairable timeout:" }
|
|
||||||
|
|
||||||
SpinBox {
|
|
||||||
from: 0
|
|
||||||
to: 3600
|
|
||||||
value: modelData.pairableTimeout
|
|
||||||
onValueModified: modelData.pairableTimeout = value
|
|
||||||
textFromValue: time => time === 0 ? "∞" : time + "s"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Repeater {
|
|
||||||
model: modelData.devices
|
|
||||||
|
|
||||||
WrapperRectangle {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
color: palette.button
|
|
||||||
border.color: palette.mid
|
|
||||||
border.width: 1
|
|
||||||
margin: 5
|
|
||||||
|
|
||||||
RowLayout {
|
|
||||||
ColumnLayout {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
|
|
||||||
RowLayout {
|
|
||||||
IconImage {
|
|
||||||
Layout.fillHeight: true
|
|
||||||
implicitWidth: height
|
|
||||||
source: Quickshell.iconPath(modelData.icon)
|
|
||||||
}
|
|
||||||
|
|
||||||
TextField {
|
|
||||||
text: modelData.name
|
|
||||||
font.bold: true
|
|
||||||
background: null
|
|
||||||
readOnly: false
|
|
||||||
selectByMouse: true
|
|
||||||
onEditingFinished: modelData.name = text
|
|
||||||
}
|
|
||||||
|
|
||||||
Label {
|
|
||||||
visible: modelData.name && modelData.name !== modelData.deviceName
|
|
||||||
text: `(${modelData.deviceName})`
|
|
||||||
color: palette.placeholderText
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
RowLayout {
|
|
||||||
Label {
|
|
||||||
text: modelData.address
|
|
||||||
color: palette.placeholderText
|
|
||||||
}
|
|
||||||
|
|
||||||
Label {
|
|
||||||
visible: modelData.batteryAvailable
|
|
||||||
text: `| Battery: ${Math.round(modelData.battery * 100)}%`
|
|
||||||
color: palette.placeholderText
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
RowLayout {
|
|
||||||
Label {
|
|
||||||
text: BluetoothDeviceState.toString(modelData.state)
|
|
||||||
|
|
||||||
color: modelData.connected ? palette.link : palette.placeholderText
|
|
||||||
}
|
|
||||||
|
|
||||||
Label {
|
|
||||||
text: modelData.pairing ? "Pairing" : (modelData.paired ? "Paired" : "Not Paired")
|
|
||||||
color: modelData.paired || modelData.pairing ? palette.link : palette.placeholderText
|
|
||||||
}
|
|
||||||
|
|
||||||
Label {
|
|
||||||
visible: modelData.bonded
|
|
||||||
text: "| Bonded"
|
|
||||||
color: palette.link
|
|
||||||
}
|
|
||||||
|
|
||||||
CheckBox {
|
|
||||||
text: "Trusted"
|
|
||||||
checked: modelData.trusted
|
|
||||||
onToggled: modelData.trusted = checked
|
|
||||||
}
|
|
||||||
|
|
||||||
CheckBox {
|
|
||||||
text: "Blocked"
|
|
||||||
checked: modelData.blocked
|
|
||||||
onToggled: modelData.blocked = checked
|
|
||||||
}
|
|
||||||
|
|
||||||
CheckBox {
|
|
||||||
text: "Wake Allowed"
|
|
||||||
checked: modelData.wakeAllowed
|
|
||||||
onToggled: modelData.wakeAllowed = checked
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ColumnLayout {
|
|
||||||
Layout.alignment: Qt.AlignRight
|
|
||||||
|
|
||||||
Button {
|
|
||||||
Layout.alignment: Qt.AlignRight
|
|
||||||
text: modelData.connected ? "Disconnect" : "Connect"
|
|
||||||
onClicked: modelData.connected = !modelData.connected
|
|
||||||
}
|
|
||||||
|
|
||||||
Button {
|
|
||||||
Layout.alignment: Qt.AlignRight
|
|
||||||
text: modelData.pairing ? "Cancel" : (modelData.paired ? "Forget" : "Pair")
|
|
||||||
onClicked: {
|
|
||||||
if (modelData.pairing) {
|
|
||||||
modelData.cancelPair();
|
|
||||||
} else if (modelData.paired) {
|
|
||||||
modelData.forget();
|
|
||||||
} else {
|
|
||||||
modelData.pair();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -9,10 +9,16 @@ if (NOT DEFINED GIT_REVISION)
|
||||||
)
|
)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
if (CRASH_HANDLER)
|
if (CRASH_REPORTER)
|
||||||
set(CRASH_HANDLER_DEF 1)
|
set(CRASH_REPORTER_DEF 1)
|
||||||
else()
|
else()
|
||||||
set(CRASH_HANDLER_DEF 0)
|
set(CRASH_REPORTER_DEF 0)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if (DISTRIBUTOR_DEBUGINFO_AVAILABLE)
|
||||||
|
set(DEBUGINFO_AVAILABLE 1)
|
||||||
|
else()
|
||||||
|
set(DEBUGINFO_AVAILABLE 0)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
configure_file(build.hpp.in build.hpp @ONLY ESCAPE_QUOTES)
|
configure_file(build.hpp.in build.hpp @ONLY ESCAPE_QUOTES)
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,12 @@
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
// NOLINTBEGIN
|
// NOLINTBEGIN
|
||||||
#define QS_VERSION "@quickshell_VERSION@"
|
|
||||||
#define QS_VERSION_MAJOR @quickshell_VERSION_MAJOR@
|
|
||||||
#define QS_VERSION_MINOR @quickshell_VERSION_MINOR@
|
|
||||||
#define QS_VERSION_PATCH @quickshell_VERSION_PATCH@
|
|
||||||
#define QS_UNRELEASED_FEATURES "@UNRELEASED_FEATURES@"
|
|
||||||
#define GIT_REVISION "@GIT_REVISION@"
|
#define GIT_REVISION "@GIT_REVISION@"
|
||||||
#define DISTRIBUTOR "@DISTRIBUTOR@"
|
#define DISTRIBUTOR "@DISTRIBUTOR@"
|
||||||
#define CRASH_HANDLER @CRASH_HANDLER_DEF@
|
#define DISTRIBUTOR_DEBUGINFO_AVAILABLE @DEBUGINFO_AVAILABLE@
|
||||||
|
#define CRASH_REPORTER @CRASH_REPORTER_DEF@
|
||||||
#define BUILD_TYPE "@CMAKE_BUILD_TYPE@"
|
#define BUILD_TYPE "@CMAKE_BUILD_TYPE@"
|
||||||
#define COMPILER "@CMAKE_CXX_COMPILER_ID@ (@CMAKE_CXX_COMPILER_VERSION@)"
|
#define COMPILER "@CMAKE_CXX_COMPILER_ID@ (@CMAKE_CXX_COMPILER_VERSION@)"
|
||||||
#define COMPILE_FLAGS "@CMAKE_CXX_FLAGS@"
|
#define COMPILE_FLAGS "@CMAKE_CXX_FLAGS@"
|
||||||
#define BUILD_CONFIGURATION "@QS_BUILD_OPTIONS@"
|
#define BUILD_CONFIGURATION "@QS_BUILD_OPTIONS@"
|
||||||
#define CRASHREPORT_URL "@CRASHREPORT_URL@"
|
|
||||||
// NOLINTEND
|
// NOLINTEND
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
pkg_check_modules(libdrm REQUIRED IMPORTED_TARGET libdrm)
|
|
||||||
qt_add_library(quickshell-core STATIC
|
qt_add_library(quickshell-core STATIC
|
||||||
plugin.cpp
|
plugin.cpp
|
||||||
shell.cpp
|
shell.cpp
|
||||||
|
|
@ -13,7 +12,6 @@ qt_add_library(quickshell-core STATIC
|
||||||
singleton.cpp
|
singleton.cpp
|
||||||
generation.cpp
|
generation.cpp
|
||||||
scan.cpp
|
scan.cpp
|
||||||
scanenv.cpp
|
|
||||||
qsintercept.cpp
|
qsintercept.cpp
|
||||||
incubator.cpp
|
incubator.cpp
|
||||||
lazyloader.cpp
|
lazyloader.cpp
|
||||||
|
|
@ -25,7 +23,7 @@ qt_add_library(quickshell-core STATIC
|
||||||
model.cpp
|
model.cpp
|
||||||
elapsedtimer.cpp
|
elapsedtimer.cpp
|
||||||
desktopentry.cpp
|
desktopentry.cpp
|
||||||
desktopentrymonitor.cpp
|
objectrepeater.cpp
|
||||||
platformmenu.cpp
|
platformmenu.cpp
|
||||||
qsmenu.cpp
|
qsmenu.cpp
|
||||||
retainable.cpp
|
retainable.cpp
|
||||||
|
|
@ -40,9 +38,6 @@ qt_add_library(quickshell-core STATIC
|
||||||
iconprovider.cpp
|
iconprovider.cpp
|
||||||
scriptmodel.cpp
|
scriptmodel.cpp
|
||||||
colorquantizer.cpp
|
colorquantizer.cpp
|
||||||
toolsupport.cpp
|
|
||||||
streamreader.cpp
|
|
||||||
debuginfo.cpp
|
|
||||||
)
|
)
|
||||||
|
|
||||||
qt_add_qml_module(quickshell-core
|
qt_add_qml_module(quickshell-core
|
||||||
|
|
@ -55,7 +50,7 @@ qt_add_qml_module(quickshell-core
|
||||||
|
|
||||||
install_qml_module(quickshell-core)
|
install_qml_module(quickshell-core)
|
||||||
|
|
||||||
target_link_libraries(quickshell-core PRIVATE Qt::Quick Qt::QuickPrivate Qt::Widgets quickshell-build PkgConfig::libdrm)
|
target_link_libraries(quickshell-core PRIVATE Qt::Quick Qt::Widgets)
|
||||||
|
|
||||||
qs_module_pch(quickshell-core SET large)
|
qs_module_pch(quickshell-core SET large)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,55 +13,39 @@
|
||||||
#include <qnumeric.h>
|
#include <qnumeric.h>
|
||||||
#include <qobject.h>
|
#include <qobject.h>
|
||||||
#include <qqmllist.h>
|
#include <qqmllist.h>
|
||||||
#include <qrect.h>
|
|
||||||
#include <qrgb.h>
|
#include <qrgb.h>
|
||||||
#include <qthreadpool.h>
|
#include <qthreadpool.h>
|
||||||
#include <qtmetamacros.h>
|
#include <qtmetamacros.h>
|
||||||
#include <qtypes.h>
|
#include <qtypes.h>
|
||||||
|
|
||||||
#include "logcat.hpp"
|
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
QS_LOGGING_CATEGORY(logColorQuantizer, "quickshell.colorquantizer", QtWarningMsg);
|
Q_LOGGING_CATEGORY(logColorQuantizer, "quickshell.colorquantizer", QtWarningMsg);
|
||||||
}
|
}
|
||||||
|
|
||||||
ColorQuantizerOperation::ColorQuantizerOperation(
|
ColorQuantizerOperation::ColorQuantizerOperation(QUrl* source, qreal depth, qreal rescaleSize)
|
||||||
QUrl* source,
|
|
||||||
qreal depth,
|
|
||||||
QRect imageRect,
|
|
||||||
qreal rescaleSize
|
|
||||||
)
|
|
||||||
: source(source)
|
: source(source)
|
||||||
, maxDepth(depth)
|
, maxDepth(depth)
|
||||||
, imageRect(imageRect)
|
|
||||||
, rescaleSize(rescaleSize) {
|
, rescaleSize(rescaleSize) {
|
||||||
this->setAutoDelete(false);
|
setAutoDelete(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
void ColorQuantizerOperation::quantizeImage(const QAtomicInteger<bool>& shouldCancel) {
|
void ColorQuantizerOperation::quantizeImage(const QAtomicInteger<bool>& shouldCancel) {
|
||||||
if (shouldCancel.loadAcquire() || this->source->isEmpty()) return;
|
if (shouldCancel.loadAcquire() || source->isEmpty()) return;
|
||||||
|
|
||||||
this->colors.clear();
|
colors.clear();
|
||||||
|
|
||||||
auto image = QImage(this->source->toLocalFile());
|
auto image = QImage(source->toLocalFile());
|
||||||
|
if ((image.width() > rescaleSize || image.height() > rescaleSize) && rescaleSize > 0) {
|
||||||
if (this->imageRect.isValid()) {
|
|
||||||
image = image.copy(this->imageRect);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((image.width() > this->rescaleSize || image.height() > this->rescaleSize)
|
|
||||||
&& this->rescaleSize > 0)
|
|
||||||
{
|
|
||||||
image = image.scaled(
|
image = image.scaled(
|
||||||
static_cast<int>(this->rescaleSize),
|
static_cast<int>(rescaleSize),
|
||||||
static_cast<int>(this->rescaleSize),
|
static_cast<int>(rescaleSize),
|
||||||
Qt::KeepAspectRatio,
|
Qt::KeepAspectRatio,
|
||||||
Qt::SmoothTransformation
|
Qt::SmoothTransformation
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (image.isNull()) {
|
if (image.isNull()) {
|
||||||
qCWarning(logColorQuantizer) << "Failed to load image from" << this->source->toString();
|
qCWarning(logColorQuantizer) << "Failed to load image from" << source;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -77,7 +61,7 @@ void ColorQuantizerOperation::quantizeImage(const QAtomicInteger<bool>& shouldCa
|
||||||
|
|
||||||
auto startTime = QDateTime::currentDateTime();
|
auto startTime = QDateTime::currentDateTime();
|
||||||
|
|
||||||
this->colors = this->quantization(pixels, 0);
|
colors = quantization(pixels, 0);
|
||||||
|
|
||||||
auto endTime = QDateTime::currentDateTime();
|
auto endTime = QDateTime::currentDateTime();
|
||||||
auto milliseconds = startTime.msecsTo(endTime);
|
auto milliseconds = startTime.msecsTo(endTime);
|
||||||
|
|
@ -91,7 +75,7 @@ QList<QColor> ColorQuantizerOperation::quantization(
|
||||||
) {
|
) {
|
||||||
if (shouldCancel.loadAcquire()) return QList<QColor>();
|
if (shouldCancel.loadAcquire()) return QList<QColor>();
|
||||||
|
|
||||||
if (depth >= this->maxDepth || rgbValues.isEmpty()) {
|
if (depth >= maxDepth || rgbValues.isEmpty()) {
|
||||||
if (rgbValues.isEmpty()) return QList<QColor>();
|
if (rgbValues.isEmpty()) return QList<QColor>();
|
||||||
|
|
||||||
auto totalR = 0;
|
auto totalR = 0;
|
||||||
|
|
@ -128,8 +112,8 @@ QList<QColor> ColorQuantizerOperation::quantization(
|
||||||
auto rightHalf = rgbValues.mid(mid);
|
auto rightHalf = rgbValues.mid(mid);
|
||||||
|
|
||||||
QList<QColor> result;
|
QList<QColor> result;
|
||||||
result.append(this->quantization(leftHalf, depth + 1));
|
result.append(quantization(leftHalf, depth + 1));
|
||||||
result.append(this->quantization(rightHalf, depth + 1));
|
result.append(quantization(rightHalf, depth + 1));
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
@ -173,7 +157,7 @@ void ColorQuantizerOperation::finishRun() {
|
||||||
}
|
}
|
||||||
|
|
||||||
void ColorQuantizerOperation::finished() {
|
void ColorQuantizerOperation::finished() {
|
||||||
emit this->done(this->colors);
|
emit this->done(colors);
|
||||||
delete this;
|
delete this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -192,50 +176,39 @@ void ColorQuantizerOperation::run() {
|
||||||
void ColorQuantizerOperation::tryCancel() { this->shouldCancel.storeRelease(true); }
|
void ColorQuantizerOperation::tryCancel() { this->shouldCancel.storeRelease(true); }
|
||||||
|
|
||||||
void ColorQuantizer::componentComplete() {
|
void ColorQuantizer::componentComplete() {
|
||||||
this->componentCompleted = true;
|
componentCompleted = true;
|
||||||
if (!this->mSource.isEmpty()) this->quantizeAsync();
|
if (!mSource.isEmpty()) quantizeAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
void ColorQuantizer::setSource(const QUrl& source) {
|
void ColorQuantizer::setSource(const QUrl& source) {
|
||||||
if (this->mSource != source) {
|
if (mSource != source) {
|
||||||
this->mSource = source;
|
mSource = source;
|
||||||
emit this->sourceChanged();
|
emit this->sourceChanged();
|
||||||
|
|
||||||
if (this->componentCompleted && !this->mSource.isEmpty()) this->quantizeAsync();
|
if (this->componentCompleted && !mSource.isEmpty()) quantizeAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void ColorQuantizer::setDepth(qreal depth) {
|
void ColorQuantizer::setDepth(qreal depth) {
|
||||||
if (this->mDepth != depth) {
|
if (mDepth != depth) {
|
||||||
this->mDepth = depth;
|
mDepth = depth;
|
||||||
emit this->depthChanged();
|
emit this->depthChanged();
|
||||||
|
|
||||||
if (this->componentCompleted && !this->mSource.isEmpty()) this->quantizeAsync();
|
if (this->componentCompleted) quantizeAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void ColorQuantizer::setImageRect(QRect imageRect) {
|
|
||||||
if (this->mImageRect != imageRect) {
|
|
||||||
this->mImageRect = imageRect;
|
|
||||||
emit this->imageRectChanged();
|
|
||||||
|
|
||||||
if (this->componentCompleted && !this->mSource.isEmpty()) this->quantizeAsync();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void ColorQuantizer::resetImageRect() { this->setImageRect(QRect()); }
|
|
||||||
|
|
||||||
void ColorQuantizer::setRescaleSize(int rescaleSize) {
|
void ColorQuantizer::setRescaleSize(int rescaleSize) {
|
||||||
if (this->mRescaleSize != rescaleSize) {
|
if (mRescaleSize != rescaleSize) {
|
||||||
this->mRescaleSize = rescaleSize;
|
mRescaleSize = rescaleSize;
|
||||||
emit this->rescaleSizeChanged();
|
emit this->rescaleSizeChanged();
|
||||||
|
|
||||||
if (this->componentCompleted && !this->mSource.isEmpty()) this->quantizeAsync();
|
if (this->componentCompleted) quantizeAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void ColorQuantizer::operationFinished(const QList<QColor>& result) {
|
void ColorQuantizer::operationFinished(const QList<QColor>& result) {
|
||||||
this->bColors = result;
|
bColors = result;
|
||||||
this->liveOperation = nullptr;
|
this->liveOperation = nullptr;
|
||||||
emit this->colorsChanged();
|
emit this->colorsChanged();
|
||||||
}
|
}
|
||||||
|
|
@ -244,13 +217,7 @@ void ColorQuantizer::quantizeAsync() {
|
||||||
if (this->liveOperation) this->cancelAsync();
|
if (this->liveOperation) this->cancelAsync();
|
||||||
|
|
||||||
qCDebug(logColorQuantizer) << "Starting color quantization asynchronously";
|
qCDebug(logColorQuantizer) << "Starting color quantization asynchronously";
|
||||||
|
this->liveOperation = new ColorQuantizerOperation(&mSource, mDepth, mRescaleSize);
|
||||||
this->liveOperation = new ColorQuantizerOperation(
|
|
||||||
&this->mSource,
|
|
||||||
this->mDepth,
|
|
||||||
this->mImageRect,
|
|
||||||
this->mRescaleSize
|
|
||||||
);
|
|
||||||
|
|
||||||
QObject::connect(
|
QObject::connect(
|
||||||
this->liveOperation,
|
this->liveOperation,
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@
|
||||||
#include <qproperty.h>
|
#include <qproperty.h>
|
||||||
#include <qqmlintegration.h>
|
#include <qqmlintegration.h>
|
||||||
#include <qqmlparserstatus.h>
|
#include <qqmlparserstatus.h>
|
||||||
#include <qrect.h>
|
|
||||||
#include <qrunnable.h>
|
#include <qrunnable.h>
|
||||||
#include <qtmetamacros.h>
|
#include <qtmetamacros.h>
|
||||||
#include <qtypes.h>
|
#include <qtypes.h>
|
||||||
|
|
@ -17,7 +16,7 @@ class ColorQuantizerOperation
|
||||||
Q_OBJECT;
|
Q_OBJECT;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit ColorQuantizerOperation(QUrl* source, qreal depth, QRect imageRect, qreal rescaleSize);
|
explicit ColorQuantizerOperation(QUrl* source, qreal depth, qreal rescaleSize);
|
||||||
|
|
||||||
void run() override;
|
void run() override;
|
||||||
void tryCancel();
|
void tryCancel();
|
||||||
|
|
@ -45,7 +44,6 @@ private:
|
||||||
QList<QColor> colors;
|
QList<QColor> colors;
|
||||||
QUrl* source;
|
QUrl* source;
|
||||||
qreal maxDepth;
|
qreal maxDepth;
|
||||||
QRect imageRect;
|
|
||||||
qreal rescaleSize;
|
qreal rescaleSize;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -80,13 +78,6 @@ class ColorQuantizer
|
||||||
/// binary split of the color space
|
/// binary split of the color space
|
||||||
Q_PROPERTY(qreal depth READ depth WRITE setDepth NOTIFY depthChanged);
|
Q_PROPERTY(qreal depth READ depth WRITE setDepth NOTIFY depthChanged);
|
||||||
|
|
||||||
// clang-format off
|
|
||||||
/// Rectangle that the source image is cropped to.
|
|
||||||
///
|
|
||||||
/// Can be set to `undefined` to reset.
|
|
||||||
Q_PROPERTY(QRect imageRect READ imageRect WRITE setImageRect RESET resetImageRect NOTIFY imageRectChanged);
|
|
||||||
// clang-format on
|
|
||||||
|
|
||||||
/// The size to rescale the image to, when rescaleSize is 0 then no scaling will be done.
|
/// The size to rescale the image to, when rescaleSize is 0 then no scaling will be done.
|
||||||
/// > [!NOTE] Results from color quantization doesn't suffer much when rescaling, it's
|
/// > [!NOTE] Results from color quantization doesn't suffer much when rescaling, it's
|
||||||
/// > reccommended to rescale, otherwise the quantization process will take much longer.
|
/// > reccommended to rescale, otherwise the quantization process will take much longer.
|
||||||
|
|
@ -100,24 +91,19 @@ public:
|
||||||
|
|
||||||
[[nodiscard]] QBindable<QList<QColor>> bindableColors() { return &this->bColors; }
|
[[nodiscard]] QBindable<QList<QColor>> bindableColors() { return &this->bColors; }
|
||||||
|
|
||||||
[[nodiscard]] QUrl source() const { return this->mSource; }
|
[[nodiscard]] QUrl source() const { return mSource; }
|
||||||
void setSource(const QUrl& source);
|
void setSource(const QUrl& source);
|
||||||
|
|
||||||
[[nodiscard]] qreal depth() const { return this->mDepth; }
|
[[nodiscard]] qreal depth() const { return mDepth; }
|
||||||
void setDepth(qreal depth);
|
void setDepth(qreal depth);
|
||||||
|
|
||||||
[[nodiscard]] QRect imageRect() const { return this->mImageRect; }
|
[[nodiscard]] qreal rescaleSize() const { return mRescaleSize; }
|
||||||
void setImageRect(QRect imageRect);
|
|
||||||
void resetImageRect();
|
|
||||||
|
|
||||||
[[nodiscard]] qreal rescaleSize() const { return this->mRescaleSize; }
|
|
||||||
void setRescaleSize(int rescaleSize);
|
void setRescaleSize(int rescaleSize);
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void colorsChanged();
|
void colorsChanged();
|
||||||
void sourceChanged();
|
void sourceChanged();
|
||||||
void depthChanged();
|
void depthChanged();
|
||||||
void imageRectChanged();
|
|
||||||
void rescaleSizeChanged();
|
void rescaleSizeChanged();
|
||||||
|
|
||||||
public slots:
|
public slots:
|
||||||
|
|
@ -131,7 +117,6 @@ private:
|
||||||
ColorQuantizerOperation* liveOperation = nullptr;
|
ColorQuantizerOperation* liveOperation = nullptr;
|
||||||
QUrl mSource;
|
QUrl mSource;
|
||||||
qreal mDepth = 0;
|
qreal mDepth = 0;
|
||||||
QRect mImageRect;
|
|
||||||
qreal mRescaleSize = 0;
|
qreal mRescaleSize = 0;
|
||||||
|
|
||||||
Q_OBJECT_BINDABLE_PROPERTY(
|
Q_OBJECT_BINDABLE_PROPERTY(
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,11 @@
|
||||||
#include "common.hpp"
|
#include "common.hpp"
|
||||||
|
|
||||||
#include <qdatetime.h>
|
#include <qdatetime.h>
|
||||||
|
#include <qprocess.h>
|
||||||
|
|
||||||
namespace qs {
|
namespace qs {
|
||||||
|
|
||||||
const QDateTime Common::LAUNCH_TIME = QDateTime::currentDateTime();
|
const QDateTime Common::LAUNCH_TIME = QDateTime::currentDateTime();
|
||||||
|
QProcessEnvironment Common::INITIAL_ENVIRONMENT = {}; // NOLINT
|
||||||
|
|
||||||
} // namespace qs
|
} // namespace qs
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ namespace qs {
|
||||||
|
|
||||||
struct Common {
|
struct Common {
|
||||||
static const QDateTime LAUNCH_TIME;
|
static const QDateTime LAUNCH_TIME;
|
||||||
static inline QProcessEnvironment INITIAL_ENVIRONMENT = {}; // NOLINT
|
static QProcessEnvironment INITIAL_ENVIRONMENT; // NOLINT
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace qs
|
} // namespace qs
|
||||||
|
|
|
||||||
|
|
@ -1,176 +0,0 @@
|
||||||
#include "debuginfo.hpp"
|
|
||||||
#include <array>
|
|
||||||
#include <cstring>
|
|
||||||
#include <string_view>
|
|
||||||
|
|
||||||
#include <fcntl.h>
|
|
||||||
#include <qconfig.h>
|
|
||||||
#include <qcontainerfwd.h>
|
|
||||||
#include <qdebug.h>
|
|
||||||
#include <qfile.h>
|
|
||||||
#include <qfloat16.h>
|
|
||||||
#include <qhashfunctions.h>
|
|
||||||
#include <qscopeguard.h>
|
|
||||||
#include <qtversion.h>
|
|
||||||
#include <unistd.h>
|
|
||||||
#include <xf86drm.h>
|
|
||||||
|
|
||||||
#include "build.hpp"
|
|
||||||
|
|
||||||
extern char** environ; // NOLINT
|
|
||||||
|
|
||||||
namespace qs::debuginfo {
|
|
||||||
|
|
||||||
QString qsVersion() {
|
|
||||||
return QS_VERSION " (revision " GIT_REVISION ", distributed by " DISTRIBUTOR ")";
|
|
||||||
}
|
|
||||||
|
|
||||||
QString qtVersion() { return qVersion() % QStringLiteral(" (built against " QT_VERSION_STR ")"); }
|
|
||||||
|
|
||||||
QString gpuInfo() {
|
|
||||||
auto deviceCount = drmGetDevices2(0, nullptr, 0);
|
|
||||||
if (deviceCount < 0) return "Failed to get DRM device count: " % QString::number(deviceCount);
|
|
||||||
auto* devices = new drmDevicePtr[deviceCount];
|
|
||||||
auto devicesArrayGuard = qScopeGuard([&] { delete[] devices; });
|
|
||||||
auto r = drmGetDevices2(0, devices, deviceCount);
|
|
||||||
if (deviceCount < 0) return "Failed to get DRM devices: " % QString::number(r);
|
|
||||||
auto devicesGuard = qScopeGuard([&] {
|
|
||||||
for (auto i = 0; i != deviceCount; ++i) drmFreeDevice(&devices[i]); // NOLINT
|
|
||||||
});
|
|
||||||
|
|
||||||
QString info;
|
|
||||||
auto stream = QTextStream(&info);
|
|
||||||
|
|
||||||
for (auto i = 0; i != deviceCount; ++i) {
|
|
||||||
auto* device = devices[i]; // NOLINT
|
|
||||||
|
|
||||||
int deviceNodeType = -1;
|
|
||||||
if (device->available_nodes & (1 << DRM_NODE_RENDER)) deviceNodeType = DRM_NODE_RENDER;
|
|
||||||
else if (device->available_nodes & (1 << DRM_NODE_PRIMARY)) deviceNodeType = DRM_NODE_PRIMARY;
|
|
||||||
|
|
||||||
if (deviceNodeType == -1) continue;
|
|
||||||
|
|
||||||
auto* deviceNode = device->nodes[DRM_NODE_RENDER]; // NOLINT
|
|
||||||
|
|
||||||
auto driver = [&]() -> QString {
|
|
||||||
auto fd = open(deviceNode, O_RDWR | O_CLOEXEC);
|
|
||||||
if (fd == -1) return "<failed to open device node>";
|
|
||||||
auto fdGuard = qScopeGuard([&] { close(fd); });
|
|
||||||
auto* ver = drmGetVersion(fd);
|
|
||||||
if (!ver) return "<drmGetVersion failed>";
|
|
||||||
auto verGuard = qScopeGuard([&] { drmFreeVersion(ver); });
|
|
||||||
|
|
||||||
// clang-format off
|
|
||||||
return QString(ver->name)
|
|
||||||
% ' ' % QString::number(ver->version_major)
|
|
||||||
% '.' % QString::number(ver->version_minor)
|
|
||||||
% '.' % QString::number(ver->version_patchlevel)
|
|
||||||
% " (" % ver->desc % ')';
|
|
||||||
// clang-format on
|
|
||||||
}();
|
|
||||||
|
|
||||||
QString product = "unknown";
|
|
||||||
QString address = "unknown";
|
|
||||||
|
|
||||||
auto hex = [](int num, int pad) { return QString::number(num, 16).rightJustified(pad, '0'); };
|
|
||||||
|
|
||||||
switch (device->bustype) {
|
|
||||||
case DRM_BUS_PCI: {
|
|
||||||
auto* b = device->businfo.pci;
|
|
||||||
auto* d = device->deviceinfo.pci;
|
|
||||||
address = "PCI " % hex(b->bus, 2) % ':' % hex(b->dev, 2) % '.' % hex(b->func, 1);
|
|
||||||
product = hex(d->vendor_id, 4) % ':' % hex(d->device_id, 4);
|
|
||||||
} break;
|
|
||||||
case DRM_BUS_USB: {
|
|
||||||
auto* b = device->businfo.usb;
|
|
||||||
auto* d = device->deviceinfo.usb;
|
|
||||||
address = "USB " % QString::number(b->bus) % ':' % QString::number(b->dev);
|
|
||||||
product = hex(d->vendor, 4) % ':' % hex(d->product, 4);
|
|
||||||
} break;
|
|
||||||
default: break;
|
|
||||||
}
|
|
||||||
|
|
||||||
stream << "GPU " << deviceNode << "\n Driver: " << driver << "\n Model: " << product
|
|
||||||
<< "\n Address: " << address << '\n';
|
|
||||||
}
|
|
||||||
|
|
||||||
return info;
|
|
||||||
}
|
|
||||||
|
|
||||||
QString systemInfo() {
|
|
||||||
QString info;
|
|
||||||
auto stream = QTextStream(&info);
|
|
||||||
|
|
||||||
stream << gpuInfo() << '\n';
|
|
||||||
|
|
||||||
stream << "/etc/os-release:";
|
|
||||||
auto osReleaseFile = QFile("/etc/os-release");
|
|
||||||
if (osReleaseFile.open(QFile::ReadOnly)) {
|
|
||||||
stream << '\n' << osReleaseFile.readAll() << '\n';
|
|
||||||
osReleaseFile.close();
|
|
||||||
} else {
|
|
||||||
stream << "FAILED TO OPEN\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
stream << "/etc/lsb-release:";
|
|
||||||
auto lsbReleaseFile = QFile("/etc/lsb-release");
|
|
||||||
if (lsbReleaseFile.open(QFile::ReadOnly)) {
|
|
||||||
stream << '\n' << lsbReleaseFile.readAll();
|
|
||||||
lsbReleaseFile.close();
|
|
||||||
} else {
|
|
||||||
stream << "FAILED TO OPEN\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
return info;
|
|
||||||
}
|
|
||||||
|
|
||||||
QString envInfo() {
|
|
||||||
QString info;
|
|
||||||
auto stream = QTextStream(&info);
|
|
||||||
|
|
||||||
for (auto** envp = environ; *envp != nullptr; ++envp) { // NOLINT
|
|
||||||
auto prefixes = std::array<std::string_view, 6> {
|
|
||||||
"QS_",
|
|
||||||
"QT_",
|
|
||||||
"QML_",
|
|
||||||
"QML2_",
|
|
||||||
"QSG_",
|
|
||||||
"XDG_CURRENT_DESKTOP=",
|
|
||||||
};
|
|
||||||
|
|
||||||
for (const auto& prefix: prefixes) {
|
|
||||||
if (strncmp(prefix.data(), *envp, prefix.length()) == 0) goto print;
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
|
|
||||||
print:
|
|
||||||
stream << *envp << '\n';
|
|
||||||
}
|
|
||||||
|
|
||||||
return info;
|
|
||||||
}
|
|
||||||
|
|
||||||
QString combinedInfo() {
|
|
||||||
QString info;
|
|
||||||
auto stream = QTextStream(&info);
|
|
||||||
|
|
||||||
stream << "===== Version Information =====\n";
|
|
||||||
stream << "Quickshell: " << qsVersion() << '\n';
|
|
||||||
stream << "Qt: " << qtVersion() << '\n';
|
|
||||||
|
|
||||||
stream << "\n===== Build Information =====\n";
|
|
||||||
stream << "Build Type: " << BUILD_TYPE << '\n';
|
|
||||||
stream << "Compiler: " << COMPILER << '\n';
|
|
||||||
stream << "Compile Flags: " << COMPILE_FLAGS << '\n';
|
|
||||||
stream << "Configuration:\n" << BUILD_CONFIGURATION << '\n';
|
|
||||||
|
|
||||||
stream << "\n===== System Information =====\n";
|
|
||||||
stream << systemInfo();
|
|
||||||
|
|
||||||
stream << "\n===== Environment (trimmed) =====\n";
|
|
||||||
stream << envInfo();
|
|
||||||
|
|
||||||
return info;
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace qs::debuginfo
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include <qcontainerfwd.h>
|
|
||||||
|
|
||||||
namespace qs::debuginfo {
|
|
||||||
|
|
||||||
QString qsVersion();
|
|
||||||
QString qtVersion();
|
|
||||||
QString gpuInfo();
|
|
||||||
QString systemInfo();
|
|
||||||
QString envInfo();
|
|
||||||
QString combinedInfo();
|
|
||||||
|
|
||||||
} // namespace qs::debuginfo
|
|
||||||
|
|
@ -1,33 +1,26 @@
|
||||||
#include "desktopentry.hpp"
|
#include "desktopentry.hpp"
|
||||||
#include <algorithm>
|
|
||||||
#include <utility>
|
|
||||||
|
|
||||||
#include <qcontainerfwd.h>
|
#include <qcontainerfwd.h>
|
||||||
#include <qdebug.h>
|
#include <qdebug.h>
|
||||||
#include <qdir.h>
|
#include <qdir.h>
|
||||||
#include <qfile.h>
|
|
||||||
#include <qfileinfo.h>
|
#include <qfileinfo.h>
|
||||||
|
#include <qhash.h>
|
||||||
|
#include <qlist.h>
|
||||||
#include <qlogging.h>
|
#include <qlogging.h>
|
||||||
#include <qloggingcategory.h>
|
#include <qloggingcategory.h>
|
||||||
#include <qnamespace.h>
|
#include <qnamespace.h>
|
||||||
#include <qobject.h>
|
#include <qobject.h>
|
||||||
#include <qobjectdefs.h>
|
|
||||||
#include <qpair.h>
|
#include <qpair.h>
|
||||||
#include <qproperty.h>
|
#include <qprocess.h>
|
||||||
#include <qscopeguard.h>
|
#include <qstringview.h>
|
||||||
#include <qtenvironmentvariables.h>
|
#include <qtenvironmentvariables.h>
|
||||||
#include <qthreadpool.h>
|
|
||||||
#include <qtmetamacros.h>
|
|
||||||
#include <ranges>
|
#include <ranges>
|
||||||
|
|
||||||
#include "../io/processcore.hpp"
|
#include "common.hpp"
|
||||||
#include "desktopentrymonitor.hpp"
|
|
||||||
#include "logcat.hpp"
|
|
||||||
#include "model.hpp"
|
#include "model.hpp"
|
||||||
#include "qmlglobal.hpp"
|
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
QS_LOGGING_CATEGORY(logDesktopEntry, "quickshell.desktopentry", QtWarningMsg);
|
Q_LOGGING_CATEGORY(logDesktopEntry, "quickshell.desktopentry", QtWarningMsg);
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Locale {
|
struct Locale {
|
||||||
|
|
@ -61,14 +54,12 @@ struct Locale {
|
||||||
|
|
||||||
[[nodiscard]] int matchScore(const Locale& other) const {
|
[[nodiscard]] int matchScore(const Locale& other) const {
|
||||||
if (this->language != other.language) return 0;
|
if (this->language != other.language) return 0;
|
||||||
|
auto territoryMatches = !this->territory.isEmpty() && this->territory == other.territory;
|
||||||
if (!other.modifier.isEmpty() && this->modifier != other.modifier) return 0;
|
auto modifierMatches = !this->modifier.isEmpty() && this->modifier == other.modifier;
|
||||||
if (!other.territory.isEmpty() && this->territory != other.territory) return 0;
|
|
||||||
|
|
||||||
auto score = 1;
|
auto score = 1;
|
||||||
|
if (territoryMatches) score += 2;
|
||||||
if (!other.territory.isEmpty()) score += 2;
|
if (modifierMatches) score += 1;
|
||||||
if (!other.modifier.isEmpty()) score += 1;
|
|
||||||
|
|
||||||
return score;
|
return score;
|
||||||
}
|
}
|
||||||
|
|
@ -94,64 +85,51 @@ struct Locale {
|
||||||
QDebug operator<<(QDebug debug, const Locale& locale) {
|
QDebug operator<<(QDebug debug, const Locale& locale) {
|
||||||
auto saver = QDebugStateSaver(debug);
|
auto saver = QDebugStateSaver(debug);
|
||||||
debug.nospace() << "Locale(language=" << locale.language << ", territory=" << locale.territory
|
debug.nospace() << "Locale(language=" << locale.language << ", territory=" << locale.territory
|
||||||
<< ", modifier=" << locale.modifier << ')';
|
<< ", modifier" << locale.modifier << ')';
|
||||||
|
|
||||||
return debug;
|
return debug;
|
||||||
}
|
}
|
||||||
|
|
||||||
ParsedDesktopEntryData DesktopEntry::parseText(const QString& id, const QString& text) {
|
void DesktopEntry::parseEntry(const QString& text) {
|
||||||
ParsedDesktopEntryData data;
|
|
||||||
data.id = id;
|
|
||||||
const auto& system = Locale::system();
|
const auto& system = Locale::system();
|
||||||
|
|
||||||
auto groupName = QString();
|
auto groupName = QString();
|
||||||
auto entries = QHash<QString, QPair<Locale, QString>>();
|
auto entries = QHash<QString, QPair<Locale, QString>>();
|
||||||
|
|
||||||
auto actionOrder = QStringList();
|
auto finishCategory = [this, &groupName, &entries]() {
|
||||||
auto pendingActions = QHash<QString, DesktopActionData>();
|
|
||||||
|
|
||||||
auto finishCategory = [&data, &groupName, &entries, &actionOrder, &pendingActions]() {
|
|
||||||
if (groupName == "Desktop Entry") {
|
if (groupName == "Desktop Entry") {
|
||||||
if (entries.value("Type").second != "Application") return;
|
if (entries["Type"].second != "Application") return;
|
||||||
|
if (entries.contains("Hidden") && entries["Hidden"].second == "true") return;
|
||||||
|
|
||||||
for (const auto& [key, pair]: entries.asKeyValueRange()) {
|
for (const auto& [key, pair]: entries.asKeyValueRange()) {
|
||||||
auto& [_, value] = pair;
|
auto& [_, value] = pair;
|
||||||
data.entries.insert(key, value);
|
this->mEntries.insert(key, value);
|
||||||
|
|
||||||
if (key == "Name") data.name = value;
|
if (key == "Name") this->mName = value;
|
||||||
else if (key == "GenericName") data.genericName = value;
|
else if (key == "GenericName") this->mGenericName = value;
|
||||||
else if (key == "StartupWMClass") data.startupClass = value;
|
else if (key == "NoDisplay") this->mNoDisplay = value == "true";
|
||||||
else if (key == "NoDisplay") data.noDisplay = value == "true";
|
else if (key == "Comment") this->mComment = value;
|
||||||
else if (key == "Hidden") data.hidden = value == "true";
|
else if (key == "Icon") this->mIcon = value;
|
||||||
else if (key == "Comment") data.comment = value;
|
else if (key == "Exec") this->mExecString = value;
|
||||||
else if (key == "Icon") data.icon = value;
|
else if (key == "Path") this->mWorkingDirectory = value;
|
||||||
else if (key == "Exec") {
|
else if (key == "Terminal") this->mTerminal = value == "true";
|
||||||
data.execString = value;
|
else if (key == "Categories") this->mCategories = value.split(u';', Qt::SkipEmptyParts);
|
||||||
data.command = DesktopEntry::parseExecString(value);
|
else if (key == "Keywords") this->mKeywords = value.split(u';', Qt::SkipEmptyParts);
|
||||||
} else if (key == "Path") data.workingDirectory = value;
|
|
||||||
else if (key == "Terminal") data.terminal = value == "true";
|
|
||||||
else if (key == "Categories") data.categories = value.split(u';', Qt::SkipEmptyParts);
|
|
||||||
else if (key == "Keywords") data.keywords = value.split(u';', Qt::SkipEmptyParts);
|
|
||||||
else if (key == "Actions") actionOrder = value.split(u';', Qt::SkipEmptyParts);
|
|
||||||
}
|
}
|
||||||
} else if (groupName.startsWith("Desktop Action ")) {
|
} else if (groupName.startsWith("Desktop Action ")) {
|
||||||
auto actionName = groupName.sliced(15);
|
auto actionName = groupName.sliced(16);
|
||||||
DesktopActionData action;
|
auto* action = new DesktopAction(actionName, this);
|
||||||
action.id = actionName;
|
|
||||||
|
|
||||||
for (const auto& [key, pair]: entries.asKeyValueRange()) {
|
for (const auto& [key, pair]: entries.asKeyValueRange()) {
|
||||||
const auto& [_, value] = pair;
|
const auto& [_, value] = pair;
|
||||||
action.entries.insert(key, value);
|
action->mEntries.insert(key, value);
|
||||||
|
|
||||||
if (key == "Name") action.name = value;
|
if (key == "Name") action->mName = value;
|
||||||
else if (key == "Icon") action.icon = value;
|
else if (key == "Icon") action->mIcon = value;
|
||||||
else if (key == "Exec") {
|
else if (key == "Exec") action->mExecString = value;
|
||||||
action.execString = value;
|
|
||||||
action.command = DesktopEntry::parseExecString(value);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pendingActions.insert(actionName, action);
|
this->mActions.insert(actionName, action);
|
||||||
}
|
}
|
||||||
|
|
||||||
entries.clear();
|
entries.clear();
|
||||||
|
|
@ -197,73 +175,16 @@ ParsedDesktopEntryData DesktopEntry::parseText(const QString& id, const QString&
|
||||||
}
|
}
|
||||||
|
|
||||||
finishCategory();
|
finishCategory();
|
||||||
|
|
||||||
for (const auto& actionId: actionOrder) {
|
|
||||||
if (pendingActions.contains(actionId)) {
|
|
||||||
data.actions.append(pendingActions.value(actionId));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
void DesktopEntry::updateState(const ParsedDesktopEntryData& newState) {
|
|
||||||
Qt::beginPropertyUpdateGroup();
|
|
||||||
this->bName = newState.name;
|
|
||||||
this->bGenericName = newState.genericName;
|
|
||||||
this->bStartupClass = newState.startupClass;
|
|
||||||
this->bNoDisplay = newState.noDisplay;
|
|
||||||
this->bComment = newState.comment;
|
|
||||||
this->bIcon = newState.icon;
|
|
||||||
this->bExecString = newState.execString;
|
|
||||||
this->bCommand = newState.command;
|
|
||||||
this->bWorkingDirectory = newState.workingDirectory;
|
|
||||||
this->bRunInTerminal = newState.terminal;
|
|
||||||
this->bCategories = newState.categories;
|
|
||||||
this->bKeywords = newState.keywords;
|
|
||||||
Qt::endPropertyUpdateGroup();
|
|
||||||
|
|
||||||
this->state = newState;
|
|
||||||
this->updateActions(newState.actions);
|
|
||||||
}
|
|
||||||
|
|
||||||
void DesktopEntry::updateActions(const QVector<DesktopActionData>& newActions) {
|
|
||||||
auto old = this->mActions;
|
|
||||||
this->mActions.clear();
|
|
||||||
|
|
||||||
for (const auto& d: newActions) {
|
|
||||||
DesktopAction* act = nullptr;
|
|
||||||
auto found = std::ranges::find(old, d.id, &DesktopAction::mId);
|
|
||||||
if (found != old.end()) {
|
|
||||||
act = *found;
|
|
||||||
old.erase(found);
|
|
||||||
} else {
|
|
||||||
act = new DesktopAction(d.id, this);
|
|
||||||
}
|
|
||||||
|
|
||||||
Qt::beginPropertyUpdateGroup();
|
|
||||||
act->bName = d.name;
|
|
||||||
act->bIcon = d.icon;
|
|
||||||
act->bExecString = d.execString;
|
|
||||||
act->bCommand = d.command;
|
|
||||||
Qt::endPropertyUpdateGroup();
|
|
||||||
|
|
||||||
act->mEntries = d.entries;
|
|
||||||
this->mActions.append(act);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (auto* leftover: old) {
|
|
||||||
leftover->deleteLater();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void DesktopEntry::execute() const {
|
void DesktopEntry::execute() const {
|
||||||
DesktopEntry::doExec(this->bCommand.value(), this->bWorkingDirectory.value());
|
DesktopEntry::doExec(this->mExecString, this->mWorkingDirectory);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool DesktopEntry::isValid() const { return !this->bName.value().isEmpty(); }
|
bool DesktopEntry::isValid() const { return !this->mName.isEmpty(); }
|
||||||
|
bool DesktopEntry::noDisplay() const { return this->mNoDisplay; }
|
||||||
|
|
||||||
QVector<DesktopAction*> DesktopEntry::actions() const { return this->mActions; }
|
QVector<DesktopAction*> DesktopEntry::actions() const { return this->mActions.values(); }
|
||||||
|
|
||||||
QVector<QString> DesktopEntry::parseExecString(const QString& execString) {
|
QVector<QString> DesktopEntry::parseExecString(const QString& execString) {
|
||||||
QVector<QString> arguments;
|
QVector<QString> arguments;
|
||||||
|
|
@ -282,22 +203,16 @@ QVector<QString> DesktopEntry::parseExecString(const QString& execString) {
|
||||||
currentArgument += '\\';
|
currentArgument += '\\';
|
||||||
escape = 0;
|
escape = 0;
|
||||||
}
|
}
|
||||||
} else if (escape == 2) {
|
|
||||||
currentArgument += c;
|
|
||||||
escape = 0;
|
|
||||||
} else if (escape != 0) {
|
} else if (escape != 0) {
|
||||||
switch (c.unicode()) {
|
if (escape != 2) {
|
||||||
case 's': currentArgument += u' '; break;
|
// Technically this is an illegal state, but the spec has a terrible double escape
|
||||||
case 'n': currentArgument += u'\n'; break;
|
// rule in strings for no discernable reason. Assuming someone might understandably
|
||||||
case 't': currentArgument += u'\t'; break;
|
// misunderstand it, treat it as a normal escape and log it.
|
||||||
case 'r': currentArgument += u'\r'; break;
|
|
||||||
case '\\': currentArgument += u'\\'; break;
|
|
||||||
default:
|
|
||||||
qCWarning(logDesktopEntry).noquote()
|
qCWarning(logDesktopEntry).noquote()
|
||||||
<< "Illegal escape sequence in desktop entry exec string:" << execString;
|
<< "Illegal escape sequence in desktop entry exec string:" << execString;
|
||||||
currentArgument += c;
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
currentArgument += c;
|
||||||
escape = 0;
|
escape = 0;
|
||||||
} else if (c == u'"' || c == u'\'') {
|
} else if (c == u'"' || c == u'\'') {
|
||||||
parsingString = false;
|
parsingString = false;
|
||||||
|
|
@ -335,52 +250,75 @@ QVector<QString> DesktopEntry::parseExecString(const QString& execString) {
|
||||||
return arguments;
|
return arguments;
|
||||||
}
|
}
|
||||||
|
|
||||||
void DesktopEntry::doExec(const QList<QString>& execString, const QString& workingDirectory) {
|
void DesktopEntry::doExec(const QString& execString, const QString& workingDirectory) {
|
||||||
qs::io::process::ProcessContext ctx;
|
auto args = DesktopEntry::parseExecString(execString);
|
||||||
ctx.setCommand(execString);
|
if (args.isEmpty()) {
|
||||||
ctx.setWorkingDirectory(workingDirectory);
|
qCWarning(logDesktopEntry) << "Tried to exec string" << execString << "which parsed as empty.";
|
||||||
QuickshellGlobal::execDetached(ctx);
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto process = QProcess();
|
||||||
|
process.setProgram(args.at(0));
|
||||||
|
process.setArguments(args.sliced(1));
|
||||||
|
if (!workingDirectory.isEmpty()) process.setWorkingDirectory(workingDirectory);
|
||||||
|
process.setProcessEnvironment(qs::Common::INITIAL_ENVIRONMENT);
|
||||||
|
process.startDetached();
|
||||||
}
|
}
|
||||||
|
|
||||||
void DesktopAction::execute() const {
|
void DesktopAction::execute() const {
|
||||||
DesktopEntry::doExec(this->bCommand.value(), this->entry->bWorkingDirectory.value());
|
DesktopEntry::doExec(this->mExecString, this->entry->mWorkingDirectory);
|
||||||
}
|
}
|
||||||
|
|
||||||
DesktopEntryScanner::DesktopEntryScanner(DesktopEntryManager* manager): manager(manager) {
|
DesktopEntryManager::DesktopEntryManager() {
|
||||||
this->setAutoDelete(true);
|
this->scanDesktopEntries();
|
||||||
|
this->populateApplications();
|
||||||
}
|
}
|
||||||
|
|
||||||
void DesktopEntryScanner::run() {
|
void DesktopEntryManager::scanDesktopEntries() {
|
||||||
const auto& desktopPaths = DesktopEntryManager::desktopPaths();
|
QList<QString> dataPaths;
|
||||||
auto scanResults = QList<ParsedDesktopEntryData>();
|
|
||||||
|
|
||||||
for (const auto& path: desktopPaths | std::views::reverse) {
|
if (qEnvironmentVariableIsSet("XDG_DATA_HOME")) {
|
||||||
auto file = QFileInfo(path);
|
dataPaths.push_back(qEnvironmentVariable("XDG_DATA_HOME"));
|
||||||
if (!file.isDir()) continue;
|
} else if (qEnvironmentVariableIsSet("HOME")) {
|
||||||
|
dataPaths.push_back(qEnvironmentVariable("HOME") + "/.local/share");
|
||||||
this->scanDirectory(QDir(path), QString(), scanResults);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
QMetaObject::invokeMethod(
|
if (qEnvironmentVariableIsSet("XDG_DATA_DIRS")) {
|
||||||
this->manager,
|
auto var = qEnvironmentVariable("XDG_DATA_DIRS");
|
||||||
"onScanCompleted",
|
dataPaths += var.split(u':', Qt::SkipEmptyParts);
|
||||||
Qt::QueuedConnection,
|
} else {
|
||||||
Q_ARG(QList<ParsedDesktopEntryData>, scanResults)
|
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 DesktopEntryScanner::scanDirectory(
|
void DesktopEntryManager::populateApplications() {
|
||||||
const QDir& dir,
|
for (auto& entry: this->desktopEntries.values()) {
|
||||||
const QString& idPrefix,
|
if (!entry->noDisplay()) this->mApplications.insertObject(entry);
|
||||||
QList<ParsedDesktopEntryData>& entries
|
}
|
||||||
) {
|
}
|
||||||
auto dirEntries = dir.entryInfoList(QDir::Dirs | QDir::Files | QDir::NoDotAndDotDot);
|
|
||||||
|
|
||||||
for (auto& entry: dirEntries) {
|
void DesktopEntryManager::scanPath(const QDir& dir, const QString& prefix) {
|
||||||
if (entry.isDir()) {
|
auto entries = dir.entryInfoList(QDir::Dirs | QDir::Files | QDir::NoDotAndDotDot);
|
||||||
auto subdirPrefix = idPrefix.isEmpty() ? entry.fileName() : idPrefix + '-' + entry.fileName();
|
|
||||||
this->scanDirectory(QDir(entry.absoluteFilePath()), subdirPrefix, entries);
|
for (auto& entry: entries) {
|
||||||
} else if (entry.isFile()) {
|
if (entry.isDir()) this->scanPath(entry.absoluteFilePath(), prefix + dir.dirName() + "-");
|
||||||
|
else if (entry.isFile()) {
|
||||||
auto path = entry.filePath();
|
auto path = entry.filePath();
|
||||||
if (!path.endsWith(".desktop")) {
|
if (!path.endsWith(".desktop")) {
|
||||||
qCDebug(logDesktopEntry) << "Skipping file" << path << "as it has no .desktop extension";
|
qCDebug(logDesktopEntry) << "Skipping file" << path << "as it has no .desktop extension";
|
||||||
|
|
@ -393,42 +331,46 @@ void DesktopEntryScanner::scanDirectory(
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto basename = QFileInfo(entry.fileName()).completeBaseName();
|
auto id = prefix + entry.fileName().sliced(0, entry.fileName().length() - 8);
|
||||||
auto id = idPrefix.isEmpty() ? basename : idPrefix + '-' + basename;
|
auto lowerId = id.toLower();
|
||||||
auto content = QString::fromUtf8(file.readAll());
|
|
||||||
|
|
||||||
auto data = DesktopEntry::parseText(id, content);
|
auto text = QString::fromUtf8(file.readAll());
|
||||||
entries.append(std::move(data));
|
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(): monitor(new DesktopEntryMonitor(this)) {
|
|
||||||
QObject::connect(
|
|
||||||
this->monitor,
|
|
||||||
&DesktopEntryMonitor::desktopEntriesChanged,
|
|
||||||
this,
|
|
||||||
&DesktopEntryManager::handleFileChanges
|
|
||||||
);
|
|
||||||
|
|
||||||
DesktopEntryScanner(this).run();
|
|
||||||
}
|
|
||||||
|
|
||||||
void DesktopEntryManager::scanDesktopEntries() {
|
|
||||||
qCDebug(logDesktopEntry) << "Starting desktop entry scan";
|
|
||||||
|
|
||||||
if (this->scanInProgress) {
|
|
||||||
qCDebug(logDesktopEntry) << "Scan already in progress, queuing another scan";
|
|
||||||
this->scanQueued = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this->scanInProgress = true;
|
|
||||||
this->scanQueued = false;
|
|
||||||
auto* scanner = new DesktopEntryScanner(this);
|
|
||||||
QThreadPool::globalInstance()->start(scanner);
|
|
||||||
}
|
|
||||||
|
|
||||||
DesktopEntryManager* DesktopEntryManager::instance() {
|
DesktopEntryManager* DesktopEntryManager::instance() {
|
||||||
static auto* instance = new DesktopEntryManager(); // NOLINT
|
static auto* instance = new DesktopEntryManager(); // NOLINT
|
||||||
return instance;
|
return instance;
|
||||||
|
|
@ -444,167 +386,14 @@ DesktopEntry* DesktopEntryManager::byId(const QString& id) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
DesktopEntry* DesktopEntryManager::heuristicLookup(const QString& name) {
|
|
||||||
if (auto* entry = this->byId(name)) return entry;
|
|
||||||
|
|
||||||
auto list = this->desktopEntries.values();
|
|
||||||
|
|
||||||
auto iter = std::ranges::find_if(list, [&](DesktopEntry* entry) {
|
|
||||||
return name == entry->bStartupClass.value();
|
|
||||||
});
|
|
||||||
|
|
||||||
if (iter != list.end()) return *iter;
|
|
||||||
|
|
||||||
iter = std::ranges::find_if(list, [&](DesktopEntry* entry) {
|
|
||||||
return name.toLower() == entry->bStartupClass.value().toLower();
|
|
||||||
});
|
|
||||||
|
|
||||||
if (iter != list.end()) return *iter;
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
ObjectModel<DesktopEntry>* DesktopEntryManager::applications() { return &this->mApplications; }
|
ObjectModel<DesktopEntry>* DesktopEntryManager::applications() { return &this->mApplications; }
|
||||||
|
|
||||||
void DesktopEntryManager::handleFileChanges() {
|
DesktopEntries::DesktopEntries() { DesktopEntryManager::instance(); }
|
||||||
qCDebug(logDesktopEntry) << "Directory change detected, performing full rescan";
|
|
||||||
|
|
||||||
if (this->scanInProgress) {
|
|
||||||
qCDebug(logDesktopEntry) << "Scan already in progress, queuing another scan";
|
|
||||||
this->scanQueued = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this->scanInProgress = true;
|
|
||||||
this->scanQueued = false;
|
|
||||||
auto* scanner = new DesktopEntryScanner(this);
|
|
||||||
QThreadPool::globalInstance()->start(scanner);
|
|
||||||
}
|
|
||||||
|
|
||||||
const QStringList& DesktopEntryManager::desktopPaths() {
|
|
||||||
static const auto paths = []() {
|
|
||||||
auto dataPaths = QStringList();
|
|
||||||
|
|
||||||
auto dataHome = qEnvironmentVariable("XDG_DATA_HOME");
|
|
||||||
if (dataHome.isEmpty() && qEnvironmentVariableIsSet("HOME"))
|
|
||||||
dataHome = qEnvironmentVariable("HOME") + "/.local/share";
|
|
||||||
if (!dataHome.isEmpty()) dataPaths.append(dataHome + "/applications");
|
|
||||||
|
|
||||||
auto dataDirs = qEnvironmentVariable("XDG_DATA_DIRS");
|
|
||||||
if (dataDirs.isEmpty()) dataDirs = "/usr/local/share:/usr/share";
|
|
||||||
|
|
||||||
for (const auto& dir: dataDirs.split(':', Qt::SkipEmptyParts)) {
|
|
||||||
dataPaths.append(dir + "/applications");
|
|
||||||
}
|
|
||||||
|
|
||||||
return dataPaths;
|
|
||||||
}();
|
|
||||||
|
|
||||||
return paths;
|
|
||||||
}
|
|
||||||
|
|
||||||
void DesktopEntryManager::onScanCompleted(const QList<ParsedDesktopEntryData>& scanResults) {
|
|
||||||
auto guard = qScopeGuard([this] {
|
|
||||||
this->scanInProgress = false;
|
|
||||||
if (this->scanQueued) {
|
|
||||||
this->scanQueued = false;
|
|
||||||
this->scanDesktopEntries();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
auto oldEntries = this->desktopEntries;
|
|
||||||
auto newEntries = QHash<QString, DesktopEntry*>();
|
|
||||||
auto newLowercaseEntries = QHash<QString, DesktopEntry*>();
|
|
||||||
|
|
||||||
for (const auto& data: scanResults) {
|
|
||||||
auto lowerId = data.id.toLower();
|
|
||||||
|
|
||||||
if (data.hidden) {
|
|
||||||
if (auto* victim = newEntries.take(data.id)) victim->deleteLater();
|
|
||||||
newLowercaseEntries.remove(lowerId);
|
|
||||||
|
|
||||||
if (auto it = oldEntries.find(data.id); it != oldEntries.end()) {
|
|
||||||
it.value()->deleteLater();
|
|
||||||
oldEntries.erase(it);
|
|
||||||
}
|
|
||||||
|
|
||||||
qCDebug(logDesktopEntry) << "Masking hidden desktop entry" << data.id;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
DesktopEntry* dentry = nullptr;
|
|
||||||
|
|
||||||
if (auto it = oldEntries.find(data.id); it != oldEntries.end()) {
|
|
||||||
dentry = it.value();
|
|
||||||
oldEntries.erase(it);
|
|
||||||
dentry->updateState(data);
|
|
||||||
} else {
|
|
||||||
dentry = new DesktopEntry(data.id, this);
|
|
||||||
dentry->updateState(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!dentry->isValid()) {
|
|
||||||
qCDebug(logDesktopEntry) << "Skipping desktop entry" << data.id;
|
|
||||||
if (!oldEntries.contains(data.id)) {
|
|
||||||
dentry->deleteLater();
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
qCDebug(logDesktopEntry) << "Found desktop entry" << data.id;
|
|
||||||
|
|
||||||
auto conflictingId = newEntries.contains(data.id);
|
|
||||||
|
|
||||||
if (conflictingId) {
|
|
||||||
qCDebug(logDesktopEntry) << "Replacing old entry for" << data.id;
|
|
||||||
if (auto* victim = newEntries.take(data.id)) victim->deleteLater();
|
|
||||||
newLowercaseEntries.remove(lowerId);
|
|
||||||
}
|
|
||||||
|
|
||||||
newEntries.insert(data.id, dentry);
|
|
||||||
|
|
||||||
if (newLowercaseEntries.contains(lowerId)) {
|
|
||||||
qCInfo(logDesktopEntry).nospace()
|
|
||||||
<< "Multiple desktop entries have the same lowercased id " << lowerId
|
|
||||||
<< ". This can cause ambiguity when byId requests are not made with the correct case "
|
|
||||||
"already.";
|
|
||||||
|
|
||||||
newLowercaseEntries.remove(lowerId);
|
|
||||||
}
|
|
||||||
|
|
||||||
newLowercaseEntries.insert(lowerId, dentry);
|
|
||||||
}
|
|
||||||
|
|
||||||
this->desktopEntries = newEntries;
|
|
||||||
this->lowercaseDesktopEntries = newLowercaseEntries;
|
|
||||||
|
|
||||||
auto newApplications = QVector<DesktopEntry*>();
|
|
||||||
for (auto* entry: this->desktopEntries.values())
|
|
||||||
if (!entry->bNoDisplay) newApplications.append(entry);
|
|
||||||
|
|
||||||
this->mApplications.diffUpdate(newApplications);
|
|
||||||
|
|
||||||
emit this->applicationsChanged();
|
|
||||||
|
|
||||||
for (auto* e: oldEntries) e->deleteLater();
|
|
||||||
}
|
|
||||||
|
|
||||||
DesktopEntries::DesktopEntries() {
|
|
||||||
QObject::connect(
|
|
||||||
DesktopEntryManager::instance(),
|
|
||||||
&DesktopEntryManager::applicationsChanged,
|
|
||||||
this,
|
|
||||||
&DesktopEntries::applicationsChanged
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
DesktopEntry* DesktopEntries::byId(const QString& id) {
|
DesktopEntry* DesktopEntries::byId(const QString& id) {
|
||||||
return DesktopEntryManager::instance()->byId(id);
|
return DesktopEntryManager::instance()->byId(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
DesktopEntry* DesktopEntries::heuristicLookup(const QString& name) {
|
|
||||||
return DesktopEntryManager::instance()->heuristicLookup(name);
|
|
||||||
}
|
|
||||||
|
|
||||||
ObjectModel<DesktopEntry>* DesktopEntries::applications() {
|
ObjectModel<DesktopEntry>* DesktopEntries::applications() {
|
||||||
return DesktopEntryManager::instance()->applications();
|
return DesktopEntryManager::instance()->applications();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,84 +6,36 @@
|
||||||
#include <qdir.h>
|
#include <qdir.h>
|
||||||
#include <qhash.h>
|
#include <qhash.h>
|
||||||
#include <qobject.h>
|
#include <qobject.h>
|
||||||
#include <qproperty.h>
|
|
||||||
#include <qqmlintegration.h>
|
#include <qqmlintegration.h>
|
||||||
#include <qrunnable.h>
|
|
||||||
#include <qtmetamacros.h>
|
#include <qtmetamacros.h>
|
||||||
|
|
||||||
#include "desktopentrymonitor.hpp"
|
|
||||||
#include "doc.hpp"
|
#include "doc.hpp"
|
||||||
#include "model.hpp"
|
#include "model.hpp"
|
||||||
|
|
||||||
class DesktopAction;
|
class DesktopAction;
|
||||||
class DesktopEntryMonitor;
|
|
||||||
|
|
||||||
struct DesktopActionData {
|
|
||||||
QString id;
|
|
||||||
QString name;
|
|
||||||
QString icon;
|
|
||||||
QString execString;
|
|
||||||
QVector<QString> command;
|
|
||||||
QHash<QString, QString> entries;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct ParsedDesktopEntryData {
|
|
||||||
QString id;
|
|
||||||
QString name;
|
|
||||||
QString genericName;
|
|
||||||
QString startupClass;
|
|
||||||
bool noDisplay = false;
|
|
||||||
bool hidden = false;
|
|
||||||
QString comment;
|
|
||||||
QString icon;
|
|
||||||
QString execString;
|
|
||||||
QVector<QString> command;
|
|
||||||
QString workingDirectory;
|
|
||||||
bool terminal = false;
|
|
||||||
QVector<QString> categories;
|
|
||||||
QVector<QString> keywords;
|
|
||||||
QHash<QString, QString> entries;
|
|
||||||
QVector<DesktopActionData> actions;
|
|
||||||
};
|
|
||||||
|
|
||||||
/// A desktop entry. See @@DesktopEntries for details.
|
/// A desktop entry. See @@DesktopEntries for details.
|
||||||
class DesktopEntry: public QObject {
|
class DesktopEntry: public QObject {
|
||||||
Q_OBJECT;
|
Q_OBJECT;
|
||||||
Q_PROPERTY(QString id MEMBER mId CONSTANT);
|
Q_PROPERTY(QString id MEMBER mId CONSTANT);
|
||||||
/// Name of the specific application, such as "Firefox".
|
/// Name of the specific application, such as "Firefox".
|
||||||
// clang-format off
|
Q_PROPERTY(QString name MEMBER mName CONSTANT);
|
||||||
Q_PROPERTY(QString name READ default WRITE default NOTIFY nameChanged BINDABLE bindableName);
|
|
||||||
/// Short description of the application, such as "Web Browser". May be empty.
|
/// Short description of the application, such as "Web Browser". May be empty.
|
||||||
Q_PROPERTY(QString genericName READ default WRITE default NOTIFY genericNameChanged BINDABLE bindableGenericName);
|
Q_PROPERTY(QString genericName MEMBER mGenericName CONSTANT);
|
||||||
/// Initial class or app id the app intends to use. May be useful for matching running apps
|
|
||||||
/// to desktop entries.
|
|
||||||
Q_PROPERTY(QString startupClass READ default WRITE default NOTIFY startupClassChanged BINDABLE bindableStartupClass);
|
|
||||||
/// If true, this application should not be displayed in menus and launchers.
|
/// If true, this application should not be displayed in menus and launchers.
|
||||||
Q_PROPERTY(bool noDisplay READ default WRITE default NOTIFY noDisplayChanged BINDABLE bindableNoDisplay);
|
Q_PROPERTY(bool noDisplay MEMBER mNoDisplay CONSTANT);
|
||||||
/// Long description of the application, such as "View websites on the internet". May be empty.
|
/// Long description of the application, such as "View websites on the internet". May be empty.
|
||||||
Q_PROPERTY(QString comment READ default WRITE default NOTIFY commentChanged BINDABLE bindableComment);
|
Q_PROPERTY(QString comment MEMBER mComment CONSTANT);
|
||||||
/// Name of the icon associated with this application. May be empty.
|
/// Name of the icon associated with this application. May be empty.
|
||||||
Q_PROPERTY(QString icon READ default WRITE default NOTIFY iconChanged BINDABLE bindableIcon);
|
Q_PROPERTY(QString icon MEMBER mIcon CONSTANT);
|
||||||
/// The raw `Exec` string from the desktop entry.
|
/// The raw `Exec` string from the desktop entry. You probably want @@execute().
|
||||||
///
|
Q_PROPERTY(QString execString MEMBER mExecString CONSTANT);
|
||||||
/// > [!WARNING] This cannot be reliably run as a command. See @@command for one you can run.
|
|
||||||
Q_PROPERTY(QString execString READ default WRITE default NOTIFY execStringChanged BINDABLE bindableExecString);
|
|
||||||
/// The parsed `Exec` command in the desktop entry.
|
|
||||||
///
|
|
||||||
/// The entry can be run with @@execute(), or by using this command in
|
|
||||||
/// @@Quickshell.Quickshell.execDetached() or @@Quickshell.Io.Process.
|
|
||||||
/// If used in `execDetached` or a `Process`, @@workingDirectory should also be passed to
|
|
||||||
/// the invoked process. See @@execute() for details.
|
|
||||||
///
|
|
||||||
/// > [!NOTE] The provided command does not invoke a terminal even if @@runInTerminal is true.
|
|
||||||
Q_PROPERTY(QVector<QString> command READ default WRITE default NOTIFY commandChanged BINDABLE bindableCommand);
|
|
||||||
/// The working directory to execute from.
|
/// The working directory to execute from.
|
||||||
Q_PROPERTY(QString workingDirectory READ default WRITE default NOTIFY workingDirectoryChanged BINDABLE bindableWorkingDirectory);
|
Q_PROPERTY(QString workingDirectory MEMBER mWorkingDirectory CONSTANT);
|
||||||
/// If the application should run in a terminal.
|
/// If the application should run in a terminal.
|
||||||
Q_PROPERTY(bool runInTerminal READ default WRITE default NOTIFY runInTerminalChanged BINDABLE bindableRunInTerminal);
|
Q_PROPERTY(bool runInTerminal MEMBER mTerminal CONSTANT);
|
||||||
Q_PROPERTY(QVector<QString> categories READ default WRITE default NOTIFY categoriesChanged BINDABLE bindableCategories);
|
Q_PROPERTY(QVector<QString> categories MEMBER mCategories CONSTANT);
|
||||||
Q_PROPERTY(QVector<QString> keywords READ default WRITE default NOTIFY keywordsChanged BINDABLE bindableKeywords);
|
Q_PROPERTY(QVector<QString> keywords MEMBER mKeywords CONSTANT);
|
||||||
// clang-format on
|
|
||||||
Q_PROPERTY(QVector<DesktopAction*> actions READ actions CONSTANT);
|
Q_PROPERTY(QVector<DesktopAction*> actions READ actions CONSTANT);
|
||||||
QML_ELEMENT;
|
QML_ELEMENT;
|
||||||
QML_UNCREATABLE("DesktopEntry instances must be retrieved from DesktopEntries");
|
QML_UNCREATABLE("DesktopEntry instances must be retrieved from DesktopEntries");
|
||||||
|
|
@ -91,83 +43,35 @@ class DesktopEntry: public QObject {
|
||||||
public:
|
public:
|
||||||
explicit DesktopEntry(QString id, QObject* parent): QObject(parent), mId(std::move(id)) {}
|
explicit DesktopEntry(QString id, QObject* parent): QObject(parent), mId(std::move(id)) {}
|
||||||
|
|
||||||
static ParsedDesktopEntryData parseText(const QString& id, const QString& text);
|
void parseEntry(const QString& text);
|
||||||
void updateState(const ParsedDesktopEntryData& newState);
|
|
||||||
|
|
||||||
/// Run the application. Currently ignores @@runInTerminal and field codes.
|
/// Run the application. Currently ignores @@runInTerminal and field codes.
|
||||||
///
|
|
||||||
/// This is equivalent to calling @@Quickshell.Quickshell.execDetached() with @@command
|
|
||||||
/// and @@DesktopEntry.workingDirectory as shown below:
|
|
||||||
///
|
|
||||||
/// ```qml
|
|
||||||
/// Quickshell.execDetached({
|
|
||||||
/// command: desktopEntry.command,
|
|
||||||
/// workingDirectory: desktopEntry.workingDirectory,
|
|
||||||
/// });
|
|
||||||
/// ```
|
|
||||||
Q_INVOKABLE void execute() const;
|
Q_INVOKABLE void execute() const;
|
||||||
|
|
||||||
[[nodiscard]] bool isValid() const;
|
[[nodiscard]] bool isValid() const;
|
||||||
|
[[nodiscard]] bool noDisplay() const;
|
||||||
[[nodiscard]] QVector<DesktopAction*> actions() const;
|
[[nodiscard]] QVector<DesktopAction*> actions() const;
|
||||||
|
|
||||||
[[nodiscard]] QBindable<QString> bindableName() const { return &this->bName; }
|
|
||||||
[[nodiscard]] QBindable<QString> bindableGenericName() const { return &this->bGenericName; }
|
|
||||||
[[nodiscard]] QBindable<QString> bindableStartupClass() const { return &this->bStartupClass; }
|
|
||||||
[[nodiscard]] QBindable<bool> bindableNoDisplay() const { return &this->bNoDisplay; }
|
|
||||||
[[nodiscard]] QBindable<QString> bindableComment() const { return &this->bComment; }
|
|
||||||
[[nodiscard]] QBindable<QString> bindableIcon() const { return &this->bIcon; }
|
|
||||||
[[nodiscard]] QBindable<QString> bindableExecString() const { return &this->bExecString; }
|
|
||||||
[[nodiscard]] QBindable<QVector<QString>> bindableCommand() const { return &this->bCommand; }
|
|
||||||
[[nodiscard]] QBindable<QString> bindableWorkingDirectory() const {
|
|
||||||
return &this->bWorkingDirectory;
|
|
||||||
}
|
|
||||||
[[nodiscard]] QBindable<bool> bindableRunInTerminal() const { return &this->bRunInTerminal; }
|
|
||||||
[[nodiscard]] QBindable<QVector<QString>> bindableCategories() const {
|
|
||||||
return &this->bCategories;
|
|
||||||
}
|
|
||||||
[[nodiscard]] QBindable<QVector<QString>> bindableKeywords() const { return &this->bKeywords; }
|
|
||||||
|
|
||||||
// currently ignores all field codes.
|
// currently ignores all field codes.
|
||||||
static QVector<QString> parseExecString(const QString& execString);
|
static QVector<QString> parseExecString(const QString& execString);
|
||||||
static void doExec(const QList<QString>& execString, const QString& workingDirectory);
|
static void doExec(const QString& execString, const QString& workingDirectory);
|
||||||
|
|
||||||
signals:
|
|
||||||
void nameChanged();
|
|
||||||
void genericNameChanged();
|
|
||||||
void startupClassChanged();
|
|
||||||
void noDisplayChanged();
|
|
||||||
void commentChanged();
|
|
||||||
void iconChanged();
|
|
||||||
void execStringChanged();
|
|
||||||
void commandChanged();
|
|
||||||
void workingDirectoryChanged();
|
|
||||||
void runInTerminalChanged();
|
|
||||||
void categoriesChanged();
|
|
||||||
void keywordsChanged();
|
|
||||||
|
|
||||||
public:
|
public:
|
||||||
QString mId;
|
QString mId;
|
||||||
|
QString mName;
|
||||||
// clang-format off
|
QString mGenericName;
|
||||||
Q_OBJECT_BINDABLE_PROPERTY(DesktopEntry, QString, bName, &DesktopEntry::nameChanged);
|
bool mNoDisplay = false;
|
||||||
Q_OBJECT_BINDABLE_PROPERTY(DesktopEntry, QString, bGenericName, &DesktopEntry::genericNameChanged);
|
QString mComment;
|
||||||
Q_OBJECT_BINDABLE_PROPERTY(DesktopEntry, QString, bStartupClass, &DesktopEntry::startupClassChanged);
|
QString mIcon;
|
||||||
Q_OBJECT_BINDABLE_PROPERTY(DesktopEntry, bool, bNoDisplay, &DesktopEntry::noDisplayChanged);
|
QString mExecString;
|
||||||
Q_OBJECT_BINDABLE_PROPERTY(DesktopEntry, QString, bComment, &DesktopEntry::commentChanged);
|
QString mWorkingDirectory;
|
||||||
Q_OBJECT_BINDABLE_PROPERTY(DesktopEntry, QString, bIcon, &DesktopEntry::iconChanged);
|
bool mTerminal = false;
|
||||||
Q_OBJECT_BINDABLE_PROPERTY(DesktopEntry, QString, bExecString, &DesktopEntry::execStringChanged);
|
QVector<QString> mCategories;
|
||||||
Q_OBJECT_BINDABLE_PROPERTY(DesktopEntry, QVector<QString>, bCommand, &DesktopEntry::commandChanged);
|
QVector<QString> mKeywords;
|
||||||
Q_OBJECT_BINDABLE_PROPERTY(DesktopEntry, QString, bWorkingDirectory, &DesktopEntry::workingDirectoryChanged);
|
|
||||||
Q_OBJECT_BINDABLE_PROPERTY(DesktopEntry, bool, bRunInTerminal, &DesktopEntry::runInTerminalChanged);
|
|
||||||
Q_OBJECT_BINDABLE_PROPERTY(DesktopEntry, QVector<QString>, bCategories, &DesktopEntry::categoriesChanged);
|
|
||||||
Q_OBJECT_BINDABLE_PROPERTY(DesktopEntry, QVector<QString>, bKeywords, &DesktopEntry::keywordsChanged);
|
|
||||||
// clang-format on
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void updateActions(const QVector<DesktopActionData>& newActions);
|
QHash<QString, QString> mEntries;
|
||||||
|
QHash<QString, DesktopAction*> mActions;
|
||||||
ParsedDesktopEntryData state;
|
|
||||||
QVector<DesktopAction*> mActions;
|
|
||||||
|
|
||||||
friend class DesktopAction;
|
friend class DesktopAction;
|
||||||
};
|
};
|
||||||
|
|
@ -176,23 +80,10 @@ private:
|
||||||
class DesktopAction: public QObject {
|
class DesktopAction: public QObject {
|
||||||
Q_OBJECT;
|
Q_OBJECT;
|
||||||
Q_PROPERTY(QString id MEMBER mId CONSTANT);
|
Q_PROPERTY(QString id MEMBER mId CONSTANT);
|
||||||
// clang-format off
|
Q_PROPERTY(QString name MEMBER mName CONSTANT);
|
||||||
Q_PROPERTY(QString name READ default WRITE default NOTIFY nameChanged BINDABLE bindableName);
|
Q_PROPERTY(QString icon MEMBER mIcon CONSTANT);
|
||||||
Q_PROPERTY(QString icon READ default WRITE default NOTIFY iconChanged BINDABLE bindableIcon);
|
/// The raw `Exec` string from the desktop entry. You probably want @@execute().
|
||||||
/// The raw `Exec` string from the action.
|
Q_PROPERTY(QString execString MEMBER mExecString CONSTANT);
|
||||||
///
|
|
||||||
/// > [!WARNING] This cannot be reliably run as a command. See @@command for one you can run.
|
|
||||||
Q_PROPERTY(QString execString READ default WRITE default NOTIFY execStringChanged BINDABLE bindableExecString);
|
|
||||||
/// The parsed `Exec` command in the action.
|
|
||||||
///
|
|
||||||
/// The entry can be run with @@execute(), or by using this command in
|
|
||||||
/// @@Quickshell.Quickshell.execDetached() or @@Quickshell.Io.Process.
|
|
||||||
/// If used in `execDetached` or a `Process`, @@DesktopEntry.workingDirectory should also be passed to
|
|
||||||
/// the invoked process.
|
|
||||||
///
|
|
||||||
/// > [!NOTE] The provided command does not invoke a terminal even if @@runInTerminal is true.
|
|
||||||
Q_PROPERTY(QVector<QString> command READ default WRITE default NOTIFY commandChanged BINDABLE bindableCommand);
|
|
||||||
// clang-format on
|
|
||||||
QML_ELEMENT;
|
QML_ELEMENT;
|
||||||
QML_UNCREATABLE("DesktopAction instances must be retrieved from a DesktopEntry");
|
QML_UNCREATABLE("DesktopAction instances must be retrieved from a DesktopEntry");
|
||||||
|
|
||||||
|
|
@ -203,52 +94,19 @@ public:
|
||||||
, mId(std::move(id)) {}
|
, mId(std::move(id)) {}
|
||||||
|
|
||||||
/// Run the application. Currently ignores @@DesktopEntry.runInTerminal and field codes.
|
/// Run the application. Currently ignores @@DesktopEntry.runInTerminal and field codes.
|
||||||
///
|
|
||||||
/// This is equivalent to calling @@Quickshell.Quickshell.execDetached() with @@command
|
|
||||||
/// and @@DesktopEntry.workingDirectory.
|
|
||||||
Q_INVOKABLE void execute() const;
|
Q_INVOKABLE void execute() const;
|
||||||
|
|
||||||
[[nodiscard]] QBindable<QString> bindableName() const { return &this->bName; }
|
|
||||||
[[nodiscard]] QBindable<QString> bindableIcon() const { return &this->bIcon; }
|
|
||||||
[[nodiscard]] QBindable<QString> bindableExecString() const { return &this->bExecString; }
|
|
||||||
[[nodiscard]] QBindable<QVector<QString>> bindableCommand() const { return &this->bCommand; }
|
|
||||||
|
|
||||||
signals:
|
|
||||||
void nameChanged();
|
|
||||||
void iconChanged();
|
|
||||||
void execStringChanged();
|
|
||||||
void commandChanged();
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
DesktopEntry* entry;
|
DesktopEntry* entry;
|
||||||
QString mId;
|
QString mId;
|
||||||
|
QString mName;
|
||||||
|
QString mIcon;
|
||||||
|
QString mExecString;
|
||||||
QHash<QString, QString> mEntries;
|
QHash<QString, QString> mEntries;
|
||||||
|
|
||||||
// clang-format off
|
|
||||||
Q_OBJECT_BINDABLE_PROPERTY(DesktopAction, QString, bName, &DesktopAction::nameChanged);
|
|
||||||
Q_OBJECT_BINDABLE_PROPERTY(DesktopAction, QString, bIcon, &DesktopAction::iconChanged);
|
|
||||||
Q_OBJECT_BINDABLE_PROPERTY(DesktopAction, QString, bExecString, &DesktopAction::execStringChanged);
|
|
||||||
Q_OBJECT_BINDABLE_PROPERTY(DesktopAction, QVector<QString>, bCommand, &DesktopAction::commandChanged);
|
|
||||||
// clang-format on
|
|
||||||
|
|
||||||
friend class DesktopEntry;
|
friend class DesktopEntry;
|
||||||
};
|
};
|
||||||
|
|
||||||
class DesktopEntryManager;
|
|
||||||
|
|
||||||
class DesktopEntryScanner: public QRunnable {
|
|
||||||
public:
|
|
||||||
explicit DesktopEntryScanner(DesktopEntryManager* manager);
|
|
||||||
|
|
||||||
void run() override;
|
|
||||||
// clang-format off
|
|
||||||
void scanDirectory(const QDir& dir, const QString& idPrefix, QList<ParsedDesktopEntryData>& entries);
|
|
||||||
// clang-format on
|
|
||||||
|
|
||||||
private:
|
|
||||||
DesktopEntryManager* manager;
|
|
||||||
};
|
|
||||||
|
|
||||||
class DesktopEntryManager: public QObject {
|
class DesktopEntryManager: public QObject {
|
||||||
Q_OBJECT;
|
Q_OBJECT;
|
||||||
|
|
||||||
|
|
@ -256,32 +114,20 @@ public:
|
||||||
void scanDesktopEntries();
|
void scanDesktopEntries();
|
||||||
|
|
||||||
[[nodiscard]] DesktopEntry* byId(const QString& id);
|
[[nodiscard]] DesktopEntry* byId(const QString& id);
|
||||||
[[nodiscard]] DesktopEntry* heuristicLookup(const QString& name);
|
|
||||||
|
|
||||||
[[nodiscard]] ObjectModel<DesktopEntry>* applications();
|
[[nodiscard]] ObjectModel<DesktopEntry>* applications();
|
||||||
|
|
||||||
static DesktopEntryManager* instance();
|
static DesktopEntryManager* instance();
|
||||||
|
|
||||||
static const QStringList& desktopPaths();
|
|
||||||
|
|
||||||
signals:
|
|
||||||
void applicationsChanged();
|
|
||||||
|
|
||||||
private slots:
|
|
||||||
void handleFileChanges();
|
|
||||||
void onScanCompleted(const QList<ParsedDesktopEntryData>& scanResults);
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
explicit DesktopEntryManager();
|
explicit DesktopEntryManager();
|
||||||
|
|
||||||
|
void populateApplications();
|
||||||
|
void scanPath(const QDir& dir, const QString& prefix = QString());
|
||||||
|
|
||||||
QHash<QString, DesktopEntry*> desktopEntries;
|
QHash<QString, DesktopEntry*> desktopEntries;
|
||||||
QHash<QString, DesktopEntry*> lowercaseDesktopEntries;
|
QHash<QString, DesktopEntry*> lowercaseDesktopEntries;
|
||||||
ObjectModel<DesktopEntry> mApplications {this};
|
ObjectModel<DesktopEntry> mApplications {this};
|
||||||
DesktopEntryMonitor* monitor = nullptr;
|
|
||||||
bool scanInProgress = false;
|
|
||||||
bool scanQueued = false;
|
|
||||||
|
|
||||||
friend class DesktopEntryScanner;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
///! Desktop entry index.
|
///! Desktop entry index.
|
||||||
|
|
@ -303,17 +149,7 @@ public:
|
||||||
explicit DesktopEntries();
|
explicit DesktopEntries();
|
||||||
|
|
||||||
/// Look up a desktop entry by name. Includes NoDisplay entries. May return null.
|
/// Look up a desktop entry by name. Includes NoDisplay entries. May return null.
|
||||||
///
|
|
||||||
/// While this function requires an exact match, @@heuristicLookup() will correctly
|
|
||||||
/// find an entry more often and is generally more useful.
|
|
||||||
Q_INVOKABLE [[nodiscard]] static DesktopEntry* byId(const QString& id);
|
Q_INVOKABLE [[nodiscard]] static DesktopEntry* byId(const QString& id);
|
||||||
/// Look up a desktop entry by name using heuristics. Unlike @@byId(),
|
|
||||||
/// if no exact matches are found this function will try to guess - potentially incorrectly.
|
|
||||||
/// May return null.
|
|
||||||
Q_INVOKABLE [[nodiscard]] static DesktopEntry* heuristicLookup(const QString& name);
|
|
||||||
|
|
||||||
[[nodiscard]] static ObjectModel<DesktopEntry>* applications();
|
[[nodiscard]] static ObjectModel<DesktopEntry>* applications();
|
||||||
|
|
||||||
signals:
|
|
||||||
void applicationsChanged();
|
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,68 +0,0 @@
|
||||||
#include "desktopentrymonitor.hpp"
|
|
||||||
|
|
||||||
#include <qdir.h>
|
|
||||||
#include <qfileinfo.h>
|
|
||||||
#include <qfilesystemwatcher.h>
|
|
||||||
#include <qobject.h>
|
|
||||||
#include <qstring.h>
|
|
||||||
#include <qtmetamacros.h>
|
|
||||||
|
|
||||||
#include "desktopentry.hpp"
|
|
||||||
|
|
||||||
namespace {
|
|
||||||
void addPathAndParents(QFileSystemWatcher& watcher, const QString& path) {
|
|
||||||
watcher.addPath(path);
|
|
||||||
|
|
||||||
auto p = QFileInfo(path).absolutePath();
|
|
||||||
while (!p.isEmpty()) {
|
|
||||||
watcher.addPath(p);
|
|
||||||
const auto parent = QFileInfo(p).dir().absolutePath();
|
|
||||||
if (parent == p) break;
|
|
||||||
p = parent;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
DesktopEntryMonitor::DesktopEntryMonitor(QObject* parent): QObject(parent) {
|
|
||||||
this->debounceTimer.setSingleShot(true);
|
|
||||||
this->debounceTimer.setInterval(100);
|
|
||||||
|
|
||||||
QObject::connect(
|
|
||||||
&this->watcher,
|
|
||||||
&QFileSystemWatcher::directoryChanged,
|
|
||||||
this,
|
|
||||||
&DesktopEntryMonitor::onDirectoryChanged
|
|
||||||
);
|
|
||||||
QObject::connect(
|
|
||||||
&this->debounceTimer,
|
|
||||||
&QTimer::timeout,
|
|
||||||
this,
|
|
||||||
&DesktopEntryMonitor::processChanges
|
|
||||||
);
|
|
||||||
|
|
||||||
this->startMonitoring();
|
|
||||||
}
|
|
||||||
|
|
||||||
void DesktopEntryMonitor::startMonitoring() {
|
|
||||||
for (const auto& path: DesktopEntryManager::desktopPaths()) {
|
|
||||||
if (!QDir(path).exists()) continue;
|
|
||||||
addPathAndParents(this->watcher, path);
|
|
||||||
this->scanAndWatch(path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void DesktopEntryMonitor::scanAndWatch(const QString& dirPath) {
|
|
||||||
auto dir = QDir(dirPath);
|
|
||||||
if (!dir.exists()) return;
|
|
||||||
|
|
||||||
this->watcher.addPath(dirPath);
|
|
||||||
|
|
||||||
auto subdirs = dir.entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot | QDir::NoSymLinks);
|
|
||||||
for (const auto& subdir: subdirs) this->watcher.addPath(subdir.absoluteFilePath());
|
|
||||||
}
|
|
||||||
|
|
||||||
void DesktopEntryMonitor::onDirectoryChanged(const QString& /*path*/) {
|
|
||||||
this->debounceTimer.start();
|
|
||||||
}
|
|
||||||
|
|
||||||
void DesktopEntryMonitor::processChanges() { emit this->desktopEntriesChanged(); }
|
|
||||||
|
|
@ -1,32 +0,0 @@
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include <qfilesystemwatcher.h>
|
|
||||||
#include <qobject.h>
|
|
||||||
#include <qstringlist.h>
|
|
||||||
#include <qtimer.h>
|
|
||||||
|
|
||||||
class DesktopEntryMonitor: public QObject {
|
|
||||||
Q_OBJECT
|
|
||||||
|
|
||||||
public:
|
|
||||||
explicit DesktopEntryMonitor(QObject* parent = nullptr);
|
|
||||||
~DesktopEntryMonitor() override = default;
|
|
||||||
DesktopEntryMonitor(const DesktopEntryMonitor&) = delete;
|
|
||||||
DesktopEntryMonitor& operator=(const DesktopEntryMonitor&) = delete;
|
|
||||||
DesktopEntryMonitor(DesktopEntryMonitor&&) = delete;
|
|
||||||
DesktopEntryMonitor& operator=(DesktopEntryMonitor&&) = delete;
|
|
||||||
|
|
||||||
signals:
|
|
||||||
void desktopEntriesChanged();
|
|
||||||
|
|
||||||
private slots:
|
|
||||||
void onDirectoryChanged(const QString& path);
|
|
||||||
void processChanges();
|
|
||||||
|
|
||||||
private:
|
|
||||||
void startMonitoring();
|
|
||||||
void scanAndWatch(const QString& dirPath);
|
|
||||||
|
|
||||||
QFileSystemWatcher watcher;
|
|
||||||
QTimer debounceTimer;
|
|
||||||
};
|
|
||||||
|
|
@ -16,20 +16,18 @@
|
||||||
#include <qqmlengine.h>
|
#include <qqmlengine.h>
|
||||||
#include <qqmlerror.h>
|
#include <qqmlerror.h>
|
||||||
#include <qqmlincubator.h>
|
#include <qqmlincubator.h>
|
||||||
#include <qquickwindow.h>
|
|
||||||
#include <qtmetamacros.h>
|
#include <qtmetamacros.h>
|
||||||
|
|
||||||
#include "iconimageprovider.hpp"
|
#include "iconimageprovider.hpp"
|
||||||
#include "imageprovider.hpp"
|
#include "imageprovider.hpp"
|
||||||
#include "incubator.hpp"
|
#include "incubator.hpp"
|
||||||
#include "logcat.hpp"
|
|
||||||
#include "plugin.hpp"
|
#include "plugin.hpp"
|
||||||
#include "qsintercept.hpp"
|
#include "qsintercept.hpp"
|
||||||
#include "reload.hpp"
|
#include "reload.hpp"
|
||||||
#include "scan.hpp"
|
#include "scan.hpp"
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
QS_LOGGING_CATEGORY(logScene, "scene");
|
Q_LOGGING_CATEGORY(logScene, "scene");
|
||||||
}
|
}
|
||||||
|
|
||||||
static QHash<const QQmlEngine*, EngineGeneration*> g_generations; // NOLINT
|
static QHash<const QQmlEngine*, EngineGeneration*> g_generations; // NOLINT
|
||||||
|
|
@ -38,7 +36,7 @@ EngineGeneration::EngineGeneration(const QDir& rootPath, QmlScanner scanner)
|
||||||
: rootPath(rootPath)
|
: rootPath(rootPath)
|
||||||
, scanner(std::move(scanner))
|
, scanner(std::move(scanner))
|
||||||
, urlInterceptor(this->rootPath)
|
, urlInterceptor(this->rootPath)
|
||||||
, interceptNetFactory(this->rootPath, this->scanner.fileIntercepts)
|
, interceptNetFactory(this->scanner.fileIntercepts)
|
||||||
, engine(new QQmlEngine()) {
|
, engine(new QQmlEngine()) {
|
||||||
g_generations.insert(this->engine, this);
|
g_generations.insert(this->engine, this);
|
||||||
|
|
||||||
|
|
@ -46,11 +44,8 @@ EngineGeneration::EngineGeneration(const QDir& rootPath, QmlScanner scanner)
|
||||||
QObject::connect(this->engine, &QQmlEngine::warnings, this, &EngineGeneration::onEngineWarnings);
|
QObject::connect(this->engine, &QQmlEngine::warnings, this, &EngineGeneration::onEngineWarnings);
|
||||||
|
|
||||||
this->engine->addUrlInterceptor(&this->urlInterceptor);
|
this->engine->addUrlInterceptor(&this->urlInterceptor);
|
||||||
this->engine->addImportPath("qs:@/");
|
|
||||||
|
|
||||||
this->engine->setNetworkAccessManagerFactory(&this->interceptNetFactory);
|
this->engine->setNetworkAccessManagerFactory(&this->interceptNetFactory);
|
||||||
this->incubationController.initLoop();
|
this->engine->setIncubationController(&this->delayedIncubationController);
|
||||||
this->engine->setIncubationController(&this->incubationController);
|
|
||||||
|
|
||||||
this->engine->addImageProvider("icon", new IconImageProvider());
|
this->engine->addImageProvider("icon", new IconImageProvider());
|
||||||
this->engine->addImageProvider("qsimage", new QsImageProvider());
|
this->engine->addImageProvider("qsimage", new QsImageProvider());
|
||||||
|
|
@ -135,7 +130,7 @@ void EngineGeneration::onReload(EngineGeneration* old) {
|
||||||
// new generation acquires it then incubators will hang intermittently
|
// new generation acquires it then incubators will hang intermittently
|
||||||
qCDebug(logIncubator) << "Locking incubation controllers of old generation" << old;
|
qCDebug(logIncubator) << "Locking incubation controllers of old generation" << old;
|
||||||
old->incubationControllersLocked = true;
|
old->incubationControllersLocked = true;
|
||||||
old->updateIncubationMode();
|
old->assignIncubationController();
|
||||||
}
|
}
|
||||||
|
|
||||||
QObject::connect(this->engine, &QQmlEngine::quit, this, &EngineGeneration::quit);
|
QObject::connect(this->engine, &QQmlEngine::quit, this, &EngineGeneration::quit);
|
||||||
|
|
@ -162,9 +157,8 @@ void EngineGeneration::postReload() {
|
||||||
if (this->engine == nullptr || this->root == nullptr) return;
|
if (this->engine == nullptr || this->root == nullptr) return;
|
||||||
|
|
||||||
QsEnginePlugin::runOnReload();
|
QsEnginePlugin::runOnReload();
|
||||||
|
PostReloadHook::postReloadTree(this->root);
|
||||||
emit this->firePostReload();
|
this->singletonRegistry.onPostReload();
|
||||||
QObject::disconnect(this, &EngineGeneration::firePostReload, nullptr, nullptr);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void EngineGeneration::setWatchingFiles(bool watching) {
|
void EngineGeneration::setWatchingFiles(bool watching) {
|
||||||
|
|
@ -209,8 +203,6 @@ bool EngineGeneration::setExtraWatchedFiles(const QVector<QString>& files) {
|
||||||
for (const auto& file: files) {
|
for (const auto& file: files) {
|
||||||
if (!this->scanner.scannedFiles.contains(file)) {
|
if (!this->scanner.scannedFiles.contains(file)) {
|
||||||
this->extraWatchedFiles.append(file);
|
this->extraWatchedFiles.append(file);
|
||||||
QByteArray data;
|
|
||||||
this->scanner.readAndHashFile(file, data);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -226,16 +218,6 @@ void EngineGeneration::onFileChanged(const QString& name) {
|
||||||
if (!this->watcher->files().contains(name)) {
|
if (!this->watcher->files().contains(name)) {
|
||||||
this->deletedWatchedFiles.push_back(name);
|
this->deletedWatchedFiles.push_back(name);
|
||||||
} else {
|
} else {
|
||||||
// some editors (e.g vscode) perform file saving in two steps: truncate + write
|
|
||||||
// ignore the first event (truncate) with size 0 to prevent incorrect live reloading
|
|
||||||
auto fileInfo = QFileInfo(name);
|
|
||||||
if (fileInfo.isFile() && fileInfo.size() == 0) return;
|
|
||||||
|
|
||||||
if (!this->scanner.hasFileContentChanged(name)) {
|
|
||||||
qCDebug(logQmlScanner) << "Ignoring file change with unchanged content:" << name;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
emit this->filesChanged();
|
emit this->filesChanged();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -244,22 +226,104 @@ void EngineGeneration::onDirectoryChanged() {
|
||||||
// try to find any files that were just deleted from a replace operation
|
// try to find any files that were just deleted from a replace operation
|
||||||
for (auto& file: this->deletedWatchedFiles) {
|
for (auto& file: this->deletedWatchedFiles) {
|
||||||
if (QFileInfo(file).exists()) {
|
if (QFileInfo(file).exists()) {
|
||||||
if (!this->scanner.hasFileContentChanged(file)) {
|
|
||||||
qCDebug(logQmlScanner) << "Ignoring restored file with unchanged content:" << file;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
emit this->filesChanged();
|
emit this->filesChanged();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void EngineGeneration::onEngineWarnings(const QList<QQmlError>& warnings) {
|
void EngineGeneration::registerIncubationController(QQmlIncubationController* controller) {
|
||||||
|
// We only want controllers that we can swap out if destroyed.
|
||||||
|
// This happens if the window owning the active controller dies.
|
||||||
|
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;
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this->incubationControllers.push_back(controller);
|
||||||
|
qCDebug(logIncubator) << "Registered incubation controller" << controller << "to generation"
|
||||||
|
<< this;
|
||||||
|
|
||||||
|
// This function can run during destruction.
|
||||||
|
if (this->engine == nullptr) return;
|
||||||
|
|
||||||
|
if (this->engine->incubationController() == &this->delayedIncubationController) {
|
||||||
|
this->assignIncubationController();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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.";
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
} else {
|
||||||
|
qCDebug(logIncubator) << "Deregistered incubation controller" << controller << "from" << this;
|
||||||
|
}
|
||||||
|
|
||||||
|
// This function can run during destruction.
|
||||||
|
if (this->engine == nullptr) return;
|
||||||
|
|
||||||
|
if (this->engine->incubationController() == controller) {
|
||||||
|
qCDebug(logIncubator
|
||||||
|
) << "Destroyed incubation controller was currently active, reassigning from pool";
|
||||||
|
this->assignIncubationController();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void EngineGeneration::incubationControllerDestroyed() {
|
||||||
|
auto* sender = this->sender();
|
||||||
|
auto* controller = dynamic_cast<QQmlIncubationController*>(sender);
|
||||||
|
|
||||||
|
if (controller == nullptr) {
|
||||||
|
qCCritical(logIncubator) << "Destroyed incubation controller" << sender << "is not known to"
|
||||||
|
<< this << ", 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// This function can run during destruction.
|
||||||
|
if (this->engine == nullptr) return;
|
||||||
|
|
||||||
|
if (this->engine->incubationController() == controller) {
|
||||||
|
qCDebug(logIncubator
|
||||||
|
) << "Destroyed incubation controller was currently active, reassigning from pool";
|
||||||
|
this->assignIncubationController();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void EngineGeneration::onEngineWarnings(const QList<QQmlError>& warnings) const {
|
||||||
for (const auto& error: warnings) {
|
for (const auto& error: warnings) {
|
||||||
const auto& url = error.url();
|
auto rel = "**/" % this->rootPath.relativeFilePath(error.url().path());
|
||||||
auto rel = url.scheme() == "qs" && url.path().startsWith("@/qs/") ? "@" % url.path().sliced(5)
|
|
||||||
: url.toString();
|
|
||||||
|
|
||||||
QString objectName;
|
QString objectName;
|
||||||
auto desc = error.description();
|
auto desc = error.description();
|
||||||
|
|
@ -296,23 +360,20 @@ void EngineGeneration::exit(int code) {
|
||||||
this->destroy();
|
this->destroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
void EngineGeneration::trackWindowIncubationController(QQuickWindow* window) {
|
void EngineGeneration::assignIncubationController() {
|
||||||
if (this->trackedWindows.contains(window)) return;
|
QQmlIncubationController* controller = nullptr;
|
||||||
|
|
||||||
QObject::connect(window, &QObject::destroyed, this, &EngineGeneration::onTrackedWindowDestroyed);
|
if (this->incubationControllersLocked || this->incubationControllers.isEmpty()) {
|
||||||
this->trackedWindows.append(window);
|
controller = &this->delayedIncubationController;
|
||||||
this->updateIncubationMode();
|
} else {
|
||||||
}
|
controller = this->incubationControllers.first();
|
||||||
|
}
|
||||||
|
|
||||||
void EngineGeneration::onTrackedWindowDestroyed(QObject* object) {
|
qCDebug(logIncubator) << "Assigning incubation controller" << controller << "to generation"
|
||||||
this->trackedWindows.removeAll(static_cast<QQuickWindow*>(object)); // NOLINT
|
<< this
|
||||||
this->updateIncubationMode();
|
<< "fallback:" << (controller == &this->delayedIncubationController);
|
||||||
}
|
|
||||||
|
|
||||||
void EngineGeneration::updateIncubationMode() {
|
this->engine->setIncubationController(controller);
|
||||||
// If we're in a situation with only hidden but tracked windows this might be wrong,
|
|
||||||
// but it seems to at least work.
|
|
||||||
this->incubationController.setIncubationMode(!this->trackedWindows.empty());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
EngineGeneration* EngineGeneration::currentGeneration() {
|
EngineGeneration* EngineGeneration::currentGeneration() {
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,6 @@
|
||||||
#include <qqmlengine.h>
|
#include <qqmlengine.h>
|
||||||
#include <qqmlerror.h>
|
#include <qqmlerror.h>
|
||||||
#include <qqmlincubator.h>
|
#include <qqmlincubator.h>
|
||||||
#include <qquickwindow.h>
|
|
||||||
#include <qtclasshelpermacros.h>
|
#include <qtclasshelpermacros.h>
|
||||||
|
|
||||||
#include "incubator.hpp"
|
#include "incubator.hpp"
|
||||||
|
|
@ -41,7 +40,8 @@ public:
|
||||||
void setWatchingFiles(bool watching);
|
void setWatchingFiles(bool watching);
|
||||||
bool setExtraWatchedFiles(const QVector<QString>& files);
|
bool setExtraWatchedFiles(const QVector<QString>& files);
|
||||||
|
|
||||||
void trackWindowIncubationController(QQuickWindow* window);
|
void registerIncubationController(QQmlIncubationController* controller);
|
||||||
|
void deregisterIncubationController(QQmlIncubationController* controller);
|
||||||
|
|
||||||
// takes ownership
|
// takes ownership
|
||||||
void registerExtension(const void* key, EngineGenerationExt* extension);
|
void registerExtension(const void* key, EngineGenerationExt* extension);
|
||||||
|
|
@ -65,7 +65,7 @@ public:
|
||||||
QFileSystemWatcher* watcher = nullptr;
|
QFileSystemWatcher* watcher = nullptr;
|
||||||
QVector<QString> deletedWatchedFiles;
|
QVector<QString> deletedWatchedFiles;
|
||||||
QVector<QString> extraWatchedFiles;
|
QVector<QString> extraWatchedFiles;
|
||||||
QsIncubationController incubationController;
|
DelayedQmlIncubationController delayedIncubationController;
|
||||||
bool reloadComplete = false;
|
bool reloadComplete = false;
|
||||||
QuickshellGlobal* qsgInstance = nullptr;
|
QuickshellGlobal* qsgInstance = nullptr;
|
||||||
|
|
||||||
|
|
@ -75,7 +75,6 @@ public:
|
||||||
signals:
|
signals:
|
||||||
void filesChanged();
|
void filesChanged();
|
||||||
void reloadFinished();
|
void reloadFinished();
|
||||||
void firePostReload();
|
|
||||||
|
|
||||||
public slots:
|
public slots:
|
||||||
void quit();
|
void quit();
|
||||||
|
|
@ -84,13 +83,13 @@ public slots:
|
||||||
private slots:
|
private slots:
|
||||||
void onFileChanged(const QString& name);
|
void onFileChanged(const QString& name);
|
||||||
void onDirectoryChanged();
|
void onDirectoryChanged();
|
||||||
void onTrackedWindowDestroyed(QObject* object);
|
void incubationControllerDestroyed();
|
||||||
static void onEngineWarnings(const QList<QQmlError>& warnings);
|
void onEngineWarnings(const QList<QQmlError>& warnings) const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void postReload();
|
void postReload();
|
||||||
void updateIncubationMode();
|
void assignIncubationController();
|
||||||
QVector<QQuickWindow*> trackedWindows;
|
QVector<QQmlIncubationController*> incubationControllers;
|
||||||
bool incubationControllersLocked = false;
|
bool incubationControllersLocked = false;
|
||||||
QHash<const void*, EngineGenerationExt*> extensions;
|
QHash<const void*, EngineGenerationExt*> extensions;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,8 @@ IconImageProvider::requestPixmap(const QString& id, QSize* size, const QSize& re
|
||||||
if (splitIdx != -1) {
|
if (splitIdx != -1) {
|
||||||
iconName = id.sliced(0, splitIdx);
|
iconName = id.sliced(0, splitIdx);
|
||||||
path = id.sliced(splitIdx + 6);
|
path = id.sliced(splitIdx + 6);
|
||||||
path = QString("/%1/%2").arg(path, iconName.sliced(iconName.lastIndexOf('/') + 1));
|
qWarning() << "Searching custom icon paths is not yet supported. Icon path will be ignored for"
|
||||||
|
<< id;
|
||||||
} else {
|
} else {
|
||||||
splitIdx = id.indexOf("?fallback=");
|
splitIdx = id.indexOf("?fallback=");
|
||||||
if (splitIdx != -1) {
|
if (splitIdx != -1) {
|
||||||
|
|
@ -31,8 +32,7 @@ IconImageProvider::requestPixmap(const QString& id, QSize* size, const QSize& re
|
||||||
}
|
}
|
||||||
|
|
||||||
auto icon = QIcon::fromTheme(iconName);
|
auto icon = QIcon::fromTheme(iconName);
|
||||||
if (icon.isNull() && !fallbackName.isEmpty()) icon = QIcon::fromTheme(fallbackName);
|
if (icon.isNull()) icon = QIcon::fromTheme(fallbackName);
|
||||||
if (icon.isNull() && !path.isEmpty()) icon = QPixmap(path);
|
|
||||||
|
|
||||||
auto targetSize = requestedSize.isValid() ? requestedSize : QSize(100, 100);
|
auto targetSize = requestedSize.isValid() ? requestedSize : QSize(100, 100);
|
||||||
if (targetSize.width() == 0 || targetSize.height() == 0) targetSize = QSize(2, 2);
|
if (targetSize.width() == 0 || targetSize.height() == 0) targetSize = QSize(2, 2);
|
||||||
|
|
|
||||||
|
|
@ -22,8 +22,8 @@ class PixmapCacheIconEngine: public QIconEngine {
|
||||||
QIcon::Mode /*unused*/,
|
QIcon::Mode /*unused*/,
|
||||||
QIcon::State /*unused*/
|
QIcon::State /*unused*/
|
||||||
) override {
|
) override {
|
||||||
qFatal()
|
qFatal(
|
||||||
<< "Unexpected icon paint request bypassed pixmap method. Please report this as a bug.";
|
) << "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 {
|
QPixmap pixmap(const QSize& size, QIcon::Mode /*unused*/, QIcon::State /*unused*/) override {
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,11 @@
|
||||||
#include "incubator.hpp"
|
#include "incubator.hpp"
|
||||||
|
|
||||||
#include <private/qsgrenderloop_p.h>
|
|
||||||
#include <qabstractanimation.h>
|
|
||||||
#include <qguiapplication.h>
|
|
||||||
#include <qlogging.h>
|
#include <qlogging.h>
|
||||||
#include <qloggingcategory.h>
|
#include <qloggingcategory.h>
|
||||||
#include <qminmax.h>
|
|
||||||
#include <qnamespace.h>
|
|
||||||
#include <qobject.h>
|
|
||||||
#include <qobjectdefs.h>
|
|
||||||
#include <qqmlincubator.h>
|
#include <qqmlincubator.h>
|
||||||
#include <qscreen.h>
|
|
||||||
#include <qtmetamacros.h>
|
#include <qtmetamacros.h>
|
||||||
|
|
||||||
#include "logcat.hpp"
|
Q_LOGGING_CATEGORY(logIncubator, "quickshell.incubator", QtWarningMsg);
|
||||||
|
|
||||||
QS_LOGGING_CATEGORY(logIncubator, "quickshell.incubator", QtWarningMsg);
|
|
||||||
|
|
||||||
void QsQmlIncubator::statusChanged(QQmlIncubator::Status status) {
|
void QsQmlIncubator::statusChanged(QQmlIncubator::Status status) {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
|
|
@ -24,112 +14,3 @@ void QsQmlIncubator::statusChanged(QQmlIncubator::Status status) {
|
||||||
default: break;
|
default: break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void QsIncubationController::initLoop() {
|
|
||||||
auto* app = static_cast<QGuiApplication*>(QGuiApplication::instance()); // NOLINT
|
|
||||||
this->renderLoop = QSGRenderLoop::instance();
|
|
||||||
|
|
||||||
QObject::connect(
|
|
||||||
app,
|
|
||||||
&QGuiApplication::screenAdded,
|
|
||||||
this,
|
|
||||||
&QsIncubationController::updateIncubationTime
|
|
||||||
);
|
|
||||||
|
|
||||||
QObject::connect(
|
|
||||||
app,
|
|
||||||
&QGuiApplication::screenRemoved,
|
|
||||||
this,
|
|
||||||
&QsIncubationController::updateIncubationTime
|
|
||||||
);
|
|
||||||
|
|
||||||
this->updateIncubationTime();
|
|
||||||
|
|
||||||
QObject::connect(
|
|
||||||
this->renderLoop,
|
|
||||||
&QSGRenderLoop::timeToIncubate,
|
|
||||||
this,
|
|
||||||
&QsIncubationController::incubate
|
|
||||||
);
|
|
||||||
|
|
||||||
QAnimationDriver* animationDriver = this->renderLoop->animationDriver();
|
|
||||||
if (animationDriver) {
|
|
||||||
QObject::connect(
|
|
||||||
animationDriver,
|
|
||||||
&QAnimationDriver::stopped,
|
|
||||||
this,
|
|
||||||
&QsIncubationController::animationStopped
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
qCInfo(logIncubator) << "Render loop does not have animation driver, animationStopped cannot "
|
|
||||||
"be used to trigger incubation.";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void QsIncubationController::setIncubationMode(bool render) {
|
|
||||||
if (render == this->followRenderloop) return;
|
|
||||||
this->followRenderloop = render;
|
|
||||||
|
|
||||||
if (render) {
|
|
||||||
qCDebug(logIncubator) << "Incubation mode changed: render loop driven";
|
|
||||||
} else {
|
|
||||||
qCDebug(logIncubator) << "Incubation mode changed: event loop driven";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!render && this->incubatingObjectCount()) this->incubateLater();
|
|
||||||
}
|
|
||||||
|
|
||||||
void QsIncubationController::timerEvent(QTimerEvent* /*event*/) {
|
|
||||||
this->killTimer(this->timerId);
|
|
||||||
this->timerId = 0;
|
|
||||||
this->incubate();
|
|
||||||
}
|
|
||||||
|
|
||||||
void QsIncubationController::incubateLater() {
|
|
||||||
if (this->followRenderloop) {
|
|
||||||
if (this->timerId != 0) {
|
|
||||||
this->killTimer(this->timerId);
|
|
||||||
this->timerId = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Incubate again at the end of the event processing queue
|
|
||||||
QMetaObject::invokeMethod(this, &QsIncubationController::incubate, Qt::QueuedConnection);
|
|
||||||
} else if (this->timerId == 0) {
|
|
||||||
// Wait for a while before processing the next batch. Using a
|
|
||||||
// timer to avoid starvation of system events.
|
|
||||||
this->timerId = this->startTimer(this->incubationTime);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void QsIncubationController::incubate() {
|
|
||||||
if ((!this->followRenderloop || this->renderLoop) && this->incubatingObjectCount()) {
|
|
||||||
if (!this->followRenderloop) {
|
|
||||||
this->incubateFor(10);
|
|
||||||
if (this->incubatingObjectCount()) this->incubateLater();
|
|
||||||
} else if (this->renderLoop->interleaveIncubation()) {
|
|
||||||
this->incubateFor(this->incubationTime);
|
|
||||||
} else {
|
|
||||||
this->incubateFor(this->incubationTime * 2);
|
|
||||||
if (this->incubatingObjectCount()) this->incubateLater();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void QsIncubationController::animationStopped() { this->incubate(); }
|
|
||||||
|
|
||||||
void QsIncubationController::incubatingObjectCountChanged(int count) {
|
|
||||||
if (count
|
|
||||||
&& (!this->followRenderloop
|
|
||||||
|| (this->renderLoop && !this->renderLoop->interleaveIncubation())))
|
|
||||||
{
|
|
||||||
this->incubateLater();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void QsIncubationController::updateIncubationTime() {
|
|
||||||
auto* screen = QGuiApplication::primaryScreen();
|
|
||||||
if (!screen) return;
|
|
||||||
|
|
||||||
// 1/3 frame on primary screen
|
|
||||||
this->incubationTime = qMax(1, static_cast<int>(1000 / screen->refreshRate() / 3));
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,11 @@
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <qloggingcategory.h>
|
||||||
#include <qobject.h>
|
#include <qobject.h>
|
||||||
#include <qpointer.h>
|
|
||||||
#include <qqmlincubator.h>
|
#include <qqmlincubator.h>
|
||||||
#include <qtmetamacros.h>
|
#include <qtmetamacros.h>
|
||||||
|
|
||||||
#include "logcat.hpp"
|
Q_DECLARE_LOGGING_CATEGORY(logIncubator);
|
||||||
|
|
||||||
QS_DECLARE_LOGGING_CATEGORY(logIncubator);
|
|
||||||
|
|
||||||
class QsQmlIncubator
|
class QsQmlIncubator
|
||||||
: public QObject
|
: public QObject
|
||||||
|
|
@ -26,37 +24,7 @@ signals:
|
||||||
void failed();
|
void failed();
|
||||||
};
|
};
|
||||||
|
|
||||||
class QSGRenderLoop;
|
class DelayedQmlIncubationController: public QQmlIncubationController {
|
||||||
|
// Do nothing.
|
||||||
class QsIncubationController
|
// This ensures lazy loaders don't start blocking before onReload creates windows.
|
||||||
: public QObject
|
|
||||||
, public QQmlIncubationController {
|
|
||||||
Q_OBJECT
|
|
||||||
|
|
||||||
public:
|
|
||||||
void initLoop();
|
|
||||||
void setIncubationMode(bool render);
|
|
||||||
void incubateLater();
|
|
||||||
|
|
||||||
protected:
|
|
||||||
void timerEvent(QTimerEvent* event) override;
|
|
||||||
|
|
||||||
public slots:
|
|
||||||
void incubate();
|
|
||||||
void animationStopped();
|
|
||||||
void updateIncubationTime();
|
|
||||||
|
|
||||||
protected:
|
|
||||||
void incubatingObjectCountChanged(int count) override;
|
|
||||||
|
|
||||||
private:
|
|
||||||
// QPointer did not work with forward declarations prior to 6.7
|
|
||||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 7, 0)
|
|
||||||
QPointer<QSGRenderLoop> renderLoop = nullptr;
|
|
||||||
#else
|
|
||||||
QSGRenderLoop* renderLoop = nullptr;
|
|
||||||
#endif
|
|
||||||
int incubationTime = 0;
|
|
||||||
int timerId = 0;
|
|
||||||
bool followRenderloop = false;
|
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -3,14 +3,12 @@
|
||||||
#include <qdatastream.h>
|
#include <qdatastream.h>
|
||||||
|
|
||||||
QDataStream& operator<<(QDataStream& stream, const InstanceInfo& info) {
|
QDataStream& operator<<(QDataStream& stream, const InstanceInfo& info) {
|
||||||
stream << info.instanceId << info.configPath << info.shellId << info.appId << info.launchTime
|
stream << info.instanceId << info.configPath << info.shellId << info.launchTime;
|
||||||
<< info.pid << info.display;
|
|
||||||
return stream;
|
return stream;
|
||||||
}
|
}
|
||||||
|
|
||||||
QDataStream& operator>>(QDataStream& stream, InstanceInfo& info) {
|
QDataStream& operator>>(QDataStream& stream, InstanceInfo& info) {
|
||||||
stream >> info.instanceId >> info.configPath >> info.shellId >> info.appId >> info.launchTime
|
stream >> info.instanceId >> info.configPath >> info.shellId >> info.launchTime;
|
||||||
>> info.pid >> info.display;
|
|
||||||
return stream;
|
return stream;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,16 +3,12 @@
|
||||||
#include <qdatetime.h>
|
#include <qdatetime.h>
|
||||||
#include <qlogging.h>
|
#include <qlogging.h>
|
||||||
#include <qstring.h>
|
#include <qstring.h>
|
||||||
#include <sys/types.h>
|
|
||||||
|
|
||||||
struct InstanceInfo {
|
struct InstanceInfo {
|
||||||
QString instanceId;
|
QString instanceId;
|
||||||
QString configPath;
|
QString configPath;
|
||||||
QString shellId;
|
QString shellId;
|
||||||
QString appId;
|
|
||||||
QDateTime launchTime;
|
QDateTime launchTime;
|
||||||
pid_t pid = -1;
|
|
||||||
QString display;
|
|
||||||
|
|
||||||
static InstanceInfo CURRENT; // NOLINT
|
static InstanceInfo CURRENT; // NOLINT
|
||||||
};
|
};
|
||||||
|
|
@ -36,8 +32,6 @@ namespace qs::crash {
|
||||||
|
|
||||||
struct CrashInfo {
|
struct CrashInfo {
|
||||||
int logFd = -1;
|
int logFd = -1;
|
||||||
int traceFd = -1;
|
|
||||||
int infoFd = -1;
|
|
||||||
|
|
||||||
static CrashInfo INSTANCE; // NOLINT
|
static CrashInfo INSTANCE; // NOLINT
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -82,6 +82,9 @@
|
||||||
/// > Notably, @@Variants does not corrently support asynchronous
|
/// > Notably, @@Variants does not corrently support asynchronous
|
||||||
/// > loading, meaning using it inside a LazyLoader will block similarly to not
|
/// > loading, meaning using it inside a LazyLoader will block similarly to not
|
||||||
/// > having a loader to start with.
|
/// > having a loader to start with.
|
||||||
|
///
|
||||||
|
/// > [!WARNING] LazyLoaders do not start loading before the first window is created,
|
||||||
|
/// > meaning if you create all windows inside of lazy loaders, none of them will ever load.
|
||||||
class LazyLoader: public Reloadable {
|
class LazyLoader: public Reloadable {
|
||||||
Q_OBJECT;
|
Q_OBJECT;
|
||||||
/// The fully loaded item if the loader is @@loading or @@active, or `null`
|
/// The fully loaded item if the loader is @@loading or @@active, or `null`
|
||||||
|
|
|
||||||
|
|
@ -1,28 +0,0 @@
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include <qlogging.h>
|
|
||||||
#include <qloggingcategory.h>
|
|
||||||
|
|
||||||
namespace qs::log {
|
|
||||||
void initLogCategoryLevel(const char* name, QtMsgType defaultLevel = QtDebugMsg);
|
|
||||||
}
|
|
||||||
|
|
||||||
// NOLINTNEXTLINE(cppcoreguidelines-macro-usage)
|
|
||||||
#define QS_DECLARE_LOGGING_CATEGORY(name) \
|
|
||||||
namespace qslogcat { \
|
|
||||||
Q_DECLARE_LOGGING_CATEGORY(name); \
|
|
||||||
} \
|
|
||||||
const QLoggingCategory& name()
|
|
||||||
|
|
||||||
// NOLINTNEXTLINE(cppcoreguidelines-macro-usage)
|
|
||||||
#define QS_LOGGING_CATEGORY(name, category, ...) \
|
|
||||||
namespace qslogcat { \
|
|
||||||
Q_LOGGING_CATEGORY(name, category __VA_OPT__(, __VA_ARGS__)); \
|
|
||||||
} \
|
|
||||||
const QLoggingCategory& name() { \
|
|
||||||
static auto* init = []() { \
|
|
||||||
qs::log::initLogCategoryLevel(category __VA_OPT__(, __VA_ARGS__)); \
|
|
||||||
return &qslogcat::name; \
|
|
||||||
}(); \
|
|
||||||
return (init) (); \
|
|
||||||
}
|
|
||||||
|
|
@ -14,7 +14,6 @@
|
||||||
#include <qlist.h>
|
#include <qlist.h>
|
||||||
#include <qlogging.h>
|
#include <qlogging.h>
|
||||||
#include <qloggingcategory.h>
|
#include <qloggingcategory.h>
|
||||||
#include <qmutex.h>
|
|
||||||
#include <qnamespace.h>
|
#include <qnamespace.h>
|
||||||
#include <qobject.h>
|
#include <qobject.h>
|
||||||
#include <qobjectdefs.h>
|
#include <qobjectdefs.h>
|
||||||
|
|
@ -28,78 +27,20 @@
|
||||||
#include <qtmetamacros.h>
|
#include <qtmetamacros.h>
|
||||||
#include <qtypes.h>
|
#include <qtypes.h>
|
||||||
#include <sys/mman.h>
|
#include <sys/mman.h>
|
||||||
#ifdef __linux__
|
|
||||||
#include <sys/sendfile.h>
|
#include <sys/sendfile.h>
|
||||||
#include <sys/types.h>
|
|
||||||
#endif
|
|
||||||
#ifdef __FreeBSD__
|
|
||||||
#include <unistd.h>
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#include "instanceinfo.hpp"
|
#include "instanceinfo.hpp"
|
||||||
#include "logcat.hpp"
|
|
||||||
#include "logging_p.hpp"
|
#include "logging_p.hpp"
|
||||||
#include "logging_qtprivate.cpp" // NOLINT
|
#include "logging_qtprivate.cpp" // NOLINT
|
||||||
#include "paths.hpp"
|
#include "paths.hpp"
|
||||||
#include "ringbuf.hpp"
|
#include "ringbuf.hpp"
|
||||||
|
|
||||||
QS_LOGGING_CATEGORY(logBare, "quickshell.bare");
|
Q_LOGGING_CATEGORY(logBare, "quickshell.bare");
|
||||||
|
|
||||||
namespace qs::log {
|
namespace qs::log {
|
||||||
using namespace qt_logging_registry;
|
using namespace qt_logging_registry;
|
||||||
|
|
||||||
QS_LOGGING_CATEGORY(logLogging, "quickshell.logging", QtWarningMsg);
|
Q_LOGGING_CATEGORY(logLogging, "quickshell.logging", QtWarningMsg);
|
||||||
|
|
||||||
namespace {
|
|
||||||
bool copyFileData(int sourceFd, int destFd, qint64 size) {
|
|
||||||
auto usize = static_cast<size_t>(size);
|
|
||||||
|
|
||||||
#ifdef __linux__
|
|
||||||
off_t offset = 0;
|
|
||||||
auto remaining = usize;
|
|
||||||
|
|
||||||
while (remaining > 0) {
|
|
||||||
auto r = sendfile(destFd, sourceFd, &offset, remaining);
|
|
||||||
if (r == -1) {
|
|
||||||
if (errno == EINTR) continue;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (r == 0) break;
|
|
||||||
remaining -= static_cast<size_t>(r);
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
#else
|
|
||||||
std::array<char, 64 * 1024> buffer = {};
|
|
||||||
auto remaining = usize;
|
|
||||||
|
|
||||||
while (remaining > 0) {
|
|
||||||
auto chunk = std::min(remaining, buffer.size());
|
|
||||||
auto r = ::read(sourceFd, buffer.data(), chunk);
|
|
||||||
if (r == -1) {
|
|
||||||
if (errno == EINTR) continue;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (r == 0) break;
|
|
||||||
|
|
||||||
auto readBytes = static_cast<size_t>(r);
|
|
||||||
size_t written = 0;
|
|
||||||
while (written < readBytes) {
|
|
||||||
auto w = ::write(destFd, buffer.data() + written, readBytes - written);
|
|
||||||
if (w == -1) {
|
|
||||||
if (errno == EINTR) continue;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
written += static_cast<size_t>(w);
|
|
||||||
}
|
|
||||||
|
|
||||||
remaining -= readBytes;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
bool LogMessage::operator==(const LogMessage& other) const {
|
bool LogMessage::operator==(const LogMessage& other) const {
|
||||||
// note: not including time
|
// note: not including time
|
||||||
|
|
@ -221,7 +162,6 @@ void LogManager::messageHandler(
|
||||||
}
|
}
|
||||||
|
|
||||||
if (display) {
|
if (display) {
|
||||||
auto locker = QMutexLocker(&self->stdoutMutex);
|
|
||||||
LogMessage::formatMessage(
|
LogMessage::formatMessage(
|
||||||
self->stdoutStream,
|
self->stdoutStream,
|
||||||
message,
|
message,
|
||||||
|
|
@ -242,24 +182,17 @@ void LogManager::filterCategory(QLoggingCategory* category) {
|
||||||
auto categoryName = QLatin1StringView(category->categoryName());
|
auto categoryName = QLatin1StringView(category->categoryName());
|
||||||
auto isQs = categoryName.startsWith(QLatin1StringView("quickshell."));
|
auto isQs = categoryName.startsWith(QLatin1StringView("quickshell."));
|
||||||
|
|
||||||
CategoryFilter filter;
|
if (instance->lastCategoryFilter) {
|
||||||
|
|
||||||
// We don't respect log filters for qs logs because some distros like to ship
|
|
||||||
// default configs that hide everything. QT_LOGGING_RULES is considered via the filter list.
|
|
||||||
if (isQs) {
|
|
||||||
// QtDebugMsg == 0, so default
|
|
||||||
auto defaultLevel = instance->defaultLevels.value(categoryName);
|
|
||||||
|
|
||||||
filter = CategoryFilter();
|
|
||||||
// clang-format off
|
|
||||||
filter.debug = instance->mDefaultLevel == QtDebugMsg || defaultLevel == QtDebugMsg;
|
|
||||||
filter.info = filter.debug || instance->mDefaultLevel == QtInfoMsg || defaultLevel == QtInfoMsg;
|
|
||||||
filter.warn = filter.info || instance->mDefaultLevel == QtWarningMsg || defaultLevel == QtWarningMsg;
|
|
||||||
filter.critical = filter.warn || instance->mDefaultLevel == QtCriticalMsg || defaultLevel == QtCriticalMsg;
|
|
||||||
// clang-format on
|
|
||||||
} else if (instance->lastCategoryFilter) {
|
|
||||||
instance->lastCategoryFilter(category);
|
instance->lastCategoryFilter(category);
|
||||||
filter = CategoryFilter(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) {
|
for (const auto& rule: *instance->rules) {
|
||||||
|
|
@ -302,23 +235,14 @@ void LogManager::init(
|
||||||
|
|
||||||
{
|
{
|
||||||
QLoggingSettingsParser parser;
|
QLoggingSettingsParser parser;
|
||||||
// Load QT_LOGGING_RULES because we ignore the last category filter for QS messages
|
|
||||||
// due to disk config files.
|
|
||||||
parser.setContent(qEnvironmentVariable("QT_LOGGING_RULES"));
|
|
||||||
instance->rules = new QList(parser.rules());
|
|
||||||
parser.setContent(rules);
|
parser.setContent(rules);
|
||||||
instance->rules->append(parser.rules());
|
instance->rules = new QList(parser.rules());
|
||||||
}
|
|
||||||
|
|
||||||
instance->lastCategoryFilter = QLoggingCategory::installFilter(&LogManager::filterCategory);
|
|
||||||
|
|
||||||
if (instance->lastCategoryFilter == &LogManager::filterCategory) {
|
|
||||||
qCFatal(logLogging) << "Quickshell's log filter has been installed twice. This is a bug.";
|
|
||||||
instance->lastCategoryFilter = nullptr;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
qInstallMessageHandler(&LogManager::messageHandler);
|
qInstallMessageHandler(&LogManager::messageHandler);
|
||||||
|
|
||||||
|
instance->lastCategoryFilter = QLoggingCategory::installFilter(&LogManager::filterCategory);
|
||||||
|
|
||||||
qCDebug(logLogging) << "Creating offthread logger...";
|
qCDebug(logLogging) << "Creating offthread logger...";
|
||||||
auto* thread = new QThread();
|
auto* thread = new QThread();
|
||||||
instance->threadProxy.moveToThread(thread);
|
instance->threadProxy.moveToThread(thread);
|
||||||
|
|
@ -333,10 +257,6 @@ void LogManager::init(
|
||||||
qCDebug(logLogging) << "Logger initialized.";
|
qCDebug(logLogging) << "Logger initialized.";
|
||||||
}
|
}
|
||||||
|
|
||||||
void initLogCategoryLevel(const char* name, QtMsgType defaultLevel) {
|
|
||||||
LogManager::instance()->defaultLevels.insert(QLatin1StringView(name), defaultLevel);
|
|
||||||
}
|
|
||||||
|
|
||||||
void LogManager::initFs() {
|
void LogManager::initFs() {
|
||||||
QMetaObject::invokeMethod(
|
QMetaObject::invokeMethod(
|
||||||
&LogManager::instance()->threadProxy,
|
&LogManager::instance()->threadProxy,
|
||||||
|
|
@ -377,12 +297,8 @@ void ThreadLogging::init() {
|
||||||
|
|
||||||
if (logMfd != -1) {
|
if (logMfd != -1) {
|
||||||
this->file = new QFile();
|
this->file = new QFile();
|
||||||
|
this->file->open(logMfd, QFile::ReadWrite, QFile::AutoCloseHandle);
|
||||||
if (this->file->open(logMfd, QFile::ReadWrite, QFile::AutoCloseHandle)) {
|
this->fileStream.setDevice(this->file);
|
||||||
this->fileStream.setDevice(this->file);
|
|
||||||
} else {
|
|
||||||
qCCritical(logLogging) << "Failed to open early logging memfd.";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dlogMfd != -1) {
|
if (dlogMfd != -1) {
|
||||||
|
|
@ -390,19 +306,14 @@ void ThreadLogging::init() {
|
||||||
|
|
||||||
this->detailedFile = new QFile();
|
this->detailedFile = new QFile();
|
||||||
// buffered by WriteBuffer
|
// buffered by WriteBuffer
|
||||||
if (this->detailedFile
|
this->detailedFile->open(dlogMfd, QFile::ReadWrite | QFile::Unbuffered, QFile::AutoCloseHandle);
|
||||||
->open(dlogMfd, QFile::ReadWrite | QFile::Unbuffered, QFile::AutoCloseHandle))
|
this->detailedWriter.setDevice(this->detailedFile);
|
||||||
{
|
|
||||||
this->detailedWriter.setDevice(this->detailedFile);
|
|
||||||
|
|
||||||
if (!this->detailedWriter.writeHeader()) {
|
if (!this->detailedWriter.writeHeader()) {
|
||||||
qCCritical(logLogging) << "Could not write header for detailed logs.";
|
qCCritical(logLogging) << "Could not write header for detailed logs.";
|
||||||
this->detailedWriter.setDevice(nullptr);
|
this->detailedWriter.setDevice(nullptr);
|
||||||
delete this->detailedFile;
|
delete this->detailedFile;
|
||||||
this->detailedFile = nullptr;
|
this->detailedFile = nullptr;
|
||||||
}
|
|
||||||
} else {
|
|
||||||
qCCritical(logLogging) << "Failed to open early detailed logging memfd.";
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -425,8 +336,7 @@ void ThreadLogging::initFs() {
|
||||||
auto* runDir = QsPaths::instance()->instanceRunDir();
|
auto* runDir = QsPaths::instance()->instanceRunDir();
|
||||||
|
|
||||||
if (!runDir) {
|
if (!runDir) {
|
||||||
qCCritical(
|
qCCritical(logLogging
|
||||||
logLogging
|
|
||||||
) << "Could not start filesystem logging as the runtime directory could not be created.";
|
) << "Could not start filesystem logging as the runtime directory could not be created.";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -437,8 +347,7 @@ void ThreadLogging::initFs() {
|
||||||
auto* detailedFile = new QFile(detailedPath);
|
auto* detailedFile = new QFile(detailedPath);
|
||||||
|
|
||||||
if (!file->open(QFile::ReadWrite | QFile::Truncate)) {
|
if (!file->open(QFile::ReadWrite | QFile::Truncate)) {
|
||||||
qCCritical(
|
qCCritical(logLogging
|
||||||
logLogging
|
|
||||||
) << "Could not start filesystem logger as the log file could not be created:"
|
) << "Could not start filesystem logger as the log file could not be created:"
|
||||||
<< path;
|
<< path;
|
||||||
delete file;
|
delete file;
|
||||||
|
|
@ -449,14 +358,13 @@ void ThreadLogging::initFs() {
|
||||||
|
|
||||||
// buffered by WriteBuffer
|
// buffered by WriteBuffer
|
||||||
if (!detailedFile->open(QFile::ReadWrite | QFile::Truncate | QFile::Unbuffered)) {
|
if (!detailedFile->open(QFile::ReadWrite | QFile::Truncate | QFile::Unbuffered)) {
|
||||||
qCCritical(
|
qCCritical(logLogging
|
||||||
logLogging
|
|
||||||
) << "Could not start detailed filesystem logger as the log file could not be created:"
|
) << "Could not start detailed filesystem logger as the log file could not be created:"
|
||||||
<< detailedPath;
|
<< detailedPath;
|
||||||
delete detailedFile;
|
delete detailedFile;
|
||||||
detailedFile = nullptr;
|
detailedFile = nullptr;
|
||||||
} else {
|
} else {
|
||||||
struct flock lock = {
|
auto lock = flock {
|
||||||
.l_type = F_WRLCK,
|
.l_type = F_WRLCK,
|
||||||
.l_whence = SEEK_SET,
|
.l_whence = SEEK_SET,
|
||||||
.l_start = 0,
|
.l_start = 0,
|
||||||
|
|
@ -478,11 +386,7 @@ void ThreadLogging::initFs() {
|
||||||
auto* oldFile = this->file;
|
auto* oldFile = this->file;
|
||||||
if (oldFile) {
|
if (oldFile) {
|
||||||
oldFile->seek(0);
|
oldFile->seek(0);
|
||||||
|
sendfile(file->handle(), oldFile->handle(), nullptr, oldFile->size());
|
||||||
if (!copyFileData(oldFile->handle(), file->handle(), oldFile->size())) {
|
|
||||||
qCritical(logLogging) << "Failed to copy log from memfd with error code " << errno
|
|
||||||
<< qt_error_string(errno);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this->file = file;
|
this->file = file;
|
||||||
|
|
@ -494,10 +398,7 @@ void ThreadLogging::initFs() {
|
||||||
auto* oldFile = this->detailedFile;
|
auto* oldFile = this->detailedFile;
|
||||||
if (oldFile) {
|
if (oldFile) {
|
||||||
oldFile->seek(0);
|
oldFile->seek(0);
|
||||||
if (!copyFileData(oldFile->handle(), detailedFile->handle(), oldFile->size())) {
|
sendfile(detailedFile->handle(), oldFile->handle(), nullptr, oldFile->size());
|
||||||
qCritical(logLogging) << "Failed to copy detailed log from memfd with error code " << errno
|
|
||||||
<< qt_error_string(errno);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
crash::CrashInfo::INSTANCE.logFd = detailedFile->handle();
|
crash::CrashInfo::INSTANCE.logFd = detailedFile->handle();
|
||||||
|
|
@ -540,14 +441,10 @@ void ThreadLogging::onMessage(const LogMessage& msg, bool showInSparse) {
|
||||||
this->fileStream << Qt::endl;
|
this->fileStream << Qt::endl;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this->detailedWriter.write(msg) || (this->detailedFile && !this->detailedFile->flush())) {
|
if (this->detailedWriter.write(msg)) {
|
||||||
this->detailedWriter.setDevice(nullptr);
|
this->detailedFile->flush();
|
||||||
|
} else if (this->detailedFile != nullptr) {
|
||||||
if (this->detailedFile) {
|
qCCritical(logLogging) << "Detailed logger failed to write. Ending detailed logs.";
|
||||||
this->detailedFile->close();
|
|
||||||
this->detailedFile = nullptr;
|
|
||||||
qCCritical(logLogging) << "Detailed logger failed to write. Ending detailed logs.";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -820,11 +717,11 @@ bool EncodedLogReader::readVarInt(quint32* slot) {
|
||||||
if (!this->reader.skip(1)) return false;
|
if (!this->reader.skip(1)) return false;
|
||||||
*slot = qFromLittleEndian(n);
|
*slot = qFromLittleEndian(n);
|
||||||
} else if ((bytes[1] != 0xff || bytes[2] != 0xff) && readLength >= 3) {
|
} else if ((bytes[1] != 0xff || bytes[2] != 0xff) && readLength >= 3) {
|
||||||
auto n = *reinterpret_cast<quint16*>(bytes.data() + 1); // NOLINT
|
auto n = *reinterpret_cast<quint16*>(bytes.data() + 1);
|
||||||
if (!this->reader.skip(3)) return false;
|
if (!this->reader.skip(3)) return false;
|
||||||
*slot = qFromLittleEndian(n);
|
*slot = qFromLittleEndian(n);
|
||||||
} else if (readLength == 7) {
|
} else if (readLength == 7) {
|
||||||
auto n = *reinterpret_cast<quint32*>(bytes.data() + 3); // NOLINT
|
auto n = *reinterpret_cast<quint32*>(bytes.data() + 3);
|
||||||
if (!this->reader.skip(7)) return false;
|
if (!this->reader.skip(7)) return false;
|
||||||
*slot = qFromLittleEndian(n);
|
*slot = qFromLittleEndian(n);
|
||||||
} else return false;
|
} else return false;
|
||||||
|
|
@ -960,7 +857,7 @@ bool LogReader::continueReading() {
|
||||||
}
|
}
|
||||||
|
|
||||||
void LogFollower::FcntlWaitThread::run() {
|
void LogFollower::FcntlWaitThread::run() {
|
||||||
struct flock lock = {
|
auto lock = flock {
|
||||||
.l_type = F_RDLCK, // won't block other read locks when we take it
|
.l_type = F_RDLCK, // won't block other read locks when we take it
|
||||||
.l_whence = SEEK_SET,
|
.l_whence = SEEK_SET,
|
||||||
.l_start = 0,
|
.l_start = 0,
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@
|
||||||
|
|
||||||
#include <utility>
|
#include <utility>
|
||||||
|
|
||||||
#include <qbytearrayview.h>
|
|
||||||
#include <qcontainerfwd.h>
|
#include <qcontainerfwd.h>
|
||||||
#include <qdatetime.h>
|
#include <qdatetime.h>
|
||||||
#include <qfile.h>
|
#include <qfile.h>
|
||||||
|
|
@ -10,13 +9,10 @@
|
||||||
#include <qlatin1stringview.h>
|
#include <qlatin1stringview.h>
|
||||||
#include <qlogging.h>
|
#include <qlogging.h>
|
||||||
#include <qloggingcategory.h>
|
#include <qloggingcategory.h>
|
||||||
#include <qmutex.h>
|
|
||||||
#include <qobject.h>
|
#include <qobject.h>
|
||||||
#include <qtmetamacros.h>
|
#include <qtmetamacros.h>
|
||||||
|
|
||||||
#include "logcat.hpp"
|
Q_DECLARE_LOGGING_CATEGORY(logBare);
|
||||||
|
|
||||||
QS_DECLARE_LOGGING_CATEGORY(logBare);
|
|
||||||
|
|
||||||
namespace qs::log {
|
namespace qs::log {
|
||||||
|
|
||||||
|
|
@ -131,15 +127,11 @@ private:
|
||||||
QString mRulesString;
|
QString mRulesString;
|
||||||
QList<qt_logging_registry::QLoggingRule>* rules = nullptr;
|
QList<qt_logging_registry::QLoggingRule>* rules = nullptr;
|
||||||
QtMsgType mDefaultLevel = QtWarningMsg;
|
QtMsgType mDefaultLevel = QtWarningMsg;
|
||||||
QHash<QLatin1StringView, QtMsgType> defaultLevels;
|
|
||||||
QHash<const void*, CategoryFilter> sparseFilters;
|
QHash<const void*, CategoryFilter> sparseFilters;
|
||||||
QHash<QLatin1StringView, CategoryFilter> allFilters;
|
QHash<QLatin1StringView, CategoryFilter> allFilters;
|
||||||
|
|
||||||
QTextStream stdoutStream;
|
QTextStream stdoutStream;
|
||||||
QMutex stdoutMutex;
|
|
||||||
LoggingThreadProxy threadProxy;
|
LoggingThreadProxy threadProxy;
|
||||||
|
|
||||||
friend void initLogCategoryLevel(const char* name, QtMsgType defaultLevel);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
bool readEncodedLogs(
|
bool readEncodedLogs(
|
||||||
|
|
|
||||||
|
|
@ -16,11 +16,10 @@
|
||||||
#include <qstringview.h>
|
#include <qstringview.h>
|
||||||
#include <qtypes.h>
|
#include <qtypes.h>
|
||||||
|
|
||||||
#include "logcat.hpp"
|
|
||||||
#include "logging_qtprivate.hpp"
|
#include "logging_qtprivate.hpp"
|
||||||
|
|
||||||
namespace qs::log {
|
namespace qs::log {
|
||||||
QS_DECLARE_LOGGING_CATEGORY(logLogging);
|
Q_DECLARE_LOGGING_CATEGORY(logLogging);
|
||||||
|
|
||||||
namespace qt_logging_registry {
|
namespace qt_logging_registry {
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,10 +12,8 @@
|
||||||
#include <qstringview.h>
|
#include <qstringview.h>
|
||||||
#include <qtypes.h>
|
#include <qtypes.h>
|
||||||
|
|
||||||
#include "logcat.hpp"
|
|
||||||
|
|
||||||
namespace qs::log {
|
namespace qs::log {
|
||||||
QS_DECLARE_LOGGING_CATEGORY(logLogging);
|
Q_DECLARE_LOGGING_CATEGORY(logLogging);
|
||||||
|
|
||||||
namespace qt_logging_registry {
|
namespace qt_logging_registry {
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,81 @@
|
||||||
#include "model.hpp"
|
#include "model.hpp"
|
||||||
|
|
||||||
#include <qbytearray.h>
|
#include <qabstractitemmodel.h>
|
||||||
#include <qhash.h>
|
#include <qhash.h>
|
||||||
#include <qnamespace.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 {
|
QHash<int, QByteArray> UntypedObjectModel::roleNames() const {
|
||||||
return {{Qt::UserRole, "modelData"}};
|
return {{Qt::UserRole, "modelData"}};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void UntypedObjectModel::insertObject(QObject* object, qsizetype index) {
|
||||||
|
auto iindex = index == -1 ? this->valuesList.length() : index;
|
||||||
|
emit this->objectInsertedPre(object, iindex);
|
||||||
|
|
||||||
|
auto intIndex = static_cast<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() {
|
UntypedObjectModel* UntypedObjectModel::emptyInstance() {
|
||||||
static auto* instance = new ObjectModel<void>(nullptr);
|
static auto* instance = new UntypedObjectModel(nullptr); // NOLINT
|
||||||
return instance;
|
return instance;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
#include <functional>
|
#include <functional>
|
||||||
|
|
||||||
#include <QtCore/qtmetamacros.h>
|
#include <bit>
|
||||||
#include <qabstractitemmodel.h>
|
#include <qabstractitemmodel.h>
|
||||||
#include <qcontainerfwd.h>
|
#include <qcontainerfwd.h>
|
||||||
#include <qobject.h>
|
#include <qobject.h>
|
||||||
|
|
@ -49,11 +49,14 @@ class UntypedObjectModel: public QAbstractListModel {
|
||||||
public:
|
public:
|
||||||
explicit UntypedObjectModel(QObject* parent): QAbstractListModel(parent) {}
|
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]] QHash<int, QByteArray> roleNames() const override;
|
||||||
|
|
||||||
[[nodiscard]] virtual QList<QObject*> values() = 0;
|
[[nodiscard]] QList<QObject*> values() const { return this->valuesList; };
|
||||||
|
void removeAt(qsizetype index);
|
||||||
|
|
||||||
Q_INVOKABLE virtual qsizetype indexOf(QObject* object) const = 0;
|
Q_INVOKABLE qsizetype indexOf(QObject* object);
|
||||||
|
|
||||||
static UntypedObjectModel* emptyInstance();
|
static UntypedObjectModel* emptyInstance();
|
||||||
|
|
||||||
|
|
@ -68,6 +71,15 @@ signals:
|
||||||
/// Sent immediately after an object is removed from the list.
|
/// Sent immediately after an object is removed from the list.
|
||||||
void objectRemovedPost(QObject* object, qsizetype index);
|
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:
|
private:
|
||||||
static qsizetype valuesCount(QQmlListProperty<QObject>* property);
|
static qsizetype valuesCount(QQmlListProperty<QObject>* property);
|
||||||
static QObject* valueAt(QQmlListProperty<QObject>* property, qsizetype index);
|
static QObject* valueAt(QQmlListProperty<QObject>* property, qsizetype index);
|
||||||
|
|
@ -78,20 +90,14 @@ class ObjectModel: public UntypedObjectModel {
|
||||||
public:
|
public:
|
||||||
explicit ObjectModel(QObject* parent): UntypedObjectModel(parent) {}
|
explicit ObjectModel(QObject* parent): UntypedObjectModel(parent) {}
|
||||||
|
|
||||||
[[nodiscard]] const QList<T*>& valueList() const { return this->mValuesList; }
|
[[nodiscard]] QVector<T*>& valueList() { return *std::bit_cast<QVector<T*>*>(&this->valuesList); }
|
||||||
[[nodiscard]] QList<T*>& valueList() { return this->mValuesList; }
|
|
||||||
|
[[nodiscard]] const QVector<T*>& valueList() const {
|
||||||
|
return *std::bit_cast<const QVector<T*>*>(&this->valuesList);
|
||||||
|
}
|
||||||
|
|
||||||
void insertObject(T* object, qsizetype index = -1) {
|
void insertObject(T* object, qsizetype index = -1) {
|
||||||
auto iindex = index == -1 ? this->mValuesList.length() : index;
|
this->UntypedObjectModel::insertObject(object, index);
|
||||||
emit this->objectInsertedPre(object, iindex);
|
|
||||||
|
|
||||||
auto intIndex = static_cast<qint32>(iindex);
|
|
||||||
this->beginInsertRows(QModelIndex(), intIndex, intIndex);
|
|
||||||
this->mValuesList.insert(iindex, object);
|
|
||||||
this->endInsertRows();
|
|
||||||
|
|
||||||
emit this->valuesChanged();
|
|
||||||
emit this->objectInsertedPost(object, iindex);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void insertObjectSorted(T* object, const std::function<bool(T*, T*)>& compare) {
|
void insertObjectSorted(T* object, const std::function<bool(T*, T*)>& compare) {
|
||||||
|
|
@ -104,71 +110,17 @@ public:
|
||||||
}
|
}
|
||||||
|
|
||||||
auto idx = iter - list.begin();
|
auto idx = iter - list.begin();
|
||||||
this->insertObject(object, idx);
|
this->UntypedObjectModel::insertObject(object, idx);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool removeObject(const T* object) {
|
void removeObject(const T* object) { this->UntypedObjectModel::removeObject(object); }
|
||||||
auto index = this->mValuesList.indexOf(object);
|
|
||||||
if (index == -1) return false;
|
|
||||||
|
|
||||||
this->removeAt(index);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void removeAt(qsizetype index) {
|
|
||||||
auto* object = this->mValuesList.at(index);
|
|
||||||
emit this->objectRemovedPre(object, index);
|
|
||||||
|
|
||||||
auto intIndex = static_cast<qint32>(index);
|
|
||||||
this->beginRemoveRows(QModelIndex(), intIndex, intIndex);
|
|
||||||
this->mValuesList.removeAt(index);
|
|
||||||
this->endRemoveRows();
|
|
||||||
|
|
||||||
emit this->valuesChanged();
|
|
||||||
emit this->objectRemovedPost(object, index);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Assumes only one instance of a specific value
|
// Assumes only one instance of a specific value
|
||||||
void diffUpdate(const QList<T*>& newValues) {
|
void diffUpdate(const QVector<T*>& newValues) {
|
||||||
for (qsizetype i = 0; i < this->mValuesList.length();) {
|
this->UntypedObjectModel::diffUpdate(*std::bit_cast<const QVector<QObject*>*>(&newValues));
|
||||||
if (newValues.contains(this->mValuesList.at(i))) i++;
|
|
||||||
else this->removeAt(i);
|
|
||||||
}
|
|
||||||
|
|
||||||
qsizetype oi = 0;
|
|
||||||
for (auto* object: newValues) {
|
|
||||||
if (this->mValuesList.length() == oi || this->mValuesList.at(oi) != object) {
|
|
||||||
this->insertObject(object, oi);
|
|
||||||
}
|
|
||||||
|
|
||||||
oi++;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static ObjectModel<T>* emptyInstance() {
|
static ObjectModel<T>* emptyInstance() {
|
||||||
return static_cast<ObjectModel<T>*>(UntypedObjectModel::emptyInstance());
|
return static_cast<ObjectModel<T>*>(UntypedObjectModel::emptyInstance());
|
||||||
}
|
}
|
||||||
|
|
||||||
[[nodiscard]] qint32 rowCount(const QModelIndex& parent) const override {
|
|
||||||
if (parent != QModelIndex()) return 0;
|
|
||||||
return static_cast<qint32>(this->mValuesList.length());
|
|
||||||
}
|
|
||||||
|
|
||||||
[[nodiscard]] QVariant data(const QModelIndex& index, qint32 role) const override {
|
|
||||||
if (role != Qt::UserRole) return QVariant();
|
|
||||||
// Values must be QObject derived, but we can't assert that here without breaking forward decls,
|
|
||||||
// so no static_cast.
|
|
||||||
return QVariant::fromValue(reinterpret_cast<QObject*>(this->mValuesList.at(index.row())));
|
|
||||||
}
|
|
||||||
|
|
||||||
qsizetype indexOf(QObject* object) const override {
|
|
||||||
return this->mValuesList.indexOf(reinterpret_cast<T*>(object));
|
|
||||||
}
|
|
||||||
|
|
||||||
[[nodiscard]] QList<QObject*> values() override {
|
|
||||||
return *reinterpret_cast<QList<QObject*>*>(&this->mValuesList);
|
|
||||||
}
|
|
||||||
|
|
||||||
private:
|
|
||||||
QList<T*> mValuesList;
|
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ headers = [
|
||||||
"model.hpp",
|
"model.hpp",
|
||||||
"elapsedtimer.hpp",
|
"elapsedtimer.hpp",
|
||||||
"desktopentry.hpp",
|
"desktopentry.hpp",
|
||||||
|
"objectrepeater.hpp",
|
||||||
"qsmenu.hpp",
|
"qsmenu.hpp",
|
||||||
"retainable.hpp",
|
"retainable.hpp",
|
||||||
"popupanchor.hpp",
|
"popupanchor.hpp",
|
||||||
|
|
|
||||||
190
src/core/objectrepeater.cpp
Normal file
190
src/core/objectrepeater.cpp
Normal file
|
|
@ -0,0 +1,190 @@
|
||||||
|
#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;
|
||||||
|
}
|
||||||
85
src/core/objectrepeater.hpp
Normal file
85
src/core/objectrepeater.hpp
Normal file
|
|
@ -0,0 +1,85 @@
|
||||||
|
#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;
|
||||||
|
};
|
||||||
|
|
@ -9,17 +9,15 @@
|
||||||
#include <qdir.h>
|
#include <qdir.h>
|
||||||
#include <qlogging.h>
|
#include <qlogging.h>
|
||||||
#include <qloggingcategory.h>
|
#include <qloggingcategory.h>
|
||||||
#include <qpair.h>
|
|
||||||
#include <qstandardpaths.h>
|
#include <qstandardpaths.h>
|
||||||
#include <qtenvironmentvariables.h>
|
#include <qtenvironmentvariables.h>
|
||||||
#include <qtversionchecks.h>
|
#include <qtversionchecks.h>
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
|
|
||||||
#include "instanceinfo.hpp"
|
#include "instanceinfo.hpp"
|
||||||
#include "logcat.hpp"
|
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
QS_LOGGING_CATEGORY(logPaths, "quickshell.paths", QtWarningMsg);
|
Q_LOGGING_CATEGORY(logPaths, "quickshell.paths", QtWarningMsg);
|
||||||
}
|
}
|
||||||
|
|
||||||
QsPaths* QsPaths::instance() {
|
QsPaths* QsPaths::instance() {
|
||||||
|
|
@ -27,19 +25,12 @@ QsPaths* QsPaths::instance() {
|
||||||
return instance;
|
return instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
void QsPaths::init(
|
void QsPaths::init(QString shellId, QString pathId, QString dataOverride, QString stateOverride) {
|
||||||
QString shellId,
|
|
||||||
QString pathId,
|
|
||||||
QString dataOverride,
|
|
||||||
QString stateOverride,
|
|
||||||
QString cacheOverride
|
|
||||||
) {
|
|
||||||
auto* instance = QsPaths::instance();
|
auto* instance = QsPaths::instance();
|
||||||
instance->shellId = std::move(shellId);
|
instance->shellId = std::move(shellId);
|
||||||
instance->pathId = std::move(pathId);
|
instance->pathId = std::move(pathId);
|
||||||
instance->shellDataOverride = std::move(dataOverride);
|
instance->shellDataOverride = std::move(dataOverride);
|
||||||
instance->shellStateOverride = std::move(stateOverride);
|
instance->shellStateOverride = std::move(stateOverride);
|
||||||
instance->shellCacheOverride = std::move(cacheOverride);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
QDir QsPaths::crashDir(const QString& id) {
|
QDir QsPaths::crashDir(const QString& id) {
|
||||||
|
|
@ -64,7 +55,7 @@ QDir* QsPaths::baseRunDir() {
|
||||||
if (this->baseRunState == DirState::Unknown) {
|
if (this->baseRunState == DirState::Unknown) {
|
||||||
auto runtimeDir = qEnvironmentVariable("XDG_RUNTIME_DIR");
|
auto runtimeDir = qEnvironmentVariable("XDG_RUNTIME_DIR");
|
||||||
if (runtimeDir.isEmpty()) {
|
if (runtimeDir.isEmpty()) {
|
||||||
runtimeDir = QString("/run/user/%1").arg(getuid());
|
runtimeDir = QString("/run/user/$1").arg(getuid());
|
||||||
qCInfo(logPaths) << "XDG_RUNTIME_DIR was not set, defaulting to" << runtimeDir;
|
qCInfo(logPaths) << "XDG_RUNTIME_DIR was not set, defaulting to" << runtimeDir;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -142,41 +133,13 @@ QDir* QsPaths::instanceRunDir() {
|
||||||
else return &this->mInstanceRunDir;
|
else return &this->mInstanceRunDir;
|
||||||
}
|
}
|
||||||
|
|
||||||
QDir* QsPaths::shellVfsDir() {
|
|
||||||
if (this->shellVfsState == DirState::Unknown) {
|
|
||||||
if (auto* baseRunDir = this->baseRunDir()) {
|
|
||||||
this->mShellVfsDir = QDir(baseRunDir->filePath("vfs"));
|
|
||||||
this->mShellVfsDir = QDir(this->mShellVfsDir.filePath(this->shellId));
|
|
||||||
|
|
||||||
qCDebug(logPaths) << "Initialized runtime vfs path:" << this->mShellVfsDir.path();
|
|
||||||
|
|
||||||
if (!this->mShellVfsDir.mkpath(".")) {
|
|
||||||
qCCritical(logPaths) << "Could not create runtime vfs directory at"
|
|
||||||
<< this->mShellVfsDir.path();
|
|
||||||
this->shellVfsState = DirState::Failed;
|
|
||||||
} else {
|
|
||||||
this->shellVfsState = DirState::Ready;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
qCCritical(logPaths) << "Could not create shell runtime vfs path as it was not possible to "
|
|
||||||
"create the base runtime path.";
|
|
||||||
|
|
||||||
this->shellVfsState = DirState::Failed;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this->shellVfsState == DirState::Failed) return nullptr;
|
|
||||||
else return &this->mShellVfsDir;
|
|
||||||
}
|
|
||||||
|
|
||||||
void QsPaths::linkRunDir() {
|
void QsPaths::linkRunDir() {
|
||||||
if (auto* runDir = this->instanceRunDir()) {
|
if (auto* runDir = this->instanceRunDir()) {
|
||||||
auto pidDir = QDir(this->baseRunDir()->filePath("by-pid"));
|
auto pidDir = QDir(this->baseRunDir()->filePath("by-pid"));
|
||||||
auto* shellDir = this->shellRunDir();
|
auto* shellDir = this->shellRunDir();
|
||||||
|
|
||||||
if (!shellDir) {
|
if (!shellDir) {
|
||||||
qCCritical(
|
qCCritical(logPaths
|
||||||
logPaths
|
|
||||||
) << "Could not create by-id symlink as the shell runtime path could not be created.";
|
) << "Could not create by-id symlink as the shell runtime path could not be created.";
|
||||||
} else {
|
} else {
|
||||||
auto shellPath = shellDir->filePath(runDir->dirName());
|
auto shellPath = shellDir->filePath(runDir->dirName());
|
||||||
|
|
@ -324,16 +287,9 @@ QDir QsPaths::shellStateDir() {
|
||||||
|
|
||||||
QDir QsPaths::shellCacheDir() {
|
QDir QsPaths::shellCacheDir() {
|
||||||
if (this->shellCacheState == DirState::Unknown) {
|
if (this->shellCacheState == DirState::Unknown) {
|
||||||
QDir dir;
|
auto dir = QDir(QStandardPaths::writableLocation(QStandardPaths::CacheLocation));
|
||||||
if (this->shellCacheOverride.isEmpty()) {
|
dir = QDir(dir.filePath("by-shell"));
|
||||||
dir = QDir(QStandardPaths::writableLocation(QStandardPaths::CacheLocation));
|
dir = QDir(dir.filePath(this->shellId));
|
||||||
dir = QDir(dir.filePath("by-shell"));
|
|
||||||
dir = QDir(dir.filePath(this->shellId));
|
|
||||||
} else {
|
|
||||||
auto basedir = QStandardPaths::writableLocation(QStandardPaths::GenericCacheLocation);
|
|
||||||
dir = QDir(this->shellCacheOverride.replace("$BASE", basedir));
|
|
||||||
}
|
|
||||||
|
|
||||||
this->mShellCacheDir = dir;
|
this->mShellCacheDir = dir;
|
||||||
|
|
||||||
qCDebug(logPaths) << "Initialized cache path:" << dir.path();
|
qCDebug(logPaths) << "Initialized cache path:" << dir.path();
|
||||||
|
|
@ -361,7 +317,7 @@ void QsPaths::createLock() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
struct flock lock = {
|
auto lock = flock {
|
||||||
.l_type = F_WRLCK,
|
.l_type = F_WRLCK,
|
||||||
.l_whence = SEEK_SET,
|
.l_whence = SEEK_SET,
|
||||||
.l_start = 0,
|
.l_start = 0,
|
||||||
|
|
@ -379,8 +335,7 @@ void QsPaths::createLock() {
|
||||||
qCDebug(logPaths) << "Created instance lock at" << path;
|
qCDebug(logPaths) << "Created instance lock at" << path;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
qCCritical(
|
qCCritical(logPaths
|
||||||
logPaths
|
|
||||||
) << "Could not create instance lock, as the instance runtime directory could not be created.";
|
) << "Could not create instance lock, as the instance runtime directory could not be created.";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -389,7 +344,7 @@ bool QsPaths::checkLock(const QString& path, InstanceLockInfo* info, bool allowD
|
||||||
auto file = QFile(QDir(path).filePath("instance.lock"));
|
auto file = QFile(QDir(path).filePath("instance.lock"));
|
||||||
if (!file.open(QFile::ReadOnly)) return false;
|
if (!file.open(QFile::ReadOnly)) return false;
|
||||||
|
|
||||||
struct flock lock = {
|
auto lock = flock {
|
||||||
.l_type = F_WRLCK,
|
.l_type = F_WRLCK,
|
||||||
.l_whence = SEEK_SET,
|
.l_whence = SEEK_SET,
|
||||||
.l_start = 0,
|
.l_start = 0,
|
||||||
|
|
@ -412,35 +367,29 @@ bool QsPaths::checkLock(const QString& path, InstanceLockInfo* info, bool allowD
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
QPair<QVector<InstanceLockInfo>, QVector<InstanceLockInfo>>
|
QVector<InstanceLockInfo> QsPaths::collectInstances(const QString& path, bool fallbackDead) {
|
||||||
QsPaths::collectInstances(const QString& path, const QString& display) {
|
|
||||||
qCDebug(logPaths) << "Collecting instances from" << path;
|
qCDebug(logPaths) << "Collecting instances from" << path;
|
||||||
auto liveInstances = QVector<InstanceLockInfo>();
|
auto instances = QVector<InstanceLockInfo>();
|
||||||
auto deadInstances = QVector<InstanceLockInfo>();
|
|
||||||
auto dir = QDir(path);
|
auto dir = QDir(path);
|
||||||
|
|
||||||
InstanceLockInfo info;
|
InstanceLockInfo info;
|
||||||
for (auto& entry: dir.entryList(QDir::Dirs | QDir::NoDotAndDotDot)) {
|
for (auto& entry: dir.entryList(QDir::Dirs | QDir::NoDotAndDotDot)) {
|
||||||
auto path = dir.filePath(entry);
|
auto path = dir.filePath(entry);
|
||||||
|
|
||||||
if (QsPaths::checkLock(path, &info, true)) {
|
if (QsPaths::checkLock(path, &info, fallbackDead)) {
|
||||||
|
if (fallbackDead && info.pid != -1) {
|
||||||
|
fallbackDead = false;
|
||||||
|
instances.clear();
|
||||||
|
}
|
||||||
|
|
||||||
qCDebug(logPaths).nospace() << "Found instance " << info.instance.instanceId << " (pid "
|
qCDebug(logPaths).nospace() << "Found instance " << info.instance.instanceId << " (pid "
|
||||||
<< info.pid << ") at " << path;
|
<< info.pid << ") at " << path;
|
||||||
|
|
||||||
if (!display.isEmpty() && info.instance.display != display) {
|
instances.push_back(info);
|
||||||
qCDebug(logPaths) << "Skipped instance with mismatched display at" << path;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (info.pid == -1) {
|
|
||||||
deadInstances.push_back(info);
|
|
||||||
} else {
|
|
||||||
liveInstances.push_back(info);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
qCDebug(logPaths) << "Skipped potential instance at" << path;
|
qCDebug(logPaths) << "Skipped potential instance at" << path;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return qMakePair(liveInstances, deadInstances);
|
return instances;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
#pragma once
|
#pragma once
|
||||||
#include <qdatetime.h>
|
#include <qdatetime.h>
|
||||||
#include <qdir.h>
|
#include <qdir.h>
|
||||||
#include <qpair.h>
|
|
||||||
#include <qtypes.h>
|
#include <qtypes.h>
|
||||||
|
|
||||||
#include "instanceinfo.hpp"
|
#include "instanceinfo.hpp"
|
||||||
|
|
@ -17,24 +16,16 @@ QDataStream& operator>>(QDataStream& stream, InstanceLockInfo& info);
|
||||||
class QsPaths {
|
class QsPaths {
|
||||||
public:
|
public:
|
||||||
static QsPaths* instance();
|
static QsPaths* instance();
|
||||||
static void init(
|
static void init(QString shellId, QString pathId, QString dataOverride, QString stateOverride);
|
||||||
QString shellId,
|
|
||||||
QString pathId,
|
|
||||||
QString dataOverride,
|
|
||||||
QString stateOverride,
|
|
||||||
QString cacheOverride
|
|
||||||
);
|
|
||||||
static QDir crashDir(const QString& id);
|
static QDir crashDir(const QString& id);
|
||||||
static QString basePath(const QString& id);
|
static QString basePath(const QString& id);
|
||||||
static QString ipcPath(const QString& id);
|
static QString ipcPath(const QString& id);
|
||||||
static bool
|
static bool
|
||||||
checkLock(const QString& path, InstanceLockInfo* info = nullptr, bool allowDead = false);
|
checkLock(const QString& path, InstanceLockInfo* info = nullptr, bool allowDead = false);
|
||||||
static QPair<QVector<InstanceLockInfo>, QVector<InstanceLockInfo>>
|
static QVector<InstanceLockInfo> collectInstances(const QString& path, bool fallbackDead = false);
|
||||||
collectInstances(const QString& path, const QString& display);
|
|
||||||
|
|
||||||
QDir* baseRunDir();
|
QDir* baseRunDir();
|
||||||
QDir* shellRunDir();
|
QDir* shellRunDir();
|
||||||
QDir* shellVfsDir();
|
|
||||||
QDir* instanceRunDir();
|
QDir* instanceRunDir();
|
||||||
void linkRunDir();
|
void linkRunDir();
|
||||||
void linkPathDir();
|
void linkPathDir();
|
||||||
|
|
@ -55,11 +46,9 @@ private:
|
||||||
QString pathId;
|
QString pathId;
|
||||||
QDir mBaseRunDir;
|
QDir mBaseRunDir;
|
||||||
QDir mShellRunDir;
|
QDir mShellRunDir;
|
||||||
QDir mShellVfsDir;
|
|
||||||
QDir mInstanceRunDir;
|
QDir mInstanceRunDir;
|
||||||
DirState baseRunState = DirState::Unknown;
|
DirState baseRunState = DirState::Unknown;
|
||||||
DirState shellRunState = DirState::Unknown;
|
DirState shellRunState = DirState::Unknown;
|
||||||
DirState shellVfsState = DirState::Unknown;
|
|
||||||
DirState instanceRunState = DirState::Unknown;
|
DirState instanceRunState = DirState::Unknown;
|
||||||
|
|
||||||
QDir mShellDataDir;
|
QDir mShellDataDir;
|
||||||
|
|
@ -71,5 +60,4 @@ private:
|
||||||
|
|
||||||
QString shellDataOverride;
|
QString shellDataOverride;
|
||||||
QString shellStateOverride;
|
QString shellStateOverride;
|
||||||
QString shellCacheOverride;
|
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@
|
||||||
#include <qwindow.h>
|
#include <qwindow.h>
|
||||||
|
|
||||||
#include "../window/proxywindow.hpp"
|
#include "../window/proxywindow.hpp"
|
||||||
|
#include "../window/windowinterface.hpp"
|
||||||
#include "iconprovider.hpp"
|
#include "iconprovider.hpp"
|
||||||
#include "model.hpp"
|
#include "model.hpp"
|
||||||
#include "platformmenu_p.hpp"
|
#include "platformmenu_p.hpp"
|
||||||
|
|
@ -90,8 +91,10 @@ bool PlatformMenuEntry::display(QObject* parentWindow, int relativeX, int relati
|
||||||
} else if (parentWindow == nullptr) {
|
} else if (parentWindow == nullptr) {
|
||||||
qCritical() << "Cannot display PlatformMenuEntry with null parent window.";
|
qCritical() << "Cannot display PlatformMenuEntry with null parent window.";
|
||||||
return false;
|
return false;
|
||||||
} else if (auto* proxy = ProxyWindowBase::forObject(parentWindow)) {
|
} else if (auto* proxy = qobject_cast<ProxyWindowBase*>(parentWindow)) {
|
||||||
window = proxy->backingWindow();
|
window = proxy->backingWindow();
|
||||||
|
} else if (auto* interface = qobject_cast<WindowInterface*>(parentWindow)) {
|
||||||
|
window = interface->proxyWindow()->backingWindow();
|
||||||
} else {
|
} else {
|
||||||
qCritical() << "PlatformMenuEntry.display() must be called with a window.";
|
qCritical() << "PlatformMenuEntry.display() must be called with a window.";
|
||||||
return false;
|
return false;
|
||||||
|
|
|
||||||
|
|
@ -9,18 +9,6 @@ static QVector<QsEnginePlugin*> plugins; // NOLINT
|
||||||
|
|
||||||
void QsEnginePlugin::registerPlugin(QsEnginePlugin& plugin) { plugins.push_back(&plugin); }
|
void QsEnginePlugin::registerPlugin(QsEnginePlugin& plugin) { plugins.push_back(&plugin); }
|
||||||
|
|
||||||
void QsEnginePlugin::preinitPluginsOnly() {
|
|
||||||
plugins.removeIf([](QsEnginePlugin* plugin) { return !plugin->applies(); });
|
|
||||||
|
|
||||||
std::ranges::sort(plugins, [](QsEnginePlugin* a, QsEnginePlugin* b) {
|
|
||||||
return b->dependencies().contains(a->name());
|
|
||||||
});
|
|
||||||
|
|
||||||
for (QsEnginePlugin* plugin: plugins) {
|
|
||||||
plugin->preinit();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void QsEnginePlugin::initPlugins() {
|
void QsEnginePlugin::initPlugins() {
|
||||||
plugins.removeIf([](QsEnginePlugin* plugin) { return !plugin->applies(); });
|
plugins.removeIf([](QsEnginePlugin* plugin) { return !plugin->applies(); });
|
||||||
|
|
||||||
|
|
@ -28,10 +16,6 @@ void QsEnginePlugin::initPlugins() {
|
||||||
return b->dependencies().contains(a->name());
|
return b->dependencies().contains(a->name());
|
||||||
});
|
});
|
||||||
|
|
||||||
for (QsEnginePlugin* plugin: plugins) {
|
|
||||||
plugin->preinit();
|
|
||||||
}
|
|
||||||
|
|
||||||
for (QsEnginePlugin* plugin: plugins) {
|
for (QsEnginePlugin* plugin: plugins) {
|
||||||
plugin->init();
|
plugin->init();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,14 +18,12 @@ public:
|
||||||
virtual QString name() { return QString(); }
|
virtual QString name() { return QString(); }
|
||||||
virtual QList<QString> dependencies() { return {}; }
|
virtual QList<QString> dependencies() { return {}; }
|
||||||
virtual bool applies() { return true; }
|
virtual bool applies() { return true; }
|
||||||
virtual void preinit() {}
|
|
||||||
virtual void init() {}
|
virtual void init() {}
|
||||||
virtual void registerTypes() {}
|
virtual void registerTypes() {}
|
||||||
virtual void constructGeneration(EngineGeneration& /*unused*/) {} // NOLINT
|
virtual void constructGeneration(EngineGeneration& /*unused*/) {} // NOLINT
|
||||||
virtual void onReload() {}
|
virtual void onReload() {}
|
||||||
|
|
||||||
static void registerPlugin(QsEnginePlugin& plugin);
|
static void registerPlugin(QsEnginePlugin& plugin);
|
||||||
static void preinitPluginsOnly();
|
|
||||||
static void initPlugins();
|
static void initPlugins();
|
||||||
static void runConstructGeneration(EngineGeneration& generation);
|
static void runConstructGeneration(EngineGeneration& generation);
|
||||||
static void runOnReload();
|
static void runOnReload();
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@
|
||||||
#include <qwindow.h>
|
#include <qwindow.h>
|
||||||
|
|
||||||
#include "../window/proxywindow.hpp"
|
#include "../window/proxywindow.hpp"
|
||||||
|
#include "../window/windowinterface.hpp"
|
||||||
#include "types.hpp"
|
#include "types.hpp"
|
||||||
|
|
||||||
bool PopupAnchorState::operator==(const PopupAnchorState& other) const {
|
bool PopupAnchorState::operator==(const PopupAnchorState& other) const {
|
||||||
|
|
@ -27,7 +28,7 @@ void PopupAnchor::markClean() { this->lastState = this->state; }
|
||||||
void PopupAnchor::markDirty() { this->lastState.reset(); }
|
void PopupAnchor::markDirty() { this->lastState.reset(); }
|
||||||
|
|
||||||
QWindow* PopupAnchor::backingWindow() const {
|
QWindow* PopupAnchor::backingWindow() const {
|
||||||
return this->bProxyWindow ? this->bProxyWindow->backingWindow() : nullptr;
|
return this->mProxyWindow ? this->mProxyWindow->backingWindow() : nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
void PopupAnchor::setWindowInternal(QObject* window) {
|
void PopupAnchor::setWindowInternal(QObject* window) {
|
||||||
|
|
@ -35,12 +36,14 @@ void PopupAnchor::setWindowInternal(QObject* window) {
|
||||||
|
|
||||||
if (this->mWindow) {
|
if (this->mWindow) {
|
||||||
QObject::disconnect(this->mWindow, nullptr, this, nullptr);
|
QObject::disconnect(this->mWindow, nullptr, this, nullptr);
|
||||||
QObject::disconnect(this->bProxyWindow, nullptr, this, nullptr);
|
QObject::disconnect(this->mProxyWindow, nullptr, this, nullptr);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (window) {
|
if (window) {
|
||||||
if (auto* proxy = ProxyWindowBase::forObject(window)) {
|
if (auto* proxy = qobject_cast<ProxyWindowBase*>(window)) {
|
||||||
this->bProxyWindow = proxy;
|
this->mProxyWindow = proxy;
|
||||||
|
} else if (auto* interface = qobject_cast<WindowInterface*>(window)) {
|
||||||
|
this->mProxyWindow = interface->proxyWindow();
|
||||||
} else {
|
} else {
|
||||||
qWarning() << "Tried to set popup anchor window to" << window
|
qWarning() << "Tried to set popup anchor window to" << window
|
||||||
<< "which is not a quickshell window.";
|
<< "which is not a quickshell window.";
|
||||||
|
|
@ -52,7 +55,7 @@ void PopupAnchor::setWindowInternal(QObject* window) {
|
||||||
QObject::connect(this->mWindow, &QObject::destroyed, this, &PopupAnchor::onWindowDestroyed);
|
QObject::connect(this->mWindow, &QObject::destroyed, this, &PopupAnchor::onWindowDestroyed);
|
||||||
|
|
||||||
QObject::connect(
|
QObject::connect(
|
||||||
this->bProxyWindow,
|
this->mProxyWindow,
|
||||||
&ProxyWindowBase::backerVisibilityChanged,
|
&ProxyWindowBase::backerVisibilityChanged,
|
||||||
this,
|
this,
|
||||||
&PopupAnchor::backingWindowVisibilityChanged
|
&PopupAnchor::backingWindowVisibilityChanged
|
||||||
|
|
@ -67,7 +70,7 @@ void PopupAnchor::setWindowInternal(QObject* window) {
|
||||||
setnull:
|
setnull:
|
||||||
if (this->mWindow) {
|
if (this->mWindow) {
|
||||||
this->mWindow = nullptr;
|
this->mWindow = nullptr;
|
||||||
this->bProxyWindow = nullptr;
|
this->mProxyWindow = nullptr;
|
||||||
|
|
||||||
emit this->windowChanged();
|
emit this->windowChanged();
|
||||||
emit this->backingWindowVisibilityChanged();
|
emit this->backingWindowVisibilityChanged();
|
||||||
|
|
@ -97,7 +100,7 @@ void PopupAnchor::setItem(QQuickItem* item) {
|
||||||
|
|
||||||
void PopupAnchor::onWindowDestroyed() {
|
void PopupAnchor::onWindowDestroyed() {
|
||||||
this->mWindow = nullptr;
|
this->mWindow = nullptr;
|
||||||
this->bProxyWindow = nullptr;
|
this->mProxyWindow = nullptr;
|
||||||
emit this->windowChanged();
|
emit this->windowChanged();
|
||||||
emit this->backingWindowVisibilityChanged();
|
emit this->backingWindowVisibilityChanged();
|
||||||
}
|
}
|
||||||
|
|
@ -183,18 +186,14 @@ void PopupAnchor::updatePlacement(const QPoint& anchorpoint, const QSize& size)
|
||||||
}
|
}
|
||||||
|
|
||||||
void PopupAnchor::updateAnchor() {
|
void PopupAnchor::updateAnchor() {
|
||||||
if (this->mItem && this->bProxyWindow) {
|
if (this->mItem && this->mProxyWindow) {
|
||||||
auto baseRect =
|
auto baseRect =
|
||||||
this->mUserRect.isEmpty() ? this->mItem->boundingRect() : this->mUserRect.qrect();
|
this->mUserRect.isEmpty() ? this->mItem->boundingRect() : this->mUserRect.qrect();
|
||||||
|
auto rect = this->mProxyWindow->contentItem()->mapFromItem(
|
||||||
auto rect = this->bProxyWindow->contentItem()->mapFromItem(
|
|
||||||
this->mItem,
|
this->mItem,
|
||||||
baseRect.marginsRemoved(this->mMargins.qmargins())
|
baseRect.marginsRemoved(this->mMargins.qmargins())
|
||||||
);
|
);
|
||||||
|
|
||||||
if (rect.width() < 1) rect.setWidth(1);
|
|
||||||
if (rect.height() < 1) rect.setHeight(1);
|
|
||||||
|
|
||||||
this->setWindowRect(rect.toRect());
|
this->setWindowRect(rect.toRect());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@
|
||||||
#include <qnamespace.h>
|
#include <qnamespace.h>
|
||||||
#include <qobject.h>
|
#include <qobject.h>
|
||||||
#include <qpoint.h>
|
#include <qpoint.h>
|
||||||
#include <qproperty.h>
|
|
||||||
#include <qqmlintegration.h>
|
#include <qqmlintegration.h>
|
||||||
#include <qquickitem.h>
|
#include <qquickitem.h>
|
||||||
#include <qsize.h>
|
#include <qsize.h>
|
||||||
|
|
@ -140,9 +139,7 @@ public:
|
||||||
void markDirty();
|
void markDirty();
|
||||||
|
|
||||||
[[nodiscard]] QObject* window() const { return this->mWindow; }
|
[[nodiscard]] QObject* window() const { return this->mWindow; }
|
||||||
[[nodiscard]] QBindable<ProxyWindowBase*> bindableProxyWindow() const {
|
[[nodiscard]] ProxyWindowBase* proxyWindow() const { return this->mProxyWindow; }
|
||||||
return &this->bProxyWindow;
|
|
||||||
}
|
|
||||||
[[nodiscard]] QWindow* backingWindow() const;
|
[[nodiscard]] QWindow* backingWindow() const;
|
||||||
void setWindowInternal(QObject* window);
|
void setWindowInternal(QObject* window);
|
||||||
void setWindow(QObject* window);
|
void setWindow(QObject* window);
|
||||||
|
|
@ -196,12 +193,11 @@ private slots:
|
||||||
private:
|
private:
|
||||||
QObject* mWindow = nullptr;
|
QObject* mWindow = nullptr;
|
||||||
QQuickItem* mItem = nullptr;
|
QQuickItem* mItem = nullptr;
|
||||||
|
ProxyWindowBase* mProxyWindow = nullptr;
|
||||||
PopupAnchorState state;
|
PopupAnchorState state;
|
||||||
Box mUserRect;
|
Box mUserRect;
|
||||||
Margins mMargins;
|
Margins mMargins;
|
||||||
std::optional<PopupAnchorState> lastState;
|
std::optional<PopupAnchorState> lastState;
|
||||||
|
|
||||||
Q_OBJECT_BINDABLE_PROPERTY(PopupAnchor, ProxyWindowBase*, bProxyWindow);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
class PopupPositioner {
|
class PopupPositioner {
|
||||||
|
|
|
||||||
|
|
@ -8,10 +8,8 @@
|
||||||
#include <qguiapplication.h>
|
#include <qguiapplication.h>
|
||||||
#include <qicon.h>
|
#include <qicon.h>
|
||||||
#include <qjsengine.h>
|
#include <qjsengine.h>
|
||||||
#include <qlist.h>
|
|
||||||
#include <qlogging.h>
|
#include <qlogging.h>
|
||||||
#include <qobject.h>
|
#include <qobject.h>
|
||||||
#include <qprocess.h>
|
|
||||||
#include <qqmlcontext.h>
|
#include <qqmlcontext.h>
|
||||||
#include <qqmlengine.h>
|
#include <qqmlengine.h>
|
||||||
#include <qqmllist.h>
|
#include <qqmllist.h>
|
||||||
|
|
@ -23,14 +21,11 @@
|
||||||
#include <qwindowdefs.h>
|
#include <qwindowdefs.h>
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
|
|
||||||
#include "../io/processcore.hpp"
|
|
||||||
#include "generation.hpp"
|
#include "generation.hpp"
|
||||||
#include "iconimageprovider.hpp"
|
#include "iconimageprovider.hpp"
|
||||||
#include "instanceinfo.hpp"
|
|
||||||
#include "paths.hpp"
|
#include "paths.hpp"
|
||||||
#include "qmlscreen.hpp"
|
#include "qmlscreen.hpp"
|
||||||
#include "rootwrapper.hpp"
|
#include "rootwrapper.hpp"
|
||||||
#include "scanenv.hpp"
|
|
||||||
|
|
||||||
QuickshellSettings::QuickshellSettings() {
|
QuickshellSettings::QuickshellSettings() {
|
||||||
QObject::connect(
|
QObject::connect(
|
||||||
|
|
@ -61,9 +56,7 @@ void QuickshellSettings::setWorkingDirectory(QString workingDirectory) { // NOLI
|
||||||
emit this->workingDirectoryChanged();
|
emit this->workingDirectoryChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool QuickshellSettings::watchFiles() const {
|
bool QuickshellSettings::watchFiles() const { return this->mWatchFiles; }
|
||||||
return this->mWatchFiles && qEnvironmentVariableIsEmpty("QS_DISABLE_FILE_WATCHER");
|
|
||||||
}
|
|
||||||
|
|
||||||
void QuickshellSettings::setWatchFiles(bool watchFiles) {
|
void QuickshellSettings::setWatchFiles(bool watchFiles) {
|
||||||
if (watchFiles == this->mWatchFiles) return;
|
if (watchFiles == this->mWatchFiles) return;
|
||||||
|
|
@ -154,22 +147,6 @@ qint32 QuickshellGlobal::processId() const { // NOLINT
|
||||||
return getpid();
|
return getpid();
|
||||||
}
|
}
|
||||||
|
|
||||||
QString QuickshellGlobal::instanceId() const { // NOLINT
|
|
||||||
return InstanceInfo::CURRENT.instanceId;
|
|
||||||
}
|
|
||||||
|
|
||||||
QString QuickshellGlobal::shellId() const { // NOLINT
|
|
||||||
return InstanceInfo::CURRENT.shellId;
|
|
||||||
}
|
|
||||||
|
|
||||||
QString QuickshellGlobal::appId() const { // NOLINT
|
|
||||||
return InstanceInfo::CURRENT.appId;
|
|
||||||
}
|
|
||||||
|
|
||||||
QDateTime QuickshellGlobal::launchTime() const { // NOLINT
|
|
||||||
return InstanceInfo::CURRENT.launchTime;
|
|
||||||
}
|
|
||||||
|
|
||||||
qsizetype QuickshellGlobal::screensCount(QQmlListProperty<QuickshellScreenInfo>* /*unused*/) {
|
qsizetype QuickshellGlobal::screensCount(QQmlListProperty<QuickshellScreenInfo>* /*unused*/) {
|
||||||
return QuickshellTracked::instance()->screens.size();
|
return QuickshellTracked::instance()->screens.size();
|
||||||
}
|
}
|
||||||
|
|
@ -200,6 +177,12 @@ void QuickshellGlobal::reload(bool hard) {
|
||||||
root->reloadGraph(hard);
|
root->reloadGraph(hard);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QString QuickshellGlobal::shellRoot() const {
|
||||||
|
auto* generation = EngineGeneration::findObjectGeneration(this);
|
||||||
|
// already canonical
|
||||||
|
return generation->rootPath.path();
|
||||||
|
}
|
||||||
|
|
||||||
QString QuickshellGlobal::workingDirectory() const { // NOLINT
|
QString QuickshellGlobal::workingDirectory() const { // NOLINT
|
||||||
return QuickshellSettings::instance()->workingDirectory();
|
return QuickshellSettings::instance()->workingDirectory();
|
||||||
}
|
}
|
||||||
|
|
@ -230,22 +213,6 @@ void QuickshellGlobal::onClipboardChanged(QClipboard::Mode mode) {
|
||||||
if (mode == QClipboard::Clipboard) emit this->clipboardTextChanged();
|
if (mode == QClipboard::Clipboard) emit this->clipboardTextChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
QString QuickshellGlobal::shellDir() const {
|
|
||||||
return EngineGeneration::findObjectGeneration(this)->rootPath.path();
|
|
||||||
}
|
|
||||||
|
|
||||||
QString QuickshellGlobal::configDir() const {
|
|
||||||
qWarning() << "Quickshell.configDir is deprecated and may be removed in a future release. Use "
|
|
||||||
"Quickshell.shellDir.";
|
|
||||||
return this->shellDir();
|
|
||||||
}
|
|
||||||
|
|
||||||
QString QuickshellGlobal::shellRoot() const {
|
|
||||||
qWarning() << "Quickshell.shellRoot is deprecated and may be removed in a future release. Use "
|
|
||||||
"Quickshell.shellDir.";
|
|
||||||
return this->shellDir();
|
|
||||||
}
|
|
||||||
|
|
||||||
QString QuickshellGlobal::dataDir() const { // NOLINT
|
QString QuickshellGlobal::dataDir() const { // NOLINT
|
||||||
return QsPaths::instance()->shellDataDir().path();
|
return QsPaths::instance()->shellDataDir().path();
|
||||||
}
|
}
|
||||||
|
|
@ -258,16 +225,6 @@ QString QuickshellGlobal::cacheDir() const { // NOLINT
|
||||||
return QsPaths::instance()->shellCacheDir().path();
|
return QsPaths::instance()->shellCacheDir().path();
|
||||||
}
|
}
|
||||||
|
|
||||||
QString QuickshellGlobal::shellPath(const QString& path) const {
|
|
||||||
return this->shellDir() % '/' % path;
|
|
||||||
}
|
|
||||||
|
|
||||||
QString QuickshellGlobal::configPath(const QString& path) const {
|
|
||||||
qWarning() << "Quickshell.configPath() is deprecated and may be removed in a future release. Use "
|
|
||||||
"Quickshell.shellPath().";
|
|
||||||
return this->shellPath(path);
|
|
||||||
}
|
|
||||||
|
|
||||||
QString QuickshellGlobal::dataPath(const QString& path) const {
|
QString QuickshellGlobal::dataPath(const QString& path) const {
|
||||||
return this->dataDir() % '/' % path;
|
return this->dataDir() % '/' % path;
|
||||||
}
|
}
|
||||||
|
|
@ -287,39 +244,6 @@ QVariant QuickshellGlobal::env(const QString& variable) { // NOLINT
|
||||||
return qEnvironmentVariable(vstr.data());
|
return qEnvironmentVariable(vstr.data());
|
||||||
}
|
}
|
||||||
|
|
||||||
void QuickshellGlobal::execDetached(QList<QString> command) {
|
|
||||||
QuickshellGlobal::execDetached(qs::io::process::ProcessContext(std::move(command)));
|
|
||||||
}
|
|
||||||
|
|
||||||
void QuickshellGlobal::execDetached(const qs::io::process::ProcessContext& context) {
|
|
||||||
if (context.command.isEmpty()) {
|
|
||||||
qWarning() << "Cannot start process as command is empty.";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const auto& cmd = context.command.first();
|
|
||||||
auto args = context.command.sliced(1);
|
|
||||||
|
|
||||||
QProcess process;
|
|
||||||
qs::io::process::setupProcessEnvironment(&process, context.clearEnvironment, context.environment);
|
|
||||||
|
|
||||||
if (!context.workingDirectory.isEmpty()) {
|
|
||||||
process.setWorkingDirectory(context.workingDirectory);
|
|
||||||
}
|
|
||||||
|
|
||||||
process.setProgram(cmd);
|
|
||||||
process.setArguments(args);
|
|
||||||
|
|
||||||
process.setStandardInputFile(QProcess::nullDevice());
|
|
||||||
|
|
||||||
if (context.unbindStdout) {
|
|
||||||
process.setStandardOutputFile(QProcess::nullDevice());
|
|
||||||
process.setStandardErrorFile(QProcess::nullDevice());
|
|
||||||
}
|
|
||||||
|
|
||||||
process.startDetached();
|
|
||||||
}
|
|
||||||
|
|
||||||
QString QuickshellGlobal::iconPath(const QString& icon) {
|
QString QuickshellGlobal::iconPath(const QString& icon) {
|
||||||
return IconImageProvider::requestString(icon);
|
return IconImageProvider::requestString(icon);
|
||||||
}
|
}
|
||||||
|
|
@ -333,16 +257,6 @@ QString QuickshellGlobal::iconPath(const QString& icon, const QString& fallback)
|
||||||
return IconImageProvider::requestString(icon, "", fallback);
|
return IconImageProvider::requestString(icon, "", fallback);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool QuickshellGlobal::hasThemeIcon(const QString& icon) { return QIcon::hasThemeIcon(icon); }
|
|
||||||
|
|
||||||
bool QuickshellGlobal::hasVersion(qint32 major, qint32 minor, const QStringList& features) {
|
|
||||||
return qs::scan::env::PreprocEnv::hasVersion(major, minor, features);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool QuickshellGlobal::hasVersion(qint32 major, qint32 minor) {
|
|
||||||
return QuickshellGlobal::hasVersion(major, minor, QStringList());
|
|
||||||
}
|
|
||||||
|
|
||||||
QuickshellGlobal* QuickshellGlobal::create(QQmlEngine* engine, QJSEngine* /*unused*/) {
|
QuickshellGlobal* QuickshellGlobal::create(QQmlEngine* engine, QJSEngine* /*unused*/) {
|
||||||
auto* qsg = new QuickshellGlobal();
|
auto* qsg = new QuickshellGlobal();
|
||||||
auto* generation = EngineGeneration::findEngineGeneration(engine);
|
auto* generation = EngineGeneration::findEngineGeneration(engine);
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,7 @@
|
||||||
|
|
||||||
#include <qclipboard.h>
|
#include <qclipboard.h>
|
||||||
#include <qcontainerfwd.h>
|
#include <qcontainerfwd.h>
|
||||||
#include <qhash.h>
|
|
||||||
#include <qjsengine.h>
|
#include <qjsengine.h>
|
||||||
#include <qlist.h>
|
|
||||||
#include <qobject.h>
|
#include <qobject.h>
|
||||||
#include <qqmlengine.h>
|
#include <qqmlengine.h>
|
||||||
#include <qqmlintegration.h>
|
#include <qqmlintegration.h>
|
||||||
|
|
@ -15,9 +13,6 @@
|
||||||
#include <qvariant.h>
|
#include <qvariant.h>
|
||||||
#include <qwindowdefs.h>
|
#include <qwindowdefs.h>
|
||||||
|
|
||||||
#include "../io/processcore.hpp"
|
|
||||||
#include "doc.hpp"
|
|
||||||
#include "instanceinfo.hpp"
|
|
||||||
#include "qmlscreen.hpp"
|
#include "qmlscreen.hpp"
|
||||||
|
|
||||||
///! Accessor for some options under the Quickshell type.
|
///! Accessor for some options under the Quickshell type.
|
||||||
|
|
@ -84,21 +79,6 @@ class QuickshellGlobal: public QObject {
|
||||||
// clang-format off
|
// clang-format off
|
||||||
/// Quickshell's process id.
|
/// Quickshell's process id.
|
||||||
Q_PROPERTY(qint32 processId READ processId CONSTANT);
|
Q_PROPERTY(qint32 processId READ processId CONSTANT);
|
||||||
/// A unique identifier for this Quickshell instance
|
|
||||||
Q_PROPERTY(QString instanceId READ instanceId CONSTANT)
|
|
||||||
/// The shell ID, used to differentiate between different shell configurations.
|
|
||||||
///
|
|
||||||
/// Defaults to a stable value derived from the config path.
|
|
||||||
/// Can be overridden with `//@ pragma ShellId <id>` in the root qml file.
|
|
||||||
Q_PROPERTY(QString shellId READ shellId CONSTANT)
|
|
||||||
/// The desktop application ID.
|
|
||||||
///
|
|
||||||
/// Defaults to `org.quickshell`.
|
|
||||||
/// Can be overridden with `//@ pragma AppId <id>` in the root qml file
|
|
||||||
/// or the `QS_APP_ID` environment variable.
|
|
||||||
Q_PROPERTY(QString appId READ appId CONSTANT)
|
|
||||||
/// The time at which this Quickshell instance was launched.
|
|
||||||
Q_PROPERTY(QDateTime launchTime READ launchTime CONSTANT)
|
|
||||||
/// All currently connected screens.
|
/// All currently connected screens.
|
||||||
///
|
///
|
||||||
/// This property updates as connected screens change.
|
/// This property updates as connected screens change.
|
||||||
|
|
@ -124,10 +104,6 @@ class QuickshellGlobal: public QObject {
|
||||||
///
|
///
|
||||||
/// The root directory is the folder containing the entrypoint to your shell, often referred
|
/// The root directory is the folder containing the entrypoint to your shell, often referred
|
||||||
/// to as `shell.qml`.
|
/// to as `shell.qml`.
|
||||||
Q_PROPERTY(QString shellDir READ shellDir CONSTANT);
|
|
||||||
/// > [!WARNING] Deprecated: Renamed to @@shellDir for clarity.
|
|
||||||
Q_PROPERTY(QString configDir READ configDir CONSTANT);
|
|
||||||
/// > [!WARNING] Deprecated: Renamed to @@shellDir for consistency.
|
|
||||||
Q_PROPERTY(QString shellRoot READ shellRoot CONSTANT);
|
Q_PROPERTY(QString shellRoot READ shellRoot CONSTANT);
|
||||||
/// Quickshell's working directory. Defaults to whereever quickshell was launched from.
|
/// Quickshell's working directory. Defaults to whereever quickshell was launched from.
|
||||||
Q_PROPERTY(QString workingDirectory READ workingDirectory WRITE setWorkingDirectory NOTIFY workingDirectoryChanged);
|
Q_PROPERTY(QString workingDirectory READ workingDirectory WRITE setWorkingDirectory NOTIFY workingDirectoryChanged);
|
||||||
|
|
@ -143,21 +119,18 @@ class QuickshellGlobal: public QObject {
|
||||||
/// Usually `~/.local/share/quickshell/by-shell/<shell-id>`
|
/// Usually `~/.local/share/quickshell/by-shell/<shell-id>`
|
||||||
///
|
///
|
||||||
/// Can be overridden using `//@ pragma DataDir $BASE/path` in the root qml file, where `$BASE`
|
/// Can be overridden using `//@ pragma DataDir $BASE/path` in the root qml file, where `$BASE`
|
||||||
/// corresponds to `$XDG_DATA_HOME` (usually `~/.local/share`).
|
/// corrosponds to `$XDG_DATA_HOME` (usually `~/.local/share`).
|
||||||
Q_PROPERTY(QString dataDir READ dataDir CONSTANT);
|
Q_PROPERTY(QString dataDir READ dataDir CONSTANT);
|
||||||
/// The per-shell state directory.
|
/// The per-shell state directory.
|
||||||
///
|
///
|
||||||
/// Usually `~/.local/state/quickshell/by-shell/<shell-id>`
|
/// Usually `~/.local/state/quickshell/by-shell/<shell-id>`
|
||||||
///
|
///
|
||||||
/// Can be overridden using `//@ pragma StateDir $BASE/path` in the root qml file, where `$BASE`
|
/// Can be overridden using `//@ pragma StateDir $BASE/path` in the root qml file, where `$BASE`
|
||||||
/// corresponds to `$XDG_STATE_HOME` (usually `~/.local/state`).
|
/// corrosponds to `$XDG_STATE_HOME` (usually `~/.local/state`).
|
||||||
Q_PROPERTY(QString stateDir READ stateDir CONSTANT);
|
Q_PROPERTY(QString stateDir READ stateDir CONSTANT);
|
||||||
/// The per-shell cache directory.
|
/// The per-shell cache directory.
|
||||||
///
|
///
|
||||||
/// Usually `~/.cache/quickshell/by-shell/<shell-id>`
|
/// Usually `~/.cache/quickshell/by-shell/<shell-id>`
|
||||||
///
|
|
||||||
/// Can be overridden using `//@ pragma CacheDir $BASE/path` in the root qml file, where `$BASE`
|
|
||||||
/// corresponds to `$XDG_CACHE_HOME` (usually `~/.cache`).
|
|
||||||
Q_PROPERTY(QString cacheDir READ cacheDir CONSTANT);
|
Q_PROPERTY(QString cacheDir READ cacheDir CONSTANT);
|
||||||
// clang-format on
|
// clang-format on
|
||||||
QML_SINGLETON;
|
QML_SINGLETON;
|
||||||
|
|
@ -165,10 +138,6 @@ class QuickshellGlobal: public QObject {
|
||||||
|
|
||||||
public:
|
public:
|
||||||
[[nodiscard]] qint32 processId() const;
|
[[nodiscard]] qint32 processId() const;
|
||||||
[[nodiscard]] QString instanceId() const;
|
|
||||||
[[nodiscard]] QString shellId() const;
|
|
||||||
[[nodiscard]] QString appId() const;
|
|
||||||
[[nodiscard]] QDateTime launchTime() const;
|
|
||||||
|
|
||||||
QQmlListProperty<QuickshellScreenInfo> screens();
|
QQmlListProperty<QuickshellScreenInfo> screens();
|
||||||
|
|
||||||
|
|
@ -183,30 +152,6 @@ public:
|
||||||
/// Returns the string value of an environment variable or null if it is not set.
|
/// Returns the string value of an environment variable or null if it is not set.
|
||||||
Q_INVOKABLE QVariant env(const QString& variable);
|
Q_INVOKABLE QVariant env(const QString& variable);
|
||||||
|
|
||||||
// MUST be before execDetached(ctx) or the other will be called with a default constructed obj.
|
|
||||||
QSDOC_HIDE Q_INVOKABLE static void execDetached(QList<QString> command);
|
|
||||||
/// Launch a process detached from Quickshell.
|
|
||||||
///
|
|
||||||
/// The context parameter can either be a list of command arguments or a JS object with the following fields:
|
|
||||||
/// - `command`: A list containing the command and all its arguments. See @@Quickshell.Io.Process.command.
|
|
||||||
/// - `environment`: Changes to make to the process environment. See @@Quickshell.Io.Process.environment.
|
|
||||||
/// - `clearEnvironment`: Removes all variables from the environment if true.
|
|
||||||
/// - `workingDirectory`: The working directory the command should run in.
|
|
||||||
///
|
|
||||||
/// > [!WARNING] This does not run command in a shell. All arguments to the command
|
|
||||||
/// > must be in separate values in the list, e.g. `["echo", "hello"]`
|
|
||||||
/// > and not `["echo hello"]`.
|
|
||||||
/// >
|
|
||||||
/// > Additionally, shell scripts must be run by your shell,
|
|
||||||
/// > e.g. `["sh", "script.sh"]` instead of `["script.sh"]` unless the script
|
|
||||||
/// > has a shebang.
|
|
||||||
///
|
|
||||||
/// > [!INFO] You can use `["sh", "-c", <your command>]` to execute your command with
|
|
||||||
/// > the system shell.
|
|
||||||
///
|
|
||||||
/// This function is equivalent to @@Quickshell.Io.Process.startDetached().
|
|
||||||
Q_INVOKABLE static void execDetached(const qs::io::process::ProcessContext& context);
|
|
||||||
|
|
||||||
/// Returns a string usable for a @@QtQuick.Image.source for a given system icon.
|
/// 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,
|
/// > [!INFO] By default, icons are loaded from the theme selected by the qt platform theme,
|
||||||
|
|
@ -222,12 +167,6 @@ public:
|
||||||
/// Setting the `fallback` parameter of `iconPath` will attempt to load the fallback
|
/// Setting the `fallback` parameter of `iconPath` will attempt to load the fallback
|
||||||
/// icon if the requested one could not be loaded.
|
/// icon if the requested one could not be loaded.
|
||||||
Q_INVOKABLE static QString iconPath(const QString& icon, const QString& fallback);
|
Q_INVOKABLE static QString iconPath(const QString& icon, const QString& fallback);
|
||||||
/// Check if specified icon has an available icon in your icon theme
|
|
||||||
Q_INVOKABLE static bool hasThemeIcon(const QString& icon);
|
|
||||||
/// Equivalent to `${Quickshell.configDir}/${path}`
|
|
||||||
Q_INVOKABLE [[nodiscard]] QString shellPath(const QString& path) const;
|
|
||||||
/// > [!WARNING] Deprecated: Renamed to @@shellPath() for clarity.
|
|
||||||
Q_INVOKABLE [[nodiscard]] QString configPath(const QString& path) const;
|
|
||||||
/// Equivalent to `${Quickshell.dataDir}/${path}`
|
/// Equivalent to `${Quickshell.dataDir}/${path}`
|
||||||
Q_INVOKABLE [[nodiscard]] QString dataPath(const QString& path) const;
|
Q_INVOKABLE [[nodiscard]] QString dataPath(const QString& path) const;
|
||||||
/// Equivalent to `${Quickshell.stateDir}/${path}`
|
/// Equivalent to `${Quickshell.stateDir}/${path}`
|
||||||
|
|
@ -239,27 +178,10 @@ public:
|
||||||
///
|
///
|
||||||
/// The popup can also be blocked by setting `QS_NO_RELOAD_POPUP=1`.
|
/// The popup can also be blocked by setting `QS_NO_RELOAD_POPUP=1`.
|
||||||
Q_INVOKABLE void inhibitReloadPopup() { this->mInhibitReloadPopup = true; }
|
Q_INVOKABLE void inhibitReloadPopup() { this->mInhibitReloadPopup = true; }
|
||||||
/// Check if Quickshell's version is at least `major.minor` and the listed
|
|
||||||
/// unreleased features are available. If Quickshell is newer than the given version
|
|
||||||
/// it is assumed that all unreleased features are present. The unreleased feature list
|
|
||||||
/// may be omitted.
|
|
||||||
///
|
|
||||||
/// > [!NOTE] You can feature gate code blocks using Quickshell's preprocessor which
|
|
||||||
/// > has the same function available.
|
|
||||||
/// >
|
|
||||||
/// > ```qml
|
|
||||||
/// > //@ if hasVersion(0, 3, ["feature"])
|
|
||||||
/// > ...
|
|
||||||
/// > //@ endif
|
|
||||||
/// > ```
|
|
||||||
Q_INVOKABLE static bool hasVersion(qint32 major, qint32 minor, const QStringList& features);
|
|
||||||
Q_INVOKABLE static bool hasVersion(qint32 major, qint32 minor);
|
|
||||||
|
|
||||||
void clearReloadPopupInhibit() { this->mInhibitReloadPopup = false; }
|
void clearReloadPopupInhibit() { this->mInhibitReloadPopup = false; }
|
||||||
[[nodiscard]] bool isReloadPopupInhibited() const { return this->mInhibitReloadPopup; }
|
[[nodiscard]] bool isReloadPopupInhibited() const { return this->mInhibitReloadPopup; }
|
||||||
|
|
||||||
[[nodiscard]] QString shellDir() const;
|
|
||||||
[[nodiscard]] QString configDir() const;
|
|
||||||
[[nodiscard]] QString shellRoot() const;
|
[[nodiscard]] QString shellRoot() const;
|
||||||
|
|
||||||
[[nodiscard]] QString workingDirectory() const;
|
[[nodiscard]] QString workingDirectory() const;
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
#include "qsintercept.hpp"
|
#include "qsintercept.hpp"
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
|
|
||||||
#include <qdir.h>
|
|
||||||
#include <qhash.h>
|
#include <qhash.h>
|
||||||
#include <qiodevice.h>
|
#include <qiodevice.h>
|
||||||
#include <qlogging.h>
|
#include <qlogging.h>
|
||||||
|
|
@ -15,9 +14,7 @@
|
||||||
#include <qtypes.h>
|
#include <qtypes.h>
|
||||||
#include <qurl.h>
|
#include <qurl.h>
|
||||||
|
|
||||||
#include "logcat.hpp"
|
Q_LOGGING_CATEGORY(logQsIntercept, "quickshell.interceptor", QtWarningMsg);
|
||||||
|
|
||||||
QS_LOGGING_CATEGORY(logQsIntercept, "quickshell.interceptor", QtWarningMsg);
|
|
||||||
|
|
||||||
QUrl QsUrlInterceptor::intercept(
|
QUrl QsUrlInterceptor::intercept(
|
||||||
const QUrl& originalUrl,
|
const QUrl& originalUrl,
|
||||||
|
|
@ -26,44 +23,27 @@ QUrl QsUrlInterceptor::intercept(
|
||||||
auto url = originalUrl;
|
auto url = originalUrl;
|
||||||
|
|
||||||
if (url.scheme() == "root") {
|
if (url.scheme() == "root") {
|
||||||
url.setScheme("qs");
|
url.setScheme("qsintercept");
|
||||||
|
|
||||||
auto path = url.path();
|
auto path = url.path();
|
||||||
if (path.startsWith('/')) path = path.sliced(1);
|
if (path.startsWith('/')) path = path.sliced(1);
|
||||||
url.setPath("@/qs/" % path);
|
url.setPath(this->configRoot.filePath(path));
|
||||||
|
|
||||||
qCDebug(logQsIntercept) << "Rewrote root intercept" << originalUrl << "to" << url;
|
qCDebug(logQsIntercept) << "Rewrote root intercept" << originalUrl << "to" << url;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (url.scheme() == "qs") {
|
// Some types such as Image take into account where they are loading from, and force
|
||||||
auto path = url.path();
|
// asynchronous loading over a network. qsintercept is considered to be over a network.
|
||||||
|
if (type == QQmlAbstractUrlInterceptor::DataType::UrlString && url.scheme() == "qsintercept") {
|
||||||
|
// Qt.resolvedUrl and context->resolvedUrl can use this on qml files, in which
|
||||||
|
// case we want to keep the intercept, otherwise objects created from those paths
|
||||||
|
// will not be able to use singletons.
|
||||||
|
if (url.path().endsWith(".qml")) return url;
|
||||||
|
|
||||||
// Our import path is on "qs:@/".
|
auto newUrl = url;
|
||||||
// We want to blackhole any import resolution outside of the config folder as it breaks Qt
|
newUrl.setScheme("file");
|
||||||
// but NOT file lookups that might be on "qs:/" due to a missing "file:/" prefix.
|
qCDebug(logQsIntercept) << "Rewrote intercept" << url << "to" << newUrl;
|
||||||
if (path.startsWith("@/qs/")) {
|
return newUrl;
|
||||||
path = this->configRoot.filePath(path.sliced(5));
|
|
||||||
} else if (!path.startsWith("/")) {
|
|
||||||
qCDebug(logQsIntercept) << "Blackholed import URL" << url;
|
|
||||||
return QUrl("qrc:/qs-blackhole");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Some types such as Image take into account where they are loading from, and force
|
|
||||||
// asynchronous loading over a network. qs: is considered to be over a network.
|
|
||||||
// In those cases we want to return a file:// url so asynchronous loading is not forced.
|
|
||||||
if (type == QQmlAbstractUrlInterceptor::DataType::UrlString) {
|
|
||||||
// Qt.resolvedUrl and context->resolvedUrl can use this on qml files, in which
|
|
||||||
// case we want to keep the intercept, otherwise objects created from those paths
|
|
||||||
// will not be able to use singletons.
|
|
||||||
if (path.endsWith(".qml")) return url;
|
|
||||||
|
|
||||||
auto newUrl = url;
|
|
||||||
newUrl.setScheme("file");
|
|
||||||
// above check asserts path starts with /qs/
|
|
||||||
newUrl.setPath(path);
|
|
||||||
qCDebug(logQsIntercept) << "Rewrote intercept" << url << "to" << newUrl;
|
|
||||||
return newUrl;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return url;
|
return url;
|
||||||
|
|
@ -85,12 +65,10 @@ qint64 QsInterceptDataReply::readData(char* data, qint64 maxSize) {
|
||||||
}
|
}
|
||||||
|
|
||||||
QsInterceptNetworkAccessManager::QsInterceptNetworkAccessManager(
|
QsInterceptNetworkAccessManager::QsInterceptNetworkAccessManager(
|
||||||
const QDir& configRoot,
|
|
||||||
const QHash<QString, QString>& fileIntercepts,
|
const QHash<QString, QString>& fileIntercepts,
|
||||||
QObject* parent
|
QObject* parent
|
||||||
)
|
)
|
||||||
: QNetworkAccessManager(parent)
|
: QNetworkAccessManager(parent)
|
||||||
, configRoot(configRoot)
|
|
||||||
, fileIntercepts(fileIntercepts) {}
|
, fileIntercepts(fileIntercepts) {}
|
||||||
|
|
||||||
QNetworkReply* QsInterceptNetworkAccessManager::createRequest(
|
QNetworkReply* QsInterceptNetworkAccessManager::createRequest(
|
||||||
|
|
@ -99,26 +77,19 @@ QNetworkReply* QsInterceptNetworkAccessManager::createRequest(
|
||||||
QIODevice* outgoingData
|
QIODevice* outgoingData
|
||||||
) {
|
) {
|
||||||
auto url = req.url();
|
auto url = req.url();
|
||||||
|
if (url.scheme() == "qsintercept") {
|
||||||
if (url.scheme() == "qs") {
|
|
||||||
auto path = url.path();
|
auto path = url.path();
|
||||||
|
|
||||||
if (path.startsWith("@/qs/")) path = this->configRoot.filePath(path.sliced(5));
|
|
||||||
// otherwise pass through to fs
|
|
||||||
|
|
||||||
qCDebug(logQsIntercept) << "Got intercept for" << path << "contains"
|
qCDebug(logQsIntercept) << "Got intercept for" << path << "contains"
|
||||||
<< this->fileIntercepts.value(path);
|
<< this->fileIntercepts.value(path);
|
||||||
|
auto data = this->fileIntercepts.value(path);
|
||||||
if (auto data = this->fileIntercepts.value(path); !data.isEmpty()) {
|
if (data != nullptr) {
|
||||||
return new QsInterceptDataReply(data, this);
|
return new QsInterceptDataReply(data, this);
|
||||||
}
|
}
|
||||||
|
|
||||||
auto fileReq = req;
|
auto fileReq = req;
|
||||||
auto fileUrl = req.url();
|
auto fileUrl = req.url();
|
||||||
fileUrl.setScheme("file");
|
fileUrl.setScheme("file");
|
||||||
fileUrl.setPath(path);
|
|
||||||
qCDebug(logQsIntercept) << "Passing through intercept" << url << "to" << fileUrl;
|
qCDebug(logQsIntercept) << "Passing through intercept" << url << "to" << fileUrl;
|
||||||
|
|
||||||
fileReq.setUrl(fileUrl);
|
fileReq.setUrl(fileUrl);
|
||||||
return this->QNetworkAccessManager::createRequest(op, fileReq, outgoingData);
|
return this->QNetworkAccessManager::createRequest(op, fileReq, outgoingData);
|
||||||
}
|
}
|
||||||
|
|
@ -127,5 +98,5 @@ QNetworkReply* QsInterceptNetworkAccessManager::createRequest(
|
||||||
}
|
}
|
||||||
|
|
||||||
QNetworkAccessManager* QsInterceptNetworkAccessManagerFactory::create(QObject* parent) {
|
QNetworkAccessManager* QsInterceptNetworkAccessManagerFactory::create(QObject* parent) {
|
||||||
return new QsInterceptNetworkAccessManager(this->configRoot, this->fileIntercepts, parent);
|
return new QsInterceptNetworkAccessManager(this->fileIntercepts, parent);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,9 +10,7 @@
|
||||||
#include <qqmlnetworkaccessmanagerfactory.h>
|
#include <qqmlnetworkaccessmanagerfactory.h>
|
||||||
#include <qurl.h>
|
#include <qurl.h>
|
||||||
|
|
||||||
#include "logcat.hpp"
|
Q_DECLARE_LOGGING_CATEGORY(logQsIntercept);
|
||||||
|
|
||||||
QS_DECLARE_LOGGING_CATEGORY(logQsIntercept);
|
|
||||||
|
|
||||||
class QsUrlInterceptor: public QQmlAbstractUrlInterceptor {
|
class QsUrlInterceptor: public QQmlAbstractUrlInterceptor {
|
||||||
public:
|
public:
|
||||||
|
|
@ -45,7 +43,6 @@ class QsInterceptNetworkAccessManager: public QNetworkAccessManager {
|
||||||
|
|
||||||
public:
|
public:
|
||||||
QsInterceptNetworkAccessManager(
|
QsInterceptNetworkAccessManager(
|
||||||
const QDir& configRoot,
|
|
||||||
const QHash<QString, QString>& fileIntercepts,
|
const QHash<QString, QString>& fileIntercepts,
|
||||||
QObject* parent = nullptr
|
QObject* parent = nullptr
|
||||||
);
|
);
|
||||||
|
|
@ -58,21 +55,15 @@ protected:
|
||||||
) override;
|
) override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
QDir configRoot;
|
|
||||||
const QHash<QString, QString>& fileIntercepts;
|
const QHash<QString, QString>& fileIntercepts;
|
||||||
};
|
};
|
||||||
|
|
||||||
class QsInterceptNetworkAccessManagerFactory: public QQmlNetworkAccessManagerFactory {
|
class QsInterceptNetworkAccessManagerFactory: public QQmlNetworkAccessManagerFactory {
|
||||||
public:
|
public:
|
||||||
QsInterceptNetworkAccessManagerFactory(
|
QsInterceptNetworkAccessManagerFactory(const QHash<QString, QString>& fileIntercepts)
|
||||||
const QDir& configRoot,
|
: fileIntercepts(fileIntercepts) {}
|
||||||
const QHash<QString, QString>& fileIntercepts
|
|
||||||
)
|
|
||||||
: configRoot(configRoot)
|
|
||||||
, fileIntercepts(fileIntercepts) {}
|
|
||||||
QNetworkAccessManager* create(QObject* parent) override;
|
QNetworkAccessManager* create(QObject* parent) override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
QDir configRoot;
|
|
||||||
const QHash<QString, QString>& fileIntercepts;
|
const QHash<QString, QString>& fileIntercepts;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -46,8 +46,8 @@ class QsMenuHandle: public QObject {
|
||||||
public:
|
public:
|
||||||
explicit QsMenuHandle(QObject* parent): QObject(parent) {}
|
explicit QsMenuHandle(QObject* parent): QObject(parent) {}
|
||||||
|
|
||||||
virtual void refHandle() {}
|
virtual void refHandle() {};
|
||||||
virtual void unrefHandle() {}
|
virtual void unrefHandle() {};
|
||||||
|
|
||||||
[[nodiscard]] virtual QsMenuEntry* menu() = 0;
|
[[nodiscard]] virtual QsMenuEntry* menu() = 0;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
#include "region.hpp"
|
#include "region.hpp"
|
||||||
#include <algorithm>
|
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
|
|
||||||
#include <qobject.h>
|
#include <qobject.h>
|
||||||
|
|
@ -19,17 +18,10 @@ PendingRegion::PendingRegion(QObject* parent): QObject(parent) {
|
||||||
QObject::connect(this, &PendingRegion::yChanged, this, &PendingRegion::changed);
|
QObject::connect(this, &PendingRegion::yChanged, this, &PendingRegion::changed);
|
||||||
QObject::connect(this, &PendingRegion::widthChanged, this, &PendingRegion::changed);
|
QObject::connect(this, &PendingRegion::widthChanged, this, &PendingRegion::changed);
|
||||||
QObject::connect(this, &PendingRegion::heightChanged, this, &PendingRegion::changed);
|
QObject::connect(this, &PendingRegion::heightChanged, this, &PendingRegion::changed);
|
||||||
QObject::connect(this, &PendingRegion::radiusChanged, this, &PendingRegion::changed);
|
|
||||||
QObject::connect(this, &PendingRegion::topLeftRadiusChanged, this, &PendingRegion::changed);
|
|
||||||
QObject::connect(this, &PendingRegion::topRightRadiusChanged, this, &PendingRegion::changed);
|
|
||||||
QObject::connect(this, &PendingRegion::bottomLeftRadiusChanged, this, &PendingRegion::changed);
|
|
||||||
QObject::connect(this, &PendingRegion::bottomRightRadiusChanged, this, &PendingRegion::changed);
|
|
||||||
QObject::connect(this, &PendingRegion::childrenChanged, this, &PendingRegion::changed);
|
QObject::connect(this, &PendingRegion::childrenChanged, this, &PendingRegion::changed);
|
||||||
}
|
}
|
||||||
|
|
||||||
void PendingRegion::setItem(QQuickItem* item) {
|
void PendingRegion::setItem(QQuickItem* item) {
|
||||||
if (item == this->mItem) return;
|
|
||||||
|
|
||||||
if (this->mItem != nullptr) {
|
if (this->mItem != nullptr) {
|
||||||
QObject::disconnect(this->mItem, nullptr, this, nullptr);
|
QObject::disconnect(this->mItem, nullptr, this, nullptr);
|
||||||
}
|
}
|
||||||
|
|
@ -51,79 +43,6 @@ void PendingRegion::onItemDestroyed() { this->mItem = nullptr; }
|
||||||
|
|
||||||
void PendingRegion::onChildDestroyed() { this->mRegions.removeAll(this->sender()); }
|
void PendingRegion::onChildDestroyed() { this->mRegions.removeAll(this->sender()); }
|
||||||
|
|
||||||
qint32 PendingRegion::radius() const { return this->mRadius; }
|
|
||||||
|
|
||||||
void PendingRegion::setRadius(qint32 radius) {
|
|
||||||
if (radius == this->mRadius) return;
|
|
||||||
this->mRadius = radius;
|
|
||||||
emit this->radiusChanged();
|
|
||||||
|
|
||||||
if (!(this->mCornerOverrides & TopLeft)) emit this->topLeftRadiusChanged();
|
|
||||||
if (!(this->mCornerOverrides & TopRight)) emit this->topRightRadiusChanged();
|
|
||||||
if (!(this->mCornerOverrides & BottomLeft)) emit this->bottomLeftRadiusChanged();
|
|
||||||
if (!(this->mCornerOverrides & BottomRight)) emit this->bottomRightRadiusChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
qint32 PendingRegion::topLeftRadius() const {
|
|
||||||
return (this->mCornerOverrides & TopLeft) ? this->mTopLeftRadius : this->mRadius;
|
|
||||||
}
|
|
||||||
|
|
||||||
void PendingRegion::setTopLeftRadius(qint32 radius) {
|
|
||||||
this->mTopLeftRadius = radius;
|
|
||||||
this->mCornerOverrides |= TopLeft;
|
|
||||||
emit this->topLeftRadiusChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
void PendingRegion::resetTopLeftRadius() {
|
|
||||||
this->mCornerOverrides &= ~TopLeft;
|
|
||||||
emit this->topLeftRadiusChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
qint32 PendingRegion::topRightRadius() const {
|
|
||||||
return (this->mCornerOverrides & TopRight) ? this->mTopRightRadius : this->mRadius;
|
|
||||||
}
|
|
||||||
|
|
||||||
void PendingRegion::setTopRightRadius(qint32 radius) {
|
|
||||||
this->mTopRightRadius = radius;
|
|
||||||
this->mCornerOverrides |= TopRight;
|
|
||||||
emit this->topRightRadiusChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
void PendingRegion::resetTopRightRadius() {
|
|
||||||
this->mCornerOverrides &= ~TopRight;
|
|
||||||
emit this->topRightRadiusChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
qint32 PendingRegion::bottomLeftRadius() const {
|
|
||||||
return (this->mCornerOverrides & BottomLeft) ? this->mBottomLeftRadius : this->mRadius;
|
|
||||||
}
|
|
||||||
|
|
||||||
void PendingRegion::setBottomLeftRadius(qint32 radius) {
|
|
||||||
this->mBottomLeftRadius = radius;
|
|
||||||
this->mCornerOverrides |= BottomLeft;
|
|
||||||
emit this->bottomLeftRadiusChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
void PendingRegion::resetBottomLeftRadius() {
|
|
||||||
this->mCornerOverrides &= ~BottomLeft;
|
|
||||||
emit this->bottomLeftRadiusChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
qint32 PendingRegion::bottomRightRadius() const {
|
|
||||||
return (this->mCornerOverrides & BottomRight) ? this->mBottomRightRadius : this->mRadius;
|
|
||||||
}
|
|
||||||
|
|
||||||
void PendingRegion::setBottomRightRadius(qint32 radius) {
|
|
||||||
this->mBottomRightRadius = radius;
|
|
||||||
this->mCornerOverrides |= BottomRight;
|
|
||||||
emit this->bottomRightRadiusChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
void PendingRegion::resetBottomRightRadius() {
|
|
||||||
this->mCornerOverrides &= ~BottomRight;
|
|
||||||
emit this->bottomRightRadiusChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
QQmlListProperty<PendingRegion> PendingRegion::regions() {
|
QQmlListProperty<PendingRegion> PendingRegion::regions() {
|
||||||
return QQmlListProperty<PendingRegion>(
|
return QQmlListProperty<PendingRegion>(
|
||||||
this,
|
this,
|
||||||
|
|
@ -169,60 +88,6 @@ QRegion PendingRegion::build() const {
|
||||||
region = QRegion(this->mX, this->mY, this->mWidth, this->mHeight, type);
|
region = QRegion(this->mX, this->mY, this->mWidth, this->mHeight, type);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this->mShape == RegionShape::Rect && !region.isEmpty()) {
|
|
||||||
auto tl = std::max(this->topLeftRadius(), 0);
|
|
||||||
auto tr = std::max(this->topRightRadius(), 0);
|
|
||||||
auto bl = std::max(this->bottomLeftRadius(), 0);
|
|
||||||
auto br = std::max(this->bottomRightRadius(), 0);
|
|
||||||
|
|
||||||
if (tl > 0 || tr > 0 || bl > 0 || br > 0) {
|
|
||||||
auto rect = region.boundingRect();
|
|
||||||
auto x = rect.x();
|
|
||||||
auto y = rect.y();
|
|
||||||
auto w = rect.width();
|
|
||||||
auto h = rect.height();
|
|
||||||
|
|
||||||
// Normalize so adjacent corners don't exceed their shared edge.
|
|
||||||
// Each corner is scaled by the tightest constraint of its two edges.
|
|
||||||
auto topScale = tl + tr > w ? static_cast<double>(w) / (tl + tr) : 1.0;
|
|
||||||
auto bottomScale = bl + br > w ? static_cast<double>(w) / (bl + br) : 1.0;
|
|
||||||
auto leftScale = tl + bl > h ? static_cast<double>(h) / (tl + bl) : 1.0;
|
|
||||||
auto rightScale = tr + br > h ? static_cast<double>(h) / (tr + br) : 1.0;
|
|
||||||
|
|
||||||
tl = static_cast<qint32>(tl * std::min(topScale, leftScale));
|
|
||||||
tr = static_cast<qint32>(tr * std::min(topScale, rightScale));
|
|
||||||
bl = static_cast<qint32>(bl * std::min(bottomScale, leftScale));
|
|
||||||
br = static_cast<qint32>(br * std::min(bottomScale, rightScale));
|
|
||||||
|
|
||||||
// Unlock each corner: subtract (cornerBox - quarterEllipse) from the
|
|
||||||
// full rect. Each corner only modifies pixels inside its own box,
|
|
||||||
// so no diagonal overlap is possible.
|
|
||||||
if (tl > 0) {
|
|
||||||
auto box = QRegion(x, y, tl, tl);
|
|
||||||
auto ellipse = QRegion(x, y, tl * 2, tl * 2, QRegion::Ellipse);
|
|
||||||
region -= box - (ellipse & box);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tr > 0) {
|
|
||||||
auto box = QRegion(x + w - tr, y, tr, tr);
|
|
||||||
auto ellipse = QRegion(x + w - tr * 2, y, tr * 2, tr * 2, QRegion::Ellipse);
|
|
||||||
region -= box - (ellipse & box);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (bl > 0) {
|
|
||||||
auto box = QRegion(x, y + h - bl, bl, bl);
|
|
||||||
auto ellipse = QRegion(x, y + h - bl * 2, bl * 2, bl * 2, QRegion::Ellipse);
|
|
||||||
region -= box - (ellipse & box);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (br > 0) {
|
|
||||||
auto box = QRegion(x + w - br, y + h - br, br, br);
|
|
||||||
auto ellipse = QRegion(x + w - br * 2, y + h - br * 2, br * 2, br * 2, QRegion::Ellipse);
|
|
||||||
region -= box - (ellipse & box);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const auto& childRegion: this->mRegions) {
|
for (const auto& childRegion: this->mRegions) {
|
||||||
region = childRegion->applyTo(region);
|
region = childRegion->applyTo(region);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -66,29 +66,6 @@ class PendingRegion: public QObject {
|
||||||
Q_PROPERTY(qint32 width MEMBER mWidth NOTIFY widthChanged);
|
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);
|
Q_PROPERTY(qint32 height MEMBER mHeight NOTIFY heightChanged);
|
||||||
// clang-format off
|
|
||||||
/// Corner radius for rounded rectangles. Only applies when @@shape is `Rect`. Defaults to 0.
|
|
||||||
///
|
|
||||||
/// Acts as the default for @@topLeftRadius, @@topRightRadius, @@bottomLeftRadius,
|
|
||||||
/// and @@bottomRightRadius.
|
|
||||||
Q_PROPERTY(qint32 radius READ radius WRITE setRadius NOTIFY radiusChanged);
|
|
||||||
/// Top-left corner radius. Only applies when @@shape is `Rect`.
|
|
||||||
///
|
|
||||||
/// Defaults to @@radius, and may be reset by assigning `undefined`.
|
|
||||||
Q_PROPERTY(qint32 topLeftRadius READ topLeftRadius WRITE setTopLeftRadius RESET resetTopLeftRadius NOTIFY topLeftRadiusChanged);
|
|
||||||
/// Top-right corner radius. Only applies when @@shape is `Rect`.
|
|
||||||
///
|
|
||||||
/// Defaults to @@radius, and may be reset by assigning `undefined`.
|
|
||||||
Q_PROPERTY(qint32 topRightRadius READ topRightRadius WRITE setTopRightRadius RESET resetTopRightRadius NOTIFY topRightRadiusChanged);
|
|
||||||
/// Bottom-left corner radius. Only applies when @@shape is `Rect`.
|
|
||||||
///
|
|
||||||
/// Defaults to @@radius, and may be reset by assigning `undefined`.
|
|
||||||
Q_PROPERTY(qint32 bottomLeftRadius READ bottomLeftRadius WRITE setBottomLeftRadius RESET resetBottomLeftRadius NOTIFY bottomLeftRadiusChanged);
|
|
||||||
/// Bottom-right corner radius. Only applies when @@shape is `Rect`.
|
|
||||||
///
|
|
||||||
/// Defaults to @@radius, and may be reset by assigning `undefined`.
|
|
||||||
Q_PROPERTY(qint32 bottomRightRadius READ bottomRightRadius WRITE setBottomRightRadius RESET resetBottomRightRadius NOTIFY bottomRightRadiusChanged);
|
|
||||||
// clang-format on
|
|
||||||
|
|
||||||
/// Regions to apply on top of this region.
|
/// Regions to apply on top of this region.
|
||||||
///
|
///
|
||||||
|
|
@ -114,25 +91,6 @@ public:
|
||||||
|
|
||||||
void setItem(QQuickItem* item);
|
void setItem(QQuickItem* item);
|
||||||
|
|
||||||
[[nodiscard]] qint32 radius() const;
|
|
||||||
void setRadius(qint32 radius);
|
|
||||||
|
|
||||||
[[nodiscard]] qint32 topLeftRadius() const;
|
|
||||||
void setTopLeftRadius(qint32 radius);
|
|
||||||
void resetTopLeftRadius();
|
|
||||||
|
|
||||||
[[nodiscard]] qint32 topRightRadius() const;
|
|
||||||
void setTopRightRadius(qint32 radius);
|
|
||||||
void resetTopRightRadius();
|
|
||||||
|
|
||||||
[[nodiscard]] qint32 bottomLeftRadius() const;
|
|
||||||
void setBottomLeftRadius(qint32 radius);
|
|
||||||
void resetBottomLeftRadius();
|
|
||||||
|
|
||||||
[[nodiscard]] qint32 bottomRightRadius() const;
|
|
||||||
void setBottomRightRadius(qint32 radius);
|
|
||||||
void resetBottomRightRadius();
|
|
||||||
|
|
||||||
QQmlListProperty<PendingRegion> regions();
|
QQmlListProperty<PendingRegion> regions();
|
||||||
|
|
||||||
[[nodiscard]] bool empty() const;
|
[[nodiscard]] bool empty() const;
|
||||||
|
|
@ -151,11 +109,6 @@ signals:
|
||||||
void yChanged();
|
void yChanged();
|
||||||
void widthChanged();
|
void widthChanged();
|
||||||
void heightChanged();
|
void heightChanged();
|
||||||
void radiusChanged();
|
|
||||||
void topLeftRadiusChanged();
|
|
||||||
void topRightRadiusChanged();
|
|
||||||
void bottomLeftRadiusChanged();
|
|
||||||
void bottomRightRadiusChanged();
|
|
||||||
void childrenChanged();
|
void childrenChanged();
|
||||||
|
|
||||||
/// Triggered when the region's geometry changes.
|
/// Triggered when the region's geometry changes.
|
||||||
|
|
@ -177,25 +130,12 @@ private:
|
||||||
static void
|
static void
|
||||||
regionsReplace(QQmlListProperty<PendingRegion>* prop, qsizetype i, PendingRegion* region);
|
regionsReplace(QQmlListProperty<PendingRegion>* prop, qsizetype i, PendingRegion* region);
|
||||||
|
|
||||||
enum CornerOverride : quint8 {
|
|
||||||
TopLeft = 0b1,
|
|
||||||
TopRight = 0b10,
|
|
||||||
BottomLeft = 0b100,
|
|
||||||
BottomRight = 0b1000,
|
|
||||||
};
|
|
||||||
|
|
||||||
QQuickItem* mItem = nullptr;
|
QQuickItem* mItem = nullptr;
|
||||||
|
|
||||||
qint32 mX = 0;
|
qint32 mX = 0;
|
||||||
qint32 mY = 0;
|
qint32 mY = 0;
|
||||||
qint32 mWidth = 0;
|
qint32 mWidth = 0;
|
||||||
qint32 mHeight = 0;
|
qint32 mHeight = 0;
|
||||||
qint32 mRadius = 0;
|
|
||||||
qint32 mTopLeftRadius = 0;
|
|
||||||
qint32 mTopRightRadius = 0;
|
|
||||||
qint32 mBottomLeftRadius = 0;
|
|
||||||
qint32 mBottomRightRadius = 0;
|
|
||||||
quint8 mCornerOverrides = 0;
|
|
||||||
|
|
||||||
QList<PendingRegion*> mRegions;
|
QList<PendingRegion*> mRegions;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -126,21 +126,12 @@ QObject* Reloadable::getChildByReloadId(QObject* parent, const QString& reloadId
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
void PostReloadHook::componentComplete() {
|
void PostReloadHook::postReloadTree(QObject* root) {
|
||||||
auto* engineGeneration = EngineGeneration::findObjectGeneration(this);
|
for (auto* child: root->children()) {
|
||||||
if (!engineGeneration || engineGeneration->reloadComplete) this->postReload();
|
PostReloadHook::postReloadTree(child);
|
||||||
else {
|
}
|
||||||
// disconnected by EngineGeneration::postReload
|
|
||||||
QObject::connect(
|
if (auto* self = dynamic_cast<PostReloadHook*>(root)) {
|
||||||
engineGeneration,
|
self->onPostReload();
|
||||||
&EngineGeneration::firePostReload,
|
|
||||||
this,
|
|
||||||
&PostReloadHook::postReload
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void PostReloadHook::postReload() {
|
|
||||||
this->isPostReload = true;
|
|
||||||
this->onPostReload();
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -57,7 +57,7 @@ public:
|
||||||
|
|
||||||
void reload(QObject* oldInstance = nullptr);
|
void reload(QObject* oldInstance = nullptr);
|
||||||
|
|
||||||
void classBegin() override {}
|
void classBegin() override {};
|
||||||
void componentComplete() override;
|
void componentComplete() override;
|
||||||
|
|
||||||
// Reload objects in the parent->child graph recursively.
|
// Reload objects in the parent->child graph recursively.
|
||||||
|
|
@ -119,23 +119,16 @@ private:
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Hook that runs after the old widget tree is dropped during a reload.
|
/// Hook that runs after the old widget tree is dropped during a reload.
|
||||||
class PostReloadHook
|
class PostReloadHook {
|
||||||
: public QObject
|
|
||||||
, public QQmlParserStatus {
|
|
||||||
Q_OBJECT;
|
|
||||||
QML_ANONYMOUS;
|
|
||||||
Q_INTERFACES(QQmlParserStatus);
|
|
||||||
|
|
||||||
public:
|
public:
|
||||||
PostReloadHook(QObject* parent = nullptr): QObject(parent) {}
|
PostReloadHook() = default;
|
||||||
void classBegin() override {}
|
virtual ~PostReloadHook() = default;
|
||||||
void componentComplete() override;
|
PostReloadHook(PostReloadHook&&) = default;
|
||||||
|
PostReloadHook(const PostReloadHook&) = default;
|
||||||
|
PostReloadHook& operator=(PostReloadHook&&) = default;
|
||||||
|
PostReloadHook& operator=(const PostReloadHook&) = default;
|
||||||
|
|
||||||
virtual void onPostReload() = 0;
|
virtual void onPostReload() = 0;
|
||||||
|
|
||||||
public slots:
|
static void postReloadTree(QObject* root);
|
||||||
void postReload();
|
|
||||||
|
|
||||||
protected:
|
|
||||||
bool isPostReload = false;
|
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@
|
||||||
|
|
||||||
#include <qdir.h>
|
#include <qdir.h>
|
||||||
#include <qfileinfo.h>
|
#include <qfileinfo.h>
|
||||||
#include <qfilesystemwatcher.h>
|
|
||||||
#include <qlogging.h>
|
#include <qlogging.h>
|
||||||
#include <qobject.h>
|
#include <qobject.h>
|
||||||
#include <qqmlcomponent.h>
|
#include <qqmlcomponent.h>
|
||||||
|
|
@ -19,26 +18,15 @@
|
||||||
#include "instanceinfo.hpp"
|
#include "instanceinfo.hpp"
|
||||||
#include "qmlglobal.hpp"
|
#include "qmlglobal.hpp"
|
||||||
#include "scan.hpp"
|
#include "scan.hpp"
|
||||||
#include "toolsupport.hpp"
|
|
||||||
|
|
||||||
RootWrapper::RootWrapper(QString rootPath, QString shellId)
|
RootWrapper::RootWrapper(QString rootPath, QString shellId)
|
||||||
: QObject(nullptr)
|
: QObject(nullptr)
|
||||||
, rootPath(std::move(rootPath))
|
, rootPath(std::move(rootPath))
|
||||||
, shellId(std::move(shellId))
|
, shellId(std::move(shellId))
|
||||||
, originalWorkingDirectory(QDir::current().absolutePath()) {
|
, originalWorkingDirectory(QDir::current().absolutePath()) {
|
||||||
QObject::connect(
|
// clang-format off
|
||||||
QuickshellSettings::instance(),
|
QObject::connect(QuickshellSettings::instance(), &QuickshellSettings::watchFilesChanged, this, &RootWrapper::onWatchFilesChanged);
|
||||||
&QuickshellSettings::watchFilesChanged,
|
// clang-format on
|
||||||
this,
|
|
||||||
&RootWrapper::onWatchFilesChanged
|
|
||||||
);
|
|
||||||
|
|
||||||
QObject::connect(
|
|
||||||
&this->configDirWatcher,
|
|
||||||
&QFileSystemWatcher::directoryChanged,
|
|
||||||
this,
|
|
||||||
&RootWrapper::updateTooling
|
|
||||||
);
|
|
||||||
|
|
||||||
this->reloadGraph(true);
|
this->reloadGraph(true);
|
||||||
|
|
||||||
|
|
@ -55,13 +43,12 @@ RootWrapper::~RootWrapper() {
|
||||||
}
|
}
|
||||||
|
|
||||||
void RootWrapper::reloadGraph(bool hard) {
|
void RootWrapper::reloadGraph(bool hard) {
|
||||||
auto rootFile = QFileInfo(this->rootPath);
|
auto rootPath = QFileInfo(this->rootPath).dir();
|
||||||
auto rootPath = rootFile.dir();
|
|
||||||
auto scanner = QmlScanner(rootPath);
|
auto scanner = QmlScanner(rootPath);
|
||||||
scanner.scanQmlRoot(this->rootPath);
|
scanner.scanQmlFile(this->rootPath);
|
||||||
|
|
||||||
qs::core::QmlToolingSupport::updateTooling(rootPath, scanner);
|
auto* generation = new EngineGeneration(rootPath, std::move(scanner));
|
||||||
this->configDirWatcher.addPath(rootPath.path());
|
generation->wrapper = this;
|
||||||
|
|
||||||
// todo: move into EngineGeneration
|
// todo: move into EngineGeneration
|
||||||
if (this->generation != nullptr) {
|
if (this->generation != nullptr) {
|
||||||
|
|
@ -71,36 +58,9 @@ void RootWrapper::reloadGraph(bool hard) {
|
||||||
|
|
||||||
QDir::setCurrent(this->originalWorkingDirectory);
|
QDir::setCurrent(this->originalWorkingDirectory);
|
||||||
|
|
||||||
if (!scanner.scanErrors.isEmpty()) {
|
auto url = QUrl::fromLocalFile(this->rootPath);
|
||||||
qCritical() << "Failed to load configuration";
|
// unless the original file comes from the qsintercept scheme
|
||||||
QString errorString = "Failed to load configuration";
|
url.setScheme("qsintercept");
|
||||||
for (auto& error: scanner.scanErrors) {
|
|
||||||
const auto& file = error.file;
|
|
||||||
QString rel;
|
|
||||||
if (file.startsWith(rootPath.path() % '/')) {
|
|
||||||
rel = '@' % file.sliced(rootPath.path().length() + 1);
|
|
||||||
} else {
|
|
||||||
rel = file;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto msg = " error in " % rel % '[' % QString::number(error.line) % ":0]: " % error.message;
|
|
||||||
errorString += '\n' % msg;
|
|
||||||
qCritical().noquote() << msg;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this->generation != nullptr && this->generation->qsgInstance != nullptr) {
|
|
||||||
emit this->generation->qsgInstance->reloadFailed(errorString);
|
|
||||||
}
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto* generation = new EngineGeneration(rootPath, std::move(scanner));
|
|
||||||
generation->wrapper = this;
|
|
||||||
|
|
||||||
QUrl url;
|
|
||||||
url.setScheme("qs");
|
|
||||||
url.setPath("@/qs/" % rootFile.fileName());
|
|
||||||
auto component = QQmlComponent(generation->engine, url);
|
auto component = QQmlComponent(generation->engine, url);
|
||||||
|
|
||||||
if (!component.isReady()) {
|
if (!component.isReady()) {
|
||||||
|
|
@ -109,9 +69,7 @@ void RootWrapper::reloadGraph(bool hard) {
|
||||||
|
|
||||||
auto errors = component.errors();
|
auto errors = component.errors();
|
||||||
for (auto& error: errors) {
|
for (auto& error: errors) {
|
||||||
const auto& url = error.url();
|
auto rel = "**/" % rootPath.relativeFilePath(error.url().path());
|
||||||
auto rel = url.scheme() == "qs" && url.path().startsWith("@/qs/") ? "@" % url.path().sliced(5)
|
|
||||||
: url.toString();
|
|
||||||
auto msg = " caused by " % rel % '[' % QString::number(error.line()) % ':'
|
auto msg = " caused by " % rel % '[' % QString::number(error.line()) % ':'
|
||||||
% QString::number(error.column()) % "]: " % error.description();
|
% QString::number(error.column()) % "]: " % error.description();
|
||||||
errorString += '\n' % msg;
|
errorString += '\n' % msg;
|
||||||
|
|
@ -207,9 +165,3 @@ void RootWrapper::onWatchFilesChanged() {
|
||||||
}
|
}
|
||||||
|
|
||||||
void RootWrapper::onWatchedFilesChanged() { this->reloadGraph(false); }
|
void RootWrapper::onWatchedFilesChanged() { this->reloadGraph(false); }
|
||||||
|
|
||||||
void RootWrapper::updateTooling() {
|
|
||||||
if (!this->generation) return;
|
|
||||||
auto configDir = QFileInfo(this->rootPath).dir();
|
|
||||||
qs::core::QmlToolingSupport::updateTooling(configDir, this->generation->scanner);
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <qfilesystemwatcher.h>
|
|
||||||
#include <qobject.h>
|
#include <qobject.h>
|
||||||
#include <qqmlengine.h>
|
#include <qqmlengine.h>
|
||||||
#include <qtclasshelpermacros.h>
|
#include <qtclasshelpermacros.h>
|
||||||
|
|
@ -23,12 +22,10 @@ private slots:
|
||||||
void generationDestroyed();
|
void generationDestroyed();
|
||||||
void onWatchFilesChanged();
|
void onWatchFilesChanged();
|
||||||
void onWatchedFilesChanged();
|
void onWatchedFilesChanged();
|
||||||
void updateTooling();
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
QString rootPath;
|
QString rootPath;
|
||||||
QString shellId;
|
QString shellId;
|
||||||
EngineGeneration* generation = nullptr;
|
EngineGeneration* generation = nullptr;
|
||||||
QString originalWorkingDirectory;
|
QString originalWorkingDirectory;
|
||||||
QFileSystemWatcher configDirWatcher;
|
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,9 @@
|
||||||
#include "scan.hpp"
|
#include "scan.hpp"
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <utility>
|
|
||||||
|
|
||||||
#include <qcontainerfwd.h>
|
#include <qcontainerfwd.h>
|
||||||
#include <qcryptographichash.h>
|
|
||||||
#include <qdir.h>
|
#include <qdir.h>
|
||||||
#include <qfileinfo.h>
|
#include <qfileinfo.h>
|
||||||
#include <qjsengine.h>
|
|
||||||
#include <qjsonarray.h>
|
#include <qjsonarray.h>
|
||||||
#include <qjsondocument.h>
|
#include <qjsondocument.h>
|
||||||
#include <qjsonobject.h>
|
#include <qjsonobject.h>
|
||||||
|
|
@ -15,107 +12,54 @@
|
||||||
#include <qloggingcategory.h>
|
#include <qloggingcategory.h>
|
||||||
#include <qpair.h>
|
#include <qpair.h>
|
||||||
#include <qstring.h>
|
#include <qstring.h>
|
||||||
|
#include <qstringliteral.h>
|
||||||
#include <qtextstream.h>
|
#include <qtextstream.h>
|
||||||
|
|
||||||
#include "logcat.hpp"
|
Q_LOGGING_CATEGORY(logQmlScanner, "quickshell.qmlscanner", QtWarningMsg);
|
||||||
#include "scanenv.hpp"
|
|
||||||
|
|
||||||
QS_LOGGING_CATEGORY(logQmlScanner, "quickshell.qmlscanner", QtWarningMsg);
|
void QmlScanner::scanDir(const QString& path) {
|
||||||
|
if (this->scannedDirs.contains(path)) return;
|
||||||
bool QmlScanner::readAndHashFile(const QString& path, QByteArray& data) {
|
this->scannedDirs.push_back(path);
|
||||||
auto file = QFile(path);
|
|
||||||
if (!file.open(QFile::ReadOnly)) return false;
|
|
||||||
data = file.readAll();
|
|
||||||
this->fileHashes.insert(path, QCryptographicHash::hash(data, QCryptographicHash::Md5));
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool QmlScanner::hasFileContentChanged(const QString& path) const {
|
|
||||||
auto it = this->fileHashes.constFind(path);
|
|
||||||
if (it == this->fileHashes.constEnd()) return true;
|
|
||||||
|
|
||||||
auto file = QFile(path);
|
|
||||||
if (!file.open(QFile::ReadOnly)) return true;
|
|
||||||
|
|
||||||
auto newHash = QCryptographicHash::hash(file.readAll(), QCryptographicHash::Md5);
|
|
||||||
return newHash != it.value();
|
|
||||||
}
|
|
||||||
|
|
||||||
void QmlScanner::scanDir(const QDir& dir) {
|
|
||||||
if (this->scannedDirs.contains(dir)) return;
|
|
||||||
this->scannedDirs.push_back(dir);
|
|
||||||
|
|
||||||
const auto& path = dir.path();
|
|
||||||
|
|
||||||
qCDebug(logQmlScanner) << "Scanning directory" << path;
|
qCDebug(logQmlScanner) << "Scanning directory" << path;
|
||||||
|
auto dir = QDir(path);
|
||||||
struct Entry {
|
|
||||||
QString name;
|
|
||||||
bool singleton = false;
|
|
||||||
bool internal = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
bool seenQmldir = false;
|
bool seenQmldir = false;
|
||||||
auto entries = QVector<Entry>();
|
auto singletons = QVector<QString>();
|
||||||
|
auto entries = QVector<QString>();
|
||||||
for (auto& name: dir.entryList(QDir::Files | QDir::NoDotAndDotDot)) {
|
for (auto& entry: dir.entryList(QDir::Files | QDir::NoDotAndDotDot)) {
|
||||||
if (name == "qmldir") {
|
if (entry == "qmldir") {
|
||||||
qCDebug(
|
qCDebug(logQmlScanner
|
||||||
logQmlScanner
|
|
||||||
) << "Found qmldir file, qmldir synthesization will be disabled for directory"
|
) << "Found qmldir file, qmldir synthesization will be disabled for directory"
|
||||||
<< path;
|
<< path;
|
||||||
seenQmldir = true;
|
seenQmldir = true;
|
||||||
} else if (name.at(0).isUpper() && name.endsWith(".qml")) {
|
} else if (entry.at(0).isUpper() && entry.endsWith(".qml")) {
|
||||||
auto& entry = entries.emplaceBack();
|
if (this->scanQmlFile(dir.filePath(entry))) {
|
||||||
|
singletons.push_back(entry);
|
||||||
if (this->scanQmlFile(dir.filePath(name), entry.singleton, entry.internal)) {
|
|
||||||
entry.name = name;
|
|
||||||
} else {
|
} else {
|
||||||
entries.pop_back();
|
entries.push_back(entry);
|
||||||
}
|
|
||||||
} else if (name.at(0).isUpper() && name.endsWith(".qml.json")) {
|
|
||||||
if (this->scanQmlJson(dir.filePath(name))) {
|
|
||||||
entries.push_back({
|
|
||||||
.name = name.first(name.length() - 5),
|
|
||||||
.singleton = true,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
} else if (entry.at(0).isUpper() && entry.endsWith(".qml.json")) {
|
||||||
|
this->scanQmlJson(dir.filePath(entry));
|
||||||
|
singletons.push_back(entry.first(entry.length() - 5));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Due to the qsintercept:// protocol a qmldir is always required, even without singletons.
|
||||||
if (!seenQmldir) {
|
if (!seenQmldir) {
|
||||||
qCDebug(logQmlScanner) << "Synthesizing qmldir for directory" << path;
|
qCDebug(logQmlScanner) << "Synthesizing qmldir for directory" << path << "singletons"
|
||||||
|
<< singletons;
|
||||||
|
|
||||||
QString qmldir;
|
QString qmldir;
|
||||||
auto stream = QTextStream(&qmldir);
|
auto stream = QTextStream(&qmldir);
|
||||||
|
|
||||||
// cant derive a module name if not in shell path
|
for (auto& singleton: singletons) {
|
||||||
if (path.startsWith(this->rootPath.path())) {
|
stream << "singleton " << singleton.sliced(0, singleton.length() - 4) << " 1.0 " << singleton
|
||||||
auto end = path.sliced(this->rootPath.path().length());
|
<< "\n";
|
||||||
|
|
||||||
// verify we have a valid module name.
|
|
||||||
for (auto& c: end) {
|
|
||||||
if (c == '/') c = '.';
|
|
||||||
else if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9')
|
|
||||||
|| c == '_')
|
|
||||||
{
|
|
||||||
} else {
|
|
||||||
qCWarning(logQmlScanner) << "Module path contains invalid characters for a module name: "
|
|
||||||
<< path.sliced(this->rootPath.path().length());
|
|
||||||
goto skipadd;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
stream << "module qs" << end << '\n';
|
|
||||||
skipadd:;
|
|
||||||
} else {
|
|
||||||
qCWarning(logQmlScanner) << "Module path" << path << "is outside of the config folder.";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const auto& entry: entries) {
|
for (auto& entry: entries) {
|
||||||
if (entry.internal) stream << "internal ";
|
stream << entry.sliced(0, entry.length() - 4) << " 1.0 " << entry << "\n";
|
||||||
if (entry.singleton) stream << "singleton ";
|
|
||||||
stream << entry.name.sliced(0, entry.name.length() - 4) << " 1.0 " << entry.name << '\n';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
qCDebug(logQmlScanner) << "Synthesized qmldir for" << path << qPrintable("\n" + qmldir);
|
qCDebug(logQmlScanner) << "Synthesized qmldir for" << path << qPrintable("\n" + qmldir);
|
||||||
|
|
@ -123,131 +67,50 @@ void QmlScanner::scanDir(const QDir& dir) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool QmlScanner::scanQmlFile(const QString& path, bool& singleton, bool& internal) {
|
bool QmlScanner::scanQmlFile(const QString& path) {
|
||||||
if (this->scannedFiles.contains(path)) return false;
|
if (this->scannedFiles.contains(path)) return false;
|
||||||
this->scannedFiles.push_back(path);
|
this->scannedFiles.push_back(path);
|
||||||
|
|
||||||
qCDebug(logQmlScanner) << "Scanning qml file" << path;
|
qCDebug(logQmlScanner) << "Scanning qml file" << path;
|
||||||
|
|
||||||
QByteArray fileData;
|
auto file = QFile(path);
|
||||||
if (!this->readAndHashFile(path, fileData)) {
|
if (!file.open(QFile::ReadOnly | QFile::Text)) {
|
||||||
qCWarning(logQmlScanner) << "Failed to open file" << path;
|
qCWarning(logQmlScanner) << "Failed to open file" << path;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto stream = QTextStream(&fileData);
|
auto stream = QTextStream(&file);
|
||||||
auto imports = QVector<QString>();
|
auto imports = QVector<QString>();
|
||||||
|
|
||||||
bool inHeader = true;
|
bool singleton = false;
|
||||||
auto ifScopes = QVector<bool>();
|
|
||||||
bool sourceMasked = false;
|
|
||||||
int lineNum = 0;
|
|
||||||
QString overrideText;
|
|
||||||
bool isOverridden = false;
|
|
||||||
|
|
||||||
auto& pragmaEngine = *QmlScanner::preprocEngine();
|
|
||||||
|
|
||||||
auto postError = [&, this](QString error) {
|
|
||||||
this->scanErrors.append({.file = path, .message = std::move(error), .line = lineNum});
|
|
||||||
};
|
|
||||||
|
|
||||||
while (!stream.atEnd()) {
|
while (!stream.atEnd()) {
|
||||||
++lineNum;
|
auto line = stream.readLine().trimmed();
|
||||||
bool hideMask = false;
|
if (!singleton && line == "pragma Singleton") {
|
||||||
auto rawLine = stream.readLine();
|
qCDebug(logQmlScanner) << "Discovered singleton" << path;
|
||||||
auto line = rawLine.trimmed();
|
singleton = true;
|
||||||
if (!sourceMasked && inHeader) {
|
} else if (line.startsWith("import")) {
|
||||||
if (!singleton && line == "pragma Singleton") {
|
|
||||||
singleton = true;
|
|
||||||
} else if (line.startsWith("import")) {
|
|
||||||
// we dont care about "import qs" as we always load the root folder
|
|
||||||
if (auto importCursor = line.indexOf(" qs."); importCursor != -1) {
|
|
||||||
importCursor += 4;
|
|
||||||
QString path;
|
|
||||||
|
|
||||||
while (importCursor != line.length()) {
|
auto startQuot = line.indexOf('"');
|
||||||
auto c = line.at(importCursor);
|
if (startQuot == -1 || line.length() < startQuot + 3) continue;
|
||||||
if (c == '.') c = '/';
|
auto endQuot = line.indexOf('"', startQuot + 1);
|
||||||
else if (c == ' ') break;
|
if (endQuot == -1) continue;
|
||||||
else if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9')
|
|
||||||
|| c == '_')
|
|
||||||
{
|
|
||||||
} else {
|
|
||||||
qCWarning(logQmlScanner) << "Import line contains invalid characters: " << line;
|
|
||||||
goto next;
|
|
||||||
}
|
|
||||||
|
|
||||||
path.append(c);
|
auto name = line.sliced(startQuot + 1, endQuot - startQuot - 1);
|
||||||
importCursor += 1;
|
imports.push_back(name);
|
||||||
}
|
} else if (line.contains('{')) break;
|
||||||
|
|
||||||
imports.append(this->rootPath.filePath(path));
|
|
||||||
} else if (auto startQuot = line.indexOf('"');
|
|
||||||
startQuot != -1 && line.length() >= startQuot + 3)
|
|
||||||
{
|
|
||||||
auto endQuot = line.indexOf('"', startQuot + 1);
|
|
||||||
if (endQuot == -1) continue;
|
|
||||||
|
|
||||||
auto name = line.sliced(startQuot + 1, endQuot - startQuot - 1);
|
|
||||||
imports.push_back(name);
|
|
||||||
}
|
|
||||||
} else if (!internal && line == "//@ pragma Internal") {
|
|
||||||
internal = true;
|
|
||||||
} else if (line.contains('{')) {
|
|
||||||
inHeader = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (line.startsWith("//@ if ")) {
|
|
||||||
auto code = line.sliced(7);
|
|
||||||
auto value = pragmaEngine.evaluate(code, path, 1234);
|
|
||||||
bool mask = true;
|
|
||||||
|
|
||||||
if (value.isError()) {
|
|
||||||
postError(QString("Evaluating if: %0").arg(value.toString()));
|
|
||||||
} else if (!value.isBool()) {
|
|
||||||
postError(QString("If expression \"%0\" is not a boolean").arg(value.toString()));
|
|
||||||
} else if (value.toBool()) {
|
|
||||||
mask = false;
|
|
||||||
}
|
|
||||||
if (!sourceMasked && mask) hideMask = true;
|
|
||||||
mask = sourceMasked || mask; // cant unmask if a nested if passes
|
|
||||||
ifScopes.append(mask);
|
|
||||||
if (mask) isOverridden = true;
|
|
||||||
sourceMasked = mask;
|
|
||||||
} else if (line.startsWith("//@ endif")) {
|
|
||||||
if (ifScopes.isEmpty()) {
|
|
||||||
postError("endif without matching if");
|
|
||||||
} else {
|
|
||||||
ifScopes.pop_back();
|
|
||||||
|
|
||||||
if (ifScopes.isEmpty()) sourceMasked = false;
|
|
||||||
else sourceMasked = ifScopes.last();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!hideMask && sourceMasked) overrideText.append("// MASKED: " % rawLine % '\n');
|
|
||||||
else overrideText.append(rawLine % '\n');
|
|
||||||
|
|
||||||
next:;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ifScopes.isEmpty()) {
|
file.close();
|
||||||
postError("unclosed preprocessor if block");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isOverridden) {
|
|
||||||
this->fileIntercepts.insert(path, overrideText);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (logQmlScanner().isDebugEnabled() && !imports.isEmpty()) {
|
if (logQmlScanner().isDebugEnabled() && !imports.isEmpty()) {
|
||||||
qCDebug(logQmlScanner) << "Found imports" << imports;
|
qCDebug(logQmlScanner) << "Found imports" << imports;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto currentdir = QDir(QFileInfo(path).absolutePath());
|
auto currentdir = QDir(QFileInfo(path).canonicalPath());
|
||||||
|
|
||||||
// the root can never be a singleton so it dosent matter if we skip it
|
// the root can never be a singleton so it dosent matter if we skip it
|
||||||
this->scanDir(currentdir);
|
this->scanDir(currentdir.path());
|
||||||
|
|
||||||
for (auto& import: imports) {
|
for (auto& import: imports) {
|
||||||
QString ipath;
|
QString ipath;
|
||||||
|
|
@ -259,44 +122,31 @@ bool QmlScanner::scanQmlFile(const QString& path, bool& singleton, bool& interna
|
||||||
ipath = currentdir.filePath(import);
|
ipath = currentdir.filePath(import);
|
||||||
}
|
}
|
||||||
|
|
||||||
auto pathInfo = QFileInfo(ipath);
|
auto cpath = QFileInfo(ipath).canonicalFilePath();
|
||||||
auto cpath = pathInfo.absoluteFilePath();
|
|
||||||
|
|
||||||
if (!pathInfo.exists()) {
|
if (cpath.isEmpty()) {
|
||||||
qCWarning(logQmlScanner) << "Ignoring unresolvable import" << ipath << "from" << path;
|
qCWarning(logQmlScanner) << "Ignoring unresolvable import" << ipath << "from" << path;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!pathInfo.isDir()) {
|
if (import.endsWith(".js")) this->scannedFiles.push_back(cpath);
|
||||||
qCDebug(logQmlScanner) << "Ignoring non-directory import" << ipath << "from" << path;
|
else this->scanDir(cpath);
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (import.endsWith(".js")) {
|
|
||||||
this->scannedFiles.push_back(cpath);
|
|
||||||
QByteArray jsData;
|
|
||||||
this->readAndHashFile(cpath, jsData);
|
|
||||||
} else this->scanDir(cpath);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return singleton;
|
||||||
}
|
}
|
||||||
|
|
||||||
void QmlScanner::scanQmlRoot(const QString& path) {
|
void QmlScanner::scanQmlJson(const QString& path) {
|
||||||
bool singleton = false;
|
|
||||||
bool internal = false;
|
|
||||||
this->scanQmlFile(path, singleton, internal);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool QmlScanner::scanQmlJson(const QString& path) {
|
|
||||||
qCDebug(logQmlScanner) << "Scanning qml.json file" << path;
|
qCDebug(logQmlScanner) << "Scanning qml.json file" << path;
|
||||||
|
|
||||||
QByteArray data;
|
auto file = QFile(path);
|
||||||
if (!this->readAndHashFile(path, data)) {
|
if (!file.open(QFile::ReadOnly | QFile::Text)) {
|
||||||
qCWarning(logQmlScanner) << "Failed to open file" << path;
|
qCWarning(logQmlScanner) << "Failed to open file" << path;
|
||||||
return false;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
auto data = file.readAll();
|
||||||
|
|
||||||
// Importing this makes CI builds fail for some reason.
|
// Importing this makes CI builds fail for some reason.
|
||||||
QJsonParseError error; // NOLINT (misc-include-cleaner)
|
QJsonParseError error; // NOLINT (misc-include-cleaner)
|
||||||
auto json = QJsonDocument::fromJson(data, &error);
|
auto json = QJsonDocument::fromJson(data, &error);
|
||||||
|
|
@ -304,7 +154,7 @@ bool QmlScanner::scanQmlJson(const QString& path) {
|
||||||
if (error.error != QJsonParseError::NoError) {
|
if (error.error != QJsonParseError::NoError) {
|
||||||
qCCritical(logQmlScanner).nospace()
|
qCCritical(logQmlScanner).nospace()
|
||||||
<< "Failed to parse qml.json file at " << path << ": " << error.errorString();
|
<< "Failed to parse qml.json file at " << path << ": " << error.errorString();
|
||||||
return false;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const QString body =
|
const QString body =
|
||||||
|
|
@ -314,7 +164,6 @@ bool QmlScanner::scanQmlJson(const QString& path) {
|
||||||
|
|
||||||
this->fileIntercepts.insert(path.first(path.length() - 5), body);
|
this->fileIntercepts.insert(path.first(path.length() - 5), body);
|
||||||
this->scannedFiles.push_back(path);
|
this->scannedFiles.push_back(path);
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
QPair<QString, QString> QmlScanner::jsonToQml(const QJsonValue& value, int indent) {
|
QPair<QString, QString> QmlScanner::jsonToQml(const QJsonValue& value, int indent) {
|
||||||
|
|
@ -367,13 +216,3 @@ QPair<QString, QString> QmlScanner::jsonToQml(const QJsonValue& value, int inden
|
||||||
return qMakePair(QStringLiteral("var"), "null");
|
return qMakePair(QStringLiteral("var"), "null");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
QJSEngine* QmlScanner::preprocEngine() {
|
|
||||||
static auto* engine = [] {
|
|
||||||
auto* engine = new QJSEngine();
|
|
||||||
engine->globalObject().setPrototype(engine->newQObject(new qs::scan::env::PreprocEnv()));
|
|
||||||
return engine;
|
|
||||||
}();
|
|
||||||
|
|
||||||
return engine;
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,12 @@
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <qbytearray.h>
|
|
||||||
#include <qcontainerfwd.h>
|
#include <qcontainerfwd.h>
|
||||||
#include <qdir.h>
|
#include <qdir.h>
|
||||||
#include <qhash.h>
|
#include <qhash.h>
|
||||||
#include <qjsengine.h>
|
|
||||||
#include <qloggingcategory.h>
|
#include <qloggingcategory.h>
|
||||||
#include <qvector.h>
|
#include <qvector.h>
|
||||||
|
|
||||||
#include "logcat.hpp"
|
Q_DECLARE_LOGGING_CATEGORY(logQmlScanner);
|
||||||
|
|
||||||
QS_DECLARE_LOGGING_CATEGORY(logQmlScanner);
|
|
||||||
|
|
||||||
// expects canonical paths
|
// expects canonical paths
|
||||||
class QmlScanner {
|
class QmlScanner {
|
||||||
|
|
@ -18,31 +14,17 @@ public:
|
||||||
QmlScanner() = default;
|
QmlScanner() = default;
|
||||||
QmlScanner(const QDir& rootPath): rootPath(rootPath) {}
|
QmlScanner(const QDir& rootPath): rootPath(rootPath) {}
|
||||||
|
|
||||||
void scanDir(const QDir& dir);
|
void scanDir(const QString& path);
|
||||||
void scanQmlRoot(const QString& path);
|
// returns if the file has a singleton
|
||||||
|
bool scanQmlFile(const QString& path);
|
||||||
|
|
||||||
QVector<QDir> scannedDirs;
|
QVector<QString> scannedDirs;
|
||||||
QVector<QString> scannedFiles;
|
QVector<QString> scannedFiles;
|
||||||
QHash<QString, QByteArray> fileHashes;
|
|
||||||
QHash<QString, QString> fileIntercepts;
|
QHash<QString, QString> fileIntercepts;
|
||||||
|
|
||||||
struct ScanError {
|
|
||||||
QString file;
|
|
||||||
QString message;
|
|
||||||
int line;
|
|
||||||
};
|
|
||||||
|
|
||||||
QVector<ScanError> scanErrors;
|
|
||||||
|
|
||||||
bool readAndHashFile(const QString& path, QByteArray& data);
|
|
||||||
[[nodiscard]] bool hasFileContentChanged(const QString& path) const;
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
QDir rootPath;
|
QDir rootPath;
|
||||||
|
|
||||||
bool scanQmlFile(const QString& path, bool& singleton, bool& internal);
|
void scanQmlJson(const QString& path);
|
||||||
bool scanQmlJson(const QString& path);
|
|
||||||
[[nodiscard]] static QPair<QString, QString> jsonToQml(const QJsonValue& value, int indent = 0);
|
[[nodiscard]] static QPair<QString, QString> jsonToQml(const QJsonValue& value, int indent = 0);
|
||||||
|
|
||||||
static QJSEngine* preprocEngine();
|
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,31 +0,0 @@
|
||||||
#include "scanenv.hpp"
|
|
||||||
|
|
||||||
#include <qcontainerfwd.h>
|
|
||||||
#include <qtenvironmentvariables.h>
|
|
||||||
|
|
||||||
#include "build.hpp"
|
|
||||||
|
|
||||||
namespace qs::scan::env {
|
|
||||||
|
|
||||||
bool PreprocEnv::hasVersion(int major, int minor, const QStringList& features) {
|
|
||||||
if (QS_VERSION_MAJOR > major) return true;
|
|
||||||
if (QS_VERSION_MAJOR == major && QS_VERSION_MINOR > minor) return true;
|
|
||||||
|
|
||||||
auto availFeatures = QString(QS_UNRELEASED_FEATURES).split(';');
|
|
||||||
|
|
||||||
for (const auto& feature: features) {
|
|
||||||
if (!availFeatures.contains(feature)) return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return QS_VERSION_MAJOR == major && QS_VERSION_MINOR == minor;
|
|
||||||
}
|
|
||||||
|
|
||||||
QString PreprocEnv::env(const QString& variable) {
|
|
||||||
return qEnvironmentVariable(variable.toStdString().c_str());
|
|
||||||
}
|
|
||||||
|
|
||||||
bool PreprocEnv::isEnvSet(const QString& variable) {
|
|
||||||
return qEnvironmentVariableIsSet(variable.toStdString().c_str());
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace qs::scan::env
|
|
||||||
|
|
@ -1,20 +0,0 @@
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include <qcontainerfwd.h>
|
|
||||||
#include <qobject.h>
|
|
||||||
#include <qtmetamacros.h>
|
|
||||||
|
|
||||||
namespace qs::scan::env {
|
|
||||||
|
|
||||||
class PreprocEnv: public QObject {
|
|
||||||
Q_OBJECT;
|
|
||||||
|
|
||||||
public:
|
|
||||||
Q_INVOKABLE static bool
|
|
||||||
hasVersion(int major, int minor, const QStringList& features = QStringList());
|
|
||||||
|
|
||||||
Q_INVOKABLE static QString env(const QString& variable);
|
|
||||||
Q_INVOKABLE static bool isEnvSet(const QString& variable);
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace qs::scan::env
|
|
||||||
|
|
@ -19,7 +19,7 @@ void ScriptModel::updateValuesUnique(const QVariantList& newValues) {
|
||||||
auto newIter = newValues.begin();
|
auto newIter = newValues.begin();
|
||||||
|
|
||||||
// TODO: cache this
|
// TODO: cache this
|
||||||
auto getCmpKey = [this](const QVariant& v) {
|
auto getCmpKey = [&](const QVariant& v) {
|
||||||
if (v.canConvert<QVariantMap>()) {
|
if (v.canConvert<QVariantMap>()) {
|
||||||
auto vMap = v.value<QVariantMap>();
|
auto vMap = v.value<QVariantMap>();
|
||||||
if (vMap.contains(this->cmpKey)) {
|
if (vMap.contains(this->cmpKey)) {
|
||||||
|
|
@ -30,7 +30,7 @@ void ScriptModel::updateValuesUnique(const QVariantList& newValues) {
|
||||||
return v;
|
return v;
|
||||||
};
|
};
|
||||||
|
|
||||||
auto variantCmp = [&, this](const QVariant& a, const QVariant& b) {
|
auto variantCmp = [&](const QVariant& a, const QVariant& b) {
|
||||||
if (!this->cmpKey.isEmpty()) return getCmpKey(a) == getCmpKey(b);
|
if (!this->cmpKey.isEmpty()) return getCmpKey(a) == getCmpKey(b);
|
||||||
else return a == b;
|
else return a == b;
|
||||||
};
|
};
|
||||||
|
|
@ -72,8 +72,8 @@ void ScriptModel::updateValuesUnique(const QVariantList& newValues) {
|
||||||
do {
|
do {
|
||||||
++iter;
|
++iter;
|
||||||
} while (iter != this->mValues.end()
|
} while (iter != this->mValues.end()
|
||||||
&& std::find_if(newIter, newValues.end(), eqPredicate(*iter))
|
&& std::find_if(newIter, newValues.end(), eqPredicate(*iter)) == newValues.end()
|
||||||
== newValues.end());
|
);
|
||||||
|
|
||||||
auto index = static_cast<qint32>(std::distance(this->mValues.begin(), iter));
|
auto index = static_cast<qint32>(std::distance(this->mValues.begin(), iter));
|
||||||
auto startIndex = static_cast<qint32>(std::distance(this->mValues.begin(), startIter));
|
auto startIndex = static_cast<qint32>(std::distance(this->mValues.begin(), startIter));
|
||||||
|
|
|
||||||
|
|
@ -51,3 +51,9 @@ void SingletonRegistry::onReload(SingletonRegistry* old) {
|
||||||
singleton->reload(old == nullptr ? nullptr : old->registry.value(url));
|
singleton->reload(old == nullptr ? nullptr : old->registry.value(url));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void SingletonRegistry::onPostReload() {
|
||||||
|
for (auto* singleton: this->registry.values()) {
|
||||||
|
PostReloadHook::postReloadTree(singleton);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ public:
|
||||||
|
|
||||||
void registerSingleton(const QUrl& url, Singleton* singleton);
|
void registerSingleton(const QUrl& url, Singleton* singleton);
|
||||||
void onReload(SingletonRegistry* old);
|
void onReload(SingletonRegistry* old);
|
||||||
|
void onPostReload();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
QHash<QUrl, Singleton*> registry;
|
QHash<QUrl, Singleton*> registry;
|
||||||
|
|
|
||||||
|
|
@ -1,98 +0,0 @@
|
||||||
#include "streamreader.hpp"
|
|
||||||
#include <cstring>
|
|
||||||
|
|
||||||
#include <qbytearray.h>
|
|
||||||
#include <qiodevice.h>
|
|
||||||
#include <qtypes.h>
|
|
||||||
|
|
||||||
void StreamReader::setDevice(QIODevice* device) {
|
|
||||||
this->reset();
|
|
||||||
this->device = device;
|
|
||||||
}
|
|
||||||
|
|
||||||
void StreamReader::startTransaction() {
|
|
||||||
this->cursor = 0;
|
|
||||||
this->failed = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool StreamReader::fill() {
|
|
||||||
auto available = this->device->bytesAvailable();
|
|
||||||
if (available <= 0) return false;
|
|
||||||
auto oldSize = this->buffer.size();
|
|
||||||
this->buffer.resize(oldSize + available);
|
|
||||||
auto bytesRead = this->device->read(this->buffer.data() + oldSize, available); // NOLINT
|
|
||||||
|
|
||||||
if (bytesRead <= 0) {
|
|
||||||
this->buffer.resize(oldSize);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
this->buffer.resize(oldSize + bytesRead);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
QByteArray StreamReader::readBytes(qsizetype count) {
|
|
||||||
if (this->failed) return {};
|
|
||||||
|
|
||||||
auto needed = this->cursor + count;
|
|
||||||
|
|
||||||
while (this->buffer.size() < needed) {
|
|
||||||
if (!this->fill()) {
|
|
||||||
this->failed = true;
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
auto result = this->buffer.mid(this->cursor, count);
|
|
||||||
this->cursor += count;
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
QByteArray StreamReader::readUntil(char terminator) {
|
|
||||||
if (this->failed) return {};
|
|
||||||
|
|
||||||
auto searchFrom = this->cursor;
|
|
||||||
auto idx = this->buffer.indexOf(terminator, searchFrom);
|
|
||||||
|
|
||||||
while (idx == -1) {
|
|
||||||
searchFrom = this->buffer.size();
|
|
||||||
if (!this->fill()) {
|
|
||||||
this->failed = true;
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
idx = this->buffer.indexOf(terminator, searchFrom);
|
|
||||||
}
|
|
||||||
|
|
||||||
auto length = idx - this->cursor + 1;
|
|
||||||
auto result = this->buffer.mid(this->cursor, length);
|
|
||||||
this->cursor += length;
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
void StreamReader::readInto(char* ptr, qsizetype count) {
|
|
||||||
auto data = this->readBytes(count);
|
|
||||||
if (!data.isEmpty()) memcpy(ptr, data.data(), count);
|
|
||||||
}
|
|
||||||
|
|
||||||
qint32 StreamReader::readI32() {
|
|
||||||
qint32 value = 0;
|
|
||||||
this->readInto(reinterpret_cast<char*>(&value), sizeof(qint32));
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool StreamReader::commitTransaction() {
|
|
||||||
if (this->failed) {
|
|
||||||
this->cursor = 0;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
this->buffer.remove(0, this->cursor);
|
|
||||||
this->cursor = 0;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void StreamReader::reset() {
|
|
||||||
this->buffer.clear();
|
|
||||||
this->cursor = 0;
|
|
||||||
}
|
|
||||||
|
|
@ -1,26 +0,0 @@
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include <qbytearray.h>
|
|
||||||
#include <qiodevice.h>
|
|
||||||
#include <qtypes.h>
|
|
||||||
|
|
||||||
class StreamReader {
|
|
||||||
public:
|
|
||||||
void setDevice(QIODevice* device);
|
|
||||||
|
|
||||||
void startTransaction();
|
|
||||||
QByteArray readBytes(qsizetype count);
|
|
||||||
QByteArray readUntil(char terminator);
|
|
||||||
void readInto(char* ptr, qsizetype count);
|
|
||||||
qint32 readI32();
|
|
||||||
bool commitTransaction();
|
|
||||||
void reset();
|
|
||||||
|
|
||||||
private:
|
|
||||||
bool fill();
|
|
||||||
|
|
||||||
QIODevice* device = nullptr;
|
|
||||||
QByteArray buffer;
|
|
||||||
qsizetype cursor = 0;
|
|
||||||
bool failed = false;
|
|
||||||
};
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
function (qs_test name)
|
function (qs_test name)
|
||||||
add_executable(${name} ${ARGN})
|
add_executable(${name} ${ARGN})
|
||||||
target_link_libraries(${name} PRIVATE Qt::Quick Qt::Test quickshell-core quickshell-window quickshell-ui quickshell-io)
|
target_link_libraries(${name} PRIVATE Qt::Quick Qt::Test quickshell-core quickshell-window quickshell-ui)
|
||||||
add_test(NAME ${name} WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}" COMMAND $<TARGET_FILE:${name}>)
|
add_test(NAME ${name} WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}" COMMAND $<TARGET_FILE:${name}>)
|
||||||
endfunction()
|
endfunction()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -115,7 +115,7 @@ void TestScriptModel::unique_data() {
|
||||||
void TestScriptModel::unique() {
|
void TestScriptModel::unique() {
|
||||||
QFETCH(const QString, oldstr);
|
QFETCH(const QString, oldstr);
|
||||||
QFETCH(const QString, newstr);
|
QFETCH(const QString, newstr);
|
||||||
QFETCH(const OpList, operations);
|
QFETCH(OpList, operations);
|
||||||
|
|
||||||
auto strToVariantList = [](const QString& str) -> QVariantList {
|
auto strToVariantList = [](const QString& str) -> QVariantList {
|
||||||
QVariantList list;
|
QVariantList list;
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue