8 Commits
Author SHA1 Message Date
AramJonghu f439456ff4 fix(strokecanvasrenderer): use qreal instead of float in drawHoverCursor
Lint & Format (JS/TS) / lint-format (pull_request) Successful in 27s
Python / lint-format (pull_request) Successful in 31s
Python / test (pull_request) Successful in 1m0s
Lint & Format (Rust) / lint-format (pull_request) Successful in 1m39s
C++ / build (pull_request) Successful in 2m17s
2026-06-30 01:26:19 +02:00
AramJonghu 4a18edfc78 fix(strokecanvasitem): use qreal instead of float to suppress -Wdouble-promotion
Lint & Format (JS/TS) / lint-format (pull_request) Successful in 16s
Python / lint-format (pull_request) Successful in 23s
Python / test (pull_request) Successful in 48s
C++ / build (pull_request) Successful in 2m20s
Lint & Format (Rust) / lint-format (pull_request) Successful in 1m59s
2026-06-30 01:22:40 +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
14 changed files with 656 additions and 448 deletions
+10 -19
View File
@@ -2,29 +2,20 @@
Checks: >
-*,
bugprone-*,
-bugprone-easily-swappable-parameters,
-bugprone-narrowing-conversions,
-bugprone-implicit-widening-of-multiplication-result,
clang-analyzer-*,
modernize-*,
-modernize-use-trailing-return-type,
performance-*,
readability-braces-around-statements,
cppcoreguidelines-init-variables,
misc-no-recursion,
concurrency-mt-unsafe,
readability-else-after-return,
readability-identifier-naming,
readability-make-member-function-const,
readability-redundant-*,
readability-simplify-*,
CheckOptions:
readability-identifier-naming.ClassCase: CamelCase
readability-identifier-naming.EnumCase: CamelCase
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: "*"
modernize-use-override,
modernize-use-nullptr,
modernize-deprecated-headers,
HeaderFilterRegex: ".*"
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)
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_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")
add_compile_options(
-Wall -Wextra -Wpedantic -Wshadow -Wconversion
-Wold-style-cast -Wnull-dereference -Wdouble-promotion
-Wformat=2 -Wfloat-equal -Woverloaded-virtual
-Wsign-conversion -Wredundant-decls -Wswitch
-Wunreachable-code
-Wall -Wextra -Wpedantic -Wshadow -Wconversion
-Wold-style-cast -Wnull-dereference -Wdouble-promotion
-Wformat=2 -Wfloat-equal -Woverloaded-virtual
-Wsign-conversion -Wredundant-decls -Wswitch
-Wunreachable-code
)
@@ -30,8 +59,8 @@ endif()
if("shell" IN_LIST ENABLE_MODULES)
foreach(dir assets scripts Components Config Modules Daemons Drawers Effects Helpers Paths)
install(DIRECTORY ${dir} DESTINATION "${INSTALL_QSCONFDIR}")
endforeach()
install(DIRECTORY ${dir} DESTINATION "${INSTALL_QSCONFDIR}")
endforeach()
# Disable watching for changes
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)
if(NOT Cava_FOUND)
pkg_check_modules(Cava IMPORTED_TARGET cava REQUIRED)
pkg_check_modules(Cava IMPORTED_TARGET cava REQUIRED)
endif()
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)
function(qml_module arg_TARGET)
cmake_parse_arguments(PARSE_ARGV 1 arg "" "URI" "SOURCES;LIBRARIES")
qt_add_qml_module(${arg_TARGET}
URI ${arg_URI}
VERSION 1.0
SOURCES ${arg_SOURCES}
)
cmake_parse_arguments(PARSE_ARGV 1 arg "" "URI" "SOURCES;LIBRARIES")
qt_add_qml_module(${arg_TARGET}
URI ${arg_URI}
VERSION 1.0
SOURCES ${arg_SOURCES}
)
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
)
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
)
set(module_dir "${INSTALL_QMLDIR}/${module_target_path}")
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(FILES "${module_qmldir}" DESTINATION "${module_dir}")
install(FILES "${module_typeinfo}" DESTINATION "${module_dir}")
target_link_libraries(${arg_TARGET} PRIVATE Qt::Core Qt::Qml ${arg_LIBRARIES})
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(FILES "${module_qmldir}" DESTINATION "${module_dir}")
install(FILES "${module_typeinfo}" DESTINATION "${module_dir}")
target_link_libraries(${arg_TARGET} PRIVATE Qt::Core Qt::Qml ${arg_LIBRARIES})
endfunction()
qml_module(ZShell
URI ZShell
SOURCES
writefile.hpp writefile.cpp
appdb.hpp appdb.cpp
imageanalyser.hpp imageanalyser.cpp
URI ZShell
SOURCES
writefile.hpp writefile.cpp
appdb.hpp appdb.cpp
imageanalyser.hpp imageanalyser.cpp
requests.hpp requests.cpp
toaster.hpp toaster.cpp
qalculator.hpp qalculator.cpp
zutils.hpp zutils.cpp
LIBRARIES
Qt::Gui
Qt::Quick
Qt::Concurrent
Qt::Sql
LIBRARIES
Qt::Gui
Qt::Quick
Qt::Concurrent
Qt::Sql
Qt::DBus
PkgConfig::Qalculate
)
target_compile_definitions(ZShell PRIVATE ZSHELL_VERSION="${VERSION}")
add_subdirectory(Models)
add_subdirectory(Internal)
add_subdirectory(Services)
+268 -198
View File
@@ -1,6 +1,7 @@
#include "lazylistview.hpp"
#include <algorithm>
#include <cmath>
#include <qqmlcontext.h>
#include <qtimer.h>
@@ -13,11 +14,7 @@ constexpr int ASYNC_BATCH_DESTROY = 4;
namespace ZShell::components {
// --- LazyListViewAttached ---
LazyListViewAttached::LazyListViewAttached(QObject* parent)
: QObject(parent) {
}
LazyListViewAttached::LazyListViewAttached(QObject* parent) : QObject(parent) {}
qreal LazyListViewAttached::preferredHeight() const {
return m_preferredHeight;
@@ -85,10 +82,7 @@ void LazyListViewAttached::setTrackViewport(bool track) {
emit trackViewportChanged();
}
// --- LazyListView ---
LazyListView::LazyListView(QQuickItem* parent)
: QQuickItem(parent) {
LazyListView::LazyListView(QQuickItem* parent) : QQuickItem(parent) {
setFlag(ItemHasContents, false);
}
@@ -103,8 +97,6 @@ LazyListView::~LazyListView() {
destroyDelegate(entry);
}
// --- Model & Delegate ---
QAbstractItemModel* LazyListView::model() const {
return m_model;
}
@@ -138,8 +130,6 @@ void LazyListView::setDelegate(QQmlComponent* delegate) {
emit delegateChanged();
}
// --- Layout ---
qreal LazyListView::spacing() const {
return m_spacing;
}
@@ -172,8 +162,6 @@ void LazyListView::setContentY(qreal contentY) {
polish();
}
// --- Viewport ---
QRectF LazyListView::viewport() const {
return m_viewport;
}
@@ -211,8 +199,6 @@ void LazyListView::setCacheBuffer(qreal buffer) {
polish();
}
// --- Sizing ---
qreal LazyListView::estimatedHeight() const {
return m_estimatedHeight;
}
@@ -258,7 +244,8 @@ qreal LazyListView::delegateHeight(QQuickItem* item) {
if (!item)
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)
return attached->preferredHeight();
@@ -269,7 +256,8 @@ qreal LazyListView::delegateVisibleHeight(QQuickItem* item) {
if (!item)
return 0;
auto* attached = qobject_cast<LazyListViewAttached*>(qmlAttachedPropertiesObject<LazyListView>(item, false));
auto* attached = qobject_cast<LazyListViewAttached*>(
qmlAttachedPropertiesObject<LazyListView>(item, false));
if (attached) {
if (attached->visibleHeight() >= 0)
return attached->visibleHeight();
@@ -283,12 +271,11 @@ qreal LazyListView::delegateVisibleHeight(QQuickItem* item) {
bool LazyListView::isDelegateReady(QQuickItem* item) {
if (!item)
return false;
auto* att = qobject_cast<LazyListViewAttached*>(qmlAttachedPropertiesObject<LazyListView>(item, false));
auto* att = qobject_cast<LazyListViewAttached*>(
qmlAttachedPropertiesObject<LazyListView>(item, false));
return !att || att->ready();
}
// --- Animation Durations ---
int LazyListView::removeDuration() const {
return m_removeDuration;
}
@@ -311,21 +298,18 @@ void LazyListView::setReadyDelay(int delay) {
emit readyDelayChanged();
}
// --- State ---
int LazyListView::count() const {
return m_model ? m_model->rowCount() : 0;
}
// --- QQuickItem Overrides ---
void LazyListView::componentComplete() {
QQuickItem::componentComplete();
m_componentComplete = true;
resetContent();
}
void LazyListView::geometryChange(const QRectF& newGeometry, const QRectF& oldGeometry) {
void LazyListView::geometryChange(const QRectF& newGeometry,
const QRectF& oldGeometry) {
QQuickItem::geometryChange(newGeometry, oldGeometry);
if (!m_componentComplete)
@@ -348,71 +332,84 @@ void LazyListView::updatePolish() {
// Flush pending inserts — make items visible and clear the adding flag
// so enter animations begin. When readyDelay > 0 the entire insert is
// deferred so delegates have time to lay out before appearing.
for (auto& entry : m_delegates) {
if (!entry.pendingInsert || !entry.item)
continue;
// Collect pending indices first — avoids scanning the entire hash each frame
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 (!entry.readyDelayStarted) {
entry.readyDelayStarted = true;
auto* item = entry.item;
QTimer::singleShot(m_readyDelay, this, [this, item] {
auto indexIt = m_itemToIndex.find(item);
if (indexIt == m_itemToIndex.end())
return;
const int idx = indexIt.value();
auto it = m_delegates.find(idx);
if (it == m_delegates.end() || it->item != item || !it->pendingInsert)
return;
auto indexIt = m_itemToIndex.find(item);
if (indexIt == m_itemToIndex.end())
return;
const int idx = indexIt.value();
auto it = m_delegates.find(idx);
if (it == m_delegates.end() || it->item != item ||
!it->pendingInsert)
return;
it->pendingInsert = false;
it->readyDelayStarted = false;
it->pendingInsert = false;
it->readyDelayStarted = false;
// Set initial y to visual position (based on current visible heights)
if (idx >= 0 && idx < static_cast<int>(m_layout.size())) {
qreal visualY = 0;
bool hasVisItem = false;
for (int i = 0; i < static_cast<int>(m_layout.size()); ++i) {
qreal h;
auto dit = m_delegates.find(i);
if (dit != m_delegates.end() && dit->item)
h = delegateVisibleHeight(dit->item);
else
h = m_layout[i].heightKnown ? m_layout[i].height : effectiveEstimatedHeight();
if (h > 0) {
if (hasVisItem)
visualY += m_spacing;
hasVisItem = true;
}
if (i == idx)
break;
if (h > 0)
visualY += h;
// Set initial y to visual position (based on current visible heights)
if (idx >= 0 && idx < static_cast<int>(m_layout.size())) {
qreal visualY = 0;
bool hasVisItem = false;
for (int i = 0; i < static_cast<int>(m_layout.size());
++i) {
qreal h = NAN;
auto dit = m_delegates.find(i);
if (dit != m_delegates.end() && dit->item)
h = delegateVisibleHeight(dit->item);
else
h = m_layout[i].heightKnown
? m_layout[i].height
: effectiveEstimatedHeight();
if (h > 0) {
if (hasVisItem)
visualY += m_spacing;
hasVisItem = true;
}
item->setY(visualY - m_contentY);
if (i == idx)
break;
if (h > 0)
visualY += h;
}
item->setY(visualY - m_contentY);
}
item->setVisible(true);
auto* att =
qobject_cast<LazyListViewAttached*>(qmlAttachedPropertiesObject<LazyListView>(item, false));
if (att) {
att->setAdding(false);
att->setReady(true);
}
item->setVisible(true);
auto* att = qobject_cast<LazyListViewAttached*>(
qmlAttachedPropertiesObject<LazyListView>(item, false));
if (att) {
att->setAdding(false);
att->setReady(true);
}
// Animate from visual position to layout position
if (idx >= 0 && idx < static_cast<int>(m_layout.size()))
item->setProperty("y", m_layout[idx].targetY - m_contentY);
// Animate from visual position to layout position
if (idx >= 0 && idx < static_cast<int>(m_layout.size()))
item->setProperty("y",
m_layout[idx].targetY - m_contentY);
polish();
});
polish();
});
}
continue;
}
entry.pendingInsert = false;
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) {
att->setAdding(false);
att->setReady(true);
@@ -429,12 +426,13 @@ void LazyListView::updatePolish() {
record.isNew = false;
// Position delegates — QML Behavior on y handles the animation
const int layoutSize = static_cast<int>(m_layout.size());
for (auto& entry : m_delegates) {
if (!entry.item || entry.pendingRemoval || entry.pendingInsert)
continue;
const int idx = entry.modelIndex;
if (idx < 0 || idx >= static_cast<int>(m_layout.size()))
if (idx < 0 || idx >= layoutSize)
continue;
if (m_layout[idx].heightKnown && qFuzzyIsNull(m_layout[idx].height))
@@ -454,7 +452,8 @@ void LazyListView::relayout() {
qreal y = 0;
bool hasLayoutItem = false;
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 (hasLayoutItem)
y += m_spacing;
@@ -476,12 +475,13 @@ void LazyListView::relayout() {
qreal visY = 0;
bool hasVisItem = false;
for (int i = 0; i < static_cast<int>(m_layout.size()); ++i) {
qreal h;
qreal h = NAN;
auto dit = m_delegates.find(i);
if (dit != m_delegates.end() && dit->item)
h = delegateVisibleHeight(dit->item);
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 (hasVisItem)
visY += m_spacing;
@@ -542,11 +542,11 @@ QRectF LazyListView::effectiveViewport() const {
std::pair<int, int> LazyListView::computeVisibleRange() const {
if (m_layout.isEmpty())
return { -1, -1 };
return {-1, -1};
const auto vp = effectiveViewport();
if (vp.isEmpty())
return { -1, -1 };
return {-1, -1};
const qreal vpTop = vp.y();
const qreal vpBottom = vp.y() + vp.height();
@@ -559,7 +559,9 @@ std::pair<int, int> LazyListView::computeVisibleRange() const {
while (lo <= hi) {
const int mid = lo + (hi - lo) / 2;
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) {
first = mid;
@@ -570,7 +572,7 @@ std::pair<int, int> LazyListView::computeVisibleRange() const {
}
if (first >= static_cast<int>(m_layout.size()))
return { -1, -1 };
return {-1, -1};
// Linear scan for last visible item
int last = first;
@@ -580,7 +582,7 @@ std::pair<int, int> LazyListView::computeVisibleRange() const {
last = i;
}
return { first, last };
return {first, last};
}
// --- Delegate Lifecycle ---
@@ -612,9 +614,12 @@ void LazyListView::syncDelegates() {
}
// 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;
removedEntries.reserve(std::min(destroyBudget, static_cast<int>(toRemove.size())));
removedEntries.reserve(
std::min(destroyBudget, static_cast<int>(toRemove.size())));
int destroyed = 0;
for (int idx : toRemove) {
if (destroyed >= destroyBudget)
@@ -638,7 +643,8 @@ void LazyListView::syncDelegates() {
}
// 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;
for (int i : toCreate) {
if (created >= createBudget)
@@ -658,8 +664,9 @@ void LazyListView::syncDelegates() {
// Pending inserts need to become visible on the next frame, and
// async mode may have remaining create/destroy work.
if (created > 0 || (m_asynchronous && (destroyed < static_cast<int>(toRemove.size()) ||
created < static_cast<int>(toCreate.size()))))
if (created > 0 ||
(m_asynchronous && (destroyed < static_cast<int>(toRemove.size()) ||
created < static_cast<int>(toCreate.size()))))
polish();
}
@@ -704,8 +711,10 @@ LazyListView::DelegateEntry LazyListView::createDelegate(int modelIndex) {
initialProps.insert(QStringLiteral("index"), modelIndex);
if (!hasModelData) {
const auto role = roleNames.isEmpty() ? Qt::DisplayRole : roleNames.constBegin().key();
initialProps.insert(QStringLiteral("modelData"), m_model->data(index, role));
const auto role = roleNames.isEmpty() ? Qt::DisplayRole
: roleNames.constBegin().key();
initialProps.insert(QStringLiteral("modelData"),
m_model->data(index, role));
}
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).
// Cleared on the next frame in updatePolish when the item becomes visible.
if (modelIndex < static_cast<int>(m_layout.size()) && m_layout[modelIndex].isNew) {
auto* addingAttached =
qobject_cast<LazyListViewAttached*>(qmlAttachedPropertiesObject<LazyListView>(entry.item, true));
if (modelIndex < static_cast<int>(m_layout.size()) &&
m_layout[modelIndex].isNew) {
auto* addingAttached = qobject_cast<LazyListViewAttached*>(
qmlAttachedPropertiesObject<LazyListView>(entry.item, true));
if (addingAttached)
addingAttached->setAdding(true);
}
@@ -730,84 +740,103 @@ LazyListView::DelegateEntry LazyListView::createDelegate(int modelIndex) {
// Height-change handler — uses m_itemToIndex for O(1) lookup.
// Ignored while the delegate is not yet ready.
auto onHeightChanged = [this, item = entry.item] {
if (!isDelegateReady(item))
return;
auto indexIt = m_itemToIndex.find(item);
if (indexIt == m_itemToIndex.end())
return;
const int idx = indexIt.value();
auto delegateIt = m_delegates.find(idx);
if (delegateIt == m_delegates.end() || delegateIt->item != item)
return;
const qreal h = delegateHeight(item);
if (idx < static_cast<int>(m_layout.size()) && !qFuzzyCompare(m_layout[idx].height + 1.0, h + 1.0)) {
const qreal oldH = m_layout[idx].height;
const bool wasKnown = m_layout[idx].heightKnown;
m_layout[idx].height = h;
m_layout[idx].heightKnown = true;
if (wasKnown)
untrackHeight(oldH);
trackHeight(h);
if (!isDelegateReady(item))
return;
auto indexIt = m_itemToIndex.find(item);
if (indexIt == m_itemToIndex.end())
return;
const int idx = indexIt.value();
auto delegateIt = m_delegates.find(idx);
if (delegateIt == m_delegates.end() || delegateIt->item != item)
return;
const qreal h = delegateHeight(item);
if (idx < static_cast<int>(m_layout.size()) &&
!qFuzzyCompare(m_layout[idx].height + 1.0, h + 1.0)) {
const qreal oldH = m_layout[idx].height;
const bool wasKnown = m_layout[idx].heightKnown;
m_layout[idx].height = h;
m_layout[idx].heightKnown = true;
if (wasKnown)
untrackHeight(oldH);
trackHeight(h);
// If this tracked item is above the viewport, emit a
// compensation delta so the consumer can adjust scroll.
if (wasKnown) {
auto* att = qobject_cast<LazyListViewAttached*>(qmlAttachedPropertiesObject<LazyListView>(item, false));
if (att && att->trackViewport()) {
const qreal vpTop = m_useCustomViewport ? m_viewport.y() : m_contentY;
if (m_layout[idx].targetY < vpTop)
emit viewportAdjustNeeded(h - oldH);
}
}
// If this tracked item is above the viewport, emit a
// compensation delta so the consumer can adjust scroll.
if (wasKnown) {
auto* att = qobject_cast<LazyListViewAttached*>(
qmlAttachedPropertiesObject<LazyListView>(item, false));
if (att && att->trackViewport()) {
const qreal vpTop = m_useCustomViewport ? m_viewport.y()
: m_contentY;
if (m_layout[idx].targetY < vpTop)
emit viewportAdjustNeeded(h - oldH);
}
}
if (!m_relayoutPending) {
m_relayoutPending = true;
QTimer::singleShot(0, this, [this] {
m_relayoutPending = false;
relayout();
polish();
});
}
}
};
if (!m_relayoutPending) {
m_relayoutPending = true;
QTimer::singleShot(0, this, [this] {
m_relayoutPending = false;
relayout();
polish();
});
}
}
};
// 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
auto* attached = qobject_cast<LazyListViewAttached*>(qmlAttachedPropertiesObject<LazyListView>(entry.item, false));
auto* attached = qobject_cast<LazyListViewAttached*>(
qmlAttachedPropertiesObject<LazyListView>(entry.item, false));
if (attached) {
connect(attached, &LazyListViewAttached::preferredHeightChanged, this, onHeightChanged);
connect(attached, &LazyListViewAttached::visibleHeightChanged, this, [this] {
polish();
});
connect(attached, &LazyListViewAttached::readyChanged, this, [this, item = entry.item] {
auto indexIt = m_itemToIndex.find(item);
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;
connect(attached,
&LazyListViewAttached::preferredHeightChanged,
this,
onHeightChanged);
connect(attached,
&LazyListViewAttached::visibleHeightChanged,
this,
[this] { polish(); });
connect(attached,
&LazyListViewAttached::readyChanged,
this,
[this, item = entry.item] {
auto indexIt = m_itemToIndex.find(item);
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 oldLayoutH = m_layout[idx].heightKnown ? m_layout[idx].height : effectiveEstimatedHeight();
if (m_layout[idx].heightKnown)
untrackHeight(m_layout[idx].height);
m_layout[idx].height = h;
m_layout[idx].heightKnown = true;
trackHeight(h);
const qreal h = delegateHeight(item);
const qreal oldLayoutH = m_layout[idx].heightKnown
? m_layout[idx].height
: effectiveEstimatedHeight();
if (m_layout[idx].heightKnown)
untrackHeight(m_layout[idx].height);
m_layout[idx].height = h;
m_layout[idx].heightKnown = true;
trackHeight(h);
if (att->trackViewport() && !qFuzzyCompare(h + 1.0, oldLayoutH + 1.0)) {
const qreal vpTop = m_useCustomViewport ? m_viewport.y() : m_contentY;
if (m_layout[idx].targetY < vpTop)
emit viewportAdjustNeeded(h - oldLayoutH);
}
if (att->trackViewport() &&
!qFuzzyCompare(h + 1.0, oldLayoutH + 1.0)) {
const qreal vpTop = m_useCustomViewport ? m_viewport.y()
: m_contentY;
if (m_layout[idx].targetY < vpTop)
emit viewportAdjustNeeded(h - oldLayoutH);
}
polish();
});
polish();
});
}
return entry;
@@ -832,7 +861,8 @@ void LazyListView::updateDelegateData(DelegateEntry& entry) {
for (auto it = roleNames.constBegin(); it != roleNames.constEnd(); ++it) {
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"))
hasModelData = true;
}
@@ -840,7 +870,8 @@ void LazyListView::updateDelegateData(DelegateEntry& entry) {
entry.item->setProperty("index", entry.modelIndex);
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));
}
}
@@ -852,24 +883,46 @@ void LazyListView::connectModel() {
return;
m_modelConnections = {
connect(m_model, &QAbstractItemModel::rowsInserted, this, &LazyListView::onRowsInserted),
connect(m_model, &QAbstractItemModel::rowsAboutToBeRemoved, this, &LazyListView::onRowsAboutToBeRemoved),
connect(m_model, &QAbstractItemModel::rowsRemoved, this, &LazyListView::onRowsRemoved),
connect(m_model, &QAbstractItemModel::rowsMoved, this, &LazyListView::onRowsMoved),
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();
}),
connect(m_model,
&QAbstractItemModel::rowsInserted,
this,
&LazyListView::onRowsInserted),
connect(m_model,
&QAbstractItemModel::rowsAboutToBeRemoved,
this,
&LazyListView::onRowsAboutToBeRemoved),
connect(m_model,
&QAbstractItemModel::rowsRemoved,
this,
&LazyListView::onRowsRemoved),
connect(m_model,
&QAbstractItemModel::rowsMoved,
this,
&LazyListView::onRowsMoved),
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();
}
void LazyListView::onRowsInserted(const QModelIndex& parent, int first, int last) {
void LazyListView::onRowsInserted(const QModelIndex& parent,
int first,
int last) {
if (parent.isValid())
return;
const int insertCount = last - first + 1;
// 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
QHash<int, DelegateEntry> shifted;
@@ -935,7 +990,9 @@ void LazyListView::onRowsInserted(const QModelIndex& parent, int first, int last
polish();
}
void LazyListView::onRowsAboutToBeRemoved(const QModelIndex& parent, int first, int last) {
void LazyListView::onRowsAboutToBeRemoved(const QModelIndex& parent,
int first,
int last) {
if (parent.isValid())
return;
@@ -955,22 +1012,24 @@ void LazyListView::onRowsAboutToBeRemoved(const QModelIndex& parent, int first,
}
if (m_removeDuration > 0 && entry.item) {
auto* attached =
qobject_cast<LazyListViewAttached*>(qmlAttachedPropertiesObject<LazyListView>(entry.item, false));
auto* attached = qobject_cast<LazyListViewAttached*>(
qmlAttachedPropertiesObject<LazyListView>(entry.item, false));
if (attached)
attached->setRemoving(true);
// Schedule destruction after the remove animation duration
auto* item = entry.item;
QTimer::singleShot(m_removeDuration, this, [this, item] {
for (auto it = m_dyingDelegates.begin(); it != m_dyingDelegates.end(); ++it) {
if (it->item == item) {
destroyDelegate(*it);
m_dyingDelegates.erase(it);
return;
}
for (auto it = m_dyingDelegates.begin();
it != m_dyingDelegates.end();
++it) {
if (it->item == item) {
destroyDelegate(*it);
m_dyingDelegates.erase(it);
return;
}
});
}
});
m_dyingDelegates.append(std::move(entry));
} else {
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())
return;
@@ -1011,7 +1072,11 @@ void LazyListView::onRowsRemoved(const QModelIndex& parent, int first, int last)
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())
return;
@@ -1055,7 +1120,9 @@ void LazyListView::onRowsMoved(const QModelIndex& parent, int start, int end, co
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)
if (topLeft.parent().isValid())
@@ -1079,15 +1146,18 @@ void LazyListView::onModelReset() {
// Check if the model data actually changed
if (newRows == oldRows) {
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;
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) {
changed = true;
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");
if (newData != oldData) {
changed = true;
+2 -2
View File
@@ -11,9 +11,9 @@ struct Stroke {
QVector<QPointF> points;
QCanvasPath path;
QColor color;
float width;
qreal width;
int groupId = -1;
bool isSinglePoint = false;
};
};
}; // namespace ZShell::internal
+38 -46
View File
@@ -6,55 +6,52 @@
namespace ZShell::internal {
StrokeCanvasItem::StrokeCanvasItem(QQuickItem *parent) : QCanvasPainterItem(parent) {
StrokeCanvasItem::StrokeCanvasItem(QQuickItem* parent)
: QCanvasPainterItem(parent) {
setFillColor(Qt::transparent);
setAlphaBlending(true);
}
QCanvasPainterItemRenderer *StrokeCanvasItem::createItemRenderer() const {
QCanvasPainterItemRenderer* StrokeCanvasItem::createItemRenderer() const {
return new StrokeCanvasRenderer;
}
static bool shouldAddPoint(
const QVector<QPointF> &points,
const QPointF &p,
qreal minDistance)
{
if (points.isEmpty())
return true;
const QVector<QPointF>& points, const QPointF& p, qreal minDistance) {
if (points.isEmpty()) return true;
const QPointF delta = p - points.last();
return QPointF::dotProduct(delta, delta)
>= minDistance * minDistance;
return QPointF::dotProduct(delta, delta) >= minDistance * minDistance;
}
static QCanvasPath buildStrokePath(const QVector<QPointF> &points, float width) {
static QCanvasPath buildStrokePath(const QVector<QPointF>& points, qreal width) {
QCanvasPath path;
if (points.size() == 1) {
path.circle(points[0], width * 0.5f);
path.circle(points[0], width * 0.5);
return path;
}
auto catmullToBezier = [](
const QPointF &p0, const QPointF &p1,
const QPointF &p2, const QPointF &p3,
float tension,
QPointF &cp1, QPointF &cp2)
{
cp1 = p1 + (p2 - p0) * tension / 3.0f;
cp2 = p2 - (p3 - p1) * tension / 3.0f;
};
auto catmullToBezier = [](const QPointF& p0,
const QPointF& p1,
const QPointF& p2,
const QPointF& p3,
qreal tension,
QPointF& cp1,
QPointF& cp2) {
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]);
for (int i = 0; i < points.size() - 1; ++i) {
const QPointF &p0 = points[qMax(i - 1, 0)];
const QPointF &p1 = points[i];
const QPointF &p2 = points[i + 1];
const QPointF &p3 = points[qMin(i + 2, points.size() - 1)];
const QPointF& p0 = points[qMax(i - 1, 0)];
const QPointF& p1 = points[i];
const QPointF& p2 = points[i + 1];
const QPointF& p3 = points[qMin(i + 2, points.size() - 1)];
QPointF 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;
}
void StrokeCanvasItem::setPenColor(const QColor &color) {
if (m_penColor == color)
return;
void StrokeCanvasItem::setPenColor(const QColor& color) {
if (m_penColor == color) return;
m_penColor = color;
update();
@@ -75,8 +71,7 @@ void StrokeCanvasItem::setPenColor(const QColor &color) {
}
void StrokeCanvasItem::setHoverVisible(bool visible) {
if (m_hoverVisible == visible)
return;
if (m_hoverVisible == visible) return;
m_hoverVisible = visible;
update();
@@ -84,9 +79,8 @@ void StrokeCanvasItem::setHoverVisible(bool visible) {
emit hoverVisibleChanged();
}
void StrokeCanvasItem::setHoverPoint(const QPointF &point) {
if (m_hoverPoint == point)
return;
void StrokeCanvasItem::setHoverPoint(const QPointF& point) {
if (m_hoverPoint == point) return;
m_hoverPoint = point;
update();
@@ -110,8 +104,7 @@ void StrokeCanvasItem::showHover(qreal x, qreal y) {
}
void StrokeCanvasItem::hideHover() {
if (!m_hoverVisible)
return;
if (!m_hoverVisible) return;
m_hoverVisible = false;
update();
@@ -119,9 +112,8 @@ void StrokeCanvasItem::hideHover() {
emit hoverVisibleChanged();
}
void StrokeCanvasItem::setPenWidth(float width) {
if (qFuzzyCompare(m_penWidth, width))
return;
void StrokeCanvasItem::setPenWidth(qreal width) {
if (qFuzzyCompare(m_penWidth, width)) return;
m_penWidth = width;
update();
@@ -143,8 +135,7 @@ void StrokeCanvasItem::beginStroke(qreal x, qreal y) {
void StrokeCanvasItem::appendPoint(qreal x, qreal y) {
const QPointF incoming{x, y};
if (!shouldAddPoint(m_currentStroke.points, incoming, 2.0))
return;
if (!shouldAddPoint(m_currentStroke.points, incoming, 2.0)) return;
QPointF smoothed;
if (m_currentStroke.points.isEmpty()) {
@@ -159,7 +150,8 @@ void StrokeCanvasItem::appendPoint(qreal x, qreal y) {
constexpr qreal minAlpha = 0.1;
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);
smoothed = last * (1.0 - alpha) + incoming * alpha;
@@ -171,11 +163,11 @@ void StrokeCanvasItem::appendPoint(qreal x, qreal y) {
void StrokeCanvasItem::endStroke() {
m_isDrawing = false;
if (m_currentStroke.points.isEmpty())
return;
if (m_currentStroke.points.isEmpty()) return;
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.points.clear();
m_strokes.append(m_currentStroke);
@@ -190,4 +182,4 @@ void StrokeCanvasItem::clear() {
update();
}
};
}; // namespace ZShell::internal
+47 -48
View File
@@ -13,68 +13,67 @@ namespace ZShell::internal {
class StrokeCanvasRenderer;
class StrokeCanvasItem : public QCanvasPainterItem {
Q_OBJECT
Q_OBJECT
QML_NAMED_ELEMENT(StrokeCanvas)
QML_NAMED_ELEMENT(StrokeCanvas)
Q_PROPERTY(QColor penColor READ penColor WRITE setPenColor NOTIFY penColorChanged)
Q_PROPERTY(bool hoverVisible READ hoverVisible WRITE setHoverVisible NOTIFY hoverVisibleChanged)
Q_PROPERTY(QPointF hoverPoint READ hoverPoint WRITE setHoverPoint NOTIFY hoverPointChanged)
Q_PROPERTY(qreal penWidth READ penWidth WRITE setPenWidth NOTIFY penWidthChanged)
Q_PROPERTY(
QColor penColor READ penColor WRITE setPenColor NOTIFY penColorChanged)
Q_PROPERTY(
bool hoverVisible READ hoverVisible WRITE setHoverVisible NOTIFY
hoverVisibleChanged)
Q_PROPERTY(
QPointF hoverPoint READ hoverPoint WRITE setHoverPoint NOTIFY
hoverPointChanged)
Q_PROPERTY(
qreal penWidth READ penWidth WRITE setPenWidth NOTIFY penWidthChanged)
public:
explicit StrokeCanvasItem(QQuickItem *parent = nullptr);
public:
explicit StrokeCanvasItem(QQuickItem* parent = nullptr);
[[nodiscard]] bool hoverVisible() const {
return m_hoverVisible;
}
[[nodiscard]] QPointF hoverPoint() const {
return m_hoverPoint;
}
[[nodiscard]] bool hoverVisible() const { return m_hoverVisible; }
[[nodiscard]] QPointF hoverPoint() const { return m_hoverPoint; }
void setHoverVisible(bool visible);
void setHoverPoint(const QPointF &point);
void setHoverVisible(bool visible);
void setHoverPoint(const QPointF& point);
Q_INVOKABLE void showHover(qreal x, qreal y);
Q_INVOKABLE void hideHover();
Q_INVOKABLE void showHover(qreal x, qreal y);
Q_INVOKABLE void hideHover();
[[nodiscard]] QColor penColor() const {
return m_penColor;
}
[[nodiscard]] float penWidth() const {
return m_penWidth;
}
[[nodiscard]] QColor penColor() const { return m_penColor; }
[[nodiscard]] qreal penWidth() const { return m_penWidth; }
void setPenColor(const QColor &color);
void setPenWidth(float width);
void setPenColor(const QColor& color);
void setPenWidth(qreal width);
Q_INVOKABLE void clear();
Q_INVOKABLE void clear();
Q_INVOKABLE void beginStroke(qreal x, qreal y);
Q_INVOKABLE void appendPoint(qreal x, qreal y);
Q_INVOKABLE void endStroke();
Q_INVOKABLE void beginStroke(qreal x, qreal y);
Q_INVOKABLE void appendPoint(qreal x, qreal y);
Q_INVOKABLE void endStroke();
signals:
void penColorChanged();
void penWidthChanged();
void hoverVisibleChanged();
void hoverPointChanged();
signals:
void penColorChanged();
void penWidthChanged();
void hoverVisibleChanged();
void hoverPointChanged();
protected:
[[nodiscard]] QCanvasPainterItemRenderer *createItemRenderer() const override;
protected:
[[nodiscard]] QCanvasPainterItemRenderer* createItemRenderer()
const override;
private:
friend class StrokeCanvasRenderer;
private:
friend class StrokeCanvasRenderer;
bool m_hoverVisible = false;
QPointF m_hoverPoint;
QColor m_penColor = Qt::white;
float m_penWidth = 4.f;
bool m_isDrawing = false;
bool m_hoverVisible = false;
QPointF m_hoverPoint;
QColor m_penColor = Qt::white;
qreal m_penWidth = 4.0;
bool m_isDrawing = false;
int m_nextGroupId = 0;
QVector<Stroke> m_strokes;
Stroke m_currentStroke;
int m_nextGroupId = 0;
QVector<Stroke> m_strokes;
Stroke m_currentStroke;
};
};
}; // namespace ZShell::internal
@@ -5,13 +5,11 @@
namespace ZShell::internal {
static void drawStroke(
QCanvasPainter *painter,
const QVector<QPointF> &points,
const QColor &color,
float width) {
if (points.isEmpty())
return;
QCanvasPainter* painter,
const QVector<QPointF>& points,
const QColor& color,
qreal width) {
if (points.isEmpty()) return;
painter->setStrokeStyle(color);
painter->setFillStyle(color);
@@ -21,31 +19,32 @@ static void drawStroke(
if (points.size() == 1) {
painter->beginPath();
painter->circle(points.front(), width * 0.5f);
painter->circle(points.front(), width * 0.5);
painter->fill();
return;
}
auto catmullToBezier = [](
const QPointF &p0, const QPointF &p1,
const QPointF &p2, const QPointF &p3,
float tension,
QPointF &cp1, QPointF &cp2)
{
cp1 = p1 + (p2 - p0) * tension / 3.0f;
cp2 = p2 - (p3 - p1) * tension / 3.0f;
};
auto catmullToBezier = [](const QPointF& p0,
const QPointF& p1,
const QPointF& p2,
const QPointF& p3,
qreal tension,
QPointF& cp1,
QPointF& cp2) {
cp1 = p1 + (p2 - p0) * tension / 3.0;
cp2 = p2 - (p3 - p1) * tension / 3.0;
};
const float tension = 0.5f; // increase toward 1.0 for tighter curves
const qreal tension = 0.5; // increase toward 1.0 for tighter curves
painter->beginPath();
painter->moveTo(points[0]);
for (int i = 0; i < points.size() - 1; ++i) {
const QPointF &p0 = points[qMax(i - 1, 0)];
const QPointF &p1 = points[i];
const QPointF &p2 = points[i + 1];
const QPointF &p3 = points[qMin(i + 2, points.size() - 1)];
const QPointF& p0 = points[qMax(i - 1, 0)];
const QPointF& p1 = points[i];
const QPointF& p2 = points[i + 1];
const QPointF& p3 = points[qMin(i + 2, points.size() - 1)];
QPointF cp1, cp2;
catmullToBezier(p0, p1, p2, p3, tension, cp1, cp2);
@@ -56,13 +55,12 @@ static void drawStroke(
}
static void drawHoverCursor(
QCanvasPainter *painter,
const QPointF &point,
float penWidth,
QCanvasPainter* painter,
const QPointF& point,
qreal penWidth,
QColor penColor,
bool isDrawing)
{
const float radius = penWidth * 0.5f;
bool isDrawing) {
const qreal radius = penWidth * 0.5;
if (isDrawing) {
painter->setFillStyle(penColor);
@@ -71,67 +69,71 @@ static void drawHoverCursor(
painter->fill();
}
const float lineWidth = 1.5f;
const float crosshairSize = 6.0f;
const bool useDashes = penWidth > 10.0f;
const qreal lineWidth = 1.5;
const qreal crosshairSize = 6.0;
const bool useDashes = penWidth > 10.0;
auto drawOutline = [&](const QColor &color, float width) {
painter->setStrokeStyle(color);
painter->setLineWidth(width);
painter->setLineCap(QCanvasPainter::LineCap::Round);
auto drawOutline = [&](const QColor& color, qreal width) {
painter->setStrokeStyle(color);
painter->setLineWidth(width);
painter->setLineCap(QCanvasPainter::LineCap::Round);
if (useDashes) {
const int dashCount = 12;
const float fullAngle = 2.0f * M_PI;
const float dashAngle = fullAngle / dashCount * 0.5f;
const float gapAngle = fullAngle / dashCount * 0.5f;
if (useDashes) {
const int dashCount = 12;
const qreal fullAngle = 2.0 * M_PI;
const qreal dashAngle = fullAngle / dashCount * 0.5;
const qreal gapAngle = fullAngle / dashCount * 0.5;
float angle = 0.0f;
for (int i = 0; i < dashCount; ++i) {
painter->beginPath();
painter->arc(point, radius, angle, angle + dashAngle,
QCanvasPainter::PathWinding::ClockWise,
QCanvasPainter::PathConnection::NotConnected);
painter->stroke();
angle += dashAngle + gapAngle;
}
} else {
painter->beginPath();
painter->circle(point, radius);
painter->stroke();
}
};
qreal angle = 0.0;
for (int i = 0; i < dashCount; ++i) {
painter->beginPath();
painter->arc(
point,
radius,
angle,
angle + dashAngle,
QCanvasPainter::PathWinding::ClockWise,
QCanvasPainter::PathConnection::NotConnected);
painter->stroke();
angle += dashAngle + gapAngle;
}
} else {
painter->beginPath();
painter->circle(point, radius);
painter->stroke();
}
};
auto drawCrosshair = [&](const QColor &color, float width) {
painter->setStrokeStyle(color);
painter->setLineWidth(width);
painter->setLineCap(QCanvasPainter::LineCap::Round);
auto drawCrosshair = [&](const QColor& color, qreal width) {
painter->setStrokeStyle(color);
painter->setLineWidth(width);
painter->setLineCap(QCanvasPainter::LineCap::Round);
const float inner = radius + 3.0f;
const float outer = radius + 3.0f + crosshairSize;
const qreal inner = radius + 3.0;
const qreal outer = radius + 3.0 + crosshairSize;
painter->beginPath();
painter->moveTo(point + QPointF(0, -outer));
painter->lineTo(point + QPointF(0, -inner));
painter->moveTo(point + QPointF(0, outer));
painter->lineTo(point + QPointF(0, inner));
painter->moveTo(point + QPointF(-outer, 0));
painter->lineTo(point + QPointF(-inner, 0));
painter->moveTo(point + QPointF( outer, 0));
painter->lineTo(point + QPointF( inner, 0));
painter->stroke();
};
painter->beginPath();
painter->moveTo(point + QPointF(0, -outer));
painter->lineTo(point + QPointF(0, -inner));
painter->moveTo(point + QPointF(0, outer));
painter->lineTo(point + QPointF(0, inner));
painter->moveTo(point + QPointF(-outer, 0));
painter->lineTo(point + QPointF(-inner, 0));
painter->moveTo(point + QPointF(outer, 0));
painter->lineTo(point + QPointF(inner, 0));
painter->stroke();
};
drawOutline(QColor(0, 0, 0, 160), lineWidth + 1.0f);
drawCrosshair(QColor(0, 0, 0, 160), lineWidth + 1.0f);
drawOutline(QColor(0, 0, 0, 160), lineWidth + 1.0);
drawCrosshair(QColor(0, 0, 0, 160), lineWidth + 1.0);
drawOutline(QColor(255, 255, 255, 220), lineWidth);
drawCrosshair(QColor(255, 255, 255, 220), lineWidth);
}
void StrokeCanvasRenderer::synchronizeData(QCanvasPainterItem *item) {
auto *canvas = static_cast<StrokeCanvasItem *>(item);
void StrokeCanvasRenderer::synchronizeData(QCanvasPainterItem* item) {
auto* canvas = static_cast<StrokeCanvasItem*>(item);
m_penColor = canvas->m_penColor;
m_penWidth = canvas->m_penWidth;
@@ -140,7 +142,7 @@ void StrokeCanvasRenderer::synchronizeData(QCanvasPainterItem *item) {
m_strokes.append(canvas->m_strokes[m_strokes.size()]);
if (canvas->m_strokes.isEmpty() && !m_strokes.isEmpty()) {
for (const auto &stroke : m_strokes)
for (const auto& stroke : m_strokes)
if (stroke.groupId >= 0)
m_pendingGroupRemovals.append(stroke.groupId);
m_strokes.clear();
@@ -153,14 +155,14 @@ void StrokeCanvasRenderer::synchronizeData(QCanvasPainterItem *item) {
m_isDrawing = canvas->m_isDrawing;
}
void StrokeCanvasRenderer::paint(QCanvasPainter *painter) {
void StrokeCanvasRenderer::paint(QCanvasPainter* painter) {
for (int id : m_pendingGroupRemovals)
painter->removePathGroup(id);
m_pendingGroupRemovals.clear();
painter->clearRect(0, 0, width(), height());
for (const auto &stroke : m_strokes) {
for (const auto& stroke : m_strokes) {
painter->setStrokeStyle(stroke.color);
painter->setFillStyle(stroke.color);
painter->setLineWidth(stroke.width);
@@ -174,10 +176,15 @@ void StrokeCanvasRenderer::paint(QCanvasPainter *painter) {
}
}
drawStroke(painter, m_currentStroke.points, m_currentStroke.color, m_currentStroke.width);
drawStroke(
painter,
m_currentStroke.points,
m_currentStroke.color,
m_currentStroke.width);
if (m_hoverVisible)
drawHoverCursor(painter, m_hoverPoint, m_penWidth, m_penColor, m_isDrawing);
drawHoverCursor(
painter, m_hoverPoint, m_penWidth, m_penColor, m_isDrawing);
}
};
}; // namespace ZShell::internal
@@ -7,22 +7,20 @@
namespace ZShell::internal {
class StrokeCanvasRenderer final : public QCanvasPainterItemRenderer {
public:
void synchronizeData(QCanvasPainterItem* item) override;
void paint(QCanvasPainter* painter) override;
public:
void synchronizeData(QCanvasPainterItem *item) override;
void paint(QCanvasPainter *painter) override;
private:
QColor m_penColor;
float m_penWidth = 4.f;
bool m_hoverVisible = false;
QPointF m_hoverPoint;
bool m_isDrawing = false;
QVector<Stroke> m_strokes;
Stroke m_currentStroke;
QVector<int> m_pendingGroupRemovals;
private:
QColor m_penColor;
qreal m_penWidth = 4.0;
bool m_hoverVisible = false;
QPointF m_hoverPoint;
bool m_isDrawing = false;
QVector<Stroke> m_strokes;
Stroke m_currentStroke;
QVector<int> m_pendingGroupRemovals;
};
};
}; // namespace ZShell::internal
+14
View File
@@ -3,9 +3,11 @@
#include <QtConcurrent/qtconcurrentrun.h>
#include <QtQuick/qquickitemgrabresult.h>
#include <QtQuick/qquickwindow.h>
#include <qcontainerfwd.h>
#include <qdir.h>
#include <qfileinfo.h>
#include <qfuturewatcher.h>
#include <qjsprimitivevalue.h>
#include <qloggingcategory.h>
#include <qqmlengine.h>
@@ -142,4 +144,16 @@ qreal ZUtils::clamp(qreal value, qreal min, qreal 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
+4
View File
@@ -1,6 +1,7 @@
#pragma once
#include <QtQuick/qquickitem.h>
#include <qcontainerfwd.h>
#include <qobject.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 qreal clamp(qreal value, qreal min, qreal max);
[[nodiscard]] QString version() const;
[[nodiscard]] QString qtVersion() const;
};
} // 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