7 Commits
Author SHA1 Message Date
AramJonghu 4023a2fb01 fix(strokecanvasitem): use qreal instead of float to suppress -Wdouble-promotion
Python / lint-format (pull_request) Successful in 26s
Lint & Format (JS/TS) / lint-format (pull_request) Successful in 38s
Python / test (pull_request) Successful in 52s
Lint & Format (Rust) / lint-format (pull_request) Successful in 1m38s
C++ / build (pull_request) Successful in 2m26s
2026-06-30 01:18:07 +02:00
AramJonghu 3eecbc16bc perf(lazylistview): collect pending indices and hoist layoutSize cast in updatePolishdbus workaround for session 2026-06-30 00:23:23 +02:00
AramJonghu b939224e9d action secret added so we have push permissions for docker
Lint & Format (JS/TS) / lint-format (pull_request) Successful in 25s
Python / lint-format (pull_request) Successful in 28s
Python / test (pull_request) Successful in 46s
Lint & Format (Rust) / lint-format (pull_request) Successful in 1m53s
C++ / build (pull_request) Successful in 2m31s
2026-06-29 23:01:54 +02:00
AramJonghu 644a1e0c78 workflow update; node and docker deps added
Lint & Format (JS/TS) / lint-format (pull_request) Successful in 12s
Python / lint-format (pull_request) Successful in 20s
Python / test (pull_request) Successful in 52s
Lint & Format (Rust) / lint-format (pull_request) Successful in 1m40s
C++ / build (pull_request) Successful in 2m43s
2026-06-29 22:40:28 +02:00
AramJonghuandzach bc9d8af0fe update to clang-tidy + workflow improvement cpp build & lint check (#130)
### Goal

Idea is to reduce the annoyances and research which clang-tidy rules make sense and actually have benefits in our project.

Also considering immediately resolving most if not all tidy warnings.

### Current Changes

- `.clang-tidy` has changes to follow the guidelines we have been following silently. Some rules were different from how we program.
    - Note that naming is completely removed and won't be checked. In a future PR (where we go over the code to fix linting issues), we can reintroduce them and make sure they fit our codebase consistently.
- tidy rules are not considered errors anymore (stopped compile).
- CI/CD addition to build check and clang-tidy check. Tried to get clang-tidy check to show when failed, but that causes complications in its checks; so now it is simply silent when failing.
- Dockerfile that creates an arch container as cache for the CI/CD, prevents redownload of dependencies each run. Instead, it runs weekly (or optionally a different interval).

### Current complications

- To push the docker container build, it would require this webserver to allow larger filesize pushes (need ~2GB). Current workaround is arguably better where docker build is pushed to runner server.
- Figuring out which clang-tidy rules make sense.

Reviewed-on: #130
Co-authored-by: AramJonghu <aramjonghu@tutamail.com>
Co-committed-by: AramJonghu <aramjonghu@tutamail.com>
2026-06-29 22:10:26 +02:00
zach 7d65578a5f proper VERSION var 2026-06-29 19:37:11 +02:00
zach f883b3d7fe versioning 2026-06-29 18:57:32 +02:00
10 changed files with 504 additions and 300 deletions
+10 -19
View File
@@ -2,29 +2,20 @@
Checks: > Checks: >
-*, -*,
bugprone-*, bugprone-*,
-bugprone-easily-swappable-parameters,
-bugprone-narrowing-conversions,
-bugprone-implicit-widening-of-multiplication-result,
clang-analyzer-*, clang-analyzer-*,
modernize-*, cppcoreguidelines-init-variables,
-modernize-use-trailing-return-type, misc-no-recursion,
performance-*, concurrency-mt-unsafe,
readability-braces-around-statements,
readability-else-after-return, readability-else-after-return,
readability-identifier-naming, readability-make-member-function-const,
readability-redundant-*, readability-redundant-*,
readability-simplify-*, readability-simplify-*,
CheckOptions: modernize-use-override,
readability-identifier-naming.ClassCase: CamelCase modernize-use-nullptr,
readability-identifier-naming.EnumCase: CamelCase modernize-deprecated-headers,
readability-identifier-naming.FunctionCase: camelBack
readability-identifier-naming.MemberCase: camelBack
readability-identifier-naming.MemberPrefix: m_
readability-identifier-naming.MethodCase: camelBack
readability-identifier-naming.NamespaceCase: CamelCase
readability-identifier-naming.ParameterCase: camelBack
readability-identifier-naming.PrivateMemberPrefix: m_
readability-identifier-naming.StaticConstantCase: UPPER_CASE
readability-identifier-naming.StaticConstantPrefix: k
readability-identifier-naming.VariableCase: camelBack
WarningsAsErrors: "*"
HeaderFilterRegex: ".*" HeaderFilterRegex: ".*"
FormatStyle: file FormatStyle: file
+30
View File
@@ -0,0 +1,30 @@
name: Rebuild CI Image
on:
schedule:
- cron: '0 6 * * 1'
workflow_dispatch:
jobs:
build:
runs-on: alpine
container:
image: node:26-alpine
env:
IMAGE: git.aramjonghu.nl/aramjonghu/zshell-ci:latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install Docker CLI
run: apk add --no-cache docker-cli
- name: Login to registry
run: echo "${{ secrets.REGISTRY_TOKEN }}" | docker login git.aramjonghu.nl --username aramjonghu --password-stdin
- name: Build image
run: docker build -t "$IMAGE" -f ci/Dockerfile .
- name: Push image
run: docker push "$IMAGE"
+29
View File
@@ -0,0 +1,29 @@
name: C++
on:
pull_request:
jobs:
build:
runs-on: alpine
container:
image: git.aramjonghu.nl/aramjonghu/zshell-ci:latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Configure
run: cmake -B build -G Ninja -DENABLE_MODULES=plugin -DCMAKE_BUILD_TYPE=Release
- name: Build
run: ninja -C build
- name: clang-tidy
continue-on-error: true
run: |
set -o pipefail
run-clang-tidy -p build -j $(nproc) -header-filter="Plugins/.*" 2>&1 | \
grep -v 'Suppressed\|non-user\|non-system\|unknown argument.*mno-direct'
+37 -8
View File
@@ -1,6 +1,35 @@
cmake_minimum_required(VERSION 3.19) cmake_minimum_required(VERSION 3.19)
project(ZShell) if(NOT DEFINED VERSION)
execute_process(
COMMAND git describe --tags --abbrev=0
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
OUTPUT_VARIABLE GIT_LAST_TAG
OUTPUT_STRIP_TRAILING_WHITESPACE
ERROR_QUIET
)
if (GIT_LAST_TAG)
string(REGEX REPLACE "^v" "" GIT_LAST_TAG "${GIT_LAST_TAG}")
string(REPLACE "." ";" VERSION_LIST "${GIT_LAST_TAG}")
list(GET VERSION_LIST 0 VERSION_MAJOR)
list(GET VERSION_LIST 1 VERSION_MINOR)
execute_process(
COMMAND git rev-list v${VERSION_MAJOR}.${VERSION_MINOR}.0..HEAD --count
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
OUTPUT_VARIABLE VERSION_PATCH
OUTPUT_STRIP_TRAILING_WHITESPACE
ERROR_QUIET
)
endif()
endif()
set(VERSION "${VERSION_MAJOR}.${VERSION_MINOR}.${VERSION_PATCH}")
project(ZShell VERSION ${VERSION_MAJOR}.${VERSION_MINOR}.${VERSION_PATCH})
message(STATUS "ZShell version: ${VERSION}")
set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_STANDARD_REQUIRED ON)
@@ -15,11 +44,11 @@ set(INSTALL_QSCONFDIR "etc/xdg/quickshell/zshell" CACHE STRING "Quickshell confi
set(INSTALL_GREETERCONFDIR "etc/xdg/quickshell/zshell-greeter" CACHE STRING "Quickshell greeter install dir") set(INSTALL_GREETERCONFDIR "etc/xdg/quickshell/zshell-greeter" CACHE STRING "Quickshell greeter install dir")
add_compile_options( add_compile_options(
-Wall -Wextra -Wpedantic -Wshadow -Wconversion -Wall -Wextra -Wpedantic -Wshadow -Wconversion
-Wold-style-cast -Wnull-dereference -Wdouble-promotion -Wold-style-cast -Wnull-dereference -Wdouble-promotion
-Wformat=2 -Wfloat-equal -Woverloaded-virtual -Wformat=2 -Wfloat-equal -Woverloaded-virtual
-Wsign-conversion -Wredundant-decls -Wswitch -Wsign-conversion -Wredundant-decls -Wswitch
-Wunreachable-code -Wunreachable-code
) )
@@ -30,8 +59,8 @@ endif()
if("shell" IN_LIST ENABLE_MODULES) if("shell" IN_LIST ENABLE_MODULES)
foreach(dir assets scripts Components Config Modules Daemons Drawers Effects Helpers Paths) foreach(dir assets scripts Components Config Modules Daemons Drawers Effects Helpers Paths)
install(DIRECTORY ${dir} DESTINATION "${INSTALL_QSCONFDIR}") install(DIRECTORY ${dir} DESTINATION "${INSTALL_QSCONFDIR}")
endforeach() endforeach()
# Disable watching for changes # Disable watching for changes
file(READ shell.qml SHELL_QML) file(READ shell.qml SHELL_QML)
+32 -30
View File
@@ -9,7 +9,7 @@ pkg_check_modules(Cava IMPORTED_TARGET libcava QUIET)
pkg_check_modules(GLIB REQUIRED glib-2.0 gobject-2.0 gio-2.0) pkg_check_modules(GLIB REQUIRED glib-2.0 gobject-2.0 gio-2.0)
if(NOT Cava_FOUND) if(NOT Cava_FOUND)
pkg_check_modules(Cava IMPORTED_TARGET cava REQUIRED) pkg_check_modules(Cava IMPORTED_TARGET cava REQUIRED)
endif() endif()
if(NOT TARGET Sensors::Sensors) if(NOT TARGET Sensors::Sensors)
@@ -24,48 +24,50 @@ set(QT_QML_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/qml")
qt_standard_project_setup(REQUIRES 6.9) qt_standard_project_setup(REQUIRES 6.9)
function(qml_module arg_TARGET) function(qml_module arg_TARGET)
cmake_parse_arguments(PARSE_ARGV 1 arg "" "URI" "SOURCES;LIBRARIES") cmake_parse_arguments(PARSE_ARGV 1 arg "" "URI" "SOURCES;LIBRARIES")
qt_add_qml_module(${arg_TARGET} qt_add_qml_module(${arg_TARGET}
URI ${arg_URI} URI ${arg_URI}
VERSION 1.0 VERSION 1.0
SOURCES ${arg_SOURCES} SOURCES ${arg_SOURCES}
) )
qt_query_qml_module(${arg_TARGET} qt_query_qml_module(${arg_TARGET}
URI module_uri URI module_uri
VERSION module_version VERSION module_version
PLUGIN_TARGET module_plugin_target PLUGIN_TARGET module_plugin_target
TARGET_PATH module_target_path TARGET_PATH module_target_path
QMLDIR module_qmldir QMLDIR module_qmldir
TYPEINFO module_typeinfo TYPEINFO module_typeinfo
) )
set(module_dir "${INSTALL_QMLDIR}/${module_target_path}") set(module_dir "${INSTALL_QMLDIR}/${module_target_path}")
install(TARGETS ${arg_TARGET} LIBRARY DESTINATION "${module_dir}" RUNTIME DESTINATION "${module_dir}") 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}") install(TARGETS "${module_plugin_target}" LIBRARY DESTINATION "${module_dir}" RUNTIME DESTINATION "${module_dir}")
install(FILES "${module_qmldir}" DESTINATION "${module_dir}") install(FILES "${module_qmldir}" DESTINATION "${module_dir}")
install(FILES "${module_typeinfo}" DESTINATION "${module_dir}") install(FILES "${module_typeinfo}" DESTINATION "${module_dir}")
target_link_libraries(${arg_TARGET} PRIVATE Qt::Core Qt::Qml ${arg_LIBRARIES}) target_link_libraries(${arg_TARGET} PRIVATE Qt::Core Qt::Qml ${arg_LIBRARIES})
endfunction() endfunction()
qml_module(ZShell qml_module(ZShell
URI ZShell URI ZShell
SOURCES SOURCES
writefile.hpp writefile.cpp writefile.hpp writefile.cpp
appdb.hpp appdb.cpp appdb.hpp appdb.cpp
imageanalyser.hpp imageanalyser.cpp imageanalyser.hpp imageanalyser.cpp
requests.hpp requests.cpp requests.hpp requests.cpp
toaster.hpp toaster.cpp toaster.hpp toaster.cpp
qalculator.hpp qalculator.cpp qalculator.hpp qalculator.cpp
zutils.hpp zutils.cpp zutils.hpp zutils.cpp
LIBRARIES LIBRARIES
Qt::Gui Qt::Gui
Qt::Quick Qt::Quick
Qt::Concurrent Qt::Concurrent
Qt::Sql Qt::Sql
Qt::DBus Qt::DBus
PkgConfig::Qalculate PkgConfig::Qalculate
) )
target_compile_definitions(ZShell PRIVATE ZSHELL_VERSION="${VERSION}")
add_subdirectory(Models) add_subdirectory(Models)
add_subdirectory(Internal) add_subdirectory(Internal)
add_subdirectory(Services) add_subdirectory(Services)
+268 -198
View File
@@ -1,6 +1,7 @@
#include "lazylistview.hpp" #include "lazylistview.hpp"
#include <algorithm> #include <algorithm>
#include <cmath>
#include <qqmlcontext.h> #include <qqmlcontext.h>
#include <qtimer.h> #include <qtimer.h>
@@ -13,11 +14,7 @@ constexpr int ASYNC_BATCH_DESTROY = 4;
namespace ZShell::components { namespace ZShell::components {
// --- LazyListViewAttached --- LazyListViewAttached::LazyListViewAttached(QObject* parent) : QObject(parent) {}
LazyListViewAttached::LazyListViewAttached(QObject* parent)
: QObject(parent) {
}
qreal LazyListViewAttached::preferredHeight() const { qreal LazyListViewAttached::preferredHeight() const {
return m_preferredHeight; return m_preferredHeight;
@@ -85,10 +82,7 @@ void LazyListViewAttached::setTrackViewport(bool track) {
emit trackViewportChanged(); emit trackViewportChanged();
} }
// --- LazyListView --- LazyListView::LazyListView(QQuickItem* parent) : QQuickItem(parent) {
LazyListView::LazyListView(QQuickItem* parent)
: QQuickItem(parent) {
setFlag(ItemHasContents, false); setFlag(ItemHasContents, false);
} }
@@ -103,8 +97,6 @@ LazyListView::~LazyListView() {
destroyDelegate(entry); destroyDelegate(entry);
} }
// --- Model & Delegate ---
QAbstractItemModel* LazyListView::model() const { QAbstractItemModel* LazyListView::model() const {
return m_model; return m_model;
} }
@@ -138,8 +130,6 @@ void LazyListView::setDelegate(QQmlComponent* delegate) {
emit delegateChanged(); emit delegateChanged();
} }
// --- Layout ---
qreal LazyListView::spacing() const { qreal LazyListView::spacing() const {
return m_spacing; return m_spacing;
} }
@@ -172,8 +162,6 @@ void LazyListView::setContentY(qreal contentY) {
polish(); polish();
} }
// --- Viewport ---
QRectF LazyListView::viewport() const { QRectF LazyListView::viewport() const {
return m_viewport; return m_viewport;
} }
@@ -211,8 +199,6 @@ void LazyListView::setCacheBuffer(qreal buffer) {
polish(); polish();
} }
// --- Sizing ---
qreal LazyListView::estimatedHeight() const { qreal LazyListView::estimatedHeight() const {
return m_estimatedHeight; return m_estimatedHeight;
} }
@@ -258,7 +244,8 @@ qreal LazyListView::delegateHeight(QQuickItem* item) {
if (!item) if (!item)
return 0; return 0;
auto* attached = qobject_cast<LazyListViewAttached*>(qmlAttachedPropertiesObject<LazyListView>(item, false)); auto* attached = qobject_cast<LazyListViewAttached*>(
qmlAttachedPropertiesObject<LazyListView>(item, false));
if (attached && attached->preferredHeight() >= 0) if (attached && attached->preferredHeight() >= 0)
return attached->preferredHeight(); return attached->preferredHeight();
@@ -269,7 +256,8 @@ qreal LazyListView::delegateVisibleHeight(QQuickItem* item) {
if (!item) if (!item)
return 0; return 0;
auto* attached = qobject_cast<LazyListViewAttached*>(qmlAttachedPropertiesObject<LazyListView>(item, false)); auto* attached = qobject_cast<LazyListViewAttached*>(
qmlAttachedPropertiesObject<LazyListView>(item, false));
if (attached) { if (attached) {
if (attached->visibleHeight() >= 0) if (attached->visibleHeight() >= 0)
return attached->visibleHeight(); return attached->visibleHeight();
@@ -283,12 +271,11 @@ qreal LazyListView::delegateVisibleHeight(QQuickItem* item) {
bool LazyListView::isDelegateReady(QQuickItem* item) { bool LazyListView::isDelegateReady(QQuickItem* item) {
if (!item) if (!item)
return false; return false;
auto* att = qobject_cast<LazyListViewAttached*>(qmlAttachedPropertiesObject<LazyListView>(item, false)); auto* att = qobject_cast<LazyListViewAttached*>(
qmlAttachedPropertiesObject<LazyListView>(item, false));
return !att || att->ready(); return !att || att->ready();
} }
// --- Animation Durations ---
int LazyListView::removeDuration() const { int LazyListView::removeDuration() const {
return m_removeDuration; return m_removeDuration;
} }
@@ -311,21 +298,18 @@ void LazyListView::setReadyDelay(int delay) {
emit readyDelayChanged(); emit readyDelayChanged();
} }
// --- State ---
int LazyListView::count() const { int LazyListView::count() const {
return m_model ? m_model->rowCount() : 0; return m_model ? m_model->rowCount() : 0;
} }
// --- QQuickItem Overrides ---
void LazyListView::componentComplete() { void LazyListView::componentComplete() {
QQuickItem::componentComplete(); QQuickItem::componentComplete();
m_componentComplete = true; m_componentComplete = true;
resetContent(); resetContent();
} }
void LazyListView::geometryChange(const QRectF& newGeometry, const QRectF& oldGeometry) { void LazyListView::geometryChange(const QRectF& newGeometry,
const QRectF& oldGeometry) {
QQuickItem::geometryChange(newGeometry, oldGeometry); QQuickItem::geometryChange(newGeometry, oldGeometry);
if (!m_componentComplete) if (!m_componentComplete)
@@ -348,71 +332,84 @@ void LazyListView::updatePolish() {
// Flush pending inserts — make items visible and clear the adding flag // Flush pending inserts — make items visible and clear the adding flag
// so enter animations begin. When readyDelay > 0 the entire insert is // so enter animations begin. When readyDelay > 0 the entire insert is
// deferred so delegates have time to lay out before appearing. // deferred so delegates have time to lay out before appearing.
for (auto& entry : m_delegates) {
if (!entry.pendingInsert || !entry.item) // Collect pending indices first — avoids scanning the entire hash each frame
continue; QList<int> pendingIndices;
for (auto it = m_delegates.constBegin(); it != m_delegates.constEnd(); ++it) {
if (it->pendingInsert && it->item)
pendingIndices.append(it.key());
}
for (int pendingIdx : pendingIndices) {
auto& entry = m_delegates[pendingIdx];
if (m_readyDelay > 0) { if (m_readyDelay > 0) {
if (!entry.readyDelayStarted) { if (!entry.readyDelayStarted) {
entry.readyDelayStarted = true; entry.readyDelayStarted = true;
auto* item = entry.item; auto* item = entry.item;
QTimer::singleShot(m_readyDelay, this, [this, item] { QTimer::singleShot(m_readyDelay, this, [this, item] {
auto indexIt = m_itemToIndex.find(item); auto indexIt = m_itemToIndex.find(item);
if (indexIt == m_itemToIndex.end()) if (indexIt == m_itemToIndex.end())
return; return;
const int idx = indexIt.value(); const int idx = indexIt.value();
auto it = m_delegates.find(idx); auto it = m_delegates.find(idx);
if (it == m_delegates.end() || it->item != item || !it->pendingInsert) if (it == m_delegates.end() || it->item != item ||
return; !it->pendingInsert)
return;
it->pendingInsert = false; it->pendingInsert = false;
it->readyDelayStarted = false; it->readyDelayStarted = false;
// Set initial y to visual position (based on current visible heights) // Set initial y to visual position (based on current visible heights)
if (idx >= 0 && idx < static_cast<int>(m_layout.size())) { if (idx >= 0 && idx < static_cast<int>(m_layout.size())) {
qreal visualY = 0; qreal visualY = 0;
bool hasVisItem = false; bool hasVisItem = false;
for (int i = 0; i < static_cast<int>(m_layout.size()); ++i) { for (int i = 0; i < static_cast<int>(m_layout.size());
qreal h; ++i) {
auto dit = m_delegates.find(i); qreal h = NAN;
if (dit != m_delegates.end() && dit->item) auto dit = m_delegates.find(i);
h = delegateVisibleHeight(dit->item); if (dit != m_delegates.end() && dit->item)
else h = delegateVisibleHeight(dit->item);
h = m_layout[i].heightKnown ? m_layout[i].height : effectiveEstimatedHeight(); else
if (h > 0) { h = m_layout[i].heightKnown
if (hasVisItem) ? m_layout[i].height
visualY += m_spacing; : effectiveEstimatedHeight();
hasVisItem = true; if (h > 0) {
} if (hasVisItem)
if (i == idx) visualY += m_spacing;
break; hasVisItem = true;
if (h > 0)
visualY += h;
} }
item->setY(visualY - m_contentY); if (i == idx)
break;
if (h > 0)
visualY += h;
} }
item->setY(visualY - m_contentY);
}
item->setVisible(true); item->setVisible(true);
auto* att = auto* att = qobject_cast<LazyListViewAttached*>(
qobject_cast<LazyListViewAttached*>(qmlAttachedPropertiesObject<LazyListView>(item, false)); qmlAttachedPropertiesObject<LazyListView>(item, false));
if (att) { if (att) {
att->setAdding(false); att->setAdding(false);
att->setReady(true); att->setReady(true);
} }
// Animate from visual position to layout position // Animate from visual position to layout position
if (idx >= 0 && idx < static_cast<int>(m_layout.size())) if (idx >= 0 && idx < static_cast<int>(m_layout.size()))
item->setProperty("y", m_layout[idx].targetY - m_contentY); item->setProperty("y",
m_layout[idx].targetY - m_contentY);
polish(); polish();
}); });
} }
continue; continue;
} }
entry.pendingInsert = false; entry.pendingInsert = false;
entry.item->setVisible(true); entry.item->setVisible(true);
auto* att = qobject_cast<LazyListViewAttached*>(qmlAttachedPropertiesObject<LazyListView>(entry.item, false)); auto* att = qobject_cast<LazyListViewAttached*>(
qmlAttachedPropertiesObject<LazyListView>(entry.item, false));
if (att) { if (att) {
att->setAdding(false); att->setAdding(false);
att->setReady(true); att->setReady(true);
@@ -429,12 +426,13 @@ void LazyListView::updatePolish() {
record.isNew = false; record.isNew = false;
// Position delegates — QML Behavior on y handles the animation // Position delegates — QML Behavior on y handles the animation
const int layoutSize = static_cast<int>(m_layout.size());
for (auto& entry : m_delegates) { for (auto& entry : m_delegates) {
if (!entry.item || entry.pendingRemoval || entry.pendingInsert) if (!entry.item || entry.pendingRemoval || entry.pendingInsert)
continue; continue;
const int idx = entry.modelIndex; const int idx = entry.modelIndex;
if (idx < 0 || idx >= static_cast<int>(m_layout.size())) if (idx < 0 || idx >= layoutSize)
continue; continue;
if (m_layout[idx].heightKnown && qFuzzyIsNull(m_layout[idx].height)) if (m_layout[idx].heightKnown && qFuzzyIsNull(m_layout[idx].height))
@@ -454,7 +452,8 @@ void LazyListView::relayout() {
qreal y = 0; qreal y = 0;
bool hasLayoutItem = false; bool hasLayoutItem = false;
for (auto& record : m_layout) { for (auto& record : m_layout) {
const qreal layoutH = record.heightKnown ? record.height : effectiveEstimatedHeight(); const qreal layoutH = record.heightKnown ? record.height
: effectiveEstimatedHeight();
if (layoutH > 0) { if (layoutH > 0) {
if (hasLayoutItem) if (hasLayoutItem)
y += m_spacing; y += m_spacing;
@@ -476,12 +475,13 @@ void LazyListView::relayout() {
qreal visY = 0; qreal visY = 0;
bool hasVisItem = false; bool hasVisItem = false;
for (int i = 0; i < static_cast<int>(m_layout.size()); ++i) { for (int i = 0; i < static_cast<int>(m_layout.size()); ++i) {
qreal h; qreal h = NAN;
auto dit = m_delegates.find(i); auto dit = m_delegates.find(i);
if (dit != m_delegates.end() && dit->item) if (dit != m_delegates.end() && dit->item)
h = delegateVisibleHeight(dit->item); h = delegateVisibleHeight(dit->item);
else else
h = m_layout[i].heightKnown ? m_layout[i].height : effectiveEstimatedHeight(); h = m_layout[i].heightKnown ? m_layout[i].height
: effectiveEstimatedHeight();
if (h > 0) { if (h > 0) {
if (hasVisItem) if (hasVisItem)
visY += m_spacing; visY += m_spacing;
@@ -542,11 +542,11 @@ QRectF LazyListView::effectiveViewport() const {
std::pair<int, int> LazyListView::computeVisibleRange() const { std::pair<int, int> LazyListView::computeVisibleRange() const {
if (m_layout.isEmpty()) if (m_layout.isEmpty())
return { -1, -1 }; return {-1, -1};
const auto vp = effectiveViewport(); const auto vp = effectiveViewport();
if (vp.isEmpty()) if (vp.isEmpty())
return { -1, -1 }; return {-1, -1};
const qreal vpTop = vp.y(); const qreal vpTop = vp.y();
const qreal vpBottom = vp.y() + vp.height(); const qreal vpBottom = vp.y() + vp.height();
@@ -559,7 +559,9 @@ std::pair<int, int> LazyListView::computeVisibleRange() const {
while (lo <= hi) { while (lo <= hi) {
const int mid = lo + (hi - lo) / 2; const int mid = lo + (hi - lo) / 2;
const auto& record = m_layout[mid]; const auto& record = m_layout[mid];
const qreal itemBottom = record.targetY + (record.heightKnown ? record.height : effectiveEstimatedHeight()); const qreal itemBottom =
record.targetY +
(record.heightKnown ? record.height : effectiveEstimatedHeight());
if (itemBottom >= vpTop) { if (itemBottom >= vpTop) {
first = mid; first = mid;
@@ -570,7 +572,7 @@ std::pair<int, int> LazyListView::computeVisibleRange() const {
} }
if (first >= static_cast<int>(m_layout.size())) if (first >= static_cast<int>(m_layout.size()))
return { -1, -1 }; return {-1, -1};
// Linear scan for last visible item // Linear scan for last visible item
int last = first; int last = first;
@@ -580,7 +582,7 @@ std::pair<int, int> LazyListView::computeVisibleRange() const {
last = i; last = i;
} }
return { first, last }; return {first, last};
} }
// --- Delegate Lifecycle --- // --- Delegate Lifecycle ---
@@ -612,9 +614,12 @@ void LazyListView::syncDelegates() {
} }
// Batch destroy // Batch destroy
const int destroyBudget = m_asynchronous ? ASYNC_BATCH_DESTROY : static_cast<int>(toRemove.size()); const int destroyBudget = m_asynchronous
? ASYNC_BATCH_DESTROY
: static_cast<int>(toRemove.size());
QVector<DelegateEntry> removedEntries; QVector<DelegateEntry> removedEntries;
removedEntries.reserve(std::min(destroyBudget, static_cast<int>(toRemove.size()))); removedEntries.reserve(
std::min(destroyBudget, static_cast<int>(toRemove.size())));
int destroyed = 0; int destroyed = 0;
for (int idx : toRemove) { for (int idx : toRemove) {
if (destroyed >= destroyBudget) if (destroyed >= destroyBudget)
@@ -638,7 +643,8 @@ void LazyListView::syncDelegates() {
} }
// Batch create // Batch create
const int createBudget = m_asynchronous ? ASYNC_BATCH_CREATE : static_cast<int>(toCreate.size()); const int createBudget = m_asynchronous ? ASYNC_BATCH_CREATE
: static_cast<int>(toCreate.size());
int created = 0; int created = 0;
for (int i : toCreate) { for (int i : toCreate) {
if (created >= createBudget) if (created >= createBudget)
@@ -658,8 +664,9 @@ void LazyListView::syncDelegates() {
// Pending inserts need to become visible on the next frame, and // Pending inserts need to become visible on the next frame, and
// async mode may have remaining create/destroy work. // async mode may have remaining create/destroy work.
if (created > 0 || (m_asynchronous && (destroyed < static_cast<int>(toRemove.size()) || if (created > 0 ||
created < static_cast<int>(toCreate.size())))) (m_asynchronous && (destroyed < static_cast<int>(toRemove.size()) ||
created < static_cast<int>(toCreate.size()))))
polish(); polish();
} }
@@ -704,8 +711,10 @@ LazyListView::DelegateEntry LazyListView::createDelegate(int modelIndex) {
initialProps.insert(QStringLiteral("index"), modelIndex); initialProps.insert(QStringLiteral("index"), modelIndex);
if (!hasModelData) { if (!hasModelData) {
const auto role = roleNames.isEmpty() ? Qt::DisplayRole : roleNames.constBegin().key(); const auto role = roleNames.isEmpty() ? Qt::DisplayRole
initialProps.insert(QStringLiteral("modelData"), m_model->data(index, role)); : roleNames.constBegin().key();
initialProps.insert(QStringLiteral("modelData"),
m_model->data(index, role));
} }
m_delegate->setInitialProperties(entry.item, initialProps); m_delegate->setInitialProperties(entry.item, initialProps);
@@ -715,9 +724,10 @@ LazyListView::DelegateEntry LazyListView::createDelegate(int modelIndex) {
// Only set adding = true for genuinely new model items (not viewport entries). // Only set adding = true for genuinely new model items (not viewport entries).
// Cleared on the next frame in updatePolish when the item becomes visible. // Cleared on the next frame in updatePolish when the item becomes visible.
if (modelIndex < static_cast<int>(m_layout.size()) && m_layout[modelIndex].isNew) { if (modelIndex < static_cast<int>(m_layout.size()) &&
auto* addingAttached = m_layout[modelIndex].isNew) {
qobject_cast<LazyListViewAttached*>(qmlAttachedPropertiesObject<LazyListView>(entry.item, true)); auto* addingAttached = qobject_cast<LazyListViewAttached*>(
qmlAttachedPropertiesObject<LazyListView>(entry.item, true));
if (addingAttached) if (addingAttached)
addingAttached->setAdding(true); addingAttached->setAdding(true);
} }
@@ -730,84 +740,103 @@ LazyListView::DelegateEntry LazyListView::createDelegate(int modelIndex) {
// Height-change handler — uses m_itemToIndex for O(1) lookup. // Height-change handler — uses m_itemToIndex for O(1) lookup.
// Ignored while the delegate is not yet ready. // Ignored while the delegate is not yet ready.
auto onHeightChanged = [this, item = entry.item] { auto onHeightChanged = [this, item = entry.item] {
if (!isDelegateReady(item)) if (!isDelegateReady(item))
return; return;
auto indexIt = m_itemToIndex.find(item); auto indexIt = m_itemToIndex.find(item);
if (indexIt == m_itemToIndex.end()) if (indexIt == m_itemToIndex.end())
return; return;
const int idx = indexIt.value(); const int idx = indexIt.value();
auto delegateIt = m_delegates.find(idx); auto delegateIt = m_delegates.find(idx);
if (delegateIt == m_delegates.end() || delegateIt->item != item) if (delegateIt == m_delegates.end() || delegateIt->item != item)
return; return;
const qreal h = delegateHeight(item); const qreal h = delegateHeight(item);
if (idx < static_cast<int>(m_layout.size()) && !qFuzzyCompare(m_layout[idx].height + 1.0, h + 1.0)) { if (idx < static_cast<int>(m_layout.size()) &&
const qreal oldH = m_layout[idx].height; !qFuzzyCompare(m_layout[idx].height + 1.0, h + 1.0)) {
const bool wasKnown = m_layout[idx].heightKnown; const qreal oldH = m_layout[idx].height;
m_layout[idx].height = h; const bool wasKnown = m_layout[idx].heightKnown;
m_layout[idx].heightKnown = true; m_layout[idx].height = h;
if (wasKnown) m_layout[idx].heightKnown = true;
untrackHeight(oldH); if (wasKnown)
trackHeight(h); untrackHeight(oldH);
trackHeight(h);
// If this tracked item is above the viewport, emit a // If this tracked item is above the viewport, emit a
// compensation delta so the consumer can adjust scroll. // compensation delta so the consumer can adjust scroll.
if (wasKnown) { if (wasKnown) {
auto* att = qobject_cast<LazyListViewAttached*>(qmlAttachedPropertiesObject<LazyListView>(item, false)); auto* att = qobject_cast<LazyListViewAttached*>(
if (att && att->trackViewport()) { qmlAttachedPropertiesObject<LazyListView>(item, false));
const qreal vpTop = m_useCustomViewport ? m_viewport.y() : m_contentY; if (att && att->trackViewport()) {
if (m_layout[idx].targetY < vpTop) const qreal vpTop = m_useCustomViewport ? m_viewport.y()
emit viewportAdjustNeeded(h - oldH); : m_contentY;
} if (m_layout[idx].targetY < vpTop)
} emit viewportAdjustNeeded(h - oldH);
}
}
if (!m_relayoutPending) { if (!m_relayoutPending) {
m_relayoutPending = true; m_relayoutPending = true;
QTimer::singleShot(0, this, [this] { QTimer::singleShot(0, this, [this] {
m_relayoutPending = false; m_relayoutPending = false;
relayout(); relayout();
polish(); polish();
}); });
} }
} }
}; };
// Watch implicitHeight as fallback // Watch implicitHeight as fallback
connect(entry.item, &QQuickItem::implicitHeightChanged, this, onHeightChanged); connect(entry.item,
&QQuickItem::implicitHeightChanged,
this,
onHeightChanged);
// Watch attached properties if the delegate uses them // Watch attached properties if the delegate uses them
auto* attached = qobject_cast<LazyListViewAttached*>(qmlAttachedPropertiesObject<LazyListView>(entry.item, false)); auto* attached = qobject_cast<LazyListViewAttached*>(
qmlAttachedPropertiesObject<LazyListView>(entry.item, false));
if (attached) { if (attached) {
connect(attached, &LazyListViewAttached::preferredHeightChanged, this, onHeightChanged); connect(attached,
connect(attached, &LazyListViewAttached::visibleHeightChanged, this, [this] { &LazyListViewAttached::preferredHeightChanged,
polish(); this,
}); onHeightChanged);
connect(attached, &LazyListViewAttached::readyChanged, this, [this, item = entry.item] { connect(attached,
auto indexIt = m_itemToIndex.find(item); &LazyListViewAttached::visibleHeightChanged,
if (indexIt == m_itemToIndex.end()) this,
return; [this] { polish(); });
const int idx = indexIt.value(); connect(attached,
if (idx >= static_cast<int>(m_layout.size())) &LazyListViewAttached::readyChanged,
return; this,
auto* att = qobject_cast<LazyListViewAttached*>(qmlAttachedPropertiesObject<LazyListView>(item, false)); [this, item = entry.item] {
if (!att || !att->ready()) auto indexIt = m_itemToIndex.find(item);
return; if (indexIt == m_itemToIndex.end())
return;
const int idx = indexIt.value();
if (idx >= static_cast<int>(m_layout.size()))
return;
auto* att = qobject_cast<LazyListViewAttached*>(
qmlAttachedPropertiesObject<LazyListView>(item, false));
if (!att || !att->ready())
return;
const qreal h = delegateHeight(item); const qreal h = delegateHeight(item);
const qreal oldLayoutH = m_layout[idx].heightKnown ? m_layout[idx].height : effectiveEstimatedHeight(); const qreal oldLayoutH = m_layout[idx].heightKnown
if (m_layout[idx].heightKnown) ? m_layout[idx].height
untrackHeight(m_layout[idx].height); : effectiveEstimatedHeight();
m_layout[idx].height = h; if (m_layout[idx].heightKnown)
m_layout[idx].heightKnown = true; untrackHeight(m_layout[idx].height);
trackHeight(h); m_layout[idx].height = h;
m_layout[idx].heightKnown = true;
trackHeight(h);
if (att->trackViewport() && !qFuzzyCompare(h + 1.0, oldLayoutH + 1.0)) { if (att->trackViewport() &&
const qreal vpTop = m_useCustomViewport ? m_viewport.y() : m_contentY; !qFuzzyCompare(h + 1.0, oldLayoutH + 1.0)) {
if (m_layout[idx].targetY < vpTop) const qreal vpTop = m_useCustomViewport ? m_viewport.y()
emit viewportAdjustNeeded(h - oldLayoutH); : m_contentY;
} if (m_layout[idx].targetY < vpTop)
emit viewportAdjustNeeded(h - oldLayoutH);
}
polish(); polish();
}); });
} }
return entry; return entry;
@@ -832,7 +861,8 @@ void LazyListView::updateDelegateData(DelegateEntry& entry) {
for (auto it = roleNames.constBegin(); it != roleNames.constEnd(); ++it) { for (auto it = roleNames.constBegin(); it != roleNames.constEnd(); ++it) {
const auto name = QString::fromUtf8(it.value()); const auto name = QString::fromUtf8(it.value());
entry.item->setProperty(name.toUtf8().constData(), m_model->data(index, it.key())); entry.item->setProperty(name.toUtf8().constData(),
m_model->data(index, it.key()));
if (name == QStringLiteral("modelData")) if (name == QStringLiteral("modelData"))
hasModelData = true; hasModelData = true;
} }
@@ -840,7 +870,8 @@ void LazyListView::updateDelegateData(DelegateEntry& entry) {
entry.item->setProperty("index", entry.modelIndex); entry.item->setProperty("index", entry.modelIndex);
if (!hasModelData) { if (!hasModelData) {
const auto role = roleNames.isEmpty() ? Qt::DisplayRole : roleNames.constBegin().key(); const auto role = roleNames.isEmpty() ? Qt::DisplayRole
: roleNames.constBegin().key();
entry.item->setProperty("modelData", m_model->data(index, role)); entry.item->setProperty("modelData", m_model->data(index, role));
} }
} }
@@ -852,24 +883,46 @@ void LazyListView::connectModel() {
return; return;
m_modelConnections = { m_modelConnections = {
connect(m_model, &QAbstractItemModel::rowsInserted, this, &LazyListView::onRowsInserted), connect(m_model,
connect(m_model, &QAbstractItemModel::rowsAboutToBeRemoved, this, &LazyListView::onRowsAboutToBeRemoved), &QAbstractItemModel::rowsInserted,
connect(m_model, &QAbstractItemModel::rowsRemoved, this, &LazyListView::onRowsRemoved), this,
connect(m_model, &QAbstractItemModel::rowsMoved, this, &LazyListView::onRowsMoved), &LazyListView::onRowsInserted),
connect(m_model, &QAbstractItemModel::dataChanged, this, &LazyListView::onDataChanged), connect(m_model,
connect(m_model, &QAbstractItemModel::modelReset, this, &LazyListView::onModelReset), &QAbstractItemModel::rowsAboutToBeRemoved,
connect(m_model, &QAbstractItemModel::layoutChanged, this, this,
[this] { &LazyListView::onRowsAboutToBeRemoved),
for (auto& entry : m_delegates) connect(m_model,
updateDelegateData(entry); &QAbstractItemModel::rowsRemoved,
polish(); this,
}), &LazyListView::onRowsRemoved),
connect(m_model, &QObject::destroyed, this, connect(m_model,
[this] { &QAbstractItemModel::rowsMoved,
m_model = nullptr; this,
resetContent(); &LazyListView::onRowsMoved),
emit modelChanged(); connect(m_model,
}), &QAbstractItemModel::dataChanged,
this,
&LazyListView::onDataChanged),
connect(m_model,
&QAbstractItemModel::modelReset,
this,
&LazyListView::onModelReset),
connect(m_model,
&QAbstractItemModel::layoutChanged,
this,
[this] {
for (auto& entry : m_delegates)
updateDelegateData(entry);
polish();
}),
connect(m_model,
&QObject::destroyed,
this,
[this] {
m_model = nullptr;
resetContent();
emit modelChanged();
}),
}; };
} }
@@ -909,13 +962,15 @@ void LazyListView::resetContent() {
polish(); polish();
} }
void LazyListView::onRowsInserted(const QModelIndex& parent, int first, int last) { void LazyListView::onRowsInserted(const QModelIndex& parent,
int first,
int last) {
if (parent.isValid()) if (parent.isValid())
return; return;
const int insertCount = last - first + 1; const int insertCount = last - first + 1;
// Insert new layout records // Insert new layout records
m_layout.insert(first, insertCount, ItemRecord{ 0, 0, false, true }); m_layout.insert(first, insertCount, ItemRecord{0, 0, false, true});
// Shift existing delegate indices // Shift existing delegate indices
QHash<int, DelegateEntry> shifted; QHash<int, DelegateEntry> shifted;
@@ -935,7 +990,9 @@ void LazyListView::onRowsInserted(const QModelIndex& parent, int first, int last
polish(); polish();
} }
void LazyListView::onRowsAboutToBeRemoved(const QModelIndex& parent, int first, int last) { void LazyListView::onRowsAboutToBeRemoved(const QModelIndex& parent,
int first,
int last) {
if (parent.isValid()) if (parent.isValid())
return; return;
@@ -955,22 +1012,24 @@ void LazyListView::onRowsAboutToBeRemoved(const QModelIndex& parent, int first,
} }
if (m_removeDuration > 0 && entry.item) { if (m_removeDuration > 0 && entry.item) {
auto* attached = auto* attached = qobject_cast<LazyListViewAttached*>(
qobject_cast<LazyListViewAttached*>(qmlAttachedPropertiesObject<LazyListView>(entry.item, false)); qmlAttachedPropertiesObject<LazyListView>(entry.item, false));
if (attached) if (attached)
attached->setRemoving(true); attached->setRemoving(true);
// Schedule destruction after the remove animation duration // Schedule destruction after the remove animation duration
auto* item = entry.item; auto* item = entry.item;
QTimer::singleShot(m_removeDuration, this, [this, item] { QTimer::singleShot(m_removeDuration, this, [this, item] {
for (auto it = m_dyingDelegates.begin(); it != m_dyingDelegates.end(); ++it) { for (auto it = m_dyingDelegates.begin();
if (it->item == item) { it != m_dyingDelegates.end();
destroyDelegate(*it); ++it) {
m_dyingDelegates.erase(it); if (it->item == item) {
return; destroyDelegate(*it);
} m_dyingDelegates.erase(it);
return;
} }
}); }
});
m_dyingDelegates.append(std::move(entry)); m_dyingDelegates.append(std::move(entry));
} else { } else {
destroyDelegate(entry); destroyDelegate(entry);
@@ -978,7 +1037,9 @@ void LazyListView::onRowsAboutToBeRemoved(const QModelIndex& parent, int first,
} }
} }
void LazyListView::onRowsRemoved(const QModelIndex& parent, int first, int last) { void LazyListView::onRowsRemoved(const QModelIndex& parent,
int first,
int last) {
if (parent.isValid()) if (parent.isValid())
return; return;
@@ -1011,7 +1072,11 @@ void LazyListView::onRowsRemoved(const QModelIndex& parent, int first, int last)
polish(); polish();
} }
void LazyListView::onRowsMoved(const QModelIndex& parent, int start, int end, const QModelIndex& destination, int row) { void LazyListView::onRowsMoved(const QModelIndex& parent,
int start,
int end,
const QModelIndex& destination,
int row) {
if (parent.isValid() || destination.isValid()) if (parent.isValid() || destination.isValid())
return; return;
@@ -1055,7 +1120,9 @@ void LazyListView::onRowsMoved(const QModelIndex& parent, int start, int end, co
polish(); polish();
} }
void LazyListView::onDataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight, const QList<int>& roles) { void LazyListView::onDataChanged(const QModelIndex& topLeft,
const QModelIndex& bottomRight,
const QList<int>& roles) {
Q_UNUSED(roles) Q_UNUSED(roles)
if (topLeft.parent().isValid()) if (topLeft.parent().isValid())
@@ -1079,15 +1146,18 @@ void LazyListView::onModelReset() {
// Check if the model data actually changed // Check if the model data actually changed
if (newRows == oldRows) { if (newRows == oldRows) {
const auto roleNames = m_model->roleNames(); const auto roleNames = m_model->roleNames();
const auto role = roleNames.isEmpty() ? Qt::DisplayRole : roleNames.constBegin().key(); const auto role = roleNames.isEmpty() ? Qt::DisplayRole
: roleNames.constBegin().key();
bool changed = false; bool changed = false;
for (auto it = m_delegates.constBegin(); it != m_delegates.constEnd(); ++it) { for (auto it = m_delegates.constBegin(); it != m_delegates.constEnd();
++it) {
if (!it->item || it.key() >= newRows) { if (!it->item || it.key() >= newRows) {
changed = true; changed = true;
break; break;
} }
const auto newData = m_model->data(m_model->index(it.key(), 0), role); const auto newData =
m_model->data(m_model->index(it.key(), 0), role);
const auto oldData = it->item->property("modelData"); const auto oldData = it->item->property("modelData");
if (newData != oldData) { if (newData != oldData) {
changed = true; changed = true;
+37 -45
View File
@@ -6,55 +6,52 @@
namespace ZShell::internal { namespace ZShell::internal {
StrokeCanvasItem::StrokeCanvasItem(QQuickItem *parent) : QCanvasPainterItem(parent) { StrokeCanvasItem::StrokeCanvasItem(QQuickItem* parent)
: QCanvasPainterItem(parent) {
setFillColor(Qt::transparent); setFillColor(Qt::transparent);
setAlphaBlending(true); setAlphaBlending(true);
} }
QCanvasPainterItemRenderer *StrokeCanvasItem::createItemRenderer() const { QCanvasPainterItemRenderer* StrokeCanvasItem::createItemRenderer() const {
return new StrokeCanvasRenderer; return new StrokeCanvasRenderer;
} }
static bool shouldAddPoint( static bool shouldAddPoint(
const QVector<QPointF> &points, const QVector<QPointF>& points, const QPointF& p, qreal minDistance) {
const QPointF &p, if (points.isEmpty()) return true;
qreal minDistance)
{
if (points.isEmpty())
return true;
const QPointF delta = p - points.last(); const QPointF delta = p - points.last();
return QPointF::dotProduct(delta, delta) return QPointF::dotProduct(delta, delta) >= minDistance * minDistance;
>= minDistance * minDistance;
} }
static QCanvasPath buildStrokePath(const QVector<QPointF> &points, float width) { static QCanvasPath buildStrokePath(const QVector<QPointF>& points, qreal width) {
QCanvasPath path; QCanvasPath path;
if (points.size() == 1) { if (points.size() == 1) {
path.circle(points[0], width * 0.5f); path.circle(points[0], width * 0.5);
return path; return path;
} }
auto catmullToBezier = []( auto catmullToBezier = [](const QPointF& p0,
const QPointF &p0, const QPointF &p1, const QPointF& p1,
const QPointF &p2, const QPointF &p3, const QPointF& p2,
float tension, const QPointF& p3,
QPointF &cp1, QPointF &cp2) qreal tension,
{ QPointF& cp1,
cp1 = p1 + (p2 - p0) * tension / 3.0f; QPointF& cp2) {
cp2 = p2 - (p3 - p1) * tension / 3.0f; cp1 = p1 + (p2 - p0) * tension / 3.0;
}; cp2 = p2 - (p3 - p1) * tension / 3.0;
};
const float tension = 0.5f; const qreal tension = 0.5;
path.moveTo(points[0]); path.moveTo(points[0]);
for (int i = 0; i < points.size() - 1; ++i) { for (int i = 0; i < points.size() - 1; ++i) {
const QPointF &p0 = points[qMax(i - 1, 0)]; const QPointF& p0 = points[qMax(i - 1, 0)];
const QPointF &p1 = points[i]; const QPointF& p1 = points[i];
const QPointF &p2 = points[i + 1]; const QPointF& p2 = points[i + 1];
const QPointF &p3 = points[qMin(i + 2, points.size() - 1)]; const QPointF& p3 = points[qMin(i + 2, points.size() - 1)];
QPointF cp1, cp2; QPointF cp1, cp2;
catmullToBezier(p0, p1, p2, p3, tension, cp1, cp2); catmullToBezier(p0, p1, p2, p3, tension, cp1, cp2);
@@ -64,9 +61,8 @@ static QCanvasPath buildStrokePath(const QVector<QPointF> &points, float width)
return path; return path;
} }
void StrokeCanvasItem::setPenColor(const QColor &color) { void StrokeCanvasItem::setPenColor(const QColor& color) {
if (m_penColor == color) if (m_penColor == color) return;
return;
m_penColor = color; m_penColor = color;
update(); update();
@@ -75,8 +71,7 @@ void StrokeCanvasItem::setPenColor(const QColor &color) {
} }
void StrokeCanvasItem::setHoverVisible(bool visible) { void StrokeCanvasItem::setHoverVisible(bool visible) {
if (m_hoverVisible == visible) if (m_hoverVisible == visible) return;
return;
m_hoverVisible = visible; m_hoverVisible = visible;
update(); update();
@@ -84,9 +79,8 @@ void StrokeCanvasItem::setHoverVisible(bool visible) {
emit hoverVisibleChanged(); emit hoverVisibleChanged();
} }
void StrokeCanvasItem::setHoverPoint(const QPointF &point) { void StrokeCanvasItem::setHoverPoint(const QPointF& point) {
if (m_hoverPoint == point) if (m_hoverPoint == point) return;
return;
m_hoverPoint = point; m_hoverPoint = point;
update(); update();
@@ -110,8 +104,7 @@ void StrokeCanvasItem::showHover(qreal x, qreal y) {
} }
void StrokeCanvasItem::hideHover() { void StrokeCanvasItem::hideHover() {
if (!m_hoverVisible) if (!m_hoverVisible) return;
return;
m_hoverVisible = false; m_hoverVisible = false;
update(); update();
@@ -120,8 +113,7 @@ void StrokeCanvasItem::hideHover() {
} }
void StrokeCanvasItem::setPenWidth(float width) { void StrokeCanvasItem::setPenWidth(float width) {
if (qFuzzyCompare(m_penWidth, width)) if (qFuzzyCompare(m_penWidth, width)) return;
return;
m_penWidth = width; m_penWidth = width;
update(); update();
@@ -143,8 +135,7 @@ void StrokeCanvasItem::beginStroke(qreal x, qreal y) {
void StrokeCanvasItem::appendPoint(qreal x, qreal y) { void StrokeCanvasItem::appendPoint(qreal x, qreal y) {
const QPointF incoming{x, y}; const QPointF incoming{x, y};
if (!shouldAddPoint(m_currentStroke.points, incoming, 2.0)) if (!shouldAddPoint(m_currentStroke.points, incoming, 2.0)) return;
return;
QPointF smoothed; QPointF smoothed;
if (m_currentStroke.points.isEmpty()) { if (m_currentStroke.points.isEmpty()) {
@@ -159,7 +150,8 @@ void StrokeCanvasItem::appendPoint(qreal x, qreal y) {
constexpr qreal minAlpha = 0.1; constexpr qreal minAlpha = 0.1;
constexpr qreal maxAlpha = 0.3; constexpr qreal maxAlpha = 0.3;
const qreal t = std::clamp((dist - minDist) / (maxDist - minDist), 0.0, 1.0); const qreal t =
std::clamp((dist - minDist) / (maxDist - minDist), 0.0, 1.0);
const qreal alpha = minAlpha + t * (maxAlpha - minAlpha); const qreal alpha = minAlpha + t * (maxAlpha - minAlpha);
smoothed = last * (1.0 - alpha) + incoming * alpha; smoothed = last * (1.0 - alpha) + incoming * alpha;
@@ -171,11 +163,11 @@ void StrokeCanvasItem::appendPoint(qreal x, qreal y) {
void StrokeCanvasItem::endStroke() { void StrokeCanvasItem::endStroke() {
m_isDrawing = false; m_isDrawing = false;
if (m_currentStroke.points.isEmpty()) if (m_currentStroke.points.isEmpty()) return;
return;
m_currentStroke.isSinglePoint = (m_currentStroke.points.size() == 1); m_currentStroke.isSinglePoint = (m_currentStroke.points.size() == 1);
m_currentStroke.path = buildStrokePath(m_currentStroke.points, m_currentStroke.width); m_currentStroke.path =
buildStrokePath(m_currentStroke.points, m_currentStroke.width);
m_currentStroke.groupId = m_nextGroupId++; m_currentStroke.groupId = m_nextGroupId++;
m_currentStroke.points.clear(); m_currentStroke.points.clear();
m_strokes.append(m_currentStroke); m_strokes.append(m_currentStroke);
@@ -190,4 +182,4 @@ void StrokeCanvasItem::clear() {
update(); update();
} }
}; }; // namespace ZShell::internal
+14
View File
@@ -3,9 +3,11 @@
#include <QtConcurrent/qtconcurrentrun.h> #include <QtConcurrent/qtconcurrentrun.h>
#include <QtQuick/qquickitemgrabresult.h> #include <QtQuick/qquickitemgrabresult.h>
#include <QtQuick/qquickwindow.h> #include <QtQuick/qquickwindow.h>
#include <qcontainerfwd.h>
#include <qdir.h> #include <qdir.h>
#include <qfileinfo.h> #include <qfileinfo.h>
#include <qfuturewatcher.h> #include <qfuturewatcher.h>
#include <qjsprimitivevalue.h>
#include <qloggingcategory.h> #include <qloggingcategory.h>
#include <qqmlengine.h> #include <qqmlengine.h>
@@ -142,4 +144,16 @@ qreal ZUtils::clamp(qreal value, qreal min, qreal max) {
return qBound(min, value, max); return qBound(min, value, max);
} }
#ifndef ZSHELL_VERSION
#define ZSHELL_VERSION ""
#endif
QString ZUtils::version() const {
return QStringLiteral(ZSHELL_VERSION);
}
QString ZUtils::qtVersion() const {
return QStringLiteral(QT_VERSION_STR);
}
} // namespace ZShell } // namespace ZShell
+4
View File
@@ -1,6 +1,7 @@
#pragma once #pragma once
#include <QtQuick/qquickitem.h> #include <QtQuick/qquickitem.h>
#include <qcontainerfwd.h>
#include <qobject.h> #include <qobject.h>
#include <qqmlintegration.h> #include <qqmlintegration.h>
@@ -26,6 +27,9 @@ Q_INVOKABLE static bool deleteFile(const QUrl& path);
Q_INVOKABLE static QString toLocalFile(const QUrl& url); Q_INVOKABLE static QString toLocalFile(const QUrl& url);
Q_INVOKABLE static qreal clamp(qreal value, qreal min, qreal max); Q_INVOKABLE static qreal clamp(qreal value, qreal min, qreal max);
[[nodiscard]] QString version() const;
[[nodiscard]] QString qtVersion() const;
}; };
} // namespace ZShell } // namespace ZShell
+43
View File
@@ -0,0 +1,43 @@
FROM archlinux:latest
RUN pacman -Syu --noconfirm
# Build toolchain
RUN pacman -S --noconfirm \
base-devel \
cmake \
ninja \
clang \
lld \
nodejs \
git
# Qt6
RUN pacman -S --noconfirm \
qt6-base \
qt6-declarative \
qt6-shadertools \
qt6-5compat \
qt6-canvaspainter
# Project-specific dependencies
RUN pacman -S --noconfirm \
libqalculate \
pipewire \
aubio \
cava \
lm_sensors \
glib2
# AUR: libcava (provides development headers for cava)
RUN pacman -S --needed --noconfirm sudo && \
useradd -m builder && \
echo "builder ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers && \
su builder -c " \
git clone https://aur.archlinux.org/libcava.git /tmp/libcava && \
cd /tmp/libcava && \
makepkg -si --noconfirm \
" && \
rm -rf /tmp/libcava && \
userdel -r builder && \
sed -i '/^builder/d' /etc/sudoers