- 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
232 lines
6.6 KiB
C++
232 lines
6.6 KiB
C++
#include "config_manager.h"
|
|
#include <QDir>
|
|
#include <QFile>
|
|
#include <QFileInfo>
|
|
#include <QDebug>
|
|
#include <QStandardPaths>
|
|
#include <QTimeZone>
|
|
|
|
ConfigManager::ConfigManager(QObject *parent)
|
|
: QObject(parent)
|
|
{
|
|
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)
|
|
{
|
|
const QString settingsPath = filename.isEmpty() ? defaultSettingsPath() : filename;
|
|
if (settingsPath.isEmpty())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
QFile file(settingsPath);
|
|
if (!file.exists())
|
|
{
|
|
qDebug() << "Config file does not exist, using defaults";
|
|
return false;
|
|
}
|
|
|
|
if (!file.open(QIODevice::ReadOnly | QIODevice::Text))
|
|
{
|
|
qDebug() << "Cannot open config file for reading:" << file.errorString();
|
|
return false;
|
|
}
|
|
|
|
QXmlStreamReader reader(&file);
|
|
|
|
// Check if it's a valid XML file
|
|
if (!reader.readNextStartElement() || reader.name() != QString("ClouckConfig"))
|
|
{
|
|
qDebug() << "Invalid config file format";
|
|
file.close();
|
|
return false;
|
|
}
|
|
|
|
// Read configuration values
|
|
while (reader.readNextStartElement())
|
|
{
|
|
QString name = reader.name().toString();
|
|
QString text = reader.readElementText();
|
|
|
|
if (name == "FontColor")
|
|
{
|
|
m_fontColor = QColor(text);
|
|
}
|
|
else if (name == "BackgroundColor")
|
|
{
|
|
m_backgroundColor = QColor(text);
|
|
}
|
|
else if (name == "FontSize")
|
|
{
|
|
m_fontSize = text.toInt();
|
|
}
|
|
else if (name == "AlwaysOnTop")
|
|
{
|
|
m_alwaysOnTop = (text.toLower() == QString("true"));
|
|
}
|
|
else if (name == "WindowPosition")
|
|
{
|
|
// Parse position format: "x,y"
|
|
QStringList coords = text.split(',');
|
|
if (coords.size() == 2)
|
|
{
|
|
int x = coords[0].toInt();
|
|
int y = coords[1].toInt();
|
|
m_windowPosition = QPoint(x, y);
|
|
}
|
|
}
|
|
else if (name == "WindowSize")
|
|
{
|
|
// Parse size format: "width,height"
|
|
QStringList dims = text.split(',');
|
|
if (dims.size() == 2)
|
|
{
|
|
int width = dims[0].toInt();
|
|
int height = dims[1].toInt();
|
|
m_windowSize = QSize(width, height);
|
|
}
|
|
}
|
|
else if (name == "PrimaryTimeZone")
|
|
{
|
|
m_primaryTimeZoneId = text;
|
|
}
|
|
else if (name == "SecondaryTimeZone")
|
|
{
|
|
m_secondaryTimeZoneId = text;
|
|
}
|
|
else if (name == "BorderColor")
|
|
{
|
|
m_borderColor = QColor(text);
|
|
}
|
|
else if (name == "LocationFontSize")
|
|
{
|
|
m_locationFontSize = text.toInt();
|
|
}
|
|
else
|
|
{
|
|
reader.skipCurrentElement();
|
|
}
|
|
}
|
|
|
|
file.close();
|
|
|
|
if (reader.hasError())
|
|
{
|
|
qDebug() << "Error reading config file:" << reader.errorString();
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool ConfigManager::saveSettings(const QString &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))
|
|
{
|
|
qDebug() << "Cannot open config file for writing:" << file.errorString();
|
|
return false;
|
|
}
|
|
|
|
QXmlStreamWriter writer(&file);
|
|
writer.setAutoFormatting(true);
|
|
writer.writeStartDocument();
|
|
|
|
// Root element
|
|
writer.writeStartElement("ClouckConfig");
|
|
|
|
// 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));
|
|
|
|
// 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");
|
|
|
|
// Window position
|
|
QString positionStr = QString("%1,%2")
|
|
.arg(m_windowPosition.x())
|
|
.arg(m_windowPosition.y());
|
|
writer.writeTextElement("WindowPosition", positionStr);
|
|
|
|
// Window size
|
|
QString sizeStr = QString("%1,%2")
|
|
.arg(m_windowSize.width())
|
|
.arg(m_windowSize.height());
|
|
writer.writeTextElement("WindowSize", sizeStr);
|
|
|
|
writer.writeTextElement("PrimaryTimeZone", m_primaryTimeZoneId);
|
|
writer.writeTextElement("SecondaryTimeZone", m_secondaryTimeZoneId);
|
|
|
|
// Close root element
|
|
writer.writeEndElement();
|
|
|
|
writer.writeEndDocument();
|
|
file.close();
|
|
|
|
return true;
|
|
}
|
|
|
|
void ConfigManager::resetToDefaults()
|
|
{
|
|
setDefaultValues();
|
|
}
|
|
|
|
bool ConfigManager::configExists(const QString &filename) const
|
|
{
|
|
const QString settingsPath = filename.isEmpty() ? defaultSettingsPath() : filename;
|
|
return !settingsPath.isEmpty() && QFile::exists(settingsPath);
|
|
}
|
|
|
|
void ConfigManager::setDefaultValues()
|
|
{
|
|
m_fontColor = QColor(0, 255, 0);
|
|
m_backgroundColor = QColor(0, 0, 0, 0);
|
|
m_fontSize = 24;
|
|
m_alwaysOnTop = true;
|
|
|
|
// Calculate initial position: bottom-right corner of primary screen
|
|
// Position will be set properly when widget is shown for the first time
|
|
m_windowPosition = QPoint(-1, -1); // Special value indicating "not set yet"
|
|
|
|
m_windowSize = QSize(240, 160); // Default clock size
|
|
m_primaryTimeZoneId = QString::fromUtf8(QTimeZone::systemTimeZoneId());
|
|
m_secondaryTimeZoneId = QStringLiteral("America/Los_Angeles");
|
|
m_borderColor = QColor(0, 0, 0, 0); // No border by default
|
|
m_locationFontSize = 12; // Smaller city label under the time
|
|
}
|