#include "clock_widget.h" #include "config_manager.h" #include "window_helper.h" #include #include #include #include #include #include #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) { setupUI(); createMenu(); loadSettings(); // Set window properties for cross-platform always-on-top Qt::WindowFlags flags = Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint | Qt::Tool; #ifdef Q_OS_MAC // On macOS, we need additional flags for proper always-on-top behavior in fullscreen flags |= Qt::WindowDoesNotAcceptFocus; setAttribute(Qt::WA_ShowWithoutActivating); #endif 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 WindowHelper::setupAlwaysOnTopForFullscreen(this); // Start timer m_timer = new QTimer(this); connect(m_timer, &QTimer::timeout, this, &ClockWidget::updateTime); m_timer->start(1000); updateTime(); } ClockWidget::~ClockWidget() { // Settings are now saved immediately after changes, no need to save here } void ClockWidget::setupUI() { 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() { m_contextMenu = new QMenu(this); QAction *alwaysOnTopAction = new QAction("Always on Top", this); alwaysOnTopAction->setCheckable(true); alwaysOnTopAction->setChecked(true); connect(alwaysOnTopAction, &QAction::triggered, this, &ClockWidget::toggleAlwaysOnTop); QAction *fontColorAction = new QAction("Set Font Color", this); connect(fontColorAction, &QAction::triggered, this, &ClockWidget::changeFontColor); QAction *bgColorAction = new QAction("Set Background Color", this); connect(bgColorAction, &QAction::triggered, this, &ClockWidget::changeBackgroundColor); 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); QAction *secondaryTimeZoneAction = new QAction("Set Second Time Zone", this); connect(secondaryTimeZoneAction, &QAction::triggered, this, &ClockWidget::changeSecondaryTimeZone); QAction *resetAction = new QAction("Reset Settings", this); connect(resetAction, &QAction::triggered, this, &ClockWidget::resetSettings); QAction *quitAction = new QAction("Quit", this); connect(quitAction, &QAction::triggered, this, &ClockWidget::quitApplication); m_contextMenu->addAction(alwaysOnTopAction); 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); m_contextMenu->addSeparator(); m_contextMenu->addAction(resetAction); m_contextMenu->addAction(quitAction); } void ClockWidget::mousePressEvent(QMouseEvent *event) { if (event->button() == Qt::LeftButton) { if (isInResizeArea(event->pos())) { m_resizing = true; m_resizeStartPos = event->globalPosition().toPoint(); m_resizeStartSize = size(); event->accept(); } else { m_dragging = true; m_dragPosition = event->globalPosition().toPoint() - frameGeometry().topLeft(); event->accept(); } } } void ClockWidget::mouseMoveEvent(QMouseEvent *event) { if (m_resizing && (event->buttons() & Qt::LeftButton)) { QPoint delta = event->globalPosition().toPoint() - m_resizeStartPos; QSize newSize = m_resizeStartSize + QSize(delta.x(), delta.y()); // Minimum size constraints newSize = newSize.expandedTo(QSize(100, 50)); resize(newSize); event->accept(); } else if (m_dragging && (event->buttons() & Qt::LeftButton)) { move(event->globalPosition().toPoint() - m_dragPosition); event->accept(); } else { // Update cursor shape when hovering over resize area if (isInResizeArea(event->pos())) { setCursor(Qt::SizeFDiagCursor); } else { setCursor(Qt::ArrowCursor); } } } void ClockWidget::mouseReleaseEvent(QMouseEvent *event) { if (event->button() == Qt::LeftButton) { if (m_resizing) { m_resizing = false; // Save the new size and position to config if (m_configManager) { m_configManager->setWindowPosition(pos()); m_configManager->setWindowSize(size()); m_configManager->saveSettings(); } event->accept(); } else if (m_dragging) { m_dragging = false; // Save the new position to config if (m_configManager) { m_configManager->setWindowPosition(pos()); m_configManager->saveSettings(); } event->accept(); } } } void ClockWidget::contextMenuEvent(QContextMenuEvent *event) { 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() { 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() { m_alwaysOnTop = !m_alwaysOnTop; Qt::WindowFlags flags = windowFlags(); if (m_alwaysOnTop) { flags |= Qt::WindowStaysOnTopHint; } else { flags &= ~Qt::WindowStaysOnTopHint; } setWindowFlags(flags); show(); // Re-apply platform-specific window properties after changing window flags if (m_alwaysOnTop) { WindowHelper::setupAlwaysOnTopForFullscreen(this); } } void ClockWidget::changeFontColor() { const std::optional color = pickColor("Font Color", m_fontColor, this); if (color) { m_fontColor = *color; m_configManager->setFontColor(m_fontColor); updateStyleSheet(); saveSettings(); } } void ClockWidget::changeBackgroundColor() { const std::optional color = pickColor("Background Color", m_backgroundColor, this); if (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 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(); } } void ClockWidget::changePrimaryTimeZone() { changeTimeZone(true); } void ClockWidget::changeSecondaryTimeZone() { changeTimeZone(false); } void ClockWidget::resetSettings() { int ret = QMessageBox::question(this, "Reset Settings", "Are you sure you want to reset all settings?"); if (ret == QMessageBox::Yes) { m_configManager->resetToDefaults(); loadSettings(); updateStyleSheet(); // Reset to default size resize(m_configManager->windowSize()); } } void ClockWidget::quitApplication() { qApp->quit(); } bool ClockWidget::isInResizeArea(const QPoint &pos) const { // Check if mouse is in the bottom-right corner resize area QRect resizeArea(rect().bottomRight() - QPoint(m_resizeBorder * 2, m_resizeBorder * 2), QSize(m_resizeBorder * 2, m_resizeBorder * 2)); 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() { m_configManager = new ConfigManager(this); // Load from XML config file if (!m_configManager->loadSettings()) { // If loading fails, config manager already has default values qDebug() << "Using default settings"; } // 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(); // Restore window position and size 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()); // Update always on top checkbox in menu QList actions = m_contextMenu->actions(); for (QAction *action : actions) { if (action->text() == QString("Always on Top")) { action->setChecked(m_alwaysOnTop); break; } } updateStyleSheet(); } void ClockWidget::saveSettings() { if (!m_configManager) return; // 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()); m_configManager->setPrimaryTimeZoneId(m_primaryTimeZoneId); m_configManager->setSecondaryTimeZoneId(m_secondaryTimeZoneId); // Save to XML file if (!m_configManager->saveSettings()) { qDebug() << "Failed to save settings to config.xml"; } } void ClockWidget::updateStyleSheet() { // 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(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) { 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 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); }