Compare commits

..
10 Commits
Author SHA1 Message Date
sebastian 45fc172fc8 feat: rename project to DualFloatingClock, add README, public release
Build and Release DualFloatingClock / build-linux (push) Failing after 32s
CI Build / build (macos-latest) (push) Canceled after 0s
CI Build / build (windows-latest) (push) Canceled after 0s
Build and Release DualFloatingClock / build-macos (push) Canceled after 0s
Build and Release DualFloatingClock / build-windows (push) Canceled after 0s
Build and Release DualFloatingClock / generate-changelog (push) Canceled after 0s
Build and Release DualFloatingClock / create-release (push) Canceled after 0s
- Full rename: CMake target, bundle, LaunchAgent label
  (com.dualfloatingclock.clock), QSettings org/app, tray tooltip,
  icns resource, build scripts, CI workflows, sample config
- Config XML: new root DualFloatingClockConfig; reader accepts legacy
  ClouckConfig root for painless migration of existing settings
- README rewritten: fork provenance (ivaquero/clouck), features, build,
  tests, usage
- Drop stale README-CN.md
2026-08-27 22:36:05 +03:00
sebastian db02df7ca2 feat: menu-bar-only app (no Dock icon) + macOS app icon
- NSApplicationActivationPolicyAccessory: app hidden from Dock and
  app switcher; lives only in the menu bar tray
- resources/clouck.icns: programmatic icon (green gradient rounded
  square, white dial, 10:10 hands), all sizes 16-1024 incl @2x
- CMake: MACOSX_BUNDLE_ICON_FILE wired, icns shipped in Resources/
2026-08-27 22:31:06 +03:00
sebastian d9008479c1 feat: tray-first UX, text outline, start-at-login toggle, font max 200
- All settings move to flat tray menu; right-click on clock disabled
- ClockLabel: custom QPainterPath text rendering with configurable
  glyph outline (BorderColor) — verified pixel-level offscreen
- Fixed glyph positioning bug (baseline math) caught by isolated test
- Window auto-fits content (adjustSize) — no more oversized clock
- Start at Login: checkable tray item, persisted in QSettings,
  LaunchAgent enable/disable (TDD: roundTrip test caught disable()
  returning false when plist already absent)
- Color dialog: hex/HTML input + 'transparent' + native picker fallback;
  opaque colors stored as #RRGGBB
- Border: glyph outline instead of box border; background still painted
  in paintEvent (QSS doesn't render with WA_TranslucentBackground on macOS)
- Removed window shadow (setHasShadow:NO), no bold, SF Pro Rounded
- Font size dialogs allow up to 200 (was 72)
- macOS: icon template 16pt, light/dark auto-adapt; LaunchAgent points
  to /Applications install
2026-08-19 22:41:33 +03:00
sebastian 6b9cb32534 feat: dual time zones, autostart, tray icon, config relocation, tests
- Dual clock (primary/secondary time zone) with IANA picker, persisted in XML
- AutoStartManager: macOS LaunchAgent (com.clouck.clock, RunAtLoad)
- Tray icon: template image, light/dark adaptation, toggle + quit menu
- ConfigManager: default path moved to QStandardPaths::AppConfigLocation
- Qt Test suite (ConfigManager, AutoStartManager) wired into CTest
- GitHub origin renamed to upstream; private Gitea repo as origin
2026-08-19 21:09:06 +03:00
ivaquero e59a674ba7 update .vscode 2026-05-27 16:59:00 +08:00
msaintms@outlook.com b5c43ba55e fix vscode 2026-05-15 20:47:19 +08:00
ivaquero 2e460fcb76 clean 2026-04-21 11:42:58 +08:00
ivaquero 1a20802dad ci: simplify 2026-04-18 14:28:42 +08:00
ivaquero 0bd7559961 doc: update readme 2026-04-18 14:23:19 +08:00
ivaquero 788ca03df8 v0.1.0 2026-04-18 14:21:04 +08:00
32 changed files with 1023 additions and 811 deletions
+1 -1
View File
@@ -52,7 +52,7 @@ Every push to main/master branches triggers CI builds to ensure code quality.
### Manual Release ### Manual Release
1. Go to Actions tab in GitHub 1. Go to Actions tab in GitHub
2. Select "Build and Release Clouck" 2. Select "Build and Release DualFloatingClock"
3. Click "Run workflow" 3. Click "Run workflow"
4. Enter version number (e.g., v1.0.0) 4. Enter version number (e.g., v1.0.0)
5. Click "Run workflow" 5. Click "Run workflow"
+2 -2
View File
@@ -50,5 +50,5 @@ jobs:
- name: Upload Build Artifact - name: Upload Build Artifact
uses: actions/upload-artifact@v7 uses: actions/upload-artifact@v7
with: with:
name: Clouck-${{ runner.os }}-cmake name: DualFloatingClock-${{ runner.os }}-cmake
path: build/Clouck* path: build/DualFloatingClock*
+54 -67
View File
@@ -1,4 +1,4 @@
name: Build and Release Clouck name: Build and Release DualFloatingClock
on: on:
push: push:
@@ -15,6 +15,10 @@ on:
required: true required: true
default: "v1.0.0" default: "v1.0.0"
permissions:
contents: write
pull-requests: read
jobs: jobs:
build-macos: build-macos:
runs-on: macos-latest runs-on: macos-latest
@@ -39,43 +43,43 @@ jobs:
ls -la build/ ls -la build/
# Look for the .app bundle or executable # Look for the .app bundle or executable
if [ -d "build/Clouck.app" ]; then if [ -d "build/DualFloatingClock.app" ]; then
echo "Found .app bundle" echo "Found .app bundle"
cp -r build/Clouck.app ./ cp -r build/DualFloatingClock.app ./
elif [ -f "build/Clouck" ]; then elif [ -f "build/DualFloatingClock" ]; then
echo "Found standalone executable" echo "Found standalone executable"
# Create app bundle # Create app bundle
mkdir -p Clouck.app/Contents/MacOS mkdir -p DualFloatingClock.app/Contents/MacOS
mkdir -p Clouck.app/Contents/Resources mkdir -p DualFloatingClock.app/Contents/Resources
cp build/Clouck Clouck.app/Contents/MacOS/ cp build/DualFloatingClock DualFloatingClock.app/Contents/MacOS/
else else
echo "Looking for executable in build directory..." echo "Looking for executable in build directory..."
find build/ -name "Clouck*" -type f find build/ -name "DualFloatingClock*" -type f
# Try to find and copy the first executable # Try to find and copy the first executable
EXECUTABLE=$(find build/ -name "Clouck*" -type f | head -1) EXECUTABLE=$(find build/ -name "DualFloatingClock*" -type f | head -1)
if [ -n "$EXECUTABLE" ]; then if [ -n "$EXECUTABLE" ]; then
echo "Found executable: $EXECUTABLE" echo "Found executable: $EXECUTABLE"
mkdir -p Clouck.app/Contents/MacOS mkdir -p DualFloatingClock.app/Contents/MacOS
mkdir -p Clouck.app/Contents/Resources mkdir -p DualFloatingClock.app/Contents/Resources
cp "$EXECUTABLE" Clouck.app/Contents/MacOS/ cp "$EXECUTABLE" DualFloatingClock.app/Contents/MacOS/
else else
echo "ERROR: No Clouck executable found!" echo "ERROR: No DualFloatingClock executable found!"
exit 1 exit 1
fi fi
fi fi
# Create Info.plist # Create Info.plist
cat > Clouck.app/Contents/Info.plist << EOF cat > DualFloatingClock.app/Contents/Info.plist << EOF
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"> <plist version="1.0">
<dict> <dict>
<key>CFBundleExecutable</key> <key>CFBundleExecutable</key>
<string>Clouck</string> <string>DualFloatingClock</string>
<key>CFBundleIdentifier</key> <key>CFBundleIdentifier</key>
<string>com.clouck.app</string> <string>com.clouck.app</string>
<key>CFBundleName</key> <key>CFBundleName</key>
<string>Clouck</string> <string>DualFloatingClock</string>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>1.0.0</string> <string>1.0.0</string>
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
@@ -88,18 +92,18 @@ jobs:
- name: Deploy Qt Dependencies - name: Deploy Qt Dependencies
run: | run: |
if [ -d "Clouck.app" ]; then if [ -d "DualFloatingClock.app" ]; then
macdeployqt Clouck.app -dmg || hdiutil create -volname "Clouck" -srcfolder Clouck.app -ov -format UDZO Clouck.dmg macdeployqt DualFloatingClock.app -dmg || hdiutil create -volname "DualFloatingClock" -srcfolder DualFloatingClock.app -ov -format UDZO DualFloatingClock.dmg
else else
echo "ERROR: Clouck.app not found!" echo "ERROR: DualFloatingClock.app not found!"
exit 1 exit 1
fi fi
- name: Upload macOS Artifact - name: Upload macOS Artifact
uses: actions/upload-artifact@v7 uses: actions/upload-artifact@v7
with: with:
name: Clouck-macos name: DualFloatingClock-macos
path: Clouck.dmg path: DualFloatingClock.dmg
build-windows: build-windows:
runs-on: windows-latest runs-on: windows-latest
@@ -123,17 +127,17 @@ jobs:
# Check what was actually built # Check what was actually built
Get-ChildItem -Path "build" -Recurse -Name Get-ChildItem -Path "build" -Recurse -Name
New-Item -ItemType Directory -Path Clouck-windows -Force New-Item -ItemType Directory -Path DualFloatingClock-win -Force
Copy-Item "build/Release/Clouck.exe" -Destination "Clouck-windows/" -ErrorAction SilentlyContinue Copy-Item "build/Release/DualFloatingClock.exe" -Destination "DualFloatingClock-win/" -ErrorAction SilentlyContinue
Copy-Item "build/Release/*.dll" -Destination "Clouck-windows/" -ErrorAction SilentlyContinue Copy-Item "build/Release/*.dll" -Destination "DualFloatingClock-win/" -ErrorAction SilentlyContinue
windeployqt --release Clouck-windows/Clouck.exe windeployqt --release DualFloatingClock-win/DualFloatingClock.exe
Compress-Archive -Path Clouck-windows -DestinationPath Clouck-windows.zip Compress-Archive -Path DualFloatingClock-win -DestinationPath DualFloatingClock-win.zip
- name: Upload Windows Artifact - name: Upload Windows Artifact
uses: actions/upload-artifact@v7 uses: actions/upload-artifact@v7
with: with:
name: Clouck-windows name: DualFloatingClock-win
path: Clouck-windows.zip path: DualFloatingClock-win.zip
build-linux: build-linux:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -165,32 +169,35 @@ jobs:
ls -la build/ ls -la build/
# Look for the executable # Look for the executable
EXECUTABLE=$(find build/ -name "Clouck" -type f -executable | head -1) EXECUTABLE=$(find build/ -name "DualFloatingClock" -type f -executable | head -1)
if [ -n "$EXECUTABLE" ]; then if [ -n "$EXECUTABLE" ]; then
echo "Found executable: $EXECUTABLE" echo "Found executable: $EXECUTABLE"
mkdir -p Clouck-linux mkdir -p DualFloatingClock-linux
cp "$EXECUTABLE" Clouck-linux/ cp "$EXECUTABLE" DualFloatingClock-linux/
else else
echo "ERROR: No Clouck executable found!" echo "ERROR: No DualFloatingClock executable found!"
exit 1 exit 1
fi fi
# Copy any Qt libraries # Copy any Qt libraries
find build/ -name "*.so*" -exec cp {} Clouck-linux/ \; 2>/dev/null || true find build/ -name "*.so*" -exec cp {} DualFloatingClock-linux/ \; 2>/dev/null || true
# Create tar.gz for distribution # Create tar.gz for distribution
tar -czf Clouck-linux.tar.gz Clouck-linux/ tar -czf DualFloatingClock-linux.tar.gz DualFloatingClock-linux/
- name: Upload Linux Artifact - name: Upload Linux Artifact
uses: actions/upload-artifact@v7 uses: actions/upload-artifact@v7
with: with:
name: Clouck-linux name: DualFloatingClock-linux
path: Clouck-linux.tar.gz path: DualFloatingClock-linux.tar.gz
generate-changelog: generate-changelog:
needs: [ build-macos, build-windows, build-linux ] needs: [ build-macos, build-windows, build-linux ]
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: always() if: always()
permissions:
contents: read
pull-requests: read
outputs: outputs:
changelog: ${{ steps.changelog.outputs.changelog }} changelog: ${{ steps.changelog.outputs.changelog }}
version: ${{ steps.version.outputs.version }} version: ${{ steps.version.outputs.version }}
@@ -220,15 +227,18 @@ jobs:
echo "$CHANGELOG" >> $GITHUB_OUTPUT echo "$CHANGELOG" >> $GITHUB_OUTPUT
echo "" >> $GITHUB_OUTPUT echo "" >> $GITHUB_OUTPUT
echo "## Downloads" >> $GITHUB_OUTPUT echo "## Downloads" >> $GITHUB_OUTPUT
echo "- **macOS**: Clouck.dmg" >> $GITHUB_OUTPUT echo "- **macOS**: DualFloatingClock.dmg" >> $GITHUB_OUTPUT
echo "- **Windows**: Clouck-windows.zip" >> $GITHUB_OUTPUT echo "- **Windows**: DualFloatingClock-win.zip" >> $GITHUB_OUTPUT
echo "- **Linux**: Clouck-linux.tar.gz" >> $GITHUB_OUTPUT echo "- **Linux**: DualFloatingClock-linux.tar.gz" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT echo "EOF" >> $GITHUB_OUTPUT
create-release: create-release:
needs: [ build-macos, build-windows, build-linux, generate-changelog ] needs: [ build-macos, build-windows, build-linux, generate-changelog ]
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: always() if: always()
permissions:
contents: write
pull-requests: read
steps: steps:
- name: Download All Artifacts - name: Download All Artifacts
@@ -242,40 +252,17 @@ jobs:
- name: Create Release - name: Create Release
uses: softprops/action-gh-release@v3 uses: softprops/action-gh-release@v3
with: with:
name: Clouck ${{ needs.generate-changelog.outputs.version }} name: DualFloatingClock ${{ needs.generate-changelog.outputs.version }}
body: | body: |
Cross-platform clock widget application Cross-platform clock widget application
${{ needs.generate-changelog.outputs.changelog }} ${{ needs.generate-changelog.outputs.changelog }}
## Installation
### macOS
1. Download Clouck.dmg
2. Open the DMG file
3. Drag Clouck.app to Applications folder
### Windows
1. Download Clouck-windows.zip
2. Extract the ZIP file
3. Run Clouck.exe
### Linux
1. Download Clouck-linux.tar.gz
2. Extract the tar.gz file
3. Run the Clouck executable
### Features
- Always-on-top functionality across all platforms
- Fullscreen mode compatibility
- Resizable interface
- Context menu with customization options
- Native platform integration
files: | files: |
artifacts/Clouck-macos/Clouck.dmg artifacts/DualFloatingClock-macos/DualFloatingClock.dmg
artifacts/Clouck-windows/Clouck-windows.zip artifacts/DualFloatingClock-win/DualFloatingClock-win.zip
artifacts/Clouck-linux/Clouck-linux.tar.gz artifacts/DualFloatingClock-linux/DualFloatingClock-linux.tar.gz
draft: false draft: false
prerelease: false prerelease: false
generate_release_notes: true generate_release_notes: true
env: token: ${{ secrets.GITHUB_TOKEN }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+8 -1
View File
@@ -88,5 +88,12 @@ Icon
.cmake .cmake
.qtc_clangd .qtc_clangd
.qmake.stash .qmake.stash
build
*.dmg *.dmg
build
test*
!tests/
!tests/*.cpp
# AW
.aw/
CLAUDE.md
+9 -9
View File
@@ -5,20 +5,20 @@
"includePath": [ "includePath": [
"${workspaceFolder}/**", "${workspaceFolder}/**",
"/opt/homebrew/opt/qt/include", "/opt/homebrew/opt/qt/include",
"/opt/homebrew/Cellar/qtbase/6.11.0/lib/QtCore.framework/Headers", "/opt/homebrew/Cellar/qtbase/6.11.1/lib/QtCore.framework/Headers",
"/opt/homebrew/Cellar/qtbase/6.11.0/lib/QtGui.framework/Headers", "/opt/homebrew/Cellar/qtbase/6.11.1/lib/QtGui.framework/Headers",
"/opt/homebrew/Cellar/qtbase/6.11.0/lib/QtWidgets.framework/Headers", "/opt/homebrew/Cellar/qtbase/6.11.1/lib/QtWidgets.framework/Headers",
"/opt/homebrew/Cellar/qttools/6.11.0/include/QtUiPlugin", "/opt/homebrew/Cellar/qttools/6.11.1/include/QtUiPlugin",
"/opt/homebrew/Cellar/qtdeclarative/6.11.0/include/QtQmlDebug" "/opt/homebrew/Cellar/qtdeclarative/6.11.1/include/QtQmlDebug"
], ],
"defines": [], "defines": [],
"compilerPath": "/usr/bin/clang++", "compilerPath": "/usr/bin/clang++",
"cStandard": "c17", "cStandard": "c17",
"cppStandard": "c++17", "cppStandard": "c++20",
"intelliSenseMode": "macos-clang-arm64", "intelliSenseMode": "macos-clang-arm64",
"configurationProvider": "kylinideteam.cmake-intellisence", "configurationProvider": "kylinideteam.cmake-intellisence",
"macFrameworkPath": [ "macFrameworkPath": [
"/opt/homebrew/Cellar/qtbase/6.11.0/lib", "/opt/homebrew/Cellar/qtbase/6.11.1/lib",
"/System/Library/Frameworks" "/System/Library/Frameworks"
] ]
}, },
@@ -27,7 +27,7 @@
"includePath": [ "includePath": [
"${workspaceFolder}/**", "${workspaceFolder}/**",
"${env:SCOOP}\\apps\\msys2-cn\\current\\ucrt64\\include", "${env:SCOOP}\\apps\\msys2-cn\\current\\ucrt64\\include",
"${env:SCOOP}\\apps\\msys2-cn\\current\\ucrt64\\include\\qt" "${env:SCOOP}\\apps\\msys2-cn\\current\\ucrt64\\include\\qt6"
], ],
"defines": [ "defines": [
"_DEBUG", "_DEBUG",
@@ -36,7 +36,7 @@
], ],
"compilerPath": "${env:SCOOP}\\apps\\msys2-cn\\current\\ucrt64\\bin\\g++.exe", "compilerPath": "${env:SCOOP}\\apps\\msys2-cn\\current\\ucrt64\\bin\\g++.exe",
"cStandard": "c17", "cStandard": "c17",
"cppStandard": "c++17", "cppStandard": "c++20",
"intelliSenseMode": "windows-gcc-x64", "intelliSenseMode": "windows-gcc-x64",
"configurationProvider": "kylinideteam.cmake-intellisence" "configurationProvider": "kylinideteam.cmake-intellisence"
} }
+46 -10
View File
@@ -1,9 +1,11 @@
cmake_minimum_required(VERSION 3.16) cmake_minimum_required(VERSION 3.16)
project(Clouck) project(DualFloatingClock)
set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_STANDARD_REQUIRED ON)
include(CTest)
# Find Qt6 # Find Qt6
find_package(Qt6 REQUIRED COMPONENTS Core Widgets) find_package(Qt6 REQUIRED COMPONENTS Core Widgets)
@@ -23,12 +25,14 @@ qt6_standard_project_setup()
# Sources # Sources
set(SOURCES set(SOURCES
main.cpp main.cpp
autostart_manager.cpp
clock_widget.cpp clock_widget.cpp
config_manager.cpp config_manager.cpp
window_helper.cpp window_helper.cpp
) )
set(HEADERS set(HEADERS
autostart_manager.h
clock_widget.h clock_widget.h
config_manager.h config_manager.h
window_helper.h window_helper.h
@@ -43,33 +47,65 @@ elseif(APPLE)
list(APPEND HEADERS window_helper_macos.h) list(APPEND HEADERS window_helper_macos.h)
endif() endif()
# macOS app icon
if(APPLE)
set(MACOSX_BUNDLE_ICON_FILE dualfloatingclock.icns)
set(APP_ICON_MACOSX ${CMAKE_CURRENT_SOURCE_DIR}/resources/dualfloatingclock.icns)
set_source_files_properties(${APP_ICON_MACOSX} PROPERTIES MACOSX_PACKAGE_LOCATION "Resources")
endif()
# Create executable # Create executable
qt6_add_executable(Clouck WIN32 MACOSX_BUNDLE qt6_add_executable(DualFloatingClock WIN32 MACOSX_BUNDLE
${SOURCES} ${SOURCES}
${HEADERS} ${HEADERS}
${APP_ICON_MACOSX}
) )
# Platform-specific sources and libraries # Platform-specific sources and libraries
if(APPLE) if(APPLE)
target_sources(Clouck PRIVATE window_helper_macos.mm) target_sources(DualFloatingClock PRIVATE window_helper_macos.mm)
target_link_libraries(Clouck PRIVATE "-framework AppKit" "-framework CoreGraphics") target_link_libraries(DualFloatingClock PRIVATE "-framework AppKit" "-framework CoreGraphics")
endif() endif()
if(WIN32) if(WIN32)
target_sources(Clouck PRIVATE window_helper_windows.cpp) target_sources(DualFloatingClock PRIVATE window_helper_windows.cpp)
target_link_libraries(Clouck PRIVATE user32 dwmapi) target_link_libraries(DualFloatingClock PRIVATE user32 dwmapi)
endif() endif()
if(UNIX AND NOT APPLE) if(UNIX AND NOT APPLE)
target_sources(Clouck PRIVATE window_helper_linux.cpp) target_sources(DualFloatingClock PRIVATE window_helper_linux.cpp)
target_link_libraries(Clouck PRIVATE ${X11_LIBRARIES} Xfixes) target_link_libraries(DualFloatingClock PRIVATE ${X11_LIBRARIES} Xfixes)
endif() endif()
# Link Qt libraries # Link Qt libraries
target_link_libraries(Clouck PRIVATE Qt6::Core Qt6::Widgets) target_link_libraries(DualFloatingClock PRIVATE Qt6::Core Qt6::Widgets)
# Set up install # Set up install
install(TARGETS Clouck install(TARGETS DualFloatingClock
RUNTIME DESTINATION bin RUNTIME DESTINATION bin
BUNDLE DESTINATION bin BUNDLE DESTINATION bin
) )
if(BUILD_TESTING)
find_package(Qt6 REQUIRED COMPONENTS Test)
qt6_add_executable(ConfigManagerTest
tests/config_manager_test.cpp
config_manager.cpp
config_manager.h
)
target_include_directories(ConfigManagerTest PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(ConfigManagerTest PRIVATE Qt6::Core Qt6::Gui Qt6::Test)
add_test(NAME ConfigManagerTest COMMAND ConfigManagerTest)
qt6_add_executable(AutoStartManagerTest
tests/autostart_manager_test.cpp
autostart_manager.cpp
autostart_manager.h
)
target_include_directories(AutoStartManagerTest PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(AutoStartManagerTest PRIVATE Qt6::Core Qt6::Test)
add_test(NAME AutoStartManagerTest COMMAND AutoStartManagerTest)
endif()
+1 -1
View File
@@ -1,7 +1,7 @@
# Created by and for Qt Creator This file was created for editing the project sources only. # Created by and for Qt Creator This file was created for editing the project sources only.
# You may attempt to use it for building too, by modifying this file here. # You may attempt to use it for building too, by modifying this file here.
TARGET = Clouck TARGET = DualFloatingClock
QT = core gui widgets QT = core gui widgets
-123
View File
@@ -1,123 +0,0 @@
# Clouck 浮动时钟
## 功能特性
**已实现的功能:**
- **无边框窗口** - 简洁的浮动时钟界面
- **拖拽移动** - 鼠标左键拖拽移动窗口位置
- **右键菜单** - 丰富的自定义选项
- **总在最前** - 始终显示在其他窗口之上
- **透明背景** - 支持半透明背景效果
- **自定义样式** - 可调整字体颜色、背景颜色、字体大小
- **设置持久化** - 自动保存和恢复用户设置
- **跨平台支持** - 基于Qt6,支持 macOS/Linux/Windows
## 使用方法
### 基本操作
- **移动时钟**:鼠标左键点击并拖拽
- **显示菜单**:右键点击时钟
- **退出应用**:右键菜单选择"退出"
### 自定义设置
- **字体颜色**:右键菜单 → 设置字体颜色
- **背景颜色**:右键菜单 → 设置背景颜色
- **字体大小**:右键菜单 → 设置字体大小
- **总在最前**:右键菜单 → 勾选/取消"总在最前"
- **重置设置**:右键菜单 → 重置设置
### 构建和运行
#### 快速构建(推荐):
```bash
# 一键构建
./build.sh
# 运行应用
./build/Clouck.app/Contents/MacOS/Clouck # macOS
./build/Clouck # Linux
```
#### 手动构建:
```bash
# 清理旧的构建文件
./clean.sh
# 生成Makefile
qmake Clouck.pro
# 编译
make
# 运行
./build/Clouck.app/Contents/MacOS/Clouck # macOS
./build/Clouck # Linux
```
#### 使用CMake构建:
```bash
mkdir build && cd build
cmake ..
make
./Clouck
```
### 项目结构
```text
qClouck/
├── build/ # 构建输出目录
│ ├── Clouck.app/ # macOS应用包
│ ├── *.o # 目标文件
│ ├── moc_*.cpp # Qt元对象编译器输出
│ └── Makefile # 构建文件
├── src/ # 源代码(建议)
├── build.sh # 一键构建脚本
├── clean.sh # 清理脚本
├── Clouck.pro # qmake项目文件
├── CMakeLists.txt # CMake项目文件
└── README.md
```
## 技术特点
- **Qt6框架**:使用最新的Qt6库
- **透明窗口**:支持RGBA透明背景
- **定时器更新**:每秒更新时钟显示
- **设置管理**:使用QSettings持久化用户偏好
- **事件处理**:自定义鼠标事件实现拖拽功能
## 开发说明
### 构建输出管理
- 所有编译生成的文件(.o, moc_*.cpp, Makefile等)都会输出到`build/`目录
- 可执行文件位于`build/Clouck.app/Contents/MacOS/Clouck`macOS)或`build/Clouck`Linux
- 使用`./clean.sh`可以快速清理所有构建文件
- 使用`./build.sh`可以进行完整的一键构建
### 自定义构建配置
可以在`Clouck.pro`中修改以下配置:
- `DESTDIR`:可执行文件输出目录
- `OBJECTS_DIR`:目标文件输出目录
- `MOC_DIR`:元对象编译器输出目录
## 更新日志
### v1.0.0 (2026-04-18)
- ✨ 初始版本发布
- ✨ 基础时钟功能
- ✨ 拖拽移动支持
- ✨ 右键菜单功能
- ✨ 样式自定义
- ✨ 设置持久化
- ✨ 构建输出目录管理
+39 -147
View File
@@ -1,169 +1,61 @@
# Clouck # DualFloatingClock
![Build Qt](https://github.com/ivaquero/clouck/workflows/Build%20Qt%20Application/badge.svg) A menu-bar-first, dual-timezone floating clock for macOS, Windows and Linux — built with Qt6.
![Release](https://github.com/ivaquero/clouck/workflows/Build%20and%20Release%20Clouck/badge.svg)
![code size](https://img.shields.io/github/languages/code-size/ivaquero/clouck.svg)
![repo size](https://img.shields.io/github/repo-size/ivaquero/clouck.svg)
This project aims to build a cross-platform floating clock based on Qt6. > **Fork provenance:** this project is a fork of [clouck](https://github.com/ivaquero/clouck)
> by [ivaquero](https://github.com/ivaquero) — a cross-platform floating clock.
> DualFloatingClock extends it with dual time zones, a flat tray menu, glyph outlines,
> per-element font sizing and a menu-bar-only presence (no Dock icon on macOS).
## Motivation ## What it does
When coding in full screen mode, I often forget the time. A borderless, always-on-top clock that shows **two configurable time zones**
When on macOS / Linux, the clock on the menu bar is too small to be noticeable, so is the clock on Windows. side by side — each with its own time and city name — plus a menu bar tray icon
for quick access to all settings. Designed for people working across time zones
who want the second (or third) clock glanceable, not hidden in world-clock widgets.
![clock](doc/clock.png) ## Features
## ✨ Features - **Dual time zones** — pick any IANA zone for each clock (`Europe/Bucharest`, `America/Los_Angeles`, …)
- **Floating clock** — frameless, always-on-top, drag anywhere, resizable from the corner; visible even in fullscreen spaces (macOS)
- **Menu-bar tray** — icon adapts to light/dark mode; flat context menu with every setting (no submenus, no clicking on the clock itself)
- **Text outline** — configurable glyph outline (color) under the text fill, so the clock stays readable on any wallpaper
- **Per-element fonts** — separate font sizes for the time and the city name (up to 200pt)
- **Colors via HTML/hex** — type `#00ff00`, `#80000000` (with alpha) or `transparent`; falls back to the native color picker
- **Start at Login** — toggle in the tray menu (macOS LaunchAgent)
- **Menu-bar-only on macOS** — no Dock icon, no ⌘Tab entry; lives in the menu bar
- **XML config** — persisted in the OS config location (e.g. `~/Library/Preferences/DualFloatingClock/DualFloatingClock/config.xml`)
- Cross Platform ## Build
- [x] MacOS
- [x] Windows
- [ ] Linux
- [x] **Main Window**
- [x] Always On Top
- [x] Frameless
- [x] Drag and Move
- [x] Resizable
- [x] Show in Fullscreen Mode
- [x] **Style**
- [x] Right Click Menu
- [x] Set Background Color
- [ ] Transparent Background
- [x] Set Font Color
- [x] Set FontSize
- [x] **Tweakable** (XML)
- [x] Read Settings
- [x] Write Settings
- [x] Reset Settings
- [ ] **Time Zone**
- [ ] Select Time Zone
## Quick Start Requires Qt6 (Core, Widgets, Test) and CMake ≥ 3.16.
### Build and Run
#### Quick Build (Recommended)
```bash ```bash
# One-click build cmake -S . -B build
./build.sh cmake --build build
# Run application # macOS — run
./build/flock.app/Contents/MacOS/flock # macOS open build/DualFloatingClock.app
./build/flock # Linux # install to /Applications
cp -R build/DualFloatingClock.app /Applications/
``` ```
#### Manual Build qmake is also supported (legacy): see `build.sh` / `DualFloatingClock.pro`.
## Tests
```bash ```bash
# Clean build files cd build && ctest
./clean.sh
# Generate Makefile
qmake flock.pro
# Build
make
# Run
./build/flock.app/Contents/MacOS/flock # macOS
./build/flock # Linux
``` ```
#### Using CMake Qt Test suites: `ConfigManagerTest` (XML round-trip, default path), `AutoStartManagerTest` (LaunchAgent contents, enable/disable round-trip).
```bash ## Usage
mkdir build && cd build
cmake ..
make
./flock
```
## 🚀 GitHub Actions - **Move**: drag the clock · **Resize**: drag the bottom-right corner
- **Settings**: right-click the **tray icon** (the clock itself has no menu — dragging-friendly)
This project uses GitHub Actions for automated building and releasing: - Colors accept `#RRGGBB`, `#AARRGGBB` or `transparent`
### CI/CD Workflows
- **CI Build**: Validates builds on all platforms for every push/PR
- **Build Qt**: Comprehensive builds using both qmake and CMake
- **Release**: Creates distributable packages for tagged releases
### Download Pre-built Binaries
Visit the [Releases](https://github.com/ivaquero/clouck/releases) page to download pre-built binaries for:
- **macOS**: DMG installer with native app bundle
- **Windows**: ZIP package with all dependencies
- **Linux**: TAR.GZ package with shared libraries
### Creating a Release
1. Tag a new version:
```bash
git tag v1.0.0
git push origin v1.0.0
```
2. GitHub Actions will automatically build and create a release with binaries for all platforms
### Configuration
The application now uses XML configuration files instead of QSettings. Configuration is stored in `config.xml` with the following options:
- **FontColor**: Font color in hex format
- **BackgroundColor**: Background color with alpha transparency
- **FontSize**: Font size in points (8-72)
- **AlwaysOnTop**: Window always stays on top
- **WindowPosition**: Window position coordinates
The configuration file is automatically created when you first run the application and make changes to settings.
### Usage
- **Move**: Left-click and drag
- **Menu**: Right-click for context menu
- **Customize**: Change colors, font size, always-on-top behavior
- **Configuration**: Settings are saved to `config.xml` and persist between sessions
### Project Structure
```text
qflock/
├── build/ # Build output directory
│ ├── flock.app/ # macOS application bundle
│ ├── *.o # Object files
│ ├── moc_*.cpp # Qt meta-object compiler output
│ └── Makefile # Build file
├── config.xml # XML configuration file
├── build.sh # One-click build script
├── clean.sh # Clean script
├── flock.pro # qmake project file
├── CMakeLists.txt # CMake project file
└── README.md
```
### Build Output Management
- All compilation files (.o, moc_*.cpp, Makefile, etc.) are output to `build/` directory
- Executable files are located at `build/flock.app/Contents/MacOS/flock` (macOS) or `build/flock` (Linux)
- Use `./clean.sh` to quickly clean all build files
- Use `./build.sh` for complete one-click build
### Next Steps
- Time zone selection
- Alarm functionality
- Enhanced UI/UX
## Requirements
- Qt6 (Core, Widgets)
- C++17 compatible compiler
- CMake 3.16+ (optional, for CMake build)
## License ## License
See LICENSE file for details. Same as upstream clouck — see [LICENSE](LICENSE).
+112
View File
@@ -0,0 +1,112 @@
#include "autostart_manager.h"
#include <QCoreApplication>
#include <QDir>
#include <QFileInfo>
#include <QSaveFile>
#include <QStandardPaths>
#include <QXmlStreamWriter>
namespace
{
const QString launchAgentLabel = QStringLiteral("com.dualfloatingclock.clock");
}
bool AutoStartManager::enableForCurrentApplication()
{
#ifdef Q_OS_MAC
return enableForPath(QCoreApplication::applicationFilePath());
#else
return false;
#endif
}
bool AutoStartManager::enableForPath(const QString &executablePath)
{
#ifdef Q_OS_MAC
const QString agentPath = launchAgentPath();
if (agentPath.isEmpty())
{
return false;
}
const QFileInfo agentFileInfo(agentPath);
if (!QDir().mkpath(agentFileInfo.absolutePath()))
{
return false;
}
QSaveFile agentFile(agentPath);
if (!agentFile.open(QIODevice::WriteOnly | QIODevice::Text))
{
return false;
}
agentFile.write(launchAgentContents(executablePath).toUtf8());
return agentFile.commit();
#else
Q_UNUSED(executablePath);
return false;
#endif
}
bool AutoStartManager::disable()
{
#ifdef Q_OS_MAC
const QString agentPath = launchAgentPath();
if (agentPath.isEmpty() || !QFile::exists(agentPath))
{
return true; // already gone — desired state reached
}
return QFile::remove(agentPath);
#else
return false;
#endif
}
bool AutoStartManager::isEnabled()
{
#ifdef Q_OS_MAC
return QFile::exists(launchAgentPath());
#else
return false;
#endif
}
QString AutoStartManager::launchAgentContents(const QString &executablePath)
{
QString contents;
QXmlStreamWriter writer(&contents);
writer.setAutoFormatting(true);
writer.writeStartDocument();
writer.writeDTD("<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">");
writer.writeStartElement("plist");
writer.writeAttribute("version", "1.0");
writer.writeStartElement("dict");
writer.writeTextElement("key", "Label");
writer.writeTextElement("string", launchAgentLabel);
writer.writeTextElement("key", "ProgramArguments");
writer.writeStartElement("array");
writer.writeTextElement("string", executablePath);
writer.writeEndElement();
writer.writeTextElement("key", "RunAtLoad");
writer.writeEmptyElement("true");
writer.writeEndElement();
writer.writeEndElement();
writer.writeEndDocument();
return contents;
}
QString AutoStartManager::launchAgentPath()
{
const QString homePath = QStandardPaths::writableLocation(QStandardPaths::HomeLocation);
if (homePath.isEmpty())
{
return QString();
}
return QDir(homePath).filePath("Library/LaunchAgents/" + launchAgentLabel + ".plist");
}
+17
View File
@@ -0,0 +1,17 @@
#ifndef AUTOSTART_MANAGER_H
#define AUTOSTART_MANAGER_H
#include <QString>
class AutoStartManager
{
public:
static bool enableForCurrentApplication();
static bool enableForPath(const QString &executablePath);
static bool disable();
static bool isEnabled();
static QString launchAgentContents(const QString &executablePath);
static QString launchAgentPath();
};
#endif // AUTOSTART_MANAGER_H
+5 -5
View File
@@ -1,7 +1,7 @@
#!/bin/bash #!/bin/bash
# Build script # Build script
echo "Starting QFlock build..." echo "Starting DualFloatingClock build..."
# Create build directory if it doesn't exist # Create build directory if it doesn't exist
mkdir -p build mkdir -p build
@@ -12,18 +12,18 @@ rm -rf build/*
# Generate Makefile using qmake # Generate Makefile using qmake
echo "Generating Makefile..." echo "Generating Makefile..."
qmake Clouck.pro qmake DualFloatingClock.pro
# Build project # Build project
echo "Starting compilation..." echo "Starting compilation..."
make make
# Check build result # Check build result
if [ -f "build/Clouck.app/Contents/MacOS/Clouck" ] || [ -f "build/Clouck" ]; then if [ -f "build/DualFloatingClock.app/Contents/MacOS/DualFloatingClock" ] || [ -f "build/DualFloatingClock" ]; then
echo "Build successful!" echo "Build successful!"
echo "Executable location:" echo "Executable location:"
[ -f "build/Clouck.app/Contents/MacOS/Clouck" ] && echo " - build/Clouck.app/Contents/MacOS/Clouck (macOS)" [ -f "build/DualFloatingClock.app/Contents/MacOS/DualFloatingClock" ] && echo " - build/DualFloatingClock.app/Contents/MacOS/DualFloatingClock (macOS)"
[ -f "build/Clouck" ] && echo " - build/Clouck (Linux)" [ -f "build/DualFloatingClock" ] && echo " - build/DualFloatingClock (Linux)"
else else
echo "Build failed!" echo "Build failed!"
exit 1 exit 1
+8 -8
View File
@@ -1,7 +1,7 @@
#!/bin/bash #!/bin/bash
# Cross-platform build script for Clouck # Cross-platform build script for DualFloatingClock
echo "Building Clouck - Cross-platform Clock Application" echo "Building DualFloatingClock - Cross-platform Clock Application"
echo "==================================================" echo "=================================================="
# Detect operating system # Detect operating system
@@ -49,19 +49,19 @@ echo ""
echo "Executable location:" echo "Executable location:"
if [ "$OS" == "macOS" ]; then if [ "$OS" == "macOS" ]; then
echo " - build/Clouck.app/Contents/MacOS/Clouck (macOS)" echo " - build/DualFloatingClock.app/Contents/MacOS/DualFloatingClock (macOS)"
elif [ "$OS" == "Windows" ]; then elif [ "$OS" == "Windows" ]; then
echo " - build/Release/Clouck.exe (Windows)" echo " - build/Release/DualFloatingClock.exe (Windows)"
elif [ "$OS" == "Linux" ]; then elif [ "$OS" == "Linux" ]; then
echo " - build/Clouck (Linux)" echo " - build/DualFloatingClock (Linux)"
fi fi
echo "" echo ""
echo "To run the application:" echo "To run the application:"
if [ "$OS" == "macOS" ]; then if [ "$OS" == "macOS" ]; then
echo " ./build/Clouck.app/Contents/MacOS/Clouck" echo " ./build/DualFloatingClock.app/Contents/MacOS/DualFloatingClock"
elif [ "$OS" == "Windows" ]; then elif [ "$OS" == "Windows" ]; then
echo " ./build/Release/Clouck.exe" echo " ./build/Release/DualFloatingClock.exe"
elif [ "$OS" == "Linux" ]; then elif [ "$OS" == "Linux" ]; then
echo " ./build/Clouck" echo " ./build/DualFloatingClock"
fi fi
+3 -3
View File
@@ -10,9 +10,9 @@ if [ -d "build" ]; then
fi fi
# Delete .app bundle # Delete .app bundle
if [ -d "Clouck.app" ]; then if [ -d "DualFloatingClock.app" ]; then
rm -rf Clouck.app rm -rf DualFloatingClock.app
echo "Clouck.app deleted" echo "DualFloatingClock.app deleted"
fi fi
echo "Cleanup complete!" echo "Cleanup complete!"
+338 -30
View File
@@ -2,12 +2,77 @@
#include "config_manager.h" #include "config_manager.h"
#include "window_helper.h" #include "window_helper.h"
#include <QVBoxLayout> #include <QVBoxLayout>
#include <QTime> #include <QDateTime>
#include <QColorDialog> #include <QColorDialog>
#include <QFontDialog> #include <QFontDialog>
#include <QInputDialog> #include <QInputDialog>
#include <QMessageBox> #include <QMessageBox>
#include <QApplication> #include <QApplication>
#include <QScreen>
#include <QTimeZone>
#include <QLineEdit>
#include <QPainter>
#include <QPainterPath>
ClockLabel::ClockLabel(QWidget *parent)
: QLabel(parent), m_textColor(Qt::white), m_outlineColor(Qt::transparent), m_outlineWidth(0)
{
setAlignment(Qt::AlignCenter);
}
void ClockLabel::setDisplayColors(const QColor &textColor, const QColor &outlineColor, qreal outlineWidth)
{
m_textColor = textColor;
m_outlineColor = outlineColor;
m_outlineWidth = outlineWidth;
update();
}
void ClockLabel::paintEvent(QPaintEvent *event)
{
Q_UNUSED(event);
QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing);
painter.setRenderHint(QPainter::TextAntialiasing);
const QFontMetricsF metrics(font());
const QStringList lines = text().split('\n');
const qreal lineHeight = metrics.height();
const qreal textBlockHeight = lineHeight * lines.size();
// First baseline so the whole block is vertically centered.
const qreal firstBaseline = (height() - textBlockHeight) / 2.0 + metrics.ascent();
for (int i = 0; i < lines.size(); ++i)
{
const QString &line = lines.at(i);
if (line.isEmpty())
{
continue;
}
const qreal lineWidth = metrics.horizontalAdvance(line);
QPainterPath linePath;
linePath.addText(0, 0, font(), line);
painter.save();
painter.translate((width() - lineWidth) / 2.0, firstBaseline + i * lineHeight);
// Outline under the fill so the glyphs stay readable on any backdrop.
if (m_outlineColor.alpha() > 0 && m_outlineWidth > 0)
{
painter.setPen(QPen(m_outlineColor, m_outlineWidth, Qt::SolidLine, Qt::RoundCap, Qt::RoundJoin));
painter.setBrush(Qt::NoBrush);
painter.drawPath(linePath);
}
painter.setPen(Qt::NoPen);
painter.setBrush(m_textColor);
painter.drawPath(linePath);
painter.restore();
}
}
ClockWidget::ClockWidget(QWidget *parent) ClockWidget::ClockWidget(QWidget *parent)
: QWidget(parent), m_dragging(false), m_resizing(false), m_resizeBorder(5) : QWidget(parent), m_dragging(false), m_resizing(false), m_resizeBorder(5)
@@ -27,6 +92,7 @@ ClockWidget::ClockWidget(QWidget *parent)
setWindowFlags(flags); setWindowFlags(flags);
setAttribute(Qt::WA_TranslucentBackground); setAttribute(Qt::WA_TranslucentBackground);
setAttribute(Qt::WA_StyledBackground); // required for widget-level QSS background/border
setMouseTracking(true); // Enable mouse tracking for resize cursor setMouseTracking(true); // Enable mouse tracking for resize cursor
// Set up platform-specific window properties after window creation // Set up platform-specific window properties after window creation
@@ -47,12 +113,18 @@ ClockWidget::~ClockWidget()
void ClockWidget::setupUI() void ClockWidget::setupUI()
{ {
m_timeLabel = new QLabel(this); m_primaryTimeLabel = new ClockLabel(this);
m_timeLabel->setAlignment(Qt::AlignCenter); m_primaryLocationLabel = new ClockLabel(this);
m_secondaryTimeLabel = new ClockLabel(this);
m_secondaryLocationLabel = new ClockLabel(this);
QVBoxLayout *layout = new QVBoxLayout(this); QVBoxLayout *layout = new QVBoxLayout(this);
layout->addWidget(m_timeLabel); layout->addWidget(m_primaryTimeLabel);
layout->addWidget(m_primaryLocationLabel);
layout->addWidget(m_secondaryTimeLabel);
layout->addWidget(m_secondaryLocationLabel);
layout->setContentsMargins(10, 10, 10, 10); layout->setContentsMargins(10, 10, 10, 10);
layout->setSpacing(2);
} }
void ClockWidget::createMenu() void ClockWidget::createMenu()
@@ -70,9 +142,21 @@ void ClockWidget::createMenu()
QAction *bgColorAction = new QAction("Set Background Color", this); QAction *bgColorAction = new QAction("Set Background Color", this);
connect(bgColorAction, &QAction::triggered, this, &ClockWidget::changeBackgroundColor); connect(bgColorAction, &QAction::triggered, this, &ClockWidget::changeBackgroundColor);
QAction *fontSizeAction = new QAction("Set Font Size", this); QAction *fontSizeAction = new QAction("Set Time Font Size", this);
connect(fontSizeAction, &QAction::triggered, this, &ClockWidget::changeFontSize); connect(fontSizeAction, &QAction::triggered, this, &ClockWidget::changeFontSize);
QAction *locationFontSizeAction = new QAction("Set Location Font Size", this);
connect(locationFontSizeAction, &QAction::triggered, this, &ClockWidget::changeLocationFontSize);
QAction *borderColorAction = new QAction("Set Border Color", this);
connect(borderColorAction, &QAction::triggered, this, &ClockWidget::changeBorderColor);
QAction *primaryTimeZoneAction = new QAction("Set First Time Zone", this);
connect(primaryTimeZoneAction, &QAction::triggered, this, &ClockWidget::changePrimaryTimeZone);
QAction *secondaryTimeZoneAction = new QAction("Set Second Time Zone", this);
connect(secondaryTimeZoneAction, &QAction::triggered, this, &ClockWidget::changeSecondaryTimeZone);
QAction *resetAction = new QAction("Reset Settings", this); QAction *resetAction = new QAction("Reset Settings", this);
connect(resetAction, &QAction::triggered, this, &ClockWidget::resetSettings); connect(resetAction, &QAction::triggered, this, &ClockWidget::resetSettings);
@@ -83,7 +167,13 @@ void ClockWidget::createMenu()
m_contextMenu->addSeparator(); m_contextMenu->addSeparator();
m_contextMenu->addAction(fontColorAction); m_contextMenu->addAction(fontColorAction);
m_contextMenu->addAction(bgColorAction); m_contextMenu->addAction(bgColorAction);
m_contextMenu->addAction(borderColorAction);
m_contextMenu->addSeparator();
m_contextMenu->addAction(fontSizeAction); m_contextMenu->addAction(fontSizeAction);
m_contextMenu->addAction(locationFontSizeAction);
m_contextMenu->addSeparator();
m_contextMenu->addAction(primaryTimeZoneAction);
m_contextMenu->addAction(secondaryTimeZoneAction);
m_contextMenu->addSeparator(); m_contextMenu->addSeparator();
m_contextMenu->addAction(resetAction); m_contextMenu->addAction(resetAction);
m_contextMenu->addAction(quitAction); m_contextMenu->addAction(quitAction);
@@ -173,13 +263,52 @@ void ClockWidget::mouseReleaseEvent(QMouseEvent *event)
void ClockWidget::contextMenuEvent(QContextMenuEvent *event) void ClockWidget::contextMenuEvent(QContextMenuEvent *event)
{ {
m_contextMenu->exec(event->globalPos()); Q_UNUSED(event);
// Menu lives in the tray icon; right-click on the clock does nothing
// so it never interferes with dragging.
}
void ClockWidget::paintEvent(QPaintEvent *event)
{
Q_UNUSED(event);
// QSS background does not render on macOS with
// WA_TranslucentBackground, so we paint it manually.
if (m_backgroundColor.alpha() > 0)
{
QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing);
painter.setPen(Qt::NoPen);
painter.setBrush(m_backgroundColor);
painter.drawRoundedRect(QRectF(contentsRect()).adjusted(0.5, 0.5, -0.5, -0.5), 10, 10);
}
} }
void ClockWidget::updateTime() void ClockWidget::updateTime()
{ {
QString currentTime = QTime::currentTime().toString("HH:mm:ss"); const QTimeZone primaryTimeZone(m_primaryTimeZoneId.toUtf8());
m_timeLabel->setText(currentTime); if (primaryTimeZone.isValid())
{
m_primaryTimeLabel->setText(QDateTime::currentDateTimeUtc().toTimeZone(primaryTimeZone).time().toString("HH:mm"));
m_primaryLocationLabel->setText(locationName(m_primaryTimeZoneId));
}
else
{
m_primaryTimeLabel->setText("Invalid time zone");
m_primaryLocationLabel->setText(m_primaryTimeZoneId);
}
const QTimeZone secondaryTimeZone(m_secondaryTimeZoneId.toUtf8());
if (secondaryTimeZone.isValid())
{
m_secondaryTimeLabel->setText(QDateTime::currentDateTimeUtc().toTimeZone(secondaryTimeZone).time().toString("HH:mm"));
m_secondaryLocationLabel->setText(locationName(m_secondaryTimeZoneId));
}
else
{
m_secondaryTimeLabel->setText("Invalid time zone");
m_secondaryLocationLabel->setText(m_secondaryTimeZoneId);
}
} }
void ClockWidget::toggleAlwaysOnTop() void ClockWidget::toggleAlwaysOnTop()
@@ -207,38 +336,76 @@ void ClockWidget::toggleAlwaysOnTop()
void ClockWidget::changeFontColor() void ClockWidget::changeFontColor()
{ {
QColor color = QColorDialog::getColor(m_fontColor, this, "Choose Font Color"); const std::optional<QColor> color = pickColor("Font Color", m_fontColor, this);
if (color.isValid()) if (color)
{ {
m_fontColor = color; m_fontColor = *color;
m_configManager->setFontColor(m_fontColor); m_configManager->setFontColor(m_fontColor);
updateStyleSheet(); updateStyleSheet();
saveSettings();
} }
} }
void ClockWidget::changeBackgroundColor() void ClockWidget::changeBackgroundColor()
{ {
QColor color = QColorDialog::getColor(m_backgroundColor, this, "Choose Background Color"); const std::optional<QColor> color = pickColor("Background Color", m_backgroundColor, this);
if (color.isValid()) if (color)
{ {
m_backgroundColor = color; m_backgroundColor = *color;
m_configManager->setBackgroundColor(m_backgroundColor); m_configManager->setBackgroundColor(m_backgroundColor);
updateStyleSheet(); updateStyleSheet();
saveSettings();
}
}
void ClockWidget::changeBorderColor()
{
const std::optional<QColor> color = pickColor("Border Color", m_borderColor, this);
if (color)
{
m_borderColor = *color;
m_configManager->setBorderColor(m_borderColor);
updateStyleSheet();
saveSettings();
} }
} }
void ClockWidget::changeFontSize() void ClockWidget::changeFontSize()
{ {
bool ok; bool ok;
int size = QInputDialog::getInt(this, "Set Font Size", "Font Size:", m_fontSize, 8, 72, 1, &ok); int size = QInputDialog::getInt(this, "Set Time Font Size", "Font Size:", m_fontSize, 8, 200, 1, &ok);
if (ok) if (ok)
{ {
m_fontSize = size; m_fontSize = size;
m_configManager->setFontSize(m_fontSize); m_configManager->setFontSize(m_fontSize);
updateStyleSheet(); updateStyleSheet();
saveSettings();
} }
} }
void ClockWidget::changeLocationFontSize()
{
bool ok;
int size = QInputDialog::getInt(this, "Set Location Font Size", "Font Size:", m_locationFontSize, 6, 200, 1, &ok);
if (ok)
{
m_locationFontSize = size;
m_configManager->setLocationFontSize(m_locationFontSize);
updateStyleSheet();
saveSettings();
}
}
void ClockWidget::changePrimaryTimeZone()
{
changeTimeZone(true);
}
void ClockWidget::changeSecondaryTimeZone()
{
changeTimeZone(false);
}
void ClockWidget::resetSettings() void ClockWidget::resetSettings()
{ {
int ret = QMessageBox::question(this, "Reset Settings", "Are you sure you want to reset all settings?"); int ret = QMessageBox::question(this, "Reset Settings", "Are you sure you want to reset all settings?");
@@ -265,6 +432,28 @@ bool ClockWidget::isInResizeArea(const QPoint &pos) const
return resizeArea.contains(pos); return resizeArea.contains(pos);
} }
void ClockWidget::positionAtBottomRight()
{
// Get primary screen geometry
QScreen *screen = QApplication::primaryScreen();
if (!screen)
{
// Fallback to default position if no screen available
move(100, 100);
return;
}
QRect screenGeometry = screen->availableGeometry();
QSize windowSize = m_configManager->windowSize();
// Calculate position: bottom-right corner with some margin
int margin = 20; // 20 pixels margin from screen edges
int x = screenGeometry.right() - windowSize.width() - margin;
int y = screenGeometry.bottom() - windowSize.height() - margin;
move(x, y);
}
void ClockWidget::loadSettings() void ClockWidget::loadSettings()
{ {
m_configManager = new ConfigManager(this); m_configManager = new ConfigManager(this);
@@ -279,11 +468,24 @@ void ClockWidget::loadSettings()
// Apply loaded settings // Apply loaded settings
m_fontColor = m_configManager->fontColor(); m_fontColor = m_configManager->fontColor();
m_backgroundColor = m_configManager->backgroundColor(); m_backgroundColor = m_configManager->backgroundColor();
m_borderColor = m_configManager->borderColor();
m_fontSize = m_configManager->fontSize(); m_fontSize = m_configManager->fontSize();
m_locationFontSize = m_configManager->locationFontSize();
m_alwaysOnTop = m_configManager->alwaysOnTop(); m_alwaysOnTop = m_configManager->alwaysOnTop();
m_primaryTimeZoneId = m_configManager->primaryTimeZoneId();
m_secondaryTimeZoneId = m_configManager->secondaryTimeZoneId();
// Restore window position and size // Restore window position and size
move(m_configManager->windowPosition()); QPoint windowPos = m_configManager->windowPosition();
if (windowPos.x() == -1 && windowPos.y() == -1)
{
// First time: position at bottom-right corner
positionAtBottomRight();
}
else
{
move(windowPos);
}
resize(m_configManager->windowSize()); resize(m_configManager->windowSize());
// Update always on top checkbox in menu // Update always on top checkbox in menu
@@ -308,10 +510,14 @@ void ClockWidget::saveSettings()
// Update config manager with current values // Update config manager with current values
m_configManager->setFontColor(m_fontColor); m_configManager->setFontColor(m_fontColor);
m_configManager->setBackgroundColor(m_backgroundColor); m_configManager->setBackgroundColor(m_backgroundColor);
m_configManager->setBorderColor(m_borderColor);
m_configManager->setFontSize(m_fontSize); m_configManager->setFontSize(m_fontSize);
m_configManager->setLocationFontSize(m_locationFontSize);
m_configManager->setAlwaysOnTop(m_alwaysOnTop); m_configManager->setAlwaysOnTop(m_alwaysOnTop);
m_configManager->setWindowPosition(pos()); m_configManager->setWindowPosition(pos());
m_configManager->setWindowSize(size()); m_configManager->setWindowSize(size());
m_configManager->setPrimaryTimeZoneId(m_primaryTimeZoneId);
m_configManager->setSecondaryTimeZoneId(m_secondaryTimeZoneId);
// Save to XML file // Save to XML file
if (!m_configManager->saveSettings()) if (!m_configManager->saveSettings())
@@ -322,19 +528,121 @@ void ClockWidget::saveSettings()
void ClockWidget::updateStyleSheet() void ClockWidget::updateStyleSheet()
{ {
QString styleSheet = QString( // Fonts via QSS; glyph fill + outline via ClockLabel::paintEvent.
"QLabel { " const auto labelFontStyleSheet = [](int size) {
"color: %1; " return QString("QLabel { font-family: 'SF Pro Rounded'; font-size: %1pt; background: transparent; }")
"font-size: %2pt; " .arg(size);
"font-weight: bold; " };
"background-color: %3; "
"border-radius: 10px; "
"padding: 5px; "
"}")
.arg(m_fontColor.name(),
QString::number(m_fontSize),
m_backgroundColor.name(QColor::HexArgb));
m_timeLabel->setStyleSheet(styleSheet); m_primaryTimeLabel->setStyleSheet(labelFontStyleSheet(m_fontSize));
setStyleSheet(QString("background-color: transparent;")); m_secondaryTimeLabel->setStyleSheet(labelFontStyleSheet(m_fontSize));
m_primaryLocationLabel->setStyleSheet(labelFontStyleSheet(m_locationFontSize));
m_secondaryLocationLabel->setStyleSheet(labelFontStyleSheet(m_locationFontSize));
// Shrink/grow the window to fit the text exactly (fonts may have changed).
adjustSize();
// Background is painted manually in paintEvent() — QSS does not render
// it on macOS with WA_TranslucentBackground. Text colors + outline go
// through ClockLabel (QSS cannot stroke glyphs).
const qreal outlineWidth = m_borderColor.alpha() > 0 ? 1.0 : 0.0;
m_primaryTimeLabel->setDisplayColors(m_fontColor, m_borderColor, outlineWidth);
m_secondaryTimeLabel->setDisplayColors(m_fontColor, m_borderColor, outlineWidth);
m_primaryLocationLabel->setDisplayColors(m_fontColor, m_borderColor, outlineWidth * 0.75);
m_secondaryLocationLabel->setDisplayColors(m_fontColor, m_borderColor, outlineWidth * 0.75);
update();
}
void ClockWidget::changeTimeZone(bool primaryClock)
{
QStringList timeZoneIds;
for (const QByteArray &timeZoneId : QTimeZone::availableTimeZoneIds())
{
timeZoneIds.append(QString::fromUtf8(timeZoneId));
}
const QString currentTimeZoneId = primaryClock ? m_primaryTimeZoneId : m_secondaryTimeZoneId;
const int currentIndex = timeZoneIds.indexOf(currentTimeZoneId);
bool accepted = false;
const QString selectedTimeZoneId = QInputDialog::getItem(
this,
primaryClock ? "Set First Time Zone" : "Set Second Time Zone",
"Time Zone:",
timeZoneIds,
currentIndex < 0 ? 0 : currentIndex,
false,
&accepted);
if (!accepted)
{
return;
}
if (primaryClock)
{
m_primaryTimeZoneId = selectedTimeZoneId;
m_configManager->setPrimaryTimeZoneId(selectedTimeZoneId);
}
else
{
m_secondaryTimeZoneId = selectedTimeZoneId;
m_configManager->setSecondaryTimeZoneId(selectedTimeZoneId);
}
if (!m_configManager->saveSettings())
{
qDebug() << "Failed to save time zone settings to config.xml";
}
updateTime();
}
QString ClockWidget::formattedTime(const QString &timeZoneId) const
{
const QTimeZone timeZone(timeZoneId.toUtf8());
if (!timeZone.isValid())
{
qWarning() << "Invalid time zone configured:" << timeZoneId;
return QString("Invalid time zone\n%1").arg(timeZoneId);
}
const QDateTime timeInZone = QDateTime::currentDateTimeUtc().toTimeZone(timeZone);
const QString cityName = QString::fromUtf8(timeZone.id()).section('/', -1).replace('_', ' ');
return QString("%1\n%2")
.arg(timeInZone.time().toString("HH:mm"), cityName);
}
QString ClockWidget::locationName(const QString &timeZoneId) const
{
return timeZoneId.section('/', -1).replace('_', ' ');
}
std::optional<QColor> ClockWidget::pickColor(const QString &title, const QColor &current, QWidget *parent)
{
bool ok = false;
const QString input = QInputDialog::getText(
parent, "Set " + title,
QString("HTML color (#RRGGBB or #AARRGGBB), or type 'transparent':"),
QLineEdit::Normal,
current.alpha() == 0 ? QStringLiteral("transparent") : current.name(QColor::HexArgb),
&ok);
if (!ok || input.trimmed().isEmpty())
{
return std::nullopt; // cancelled — keep current color
}
const QString normalized = input.trimmed().toLower();
if (normalized == "transparent")
{
return QColor(Qt::transparent);
}
const QColor parsed(normalized);
if (parsed.isValid())
{
return parsed;
}
// Not valid HTML text — fall back to the visual color dialog.
return QColorDialog::getColor(current, parent, "Choose " + title);
} }
+44 -1
View File
@@ -8,9 +8,30 @@
#include <QMenu> #include <QMenu>
#include <QAction> #include <QAction>
#include <QColor> #include <QColor>
#include <optional>
class ConfigManager; class ConfigManager;
// QLabel that paints its text with an optional outline (border around the
// glyphs, not a box). QSS cannot stroke text, so we use a QPainterPath.
class ClockLabel : public QLabel
{
Q_OBJECT
public:
explicit ClockLabel(QWidget *parent = nullptr);
void setDisplayColors(const QColor &textColor, const QColor &outlineColor, qreal outlineWidth);
protected:
void paintEvent(QPaintEvent *event) override;
private:
QColor m_textColor;
QColor m_outlineColor;
qreal m_outlineWidth;
};
class ClockWidget : public QWidget class ClockWidget : public QWidget
{ {
Q_OBJECT Q_OBJECT
@@ -19,18 +40,28 @@ public:
explicit ClockWidget(QWidget *parent = nullptr); explicit ClockWidget(QWidget *parent = nullptr);
~ClockWidget(); ~ClockWidget();
QMenu *menu() const { return m_contextMenu; }
QString primaryTimeZoneId() const { return m_primaryTimeZoneId; }
QString secondaryTimeZoneId() const { return m_secondaryTimeZoneId; }
protected: protected:
void mousePressEvent(QMouseEvent *event) override; void mousePressEvent(QMouseEvent *event) override;
void mouseMoveEvent(QMouseEvent *event) override; void mouseMoveEvent(QMouseEvent *event) override;
void mouseReleaseEvent(QMouseEvent *event) override; void mouseReleaseEvent(QMouseEvent *event) override;
void contextMenuEvent(QContextMenuEvent *event) override; void contextMenuEvent(QContextMenuEvent *event) override;
void paintEvent(QPaintEvent *event) override;
private slots: private slots:
void updateTime(); void updateTime();
void toggleAlwaysOnTop(); void toggleAlwaysOnTop();
void changeFontColor(); void changeFontColor();
void changeBackgroundColor(); void changeBackgroundColor();
void changeBorderColor();
void changeFontSize(); void changeFontSize();
void changeLocationFontSize();
void changePrimaryTimeZone();
void changeSecondaryTimeZone();
void resetSettings(); void resetSettings();
void quitApplication(); void quitApplication();
@@ -40,8 +71,15 @@ private:
void loadSettings(); void loadSettings();
void saveSettings(); void saveSettings();
void updateStyleSheet(); void updateStyleSheet();
void changeTimeZone(bool primaryClock);
QString formattedTime(const QString &timeZoneId) const;
QString locationName(const QString &timeZoneId) const;
std::optional<QColor> pickColor(const QString &title, const QColor &current, QWidget *parent);
QLabel *m_timeLabel; ClockLabel *m_primaryTimeLabel;
ClockLabel *m_primaryLocationLabel;
ClockLabel *m_secondaryTimeLabel;
ClockLabel *m_secondaryLocationLabel;
QTimer *m_timer; QTimer *m_timer;
QMenu *m_contextMenu; QMenu *m_contextMenu;
@@ -59,11 +97,16 @@ private:
ConfigManager *m_configManager; ConfigManager *m_configManager;
QColor m_fontColor; QColor m_fontColor;
QColor m_backgroundColor; QColor m_backgroundColor;
QColor m_borderColor;
int m_fontSize; int m_fontSize;
int m_locationFontSize;
bool m_alwaysOnTop; bool m_alwaysOnTop;
QString m_primaryTimeZoneId;
QString m_secondaryTimeZoneId;
// Helper methods // Helper methods
bool isInResizeArea(const QPoint &pos) const; bool isInResizeArea(const QPoint &pos) const;
void positionAtBottomRight();
#ifdef Q_OS_MAC #ifdef Q_OS_MAC
void setupMacOSWindowProperties(); void setupMacOSWindowProperties();
+8 -6
View File
@@ -1,9 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<QFlockConfig> <DualFloatingClockConfig>
<FontColor>#ffffffff</FontColor> <FontColor>#ff00ff00</FontColor>
<BackgroundColor>#96000000</BackgroundColor> <BackgroundColor>#00000000</BackgroundColor>
<FontSize>24</FontSize> <FontSize>24</FontSize>
<AlwaysOnTop>true</AlwaysOnTop> <AlwaysOnTop>true</AlwaysOnTop>
<WindowPosition>1132,708</WindowPosition> <WindowPosition>1236,784</WindowPosition>
<WindowSize>137,59</WindowSize> <WindowSize>240,160</WindowSize>
</QFlockConfig> <PrimaryTimeZone>Europe/Bucharest</PrimaryTimeZone>
<SecondaryTimeZone>America/Los_Angeles</SecondaryTimeZone>
</DualFloatingClockConfig>
+83 -15
View File
@@ -1,6 +1,10 @@
#include "config_manager.h" #include "config_manager.h"
#include <QDir>
#include <QFile> #include <QFile>
#include <QFileInfo>
#include <QDebug> #include <QDebug>
#include <QStandardPaths>
#include <QTimeZone>
ConfigManager::ConfigManager(QObject *parent) ConfigManager::ConfigManager(QObject *parent)
: QObject(parent) : QObject(parent)
@@ -8,9 +12,27 @@ ConfigManager::ConfigManager(QObject *parent)
setDefaultValues(); setDefaultValues();
} }
QString ConfigManager::defaultSettingsPath()
{
const QString configDirectory = QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation);
if (configDirectory.isEmpty())
{
qWarning() << "No application configuration directory is available";
return QString();
}
return QDir(configDirectory).filePath("config.xml");
}
bool ConfigManager::loadSettings(const QString &filename) bool ConfigManager::loadSettings(const QString &filename)
{ {
QFile file(filename); const QString settingsPath = filename.isEmpty() ? defaultSettingsPath() : filename;
if (settingsPath.isEmpty())
{
return false;
}
QFile file(settingsPath);
if (!file.exists()) if (!file.exists())
{ {
qDebug() << "Config file does not exist, using defaults"; qDebug() << "Config file does not exist, using defaults";
@@ -26,7 +48,8 @@ bool ConfigManager::loadSettings(const QString &filename)
QXmlStreamReader reader(&file); QXmlStreamReader reader(&file);
// Check if it's a valid XML file // Check if it's a valid XML file
if (!reader.readNextStartElement() || reader.name() != QString("QFlockConfig")) if (!reader.readNextStartElement()
|| (reader.name() != QString("DualFloatingClockConfig") && reader.name() != QString("ClouckConfig")))
{ {
qDebug() << "Invalid config file format"; qDebug() << "Invalid config file format";
file.close(); file.close();
@@ -77,6 +100,22 @@ bool ConfigManager::loadSettings(const QString &filename)
m_windowSize = QSize(width, height); m_windowSize = QSize(width, height);
} }
} }
else if (name == "PrimaryTimeZone")
{
m_primaryTimeZoneId = text;
}
else if (name == "SecondaryTimeZone")
{
m_secondaryTimeZoneId = text;
}
else if (name == "BorderColor")
{
m_borderColor = QColor(text);
}
else if (name == "LocationFontSize")
{
m_locationFontSize = text.toInt();
}
else else
{ {
reader.skipCurrentElement(); reader.skipCurrentElement();
@@ -96,7 +135,20 @@ bool ConfigManager::loadSettings(const QString &filename)
bool ConfigManager::saveSettings(const QString &filename) bool ConfigManager::saveSettings(const QString &filename)
{ {
QFile file(filename); const QString settingsPath = filename.isEmpty() ? defaultSettingsPath() : filename;
if (settingsPath.isEmpty())
{
return false;
}
const QFileInfo fileInfo(settingsPath);
if (!QDir().mkpath(fileInfo.absolutePath()))
{
qDebug() << "Cannot create configuration directory:" << fileInfo.absolutePath();
return false;
}
QFile file(settingsPath);
if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) if (!file.open(QIODevice::WriteOnly | QIODevice::Text))
{ {
qDebug() << "Cannot open config file for writing:" << file.errorString(); qDebug() << "Cannot open config file for writing:" << file.errorString();
@@ -108,16 +160,20 @@ bool ConfigManager::saveSettings(const QString &filename)
writer.writeStartDocument(); writer.writeStartDocument();
// Root element // Root element
writer.writeStartElement("QFlockConfig"); writer.writeStartElement("DualFloatingClockConfig");
// Font color // Colors: store #RRGGBB when fully opaque, #AARRGGBB when translucent —
writer.writeTextElement("FontColor", m_fontColor.name(QColor::HexArgb)); // the XML stays close to what the user typed.
const auto colorString = [](const QColor &color) {
return color.alpha() == 255 ? color.name(QColor::HexRgb) : color.name(QColor::HexArgb);
};
writer.writeTextElement("FontColor", colorString(m_fontColor));
writer.writeTextElement("BackgroundColor", colorString(m_backgroundColor));
writer.writeTextElement("BorderColor", colorString(m_borderColor));
// Background color // Font sizes
writer.writeTextElement("BackgroundColor", m_backgroundColor.name(QColor::HexArgb));
// Font size
writer.writeTextElement("FontSize", QString::number(m_fontSize)); writer.writeTextElement("FontSize", QString::number(m_fontSize));
writer.writeTextElement("LocationFontSize", QString::number(m_locationFontSize));
// Always on top // Always on top
writer.writeTextElement("AlwaysOnTop", m_alwaysOnTop ? "true" : "false"); writer.writeTextElement("AlwaysOnTop", m_alwaysOnTop ? "true" : "false");
@@ -134,6 +190,9 @@ bool ConfigManager::saveSettings(const QString &filename)
.arg(m_windowSize.height()); .arg(m_windowSize.height());
writer.writeTextElement("WindowSize", sizeStr); writer.writeTextElement("WindowSize", sizeStr);
writer.writeTextElement("PrimaryTimeZone", m_primaryTimeZoneId);
writer.writeTextElement("SecondaryTimeZone", m_secondaryTimeZoneId);
// Close root element // Close root element
writer.writeEndElement(); writer.writeEndElement();
@@ -150,15 +209,24 @@ void ConfigManager::resetToDefaults()
bool ConfigManager::configExists(const QString &filename) const bool ConfigManager::configExists(const QString &filename) const
{ {
return QFile::exists(filename); const QString settingsPath = filename.isEmpty() ? defaultSettingsPath() : filename;
return !settingsPath.isEmpty() && QFile::exists(settingsPath);
} }
void ConfigManager::setDefaultValues() void ConfigManager::setDefaultValues()
{ {
m_fontColor = QColor(255, 255, 255); // White m_fontColor = QColor(0, 255, 0);
m_backgroundColor = QColor(0, 0, 0, 150); // Semi-transparent black m_backgroundColor = QColor(0, 0, 0, 0);
m_fontSize = 24; m_fontSize = 24;
m_alwaysOnTop = true; m_alwaysOnTop = true;
m_windowPosition = QPoint(100, 100);
m_windowSize = QSize(200, 80); // Default clock size // Calculate initial position: bottom-right corner of primary screen
// Position will be set properly when widget is shown for the first time
m_windowPosition = QPoint(-1, -1); // Special value indicating "not set yet"
m_windowSize = QSize(240, 160); // Default clock size
m_primaryTimeZoneId = QString::fromUtf8(QTimeZone::systemTimeZoneId());
m_secondaryTimeZoneId = QStringLiteral("America/Los_Angeles");
m_borderColor = QColor(0, 0, 0, 0); // No border by default
m_locationFontSize = 12; // Smaller city label under the time
} }
+17 -3
View File
@@ -16,11 +16,13 @@ class ConfigManager : public QObject
public: public:
explicit ConfigManager(QObject *parent = nullptr); explicit ConfigManager(QObject *parent = nullptr);
static QString defaultSettingsPath();
// Load settings from XML file // Load settings from XML file
bool loadSettings(const QString &filename = "config.xml"); bool loadSettings(const QString &filename = QString());
// Save settings to XML file // Save settings to XML file
bool saveSettings(const QString &filename = "config.xml"); bool saveSettings(const QString &filename = QString());
// Getters // Getters
QColor fontColor() const { return m_fontColor; } QColor fontColor() const { return m_fontColor; }
@@ -29,6 +31,10 @@ public:
bool alwaysOnTop() const { return m_alwaysOnTop; } bool alwaysOnTop() const { return m_alwaysOnTop; }
QPoint windowPosition() const { return m_windowPosition; } QPoint windowPosition() const { return m_windowPosition; }
QSize windowSize() const { return m_windowSize; } QSize windowSize() const { return m_windowSize; }
QString primaryTimeZoneId() const { return m_primaryTimeZoneId; }
QString secondaryTimeZoneId() const { return m_secondaryTimeZoneId; }
QColor borderColor() const { return m_borderColor; }
int locationFontSize() const { return m_locationFontSize; }
// Setters // Setters
void setFontColor(const QColor &color) { m_fontColor = color; } void setFontColor(const QColor &color) { m_fontColor = color; }
@@ -37,12 +43,16 @@ public:
void setAlwaysOnTop(bool onTop) { m_alwaysOnTop = onTop; } void setAlwaysOnTop(bool onTop) { m_alwaysOnTop = onTop; }
void setWindowPosition(const QPoint &pos) { m_windowPosition = pos; } void setWindowPosition(const QPoint &pos) { m_windowPosition = pos; }
void setWindowSize(const QSize &size) { m_windowSize = size; } void setWindowSize(const QSize &size) { m_windowSize = size; }
void setPrimaryTimeZoneId(const QString &timeZoneId) { m_primaryTimeZoneId = timeZoneId; }
void setSecondaryTimeZoneId(const QString &timeZoneId) { m_secondaryTimeZoneId = timeZoneId; }
void setBorderColor(const QColor &color) { m_borderColor = color; }
void setLocationFontSize(int size) { m_locationFontSize = size; }
// Reset to default values // Reset to default values
void resetToDefaults(); void resetToDefaults();
// Check if config file exists // Check if config file exists
bool configExists(const QString &filename = "config.xml") const; bool configExists(const QString &filename = QString()) const;
private: private:
// Default values // Default values
@@ -55,6 +65,10 @@ private:
bool m_alwaysOnTop; bool m_alwaysOnTop;
QPoint m_windowPosition; QPoint m_windowPosition;
QSize m_windowSize; QSize m_windowSize;
QString m_primaryTimeZoneId;
QString m_secondaryTimeZoneId;
QColor m_borderColor;
int m_locationFontSize;
}; };
#endif // CONFIG_MANAGER_H #endif // CONFIG_MANAGER_H
+130 -3
View File
@@ -1,16 +1,143 @@
#include <QApplication> #include <QApplication>
#include <QAction>
#include <QMenu>
#include <QPainter>
#include <QSettings>
#include <QStyleHints>
#include <QSystemTrayIcon>
#include "autostart_manager.h"
#include "clock_widget.h" #include "clock_widget.h"
namespace
{
// Standard macOS menu bar icon extent (points). Matches surrounding icons.
constexpr int trayIconExtent = 16;
QIcon createTrayIcon(const QColor &color)
{
// Render at 16 logical points times the device pixel ratio so the
// bitmap is crisp on Retina but occupies exactly one menu bar cell.
const qreal devicePixelRatio = qApp->devicePixelRatio();
QPixmap pixmap(qRound(trayIconExtent * devicePixelRatio),
qRound(trayIconExtent * devicePixelRatio));
pixmap.setDevicePixelRatio(devicePixelRatio);
pixmap.fill(Qt::transparent);
QPainter painter(&pixmap);
painter.setRenderHint(QPainter::Antialiasing);
painter.setPen(QPen(color, 1.5));
// Draw inside the 16pt coordinate space; center with a small margin.
const QPointF center(trayIconExtent / 2.0, trayIconExtent / 2.0);
const qreal radius = trayIconExtent / 2.0 - 1.5;
painter.drawEllipse(center, radius, radius);
painter.drawLine(QPointF(center.x(), center.y() - radius * 0.55), center);
painter.drawLine(center, QPointF(center.x() + radius * 0.5, center.y() + radius * 0.3));
QIcon icon;
icon.addPixmap(pixmap, QIcon::Normal, QIcon::Off);
return icon;
}
QColor trayIconColorForScheme(Qt::ColorScheme scheme)
{
return scheme == Qt::ColorScheme::Dark ? QColor(Qt::white) : QColor(Qt::black);
}
}
int main(int argc, char *argv[]) int main(int argc, char *argv[])
{ {
QApplication app(argc, argv); QApplication app(argc, argv);
app.setApplicationName("QFlock"); // Menu-bar-only app: no Dock icon, no app switcher entry.
app.setOrganizationName("QFlock"); #ifdef Q_OS_MAC
app.setApplicationDisplayName("QFlock"); extern void clouck_setActivationPolicyAccessory(); // defined in window_helper_macos.mm
clouck_setActivationPolicyAccessory();
#endif
app.setApplicationName("DualFloatingClock");
app.setOrganizationName("DualFloatingClock");
app.setApplicationDisplayName("DualFloatingClock");
ClockWidget clock; ClockWidget clock;
clock.show(); clock.show();
// Start at Login: honor the persisted choice instead of always forcing it.
{
QSettings autoStartSettings("DualFloatingClock", "DualFloatingClock");
const bool startAtLogin = autoStartSettings.value("startAtLogin", true).toBool();
if (startAtLogin)
{
AutoStartManager::enableForCurrentApplication();
}
else
{
AutoStartManager::disable();
}
autoStartSettings.setValue("startAtLogin", startAtLogin);
}
const QStyleHints *styleHints = app.styleHints();
QSystemTrayIcon trayIcon(createTrayIcon(trayIconColorForScheme(styleHints->colorScheme())), &app);
trayIcon.setToolTip("DualFloatingClock");
QObject::connect(styleHints, &QStyleHints::colorSchemeChanged, &trayIcon,
[&trayIcon](Qt::ColorScheme scheme) {
trayIcon.setIcon(createTrayIcon(trayIconColorForScheme(scheme)));
});
QMenu trayMenu;
QAction *toggleClockAction = trayMenu.addAction("Turn Clock Off");
QAction *startAtLoginAction = trayMenu.addAction("Start at Login");
{
QSettings autoStartSettings("DualFloatingClock", "DualFloatingClock");
startAtLoginAction->setCheckable(true);
startAtLoginAction->setChecked(autoStartSettings.value("startAtLogin", true).toBool());
}
QObject::connect(startAtLoginAction, &QAction::toggled, &app, [](bool checked) {
QSettings autoStartSettings("DualFloatingClock", "DualFloatingClock");
autoStartSettings.setValue("startAtLogin", checked);
if (checked)
{
AutoStartManager::enableForCurrentApplication();
}
else
{
AutoStartManager::disable();
}
});
trayMenu.addSeparator();
// Clock settings live directly in the tray menu — no submenu.
const QList<QAction *> clockActions = clock.menu()->actions();
for (QAction *action : clockActions)
{
trayMenu.addAction(action);
}
trayMenu.addSeparator();
QAction *quitAction = trayMenu.addAction("Quit");
const auto toggleClock = [&clock, toggleClockAction]() {
if (clock.isVisible())
{
clock.hide();
toggleClockAction->setText("Turn Clock On");
return;
}
clock.show();
clock.raise();
toggleClockAction->setText("Turn Clock Off");
};
QObject::connect(toggleClockAction, &QAction::triggered, &app, toggleClock);
QObject::connect(quitAction, &QAction::triggered, &app, &QApplication::quit);
trayIcon.setContextMenu(&trayMenu);
trayIcon.show();
return app.exec(); return app.exec();
} }
Binary file not shown.
-27
View File
@@ -1,27 +0,0 @@
#!/bin/bash
echo "QFlock Clock Application - macOS Fullscreen Always-on-Top Feature Test"
echo "======================================================================"
echo ""
echo "[SUCCESS] Compilation successful! Application build completed"
echo "[SUCCESS] macOS fullscreen always-on-top feature integrated"
echo ""
echo "Feature highlights:"
echo "• Basic always-on-top using Qt::WindowStaysOnTopHint"
echo "• Native macOS window level with NSWindowLevel"
echo "• Support for all desktop spaces (NSWindowCollectionBehaviorCanJoinAllSpaces)"
echo "• Fullscreen mode support (NSWindowCollectionBehaviorFullScreenAuxiliary)"
echo "• Prevents window focus stealing (Qt::WindowDoesNotAcceptFocus)"
echo ""
echo "Testing method:"
echo "1. Run ./build/Clouck.app/Contents/MacOS/Clouck"
echo "2. Open Safari or other apps in fullscreen mode"
echo "3. Observe if clock stays on top layer"
echo ""
echo "Optimized file structure:"
echo "• clock_widget.h/cpp - Main clock component"
echo "• config_manager.h/cpp - Configuration management"
echo "• window_helper_macos.h/mm - macOS native window helper"
echo ""
echo "Press any key to exit..."
read -n 1 -s
-39
View File
@@ -1,39 +0,0 @@
#include <QCoreApplication>
#include <QDebug>
#include <QFile>
#include <QSize>
#include <QPoint>
#include "config_manager.h"
int main(int argc, char *argv[])
{
QCoreApplication app(argc, argv);
ConfigManager configManager;
// Set some test values
configManager.setWindowPosition(QPoint(100, 200));
configManager.setWindowSize(QSize(300, 400));
// Save to file
if (configManager.saveSettings("test_config.xml"))
{
qDebug() << "Config saved successfully";
// Read the file content
QFile file("test_config.xml");
if (file.open(QIODevice::ReadOnly | QIODevice::Text))
{
QString content = file.readAll();
qDebug() << "File content:";
qDebug() << content;
file.close();
}
}
else
{
qDebug() << "Failed to save config";
}
return 0;
}
-128
View File
@@ -1,128 +0,0 @@
#!/bin/bash
# Cross-platform test script for Clouck
echo "Clouck Cross-Platform Test Suite"
echo "================================"
echo ""
# Detect operating system
if [[ "$OSTYPE" == "darwin"* ]]; then
OS="macOS"
EXECUTABLE="./build/Clouck"
elif [[ "$OSTYPE" == "linux-gnu"* ]]; then
OS="Linux"
EXECUTABLE="./build/Clouck"
elif [[ "$OSTYPE" == "msys" ]] || [[ "$OSTYPE" == "cygwin" ]] || [[ "$OSTYPE" == "win32" ]]; then
OS="Windows"
EXECUTABLE="./build/Clouck"
else
echo "Unsupported operating system: $OSTYPE"
exit 1
fi
echo "Detected OS: $OS"
echo "Executable: $EXECUTABLE"
echo ""
# Check if executable exists
if [ ! -f "$EXECUTABLE" ]; then
echo "[ERROR] Executable not found: $EXECUTABLE"
echo "Please build the project first using: ./build_cross_platform.sh"
exit 1
fi
echo "[SUCCESS] Executable found"
echo ""
# Test 1: Basic functionality
echo "Test 1: Basic Clock Functionality"
echo "---------------------------------"
echo "• Starting Clouck application..."
echo "• Application should display a clock widget"
echo "• Clock should update every second"
echo ""
# Start the application in background
$EXECUTABLE &
PID=$!
sleep 3
# Check if application is running
if ps -p $PID >/dev/null; then
echo "[SUCCESS] Application started successfully (PID: $PID)"
else
echo "[ERROR] Application failed to start"
exit 1
fi
echo ""
echo "Test 2: Always-on-Top Feature"
echo "------------------------------"
if [ "$OS" == "macOS" ]; then
echo "• Open Safari or any other application"
echo "• Enter fullscreen mode (Control + Command + F)"
echo "• Clock should remain visible on top"
elif [ "$OS" == "Windows" ]; then
echo "• Open any application in fullscreen mode"
echo "• Clock should remain visible on top"
elif [ "$OS" == "Linux" ]; then
echo "• Switch to different workspace"
echo "• Clock should appear on all workspaces"
fi
echo ""
echo "Test 3: Resizable Feature"
echo "-------------------------"
echo "• Move mouse to bottom-right corner of clock"
echo "• Cursor should change to diagonal resize arrow"
echo "• Click and drag to resize"
echo "• Release mouse to save new size"
echo ""
echo "Test 4: Context Menu"
echo "---------------------"
echo "• Right-click on clock to show context menu"
echo "• Test various menu options:"
echo " - Change color"
echo " - Change font"
echo " - Toggle always on top"
echo " - Toggle click through"
echo " - Quit application"
echo ""
echo "Platform-Specific Features:"
echo "---------------------------"
if [ "$OS" == "macOS" ]; then
echo "• Native macOS window level integration"
echo "• Support for all desktop spaces"
echo "• Fullscreen mode compatibility"
elif [ "$OS" == "Windows" ]; then
echo "• Windows native window management"
echo "• Taskbar exclusion"
echo "• Click-through support"
elif [ "$OS" == "Linux" ]; then
echo "• X11 window manager integration"
echo "• Workspace stickiness"
echo "• EWMH (Extended Window Manager Hints) support"
fi
echo ""
echo "To stop testing:"
echo "• Right-click on clock and select 'Quit'"
echo "• Or run: kill $PID"
echo ""
echo "Test the features now! Press any key when finished..."
read -n 1 -s
echo ""
echo "Stopping application..."
kill $PID 2>/dev/null
sleep 1
echo ""
echo "Test completed!"
echo ""
echo "Configuration file location:"
echo "• macOS: ~/Library/Preferences/Clouck/config.xml"
echo "• Windows: %APPDATA%\\Clouck\\config.xml"
echo "• Linux: ~/.config/Clouck/config.xml"
-15
View File
@@ -1,15 +0,0 @@
#!/bin/bash
echo "Testing QFlock Clock Application Fullscreen Always-on-Top Feature..."
echo ""
echo "Test Steps:"
echo "1. Clock application should already be running"
echo "2. Open any application (like Safari) and enter fullscreen mode"
echo "3. Observe if the clock still displays above the fullscreen application"
echo "4. You can use Command+Tab to switch applications, the clock should remain visible"
echo ""
echo "Press any key to exit test instructions..."
read -n 1 -s
echo ""
echo "Test complete! If the clock remains visible in fullscreen mode, the feature is working correctly."
-80
View File
@@ -1,80 +0,0 @@
#!/bin/bash
# GitHub Actions Local Testing Script
# This script helps test GitHub Actions workflows locally using act
echo "GitHub Actions Local Testing for Clouck"
echo "========================================"
# Check if act is installed
if ! command -v act &> /dev/null; then
echo "❌ act is not installed. Installing..."
echo "Please install act from: https://github.com/nektos/act"
echo "On macOS: brew install act"
echo "On Linux: curl https://raw.githubusercontent.com/nektos/act/master/install.sh | bash"
exit 1
fi
# Display available workflows
echo "Available workflows:"
echo "1. CI Build (ci.yml) - Quick validation builds"
echo "2. Build Qt (build.yml) - Comprehensive builds"
echo "3. Release (release.yml) - Create releases"
echo "4. All workflows"
echo ""
# Function to run workflow
run_workflow() {
local workflow=$1
local event=$2
echo "Running $workflow with event: $event"
act -W .github/workflows/$workflow -e $event --dry-run
}
# Menu
read -p "Which workflow do you want to test? (1-4): " choice
case $choice in
1)
echo "Testing CI Build workflow..."
run_workflow "ci.yml" "push"
;;
2)
echo "Testing Build Qt workflow..."
run_workflow "build.yml" "push"
;;
3)
echo "Testing Release workflow..."
# Create a mock release event
cat > /tmp/release-event.json << EOF
{
"ref": "refs/tags/v1.0.0",
"repository": {
"name": "floating-clock",
"owner": {
"login": "ivaquero"
}
}
}
EOF
run_workflow "release.yml" "/tmp/release-event.json"
;;
4)
echo "Testing all workflows..."
act --dry-run
;;
*)
echo "Invalid choice. Exiting."
exit 1
;;
esac
echo ""
echo "To run the workflows for real (not dry-run), remove the --dry-run flag"
echo "Example: act -W .github/workflows/ci.yml"
echo ""
echo "For local testing with Docker, you may need to use different images:"
echo "act -P ubuntu-latest=nektos/act-environments-ubuntu:18.04"
echo "act -P macos-latest=nektos/act-environments-macos:latest"
echo "act -P windows-latest=nektos/act-environments-windows:latest"
-57
View File
@@ -1,57 +0,0 @@
#!/bin/bash
echo "Testing QFlock Resizable Feature"
echo "================================"
echo ""
echo "1. Starting QFlock application..."
./build/Clouck.app/Contents/MacOS/Clouck &
PID=$!
sleep 2
echo ""
echo "2. Checking if application is running..."
if ps -p $PID >/dev/null; then
echo "[SUCCESS] Application started successfully (PID: $PID)"
else
echo "[ERROR] Application failed to start"
exit 1
fi
echo ""
echo "3. Checking configuration file..."
if [ -f "config.xml" ]; then
echo "[INFO] Configuration file exists"
echo "Current configuration:"
cat config.xml
else
echo "[WARNING] Configuration file not found"
fi
echo ""
echo "4. Instructions for testing resizable feature:"
echo " - Move mouse to bottom-right corner of the clock"
echo " - Cursor should change to diagonal resize arrow"
echo " - Click and drag to resize"
echo " - Release mouse to save new size"
echo " - Size will be saved to config.xml"
echo ""
echo "5. To stop the application:"
echo " - Right-click on clock and select 'Quit'"
echo " - Or run: kill $PID"
echo ""
echo "Test the resizable feature now!"
echo "Press any key to continue..."
read -n 1 -s
echo ""
echo "6. Checking final configuration..."
if [ -f "config.xml" ]; then
echo "Final configuration:"
cat config.xml
else
echo "Configuration file not found"
fi
echo ""
echo "Test completed!"
-29
View File
@@ -1,29 +0,0 @@
# Build and Run Test
echo "Building QFlock with XML configuration..."
# Clean previous build
./clean.sh
# Build the project
./build.sh
# Check if build was successful
if [ $? -eq 0 ]; then
echo "Build successful! Testing XML configuration..."
# Show config file content
echo "Config file content:"
if [ -f "config.xml" ]; then
cat config.xml
else
echo "No config.xml found (will be created on first run)"
fi
echo ""
echo "You can now run the application:"
echo "./build/flock.app/Contents/MacOS/flock # macOS"
echo "./build/flock # Linux"
else
echo "Build failed!"
fi
+48
View File
@@ -0,0 +1,48 @@
#include "autostart_manager.h"
#include <QTemporaryDir>
#include <QtTest>
class AutoStartManagerTest : public QObject
{
Q_OBJECT
private slots:
void createsRunAtLoadLaunchAgent();
void roundTripsPlistRoundTrip();
};
void AutoStartManagerTest::createsRunAtLoadLaunchAgent()
{
const QString executablePath = "/Applications/DualFloatingClock.app/Contents/MacOS/DualFloatingClock";
const QString contents = AutoStartManager::launchAgentContents(executablePath);
QVERIFY(contents.contains("<key>ProgramArguments</key>"));
QVERIFY(contents.contains(executablePath));
QVERIFY(contents.contains("<key>RunAtLoad</key>"));
QVERIFY(contents.contains("<true/>"));
}
void AutoStartManagerTest::roundTripsPlistRoundTrip()
{
// Disabled-by-default: no plist written when enable is false,
// enableForPath + disable must leave no residue.
QTemporaryDir home;
QVERIFY(home.isValid());
qputenv("HOME", home.path().toUtf8());
QVERIFY(AutoStartManager::disable());
QVERIFY(!QFile::exists(AutoStartManager::launchAgentPath()));
QVERIFY(!AutoStartManager::isEnabled());
QVERIFY(AutoStartManager::enableForPath("/Applications/DualFloatingClock.app/Contents/MacOS/DualFloatingClock"));
QVERIFY(AutoStartManager::isEnabled());
QVERIFY(QFile::exists(AutoStartManager::launchAgentPath()));
QVERIFY(AutoStartManager::disable());
QVERIFY(!AutoStartManager::isEnabled());
}
QTEST_APPLESS_MAIN(AutoStartManagerTest)
#include "autostart_manager_test.moc"
+40
View File
@@ -0,0 +1,40 @@
#include "config_manager.h"
#include <QDir>
#include <QTemporaryDir>
#include <QtTest>
class ConfigManagerTest : public QObject
{
Q_OBJECT
private slots:
void roundTripsBothClockTimeZones();
void usesAnAbsoluteDefaultSettingsPath();
};
void ConfigManagerTest::roundTripsBothClockTimeZones()
{
QTemporaryDir directory;
QVERIFY(directory.isValid());
const QString filename = directory.filePath("config.xml");
ConfigManager writer;
writer.setPrimaryTimeZoneId("Europe/Bucharest");
writer.setSecondaryTimeZoneId("America/Los_Angeles");
QVERIFY(writer.saveSettings(filename));
ConfigManager reader;
QVERIFY(reader.loadSettings(filename));
QCOMPARE(reader.primaryTimeZoneId(), QString("Europe/Bucharest"));
QCOMPARE(reader.secondaryTimeZoneId(), QString("America/Los_Angeles"));
}
void ConfigManagerTest::usesAnAbsoluteDefaultSettingsPath()
{
QVERIFY(QDir::isAbsolutePath(ConfigManager::defaultSettingsPath()));
}
QTEST_APPLESS_MAIN(ConfigManagerTest)
#include "config_manager_test.moc"
+8
View File
@@ -30,7 +30,15 @@ void MacOSWindowHelper::setupAlwaysOnTopForFullscreen(QWidget *widget)
// Ensure it stays above dock and menu bar // Ensure it stays above dock and menu bar
[nativeWindow setLevel:CGWindowLevelForKey(kCGFloatingWindowLevelKey)]; [nativeWindow setLevel:CGWindowLevelForKey(kCGFloatingWindowLevelKey)];
// Kill the system window shadow — text must float with no halo.
[nativeWindow setHasShadow:NO];
} }
} }
void clouck_setActivationPolicyAccessory()
{
[NSApp setActivationPolicy:NSApplicationActivationPolicyAccessory];
}
#endif // Q_OS_MAC #endif // Q_OS_MAC
+2 -1
View File
@@ -5,7 +5,6 @@
#ifdef Q_OS_WIN #ifdef Q_OS_WIN
#include <Windows.h> #include <Windows.h>
#endif
class WindowHelperWindows class WindowHelperWindows
{ {
@@ -19,4 +18,6 @@ private:
static HWND getWindowHandle(QWidget *widget); static HWND getWindowHandle(QWidget *widget);
}; };
#endif // Q_OS_WIN
#endif // WINDOW_HELPER_WINDOWS_H #endif // WINDOW_HELPER_WINDOWS_H