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
This commit is contained in:
@@ -91,3 +91,9 @@ Icon
|
|||||||
*.dmg
|
*.dmg
|
||||||
build
|
build
|
||||||
test*
|
test*
|
||||||
|
!tests/
|
||||||
|
!tests/*.cpp
|
||||||
|
|
||||||
|
# AW
|
||||||
|
.aw/
|
||||||
|
CLAUDE.md
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ project(Clouck)
|
|||||||
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
|
||||||
@@ -73,3 +77,27 @@ install(TARGETS Clouck
|
|||||||
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()
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
#include "autostart_manager.h"
|
||||||
|
|
||||||
|
#include <QCoreApplication>
|
||||||
|
#include <QDir>
|
||||||
|
#include <QFileInfo>
|
||||||
|
#include <QSaveFile>
|
||||||
|
#include <QStandardPaths>
|
||||||
|
#include <QXmlStreamWriter>
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
const QString launchAgentLabel = QStringLiteral("com.clouck.clock");
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AutoStartManager::enableForCurrentApplication()
|
||||||
|
{
|
||||||
|
#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(QCoreApplication::applicationFilePath()).toUtf8());
|
||||||
|
return agentFile.commit();
|
||||||
|
#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");
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
#ifndef AUTOSTART_MANAGER_H
|
||||||
|
#define AUTOSTART_MANAGER_H
|
||||||
|
|
||||||
|
#include <QString>
|
||||||
|
|
||||||
|
class AutoStartManager
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
static bool enableForCurrentApplication();
|
||||||
|
static QString launchAgentContents(const QString &executablePath);
|
||||||
|
|
||||||
|
private:
|
||||||
|
static QString launchAgentPath();
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // AUTOSTART_MANAGER_H
|
||||||
+94
-7
@@ -2,13 +2,14 @@
|
|||||||
#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 <QScreen>
|
||||||
|
#include <QTimeZone>
|
||||||
|
|
||||||
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)
|
||||||
@@ -48,11 +49,14 @@ ClockWidget::~ClockWidget()
|
|||||||
|
|
||||||
void ClockWidget::setupUI()
|
void ClockWidget::setupUI()
|
||||||
{
|
{
|
||||||
m_timeLabel = new QLabel(this);
|
m_primaryTimeLabel = new QLabel(this);
|
||||||
m_timeLabel->setAlignment(Qt::AlignCenter);
|
m_secondaryTimeLabel = new QLabel(this);
|
||||||
|
m_primaryTimeLabel->setAlignment(Qt::AlignCenter);
|
||||||
|
m_secondaryTimeLabel->setAlignment(Qt::AlignCenter);
|
||||||
|
|
||||||
QVBoxLayout *layout = new QVBoxLayout(this);
|
QVBoxLayout *layout = new QVBoxLayout(this);
|
||||||
layout->addWidget(m_timeLabel);
|
layout->addWidget(m_primaryTimeLabel);
|
||||||
|
layout->addWidget(m_secondaryTimeLabel);
|
||||||
layout->setContentsMargins(10, 10, 10, 10);
|
layout->setContentsMargins(10, 10, 10, 10);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,6 +78,12 @@ void ClockWidget::createMenu()
|
|||||||
QAction *fontSizeAction = new QAction("Set Font Size", this);
|
QAction *fontSizeAction = new QAction("Set Font Size", this);
|
||||||
connect(fontSizeAction, &QAction::triggered, this, &ClockWidget::changeFontSize);
|
connect(fontSizeAction, &QAction::triggered, this, &ClockWidget::changeFontSize);
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
@@ -86,6 +96,9 @@ void ClockWidget::createMenu()
|
|||||||
m_contextMenu->addAction(bgColorAction);
|
m_contextMenu->addAction(bgColorAction);
|
||||||
m_contextMenu->addAction(fontSizeAction);
|
m_contextMenu->addAction(fontSizeAction);
|
||||||
m_contextMenu->addSeparator();
|
m_contextMenu->addSeparator();
|
||||||
|
m_contextMenu->addAction(primaryTimeZoneAction);
|
||||||
|
m_contextMenu->addAction(secondaryTimeZoneAction);
|
||||||
|
m_contextMenu->addSeparator();
|
||||||
m_contextMenu->addAction(resetAction);
|
m_contextMenu->addAction(resetAction);
|
||||||
m_contextMenu->addAction(quitAction);
|
m_contextMenu->addAction(quitAction);
|
||||||
}
|
}
|
||||||
@@ -179,8 +192,8 @@ void ClockWidget::contextMenuEvent(QContextMenuEvent *event)
|
|||||||
|
|
||||||
void ClockWidget::updateTime()
|
void ClockWidget::updateTime()
|
||||||
{
|
{
|
||||||
QString currentTime = QTime::currentTime().toString("HH:mm:ss");
|
m_primaryTimeLabel->setText(formattedTime(m_primaryTimeZoneId));
|
||||||
m_timeLabel->setText(currentTime);
|
m_secondaryTimeLabel->setText(formattedTime(m_secondaryTimeZoneId));
|
||||||
}
|
}
|
||||||
|
|
||||||
void ClockWidget::toggleAlwaysOnTop()
|
void ClockWidget::toggleAlwaysOnTop()
|
||||||
@@ -240,6 +253,16 @@ void ClockWidget::changeFontSize()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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?");
|
||||||
@@ -304,6 +327,8 @@ void ClockWidget::loadSettings()
|
|||||||
m_backgroundColor = m_configManager->backgroundColor();
|
m_backgroundColor = m_configManager->backgroundColor();
|
||||||
m_fontSize = m_configManager->fontSize();
|
m_fontSize = m_configManager->fontSize();
|
||||||
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
|
||||||
QPoint windowPos = m_configManager->windowPosition();
|
QPoint windowPos = m_configManager->windowPosition();
|
||||||
@@ -344,6 +369,8 @@ void ClockWidget::saveSettings()
|
|||||||
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())
|
||||||
@@ -357,6 +384,7 @@ void ClockWidget::updateStyleSheet()
|
|||||||
QString styleSheet = QString(
|
QString styleSheet = QString(
|
||||||
"QLabel { "
|
"QLabel { "
|
||||||
"color: %1; "
|
"color: %1; "
|
||||||
|
"font-family: 'SF Pro Rounded'; "
|
||||||
"font-size: %2pt; "
|
"font-size: %2pt; "
|
||||||
"font-weight: bold; "
|
"font-weight: bold; "
|
||||||
"background-color: %3; "
|
"background-color: %3; "
|
||||||
@@ -367,6 +395,65 @@ void ClockWidget::updateStyleSheet()
|
|||||||
QString::number(m_fontSize),
|
QString::number(m_fontSize),
|
||||||
m_backgroundColor.name(QColor::HexArgb));
|
m_backgroundColor.name(QColor::HexArgb));
|
||||||
|
|
||||||
m_timeLabel->setStyleSheet(styleSheet);
|
m_primaryTimeLabel->setStyleSheet(styleSheet);
|
||||||
|
m_secondaryTimeLabel->setStyleSheet(styleSheet);
|
||||||
setStyleSheet(QString("background-color: transparent;"));
|
setStyleSheet(QString("background-color: transparent;"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|||||||
+8
-1
@@ -31,6 +31,8 @@ private slots:
|
|||||||
void changeFontColor();
|
void changeFontColor();
|
||||||
void changeBackgroundColor();
|
void changeBackgroundColor();
|
||||||
void changeFontSize();
|
void changeFontSize();
|
||||||
|
void changePrimaryTimeZone();
|
||||||
|
void changeSecondaryTimeZone();
|
||||||
void resetSettings();
|
void resetSettings();
|
||||||
void quitApplication();
|
void quitApplication();
|
||||||
|
|
||||||
@@ -40,8 +42,11 @@ private:
|
|||||||
void loadSettings();
|
void loadSettings();
|
||||||
void saveSettings();
|
void saveSettings();
|
||||||
void updateStyleSheet();
|
void updateStyleSheet();
|
||||||
|
void changeTimeZone(bool primaryClock);
|
||||||
|
QString formattedTime(const QString &timeZoneId) const;
|
||||||
|
|
||||||
QLabel *m_timeLabel;
|
QLabel *m_primaryTimeLabel;
|
||||||
|
QLabel *m_secondaryTimeLabel;
|
||||||
QTimer *m_timer;
|
QTimer *m_timer;
|
||||||
QMenu *m_contextMenu;
|
QMenu *m_contextMenu;
|
||||||
|
|
||||||
@@ -61,6 +66,8 @@ private:
|
|||||||
QColor m_backgroundColor;
|
QColor m_backgroundColor;
|
||||||
int m_fontSize;
|
int m_fontSize;
|
||||||
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;
|
||||||
|
|||||||
+5
-3
@@ -1,9 +1,11 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<ClouckConfig>
|
<ClouckConfig>
|
||||||
<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>1236,784</WindowPosition>
|
<WindowPosition>1236,784</WindowPosition>
|
||||||
<WindowSize>137,59</WindowSize>
|
<WindowSize>240,160</WindowSize>
|
||||||
|
<PrimaryTimeZone>Europe/Bucharest</PrimaryTimeZone>
|
||||||
|
<SecondaryTimeZone>America/Los_Angeles</SecondaryTimeZone>
|
||||||
</ClouckConfig>
|
</ClouckConfig>
|
||||||
|
|||||||
+55
-6
@@ -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";
|
||||||
@@ -77,6 +99,14 @@ 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
|
else
|
||||||
{
|
{
|
||||||
reader.skipCurrentElement();
|
reader.skipCurrentElement();
|
||||||
@@ -96,7 +126,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();
|
||||||
@@ -134,6 +177,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,13 +196,14 @@ 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;
|
||||||
|
|
||||||
@@ -164,5 +211,7 @@ void ConfigManager::setDefaultValues()
|
|||||||
// Position will be set properly when widget is shown for the first time
|
// 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_windowPosition = QPoint(-1, -1); // Special value indicating "not set yet"
|
||||||
|
|
||||||
m_windowSize = QSize(200, 80); // Default clock size
|
m_windowSize = QSize(240, 160); // Default clock size
|
||||||
|
m_primaryTimeZoneId = QString::fromUtf8(QTimeZone::systemTimeZoneId());
|
||||||
|
m_secondaryTimeZoneId = QStringLiteral("America/Los_Angeles");
|
||||||
}
|
}
|
||||||
|
|||||||
+11
-3
@@ -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,8 @@ 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; }
|
||||||
|
|
||||||
// Setters
|
// Setters
|
||||||
void setFontColor(const QColor &color) { m_fontColor = color; }
|
void setFontColor(const QColor &color) { m_fontColor = color; }
|
||||||
@@ -37,12 +41,14 @@ 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; }
|
||||||
|
|
||||||
// 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 +61,8 @@ 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;
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // CONFIG_MANAGER_H
|
#endif // CONFIG_MANAGER_H
|
||||||
|
|||||||
@@ -1,6 +1,45 @@
|
|||||||
#include <QApplication>
|
#include <QApplication>
|
||||||
|
#include <QAction>
|
||||||
|
#include <QMenu>
|
||||||
|
#include <QPainter>
|
||||||
|
#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)
|
||||||
|
{
|
||||||
|
QPixmap pixmap(trayIconExtent, trayIconExtent);
|
||||||
|
pixmap.setDevicePixelRatio(qApp->devicePixelRatio());
|
||||||
|
pixmap.fill(Qt::transparent);
|
||||||
|
|
||||||
|
QPainter painter(&pixmap);
|
||||||
|
painter.setRenderHint(QPainter::Antialiasing);
|
||||||
|
painter.setPen(QPen(color, 1.5));
|
||||||
|
painter.drawEllipse(QPointF(8, 8), 5.5, 5.5);
|
||||||
|
painter.drawLine(QPointF(8, 4.5), QPointF(8, 8));
|
||||||
|
painter.drawLine(QPointF(8, 8), QPointF(10.5, 9.5));
|
||||||
|
|
||||||
|
QIcon icon(pixmap);
|
||||||
|
#ifdef Q_OS_MAC
|
||||||
|
// Template image: the system tints it black/white to match the
|
||||||
|
// active appearance, like native menu bar icons.
|
||||||
|
icon.setIsMask(true);
|
||||||
|
#endif
|
||||||
|
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);
|
||||||
@@ -12,5 +51,46 @@ int main(int argc, char *argv[])
|
|||||||
ClockWidget clock;
|
ClockWidget clock;
|
||||||
clock.show();
|
clock.show();
|
||||||
|
|
||||||
|
AutoStartManager::enableForCurrentApplication();
|
||||||
|
|
||||||
|
const QStyleHints *styleHints = app.styleHints();
|
||||||
|
QSystemTrayIcon trayIcon(createTrayIcon(trayIconColorForScheme(styleHints->colorScheme())), &app);
|
||||||
|
trayIcon.setToolTip("Clouck");
|
||||||
|
|
||||||
|
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 *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);
|
||||||
|
QObject::connect(&trayIcon, &QSystemTrayIcon::activated, &app,
|
||||||
|
[toggleClock](QSystemTrayIcon::ActivationReason reason) {
|
||||||
|
if (reason == QSystemTrayIcon::Trigger)
|
||||||
|
{
|
||||||
|
toggleClock();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
trayIcon.setContextMenu(&trayMenu);
|
||||||
|
trayIcon.show();
|
||||||
|
|
||||||
return app.exec();
|
return app.exec();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
#include "autostart_manager.h"
|
||||||
|
|
||||||
|
#include <QtTest>
|
||||||
|
|
||||||
|
class AutoStartManagerTest : public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
private slots:
|
||||||
|
void createsRunAtLoadLaunchAgent();
|
||||||
|
};
|
||||||
|
|
||||||
|
void AutoStartManagerTest::createsRunAtLoadLaunchAgent()
|
||||||
|
{
|
||||||
|
const QString executablePath = "/Applications/Clouck.app/Contents/MacOS/Clouck";
|
||||||
|
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/>"));
|
||||||
|
}
|
||||||
|
|
||||||
|
QTEST_APPLESS_MAIN(AutoStartManagerTest)
|
||||||
|
|
||||||
|
#include "autostart_manager_test.moc"
|
||||||
@@ -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"
|
||||||
Reference in New Issue
Block a user