Compare commits

..

566 commits

Author SHA1 Message Date
a5431dd02d
version: bump to 0.2.0 2025-07-26 22:50:52 -07:00
f0d5f48a82
docs: add changelogs 2025-07-26 22:50:32 -07:00
1c026545e9
core/desktopentry: use this-> in heuristicLookup 2025-07-26 22:50:17 -07:00
0416032a7c
core/reloader: trigger postReload with a signal
A signal is now used over the previous tree-searching method as some
QML components such as Repeater fail to reparent created children to
themselves, which breaks the tree.
2025-07-26 17:52:06 -07:00
1644ed5e19
bluetooth: do not try to enable rfkilled devices
Bluez will not do this and reports a property change failure.
2025-07-26 17:02:35 -07:00
91c9db581e
wayland/screencopy: handle buffer creation failures 2025-07-26 00:48:21 -07:00
ab096b7e78
wayland/screencopy: reset buffer requests between frames
Prevents buffer requests from collecting a huge set of duplicate
dmabuf and shm formats.
2025-07-26 00:45:31 -07:00
448623de5a
service/notifications: use bytes over bits in pixmap rowstride check
Fixes incorrect rowstride warnings.
2025-07-25 22:08:15 -07:00
dfededc901
launch: ignore QT_STYLE_OVERRIDE and QT_QUICK_CONTROLS_STYLE
QT_STYLE_OVERRIDE often results in unexpected QML dependencies that
don't exist being required. QT_QUICK_CONTROLS_STYLE can vary across
systems and produce unexpected results.
2025-07-25 18:24:43 -07:00
4dad447570
docs: remove }; in headers + typo fixes
}; breaks the docgen regex
2025-07-24 17:15:03 -07:00
Karboggy
3bbf39c67e
core/reloader: fix file watcher compatibility with vscode 2025-07-24 15:42:58 -07:00
cameron
f90bef2d99 hyprland/workspace: Use name instead of id for activate 2025-07-24 15:40:54 +10:00
db77c71c21
wayland/layershell: use width over height in horizontal auto exclude
Fixes #135
2025-07-21 02:38:50 -07:00
fcffbbced8
core/desktopentry: lookup wm class in nodisplay entries 2025-07-19 14:26:18 -07:00
759bd721df
core/log: stop trying to store detailed logs after write fail
Not stopping will cause the logger's write buffer to fill until OOM if
writing fails.
2025-07-19 03:41:24 -07:00
63a6d27213
core/qmlglobal: configDir, configPath() -> shellDir, shellPath() 2025-07-19 02:58:55 -07:00
77de23bb71
core/desktopentry: add StartupWMClass and heuristicLookup 2025-07-18 22:32:48 -07:00
7b417bb808
build: add /lib/qt-6 to wrapped nix package
Fixes #130
2025-07-18 17:58:20 -07:00
Rexiel Scarlet
e55d519c28 build: split derivation for extensible wrapper 2025-07-18 15:25:46 +04:00
ecc4a1249d
all: mask various useless dbus errors 2025-07-18 04:14:58 -07:00
6572a7f61d
tooling: derive import paths from QML engine import paths
Due to distro patches and default locations, we can't correctly derive
it without calling the QQmlEngine function.
2025-07-18 00:33:58 -07:00
e885f4aec1
tooling: check if .qmlls.ini is a symlink in addition to exists
QFileInfo::exists() returns false on broken symlinks.
2025-07-18 00:07:25 -07:00
ipg0
115d6717a8
services/tray: use normal icon as fallback for attention custom icon
Signed-off-by: ipg0 <pyromancy00@gmail.com>
2025-07-17 22:27:46 +03:00
91dcb41d22
services/pipewire: destroy qml ifaces early to avoid user callbacks
Consumers of defaultAudio*Changed signals can run code between
safeDestroy being called and PwObjectIface destruction due to
signal connection order. This change destroys ifaces earlier so they
are nulled by the time a changed signal is fired from destruction,
preventing access between ~PwNode() and ~QObject() completion.

Fixes #116 #122 #124
2025-07-17 00:22:58 -07:00
201c559dcd
core: add Internal pragma 2025-07-16 20:13:59 -07:00
78e3874ac6
tooling: add per-shell tooling lock to prevent races 2025-07-16 17:47:28 -07:00
986749cdb9
tooling: add automatic QMLLS support for new imports and singletons 2025-07-16 14:35:46 -07:00
4d8055f1cd
build: fix PostReloadHook resolution in LSP 2025-07-15 19:03:27 -07:00
a45fc03c7d
service/tray: fix missing documentation for invokables
'};' prior to invokables caused the docgen regex to miss them
2025-07-15 15:58:56 -07:00
ipg0
c40074dd56
service/notifications: add inline-reply action support
Signed-off-by: ipg0 <pyromancy00@gmail.com>
2025-07-15 15:49:59 -07:00
3dfb7d8827
core/window: handle graphics context loss 2025-07-15 15:40:10 -07:00
a2146f6394
core/window: add closed() signal to all window types 2025-07-15 15:39:55 -07:00
5706c09e6f
core/window: clean up window interface property proxies 2025-07-15 14:06:26 -07:00
5ac9096c1c
Revert "core/region: use QList over QQmlListProperty for child regions"
This reverts commit 0c9c5be8dd.

Using QList breaks the default property usage.
2025-07-14 02:56:34 -07:00
05fbead660
x11/panelwindow: calc screen geom with exclusions of other panels
Fixes a typo in 9604302 which calculated panel stack offsets from the
current panel instead of others in the stack.
2025-07-13 22:54:20 -07:00
478aa2bda1
core/window: run polish in onExposed instead of polishItems
Fixes hyprland visible regions created before window expose.
2025-07-13 22:27:44 -07:00
cee1f5837e
service/mpris: make lengthSupported bindable and notify for changes
Fixes #109
2025-07-13 20:32:51 -07:00
71334bfcaf
core/desktopentry: expose exec command and use execDetached on call 2025-07-13 20:05:54 -07:00
de25787451
io/process: null stdio channels in detached processes 2025-07-13 20:05:54 -07:00
b011cd9d33
core/window: set FloatingWindow default max size to QWINDOWSIZE_MAX
Was previously zero, which will shrink the window to 1px depending on
the display server.
2025-07-13 20:05:53 -07:00
1e1ba93713
core/window: add manual PanelWindow tester 2025-07-13 18:35:41 -07:00
59d29bb254
x11/panelwindow: use Qt window default screen if none is provided
Fixes panels not updating geometry or attachments under X if a screen
was not explicitly provided by the user.
2025-07-13 18:30:48 -07:00
9604302415
x11/panelwindow: convert to bindable properties 2025-07-13 17:50:17 -07:00
479ff58f84
wayland/layershell: support opposite-to-exclusion edge margins 2025-07-13 17:08:12 -07:00
3b4ebc5f16
wayland/layershell: support auto exclusive zone without constraint 2025-07-13 17:02:18 -07:00
bb206e3a19
core/window: run window-level polish along with item polish
Fixes input masks not updating after a reload.
2025-07-12 22:02:33 -07:00
0c9c5be8dd
core/region: use QList over QQmlListProperty for child regions 2025-07-12 22:02:31 -07:00
49a3752b9d
core: correctly deregister QML incubators on destruction
Previously we'd try to cast the QObject* sender from
QObject::destroyed to a QQmlIncubationController*. This will always
return nullptr because C++ destructors change the type of the object
and the QQmlIncubationController destructor has already run at this
point. We now store controllers as QObject*s.

Fixes #108
2025-07-11 00:38:58 -07:00
026aac3756
build: add icon and desktop file 2025-07-10 21:57:10 -07:00
d7079b7524
core: allow qml scanner to detect namespaced and versioned imports 2025-07-10 04:28:05 -07:00
6f774af11e
core/colorquant: print image source url vs pointer on err 2025-07-10 04:05:16 -07:00
5703fbae21
wayland/lock: handle null window in configure()
Has caused a crash.
2025-07-10 04:01:00 -07:00
07ea4de248
io/ipchandler: add registry logs 2025-07-10 03:50:11 -07:00
2629e211fa
crash: initialize QApplication after logging to run cat filter 2025-07-10 03:40:55 -07:00
b4c62b8ff9
core: only log warn+ from quickshell.paths 2025-07-10 03:40:30 -07:00
1af08c0c52
core: only call QmlScanner::scanDir() on directories
Removes a bogus warning message.
2025-07-10 03:12:43 -07:00
4b35d7b51b
core: support qs. imports 2025-07-10 01:50:53 -07:00
3d594e16dd
core/log: track default logging categories
Fixes a bug in fb37be7 which ignored default logging categories due to
skipping QLoggingRegistry's filter.
2025-07-08 13:49:06 -07:00
5d7e07508a
bluetooth: fix defaultAdapter reactivity
Fixes #100
2025-07-07 02:21:50 -07:00
87d99b866f
services/pipewire: destroy bound audio object when node is destroyed
Fixes a leak and prevents a UAF via device volume signals
derefing the freed node.

Fixes #91
2025-07-04 20:29:50 -07:00
7eff415b25
core/qmlglobal: re-add shellRoot as a deprecated property 2025-07-04 20:06:22 -07:00
3cc7ced3a0
core/window: fix QsWindow being null for WlrLayershell 2025-07-04 17:58:55 -07:00
fb37be7611
core/log: ignore on-disk logging configs for quickshell* rules.
Fixes fedora hiding all command output by default.
2025-07-04 16:43:01 -07:00
9708d8212a
core/reloader: trigger onPostReload if launched post-reload
This is similar to the check in Reloadable, and fixes a number of hard
to debug issues with Process, IpcHandler, NotificationServer, and
GlobalShortcut not working depending on where you put them in a QML file.
2025-07-04 16:02:14 -07:00
0e6518a706
core/command: improve dead instance selection
Prints dead instances if they exist, as well as allowing dead instance
selection for a substring if no live instances exist.
2025-07-02 22:47:19 -07:00
86591f122d
io/process: mask the "QProcess destroyed for running process" warn 2025-07-02 20:16:47 -07:00
f681e2016f
bluetooth: add bluetooth integration
Missing support for things that require an agent, but has most basics.

Closes #17
2025-07-01 00:25:16 -07:00
1d02292fbf
hyprland/ipc: actually set lastIpcObject 2025-06-27 04:09:14 -07:00
f842b84a5a
widgets/wrapper: round child position when centering
Fixes misalignment when resizeChild is false and wrapper width is odd.
2025-06-26 12:43:59 -07:00
d949f91347
wayland/screencopy: apply output transform to wlr screencopy
Note that this only fixes output copies, and not toplevel copies.
Toplevel copies are harder because a toplevel can be on more than
one output. Hopefully we'll all be using image-copy-capture before
this one comes up.

Fixes #75
2025-06-25 12:34:00 -07:00
27f97c3283
wayland/toplevel: refactor toplevel output tracking to its own file 2025-06-24 19:38:34 -07:00
20c3da01f1
io/fileview: null watcher ptr after deletion to avoid UAF
Fixes #69
2025-06-21 12:57:15 -07:00
8be18c05ed
hyprland/ipc: expose HyprlandToplevel jsons 2025-06-20 21:31:44 -07:00
98d09b5a36
io/process: add Process.exec() 2025-06-20 20:32:42 -07:00
8fc3e1cb6e
docs: include HyprlandToplevel in module file 2025-06-20 19:06:59 -07:00
c17ea54371
wayland/lock: check for protocol availability before use
Fixes #66
2025-06-20 16:37:22 -07:00
Maeeen
362c8e1b69
hyprland/ipc: expose Hyprland toplevels 2025-06-20 04:09:37 -07:00
c115df8d34
docs: mention github mirror in README 2025-06-20 03:34:05 -07:00
02362c3e94
services/pipewire: add missing ; after Q_ENUM for docgen 2025-06-20 02:53:30 -07:00
3d3b7f1c05
wayland/lock: avoid creating lock surfaces for the fallback screen
Fixes #61
2025-06-19 14:54:52 -07:00
79b2204af8
io/socketserver: correctly order startup/teardown across generations
Fixes #60
2025-06-19 13:50:47 -07:00
95d0af8113
services/pipewire: update volume props from device for device nodes
Yet another device node edge case. In addition to only writing via
a pw_device when present, now we only read from one as well.

This fixes missing state changes not conveyed by the pw_node.

Fixes #35
2025-06-19 05:12:24 -07:00
579d589290
core/popupanchor: ensure item-derived rect is at least 1x1 pixels 2025-06-18 13:41:14 -07:00
9a30333405
build: clarify shared libraries 2025-06-15 23:00:56 -07:00
d9164578a2
core/window: add title property to floating windows 2025-06-15 03:12:51 -07:00
20322484b9
wayland/layershell: fix bridge destructor use after free on reload
Under some conditions, Qt will recreate the layer surface. The layer
surface destructor tries to destroy the bridge, but doesn't actually
need to because the bridge is a child of the QWindow owning the layer,
meaning not destroying it is actually completely fine.
2025-06-15 02:52:16 -07:00
0499518143
core/qmlglobal: add execDetached functions for spawning processes 2025-06-15 02:52:16 -07:00
0140356d99
core/qmlglobal!: rename shellRoot to configDir + add configPath 2025-06-14 14:45:04 -07:00
71fe3d9165
x11/panelwindow: do not look up engine generation in ~XPanelWindow()
Looking up engine generation in the destructor causes occasional
crashes. This commit caches it to prevent that from happening.
2025-06-13 20:12:32 -07:00
517143adf9
all: fix new lints 2025-06-12 17:01:13 -07:00
05b5eccf2e
build: update build guide, nix and guix packages 2025-06-12 12:59:36 -07:00
703a378908
core: change version number 2025-06-11 14:26:41 -07:00
09981a0498
core/log: print path to detailed log instead of text log on launch 2025-06-11 14:04:11 -07:00
2a8479d635
core/generation: use deleteLater() to delete file watchers
Should fix crashes similar to those in FileView.
2025-06-11 13:47:30 -07:00
2b01a75679
io/process: add StdioCollector data stream parser 2025-06-09 22:29:02 -07:00
0224fa942b
io/fileview: use deleteLater() to delete file watchers
Fixes crashes when writing a file while watching it.
2025-06-09 21:45:42 -07:00
91000a582b
docs: point readme to website 2025-06-09 14:45:21 -07:00
dcd9e3aed8
hyprland/ipc: implement toplevel address association 2025-06-09 14:45:21 -07:00
nydragon
ee570ec623
services/pipewire: expose node type 2025-06-07 03:26:55 -07:00
6b3d64e32a
widgets/wrapper: use top/bottom margins in implicitHeight not l/r
oops
2025-06-06 21:20:03 -07:00
aa547bad84
wayland/popupanchor: consider window size in anchor dirty state
With nonstandard edges or gravity, the compositor might not reposition
a popup when its size changes, and its unclear if its supposed to.
2025-05-30 02:55:34 -07:00
d1df932d60
core/popupanchor: add margins property 2025-05-30 00:33:07 -07:00
ef077ddd24
core/panelwindow: move Margins to types.hpp 2025-05-30 00:33:00 -07:00
2773e5468f
core/process: ignore environment changes made by the Env pragma
This pragma ends up used to set things like QQC theme which
shouldn't be cascaded into child processes.
2025-05-29 23:30:55 -07:00
4a0f6382b0
core/window: expose coordinate mapping functions on QsWindow 2025-05-29 22:42:14 -07:00
6d42d26c79
core/popupanchor: add item-relative anchor rect support 2025-05-29 20:07:00 -07:00
adcef7fc30
core/popupwindow: wait for polish to reposition 2025-05-29 16:17:15 -07:00
b67f92bc13
all: use BINDABLE only with trivial setters
Fixes various bugs caused by the QML engine bypassing setters
when BINDABLE is specified (even if the bindable is const).
Also restructures all properties using BINDABLE to have
a default READ and WRITE to ensure this doesn't happen again.
2025-05-29 16:08:39 -07:00
2e3c15f7a1
wayland/layershell: use bindable implicit size in exclusive zone calc
Fixes #42
2025-05-28 02:07:21 -07:00
5ae8e4901a
core/window: move implicit width/height to bindable properties 2025-05-28 02:07:21 -07:00
cb195d4b2a
launch: look for configs in all XDG config dirs 2025-05-27 16:43:09 -07:00
b898592db7
core: don't show " at " in qml warnings if object name is empty 2025-05-26 17:44:45 -07:00
ee31e5d226
ui/reload: use monospace font for error message 2025-05-26 04:45:54 -07:00
ec433d1a70
core: improve log format for QML errors and warnings 2025-05-26 03:35:52 -07:00
4472b27039
core/reloader: watch new files detected in failed reloads 2025-05-26 02:28:01 -07:00
e931b85464
core/window: add min/max size to FloatingWindow 2025-05-25 23:24:59 -07:00
05ed9ff74c
wayland/screencopy: add constrained implicitSize for ScreencopyView 2025-05-25 21:00:20 -07:00
7390ae28e4
widgets/cliprect: override parent's implicit size instead of wrapper
Adding implicitSize to margin wrappers broke implicit sizing
of ClippingWrapperRectangle.
2025-05-25 21:00:20 -07:00
73e673ea1c
widgets/cliprect: set default background color to white
Matches Rectangle.
2025-05-25 18:02:07 -07:00
abd9a3c5f8
wayland/screencopy: use all dmabuf planes and modifiers in egl image
Fixes black texture on nvidia
2025-05-25 17:47:58 -07:00
bf235d3d4d
wayland/layershell: ensure bridge is nulled on layer destruction
Fixes rare race condition crashes.
2025-05-25 16:11:57 -07:00
2bcd9e07fd
widgets/wrapper: default resizeChild to true
Better reflects how wrapper types are used 99% of the time.
2025-05-25 18:12:09 -07:00
428aec950e
widgets/wrapper: set WrapperRectangle border.width to 0
Works around the implicit 1px border applied to Rectangles
when border is accessed, and works around QTBUG-137166.
2025-05-25 18:12:09 -07:00
23ef14c31d
widgets/wrapper: apply implicit size override on componentComplete 2025-05-25 18:12:03 -07:00
d872ea888d
service/tray: hide missing prop warnings for ToolTip 2025-05-24 03:21:26 -07:00
c4a7d16478
io/process: emit read for data remaining in buffer on exit 2025-05-24 02:39:01 -07:00
89e796cb21
widgets/cliprect: default data property instead of children property 2025-05-24 02:00:55 -07:00
3cf96ecf97
widgets/wrapper: support overriding implicit size 2025-05-24 01:55:55 -07:00
e135de9ec6
widgets/wrapper: use bindable properties everywhere
Also fixes changes to margin not updating geometry
2025-05-24 01:25:35 -07:00
8b5b12b722
core/scriptmodel: update model data after objectProp eq
Updates the values list and sends dataChanged if objectProp
compared equal but the new value is actually different.
2025-05-23 18:03:59 -07:00
2e33ef5b7f
hyprland/ipc: track workspace fullscreen state 2025-05-19 00:01:04 -07:00
edfc4c681c
wayland/layershell: ensure exclusive zone is updated on zone/mode chg 2025-05-18 23:15:52 -07:00
644254d9ec
wayland/toplevel: expose visible outputs 2025-05-18 23:13:17 -07:00
5193426cd7
core/qmljson: add support for synthesized .qml.json files 2025-05-18 20:39:48 -07:00
6026c4ce27
service/mpris: expose dbus service name 2025-05-18 12:33:43 -07:00
c77a12d7bb
ui/reload: add textual fallback for copy and close icons 2025-05-18 12:23:02 -07:00
56b4ef3d21
service/tray: silence compliance warnings
Almost every tray item is missing properties and these
messages pollute the log, masking more useful warnings.
2025-05-18 03:42:22 -07:00
2e905f6447
core/scriptmodel: add objectProp, allowing js objects to be compared 2025-05-17 19:34:57 -07:00
61f00a0442
core/model: return ObjectModel values list directly 2025-05-17 17:03:03 -07:00
6dbc310df4
widgets/cliprect: fix premultiplied alpha blending 2025-05-17 17:03:03 -07:00
8124a63ee4
ui: add native reload popup 2025-05-17 17:03:03 -07:00
5c1d600e84
core/window: fix UAF in createQQuickWindow 2025-05-17 04:01:45 -07:00
4d74851fd0
widgets/wrapper: add WrapperMouseArea 2025-05-17 00:38:09 -07:00
4d7d06bb9b
core/qmlglobal: add clipboard support 2025-05-16 22:16:28 -07:00
325a51c82d
core: add DataDir and StateDir pragmas 2025-05-16 20:54:21 -07:00
a05c0de53b
core/qmlglobal: add dataPath(), statePath() and cachePath() 2025-05-16 20:33:40 -07:00
c1c24c2998
core/qmlglobal: expose dataDir and stateDir 2025-05-16 20:14:59 -07:00
69c7f4fe77
core/qmlglobal: expose cacheDir 2025-05-16 20:01:38 -07:00
fee4942771
io/fileview: add adapter support and JsonAdapter 2025-05-16 20:01:38 -07:00
cb69c2d016
service/upower: do not crash when trying to set power profile w/o ppd 2025-05-15 21:19:27 -07:00
48a56381a6
hyprland/focus_grab: prevent grab object leak on activate 2025-05-14 17:07:14 -07:00
baa9e5e074
ci: fix perl warnings in lint 2025-05-13 20:31:33 -07:00
e342ba322e
ci: add qt 6.8.2, 6.8.3 and 6.9.0 2025-05-13 20:14:33 -07:00
4ae0eae3da
wayland/toplevel: add close() request
Closes #37
2025-05-13 18:59:51 -07:00
e0cff677a5
wayland/layershell: refactor layer shell surface integration
In addition to the much needed cleanup:

- The bridge/extension type is now directly tied to the QWindow
instead of the WlrLayershell object, and is much smaller.
- Layer requests are now comitted via polish instead of for each
change individually.
2025-05-13 14:56:49 -07:00
6a8284dae3
core/window: add implicit size properties to window types 2025-05-12 19:42:46 -07:00
ead9141aca
widgets/wrapper: add distinct top/bottom/left/right margins 2025-05-12 13:53:07 -07:00
ca26210cc4
core/desktopentry: check XDG_DATA_HOME according to base dirs spec
Closes #34
2025-05-11 18:30:31 -07:00
8863bf55ff
core/window: fix null QObject::connect in ProxyWindowBase::setScreen 2025-05-05 22:52:47 -07:00
67524f9d8e
wayland/lock: fix protocol errors with Qt 6.9.0
QWaylandWindow::initWindow now forces a null surface commit which is
illegal. This change swaps the surface out for a dummy during
initWindow.
2025-04-26 16:19:07 -07:00
ed528268e0
core/scriptmodel: detatch mValues when accessed during update
Fixes iterator invalidation caused by the QML engine.
2025-04-06 01:40:55 -07:00
Andrew Wong
3a97da0029
guix: add wrap-program phase
This lets quickshell find QML modules outside of 'guix shell'.
2025-03-30 00:18:06 -07:00
4ea77a8eb6
wayland: update QWaylandShellSurface impls to use updateExposure()
For Qt 6.9.0
2025-03-27 15:57:57 -07:00
14aa1793df
i3/ipc: fix workspace and monitor focus being unset on launch 2025-03-27 15:56:53 -07:00
2028766e61
i3/ipc: provide default sorting for workspaces 2025-03-27 14:52:58 -07:00
d6a4ebc742
hyprland/ipc: provide default sorting for workspaces 2025-03-27 14:39:27 -07:00
fa74449139
service/tray: add "Communications" category
Closes #28
2025-03-27 14:01:41 -07:00
69430e3873
service/tray: provide default sorting for SystemTray.items
Items are first sorted by category, and then by name.
2025-03-27 13:51:43 -07:00
392f56c40e
hyprland/ipc: further cleanup + add Hyprland.focusedWorkspace 2025-03-27 00:25:21 -07:00
67b2682604
i3/ipc: general cleanup + add active property
Brings the I3 ipc interface inline with the Hyprland one.
2025-03-27 00:05:05 -07:00
8f11d60999
hyprland/ipc: make monitor bindable + fix property errors 2025-03-26 22:14:35 -07:00
62ccab5d30
hyprland/ipc: expose active and focused properties + activate() 2025-03-26 03:11:36 -07:00
Andrew Wong
207e6114a3
guix: add guix package definition
This allows the repository to be used as a channel. It can also be used to
enter a development/trial shell.
2025-03-24 22:04:18 -07:00
1a20c39fba
i3/ipc: convert to bindable properties 2025-03-21 02:46:47 -07:00
3b2d84caf0
hyprland/ipc: convert to bindable properties 2025-03-21 02:46:09 -07:00
eabf79ebb6
core/command: allow qs log to retrieve logs of dead instances
If no live instances are found matching the current config, the
youngest dead instance will be used instead.
2025-03-19 15:35:10 -07:00
0662c37d67
io/process!: replace manageLifetime with startDetached
In most cases this is what was desired for usages of
manageLifetime. Starting the process in a detached state also makes
sure the process hierarchy will not result in the child being killed when
Quickshell is killed.
2025-03-12 01:37:38 -07:00
burein-ita
c5bea858a0 io/process: hack around moc parse error
MOC default include paths contain macros that cause name collisions
2025-03-03 16:08:49 -06:00
burein-ita
9534778a78 wayland/screencopy: Add missing include needed for musl
Musl stdlib does not include sys/types.h resulting in undefined dev_t.
2025-02-28 19:37:19 -06:00
d1a172751d
service/mpris: hack around more non-compliant players
Mpris is currently winning the competition for least compliant clients.
2025-02-21 02:38:12 -08:00
1eabf5b3c3
io/fileview: ensure directory is watched for file creation 2025-02-19 21:58:05 -08:00
aeb347ba91
wayland/toplevel: add pending state for outputs entered before qscreen init
Fixes a crash in sway, and potentially other compositors, when a
toplevel enters an output before Qt has created a QScreen for it.
2025-01-31 23:54:11 -08:00
c3ed3b0ee2
hyprland/ipc: fix nullptr workspace read in log line 2025-01-31 23:28:49 -08:00
50026f0934
i3/ipc: ensure workspace does not exist at init before model add 2025-01-31 23:28:14 -08:00
fb326e0e9c
i3/ipc: fix wrong event handler being called for GetWorkspaces
Prevented the workspace list from being initialized correctly.
2025-01-29 17:21:30 -08:00
d58b7b5dcb
core/colorquant: add ColorQuantizer 2025-01-28 13:59:44 -05:00
fb343ab639
hyprland/ipc: prefer ID based workspace lookups to name based ones
Should (hopefully) reduce race condition issues.
2025-01-27 22:19:28 -08:00
d3b1a65911
hyprland/ipc: reduce impact of racing workspace queries 2025-01-27 21:13:53 -08:00
9506c1bb62
docs: update CONTRIBUTING style guide 2025-01-26 18:37:53 -08:00
4f2610dece
io/ipchandler: add prop get 2025-01-26 03:57:07 -08:00
9417d6fa57
core/command: deprecate qs msg 2025-01-25 01:00:42 -08:00
420529362f
core/clock: expose date as a QDateTime 2025-01-24 23:53:31 -08:00
325be8857c
core/command: add option to select newest matching instance 2025-01-24 16:30:32 -08:00
b289bfa504
hyprland/surface: add visibleMask 2025-01-23 14:00:16 -08:00
cdaff2967f
core/icon: stop reusing image ids (dbusmenu, notifications)
Fixes issues caused by the QML engine caching old pixmaps using the
same IDs as new ones, notably dbusmenu icons.
2025-01-22 23:10:49 -08:00
c6791cf1f2
core/window: fix screen assignments being completely broken 2025-01-22 20:13:29 -08:00
b73eff0e47
core/screen: add model and serial number properties 2025-01-22 19:38:18 -08:00
Richard Bainesly
6a017d63d6
fix single quote parsing 2025-01-22 19:25:45 -08:00
3c7dfcb220
hyprland/ipc: handle renameworkspace 2025-01-22 04:16:08 -08:00
b336129c34
core/window: add QsWindow.devicePixelRatio 2025-01-22 03:33:46 -08:00
bc73d35d03
wayland/screencopy: fix ScreencopyContext leak in ScreencopyView
Also caused an FD leak.
2025-01-20 15:53:04 -08:00
6464ead0f1
core/window: move input mask handling + commit scheduling to polish 2025-01-20 01:14:28 -08:00
d6b58521e9
core!: fix typo in ShellScreen.primaryOrientation 2025-01-19 01:00:03 -08:00
d195ca7680
wayland/screencopy: fix UAF in dmabuf modifier collection
The QList optimization the code was for no longer exists.
2025-01-15 03:24:19 -08:00
ca79715cce
wayland/screencopy: log more information during buffer creation 2025-01-15 02:52:08 -08:00
c2ed5bf559
core/stacklist: add tests 2025-01-15 02:47:14 -08:00
6024c37492
core/scriptmodel: improve docs 2025-01-14 15:30:48 -08:00
6d8022b709
service/pipewire: add registry and node ready properties 2025-01-14 15:30:47 -08:00
Richard Bainesly
8b6aa624a2
fix fd leaks in scanPath
use auto
2025-01-14 13:05:15 -08:00
cd429142a4
wayland/screencopy: add screencopy 2025-01-14 05:08:07 -08:00
918dd2392d
build/wayland: do not link to a target in wl_proto 2025-01-11 23:59:19 -08:00
2c411fce5a
all: fix new lints 2025-01-07 03:11:19 -08:00
26d443aa50
ci: add 6.8.1 2025-01-06 22:21:32 -08:00
af86d5fd19
hyprland/surface: remove debug print 2025-01-05 23:53:03 -08:00
761d99d644
service/mpris: reset position timestamps on seek
Moving the onPositionUpdated callback to a bpPosition binding caused
it not to fire when Position was changed to the same value, which can
happen when quickly changing tracks before the player has sent a new
position.

This reverts the above change while still updating position on seek.
2025-01-05 01:55:33 -08:00
fca058e66c
service/upower: add device model property 2025-01-04 04:38:03 -08:00
eaf854935b
service/upower: correctly deserialize UPowerDeviceState::Discharging
???
2025-01-04 03:37:53 -08:00
f3b7171b25
core/window: allow explicit surface format selection 2025-01-04 03:04:41 -08:00
dc3a79600d
core/command: avoid running when cli11 forces returning 0
Fixes running when --help is passed.
2025-01-03 02:42:32 -08:00
47bcf8ee61
service/upower: add power-profiles support 2025-01-02 21:54:36 -08:00
66b9917e70
service/mpris: trigger onPositionUpdated when seeking 2025-01-01 19:56:51 -08:00
3a40174ed6
hyprland/surface: add hyprland surface opacity support 2025-01-01 17:45:23 -08:00
08836ca1f3
core/scriptmodel: add expression model for unique lists 2024-12-27 04:16:12 -08:00
2f194b7894
service/upower: track device additions/removals
Also ensures displayDevice is always present.
2024-12-20 15:58:44 -08:00
611cd76abc
core/proxywindow: connect mScreen's destroy signal in all cases
Fixes edge case crashes when unplugging and replugging monitors.
2024-12-19 03:34:07 -08:00
27840db7a6
service/mpris: don't send postTrackChanged unless trackChanged sent 2024-12-13 15:04:28 -08:00
a053373d57
core/qsmenu!: improve menu layout change UX
Exposes QsMenuOpener.children as an ObjectModel instead of a list to
allow smoother layout change handling in custom menu renderers.

Fixes QsMenuAnchor/platform menus closing whenever menu content changes.
2024-12-13 01:30:11 -08:00
3fc1c914c7
lint: remove reinterpret_cast lint
Unhelpful.
2024-12-06 20:18:38 -08:00
be5e5fc4a5
lint: remove broken lint selection
The first line ended with a period instead of a comma, but both lints
were additionally unhelpful for quickshell.
2024-12-06 19:43:16 -08:00
ded3708762
io/fileview: correctly mark signals as signals in docs 2024-12-06 03:19:58 -08:00
69d13967c9
io/fileview: add support for watching changes 2024-12-06 02:32:19 -08:00
ccf885081c
build: add progress bar to just lint-changed 2024-12-06 01:20:05 -08:00
70be74e80d
io/fileview: add write support
FileView is now getting somewhat out of hand. The asynchronous parts
especially need to be redone, but this will work for now.
2024-12-06 01:18:31 -08:00
2d05c7a89e
core/menu: correctly handle menu destruction while open 2024-12-05 19:46:08 -08:00
26280b34b4
widgets/cliprect: fix typo in bottomRightRadius 2024-12-03 23:27:50 -08:00
af14a416c1
widgets/wrapper: update child geometry when implicit size changes
The implicit size update from a child item of a MarginWrapper
component triggers an implicit size update of the wrapper component,
but this does not necessarily result in the actual size of the wrapper
changing (e.g. when it is positioned by a layout).
2024-11-29 02:03:54 -08:00
cb05e9a327
core/reloader: fix incubator warnings 2024-11-29 01:37:14 -08:00
8882f7ca50
core/proxywindow: fix ProxiedWindow proxy pointer after reload
Previously was not updated after reload, causing QsWindowAttached to
use the old window pointer after it had been freed.
2024-11-29 00:11:56 -08:00
59298f6507
i3: fix Q_PROPERTY definitions for use with typegen
The regex currently can't handle line breaks
2024-11-27 23:47:28 -08:00
fd87be1355
widgets/cliprect: pass user input to contained items 2024-11-27 23:43:03 -08:00
b6a79fe99c
core/proxywindow: improve QsWindowAttached robustness
Can now track window parent window changes.
Added tests.
2024-11-27 23:30:38 -08:00
539692bc11
service/tray: re-add Q_INVOKABLE to invokable functions
Was accidentally removed in the last refactor.
2024-11-26 14:48:54 -08:00
87a57b7a2c
launch: don't try to write daemon exit from monitor process 2024-11-24 13:38:14 -08:00
e3d003e7ab
core/popupanchor: emit anchoring() before checking anchor props 2024-11-24 13:22:10 -08:00
6f9993394a
hyprland/ipc: pad event argument list to given count
Fixes crash when assuming more arguments than given will be available,
and trailing ",".
2024-11-24 13:21:09 -08:00
Nydragon
31adcaac76
i3/sway: add support for the I3 and Sway IPC
sway: add urgent and focused dispatchers to workspaces

flake: add sway toggle

WIP sway: add monitor status

sway: handle multiple ipc events in one line

sway: reuse socket connection for dispatches & better command type handling

WIP sway: add associated monitor to a workspace

i3/sway: update to allow for i3 compatibility

i3/sway: manage setting the focused monitors

i3/sway: fix multi monitor crash

i3/sway: fix linting errors

i3/sway: update nix package flag naming to i3

i3/sway: add documentation, fix module.md and impl monitorFor

i3/sway: handle more workspace ipc events

i3/sway: fix review

i3/sway: fix crash due to newline breaking up an IPC message

i3/sway: handle broken messages by forwarding to the next magic sequence

i3/sway: break loop when buffer is empty

i3/sway: fix monitor focus & focused monitor signal not being emitted

i3/sway: use datastreams instead of qbytearrays for socket reading

i3/sway: fix lint issues

i3/sway: drop second socket connection, remove dispatch return value, recreate IPC connection on fatal error

i3/sway: handle run_command responses

i3/sway: remove reconnection on unknown event

i3/sway: fix formatting, lint & avoid writing to socket if connection is not open
2024-11-24 12:50:22 +01:00
Nydragon
84ce47b6d3
build: add justfile entry to lint only changed files 2024-11-24 12:50:10 +01:00
2571766d3b
all: fix clang 18 lints 2024-11-24 03:36:04 -08:00
e957e88ccb
ci: run lints and test compile on arch 2024-11-24 02:09:41 -08:00
9b409c0e38
ci: use qt6.7.2 from cached nixpkgs commit
No point in building qtdeclarative every ci run.
2024-11-23 05:39:27 -08:00
cb426973d7
ci: test compilation against supported qt version / compiler matrix 2024-11-23 05:20:51 -08:00
57a5d8e1ed
core/reloader: wrap QQuickItem root nodes in a floating window
Useful for testing
2024-11-22 20:18:04 -08:00
c21df95087
core/reloader: do not require ShellRoot 2024-11-22 19:40:39 -08:00
2996e40ff9
core/plugin: rename QuickshellPlugin to QsEnginePlugin
Fixes conflict with the autogenerated qml plugin of the same name.
2024-11-22 18:59:15 -08:00
afa1b6f88b
wayland/layershell: link to xdg-shell protocol codegen 2024-11-22 18:57:11 -08:00
a8901fde67
debug/lint: run lints on reload for visible windows 2024-11-22 17:55:45 -08:00
8d63006bba
widgets/wrapper: fix default child not being assigned initially 2024-11-22 17:35:02 -08:00
5b01ec032e
debug/linter: remove log messages
Printing qml items here breaks the qml engine in a way that made
transparent rectangles in the scene graph opaque.
2024-11-22 16:15:03 -08:00
f0aca2030e
core/proxywindow: notify for width/height changes after connect
Previously the content item would resize but w/h wouldn't be updated.
2024-11-22 15:39:39 -08:00
5301227ec1
service/tray: fix compile on qt versions older than 6.8 2024-11-22 15:35:21 -08:00
ec143d6119
dbus/properties: remove non bindable based dbus property impl 2024-11-21 19:54:07 -08:00
324fe9274d
all: remove unused dbus props and warnings for non-required ones 2024-11-21 19:45:45 -08:00
b43b4a06d0
service/tray: adopt bindable properties 2024-11-21 19:44:51 -08:00
0e9e593078
dbus/properties: allow removing to/from wire transforms
Useful when properties are only read/written in one direction.
2024-11-21 19:14:06 -08:00
ac50767873
service/tray!: refactor qml bindings to StatusNotifierItem
Breaking: Dropped SystemTrayMenuWatcher.
2024-11-21 05:10:54 -08:00
f53e6fb515
dbus/dbusmenu: use bindable dbus properties 2024-11-21 04:06:24 -08:00
ff55ac874b
service/upower: adopt bindable properties 2024-11-21 03:40:53 -08:00
d4deb11216
dbus/properties: support data transformation/validation before store 2024-11-21 03:28:33 -08:00
a13c9d91b5
service/notifications: adopt bindable properties 2024-11-20 22:26:51 -08:00
abb900b7ff
service/mpris!: do not provide fallback track information
See the [!TIP] messages for more information.
2024-11-20 19:58:57 -08:00
e2ef7b7982
service/mpris: add isPlaying 2024-11-20 19:52:11 -08:00
db9e633197
service/mpris: adopt bindable properties 2024-11-20 19:31:40 -08:00
1955deee74
dbus/properties: add QObjectBindableProperty based dbus property
Many times more lightweight than the original QObject-based one.
2024-11-20 19:22:23 -08:00
4163713bc4
dbus/properties: decouple properties from AbstractDBusProperty
Importantly, this decouples properties from having to be QObjects,
allowing future property types to be much lighter.
2024-11-20 03:28:48 -08:00
dca75b7d6a
service/mpris: clarify trackinfo emit order and use QBindings 2024-11-20 00:52:47 -08:00
8450543e09
service/mpris!: convert trackArtists from list<string> to string
Most people treat it as a string already, which breaks in Qt 6.8,
and I have not seen a meaningful multi-artist response.
2024-11-19 18:28:19 -08:00
dbaaf55eb6
core/popupwindow: remove parentWindow deprecation message
Was being falsely triggered by lints.
2024-11-19 17:20:53 -08:00
eb5a5b8b67
debug: run lints after window expose
Ensures items are at their final sizes before checking them,
fixing some false positives.
2024-11-19 15:58:55 -08:00
6ceee06884
debug: add lint for zero sized items 2024-11-19 15:25:42 -08:00
66b494d760
build: add qs_add_link_dependencies
Further inspection as to what libraries actually require which others
will be required before this can be used as a hint for shared builds.
2024-11-19 13:58:34 -08:00
f4066cb4ed
core/popupanchor: add anchoring signal for last second repositioning 2024-11-19 03:29:31 -08:00
ee93306312
widgets/wrapper: fix margin wrapper reactvity and margins
Fixed reactivity of the paren't actual size not working before child
had been assigned.

Fixed incorrect margins when actual size is less than implicit size.
2024-11-19 02:57:04 -08:00
033e810871
widgets: add ClippingWrapperRectangle 2024-11-19 02:52:49 -08:00
401ee4cec6
widgets: add wrapper components and managers 2024-11-19 02:02:55 -08:00
79fca3cab8
docs: mention spirv-tools in BUILD.md 2024-11-17 21:38:56 -08:00
36174854ad
services/tray: fix const lint in item 2024-11-17 19:28:07 -08:00
fdc13023b7
widgets: add ClippingRectangle 2024-11-17 19:27:59 -08:00
68ba5005ce
core/icon: ability to specify a fallback or check if an icon exists 2024-11-17 14:46:34 -08:00
d2667369e1
core/qmlglobal: add shellRoot property 2024-11-17 01:49:27 -08:00
7db3772641
core/generation: short circuit findObjectGeneration if only one exists 2024-11-17 01:46:49 -08:00
29d31f5d3b
docs: add note that private qt headers are required for some libs 2024-11-17 01:36:25 -08:00
36d1dbeb69
service/tray: report misbehaving tray hosts
I've debugged broken tray items that just end up being a bad host far
too many times.
2024-11-17 01:30:54 -08:00
0445eee33a
io/process: support commands at file:// and root:// paths. 2024-11-17 00:47:22 -08:00
60dfa67ec7
io/fileview: support zero-sized files (/proc) 2024-11-14 17:54:16 -08:00
0dd19d4a18
core/proxywindow: remove blank frame when destroying window
Removes the blank frame caused by removing the content item from the
window. Fixes an issue with hyprland's window exit animations.
2024-11-12 04:35:42 -08:00
2c0e46cedb
core/lazyloader: fix incubator UAF in forceCompletion
The incubator was deleted via onIncubationCompleted before it was done
working when completed via forceCompletion().
2024-11-12 03:23:59 -08:00
74f371850d
launch: fix use after free of command options 2024-11-11 22:01:08 -08:00
b528be9426
all: fix gcc warnings 2024-11-05 13:31:24 -08:00
92252c36a3
build: fix gcc 2024-11-05 12:14:45 -08:00
7ffce72b31
all: optimize build 2024-11-05 04:15:17 -08:00
1168879d6d
build: only install necessary qml module files 2024-11-04 14:13:37 -08:00
2e18340995
build: allow specifying QML install dir 2024-11-04 13:42:21 -08:00
cdeec6ee83
all: use fully qualified type names in signals and invokables
Further fixes qmllint/qmlls
2024-11-01 21:10:21 -07:00
98cdb87181
all: use UntypedObjectModel instead of ObjectModel in Q_PROPERTY
Fixes qmllint/qmlls type deduction for ObjectModels
2024-11-01 03:12:07 -07:00
746b0e70d7
all: use fully qualified type names in Q_PROPERTY
Fixes type deduction issues with qmllint/qmlls.
2024-11-01 01:43:22 -07:00
a931adf033
all: add DEPENDENCIES entries to qml modules
Fixes some qmlls/qmllint issues.
2024-10-31 14:05:02 -07:00
9980f8587e
window: generate qmltypes 2024-10-31 14:04:58 -07:00
4e48c6eefb
all: refactor windows code out of core
There are still some links from core to window but its now separate
enough to fix PanelWindow in qml tooling.
2024-10-28 16:18:41 -07:00
1adad9e822
build: avoid creating qs symlink in privileged directory 2024-10-18 14:57:13 -07:00
4c2d7a7e41
crash: print warning messages for run/buildtime Qt version mismatch 2024-10-17 14:58:45 -07:00
89d04f34a5
build: find waylandscanner and qtwaylandscanner from imported target
Removes the QTWAYLANDSCANNER env hack.
2024-10-16 00:08:17 -07:00
23f59ec4c3
crash: add build configuration and distributor information
Also adds distributor to --version and
build configuration to --version --verbose
2024-10-16 00:08:15 -07:00
8e40112d14
service/pipewire: ignore metadata updates with null keys
Fixes #6
2024-10-06 00:57:19 -07:00
3ed39b2a79
service/pipewire: fix metadata permission checks 2024-09-26 15:52:31 -07:00
fbaec141c0
service/pipewire: improve documentation 2024-09-24 01:59:38 -07:00
fdc78ae16f
service/pipewire: add a way to set preferred default nodes 2024-09-24 01:59:01 -07:00
f889f08901
service/pipewire: refactor defaults and metadata handling 2024-09-23 23:53:54 -07:00
7f9762be53
service/pipewire: disconnect link tracker from registry on node destroy
Caused duplicate entries to be created due to double connection, which
then caused a crash.
2024-09-17 23:44:41 -07:00
931aca5392
service/pipewire: don't use configured default devices
These don't appear to be intended for use by applications, only the
non configured ones.

This fixes the default device being unset on many computers and the
device being lost on actions like headphone unplug which replace it.
2024-09-17 23:04:06 -07:00
bd8978375b
core/icon: allow changing the icon theme 2024-09-17 14:21:34 -07:00
7a283089b1
core/command: rename --instance to --id and --info to --show
Fixes conflicting short flags.
2024-09-17 14:04:54 -07:00
c57ac4b1f2
core/menu: disconnect menu before unref when changed 2024-09-15 16:06:20 -07:00
08966f91c5
service/tray: always mark the root menu item as having children
Blueman doesn't for some reason. This causes
PlatformMenuEntry::display to crash after ::relayout created a QAction
instead of a QMenu.

Fixes #5
2024-09-15 15:57:29 -07:00
84e3f04f3c
service/tray: disconnect menu from handle on deletion
Fixes loaded being set to true after deleting the menu.
2024-09-15 15:32:01 -07:00
bdc9fe958b
service/tray: delete image pixmaps created with new[] using delete[] 2024-09-15 13:50:00 -07:00
01f2be057e
widgets/iconimage: add typegen hints to alias properties 2024-09-15 02:23:46 -07:00
abe0327e67
widgets: add IconImage widget
Docs currently cannot be generated due to lack of qml parsing support
in typegen.
2024-09-14 03:10:44 -07:00
accdc59a1c
wayland/all: scale layers and popup anchors correctly
Layers now scale window size and exclusive zone to native
pixels. Popup anchors do the same.
2024-09-14 01:31:39 -07:00
293341c9e1
core/reloader: ensure generation ptrs are removed on destroy
Broke things that used currentGeneration, and we shouldn't have a list
of dangling pointers anyway.
2024-09-13 04:18:52 -07:00
5e2fb14551
io/ipchandler: add IpcHandler and qs msg
Also reworks the whole ipc system to use serialized variants.
2024-09-13 02:50:42 -07:00
3690812919
core/log: fix encoding 29 second deltas (again)
Forgot the second if statement and didn't actually fix the bug last time.
2024-09-10 16:30:50 -07:00
01f6331cb7
core/command: add --daemonize 2024-09-10 15:53:16 -07:00
9d21a01153
core/command: add --no-duplicate 2024-09-10 14:35:30 -07:00
47ec85ffef
core/command: make log --file positional
Also frees up -f for --follow.
2024-09-10 04:55:44 -07:00
01deefe241
core/log: encode category log levels 2024-09-10 04:48:54 -07:00
a82fbf40c2
core/command: add log --follow 2024-09-10 03:31:49 -07:00
c78381f6d0
core/command: add --tail to log subcommand 2024-09-10 01:02:43 -07:00
f810c63ffc
core/command: allow log files to be specified w/ instance selectors 2024-09-10 00:32:39 -07:00
19d74595d6
core/window: premultiply background colors
Apparently these are supposed to be premultiplied. Some docs would be nice.
2024-09-10 00:01:17 -07:00
2c485e415d
nix: update lockfile to avoid mesa mismatches 2024-09-09 03:27:58 -07:00
8cdb41317f
nix: modernize cmake options 2024-09-09 03:23:27 -07:00
85be3861ce
io/fileview: add FileView 2024-09-09 03:15:16 -07:00
3a1eec0ed5
core/log: fix sparse logs being on by default 2024-09-05 21:44:05 -07:00
465d5402f2
crash: fix off-end read when copying environ array 2024-09-02 22:19:36 -07:00
397476244c
x11/panelwindow: add option to disable Xinerama aware struts
Breaks bad WMs less.
2024-09-01 19:00:13 -07:00
6cb7d894ab
x11/panelwindow: fix multi monitor struts 2024-09-01 18:26:54 -07:00
95245cb6a5
x11/panelwindow: fix strut start/end, patch around awesome, resize all panels 2024-09-01 17:32:47 -07:00
94e881e6b0
core!: refactor launch sequence
Also includes slight changes to the command syntax.
See --help for details.
2024-09-01 14:17:39 -07:00
da043e092a
core/ipc: add ipc server/client
Currently can only kill a remote instance.
2024-08-30 21:45:20 -07:00
13b6eeaa22
core/reloader: null generation ref in reloadables on destruction
On the post-reload reloadable initialzation path, a timer is used to
delay reload(). This change fixes a UAF when switching generations
while that timer is running.
2024-08-30 16:29:59 -07:00
3edb3f4efa
core/reloader: disconnect old generation before reloading
Previously the old generation was not disconnected, causing the root
wrapper's generation pointer to be nulled when pointing to the new
generation, leaving multiple shell versions running simultaneously.
2024-08-30 16:29:48 -07:00
60349f1894
core: set application name to avoid bin name fallback 2024-08-29 14:43:25 -07:00
77c5a2d569
build: add "qs" as a symlink to the "quickshell" binary 2024-08-29 14:11:40 -07:00
f6ad617b67
service/pipewire: ignore insignificant device volume changes
Fixes devices getting stuck in a "waiting for volume change
acknowledgement" state forever.
2024-08-29 13:21:07 -07:00
a116f39c63
core/desktopentry: prioritize fallback keys over mismatched keys
The fallback key will now be selected when there isn't a more specific
key to select, instead of the first key.
2024-08-28 22:05:21 -07:00
af29bc277e
core: add by-pid symlinks to instance runtime paths 2024-08-28 17:53:39 -07:00
9967e2e03b
core: fix UAF when calling Qt.quit or Qt.exit
A pointer to the last generation had shutdown() called on it after deletion.
2024-08-28 15:30:02 -07:00
e327d6750d
build: fix -DCRASH_REPORTER=OFF 2024-08-28 11:32:14 -07:00
79b22af093
service/pipewire: avoid overloading devices with volume changes
Wait until in-flight changes have been responded to before sending more.
2024-08-27 20:34:11 -07:00
c60871a7fb
service/pipewire: set device node volumes with device object
Fixes discrepancies between pulse and qs volumes, and volumes not
persisting across reboot or vt switches.
2024-08-27 01:34:34 -07:00
Nydragon
b40d4147e0
build: add opt-in installation of QML lib
Override the package with `withQMLLib = true;` to enable lib
installation, alternatively add `-DINSTALL_QML_LIB=ON` to your cmake
build command.

Co-authored-by: a-usr <81042605+a-usr@users.noreply.github.com>
2024-08-25 22:59:41 +02:00
f95e7dbaf6
hyprland/focus_grab: wait for surface creation if null
Fixes an occasional crash with QWaylandWindow::surface() returning null.
2024-08-20 16:41:04 -07:00
fe1d15e8f6
crash: add crash reporter 2024-08-20 00:55:07 -07:00
5040f3796c
core/reloader: delay post-reload reload hooks
Ensures onReload runs after Component.onCompleted.
2024-08-18 19:54:36 -07:00
5a038f085d
service/mpris: support trackids in object path form
Chromium reports trackids as object paths, which caused us to fall
back to Seek, which is also entirely broken on chromium.
2024-08-18 13:41:16 -07:00
e223408143
service/mpris: fix display position when paused 2024-08-18 13:07:52 -07:00
f89c504b55
core/menu: opening platform menus w/o QApplication no longer crashes
An error is displayed instead.
2024-08-16 16:47:50 -07:00
1d2bf5d7b4
core/clock: fix behavior with odd time changes 2024-08-16 02:35:03 -07:00
815867c178
x11/panelwindow: fix multi monitor
Previously attached panels to the virtual desktop geometry instead of
the screen geometry.
2024-08-15 18:46:18 -07:00
22c397bbb0
x11/panelwindow: respect exclusive zones per layer 2024-08-15 17:15:30 -07:00
23cd6cd9e1
x11/panelwindow: set _NET_WM_DESKTOP to stay on all desktops 2024-08-15 17:14:00 -07:00
683d92a05f
core/command: add --version 2024-08-10 01:59:40 -07:00
14852700cb
core/log: ensure malformed logs cannot overflow ring buffer 2024-08-10 01:40:51 -07:00
5f4d7f89db
core/log: fix log corruption with messages at 29 second deltas
29, or 0x1d is used as a marker to mean the log level and time delta
cannot fit in a single byte, and the time delta will be a varint
following the current byte.

Prior to this commit, 29 second deltas would be written as 0x1d
instead of 0x1d1d, which the parser interpreted as a hint to read the
next byte, causing the parser to become offset by one byte and all
following logs to be potentially corrupt.
2024-08-10 01:35:52 -07:00
53b8f1ee0b
core/log: add read-log --no-time 2024-08-09 23:58:30 -07:00
c2b4610acb
core/log: add read-log --filter 2024-08-09 23:45:46 -07:00
0fc98652a8
core/log: create fully detailed logs by default
The .qslog logs now log messages for quickshell* by default.
2024-08-09 20:24:17 -07:00
291179ede2
core/command: rewrite command parser with CLI11 2024-08-09 19:25:18 -07:00
bdbf5b9af9
core/log: add custom log encoder for smaller log storage
Will be used to store more detailed logs in the future without using
as much disk space.
2024-08-09 14:43:18 -07:00
8364e94d26
core/log: capture early logs in fs logger 2024-08-07 15:53:11 -07:00
7c7326ec52
core/log: add timestamps to log files 2024-08-07 13:40:37 -07:00
38ba3fff24
core/popupanchor: pick flip direction based on available width 2024-08-06 22:24:31 -07:00
6bf4826ae7
core/log: add filesystem logger 2024-08-02 21:37:52 -07:00
46f48f2f87
core/log: add fancy logger 2024-08-02 18:52:05 -07:00
533b389742
nix: build with split debuginfo in release mode 2024-08-02 13:56:30 -07:00
d582bb7b57
core: add per-config shell id
Will be useful for future functionality such as IPC and caching.
2024-08-02 02:09:55 -07:00
79b2fea52e
core/util: fix MemberMetadata compile on gcc 2024-08-02 01:32:12 -07:00
2c87cc3803
core: stop using the simple animation driver by default 2024-08-01 21:47:18 -07:00
cb2862eca9
wayland/toplevel_management: add ToplevelManager.activeToplevel 2024-07-31 23:10:08 -07:00
9555b201fe
core/clock: fix instability causing timer to fire multiple times
If the signal was fired slightly before the scheduled time, it would
schedule itself again a couple ms in the future.
2024-07-31 23:09:39 -07:00
a4903eaefc
core/clock: fix breakage at midnight
The difference between 23:59 and 00:00 is -23:59, not 00:01.
2024-07-31 01:51:53 -07:00
76744c903a
core/clock: add SystemClock 2024-07-30 23:24:54 -07:00
ba1e535f9c
core/util: add experimental member macros
An experiment that should reduce boilerplate for properties that just
access a backing value. Code also exists for using it as an interface
for other properties as well, but isn't currently in use.
2024-07-30 20:23:57 -07:00
8873a06962
service/notifications: use DROP_EMIT_SET for notification properties 2024-07-30 12:20:39 -07:00
3a8e67e8ab
core/util: move DropEmitter to utils and add generic accessor macros 2024-07-30 12:19:59 -07:00
abc0201f6e
service/mpris: add uniqueId property to detect track changes
Also emits all property changes after trackChanged
2024-07-29 01:34:44 -07:00
d9f66e63a3
service/upower!: divide percentage by 100
Brings range back to the expected 0-1 instead of 0-100.
2024-07-28 20:28:45 -07:00
18563b1273
wayland/popupanchor: fix anchor state breaking show after reposition
If the popup was hidden and reposition was called to update qt's
initial positioning properties it would be cancelled by the dirty
marker being unset. This includes if the popup is shown or not into
its dirty state.
2024-07-27 02:28:21 -07:00
4b2e569e94
core/types: allow implicit conversion from point to box 2024-07-26 10:06:56 -07:00
58c3718287
core/types: add implicit coversion from rect to box 2024-07-26 00:55:42 -07:00
6b9b1fcb53
core/menu: add QsMenuAnchor for more control of platform menus 2024-07-25 20:44:26 -07:00
54350277be
core/menu: add handle support to QsMenuOpener + add handle to tray 2024-07-25 02:51:17 -07:00
acdbe73c10
dbus/dbusmenu: separate menu handles from status notifier items
No api changes yet.
2024-07-25 01:32:05 -07:00
a71a6fb3ac
core/popupanchor: fix flip with opposite anchors and gravity
Flips into the anchor rect instead of over it when anchors and gravity
are opposite.
2024-07-24 01:36:51 -07:00
60388f10ca
core/popupanchor: reposition on popup size change 2024-07-24 00:44:42 -07:00
ebfa8ec448
core/popupanchor: rework popup anchoring and add PopupAnchor 2024-07-23 22:12:27 -07:00
14910b1b60
docs: mention member reference syntax in CONTRIBUTING 2024-07-21 17:44:09 -07:00
a9e4720fae
docs: use new member reference shorthand 2024-07-21 17:41:49 -07:00
dfcf533424
core/window!: rename QSWindow to QsWindow 2024-07-21 16:15:11 -07:00
aa3f7daea2
wayland/platformmenu: fix flipped positions and submenu y positions 2024-07-19 02:55:38 -07:00
6367b56f55
core/window: fix attached property prior to backer creation 2024-07-18 01:57:40 -07:00
e48af44607
core/window: add QsWindow attached object to contained Items 2024-07-17 20:54:29 -07:00
d1c33d48cd
docs: explain type reference shorthand in CONTRIBUTING 2024-07-14 16:22:01 -07:00
e9cacbd92d
all: use type/prop shorthand in docs 2024-07-14 16:17:51 -07:00
c4cc662bcc
core/objectmodel: fix objectInserted signal indexes 2024-07-12 22:52:40 -07:00
e23923d9a2
service/notifications: make notifications Retainable 2024-07-12 21:25:46 -07:00
609834d8f2
core/retainable: add Retainable and RetainableLock 2024-07-12 21:21:35 -07:00
7c5632ef5f
service/upower: start upower dbus service if inactive 2024-07-12 20:16:10 -07:00
d630cc7f76
service/notifications: add notifications service 2024-07-12 00:50:00 -07:00
79cbfba48a
wayland/layershell: add warning that exclusive focus is not a lock
Apparently this needed to be said.
2024-07-11 22:32:21 -07:00
c758421af6
core/reloader: fix Reloadable::onReload being called multiple times
onReload was called multiple times due to Reloadable::reloadRecursive
calling onReload instead of reload, which didn't set reloadComplete.
This allowed the componentComplete fallback to call reload later.
2024-07-11 01:43:54 -07:00
49b309247d
all: fix formatting 2024-07-11 00:16:44 -07:00
bb33c9a0c4
core/global: add Quickshell.iconPath
Replaces "image://icon/" in user facing code.
2024-07-11 00:09:34 -07:00
Ben
24f54f579f service/upower: add upower service 2024-07-10 14:21:34 -04:00
497c9c4e50
core/window: ensure items are polished before setting window visible
Hacks around a bug in layouts that commonly results in popups being
wrongly sized for at least a frame.
2024-07-10 03:44:55 -07:00
db23c0264a
core/desktopentry: paper over id casing issues 2024-07-08 15:37:49 -07:00
fdbb490537
service/tray: fix crash when display is called on a menuless item 2024-07-02 10:52:11 -07:00
b4be383695
service/tray: log menu refcount updates 2024-07-02 10:50:07 -07:00
ec362637b8
service/tray!: redesign menus / dbusmenu and add native menu support
Reworks dbusmenu menus to be displayable with a system context menu.

Breaks the entire DBusMenu api.
2024-07-01 20:50:30 -07:00
c31bbea837
docs: add breaking change notice 2024-07-01 20:50:07 -07:00
d8b900ed0b
lint: allow implicit bool conversions 2024-06-28 01:05:59 -07:00
8547d12396
service/pipewire: make binding warnings in docs more obvious 2024-06-27 20:45:27 -07:00
d7149d5641
core/objectrepeater: soft-remove in favor of Instantiator
RIP my time.
2024-06-23 14:05:34 -07:00
c78c86425d
core/objectrepeater: delete delegate instances after removal 2024-06-23 03:18:27 -07:00
09d8a7a07d
core/objectrepeater: add ObjectRepeater 2024-06-22 01:57:48 -07:00
d8fa9e7bb3
service/mpris: add properties for common track metadata
This was done to work around bad player implementations sending weird
metadata, such as removing the art url halfway through a song.
2024-06-21 19:03:40 -07:00
c56a3ec966
service/mpris: add shorthand for playback state changes 2024-06-21 16:31:02 -07:00
b6612bd56c
core/panelwindow: remove QSDOC_HIDE for above and focusable props 2024-06-21 10:11:57 -07:00
3573663ab6
service/greetd: add greetd service 2024-06-20 15:39:49 -07:00
72956185bd
wayland/lock: only update surfaces on screens changed if locked
Fixes crash when a WlSessionLock object exists but is unlocked and a
screen is added or removed.
2024-06-19 11:16:51 -07:00
59cf60d83e
service/pam: add responseVisible
Fixes misunderstanding of "echo".
2024-06-19 00:31:09 -07:00
6efa05a8eb
core: run full destruction sequence before exiting
Fixes QTimer messages.
2024-06-18 20:58:33 -07:00
3033cba52d
all: fix failing lints 2024-06-18 20:46:58 -07:00
8ec245ac66
wayland/lock: initialize lock content before starting lock
Reduces any chances of the compositor displaying a blank frame first.
2024-06-18 20:34:16 -07:00
71a65c4d3c
docs: mention Fedora COPR package 2024-06-18 17:57:20 -07:00
9e58077c61
core: fix shutdown sequence crashing 2024-06-18 17:03:38 -07:00
3991726b9b
docs: document PAM feature in build instructions 2024-06-18 15:25:10 -07:00
ae762f5c6e
hyprland/ipc: ensure requests are flushed 2024-06-18 12:26:23 -07:00
e89035b18c
service/pam: move pam execution to subprocess to allow killing it
Many pam modules can't be aborted well without this.
2024-06-18 03:29:25 -07:00
b5c8774a79
service/pam: send completed messages after destroying pam conv
Allows context to be restarted in a complete handler.
2024-06-17 20:30:23 -07:00
7e5d128a91
service/pam: add pam service 2024-06-17 18:32:13 -07:00
f655875547
core/desktopentry: add limited desktop entry api 2024-06-16 01:58:24 -07:00
ce5ddbf8ba
core: add $XDG_DATA_DIRS/pixmaps to QIcon fallback path
Picks up some missing app icons.
2024-06-14 19:18:43 -07:00
d8b72b4c31
wayland/lock: notify on screen change 2024-06-13 16:25:07 -07:00
523de78796
wayland/layershell: ensure state changes are comitted without render
Previously they were not comitted and did not apply until the next
rendered frame.
2024-06-13 16:23:28 -07:00
67783ec24c
core/transformwatcher: fix crash when a or b is destroyed
Usually happens during reload.
2024-06-09 15:42:38 -07:00
b5b9c1f6c3
wayland/toplevel_management: add foreign toplevel management 2024-06-07 04:31:20 -07:00
5d1def3e49
hyprland/ipc: fix monitorFor returning null during HyprlandIpc init 2024-06-06 00:59:17 -07:00
bc349998df
hyprland/ipc: match by name in refreshMonitors instead of id
Was causing ghost/duplicate monitors from usages where the id was not known.
2024-06-06 00:58:10 -07:00
ef1a4134f0
hyprland/ipc: re-request monitors and workspaces on fail 2024-06-06 00:46:38 -07:00
d14ca70984
hyprland/ipc: add hyprland ipc
Only monitors and workspaces are fully tracked for now.
2024-06-05 19:26:20 -07:00
be237b6ab5
core/elapsedtimer: add ElapsedTimer 2024-06-04 13:48:54 -07:00
37fecfc990
docs: add commit style instructions 2024-06-03 00:38:22 -07:00
b1f5a5eb94
service/mpris: preserve mpris watcher and players across reload 2024-06-02 16:18:45 -07:00
9d5dd402b9
docs: recommend packagers add a dependency on qtsvg 2024-06-02 15:37:47 -07:00
29f02d837d
all: remove NVIDIA workarounds
They fixed the driver.
2024-06-02 15:36:33 -07:00
7d20b472dd
misc: remove the docs and examples submodules
They have not been correctly updated in lock-step for a while now.
2024-06-02 15:23:19 -07:00
bd504daf56
docs: add build, packaging and development instructions 2024-06-02 14:50:23 -07:00
238ca8cf0b
core/reloader: fix crashing on failed reload 2024-05-31 04:03:00 -07:00
a8506edbb9
build: link jemalloc by default to reduce heap fragmentation
The QML engine and the quickshell reloader both cause large amounts of
heap fragmentation that stacks up over time, leading to a perceived
memory leak. Jemalloc is able to handle the fragmentation much better,
leading to lower user facing memory usage.
2024-05-31 01:28:35 -07:00
d56c07ceb3
core/reloader: simplify generation teardown
The extra complexity previously masked the use after free in 6c95267.
2024-05-31 00:27:18 -07:00
84bb4098ad
core/reloader: fix incorrect generation teardown on hard reload 2024-05-31 00:26:34 -07:00
6c9526761c
wayland: fix UAF in layershell surface destructor 2024-05-31 00:24:58 -07:00
7feae55ebe
core/reloader: add reload signals for visual notifications 2024-05-30 02:39:37 -07:00
569c40494d
all: import module dependencies via qmldir
Improves compatibility with qml tooling.
2024-05-29 19:29:57 -07:00
0519acf1d6
core: support root: and root:/ paths for the config root
This works everywhere urls are accepted and rewrites them from the
config root as a qsintercept url.
2024-05-29 15:07:10 -07:00
33fac67798
core: use the simple animation driver
Seems to provide much higher quality animations.
2024-05-28 20:22:01 -07:00
7ad3671dd1
core/reloader: fix file watcher compatibility with vim 2024-05-28 15:36:25 -07:00
4e92d82992
core: add options to enable QML debugging 2024-05-27 22:51:49 -07:00
5a84e73442
core/objectmodel: add signals for changes to the list 2024-05-23 19:16:08 -07:00
06240ccf80
service/mpris: improve compatibility with noncompliant players 2024-05-23 18:15:49 -07:00
5016dbf0d4
all: replace list properties with ObjectModels 2024-05-23 17:28:07 -07:00
6326f60ce2
service/mpris: re-query position on playback and metadata change 2024-05-23 02:38:26 -07:00
ac339cb23b
service/mpris: expose desktopEntry property 2024-05-22 05:40:03 -07:00
f2df3da596
service/mpris: fix position being incorrect after pausing 2024-05-22 04:34:56 -07:00
ed3708f5cb
service/mpris: add trackChanged signal 2024-05-21 05:07:24 -07:00
af45502913
service/mpris: add mpris module 2024-05-21 04:10:30 -07:00
4ee9ac7f7c
service/mpris: finish mpris implementation 2024-05-21 04:09:19 -07:00
3b6d1c3bd8
feat: mpris 2024-05-21 04:09:19 -07:00
73cfeba61b
x11: add XPanelWindow 2024-05-20 02:16:44 -07:00
908ba3eef5
hyprland/global_shortcuts: fix crash when protocol is not present 2024-05-19 02:50:14 -07:00
3e80c4a4fd
service/pipewire: add pipewire module 2024-05-19 02:29:21 -07:00
bba8cb8a7d
hyprland/global_shortcuts: add GlobalShortcut 2024-05-06 22:19:50 -07:00
87a884ca36
hyprland/focus_grab: add HyprlandFocusGrab 2024-05-06 00:02:51 -07:00
e7cfb5cf37
service/tray: move menu access to SystemTrayMenuWatcher 2024-05-01 02:55:23 -07:00
3c0456a3c0
core/boundcomponent: add BoundComponent 2024-05-01 02:14:32 -07:00
d64bf59bb0
core/intercept: do not rewrite paths when url ends in .qml
Fixes component baseUrls becoming file:// paths which cannot access singletons.
2024-04-30 23:11:54 -07:00
53d69fd2c0
docs: bump docs submodule 2024-04-30 17:03:39 -07:00
4db28fe725
core/lazyloader: add activeAsync property 2024-04-30 17:01:09 -07:00
658f3cf411
docs: add API documentation for SystemTray and DBusMenu 2024-04-30 01:27:06 -07:00
61061644a5
dbus/dbusmenu: add DBusMenu support 2024-04-29 23:57:26 -07:00
7cc1b54587
service/tray: rework tray image providers 2024-04-29 22:28:09 -07:00
aa9f8cd001
dbus/properties: dbus/dbusutil -> dbus/properties 2024-04-29 18:37:57 -07:00
74d1bb9bc2
dbus/properties: fix QDBusPendingCallWatcher leak 2024-04-29 18:32:53 -07:00
a1d82729bc
service/tray: log failures when calling Activate or SecondaryActivate 2024-04-29 18:31:35 -07:00
c71fdd62d0
service/tray: log icon render failures due to IconThemePath 2024-04-29 18:16:04 -07:00
1f49c55711
wayland/lock: fix post-reload lockscreen creation
This broke due to the changes that ensure onReload always runs.
2024-04-25 14:18:25 -07:00
ce4e697667
service/tray: use bilinear scaling for tray icons
Apparently some programs think it is a good idea to send 1000x+ images.
2024-04-21 00:16:22 -07:00
c6e5a35745
core/reloader: fix more crashes (not all of them) 2024-04-20 02:59:50 -07:00
31462b9797
core/reloader: fix UAF of old generation during scene destroy 2024-04-20 00:36:25 -07:00
97bcdbecc1
service/tray: add activate, secondaryActivate and scroll methods 2024-04-19 22:03:06 -07:00
94a1140aab
core/popup: fix popup never becoming visible when lazy loaded 2024-04-19 16:14:59 -07:00
6eb68d2cd7
core/reloader: fix late creation of Reloadable types 2024-04-19 15:45:07 -07:00
61812343f5
service/tray: account for more edge cases and add placeholder img 2024-04-19 04:21:21 -07:00
54bf485101
nix: add qtsvg dependency by default 2024-04-19 02:46:38 -07:00
23d0c2e01d
io/socket: add flush() 2024-04-18 04:13:20 -07:00
a06af243ad
core/transformwatcher: add TransformWatcher 2024-04-17 04:31:02 -07:00
fd5b73adbb
wayland: fix Qt 6.7 compatibility 2024-04-12 01:21:26 -07:00
98318c4dcb
nix: fix git revision in nix package 2024-04-08 00:35:49 -07:00
0b2baea230
core: add IgnoreSystemSettings pragma 2024-04-07 23:21:06 -07:00
ff8e252944
core: fix build warnings 2024-04-07 23:12:19 -07:00
082c3c480f
core: add pragmas
UseQApplication: use QApplication over QGuiApplication (for qqc2-desktop-style)
NativeTextRendering: use NativeTextRendering over QtRendering for text
Env VAR = VAL: define environment variables (usually qt ones)
2024-04-07 22:35:23 -07:00
c0847366dd
core/window: fix reloads breaking for indirect window children 2024-04-07 14:17:57 -07:00
6214ac1002
service/tray: mostly complete StatusNotifierItem implementation
Notably missing dbusmenu which makes it actually useful.
2024-04-06 02:19:40 -07:00
d47a7f2cff
core/icon: add icon image provider 2024-04-05 17:54:51 -07:00
8e530b6b77
dbus: create property helper classes
Handles asynchronous property updates, part of the work for StatusNotifierItems.
2024-04-04 22:48:58 -07:00
8529a2eb22
readme: add matrix link 2024-04-01 22:46:08 -07:00
54b3d338dc
core/process: fix stdinEnabled documentation 2024-03-31 01:05:09 -07:00
83afce7f68
core/screen: expose x and y positions 2024-03-29 05:41:39 -07:00
439788fce0
core/popup: force window destruction to avoid crashes 2024-03-28 02:29:29 -07:00
c3fe93efe6
core/window: fix backingWindowVisible always reporting true 2024-03-27 02:52:56 -07:00
3026d3400a
all/window: use global screeninfo pool for screen getter 2024-03-27 02:43:14 -07:00
9cbd5abd96
core/window: ensure window cannot be made visible during reload
This causes duplicate windows.
2024-03-27 02:38:54 -07:00
9625129844
core: replace throws with qFatal 2024-03-27 02:13:47 -07:00
055b191a67
core/window: add windowTransform and backingWindowVisible properties 2024-03-27 01:37:45 -07:00
3a0381dcbe
core/window: backing windows can now be destroyed and recreated
This fixes a crash in layershells and the setVisible crash on nvidia.
2024-03-27 00:45:53 -07:00
b6dc6967a1
core/generation: fix incuabation controller use after free
qobject_casts were failing causing old controllers to never be removed
from the list.
2024-03-25 02:02:46 -07:00
c6bf826031
core/reloader: fix Quickshell.reload not working
Has been broken since engine generations were introduced.
2024-03-25 01:57:15 -07:00
4eb5dc5593
core/variants: expose instances list as a property 2024-03-21 05:32:55 -07:00
f09f591e6a
core/region: improve child handling
- Children are no longer reparented
- `regions` is now a full list property
2024-03-21 05:26:04 -07:00
8e25c1cee0
core/easingcurve: add EasingCurve type 2024-03-21 02:54:21 -07:00
dd811ac423
core/window: fix mask reactivity
Masks previously would not update if the item was changed, and full
transparency was decided incorrectly.
2024-03-20 22:32:34 -07:00
31264ac7d1
core/singleton: fix PostReloadHook in singletons 2024-03-20 01:37:30 -07:00
9f38908bdf
core/intercept: do not intercept non qml files
Avoids forcing Images to lazy load which causes unexpected flashes.
2024-03-19 21:30:46 -07:00
518977932d
core/lazyloader: add LazyLoader
Also fixes qml incubation in general, which was completely broken,
meaning the native qml Loader type should also work now.
2024-03-19 05:35:44 -07:00
8d742e315e
core/window: fix floating window reloading recreating the window 2024-03-16 02:49:41 -07:00
300c0d97fb
core/window: fix white flash before window content is set 2024-03-15 04:01:10 -07:00
5731af562b
misc: update submodules 2024-03-14 05:01:56 -07:00
41803ee235
core/window: fix windows not taking screen assignments during reload 2024-03-14 04:48:27 -07:00
48156a55b3
core/variants: restructure Variants to match the design of Repeater 2024-03-14 04:46:44 -07:00
ffbdac9977
core: synthesized qmldir files and new qml scanning strategy 2024-03-14 00:16:22 -07:00
1687ff3614
reload: encapsulate each engine generation more 2024-03-13 22:53:05 -07:00
211f454de9
singleton: add reloadable Singleton type 2024-03-13 00:57:03 -07:00
463f9a297f
root: recreate the qml engine on reload instead of clearing it
This causes singletons to be recreated instead of kept alive.
2024-03-12 14:55:51 -07:00
9f6ef37f61
build: improve parallelism by removing core dependency on modules 2024-03-12 00:04:20 -07:00
c44041653c
build: greatly speed up build times using pch 2024-03-11 18:18:55 -07:00
3480707e99
wayland: namespace type names a bit to prevent future conflicts 2024-03-11 06:21:23 -07:00
1e647cee51
readme: mention hosted docs 2024-03-11 06:01:51 -07:00
b675b3676c
popups: add popup windows 2024-03-11 05:44:56 -07:00
8cf0659444
window: fix empty masks not applying 2024-03-10 04:08:42 -07:00
7a15495e3f
layershell: fix protocol error on popup attachment 2024-03-10 03:02:40 -07:00
5f9bb9b46c
window: changing screen now recreates the window on the new screen 2024-03-09 05:06:49 -08:00
31365dd179
misc: make the last window closing not quit by default 2024-03-09 03:23:58 -08:00
3789709820
screens: make screen list changes not recreate QuickshellScreenInfos
Fixes Variants recreating windows on existing screens and causing flickering.
2024-03-09 02:39:15 -08:00
15cd78e30c
screens: add qDebug<< impl to QuickshellScreenInfo 2024-03-09 02:35:48 -08:00
fc93591cab
variants: fix onReload not being called after variant updates 2024-03-09 02:35:07 -08:00
465 changed files with 48473 additions and 2543 deletions

View file

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

View file

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

1
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View file

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

82
.github/ISSUE_TEMPLATE/crash.yml vendored Normal file
View file

@ -0,0 +1,82 @@
name: Crash Report
description: Quickshell has crashed
labels: ["bug", "crash"]
body:
- type: textarea
id: crashinfo
attributes:
label: General crash information
description: |
Paste the contents of the `info.txt` file in your crash folder here.
value: "<details> <summary>General information</summary>
```
<Paste the contents of the file here inside of the triple backticks>
```
</details>"
validations:
required: true
- type: textarea
id: userinfo
attributes:
label: What caused the crash
description: |
Any information likely to help debug the crash. What were you doing when the crash occurred,
what changes did you make, can you get it to happen again?
- type: textarea
id: dump
attributes:
label: Minidump
description: |
Attach `minidump.dmp.log` here. If it is too big to upload, compress it.
You may skip this step if quickshell crashed while processing a password
or other sensitive information. If you skipped it write why instead.
validations:
required: true
- type: textarea
id: logs
attributes:
label: Log file
description: |
Attach `log.qslog.log` here. If it is too big to upload, compress it.
You can preview the log if you'd like using `quickshell read-log <path-to-log>`.
validations:
required: true
- type: textarea
id: config
attributes:
label: Configuration
description: |
Attach your configuration here, preferrably in full (not just one file).
Compress it into a zip, tar, etc.
This will help us reproduce the crash ourselves.
- type: textarea
id: bt
attributes:
label: Backtrace
description: |
If you have gdb installed and use systemd, or otherwise know how to get a backtrace,
we would appreciate one. (You may have gdb installed without knowing it)
1. Run `coredumpctl debug <pid>` where `pid` is the number shown after "Crashed process ID"
in the crash reporter.
2. Once it loads, type `bt -full` (then enter)
3. Copy the output and attach it as a file or in a spoiler.
- type: textarea
id: exe
attributes:
label: Executable
description: |
If the crash folder contains a executable.txt file, upload it here. If not you can ignore this field.
If it is too big to upload, compress it.
Note: executable.txt is the quickshell binary. It has a .txt extension due to github's limitations on
filetypes.

56
.github/workflows/build.yml vendored Normal file
View file

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

25
.github/workflows/lint.yml vendored Normal file
View file

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

4
.gitignore vendored
View file

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

6
.gitmodules vendored
View file

@ -1,6 +0,0 @@
[submodule "docs"]
path = docs
url = https://git.outfoxxed.me/outfoxxed/quickshell-docs
[submodule "examples"]
path = examples
url = https://git.outfoxxed.me/outfoxxed/quickshell-examples

251
BUILD.md Normal file
View file

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

View file

@ -1,35 +1,93 @@
cmake_minimum_required(VERSION 3.20)
project(quickshell VERSION "0.1.0")
project(quickshell VERSION "0.2.0" LANGUAGES CXX C)
set(QT_MIN_VERSION "6.6.0")
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
option(TESTS "Build tests" OFF)
set(QS_BUILD_OPTIONS "")
option(SOCKETS "Enable unix socket support" ON)
option(WAYLAND "Enable wayland support" ON)
option(WAYLAND_WLR_LAYERSHELL "Support the zwlr_layer_shell_v1 wayland protocol" ON)
option(WAYLAND_SESSION_LOCK "Support the ext_session_lock_v1 wayland protocol" ON)
function(boption VAR NAME DEFAULT)
cmake_parse_arguments(PARSE_ARGV 3 arg "" "REQUIRES" "")
option(${VAR} ${NAME} ${DEFAULT})
set(STATUS "${VAR}_status")
set(EFFECTIVE "${VAR}_effective")
set(${STATUS} ${${VAR}})
set(${EFFECTIVE} ${${VAR}})
if (${${VAR}} AND DEFINED arg_REQUIRES)
set(REQUIRED_EFFECTIVE "${arg_REQUIRES}_effective")
if (NOT ${${REQUIRED_EFFECTIVE}})
set(${STATUS} "OFF (Requires ${arg_REQUIRES})")
set(${EFFECTIVE} OFF)
endif()
endif()
set(${EFFECTIVE} "${${EFFECTIVE}}" PARENT_SCOPE)
message(STATUS " ${NAME}: ${${STATUS}}")
string(APPEND QS_BUILD_OPTIONS "\\n ${NAME}: ${${STATUS}}")
set(QS_BUILD_OPTIONS "${QS_BUILD_OPTIONS}" PARENT_SCOPE)
endfunction()
set(DISTRIBUTOR "Unset" CACHE STRING "Distributor")
string(APPEND QS_BUILD_OPTIONS " Distributor: ${DISTRIBUTOR}")
message(STATUS "Quickshell configuration")
message(STATUS " Build tests: ${BUILD_TESTING}")
message(STATUS " Sockets: ${SOCKETS}")
message(STATUS " Wayland: ${WAYLAND}")
if (WAYLAND)
message(STATUS " Wlroots Layershell: ${WAYLAND_WLR_LAYERSHELL}")
message(STATUS " Session Lock: ${WAYLAND_SESSION_LOCK}")
endif ()
message(STATUS " Distributor: ${DISTRIBUTOR}")
boption(DISTRIBUTOR_DEBUGINFO_AVAILABLE "Distributor provided debuginfo" NO)
boption(NO_PCH "Disable precompild headers (dev)" OFF)
boption(BUILD_TESTING "Build tests (dev)" OFF)
boption(ASAN "ASAN (dev)" OFF) # note: better output with gcc than clang
boption(FRAME_POINTERS "Keep Frame Pointers (dev)" ${ASAN})
if (NOT DEFINED GIT_REVISION)
execute_process(
COMMAND git rev-parse HEAD
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
OUTPUT_VARIABLE GIT_REVISION
)
boption(CRASH_REPORTER "Crash Handling" ON)
boption(USE_JEMALLOC "Use jemalloc" ON)
boption(SOCKETS "Unix Sockets" ON)
boption(WAYLAND "Wayland" ON)
boption(WAYLAND_WLR_LAYERSHELL " Wlroots Layer-Shell" ON REQUIRES WAYLAND)
boption(WAYLAND_SESSION_LOCK " Session Lock" ON REQUIRES WAYLAND)
boption(WAYLAND_TOPLEVEL_MANAGEMENT " Foreign Toplevel Management" ON REQUIRES WAYLAND)
boption(HYPRLAND " Hyprland" ON REQUIRES WAYLAND)
boption(HYPRLAND_IPC " Hyprland IPC" ON REQUIRES HYPRLAND)
boption(HYPRLAND_GLOBAL_SHORTCUTS " Hyprland Global Shortcuts" ON REQUIRES HYPRLAND)
boption(HYPRLAND_FOCUS_GRAB " Hyprland Focus Grabbing" ON REQUIRES HYPRLAND)
boption(HYPRLAND_SURFACE_EXTENSIONS " Hyprland Surface Extensions" ON REQUIRES HYPRLAND)
boption(SCREENCOPY " Screencopy" ON REQUIRES WAYLAND)
boption(SCREENCOPY_ICC " Image Copy Capture" ON REQUIRES WAYLAND)
boption(SCREENCOPY_WLR " Wlroots Screencopy" ON REQUIRES WAYLAND)
boption(SCREENCOPY_HYPRLAND_TOPLEVEL " Hyprland Toplevel Export" ON REQUIRES WAYLAND)
boption(X11 "X11" ON)
boption(I3 "I3/Sway" ON)
boption(I3_IPC " I3/Sway IPC" ON REQUIRES I3)
boption(SERVICE_STATUS_NOTIFIER "System Tray" ON)
boption(SERVICE_PIPEWIRE "PipeWire" ON)
boption(SERVICE_MPRIS "Mpris" ON)
boption(SERVICE_PAM "Pam" ON)
boption(SERVICE_GREETD "Greetd" ON)
boption(SERVICE_UPOWER "UPower" ON)
boption(SERVICE_NOTIFICATIONS "Notifications" ON)
boption(BLUETOOTH "Bluetooth" ON)
include(cmake/install-qml-module.cmake)
include(cmake/util.cmake)
add_compile_options(-Wall -Wextra -Wno-vla-cxx-extension)
# pipewire defines this, breaking PCH
add_compile_definitions(_REENTRANT)
if (FRAME_POINTERS)
add_compile_options(-fno-omit-frame-pointer)
endif()
add_compile_options(-Wall -Wextra)
if (ASAN)
add_compile_options(-fsanitize=address)
add_link_options(-fsanitize=address)
endif()
# nix workaround
if (CMAKE_EXPORT_COMPILE_COMMANDS)
@ -41,34 +99,61 @@ if (NOT CMAKE_BUILD_TYPE)
set(CMAKE_BUILD_TYPE Debug)
endif()
set(QT_DEPS Qt6::Gui Qt6::Qml Qt6::Quick Qt6::QuickControls2)
set(QT_FPDEPS Gui Qml Quick QuickControls2)
set(QT_FPDEPS Gui Qml Quick QuickControls2 Widgets ShaderTools)
include(cmake/pch.cmake)
if (BUILD_TESTING)
enable_testing()
add_definitions(-DQS_TEST)
list(APPEND QT_FPDEPS Test)
endif()
if (SOCKETS)
list(APPEND QT_DEPS Qt6::Network)
list(APPEND QT_FPDEPS Network)
endif()
if (WAYLAND)
list(APPEND QT_DEPS Qt6::WaylandClient Qt6::WaylandClientPrivate)
list(APPEND QT_FPDEPS WaylandClient)
endif()
if (SERVICE_STATUS_NOTIFIER OR SERVICE_MPRIS OR SERVICE_UPOWER OR SERVICE_NOTIFICATIONS OR BLUETOOTH)
set(DBUS ON)
endif()
if (DBUS)
list(APPEND QT_FPDEPS DBus)
endif()
find_package(Qt6 REQUIRED COMPONENTS ${QT_FPDEPS})
set(CMAKE_AUTOUIC OFF)
qt_standard_project_setup(REQUIRES 6.6)
set(QT_QML_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/qml_modules)
add_subdirectory(src/core)
add_subdirectory(src/io)
add_subdirectory(src)
if (WAYLAND)
add_subdirectory(src/wayland)
endif ()
if (USE_JEMALLOC)
find_package(PkgConfig REQUIRED)
# IMPORTED_TARGET not working for some reason
pkg_check_modules(JEMALLOC REQUIRED jemalloc)
target_link_libraries(quickshell PRIVATE ${JEMALLOC_LIBRARIES})
endif()
install(TARGETS quickshell RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR})
install(CODE "
execute_process(
COMMAND ${CMAKE_COMMAND} -E create_symlink \
${CMAKE_INSTALL_FULL_BINDIR}/quickshell \$ENV{DESTDIR}${CMAKE_INSTALL_FULL_BINDIR}/qs
)
")
install(
FILES ${CMAKE_SOURCE_DIR}/assets/org.quickshell.desktop
DESTINATION ${CMAKE_INSTALL_DATADIR}/applications
)
install(
FILES ${CMAKE_SOURCE_DIR}/assets/quickshell.svg
DESTINATION ${CMAKE_INSTALL_DATADIR}/icons/hicolor/scalable/apps
RENAME org.quickshell.svg
)

235
CONTRIBUTING.md Normal file
View file

@ -0,0 +1,235 @@
# Contributing / Development
Instructions for development setup and upstreaming patches.
If you just want to build or package quickshell see [BUILD.md](BUILD.md).
## Development
Install the dependencies listed in [BUILD.md](BUILD.md).
You probably want all of them even if you don't use all of them
to ensure tests work correctly and avoid passing a bunch of configure
flags when you need to wipe the build directory.
Quickshell also uses `just` for common development command aliases.
The dependencies are also available as a nix shell or nix flake which we recommend
using with nix-direnv.
Common aliases:
- `just configure [<debug|release> [extra cmake args]]` (note that you must specify debug/release to specify extra args)
- `just build` - runs the build, configuring if not configured already.
- `just run [args]` - runs quickshell with the given arguments
- `just clean` - clean up build artifacts. `just clean build` is somewhat common.
### Formatting
All contributions should be formatted similarly to what already exists.
Group related functionality together.
Run the formatter using `just fmt`.
If the results look stupid, fix the clang-format file if possible,
or disable clang-format in the affected area
using `// clang-format off` and `// clang-format on`.
#### Style preferences not caught by clang-format
These are flexible. You can ignore them if it looks or works better to
for one reason or another.
Use `auto` if the type of a variable can be deduced automatically, instead of
redeclaring the returned value's type. Additionally, auto should be used when a
constructor takes arguments.
```cpp
auto x = <expr>; // ok
auto x = QString::number(3); // ok
QString x; // ok
QString x = "foo"; // ok
auto x = QString("foo"); // ok
auto x = QString(); // avoid
QString x(); // avoid
QString x("foo"); // avoid
```
Put newlines around logical units of code, and after closing braces. If the
most reasonable logical unit of code takes only a single line, it should be
merged into the next single line logical unit if applicable.
```cpp
// multiple units
auto x = <expr>; // unit 1
auto y = <expr>; // unit 2
auto x = <expr>; // unit 1
emit this->y(); // unit 2
auto x1 = <expr>; // unit 1
auto x2 = <expr>; // unit 1
auto x3 = <expr>; // unit 1
auto y1 = <expr>; // unit 2
auto y2 = <expr>; // unit 2
auto y3 = <expr>; // unit 2
// one unit
auto x = <expr>;
if (x...) {
// ...
}
// if more than one variable needs to be used then add a newline
auto x = <expr>;
auto y = <expr>;
if (x && y) {
// ...
}
```
Class formatting:
```cpp
//! Doc comment summary
/// Doc comment body
class Foo: public QObject {
// The Q_OBJECT macro comes first. Macros are ; terminated.
Q_OBJECT;
QML_ELEMENT;
QML_CLASSINFO(...);
// Properties must stay on a single line or the doc generator won't be able to pick them up
Q_PROPERTY(...);
/// Doc comment
Q_PROPERTY(...);
/// Doc comment
Q_PROPERTY(...);
public:
// Classes should have explicit constructors if they aren't intended to
// implicitly cast. The constructor can be inline in the header if it has no body.
explicit Foo(QObject* parent = nullptr): QObject(parent) {}
// Instance functions if applicable.
static Foo* instance();
// Member functions unrelated to properties come next
void function();
void function();
void function();
// Then Q_INVOKABLEs
Q_INVOKABLE function();
/// Doc comment
Q_INVOKABLE function();
/// Doc comment
Q_INVOKABLE function();
// Then property related functions, in the order (bindable, getter, setter).
// Related functions may be included here as well. Function bodies may be inline
// if they are a single expression. There should be a newline between each
// property's methods.
[[nodiscard]] QBindable<T> bindableFoo() { return &this->bFoo; }
[[nodiscard]] T foo() const { return this->foo; }
void setFoo();
[[nodiscard]] T bar() const { return this->foo; }
void setBar();
signals:
// Signals that are not property change related go first.
// Property change signals go in property definition order.
void asd();
void asd2();
void fooChanged();
void barChanged();
public slots:
// generally Q_INVOKABLEs are preferred to public slots.
void slot();
private slots:
// ...
private:
// statics, then functions, then fields
static const foo BAR;
static void foo();
void foo();
void bar();
// property related members are prefixed with `m`.
QString mFoo;
QString bar;
// Bindables go last and should be prefixed with `b`.
Q_OBJECT_BINDABLE_PROPERTY(Foo, QString, bFoo, &Foo::fooChanged);
};
```
### Linter
All contributions should pass the linter.
Note that running the linter requires disabling precompiled
headers and including the test codepaths:
```sh
$ just configure debug -DNO_PCH=ON -DBUILD_TESTING=ON
$ just lint-changed
```
If the linter is complaining about something that you think it should not,
please disable the lint in your MR and explain your reasoning if it isn't obvious.
### Tests
If you feel like the feature you are working on is very complex or likely to break,
please write some tests. We will ask you to directly if you send in an MR for an
overly complex or breakable feature.
At least all tests that passed before your changes should still be passing
by the time your contribution is ready.
You can run the tests using `just test` but you must enable them first
using `-DBUILD_TESTING=ON`.
### Documentation
Most of quickshell's documentation is automatically generated from the source code.
You should annotate `Q_PROPERTY`s and `Q_INVOKABLE`s with doc comments. Note that the parser
cannot handle random line breaks and will usually require you to disable clang-format if the
lines are too long.
Before submitting an MR, if adding new features please make sure the documentation is generated
reasonably using the `quickshell-docs` repo. We recommend checking it out at `/docs` in this repo.
Doc comments take the form `///` or `///!` (summary) and work with markdown.
You can reference other types using the `@@[Module.][Type.][member]` shorthand
where all parts are optional. If module or type are not specified they will
be inferred as the current module. Member can be a `property`, `function()` or `signal(s)`.
Look at existing code for how it works.
Quickshell modules additionally have a `module.md` file which contains a summary, description,
and list of headers to scan for documentation.
## Contributing
### Commits
Please structure your commit messages as `scope[!]: commit` where
the scope is something like `core` or `service/mpris`. (pick what has been
used historically or what makes sense if new). Add `!` for changes that break
existing APIs or functionality.
Commit descriptions should contain a summary of the changes if they are not
sufficiently addressed in the commit message.
Please squash/rebase additions or edits to previous changes and follow the
commit style to keep the history easily searchable at a glance.
Depending on the change, it is often reasonable to squash it into just
a single commit. (If you do not follow this we will squash your changes
for you.)
### Sending patches
You may contribute by submitting a pull request on github, asking for
an account on our git server, or emailing patches / git bundles
directly to `outfoxxed@outfoxxed.me`.
### Getting help
If you're getting stuck, you can come talk to us in the
[quickshell-development matrix room](https://matrix.to/#/#quickshell-development:outfoxxed.me)
for help on implementation, conventions, etc.
Feel free to ask for advice early in your implementation if you are
unsure.

View file

@ -4,7 +4,13 @@ fmt:
find src -type f \( -name "*.cpp" -o -name "*.hpp" \) -print0 | xargs -0 clang-format -i
lint:
find src -type f -name "*.cpp" -print0 | parallel -q0 --eta clang-tidy --load={{ env_var("TIDYFOX") }}
find src -type f -name "*.cpp" -print0 | parallel -j$(nproc) -q0 --no-notice --will-cite --tty --bar clang-tidy --load={{ env_var("TIDYFOX") }}
lint-ci:
find src -type f -name "*.cpp" -print0 | parallel -j$(nproc) -q0 --no-notice --will-cite --tty clang-tidy --load={{ env_var("TIDYFOX") }}
lint-changed:
git diff --name-only HEAD | grep "^.*\.cpp\$" | parallel -j$(nproc) --no-notice --will-cite --tty --bar clang-tidy --load={{ env_var("TIDYFOX") }}
configure target='debug' *FLAGS='':
cmake -GNinja -B {{builddir}} \
@ -26,7 +32,7 @@ clean:
rm -rf {{builddir}}
run *ARGS='': build
{{builddir}}/src/core/quickshell {{ARGS}}
{{builddir}}/src/quickshell {{ARGS}}
test *ARGS='': build
ctest --test-dir {{builddir}} --output-on-failure {{ARGS}}

110
README.md
View file

@ -1,107 +1,13 @@
# quickshell
# Quickshell
See the [website](https://quickshell.outfoxxed.me) for more information
and installation instructions.
Simple and flexbile QtQuick based desktop shell toolkit.
This repo is hosted at:
- https://git.outfoxxed.me/quickshell/quickshell
- https://github.com/quickshell-mirror/quickshell
Hosts: [outfoxxed's gitea], [github]
[outfoxxed's gitea]: https://git.outfoxxed.me/outfoxxed/quickshell
[github]: https://github.com/outfoxxed/quickshell
Documentation can be built from the [quickshell-docs](https://git.outfoxxed.me/outfoxxed/quickshell-docs) repo,
though is currently pretty lacking.
Some fully working examples can be found in the [quickshell-examples](https://git.outfoxxed.me/outfoxxed/quickshell-examples)
repo.
Both the documentation and examples are included as submodules with revisions that work with the current
version of quickshell.
You can clone everything with
```
$ git clone --recursive https://git.outfoxxed.me/outfoxxed/quickshell.git
```
Or clone missing submodules later with
```
$ git submodule update --init --recursive
```
# Installation
## Nix
This repo has a nix flake you can use to install the package directly:
```nix
{
inputs = {
nixpkgs.url = "nixpkgs/nixos-unstable";
quickshell = {
url = "git+https://git.outfoxxed.me/outfoxxed/quickshell";
inputs.nixpkgs.follows = "nixpkgs";
};
};
}
```
Quickshell's binary is available at `quickshell.packages.<system>.default` to be added to
lists such as `environment.systemPackages` or `home.packages`.
## Manual
If not using nix, you'll have to build from source.
### Dependencies
To build quickshell at all, you will need the following packages (names may vary by distro)
- just
- cmake
- pkg-config
- ninja
- Qt6 [ QtBase, QtDeclarative ]
To build with wayland support you will additionally need:
- wayland
- wayland-scanner (may be part of wayland on some distros)
- wayland-protocols
- Qt6 [ QtWayland ]
### Building
To make a release build of quickshell run:
```sh
$ just release
```
If you have all the dependencies installed and they are in expected
locations this will build correctly.
To install to /usr/local/bin run as root (usually `sudo`) in the same folder:
```
$ just install
```
### Building (Nix)
You can build directly using the provided nix flake or nix package.
```
nix build
nix build -f package.nix # calls default.nix with a basic callPackage expression
```
# Development
For nix there is a devshell available from `shell.nix` and as a devShell
output from the flake.
The Justfile contains various useful aliases:
- `just configure [<debug|release> [extra cmake args]]`
- `just build` (runs configure for debug mode)
- `just run [args]`
- `just clean`
- `just test [args]` (configure with `-DBUILD_TESTING=ON` first)
- `just fmt`
- `just lint`
# Contributing / Development
See [CONTRIBUTING.md](CONTRIBUTING.md) for details.
#### License

View file

@ -0,0 +1,7 @@
[Desktop Entry]
Version=1.5
Type=Application
NoDisplay=true
Name=Quickshell
Icon=org.quickshell

1
assets/quickshell.svg Normal file
View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="724.635" height="724.635"><path fill="#359757" stroke="#359757" stroke-linecap="square" stroke-linejoin="round" stroke-width="74.755" d="m37.378 160.237 122.859-122.86h527.02v527.02l-122.86 122.86H37.378Z"/><path fill="#fff" stroke="#fff" stroke-linecap="round" stroke-linejoin="round" stroke-width="12.201" d="M323.051 96.412a268.74 268.74 0 0 0-3.51.542c-4.052 14.481-7.815 29.941-14.904 42.692a230.02 230.02 0 0 0-59.406 24.679c-14.036-3.974-27.647-12.214-40.766-19.562a268.788 268.788 0 0 0-60.035 60.16c7.376 13.105 15.645 26.698 19.648 40.726a230.02 230.02 0 0 0-24.554 59.458c-12.735 7.115-28.186 10.913-42.66 14.994a268.789 268.789 0 0 0 .09 84.992c14.48 4.05 29.941 7.814 42.691 14.903a230.02 230.02 0 0 0 24.68 59.406c-3.974 14.037-12.215 27.647-19.563 40.766a268.788 268.788 0 0 0 60.161 60.035c13.104-7.376 26.696-15.645 40.725-19.648a230.02 230.02 0 0 0 59.457 24.555c7.116 12.735 10.913 28.186 14.995 42.659a268.788 268.788 0 0 0 84.99-.09c4.052-14.482 7.817-29.941 14.906-42.691a230.02 230.02 0 0 0 59.405-24.68c14.037 3.974 33.069 17.638 46.188 24.986a268.788 268.788 0 0 0 60.035-60.161c-7.376-13.105-21.068-32.12-25.071-46.149a230.02 230.02 0 0 0 24.555-59.457c12.735-7.116 28.186-10.912 42.659-14.993a268.788 268.788 0 0 0-.089-84.993c-14.482-4.051-29.942-7.814-42.692-14.904a230.02 230.02 0 0 0-24.68-59.405c3.974-14.037 12.216-27.647 19.565-40.767a268.788 268.788 0 0 0-60.161-60.035c-13.105 7.376-26.698 15.645-40.726 19.649a230.02 230.02 0 0 0-59.458-24.555c-7.115-12.735-10.913-28.187-14.994-42.66a268.788 268.788 0 0 0-81.481-.452zm15.778 106.85c58.282-8.328 116.455 15.865 151.643 63.065 35.19 47.2 41.766 109.86 17.144 163.337l-41.728-22.688s-38.558 31.44-57.344 63.012l23.893 36.326a160.78 160.78 0 0 1-46.633 15.058c-87.854 12.99-169.6-47.708-182.573-135.564-12.974-87.855 47.74-169.59 135.598-182.546Z"/></svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

1
changelog/v0.1.0.md Normal file
View file

@ -0,0 +1 @@
Initial release

84
changelog/v0.2.0.md Normal file
View file

@ -0,0 +1,84 @@
## Breaking Changes
- Files outside of the shell directory can no longer be referenced with relative paths, e.g. '../../foo.png'.
- PanelWindow's Automatic exclusion mode now adds an exclusion zone for panels with a single anchor.
- `QT_QUICK_CONTROLS_STYLE` and `QT_STYLE_OVERRIDE` are ignored unless `//@ pragma RespectSystemStyle` is set.
## New Features
### Root-Relative Imports
Quickshell 0.2 comes with a new method to import QML modules which is supported by QMLLS.
This replaces "root:/" imports for QML modules.
The new syntax is `import qs.path.to.module`, where `path/to/module` is the path to
a module/subdirectory relative to the config root (`qs`).
### Better LSP support
LSP support for Singletons and Root-Relative imports can be enabled by creating a file named
`.qmlls.ini` in the shell root directory. Quickshell will detect this file and automatically
populate it with an LSP configuration. This file should be gitignored in your configuration,
as it is system dependent.
The generated configuration also includes QML import paths available to Quickshell, meaning
QMLLS no longer requires the `-E` flag.
### Bluetooth Module
Quickshell can now manage your bluetooth devices through BlueZ. While authenticated pairing
has not landed in 0.2, support for connecting and disconnecting devices, basic device information,
and non-authenticated pairing are now supported.
### Other Features
- Added `HyprlandToplevel` and related toplevel/window management APIs in the Hyprland module.
- Added `Quickshell.execDetached()`, which spawns a detached process without a `Process` object.
- Added `Process.exec()` for easier reconfiguration of process commands when starting them.
- Added `FloatingWindow.title`, which allows changing the title of a floating window.
- Added `signal QsWindow.closed()`, fired when a window is closed externally.
- Added support for inline replies in notifications, when supported by applications.
- Added `DesktopEntry.startupWmClass` and `DesktopEntry.heuristicLookup()` to better identify toplevels.
- Added `DesktopEntry.command` which can be run as an alternative to `DesktopEntry.execute()`.
- Added `//@ pragma Internal`, which makes a QML component impossible to import outside of its module.
- Added dead instance selection for some subcommands, such as `qs log` and `qs list`.
## Other Changes
- `Quickshell.shellRoot` has been renamed to `Quickshell.shellDir`.
- PanelWindow margins opposite the window's anchorpoint are now added to exclusion zone.
- stdout/stderr or detached processes and executed desktop entries are now hidden by default.
- Various warnings caused by other applications Quickshell communicates with over D-BUS have been hidden in logs.
- Quickshell's new logo is now shown in any floating windows.
## Bug Fixes
- Fixed pipewire device volume and mute states not updating before the device has been used.
- Fixed a crash when changing the volume of any pipewire device on a sound card another removed device was using.
- Fixed a crash when accessing a removed previous default pipewire node from the default sink/source changed signals.
- Fixed session locks crashing if all monitors are disconnected.
- Fixed session locks crashing if unsupported by the compositor.
- Fixed a crash when creating a session lock and destroying it before acknowledged by the compositor.
- Fixed window input masks not updating after a reload.
- Fixed PanelWindows being unconfigurable unless `screen` was set under X11.
- Fixed a crash when anchoring a popup to a zero sized `Item`.
- Fixed `FileView` crashing if `watchChanges` was used.
- Fixed `SocketServer` sockets disappearing after a reload.
- Fixed `ScreencopyView` having incorrect rotation when displaying a rotated monitor.
- Fixed `MarginWrapperManager` breaking pixel alignment of child items when centering.
- Fixed `IpcHandler`, `NotificationServer` and `GlobalShortcut` not activating with certain QML structures.
- Fixed tracking of QML incubator destruction and deregistration, which occasionally caused crashes.
- Fixed FloatingWindows being constrained to the smallest window manager supported size unless max size was set.
- Fixed `MprisPlayer.lengthSupported` not updating reactively.
- Fixed normal tray icon being ignored when status is `NeedsAttention` and no attention icon is provided.
- Fixed `HyprlandWorkspace.activate()` sending invalid commands to Hyprland for named or special workspaces.
- Fixed file watcher occasionally breaking when using VSCode to edit QML files.
- Fixed crashes when screencopy buffer creation fails.
- Fixed a crash when wayland layer surfaces are recreated for the same window.
- Fixed the `QsWindow` attached object not working when using `WlrLayershell` directly.
- Fixed a crash when attempting to create a window without available VRAM.
- Fixed OOM crash when failing to write to detailed log file.
- Prevented distro logging configurations for Qt from interfering with Quickshell commands.
- Removed the "QProcess destroyed for running process" warning when destroying `Process` objects.
- Fixed `ColorQuantizer` printing a pointer to an error message instead of an error message.
- Fixed notification pixmap rowstride warning showing for correct rowstrides.

8
ci/matrix.nix Normal file
View file

@ -0,0 +1,8 @@
{
qtver,
compiler,
}: let
nixpkgs = (import ./nix-checkouts.nix).${builtins.replaceStrings ["."] ["_"] qtver};
compilerOverride = (nixpkgs.callPackage ./variations.nix {}).${compiler};
pkg = (nixpkgs.callPackage ../default.nix {}).override compilerOverride;
in pkg

78
ci/nix-checkouts.nix Normal file
View file

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

7
ci/variations.nix Normal file
View file

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

View file

@ -0,0 +1,89 @@
set(INSTALL_QMLDIR "" CACHE STRING "QML install dir")
set(INSTALL_QML_PREFIX "" CACHE STRING "QML install prefix")
# There doesn't seem to be a standard cross-distro qml install path.
if ("${INSTALL_QMLDIR}" STREQUAL "" AND "${INSTALL_QML_PREFIX}" STREQUAL "")
message(WARNING "Neither INSTALL_QMLDIR nor INSTALL_QML_PREFIX is set. QML modules will not be installed.")
else()
if ("${INSTALL_QMLDIR}" STREQUAL "")
set(QML_FULL_INSTALLDIR "${CMAKE_INSTALL_PREFIX}/${INSTALL_QML_PREFIX}")
else()
set(QML_FULL_INSTALLDIR "${INSTALL_QMLDIR}")
endif()
message(STATUS "QML install dir: ${QML_FULL_INSTALLDIR}")
endif()
# Install a given target as a QML module. This is mostly pulled from ECM, as there does not seem
# to be an official way to do it.
# see https://github.com/KDE/extra-cmake-modules/blob/fe0f606bf7f222e36f7560fd7a2c33ef993e23bb/modules/ECMQmlModule6.cmake#L160
function(install_qml_module arg_TARGET)
if (NOT DEFINED QML_FULL_INSTALLDIR)
return()
endif()
qt_query_qml_module(${arg_TARGET}
URI module_uri
VERSION module_version
PLUGIN_TARGET module_plugin_target
TARGET_PATH module_target_path
QMLDIR module_qmldir
TYPEINFO module_typeinfo
QML_FILES module_qml_files
RESOURCES module_resources
)
set(module_dir "${QML_FULL_INSTALLDIR}/${module_target_path}")
if (NOT TARGET "${module_plugin_target}")
message(FATAL_ERROR "install_qml_modules called for a target without a plugin")
endif()
get_target_property(target_type "${arg_TARGET}" TYPE)
if (NOT "${target_type}" STREQUAL "STATIC_LIBRARY")
install(
TARGETS "${arg_TARGET}"
LIBRARY DESTINATION "${module_dir}"
RUNTIME DESTINATION "${module_dir}"
)
install(
TARGETS "${module_plugin_target}"
LIBRARY DESTINATION "${module_dir}"
RUNTIME DESTINATION "${module_dir}"
)
endif()
install(FILES "${module_qmldir}" DESTINATION "${module_dir}")
install(FILES "${module_typeinfo}" DESTINATION "${module_dir}")
# Install QML files
list(LENGTH module_qml_files num_files)
if (NOT "${module_qml_files}" MATCHES "NOTFOUND" AND ${num_files} GREATER 0)
qt_query_qml_module(${arg_TARGET} QML_FILES_DEPLOY_PATHS qml_files_deploy_paths)
math(EXPR last_index "${num_files} - 1")
foreach(i RANGE 0 ${last_index})
list(GET module_qml_files ${i} src_file)
list(GET qml_files_deploy_paths ${i} deploy_path)
get_filename_component(dst_name "${deploy_path}" NAME)
get_filename_component(dest_dir "${deploy_path}" DIRECTORY)
install(FILES "${src_file}" DESTINATION "${module_dir}/${dest_dir}" RENAME "${dst_name}")
endforeach()
endif()
# Install resources
list(LENGTH module_resources num_files)
if (NOT "${module_resources}" MATCHES "NOTFOUND" AND ${num_files} GREATER 0)
qt_query_qml_module(${arg_TARGET} RESOURCES_DEPLOY_PATHS resources_deploy_paths)
math(EXPR last_index "${num_files} - 1")
foreach(i RANGE 0 ${last_index})
list(GET module_resources ${i} src_file)
list(GET resources_deploy_paths ${i} deploy_path)
get_filename_component(dst_name "${deploy_path}" NAME)
get_filename_component(dest_dir "${deploy_path}" DIRECTORY)
install(FILES "${src_file}" DESTINATION "${module_dir}/${dest_dir}" RENAME "${dst_name}")
endforeach()
endif()
endfunction()

85
cmake/pch.cmake Normal file
View file

@ -0,0 +1,85 @@
# pch breaks clang-tidy..... somehow
if (NOT NO_PCH)
file(GENERATE
OUTPUT ${CMAKE_BINARY_DIR}/pchstub.cpp
CONTENT "// intentionally empty"
)
endif()
function (qs_pch target)
if (NO_PCH)
return()
endif()
cmake_parse_arguments(PARSE_ARGV 1 arg "" "SET" "")
if ("${arg_SET}" STREQUAL "")
set(arg_SET "common")
endif()
target_precompile_headers(${target} REUSE_FROM "qs-pchset-${arg_SET}")
endfunction()
function (qs_module_pch target)
qs_pch(${target} ${ARGN})
qs_pch("${target}plugin" SET plugin)
qs_pch("${target}plugin_init" SET plugin)
endfunction()
function (qs_add_pchset SETNAME)
if (NO_PCH)
return()
endif()
cmake_parse_arguments(PARSE_ARGV 1 arg "" "" "HEADERS;DEPENDENCIES")
set(LIBNAME "qs-pchset-${SETNAME}")
add_library(${LIBNAME} ${CMAKE_BINARY_DIR}/pchstub.cpp)
target_link_libraries(${LIBNAME} ${arg_DEPENDENCIES})
target_precompile_headers(${LIBNAME} PUBLIC ${arg_HEADERS})
endfunction()
set(COMMON_PCH_SET
<chrono>
<memory>
<vector>
<qdebug.h>
<qobject.h>
<qmetatype.h>
<qstring.h>
<qchar.h>
<qlist.h>
<qabstractitemmodel.h>
)
qs_add_pchset(common
DEPENDENCIES Qt::Quick
HEADERS ${COMMON_PCH_SET}
)
qs_add_pchset(large
DEPENDENCIES Qt::Quick
HEADERS
${COMMON_PCH_SET}
<qiodevice.h>
<qevent.h>
<qcoreapplication.h>
<qqmlengine.h>
<qquickitem.h>
<qquickwindow.h>
<qcolor.h>
<qdir.h>
<qtimer.h>
<qabstractitemmodel.h>
)
# including qplugin.h directly will cause required symbols to disappear
qs_add_pchset(plugin
DEPENDENCIES Qt::Qml
HEADERS
<qobject.h>
<qjsonobject.h>
<qpointer.h>
)

29
cmake/util.cmake Normal file
View file

@ -0,0 +1,29 @@
# Adds a dependency hint to the link order, but does not block build on the dependency.
function (qs_add_link_dependencies target)
set_property(
TARGET ${target}
APPEND PROPERTY INTERFACE_LINK_LIBRARIES
${ARGN}
)
endfunction()
function (qs_append_qmldir target text)
get_property(qmldir_content TARGET ${target} PROPERTY _qt_internal_qmldir_content)
if ("${qmldir_content}" STREQUAL "")
message(WARNING "qs_append_qmldir depends on private Qt cmake code, which has broken.")
return()
endif()
set_property(TARGET ${target} APPEND_STRING PROPERTY _qt_internal_qmldir_content ${text})
endfunction()
# DEPENDENCIES introduces a cmake dependency which we don't need with static modules.
# This greatly improves comp speed by not introducing those dependencies.
function (qs_add_module_deps_light target)
foreach (dep IN LISTS ARGN)
string(APPEND qmldir_extra "depends ${dep}\n")
endforeach()
qs_append_qmldir(${target} "${qmldir_extra}")
endfunction()

View file

@ -3,13 +3,24 @@
nix-gitignore,
pkgs,
keepDebugInfo,
stdenv ? (keepDebugInfo pkgs.stdenv),
buildStdenv ? pkgs.clangStdenv,
pkg-config,
cmake,
ninja,
spirv-tools,
qt6,
breakpad,
jemalloc,
cli11,
wayland,
wayland-protocols,
wayland-scanner,
xorg,
libdrm,
libgbm ? null,
pipewire,
pam,
gitRev ? (let
headExists = builtins.pathExists ./.git/HEAD;
@ -21,51 +32,101 @@
then builtins.readFile ./.git/refs/heads/${builtins.elemAt matches 0}
else headContent)
else "unknown"),
debug ? false,
enableWayland ? true,
}: stdenv.mkDerivation {
pname = "quickshell${lib.optionalString debug "-debug"}";
version = "0.1.0";
src = nix-gitignore.gitignoreSource [] ./.;
withCrashReporter ? true,
withJemalloc ? true, # masks heap fragmentation
withQtSvg ? true,
withWayland ? true,
withX11 ? true,
withPipewire ? true,
withPam ? true,
withHyprland ? true,
withI3 ? true,
}: let
unwrapped = buildStdenv.mkDerivation {
pname = "quickshell${lib.optionalString debug "-debug"}";
version = "0.2.0";
src = nix-gitignore.gitignoreSource "/default.nix\n" ./.;
nativeBuildInputs = with pkgs; [
cmake
ninja
qt6.wrapQtAppsHook
] ++ (lib.optionals enableWayland [
pkg-config
wayland-protocols
wayland-scanner
]);
dontWrapQtApps = true; # see wrappers
buildInputs = with pkgs; [
qt6.qtbase
qt6.qtdeclarative
] ++ (lib.optionals enableWayland [ qt6.qtwayland wayland ]);
nativeBuildInputs = [
cmake
ninja
qt6.qtshadertools
spirv-tools
pkg-config
]
++ lib.optional withWayland wayland-scanner;
QTWAYLANDSCANNER = lib.optionalString enableWayland "${qt6.qtwayland}/libexec/qtwaylandscanner";
buildInputs = [
qt6.qtbase
qt6.qtdeclarative
cli11
]
++ lib.optional withQtSvg qt6.qtsvg
++ lib.optional withCrashReporter breakpad
++ lib.optional withJemalloc jemalloc
++ lib.optionals withWayland [ qt6.qtwayland wayland wayland-protocols ]
++ lib.optionals (withWayland && libgbm != null) [ libdrm libgbm ]
++ lib.optional withX11 xorg.libxcb
++ lib.optional withPam pam
++ lib.optional withPipewire pipewire;
configurePhase = let
cmakeBuildType = if debug
then "Debug"
else "RelWithDebInfo";
in ''
cmakeBuildType=${cmakeBuildType} # qt6 setup hook resets this for some godforsaken reason
cmakeConfigurePhase
'';
cmakeBuildType = if debug then "Debug" else "RelWithDebInfo";
cmakeFlags = [
"-DGIT_REVISION=${gitRev}"
] ++ lib.optional (!enableWayland) "-DWAYLAND=OFF";
cmakeFlags = [
(lib.cmakeFeature "DISTRIBUTOR" "Official-Nix-Flake")
(lib.cmakeFeature "INSTALL_QML_PREFIX" qt6.qtbase.qtQmlPrefix)
(lib.cmakeBool "DISTRIBUTOR_DEBUGINFO_AVAILABLE" true)
(lib.cmakeFeature "GIT_REVISION" gitRev)
(lib.cmakeBool "CRASH_REPORTER" withCrashReporter)
(lib.cmakeBool "USE_JEMALLOC" withJemalloc)
(lib.cmakeBool "WAYLAND" withWayland)
(lib.cmakeBool "SCREENCOPY" (libgbm != null))
(lib.cmakeBool "SERVICE_PIPEWIRE" withPipewire)
(lib.cmakeBool "SERVICE_PAM" withPam)
(lib.cmakeBool "HYPRLAND" withHyprland)
(lib.cmakeBool "I3" withI3)
];
buildPhase = "ninjaBuildPhase";
enableParallelBuilding = true;
dontStrip = true;
# How to get debuginfo in gdb from a release build:
# 1. build `quickshell.debug`
# 2. set NIX_DEBUG_INFO_DIRS="<quickshell.debug store path>/lib/debug"
# 3. launch gdb / coredumpctl and debuginfo will work
separateDebugInfo = !debug;
dontStrip = debug;
meta = with lib; {
homepage = "https://git.outfoxxed.me/outfoxxed/quickshell";
description = "Simple and flexbile QtQuick based desktop shell toolkit";
license = licenses.lgpl3Only;
platforms = platforms.linux;
meta = with lib; {
homepage = "https://quickshell.org";
description = "Flexbile QtQuick based desktop shell toolkit";
license = licenses.lgpl3Only;
platforms = platforms.linux;
mainProgram = "quickshell";
};
};
}
wrapper = unwrapped.stdenv.mkDerivation {
inherit (unwrapped) version meta buildInputs;
pname = "${unwrapped.pname}-wrapped";
nativeBuildInputs = unwrapped.nativeBuildInputs ++ [ qt6.wrapQtAppsHook ];
dontUnpack = true;
dontConfigure = true;
dontBuild = true;
installPhase = ''
mkdir -p $out
cp -r ${unwrapped}/* $out
'';
passthru = {
unwrapped = unwrapped;
withModules = modules: wrapper.overrideAttrs (prev: {
buildInputs = prev.buildInputs ++ modules;
});
};
};
in wrapper

1
docs

@ -1 +0,0 @@
Subproject commit 70989dc619bcdc29dc4880b4ff5257d6ad188a18

@ -1 +0,0 @@
Subproject commit 9c83cc248c968b18a827b4fa4c616a8d362176e1

6
flake.lock generated
View file

@ -2,11 +2,11 @@
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1709237383,
"narHash": "sha256-cy6ArO4k5qTx+l5o+0mL9f5fa86tYUX3ozE1S+Txlds=",
"lastModified": 1749285348,
"narHash": "sha256-frdhQvPbmDYaScPFiCnfdh3B/Vh81Uuoo0w5TkWmmjU=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "1536926ef5621b09bba54035ae2bb6d806d72ac8",
"rev": "3e3afe5174c561dee0df6f2c2b2236990146329f",
"type": "github"
},
"original": {

View file

@ -4,12 +4,16 @@
};
outputs = { self, nixpkgs }: let
forEachSystem = fn: nixpkgs.lib.genAttrs
[ "x86_64-linux" "aarch64-linux" ]
(system: fn system nixpkgs.legacyPackages.${system});
forEachSystem = fn:
nixpkgs.lib.genAttrs
nixpkgs.lib.platforms.linux
(system: fn system nixpkgs.legacyPackages.${system});
in {
packages = forEachSystem (system: pkgs: rec {
quickshell = import ./package.nix { inherit pkgs; };
quickshell = pkgs.callPackage ./default.nix {
gitRev = self.rev or self.dirtyRev;
};
default = quickshell;
});

View file

@ -1 +0,0 @@
{ pkgs ? import <nixpkgs> {}, ... }: pkgs.callPackage ./default.nix {}

77
quickshell.scm Normal file
View file

@ -0,0 +1,77 @@
(define-module (quickshell)
#:use-module ((guix licenses) #:prefix license:)
#:use-module (gnu packages cpp)
#:use-module (gnu packages freedesktop)
#:use-module (gnu packages gcc)
#:use-module (gnu packages gl)
#:use-module (gnu packages jemalloc)
#:use-module (gnu packages linux)
#:use-module (gnu packages ninja)
#:use-module (gnu packages pkg-config)
#:use-module (gnu packages qt)
#:use-module (gnu packages vulkan)
#:use-module (gnu packages xdisorg)
#:use-module (gnu packages xorg)
#:use-module (guix build-system cmake)
#:use-module (guix download)
#:use-module (guix gexp)
#:use-module (guix git-download)
#:use-module (guix packages)
#:use-module (guix packages)
#:use-module (guix utils))
(define-public quickshell-git
(package
(name "quickshell")
(version "git")
(source (local-file "." "quickshell-checkout"
#:recursive? #t
#:select? (or (git-predicate (current-source-directory))
(const #t))))
(build-system cmake-build-system)
(propagated-inputs (list qtbase qtdeclarative qtsvg))
(native-inputs (list ninja
gcc-14
pkg-config
qtshadertools
spirv-tools
wayland-protocols
cli11))
(inputs (list jemalloc
libdrm
libxcb
libxkbcommon
linux-pam
mesa
pipewire
qtbase
qtdeclarative
qtwayland
vulkan-headers
wayland))
(arguments
(list #:tests? #f
#:configure-flags
#~(list "-GNinja"
"-DDISTRIBUTOR=\"In-tree Guix channel\""
"-DDISTRIBUTOR_DEBUGINFO_AVAILABLE=NO"
;; Breakpad is not currently packaged for Guix.
"-DCRASH_REPORTER=OFF")
#:phases
#~(modify-phases %standard-phases
(replace 'build (lambda _ (invoke "cmake" "--build" ".")))
(replace 'install (lambda _ (invoke "cmake" "--install" ".")))
(add-after 'install 'wrap-program
(lambda* (#:key inputs #:allow-other-keys)
(wrap-program (string-append #$output "/bin/quickshell")
`("QML_IMPORT_PATH" ":"
= (,(getenv "QML_IMPORT_PATH")))))))))
(home-page "https://quickshell.outfoxxed.me")
(synopsis "QtQuick-based desktop shell toolkit")
(description
"Quickshell is a flexible QtQuick-based toolkit for creating and
customizing toolbars, notification centers, and other desktop
environment tools in a live programming environment.")
(license license:lgpl3)))
quickshell-git

View file

@ -10,18 +10,17 @@
rev = "1f062cc198d1112d13e5128fa1f2ee3dbffe613b";
sha256 = "kbt0Zc1qHE5fhqBkKz8iue+B+ZANjF1AR/RdgmX1r0I=";
}) { inherit pkgs; };
in pkgs.mkShell {
in pkgs.mkShell.override { stdenv = quickshell.stdenv; } {
inputsFrom = [ quickshell ];
nativeBuildInputs = with pkgs; [
just
clang-tools_17
clang-tools
parallel
makeWrapper
];
TIDYFOX = "${tidyfox}/lib/libtidyfox.so";
QTWAYLANDSCANNER = quickshell.QTWAYLANDSCANNER;
shellHook = ''
export CMAKE_BUILD_PARALLEL_LEVEL=$(nproc)

35
src/CMakeLists.txt Normal file
View file

@ -0,0 +1,35 @@
qt_add_executable(quickshell main.cpp)
install(TARGETS quickshell RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR})
add_subdirectory(build)
add_subdirectory(launch)
add_subdirectory(core)
add_subdirectory(debug)
add_subdirectory(ipc)
add_subdirectory(window)
add_subdirectory(io)
add_subdirectory(widgets)
add_subdirectory(ui)
if (CRASH_REPORTER)
add_subdirectory(crash)
endif()
if (DBUS)
add_subdirectory(dbus)
endif()
if (WAYLAND)
add_subdirectory(wayland)
endif()
if (X11)
add_subdirectory(x11)
endif()
add_subdirectory(services)
if (BLUETOOTH)
add_subdirectory(bluetooth)
endif()

View file

@ -0,0 +1,42 @@
set_source_files_properties(org.bluez.Adapter.xml PROPERTIES
CLASSNAME DBusBluezAdapterInterface
)
set_source_files_properties(org.bluez.Device.xml PROPERTIES
CLASSNAME DBusBluezDeviceInterface
)
qt_add_dbus_interface(DBUS_INTERFACES
org.bluez.Adapter.xml
dbus_adapter
)
qt_add_dbus_interface(DBUS_INTERFACES
org.bluez.Device.xml
dbus_device
)
qt_add_library(quickshell-bluetooth STATIC
adapter.cpp
bluez.cpp
device.cpp
${DBUS_INTERFACES}
)
qt_add_qml_module(quickshell-bluetooth
URI Quickshell.Bluetooth
VERSION 0.1
DEPENDENCIES QtQml
)
install_qml_module(quickshell-bluetooth)
# dbus headers
target_include_directories(quickshell-bluetooth PRIVATE ${CMAKE_CURRENT_BINARY_DIR})
target_link_libraries(quickshell-bluetooth PRIVATE Qt::Qml Qt::DBus)
qs_add_link_dependencies(quickshell-bluetooth quickshell-dbus)
qs_module_pch(quickshell-bluetooth SET dbus)
target_link_libraries(quickshell PRIVATE quickshell-bluetoothplugin)

224
src/bluetooth/adapter.cpp Normal file
View file

@ -0,0 +1,224 @@
#include "adapter.hpp"
#include <qcontainerfwd.h>
#include <qdbusconnection.h>
#include <qdbusextratypes.h>
#include <qdbuspendingcall.h>
#include <qdbuspendingreply.h>
#include <qdebug.h>
#include <qlogging.h>
#include <qloggingcategory.h>
#include <qstring.h>
#include <qstringliteral.h>
#include <qtypes.h>
#include "../core/logcat.hpp"
#include "../dbus/properties.hpp"
#include "dbus_adapter.h"
namespace qs::bluetooth {
namespace {
QS_LOGGING_CATEGORY(logAdapter, "quickshell.bluetooth.adapter", QtWarningMsg);
}
QString BluetoothAdapterState::toString(BluetoothAdapterState::Enum state) {
switch (state) {
case BluetoothAdapterState::Disabled: return QStringLiteral("Disabled");
case BluetoothAdapterState::Enabled: return QStringLiteral("Enabled");
case BluetoothAdapterState::Enabling: return QStringLiteral("Enabling");
case BluetoothAdapterState::Disabling: return QStringLiteral("Disabling");
case BluetoothAdapterState::Blocked: return QStringLiteral("Blocked");
default: return QStringLiteral("Unknown");
}
}
BluetoothAdapter::BluetoothAdapter(const QString& path, QObject* parent): QObject(parent) {
this->mInterface =
new DBusBluezAdapterInterface("org.bluez", path, QDBusConnection::systemBus(), this);
if (!this->mInterface->isValid()) {
qCWarning(logAdapter) << "Could not create DBus interface for adapter at" << path;
this->mInterface = nullptr;
return;
}
this->properties.setInterface(this->mInterface);
}
QString BluetoothAdapter::adapterId() const {
auto path = this->path();
return path.sliced(path.lastIndexOf('/') + 1);
}
void BluetoothAdapter::setEnabled(bool enabled) {
if (enabled == this->bEnabled) return;
if (enabled && this->bState == BluetoothAdapterState::Blocked) {
qCCritical(logAdapter) << "Cannot enable adapter because it is blocked by rfkill.";
return;
}
this->bEnabled = enabled;
this->pEnabled.write();
}
void BluetoothAdapter::setDiscoverable(bool discoverable) {
if (discoverable == this->bDiscoverable) return;
this->bDiscoverable = discoverable;
this->pDiscoverable.write();
}
void BluetoothAdapter::setDiscovering(bool discovering) {
if (discovering) {
this->startDiscovery();
} else {
this->stopDiscovery();
}
}
void BluetoothAdapter::setDiscoverableTimeout(quint32 timeout) {
if (timeout == this->bDiscoverableTimeout) return;
this->bDiscoverableTimeout = timeout;
this->pDiscoverableTimeout.write();
}
void BluetoothAdapter::setPairable(bool pairable) {
if (pairable == this->bPairable) return;
this->bPairable = pairable;
this->pPairable.write();
}
void BluetoothAdapter::setPairableTimeout(quint32 timeout) {
if (timeout == this->bPairableTimeout) return;
this->bPairableTimeout = timeout;
this->pPairableTimeout.write();
}
void BluetoothAdapter::addInterface(const QString& interface, const QVariantMap& properties) {
if (interface == "org.bluez.Adapter1") {
this->properties.updatePropertySet(properties, false);
qCDebug(logAdapter) << "Updated Adapter properties for" << this;
}
}
void BluetoothAdapter::removeDevice(const QString& devicePath) {
qCDebug(logAdapter) << "Removing device" << devicePath << "from adapter" << this;
auto reply = this->mInterface->RemoveDevice(QDBusObjectPath(devicePath));
auto* watcher = new QDBusPendingCallWatcher(reply, this);
QObject::connect(
watcher,
&QDBusPendingCallWatcher::finished,
this,
[this, devicePath](QDBusPendingCallWatcher* watcher) {
const QDBusPendingReply<> reply = *watcher;
if (reply.isError()) {
qCWarning(logAdapter).nospace()
<< "Failed to remove device " << devicePath << " from adapter" << this << ": "
<< reply.error().message();
} else {
qCDebug(logAdapter) << "Successfully removed device" << devicePath << "from adapter"
<< this;
}
delete watcher;
}
);
}
void BluetoothAdapter::startDiscovery() {
if (this->bDiscovering) return;
qCDebug(logAdapter) << "Starting discovery for adapter" << this;
auto reply = this->mInterface->StartDiscovery();
auto* watcher = new QDBusPendingCallWatcher(reply, this);
QObject::connect(
watcher,
&QDBusPendingCallWatcher::finished,
this,
[this](QDBusPendingCallWatcher* watcher) {
const QDBusPendingReply<> reply = *watcher;
if (reply.isError()) {
qCWarning(logAdapter).nospace()
<< "Failed to start discovery on adapter" << this << ": " << reply.error().message();
} else {
qCDebug(logAdapter) << "Successfully started discovery on adapter" << this;
}
delete watcher;
}
);
}
void BluetoothAdapter::stopDiscovery() {
if (!this->bDiscovering) return;
qCDebug(logAdapter) << "Stopping discovery for adapter" << this;
auto reply = this->mInterface->StopDiscovery();
auto* watcher = new QDBusPendingCallWatcher(reply, this);
QObject::connect(
watcher,
&QDBusPendingCallWatcher::finished,
this,
[this](QDBusPendingCallWatcher* watcher) {
const QDBusPendingReply<> reply = *watcher;
if (reply.isError()) {
qCWarning(logAdapter).nospace()
<< "Failed to stop discovery on adapter " << this << ": " << reply.error().message();
} else {
qCDebug(logAdapter) << "Successfully stopped discovery on adapter" << this;
}
delete watcher;
}
);
}
} // namespace qs::bluetooth
namespace qs::dbus {
using namespace qs::bluetooth;
DBusResult<BluetoothAdapterState::Enum>
DBusDataTransform<BluetoothAdapterState::Enum>::fromWire(const Wire& wire) {
if (wire == QStringLiteral("off")) {
return BluetoothAdapterState::Disabled;
} else if (wire == QStringLiteral("on")) {
return BluetoothAdapterState::Enabled;
} else if (wire == QStringLiteral("off-enabling")) {
return BluetoothAdapterState::Enabling;
} else if (wire == QStringLiteral("on-disabling")) {
return BluetoothAdapterState::Disabling;
} else if (wire == QStringLiteral("off-blocked")) {
return BluetoothAdapterState::Blocked;
} else {
return QDBusError(
QDBusError::InvalidArgs,
QString("Invalid BluetoothAdapterState: %1").arg(wire)
);
}
}
} // namespace qs::dbus
QDebug operator<<(QDebug debug, const qs::bluetooth::BluetoothAdapter* adapter) {
auto saver = QDebugStateSaver(debug);
if (adapter) {
debug.nospace() << "BluetoothAdapter(" << static_cast<const void*>(adapter)
<< ", path=" << adapter->path() << ")";
} else {
debug << "BluetoothAdapter(nullptr)";
}
return debug;
}

173
src/bluetooth/adapter.hpp Normal file
View file

@ -0,0 +1,173 @@
#pragma once
#include <qobject.h>
#include <qqmlintegration.h>
#include <qtmetamacros.h>
#include "../core/doc.hpp"
#include "../core/model.hpp"
#include "../dbus/properties.hpp"
#include "dbus_adapter.h"
namespace qs::bluetooth {
///! Power state of a Bluetooth adapter.
class BluetoothAdapterState: public QObject {
Q_OBJECT;
QML_ELEMENT;
QML_SINGLETON;
public:
enum Enum : quint8 {
/// The adapter is powered off.
Disabled = 0,
/// The adapter is powered on.
Enabled = 1,
/// The adapter is transitioning from off to on.
Enabling = 2,
/// The adapter is transitioning from on to off.
Disabling = 3,
/// The adapter is blocked by rfkill.
Blocked = 4,
};
Q_ENUM(Enum);
Q_INVOKABLE static QString toString(BluetoothAdapterState::Enum state);
};
} // namespace qs::bluetooth
namespace qs::dbus {
template <>
struct DBusDataTransform<qs::bluetooth::BluetoothAdapterState::Enum> {
using Wire = QString;
using Data = qs::bluetooth::BluetoothAdapterState::Enum;
static DBusResult<Data> fromWire(const Wire& wire);
};
} // namespace qs::dbus
namespace qs::bluetooth {
class BluetoothAdapter;
class BluetoothDevice;
///! A Bluetooth adapter
class BluetoothAdapter: public QObject {
Q_OBJECT;
QML_ELEMENT;
QML_UNCREATABLE("");
// clang-format off
/// System provided name of the adapter. See @@adapterId for the internal identifier.
Q_PROPERTY(QString name READ default NOTIFY nameChanged BINDABLE bindableName);
/// True if the adapter is currently enabled. More detailed state is available from @@state.
Q_PROPERTY(bool enabled READ enabled WRITE setEnabled NOTIFY enabledChanged);
/// Detailed power state of the adapter.
Q_PROPERTY(BluetoothAdapterState::Enum state READ default NOTIFY stateChanged BINDABLE bindableState);
/// True if the adapter can be discovered by other bluetooth devices.
Q_PROPERTY(bool discoverable READ discoverable WRITE setDiscoverable NOTIFY discoverableChanged);
/// Timeout in seconds for how long the adapter stays discoverable after @@discoverable is set to true.
/// A value of 0 means the adapter stays discoverable forever.
Q_PROPERTY(quint32 discoverableTimeout READ discoverableTimeout WRITE setDiscoverableTimeout NOTIFY discoverableTimeoutChanged);
/// True if the adapter is scanning for new devices.
Q_PROPERTY(bool discovering READ discovering WRITE setDiscovering NOTIFY discoveringChanged);
/// True if the adapter is accepting incoming pairing requests.
///
/// This only affects incoming pairing requests and should typically only be changed
/// by system settings applications. Defaults to true.
Q_PROPERTY(bool pairable READ pairable WRITE setPairable NOTIFY pairableChanged);
/// Timeout in seconds for how long the adapter stays pairable after @@pairable is set to true.
/// A value of 0 means the adapter stays pairable forever. Defaults to 0.
Q_PROPERTY(quint32 pairableTimeout READ pairableTimeout WRITE setPairableTimeout NOTIFY pairableTimeoutChanged);
/// Bluetooth devices connected to this adapter.
QSDOC_TYPE_OVERRIDE(ObjectModel<qs::bluetooth::BluetoothDevice>*);
Q_PROPERTY(UntypedObjectModel* devices READ devices CONSTANT);
/// The internal ID of the adapter (e.g., "hci0").
Q_PROPERTY(QString adapterId READ adapterId CONSTANT);
/// DBus path of the adapter under the `org.bluez` system service.
Q_PROPERTY(QString dbusPath READ path CONSTANT);
// clang-format on
public:
explicit BluetoothAdapter(const QString& path, QObject* parent = nullptr);
[[nodiscard]] bool isValid() const { return this->mInterface->isValid(); }
[[nodiscard]] QString path() const { return this->mInterface->path(); }
[[nodiscard]] QString adapterId() const;
[[nodiscard]] bool enabled() const { return this->bEnabled; }
void setEnabled(bool enabled);
[[nodiscard]] bool discoverable() const { return this->bDiscoverable; }
void setDiscoverable(bool discoverable);
[[nodiscard]] bool discovering() const { return this->bDiscovering; }
void setDiscovering(bool discovering);
[[nodiscard]] quint32 discoverableTimeout() const { return this->bDiscoverableTimeout; }
void setDiscoverableTimeout(quint32 timeout);
[[nodiscard]] bool pairable() const { return this->bPairable; }
void setPairable(bool pairable);
[[nodiscard]] quint32 pairableTimeout() const { return this->bPairableTimeout; }
void setPairableTimeout(quint32 timeout);
[[nodiscard]] QBindable<QString> bindableName() { return &this->bName; }
[[nodiscard]] QBindable<bool> bindableEnabled() { return &this->bEnabled; }
[[nodiscard]] QBindable<BluetoothAdapterState::Enum> bindableState() { return &this->bState; }
[[nodiscard]] QBindable<bool> bindableDiscoverable() { return &this->bDiscoverable; }
[[nodiscard]] QBindable<quint32> bindableDiscoverableTimeout() {
return &this->bDiscoverableTimeout;
}
[[nodiscard]] QBindable<bool> bindableDiscovering() { return &this->bDiscovering; }
[[nodiscard]] QBindable<bool> bindablePairable() { return &this->bPairable; }
[[nodiscard]] QBindable<quint32> bindablePairableTimeout() { return &this->bPairableTimeout; }
[[nodiscard]] ObjectModel<BluetoothDevice>* devices() { return &this->mDevices; }
void addInterface(const QString& interface, const QVariantMap& properties);
void removeDevice(const QString& devicePath);
void startDiscovery();
void stopDiscovery();
signals:
void nameChanged();
void enabledChanged();
void stateChanged();
void discoverableChanged();
void discoverableTimeoutChanged();
void discoveringChanged();
void pairableChanged();
void pairableTimeoutChanged();
private:
DBusBluezAdapterInterface* mInterface = nullptr;
ObjectModel<BluetoothDevice> mDevices {this};
// clang-format off
Q_OBJECT_BINDABLE_PROPERTY(BluetoothAdapter, QString, bName, &BluetoothAdapter::nameChanged);
Q_OBJECT_BINDABLE_PROPERTY(BluetoothAdapter, bool, bEnabled, &BluetoothAdapter::enabledChanged);
Q_OBJECT_BINDABLE_PROPERTY(BluetoothAdapter, BluetoothAdapterState::Enum, bState, &BluetoothAdapter::stateChanged);
Q_OBJECT_BINDABLE_PROPERTY(BluetoothAdapter, bool, bDiscoverable, &BluetoothAdapter::discoverableChanged);
Q_OBJECT_BINDABLE_PROPERTY(BluetoothAdapter, quint32, bDiscoverableTimeout, &BluetoothAdapter::discoverableTimeoutChanged);
Q_OBJECT_BINDABLE_PROPERTY(BluetoothAdapter, bool, bDiscovering, &BluetoothAdapter::discoveringChanged);
Q_OBJECT_BINDABLE_PROPERTY(BluetoothAdapter, bool, bPairable, &BluetoothAdapter::pairableChanged);
Q_OBJECT_BINDABLE_PROPERTY(BluetoothAdapter, quint32, bPairableTimeout, &BluetoothAdapter::pairableTimeoutChanged);
QS_DBUS_BINDABLE_PROPERTY_GROUP(BluetoothAdapter, properties);
QS_DBUS_PROPERTY_BINDING(BluetoothAdapter, pName, bName, properties, "Alias");
QS_DBUS_PROPERTY_BINDING(BluetoothAdapter, pEnabled, bEnabled, properties, "Powered");
QS_DBUS_PROPERTY_BINDING(BluetoothAdapter, pState, bState, properties, "PowerState");
QS_DBUS_PROPERTY_BINDING(BluetoothAdapter, pDiscoverable, bDiscoverable, properties, "Discoverable");
QS_DBUS_PROPERTY_BINDING(BluetoothAdapter, pDiscoverableTimeout, bDiscoverableTimeout, properties, "DiscoverableTimeout");
QS_DBUS_PROPERTY_BINDING(BluetoothAdapter, pDiscovering, bDiscovering, properties, "Discovering");
QS_DBUS_PROPERTY_BINDING(BluetoothAdapter, pPairable, bPairable, properties, "Pairable");
QS_DBUS_PROPERTY_BINDING(BluetoothAdapter, pPairableTimeout, bPairableTimeout, properties, "PairableTimeout");
// clang-format on
};
} // namespace qs::bluetooth
QDebug operator<<(QDebug debug, const qs::bluetooth::BluetoothAdapter* adapter);

168
src/bluetooth/bluez.cpp Normal file
View file

@ -0,0 +1,168 @@
#include "bluez.hpp"
#include <qcontainerfwd.h>
#include <qdbusconnection.h>
#include <qdbusextratypes.h>
#include <qlogging.h>
#include <qloggingcategory.h>
#include <qobject.h>
#include <qtmetamacros.h>
#include "../core/logcat.hpp"
#include "../dbus/dbus_objectmanager_types.hpp"
#include "../dbus/objectmanager.hpp"
#include "adapter.hpp"
#include "device.hpp"
namespace qs::bluetooth {
namespace {
QS_LOGGING_CATEGORY(logBluetooth, "quickshell.bluetooth", QtWarningMsg);
}
Bluez* Bluez::instance() {
static auto* instance = new Bluez();
return instance;
}
Bluez::Bluez() { this->init(); }
void Bluez::updateDefaultAdapter() {
const auto& adapters = this->mAdapters.valueList();
this->bDefaultAdapter = adapters.empty() ? nullptr : adapters.first();
}
void Bluez::init() {
qCDebug(logBluetooth) << "Connecting to BlueZ";
auto bus = QDBusConnection::systemBus();
if (!bus.isConnected()) {
qCWarning(logBluetooth) << "Could not connect to DBus. Bluetooth integration is not available.";
return;
}
this->objectManager = new qs::dbus::DBusObjectManager(this);
QObject::connect(
this->objectManager,
&qs::dbus::DBusObjectManager::interfacesAdded,
this,
&Bluez::onInterfacesAdded
);
QObject::connect(
this->objectManager,
&qs::dbus::DBusObjectManager::interfacesRemoved,
this,
&Bluez::onInterfacesRemoved
);
if (!this->objectManager->setInterface("org.bluez", "/", bus)) {
qCDebug(logBluetooth) << "BlueZ is not running. Bluetooth integration will not work.";
return;
}
}
void Bluez::onInterfacesAdded(
const QDBusObjectPath& path,
const DBusObjectManagerInterfaces& interfaces
) {
if (auto* adapter = this->mAdapterMap.value(path.path())) {
for (const auto& [interface, properties]: interfaces.asKeyValueRange()) {
adapter->addInterface(interface, properties);
}
} else if (auto* device = this->mDeviceMap.value(path.path())) {
for (const auto& [interface, properties]: interfaces.asKeyValueRange()) {
device->addInterface(interface, properties);
}
} else if (interfaces.contains("org.bluez.Adapter1")) {
auto* adapter = new BluetoothAdapter(path.path(), this);
if (!adapter->isValid()) {
qCWarning(logBluetooth) << "Adapter path is not valid, cannot track: " << device;
delete adapter;
return;
}
qCDebug(logBluetooth) << "Tracked new adapter" << adapter;
for (const auto& [interface, properties]: interfaces.asKeyValueRange()) {
adapter->addInterface(interface, properties);
}
for (auto* device: this->mDevices.valueList()) {
if (device->adapterPath() == path) {
adapter->devices()->insertObject(device);
qCDebug(logBluetooth) << "Added tracked device" << device << "to new adapter" << adapter;
emit device->adapterChanged();
}
}
this->mAdapterMap.insert(path.path(), adapter);
this->mAdapters.insertObject(adapter);
this->updateDefaultAdapter();
} else if (interfaces.contains("org.bluez.Device1")) {
auto* device = new BluetoothDevice(path.path(), this);
if (!device->isValid()) {
qCWarning(logBluetooth) << "Device path is not valid, cannot track: " << device;
delete device;
return;
}
qCDebug(logBluetooth) << "Tracked new device" << device;
for (const auto& [interface, properties]: interfaces.asKeyValueRange()) {
device->addInterface(interface, properties);
}
if (auto* adapter = device->adapter()) {
adapter->devices()->insertObject(device);
qCDebug(logBluetooth) << "Added device" << device << "to adapter" << adapter;
}
this->mDeviceMap.insert(path.path(), device);
this->mDevices.insertObject(device);
}
}
void Bluez::onInterfacesRemoved(const QDBusObjectPath& path, const QStringList& interfaces) {
if (auto* adapter = this->mAdapterMap.value(path.path())) {
if (interfaces.contains("org.bluez.Adapter1")) {
qCDebug(logBluetooth) << "Adapter removed:" << adapter;
this->mAdapterMap.remove(path.path());
this->mAdapters.removeObject(adapter);
this->updateDefaultAdapter();
delete adapter;
}
} else if (auto* device = this->mDeviceMap.value(path.path())) {
if (interfaces.contains("org.bluez.Device1")) {
qCDebug(logBluetooth) << "Device removed:" << device;
if (auto* adapter = device->adapter()) {
adapter->devices()->removeObject(device);
}
this->mDeviceMap.remove(path.path());
this->mDevices.removeObject(device);
delete device;
} else {
for (const auto& interface: interfaces) {
device->removeInterface(interface);
}
}
}
}
BluezQml::BluezQml() {
QObject::connect(
Bluez::instance(),
&Bluez::defaultAdapterChanged,
this,
&BluezQml::defaultAdapterChanged
);
}
} // namespace qs::bluetooth

98
src/bluetooth/bluez.hpp Normal file
View file

@ -0,0 +1,98 @@
#pragma once
#include <qcontainerfwd.h>
#include <qhash.h>
#include <qobject.h>
#include <qproperty.h>
#include <qqmlintegration.h>
#include <qtmetamacros.h>
#include "../core/doc.hpp"
#include "../core/model.hpp"
#include "../dbus/dbus_objectmanager_types.hpp"
#include "../dbus/objectmanager.hpp"
namespace qs::bluetooth {
class BluetoothAdapter;
class BluetoothDevice;
class Bluez: public QObject {
Q_OBJECT;
public:
[[nodiscard]] ObjectModel<BluetoothAdapter>* adapters() { return &this->mAdapters; }
[[nodiscard]] ObjectModel<BluetoothDevice>* devices() { return &this->mDevices; }
[[nodiscard]] BluetoothAdapter* adapter(const QString& path) {
return this->mAdapterMap.value(path);
}
static Bluez* instance();
signals:
void defaultAdapterChanged();
private slots:
void
onInterfacesAdded(const QDBusObjectPath& path, const DBusObjectManagerInterfaces& interfaces);
void onInterfacesRemoved(const QDBusObjectPath& path, const QStringList& interfaces);
void updateDefaultAdapter();
private:
explicit Bluez();
void init();
qs::dbus::DBusObjectManager* objectManager = nullptr;
QHash<QString, BluetoothAdapter*> mAdapterMap;
QHash<QString, BluetoothDevice*> mDeviceMap;
ObjectModel<BluetoothAdapter> mAdapters {this};
ObjectModel<BluetoothDevice> mDevices {this};
public:
Q_OBJECT_BINDABLE_PROPERTY(
Bluez,
BluetoothAdapter*,
bDefaultAdapter,
&Bluez::defaultAdapterChanged
);
};
///! Bluetooth manager
/// Provides access to bluetooth devices and adapters.
class BluezQml: public QObject {
Q_OBJECT;
QML_NAMED_ELEMENT(Bluetooth);
QML_SINGLETON;
// clang-format off
/// The default bluetooth adapter. Usually there is only one.
Q_PROPERTY(BluetoothAdapter* defaultAdapter READ default NOTIFY defaultAdapterChanged BINDABLE bindableDefaultAdapter);
QSDOC_TYPE_OVERRIDE(ObjectModel<qs::bluetooth::BluetoothAdapter>*);
/// A list of all bluetooth adapters. See @@defaultAdapter for the default.
Q_PROPERTY(UntypedObjectModel* adapters READ adapters CONSTANT);
QSDOC_TYPE_OVERRIDE(ObjectModel<qs::bluetooth::BluetoothDevice>*);
/// A list of all connected bluetooth devices across all adapters.
/// See @@BluetoothAdapter.devices for the devices connected to a single adapter.
Q_PROPERTY(UntypedObjectModel* devices READ devices CONSTANT);
// clang-format on
signals:
void defaultAdapterChanged();
public:
explicit BluezQml();
[[nodiscard]] static ObjectModel<BluetoothAdapter>* adapters() {
return Bluez::instance()->adapters();
}
[[nodiscard]] static ObjectModel<BluetoothDevice>* devices() {
return Bluez::instance()->devices();
}
[[nodiscard]] static QBindable<BluetoothAdapter*> bindableDefaultAdapter() {
return &Bluez::instance()->bDefaultAdapter;
}
};
} // namespace qs::bluetooth

319
src/bluetooth/device.cpp Normal file
View file

@ -0,0 +1,319 @@
#include "device.hpp"
#include <qcontainerfwd.h>
#include <qdbusconnection.h>
#include <qdbuspendingcall.h>
#include <qdbuspendingreply.h>
#include <qdebug.h>
#include <qlogging.h>
#include <qloggingcategory.h>
#include <qstring.h>
#include <qstringliteral.h>
#include <qtmetamacros.h>
#include <qtypes.h>
#include "../core/logcat.hpp"
#include "../dbus/properties.hpp"
#include "adapter.hpp"
#include "bluez.hpp"
#include "dbus_device.h"
namespace qs::bluetooth {
namespace {
QS_LOGGING_CATEGORY(logDevice, "quickshell.bluetooth.device", QtWarningMsg);
}
QString BluetoothDeviceState::toString(BluetoothDeviceState::Enum state) {
switch (state) {
case BluetoothDeviceState::Disconnected: return QStringLiteral("Disconnected");
case BluetoothDeviceState::Connected: return QStringLiteral("Connected");
case BluetoothDeviceState::Disconnecting: return QStringLiteral("Disconnecting");
case BluetoothDeviceState::Connecting: return QStringLiteral("Connecting");
default: return QStringLiteral("Unknown");
}
}
BluetoothDevice::BluetoothDevice(const QString& path, QObject* parent): QObject(parent) {
this->mInterface =
new DBusBluezDeviceInterface("org.bluez", path, QDBusConnection::systemBus(), this);
if (!this->mInterface->isValid()) {
qCWarning(logDevice) << "Could not create DBus interface for device at" << path;
delete this->mInterface;
this->mInterface = nullptr;
return;
}
this->properties.setInterface(this->mInterface);
}
BluetoothAdapter* BluetoothDevice::adapter() const {
return Bluez::instance()->adapter(this->bAdapterPath.value().path());
}
void BluetoothDevice::setConnected(bool connected) {
if (connected == this->bConnected) return;
if (connected) {
this->connect();
} else {
this->disconnect();
}
}
void BluetoothDevice::setTrusted(bool trusted) {
if (trusted == this->bTrusted) return;
this->bTrusted = trusted;
this->pTrusted.write();
}
void BluetoothDevice::setBlocked(bool blocked) {
if (blocked == this->bBlocked) return;
this->bBlocked = blocked;
this->pBlocked.write();
}
void BluetoothDevice::setName(const QString& name) {
if (name == this->bName) return;
this->bName = name;
this->pName.write();
}
void BluetoothDevice::setWakeAllowed(bool wakeAllowed) {
if (wakeAllowed == this->bWakeAllowed) return;
this->bWakeAllowed = wakeAllowed;
this->pWakeAllowed.write();
}
void BluetoothDevice::connect() {
if (this->bConnected) {
qCCritical(logDevice) << "Device" << this << "is already connected";
return;
}
if (this->bState == BluetoothDeviceState::Connecting) {
qCCritical(logDevice) << "Device" << this << "is already connecting";
return;
}
qCDebug(logDevice) << "Connecting to device" << this;
this->bState = BluetoothDeviceState::Connecting;
auto reply = this->mInterface->Connect();
auto* watcher = new QDBusPendingCallWatcher(reply, this);
QObject::connect(
watcher,
&QDBusPendingCallWatcher::finished,
this,
[this](QDBusPendingCallWatcher* watcher) {
const QDBusPendingReply<> reply = *watcher;
if (reply.isError()) {
qCWarning(logDevice).nospace()
<< "Failed to connect to device " << this << ": " << reply.error().message();
this->bState = this->bConnected ? BluetoothDeviceState::Connected
: BluetoothDeviceState::Disconnected;
} else {
qCDebug(logDevice) << "Successfully connected to to device" << this;
}
delete watcher;
}
);
}
void BluetoothDevice::disconnect() {
if (!this->bConnected) {
qCCritical(logDevice) << "Device" << this << "is already disconnected";
return;
}
if (this->bState == BluetoothDeviceState::Disconnecting) {
qCCritical(logDevice) << "Device" << this << "is already disconnecting";
return;
}
qCDebug(logDevice) << "Disconnecting from device" << this;
this->bState = BluetoothDeviceState::Disconnecting;
auto reply = this->mInterface->Disconnect();
auto* watcher = new QDBusPendingCallWatcher(reply, this);
QObject::connect(
watcher,
&QDBusPendingCallWatcher::finished,
this,
[this](QDBusPendingCallWatcher* watcher) {
const QDBusPendingReply<> reply = *watcher;
if (reply.isError()) {
qCWarning(logDevice).nospace()
<< "Failed to disconnect from device " << this << ": " << reply.error().message();
this->bState = this->bConnected ? BluetoothDeviceState::Connected
: BluetoothDeviceState::Disconnected;
} else {
qCDebug(logDevice) << "Successfully disconnected from from device" << this;
}
delete watcher;
}
);
}
void BluetoothDevice::pair() {
if (this->bPaired) {
qCCritical(logDevice) << "Device" << this << "is already paired";
return;
}
if (this->bPairing) {
qCCritical(logDevice) << "Device" << this << "is already pairing";
return;
}
qCDebug(logDevice) << "Pairing with device" << this;
this->bPairing = true;
auto reply = this->mInterface->Pair();
auto* watcher = new QDBusPendingCallWatcher(reply, this);
QObject::connect(
watcher,
&QDBusPendingCallWatcher::finished,
this,
[this](QDBusPendingCallWatcher* watcher) {
const QDBusPendingReply<> reply = *watcher;
if (reply.isError()) {
qCWarning(logDevice).nospace()
<< "Failed to pair with device " << this << ": " << reply.error().message();
} else {
qCDebug(logDevice) << "Successfully initiated pairing with device" << this;
}
this->bPairing = false;
delete watcher;
}
);
}
void BluetoothDevice::cancelPair() {
if (!this->bPairing) {
qCCritical(logDevice) << "Device" << this << "is not currently pairing";
return;
}
qCDebug(logDevice) << "Cancelling pairing with device" << this;
auto reply = this->mInterface->CancelPairing();
auto* watcher = new QDBusPendingCallWatcher(reply, this);
QObject::connect(
watcher,
&QDBusPendingCallWatcher::finished,
this,
[this](QDBusPendingCallWatcher* watcher) {
const QDBusPendingReply<> reply = *watcher;
if (reply.isError()) {
qCWarning(logDevice) << "Failed to cancel pairing with device" << this << ":"
<< reply.error().message();
} else {
qCDebug(logDevice) << "Successfully cancelled pairing with device" << this;
}
this->bPairing = false;
delete watcher;
}
);
}
void BluetoothDevice::forget() {
if (!this->mInterface || !this->mInterface->isValid()) {
qCCritical(logDevice) << "Cannot forget - device interface is invalid";
return;
}
if (auto* adapter = Bluez::instance()->adapter(this->bAdapterPath.value().path())) {
qCDebug(logDevice) << "Forgetting device" << this << "via adapter" << adapter;
adapter->removeDevice(this->path());
} else {
qCCritical(logDevice) << "Could not find adapter for path" << this->bAdapterPath.value().path()
<< "to forget from";
}
}
void BluetoothDevice::addInterface(const QString& interface, const QVariantMap& properties) {
if (interface == "org.bluez.Device1") {
this->properties.updatePropertySet(properties, false);
qCDebug(logDevice) << "Updated Device properties for" << this;
} else if (interface == "org.bluez.Battery1") {
if (!this->mBatteryInterface) {
this->mBatteryInterface = new QDBusInterface(
"org.bluez",
this->path(),
"org.bluez.Battery1",
QDBusConnection::systemBus(),
this
);
if (!this->mBatteryInterface->isValid()) {
qCWarning(logDevice) << "Could not create Battery interface for device at" << this;
delete this->mBatteryInterface;
this->mBatteryInterface = nullptr;
return;
}
}
this->batteryProperties.setInterface(this->mBatteryInterface);
this->batteryProperties.updatePropertySet(properties, false);
emit this->batteryAvailableChanged();
qCDebug(logDevice) << "Updated Battery properties for" << this;
}
}
void BluetoothDevice::removeInterface(const QString& interface) {
if (interface == "org.bluez.Battery1" && this->mBatteryInterface) {
this->batteryProperties.setInterface(nullptr);
delete this->mBatteryInterface;
this->mBatteryInterface = nullptr;
this->bBattery = 0;
emit this->batteryAvailableChanged();
qCDebug(logDevice) << "Battery interface removed from device" << this;
}
}
void BluetoothDevice::onConnectedChanged() {
this->bState =
this->bConnected ? BluetoothDeviceState::Connected : BluetoothDeviceState::Disconnected;
emit this->connectedChanged();
}
} // namespace qs::bluetooth
namespace qs::dbus {
using namespace qs::bluetooth;
DBusResult<qreal> DBusDataTransform<BatteryPercentage>::fromWire(quint8 percentage) {
return DBusResult(percentage * 0.01);
}
} // namespace qs::dbus
QDebug operator<<(QDebug debug, const qs::bluetooth::BluetoothDevice* device) {
auto saver = QDebugStateSaver(debug);
if (device) {
debug.nospace() << "BluetoothDevice(" << static_cast<const void*>(device)
<< ", path=" << device->path() << ")";
} else {
debug << "BluetoothDevice(nullptr)";
}
return debug;
}

225
src/bluetooth/device.hpp Normal file
View file

@ -0,0 +1,225 @@
#pragma once
#include <qobject.h>
#include <qqmlintegration.h>
#include <qtmetamacros.h>
#include <qtypes.h>
#include "../dbus/properties.hpp"
#include "dbus_device.h"
namespace qs::bluetooth {
///! Connection state of a Bluetooth device.
class BluetoothDeviceState: public QObject {
Q_OBJECT;
QML_ELEMENT;
QML_SINGLETON;
public:
enum Enum : quint8 {
/// The device is not connected.
Disconnected = 0,
/// The device is connected.
Connected = 1,
/// The device is disconnecting.
Disconnecting = 2,
/// The device is connecting.
Connecting = 3,
};
Q_ENUM(Enum);
Q_INVOKABLE static QString toString(BluetoothDeviceState::Enum state);
};
struct BatteryPercentage {};
} // namespace qs::bluetooth
namespace qs::dbus {
template <>
struct DBusDataTransform<qs::bluetooth::BatteryPercentage> {
using Wire = quint8;
using Data = qreal;
static DBusResult<Data> fromWire(Wire percentage);
};
} // namespace qs::dbus
namespace qs::bluetooth {
class BluetoothAdapter;
///! A tracked Bluetooth device.
class BluetoothDevice: public QObject {
Q_OBJECT;
QML_ELEMENT;
QML_UNCREATABLE("");
// clang-format off
/// MAC address of the device.
Q_PROPERTY(QString address READ default NOTIFY addressChanged BINDABLE bindableAddress);
/// The name of the Bluetooth device. This property may be written to create an alias, or set to
/// an empty string to fall back to the device provided name.
///
/// See @@deviceName for the name provided by the device.
Q_PROPERTY(QString name READ name WRITE setName NOTIFY nameChanged);
/// The name of the Bluetooth device, ignoring user provided aliases. See also @@name
/// which returns a user provided alias if set.
Q_PROPERTY(QString deviceName READ default NOTIFY deviceNameChanged BINDABLE bindableDeviceName);
/// System icon representing the device type. Use @@Quickshell.Quickshell.iconPath() to display this in an image.
Q_PROPERTY(QString icon READ default NOTIFY iconChanged BINDABLE bindableIcon);
/// Connection state of the device.
Q_PROPERTY(BluetoothDeviceState::Enum state READ default NOTIFY stateChanged BINDABLE bindableState);
/// True if the device is currently connected to the computer.
///
/// Setting this property is equivalent to calling @@connect() and @@disconnect().
///
/// > [!NOTE] @@state provides more detailed information if required.
Q_PROPERTY(bool connected READ connected WRITE setConnected NOTIFY connectedChanged);
/// True if the device is paired to the computer.
///
/// > [!NOTE] @@pair() can be used to pair a device, however you must @@forget() the device to unpair it.
Q_PROPERTY(bool paired READ default NOTIFY pairedChanged BINDABLE bindablePaired);
/// True if pairing information is stored for future connections.
Q_PROPERTY(bool bonded READ default NOTIFY bondedChanged BINDABLE bindableBonded);
/// True if the device is currently being paired.
///
/// > [!NOTE] @@cancelPair() can be used to cancel the pairing process.
Q_PROPERTY(bool pairing READ pairing NOTIFY pairingChanged);
/// True if the device is considered to be trusted by the system.
/// Trusted devices are allowed to reconnect themselves to the system without intervention.
Q_PROPERTY(bool trusted READ trusted WRITE setTrusted NOTIFY trustedChanged);
/// True if the device is blocked from connecting.
/// If a device is blocked, any connection attempts will be immediately rejected by the system.
Q_PROPERTY(bool blocked READ blocked WRITE setBlocked NOTIFY blockedChanged);
/// True if the device is allowed to wake up the host system from suspend.
Q_PROPERTY(bool wakeAllowed READ wakeAllowed WRITE setWakeAllowed NOTIFY wakeAllowedChanged);
/// True if the connected device reports its battery level. Battery level can be accessed via @@battery.
Q_PROPERTY(bool batteryAvailable READ batteryAvailable NOTIFY batteryAvailableChanged);
/// Battery level of the connected device, from `0.0` to `1.0`. Only valid if @@batteryAvailable is true.
Q_PROPERTY(qreal battery READ default NOTIFY batteryChanged BINDABLE bindableBattery);
/// The Bluetooth adapter this device belongs to.
Q_PROPERTY(BluetoothAdapter* adapter READ adapter NOTIFY adapterChanged);
/// DBus path of the device under the `org.bluez` system service.
Q_PROPERTY(QString dbusPath READ path CONSTANT);
// clang-format on
public:
explicit BluetoothDevice(const QString& path, QObject* parent = nullptr);
/// Attempt to connect to the device.
Q_INVOKABLE void connect();
/// Disconnect from the device.
Q_INVOKABLE void disconnect();
/// Attempt to pair the device.
///
/// > [!NOTE] @@paired and @@pairing return the current pairing status of the device.
Q_INVOKABLE void pair();
/// Cancel an active pairing attempt.
Q_INVOKABLE void cancelPair();
/// Forget the device.
Q_INVOKABLE void forget();
[[nodiscard]] bool isValid() const { return this->mInterface && this->mInterface->isValid(); }
[[nodiscard]] QString path() const {
return this->mInterface ? this->mInterface->path() : QString();
}
[[nodiscard]] bool batteryAvailable() const { return this->mBatteryInterface != nullptr; }
[[nodiscard]] BluetoothAdapter* adapter() const;
[[nodiscard]] QDBusObjectPath adapterPath() const { return this->bAdapterPath.value(); }
[[nodiscard]] bool connected() const { return this->bConnected; }
void setConnected(bool connected);
[[nodiscard]] bool trusted() const { return this->bTrusted; }
void setTrusted(bool trusted);
[[nodiscard]] bool blocked() const { return this->bBlocked; }
void setBlocked(bool blocked);
[[nodiscard]] QString name() const { return this->bName; }
void setName(const QString& name);
[[nodiscard]] bool wakeAllowed() const { return this->bWakeAllowed; }
void setWakeAllowed(bool wakeAllowed);
[[nodiscard]] bool pairing() const { return this->bPairing; }
[[nodiscard]] QBindable<QString> bindableAddress() { return &this->bAddress; }
[[nodiscard]] QBindable<QString> bindableDeviceName() { return &this->bDeviceName; }
[[nodiscard]] QBindable<QString> bindableName() { return &this->bName; }
[[nodiscard]] QBindable<bool> bindableConnected() { return &this->bConnected; }
[[nodiscard]] QBindable<bool> bindablePaired() { return &this->bPaired; }
[[nodiscard]] QBindable<bool> bindableBonded() { return &this->bBonded; }
[[nodiscard]] QBindable<bool> bindableTrusted() { return &this->bTrusted; }
[[nodiscard]] QBindable<bool> bindableBlocked() { return &this->bBlocked; }
[[nodiscard]] QBindable<bool> bindableWakeAllowed() { return &this->bWakeAllowed; }
[[nodiscard]] QBindable<QString> bindableIcon() { return &this->bIcon; }
[[nodiscard]] QBindable<qreal> bindableBattery() { return &this->bBattery; }
[[nodiscard]] QBindable<BluetoothDeviceState::Enum> bindableState() { return &this->bState; }
void addInterface(const QString& interface, const QVariantMap& properties);
void removeInterface(const QString& interface);
signals:
void addressChanged();
void deviceNameChanged();
void nameChanged();
void connectedChanged();
void stateChanged();
void pairedChanged();
void bondedChanged();
void pairingChanged();
void trustedChanged();
void blockedChanged();
void wakeAllowedChanged();
void iconChanged();
void batteryAvailableChanged();
void batteryChanged();
void adapterChanged();
private:
void onConnectedChanged();
DBusBluezDeviceInterface* mInterface = nullptr;
QDBusInterface* mBatteryInterface = nullptr;
// clang-format off
Q_OBJECT_BINDABLE_PROPERTY(BluetoothDevice, QString, bAddress, &BluetoothDevice::addressChanged);
Q_OBJECT_BINDABLE_PROPERTY(BluetoothDevice, QString, bDeviceName, &BluetoothDevice::deviceNameChanged);
Q_OBJECT_BINDABLE_PROPERTY(BluetoothDevice, QString, bName, &BluetoothDevice::nameChanged);
Q_OBJECT_BINDABLE_PROPERTY(BluetoothDevice, bool, bConnected, &BluetoothDevice::onConnectedChanged);
Q_OBJECT_BINDABLE_PROPERTY(BluetoothDevice, bool, bPaired, &BluetoothDevice::pairedChanged);
Q_OBJECT_BINDABLE_PROPERTY(BluetoothDevice, bool, bBonded, &BluetoothDevice::bondedChanged);
Q_OBJECT_BINDABLE_PROPERTY(BluetoothDevice, bool, bTrusted, &BluetoothDevice::trustedChanged);
Q_OBJECT_BINDABLE_PROPERTY(BluetoothDevice, bool, bBlocked, &BluetoothDevice::blockedChanged);
Q_OBJECT_BINDABLE_PROPERTY(BluetoothDevice, bool, bWakeAllowed, &BluetoothDevice::wakeAllowedChanged);
Q_OBJECT_BINDABLE_PROPERTY(BluetoothDevice, QString, bIcon, &BluetoothDevice::iconChanged);
Q_OBJECT_BINDABLE_PROPERTY(BluetoothDevice, QDBusObjectPath, bAdapterPath);
Q_OBJECT_BINDABLE_PROPERTY(BluetoothDevice, qreal, bBattery, &BluetoothDevice::batteryChanged);
Q_OBJECT_BINDABLE_PROPERTY(BluetoothDevice, BluetoothDeviceState::Enum, bState, &BluetoothDevice::stateChanged);
Q_OBJECT_BINDABLE_PROPERTY(BluetoothDevice, bool, bPairing, &BluetoothDevice::pairingChanged);
QS_DBUS_BINDABLE_PROPERTY_GROUP(BluetoothDevice, properties);
QS_DBUS_PROPERTY_BINDING(BluetoothDevice, pAddress, bAddress, properties, "Address");
QS_DBUS_PROPERTY_BINDING(BluetoothDevice, pDeviceName, bDeviceName, properties, "Name");
QS_DBUS_PROPERTY_BINDING(BluetoothDevice, pName, bName, properties, "Alias");
QS_DBUS_PROPERTY_BINDING(BluetoothDevice, pConnected, bConnected, properties, "Connected");
QS_DBUS_PROPERTY_BINDING(BluetoothDevice, pPaired, bPaired, properties, "Paired");
QS_DBUS_PROPERTY_BINDING(BluetoothDevice, pBonded, bBonded, properties, "Bonded");
QS_DBUS_PROPERTY_BINDING(BluetoothDevice, pTrusted, bTrusted, properties, "Trusted");
QS_DBUS_PROPERTY_BINDING(BluetoothDevice, pBlocked, bBlocked, properties, "Blocked");
QS_DBUS_PROPERTY_BINDING(BluetoothDevice, pWakeAllowed, bWakeAllowed, properties, "WakeAllowed");
QS_DBUS_PROPERTY_BINDING(BluetoothDevice, pIcon, bIcon, properties, "Icon");
QS_DBUS_PROPERTY_BINDING(BluetoothDevice, pAdapterPath, bAdapterPath, properties, "Adapter");
QS_DBUS_BINDABLE_PROPERTY_GROUP(BluetoothDevice, batteryProperties);
QS_DBUS_PROPERTY_BINDING(BluetoothDevice, BatteryPercentage, pBattery, bBattery, batteryProperties, "Percentage", true);
// clang-format on
};
} // namespace qs::bluetooth
QDebug operator<<(QDebug debug, const qs::bluetooth::BluetoothDevice* device);

12
src/bluetooth/module.md Normal file
View file

@ -0,0 +1,12 @@
name = "Quickshell.Bluetooth"
description = "Bluetooth API"
headers = [
"bluez.hpp",
"adapter.hpp",
"device.hpp",
]
-----
This module exposes Bluetooth management APIs provided by the BlueZ DBus interface.
Both DBus and BlueZ must be running to use it.
See the @@Quickshell.Bluetooth.Bluetooth singleton.

View file

@ -0,0 +1,9 @@
<node>
<interface name="org.bluez.Adapter1">
<method name="StartDiscovery"/>
<method name="StopDiscovery"/>
<method name="RemoveDevice">
<arg name="device" type="o"/>
</method>
</interface>
</node>

View file

@ -0,0 +1,8 @@
<node>
<interface name="org.bluez.Device1">
<method name="Connect"/>
<method name="Disconnect"/>
<method name="Pair"/>
<method name="CancelPairing"/>
</interface>
</node>

View file

@ -0,0 +1,200 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import Quickshell.Widgets
import Quickshell.Bluetooth
FloatingWindow {
color: contentItem.palette.window
ListView {
anchors.fill: parent
anchors.margins: 5
model: Bluetooth.adapters
delegate: WrapperRectangle {
width: parent.width
color: "transparent"
border.color: palette.button
border.width: 1
margin: 5
ColumnLayout {
Label { text: `Adapter: ${modelData.name} (${modelData.adapterId})` }
RowLayout {
Layout.fillWidth: true
CheckBox {
text: "Enable"
checked: modelData.enabled
onToggled: modelData.enabled = checked
}
Label {
color: modelData.state === BluetoothAdapterState.Blocked ? palette.errorText : palette.placeholderText
text: BluetoothAdapterState.toString(modelData.state)
}
CheckBox {
text: "Discoverable"
checked: modelData.discoverable
onToggled: modelData.discoverable = checked
}
CheckBox {
text: "Discovering"
checked: modelData.discovering
onToggled: modelData.discovering = checked
}
CheckBox {
text: "Pairable"
checked: modelData.pairable
onToggled: modelData.pairable = checked
}
}
RowLayout {
Layout.fillWidth: true
Label { text: "Discoverable timeout:" }
SpinBox {
from: 0
to: 3600
value: modelData.discoverableTimeout
onValueModified: modelData.discoverableTimeout = value
textFromValue: time => time === 0 ? "∞" : time + "s"
}
Label { text: "Pairable timeout:" }
SpinBox {
from: 0
to: 3600
value: modelData.pairableTimeout
onValueModified: modelData.pairableTimeout = value
textFromValue: time => time === 0 ? "∞" : time + "s"
}
}
Repeater {
model: modelData.devices
WrapperRectangle {
Layout.fillWidth: true
color: palette.button
border.color: palette.mid
border.width: 1
margin: 5
RowLayout {
ColumnLayout {
Layout.fillWidth: true
RowLayout {
IconImage {
Layout.fillHeight: true
implicitWidth: height
source: Quickshell.iconPath(modelData.icon)
}
TextField {
text: modelData.name
font.bold: true
background: null
readOnly: false
selectByMouse: true
onEditingFinished: modelData.name = text
}
Label {
visible: modelData.name && modelData.name !== modelData.deviceName
text: `(${modelData.deviceName})`
color: palette.placeholderText
}
}
RowLayout {
Label {
text: modelData.address
color: palette.placeholderText
}
Label {
visible: modelData.batteryAvailable
text: `| Battery: ${Math.round(modelData.battery * 100)}%`
color: palette.placeholderText
}
}
RowLayout {
Label {
text: BluetoothDeviceState.toString(modelData.state)
color: modelData.connected ? palette.link : palette.placeholderText
}
Label {
text: modelData.pairing ? "Pairing" : (modelData.paired ? "Paired" : "Not Paired")
color: modelData.paired || modelData.pairing ? palette.link : palette.placeholderText
}
Label {
visible: modelData.bonded
text: "| Bonded"
color: palette.link
}
CheckBox {
text: "Trusted"
checked: modelData.trusted
onToggled: modelData.trusted = checked
}
CheckBox {
text: "Blocked"
checked: modelData.blocked
onToggled: modelData.blocked = checked
}
CheckBox {
text: "Wake Allowed"
checked: modelData.wakeAllowed
onToggled: modelData.wakeAllowed = checked
}
}
}
ColumnLayout {
Layout.alignment: Qt.AlignRight
Button {
Layout.alignment: Qt.AlignRight
text: modelData.connected ? "Disconnect" : "Connect"
onClicked: modelData.connected = !modelData.connected
}
Button {
Layout.alignment: Qt.AlignRight
text: modelData.pairing ? "Cancel" : (modelData.paired ? "Forget" : "Pair")
onClicked: {
if (modelData.pairing) {
modelData.cancelPair();
} else if (modelData.paired) {
modelData.forget();
} else {
modelData.pair();
}
}
}
}
}
}
}
}
}
}
}

26
src/build/CMakeLists.txt Normal file
View file

@ -0,0 +1,26 @@
add_library(quickshell-build INTERFACE)
if (NOT DEFINED GIT_REVISION)
execute_process(
COMMAND git rev-parse HEAD
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
OUTPUT_VARIABLE GIT_REVISION
OUTPUT_STRIP_TRAILING_WHITESPACE
)
endif()
if (CRASH_REPORTER)
set(CRASH_REPORTER_DEF 1)
else()
set(CRASH_REPORTER_DEF 0)
endif()
if (DISTRIBUTOR_DEBUGINFO_AVAILABLE)
set(DEBUGINFO_AVAILABLE 1)
else()
set(DEBUGINFO_AVAILABLE 0)
endif()
configure_file(build.hpp.in build.hpp @ONLY ESCAPE_QUOTES)
target_include_directories(quickshell-build INTERFACE ${CMAKE_CURRENT_BINARY_DIR})

12
src/build/build.hpp.in Normal file
View file

@ -0,0 +1,12 @@
#pragma once
// NOLINTBEGIN
#define GIT_REVISION "@GIT_REVISION@"
#define DISTRIBUTOR "@DISTRIBUTOR@"
#define DISTRIBUTOR_DEBUGINFO_AVAILABLE @DEBUGINFO_AVAILABLE@
#define CRASH_REPORTER @CRASH_REPORTER_DEF@
#define BUILD_TYPE "@CMAKE_BUILD_TYPE@"
#define COMPILER "@CMAKE_CXX_COMPILER_ID@ (@CMAKE_CXX_COMPILER_VERSION@)"
#define COMPILE_FLAGS "@CMAKE_CXX_FLAGS@"
#define BUILD_CONFIGURATION "@QS_BUILD_OPTIONS@"
// NOLINTEND

View file

@ -1,23 +1,62 @@
qt_add_executable(quickshell
main.cpp
qt_add_library(quickshell-core STATIC
plugin.cpp
shell.cpp
variants.cpp
rootwrapper.cpp
proxywindow.cpp
reload.cpp
rootwrapper.cpp
qmlglobal.cpp
qmlscreen.cpp
watcher.cpp
region.cpp
persistentprops.cpp
windowinterface.cpp
floatingwindow.cpp
panelinterface.cpp
singleton.cpp
generation.cpp
scan.cpp
qsintercept.cpp
incubator.cpp
lazyloader.cpp
easingcurve.cpp
iconimageprovider.cpp
imageprovider.cpp
transformwatcher.cpp
boundcomponent.cpp
model.cpp
elapsedtimer.cpp
desktopentry.cpp
objectrepeater.cpp
platformmenu.cpp
qsmenu.cpp
retainable.cpp
popupanchor.cpp
types.cpp
qsmenuanchor.cpp
clock.cpp
logging.cpp
paths.cpp
instanceinfo.cpp
common.cpp
iconprovider.cpp
scriptmodel.cpp
colorquantizer.cpp
toolsupport.cpp
)
set_source_files_properties(main.cpp PROPERTIES COMPILE_DEFINITIONS GIT_REVISION="${GIT_REVISION}")
qt_add_qml_module(quickshell URI Quickshell VERSION 0.1)
qt_add_qml_module(quickshell-core
URI Quickshell
VERSION 0.1
DEPENDENCIES QtQuick
OPTIONAL_IMPORTS Quickshell._Window
DEFAULT_IMPORTS Quickshell._Window
)
target_link_libraries(quickshell PRIVATE ${QT_DEPS})
install_qml_module(quickshell-core)
target_link_libraries(quickshell-core PRIVATE Qt::Quick Qt::Widgets)
qs_module_pch(quickshell-core SET large)
target_link_libraries(quickshell PRIVATE quickshell-coreplugin)
if (BUILD_TESTING)
add_subdirectory(test)
endif()

258
src/core/boundcomponent.cpp Normal file
View file

@ -0,0 +1,258 @@
#include "boundcomponent.hpp"
#include <utility>
#include <qcontainerfwd.h>
#include <qlogging.h>
#include <qmetaobject.h>
#include <qobject.h>
#include <qobjectdefs.h>
#include <qqmlcomponent.h>
#include <qqmlcontext.h>
#include <qqmlengine.h>
#include <qqmlerror.h>
#include <qquickitem.h>
#include <qtmetamacros.h>
#include "incubator.hpp"
QObject* BoundComponent::item() const { return this->object; }
QQmlComponent* BoundComponent::sourceComponent() const { return this->mComponent; }
void BoundComponent::setSourceComponent(QQmlComponent* component) {
if (component == this->mComponent) return;
if (this->componentCompleted) {
qWarning() << "BoundComponent.component cannot be set after creation";
return;
}
this->disconnectComponent();
this->ownsComponent = false;
this->mComponent = component;
if (component != nullptr) {
QObject::connect(component, &QObject::destroyed, this, &BoundComponent::onComponentDestroyed);
}
emit this->sourceComponentChanged();
}
void BoundComponent::disconnectComponent() {
if (this->mComponent == nullptr) return;
if (this->ownsComponent) {
delete this->mComponent;
} else {
QObject::disconnect(this->mComponent, nullptr, this, nullptr);
}
this->mComponent = nullptr;
}
void BoundComponent::onComponentDestroyed() { this->mComponent = nullptr; }
QString BoundComponent::source() const { return this->mSource; }
void BoundComponent::setSource(QString source) {
if (source == this->mSource) return;
if (this->componentCompleted) {
qWarning() << "BoundComponent.url cannot be set after creation";
return;
}
auto* context = QQmlEngine::contextForObject(this);
auto* component = new QQmlComponent(context->engine(), context->resolvedUrl(source), this);
if (component->isError()) {
qWarning() << component->errorString().toStdString().c_str();
delete component;
} else {
this->disconnectComponent();
this->ownsComponent = true;
this->mSource = std::move(source);
this->mComponent = component;
emit this->sourceChanged();
emit this->sourceComponentChanged();
}
}
bool BoundComponent::bindValues() const { return this->mBindValues; }
void BoundComponent::setBindValues(bool bindValues) {
if (this->componentCompleted) {
qWarning() << "BoundComponent.bindValues cannot be set after creation";
return;
}
this->mBindValues = bindValues;
emit this->bindValuesChanged();
}
void BoundComponent::componentComplete() {
this->QQuickItem::componentComplete();
this->componentCompleted = true;
this->tryCreate();
}
void BoundComponent::tryCreate() {
if (this->mComponent == nullptr) {
qWarning() << "BoundComponent has no component";
return;
}
auto initialProperties = QVariantMap();
const auto* metaObject = this->metaObject();
for (auto i = metaObject->propertyOffset(); i < metaObject->propertyCount(); i++) {
const auto prop = metaObject->property(i);
if (prop.isReadable()) {
initialProperties.insert(prop.name(), prop.read(this));
}
}
this->incubator = new QsQmlIncubator(QsQmlIncubator::AsynchronousIfNested, this);
this->incubator->setInitialProperties(initialProperties);
// clang-format off
QObject::connect(this->incubator, &QsQmlIncubator::completed, this, &BoundComponent::onIncubationCompleted);
QObject::connect(this->incubator, &QsQmlIncubator::failed, this, &BoundComponent::onIncubationFailed);
// clang-format on
this->mComponent->create(*this->incubator, QQmlEngine::contextForObject(this));
}
void BoundComponent::onIncubationCompleted() {
this->object = this->incubator->object();
delete this->incubator;
this->disconnectComponent();
this->object->setParent(this);
this->mItem = qobject_cast<QQuickItem*>(this->object);
const auto* metaObject = this->metaObject();
const auto* objectMetaObject = this->object->metaObject();
if (this->mBindValues) {
for (auto i = metaObject->propertyOffset(); i < metaObject->propertyCount(); i++) {
const auto prop = metaObject->property(i);
if (prop.isReadable() && prop.hasNotifySignal()) {
const auto objectPropIndex = objectMetaObject->indexOfProperty(prop.name());
if (objectPropIndex == -1) {
qWarning() << "property" << prop.name()
<< "defined on BoundComponent but not on its contained object.";
continue;
}
const auto objectProp = objectMetaObject->property(objectPropIndex);
if (objectProp.isWritable()) {
auto* proxy = new BoundComponentPropertyProxy(this, this->object, prop, objectProp);
proxy->onNotified(); // any changes that might've happened before connection
} else {
qWarning() << "property" << prop.name()
<< "defined on BoundComponent is not writable for its contained object.";
}
}
}
}
for (auto i = metaObject->methodOffset(); i < metaObject->methodCount(); i++) {
const auto method = metaObject->method(i);
if (method.name().startsWith("on") && method.name().length() > 2) {
auto sig = QString(method.methodSignature()).sliced(2);
if (!sig[0].isUpper()) continue;
sig[0] = sig[0].toLower();
auto name = sig.sliced(0, sig.indexOf('('));
auto mostViableSignal = QMetaMethod();
for (auto i = 0; i < objectMetaObject->methodCount(); i++) {
const auto method = objectMetaObject->method(i);
if (method.methodSignature() == sig) {
mostViableSignal = method;
break;
}
if (method.name() == name) {
if (mostViableSignal.isValid()) {
qWarning() << "Multiple candidates, so none will be attached for signal" << name;
goto next;
}
mostViableSignal = method;
}
}
if (!mostViableSignal.isValid()) {
qWarning() << "Function" << method.name() << "appears to be a signal handler for" << name
<< "but it does not match any signals on the target object";
goto next;
}
QMetaObject::connect(
this->object,
mostViableSignal.methodIndex(),
this,
method.methodIndex()
);
}
next:;
}
if (this->mItem != nullptr) {
this->mItem->setParentItem(this);
// clang-format off
QObject::connect(this, &QQuickItem::widthChanged, this, &BoundComponent::updateSize);
QObject::connect(this, &QQuickItem::heightChanged, this, &BoundComponent::updateSize);
QObject::connect(this->mItem, &QQuickItem::implicitWidthChanged, this, &BoundComponent::updateImplicitSize);
QObject::connect(this->mItem, &QQuickItem::implicitHeightChanged, this, &BoundComponent::updateImplicitSize);
// clang-format on
this->updateImplicitSize();
this->updateSize();
}
emit this->loaded();
}
void BoundComponent::onIncubationFailed() {
qWarning() << "Failed to create BoundComponent";
for (auto& error: this->incubator->errors()) {
qWarning() << error;
}
delete this->incubator;
this->disconnectComponent();
}
void BoundComponent::updateSize() { this->mItem->setSize(this->size()); }
void BoundComponent::updateImplicitSize() {
this->setImplicitWidth(this->mItem->implicitWidth());
this->setImplicitHeight(this->mItem->implicitHeight());
}
BoundComponentPropertyProxy::BoundComponentPropertyProxy(
QObject* from,
QObject* to,
QMetaProperty fromProperty,
QMetaProperty toProperty
)
: QObject(from)
, from(from)
, to(to)
, fromProperty(fromProperty)
, toProperty(toProperty) {
const auto* metaObject = this->metaObject();
auto method = metaObject->indexOfSlot("onNotified()");
QMetaObject::connect(from, fromProperty.notifySignal().methodIndex(), this, method);
}
void BoundComponentPropertyProxy::onNotified() {
this->toProperty.write(this->to, this->fromProperty.read(this->from));
}

125
src/core/boundcomponent.hpp Normal file
View file

@ -0,0 +1,125 @@
#pragma once
#include <qmetaobject.h>
#include <qobject.h>
#include <qqmlcomponent.h>
#include <qqmlparserstatus.h>
#include <qquickitem.h>
#include <qsignalmapper.h>
#include <qtmetamacros.h>
#include "incubator.hpp"
///! Component loader that allows setting initial properties.
/// Component loader that allows setting initial properties, primarily useful for
/// escaping cyclic dependency errors.
///
/// Properties defined on the BoundComponent will be applied to its loaded component,
/// including required properties, and will remain reactive. Functions created with
/// the names of signal handlers will also be attached to signals of the loaded component.
///
/// ```qml {filename="MyComponent.qml"}
/// MouseArea {
/// required property color color;
/// width: 100
/// height: 100
///
/// Rectangle {
/// anchors.fill: parent
/// color: parent.color
/// }
/// }
/// ```
///
/// ```qml
/// BoundComponent {
/// source: "MyComponent.qml"
///
/// // this is the same as assigning to `color` on MyComponent if loaded normally.
/// property color color: "red";
///
/// // this will be triggered when the `clicked` signal from the MouseArea is sent.
/// function onClicked() {
/// color = "blue";
/// }
/// }
/// ```
class BoundComponent: public QQuickItem {
Q_OBJECT;
// clang-format off
/// The loaded component. Will be null until it has finished loading.
Q_PROPERTY(QObject* item READ item NOTIFY loaded);
/// The source to load, as a Component.
Q_PROPERTY(QQmlComponent* sourceComponent READ sourceComponent WRITE setSourceComponent NOTIFY sourceComponentChanged);
/// The source to load, as a Url.
Q_PROPERTY(QString source READ source WRITE setSource NOTIFY sourceChanged);
/// If property values should be bound after they are initially set. Defaults to `true`.
Q_PROPERTY(bool bindValues READ bindValues WRITE setBindValues NOTIFY bindValuesChanged);
Q_PROPERTY(qreal implicitWidth READ implicitWidth NOTIFY implicitWidthChanged);
Q_PROPERTY(qreal implicitHeight READ implicitHeight NOTIFY implicitHeightChanged);
// clang-format on
QML_ELEMENT;
public:
explicit BoundComponent(QQuickItem* parent = nullptr): QQuickItem(parent) {}
void componentComplete() override;
[[nodiscard]] QObject* item() const;
[[nodiscard]] QQmlComponent* sourceComponent() const;
void setSourceComponent(QQmlComponent* sourceComponent);
[[nodiscard]] QString source() const;
void setSource(QString source);
[[nodiscard]] bool bindValues() const;
void setBindValues(bool bindValues);
signals:
void loaded();
void sourceComponentChanged();
void sourceChanged();
void bindValuesChanged();
private slots:
void onComponentDestroyed();
void onIncubationCompleted();
void onIncubationFailed();
void updateSize();
void updateImplicitSize();
private:
void disconnectComponent();
void tryCreate();
QString mSource;
bool mBindValues = true;
QQmlComponent* mComponent = nullptr;
bool ownsComponent = false;
QsQmlIncubator* incubator = nullptr;
QObject* object = nullptr;
QQuickItem* mItem = nullptr;
bool componentCompleted = false;
};
class BoundComponentPropertyProxy: public QObject {
Q_OBJECT;
public:
BoundComponentPropertyProxy(
QObject* from,
QObject* to,
QMetaProperty fromProperty,
QMetaProperty toProperty
);
public slots:
void onNotified();
private:
QObject* from;
QObject* to;
QMetaProperty fromProperty;
QMetaProperty toProperty;
};

88
src/core/clock.cpp Normal file
View file

@ -0,0 +1,88 @@
#include "clock.hpp"
#include <qdatetime.h>
#include <qobject.h>
#include <qtimer.h>
#include <qtmetamacros.h>
#include <qtypes.h>
SystemClock::SystemClock(QObject* parent): QObject(parent) {
QObject::connect(&this->timer, &QTimer::timeout, this, &SystemClock::onTimeout);
this->update();
}
bool SystemClock::enabled() const { return this->mEnabled; }
void SystemClock::setEnabled(bool enabled) {
if (enabled == this->mEnabled) return;
this->mEnabled = enabled;
emit this->enabledChanged();
this->update();
}
SystemClock::Enum SystemClock::precision() const { return this->mPrecision; }
void SystemClock::setPrecision(SystemClock::Enum precision) {
if (precision == this->mPrecision) return;
this->mPrecision = precision;
emit this->precisionChanged();
this->update();
}
void SystemClock::onTimeout() {
this->setTime(this->targetTime);
this->schedule(this->targetTime);
}
void SystemClock::update() {
if (this->mEnabled) {
this->setTime(QDateTime::fromMSecsSinceEpoch(0));
this->schedule(QDateTime::fromMSecsSinceEpoch(0));
} else {
this->timer.stop();
}
}
void SystemClock::setTime(const QDateTime& targetTime) {
auto currentTime = QDateTime::currentDateTime();
auto offset = currentTime.msecsTo(targetTime);
this->currentTime = offset > -500 && offset < 500 ? targetTime : currentTime;
auto time = this->currentTime.time();
this->currentTime.setTime(QTime(
this->mPrecision >= SystemClock::Hours ? time.hour() : 0,
this->mPrecision >= SystemClock::Minutes ? time.minute() : 0,
this->mPrecision >= SystemClock::Seconds ? time.second() : 0
));
emit this->dateChanged();
}
void SystemClock::schedule(const QDateTime& targetTime) {
auto secondPrecision = this->mPrecision >= SystemClock::Seconds;
auto minutePrecision = this->mPrecision >= SystemClock::Minutes;
auto hourPrecision = this->mPrecision >= SystemClock::Hours;
auto currentTime = QDateTime::currentDateTime();
auto offset = currentTime.msecsTo(targetTime);
// timer skew
auto nextTime = offset > 0 && offset < 500 ? targetTime : currentTime;
auto baseTimeT = nextTime.time();
nextTime.setTime(QTime(
hourPrecision ? baseTimeT.hour() : 0,
minutePrecision ? baseTimeT.minute() : 0,
secondPrecision ? baseTimeT.second() : 0
));
if (secondPrecision) nextTime = nextTime.addSecs(1);
else if (minutePrecision) nextTime = nextTime.addSecs(60);
else if (hourPrecision) nextTime = nextTime.addSecs(3600);
auto delay = currentTime.msecsTo(nextTime);
this->timer.start(static_cast<qint32>(delay));
this->targetTime = nextTime;
}

91
src/core/clock.hpp Normal file
View file

@ -0,0 +1,91 @@
#pragma once
#include <qdatetime.h>
#include <qobject.h>
#include <qqmlintegration.h>
#include <qtimer.h>
#include <qtmetamacros.h>
#include <qtypes.h>
///! System clock accessor.
/// SystemClock is a view into the system's clock.
/// It updates at hour, minute, or second intervals depending on @@precision.
///
/// # Examples
/// ```qml
/// SystemClock {
/// id: clock
/// precision: SystemClock.Seconds
/// }
///
/// @@QtQuick.Text {
/// text: Qt.formatDateTime(clock.date, "hh:mm:ss - yyyy-MM-dd")
/// }
/// ```
///
/// > [!WARNING] Clock updates will trigger within 50ms of the system clock changing,
/// > however this can be either before or after the clock changes (+-50ms). If you
/// > need a date object, use @@date instead of constructing a new one, or the time
/// > of the constructed object could be off by up to a second.
class SystemClock: public QObject {
Q_OBJECT;
/// If the clock should update. Defaults to true.
///
/// Setting enabled to false pauses the clock.
Q_PROPERTY(bool enabled READ enabled WRITE setEnabled NOTIFY enabledChanged);
/// The precision the clock should measure at. Defaults to `SystemClock.Seconds`.
Q_PROPERTY(SystemClock::Enum precision READ precision WRITE setPrecision NOTIFY precisionChanged);
/// The current date and time.
///
/// > [!TIP] You can use @@QtQml.Qt.formatDateTime() to get the time as a string in
/// > your format of choice.
Q_PROPERTY(QDateTime date READ date NOTIFY dateChanged);
/// The current hour.
Q_PROPERTY(quint32 hours READ hours NOTIFY dateChanged);
/// The current minute, or 0 if @@precision is `SystemClock.Hours`.
Q_PROPERTY(quint32 minutes READ minutes NOTIFY dateChanged);
/// The current second, or 0 if @@precision is `SystemClock.Hours` or `SystemClock.Minutes`.
Q_PROPERTY(quint32 seconds READ seconds NOTIFY dateChanged);
QML_ELEMENT;
public:
// must be named enum until docgen is ready to handle member enums better
enum Enum : quint8 {
Hours = 1,
Minutes = 2,
Seconds = 3,
};
Q_ENUM(Enum);
explicit SystemClock(QObject* parent = nullptr);
[[nodiscard]] bool enabled() const;
void setEnabled(bool enabled);
[[nodiscard]] SystemClock::Enum precision() const;
void setPrecision(SystemClock::Enum precision);
[[nodiscard]] QDateTime date() const { return this->currentTime; }
[[nodiscard]] quint32 hours() const { return this->currentTime.time().hour(); }
[[nodiscard]] quint32 minutes() const { return this->currentTime.time().minute(); }
[[nodiscard]] quint32 seconds() const { return this->currentTime.time().second(); }
signals:
void enabledChanged();
void precisionChanged();
void dateChanged();
private slots:
void onTimeout();
private:
bool mEnabled = true;
SystemClock::Enum mPrecision = SystemClock::Seconds;
QTimer timer;
QDateTime currentTime;
QDateTime targetTime;
void update();
void setTime(const QDateTime& targetTime);
void schedule(const QDateTime& targetTime);
};

242
src/core/colorquantizer.cpp Normal file
View file

@ -0,0 +1,242 @@
#include "colorquantizer.hpp"
#include <algorithm>
#include <qatomic.h>
#include <qcolor.h>
#include <qdatetime.h>
#include <qimage.h>
#include <qlist.h>
#include <qlogging.h>
#include <qloggingcategory.h>
#include <qminmax.h>
#include <qnamespace.h>
#include <qnumeric.h>
#include <qobject.h>
#include <qqmllist.h>
#include <qrgb.h>
#include <qthreadpool.h>
#include <qtmetamacros.h>
#include <qtypes.h>
#include "logcat.hpp"
namespace {
QS_LOGGING_CATEGORY(logColorQuantizer, "quickshell.colorquantizer", QtWarningMsg);
}
ColorQuantizerOperation::ColorQuantizerOperation(QUrl* source, qreal depth, qreal rescaleSize)
: source(source)
, maxDepth(depth)
, rescaleSize(rescaleSize) {
setAutoDelete(false);
}
void ColorQuantizerOperation::quantizeImage(const QAtomicInteger<bool>& shouldCancel) {
if (shouldCancel.loadAcquire() || source->isEmpty()) return;
colors.clear();
auto image = QImage(source->toLocalFile());
if ((image.width() > rescaleSize || image.height() > rescaleSize) && rescaleSize > 0) {
image = image.scaled(
static_cast<int>(rescaleSize),
static_cast<int>(rescaleSize),
Qt::KeepAspectRatio,
Qt::SmoothTransformation
);
}
if (image.isNull()) {
qCWarning(logColorQuantizer) << "Failed to load image from" << source->toString();
return;
}
QList<QColor> pixels;
for (int y = 0; y != image.height(); ++y) {
for (int x = 0; x != image.width(); ++x) {
auto pixel = image.pixel(x, y);
if (qAlpha(pixel) == 0) continue;
pixels.append(QColor::fromRgb(pixel));
}
}
auto startTime = QDateTime::currentDateTime();
colors = quantization(pixels, 0);
auto endTime = QDateTime::currentDateTime();
auto milliseconds = startTime.msecsTo(endTime);
qCDebug(logColorQuantizer) << "Color Quantization took: " << milliseconds << "ms";
}
QList<QColor> ColorQuantizerOperation::quantization(
QList<QColor>& rgbValues,
qreal depth,
const QAtomicInteger<bool>& shouldCancel
) {
if (shouldCancel.loadAcquire()) return QList<QColor>();
if (depth >= maxDepth || rgbValues.isEmpty()) {
if (rgbValues.isEmpty()) return QList<QColor>();
auto totalR = 0;
auto totalG = 0;
auto totalB = 0;
for (const auto& color: rgbValues) {
if (shouldCancel.loadAcquire()) return QList<QColor>();
totalR += color.red();
totalG += color.green();
totalB += color.blue();
}
auto avgColor = QColor(
qRound(totalR / static_cast<double>(rgbValues.size())),
qRound(totalG / static_cast<double>(rgbValues.size())),
qRound(totalB / static_cast<double>(rgbValues.size()))
);
return QList<QColor>() << avgColor;
}
auto dominantChannel = findBiggestColorRange(rgbValues);
std::ranges::sort(rgbValues, [dominantChannel](const auto& a, const auto& b) {
if (dominantChannel == 'r') return a.red() < b.red();
else if (dominantChannel == 'g') return a.green() < b.green();
return a.blue() < b.blue();
});
auto mid = rgbValues.size() / 2;
auto leftHalf = rgbValues.mid(0, mid);
auto rightHalf = rgbValues.mid(mid);
QList<QColor> result;
result.append(quantization(leftHalf, depth + 1));
result.append(quantization(rightHalf, depth + 1));
return result;
}
char ColorQuantizerOperation::findBiggestColorRange(const QList<QColor>& rgbValues) {
if (rgbValues.isEmpty()) return 'r';
auto rMin = 255;
auto gMin = 255;
auto bMin = 255;
auto rMax = 0;
auto gMax = 0;
auto bMax = 0;
for (const auto& color: rgbValues) {
rMin = qMin(rMin, color.red());
gMin = qMin(gMin, color.green());
bMin = qMin(bMin, color.blue());
rMax = qMax(rMax, color.red());
gMax = qMax(gMax, color.green());
bMax = qMax(bMax, color.blue());
}
auto rRange = rMax - rMin;
auto gRange = gMax - gMin;
auto bRange = bMax - bMin;
auto biggestRange = qMax(rRange, qMax(gRange, bRange));
if (biggestRange == rRange) {
return 'r';
} else if (biggestRange == gRange) {
return 'g';
} else {
return 'b';
}
}
void ColorQuantizerOperation::finishRun() {
QMetaObject::invokeMethod(this, &ColorQuantizerOperation::finished, Qt::QueuedConnection);
}
void ColorQuantizerOperation::finished() {
emit this->done(colors);
delete this;
}
void ColorQuantizerOperation::run() {
if (!this->shouldCancel) {
this->quantizeImage();
if (this->shouldCancel.loadAcquire()) {
qCDebug(logColorQuantizer) << "Color quantization" << this << "cancelled";
}
}
this->finishRun();
}
void ColorQuantizerOperation::tryCancel() { this->shouldCancel.storeRelease(true); }
void ColorQuantizer::componentComplete() {
componentCompleted = true;
if (!mSource.isEmpty()) quantizeAsync();
}
void ColorQuantizer::setSource(const QUrl& source) {
if (mSource != source) {
mSource = source;
emit this->sourceChanged();
if (this->componentCompleted && !mSource.isEmpty()) quantizeAsync();
}
}
void ColorQuantizer::setDepth(qreal depth) {
if (mDepth != depth) {
mDepth = depth;
emit this->depthChanged();
if (this->componentCompleted) quantizeAsync();
}
}
void ColorQuantizer::setRescaleSize(int rescaleSize) {
if (mRescaleSize != rescaleSize) {
mRescaleSize = rescaleSize;
emit this->rescaleSizeChanged();
if (this->componentCompleted) quantizeAsync();
}
}
void ColorQuantizer::operationFinished(const QList<QColor>& result) {
bColors = result;
this->liveOperation = nullptr;
emit this->colorsChanged();
}
void ColorQuantizer::quantizeAsync() {
if (this->liveOperation) this->cancelAsync();
qCDebug(logColorQuantizer) << "Starting color quantization asynchronously";
this->liveOperation = new ColorQuantizerOperation(&mSource, mDepth, mRescaleSize);
QObject::connect(
this->liveOperation,
&ColorQuantizerOperation::done,
this,
&ColorQuantizer::operationFinished
);
QThreadPool::globalInstance()->start(this->liveOperation);
}
void ColorQuantizer::cancelAsync() {
if (!this->liveOperation) return;
this->liveOperation->tryCancel();
QThreadPool::globalInstance()->waitForDone();
QObject::disconnect(this->liveOperation, nullptr, this, nullptr);
this->liveOperation = nullptr;
}

128
src/core/colorquantizer.hpp Normal file
View file

@ -0,0 +1,128 @@
#pragma once
#include <qlist.h>
#include <qobject.h>
#include <qproperty.h>
#include <qqmlintegration.h>
#include <qqmlparserstatus.h>
#include <qrunnable.h>
#include <qtmetamacros.h>
#include <qtypes.h>
#include <qurl.h>
class ColorQuantizerOperation
: public QObject
, public QRunnable {
Q_OBJECT;
public:
explicit ColorQuantizerOperation(QUrl* source, qreal depth, qreal rescaleSize);
void run() override;
void tryCancel();
signals:
void done(QList<QColor> colors);
private slots:
void finished();
private:
static char findBiggestColorRange(const QList<QColor>& rgbValues);
void quantizeImage(const QAtomicInteger<bool>& shouldCancel = false);
QList<QColor> quantization(
QList<QColor>& rgbValues,
qreal depth,
const QAtomicInteger<bool>& shouldCancel = false
);
void finishRun();
QAtomicInteger<bool> shouldCancel = false;
QList<QColor> colors;
QUrl* source;
qreal maxDepth;
qreal rescaleSize;
};
///! Color Quantization Utility
/// A color quantization utility used for getting prevalent colors in an image, by
/// averaging out the image's color data recursively.
///
/// #### Example
/// ```qml
/// ColorQuantizer {
/// id: colorQuantizer
/// source: Qt.resolvedUrl("./yourImage.png")
/// depth: 3 // Will produce 8 colors (2³)
/// rescaleSize: 64 // Rescale to 64x64 for faster processing
/// }
/// ```
class ColorQuantizer
: public QObject
, public QQmlParserStatus {
Q_OBJECT;
QML_ELEMENT;
Q_INTERFACES(QQmlParserStatus);
/// Access the colors resulting from the color quantization performed.
/// > [!NOTE] The amount of colors returned from the quantization is determined by
/// > the property depth, specifically 2ⁿ where n is the depth.
Q_PROPERTY(QList<QColor> colors READ default NOTIFY colorsChanged BINDABLE bindableColors);
/// Path to the image you'd like to run the color quantization on.
Q_PROPERTY(QUrl source READ source WRITE setSource NOTIFY sourceChanged);
/// Max depth for the color quantization. Each level of depth represents another
/// binary split of the color space
Q_PROPERTY(qreal depth READ depth WRITE setDepth NOTIFY depthChanged);
/// The size to rescale the image to, when rescaleSize is 0 then no scaling will be done.
/// > [!NOTE] Results from color quantization doesn't suffer much when rescaling, it's
/// > reccommended to rescale, otherwise the quantization process will take much longer.
Q_PROPERTY(qreal rescaleSize READ rescaleSize WRITE setRescaleSize NOTIFY rescaleSizeChanged);
public:
explicit ColorQuantizer(QObject* parent = nullptr): QObject(parent) {}
void componentComplete() override;
void classBegin() override {}
[[nodiscard]] QBindable<QList<QColor>> bindableColors() { return &this->bColors; }
[[nodiscard]] QUrl source() const { return mSource; }
void setSource(const QUrl& source);
[[nodiscard]] qreal depth() const { return mDepth; }
void setDepth(qreal depth);
[[nodiscard]] qreal rescaleSize() const { return mRescaleSize; }
void setRescaleSize(int rescaleSize);
signals:
void colorsChanged();
void sourceChanged();
void depthChanged();
void rescaleSizeChanged();
public slots:
void operationFinished(const QList<QColor>& result);
private:
void quantizeAsync();
void cancelAsync();
bool componentCompleted = false;
ColorQuantizerOperation* liveOperation = nullptr;
QUrl mSource;
qreal mDepth = 0;
qreal mRescaleSize = 0;
Q_OBJECT_BINDABLE_PROPERTY(
ColorQuantizer,
QList<QColor>,
bColors,
&ColorQuantizer::colorsChanged
);
};

9
src/core/common.cpp Normal file
View file

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

13
src/core/common.hpp Normal file
View file

@ -0,0 +1,13 @@
#pragma once
#include <qdatetime.h>
#include <qprocess.h>
namespace qs {
struct Common {
static const QDateTime LAUNCH_TIME;
static inline QProcessEnvironment INITIAL_ENVIRONMENT = {}; // NOLINT
};
} // namespace qs

422
src/core/desktopentry.cpp Normal file
View file

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

204
src/core/desktopentry.hpp Normal file
View file

@ -0,0 +1,204 @@
#pragma once
#include <utility>
#include <qcontainerfwd.h>
#include <qdir.h>
#include <qhash.h>
#include <qobject.h>
#include <qqmlintegration.h>
#include <qtmetamacros.h>
#include "doc.hpp"
#include "model.hpp"
class DesktopAction;
/// A desktop entry. See @@DesktopEntries for details.
class DesktopEntry: public QObject {
Q_OBJECT;
Q_PROPERTY(QString id MEMBER mId CONSTANT);
/// Name of the specific application, such as "Firefox".
Q_PROPERTY(QString name MEMBER mName CONSTANT);
/// Short description of the application, such as "Web Browser". May be empty.
Q_PROPERTY(QString genericName MEMBER mGenericName CONSTANT);
/// Initial class or app id the app intends to use. May be useful for matching running apps
/// to desktop entries.
Q_PROPERTY(QString startupClass MEMBER mStartupClass CONSTANT);
/// If true, this application should not be displayed in menus and launchers.
Q_PROPERTY(bool noDisplay MEMBER mNoDisplay CONSTANT);
/// Long description of the application, such as "View websites on the internet". May be empty.
Q_PROPERTY(QString comment MEMBER mComment CONSTANT);
/// Name of the icon associated with this application. May be empty.
Q_PROPERTY(QString icon MEMBER mIcon CONSTANT);
/// The raw `Exec` string from the desktop entry.
///
/// > [!WARNING] This cannot be reliably run as a command. See @@command for one you can run.
Q_PROPERTY(QString execString MEMBER mExecString CONSTANT);
/// The parsed `Exec` command in the desktop entry.
///
/// The entry can be run with @@execute(), or by using this command in
/// @@Quickshell.Quickshell.execDetached() or @@Quickshell.Io.Process.
/// If used in `execDetached` or a `Process`, @@workingDirectory should also be passed to
/// the invoked process. See @@execute() for details.
///
/// > [!NOTE] The provided command does not invoke a terminal even if @@runInTerminal is true.
Q_PROPERTY(QVector<QString> command MEMBER mCommand CONSTANT);
/// The working directory to execute from.
Q_PROPERTY(QString workingDirectory MEMBER mWorkingDirectory CONSTANT);
/// If the application should run in a terminal.
Q_PROPERTY(bool runInTerminal MEMBER mTerminal CONSTANT);
Q_PROPERTY(QVector<QString> categories MEMBER mCategories CONSTANT);
Q_PROPERTY(QVector<QString> keywords MEMBER mKeywords CONSTANT);
Q_PROPERTY(QVector<DesktopAction*> actions READ actions CONSTANT);
QML_ELEMENT;
QML_UNCREATABLE("DesktopEntry instances must be retrieved from DesktopEntries");
public:
explicit DesktopEntry(QString id, QObject* parent): QObject(parent), mId(std::move(id)) {}
void parseEntry(const QString& text);
/// Run the application. Currently ignores @@runInTerminal and field codes.
///
/// This is equivalent to calling @@Quickshell.Quickshell.execDetached() with @@command
/// and @@DesktopEntry.workingDirectory as shown below:
///
/// ```qml
/// Quickshell.execDetached({
/// command: desktopEntry.command,
/// workingDirectory: desktopEntry.workingDirectory,
/// });
/// ```
Q_INVOKABLE void execute() const;
[[nodiscard]] bool isValid() const;
[[nodiscard]] bool noDisplay() const;
[[nodiscard]] QVector<DesktopAction*> actions() const;
// currently ignores all field codes.
static QVector<QString> parseExecString(const QString& execString);
static void doExec(const QList<QString>& execString, const QString& workingDirectory);
public:
QString mId;
QString mName;
QString mGenericName;
QString mStartupClass;
bool mNoDisplay = false;
QString mComment;
QString mIcon;
QString mExecString;
QVector<QString> mCommand;
QString mWorkingDirectory;
bool mTerminal = false;
QVector<QString> mCategories;
QVector<QString> mKeywords;
private:
QHash<QString, QString> mEntries;
QHash<QString, DesktopAction*> mActions;
friend class DesktopAction;
};
/// An action of a @@DesktopEntry$.
class DesktopAction: public QObject {
Q_OBJECT;
Q_PROPERTY(QString id MEMBER mId CONSTANT);
Q_PROPERTY(QString name MEMBER mName CONSTANT);
Q_PROPERTY(QString icon MEMBER mIcon CONSTANT);
/// The raw `Exec` string from the action.
///
/// > [!WARNING] This cannot be reliably run as a command. See @@command for one you can run.
Q_PROPERTY(QString execString MEMBER mExecString CONSTANT);
/// The parsed `Exec` command in the action.
///
/// The entry can be run with @@execute(), or by using this command in
/// @@Quickshell.Quickshell.execDetached() or @@Quickshell.Io.Process.
/// If used in `execDetached` or a `Process`, @@DesktopEntry.workingDirectory should also be passed to
/// the invoked process.
///
/// > [!NOTE] The provided command does not invoke a terminal even if @@runInTerminal is true.
Q_PROPERTY(QVector<QString> command MEMBER mCommand CONSTANT);
QML_ELEMENT;
QML_UNCREATABLE("DesktopAction instances must be retrieved from a DesktopEntry");
public:
explicit DesktopAction(QString id, DesktopEntry* entry)
: QObject(entry)
, entry(entry)
, mId(std::move(id)) {}
/// Run the application. Currently ignores @@DesktopEntry.runInTerminal and field codes.
///
/// This is equivalent to calling @@Quickshell.Quickshell.execDetached() with @@command
/// and @@DesktopEntry.workingDirectory.
Q_INVOKABLE void execute() const;
private:
DesktopEntry* entry;
QString mId;
QString mName;
QString mIcon;
QString mExecString;
QVector<QString> mCommand;
QHash<QString, QString> mEntries;
friend class DesktopEntry;
};
class DesktopEntryManager: public QObject {
Q_OBJECT;
public:
void scanDesktopEntries();
[[nodiscard]] DesktopEntry* byId(const QString& id);
[[nodiscard]] DesktopEntry* heuristicLookup(const QString& name);
[[nodiscard]] ObjectModel<DesktopEntry>* applications();
static DesktopEntryManager* instance();
private:
explicit DesktopEntryManager();
void populateApplications();
void scanPath(const QDir& dir, const QString& prefix = QString());
QHash<QString, DesktopEntry*> desktopEntries;
QHash<QString, DesktopEntry*> lowercaseDesktopEntries;
ObjectModel<DesktopEntry> mApplications {this};
};
///! Desktop entry index.
/// Index of desktop entries according to the [desktop entry specification].
///
/// Primarily useful for looking up icons and metadata from an id, as there is
/// currently no mechanism for usage based sorting of entries and other launcher niceties.
///
/// [desktop entry specification]: https://specifications.freedesktop.org/desktop-entry-spec/latest/
class DesktopEntries: public QObject {
Q_OBJECT;
/// All desktop entries of type Application that are not Hidden or NoDisplay.
QSDOC_TYPE_OVERRIDE(ObjectModel<DesktopEntry>*);
Q_PROPERTY(UntypedObjectModel* applications READ applications CONSTANT);
QML_ELEMENT;
QML_SINGLETON;
public:
explicit DesktopEntries();
/// Look up a desktop entry by name. Includes NoDisplay entries. May return null.
///
/// While this function requires an exact match, @@heuristicLookup() will correctly
/// find an entry more often and is generally more useful.
Q_INVOKABLE [[nodiscard]] static DesktopEntry* byId(const QString& id);
/// Look up a desktop entry by name using heuristics. Unlike @@byId(),
/// if no exact matches are found this function will try to guess - potentially incorrectly.
/// May return null.
Q_INVOKABLE [[nodiscard]] static DesktopEntry* heuristicLookup(const QString& name);
[[nodiscard]] static ObjectModel<DesktopEntry>* applications();
};

View file

@ -9,3 +9,15 @@
// make the type visible in the docs even if not a QML_ELEMENT
#define QSDOC_ELEMENT
#define QSDOC_NAMED_ELEMENT(name)
// unmark uncreatable (will be overlayed by other types)
#define QSDOC_CREATABLE
// change the cname used for this type
#define QSDOC_CNAME(name)
// overridden properties
#define QSDOC_PROPERTY_OVERRIDE(...)
// override types of properties for docs
#define QSDOC_TYPE_OVERRIDE(type)

33
src/core/easingcurve.cpp Normal file
View file

@ -0,0 +1,33 @@
#include "easingcurve.hpp"
#include <utility>
#include <qeasingcurve.h>
#include <qpoint.h>
#include <qrect.h>
#include <qtmetamacros.h>
#include <qtypes.h>
qreal EasingCurve::valueAt(qreal x) const { return this->mCurve.valueForProgress(x); }
qreal EasingCurve::interpolate(qreal x, qreal a, qreal b) const {
return a + (b - a) * this->valueAt(x);
}
QPointF EasingCurve::interpolate(qreal x, const QPointF& a, const QPointF& b) const {
return QPointF(this->interpolate(x, a.x(), b.x()), this->interpolate(x, a.y(), b.y()));
}
QRectF EasingCurve::interpolate(qreal x, const QRectF& a, const QRectF& b) const {
return QRectF(
this->interpolate(x, a.topLeft(), b.topLeft()),
this->interpolate(x, a.bottomRight(), b.bottomRight())
);
}
QEasingCurve EasingCurve::curve() const { return this->mCurve; }
void EasingCurve::setCurve(QEasingCurve curve) {
if (this->mCurve == curve) return;
this->mCurve = std::move(curve);
emit this->curveChanged();
}

40
src/core/easingcurve.hpp Normal file
View file

@ -0,0 +1,40 @@
#pragma once
#include <qeasingcurve.h>
#include <qobject.h>
#include <qpoint.h>
#include <qqmlintegration.h>
#include <qrect.h>
#include <qtmetamacros.h>
///! Easing curve.
/// Directly accessible easing curve as used in property animations.
class EasingCurve: public QObject {
Q_OBJECT;
/// Easing curve settings. Works exactly the same as
/// [PropertyAnimation.easing](https://doc.qt.io/qt-6/qml-qtquick-propertyanimation.html#easing-prop).
Q_PROPERTY(QEasingCurve curve READ curve WRITE setCurve NOTIFY curveChanged);
QML_ELEMENT;
public:
EasingCurve(QObject* parent = nullptr): QObject(parent) {}
/// Returns the Y value for the given X value on the curve
/// from 0.0 to 1.0.
Q_INVOKABLE [[nodiscard]] qreal valueAt(qreal x) const;
/// Interpolates between two values using the given X coordinate.
Q_INVOKABLE [[nodiscard]] qreal interpolate(qreal x, qreal a, qreal b) const;
/// Interpolates between two points using the given X coordinate.
Q_INVOKABLE [[nodiscard]] QPointF interpolate(qreal x, const QPointF& a, const QPointF& b) const;
/// Interpolates two rects using the given X coordinate.
Q_INVOKABLE [[nodiscard]] QRectF interpolate(qreal x, const QRectF& a, const QRectF& b) const;
[[nodiscard]] QEasingCurve curve() const;
void setCurve(QEasingCurve curve);
signals:
void curveChanged();
private:
QEasingCurve mCurve;
};

22
src/core/elapsedtimer.cpp Normal file
View file

@ -0,0 +1,22 @@
#include "elapsedtimer.hpp"
#include <qtypes.h>
ElapsedTimer::ElapsedTimer() { this->timer.start(); }
qreal ElapsedTimer::elapsed() { return static_cast<qreal>(this->elapsedNs()) / 1000000000.0; }
qreal ElapsedTimer::restart() { return static_cast<qreal>(this->restartNs()) / 1000000000.0; }
qint64 ElapsedTimer::elapsedMs() { return this->timer.elapsed(); }
qint64 ElapsedTimer::restartMs() { return this->timer.restart(); }
qint64 ElapsedTimer::elapsedNs() { return this->timer.nsecsElapsed(); }
qint64 ElapsedTimer::restartNs() {
// see qelapsedtimer.cpp
auto old = this->timer;
this->timer.start();
return old.durationTo(this->timer).count();
}

45
src/core/elapsedtimer.hpp Normal file
View file

@ -0,0 +1,45 @@
#pragma once
#include <qelapsedtimer.h>
#include <qobject.h>
#include <qqmlintegration.h>
#include <qtmetamacros.h>
#include <qtypes.h>
///! Measures time between events
/// The ElapsedTimer measures time since its last restart, and is useful
/// for determining the time between events that don't supply it.
class ElapsedTimer: public QObject {
Q_OBJECT;
QML_ELEMENT;
public:
explicit ElapsedTimer();
/// Return the number of seconds since the timer was last
/// started or restarted, with nanosecond precision.
Q_INVOKABLE qreal elapsed();
/// Restart the timer, returning the number of seconds since
/// the timer was last started or restarted, with nanosecond precision.
Q_INVOKABLE qreal restart();
/// Return the number of milliseconds since the timer was last
/// started or restarted.
Q_INVOKABLE qint64 elapsedMs();
/// Restart the timer, returning the number of milliseconds since
/// the timer was last started or restarted.
Q_INVOKABLE qint64 restartMs();
/// Return the number of nanoseconds since the timer was last
/// started or restarted.
Q_INVOKABLE qint64 elapsedNs();
/// Restart the timer, returning the number of nanoseconds since
/// the timer was last started or restarted.
Q_INVOKABLE qint64 restartNs();
private:
QElapsedTimer timer;
};

View file

@ -0,0 +1,16 @@
#pragma once
#include "qsintercept.hpp"
#include "scan.hpp"
#include "singleton.hpp"
class EngineContext {
public:
explicit EngineContext(const QmlScanner& scanner);
private:
const QmlScanner& scanner;
QQmlEngine engine;
QsInterceptNetworkAccessManagerFactory interceptFactory;
SingletonRegistry singletonRegistry;
};

View file

@ -1,62 +0,0 @@
#include "floatingwindow.hpp"
#include <qobject.h>
#include <qqmllist.h>
#include <qquickitem.h>
#include <qtypes.h>
#include "proxywindow.hpp"
#include "windowinterface.hpp"
void ProxyFloatingWindow::setWidth(qint32 width) {
if (this->window == nullptr || !this->window->isVisible()) {
this->ProxyWindowBase::setWidth(width);
}
}
void ProxyFloatingWindow::setHeight(qint32 height) {
if (this->window == nullptr || !this->window->isVisible()) {
this->ProxyWindowBase::setHeight(height);
}
}
// FloatingWindowInterface
FloatingWindowInterface::FloatingWindowInterface(QObject* parent)
: WindowInterface(parent)
, window(new ProxyFloatingWindow(this)) {
// clang-format off
QObject::connect(this->window, &ProxyWindowBase::windowConnected, this, &FloatingWindowInterface::windowConnected);
QObject::connect(this->window, &ProxyWindowBase::visibleChanged, this, &FloatingWindowInterface::visibleChanged);
QObject::connect(this->window, &ProxyWindowBase::heightChanged, this, &FloatingWindowInterface::heightChanged);
QObject::connect(this->window, &ProxyWindowBase::widthChanged, this, &FloatingWindowInterface::widthChanged);
QObject::connect(this->window, &ProxyWindowBase::screenChanged, this, &FloatingWindowInterface::screenChanged);
QObject::connect(this->window, &ProxyWindowBase::colorChanged, this, &FloatingWindowInterface::colorChanged);
QObject::connect(this->window, &ProxyWindowBase::maskChanged, this, &FloatingWindowInterface::maskChanged);
// clang-format on
}
void FloatingWindowInterface::onReload(QObject* oldInstance) {
auto* old = qobject_cast<FloatingWindowInterface*>(oldInstance);
this->window->onReload(old != nullptr ? old->window : nullptr);
}
QQmlListProperty<QObject> FloatingWindowInterface::data() { return this->window->data(); }
QQuickItem* FloatingWindowInterface::contentItem() const { return this->window->contentItem(); }
// NOLINTBEGIN
#define proxyPair(type, get, set) \
type FloatingWindowInterface::get() const { return this->window->get(); } \
void FloatingWindowInterface::set(type value) { this->window->set(value); }
proxyPair(bool, isVisible, setVisible);
proxyPair(qint32, width, setWidth);
proxyPair(qint32, height, setHeight);
proxyPair(QuickshellScreenInfo*, screen, setScreen);
proxyPair(QColor, color, setColor);
proxyPair(PendingRegion*, mask, setMask);
#undef proxyPair
#undef proxySet
#undef proxyGet
// NOLINTEND

View file

@ -1,56 +0,0 @@
#pragma once
#include <qobject.h>
#include <qtmetamacros.h>
#include "proxywindow.hpp"
class ProxyFloatingWindow: public ProxyWindowBase {
Q_OBJECT;
public:
explicit ProxyFloatingWindow(QObject* parent = nullptr): ProxyWindowBase(parent) {}
// Setting geometry while the window is visible makes the content item shrinks but not the window
// which is awful so we disable it for floating windows.
void setWidth(qint32 width) override;
void setHeight(qint32 height) override;
};
///! Standard floating window.
class FloatingWindowInterface: public WindowInterface {
Q_OBJECT;
QML_NAMED_ELEMENT(FloatingWindow);
public:
explicit FloatingWindowInterface(QObject* parent = nullptr);
void onReload(QObject* oldInstance) override;
[[nodiscard]] QQuickItem* contentItem() const override;
// NOLINTBEGIN
[[nodiscard]] bool isVisible() const override;
void setVisible(bool visible) override;
[[nodiscard]] qint32 width() const override;
void setWidth(qint32 width) override;
[[nodiscard]] qint32 height() const override;
void setHeight(qint32 height) override;
[[nodiscard]] QuickshellScreenInfo* screen() const override;
void setScreen(QuickshellScreenInfo* screen) override;
[[nodiscard]] QColor color() const override;
void setColor(QColor color) override;
[[nodiscard]] PendingRegion* mask() const override;
void setMask(PendingRegion* mask) override;
[[nodiscard]] QQmlListProperty<QObject> data() override;
// NOLINTEND
private:
ProxyFloatingWindow* window;
};

413
src/core/generation.cpp Normal file
View file

@ -0,0 +1,413 @@
#include "generation.hpp"
#include <utility>
#include <qcontainerfwd.h>
#include <qcoreapplication.h>
#include <qdebug.h>
#include <qdir.h>
#include <qfileinfo.h>
#include <qfilesystemwatcher.h>
#include <qhash.h>
#include <qlist.h>
#include <qlogging.h>
#include <qloggingcategory.h>
#include <qnamespace.h>
#include <qobject.h>
#include <qqmlcontext.h>
#include <qqmlengine.h>
#include <qqmlerror.h>
#include <qqmlincubator.h>
#include <qtmetamacros.h>
#include "iconimageprovider.hpp"
#include "imageprovider.hpp"
#include "incubator.hpp"
#include "logcat.hpp"
#include "plugin.hpp"
#include "qsintercept.hpp"
#include "reload.hpp"
#include "scan.hpp"
namespace {
QS_LOGGING_CATEGORY(logScene, "scene");
}
static QHash<const QQmlEngine*, EngineGeneration*> g_generations; // NOLINT
EngineGeneration::EngineGeneration(const QDir& rootPath, QmlScanner scanner)
: rootPath(rootPath)
, scanner(std::move(scanner))
, urlInterceptor(this->rootPath)
, interceptNetFactory(this->rootPath, this->scanner.fileIntercepts)
, engine(new QQmlEngine()) {
g_generations.insert(this->engine, this);
this->engine->setOutputWarningsToStandardError(false);
QObject::connect(this->engine, &QQmlEngine::warnings, this, &EngineGeneration::onEngineWarnings);
this->engine->addUrlInterceptor(&this->urlInterceptor);
this->engine->addImportPath("qs:@/");
this->engine->setNetworkAccessManagerFactory(&this->interceptNetFactory);
this->engine->setIncubationController(&this->delayedIncubationController);
this->engine->addImageProvider("icon", new IconImageProvider());
this->engine->addImageProvider("qsimage", new QsImageProvider());
this->engine->addImageProvider("qspixmap", new QsPixmapProvider());
QsEnginePlugin::runConstructGeneration(*this);
}
EngineGeneration::EngineGeneration(): EngineGeneration(QDir(), QmlScanner()) {}
EngineGeneration::~EngineGeneration() {
if (this->engine != nullptr) {
qFatal() << this << "destroyed without calling destroy()";
}
}
void EngineGeneration::destroy() {
if (this->destroying) return;
this->destroying = true;
if (this->watcher != nullptr) {
// Multiple generations can detect a reload at the same time.
QObject::disconnect(this->watcher, nullptr, this, nullptr);
this->watcher->deleteLater();
this->watcher = nullptr;
}
for (auto* extension: this->extensions.values()) {
delete extension;
}
if (this->root != nullptr) {
QObject::connect(this->root, &QObject::destroyed, this, [this]() {
// prevent further js execution between garbage collection and engine destruction.
this->engine->setInterrupted(true);
g_generations.remove(this->engine);
// Garbage is not collected during engine destruction.
this->engine->collectGarbage();
delete this->engine;
this->engine = nullptr;
auto terminate = this->shouldTerminate;
auto code = this->exitCode;
delete this;
if (terminate) QCoreApplication::exit(code);
});
this->root->deleteLater();
this->root = nullptr;
} else {
g_generations.remove(this->engine);
// the engine has never been used, no need to clean up
delete this->engine;
this->engine = nullptr;
auto terminate = this->shouldTerminate;
auto code = this->exitCode;
delete this;
if (terminate) QCoreApplication::exit(code);
}
}
void EngineGeneration::shutdown() {
if (this->destroying) return;
delete this->root;
this->root = nullptr;
delete this->engine;
this->engine = nullptr;
delete this;
}
void EngineGeneration::onReload(EngineGeneration* old) {
if (old != nullptr) {
// if the old generation holds the window incubation controller as the
// new generation acquires it then incubators will hang intermittently
qCDebug(logIncubator) << "Locking incubation controllers of old generation" << old;
old->incubationControllersLocked = true;
old->assignIncubationController();
}
QObject::connect(this->engine, &QQmlEngine::quit, this, &EngineGeneration::quit);
QObject::connect(this->engine, &QQmlEngine::exit, this, &EngineGeneration::exit);
if (auto* reloadable = qobject_cast<Reloadable*>(this->root)) {
reloadable->reload(old ? old->root : nullptr);
}
this->singletonRegistry.onReload(old == nullptr ? nullptr : &old->singletonRegistry);
this->reloadComplete = true;
emit this->reloadFinished();
if (old != nullptr) {
QObject::connect(old, &QObject::destroyed, this, [this]() { this->postReload(); });
old->destroy();
} else {
this->postReload();
}
}
void EngineGeneration::postReload() {
// This can be called on a generation during its destruction.
if (this->engine == nullptr || this->root == nullptr) return;
QsEnginePlugin::runOnReload();
emit this->firePostReload();
QObject::disconnect(this, &EngineGeneration::firePostReload, nullptr, nullptr);
}
void EngineGeneration::setWatchingFiles(bool watching) {
if (watching) {
if (this->watcher == nullptr) {
this->watcher = new QFileSystemWatcher();
for (auto& file: this->scanner.scannedFiles) {
this->watcher->addPath(file);
this->watcher->addPath(QFileInfo(file).dir().absolutePath());
}
for (auto& file: this->extraWatchedFiles) {
this->watcher->addPath(file);
this->watcher->addPath(QFileInfo(file).dir().absolutePath());
}
QObject::connect(
this->watcher,
&QFileSystemWatcher::fileChanged,
this,
&EngineGeneration::onFileChanged
);
QObject::connect(
this->watcher,
&QFileSystemWatcher::directoryChanged,
this,
&EngineGeneration::onDirectoryChanged
);
}
} else {
if (this->watcher != nullptr) {
this->watcher->deleteLater();
this->watcher = nullptr;
}
}
}
bool EngineGeneration::setExtraWatchedFiles(const QVector<QString>& files) {
this->extraWatchedFiles.clear();
for (const auto& file: files) {
if (!this->scanner.scannedFiles.contains(file)) {
this->extraWatchedFiles.append(file);
}
}
if (this->watcher) {
this->setWatchingFiles(false);
this->setWatchingFiles(true);
}
return !this->extraWatchedFiles.isEmpty();
}
void EngineGeneration::onFileChanged(const QString& name) {
if (!this->watcher->files().contains(name)) {
this->deletedWatchedFiles.push_back(name);
} else {
// some editors (e.g vscode) perform file saving in two steps: truncate + write
// ignore the first event (truncate) with size 0 to prevent incorrect live reloading
auto fileInfo = QFileInfo(name);
if (fileInfo.isFile() && fileInfo.size() == 0) return;
emit this->filesChanged();
}
}
void EngineGeneration::onDirectoryChanged() {
// try to find any files that were just deleted from a replace operation
for (auto& file: this->deletedWatchedFiles) {
if (QFileInfo(file).exists()) {
emit this->filesChanged();
break;
}
}
}
void EngineGeneration::registerIncubationController(QQmlIncubationController* controller) {
// We only want controllers that we can swap out if destroyed.
// This happens if the window owning the active controller dies.
auto* obj = dynamic_cast<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) {
for (const auto& error: warnings) {
const auto& url = error.url();
auto rel = url.scheme() == "qs" && url.path().startsWith("@/qs/") ? "@" % url.path().sliced(5)
: url.toString();
QString objectName;
auto desc = error.description();
if (auto i = desc.indexOf(": "); i != -1 && desc.startsWith("QML ")) {
objectName = desc.first(i) + " at ";
desc = desc.sliced(i + 2);
}
qCWarning(logScene).noquote().nospace()
<< objectName << rel << '[' << error.line() << ':' << error.column() << "]: " << desc;
}
}
void EngineGeneration::registerExtension(const void* key, EngineGenerationExt* extension) {
if (this->extensions.contains(key)) {
delete this->extensions.value(key);
}
this->extensions.insert(key, extension);
}
EngineGenerationExt* EngineGeneration::findExtension(const void* key) {
return this->extensions.value(key);
}
void EngineGeneration::quit() {
this->shouldTerminate = true;
this->destroy();
}
void EngineGeneration::exit(int code) {
this->shouldTerminate = true;
this->exitCode = code;
this->destroy();
}
void EngineGeneration::assignIncubationController() {
QQmlIncubationController* controller = nullptr;
if (this->incubationControllersLocked || this->incubationControllers.isEmpty()) {
controller = &this->delayedIncubationController;
} else {
controller = dynamic_cast<QQmlIncubationController*>(this->incubationControllers.first());
}
qCDebug(logIncubator) << "Assigning incubation controller" << controller << "to generation"
<< this
<< "fallback:" << (controller == &this->delayedIncubationController);
this->engine->setIncubationController(controller);
}
EngineGeneration* EngineGeneration::currentGeneration() {
if (g_generations.size() == 1) {
return *g_generations.begin();
} else return nullptr;
}
EngineGeneration* EngineGeneration::findEngineGeneration(const QQmlEngine* engine) {
return g_generations.value(engine);
}
EngineGeneration* EngineGeneration::findObjectGeneration(const QObject* object) {
// Objects can still attempt to find their generation after it has been destroyed.
// if (g_generations.size() == 1) return EngineGeneration::currentGeneration();
while (object != nullptr) {
auto* context = QQmlEngine::contextForObject(object);
if (context != nullptr) {
if (auto* generation = EngineGeneration::findEngineGeneration(context->engine())) {
return generation;
}
}
object = object->parent();
}
return nullptr;
}

100
src/core/generation.hpp Normal file
View file

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

View file

@ -0,0 +1,84 @@
#include "iconimageprovider.hpp"
#include <algorithm>
#include <qcolor.h>
#include <qicon.h>
#include <qlogging.h>
#include <qpainter.h>
#include <qpixmap.h>
#include <qsize.h>
#include <qstring.h>
QPixmap
IconImageProvider::requestPixmap(const QString& id, QSize* size, const QSize& requestedSize) {
QString iconName;
QString fallbackName;
QString path;
auto splitIdx = id.indexOf("?path=");
if (splitIdx != -1) {
iconName = id.sliced(0, splitIdx);
path = id.sliced(splitIdx + 6);
qWarning() << "Searching custom icon paths is not yet supported. Icon path will be ignored for"
<< id;
} else {
splitIdx = id.indexOf("?fallback=");
if (splitIdx != -1) {
iconName = id.sliced(0, splitIdx);
fallbackName = id.sliced(splitIdx + 10);
} else {
iconName = id;
}
}
auto icon = QIcon::fromTheme(iconName);
if (icon.isNull()) icon = QIcon::fromTheme(fallbackName);
auto targetSize = requestedSize.isValid() ? requestedSize : QSize(100, 100);
if (targetSize.width() == 0 || targetSize.height() == 0) targetSize = QSize(2, 2);
auto pixmap = icon.pixmap(targetSize.width(), targetSize.height());
if (pixmap.isNull()) {
qWarning() << "Could not load icon" << id << "at size" << targetSize << "from request";
pixmap = IconImageProvider::missingPixmap(targetSize);
}
if (size != nullptr) *size = pixmap.size();
return pixmap;
}
QPixmap IconImageProvider::missingPixmap(const QSize& size) {
auto width = size.width() % 2 == 0 ? size.width() : size.width() + 1;
auto height = size.height() % 2 == 0 ? size.height() : size.height() + 1;
width = std::max(width, 2);
height = std::max(height, 2);
auto pixmap = QPixmap(width, height);
pixmap.fill(QColorConstants::Black);
auto painter = QPainter(&pixmap);
auto halfWidth = width / 2;
auto halfHeight = height / 2;
auto purple = QColor(0xd900d8);
painter.fillRect(halfWidth, 0, halfWidth, halfHeight, purple);
painter.fillRect(0, halfHeight, halfWidth, halfHeight, purple);
return pixmap;
}
QString IconImageProvider::requestString(
const QString& icon,
const QString& path,
const QString& fallback
) {
auto req = "image://icon/" + icon;
if (!path.isEmpty()) {
req += "?path=" + path;
}
if (!fallback.isEmpty()) {
req += "?fallback=" + fallback;
}
return req;
}

View file

@ -0,0 +1,19 @@
#pragma once
#include <qpixmap.h>
#include <qquickimageprovider.h>
class IconImageProvider: public QQuickImageProvider {
public:
explicit IconImageProvider(): QQuickImageProvider(QQuickImageProvider::Pixmap) {}
QPixmap requestPixmap(const QString& id, QSize* size, const QSize& requestedSize) override;
static QPixmap missingPixmap(const QSize& size);
static QString requestString(
const QString& icon,
const QString& path = QString(),
const QString& fallback = QString()
);
};

105
src/core/iconprovider.cpp Normal file
View file

@ -0,0 +1,105 @@
#include "iconprovider.hpp"
#include <utility>
#include <qicon.h>
#include <qiconengine.h>
#include <qlogging.h>
#include <qobject.h>
#include <qpixmap.h>
#include <qqmlengine.h>
#include <qquickimageprovider.h>
#include <qrect.h>
#include <qsize.h>
#include <qstring.h>
#include "generation.hpp"
// QMenu re-calls pixmap() every time the mouse moves so its important to cache it.
class PixmapCacheIconEngine: public QIconEngine {
void paint(
QPainter* /*unused*/,
const QRect& /*unused*/,
QIcon::Mode /*unused*/,
QIcon::State /*unused*/
) override {
qFatal(
) << "Unexpected icon paint request bypassed pixmap method. Please report this as a bug.";
}
QPixmap pixmap(const QSize& size, QIcon::Mode /*unused*/, QIcon::State /*unused*/) override {
if (this->lastPixmap.isNull() || size != this->lastSize) {
this->lastPixmap = this->createPixmap(size);
this->lastSize = size;
}
return this->lastPixmap;
}
virtual QPixmap createPixmap(const QSize& size) = 0;
private:
QSize lastSize;
QPixmap lastPixmap;
};
class ImageProviderIconEngine: public PixmapCacheIconEngine {
public:
explicit ImageProviderIconEngine(QQuickImageProvider* provider, QString id)
: provider(provider)
, id(std::move(id)) {}
QPixmap createPixmap(const QSize& size) override {
if (this->provider->imageType() == QQmlImageProviderBase::Pixmap) {
return this->provider->requestPixmap(this->id, nullptr, size);
} else if (this->provider->imageType() == QQmlImageProviderBase::Image) {
auto image = this->provider->requestImage(this->id, nullptr, size);
return QPixmap::fromImage(image);
} else {
qFatal() << "Unexpected ImageProviderIconEngine image type" << this->provider->imageType();
return QPixmap(); // never reached, satisfies lint
}
}
[[nodiscard]] QIconEngine* clone() const override {
return new ImageProviderIconEngine(this->provider, this->id);
}
private:
QQuickImageProvider* provider;
QString id;
};
QIcon getEngineImageAsIcon(QQmlEngine* engine, const QUrl& url) {
if (!engine || url.isEmpty()) return QIcon();
auto scheme = url.scheme();
if (scheme == "image") {
auto providerName = url.authority();
auto path = url.path();
if (!path.isEmpty()) path = path.sliced(1);
auto* provider = qobject_cast<QQuickImageProvider*>(engine->imageProvider(providerName));
if (provider == nullptr) {
qWarning() << "iconByUrl failed: no provider found for" << url;
return QIcon();
}
if (provider->imageType() == QQmlImageProviderBase::Pixmap
|| provider->imageType() == QQmlImageProviderBase::Image)
{
return QIcon(new ImageProviderIconEngine(provider, path));
}
} else {
qWarning() << "iconByUrl failed: unsupported scheme" << scheme << "in path" << url;
}
return QIcon();
}
QIcon getCurrentEngineImageAsIcon(const QUrl& url) {
auto* generation = EngineGeneration::currentGeneration();
if (!generation) return QIcon();
return getEngineImageAsIcon(generation->engine, url);
}

View file

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

View file

@ -0,0 +1,93 @@
#include "imageprovider.hpp"
#include <qcontainerfwd.h>
#include <qdebug.h>
#include <qimage.h>
#include <qlogging.h>
#include <qmap.h>
#include <qobject.h>
#include <qpixmap.h>
#include <qqmlengine.h>
#include <qtypes.h>
namespace {
namespace {
QMap<QString, QsImageHandle*> liveImages; // NOLINT
quint32 handleIndex = 0; // NOLINT
} // namespace
void parseReq(const QString& req, QString& target, QString& param) {
auto splitIdx = req.indexOf('/');
if (splitIdx != -1) {
target = req.sliced(0, splitIdx);
param = req.sliced(splitIdx + 1);
} else {
target = req;
}
}
} // namespace
QsImageHandle::QsImageHandle(QQmlImageProviderBase::ImageType type)
: type(type)
, id(QString::number(++handleIndex)) {
liveImages.insert(this->id, this);
}
QsImageHandle::~QsImageHandle() { liveImages.remove(this->id); }
QString QsImageHandle::url() const {
QString url = "image://";
if (this->type == QQmlImageProviderBase::Image) url += "qsimage";
else if (this->type == QQmlImageProviderBase::Pixmap) url += "qspixmap";
url += "/" + this->id;
return url;
}
QImage
QsImageHandle::requestImage(const QString& /*unused*/, QSize* /*unused*/, const QSize& /*unused*/) {
qWarning() << "Image handle" << this << "does not provide QImages";
return QImage();
}
QPixmap QsImageHandle::
requestPixmap(const QString& /*unused*/, QSize* /*unused*/, const QSize& /*unused*/) {
qWarning() << "Image handle" << this << "does not provide QPixmaps";
return QPixmap();
}
QImage QsImageProvider::requestImage(const QString& id, QSize* size, const QSize& requestedSize) {
QString target;
QString param;
parseReq(id, target, param);
auto* handle = liveImages.value(target);
if (handle != nullptr) {
return handle->requestImage(param, size, requestedSize);
} else {
qWarning() << "Requested image from unknown handle" << id;
return QImage();
}
}
QPixmap
QsPixmapProvider::requestPixmap(const QString& id, QSize* size, const QSize& requestedSize) {
QString target;
QString param;
parseReq(id, target, param);
auto* handle = liveImages.value(target);
if (handle != nullptr) {
return handle->requestPixmap(param, size, requestedSize);
} else {
qWarning() << "Requested image from unknown handle" << id;
return QPixmap();
}
}
QString QsIndexedImageHandle::url() const {
return this->QsImageHandle::url() % '/' % QString::number(this->changeIndex);
}
void QsIndexedImageHandle::imageChanged() { ++this->changeIndex; }

View file

@ -0,0 +1,48 @@
#pragma once
#include <qimage.h>
#include <qmap.h>
#include <qobject.h>
#include <qqmlengine.h>
#include <qquickimageprovider.h>
#include <qtclasshelpermacros.h>
#include <qtmetamacros.h>
class QsImageProvider: public QQuickImageProvider {
public:
explicit QsImageProvider(): QQuickImageProvider(QQuickImageProvider::Image) {}
QImage requestImage(const QString& id, QSize* size, const QSize& requestedSize) override;
};
class QsPixmapProvider: public QQuickImageProvider {
public:
explicit QsPixmapProvider(): QQuickImageProvider(QQuickImageProvider::Pixmap) {}
QPixmap requestPixmap(const QString& id, QSize* size, const QSize& requestedSize) override;
};
class QsImageHandle {
public:
explicit QsImageHandle(QQmlImageProviderBase::ImageType type);
virtual ~QsImageHandle();
Q_DISABLE_COPY_MOVE(QsImageHandle);
[[nodiscard]] virtual QString url() const;
virtual QImage requestImage(const QString& id, QSize* size, const QSize& requestedSize);
virtual QPixmap requestPixmap(const QString& id, QSize* size, const QSize& requestedSize);
private:
QQmlImageProviderBase::ImageType type;
QString id;
};
class QsIndexedImageHandle: public QsImageHandle {
public:
explicit QsIndexedImageHandle(QQmlImageProviderBase::ImageType type): QsImageHandle(type) {}
[[nodiscard]] QString url() const override;
void imageChanged();
private:
quint32 changeIndex = 0;
};

17
src/core/incubator.cpp Normal file
View file

@ -0,0 +1,17 @@
#include "incubator.hpp"
#include <qlogging.h>
#include <qqmlincubator.h>
#include <qtmetamacros.h>
#include "logcat.hpp"
QS_LOGGING_CATEGORY(logIncubator, "quickshell.incubator", QtWarningMsg);
void QsQmlIncubator::statusChanged(QQmlIncubator::Status status) {
switch (status) {
case QQmlIncubator::Ready: emit this->completed(); break;
case QQmlIncubator::Error: emit this->failed(); break;
default: break;
}
}

31
src/core/incubator.hpp Normal file
View file

@ -0,0 +1,31 @@
#pragma once
#include <qobject.h>
#include <qqmlincubator.h>
#include <qtmetamacros.h>
#include "logcat.hpp"
QS_DECLARE_LOGGING_CATEGORY(logIncubator);
class QsQmlIncubator
: public QObject
, public QQmlIncubator {
Q_OBJECT;
public:
explicit QsQmlIncubator(QsQmlIncubator::IncubationMode mode, QObject* parent = nullptr)
: QObject(parent)
, QQmlIncubator(mode) {}
void statusChanged(QQmlIncubator::Status status) override;
signals:
void completed();
void failed();
};
class DelayedQmlIncubationController: public QQmlIncubationController {
// Do nothing.
// This ensures lazy loaders don't start blocking before onReload creates windows.
};

35
src/core/instanceinfo.cpp Normal file
View file

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

41
src/core/instanceinfo.hpp Normal file
View file

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

200
src/core/lazyloader.cpp Normal file
View file

@ -0,0 +1,200 @@
#include "lazyloader.hpp"
#include <utility>
#include <qlogging.h>
#include <qobject.h>
#include <qqmlcomponent.h>
#include <qqmlcontext.h>
#include <qqmlengine.h>
#include <qqmlincubator.h>
#include <qtmetamacros.h>
#include "incubator.hpp"
#include "reload.hpp"
void LazyLoader::onReload(QObject* oldInstance) {
auto* old = qobject_cast<LazyLoader*>(oldInstance);
this->incubateIfReady(true);
if (old != nullptr && old->mItem != nullptr && this->incubator != nullptr) {
this->incubator->forceCompletion();
}
if (this->mItem != nullptr) {
if (auto* reloadable = qobject_cast<Reloadable*>(this->mItem)) {
reloadable->reload(old == nullptr ? nullptr : old->mItem);
} else {
Reloadable::reloadRecursive(this->mItem, old);
}
}
}
QObject* LazyLoader::item() {
if (this->isLoading()) this->setActive(true);
return this->mItem;
}
void LazyLoader::setItem(QObject* item) {
if (item == this->mItem) return;
if (this->mItem != nullptr) {
this->mItem->deleteLater();
}
this->mItem = item;
if (item != nullptr) {
item->setParent(this);
}
this->targetActive = this->isActive();
emit this->itemChanged();
emit this->activeChanged();
}
bool LazyLoader::isLoading() const { return this->incubator != nullptr; }
void LazyLoader::setLoading(bool loading) {
if (loading == this->targetLoading || this->isActive()) return;
this->targetLoading = loading;
if (loading) {
this->incubateIfReady();
} else if (this->mItem != nullptr) {
this->mItem->deleteLater();
this->mItem = nullptr;
} else if (this->incubator != nullptr) {
delete this->incubator;
this->incubator = nullptr;
}
}
bool LazyLoader::isActive() const { return this->mItem != nullptr; }
void LazyLoader::setActive(bool active) {
if (active == this->targetActive) return;
this->targetActive = active;
if (active) {
if (this->isLoading()) {
this->incubator->forceCompletion();
} else if (!this->isActive()) {
this->incubateIfReady();
}
} else if (this->isActive()) {
this->setItem(nullptr);
}
}
void LazyLoader::setActiveAsync(bool active) {
if (active == (this->targetActive || this->targetLoading)) return;
if (active) this->setLoading(true);
else this->setActive(false);
}
QQmlComponent* LazyLoader::component() const {
return this->cleanupComponent ? nullptr : this->mComponent;
}
void LazyLoader::setComponent(QQmlComponent* component) {
if (this->cleanupComponent) this->setSource(nullptr);
if (component == this->mComponent) return;
this->cleanupComponent = false;
if (this->mComponent != nullptr) {
QObject::disconnect(this->mComponent, nullptr, this, nullptr);
}
this->mComponent = component;
if (component != nullptr) {
QObject::connect(
this->mComponent,
&QObject::destroyed,
this,
&LazyLoader::onComponentDestroyed
);
}
emit this->componentChanged();
}
void LazyLoader::onComponentDestroyed() {
this->mComponent = nullptr;
// todo: figure out what happens to the incubator
}
QString LazyLoader::source() const { return this->mSource; }
void LazyLoader::setSource(QString source) {
if (!this->cleanupComponent) this->setComponent(nullptr);
if (source == this->mSource) return;
this->cleanupComponent = true;
this->mSource = std::move(source);
delete this->mComponent;
if (!this->mSource.isEmpty()) {
auto* context = QQmlEngine::contextForObject(this);
this->mComponent = new QQmlComponent(
context == nullptr ? nullptr : context->engine(),
context == nullptr ? this->mSource : context->resolvedUrl(this->mSource)
);
if (this->mComponent->isError()) {
qWarning() << this->mComponent->errorString().toStdString().c_str();
delete this->mComponent;
this->mComponent = nullptr;
}
} else {
this->mComponent = nullptr;
}
emit this->sourceChanged();
}
void LazyLoader::incubateIfReady(bool overrideReloadCheck) {
if (!(this->reloadComplete || overrideReloadCheck) || !(this->targetLoading || this->targetActive)
|| this->mComponent == nullptr || this->incubator != nullptr)
{
return;
}
this->incubator = new QsQmlIncubator(
this->targetActive ? QQmlIncubator::Synchronous : QQmlIncubator::Asynchronous,
this
);
// clang-format off
QObject::connect(this->incubator, &QsQmlIncubator::completed, this, &LazyLoader::onIncubationCompleted);
QObject::connect(this->incubator, &QsQmlIncubator::failed, this, &LazyLoader::onIncubationFailed);
// clang-format on
emit this->loadingChanged();
this->mComponent->create(*this->incubator, QQmlEngine::contextForObject(this->mComponent));
}
void LazyLoader::onIncubationCompleted() {
this->setItem(this->incubator->object());
// The incubator is not necessarily inert at the time of this callback,
// so deleteLater is required.
this->incubator->deleteLater();
this->incubator = nullptr;
this->targetLoading = false;
emit this->loadingChanged();
}
void LazyLoader::onIncubationFailed() {
qWarning() << "Failed to create LazyLoader component";
for (auto& error: this->incubator->errors()) {
qWarning() << error;
}
delete this->incubator;
this->targetLoading = false;
emit this->loadingChanged();
}

173
src/core/lazyloader.hpp Normal file
View file

@ -0,0 +1,173 @@
#pragma once
#include <QtQml/qqmlcomponent.h>
#include <qobject.h>
#include <qqmlincubator.h>
#include <qqmlintegration.h>
#include <qtmetamacros.h>
#include "incubator.hpp"
#include "reload.hpp"
///! Asynchronous component loader.
/// The LazyLoader can be used to prepare components that don't need to be
/// created immediately, such as windows that aren't visible until triggered
/// by another action. It works on creating the component in the gaps between
/// frame rendering to prevent blocking the interface thread.
/// It can also be used to preserve memory by loading components only
/// when you need them and unloading them afterward.
///
/// Note that when reloading the UI due to changes, lazy loaders will always
/// load synchronously so windows can be reused.
///
/// #### Example
/// The following example creates a PopupWindow asynchronously as the bar loads.
/// This means the bar can be shown onscreen before the popup is ready, however
/// trying to show the popup before it has finished loading in the background
/// will cause the UI thread to block.
///
/// ```qml
/// import QtQuick
/// import QtQuick.Controls
/// import Quickshell
///
/// ShellRoot {
/// PanelWindow {
/// id: window
/// height: 50
///
/// anchors {
/// bottom: true
/// left: true
/// right: true
/// }
///
/// LazyLoader {
/// id: popupLoader
///
/// // start loading immediately
/// loading: true
///
/// // this window will be loaded in the background during spare
/// // frame time unless active is set to true, where it will be
/// // loaded in the foreground
/// PopupWindow {
/// // position the popup above the button
/// parentWindow: window
/// relativeX: window.width / 2 - width / 2
/// relativeY: -height
///
/// // some heavy component here
///
/// width: 200
/// height: 200
/// }
/// }
///
/// Button {
/// anchors.centerIn: parent
/// text: "show popup"
///
/// // accessing popupLoader.item will force the loader to
/// // finish loading on the UI thread if it isn't finished yet.
/// onClicked: popupLoader.item.visible = !popupLoader.item.visible
/// }
/// }
/// }
/// ```
///
/// > [!WARNING] Components that internally load other components must explicitly
/// > support asynchronous loading to avoid blocking.
/// >
/// > Notably, @@Variants does not corrently support asynchronous
/// > loading, meaning using it inside a LazyLoader will block similarly to not
/// > having a loader to start with.
///
/// > [!WARNING] LazyLoaders do not start loading before the first window is created,
/// > meaning if you create all windows inside of lazy loaders, none of them will ever load.
class LazyLoader: public Reloadable {
Q_OBJECT;
/// The fully loaded item if the loader is @@loading or @@active, or `null`
/// if neither @@loading nor @@active.
///
/// Note that the item is owned by the LazyLoader, and destroying the LazyLoader
/// will destroy the item.
///
/// > [!WARNING] If you access the `item` of a loader that is currently loading,
/// > it will block as if you had set `active` to true immediately beforehand.
/// >
/// > You can instead set @@loading and listen to @@activeChanged(s) signal to
/// > ensure loading happens asynchronously.
Q_PROPERTY(QObject* item READ item NOTIFY itemChanged);
/// If the loader is actively loading.
///
/// If the component is not loaded, setting this property to true will start
/// loading it asynchronously. If the component is already loaded, setting
/// this property has no effect.
///
/// See also: @@activeAsync.
Q_PROPERTY(bool loading READ isLoading WRITE setLoading NOTIFY loadingChanged);
/// If the component is fully loaded.
///
/// Setting this property to `true` will force the component to load to completion,
/// blocking the UI, and setting it to `false` will destroy the component, requiring
/// it to be loaded again.
///
/// See also: @@activeAsync.
Q_PROPERTY(bool active READ isActive WRITE setActive NOTIFY activeChanged);
/// If the component is fully loaded.
///
/// Setting this property to true will asynchronously load the component similarly to
/// @@loading. Reading it or setting it to false will behanve
/// the same as @@active.
Q_PROPERTY(bool activeAsync READ isActive WRITE setActiveAsync NOTIFY activeChanged);
/// The component to load. Mutually exclusive to @@source.
Q_PROPERTY(QQmlComponent* component READ component WRITE setComponent NOTIFY componentChanged);
/// The URI to load the component from. Mutually exclusive to @@component.
Q_PROPERTY(QString source READ source WRITE setSource NOTIFY sourceChanged);
Q_CLASSINFO("DefaultProperty", "component");
QML_ELEMENT;
public:
void onReload(QObject* oldInstance) override;
[[nodiscard]] bool isActive() const;
void setActive(bool active);
void setActiveAsync(bool active);
[[nodiscard]] bool isLoading() const;
void setLoading(bool loading);
[[nodiscard]] QObject* item();
void setItem(QObject* item);
[[nodiscard]] QQmlComponent* component() const;
void setComponent(QQmlComponent* component);
[[nodiscard]] QString source() const;
void setSource(QString source);
signals:
void activeChanged();
void loadingChanged();
void itemChanged();
void sourceChanged();
void componentChanged();
private slots:
void onIncubationCompleted();
void onIncubationFailed();
void onComponentDestroyed();
private:
void incubateIfReady(bool overrideReloadCheck = false);
void waitForObjectCreation();
bool targetLoading = false;
bool targetActive = false;
QObject* mItem = nullptr;
QString mSource;
QQmlComponent* mComponent = nullptr;
QsQmlIncubator* incubator = nullptr;
bool cleanupComponent = false;
};

28
src/core/logcat.hpp Normal file
View file

@ -0,0 +1,28 @@
#pragma once
#include <qlogging.h>
#include <qloggingcategory.h>
namespace qs::log {
void initLogCategoryLevel(const char* name, QtMsgType defaultLevel = QtDebugMsg);
}
// NOLINTNEXTLINE(cppcoreguidelines-macro-usage)
#define QS_DECLARE_LOGGING_CATEGORY(name) \
namespace qslogcat { \
Q_DECLARE_LOGGING_CATEGORY(name); \
} \
const QLoggingCategory& name()
// NOLINTNEXTLINE(cppcoreguidelines-macro-usage)
#define QS_LOGGING_CATEGORY(name, category, ...) \
namespace qslogcat { \
Q_LOGGING_CATEGORY(name, category __VA_OPT__(, __VA_ARGS__)); \
} \
const QLoggingCategory& name() { \
static auto* init = []() { \
qs::log::initLogCategoryLevel(category __VA_OPT__(, __VA_ARGS__)); \
return &qslogcat::name; \
}(); \
return (init) (); \
}

957
src/core/logging.cpp Normal file
View file

@ -0,0 +1,957 @@
#include "logging.hpp"
#include <array>
#include <cerrno>
#include <cstdio>
#include <fcntl.h>
#include <qbytearrayview.h>
#include <qcoreapplication.h>
#include <qdatetime.h>
#include <qendian.h>
#include <qfilesystemwatcher.h>
#include <qhash.h>
#include <qhashfunctions.h>
#include <qlist.h>
#include <qlogging.h>
#include <qloggingcategory.h>
#include <qnamespace.h>
#include <qobject.h>
#include <qobjectdefs.h>
#include <qpair.h>
#include <qstring.h>
#include <qstringview.h>
#include <qsysinfo.h>
#include <qtenvironmentvariables.h>
#include <qtextstream.h>
#include <qthread.h>
#include <qtmetamacros.h>
#include <qtypes.h>
#include <sys/mman.h>
#include <sys/sendfile.h>
#include "instanceinfo.hpp"
#include "logcat.hpp"
#include "logging_p.hpp"
#include "logging_qtprivate.cpp" // NOLINT
#include "paths.hpp"
#include "ringbuf.hpp"
QS_LOGGING_CATEGORY(logBare, "quickshell.bare");
namespace qs::log {
using namespace qt_logging_registry;
QS_LOGGING_CATEGORY(logLogging, "quickshell.logging", QtWarningMsg);
bool LogMessage::operator==(const LogMessage& other) const {
// note: not including time
return this->type == other.type && this->category == other.category && this->body == other.body;
}
size_t qHash(const LogMessage& message) {
return qHash(message.type) ^ qHash(message.category) ^ qHash(message.body);
}
void LogMessage::formatMessage(
QTextStream& stream,
const LogMessage& msg,
bool color,
bool timestamp,
const QString& prefix
) {
if (!prefix.isEmpty()) {
if (color) stream << "\033[90m";
stream << '[' << prefix << ']';
if (timestamp) stream << ' ';
if (color) stream << "\033[0m";
}
if (timestamp) {
if (color) stream << "\033[90m";
stream << msg.time.toString("yyyy-MM-dd hh:mm:ss.zzz");
}
if (msg.category == "quickshell.bare") {
if (!prefix.isEmpty()) stream << ' ';
stream << msg.body;
} else {
if (color) {
switch (msg.type) {
case QtDebugMsg: stream << "\033[34m DEBUG"; break;
case QtInfoMsg: stream << "\033[32m INFO"; break;
case QtWarningMsg: stream << "\033[33m WARN"; break;
case QtCriticalMsg: stream << "\033[31m ERROR"; break;
case QtFatalMsg: stream << "\033[31m FATAL"; break;
}
} else {
switch (msg.type) {
case QtDebugMsg: stream << " DEBUG"; break;
case QtInfoMsg: stream << " INFO"; break;
case QtWarningMsg: stream << " WARN"; break;
case QtCriticalMsg: stream << " ERROR"; break;
case QtFatalMsg: stream << " FATAL"; break;
}
}
const auto isDefault = msg.category == "default";
if (color && !isDefault && msg.type != QtFatalMsg) stream << "\033[97m";
if (!isDefault) {
stream << ' ' << msg.category;
}
if (color && msg.type != QtFatalMsg) stream << "\033[0m";
stream << ": " << msg.body;
if (color && msg.type == QtFatalMsg) stream << "\033[0m";
}
}
bool CategoryFilter::shouldDisplay(QtMsgType type) const {
switch (type) {
case QtDebugMsg: return this->debug;
case QtInfoMsg: return this->info;
case QtWarningMsg: return this->warn;
case QtCriticalMsg: return this->critical;
default: return true;
}
}
void CategoryFilter::apply(QLoggingCategory* category) const {
category->setEnabled(QtDebugMsg, this->debug);
category->setEnabled(QtInfoMsg, this->info);
category->setEnabled(QtWarningMsg, this->warn);
category->setEnabled(QtCriticalMsg, this->critical);
}
void CategoryFilter::applyRule(
QLatin1StringView category,
const qt_logging_registry::QLoggingRule& rule
) {
auto filterpass = rule.pass(category, QtDebugMsg);
if (filterpass != 0) this->debug = filterpass > 0;
filterpass = rule.pass(category, QtInfoMsg);
if (filterpass != 0) this->info = filterpass > 0;
filterpass = rule.pass(category, QtWarningMsg);
if (filterpass != 0) this->warn = filterpass > 0;
filterpass = rule.pass(category, QtCriticalMsg);
if (filterpass != 0) this->critical = filterpass > 0;
}
LogManager::LogManager(): stdoutStream(stdout) {}
void LogManager::messageHandler(
QtMsgType type,
const QMessageLogContext& context,
const QString& msg
) {
auto message = LogMessage(type, QLatin1StringView(context.category), msg.toUtf8());
auto* self = LogManager::instance();
auto display = true;
const auto* key = static_cast<const void*>(context.category);
if (self->sparseFilters.contains(key)) {
display = self->sparseFilters.value(key).shouldDisplay(type);
}
if (display) {
LogMessage::formatMessage(
self->stdoutStream,
message,
self->colorLogs,
self->timestampLogs,
self->prefix
);
self->stdoutStream << Qt::endl;
}
emit self->logMessage(message, display);
}
void LogManager::filterCategory(QLoggingCategory* category) {
auto* instance = LogManager::instance();
auto categoryName = QLatin1StringView(category->categoryName());
auto isQs = categoryName.startsWith(QLatin1StringView("quickshell."));
CategoryFilter filter;
// We don't respect log filters for qs logs because some distros like to ship
// default configs that hide everything. QT_LOGGING_RULES is considered via the filter list.
if (isQs) {
// QtDebugMsg == 0, so default
auto defaultLevel = instance->defaultLevels.value(categoryName);
filter = CategoryFilter();
// clang-format off
filter.debug = instance->mDefaultLevel == QtDebugMsg || defaultLevel == QtDebugMsg;
filter.info = filter.debug || instance->mDefaultLevel == QtInfoMsg || defaultLevel == QtInfoMsg;
filter.warn = filter.info || instance->mDefaultLevel == QtWarningMsg || defaultLevel == QtWarningMsg;
filter.critical = filter.warn || instance->mDefaultLevel == QtCriticalMsg || defaultLevel == QtCriticalMsg;
// clang-format on
} else if (instance->lastCategoryFilter) {
instance->lastCategoryFilter(category);
filter = CategoryFilter(category);
}
for (const auto& rule: *instance->rules) {
filter.applyRule(categoryName, rule);
}
if (isQs && !instance->sparse) {
// We assume the category name pointer will always be the same and be comparable in the message handler.
instance->sparseFilters.insert(static_cast<const void*>(category->categoryName()), filter);
// all enabled by default
CategoryFilter().apply(category);
} else {
filter.apply(category);
}
instance->allFilters.insert(categoryName, filter);
}
LogManager* LogManager::instance() {
static auto* instance = new LogManager(); // NOLINT
return instance;
}
void LogManager::init(
bool color,
bool timestamp,
bool sparseOnly,
QtMsgType defaultLevel,
const QString& rules,
const QString& prefix
) {
auto* instance = LogManager::instance();
instance->colorLogs = color;
instance->timestampLogs = timestamp;
instance->sparse = sparseOnly;
instance->prefix = prefix;
instance->mDefaultLevel = defaultLevel;
instance->mRulesString = rules;
{
QLoggingSettingsParser parser;
// Load QT_LOGGING_RULES because we ignore the last category filter for QS messages
// due to disk config files.
parser.setContent(qEnvironmentVariable("QT_LOGGING_RULES"));
instance->rules = new QList(parser.rules());
parser.setContent(rules);
instance->rules->append(parser.rules());
}
qInstallMessageHandler(&LogManager::messageHandler);
instance->lastCategoryFilter = QLoggingCategory::installFilter(&LogManager::filterCategory);
qCDebug(logLogging) << "Creating offthread logger...";
auto* thread = new QThread();
instance->threadProxy.moveToThread(thread);
thread->start();
QMetaObject::invokeMethod(
&instance->threadProxy,
&LoggingThreadProxy::initInThread,
Qt::BlockingQueuedConnection
);
qCDebug(logLogging) << "Logger initialized.";
}
void initLogCategoryLevel(const char* name, QtMsgType defaultLevel) {
LogManager::instance()->defaultLevels.insert(QLatin1StringView(name), defaultLevel);
}
void LogManager::initFs() {
QMetaObject::invokeMethod(
&LogManager::instance()->threadProxy,
"initFs",
Qt::BlockingQueuedConnection
);
}
QString LogManager::rulesString() const { return this->mRulesString; }
QtMsgType LogManager::defaultLevel() const { return this->mDefaultLevel; }
bool LogManager::isSparse() const { return this->sparse; }
CategoryFilter LogManager::getFilter(QLatin1StringView category) {
return this->allFilters.value(category);
}
void LoggingThreadProxy::initInThread() {
this->logging = new ThreadLogging(this);
this->logging->init();
}
void LoggingThreadProxy::initFs() { this->logging->initFs(); }
void ThreadLogging::init() {
auto logMfd = memfd_create("quickshell:logs", 0);
if (logMfd == -1) {
qCCritical(logLogging) << "Failed to create memfd for initial log storage"
<< qt_error_string(-1);
}
auto dlogMfd = memfd_create("quickshell:detailedlogs", 0);
if (dlogMfd == -1) {
qCCritical(logLogging) << "Failed to create memfd for initial detailed log storage"
<< qt_error_string(-1);
}
if (logMfd != -1) {
this->file = new QFile();
this->file->open(logMfd, QFile::ReadWrite, QFile::AutoCloseHandle);
this->fileStream.setDevice(this->file);
}
if (dlogMfd != -1) {
crash::CrashInfo::INSTANCE.logFd = dlogMfd;
this->detailedFile = new QFile();
// buffered by WriteBuffer
this->detailedFile->open(dlogMfd, QFile::ReadWrite | QFile::Unbuffered, QFile::AutoCloseHandle);
this->detailedWriter.setDevice(this->detailedFile);
if (!this->detailedWriter.writeHeader()) {
qCCritical(logLogging) << "Could not write header for detailed logs.";
this->detailedWriter.setDevice(nullptr);
delete this->detailedFile;
this->detailedFile = nullptr;
}
}
// This connection is direct so it works while the event loop is destroyed between
// QCoreApplication delete and Q(Gui)Application launch.
QObject::connect(
LogManager::instance(),
&LogManager::logMessage,
this,
&ThreadLogging::onMessage,
Qt::DirectConnection
);
qCDebug(logLogging) << "Created memfd" << logMfd << "for early logs.";
qCDebug(logLogging) << "Created memfd" << dlogMfd << "for early detailed logs.";
}
void ThreadLogging::initFs() {
qCDebug(logLogging) << "Starting filesystem logging...";
auto* runDir = QsPaths::instance()->instanceRunDir();
if (!runDir) {
qCCritical(logLogging
) << "Could not start filesystem logging as the runtime directory could not be created.";
return;
}
auto path = runDir->filePath("log.log");
auto detailedPath = runDir->filePath("log.qslog");
auto* file = new QFile(path);
auto* detailedFile = new QFile(detailedPath);
if (!file->open(QFile::ReadWrite | QFile::Truncate)) {
qCCritical(logLogging
) << "Could not start filesystem logger as the log file could not be created:"
<< path;
delete file;
file = nullptr;
} else {
qInfo() << "Saving logs to" << detailedPath;
}
// buffered by WriteBuffer
if (!detailedFile->open(QFile::ReadWrite | QFile::Truncate | QFile::Unbuffered)) {
qCCritical(logLogging
) << "Could not start detailed filesystem logger as the log file could not be created:"
<< detailedPath;
delete detailedFile;
detailedFile = nullptr;
} else {
auto lock = flock {
.l_type = F_WRLCK,
.l_whence = SEEK_SET,
.l_start = 0,
.l_len = 0,
.l_pid = 0,
};
if (fcntl(detailedFile->handle(), F_SETLK, &lock) != 0) { // NOLINT
qCWarning(logLogging) << "Unable to set lock marker on detailed log file. --follow from "
"other instances will not work.";
}
qCInfo(logLogging) << "Saving detailed logs to" << path;
}
qCDebug(logLogging) << "Copying memfd logs to log file...";
if (file) {
auto* oldFile = this->file;
if (oldFile) {
oldFile->seek(0);
sendfile(file->handle(), oldFile->handle(), nullptr, oldFile->size());
}
this->file = file;
this->fileStream.setDevice(file);
delete oldFile;
}
if (detailedFile) {
auto* oldFile = this->detailedFile;
if (oldFile) {
oldFile->seek(0);
sendfile(detailedFile->handle(), oldFile->handle(), nullptr, oldFile->size());
}
crash::CrashInfo::INSTANCE.logFd = detailedFile->handle();
this->detailedFile = detailedFile;
this->detailedWriter.setDevice(detailedFile);
if (!oldFile) {
if (!this->detailedWriter.writeHeader()) {
qCCritical(logLogging) << "Could not write header for detailed logs.";
this->detailedWriter.setDevice(nullptr);
delete this->detailedFile;
this->detailedFile = nullptr;
}
}
delete oldFile;
}
qCDebug(logLogging) << "Switched logging to disk logs.";
auto* logManager = LogManager::instance();
QObject::disconnect(logManager, &LogManager::logMessage, this, &ThreadLogging::onMessage);
QObject::connect(
logManager,
&LogManager::logMessage,
this,
&ThreadLogging::onMessage,
Qt::QueuedConnection
);
qCDebug(logLogging) << "Switched threaded logger to queued eventloop connection.";
}
void ThreadLogging::onMessage(const LogMessage& msg, bool showInSparse) {
if (showInSparse) {
if (this->fileStream.device() == nullptr) return;
LogMessage::formatMessage(this->fileStream, msg, false, true);
this->fileStream << Qt::endl;
}
if (!this->detailedWriter.write(msg) || (this->detailedFile && !this->detailedFile->flush())) {
if (this->detailedFile) {
qCCritical(logLogging) << "Detailed logger failed to write. Ending detailed logs.";
}
this->detailedWriter.setDevice(nullptr);
this->detailedFile->close();
this->detailedFile = nullptr;
}
}
CompressedLogType compressedTypeOf(QtMsgType type) {
switch (type) {
case QtDebugMsg: return CompressedLogType::Debug;
case QtInfoMsg: return CompressedLogType::Info;
case QtWarningMsg: return CompressedLogType::Warn;
case QtCriticalMsg:
case QtFatalMsg: return CompressedLogType::Critical;
}
return CompressedLogType::Info; // unreachable under normal conditions
}
QtMsgType typeOfCompressed(CompressedLogType type) {
switch (type) {
case CompressedLogType::Debug: return QtDebugMsg;
case CompressedLogType::Info: return QtInfoMsg;
case CompressedLogType::Warn: return QtWarningMsg;
case CompressedLogType::Critical: return QtCriticalMsg;
}
return QtInfoMsg; // unreachable under normal conditions
}
void WriteBuffer::setDevice(QIODevice* device) { this->device = device; }
bool WriteBuffer::hasDevice() const { return this->device; }
bool WriteBuffer::flush() {
auto written = this->device->write(this->buffer);
auto success = written == this->buffer.length();
this->buffer.clear();
return success;
}
void WriteBuffer::writeBytes(const char* data, qsizetype length) {
this->buffer.append(data, length);
}
void WriteBuffer::writeU8(quint8 data) { this->writeBytes(reinterpret_cast<char*>(&data), 1); }
void WriteBuffer::writeU16(quint16 data) {
data = qToLittleEndian(data);
this->writeBytes(reinterpret_cast<char*>(&data), 2);
}
void WriteBuffer::writeU32(quint32 data) {
data = qToLittleEndian(data);
this->writeBytes(reinterpret_cast<char*>(&data), 4);
}
void WriteBuffer::writeU64(quint64 data) {
data = qToLittleEndian(data);
this->writeBytes(reinterpret_cast<char*>(&data), 8);
}
void DeviceReader::setDevice(QIODevice* device) { this->device = device; }
bool DeviceReader::hasDevice() const { return this->device; }
bool DeviceReader::readBytes(char* data, qsizetype length) {
return this->device->read(data, length) == length;
}
qsizetype DeviceReader::peekBytes(char* data, qsizetype length) {
return this->device->peek(data, length);
}
bool DeviceReader::skip(qsizetype length) { return this->device->skip(length) == length; }
bool DeviceReader::readU8(quint8* data) {
return this->readBytes(reinterpret_cast<char*>(data), 1);
}
bool DeviceReader::readU16(quint16* data) {
return this->readBytes(reinterpret_cast<char*>(data), 2);
}
bool DeviceReader::readU32(quint32* data) {
return this->readBytes(reinterpret_cast<char*>(data), 4);
}
bool DeviceReader::readU64(quint64* data) {
return this->readBytes(reinterpret_cast<char*>(data), 8);
}
void EncodedLogWriter::setDevice(QIODevice* target) { this->buffer.setDevice(target); }
void EncodedLogReader::setDevice(QIODevice* source) { this->reader.setDevice(source); }
constexpr quint8 LOG_VERSION = 2;
bool EncodedLogWriter::writeHeader() {
this->buffer.writeU8(LOG_VERSION);
return this->buffer.flush();
}
bool EncodedLogReader::readHeader(bool* success, quint8* version, quint8* readerVersion) {
if (!this->reader.readU8(version)) return false;
*success = *version == LOG_VERSION;
*readerVersion = LOG_VERSION;
return true;
}
bool EncodedLogWriter::write(const LogMessage& message) {
if (!this->buffer.hasDevice()) return false;
LogMessage* prevMessage = nullptr;
auto index = this->recentMessages.indexOf(message, &prevMessage);
// If its a dupe, save memory by reusing the buffer of the first message and letting
// the new one be deallocated.
auto body = prevMessage ? prevMessage->body : message.body;
this->recentMessages.emplace(message.type, message.category, body, message.time);
if (index != -1) {
auto secondDelta = this->lastMessageTime.secsTo(message.time);
if (secondDelta < 16 && index < 16) {
this->writeOp(EncodedLogOpcode::RecentMessageShort);
this->buffer.writeU8(index | (secondDelta << 4));
} else {
this->writeOp(EncodedLogOpcode::RecentMessageLong);
this->buffer.writeU8(index);
this->writeVarInt(secondDelta);
}
goto finish;
} else {
auto categoryId = this->getOrCreateCategory(message.category);
this->writeVarInt(categoryId);
auto writeFullTimestamp = [this, &message]() {
this->buffer.writeU64(message.time.toSecsSinceEpoch());
};
if (message.type == QtFatalMsg) {
this->buffer.writeU8(0xff);
writeFullTimestamp();
} else {
quint8 field = compressedTypeOf(message.type);
auto secondDelta = this->lastMessageTime.secsTo(message.time);
if (secondDelta >= 0x1d) {
// 0x1d = followed by delta int
// 0x1e = followed by epoch delta int
field |= (secondDelta < 0xffff ? 0x1d : 0x1e) << 3;
} else {
field |= secondDelta << 3;
}
this->buffer.writeU8(field);
if (secondDelta >= 0x1d) {
if (secondDelta > 0xffff) {
writeFullTimestamp();
} else {
this->writeVarInt(secondDelta);
}
}
}
this->writeString(message.body);
}
finish:
// copy with second precision
this->lastMessageTime = QDateTime::fromSecsSinceEpoch(message.time.toSecsSinceEpoch());
return this->buffer.flush();
}
bool EncodedLogReader::read(LogMessage* slot) {
start:
quint32 next = 0;
if (!this->readVarInt(&next)) return false;
if (next < EncodedLogOpcode::BeginCategories) {
if (next == EncodedLogOpcode::RegisterCategory) {
if (!this->registerCategory()) return false;
goto start;
} else if (next == EncodedLogOpcode::RecentMessageShort
|| next == EncodedLogOpcode::RecentMessageLong)
{
quint8 index = 0;
quint32 secondDelta = 0;
if (next == EncodedLogOpcode::RecentMessageShort) {
quint8 field = 0;
if (!this->reader.readU8(&field)) return false;
index = field & 0xf;
secondDelta = field >> 4;
} else {
if (!this->reader.readU8(&index)) return false;
if (!this->readVarInt(&secondDelta)) return false;
}
if (index >= this->recentMessages.size()) return false;
*slot = this->recentMessages.at(index);
this->lastMessageTime = this->lastMessageTime.addSecs(static_cast<qint64>(secondDelta));
slot->time = this->lastMessageTime;
}
} else {
auto categoryId = next - EncodedLogOpcode::BeginCategories;
auto category = this->categories.value(categoryId);
quint8 field = 0;
if (!this->reader.readU8(&field)) return false;
auto msgType = QtDebugMsg;
quint64 secondDelta = 0;
auto needsTimeRead = false;
if (field == 0xff) {
msgType = QtFatalMsg;
needsTimeRead = true;
} else {
msgType = typeOfCompressed(static_cast<CompressedLogType>(field & 0x07));
secondDelta = field >> 3;
if (secondDelta == 0x1d) {
quint32 slot = 0;
if (!this->readVarInt(&slot)) return false;
secondDelta = slot;
} else if (secondDelta == 0x1e) {
needsTimeRead = true;
}
}
if (needsTimeRead) {
if (!this->reader.readU64(&secondDelta)) return false;
}
this->lastMessageTime = this->lastMessageTime.addSecs(static_cast<qint64>(secondDelta));
QByteArray body;
if (!this->readString(&body)) return false;
*slot = LogMessage(msgType, QLatin1StringView(category.first), body, this->lastMessageTime);
slot->readCategoryId = categoryId;
}
this->recentMessages.emplace(*slot);
return true;
}
CategoryFilter EncodedLogReader::categoryFilterById(quint16 id) {
return this->categories.value(id).second;
}
void EncodedLogWriter::writeOp(EncodedLogOpcode opcode) { this->buffer.writeU8(opcode); }
void EncodedLogWriter::writeVarInt(quint32 n) {
if (n < 0xff) {
this->buffer.writeU8(n);
} else if (n < 0xffff) {
this->buffer.writeU8(0xff);
this->buffer.writeU16(n);
} else {
this->buffer.writeU8(0xff);
this->buffer.writeU16(0xffff);
this->buffer.writeU32(n);
}
}
bool EncodedLogReader::readVarInt(quint32* slot) {
auto bytes = std::array<quint8, 7>();
auto readLength = this->reader.peekBytes(reinterpret_cast<char*>(bytes.data()), 7);
if (bytes[0] != 0xff && readLength >= 1) {
auto n = *reinterpret_cast<quint8*>(bytes.data());
if (!this->reader.skip(1)) return false;
*slot = qFromLittleEndian(n);
} else if ((bytes[1] != 0xff || bytes[2] != 0xff) && readLength >= 3) {
auto n = *reinterpret_cast<quint16*>(bytes.data() + 1);
if (!this->reader.skip(3)) return false;
*slot = qFromLittleEndian(n);
} else if (readLength == 7) {
auto n = *reinterpret_cast<quint32*>(bytes.data() + 3);
if (!this->reader.skip(7)) return false;
*slot = qFromLittleEndian(n);
} else return false;
return true;
}
void EncodedLogWriter::writeString(QByteArrayView bytes) {
this->writeVarInt(bytes.length());
this->buffer.writeBytes(bytes.constData(), bytes.length());
}
bool EncodedLogReader::readString(QByteArray* slot) {
quint32 length = 0;
if (!this->readVarInt(&length)) return false;
*slot = QByteArray(length, Qt::Uninitialized);
auto r = this->reader.readBytes(slot->data(), slot->size());
return r;
}
quint16 EncodedLogWriter::getOrCreateCategory(QLatin1StringView category) {
if (this->categories.contains(category)) {
return this->categories.value(category);
} else {
this->writeOp(EncodedLogOpcode::RegisterCategory);
// id is implicitly the next available id
this->writeString(category);
auto id = this->nextCategory++;
this->categories.insert(category, id);
auto filter = LogManager::instance()->getFilter(category);
quint8 flags = 0;
flags |= filter.debug << 0;
flags |= filter.info << 1;
flags |= filter.warn << 2;
flags |= filter.critical << 3;
this->buffer.writeU8(flags);
return id;
}
}
bool EncodedLogReader::registerCategory() {
QByteArray name;
quint8 flags = 0;
if (!this->readString(&name)) return false;
if (!this->reader.readU8(&flags)) return false;
CategoryFilter filter;
filter.debug = (flags >> 0) & 1;
filter.info = (flags >> 1) & 1;
filter.warn = (flags >> 2) & 1;
filter.critical = (flags >> 3) & 1;
this->categories.append(qMakePair(name, filter));
return true;
}
bool LogReader::initialize() {
this->reader.setDevice(this->file);
bool readable = false;
quint8 logVersion = 0;
quint8 readerVersion = 0;
if (!this->reader.readHeader(&readable, &logVersion, &readerVersion)) {
qCritical() << "Failed to read log header.";
return false;
}
if (!readable) {
qCritical() << "This log was encoded with version" << logVersion
<< "of the quickshell log encoder, which cannot be decoded by the current "
"version of quickshell, with log version"
<< readerVersion;
return false;
}
return true;
}
bool LogReader::continueReading() {
auto color = LogManager::instance()->colorLogs;
auto tailRing = RingBuffer<LogMessage>(this->remainingTail);
LogMessage message;
auto stream = QTextStream(stdout);
auto readCursor = this->file->pos();
while (this->reader.read(&message)) {
readCursor = this->file->pos();
CategoryFilter filter;
if (this->filters.contains(message.readCategoryId)) {
filter = this->filters.value(message.readCategoryId);
} else {
filter = this->reader.categoryFilterById(message.readCategoryId);
for (const auto& rule: this->rules) {
filter.applyRule(message.category, rule);
}
this->filters.insert(message.readCategoryId, filter);
}
if (filter.shouldDisplay(message.type)) {
if (this->remainingTail == 0) {
LogMessage::formatMessage(stream, message, color, this->timestamps);
stream << '\n';
} else {
tailRing.emplace(message);
}
}
}
if (this->remainingTail != 0) {
for (auto i = tailRing.size() - 1; i != -1; i--) {
auto& message = tailRing.at(i);
LogMessage::formatMessage(stream, message, color, this->timestamps);
stream << '\n';
}
}
stream << Qt::flush;
if (this->file->pos() != readCursor) {
qCritical() << "An error occurred parsing the end of this log file.";
qCritical() << "Remaining data:" << this->file->readAll();
return false;
}
return true;
}
void LogFollower::FcntlWaitThread::run() {
auto lock = flock {
.l_type = F_RDLCK, // won't block other read locks when we take it
.l_whence = SEEK_SET,
.l_start = 0,
.l_len = 0,
.l_pid = 0,
};
auto r = fcntl(this->follower->reader->file->handle(), F_SETLKW, &lock); // NOLINT
if (r != 0) {
qCWarning(logLogging).nospace()
<< "Failed to wait for write locks to be removed from log file with error code " << errno
<< ": " << qt_error_string();
}
}
bool LogFollower::follow() {
QObject::connect(&this->waitThread, &QThread::finished, this, &LogFollower::onFileLocked);
QObject::connect(
&this->fileWatcher,
&QFileSystemWatcher::fileChanged,
this,
&LogFollower::onFileChanged
);
this->fileWatcher.addPath(this->path);
this->waitThread.start();
auto r = QCoreApplication::exec();
return r == 0;
}
void LogFollower::onFileChanged() {
if (!this->reader->continueReading()) {
QCoreApplication::exit(1);
}
}
void LogFollower::onFileLocked() {
if (!this->reader->continueReading()) {
QCoreApplication::exit(1);
} else {
QCoreApplication::exit(0);
}
}
bool readEncodedLogs(
QFile* file,
const QString& path,
bool timestamps,
int tail,
bool follow,
const QString& rulespec
) {
QList<QLoggingRule> rules;
{
QLoggingSettingsParser parser;
parser.setContent(rulespec);
rules = parser.rules();
}
auto reader = LogReader(file, timestamps, tail, rules);
if (!reader.initialize()) return false;
if (!reader.continueReading()) return false;
if (follow) {
auto follower = LogFollower(&reader, path);
return follower.follow();
}
return true;
}
} // namespace qs::log

154
src/core/logging.hpp Normal file
View file

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

190
src/core/logging_p.hpp Normal file
View file

@ -0,0 +1,190 @@
#pragma once
#include <utility>
#include <qbytearrayview.h>
#include <qcontainerfwd.h>
#include <qfile.h>
#include <qfilesystemwatcher.h>
#include <qlogging.h>
#include <qobject.h>
#include <qthread.h>
#include <qtmetamacros.h>
#include <qtypes.h>
#include "logging.hpp"
#include "logging_qtprivate.hpp"
#include "ringbuf.hpp"
namespace qs::log {
enum EncodedLogOpcode : quint8 {
RegisterCategory = 0,
RecentMessageShort,
RecentMessageLong,
BeginCategories,
};
enum CompressedLogType : quint8 {
Debug = 0,
Info = 1,
Warn = 2,
Critical = 3,
};
CompressedLogType compressedTypeOf(QtMsgType type);
QtMsgType typeOfCompressed(CompressedLogType type);
class WriteBuffer {
public:
void setDevice(QIODevice* device);
[[nodiscard]] bool hasDevice() const;
[[nodiscard]] bool flush();
void writeBytes(const char* data, qsizetype length);
void writeU8(quint8 data);
void writeU16(quint16 data);
void writeU32(quint32 data);
void writeU64(quint64 data);
private:
QIODevice* device = nullptr;
QByteArray buffer;
};
class DeviceReader {
public:
void setDevice(QIODevice* device);
[[nodiscard]] bool hasDevice() const;
[[nodiscard]] bool readBytes(char* data, qsizetype length);
// peek UP TO length
[[nodiscard]] qsizetype peekBytes(char* data, qsizetype length);
[[nodiscard]] bool skip(qsizetype length);
[[nodiscard]] bool readU8(quint8* data);
[[nodiscard]] bool readU16(quint16* data);
[[nodiscard]] bool readU32(quint32* data);
[[nodiscard]] bool readU64(quint64* data);
private:
QIODevice* device = nullptr;
};
class EncodedLogWriter {
public:
void setDevice(QIODevice* target);
[[nodiscard]] bool writeHeader();
[[nodiscard]] bool write(const LogMessage& message);
private:
void writeOp(EncodedLogOpcode opcode);
void writeVarInt(quint32 n);
void writeString(QByteArrayView bytes);
quint16 getOrCreateCategory(QLatin1StringView category);
WriteBuffer buffer;
QHash<QLatin1StringView, quint16> categories;
quint16 nextCategory = EncodedLogOpcode::BeginCategories;
QDateTime lastMessageTime = QDateTime::fromSecsSinceEpoch(0);
HashBuffer<LogMessage> recentMessages {256};
};
class EncodedLogReader {
public:
void setDevice(QIODevice* source);
[[nodiscard]] bool readHeader(bool* success, quint8* logVersion, quint8* readerVersion);
// WARNING: log messages written to the given slot are invalidated when the log reader is destroyed.
[[nodiscard]] bool read(LogMessage* slot);
[[nodiscard]] CategoryFilter categoryFilterById(quint16 id);
private:
[[nodiscard]] bool readVarInt(quint32* slot);
[[nodiscard]] bool readString(QByteArray* slot);
[[nodiscard]] bool registerCategory();
DeviceReader reader;
QVector<QPair<QByteArray, CategoryFilter>> categories;
QDateTime lastMessageTime = QDateTime::fromSecsSinceEpoch(0);
RingBuffer<LogMessage> recentMessages {256};
};
class ThreadLogging: public QObject {
Q_OBJECT;
public:
explicit ThreadLogging(QObject* parent): QObject(parent) {}
void init();
void initFs();
void setupFileLogging();
private slots:
void onMessage(const LogMessage& msg, bool showInSparse);
private:
QFile* file = nullptr;
QTextStream fileStream;
QFile* detailedFile = nullptr;
EncodedLogWriter detailedWriter;
};
class LogFollower;
class LogReader {
public:
explicit LogReader(
QFile* file,
bool timestamps,
int tail,
QList<qt_logging_registry::QLoggingRule> rules
)
: file(file)
, timestamps(timestamps)
, remainingTail(tail)
, rules(std::move(rules)) {}
bool initialize();
bool continueReading();
private:
QFile* file;
EncodedLogReader reader;
bool timestamps;
int remainingTail;
QHash<quint16, CategoryFilter> filters;
QList<qt_logging_registry::QLoggingRule> rules;
friend class LogFollower;
};
class LogFollower: public QObject {
Q_OBJECT;
public:
explicit LogFollower(LogReader* reader, QString path): reader(reader), path(std::move(path)) {}
bool follow();
private slots:
void onFileChanged();
void onFileLocked();
private:
LogReader* reader;
QString path;
QFileSystemWatcher fileWatcher;
class FcntlWaitThread: public QThread {
public:
explicit FcntlWaitThread(LogFollower* follower): follower(follower) {}
protected:
void run() override;
private:
LogFollower* follower;
};
FcntlWaitThread waitThread {this};
};
} // namespace qs::log

View file

@ -0,0 +1,139 @@
// The logging rule parser from qloggingregistry_p.h and qloggingregistry.cpp.
// Was unable to properly link the functions when directly using the headers (which we depend
// on anyway), so below is a slightly stripped down copy. Making the originals link would
// be preferable.
#include <utility>
#include <qbytearrayview.h>
#include <qchar.h>
#include <qflags.h>
#include <qlist.h>
#include <qlogging.h>
#include <qloggingcategory.h>
#include <qstringtokenizer.h>
#include <qstringview.h>
#include <qtypes.h>
#include "logcat.hpp"
#include "logging_qtprivate.hpp"
namespace qs::log {
QS_DECLARE_LOGGING_CATEGORY(logLogging);
namespace qt_logging_registry {
class QLoggingSettingsParser {
public:
void setContent(QStringView content);
[[nodiscard]] QList<QLoggingRule> rules() const { return this->mRules; }
private:
void parseNextLine(QStringView line);
private:
QList<QLoggingRule> mRules;
};
void QLoggingSettingsParser::setContent(QStringView content) {
this->mRules.clear();
for (auto line: qTokenize(content, u';')) this->parseNextLine(line);
}
void QLoggingSettingsParser::parseNextLine(QStringView line) {
// Remove whitespace at start and end of line:
line = line.trimmed();
const qsizetype equalPos = line.indexOf(u'=');
if (equalPos != -1) {
if (line.lastIndexOf(u'=') == equalPos) {
const auto key = line.left(equalPos).trimmed();
const QStringView pattern = key;
const auto valueStr = line.mid(equalPos + 1).trimmed();
int value = -1;
if (valueStr == QString("true")) value = 1;
else if (valueStr == QString("false")) value = 0;
QLoggingRule rule(pattern, (value == 1));
if (rule.flags != 0 && (value != -1)) this->mRules.append(std::move(rule));
else
qCWarning(logLogging, "Ignoring malformed logging rule: '%s'", line.toUtf8().constData());
} else {
qCWarning(logLogging, "Ignoring malformed logging rule: '%s'", line.toUtf8().constData());
}
}
}
QLoggingRule::QLoggingRule(QStringView pattern, bool enabled): messageType(-1), enabled(enabled) {
this->parse(pattern);
}
void QLoggingRule::parse(QStringView pattern) {
QStringView p;
// strip trailing ".messagetype"
if (pattern.endsWith(QString(".debug"))) {
p = pattern.chopped(6); // strlen(".debug")
this->messageType = QtDebugMsg;
} else if (pattern.endsWith(QString(".info"))) {
p = pattern.chopped(5); // strlen(".info")
this->messageType = QtInfoMsg;
} else if (pattern.endsWith(QString(".warning"))) {
p = pattern.chopped(8); // strlen(".warning")
this->messageType = QtWarningMsg;
} else if (pattern.endsWith(QString(".critical"))) {
p = pattern.chopped(9); // strlen(".critical")
this->messageType = QtCriticalMsg;
} else {
p = pattern;
}
const QChar asterisk = u'*';
if (!p.contains(asterisk)) {
this->flags = FullText;
} else {
if (p.endsWith(asterisk)) {
this->flags |= LeftFilter;
p = p.chopped(1);
}
if (p.startsWith(asterisk)) {
this->flags |= RightFilter;
p = p.mid(1);
}
if (p.contains(asterisk)) // '*' only supported at start/end
this->flags = PatternFlags();
}
this->category = p.toString();
}
int QLoggingRule::pass(QLatin1StringView cat, QtMsgType msgType) const {
// check message type
if (this->messageType > -1 && this->messageType != msgType) return 0;
if (this->flags == FullText) {
// full match
if (this->category == cat) return (this->enabled ? 1 : -1);
else return 0;
}
const qsizetype idx = cat.indexOf(this->category);
if (idx >= 0) {
if (this->flags == MidFilter) {
// matches somewhere
return (this->enabled ? 1 : -1);
} else if (this->flags == LeftFilter) {
// matches left
if (idx == 0) return (this->enabled ? 1 : -1);
} else if (this->flags == RightFilter) {
// matches right
if (idx == (cat.size() - this->category.size())) return (this->enabled ? 1 : -1);
}
}
return 0;
}
} // namespace qt_logging_registry
} // namespace qs::log

View file

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

View file

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

81
src/core/model.cpp Normal file
View file

@ -0,0 +1,81 @@
#include "model.hpp"
#include <qabstractitemmodel.h>
#include <qhash.h>
#include <qnamespace.h>
#include <qobject.h>
#include <qqmllist.h>
#include <qtmetamacros.h>
#include <qtypes.h>
#include <qvariant.h>
qint32 UntypedObjectModel::rowCount(const QModelIndex& parent) const {
if (parent != QModelIndex()) return 0;
return static_cast<qint32>(this->valuesList.length());
}
QVariant UntypedObjectModel::data(const QModelIndex& index, qint32 role) const {
if (role != Qt::UserRole) return QVariant();
return QVariant::fromValue(this->valuesList.at(index.row()));
}
QHash<int, QByteArray> UntypedObjectModel::roleNames() const {
return {{Qt::UserRole, "modelData"}};
}
void UntypedObjectModel::insertObject(QObject* object, qsizetype index) {
auto iindex = index == -1 ? this->valuesList.length() : index;
emit this->objectInsertedPre(object, iindex);
auto intIndex = static_cast<qint32>(iindex);
this->beginInsertRows(QModelIndex(), intIndex, intIndex);
this->valuesList.insert(iindex, object);
this->endInsertRows();
emit this->valuesChanged();
emit this->objectInsertedPost(object, iindex);
}
void UntypedObjectModel::removeAt(qsizetype index) {
auto* object = this->valuesList.at(index);
emit this->objectRemovedPre(object, index);
auto intIndex = static_cast<qint32>(index);
this->beginRemoveRows(QModelIndex(), intIndex, intIndex);
this->valuesList.removeAt(index);
this->endRemoveRows();
emit this->valuesChanged();
emit this->objectRemovedPost(object, index);
}
bool UntypedObjectModel::removeObject(const QObject* object) {
auto index = this->valuesList.indexOf(object);
if (index == -1) return false;
this->removeAt(index);
return true;
}
void UntypedObjectModel::diffUpdate(const QVector<QObject*>& newValues) {
for (qsizetype i = 0; i < this->valuesList.length();) {
if (newValues.contains(this->valuesList.at(i))) i++;
else this->removeAt(i);
}
qsizetype oi = 0;
for (auto* object: newValues) {
if (this->valuesList.length() == oi || this->valuesList.at(oi) != object) {
this->insertObject(object, oi);
}
oi++;
}
}
qsizetype UntypedObjectModel::indexOf(QObject* object) { return this->valuesList.indexOf(object); }
UntypedObjectModel* UntypedObjectModel::emptyInstance() {
static auto* instance = new UntypedObjectModel(nullptr); // NOLINT
return instance;
}

126
src/core/model.hpp Normal file
View file

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

View file

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

190
src/core/objectrepeater.cpp Normal file
View file

@ -0,0 +1,190 @@
#include "objectrepeater.hpp"
#include <utility>
#include <qabstractitemmodel.h>
#include <qcontainerfwd.h>
#include <qhash.h>
#include <qlogging.h>
#include <qobject.h>
#include <qqmlcomponent.h>
#include <qqmlcontext.h>
#include <qqmlengine.h>
#include <qqmllist.h>
#include <qtmetamacros.h>
#include <qtypes.h>
#include <qvariant.h>
QVariant ObjectRepeater::model() const { return this->mModel; }
void ObjectRepeater::setModel(QVariant model) {
if (model == this->mModel) return;
if (this->itemModel != nullptr) {
QObject::disconnect(this->itemModel, nullptr, this, nullptr);
}
this->mModel = std::move(model);
emit this->modelChanged();
this->reloadElements();
}
void ObjectRepeater::onModelDestroyed() {
this->mModel.clear();
this->itemModel = nullptr;
emit this->modelChanged();
this->reloadElements();
}
QQmlComponent* ObjectRepeater::delegate() const { return this->mDelegate; }
void ObjectRepeater::setDelegate(QQmlComponent* delegate) {
if (delegate == this->mDelegate) return;
if (this->mDelegate != nullptr) {
QObject::disconnect(this->mDelegate, nullptr, this, nullptr);
}
this->mDelegate = delegate;
if (delegate != nullptr) {
QObject::connect(
this->mDelegate,
&QObject::destroyed,
this,
&ObjectRepeater::onDelegateDestroyed
);
}
emit this->delegateChanged();
this->reloadElements();
}
void ObjectRepeater::onDelegateDestroyed() {
this->mDelegate = nullptr;
emit this->delegateChanged();
this->reloadElements();
}
void ObjectRepeater::reloadElements() {
for (auto i = this->valuesList.length() - 1; i >= 0; i--) {
this->removeComponent(i);
}
if (this->mDelegate == nullptr || !this->mModel.isValid()) return;
if (this->mModel.canConvert<QAbstractItemModel*>()) {
auto* model = this->mModel.value<QAbstractItemModel*>();
this->itemModel = model;
this->insertModelElements(model, 0, model->rowCount() - 1); // -1 is fine
// clang-format off
QObject::connect(model, &QObject::destroyed, this, &ObjectRepeater::onModelDestroyed);
QObject::connect(model, &QAbstractItemModel::rowsInserted, this, &ObjectRepeater::onModelRowsInserted);
QObject::connect(model, &QAbstractItemModel::rowsRemoved, this, &ObjectRepeater::onModelRowsRemoved);
QObject::connect(model, &QAbstractItemModel::rowsMoved, this, &ObjectRepeater::onModelRowsMoved);
QObject::connect(model, &QAbstractItemModel::modelAboutToBeReset, this, &ObjectRepeater::onModelAboutToBeReset);
// clang-format on
} else if (this->mModel.canConvert<QQmlListReference>()) {
auto values = this->mModel.value<QQmlListReference>();
auto len = values.count();
for (auto i = 0; i != len; i++) {
this->insertComponent(i, {{"modelData", QVariant::fromValue(values.at(i))}});
}
} else if (this->mModel.canConvert<QVector<QVariant>>()) {
auto values = this->mModel.value<QVector<QVariant>>();
for (auto& value: values) {
this->insertComponent(this->valuesList.length(), {{"modelData", value}});
}
} else {
qCritical() << this
<< "Cannot create components as the model is not compatible:" << this->mModel;
}
}
void ObjectRepeater::insertModelElements(QAbstractItemModel* model, int first, int last) {
auto roles = model->roleNames();
auto roleDataVec = QVector<QModelRoleData>();
for (auto id: roles.keys()) {
roleDataVec.push_back(QModelRoleData(id));
}
auto values = QModelRoleDataSpan(roleDataVec);
auto props = QVariantMap();
for (auto i = first; i != last + 1; i++) {
auto index = model->index(i, 0);
model->multiData(index, values);
for (auto [id, name]: roles.asKeyValueRange()) {
props.insert(name, *values.dataForRole(id));
}
this->insertComponent(i, props);
props.clear();
}
}
void ObjectRepeater::onModelRowsInserted(const QModelIndex& parent, int first, int last) {
if (parent != QModelIndex()) return;
this->insertModelElements(this->itemModel, first, last);
}
void ObjectRepeater::onModelRowsRemoved(const QModelIndex& parent, int first, int last) {
if (parent != QModelIndex()) return;
for (auto i = last; i != first - 1; i--) {
this->removeComponent(i);
}
}
void ObjectRepeater::onModelRowsMoved(
const QModelIndex& sourceParent,
int sourceStart,
int sourceEnd,
const QModelIndex& destParent,
int destStart
) {
auto hasSource = sourceParent != QModelIndex();
auto hasDest = destParent != QModelIndex();
if (!hasSource && !hasDest) return;
if (hasSource) {
this->onModelRowsRemoved(sourceParent, sourceStart, sourceEnd);
}
if (hasDest) {
this->onModelRowsInserted(destParent, destStart, destStart + (sourceEnd - sourceStart));
}
}
void ObjectRepeater::onModelAboutToBeReset() {
auto last = static_cast<int>(this->valuesList.length() - 1);
this->onModelRowsRemoved(QModelIndex(), 0, last); // -1 is fine
}
void ObjectRepeater::insertComponent(qsizetype index, const QVariantMap& properties) {
auto* context = QQmlEngine::contextForObject(this);
auto* instance = this->mDelegate->createWithInitialProperties(properties, context);
if (instance == nullptr) {
qWarning().noquote() << this->mDelegate->errorString();
qWarning() << this << "failed to create object for model data" << properties;
} else {
QQmlEngine::setObjectOwnership(instance, QQmlEngine::CppOwnership);
instance->setParent(this);
}
this->insertObject(instance, index);
}
void ObjectRepeater::removeComponent(qsizetype index) {
auto* instance = this->valuesList.at(index);
this->removeAt(index);
delete instance;
}

View file

@ -0,0 +1,85 @@
#pragma once
#include <qabstractitemmodel.h>
#include <qobject.h>
#include <qqmlcomponent.h>
#include <qqmlintegration.h>
#include <qtmetamacros.h>
#include <qtypes.h>
#include <qvariant.h>
#include "model.hpp"
///! A Repeater / for loop / map for non Item derived objects.
/// > [!ERROR] Removed in favor of @@QtQml.Models.Instantiator
///
/// The ObjectRepeater creates instances of the provided delegate for every entry in the
/// given model, similarly to a @@QtQuick.Repeater but for non visual types.
class ObjectRepeater: public ObjectModel<QObject> {
Q_OBJECT;
/// The model providing data to the ObjectRepeater.
///
/// Currently accepted model types are `list<T>` lists, javascript arrays,
/// and [QAbstractListModel] derived models, though only one column will be repeated
/// from the latter.
///
/// Note: @@ObjectModel is a [QAbstractListModel] with a single column.
///
/// [QAbstractListModel]: https://doc.qt.io/qt-6/qabstractlistmodel.html
Q_PROPERTY(QVariant model READ model WRITE setModel NOTIFY modelChanged);
/// The delegate component to repeat.
///
/// The delegate is given the same properties as in a Repeater, except `index` which
/// is not currently implemented.
///
/// If the model is a `list<T>` or javascript array, a `modelData` property will be
/// exposed containing the entry from the model. If the model is a [QAbstractListModel],
/// the roles from the model will be exposed.
///
/// Note: @@ObjectModel has a single role named `modelData` for compatibility with normal lists.
///
/// [QAbstractListModel]: https://doc.qt.io/qt-6/qabstractlistmodel.html
Q_PROPERTY(QQmlComponent* delegate READ delegate WRITE setDelegate NOTIFY delegateChanged);
Q_CLASSINFO("DefaultProperty", "delegate");
QML_ELEMENT;
QML_UNCREATABLE("ObjectRepeater has been removed in favor of QtQml.Models.Instantiator.");
public:
explicit ObjectRepeater(QObject* parent = nullptr): ObjectModel(parent) {}
[[nodiscard]] QVariant model() const;
void setModel(QVariant model);
[[nodiscard]] QQmlComponent* delegate() const;
void setDelegate(QQmlComponent* delegate);
signals:
void modelChanged();
void delegateChanged();
private slots:
void onDelegateDestroyed();
void onModelDestroyed();
void onModelRowsInserted(const QModelIndex& parent, int first, int last);
void onModelRowsRemoved(const QModelIndex& parent, int first, int last);
void onModelRowsMoved(
const QModelIndex& sourceParent,
int sourceStart,
int sourceEnd,
const QModelIndex& destParent,
int destStart
);
void onModelAboutToBeReset();
private:
void reloadElements();
void insertModelElements(QAbstractItemModel* model, int first, int last);
void insertComponent(qsizetype index, const QVariantMap& properties);
void removeComponent(qsizetype index);
QVariant mModel;
QAbstractItemModel* itemModel = nullptr;
QQmlComponent* mDelegate = nullptr;
};

425
src/core/paths.cpp Normal file
View file

@ -0,0 +1,425 @@
#include "paths.hpp"
#include <cerrno>
#include <cstdio>
#include <utility>
#include <fcntl.h>
#include <qcontainerfwd.h>
#include <qdatastream.h>
#include <qdir.h>
#include <qlogging.h>
#include <qloggingcategory.h>
#include <qpair.h>
#include <qstandardpaths.h>
#include <qtenvironmentvariables.h>
#include <qtversionchecks.h>
#include <unistd.h>
#include "instanceinfo.hpp"
#include "logcat.hpp"
namespace {
QS_LOGGING_CATEGORY(logPaths, "quickshell.paths", QtWarningMsg);
}
QsPaths* QsPaths::instance() {
static auto* instance = new QsPaths(); // NOLINT
return instance;
}
void QsPaths::init(QString shellId, QString pathId, QString dataOverride, QString stateOverride) {
auto* instance = QsPaths::instance();
instance->shellId = std::move(shellId);
instance->pathId = std::move(pathId);
instance->shellDataOverride = std::move(dataOverride);
instance->shellStateOverride = std::move(stateOverride);
}
QDir QsPaths::crashDir(const QString& id) {
auto dir = QDir(QStandardPaths::writableLocation(QStandardPaths::CacheLocation));
dir = QDir(dir.filePath("crashes"));
dir = QDir(dir.filePath(id));
return dir;
}
QString QsPaths::basePath(const QString& id) {
auto path = QsPaths::instance()->baseRunDir()->filePath("by-id");
path = QDir(path).filePath(id);
return path;
}
QString QsPaths::ipcPath(const QString& id) {
return QDir(QsPaths::basePath(id)).filePath("ipc.sock");
}
QDir* QsPaths::baseRunDir() {
if (this->baseRunState == DirState::Unknown) {
auto runtimeDir = qEnvironmentVariable("XDG_RUNTIME_DIR");
if (runtimeDir.isEmpty()) {
runtimeDir = QString("/run/user/$1").arg(getuid());
qCInfo(logPaths) << "XDG_RUNTIME_DIR was not set, defaulting to" << runtimeDir;
}
this->mBaseRunDir = QDir(runtimeDir);
this->mBaseRunDir = QDir(this->mBaseRunDir.filePath("quickshell"));
qCDebug(logPaths) << "Initialized base runtime path:" << this->mBaseRunDir.path();
if (!this->mBaseRunDir.mkpath(".")) {
qCCritical(logPaths) << "Could not create base runtime directory at"
<< this->mBaseRunDir.path();
this->baseRunState = DirState::Failed;
} else {
this->baseRunState = DirState::Ready;
}
}
if (this->baseRunState == DirState::Failed) return nullptr;
else return &this->mBaseRunDir;
}
QDir* QsPaths::shellRunDir() {
if (this->shellRunState == DirState::Unknown) {
if (auto* baseRunDir = this->baseRunDir()) {
this->mShellRunDir = QDir(baseRunDir->filePath("by-shell"));
this->mShellRunDir = QDir(this->mShellRunDir.filePath(this->shellId));
qCDebug(logPaths) << "Initialized runtime path:" << this->mShellRunDir.path();
if (!this->mShellRunDir.mkpath(".")) {
qCCritical(logPaths) << "Could not create runtime directory at"
<< this->mShellRunDir.path();
this->shellRunState = DirState::Failed;
} else {
this->shellRunState = DirState::Ready;
}
} else {
qCCritical(logPaths) << "Could not create shell runtime path as it was not possible to "
"create the base runtime path.";
this->shellRunState = DirState::Failed;
}
}
if (this->shellRunState == DirState::Failed) return nullptr;
else return &this->mShellRunDir;
}
QDir* QsPaths::instanceRunDir() {
if (this->instanceRunState == DirState::Unknown) {
auto* runDir = this->baseRunDir();
if (!runDir) {
qCCritical(logPaths) << "Cannot create instance runtime directory as main runtim directory "
"could not be created.";
this->instanceRunState = DirState::Failed;
} else {
auto byIdDir = QDir(runDir->filePath("by-id"));
this->mInstanceRunDir = byIdDir.filePath(InstanceInfo::CURRENT.instanceId);
qCDebug(logPaths) << "Initialized instance runtime path:" << this->mInstanceRunDir.path();
if (!this->mInstanceRunDir.mkpath(".")) {
qCCritical(logPaths) << "Could not create instance runtime directory at"
<< this->mInstanceRunDir.path();
this->instanceRunState = DirState::Failed;
} else {
this->instanceRunState = DirState::Ready;
}
}
}
if (this->shellRunState == DirState::Failed) return nullptr;
else return &this->mInstanceRunDir;
}
QDir* QsPaths::shellVfsDir() {
if (this->shellVfsState == DirState::Unknown) {
if (auto* baseRunDir = this->baseRunDir()) {
this->mShellVfsDir = QDir(baseRunDir->filePath("vfs"));
this->mShellVfsDir = QDir(this->mShellVfsDir.filePath(this->shellId));
qCDebug(logPaths) << "Initialized runtime vfs path:" << this->mShellVfsDir.path();
if (!this->mShellVfsDir.mkpath(".")) {
qCCritical(logPaths) << "Could not create runtime vfs directory at"
<< this->mShellVfsDir.path();
this->shellVfsState = DirState::Failed;
} else {
this->shellVfsState = DirState::Ready;
}
} else {
qCCritical(logPaths) << "Could not create shell runtime vfs path as it was not possible to "
"create the base runtime path.";
this->shellVfsState = DirState::Failed;
}
}
if (this->shellVfsState == DirState::Failed) return nullptr;
else return &this->mShellVfsDir;
}
void QsPaths::linkRunDir() {
if (auto* runDir = this->instanceRunDir()) {
auto pidDir = QDir(this->baseRunDir()->filePath("by-pid"));
auto* shellDir = this->shellRunDir();
if (!shellDir) {
qCCritical(logPaths
) << "Could not create by-id symlink as the shell runtime path could not be created.";
} else {
auto shellPath = shellDir->filePath(runDir->dirName());
QFile::remove(shellPath);
auto r =
symlinkat(runDir->filesystemCanonicalPath().c_str(), 0, shellPath.toStdString().c_str());
if (r != 0) {
qCCritical(logPaths).nospace()
<< "Could not create id symlink to " << runDir->path() << " at " << shellPath
<< " with error code " << errno << ": " << qt_error_string();
} else {
qCDebug(logPaths) << "Created shellid symlink" << shellPath << "to instance runtime path"
<< runDir->path();
}
}
if (!pidDir.mkpath(".")) {
qCCritical(logPaths) << "Could not create PID symlink directory.";
} else {
auto pidPath = pidDir.filePath(QString::number(getpid()));
QFile::remove(pidPath);
auto r =
symlinkat(runDir->filesystemCanonicalPath().c_str(), 0, pidPath.toStdString().c_str());
if (r != 0) {
qCCritical(logPaths).nospace()
<< "Could not create PID symlink to " << runDir->path() << " at " << pidPath
<< " with error code " << errno << ": " << qt_error_string();
} else {
qCDebug(logPaths) << "Created PID symlink" << pidPath << "to instance runtime path"
<< runDir->path();
}
}
} else {
qCCritical(logPaths) << "Could not create PID symlink to runtime directory, as the runtime "
"directory could not be created.";
}
}
void QsPaths::linkPathDir() {
if (auto* runDir = this->shellRunDir()) {
auto pathDir = QDir(this->baseRunDir()->filePath("by-path"));
if (!pathDir.mkpath(".")) {
qCCritical(logPaths) << "Could not create path symlink directory.";
return;
}
auto linkPath = pathDir.filePath(this->pathId);
QFile::remove(linkPath);
auto r =
symlinkat(runDir->filesystemCanonicalPath().c_str(), 0, linkPath.toStdString().c_str());
if (r != 0) {
qCCritical(logPaths).nospace()
<< "Could not create path symlink to " << runDir->path() << " at " << linkPath
<< " with error code " << errno << ": " << qt_error_string();
} else {
qCDebug(logPaths) << "Created path symlink" << linkPath << "to shell runtime path"
<< runDir->path();
}
} else {
qCCritical(logPaths) << "Could not create path symlink to shell runtime directory, as the "
"shell runtime directory could not be created.";
}
}
QDir QsPaths::shellDataDir() {
if (this->shellDataState == DirState::Unknown) {
QDir dir;
if (this->shellDataOverride.isEmpty()) {
dir = QDir(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation));
dir = QDir(dir.filePath("by-shell"));
dir = QDir(dir.filePath(this->shellId));
} else {
auto basedir = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation);
dir = QDir(this->shellDataOverride.replace("$BASE", basedir));
}
this->mShellDataDir = dir;
qCDebug(logPaths) << "Initialized data path:" << dir.path();
if (!dir.mkpath(".")) {
qCCritical(logPaths) << "Could not create data directory at" << dir.path();
this->shellDataState = DirState::Failed;
} else {
this->shellDataState = DirState::Ready;
}
}
// Returning no path on fail might result in files being written in unintended locations.
return this->mShellDataDir;
}
QDir QsPaths::shellStateDir() {
if (this->shellStateState == DirState::Unknown) {
#if QT_VERSION < QT_VERSION_CHECK(6, 7, 0)
QDir dir;
if (qEnvironmentVariableIsSet("XDG_STATE_HOME")) {
dir = QDir(qEnvironmentVariable("XDG_STATE_HOME"));
} else {
auto home = QDir(QStandardPaths::writableLocation(QStandardPaths::HomeLocation));
dir = QDir(home.filePath(".local/state"));
}
if (this->shellStateOverride.isEmpty()) {
dir = QDir(dir.filePath("quickshell/by-shell"));
dir = QDir(dir.filePath(this->shellId));
} else {
dir = QDir(this->shellStateOverride.replace("$BASE", dir.path()));
}
#else
QDir dir;
if (this->shellStateOverride.isEmpty()) {
dir = QDir(QStandardPaths::writableLocation(QStandardPaths::StateLocation));
dir = QDir(dir.filePath("by-shell"));
dir = QDir(dir.filePath(this->shellId));
} else {
auto basedir = QStandardPaths::writableLocation(QStandardPaths::GenericStateLocation);
dir = QDir(this->shellStateOverride.replace("$BASE", basedir));
}
#endif
this->mShellStateDir = dir;
qCDebug(logPaths) << "Initialized state path:" << dir.path();
if (!dir.mkpath(".")) {
qCCritical(logPaths) << "Could not create state directory at" << dir.path();
this->shellStateState = DirState::Failed;
} else {
this->shellStateState = DirState::Ready;
}
}
// Returning no path on fail might result in files being written in unintended locations.
return this->mShellStateDir;
}
QDir QsPaths::shellCacheDir() {
if (this->shellCacheState == DirState::Unknown) {
auto dir = QDir(QStandardPaths::writableLocation(QStandardPaths::CacheLocation));
dir = QDir(dir.filePath("by-shell"));
dir = QDir(dir.filePath(this->shellId));
this->mShellCacheDir = dir;
qCDebug(logPaths) << "Initialized cache path:" << dir.path();
if (!dir.mkpath(".")) {
qCCritical(logPaths) << "Could not create cache directory at" << dir.path();
this->shellCacheState = DirState::Failed;
} else {
this->shellCacheState = DirState::Ready;
}
}
// Returning no path on fail might result in files being written in unintended locations.
return this->mShellCacheDir;
}
void QsPaths::createLock() {
if (auto* runDir = this->instanceRunDir()) {
auto path = runDir->filePath("instance.lock");
auto* file = new QFile(path); // leaked
if (!file->open(QFile::ReadWrite | QFile::Truncate)) {
qCCritical(logPaths) << "Could not create instance lock at" << path;
return;
}
auto lock = flock {
.l_type = F_WRLCK,
.l_whence = SEEK_SET,
.l_start = 0,
.l_len = 0,
.l_pid = 0,
};
if (fcntl(file->handle(), F_SETLK, &lock) != 0) { // NOLINT
qCCritical(logPaths).nospace() << "Could not lock instance lock at " << path
<< " with error code " << errno << ": " << qt_error_string();
} else {
auto stream = QDataStream(file);
stream << InstanceInfo::CURRENT;
file->flush();
qCDebug(logPaths) << "Created instance lock at" << path;
}
} else {
qCCritical(logPaths
) << "Could not create instance lock, as the instance runtime directory could not be created.";
}
}
bool QsPaths::checkLock(const QString& path, InstanceLockInfo* info, bool allowDead) {
auto file = QFile(QDir(path).filePath("instance.lock"));
if (!file.open(QFile::ReadOnly)) return false;
auto lock = flock {
.l_type = F_WRLCK,
.l_whence = SEEK_SET,
.l_start = 0,
.l_len = 0,
.l_pid = 0,
};
fcntl(file.handle(), F_GETLK, &lock); // NOLINT
auto isLocked = lock.l_type != F_UNLCK;
if (!isLocked && !allowDead) return false;
if (info) {
info->pid = isLocked ? lock.l_pid : -1;
auto stream = QDataStream(&file);
stream >> info->instance;
}
return true;
}
QPair<QVector<InstanceLockInfo>, QVector<InstanceLockInfo>>
QsPaths::collectInstances(const QString& path) {
qCDebug(logPaths) << "Collecting instances from" << path;
auto liveInstances = QVector<InstanceLockInfo>();
auto deadInstances = QVector<InstanceLockInfo>();
auto dir = QDir(path);
InstanceLockInfo info;
for (auto& entry: dir.entryList(QDir::Dirs | QDir::NoDotAndDotDot)) {
auto path = dir.filePath(entry);
if (QsPaths::checkLock(path, &info, true)) {
qCDebug(logPaths).nospace() << "Found instance " << info.instance.instanceId << " (pid "
<< info.pid << ") at " << path;
if (info.pid == -1) {
deadInstances.push_back(info);
} else {
liveInstances.push_back(info);
}
} else {
qCDebug(logPaths) << "Skipped potential instance at" << path;
}
}
return qMakePair(liveInstances, deadInstances);
}

68
src/core/paths.hpp Normal file
View file

@ -0,0 +1,68 @@
#pragma once
#include <qdatetime.h>
#include <qdir.h>
#include <qpair.h>
#include <qtypes.h>
#include "instanceinfo.hpp"
struct InstanceLockInfo {
pid_t pid = -1;
InstanceInfo instance;
};
QDataStream& operator<<(QDataStream& stream, const InstanceLockInfo& info);
QDataStream& operator>>(QDataStream& stream, InstanceLockInfo& info);
class QsPaths {
public:
static QsPaths* instance();
static void init(QString shellId, QString pathId, QString dataOverride, QString stateOverride);
static QDir crashDir(const QString& id);
static QString basePath(const QString& id);
static QString ipcPath(const QString& id);
static bool
checkLock(const QString& path, InstanceLockInfo* info = nullptr, bool allowDead = false);
static QPair<QVector<InstanceLockInfo>, QVector<InstanceLockInfo>>
collectInstances(const QString& path);
QDir* baseRunDir();
QDir* shellRunDir();
QDir* shellVfsDir();
QDir* instanceRunDir();
void linkRunDir();
void linkPathDir();
void createLock();
QDir shellDataDir();
QDir shellStateDir();
QDir shellCacheDir();
private:
enum class DirState : quint8 {
Unknown = 0,
Ready = 1,
Failed = 2,
};
QString shellId;
QString pathId;
QDir mBaseRunDir;
QDir mShellRunDir;
QDir mShellVfsDir;
QDir mInstanceRunDir;
DirState baseRunState = DirState::Unknown;
DirState shellRunState = DirState::Unknown;
DirState shellVfsState = DirState::Unknown;
DirState instanceRunState = DirState::Unknown;
QDir mShellDataDir;
QDir mShellStateDir;
QDir mShellCacheDir;
DirState shellDataState = DirState::Unknown;
DirState shellStateState = DirState::Unknown;
DirState shellCacheState = DirState::Unknown;
QString shellDataOverride;
QString shellStateOverride;
};

324
src/core/platformmenu.cpp Normal file
View file

@ -0,0 +1,324 @@
#include "platformmenu.hpp"
#include <functional>
#include <utility>
#include <qaction.h>
#include <qactiongroup.h>
#include <qapplication.h>
#include <qcontainerfwd.h>
#include <qcoreapplication.h>
#include <qicon.h>
#include <qlogging.h>
#include <qmenu.h>
#include <qnamespace.h>
#include <qobject.h>
#include <qpoint.h>
#include <qqmllist.h>
#include <qtmetamacros.h>
#include <qwindow.h>
#include "../window/proxywindow.hpp"
#include "../window/windowinterface.hpp"
#include "iconprovider.hpp"
#include "model.hpp"
#include "platformmenu_p.hpp"
#include "popupanchor.hpp"
#include "qsmenu.hpp"
namespace qs::menu::platform {
namespace {
QVector<std::function<void(PlatformMenuQMenu*)>> CREATION_HOOKS; // NOLINT
PlatformMenuQMenu* ACTIVE_MENU = nullptr; // NOLINT
} // namespace
PlatformMenuQMenu::~PlatformMenuQMenu() {
if (this == ACTIVE_MENU) {
ACTIVE_MENU = nullptr;
}
}
void PlatformMenuQMenu::setVisible(bool visible) {
if (visible) {
for (auto& hook: CREATION_HOOKS) {
hook(this);
}
} else {
if (this == ACTIVE_MENU) {
ACTIVE_MENU = nullptr;
}
}
this->QMenu::setVisible(visible);
}
PlatformMenuEntry::PlatformMenuEntry(QsMenuEntry* menu): QObject(menu), menu(menu) {
this->relayout();
// clang-format off
QObject::connect(menu, &QsMenuEntry::enabledChanged, this, &PlatformMenuEntry::onEnabledChanged);
QObject::connect(menu, &QsMenuEntry::textChanged, this, &PlatformMenuEntry::onTextChanged);
QObject::connect(menu, &QsMenuEntry::iconChanged, this, &PlatformMenuEntry::onIconChanged);
QObject::connect(menu, &QsMenuEntry::buttonTypeChanged, this, &PlatformMenuEntry::onButtonTypeChanged);
QObject::connect(menu, &QsMenuEntry::checkStateChanged, this, &PlatformMenuEntry::onCheckStateChanged);
QObject::connect(menu, &QsMenuEntry::hasChildrenChanged, this, &PlatformMenuEntry::relayoutParent);
QObject::connect(menu->children(), &UntypedObjectModel::valuesChanged, this, &PlatformMenuEntry::relayout);
// clang-format on
}
PlatformMenuEntry::~PlatformMenuEntry() {
this->clearChildren();
delete this->qaction;
delete this->qmenu;
}
void PlatformMenuEntry::registerCreationHook(std::function<void(PlatformMenuQMenu*)> hook) {
CREATION_HOOKS.push_back(std::move(hook));
}
bool PlatformMenuEntry::display(QObject* parentWindow, int relativeX, int relativeY) {
QWindow* window = nullptr;
if (qobject_cast<QApplication*>(QCoreApplication::instance()) == nullptr) {
qCritical() << "Cannot display PlatformMenuEntry as quickshell was not started in "
"QApplication mode.";
qCritical() << "To use platform menus, add `//@ pragma UseQApplication` to the top of your "
"root QML file and restart quickshell.";
return false;
} else if (this->qmenu == nullptr) {
qCritical() << "Cannot display PlatformMenuEntry as it is not a menu.";
return false;
} else if (parentWindow == nullptr) {
qCritical() << "Cannot display PlatformMenuEntry with null parent window.";
return false;
} else if (auto* proxy = qobject_cast<ProxyWindowBase*>(parentWindow)) {
window = proxy->backingWindow();
} else if (auto* interface = qobject_cast<WindowInterface*>(parentWindow)) {
window = interface->proxyWindow()->backingWindow();
} else {
qCritical() << "PlatformMenuEntry.display() must be called with a window.";
return false;
}
if (window == nullptr) {
qCritical() << "Cannot display PlatformMenuEntry from a parent window that is not visible.";
return false;
}
if (ACTIVE_MENU && this->qmenu != ACTIVE_MENU) {
ACTIVE_MENU->close();
}
ACTIVE_MENU = this->qmenu;
auto point = window->mapToGlobal(QPoint(relativeX, relativeY));
this->qmenu->createWinId();
this->qmenu->windowHandle()->setTransientParent(window);
// Skips screen edge repositioning so it can be left to the compositor on wayland.
this->qmenu->targetPosition = point;
this->qmenu->popup(point);
return true;
}
bool PlatformMenuEntry::display(PopupAnchor* anchor) {
if (qobject_cast<QApplication*>(QCoreApplication::instance()) == nullptr) {
qCritical() << "Cannot display PlatformMenuEntry as quickshell was not started in "
"QApplication mode.";
qCritical() << "To use platform menus, add `//@ pragma UseQApplication` to the top of your "
"root QML file and restart quickshell.";
return false;
} else if (!anchor->backingWindow() || !anchor->backingWindow()->isVisible()) {
qCritical() << "Cannot display PlatformMenuEntry on anchor without visible window.";
return false;
}
if (ACTIVE_MENU && this->qmenu != ACTIVE_MENU) {
ACTIVE_MENU->close();
}
ACTIVE_MENU = this->qmenu;
this->qmenu->createWinId();
this->qmenu->windowHandle()->setTransientParent(anchor->backingWindow());
// Update the window geometry to the menu's actual dimensions so reposition
// can accurately adjust it if applicable for the current platform.
this->qmenu->windowHandle()->setGeometry({{0, 0}, this->qmenu->sizeHint()});
PopupPositioner::instance()->reposition(anchor, this->qmenu->windowHandle(), false);
// Open the menu at the position determined by the popup positioner.
this->qmenu->popup(this->qmenu->windowHandle()->position());
return true;
}
void PlatformMenuEntry::relayout() {
if (qobject_cast<QApplication*>(QCoreApplication::instance()) == nullptr) {
return;
}
if (this->menu->hasChildren()) {
delete this->qaction;
this->qaction = nullptr;
if (this->qmenu == nullptr) {
this->qmenu = new PlatformMenuQMenu();
QObject::connect(this->qmenu, &QMenu::aboutToShow, this, &PlatformMenuEntry::onAboutToShow);
QObject::connect(this->qmenu, &QMenu::aboutToHide, this, &PlatformMenuEntry::onAboutToHide);
} else {
this->clearChildren();
}
this->qmenu->setTitle(this->menu->text());
auto icon = this->menu->icon();
if (!icon.isEmpty()) {
this->qmenu->setIcon(getCurrentEngineImageAsIcon(icon));
}
const auto& children = this->menu->children()->valueList();
auto len = children.count();
for (auto i = 0; i < len; i++) {
auto* child = children.at(i);
auto* instance = new PlatformMenuEntry(child);
QObject::connect(instance, &QObject::destroyed, this, &PlatformMenuEntry::onChildDestroyed);
QObject::connect(
instance,
&PlatformMenuEntry::relayoutParent,
this,
&PlatformMenuEntry::relayout
);
this->childEntries.push_back(instance);
instance->addToQMenu(this->qmenu);
}
} else if (!this->menu->isSeparator()) {
this->clearChildren();
delete this->qmenu;
this->qmenu = nullptr;
if (this->qaction == nullptr) {
this->qaction = new QAction(this);
QObject::connect(
this->qaction,
&QAction::triggered,
this,
&PlatformMenuEntry::onActionTriggered
);
}
this->qaction->setText(this->menu->text());
auto icon = this->menu->icon();
if (!icon.isEmpty()) {
this->qaction->setIcon(getCurrentEngineImageAsIcon(icon));
}
this->qaction->setEnabled(this->menu->enabled());
this->qaction->setCheckable(this->menu->buttonType() != QsMenuButtonType::None);
if (this->menu->buttonType() == QsMenuButtonType::RadioButton) {
if (!this->qactiongroup) this->qactiongroup = new QActionGroup(this);
this->qaction->setActionGroup(this->qactiongroup);
}
this->qaction->setChecked(this->menu->checkState() != Qt::Unchecked);
} else {
delete this->qmenu;
delete this->qaction;
this->qmenu = nullptr;
this->qaction = nullptr;
}
}
void PlatformMenuEntry::onAboutToShow() { this->menu->ref(); }
void PlatformMenuEntry::onAboutToHide() {
this->menu->unref();
emit this->closed();
}
void PlatformMenuEntry::onActionTriggered() {
auto* action = qobject_cast<PlatformMenuEntry*>(this->sender()->parent());
emit action->menu->triggered();
}
void PlatformMenuEntry::onChildDestroyed() { this->childEntries.removeOne(this->sender()); }
void PlatformMenuEntry::onEnabledChanged() {
if (this->qaction != nullptr) {
this->qaction->setEnabled(this->menu->enabled());
}
}
void PlatformMenuEntry::onTextChanged() {
if (this->qmenu != nullptr) {
this->qmenu->setTitle(this->menu->text());
} else if (this->qaction != nullptr) {
this->qaction->setText(this->menu->text());
}
}
void PlatformMenuEntry::onIconChanged() {
if (this->qmenu == nullptr && this->qaction == nullptr) return;
auto iconName = this->menu->icon();
QIcon icon;
if (!iconName.isEmpty()) {
icon = getCurrentEngineImageAsIcon(iconName);
}
if (this->qmenu != nullptr) {
this->qmenu->setIcon(icon);
} else if (this->qaction != nullptr) {
this->qaction->setIcon(icon);
}
}
void PlatformMenuEntry::onButtonTypeChanged() {
if (this->qaction != nullptr) {
QActionGroup* group = nullptr;
if (this->menu->buttonType() == QsMenuButtonType::RadioButton) {
if (!this->qactiongroup) this->qactiongroup = new QActionGroup(this);
group = this->qactiongroup;
}
this->qaction->setActionGroup(group);
}
}
void PlatformMenuEntry::onCheckStateChanged() {
if (this->qaction != nullptr) {
this->qaction->setChecked(this->menu->checkState() != Qt::Unchecked);
}
}
void PlatformMenuEntry::clearChildren() {
for (auto* child: this->childEntries) {
delete child;
}
this->childEntries.clear();
}
void PlatformMenuEntry::addToQMenu(PlatformMenuQMenu* menu) {
if (this->qmenu != nullptr) {
menu->addMenu(this->qmenu);
this->qmenu->containingMenu = menu;
} else if (this->qaction != nullptr) {
menu->addAction(this->qaction);
} else {
menu->addSeparator();
}
}
} // namespace qs::menu::platform

63
src/core/platformmenu.hpp Normal file
View file

@ -0,0 +1,63 @@
#pragma once
#include <functional>
#include <qaction.h>
#include <qactiongroup.h>
#include <qcontainerfwd.h>
#include <qobject.h>
#include <qqmlintegration.h>
#include <qqmllist.h>
#include <qtclasshelpermacros.h>
#include <qtmetamacros.h>
#include "../core/popupanchor.hpp"
#include "qsmenu.hpp"
namespace qs::menu::platform {
class PlatformMenuQMenu;
class PlatformMenuEntry: public QObject {
Q_OBJECT;
public:
explicit PlatformMenuEntry(QsMenuEntry* menu);
~PlatformMenuEntry() override;
Q_DISABLE_COPY_MOVE(PlatformMenuEntry);
bool display(QObject* parentWindow, int relativeX, int relativeY);
bool display(PopupAnchor* anchor);
static void registerCreationHook(std::function<void(PlatformMenuQMenu*)> hook);
signals:
void closed();
void relayoutParent();
public slots:
void relayout();
private slots:
void onAboutToShow();
void onAboutToHide();
void onActionTriggered();
void onChildDestroyed();
void onEnabledChanged();
void onTextChanged();
void onIconChanged();
void onButtonTypeChanged();
void onCheckStateChanged();
private:
void clearChildren();
void addToQMenu(PlatformMenuQMenu* menu);
QsMenuEntry* menu;
PlatformMenuQMenu* qmenu = nullptr;
QAction* qaction = nullptr;
QActionGroup* qactiongroup = nullptr;
QVector<PlatformMenuEntry*> childEntries;
};
} // namespace qs::menu::platform

View file

@ -0,0 +1,19 @@
#pragma once
#include <qmenu.h>
#include <qpoint.h>
namespace qs::menu::platform {
class PlatformMenuQMenu: public QMenu {
public:
explicit PlatformMenuQMenu() = default;
~PlatformMenuQMenu() override;
Q_DISABLE_COPY_MOVE(PlatformMenuQMenu);
void setVisible(bool visible) override;
PlatformMenuQMenu* containingMenu = nullptr;
QPoint targetPosition;
};
} // namespace qs::menu::platform

View file

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

View file

@ -2,23 +2,30 @@
#include <qcontainerfwd.h>
#include <qfunctionpointer.h>
#include <qlist.h>
class QuickshellPlugin {
class EngineGeneration;
class QsEnginePlugin {
public:
QuickshellPlugin() = default;
virtual ~QuickshellPlugin() = default;
QuickshellPlugin(QuickshellPlugin&&) = delete;
QuickshellPlugin(const QuickshellPlugin&) = delete;
void operator=(QuickshellPlugin&&) = delete;
void operator=(const QuickshellPlugin&) = delete;
QsEnginePlugin() = default;
virtual ~QsEnginePlugin() = default;
QsEnginePlugin(QsEnginePlugin&&) = delete;
QsEnginePlugin(const QsEnginePlugin&) = delete;
void operator=(QsEnginePlugin&&) = delete;
void operator=(const QsEnginePlugin&) = delete;
virtual QString name() { return QString(); }
virtual QList<QString> dependencies() { return {}; }
virtual bool applies() { return true; }
virtual void init() {}
virtual void registerTypes() {}
virtual void constructGeneration(EngineGeneration& /*unused*/) {} // NOLINT
virtual void onReload() {}
static void registerPlugin(QuickshellPlugin& plugin);
static void registerPlugin(QsEnginePlugin& plugin);
static void initPlugins();
static void runConstructGeneration(EngineGeneration& generation);
static void runOnReload();
};
@ -26,6 +33,6 @@ public:
#define QS_REGISTER_PLUGIN(clazz) \
[[gnu::constructor]] void qsInitPlugin() { \
static clazz plugin; \
QuickshellPlugin::registerPlugin(plugin); \
QsEnginePlugin::registerPlugin(plugin); \
}
// NOLINTEND

386
src/core/popupanchor.cpp Normal file
View file

@ -0,0 +1,386 @@
#include "popupanchor.hpp"
#include <algorithm>
#include <qcontainerfwd.h>
#include <qlogging.h>
#include <qobject.h>
#include <qquickitem.h>
#include <qsize.h>
#include <qtmetamacros.h>
#include <qvectornd.h>
#include <qwindow.h>
#include "../window/proxywindow.hpp"
#include "../window/windowinterface.hpp"
#include "types.hpp"
bool PopupAnchorState::operator==(const PopupAnchorState& other) const {
return this->rect == other.rect && this->edges == other.edges && this->gravity == other.gravity
&& this->adjustment == other.adjustment && this->anchorpoint == other.anchorpoint
&& this->size == other.size;
}
bool PopupAnchor::isDirty() const {
return !this->lastState.has_value() || this->state != this->lastState.value();
}
void PopupAnchor::markClean() { this->lastState = this->state; }
void PopupAnchor::markDirty() { this->lastState.reset(); }
QWindow* PopupAnchor::backingWindow() const {
return this->mProxyWindow ? this->mProxyWindow->backingWindow() : nullptr;
}
void PopupAnchor::setWindowInternal(QObject* window) {
if (window == this->mWindow) return;
if (this->mWindow) {
QObject::disconnect(this->mWindow, nullptr, this, nullptr);
QObject::disconnect(this->mProxyWindow, nullptr, this, nullptr);
}
if (window) {
if (auto* proxy = qobject_cast<ProxyWindowBase*>(window)) {
this->mProxyWindow = proxy;
} else if (auto* interface = qobject_cast<WindowInterface*>(window)) {
this->mProxyWindow = interface->proxyWindow();
} else {
qWarning() << "Tried to set popup anchor window to" << window
<< "which is not a quickshell window.";
goto setnull;
}
this->mWindow = window;
QObject::connect(this->mWindow, &QObject::destroyed, this, &PopupAnchor::onWindowDestroyed);
QObject::connect(
this->mProxyWindow,
&ProxyWindowBase::backerVisibilityChanged,
this,
&PopupAnchor::backingWindowVisibilityChanged
);
emit this->windowChanged();
emit this->backingWindowVisibilityChanged();
return;
}
setnull:
if (this->mWindow) {
this->mWindow = nullptr;
this->mProxyWindow = nullptr;
emit this->windowChanged();
emit this->backingWindowVisibilityChanged();
}
}
void PopupAnchor::setWindow(QObject* window) {
this->setItem(nullptr);
this->setWindowInternal(window);
}
void PopupAnchor::setItem(QQuickItem* item) {
if (item == this->mItem) return;
if (this->mItem) {
QObject::disconnect(this->mItem, nullptr, this, nullptr);
}
this->mItem = item;
this->onItemWindowChanged();
if (item) {
QObject::connect(item, &QObject::destroyed, this, &PopupAnchor::onItemDestroyed);
QObject::connect(item, &QQuickItem::windowChanged, this, &PopupAnchor::onItemWindowChanged);
}
}
void PopupAnchor::onWindowDestroyed() {
this->mWindow = nullptr;
this->mProxyWindow = nullptr;
emit this->windowChanged();
emit this->backingWindowVisibilityChanged();
}
void PopupAnchor::onItemDestroyed() {
this->mItem = nullptr;
emit this->itemChanged();
this->setWindowInternal(nullptr);
}
void PopupAnchor::onItemWindowChanged() {
if (auto* window = qobject_cast<ProxiedWindow*>(this->mItem->window())) {
this->setWindowInternal(window->proxy());
} else {
this->setWindowInternal(nullptr);
}
}
void PopupAnchor::setRect(Box rect) {
if (rect.w <= 0) rect.w = 1;
if (rect.h <= 0) rect.h = 1;
if (rect == this->mUserRect) return;
this->mUserRect = rect;
emit this->rectChanged();
this->setWindowRect(rect.qrect().marginsRemoved(this->mMargins.qmargins()));
}
void PopupAnchor::setMargins(Margins margins) {
if (margins == this->mMargins) return;
this->mMargins = margins;
emit this->marginsChanged();
this->setWindowRect(this->mUserRect.qrect().marginsRemoved(margins.qmargins()));
}
void PopupAnchor::setWindowRect(QRect rect) {
if (rect.width() <= 0) rect.setWidth(1);
if (rect.height() <= 0) rect.setHeight(1);
if (rect == this->state.rect) return;
this->state.rect = rect;
emit this->windowRectChanged();
}
void PopupAnchor::resetRect() { this->mUserRect = Box(); }
void PopupAnchor::setEdges(Edges::Flags edges) {
if (edges == this->state.edges) return;
if (Edges::isOpposing(edges)) {
qWarning() << "Cannot set opposing edges for anchor edges. Tried to set" << edges;
return;
}
this->state.edges = edges;
emit this->edgesChanged();
}
void PopupAnchor::setGravity(Edges::Flags gravity) {
if (gravity == this->state.gravity) return;
if (Edges::isOpposing(gravity)) {
qWarning() << "Cannot set opposing edges for anchor gravity. Tried to set" << gravity;
return;
}
this->state.gravity = gravity;
emit this->gravityChanged();
}
void PopupAnchor::setAdjustment(PopupAdjustment::Flags adjustment) {
if (adjustment == this->state.adjustment) return;
this->state.adjustment = adjustment;
emit this->adjustmentChanged();
}
void PopupAnchor::updatePlacement(const QPoint& anchorpoint, const QSize& size) {
this->state.anchorpoint = anchorpoint;
this->state.size = size;
}
void PopupAnchor::updateAnchor() {
if (this->mItem && this->mProxyWindow) {
auto baseRect =
this->mUserRect.isEmpty() ? this->mItem->boundingRect() : this->mUserRect.qrect();
auto rect = this->mProxyWindow->contentItem()->mapFromItem(
this->mItem,
baseRect.marginsRemoved(this->mMargins.qmargins())
);
if (rect.width() < 1) rect.setWidth(1);
if (rect.height() < 1) rect.setHeight(1);
this->setWindowRect(rect.toRect());
}
emit this->anchoring();
}
static PopupPositioner* POSITIONER = nullptr; // NOLINT
void PopupPositioner::reposition(PopupAnchor* anchor, QWindow* window, bool onlyIfDirty) {
auto* parentWindow = window->transientParent();
if (!parentWindow) {
qFatal() << "Cannot reposition popup that does not have a transient parent.";
}
auto parentGeometry = parentWindow->geometry();
auto windowGeometry = window->geometry();
anchor->updateAnchor();
anchor->updatePlacement(parentGeometry.topLeft(), windowGeometry.size());
if (onlyIfDirty && !anchor->isDirty()) return;
anchor->markClean();
auto adjustment = anchor->adjustment();
auto screenGeometry = parentWindow->screen()->geometry();
auto anchorRectGeometry = anchor->windowRect().translated(parentGeometry.topLeft());
auto anchorEdges = anchor->edges();
auto anchorGravity = anchor->gravity();
auto width = windowGeometry.width();
auto height = windowGeometry.height();
auto anchorX = anchorEdges.testFlag(Edges::Left) ? anchorRectGeometry.left()
: anchorEdges.testFlag(Edges::Right) ? anchorRectGeometry.right()
: anchorRectGeometry.center().x();
auto anchorY = anchorEdges.testFlag(Edges::Top) ? anchorRectGeometry.top()
: anchorEdges.testFlag(Edges::Bottom) ? anchorRectGeometry.bottom()
: anchorRectGeometry.center().y();
auto calcEffectiveX = [&](Edges::Flags anchorGravity, int anchorX) {
auto ex = anchorGravity.testFlag(Edges::Left) ? anchorX - windowGeometry.width()
: anchorGravity.testFlag(Edges::Right) ? anchorX - 1
: anchorX - windowGeometry.width() / 2;
return ex + 1;
};
auto calcEffectiveY = [&](Edges::Flags anchorGravity, int anchorY) {
auto ey = anchorGravity.testFlag(Edges::Top) ? anchorY - windowGeometry.height()
: anchorGravity.testFlag(Edges::Bottom) ? anchorY - 1
: anchorY - windowGeometry.height() / 2;
return ey + 1;
};
auto calcRemainingWidth = [&](int effectiveX) {
auto width = windowGeometry.width();
if (effectiveX < screenGeometry.left()) {
auto diff = screenGeometry.left() - effectiveX;
effectiveX = screenGeometry.left();
width -= diff;
}
auto effectiveX2 = effectiveX + width;
if (effectiveX2 > screenGeometry.right()) {
width -= effectiveX2 - screenGeometry.right() - 1;
}
return QPair<int, int>(effectiveX, width);
};
auto calcRemainingHeight = [&](int effectiveY) {
auto height = windowGeometry.height();
if (effectiveY < screenGeometry.left()) {
auto diff = screenGeometry.top() - effectiveY;
effectiveY = screenGeometry.top();
height -= diff;
}
auto effectiveY2 = effectiveY + height;
if (effectiveY2 > screenGeometry.bottom()) {
height -= effectiveY2 - screenGeometry.bottom() - 1;
}
return QPair<int, int>(effectiveY, height);
};
auto effectiveX = calcEffectiveX(anchorGravity, anchorX);
auto effectiveY = calcEffectiveY(anchorGravity, anchorY);
if (adjustment.testFlag(PopupAdjustment::FlipX)) {
const bool flip = (anchorGravity.testFlag(Edges::Left) && effectiveX < screenGeometry.left())
|| (anchorGravity.testFlag(Edges::Right)
&& effectiveX + windowGeometry.width() > screenGeometry.right());
if (flip) {
auto newAnchorGravity = anchorGravity ^ (Edges::Left | Edges::Right);
auto newAnchorX = anchorEdges.testFlags(Edges::Left) ? anchorRectGeometry.right()
: anchorEdges.testFlags(Edges::Right) ? anchorRectGeometry.left()
: anchorX;
auto newEffectiveX = calcEffectiveX(newAnchorGravity, newAnchorX);
// TODO IN HL: pick constraint monitor based on anchor rect position in window
// if the available width when flipped is more than the available width without flipping then flip
if (calcRemainingWidth(newEffectiveX).second > calcRemainingWidth(effectiveX).second) {
anchorGravity = newAnchorGravity;
anchorX = newAnchorX;
effectiveX = newEffectiveX;
}
}
}
if (adjustment.testFlag(PopupAdjustment::FlipY)) {
const bool flip = (anchorGravity.testFlag(Edges::Top) && effectiveY < screenGeometry.top())
|| (anchorGravity.testFlag(Edges::Bottom)
&& effectiveY + windowGeometry.height() > screenGeometry.bottom());
if (flip) {
auto newAnchorGravity = anchorGravity ^ (Edges::Top | Edges::Bottom);
auto newAnchorY = anchorEdges.testFlags(Edges::Top) ? anchorRectGeometry.bottom()
: anchorEdges.testFlags(Edges::Bottom) ? anchorRectGeometry.top()
: anchorY;
auto newEffectiveY = calcEffectiveY(newAnchorGravity, newAnchorY);
// if the available width when flipped is more than the available width without flipping then flip
if (calcRemainingHeight(newEffectiveY).second > calcRemainingHeight(effectiveY).second) {
anchorGravity = newAnchorGravity;
anchorY = newAnchorY;
effectiveY = newEffectiveY;
}
}
}
// Slide order is important for the case where the window is too large to fit on screen.
if (adjustment.testFlag(PopupAdjustment::SlideX)) {
if (effectiveX + windowGeometry.width() > screenGeometry.right()) {
effectiveX = screenGeometry.right() - windowGeometry.width() + 1;
}
effectiveX = std::max(effectiveX, screenGeometry.left());
}
if (adjustment.testFlag(PopupAdjustment::SlideY)) {
if (effectiveY + windowGeometry.height() > screenGeometry.bottom()) {
effectiveY = screenGeometry.bottom() - windowGeometry.height() + 1;
}
effectiveY = std::max(effectiveY, screenGeometry.top());
}
if (adjustment.testFlag(PopupAdjustment::ResizeX)) {
auto [newX, newWidth] = calcRemainingWidth(effectiveX);
effectiveX = newX;
width = newWidth;
}
if (adjustment.testFlag(PopupAdjustment::ResizeY)) {
auto [newY, newHeight] = calcRemainingHeight(effectiveY);
effectiveY = newY;
height = newHeight;
}
window->setGeometry({effectiveX, effectiveY, width, height});
}
bool PopupPositioner::shouldRepositionOnMove() const { return true; }
PopupPositioner* PopupPositioner::instance() {
if (POSITIONER == nullptr) {
POSITIONER = new PopupPositioner();
}
return POSITIONER;
}
void PopupPositioner::setInstance(PopupPositioner* instance) {
delete POSITIONER;
POSITIONER = instance;
}

214
src/core/popupanchor.hpp Normal file
View file

@ -0,0 +1,214 @@
#pragma once
#include <optional>
#include <qflags.h>
#include <qnamespace.h>
#include <qobject.h>
#include <qpoint.h>
#include <qqmlintegration.h>
#include <qquickitem.h>
#include <qsize.h>
#include <qtclasshelpermacros.h>
#include <qtmetamacros.h>
#include <qtypes.h>
#include <qvariant.h>
#include <qvectornd.h>
#include <qwindow.h>
#include "../window/proxywindow.hpp"
#include "doc.hpp"
#include "types.hpp"
///! Adjustment strategy for popups that do not fit on screen.
/// Adjustment strategy for popups. See @@PopupAnchor.adjustment.
///
/// Adjustment flags can be combined with the `|` operator.
///
/// `Flip` will be applied first, then `Slide`, then `Resize`.
namespace PopupAdjustment { // NOLINT
Q_NAMESPACE;
QML_ELEMENT;
enum Enum : quint8 {
None = 0,
/// If the X axis is constrained, the popup will slide along the X axis until it fits onscreen.
SlideX = 1,
/// If the Y axis is constrained, the popup will slide along the Y axis until it fits onscreen.
SlideY = 2,
/// Alias for `SlideX | SlideY`.
Slide = SlideX | SlideY,
/// If the X axis is constrained, the popup will invert its horizontal gravity if any.
FlipX = 4,
/// If the Y axis is constrained, the popup will invert its vertical gravity if any.
FlipY = 8,
/// Alias for `FlipX | FlipY`.
Flip = FlipX | FlipY,
/// If the X axis is constrained, the width of the popup will be reduced to fit on screen.
ResizeX = 16,
/// If the Y axis is constrained, the height of the popup will be reduced to fit on screen.
ResizeY = 32,
/// Alias for `ResizeX | ResizeY`
Resize = ResizeX | ResizeY,
/// Alias for `Flip | Slide | Resize`.
All = Slide | Flip | Resize,
};
Q_ENUM_NS(Enum);
Q_DECLARE_FLAGS(Flags, Enum);
} // namespace PopupAdjustment
Q_DECLARE_OPERATORS_FOR_FLAGS(PopupAdjustment::Flags);
struct PopupAnchorState {
bool operator==(const PopupAnchorState& other) const;
QRect rect = {0, 0, 1, 1};
Edges::Flags edges = Edges::Top | Edges::Left;
Edges::Flags gravity = Edges::Bottom | Edges::Right;
PopupAdjustment::Flags adjustment = PopupAdjustment::Slide;
QPoint anchorpoint;
QSize size;
};
///! Anchorpoint or positioner for popup windows.
class PopupAnchor: public QObject {
Q_OBJECT;
// clang-format off
/// The window to anchor / attach the popup to. Setting this property unsets @@item.
Q_PROPERTY(QObject* window READ window WRITE setWindow NOTIFY windowChanged);
/// The item to anchor / attach the popup to. Setting this property unsets @@window.
///
/// The popup's position relative to its parent window is only calculated when it is
/// initially shown (directly before @@anchoring(s) is emitted), meaning its anchor
/// rectangle will be set relative to the item's position in the window at that time.
/// @@updateAnchor() can be called to update the anchor rectangle if the item's position
/// has changed.
///
/// > [!NOTE] If a more flexible way to position a popup relative to an item is needed,
/// > set @@window to the item's parent window, and handle the @@anchoring signal to
/// > position the popup relative to the window's contentItem.
Q_PROPERTY(QQuickItem* item READ item WRITE setItem NOTIFY itemChanged);
/// The anchorpoints the popup will attach to, relative to @@item or @@window.
/// Which anchors will be used is determined by the @@edges, @@gravity, and @@adjustment.
///
/// If using @@item, the default anchor rectangle matches the dimensions of the item.
///
/// If you leave @@edges, @@gravity and @@adjustment at their default values,
/// setting more than `x` and `y` does not matter. The anchor rect cannot
/// be smaller than 1x1 pixels.
///
/// [coordinate mapping functions]: https://doc.qt.io/qt-6/qml-qtquick-item.html#mapFromItem-method
Q_PROPERTY(Box rect READ rect WRITE setRect RESET resetRect NOTIFY rectChanged);
/// A margin applied to the anchor rect.
///
/// This is most useful when @@item is used and @@rect is left at its default
/// value (matching the Item's dimensions).
Q_PROPERTY(Margins margins READ margins WRITE setMargins NOTIFY marginsChanged);
/// The point on the anchor rectangle the popup should anchor to.
/// Opposing edges suchs as `Edges.Left | Edges.Right` are not allowed.
///
/// Defaults to `Edges.Top | Edges.Left`.
Q_PROPERTY(Edges::Flags edges READ edges WRITE setEdges NOTIFY edgesChanged);
/// The direction the popup should expand towards, relative to the anchorpoint.
/// Opposing edges suchs as `Edges.Left | Edges.Right` are not allowed.
///
/// Defaults to `Edges.Bottom | Edges.Right`.
Q_PROPERTY(Edges::Flags gravity READ gravity WRITE setGravity NOTIFY gravityChanged);
/// The strategy used to adjust the popup's position if it would otherwise not fit on screen,
/// based on the anchor @@rect, preferred @@edges, and @@gravity.
///
/// See the documentation for @@PopupAdjustment for details.
Q_PROPERTY(PopupAdjustment::Flags adjustment READ adjustment WRITE setAdjustment NOTIFY adjustmentChanged);
// clang-format on
QML_ELEMENT;
QML_UNCREATABLE("");
public:
explicit PopupAnchor(QObject* parent): QObject(parent) {}
/// Update the popup's anchor rect relative to its parent window.
///
/// If anchored to an item, popups anchors will not automatically follow
/// the item if its position changes. This function can be called to
/// recalculate the anchors.
Q_INVOKABLE void updateAnchor();
[[nodiscard]] bool isDirty() const;
void markClean();
void markDirty();
[[nodiscard]] QObject* window() const { return this->mWindow; }
[[nodiscard]] ProxyWindowBase* proxyWindow() const { return this->mProxyWindow; }
[[nodiscard]] QWindow* backingWindow() const;
void setWindowInternal(QObject* window);
void setWindow(QObject* window);
[[nodiscard]] QQuickItem* item() const { return this->mItem; }
void setItem(QQuickItem* item);
[[nodiscard]] QRect windowRect() const { return this->state.rect; }
void setWindowRect(QRect rect);
[[nodiscard]] Box rect() const { return this->mUserRect; }
void setRect(Box rect);
void resetRect();
[[nodiscard]] Margins margins() const { return this->mMargins; }
void setMargins(Margins margins);
[[nodiscard]] Edges::Flags edges() const { return this->state.edges; }
void setEdges(Edges::Flags edges);
[[nodiscard]] Edges::Flags gravity() const { return this->state.gravity; }
void setGravity(Edges::Flags gravity);
[[nodiscard]] PopupAdjustment::Flags adjustment() const { return this->state.adjustment; }
void setAdjustment(PopupAdjustment::Flags adjustment);
void updatePlacement(const QPoint& anchorpoint, const QSize& size);
signals:
/// Emitted when this anchor is about to be used. Mostly useful for modifying
/// the anchor @@rect using [coordinate mapping functions], which are not reactive.
///
/// [coordinate mapping functions]: https://doc.qt.io/qt-6/qml-qtquick-item.html#mapFromItem-method
void anchoring();
void windowChanged();
void itemChanged();
QSDOC_HIDE void backingWindowVisibilityChanged();
QSDOC_HIDE void windowRectChanged();
void rectChanged();
void marginsChanged();
void edgesChanged();
void gravityChanged();
void adjustmentChanged();
private slots:
void onWindowDestroyed();
void onItemDestroyed();
void onItemWindowChanged();
private:
QObject* mWindow = nullptr;
QQuickItem* mItem = nullptr;
ProxyWindowBase* mProxyWindow = nullptr;
PopupAnchorState state;
Box mUserRect;
Margins mMargins;
std::optional<PopupAnchorState> lastState;
};
class PopupPositioner {
public:
explicit PopupPositioner() = default;
virtual ~PopupPositioner() = default;
Q_DISABLE_COPY_MOVE(PopupPositioner);
virtual void reposition(PopupAnchor* anchor, QWindow* window, bool onlyIfDirty = true);
[[nodiscard]] virtual bool shouldRepositionOnMove() const;
static PopupPositioner* instance();
static void setInstance(PopupPositioner* instance);
};

View file

@ -1,223 +0,0 @@
#include "proxywindow.hpp"
#include <qobject.h>
#include <qqmlengine.h>
#include <qqmllist.h>
#include <qquickitem.h>
#include <qquickwindow.h>
#include <qregion.h>
#include <qtmetamacros.h>
#include <qtypes.h>
#include <qwindow.h>
#include "qmlscreen.hpp"
#include "region.hpp"
#include "reload.hpp"
ProxyWindowBase::ProxyWindowBase(QObject* parent)
: Reloadable(parent)
, mContentItem(new QQuickItem()) {
QQmlEngine::setObjectOwnership(this->mContentItem, QQmlEngine::CppOwnership);
this->mContentItem->setParent(this);
QObject::connect(this, &ProxyWindowBase::widthChanged, this, &ProxyWindowBase::onWidthChanged);
QObject::connect(this, &ProxyWindowBase::heightChanged, this, &ProxyWindowBase::onHeightChanged);
}
ProxyWindowBase::~ProxyWindowBase() {
if (this->window != nullptr) {
this->window->deleteLater();
}
}
void ProxyWindowBase::onReload(QObject* oldInstance) {
this->window = this->createWindow(oldInstance);
this->setupWindow();
Reloadable::reloadRecursive(this->mContentItem, oldInstance);
this->mContentItem->setParentItem(this->window->contentItem());
this->mContentItem->setWidth(this->width());
this->mContentItem->setHeight(this->height());
// without this the dangling screen pointer wont be updated to a real screen
emit this->screenChanged();
emit this->windowConnected();
this->window->setVisible(this->mVisible);
}
QQuickWindow* ProxyWindowBase::createWindow(QObject* oldInstance) {
auto* old = qobject_cast<ProxyWindowBase*>(oldInstance);
if (old == nullptr || old->window == nullptr) {
return new QQuickWindow();
} else {
return old->disownWindow();
}
}
void ProxyWindowBase::setupWindow() {
// clang-format off
QObject::connect(this->window, &QWindow::visibilityChanged, this, &ProxyWindowBase::visibleChanged);
QObject::connect(this->window, &QWindow::widthChanged, this, &ProxyWindowBase::widthChanged);
QObject::connect(this->window, &QWindow::heightChanged, this, &ProxyWindowBase::heightChanged);
QObject::connect(this->window, &QWindow::screenChanged, this, &ProxyWindowBase::screenChanged);
QObject::connect(this->window, &QQuickWindow::colorChanged, this, &ProxyWindowBase::colorChanged);
QObject::connect(this, &ProxyWindowBase::maskChanged, this, &ProxyWindowBase::onMaskChanged);
QObject::connect(this, &ProxyWindowBase::widthChanged, this, &ProxyWindowBase::onMaskChanged);
QObject::connect(this, &ProxyWindowBase::heightChanged, this, &ProxyWindowBase::onMaskChanged);
// clang-format on
this->window->setScreen(this->mScreen);
this->setWidth(this->mWidth);
this->setHeight(this->mHeight);
this->setColor(this->mColor);
this->updateMask();
}
QQuickWindow* ProxyWindowBase::disownWindow() {
QObject::disconnect(this->window, nullptr, this, nullptr);
this->mContentItem->setParentItem(nullptr);
auto* window = this->window;
this->window = nullptr;
return window;
}
QQuickWindow* ProxyWindowBase::backingWindow() const { return this->window; }
QQuickItem* ProxyWindowBase::contentItem() const { return this->mContentItem; }
bool ProxyWindowBase::isVisible() const {
if (this->window == nullptr) return this->mVisible;
else return this->window->isVisible();
}
void ProxyWindowBase::setVisible(bool visible) {
if (this->window == nullptr) {
this->mVisible = visible;
emit this->visibleChanged();
} else this->window->setVisible(visible);
}
qint32 ProxyWindowBase::width() const {
if (this->window == nullptr) return this->mWidth;
else return this->window->width();
}
void ProxyWindowBase::setWidth(qint32 width) {
if (this->window == nullptr) {
this->mWidth = width;
emit this->widthChanged();
} else this->window->setWidth(width);
}
qint32 ProxyWindowBase::height() const {
if (this->window == nullptr) return this->mHeight;
else return this->window->height();
}
void ProxyWindowBase::setHeight(qint32 height) {
if (this->window == nullptr) {
this->mHeight = height;
emit this->heightChanged();
} else this->window->setHeight(height);
}
void ProxyWindowBase::setScreen(QuickshellScreenInfo* screen) {
if (this->mScreen != nullptr) {
QObject::disconnect(this->mScreen, nullptr, this, nullptr);
}
auto* qscreen = screen == nullptr ? nullptr : screen->screen;
if (qscreen != nullptr) {
QObject::connect(qscreen, &QObject::destroyed, this, &ProxyWindowBase::onScreenDestroyed);
}
if (this->window == nullptr) {
this->mScreen = qscreen;
emit this->screenChanged();
} else this->window->setScreen(qscreen);
}
void ProxyWindowBase::onScreenDestroyed() { this->mScreen = nullptr; }
QuickshellScreenInfo* ProxyWindowBase::screen() const {
QScreen* qscreen = nullptr;
if (this->window == nullptr) {
if (this->mScreen != nullptr) qscreen = this->mScreen;
} else {
qscreen = this->window->screen();
}
return new QuickshellScreenInfo(
const_cast<ProxyWindowBase*>(this), // NOLINT
qscreen
);
}
QColor ProxyWindowBase::color() const {
if (this->window == nullptr) return this->mColor;
else return this->window->color();
}
void ProxyWindowBase::setColor(QColor color) {
if (this->window == nullptr) {
this->mColor = color;
emit this->colorChanged();
} else this->window->setColor(color);
}
PendingRegion* ProxyWindowBase::mask() const { return this->mMask; }
void ProxyWindowBase::setMask(PendingRegion* mask) {
if (mask == this->mMask) return;
if (this->mMask != nullptr) {
QObject::disconnect(this->mMask, nullptr, this, nullptr);
}
this->mMask = mask;
if (mask != nullptr) {
mask->setParent(this);
QObject::connect(mask, &QObject::destroyed, this, &ProxyWindowBase::onMaskDestroyed);
QObject::connect(mask, &PendingRegion::changed, this, &ProxyWindowBase::maskChanged);
}
emit this->maskChanged();
}
void ProxyWindowBase::onMaskChanged() {
if (this->window != nullptr) this->updateMask();
}
void ProxyWindowBase::onMaskDestroyed() {
this->mMask = nullptr;
emit this->maskChanged();
}
void ProxyWindowBase::updateMask() {
QRegion mask;
if (this->mMask != nullptr) {
// if left as the default, dont combine it with the whole window area, leave it as is.
if (this->mMask->mIntersection == Intersection::Combine) {
mask = this->mMask->build();
} else {
auto windowRegion = QRegion(QRect(0, 0, this->width(), this->height()));
mask = this->mMask->applyTo(windowRegion);
}
}
this->window->setMask(mask);
}
QQmlListProperty<QObject> ProxyWindowBase::data() {
return this->mContentItem->property("data").value<QQmlListProperty<QObject>>();
}
void ProxyWindowBase::onWidthChanged() { this->mContentItem->setWidth(this->width()); }
void ProxyWindowBase::onHeightChanged() { this->mContentItem->setHeight(this->height()); }

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