refactor: move clock rendering to qml
This commit is contained in:
@@ -1,2 +0,0 @@
|
||||
BasedOnStyle: Google
|
||||
IndentWidth: 4
|
||||
@@ -15,7 +15,6 @@
|
||||
# Build artifacts
|
||||
build/
|
||||
*.plasmoid
|
||||
package/contents/ui/cppbridge/
|
||||
|
||||
# Temporary files
|
||||
*~
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
)
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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_;
|
||||
};
|
||||
@@ -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
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
};
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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_;
|
||||
};
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user