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 artifacts
|
||||||
build/
|
build/
|
||||||
*.plasmoid
|
*.plasmoid
|
||||||
package/contents/ui/cppbridge/
|
|
||||||
|
|
||||||
# Temporary files
|
# Temporary files
|
||||||
*~
|
*~
|
||||||
|
|||||||
@@ -6,9 +6,6 @@ METADATA_FILE := $(PACKAGE_DIR)/metadata.json
|
|||||||
WIDGET_ID := $(shell grep -oP '"Id": "\K[^"]+' $(METADATA_FILE))
|
WIDGET_ID := $(shell grep -oP '"Id": "\K[^"]+' $(METADATA_FILE))
|
||||||
BUILD_DIR := build
|
BUILD_DIR := build
|
||||||
PLASMOID_FILE := $(WIDGET_ID).plasmoid
|
PLASMOID_FILE := $(WIDGET_ID).plasmoid
|
||||||
CPP_DIR := cpp
|
|
||||||
CPP_BUILD_DIR := $(BUILD_DIR)/cpp
|
|
||||||
CPP_MODULE_DIR := $(PACKAGE_DIR)/contents/ui/cppbridge
|
|
||||||
|
|
||||||
# Default target
|
# Default target
|
||||||
.PHONY: help
|
.PHONY: help
|
||||||
@@ -18,13 +15,11 @@ help:
|
|||||||
@echo "Usage:"
|
@echo "Usage:"
|
||||||
@echo " make dev Run widget in plasmoidviewer from local package (MODE=panel|desktop|hidpi)"
|
@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 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 install Install/upgrade widget in local Plasma package store"
|
||||||
@echo " make uninstall Remove widget from local system"
|
@echo " make uninstall Remove widget from local system"
|
||||||
@echo " make package Build .plasmoid file for distribution"
|
@echo " make package Build .plasmoid file for distribution"
|
||||||
@echo " make clean Remove build artifacts"
|
@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 lint Run non-mutating lint and formatting checks"
|
||||||
@echo " make hooks-install Configure Git to use versioned hooks from .githooks/"
|
@echo " make hooks-install Configure Git to use versioned hooks from .githooks/"
|
||||||
|
|
||||||
@@ -35,28 +30,11 @@ hooks-install:
|
|||||||
git config core.hooksPath .githooks
|
git config core.hooksPath .githooks
|
||||||
@echo "Configured 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
|
# Testing Targets
|
||||||
# Run plasmoidviewer from the local package directory.
|
# Run plasmoidviewer from the local package directory.
|
||||||
# Optional MODE values: panel, desktop, hidpi (omit MODE for default).
|
# Optional MODE values: panel, desktop, hidpi (omit MODE for default).
|
||||||
.PHONY: dev
|
.PHONY: dev
|
||||||
dev: cpp-build
|
dev:
|
||||||
@mode="$(MODE)"; \
|
@mode="$(MODE)"; \
|
||||||
case "$$mode" in \
|
case "$$mode" in \
|
||||||
""|default) \
|
""|default) \
|
||||||
@@ -75,7 +53,7 @@ dev: cpp-build
|
|||||||
# Development Tools
|
# Development Tools
|
||||||
# Start file watcher for iterative development and reload workflow.
|
# Start file watcher for iterative development and reload workflow.
|
||||||
.PHONY: watch
|
.PHONY: watch
|
||||||
watch: cpp-build
|
watch:
|
||||||
./scripts/watch.sh
|
./scripts/watch.sh
|
||||||
|
|
||||||
# Format all QML files in the package directory in-place.
|
# 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; }
|
@command -v qmlformat >/dev/null 2>&1 || { echo "Missing required tool: qmlformat"; exit 1; }
|
||||||
find $(PACKAGE_DIR) -name "*.qml" -type f -exec qmlformat -i {} +
|
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.
|
# Format JSON/YAML/Markdown files with prettier.
|
||||||
.PHONY: format-prettier
|
.PHONY: format-prettier
|
||||||
format-prettier:
|
format-prettier:
|
||||||
@@ -102,7 +74,7 @@ format-prettier:
|
|||||||
|
|
||||||
# Run all formatting passes.
|
# Run all formatting passes.
|
||||||
.PHONY: format
|
.PHONY: format
|
||||||
format: format-qml format-cpp format-prettier
|
format: format-qml format-prettier
|
||||||
|
|
||||||
# Linting
|
# Linting
|
||||||
# Check QML formatting without changing files.
|
# Check QML formatting without changing files.
|
||||||
@@ -119,16 +91,10 @@ lint-qml-format:
|
|||||||
|
|
||||||
# Run semantic QML lint checks.
|
# Run semantic QML lint checks.
|
||||||
.PHONY: lint-qml
|
.PHONY: lint-qml
|
||||||
lint-qml: cpp-build
|
lint-qml:
|
||||||
@command -v qmllint >/dev/null 2>&1 || { echo "Missing required tool: qmllint"; exit 1; }
|
@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
|
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.
|
# Check JSON/YAML/Markdown formatting without changing files.
|
||||||
.PHONY: lint-prettier
|
.PHONY: lint-prettier
|
||||||
lint-prettier:
|
lint-prettier:
|
||||||
@@ -141,12 +107,12 @@ lint-prettier:
|
|||||||
|
|
||||||
# Run all lint and formatting checks.
|
# Run all lint and formatting checks.
|
||||||
.PHONY: lint
|
.PHONY: lint
|
||||||
lint: lint-qml-format lint-qml lint-cpp lint-prettier
|
lint: lint-qml-format lint-qml lint-prettier
|
||||||
|
|
||||||
# Installation
|
# Installation
|
||||||
# Install/upgrade widget in local Plasma package store.
|
# Install/upgrade widget in local Plasma package store.
|
||||||
.PHONY: install
|
.PHONY: install
|
||||||
install: cpp-build
|
install:
|
||||||
@if kpackagetool6 --type Plasma/Applet --show $(WIDGET_ID) >/dev/null 2>&1; then \
|
@if kpackagetool6 --type Plasma/Applet --show $(WIDGET_ID) >/dev/null 2>&1; then \
|
||||||
echo "Upgrading existing package: $(WIDGET_ID)"; \
|
echo "Upgrading existing package: $(WIDGET_ID)"; \
|
||||||
kpackagetool6 --type Plasma/Applet --upgrade $(PACKAGE_DIR); \
|
kpackagetool6 --type Plasma/Applet --upgrade $(PACKAGE_DIR); \
|
||||||
@@ -157,7 +123,7 @@ install: cpp-build
|
|||||||
|
|
||||||
# Upgrade an already installed widget package with local changes.
|
# Upgrade an already installed widget package with local changes.
|
||||||
.PHONY: upgrade
|
.PHONY: upgrade
|
||||||
upgrade: cpp-build
|
upgrade:
|
||||||
kpackagetool6 --type Plasma/Applet --upgrade $(PACKAGE_DIR)
|
kpackagetool6 --type Plasma/Applet --upgrade $(PACKAGE_DIR)
|
||||||
|
|
||||||
# Remove installed widget package from local system.
|
# Remove installed widget package from local system.
|
||||||
@@ -173,7 +139,7 @@ uninstall:
|
|||||||
# Packaging
|
# Packaging
|
||||||
# Create distributable .plasmoid archive from current package contents.
|
# Create distributable .plasmoid archive from current package contents.
|
||||||
.PHONY: package
|
.PHONY: package
|
||||||
package: cpp-build
|
package:
|
||||||
mkdir -p $(BUILD_DIR)
|
mkdir -p $(BUILD_DIR)
|
||||||
rm -f $(BUILD_DIR)/$(PLASMOID_FILE)
|
rm -f $(BUILD_DIR)/$(PLASMOID_FILE)
|
||||||
|
|
||||||
@@ -186,4 +152,4 @@ package: cpp-build
|
|||||||
# Delete all generated build artifacts for a clean workspace.
|
# Delete all generated build artifacts for a clean workspace.
|
||||||
.PHONY: clean
|
.PHONY: clean
|
||||||
clean:
|
clean:
|
||||||
rm -rf $(BUILD_DIR) $(CPP_MODULE_DIR)
|
rm -rf $(BUILD_DIR)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# katoclock
|
# 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/)
|
[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
|
- clone the repo
|
||||||
- `make install` from repo root
|
- `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
|
||||||
import QtQuick.Layouts
|
import QtQuick.Layouts
|
||||||
import "cppbridge" as CppBridge
|
|
||||||
import org.kde.kirigami as Kirigami
|
import org.kde.kirigami as Kirigami
|
||||||
import org.kde.plasma.components as PlasmaComponents
|
|
||||||
import org.kde.plasma.core as PlasmaCore
|
import org.kde.plasma.core as PlasmaCore
|
||||||
import org.kde.plasma.plasmoid
|
import org.kde.plasma.plasmoid
|
||||||
|
|
||||||
PlasmoidItem {
|
PlasmoidItem {
|
||||||
id: root
|
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
|
width: Kirigami.Units.gridUnit * 20
|
||||||
height: Kirigami.Units.gridUnit * 20
|
height: Kirigami.Units.gridUnit * 20
|
||||||
Plasmoid.backgroundHints: PlasmaCore.Types.NoBackground
|
Plasmoid.backgroundHints: PlasmaCore.Types.NoBackground
|
||||||
toolTipMainText: "katoclock"
|
toolTipMainText: "katoclock"
|
||||||
Component.onCompleted: {
|
Component.onCompleted: refreshTime()
|
||||||
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);
|
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
id: minuteTimer
|
||||||
|
|
||||||
|
repeat: false
|
||||||
|
onTriggered: root.refreshTime()
|
||||||
}
|
}
|
||||||
|
|
||||||
CppBridge.ClockBridge {
|
component ClockHand: Item {
|
||||||
id: clockBridge
|
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 {
|
component ClockImageView: Item {
|
||||||
// asset image size -> 421x600, 1 is cropping artifact but I'll live with it
|
implicitWidth: root.clockWidth
|
||||||
implicitWidth: 421
|
implicitHeight: root.clockHeight
|
||||||
implicitHeight: 600
|
|
||||||
|
|
||||||
Image {
|
Item {
|
||||||
anchors.fill: parent
|
width: root.clockWidth
|
||||||
visible: clockBridge.ready
|
height: root.clockHeight
|
||||||
source: clockBridge.imageSource
|
anchors.centerIn: parent
|
||||||
fillMode: Image.PreserveAspectFit
|
scale: Math.min(parent.width / width, parent.height / height)
|
||||||
asynchronous: true
|
|
||||||
cache: false
|
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 {
|
fullRepresentation: Item {
|
||||||
Layout.minimumWidth: Kirigami.Units.gridUnit * 12
|
Layout.minimumWidth: Kirigami.Units.gridUnit * 12
|
||||||
Layout.minimumHeight: Kirigami.Units.gridUnit * 12
|
Layout.minimumHeight: Kirigami.Units.gridUnit * 12
|
||||||
@@ -64,7 +133,6 @@ PlasmoidItem {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// compact representation (icon in panel)
|
|
||||||
compactRepresentation: Item {
|
compactRepresentation: Item {
|
||||||
Layout.minimumWidth: Kirigami.Units.gridUnit * 6
|
Layout.minimumWidth: Kirigami.Units.gridUnit * 6
|
||||||
Layout.minimumHeight: Kirigami.Units.gridUnit * 6
|
Layout.minimumHeight: Kirigami.Units.gridUnit * 6
|
||||||
|
|||||||
Reference in New Issue
Block a user