gui: close-to-tray option + fix session list disrupting rename

- Add configurable close-to-tray (Settings > System Tray)
  - QSystemTrayIcon with show/hide toggle and quit action
  - closeToTray property persisted in theme.conf
  - Window hides instead of quitting when enabled

- Fix session rename being interrupted by state changes
  - onStateChanged no longer calls refreshSessions()
  - State updates in-place via sessionStateUpdated signal
  - Delegates track state locally, preserving rename TextField
This commit is contained in:
Levi Neely 2026-07-16 14:34:14 +02:00
parent 1d646f7fab
commit f45cb8199d
7 changed files with 196 additions and 3 deletions

View File

@ -10,6 +10,13 @@ ApplicationWindow {
height: 600
visible: true
onClosing: {
if (theme.closeToTray && systemTrayAvailable) {
close.accepted = false
root.hide()
}
}
color: palette.window
font.family: theme.fontFamily || Qt.application.font.family
font.pointSize: theme.fontSize > 0 ? theme.fontSize : Qt.application.font.pointSize
@ -136,6 +143,37 @@ ApplicationWindow {
text: "Reset to system font"
onClicked: theme.resetFont()
}
// --- Close to tray section ---
Rectangle {
Layout.fillWidth: true
height: 1
color: palette.mid
Layout.topMargin: 8
}
Label {
text: "System Tray"
font.bold: true
}
CheckBox {
text: "Close to tray instead of quitting"
checked: theme.closeToTray
onToggled: theme.closeToTray = checked
enabled: systemTrayAvailable
}
Label {
text: systemTrayAvailable
? "Window will hide to the system tray when closed."
: "System tray is not available."
opacity: 0.5
font.pixelSize: Qt.application.font.pixelSize * 0.85
wrapMode: Text.Wrap
Layout.fillWidth: true
visible: theme.closeToTray || !systemTrayAvailable
}
}
}
@ -206,6 +244,15 @@ ApplicationWindow {
highlighted: modelData.id === dbus.activeSessionId
property bool renaming: false
property string sessionState: modelData.state
Connections {
target: dbus
function onSessionStateUpdated(sessionId, newState) {
if (sessionId === modelData.id)
sessionDelegate.sessionState = newState
}
}
background: Rectangle {
color: sessionDelegate.highlighted
@ -221,7 +268,7 @@ ApplicationWindow {
height: 8
radius: 4
color: {
var state = modelData.state
var state = sessionDelegate.sessionState
if (state === "idle") return "#4caf50"
if (state.startsWith("calling")) return "#ff9800"
return "#2196f3"

View File

@ -7,6 +7,10 @@
#include <QPainter>
#include <QFont>
#include <QFontDatabase>
#include <QSystemTrayIcon>
#include <QMenu>
#include <QAction>
#include <QWindow>
#include "olliedbusclient.h"
#include "thememanager.h"
@ -77,6 +81,8 @@ int main(int argc, char *argv[])
engine.rootContext()->setContextProperty("dbus", dbus);
engine.rootContext()->setContextProperty("theme", theme);
engine.rootContext()->setContextProperty("chatModel", chatModel);
engine.rootContext()->setContextProperty("systemTrayAvailable",
QSystemTrayIcon::isSystemTrayAvailable());
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
const QUrl url(QStringLiteral("qrc:/gui/main.qml"));
@ -91,5 +97,54 @@ int main(int argc, char *argv[])
}, Qt::QueuedConnection);
engine.load(url);
// --- System tray icon (shown when close-to-tray is enabled) ---
auto *trayIcon = new QSystemTrayIcon(&app);
trayIcon->setIcon(app.windowIcon());
trayIcon->setToolTip("Ollie");
auto *trayMenu = new QMenu();
auto *showAction = trayMenu->addAction("Show");
trayMenu->addSeparator();
auto *quitAction = trayMenu->addAction("Quit");
trayIcon->setContextMenu(trayMenu);
// Toggle window visibility on tray icon activation
QObject::connect(trayIcon, &QSystemTrayIcon::activated,
[&engine](QSystemTrayIcon::ActivationReason reason) {
if (reason != QSystemTrayIcon::Trigger)
return;
auto roots = engine.rootObjects();
if (roots.isEmpty()) return;
auto *window = qobject_cast<QWindow *>(roots.first());
if (!window) return;
if (window->isVisible()) {
window->hide();
} else {
window->show();
window->raise();
window->requestActivate();
}
});
QObject::connect(showAction, &QAction::triggered, [&engine]() {
auto roots = engine.rootObjects();
if (roots.isEmpty()) return;
auto *window = qobject_cast<QWindow *>(roots.first());
if (!window) return;
window->show();
window->raise();
window->requestActivate();
});
QObject::connect(quitAction, &QAction::triggered, &app, &QApplication::quit);
// Show/hide tray icon based on setting
auto updateTrayVisibility = [trayIcon, theme]() {
trayIcon->setVisible(theme->closeToTray());
};
updateTrayVisibility();
QObject::connect(theme, &ThemeManager::closeToTrayChanged, updateTrayVisibility);
return app.exec();
}

View File

@ -10,6 +10,13 @@ ApplicationWindow {
height: 600
visible: true
onClosing: function(close) {
if (theme.closeToTray && systemTrayAvailable) {
close.accepted = false
root.hide()
}
}
color: palette.window
font.family: theme.fontFamily || Qt.application.font.family
font.pointSize: theme.fontSize > 0 ? theme.fontSize : Qt.application.font.pointSize
@ -137,6 +144,37 @@ ApplicationWindow {
text: "Reset to system font"
onClicked: theme.resetFont()
}
// --- Close to tray section ---
Rectangle {
Layout.fillWidth: true
height: 1
color: palette.mid
Layout.topMargin: 8
}
Label {
text: "System Tray"
font.bold: true
}
CheckBox {
text: "Close to tray instead of quitting"
checked: theme.closeToTray
onToggled: theme.closeToTray = checked
enabled: systemTrayAvailable
}
Label {
text: systemTrayAvailable
? "Window will hide to the system tray when closed."
: "System tray is not available."
opacity: 0.5
font.pixelSize: Qt.application.font.pixelSize * 0.85
wrapMode: Text.Wrap
Layout.fillWidth: true
visible: theme.closeToTray || !systemTrayAvailable
}
}
}
@ -207,6 +245,15 @@ ApplicationWindow {
highlighted: modelData.id === dbus.activeSessionId
property bool renaming: false
property string sessionState: modelData.state
Connections {
target: dbus
function onSessionStateUpdated(sessionId, newState) {
if (sessionId === modelData.id)
sessionDelegate.sessionState = newState
}
}
background: Rectangle {
color: sessionDelegate.highlighted
@ -222,7 +269,7 @@ ApplicationWindow {
height: 8
radius: 4
color: {
let state = modelData.state
let state = sessionDelegate.sessionState
if (state === "idle") return "#4caf50"
if (state.startsWith("calling")) return "#ff9800"
return "#2196f3"

View File

@ -13,6 +13,7 @@ ThemeManager::ThemeManager(QObject *parent)
: QObject(parent)
, m_mode("system")
, m_fontSize(0)
, m_closeToTray(false)
, m_watcher(nullptr)
{
loadSettings();
@ -372,6 +373,22 @@ QStringList ThemeManager::availableStyles() const
#endif
}
// --- Close-to-tray ---
bool ThemeManager::closeToTray() const
{
return m_closeToTray;
}
void ThemeManager::setCloseToTray(bool enabled)
{
if (enabled == m_closeToTray)
return;
m_closeToTray = enabled;
saveSettings();
emit closeToTrayChanged();
}
void ThemeManager::resetFont()
{
m_fontFamily.clear();
@ -430,6 +447,8 @@ void ThemeManager::loadSettings()
m_fontFamily = settings.value("Font/family", "").toString();
m_fontSize = settings.value("Font/size", 0).toInt();
m_fontStyle = settings.value("Font/style", "").toString();
m_closeToTray = settings.value("General/closeToTray", false).toBool();
}
}
@ -444,4 +463,5 @@ void ThemeManager::saveSettings()
settings.setValue("Font/family", m_fontFamily);
settings.setValue("Font/size", m_fontSize);
settings.setValue("Font/style", m_fontStyle);
settings.setValue("General/closeToTray", m_closeToTray);
}

View File

@ -46,6 +46,9 @@ class ThemeManager : public QObject
Q_PROPERTY(QStringList availableFamilies READ availableFamilies CONSTANT)
Q_PROPERTY(QStringList availableStyles READ availableStyles NOTIFY fontChanged)
// Close-to-tray
Q_PROPERTY(bool closeToTray READ closeToTray WRITE setCloseToTray NOTIFY closeToTrayChanged)
public:
enum Mode {
System, // Follow KDE system color scheme
@ -91,6 +94,10 @@ public:
QStringList availableFamilies() const;
QStringList availableStyles() const;
// Close-to-tray
bool closeToTray() const;
void setCloseToTray(bool enabled);
Q_INVOKABLE void resetFont();
Q_INVOKABLE QString chooseFolder(const QString &title, const QString &startDir = QString());
@ -101,6 +108,7 @@ signals:
void modeChanged();
void paletteChanged();
void fontChanged();
void closeToTrayChanged();
private:
QPalette paletteFromScheme(const QString &path) const;
@ -116,6 +124,7 @@ private:
QString m_fontFamily;
int m_fontSize;
QString m_fontStyle;
bool m_closeToTray;
QFileSystemWatcher *m_watcher;
KConfigWatcher::Ptr m_configWatcher;
static constexpr const char *SCHEMES_DIR = "/usr/share/color-schemes";

View File

@ -426,7 +426,21 @@ void OllieDBusClient::onStateChanged(const QString &session_id, const QString &n
m_activeState = new_state;
emit activeStateChanged();
}
refreshSessions();
// Update state in-place without emitting sessionsChanged() — that would
// cause the ListView to recreate all delegates, breaking inline rename.
// The QML state indicator for the active session uses dbus.activeState
// (updated above). For other sessions the dot updates on the next real
// refresh (session created/killed/renamed).
for (int i = 0; i < m_sessions.size(); ++i) {
QVariantMap session = m_sessions[i].toMap();
if (session["id"].toString() == session_id) {
session["state"] = new_state;
m_sessions[i] = session;
emit sessionStateUpdated(session_id, new_state);
return;
}
}
}
void OllieDBusClient::onChatUpdated(const QString &session_id, qlonglong offset, const QString &new_text)

View File

@ -58,6 +58,7 @@ signals:
void activeCwdChanged();
void activeModelChanged();
void activeBackendChanged();
void sessionStateUpdated(const QString &sessionId, const QString &newState);
private slots:
void onSessionCreated(const QString &session_id);