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
This commit is contained in:
+34
-1
@@ -14,6 +14,15 @@ const QString launchAgentLabel = QStringLiteral("com.clouck.clock");
|
|||||||
|
|
||||||
bool AutoStartManager::enableForCurrentApplication()
|
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
|
#ifdef Q_OS_MAC
|
||||||
const QString agentPath = launchAgentPath();
|
const QString agentPath = launchAgentPath();
|
||||||
if (agentPath.isEmpty())
|
if (agentPath.isEmpty())
|
||||||
@@ -33,8 +42,32 @@ bool AutoStartManager::enableForCurrentApplication()
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
agentFile.write(launchAgentContents(QCoreApplication::applicationFilePath()).toUtf8());
|
agentFile.write(launchAgentContents(executablePath).toUtf8());
|
||||||
return agentFile.commit();
|
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
|
#else
|
||||||
return false;
|
return false;
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
+3
-2
@@ -7,9 +7,10 @@ class AutoStartManager
|
|||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
static bool enableForCurrentApplication();
|
static bool enableForCurrentApplication();
|
||||||
|
static bool enableForPath(const QString &executablePath);
|
||||||
|
static bool disable();
|
||||||
|
static bool isEnabled();
|
||||||
static QString launchAgentContents(const QString &executablePath);
|
static QString launchAgentContents(const QString &executablePath);
|
||||||
|
|
||||||
private:
|
|
||||||
static QString launchAgentPath();
|
static QString launchAgentPath();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+220
-31
@@ -10,6 +10,69 @@
|
|||||||
#include <QApplication>
|
#include <QApplication>
|
||||||
#include <QScreen>
|
#include <QScreen>
|
||||||
#include <QTimeZone>
|
#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)
|
||||||
@@ -29,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
|
||||||
@@ -49,15 +113,18 @@ ClockWidget::~ClockWidget()
|
|||||||
|
|
||||||
void ClockWidget::setupUI()
|
void ClockWidget::setupUI()
|
||||||
{
|
{
|
||||||
m_primaryTimeLabel = new QLabel(this);
|
m_primaryTimeLabel = new ClockLabel(this);
|
||||||
m_secondaryTimeLabel = new QLabel(this);
|
m_primaryLocationLabel = new ClockLabel(this);
|
||||||
m_primaryTimeLabel->setAlignment(Qt::AlignCenter);
|
m_secondaryTimeLabel = new ClockLabel(this);
|
||||||
m_secondaryTimeLabel->setAlignment(Qt::AlignCenter);
|
m_secondaryLocationLabel = new ClockLabel(this);
|
||||||
|
|
||||||
QVBoxLayout *layout = new QVBoxLayout(this);
|
QVBoxLayout *layout = new QVBoxLayout(this);
|
||||||
layout->addWidget(m_primaryTimeLabel);
|
layout->addWidget(m_primaryTimeLabel);
|
||||||
|
layout->addWidget(m_primaryLocationLabel);
|
||||||
layout->addWidget(m_secondaryTimeLabel);
|
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()
|
||||||
@@ -75,9 +142,15 @@ 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);
|
QAction *primaryTimeZoneAction = new QAction("Set First Time Zone", this);
|
||||||
connect(primaryTimeZoneAction, &QAction::triggered, this, &ClockWidget::changePrimaryTimeZone);
|
connect(primaryTimeZoneAction, &QAction::triggered, this, &ClockWidget::changePrimaryTimeZone);
|
||||||
|
|
||||||
@@ -94,7 +167,10 @@ 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->addSeparator();
|
||||||
m_contextMenu->addAction(primaryTimeZoneAction);
|
m_contextMenu->addAction(primaryTimeZoneAction);
|
||||||
m_contextMenu->addAction(secondaryTimeZoneAction);
|
m_contextMenu->addAction(secondaryTimeZoneAction);
|
||||||
@@ -187,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()
|
||||||
{
|
{
|
||||||
m_primaryTimeLabel->setText(formattedTime(m_primaryTimeZoneId));
|
const QTimeZone primaryTimeZone(m_primaryTimeZoneId.toUtf8());
|
||||||
m_secondaryTimeLabel->setText(formattedTime(m_secondaryTimeZoneId));
|
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()
|
||||||
@@ -221,35 +336,63 @@ 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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -325,7 +468,9 @@ 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_primaryTimeZoneId = m_configManager->primaryTimeZoneId();
|
||||||
m_secondaryTimeZoneId = m_configManager->secondaryTimeZoneId();
|
m_secondaryTimeZoneId = m_configManager->secondaryTimeZoneId();
|
||||||
@@ -365,7 +510,9 @@ 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());
|
||||||
@@ -381,23 +528,29 @@ 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-family: 'SF Pro Rounded'; "
|
.arg(size);
|
||||||
"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));
|
|
||||||
|
|
||||||
m_primaryTimeLabel->setStyleSheet(styleSheet);
|
m_primaryTimeLabel->setStyleSheet(labelFontStyleSheet(m_fontSize));
|
||||||
m_secondaryTimeLabel->setStyleSheet(styleSheet);
|
m_secondaryTimeLabel->setStyleSheet(labelFontStyleSheet(m_fontSize));
|
||||||
setStyleSheet(QString("background-color: transparent;"));
|
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)
|
void ClockWidget::changeTimeZone(bool primaryClock)
|
||||||
@@ -457,3 +610,39 @@ QString ClockWidget::formattedTime(const QString &timeZoneId) const
|
|||||||
return QString("%1\n%2")
|
return QString("%1\n%2")
|
||||||
.arg(timeInZone.time().toString("HH:mm"), cityName);
|
.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 ¤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);
|
||||||
|
}
|
||||||
|
|||||||
+34
-2
@@ -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,23 @@ public:
|
|||||||
explicit ClockWidget(QWidget *parent = nullptr);
|
explicit ClockWidget(QWidget *parent = nullptr);
|
||||||
~ClockWidget();
|
~ClockWidget();
|
||||||
|
|
||||||
|
QMenu *menu() const { return m_contextMenu; }
|
||||||
|
|
||||||
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 changePrimaryTimeZone();
|
||||||
void changeSecondaryTimeZone();
|
void changeSecondaryTimeZone();
|
||||||
void resetSettings();
|
void resetSettings();
|
||||||
@@ -44,9 +70,13 @@ private:
|
|||||||
void updateStyleSheet();
|
void updateStyleSheet();
|
||||||
void changeTimeZone(bool primaryClock);
|
void changeTimeZone(bool primaryClock);
|
||||||
QString formattedTime(const QString &timeZoneId) const;
|
QString formattedTime(const QString &timeZoneId) const;
|
||||||
|
QString locationName(const QString &timeZoneId) const;
|
||||||
|
std::optional<QColor> pickColor(const QString &title, const QColor ¤t, QWidget *parent);
|
||||||
|
|
||||||
QLabel *m_primaryTimeLabel;
|
ClockLabel *m_primaryTimeLabel;
|
||||||
QLabel *m_secondaryTimeLabel;
|
ClockLabel *m_primaryLocationLabel;
|
||||||
|
ClockLabel *m_secondaryTimeLabel;
|
||||||
|
ClockLabel *m_secondaryLocationLabel;
|
||||||
QTimer *m_timer;
|
QTimer *m_timer;
|
||||||
QMenu *m_contextMenu;
|
QMenu *m_contextMenu;
|
||||||
|
|
||||||
@@ -64,7 +94,9 @@ 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_primaryTimeZoneId;
|
||||||
QString m_secondaryTimeZoneId;
|
QString m_secondaryTimeZoneId;
|
||||||
|
|||||||
+20
-6
@@ -107,6 +107,14 @@ bool ConfigManager::loadSettings(const QString &filename)
|
|||||||
{
|
{
|
||||||
m_secondaryTimeZoneId = text;
|
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();
|
||||||
@@ -153,14 +161,18 @@ bool ConfigManager::saveSettings(const QString &filename)
|
|||||||
// Root element
|
// Root element
|
||||||
writer.writeStartElement("ClouckConfig");
|
writer.writeStartElement("ClouckConfig");
|
||||||
|
|
||||||
// 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");
|
||||||
@@ -214,4 +226,6 @@ void ConfigManager::setDefaultValues()
|
|||||||
m_windowSize = QSize(240, 160); // Default clock size
|
m_windowSize = QSize(240, 160); // Default clock size
|
||||||
m_primaryTimeZoneId = QString::fromUtf8(QTimeZone::systemTimeZoneId());
|
m_primaryTimeZoneId = QString::fromUtf8(QTimeZone::systemTimeZoneId());
|
||||||
m_secondaryTimeZoneId = QStringLiteral("America/Los_Angeles");
|
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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,6 +33,8 @@ public:
|
|||||||
QSize windowSize() const { return m_windowSize; }
|
QSize windowSize() const { return m_windowSize; }
|
||||||
QString primaryTimeZoneId() const { return m_primaryTimeZoneId; }
|
QString primaryTimeZoneId() const { return m_primaryTimeZoneId; }
|
||||||
QString secondaryTimeZoneId() const { return m_secondaryTimeZoneId; }
|
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; }
|
||||||
@@ -43,6 +45,8 @@ public:
|
|||||||
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 setPrimaryTimeZoneId(const QString &timeZoneId) { m_primaryTimeZoneId = timeZoneId; }
|
||||||
void setSecondaryTimeZoneId(const QString &timeZoneId) { m_secondaryTimeZoneId = 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();
|
||||||
@@ -63,6 +67,8 @@ private:
|
|||||||
QSize m_windowSize;
|
QSize m_windowSize;
|
||||||
QString m_primaryTimeZoneId;
|
QString m_primaryTimeZoneId;
|
||||||
QString m_secondaryTimeZoneId;
|
QString m_secondaryTimeZoneId;
|
||||||
|
QColor m_borderColor;
|
||||||
|
int m_locationFontSize;
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // CONFIG_MANAGER_H
|
#endif // CONFIG_MANAGER_H
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
#include <QAction>
|
#include <QAction>
|
||||||
#include <QMenu>
|
#include <QMenu>
|
||||||
#include <QPainter>
|
#include <QPainter>
|
||||||
|
#include <QSettings>
|
||||||
#include <QStyleHints>
|
#include <QStyleHints>
|
||||||
#include <QSystemTrayIcon>
|
#include <QSystemTrayIcon>
|
||||||
#include "autostart_manager.h"
|
#include "autostart_manager.h"
|
||||||
@@ -14,23 +15,27 @@ constexpr int trayIconExtent = 16;
|
|||||||
|
|
||||||
QIcon createTrayIcon(const QColor &color)
|
QIcon createTrayIcon(const QColor &color)
|
||||||
{
|
{
|
||||||
QPixmap pixmap(trayIconExtent, trayIconExtent);
|
// Render at 16 logical points times the device pixel ratio so the
|
||||||
pixmap.setDevicePixelRatio(qApp->devicePixelRatio());
|
// 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);
|
pixmap.fill(Qt::transparent);
|
||||||
|
|
||||||
QPainter painter(&pixmap);
|
QPainter painter(&pixmap);
|
||||||
painter.setRenderHint(QPainter::Antialiasing);
|
painter.setRenderHint(QPainter::Antialiasing);
|
||||||
painter.setPen(QPen(color, 1.5));
|
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);
|
// Draw inside the 16pt coordinate space; center with a small margin.
|
||||||
#ifdef Q_OS_MAC
|
const QPointF center(trayIconExtent / 2.0, trayIconExtent / 2.0);
|
||||||
// Template image: the system tints it black/white to match the
|
const qreal radius = trayIconExtent / 2.0 - 1.5;
|
||||||
// active appearance, like native menu bar icons.
|
painter.drawEllipse(center, radius, radius);
|
||||||
icon.setIsMask(true);
|
painter.drawLine(QPointF(center.x(), center.y() - radius * 0.55), center);
|
||||||
#endif
|
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;
|
return icon;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,7 +56,20 @@ int main(int argc, char *argv[])
|
|||||||
ClockWidget clock;
|
ClockWidget clock;
|
||||||
clock.show();
|
clock.show();
|
||||||
|
|
||||||
|
// 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();
|
AutoStartManager::enableForCurrentApplication();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
AutoStartManager::disable();
|
||||||
|
}
|
||||||
|
autoStartSettings.setValue("startAtLogin", startAtLogin);
|
||||||
|
}
|
||||||
|
|
||||||
const QStyleHints *styleHints = app.styleHints();
|
const QStyleHints *styleHints = app.styleHints();
|
||||||
QSystemTrayIcon trayIcon(createTrayIcon(trayIconColorForScheme(styleHints->colorScheme())), &app);
|
QSystemTrayIcon trayIcon(createTrayIcon(trayIconColorForScheme(styleHints->colorScheme())), &app);
|
||||||
@@ -64,6 +82,36 @@ int main(int argc, char *argv[])
|
|||||||
|
|
||||||
QMenu trayMenu;
|
QMenu trayMenu;
|
||||||
QAction *toggleClockAction = trayMenu.addAction("Turn Clock Off");
|
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<QAction *> clockActions = clock.menu()->actions();
|
||||||
|
for (QAction *action : clockActions)
|
||||||
|
{
|
||||||
|
trayMenu.addAction(action);
|
||||||
|
}
|
||||||
|
|
||||||
|
trayMenu.addSeparator();
|
||||||
QAction *quitAction = trayMenu.addAction("Quit");
|
QAction *quitAction = trayMenu.addAction("Quit");
|
||||||
|
|
||||||
const auto toggleClock = [&clock, toggleClockAction]() {
|
const auto toggleClock = [&clock, toggleClockAction]() {
|
||||||
@@ -81,13 +129,6 @@ int main(int argc, char *argv[])
|
|||||||
|
|
||||||
QObject::connect(toggleClockAction, &QAction::triggered, &app, toggleClock);
|
QObject::connect(toggleClockAction, &QAction::triggered, &app, toggleClock);
|
||||||
QObject::connect(quitAction, &QAction::triggered, &app, &QApplication::quit);
|
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.setContextMenu(&trayMenu);
|
||||||
trayIcon.show();
|
trayIcon.show();
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
#include "autostart_manager.h"
|
#include "autostart_manager.h"
|
||||||
|
|
||||||
|
#include <QTemporaryDir>
|
||||||
#include <QtTest>
|
#include <QtTest>
|
||||||
|
|
||||||
class AutoStartManagerTest : public QObject
|
class AutoStartManagerTest : public QObject
|
||||||
@@ -8,6 +9,7 @@ class AutoStartManagerTest : public QObject
|
|||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
void createsRunAtLoadLaunchAgent();
|
void createsRunAtLoadLaunchAgent();
|
||||||
|
void roundTripsPlistRoundTrip();
|
||||||
};
|
};
|
||||||
|
|
||||||
void AutoStartManagerTest::createsRunAtLoadLaunchAgent()
|
void AutoStartManagerTest::createsRunAtLoadLaunchAgent()
|
||||||
@@ -21,6 +23,26 @@ void AutoStartManagerTest::createsRunAtLoadLaunchAgent()
|
|||||||
QVERIFY(contents.contains("<true/>"));
|
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/Clouck.app/Contents/MacOS/Clouck"));
|
||||||
|
QVERIFY(AutoStartManager::isEnabled());
|
||||||
|
QVERIFY(QFile::exists(AutoStartManager::launchAgentPath()));
|
||||||
|
|
||||||
|
QVERIFY(AutoStartManager::disable());
|
||||||
|
QVERIFY(!AutoStartManager::isEnabled());
|
||||||
|
}
|
||||||
|
|
||||||
QTEST_APPLESS_MAIN(AutoStartManagerTest)
|
QTEST_APPLESS_MAIN(AutoStartManagerTest)
|
||||||
|
|
||||||
#include "autostart_manager_test.moc"
|
#include "autostart_manager_test.moc"
|
||||||
|
|||||||
@@ -30,6 +30,9 @@ 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];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user