refactor: move clock rendering to qml

This commit is contained in:
2026-05-31 00:10:54 +00:00
parent c44e9f57d2
commit e1a523ea67
15 changed files with 112 additions and 634 deletions
-2
View File
@@ -1,2 +0,0 @@
BasedOnStyle: Google
IndentWidth: 4
-3
View File
@@ -1,3 +0,0 @@
CompileFlags:
# C++20
Add: [-std=c++20]
-1
View File
@@ -15,7 +15,6 @@
# Build artifacts
build/
*.plasmoid
package/contents/ui/cppbridge/
# Temporary files
*~
+10 -44
View File
@@ -6,9 +6,6 @@ METADATA_FILE := $(PACKAGE_DIR)/metadata.json
WIDGET_ID := $(shell grep -oP '"Id": "\K[^"]+' $(METADATA_FILE))
BUILD_DIR := build
PLASMOID_FILE := $(WIDGET_ID).plasmoid
CPP_DIR := cpp
CPP_BUILD_DIR := $(BUILD_DIR)/cpp
CPP_MODULE_DIR := $(PACKAGE_DIR)/contents/ui/cppbridge
# Default target
.PHONY: help
@@ -18,13 +15,11 @@ help:
@echo "Usage:"
@echo " make dev Run widget in plasmoidviewer from local package (MODE=panel|desktop|hidpi)"
@echo " make watch Start Development Mode with Hot Reload"
@echo " make cpp-build Build the C++ QML bridge module"
@echo " make cpp-clean Remove C++ bridge build artifacts"
@echo " make install Install/upgrade widget in local Plasma package store"
@echo " make uninstall Remove widget from local system"
@echo " make package Build .plasmoid file for distribution"
@echo " make clean Remove build artifacts"
@echo " make format Format QML, C++, JSON, YAML, and Markdown files"
@echo " make format Format QML, JSON, YAML, and Markdown files"
@echo " make lint Run non-mutating lint and formatting checks"
@echo " make hooks-install Configure Git to use versioned hooks from .githooks/"
@@ -35,28 +30,11 @@ hooks-install:
git config core.hooksPath .githooks
@echo "Configured core.hooksPath=.githooks"
# C++ Bridge Build Targets
# Generate CMake build files for the C++ QML bridge.
.PHONY: cpp-configure
cpp-configure:
mkdir -p $(CPP_BUILD_DIR)
cd $(CPP_DIR) && cmake --preset clang-default
# Build the C++ QML bridge module binaries.
.PHONY: cpp-build
cpp-build: cpp-configure
cmake --build $(CPP_BUILD_DIR)
# Remove C++ bridge build output and copied module artifacts.
.PHONY: cpp-clean
cpp-clean:
rm -rf $(CPP_BUILD_DIR) $(CPP_MODULE_DIR)
# Testing Targets
# Run plasmoidviewer from the local package directory.
# Optional MODE values: panel, desktop, hidpi (omit MODE for default).
.PHONY: dev
dev: cpp-build
dev:
@mode="$(MODE)"; \
case "$$mode" in \
""|default) \
@@ -75,7 +53,7 @@ dev: cpp-build
# Development Tools
# Start file watcher for iterative development and reload workflow.
.PHONY: watch
watch: cpp-build
watch:
./scripts/watch.sh
# Format all QML files in the package directory in-place.
@@ -84,12 +62,6 @@ format-qml:
@command -v qmlformat >/dev/null 2>&1 || { echo "Missing required tool: qmlformat"; exit 1; }
find $(PACKAGE_DIR) -name "*.qml" -type f -exec qmlformat -i {} +
# Format C/C++ source and header files with clang-format.
.PHONY: format-cpp
format-cpp:
@command -v clang-format >/dev/null 2>&1 || { echo "Missing required tool: clang-format"; exit 1; }
find $(CPP_DIR) -type f \( -name "*.c" -o -name "*.cc" -o -name "*.cpp" -o -name "*.h" -o -name "*.hh" -o -name "*.hpp" \) -exec clang-format -i {} +
# Format JSON/YAML/Markdown files with prettier.
.PHONY: format-prettier
format-prettier:
@@ -102,7 +74,7 @@ format-prettier:
# Run all formatting passes.
.PHONY: format
format: format-qml format-cpp format-prettier
format: format-qml format-prettier
# Linting
# Check QML formatting without changing files.
@@ -119,16 +91,10 @@ lint-qml-format:
# Run semantic QML lint checks.
.PHONY: lint-qml
lint-qml: cpp-build
lint-qml:
@command -v qmllint >/dev/null 2>&1 || { echo "Missing required tool: qmllint"; exit 1; }
find $(PACKAGE_DIR) -name "*.qml" -type f -print0 | xargs -0 -r qmllint
# Check C/C++ formatting without changing files.
.PHONY: lint-cpp
lint-cpp:
@command -v clang-format >/dev/null 2>&1 || { echo "Missing required tool: clang-format"; exit 1; }
find $(CPP_DIR) -type f \( -name "*.c" -o -name "*.cc" -o -name "*.cpp" -o -name "*.h" -o -name "*.hh" -o -name "*.hpp" \) -print0 | xargs -0 -r clang-format --dry-run --Werror
# Check JSON/YAML/Markdown formatting without changing files.
.PHONY: lint-prettier
lint-prettier:
@@ -141,12 +107,12 @@ lint-prettier:
# Run all lint and formatting checks.
.PHONY: lint
lint: lint-qml-format lint-qml lint-cpp lint-prettier
lint: lint-qml-format lint-qml lint-prettier
# Installation
# Install/upgrade widget in local Plasma package store.
.PHONY: install
install: cpp-build
install:
@if kpackagetool6 --type Plasma/Applet --show $(WIDGET_ID) >/dev/null 2>&1; then \
echo "Upgrading existing package: $(WIDGET_ID)"; \
kpackagetool6 --type Plasma/Applet --upgrade $(PACKAGE_DIR); \
@@ -157,7 +123,7 @@ install: cpp-build
# Upgrade an already installed widget package with local changes.
.PHONY: upgrade
upgrade: cpp-build
upgrade:
kpackagetool6 --type Plasma/Applet --upgrade $(PACKAGE_DIR)
# Remove installed widget package from local system.
@@ -173,7 +139,7 @@ uninstall:
# Packaging
# Create distributable .plasmoid archive from current package contents.
.PHONY: package
package: cpp-build
package:
mkdir -p $(BUILD_DIR)
rm -f $(BUILD_DIR)/$(PLASMOID_FILE)
@@ -186,4 +152,4 @@ package: cpp-build
# Delete all generated build artifacts for a clean workspace.
.PHONY: clean
clean:
rm -rf $(BUILD_DIR) $(CPP_MODULE_DIR)
rm -rf $(BUILD_DIR)
+5 -1
View File
@@ -1,6 +1,6 @@
# katoclock
Just a cat with a clock. Bootstrapped from my [kde-6-widget-starter](https://github.com/ruinivist/kde-6-widget-starter)
Just a QML-only cat clock widget for KDE Plasma 6. Bootstrapped from my [kde-6-widget-starter](https://github.com/ruinivist/kde-6-widget-starter)
[Pling page](https://www.pling.com/p/2349620/)
@@ -10,3 +10,7 @@ Just a cat with a clock. Bootstrapped from my [kde-6-widget-starter](https://git
- clone the repo
- `make install` from repo root
# Package
- `make package` builds a `.plasmoid` archive from the QML package and bundled image assets
-39
View File
@@ -1,39 +0,0 @@
cmake_minimum_required(VERSION 3.23)
project(cppbridge LANGUAGES CXX)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
find_package(Qt6 REQUIRED COMPONENTS Core Qml Quick)
find_package(OpenCV 4 REQUIRED COMPONENTS core imgcodecs imgproc)
qt_standard_project_setup(REQUIRES 6.5)
qt_add_qml_module(cppbridgeplugin
URI cppbridge
VERSION 1.0
OUTPUT_DIRECTORY "${CMAKE_SOURCE_DIR}/../package/contents/ui/cppbridge"
SOURCES
src/clockimageprovider.hpp
src/clockimageprovider.cpp
src/config.hpp
src/renderer.hpp
src/renderer.cpp
src/timebridge.h
src/timebridge.cpp
)
target_include_directories(cppbridgeplugin PRIVATE
"${CMAKE_CURRENT_SOURCE_DIR}/src"
${OpenCV_INCLUDE_DIRS}
)
target_link_libraries(cppbridgeplugin PRIVATE
Qt6::Core
Qt6::Qml
Qt6::Quick
opencv_core
opencv_imgcodecs
opencv_imgproc
)
set_target_properties(cppbridgeplugin PROPERTIES
CXX_STANDARD 20
CXX_STANDARD_REQUIRED ON
)
-28
View File
@@ -1,28 +0,0 @@
{
"version": 6,
"cmakeMinimumRequired": {
"major": 3,
"minor": 23,
"patch": 0
},
"configurePresets": [
{
"name": "clang-default",
"displayName": "Clang Default",
"description": "Configure build/cpp with clang/clang++ and export compile commands.",
"generator": "Unix Makefiles",
"binaryDir": "${sourceDir}/../build/cpp",
"cacheVariables": {
"CMAKE_C_COMPILER": "clang",
"CMAKE_CXX_COMPILER": "clang++",
"CMAKE_EXPORT_COMPILE_COMMANDS": "ON"
}
}
],
"buildPresets": [
{
"name": "clang-default",
"configurePreset": "clang-default"
}
]
}
-32
View File
@@ -1,32 +0,0 @@
#include "clockimageprovider.hpp"
#include <QMutexLocker>
ClockImageProvider::ClockImageProvider()
: QQuickImageProvider(QQuickImageProvider::Image) {}
QImage ClockImageProvider::requestImage(const QString&, QSize* size,
const QSize& requestedSize) {
QImage frame;
{
QMutexLocker locker(&mutex_);
frame = image_;
}
if (size != nullptr) {
*size = frame.size();
}
if (!frame.isNull() && requestedSize.width() > 0 &&
requestedSize.height() > 0) {
return frame.scaled(requestedSize, Qt::KeepAspectRatio,
Qt::SmoothTransformation);
}
return frame;
}
void ClockImageProvider::setImage(const QImage& image) {
QMutexLocker locker(&mutex_);
image_ = image;
}
-19
View File
@@ -1,19 +0,0 @@
#pragma once
#include <QImage>
#include <QMutex>
#include <QQuickImageProvider>
class ClockImageProvider : public QQuickImageProvider {
public:
ClockImageProvider();
QImage requestImage(const QString& id, QSize* size,
const QSize& requestedSize) override;
void setImage(const QImage& image);
private:
QMutex mutex_;
QImage image_;
};
-21
View File
@@ -1,21 +0,0 @@
#pragma once
#include <opencv2/core.hpp>
// file full of magic numbers
// most are hand tuned based on the cat image I have
namespace Config {
const cv::Point2f CLOCK_CENTER(300.0f, 286.0f);
const double HAND_SCALE = 0.22;
const double HOUR_HAND_SCALE = HAND_SCALE * 0.7;
const double MINUTE_HAND_SCALE = HAND_SCALE * 0.7;
const cv::Point2f HOUR_HAND_PIVOT_FRACTION(0.126f, 0.502f);
const cv::Point2f MINUTE_HAND_PIVOT_FRACTION(0.101f, 0.488f);
// Asset convention: unrotated hand points right (+X).
// Clock convention: 12:00 points up (-Y), hence +90 degrees.
const double HAND_BASE_ANGLE_OFFSET = 90.0;
} // namespace Config
-112
View File
@@ -1,112 +0,0 @@
#include "renderer.hpp"
#include <iostream>
#include <opencv2/imgcodecs.hpp>
#include <opencv2/imgproc.hpp>
#include "config.hpp"
Renderer::Renderer() {}
bool Renderer::load_assets(const std::string& bg_path,
const std::string& hour_hand_path,
const std::string& minute_hand_path) {
background_ = cv::imread(bg_path, cv::IMREAD_UNCHANGED);
if (background_.empty()) {
std::cerr << "failed to load background: " << bg_path << std::endl;
return false;
}
if (background_.channels() != 4) {
std::cerr << "expected rgba png for background";
return false;
}
// Load hands with alpha channel.
hour_hand_ = cv::imread(hour_hand_path, cv::IMREAD_UNCHANGED);
if (hour_hand_.empty()) {
std::cerr << "failed to load hour hand: " << hour_hand_path
<< std::endl;
return false;
}
minute_hand_ = cv::imread(minute_hand_path, cv::IMREAD_UNCHANGED);
if (minute_hand_.empty()) {
std::cerr << "failed to load minute hand: " << minute_hand_path
<< std::endl;
return false;
}
if (hour_hand_.channels() != 4 || minute_hand_.channels() != 4) {
std::cerr << "expected rgba png for hands" << std::endl;
return false;
}
return true;
}
cv::Mat Renderer::render(double hour_angle, double minute_angle) {
auto clock_to_cv_rotation = [](double clock_angle) {
return Config::HAND_BASE_ANGLE_OFFSET - clock_angle;
};
cv::Mat warped_hour = transform_hand(
hour_hand_, clock_to_cv_rotation(hour_angle), Config::HOUR_HAND_SCALE,
Config::HOUR_HAND_PIVOT_FRACTION);
cv::Mat warped_minute = transform_hand(
minute_hand_, clock_to_cv_rotation(minute_angle),
Config::MINUTE_HAND_SCALE, Config::MINUTE_HAND_PIVOT_FRACTION);
cv::Mat keyframe = background_.clone();
// Layer order matters: hour underneath minute.
blend_overlay(keyframe, warped_hour);
blend_overlay(keyframe, warped_minute);
return keyframe;
}
cv::Mat Renderer::transform_hand(const cv::Mat& hand_sprite, double angle_deg,
double scale,
const cv::Point2f& pivot_fraction) {
cv::Mat oriented_sprite;
cv::flip(hand_sprite, oriented_sprite, 0);
if (scale <= 0.0) {
return cv::Mat::zeros(background_.size(), CV_8UC4);
}
cv::Point2f pivot(oriented_sprite.cols * pivot_fraction.x,
oriented_sprite.rows * pivot_fraction.y);
cv::Mat rot_mat = cv::getRotationMatrix2D(pivot, angle_deg, scale);
rot_mat.at<double>(0, 2) += Config::CLOCK_CENTER.x - pivot.x;
rot_mat.at<double>(1, 2) += Config::CLOCK_CENTER.y - pivot.y;
cv::Mat overlay = cv::Mat::zeros(background_.size(), CV_8UC4);
cv::warpAffine(oriented_sprite, overlay, rot_mat, background_.size(),
cv::INTER_CUBIC, cv::BORDER_CONSTANT,
cv::Scalar(0, 0, 0, 0));
return overlay;
}
void Renderer::blend_overlay(cv::Mat& bg, const cv::Mat& fg) {
// Blend hand RGB onto background RGB using hand alpha
for (int y = 0; y < fg.rows; ++y) {
const cv::Vec4b* fg_ptr = fg.ptr<cv::Vec4b>(y);
cv::Vec4b* bg_ptr = bg.ptr<cv::Vec4b>(y);
for (int x = 0; x < fg.cols; ++x) {
const cv::Vec4b& fg_pixel = fg_ptr[x];
const double alpha = fg_pixel[3] / 255.0;
if (alpha > 0.0) {
const cv::Vec4b bg_pixel = bg_ptr[x];
for (int c = 0; c < 3; ++c) {
bg_ptr[x][c] = static_cast<uchar>(
fg_pixel[c] * alpha + bg_pixel[c] * (1.0 - alpha));
}
}
}
}
}
-28
View File
@@ -1,28 +0,0 @@
#pragma once
#include <opencv2/core.hpp>
#include <string>
class Renderer {
public:
Renderer();
// Load assets from disk. Returns false if any fail to load.
bool load_assets(const std::string& bg_path,
const std::string& hour_hand_path,
const std::string& minute_hand_path);
// Composites the hands onto the background at the given angles (degrees).
// Returns the result image.
cv::Mat render(double hour_angle, double minute_angle);
private:
cv::Mat background_;
cv::Mat hour_hand_;
cv::Mat minute_hand_;
// Helpers
cv::Mat transform_hand(const cv::Mat& hand_sprite, double angle_deg,
double scale, const cv::Point2f& pivot_fraction);
void blend_overlay(cv::Mat& bg, const cv::Mat& fg);
};
-216
View File
@@ -1,216 +0,0 @@
#include "timebridge.h"
#include <QQmlEngine>
#include <QTime>
#include <QUuid>
#include <opencv2/imgproc.hpp>
#include "clockimageprovider.hpp"
#include "renderer.hpp"
namespace {
constexpr int kMillisPerMinute = 60 * 1000;
}
ClockBridge::ClockBridge(QObject* parent) : QObject(parent) {
updateTimer_.setSingleShot(true);
connect(&updateTimer_, &QTimer::timeout, this,
&ClockBridge::onUpdateTimerTimeout);
}
ClockBridge::~ClockBridge() {
updateTimer_.stop();
if (renderer_ != nullptr) {
delete renderer_;
renderer_ = nullptr;
}
}
QString ClockBridge::imageSource() const { return imageSource_; }
bool ClockBridge::ready() const { return ready_; }
QString ClockBridge::errorString() const { return errorString_; }
bool ClockBridge::initialize(const QUrl& bg, const QUrl& hour,
const QUrl& minute) {
updateTimer_.stop();
initialized_ = false;
setReady(false);
const QString bgPath = toLocalPath(bg);
const QString hourPath = toLocalPath(hour);
const QString minutePath = toLocalPath(minute);
if (bgPath.isEmpty() || hourPath.isEmpty() || minutePath.isEmpty()) {
setErrorString(QStringLiteral("Clock assets must be local file URLs."));
return false;
}
if (!ensureImageProvider()) {
return false;
}
if (renderer_ == nullptr) {
renderer_ = new Renderer();
}
if (!renderer_->load_assets(bgPath.toStdString(), hourPath.toStdString(),
minutePath.toStdString())) {
setErrorString(QStringLiteral("Failed to load clock assets."));
return false;
}
initialized_ = true;
if (!renderForCurrentTime()) {
initialized_ = false;
return false;
}
scheduleNextMinuteTick();
return true;
}
void ClockBridge::refreshNow() {
if (!initialized_) {
setErrorString(QStringLiteral("ClockBridge is not initialized."));
setReady(false);
return;
}
renderForCurrentTime();
scheduleNextMinuteTick();
}
void ClockBridge::onUpdateTimerTimeout() {
if (!initialized_) {
return;
}
renderForCurrentTime();
scheduleNextMinuteTick();
}
bool ClockBridge::ensureImageProvider() {
if (imageProvider_ != nullptr) {
return true;
}
QQmlEngine* engine = qmlEngine(this);
if (engine == nullptr) {
setErrorString(QStringLiteral("ClockBridge has no active QQmlEngine."));
return false;
}
providerId_ = QStringLiteral("catclock_%1")
.arg(QUuid::createUuid().toString(QUuid::Id128));
if (engine->imageProvider(providerId_) != nullptr) {
setErrorString(QStringLiteral("Image provider ID collision."));
return false;
}
imageProvider_ = new ClockImageProvider();
engine->addImageProvider(providerId_, imageProvider_);
return true;
}
bool ClockBridge::renderForCurrentTime() {
if (renderer_ == nullptr || imageProvider_ == nullptr) {
setErrorString(QStringLiteral("ClockBridge is not initialized."));
setReady(false);
return false;
}
const QTime now = QTime::currentTime();
const int minute = now.minute();
const int hour = now.hour();
const double minuteAngle = minute * 6.0;
const double hourAngle = (hour % 12) * 30.0 + minute * 0.5;
cv::Mat frame = renderer_->render(hourAngle, minuteAngle);
if (frame.empty()) {
setErrorString(QStringLiteral("Renderer returned an empty frame."));
setReady(false);
return false;
}
cv::Mat rgbaFrame;
cv::cvtColor(frame, rgbaFrame, cv::COLOR_BGRA2RGBA);
QImage image(rgbaFrame.data, rgbaFrame.cols, rgbaFrame.rows,
static_cast<int>(rgbaFrame.step), QImage::Format_RGBA8888);
imageProvider_->setImage(image.copy());
setErrorString(QString());
setReady(true);
bumpFrameRevision();
return true;
}
void ClockBridge::scheduleNextMinuteTick() {
if (!initialized_) {
return;
}
const QTime now = QTime::currentTime();
int delayMs = kMillisPerMinute - (now.second() * 1000 + now.msec());
if (delayMs <= 0 || delayMs > kMillisPerMinute) {
delayMs = kMillisPerMinute;
}
updateTimer_.start(delayMs);
}
void ClockBridge::setReady(bool value) {
if (ready_ == value) {
return;
}
ready_ = value;
emit readyChanged();
}
void ClockBridge::setErrorString(const QString& value) {
if (errorString_ == value) {
return;
}
errorString_ = value;
emit errorStringChanged();
}
QString ClockBridge::toLocalPath(const QUrl& url) const {
if (url.isEmpty()) {
return QString();
}
if (url.isLocalFile()) {
return url.toLocalFile();
}
if (url.scheme().isEmpty()) {
return url.toString();
}
return QString();
}
QString ClockBridge::buildImageSource() const {
if (providerId_.isEmpty()) {
return QString();
}
return QStringLiteral("image://%1/frame?rev=%2")
.arg(providerId_)
.arg(frameRevision_);
}
// to signal image inddeed changed even if the path is same
// we update revision
void ClockBridge::bumpFrameRevision() {
++frameRevision_;
const QString nextSource = buildImageSource();
if (imageSource_ == nextSource) {
return;
}
imageSource_ = nextSource;
emit imageSourceChanged();
}
-59
View File
@@ -1,59 +0,0 @@
#pragma once
#include <qqmlintegration.h>
#include <QObject>
#include <QString>
#include <QTimer>
#include <QUrl>
class ClockImageProvider;
class Renderer;
class ClockBridge : public QObject {
Q_OBJECT
QML_ELEMENT
Q_PROPERTY(QString imageSource READ imageSource NOTIFY imageSourceChanged)
Q_PROPERTY(bool ready READ ready NOTIFY readyChanged)
Q_PROPERTY(QString errorString READ errorString NOTIFY errorStringChanged)
public:
explicit ClockBridge(QObject* parent = nullptr);
~ClockBridge() override;
QString imageSource() const;
bool ready() const;
QString errorString() const;
Q_INVOKABLE bool initialize(const QUrl& bg, const QUrl& hour,
const QUrl& minute);
Q_INVOKABLE void refreshNow();
signals:
void imageSourceChanged();
void readyChanged();
void errorStringChanged();
private slots:
void onUpdateTimerTimeout();
private:
bool ensureImageProvider();
bool renderForCurrentTime();
void scheduleNextMinuteTick();
void setReady(bool value);
void setErrorString(const QString& value);
QString toLocalPath(const QUrl& url) const;
QString buildImageSource() const;
void bumpFrameRevision();
Renderer* renderer_ = nullptr;
ClockImageProvider* imageProvider_ = nullptr;
QString providerId_;
QString imageSource_;
QString errorString_;
bool ready_ = false;
bool initialized_ = false;
int frameRevision_ = 0;
QTimer updateTimer_;
};
+97 -29
View File
@@ -1,56 +1,125 @@
import QtQuick
import QtQuick.Layouts
import "cppbridge" as CppBridge
import org.kde.kirigami as Kirigami
import org.kde.plasma.components as PlasmaComponents
import org.kde.plasma.core as PlasmaCore
import org.kde.plasma.plasmoid
PlasmoidItem {
id: root
// Widget setup
readonly property real clockWidth: 421
readonly property real clockHeight: 600
readonly property real clockCenterX: 300
readonly property real clockCenterY: 286
readonly property real handScale: 0.154
property real hourAngle: 0
property real minuteAngle: 0
function refreshTime() {
const now = new Date();
const minute = now.getMinutes();
const hour = now.getHours();
root.minuteAngle = minute * 6;
root.hourAngle = (hour % 12) * 30 + minute * 0.5;
let nextTick = 60000 - (now.getSeconds() * 1000 + now.getMilliseconds());
if (nextTick <= 0 || nextTick > 60000)
nextTick = 60000;
minuteTimer.interval = nextTick;
minuteTimer.restart();
}
width: Kirigami.Units.gridUnit * 20
height: Kirigami.Units.gridUnit * 20
Plasmoid.backgroundHints: PlasmaCore.Types.NoBackground
toolTipMainText: "katoclock"
Component.onCompleted: {
const initialized = clockBridge.initialize(Qt.resolvedUrl("assets/background.png"), Qt.resolvedUrl("assets/hand_hour.png"), Qt.resolvedUrl("assets/hand_minute.png"));
if (!initialized)
console.warn(clockBridge.errorString);
Component.onCompleted: refreshTime()
Timer {
id: minuteTimer
repeat: false
onTriggered: root.refreshTime()
}
CppBridge.ClockBridge {
id: clockBridge
component ClockHand: Item {
required property url imageSource
required property real sourceWidth
required property real sourceHeight
required property real pivotFractionX
required property real pivotFractionY
required property real clockAngle
readonly property real pivotX: width * pivotFractionX
readonly property real pivotY: height * pivotFractionY
width: sourceWidth * root.handScale
height: sourceHeight * root.handScale
x: root.clockCenterX - pivotX
y: root.clockCenterY - pivotY
Image {
id: handImage
anchors.fill: parent
source: imageSource
smooth: true
mipmap: true
transform: Scale {
origin.x: handImage.width / 2
origin.y: handImage.height / 2
yScale: -1
}
}
transform: Rotation {
origin.x: pivotX
origin.y: pivotY
angle: clockAngle - 90
}
}
component ClockImageView: Item {
// asset image size -> 421x600, 1 is cropping artifact but I'll live with it
implicitWidth: 421
implicitHeight: 600
implicitWidth: root.clockWidth
implicitHeight: root.clockHeight
Image {
anchors.fill: parent
visible: clockBridge.ready
source: clockBridge.imageSource
fillMode: Image.PreserveAspectFit
asynchronous: true
cache: false
}
Item {
width: root.clockWidth
height: root.clockHeight
anchors.centerIn: parent
scale: Math.min(parent.width / width, parent.height / height)
Image {
anchors.fill: parent
source: Qt.resolvedUrl("assets/background.png")
smooth: true
mipmap: true
}
ClockHand {
imageSource: Qt.resolvedUrl("assets/hand_hour.png")
sourceWidth: 227
sourceHeight: 76
pivotFractionX: 0.126
pivotFractionY: 0.502
clockAngle: root.hourAngle
}
ClockHand {
imageSource: Qt.resolvedUrl("assets/hand_minute.png")
sourceWidth: 316
sourceHeight: 78
pivotFractionX: 0.101
pivotFractionY: 0.488
clockAngle: root.minuteAngle
}
PlasmaComponents.Label {
anchors.fill: parent
visible: !clockBridge.ready
text: clockBridge.errorString.length > 0 ? clockBridge.errorString : "Loading..."
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
wrapMode: Text.Wrap
}
}
// full representation (popup or desktop widget)
fullRepresentation: Item {
Layout.minimumWidth: Kirigami.Units.gridUnit * 12
Layout.minimumHeight: Kirigami.Units.gridUnit * 12
@@ -64,7 +133,6 @@ PlasmoidItem {
}
// compact representation (icon in panel)
compactRepresentation: Item {
Layout.minimumWidth: Kirigami.Units.gridUnit * 6
Layout.minimumHeight: Kirigami.Units.gridUnit * 6