Compare commits

...

23 commits

Author SHA1 Message Date
outfoxxed
a1a150fab0
version: bump to 0.2.1 2025-10-11 17:14:14 -07:00
Gregor Kleen
40bc09b800
service/greetd: always send responses 2025-10-11 17:06:06 -07:00
bbedward
a94418f45d
core/desktopentry: don't match keys with wrong modifier or country 2025-10-11 17:06:06 -07:00
bbedward
8d2a2d3dd2
core/desktopentry: mask entries with priority less than hidden entry 2025-10-11 17:06:06 -07:00
outfoxxed
e10747addd
all: fix gcc warnings and lints 2025-10-11 17:06:06 -07:00
outfoxxed
b254f6dabc
nix: remove qtwayland dependency when qt >= 6.10
QtWaylandClient was moved into QtBase in 6.10. The QtWayland packages
is now only the wayland server code which Quickshell does not need.
2025-10-11 17:06:06 -07:00
outfoxxed
7fc6635ca8
wayland/lock: support Qt 6.10 2025-10-11 17:06:06 -07:00
outfoxxed
522d126d1b
services/pipewire: consider device volume step when sending updates
Previously a hardcoded 0.0001 offset was used to determine if a volume
change was significant enough to send to a device, however some
devices have a much more granular step size, which caused future
volume updates to be blocked.

This change replaces the hardcoded offset with the volumeStep device
route property which should be large enough for the device to work with.

Fixes #279
2025-10-11 17:06:06 -07:00
outfoxxed
f5ca8453c0
docs: start tracking qs-next changelog 2025-10-11 17:06:04 -07:00
outfoxxed
e81e6da08f
ci: fix magic-nix-cache write permissions 2025-10-11 17:05:30 -07:00
outfoxxed
f73729754d
build: explicitly depend on private qt modules
In Qt 6.10, private Qt modules must be depended on explicitly.
2025-10-11 17:05:30 -07:00
outfoxxed
344ca340ba
all: fix lints 2025-10-11 17:05:26 -07:00
outfoxxed
6cc1b6f36a
nix: update flake + tidyfox 2025-10-11 17:04:23 -07:00
outfoxxed
354afeaa35
ci: add detsys nix cache 2025-10-11 17:04:23 -07:00
outfoxxed
62df8d917f
ci: use unwrapped package for dependencies derivation
Since adding the wrapper, CI built qs as it was a dependency of the
wrapper instead of dependencies of qs itself.
2025-10-11 17:04:23 -07:00
outfoxxed
2f0f2d1201
ci: add qt 6.9.2 and 6.9.1 checkouts 2025-10-11 17:04:23 -07:00
outfoxxed
4b825e7051
nix: add overlay 2025-10-11 17:04:23 -07:00
outfoxxed
d4b19e4a30
build: fix cross compilation 2025-10-11 17:04:23 -07:00
outfoxxed
b9cce25061
core: derive incubation controllers from tracked windows list
Replaces the attempts to track incubation controllers directly with a
list of all known windows, then pulls the first usable incubation
controller when an assignment is requested.

This should finally fix incubation controller related use after free crashes.
2025-10-11 17:04:23 -07:00
bbedward
996efc93b7
core/desktopentry: watch for changes and rescan entries 2025-10-11 17:04:22 -07:00
outfoxxed
2115f31416
ci: use latest wayland-protocol for all test cases
Fixes missing protocols on old nixpkgs versions
2025-10-11 17:04:22 -07:00
kossLAN
e4d33fa52f
hyprland/ipc: fix focusedWorkspaceChanged connection 2025-10-11 17:04:22 -07:00
Derock
83f5af522d
core/log: fix nullptr crash in ThreadLogging 2025-10-11 17:04:00 -07:00
54 changed files with 863 additions and 418 deletions

View file

@ -6,17 +6,23 @@ jobs:
name: Nix name: Nix
strategy: strategy:
matrix: matrix:
qtver: [qt6.9.0, qt6.8.3, qt6.8.2, qt6.8.1, qt6.8.0, qt6.7.3, qt6.7.2, qt6.7.1, qt6.7.0, qt6.6.3, qt6.6.2, qt6.6.1, qt6.6.0] qtver: [qt6.9.2, qt6.9.1, qt6.9.0, qt6.8.3, qt6.8.2, qt6.8.1, qt6.8.0, qt6.7.3, qt6.7.2, qt6.7.1, qt6.7.0, qt6.6.3, qt6.6.2, qt6.6.1, qt6.6.0]
compiler: [clang, gcc] 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 }}"; }).inputDerivation' run: nix-build --no-out-link --expr '((import ./ci/matrix.nix) { qtver = "${{ matrix.qtver }}"; compiler = "${{ matrix.compiler }}"; }).unwrapped.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 }}"; }'

View file

@ -5,11 +5,17 @@ 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

View file

@ -55,7 +55,7 @@ On some distros, private Qt headers are in separate packages which you may have
We currently require private headers for the following libraries: We currently require private headers for the following libraries:
- `qt6declarative` - `qt6declarative`
- `qt6wayland` - `qt6wayland` (for Qt versions prior to 6.10)
We recommend an implicit dependency on `qt6svg`. If it is not installed, svg images and 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.
@ -104,7 +104,7 @@ Currently supported Qt versions: `6.6`, `6.7`.
To disable: `-DWAYLAND=OFF` To disable: `-DWAYLAND=OFF`
Dependencies: Dependencies:
- `qt6wayland` - `qt6wayland` (for Qt versions prior to 6.10)
- `wayland` (libwayland-client) - `wayland` (libwayland-client)
- `wayland-scanner` (build time) - `wayland-scanner` (build time)
- `wayland-protocols` (static library) - `wayland-protocols` (static library)

View file

@ -1,5 +1,5 @@
cmake_minimum_required(VERSION 3.20) cmake_minimum_required(VERSION 3.20)
project(quickshell VERSION "0.2.0" LANGUAGES CXX C) project(quickshell VERSION "0.2.1" LANGUAGES CXX C)
set(QT_MIN_VERSION "6.6.0") set(QT_MIN_VERSION "6.6.0")
set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD 20)
@ -100,6 +100,7 @@ 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)
@ -115,6 +116,7 @@ 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) if (SERVICE_STATUS_NOTIFIER OR SERVICE_MPRIS OR SERVICE_UPOWER OR SERVICE_NOTIFICATIONS OR BLUETOOTH)
@ -127,6 +129,13 @@ 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)

View file

@ -12,6 +12,9 @@ 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 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" } }} \

17
changelog/v0.2.1.md Normal file
View file

@ -0,0 +1,17 @@
## New Features
- Changes to desktop entries are now tracked in real time.
## Other Changes
- Added support for Qt 6.10
## Bug Fixes
- Fixed volumes getting stuck on change for pipewire devices with few volume steps.
- Fixed a crash when running out of disk space to write log files.
- Fixed a rare crash when disconnecting a monitor.
- Fixed build issues preventing cross compilation from working.
- Fixed dekstop entries with lower priority than a hidden entry not being hidden.
- Fixed desktop entry keys with mismatched modifier or country not being discarded.
- Fixed greetd hanging when authenticating with a fingerprint.

View file

@ -2,7 +2,10 @@
qtver, qtver,
compiler, compiler,
}: let }: let
nixpkgs = (import ./nix-checkouts.nix).${builtins.replaceStrings ["."] ["_"] qtver}; checkouts = import ./nix-checkouts.nix;
nixpkgs = checkouts.${builtins.replaceStrings ["."] ["_"] qtver};
compilerOverride = (nixpkgs.callPackage ./variations.nix {}).${compiler}; 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

View file

@ -7,9 +7,18 @@ 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 { in rec {
# For old qt versions, grab the commit before the version bump that has all the patches latest = qt6_9_0;
# instead of the bumped version.
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";

View file

@ -2,6 +2,6 @@
clangStdenv, clangStdenv,
gccStdenv, gccStdenv,
}: { }: {
clang = { buildStdenv = clangStdenv; }; clang = { stdenv = clangStdenv; };
gcc = { buildStdenv = gccStdenv; }; gcc = { stdenv = gccStdenv; };
} }

View file

@ -2,8 +2,8 @@
lib, lib,
nix-gitignore, nix-gitignore,
pkgs, pkgs,
stdenv,
keepDebugInfo, keepDebugInfo,
buildStdenv ? pkgs.clangStdenv,
pkg-config, pkg-config,
cmake, cmake,
@ -44,7 +44,7 @@
withHyprland ? true, withHyprland ? true,
withI3 ? true, withI3 ? true,
}: let }: let
unwrapped = buildStdenv.mkDerivation { unwrapped = stdenv.mkDerivation {
pname = "quickshell${lib.optionalString debug "-debug"}"; pname = "quickshell${lib.optionalString debug "-debug"}";
version = "0.2.0"; version = "0.2.0";
src = nix-gitignore.gitignoreSource "/default.nix\n" ./.; src = nix-gitignore.gitignoreSource "/default.nix\n" ./.;
@ -54,11 +54,14 @@
nativeBuildInputs = [ nativeBuildInputs = [
cmake cmake
ninja ninja
qt6.qtshadertools
spirv-tools spirv-tools
pkg-config pkg-config
] ]
++ lib.optional withWayland wayland-scanner; ++ 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 = [ buildInputs = [
qt6.qtbase qt6.qtbase
@ -68,7 +71,8 @@
++ lib.optional withQtSvg qt6.qtsvg ++ lib.optional withQtSvg qt6.qtsvg
++ lib.optional withCrashReporter breakpad ++ lib.optional withCrashReporter breakpad
++ lib.optional withJemalloc jemalloc ++ lib.optional withJemalloc jemalloc
++ lib.optionals withWayland [ qt6.qtwayland wayland wayland-protocols ] ++ lib.optional (withWayland && lib.strings.compareVersions qt6.qtbase.version "6.10.0" == -1) qt6.qtwayland
++ lib.optionals withWayland [ wayland wayland-protocols ]
++ lib.optionals (withWayland && libgbm != null) [ libdrm libgbm ] ++ lib.optionals (withWayland && libgbm != null) [ libdrm libgbm ]
++ lib.optional withX11 xorg.libxcb ++ lib.optional withX11 xorg.libxcb
++ lib.optional withPam pam ++ lib.optional withPam pam

6
flake.lock generated
View file

@ -2,11 +2,11 @@
"nodes": { "nodes": {
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1749285348, "lastModified": 1758690382,
"narHash": "sha256-frdhQvPbmDYaScPFiCnfdh3B/Vh81Uuoo0w5TkWmmjU=", "narHash": "sha256-NY3kSorgqE5LMm1LqNwGne3ZLMF2/ILgLpFr1fS4X3o=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "3e3afe5174c561dee0df6f2c2b2236990146329f", "rev": "e643668fd71b949c53f8626614b21ff71a07379d",
"type": "github" "type": "github"
}, },
"original": { "original": {

View file

@ -4,23 +4,28 @@
}; };
outputs = { self, nixpkgs }: let outputs = { self, nixpkgs }: let
overlayPkgs = p: p.appendOverlays [ self.overlays.default ];
forEachSystem = fn: forEachSystem = fn:
nixpkgs.lib.genAttrs nixpkgs.lib.genAttrs
nixpkgs.lib.platforms.linux nixpkgs.lib.platforms.linux
(system: fn system nixpkgs.legacyPackages.${system}); (system: fn system (overlayPkgs nixpkgs.legacyPackages.${system}));
in { in {
packages = forEachSystem (system: pkgs: rec { overlays.default = import ./overlay.nix {
quickshell = pkgs.callPackage ./default.nix { rev = self.rev or self.dirtyRev;
gitRev = self.rev or self.dirtyRev;
}; };
packages = forEachSystem (system: pkgs: rec {
quickshell = pkgs.quickshell;
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;
inherit (self.packages.${system}) quickshell; quickshell = self.packages.${system}.quickshell.override {
stdenv = pkgs.clangStdenv;
};
}; };
}); });
}; };

5
overlay.nix Normal file
View file

@ -0,0 +1,5 @@
{ rev ? null }: (final: prev: {
quickshell = final.callPackage ./default.nix {
gitRev = rev;
};
})

View file

@ -1,14 +1,15 @@
{ {
pkgs ? import <nixpkgs> {}, pkgs ? import <nixpkgs> {},
quickshell ? pkgs.callPackage ./default.nix {}, stdenv ? pkgs.clangStdenv, # faster compiles than gcc
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 = "1f062cc198d1112d13e5128fa1f2ee3dbffe613b"; rev = "9d85d7e7dea2602aa74ec3168955fee69967e92f";
sha256 = "kbt0Zc1qHE5fhqBkKz8iue+B+ZANjF1AR/RdgmX1r0I="; hash = "sha256-77ERiweF6lumonp2c/124rAoVG6/o9J+Aajhttwtu0w=";
}) { inherit pkgs; }; }) { inherit pkgs; };
in pkgs.mkShell.override { stdenv = quickshell.stdenv; } { in pkgs.mkShell.override { stdenv = quickshell.stdenv; } {
inputsFrom = [ quickshell ]; inputsFrom = [ quickshell ];

View file

@ -23,6 +23,7 @@ qt_add_library(quickshell-core STATIC
model.cpp model.cpp
elapsedtimer.cpp elapsedtimer.cpp
desktopentry.cpp desktopentry.cpp
desktopentrymonitor.cpp
objectrepeater.cpp objectrepeater.cpp
platformmenu.cpp platformmenu.cpp
qsmenu.cpp qsmenu.cpp

View file

@ -28,26 +28,28 @@ ColorQuantizerOperation::ColorQuantizerOperation(QUrl* source, qreal depth, qrea
: source(source) : source(source)
, maxDepth(depth) , maxDepth(depth)
, rescaleSize(rescaleSize) { , rescaleSize(rescaleSize) {
setAutoDelete(false); this->setAutoDelete(false);
} }
void ColorQuantizerOperation::quantizeImage(const QAtomicInteger<bool>& shouldCancel) { void ColorQuantizerOperation::quantizeImage(const QAtomicInteger<bool>& shouldCancel) {
if (shouldCancel.loadAcquire() || source->isEmpty()) return; if (shouldCancel.loadAcquire() || this->source->isEmpty()) return;
colors.clear(); this->colors.clear();
auto image = QImage(source->toLocalFile()); auto image = QImage(this->source->toLocalFile());
if ((image.width() > rescaleSize || image.height() > rescaleSize) && rescaleSize > 0) { if ((image.width() > this->rescaleSize || image.height() > this->rescaleSize)
&& this->rescaleSize > 0)
{
image = image.scaled( image = image.scaled(
static_cast<int>(rescaleSize), static_cast<int>(this->rescaleSize),
static_cast<int>(rescaleSize), static_cast<int>(this->rescaleSize),
Qt::KeepAspectRatio, Qt::KeepAspectRatio,
Qt::SmoothTransformation Qt::SmoothTransformation
); );
} }
if (image.isNull()) { if (image.isNull()) {
qCWarning(logColorQuantizer) << "Failed to load image from" << source->toString(); qCWarning(logColorQuantizer) << "Failed to load image from" << this->source->toString();
return; return;
} }
@ -63,7 +65,7 @@ void ColorQuantizerOperation::quantizeImage(const QAtomicInteger<bool>& shouldCa
auto startTime = QDateTime::currentDateTime(); auto startTime = QDateTime::currentDateTime();
colors = quantization(pixels, 0); this->colors = this->quantization(pixels, 0);
auto endTime = QDateTime::currentDateTime(); auto endTime = QDateTime::currentDateTime();
auto milliseconds = startTime.msecsTo(endTime); auto milliseconds = startTime.msecsTo(endTime);
@ -77,7 +79,7 @@ QList<QColor> ColorQuantizerOperation::quantization(
) { ) {
if (shouldCancel.loadAcquire()) return QList<QColor>(); if (shouldCancel.loadAcquire()) return QList<QColor>();
if (depth >= maxDepth || rgbValues.isEmpty()) { if (depth >= this->maxDepth || rgbValues.isEmpty()) {
if (rgbValues.isEmpty()) return QList<QColor>(); if (rgbValues.isEmpty()) return QList<QColor>();
auto totalR = 0; auto totalR = 0;
@ -114,8 +116,8 @@ QList<QColor> ColorQuantizerOperation::quantization(
auto rightHalf = rgbValues.mid(mid); auto rightHalf = rgbValues.mid(mid);
QList<QColor> result; QList<QColor> result;
result.append(quantization(leftHalf, depth + 1)); result.append(this->quantization(leftHalf, depth + 1));
result.append(quantization(rightHalf, depth + 1)); result.append(this->quantization(rightHalf, depth + 1));
return result; return result;
} }
@ -159,7 +161,7 @@ void ColorQuantizerOperation::finishRun() {
} }
void ColorQuantizerOperation::finished() { void ColorQuantizerOperation::finished() {
emit this->done(colors); emit this->done(this->colors);
delete this; delete this;
} }
@ -178,39 +180,39 @@ void ColorQuantizerOperation::run() {
void ColorQuantizerOperation::tryCancel() { this->shouldCancel.storeRelease(true); } void ColorQuantizerOperation::tryCancel() { this->shouldCancel.storeRelease(true); }
void ColorQuantizer::componentComplete() { void ColorQuantizer::componentComplete() {
componentCompleted = true; this->componentCompleted = true;
if (!mSource.isEmpty()) quantizeAsync(); if (!this->mSource.isEmpty()) this->quantizeAsync();
} }
void ColorQuantizer::setSource(const QUrl& source) { void ColorQuantizer::setSource(const QUrl& source) {
if (mSource != source) { if (this->mSource != source) {
mSource = source; this->mSource = source;
emit this->sourceChanged(); emit this->sourceChanged();
if (this->componentCompleted && !mSource.isEmpty()) quantizeAsync(); if (this->componentCompleted && !this->mSource.isEmpty()) this->quantizeAsync();
} }
} }
void ColorQuantizer::setDepth(qreal depth) { void ColorQuantizer::setDepth(qreal depth) {
if (mDepth != depth) { if (this->mDepth != depth) {
mDepth = depth; this->mDepth = depth;
emit this->depthChanged(); emit this->depthChanged();
if (this->componentCompleted) quantizeAsync(); if (this->componentCompleted) this->quantizeAsync();
} }
} }
void ColorQuantizer::setRescaleSize(int rescaleSize) { void ColorQuantizer::setRescaleSize(int rescaleSize) {
if (mRescaleSize != rescaleSize) { if (this->mRescaleSize != rescaleSize) {
mRescaleSize = rescaleSize; this->mRescaleSize = rescaleSize;
emit this->rescaleSizeChanged(); emit this->rescaleSizeChanged();
if (this->componentCompleted) quantizeAsync(); if (this->componentCompleted) this->quantizeAsync();
} }
} }
void ColorQuantizer::operationFinished(const QList<QColor>& result) { void ColorQuantizer::operationFinished(const QList<QColor>& result) {
bColors = result; this->bColors = result;
this->liveOperation = nullptr; this->liveOperation = nullptr;
emit this->colorsChanged(); emit this->colorsChanged();
} }
@ -219,7 +221,8 @@ 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->mRescaleSize);
QObject::connect( QObject::connect(
this->liveOperation, this->liveOperation,

View file

@ -91,13 +91,13 @@ public:
[[nodiscard]] QBindable<QList<QColor>> bindableColors() { return &this->bColors; } [[nodiscard]] QBindable<QList<QColor>> bindableColors() { return &this->bColors; }
[[nodiscard]] QUrl source() const { return mSource; } [[nodiscard]] QUrl source() const { return this->mSource; }
void setSource(const QUrl& source); void setSource(const QUrl& source);
[[nodiscard]] qreal depth() const { return mDepth; } [[nodiscard]] qreal depth() const { return this->mDepth; }
void setDepth(qreal depth); void setDepth(qreal depth);
[[nodiscard]] qreal rescaleSize() const { return mRescaleSize; } [[nodiscard]] qreal rescaleSize() const { return this->mRescaleSize; }
void setRescaleSize(int rescaleSize); void setRescaleSize(int rescaleSize);
signals: signals:

View file

@ -1,22 +1,27 @@
#include "desktopentry.hpp" #include "desktopentry.hpp"
#include <algorithm> #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 <qstringview.h> #include <qproperty.h>
#include <qscopeguard.h>
#include <qtenvironmentvariables.h> #include <qtenvironmentvariables.h>
#include <qthreadpool.h>
#include <qtmetamacros.h>
#include <ranges> #include <ranges>
#include "../io/processcore.hpp" #include "../io/processcore.hpp"
#include "desktopentrymonitor.hpp"
#include "logcat.hpp" #include "logcat.hpp"
#include "model.hpp" #include "model.hpp"
#include "qmlglobal.hpp" #include "qmlglobal.hpp"
@ -56,12 +61,14 @@ 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;
auto modifierMatches = !this->modifier.isEmpty() && this->modifier == other.modifier; if (!other.modifier.isEmpty() && this->modifier != other.modifier) return 0;
if (!other.territory.isEmpty() && this->territory != other.territory) return 0;
auto score = 1; auto score = 1;
if (territoryMatches) score += 2;
if (modifierMatches) score += 1; if (!other.territory.isEmpty()) score += 2;
if (!other.modifier.isEmpty()) score += 1;
return score; return score;
} }
@ -87,57 +94,60 @@ 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;
} }
void DesktopEntry::parseEntry(const QString& text) { ParsedDesktopEntryData DesktopEntry::parseText(const QString& id, const QString& text) {
ParsedDesktopEntryData data;
data.id = id;
const auto& system = Locale::system(); 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 finishCategory = [this, &groupName, &entries]() { auto finishCategory = [&data, &groupName, &entries]() {
if (groupName == "Desktop Entry") { if (groupName == "Desktop Entry") {
if (entries["Type"].second != "Application") return; if (entries.value("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;
this->mEntries.insert(key, value); data.entries.insert(key, value);
if (key == "Name") this->mName = value; if (key == "Name") data.name = value;
else if (key == "GenericName") this->mGenericName = value; else if (key == "GenericName") data.genericName = value;
else if (key == "StartupWMClass") this->mStartupClass = 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 == "Icon") data.icon = value;
else if (key == "Exec") { else if (key == "Exec") {
this->mExecString = value; data.execString = value;
this->mCommand = DesktopEntry::parseExecString(value); data.command = DesktopEntry::parseExecString(value);
} else if (key == "Path") this->mWorkingDirectory = value; } else if (key == "Path") data.workingDirectory = value;
else if (key == "Terminal") this->mTerminal = value == "true"; else if (key == "Terminal") data.terminal = value == "true";
else if (key == "Categories") this->mCategories = value.split(u';', Qt::SkipEmptyParts); else if (key == "Categories") data.categories = value.split(u';', Qt::SkipEmptyParts);
else if (key == "Keywords") this->mKeywords = value.split(u';', Qt::SkipEmptyParts); else if (key == "Keywords") data.keywords = value.split(u';', Qt::SkipEmptyParts);
} }
} else if (groupName.startsWith("Desktop Action ")) { } else if (groupName.startsWith("Desktop Action ")) {
auto actionName = groupName.sliced(16); auto actionName = groupName.sliced(16);
auto* action = new DesktopAction(actionName, this); DesktopActionData action;
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->mEntries.insert(key, value); action.entries.insert(key, value);
if (key == "Name") action->mName = value; if (key == "Name") action.name = value;
else if (key == "Icon") action->mIcon = value; else if (key == "Icon") action.icon = value;
else if (key == "Exec") { else if (key == "Exec") {
action->mExecString = value; action.execString = value;
action->mCommand = DesktopEntry::parseExecString(value); action.command = DesktopEntry::parseExecString(value);
} }
} }
this->mActions.insert(actionName, action); data.actions.insert(actionName, action);
} }
entries.clear(); entries.clear();
@ -183,14 +193,62 @@ void DesktopEntry::parseEntry(const QString& text) {
} }
finishCategory(); finishCategory();
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 QHash<QString, DesktopActionData>& newActions) {
auto old = this->mActions;
for (const auto& [key, d]: newActions.asKeyValueRange()) {
DesktopAction* act = nullptr;
if (auto found = old.find(key); found != old.end()) {
act = found.value();
old.erase(found);
} else {
act = new DesktopAction(d.id, this);
this->mActions.insert(key, act);
}
Qt::beginPropertyUpdateGroup();
act->bName = d.name;
act->bIcon = d.icon;
act->bExecString = d.execString;
act->bCommand = d.command;
Qt::endPropertyUpdateGroup();
act->mEntries = d.entries;
}
for (auto* leftover: old) {
leftover->deleteLater();
}
} }
void DesktopEntry::execute() const { void DesktopEntry::execute() const {
DesktopEntry::doExec(this->mCommand, this->mWorkingDirectory); DesktopEntry::doExec(this->bCommand.value(), this->bWorkingDirectory.value());
} }
bool DesktopEntry::isValid() const { return !this->mName.isEmpty(); } bool DesktopEntry::isValid() const { return !this->bName.value().isEmpty(); }
bool DesktopEntry::noDisplay() const { return this->mNoDisplay; }
QVector<DesktopAction*> DesktopEntry::actions() const { return this->mActions.values(); } QVector<DesktopAction*> DesktopEntry::actions() const { return this->mActions.values(); }
@ -266,59 +324,44 @@ void DesktopEntry::doExec(const QList<QString>& execString, const QString& worki
} }
void DesktopAction::execute() const { void DesktopAction::execute() const {
DesktopEntry::doExec(this->mCommand, this->entry->mWorkingDirectory); DesktopEntry::doExec(this->bCommand.value(), this->entry->bWorkingDirectory.value());
} }
DesktopEntryManager::DesktopEntryManager() { DesktopEntryScanner::DesktopEntryScanner(DesktopEntryManager* manager): manager(manager) {
this->scanDesktopEntries(); this->setAutoDelete(true);
this->populateApplications();
} }
void DesktopEntryManager::scanDesktopEntries() { void DesktopEntryScanner::run() {
QList<QString> dataPaths; const auto& desktopPaths = DesktopEntryManager::desktopPaths();
auto scanResults = QList<ParsedDesktopEntryData>();
if (qEnvironmentVariableIsSet("XDG_DATA_HOME")) { for (const auto& path: desktopPaths | std::views::reverse) {
dataPaths.push_back(qEnvironmentVariable("XDG_DATA_HOME")); auto file = QFileInfo(path);
} else if (qEnvironmentVariableIsSet("HOME")) { if (!file.isDir()) continue;
dataPaths.push_back(qEnvironmentVariable("HOME") + "/.local/share");
this->scanDirectory(QDir(path), QString(), scanResults);
} }
if (qEnvironmentVariableIsSet("XDG_DATA_DIRS")) { QMetaObject::invokeMethod(
auto var = qEnvironmentVariable("XDG_DATA_DIRS"); this->manager,
dataPaths += var.split(u':', Qt::SkipEmptyParts); "onScanCompleted",
} else { Qt::QueuedConnection,
dataPaths.push_back("/usr/local/share"); Q_ARG(QList<ParsedDesktopEntryData>, scanResults)
dataPaths.push_back("/usr/share"); );
}
qCDebug(logDesktopEntry) << "Creating desktop entry scanners";
for (auto& path: std::ranges::reverse_view(dataPaths)) {
auto p = QDir(path).filePath("applications");
auto file = QFileInfo(p);
if (!file.isDir()) {
qCDebug(logDesktopEntry) << "Not scanning path" << p << "as it is not a directory";
continue;
}
qCDebug(logDesktopEntry) << "Scanning path" << p;
this->scanPath(p);
}
} }
void DesktopEntryManager::populateApplications() { void DesktopEntryScanner::scanDirectory(
for (auto& entry: this->desktopEntries.values()) { const QDir& dir,
if (!entry->noDisplay()) this->mApplications.insertObject(entry); const QString& idPrefix,
} QList<ParsedDesktopEntryData>& entries
} ) {
auto dirEntries = dir.entryInfoList(QDir::Dirs | QDir::Files | QDir::NoDotAndDotDot);
void DesktopEntryManager::scanPath(const QDir& dir, const QString& prefix) { for (auto& entry: dirEntries) {
auto entries = dir.entryInfoList(QDir::Dirs | QDir::Files | QDir::NoDotAndDotDot); if (entry.isDir()) {
auto subdirPrefix = idPrefix.isEmpty() ? entry.fileName() : idPrefix + '-' + entry.fileName();
for (auto& entry: entries) { this->scanDirectory(QDir(entry.absoluteFilePath()), subdirPrefix, entries);
if (entry.isDir()) this->scanPath(entry.absoluteFilePath(), prefix + dir.dirName() + "-"); } else if (entry.isFile()) {
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";
@ -331,46 +374,42 @@ void DesktopEntryManager::scanPath(const QDir& dir, const QString& prefix) {
continue; continue;
} }
auto id = prefix + entry.fileName().sliced(0, entry.fileName().length() - 8); auto basename = QFileInfo(entry.fileName()).completeBaseName();
auto lowerId = id.toLower(); auto id = idPrefix.isEmpty() ? basename : idPrefix + '-' + basename;
auto content = QString::fromUtf8(file.readAll());
auto text = QString::fromUtf8(file.readAll()); auto data = DesktopEntry::parseText(id, content);
auto* dentry = new DesktopEntry(id, this); entries.append(std::move(data));
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;
@ -391,14 +430,14 @@ DesktopEntry* DesktopEntryManager::heuristicLookup(const QString& name) {
auto list = this->desktopEntries.values(); auto list = this->desktopEntries.values();
auto iter = std::ranges::find_if(list, [&](const DesktopEntry* entry) { auto iter = std::ranges::find_if(list, [&](DesktopEntry* entry) {
return name == entry->mStartupClass; return name == entry->bStartupClass.value();
}); });
if (iter != list.end()) return *iter; if (iter != list.end()) return *iter;
iter = std::ranges::find_if(list, [&](const DesktopEntry* entry) { iter = std::ranges::find_if(list, [&](DesktopEntry* entry) {
return name.toLower() == entry->mStartupClass.toLower(); return name.toLower() == entry->bStartupClass.value().toLower();
}); });
if (iter != list.end()) return *iter; if (iter != list.end()) return *iter;
@ -407,7 +446,137 @@ DesktopEntry* DesktopEntryManager::heuristicLookup(const QString& name) {
ObjectModel<DesktopEntry>* DesktopEntryManager::applications() { return &this->mApplications; } ObjectModel<DesktopEntry>* DesktopEntryManager::applications() { return &this->mApplications; }
DesktopEntries::DesktopEntries() { DesktopEntryManager::instance(); } void DesktopEntryManager::handleFileChanges() {
qCDebug(logDesktopEntry) << "Directory change detected, performing full rescan";
if (this->scanInProgress) {
qCDebug(logDesktopEntry) << "Scan already in progress, queuing another scan";
this->scanQueued = true;
return;
}
this->scanInProgress = true;
this->scanQueued = false;
auto* scanner = new DesktopEntryScanner(this);
QThreadPool::globalInstance()->start(scanner);
}
const QStringList& DesktopEntryManager::desktopPaths() {
static const auto paths = []() {
auto dataPaths = QStringList();
auto dataHome = qEnvironmentVariable("XDG_DATA_HOME");
if (dataHome.isEmpty() && qEnvironmentVariableIsSet("HOME"))
dataHome = qEnvironmentVariable("HOME") + "/.local/share";
if (!dataHome.isEmpty()) dataPaths.append(dataHome + "/applications");
auto dataDirs = qEnvironmentVariable("XDG_DATA_DIRS");
if (dataDirs.isEmpty()) dataDirs = "/usr/local/share:/usr/share";
for (const auto& dir: dataDirs.split(':', Qt::SkipEmptyParts)) {
dataPaths.append(dir + "/applications");
}
return dataPaths;
}();
return paths;
}
void DesktopEntryManager::onScanCompleted(const QList<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);

View file

@ -6,35 +6,68 @@
#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;
QHash<QString, 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".
Q_PROPERTY(QString name MEMBER mName CONSTANT); // clang-format off
Q_PROPERTY(QString name READ default WRITE default NOTIFY nameChanged BINDABLE bindableName);
/// Short description of the application, such as "Web Browser". May be empty. /// Short description of the application, such as "Web Browser". May be empty.
Q_PROPERTY(QString genericName MEMBER mGenericName CONSTANT); Q_PROPERTY(QString genericName READ default WRITE default NOTIFY genericNameChanged BINDABLE bindableGenericName);
/// Initial class or app id the app intends to use. May be useful for matching running apps /// Initial class or app id the app intends to use. May be useful for matching running apps
/// to desktop entries. /// to desktop entries.
Q_PROPERTY(QString startupClass MEMBER mStartupClass CONSTANT); 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 MEMBER mNoDisplay CONSTANT); Q_PROPERTY(bool noDisplay READ default WRITE default NOTIFY noDisplayChanged BINDABLE bindableNoDisplay);
/// Long description of the application, such as "View websites on the internet". May be empty. /// Long description of the application, such as "View websites on the internet". May be empty.
Q_PROPERTY(QString comment MEMBER mComment CONSTANT); Q_PROPERTY(QString comment READ default WRITE default NOTIFY commentChanged BINDABLE bindableComment);
/// Name of the icon associated with this application. May be empty. /// Name of the icon associated with this application. May be empty.
Q_PROPERTY(QString icon MEMBER mIcon CONSTANT); Q_PROPERTY(QString icon READ default WRITE default NOTIFY iconChanged BINDABLE bindableIcon);
/// The raw `Exec` string from the desktop entry. /// The raw `Exec` string from the desktop entry.
/// ///
/// > [!WARNING] This cannot be reliably run as a command. See @@command for one you can run. /// > [!WARNING] This cannot be reliably run as a command. See @@command for one you can run.
Q_PROPERTY(QString execString MEMBER mExecString CONSTANT); Q_PROPERTY(QString execString READ default WRITE default NOTIFY execStringChanged BINDABLE bindableExecString);
/// The parsed `Exec` command in the desktop entry. /// The parsed `Exec` command in the desktop entry.
/// ///
/// The entry can be run with @@execute(), or by using this command in /// The entry can be run with @@execute(), or by using this command in
@ -43,13 +76,14 @@ class DesktopEntry: public QObject {
/// the invoked process. See @@execute() for details. /// the invoked process. See @@execute() for details.
/// ///
/// > [!NOTE] The provided command does not invoke a terminal even if @@runInTerminal is true. /// > [!NOTE] The provided command does not invoke a terminal even if @@runInTerminal is true.
Q_PROPERTY(QVector<QString> command MEMBER mCommand CONSTANT); 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 MEMBER mWorkingDirectory CONSTANT); Q_PROPERTY(QString workingDirectory READ default WRITE default NOTIFY workingDirectoryChanged BINDABLE bindableWorkingDirectory);
/// If the application should run in a terminal. /// If the application should run in a terminal.
Q_PROPERTY(bool runInTerminal MEMBER mTerminal CONSTANT); Q_PROPERTY(bool runInTerminal READ default WRITE default NOTIFY runInTerminalChanged BINDABLE bindableRunInTerminal);
Q_PROPERTY(QVector<QString> categories MEMBER mCategories CONSTANT); Q_PROPERTY(QVector<QString> categories READ default WRITE default NOTIFY categoriesChanged BINDABLE bindableCategories);
Q_PROPERTY(QVector<QString> keywords MEMBER mKeywords CONSTANT); Q_PROPERTY(QVector<QString> keywords READ default WRITE default NOTIFY keywordsChanged BINDABLE bindableKeywords);
// 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");
@ -57,7 +91,8 @@ 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)) {}
void parseEntry(const QString& text); static ParsedDesktopEntryData parseText(const QString& id, const QString& text);
void updateState(const ParsedDesktopEntryData& newState);
/// Run the application. Currently ignores @@runInTerminal and field codes. /// Run the application. Currently ignores @@runInTerminal and field codes.
/// ///
@ -73,30 +108,65 @@ public:
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 QList<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;
QString mGenericName; // clang-format off
QString mStartupClass; 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);
QVector<QString> mCommand; Q_OBJECT_BINDABLE_PROPERTY(DesktopEntry, QString, bIcon, &DesktopEntry::iconChanged);
QString mWorkingDirectory; Q_OBJECT_BINDABLE_PROPERTY(DesktopEntry, QString, bExecString, &DesktopEntry::execStringChanged);
bool mTerminal = false; Q_OBJECT_BINDABLE_PROPERTY(DesktopEntry, QVector<QString>, bCommand, &DesktopEntry::commandChanged);
QVector<QString> mCategories; Q_OBJECT_BINDABLE_PROPERTY(DesktopEntry, QString, bWorkingDirectory, &DesktopEntry::workingDirectoryChanged);
QVector<QString> mKeywords; 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:
QHash<QString, QString> mEntries; void updateActions(const QHash<QString, DesktopActionData>& newActions);
ParsedDesktopEntryData state;
QHash<QString, DesktopAction*> mActions; QHash<QString, DesktopAction*> mActions;
friend class DesktopAction; friend class DesktopAction;
@ -106,12 +176,13 @@ 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);
Q_PROPERTY(QString name MEMBER mName CONSTANT); // clang-format off
Q_PROPERTY(QString icon MEMBER mIcon CONSTANT); Q_PROPERTY(QString name READ default WRITE default NOTIFY nameChanged BINDABLE bindableName);
Q_PROPERTY(QString icon READ default WRITE default NOTIFY iconChanged BINDABLE bindableIcon);
/// The raw `Exec` string from the action. /// The raw `Exec` string from the action.
/// ///
/// > [!WARNING] This cannot be reliably run as a command. See @@command for one you can run. /// > [!WARNING] This cannot be reliably run as a command. See @@command for one you can run.
Q_PROPERTY(QString execString MEMBER mExecString CONSTANT); Q_PROPERTY(QString execString READ default WRITE default NOTIFY execStringChanged BINDABLE bindableExecString);
/// The parsed `Exec` command in the action. /// The parsed `Exec` command in the action.
/// ///
/// The entry can be run with @@execute(), or by using this command in /// The entry can be run with @@execute(), or by using this command in
@ -120,7 +191,8 @@ class DesktopAction: public QObject {
/// the invoked process. /// the invoked process.
/// ///
/// > [!NOTE] The provided command does not invoke a terminal even if @@runInTerminal is true. /// > [!NOTE] The provided command does not invoke a terminal even if @@runInTerminal is true.
Q_PROPERTY(QVector<QString> command MEMBER mCommand CONSTANT); 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");
@ -136,18 +208,47 @@ public:
/// and @@DesktopEntry.workingDirectory. /// 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;
QVector<QString> mCommand;
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;
@ -161,15 +262,26 @@ public:
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.
@ -201,4 +313,7 @@ public:
Q_INVOKABLE [[nodiscard]] static DesktopEntry* heuristicLookup(const QString& name); Q_INVOKABLE [[nodiscard]] static DesktopEntry* heuristicLookup(const QString& name);
[[nodiscard]] static ObjectModel<DesktopEntry>* applications(); [[nodiscard]] static ObjectModel<DesktopEntry>* applications();
signals:
void applicationsChanged();
}; };

View file

@ -0,0 +1,68 @@
#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(); }

View file

@ -0,0 +1,32 @@
#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;
};

View file

@ -11,12 +11,12 @@
#include <qlist.h> #include <qlist.h>
#include <qlogging.h> #include <qlogging.h>
#include <qloggingcategory.h> #include <qloggingcategory.h>
#include <qnamespace.h>
#include <qobject.h> #include <qobject.h>
#include <qqmlcontext.h> #include <qqmlcontext.h>
#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"
@ -242,90 +242,6 @@ void EngineGeneration::onDirectoryChanged() {
} }
} }
void EngineGeneration::registerIncubationController(QQmlIncubationController* controller) {
// We only want controllers that we can swap out if destroyed.
// This happens if the window owning the active controller dies.
auto* obj = dynamic_cast<QObject*>(controller);
if (!obj) {
qCWarning(logIncubator) << "Could not register incubation controller as it is not a QObject"
<< controller;
return;
}
QObject::connect(
obj,
&QObject::destroyed,
this,
&EngineGeneration::incubationControllerDestroyed,
Qt::UniqueConnection
);
this->incubationControllers.push_back(obj);
qCDebug(logIncubator) << "Registered incubation controller" << obj << "to generation" << this;
// This function can run during destruction.
if (this->engine == nullptr) return;
if (this->engine->incubationController() == &this->delayedIncubationController) {
this->assignIncubationController();
}
}
// Multiple controllers may be destroyed at once. Dynamic casts must be performed before working
// with any controllers. The QQmlIncubationController destructor will already have run by the
// point QObject::destroyed is called, so we can't cast to that.
void EngineGeneration::deregisterIncubationController(QQmlIncubationController* controller) {
auto* obj = dynamic_cast<QObject*>(controller);
if (!obj) {
qCCritical(logIncubator) << "Deregistering incubation controller which is not a QObject, "
"however only QObject controllers should be registered.";
}
QObject::disconnect(obj, nullptr, this, nullptr);
if (this->incubationControllers.removeOne(obj)) {
qCDebug(logIncubator) << "Deregistered incubation controller" << obj << "from" << this;
} else {
qCCritical(logIncubator) << "Failed to deregister incubation controller" << obj << "from"
<< this << "as it was not registered to begin with";
qCCritical(logIncubator) << "Current registered incuabation controllers"
<< this->incubationControllers;
}
// This function can run during destruction.
if (this->engine == nullptr) return;
if (this->engine->incubationController() == controller) {
qCDebug(logIncubator
) << "Destroyed incubation controller was currently active, reassigning from pool";
this->assignIncubationController();
}
}
void EngineGeneration::incubationControllerDestroyed() {
auto* sender = this->sender();
if (this->incubationControllers.removeAll(sender) != 0) {
qCDebug(logIncubator) << "Destroyed incubation controller" << sender << "deregistered from"
<< this;
} else {
qCCritical(logIncubator) << "Destroyed incubation controller" << sender
<< "was not registered, but its destruction was observed by" << this;
return;
}
// This function can run during destruction.
if (this->engine == nullptr) return;
if (dynamic_cast<QObject*>(this->engine->incubationController()) == sender) {
qCDebug(logIncubator
) << "Destroyed incubation controller was currently active, reassigning from pool";
this->assignIncubationController();
}
}
void EngineGeneration::onEngineWarnings(const QList<QQmlError>& warnings) { void EngineGeneration::onEngineWarnings(const QList<QQmlError>& warnings) {
for (const auto& error: warnings) { for (const auto& error: warnings) {
const auto& url = error.url(); const auto& url = error.url();
@ -367,13 +283,27 @@ void EngineGeneration::exit(int code) {
this->destroy(); this->destroy();
} }
void EngineGeneration::assignIncubationController() { void EngineGeneration::trackWindowIncubationController(QQuickWindow* window) {
QQmlIncubationController* controller = nullptr; if (this->trackedWindows.contains(window)) return;
if (this->incubationControllersLocked || this->incubationControllers.isEmpty()) { QObject::connect(window, &QObject::destroyed, this, &EngineGeneration::onTrackedWindowDestroyed);
controller = &this->delayedIncubationController; this->trackedWindows.append(window);
} else { this->assignIncubationController();
controller = dynamic_cast<QQmlIncubationController*>(this->incubationControllers.first()); }
void EngineGeneration::onTrackedWindowDestroyed(QObject* object) {
this->trackedWindows.removeAll(static_cast<QQuickWindow*>(object)); // NOLINT
this->assignIncubationController();
}
void EngineGeneration::assignIncubationController() {
QQmlIncubationController* controller = &this->delayedIncubationController;
for (auto* window: this->trackedWindows) {
if (auto* wctl = window->incubationController()) {
controller = wctl;
break;
}
} }
qCDebug(logIncubator) << "Assigning incubation controller" << controller << "to generation" qCDebug(logIncubator) << "Assigning incubation controller" << controller << "to generation"

View file

@ -9,6 +9,7 @@
#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"
@ -40,8 +41,7 @@ public:
void setWatchingFiles(bool watching); void setWatchingFiles(bool watching);
bool setExtraWatchedFiles(const QVector<QString>& files); bool setExtraWatchedFiles(const QVector<QString>& files);
void registerIncubationController(QQmlIncubationController* controller); void trackWindowIncubationController(QQuickWindow* window);
void deregisterIncubationController(QQmlIncubationController* controller);
// takes ownership // takes ownership
void registerExtension(const void* key, EngineGenerationExt* extension); void registerExtension(const void* key, EngineGenerationExt* extension);
@ -84,13 +84,13 @@ public slots:
private slots: private slots:
void onFileChanged(const QString& name); void onFileChanged(const QString& name);
void onDirectoryChanged(); void onDirectoryChanged();
void incubationControllerDestroyed(); void onTrackedWindowDestroyed(QObject* object);
static void onEngineWarnings(const QList<QQmlError>& warnings); static void onEngineWarnings(const QList<QQmlError>& warnings);
private: private:
void postReload(); void postReload();
void assignIncubationController(); void assignIncubationController();
QVector<QObject*> incubationControllers; QVector<QQuickWindow*> trackedWindows;
bool incubationControllersLocked = false; bool incubationControllersLocked = false;
QHash<const void*, EngineGenerationExt*> extensions; QHash<const void*, EngineGenerationExt*> extensions;

View file

@ -313,8 +313,12 @@ 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) {
@ -322,7 +326,9 @@ void ThreadLogging::init() {
this->detailedFile = new QFile(); this->detailedFile = new QFile();
// buffered by WriteBuffer // buffered by WriteBuffer
this->detailedFile->open(dlogMfd, QFile::ReadWrite | QFile::Unbuffered, QFile::AutoCloseHandle); if (this->detailedFile
->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()) {
@ -331,6 +337,9 @@ void ThreadLogging::init() {
delete this->detailedFile; delete this->detailedFile;
this->detailedFile = nullptr; this->detailedFile = nullptr;
} }
} else {
qCCritical(logLogging) << "Failed to open early detailed logging memfd.";
}
} }
// This connection is direct so it works while the event loop is destroyed between // This connection is direct so it works while the event loop is destroyed between
@ -458,13 +467,13 @@ void ThreadLogging::onMessage(const LogMessage& msg, bool showInSparse) {
} }
if (!this->detailedWriter.write(msg) || (this->detailedFile && !this->detailedFile->flush())) { if (!this->detailedWriter.write(msg) || (this->detailedFile && !this->detailedFile->flush())) {
if (this->detailedFile) {
qCCritical(logLogging) << "Detailed logger failed to write. Ending detailed logs.";
}
this->detailedWriter.setDevice(nullptr); this->detailedWriter.setDevice(nullptr);
if (this->detailedFile) {
this->detailedFile->close(); this->detailedFile->close();
this->detailedFile = nullptr; this->detailedFile = nullptr;
qCCritical(logLogging) << "Detailed logger failed to write. Ending detailed logs.";
}
} }
} }

View file

@ -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 = [&](const QVariant& v) { auto getCmpKey = [this](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 = [&](const QVariant& a, const QVariant& b) { auto variantCmp = [&, this](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;
}; };

View file

@ -77,7 +77,11 @@ void CrashHandler::setRelaunchInfo(const RelaunchInfo& info) {
} }
QFile file; QFile file;
file.open(this->d->infoFd, QFile::ReadWrite);
if (!file.open(this->d->infoFd, QFile::ReadWrite)) {
qCCritical(logCrashHandler
) << "Failed to open instance info memfd, crash recovery will not work.";
}
QDataStream ds(&file); QDataStream ds(&file);
ds << info; ds << info;

View file

@ -161,7 +161,10 @@ void qsCheckCrash(int argc, char** argv) {
auto infoFd = qEnvironmentVariable("__QUICKSHELL_CRASH_INFO_FD").toInt(); auto infoFd = qEnvironmentVariable("__QUICKSHELL_CRASH_INFO_FD").toInt();
QFile file; QFile file;
file.open(infoFd, QFile::ReadOnly, QFile::AutoCloseHandle); if (!file.open(infoFd, QFile::ReadOnly, QFile::AutoCloseHandle)) {
qFatal() << "Failed to open instance info fd.";
}
file.seek(0); file.seek(0);
auto ds = QDataStream(&file); auto ds = QDataStream(&file);

View file

@ -183,7 +183,7 @@ void DBusMenuItem::updateProperties(const QVariantMap& properties, const QString
} }
} else if (removed.isEmpty() || removed.contains("icon-data")) { } else if (removed.isEmpty() || removed.contains("icon-data")) {
imageChanged = this->image.hasData(); imageChanged = this->image.hasData();
image.data.clear(); this->image.data.clear();
} }
auto type = properties.value("type"); auto type = properties.value("type");

View file

@ -36,7 +36,7 @@ class DBusMenuPngImage: public QsIndexedImageHandle {
public: public:
explicit DBusMenuPngImage(): QsIndexedImageHandle(QQuickImageProvider::Image) {} explicit DBusMenuPngImage(): QsIndexedImageHandle(QQuickImageProvider::Image) {}
[[nodiscard]] bool hasData() const { return !data.isEmpty(); } [[nodiscard]] bool hasData() const { return !this->data.isEmpty(); }
QImage requestImage(const QString& id, QSize* size, const QSize& requestedSize) override; QImage requestImage(const QString& id, QSize* size, const QSize& requestedSize) override;
QByteArray data; QByteArray data;

View file

@ -93,7 +93,8 @@ void FileViewReader::run() {
FileViewReader::read(this->owner, this->state, this->doStringConversion, this->shouldCancel); FileViewReader::read(this->owner, this->state, this->doStringConversion, this->shouldCancel);
if (this->shouldCancel.loadAcquire()) { if (this->shouldCancel.loadAcquire()) {
qCDebug(logFileView) << "Read" << this << "of" << state.path << "canceled for" << this->owner; qCDebug(logFileView) << "Read" << this << "of" << this->state.path << "canceled for"
<< this->owner;
} }
} }
@ -206,7 +207,7 @@ void FileViewWriter::run() {
FileViewWriter::write(this->owner, this->state, this->doAtomicWrite, this->shouldCancel); FileViewWriter::write(this->owner, this->state, this->doAtomicWrite, this->shouldCancel);
if (this->shouldCancel.loadAcquire()) { if (this->shouldCancel.loadAcquire()) {
qCDebug(logFileView) << "Write" << this << "of" << state.path << "canceled for" qCDebug(logFileView) << "Write" << this << "of" << this->state.path << "canceled for"
<< this->owner; << this->owner;
} }
} }

View file

@ -44,7 +44,7 @@ void JsonAdapter::deserializeAdapter(const QByteArray& data) {
this->deserializeRec(json.object(), this, &JsonAdapter::staticMetaObject); this->deserializeRec(json.object(), this, &JsonAdapter::staticMetaObject);
for (auto* object: oldCreatedObjects) { for (auto* object: this->oldCreatedObjects) {
delete object; // FIXME: QMetaType::destroy? delete object; // FIXME: QMetaType::destroy?
} }
@ -56,7 +56,7 @@ void JsonAdapter::deserializeAdapter(const QByteArray& data) {
void JsonAdapter::connectNotifiers() { void JsonAdapter::connectNotifiers() {
auto notifySlot = JsonAdapter::staticMetaObject.indexOfSlot("onPropertyChanged()"); auto notifySlot = JsonAdapter::staticMetaObject.indexOfSlot("onPropertyChanged()");
connectNotifiersRec(notifySlot, this, &JsonAdapter::staticMetaObject); this->connectNotifiersRec(notifySlot, this, &JsonAdapter::staticMetaObject);
} }
void JsonAdapter::connectNotifiersRec(int notifySlot, QObject* obj, const QMetaObject* base) { void JsonAdapter::connectNotifiersRec(int notifySlot, QObject* obj, const QMetaObject* base) {
@ -71,7 +71,7 @@ void JsonAdapter::connectNotifiersRec(int notifySlot, QObject* obj, const QMetaO
auto val = prop.read(obj); auto val = prop.read(obj);
if (val.canView<JsonObject*>()) { if (val.canView<JsonObject*>()) {
auto* pobj = prop.read(obj).view<JsonObject*>(); auto* pobj = prop.read(obj).view<JsonObject*>();
if (pobj) connectNotifiersRec(notifySlot, pobj, &JsonObject::staticMetaObject); if (pobj) this->connectNotifiersRec(notifySlot, pobj, &JsonObject::staticMetaObject);
} else if (val.canConvert<QQmlListProperty<JsonObject>>()) { } else if (val.canConvert<QQmlListProperty<JsonObject>>()) {
auto listVal = val.value<QQmlListProperty<JsonObject>>(); auto listVal = val.value<QQmlListProperty<JsonObject>>();
@ -79,7 +79,7 @@ void JsonAdapter::connectNotifiersRec(int notifySlot, QObject* obj, const QMetaO
for (auto i = 0; i != len; i++) { for (auto i = 0; i != len; i++) {
auto* pobj = listVal.at(&listVal, i); auto* pobj = listVal.at(&listVal, i);
if (pobj) connectNotifiersRec(notifySlot, pobj, &JsonObject::staticMetaObject); if (pobj) this->connectNotifiersRec(notifySlot, pobj, &JsonObject::staticMetaObject);
} }
} }
} }
@ -111,7 +111,7 @@ QJsonObject JsonAdapter::serializeRec(const QObject* obj, const QMetaObject* bas
auto* pobj = val.view<JsonObject*>(); auto* pobj = val.view<JsonObject*>();
if (pobj) { if (pobj) {
json.insert(prop.name(), serializeRec(pobj, &JsonObject::staticMetaObject)); json.insert(prop.name(), this->serializeRec(pobj, &JsonObject::staticMetaObject));
} else { } else {
json.insert(prop.name(), QJsonValue::Null); json.insert(prop.name(), QJsonValue::Null);
} }
@ -124,7 +124,7 @@ QJsonObject JsonAdapter::serializeRec(const QObject* obj, const QMetaObject* bas
auto* pobj = listVal.at(&listVal, i); auto* pobj = listVal.at(&listVal, i);
if (pobj) { if (pobj) {
array.push_back(serializeRec(pobj, &JsonObject::staticMetaObject)); array.push_back(this->serializeRec(pobj, &JsonObject::staticMetaObject));
} else { } else {
array.push_back(QJsonValue::Null); array.push_back(QJsonValue::Null);
} }
@ -178,8 +178,8 @@ void JsonAdapter::deserializeRec(const QJsonObject& json, QObject* obj, const QM
currentValue->setParent(this); currentValue->setParent(this);
this->createdObjects.push_back(currentValue); this->createdObjects.push_back(currentValue);
} else if (oldCreatedObjects.removeOne(currentValue)) { } else if (this->oldCreatedObjects.removeOne(currentValue)) {
createdObjects.push_back(currentValue); this->createdObjects.push_back(currentValue);
} }
this->deserializeRec(jval.toObject(), currentValue, &JsonObject::staticMetaObject); this->deserializeRec(jval.toObject(), currentValue, &JsonObject::staticMetaObject);
@ -212,8 +212,8 @@ void JsonAdapter::deserializeRec(const QJsonObject& json, QObject* obj, const QM
if (jsonValue.isObject()) { if (jsonValue.isObject()) {
if (isNew) { if (isNew) {
currentValue = lp.at(&lp, i); currentValue = lp.at(&lp, i);
if (oldCreatedObjects.removeOne(currentValue)) { if (this->oldCreatedObjects.removeOne(currentValue)) {
createdObjects.push_back(currentValue); this->createdObjects.push_back(currentValue);
} }
} else { } else {
// FIXME: should be the type inside the QQmlListProperty but how can we get that? // FIXME: should be the type inside the QQmlListProperty but how can we get that?

View file

@ -509,7 +509,7 @@ int runCommand(int argc, char** argv, QCoreApplication* coreApplication) {
if (state.misc.printVersion) { if (state.misc.printVersion) {
qCInfo(logBare).noquote().nospace() qCInfo(logBare).noquote().nospace()
<< "quickshell 0.2.0, revision " << GIT_REVISION << ", distributed by: " << DISTRIBUTOR; << "quickshell 0.2.1, revision " << GIT_REVISION << ", distributed by: " << DISTRIBUTOR;
if (state.log.verbosity > 1) { if (state.log.verbosity > 1) {
qCInfo(logBare).noquote() << "\nBuildtime Qt Version:" << QT_VERSION_STR; qCInfo(logBare).noquote() << "\nBuildtime Qt Version:" << QT_VERSION_STR;

View file

@ -32,7 +32,10 @@ void checkCrashRelaunch(char** argv, QCoreApplication* coreApplication) {
auto lastInfoFd = lastInfoFdStr.toInt(); auto lastInfoFd = lastInfoFdStr.toInt();
QFile file; QFile file;
file.open(lastInfoFd, QFile::ReadOnly, QFile::AutoCloseHandle); if (!file.open(lastInfoFd, QFile::ReadOnly, QFile::AutoCloseHandle)) {
qFatal() << "Failed to open crash info fd. Cannot restart.";
}
file.seek(0); file.seek(0);
auto ds = QDataStream(&file); auto ds = QDataStream(&file);

View file

@ -225,6 +225,10 @@ void GreetdConnection::onSocketReady() {
this->mResponseRequired = responseRequired; this->mResponseRequired = responseRequired;
emit this->authMessage(message, error, responseRequired, echoResponse); emit this->authMessage(message, error, responseRequired, echoResponse);
if (!responseRequired) {
this->sendRequest({{"type", "post_auth_message_response"}});
}
} else goto unexpected; } else goto unexpected;
return; return;

View file

@ -100,7 +100,7 @@ MprisPlayer::MprisPlayer(const QString& address, QObject* parent): QObject(paren
} else return static_cast<qlonglong>(-1); } else return static_cast<qlonglong>(-1);
}); });
this->bLengthSupported.setBinding([this]() { return this->bInternalLength != -1; }); this->bLengthSupported.setBinding([this]() { return this->bInternalLength.value() != -1; });
this->bIsPlaying.setBinding([this]() { this->bIsPlaying.setBinding([this]() {
return this->bPlaybackState == MprisPlaybackState::Playing; return this->bPlaybackState == MprisPlaybackState::Playing;
@ -378,7 +378,7 @@ void MprisPlayer::onPlaybackStatusUpdated() {
// For exceptionally bad players that update playback timestamps at an indeterminate time AFTER // For exceptionally bad players that update playback timestamps at an indeterminate time AFTER
// updating playback state. (Youtube) // updating playback state. (Youtube)
QTimer::singleShot(100, this, [&]() { this->pPosition.requestUpdate(); }); QTimer::singleShot(100, this, [this]() { this->pPosition.requestUpdate(); });
// For exceptionally bad players that don't update length (or other metadata) until a new track actually // For exceptionally bad players that don't update length (or other metadata) until a new track actually
// starts playing, and then don't trigger a metadata update when they do. (Jellyfin) // starts playing, and then don't trigger a metadata update when they do. (Jellyfin)

View file

@ -127,7 +127,7 @@ void Notification::updateProperties(
if (appIcon.isEmpty() && !this->bDesktopEntry.value().isEmpty()) { if (appIcon.isEmpty() && !this->bDesktopEntry.value().isEmpty()) {
if (auto* entry = DesktopEntryManager::instance()->byId(this->bDesktopEntry.value())) { if (auto* entry = DesktopEntryManager::instance()->byId(this->bDesktopEntry.value())) {
appIcon = entry->mIcon; appIcon = entry->bIcon.value();
} }
} }

View file

@ -135,8 +135,8 @@ void PwDevice::polled() {
// It is far more likely that the list content has not come in yet than it having no entries, // It is far more likely that the list content has not come in yet than it having no entries,
// and there isn't a way to check in the case that there *aren't* actually any entries. // and there isn't a way to check in the case that there *aren't* actually any entries.
if (!this->stagingIndexes.isEmpty()) { if (!this->stagingIndexes.isEmpty()) {
this->routeDeviceIndexes.removeIf([&](const std::pair<qint32, qint32>& entry) { this->routeDeviceIndexes.removeIf([&, this](const std::pair<qint32, qint32>& entry) {
if (!stagingIndexes.contains(entry.first)) { if (!this->stagingIndexes.contains(entry.first)) {
qCDebug(logDevice).nospace() << "Removed device/index pair [device: " << entry.first qCDebug(logDevice).nospace() << "Removed device/index pair [device: " << entry.first
<< ", index: " << entry.second << "] for" << this; << ", index: " << entry.second << "] for" << this;
return true; return true;

View file

@ -304,6 +304,8 @@ void PwNodeBoundAudio::updateVolumeProps(const PwVolumeProps& volumeProps) {
return; return;
} }
this->volumeStep = volumeProps.volumeStep;
// It is important that the lengths of channels and volumes stay in sync whenever you read them. // It is important that the lengths of channels and volumes stay in sync whenever you read them.
auto channelsChanged = false; auto channelsChanged = false;
auto volumesChanged = false; auto volumesChanged = false;
@ -435,11 +437,12 @@ void PwNodeBoundAudio::setVolumes(const QVector<float>& volumes) {
<< "via device"; << "via device";
this->waitingVolumes = realVolumes; this->waitingVolumes = realVolumes;
} else { } else {
if (this->volumeStep != -1) {
auto significantChange = this->mServerVolumes.isEmpty(); auto significantChange = this->mServerVolumes.isEmpty();
for (auto i = 0; i < this->mServerVolumes.length(); i++) { for (auto i = 0; i < this->mServerVolumes.length(); i++) {
auto serverVolume = this->mServerVolumes.value(i); auto serverVolume = this->mServerVolumes.value(i);
auto targetVolume = realVolumes.value(i); auto targetVolume = realVolumes.value(i);
if (targetVolume == 0 || abs(targetVolume - serverVolume) >= 0.0001) { if (targetVolume == 0 || abs(targetVolume - serverVolume) >= this->volumeStep) {
significantChange = true; significantChange = true;
break; break;
} }
@ -457,9 +460,12 @@ void PwNodeBoundAudio::setVolumes(const QVector<float>& volumes) {
} else { } else {
// Insignificant changes won't cause an info event on the device, leaving qs hung in the // Insignificant changes won't cause an info event on the device, leaving qs hung in the
// "waiting for acknowledgement" state forever. // "waiting for acknowledgement" state forever.
qCInfo(logNode) << "Ignoring volume change for" << this->node << "to" << realVolumes qCInfo(logNode).nospace()
<< "from" << this->mServerVolumes << "Ignoring volume change for " << this->node << " to " << realVolumes << " from "
<< "as it is a device node and the change is too small."; << this->mServerVolumes
<< " as it is a device node and the change is too small (min step: "
<< this->volumeStep << ").";
}
} }
} }
} else { } else {
@ -519,6 +525,7 @@ PwVolumeProps PwVolumeProps::parseSpaPod(const spa_pod* param) {
const auto* volumesProp = spa_pod_find_prop(param, nullptr, SPA_PROP_channelVolumes); const auto* volumesProp = spa_pod_find_prop(param, nullptr, SPA_PROP_channelVolumes);
const auto* channelsProp = spa_pod_find_prop(param, nullptr, SPA_PROP_channelMap); const auto* channelsProp = spa_pod_find_prop(param, nullptr, SPA_PROP_channelMap);
const auto* muteProp = spa_pod_find_prop(param, nullptr, SPA_PROP_mute); const auto* muteProp = spa_pod_find_prop(param, nullptr, SPA_PROP_mute);
const auto* volumeStepProp = spa_pod_find_prop(param, nullptr, SPA_PROP_volumeStep);
const auto* volumes = reinterpret_cast<const spa_pod_array*>(&volumesProp->value); const auto* volumes = reinterpret_cast<const spa_pod_array*>(&volumesProp->value);
const auto* channels = reinterpret_cast<const spa_pod_array*>(&channelsProp->value); const auto* channels = reinterpret_cast<const spa_pod_array*>(&channelsProp->value);
@ -537,6 +544,12 @@ PwVolumeProps PwVolumeProps::parseSpaPod(const spa_pod* param) {
spa_pod_get_bool(&muteProp->value, &props.mute); spa_pod_get_bool(&muteProp->value, &props.mute);
if (volumeStepProp) {
spa_pod_get_float(&volumeStepProp->value, &props.volumeStep);
} else {
props.volumeStep = -1;
}
return props; return props;
} }

View file

@ -158,6 +158,7 @@ struct PwVolumeProps {
QVector<PwAudioChannel::Enum> channels; QVector<PwAudioChannel::Enum> channels;
QVector<float> volumes; QVector<float> volumes;
bool mute = false; bool mute = false;
float volumeStep = -1;
static PwVolumeProps parseSpaPod(const spa_pod* param); static PwVolumeProps parseSpaPod(const spa_pod* param);
}; };
@ -214,6 +215,7 @@ private:
QVector<float> mServerVolumes; QVector<float> mServerVolumes;
QVector<float> mDeviceVolumes; QVector<float> mDeviceVolumes;
QVector<float> waitingVolumes; QVector<float> waitingVolumes;
float volumeStep = -1;
PwNode* node; PwNode* node;
}; };

View file

@ -73,7 +73,7 @@ UPowerDevice::UPowerDevice(QObject* parent): QObject(parent) {
return this->bType == UPowerDeviceType::Battery && this->bPowerSupply; return this->bType == UPowerDeviceType::Battery && this->bPowerSupply;
}); });
this->bHealthSupported.setBinding([this]() { return this->bHealthPercentage != 0; }); this->bHealthSupported.setBinding([this]() { return this->bHealthPercentage.value() != 0; });
} }
void UPowerDevice::init(const QString& path) { void UPowerDevice::init(const QString& path) {
@ -101,7 +101,7 @@ QString UPowerDevice::address() const { return this->device ? this->device->serv
QString UPowerDevice::path() const { return this->device ? this->device->path() : QString(); } QString UPowerDevice::path() const { return this->device ? this->device->path() : QString(); }
void UPowerDevice::onGetAllFinished() { void UPowerDevice::onGetAllFinished() {
qCDebug(logUPowerDevice) << "UPowerDevice" << device->path() << "ready."; qCDebug(logUPowerDevice) << "UPowerDevice" << this->device->path() << "ready.";
this->bReady = true; this->bReady = true;
} }

View file

@ -164,6 +164,7 @@ QString DBusDataTransform<PowerProfile::Enum>::toWire(Data data) {
case PowerProfile::PowerSaver: return QStringLiteral("power-saver"); case PowerProfile::PowerSaver: return QStringLiteral("power-saver");
case PowerProfile::Balanced: return QStringLiteral("balanced"); case PowerProfile::Balanced: return QStringLiteral("balanced");
case PowerProfile::Performance: return QStringLiteral("performance"); case PowerProfile::Performance: return QStringLiteral("performance");
default: qFatal() << "Attempted to convert invalid power profile" << data << "to wire format.";
} }
} }

View file

@ -25,7 +25,7 @@ ReloadPopup::ReloadPopup(QString instanceId, bool failed, QString errorString)
this->popup = component.createWithInitialProperties({{"reloadInfo", QVariant::fromValue(this)}}); this->popup = component.createWithInitialProperties({{"reloadInfo", QVariant::fromValue(this)}});
if (!popup) { if (!this->popup) {
qCritical() << "Failed to open reload popup:" << component.errorString(); qCritical() << "Failed to open reload popup:" << component.errorString();
} }

View file

@ -1,6 +1,6 @@
find_package(PkgConfig REQUIRED) find_package(PkgConfig REQUIRED)
find_package(WaylandScanner REQUIRED) find_package(WaylandScanner REQUIRED)
pkg_check_modules(wayland REQUIRED IMPORTED_TARGET wayland-client wayland-protocols) pkg_check_modules(wayland REQUIRED IMPORTED_TARGET wayland-client wayland-protocols>=1.41)
# wayland protocols # wayland protocols
@ -12,13 +12,13 @@ if(NOT TARGET Qt6::qtwaylandscanner)
message(FATAL_ERROR "qtwaylandscanner executable not found. Most likely there is an issue with your Qt installation.") message(FATAL_ERROR "qtwaylandscanner executable not found. Most likely there is an issue with your Qt installation.")
endif() endif()
execute_process( pkg_get_variable(WAYLAND_PROTOCOLS wayland-protocols pkgdatadir)
COMMAND pkg-config --variable=pkgdatadir wayland-protocols
OUTPUT_VARIABLE WAYLAND_PROTOCOLS
OUTPUT_STRIP_TRAILING_WHITESPACE
)
message(STATUS "Found wayland-protocols at ${WAYLAND_PROTOCOLS_DIR}") if(WAYLAND_PROTOCOLS)
message(STATUS "Found wayland protocols at ${WAYLAND_PROTOCOLS}")
else()
message(FATAL_ERROR "Could not find wayland protocols")
endif()
qs_add_pchset(wayland-protocol qs_add_pchset(wayland-protocol
DEPENDENCIES Qt::Core Qt::WaylandClient Qt::WaylandClientPrivate DEPENDENCIES Qt::Core Qt::WaylandClient Qt::WaylandClientPrivate

View file

@ -77,7 +77,7 @@ QDebug& operator<<(QDebug& debug, const WlDmaBuffer* buffer) {
} }
GbmDeviceHandle::~GbmDeviceHandle() { GbmDeviceHandle::~GbmDeviceHandle() {
if (device) { if (this->device) {
MANAGER->unrefGbmDevice(this->device); MANAGER->unrefGbmDevice(this->device);
} }
} }
@ -522,7 +522,7 @@ WlDmaBuffer::~WlDmaBuffer() {
bool WlDmaBuffer::isCompatible(const WlBufferRequest& request) const { bool WlDmaBuffer::isCompatible(const WlBufferRequest& request) const {
if (request.width != this->width || request.height != this->height) return false; if (request.width != this->width || request.height != this->height) return false;
auto matchingFormat = std::ranges::find_if(request.dmabuf.formats, [&](const auto& format) { auto matchingFormat = std::ranges::find_if(request.dmabuf.formats, [this](const auto& format) {
return format.format == this->format return format.format == this->format
&& (format.modifiers.isEmpty() && (format.modifiers.isEmpty()
|| std::ranges::find(format.modifiers, this->modifier) != format.modifiers.end()); || std::ranges::find(format.modifiers, this->modifier) != format.modifiers.end());

View file

@ -40,7 +40,7 @@ public:
'\0'} '\0'}
) { ) {
for (auto i = 3; i != 0; i--) { for (auto i = 3; i != 0; i--) {
if (chars[i] == ' ') chars[i] = '\0'; if (this->chars[i] == ' ') this->chars[i] = '\0';
else break; else break;
} }
} }

View file

@ -24,9 +24,9 @@ HyprlandIpcQml::HyprlandIpcQml() {
QObject::connect( QObject::connect(
instance, instance,
&HyprlandIpc::focusedMonitorChanged, &HyprlandIpc::focusedWorkspaceChanged,
this, this,
&HyprlandIpcQml::focusedMonitorChanged &HyprlandIpcQml::focusedWorkspaceChanged
); );
QObject::connect( QObject::connect(

View file

@ -68,11 +68,11 @@ void WlrScreencopyContext::captureFrame() {
this->request.reset(); this->request.reset();
if (this->region.isEmpty()) { if (this->region.isEmpty()) {
this->init(manager->capture_output(this->paintCursors ? 1 : 0, screen->output())); this->init(this->manager->capture_output(this->paintCursors ? 1 : 0, this->screen->output()));
} else { } else {
this->init(manager->capture_output_region( this->init(this->manager->capture_output_region(
this->paintCursors ? 1 : 0, this->paintCursors ? 1 : 0,
screen->output(), this->screen->output(),
this->region.x(), this->region.x(),
this->region.y(), this->region.y(),
this->region.width(), this->region.width(),

View file

@ -10,10 +10,11 @@
QtWaylandClient::QWaylandShellSurface* QtWaylandClient::QWaylandShellSurface*
QSWaylandSessionLockIntegration::createShellSurface(QtWaylandClient::QWaylandWindow* window) { QSWaylandSessionLockIntegration::createShellSurface(QtWaylandClient::QWaylandWindow* window) {
auto* lock = LockWindowExtension::get(window->window()); auto* lock = LockWindowExtension::get(window->window());
if (lock == nullptr || lock->surface == nullptr || !lock->surface->isExposed()) { if (lock == nullptr || lock->surface == nullptr) {
qFatal() << "Visibility canary failed. A window with a LockWindowExtension MUST be set to " qFatal() << "Visibility canary failed. A window with a LockWindowExtension MUST be set to "
"visible via LockWindowExtension::setVisible"; "visible via LockWindowExtension::setVisible";
} }
return lock->surface; QSWaylandSessionLockSurface* surface = lock->surface; // shut up the unused include linter
return surface;
} }

View file

@ -48,16 +48,6 @@ void QSWaylandSessionLockSurface::applyConfigure() {
this->window()->resizeFromApplyConfigure(this->size); this->window()->resizeFromApplyConfigure(this->size);
} }
bool QSWaylandSessionLockSurface::handleExpose(const QRegion& region) {
if (this->initBuf != nullptr) {
// at this point qt's next commit to the surface will have a new buffer, and we can safely delete this one.
delete this->initBuf;
this->initBuf = nullptr;
}
return this->QtWaylandClient::QWaylandShellSurface::handleExpose(region);
}
void QSWaylandSessionLockSurface::setExtension(LockWindowExtension* ext) { void QSWaylandSessionLockSurface::setExtension(LockWindowExtension* ext) {
if (ext == nullptr) { if (ext == nullptr) {
if (this->window() != nullptr) this->window()->window()->close(); if (this->window() != nullptr) this->window()->window()->close();
@ -71,11 +61,6 @@ void QSWaylandSessionLockSurface::setExtension(LockWindowExtension* ext) {
} }
} }
void QSWaylandSessionLockSurface::setVisible() {
if (this->configured && !this->visible) this->initVisible();
this->visible = true;
}
void QSWaylandSessionLockSurface::ext_session_lock_surface_v1_configure( void QSWaylandSessionLockSurface::ext_session_lock_surface_v1_configure(
quint32 serial, quint32 serial,
quint32 width, quint32 width,
@ -97,13 +82,41 @@ void QSWaylandSessionLockSurface::ext_session_lock_surface_v1_configure(
#else #else
this->window()->updateExposure(); this->window()->updateExposure();
#endif #endif
#if QT_VERSION < QT_VERSION_CHECK(6, 10, 0)
if (this->visible) this->initVisible(); if (this->visible) this->initVisible();
#endif
} else { } else {
// applyConfigureWhenPossible runs too late and causes a protocol error on reconfigure. // applyConfigureWhenPossible runs too late and causes a protocol error on reconfigure.
this->window()->resizeFromApplyConfigure(this->size); this->window()->resizeFromApplyConfigure(this->size);
} }
} }
#if QT_VERSION >= QT_VERSION_CHECK(6, 10, 0)
bool QSWaylandSessionLockSurface::commitSurfaceRole() const { return false; }
void QSWaylandSessionLockSurface::setVisible() { this->window()->window()->setVisible(true); }
#else
bool QSWaylandSessionLockSurface::handleExpose(const QRegion& region) {
if (this->initBuf != nullptr) {
// at this point qt's next commit to the surface will have a new buffer, and we can safely delete this one.
delete this->initBuf;
this->initBuf = nullptr;
}
return this->QtWaylandClient::QWaylandShellSurface::handleExpose(region);
}
void QSWaylandSessionLockSurface::setVisible() {
if (this->configured && !this->visible) this->initVisible();
this->visible = true;
}
#endif
#if QT_VERSION < QT_VERSION_CHECK(6, 9, 0) #if QT_VERSION < QT_VERSION_CHECK(6, 9, 0)
#include <private/qwaylandshmbackingstore_p.h> #include <private/qwaylandshmbackingstore_p.h>
@ -123,7 +136,7 @@ void QSWaylandSessionLockSurface::initVisible() {
this->window()->window()->setVisible(true); this->window()->window()->setVisible(true);
} }
#else #elif QT_VERSION < QT_VERSION_CHECK(6, 10, 0)
#include <cmath> #include <cmath>

View file

@ -5,6 +5,7 @@
#include <private/qwaylandwindow_p.h> #include <private/qwaylandwindow_p.h>
#include <qregion.h> #include <qregion.h>
#include <qtclasshelpermacros.h> #include <qtclasshelpermacros.h>
#include <qtversionchecks.h>
#include <qtypes.h> #include <qtypes.h>
#include <qwayland-ext-session-lock-v1.h> #include <qwayland-ext-session-lock-v1.h>
@ -20,7 +21,12 @@ public:
[[nodiscard]] bool isExposed() const override; [[nodiscard]] bool isExposed() const override;
void applyConfigure() override; void applyConfigure() override;
#if QT_VERSION >= QT_VERSION_CHECK(6, 10, 0)
[[nodiscard]] bool commitSurfaceRole() const override;
#else
bool handleExpose(const QRegion& region) override; bool handleExpose(const QRegion& region) override;
#endif
void setExtension(LockWindowExtension* ext); void setExtension(LockWindowExtension* ext);
void setVisible(); void setVisible();
@ -29,11 +35,13 @@ private:
void void
ext_session_lock_surface_v1_configure(quint32 serial, quint32 width, quint32 height) override; ext_session_lock_surface_v1_configure(quint32 serial, quint32 width, quint32 height) override;
#if QT_VERSION < QT_VERSION_CHECK(6, 10, 0)
void initVisible(); void initVisible();
bool visible = false;
QtWaylandClient::QWaylandShmBuffer* initBuf = nullptr;
#endif
LockWindowExtension* ext = nullptr; LockWindowExtension* ext = nullptr;
QSize size; QSize size;
bool configured = false; bool configured = false;
bool visible = false;
QtWaylandClient::QWaylandShmBuffer* initBuf = nullptr;
}; };

View file

@ -28,9 +28,10 @@ WlrLayershell::WlrLayershell(QObject* parent): ProxyWindowBase(parent) {
case Qt::BottomEdge: return this->bImplicitHeight + margins.top; case Qt::BottomEdge: return this->bImplicitHeight + margins.top;
case Qt::LeftEdge: return this->bImplicitWidth + margins.right; case Qt::LeftEdge: return this->bImplicitWidth + margins.right;
case Qt::RightEdge: return this->bImplicitWidth + margins.left; case Qt::RightEdge: return this->bImplicitWidth + margins.left;
default: return 0;
} }
} }
return 0;
}); });
this->bcExclusionEdge.setBinding([this] { return this->bAnchors.value().exclusionEdge(); }); this->bcExclusionEdge.setBinding([this] { return this->bAnchors.value().exclusionEdge(); });

View file

@ -153,13 +153,7 @@ void ProxyWindowBase::createWindow() {
void ProxyWindowBase::deleteWindow(bool keepItemOwnership) { void ProxyWindowBase::deleteWindow(bool keepItemOwnership) {
if (this->window != nullptr) emit this->windowDestroyed(); if (this->window != nullptr) emit this->windowDestroyed();
if (auto* window = this->disownWindow(keepItemOwnership)) { if (auto* window = this->disownWindow(keepItemOwnership)) window->deleteLater();
if (auto* generation = EngineGeneration::findObjectGeneration(this)) {
generation->deregisterIncubationController(window->incubationController());
}
window->deleteLater();
}
} }
ProxiedWindow* ProxyWindowBase::disownWindow(bool keepItemOwnership) { ProxiedWindow* ProxyWindowBase::disownWindow(bool keepItemOwnership) {
@ -185,7 +179,7 @@ void ProxyWindowBase::connectWindow() {
if (auto* generation = EngineGeneration::findObjectGeneration(this)) { if (auto* generation = EngineGeneration::findObjectGeneration(this)) {
// All windows have effectively the same incubation controller so it dosen't matter // All windows have effectively the same incubation controller so it dosen't matter
// which window it belongs to. We do want to replace the delay one though. // which window it belongs to. We do want to replace the delay one though.
generation->registerIncubationController(this->window->incubationController()); generation->trackWindowIncubationController(this->window);
} }
this->window->setProxy(this); this->window->setProxy(this);

View file

@ -532,7 +532,7 @@ QString I3IpcEvent::eventToString(EventCode event) {
case EventCode::BarStateUpdate: return "bar_state_update"; break; case EventCode::BarStateUpdate: return "bar_state_update"; break;
case EventCode::Input: return "input"; break; case EventCode::Input: return "input"; break;
case EventCode::Unknown: return "unknown"; break; default: return "unknown"; break;
} }
} }

View file

@ -115,6 +115,8 @@ XPanelWindow::XPanelWindow(QObject* parent): ProxyWindowBase(parent) {
return 0; return 0;
} }
} }
return 0;
}); });
this->bcExclusionEdge.setBinding([this] { return this->bAnchors.value().exclusionEdge(); }); this->bcExclusionEdge.setBinding([this] { return this->bAnchors.value().exclusionEdge(); });