From d9008479c1b6dae650d326bb8fa4404befc2138b Mon Sep 17 00:00:00 2001 From: Sebastian Petrescu Date: Wed, 19 Aug 2026 22:41:33 +0300 Subject: [PATCH] feat: tray-first UX, text outline, start-at-login toggle, font max 200 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- autostart_manager.cpp | 35 ++++- autostart_manager.h | 5 +- clock_widget.cpp | 251 +++++++++++++++++++++++++++---- clock_widget.h | 36 ++++- config_manager.cpp | 26 +++- config_manager.h | 6 + main.cpp | 79 +++++++--- tests/autostart_manager_test.cpp | 22 +++ window_helper_macos.mm | 3 + 9 files changed, 402 insertions(+), 61 deletions(-) diff --git a/autostart_manager.cpp b/autostart_manager.cpp index e870327..fbbe8a1 100644 --- a/autostart_manager.cpp +++ b/autostart_manager.cpp @@ -14,6 +14,15 @@ const QString launchAgentLabel = QStringLiteral("com.clouck.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()) @@ -33,8 +42,32 @@ bool AutoStartManager::enableForCurrentApplication() return false; } - agentFile.write(launchAgentContents(QCoreApplication::applicationFilePath()).toUtf8()); + 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 diff --git a/autostart_manager.h b/autostart_manager.h index f0ab9ed..f84907e 100644 --- a/autostart_manager.h +++ b/autostart_manager.h @@ -7,9 +7,10 @@ class AutoStartManager { public: static bool enableForCurrentApplication(); + static bool enableForPath(const QString &executablePath); + static bool disable(); + static bool isEnabled(); static QString launchAgentContents(const QString &executablePath); - -private: static QString launchAgentPath(); }; diff --git a/clock_widget.cpp b/clock_widget.cpp index 51392fd..4be65b6 100644 --- a/clock_widget.cpp +++ b/clock_widget.cpp @@ -10,6 +10,69 @@ #include #include #include +#include +#include +#include + +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) : QWidget(parent), m_dragging(false), m_resizing(false), m_resizeBorder(5) @@ -29,6 +92,7 @@ ClockWidget::ClockWidget(QWidget *parent) setWindowFlags(flags); setAttribute(Qt::WA_TranslucentBackground); + setAttribute(Qt::WA_StyledBackground); // required for widget-level QSS background/border setMouseTracking(true); // Enable mouse tracking for resize cursor // Set up platform-specific window properties after window creation @@ -49,15 +113,18 @@ ClockWidget::~ClockWidget() void ClockWidget::setupUI() { - m_primaryTimeLabel = new QLabel(this); - m_secondaryTimeLabel = new QLabel(this); - m_primaryTimeLabel->setAlignment(Qt::AlignCenter); - m_secondaryTimeLabel->setAlignment(Qt::AlignCenter); + m_primaryTimeLabel = new ClockLabel(this); + m_primaryLocationLabel = new ClockLabel(this); + m_secondaryTimeLabel = new ClockLabel(this); + m_secondaryLocationLabel = new ClockLabel(this); QVBoxLayout *layout = new QVBoxLayout(this); layout->addWidget(m_primaryTimeLabel); + layout->addWidget(m_primaryLocationLabel); layout->addWidget(m_secondaryTimeLabel); + layout->addWidget(m_secondaryLocationLabel); layout->setContentsMargins(10, 10, 10, 10); + layout->setSpacing(2); } void ClockWidget::createMenu() @@ -75,9 +142,15 @@ void ClockWidget::createMenu() QAction *bgColorAction = new QAction("Set Background Color", this); 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); + 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); @@ -94,7 +167,10 @@ void ClockWidget::createMenu() m_contextMenu->addSeparator(); m_contextMenu->addAction(fontColorAction); m_contextMenu->addAction(bgColorAction); + m_contextMenu->addAction(borderColorAction); + m_contextMenu->addSeparator(); m_contextMenu->addAction(fontSizeAction); + m_contextMenu->addAction(locationFontSizeAction); m_contextMenu->addSeparator(); m_contextMenu->addAction(primaryTimeZoneAction); m_contextMenu->addAction(secondaryTimeZoneAction); @@ -187,13 +263,52 @@ void ClockWidget::mouseReleaseEvent(QMouseEvent *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() { - m_primaryTimeLabel->setText(formattedTime(m_primaryTimeZoneId)); - m_secondaryTimeLabel->setText(formattedTime(m_secondaryTimeZoneId)); + const QTimeZone primaryTimeZone(m_primaryTimeZoneId.toUtf8()); + 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() @@ -221,35 +336,63 @@ void ClockWidget::toggleAlwaysOnTop() void ClockWidget::changeFontColor() { - QColor color = QColorDialog::getColor(m_fontColor, this, "Choose Font Color"); - if (color.isValid()) + const std::optional color = pickColor("Font Color", m_fontColor, this); + if (color) { - m_fontColor = color; + m_fontColor = *color; m_configManager->setFontColor(m_fontColor); updateStyleSheet(); + saveSettings(); } } void ClockWidget::changeBackgroundColor() { - QColor color = QColorDialog::getColor(m_backgroundColor, this, "Choose Background Color"); - if (color.isValid()) + const std::optional color = pickColor("Background Color", m_backgroundColor, this); + if (color) { - m_backgroundColor = color; + m_backgroundColor = *color; m_configManager->setBackgroundColor(m_backgroundColor); updateStyleSheet(); + saveSettings(); + } +} + +void ClockWidget::changeBorderColor() +{ + const std::optional color = pickColor("Border Color", m_borderColor, this); + if (color) + { + m_borderColor = *color; + m_configManager->setBorderColor(m_borderColor); + updateStyleSheet(); + saveSettings(); } } void ClockWidget::changeFontSize() { 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) { m_fontSize = size; m_configManager->setFontSize(m_fontSize); 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(); } } @@ -325,7 +468,9 @@ void ClockWidget::loadSettings() // Apply loaded settings m_fontColor = m_configManager->fontColor(); m_backgroundColor = m_configManager->backgroundColor(); + m_borderColor = m_configManager->borderColor(); m_fontSize = m_configManager->fontSize(); + m_locationFontSize = m_configManager->locationFontSize(); m_alwaysOnTop = m_configManager->alwaysOnTop(); m_primaryTimeZoneId = m_configManager->primaryTimeZoneId(); m_secondaryTimeZoneId = m_configManager->secondaryTimeZoneId(); @@ -365,7 +510,9 @@ void ClockWidget::saveSettings() // Update config manager with current values m_configManager->setFontColor(m_fontColor); m_configManager->setBackgroundColor(m_backgroundColor); + m_configManager->setBorderColor(m_borderColor); m_configManager->setFontSize(m_fontSize); + m_configManager->setLocationFontSize(m_locationFontSize); m_configManager->setAlwaysOnTop(m_alwaysOnTop); m_configManager->setWindowPosition(pos()); m_configManager->setWindowSize(size()); @@ -381,23 +528,29 @@ void ClockWidget::saveSettings() void ClockWidget::updateStyleSheet() { - QString styleSheet = QString( - "QLabel { " - "color: %1; " - "font-family: 'SF Pro Rounded'; " - "font-size: %2pt; " - "font-weight: bold; " - "background-color: %3; " - "border-radius: 10px; " - "padding: 5px; " - "}") - .arg(m_fontColor.name(), - QString::number(m_fontSize), - m_backgroundColor.name(QColor::HexArgb)); + // Fonts via QSS; glyph fill + outline via ClockLabel::paintEvent. + const auto labelFontStyleSheet = [](int size) { + return QString("QLabel { font-family: 'SF Pro Rounded'; font-size: %1pt; background: transparent; }") + .arg(size); + }; - m_primaryTimeLabel->setStyleSheet(styleSheet); - m_secondaryTimeLabel->setStyleSheet(styleSheet); - setStyleSheet(QString("background-color: transparent;")); + m_primaryTimeLabel->setStyleSheet(labelFontStyleSheet(m_fontSize)); + 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) @@ -457,3 +610,39 @@ QString ClockWidget::formattedTime(const QString &timeZoneId) const 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 ClockWidget::pickColor(const QString &title, const QColor ¤t, 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); +} diff --git a/clock_widget.h b/clock_widget.h index afac2bd..8c2f289 100644 --- a/clock_widget.h +++ b/clock_widget.h @@ -8,9 +8,30 @@ #include #include #include +#include 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 { Q_OBJECT @@ -19,18 +40,23 @@ public: explicit ClockWidget(QWidget *parent = nullptr); ~ClockWidget(); + QMenu *menu() const { return m_contextMenu; } + protected: void mousePressEvent(QMouseEvent *event) override; void mouseMoveEvent(QMouseEvent *event) override; void mouseReleaseEvent(QMouseEvent *event) override; void contextMenuEvent(QContextMenuEvent *event) override; + void paintEvent(QPaintEvent *event) override; private slots: void updateTime(); void toggleAlwaysOnTop(); void changeFontColor(); void changeBackgroundColor(); + void changeBorderColor(); void changeFontSize(); + void changeLocationFontSize(); void changePrimaryTimeZone(); void changeSecondaryTimeZone(); void resetSettings(); @@ -44,9 +70,13 @@ private: void updateStyleSheet(); void changeTimeZone(bool primaryClock); QString formattedTime(const QString &timeZoneId) const; + QString locationName(const QString &timeZoneId) const; + std::optional pickColor(const QString &title, const QColor ¤t, QWidget *parent); - QLabel *m_primaryTimeLabel; - QLabel *m_secondaryTimeLabel; + ClockLabel *m_primaryTimeLabel; + ClockLabel *m_primaryLocationLabel; + ClockLabel *m_secondaryTimeLabel; + ClockLabel *m_secondaryLocationLabel; QTimer *m_timer; QMenu *m_contextMenu; @@ -64,7 +94,9 @@ private: ConfigManager *m_configManager; QColor m_fontColor; QColor m_backgroundColor; + QColor m_borderColor; int m_fontSize; + int m_locationFontSize; bool m_alwaysOnTop; QString m_primaryTimeZoneId; QString m_secondaryTimeZoneId; diff --git a/config_manager.cpp b/config_manager.cpp index 6ccf54f..ef7d867 100644 --- a/config_manager.cpp +++ b/config_manager.cpp @@ -107,6 +107,14 @@ bool ConfigManager::loadSettings(const QString &filename) { m_secondaryTimeZoneId = text; } + else if (name == "BorderColor") + { + m_borderColor = QColor(text); + } + else if (name == "LocationFontSize") + { + m_locationFontSize = text.toInt(); + } else { reader.skipCurrentElement(); @@ -153,14 +161,18 @@ bool ConfigManager::saveSettings(const QString &filename) // Root element writer.writeStartElement("ClouckConfig"); - // Font color - writer.writeTextElement("FontColor", m_fontColor.name(QColor::HexArgb)); + // Colors: store #RRGGBB when fully opaque, #AARRGGBB when translucent — + // 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 - writer.writeTextElement("BackgroundColor", m_backgroundColor.name(QColor::HexArgb)); - - // Font size + // Font sizes writer.writeTextElement("FontSize", QString::number(m_fontSize)); + writer.writeTextElement("LocationFontSize", QString::number(m_locationFontSize)); // Always on top writer.writeTextElement("AlwaysOnTop", m_alwaysOnTop ? "true" : "false"); @@ -214,4 +226,6 @@ void ConfigManager::setDefaultValues() 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 } diff --git a/config_manager.h b/config_manager.h index 7e2a654..2741790 100644 --- a/config_manager.h +++ b/config_manager.h @@ -33,6 +33,8 @@ public: 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 void setFontColor(const QColor &color) { m_fontColor = color; } @@ -43,6 +45,8 @@ public: 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 void resetToDefaults(); @@ -63,6 +67,8 @@ private: QSize m_windowSize; QString m_primaryTimeZoneId; QString m_secondaryTimeZoneId; + QColor m_borderColor; + int m_locationFontSize; }; #endif // CONFIG_MANAGER_H diff --git a/main.cpp b/main.cpp index 70c8175..049d642 100644 --- a/main.cpp +++ b/main.cpp @@ -2,6 +2,7 @@ #include #include #include +#include #include #include #include "autostart_manager.h" @@ -14,23 +15,27 @@ constexpr int trayIconExtent = 16; QIcon createTrayIcon(const QColor &color) { - QPixmap pixmap(trayIconExtent, trayIconExtent); - pixmap.setDevicePixelRatio(qApp->devicePixelRatio()); + // 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)); - 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 + // 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; } @@ -51,7 +56,20 @@ int main(int argc, char *argv[]) ClockWidget clock; clock.show(); - AutoStartManager::enableForCurrentApplication(); + // Start at Login: honor the persisted choice instead of always forcing it. + { + QSettings autoStartSettings("Clouck", "Clouck"); + 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); @@ -64,6 +82,36 @@ int main(int argc, char *argv[]) QMenu trayMenu; QAction *toggleClockAction = trayMenu.addAction("Turn Clock Off"); + + QAction *startAtLoginAction = trayMenu.addAction("Start at Login"); + { + QSettings autoStartSettings("Clouck", "Clouck"); + startAtLoginAction->setCheckable(true); + startAtLoginAction->setChecked(autoStartSettings.value("startAtLogin", true).toBool()); + } + QObject::connect(startAtLoginAction, &QAction::toggled, &app, [](bool checked) { + QSettings autoStartSettings("Clouck", "Clouck"); + 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 clockActions = clock.menu()->actions(); + for (QAction *action : clockActions) + { + trayMenu.addAction(action); + } + + trayMenu.addSeparator(); QAction *quitAction = trayMenu.addAction("Quit"); const auto toggleClock = [&clock, toggleClockAction]() { @@ -81,13 +129,6 @@ int main(int argc, char *argv[]) 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(); diff --git a/tests/autostart_manager_test.cpp b/tests/autostart_manager_test.cpp index 55eda3f..2a2ccd3 100644 --- a/tests/autostart_manager_test.cpp +++ b/tests/autostart_manager_test.cpp @@ -1,5 +1,6 @@ #include "autostart_manager.h" +#include #include class AutoStartManagerTest : public QObject @@ -8,6 +9,7 @@ class AutoStartManagerTest : public QObject private slots: void createsRunAtLoadLaunchAgent(); + void roundTripsPlistRoundTrip(); }; void AutoStartManagerTest::createsRunAtLoadLaunchAgent() @@ -21,6 +23,26 @@ void AutoStartManagerTest::createsRunAtLoadLaunchAgent() QVERIFY(contents.contains("")); } +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/Clouck.app/Contents/MacOS/Clouck")); + QVERIFY(AutoStartManager::isEnabled()); + QVERIFY(QFile::exists(AutoStartManager::launchAgentPath())); + + QVERIFY(AutoStartManager::disable()); + QVERIFY(!AutoStartManager::isEnabled()); +} + QTEST_APPLESS_MAIN(AutoStartManagerTest) #include "autostart_manager_test.moc" diff --git a/window_helper_macos.mm b/window_helper_macos.mm index e5be438..ee7f957 100644 --- a/window_helper_macos.mm +++ b/window_helper_macos.mm @@ -30,6 +30,9 @@ void MacOSWindowHelper::setupAlwaysOnTopForFullscreen(QWidget *widget) // Ensure it stays above dock and menu bar [nativeWindow setLevel:CGWindowLevelForKey(kCGFloatingWindowLevelKey)]; + + // Kill the system window shadow — text must float with no halo. + [nativeWindow setHasShadow:NO]; } }