diff --git a/gui/main-kf5.qml b/gui/main-kf5.qml index 23f36d8..8e7c0a3 100644 --- a/gui/main-kf5.qml +++ b/gui/main-kf5.qml @@ -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" diff --git a/gui/main.cpp b/gui/main.cpp index 15319b5..8282384 100644 --- a/gui/main.cpp +++ b/gui/main.cpp @@ -7,6 +7,10 @@ #include #include #include +#include +#include +#include +#include #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(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(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(); } diff --git a/gui/main.qml b/gui/main.qml index b391395..9f12fd3 100644 --- a/gui/main.qml +++ b/gui/main.qml @@ -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" diff --git a/gui/thememanager.cpp b/gui/thememanager.cpp index 640cfd1..43fbcd1 100644 --- a/gui/thememanager.cpp +++ b/gui/thememanager.cpp @@ -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); } diff --git a/gui/thememanager.h b/gui/thememanager.h index 1053b50..9f3676e 100644 --- a/gui/thememanager.h +++ b/gui/thememanager.h @@ -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"; diff --git a/plasmoid/plugin/olliedbusclient.cpp b/plasmoid/plugin/olliedbusclient.cpp index b143ddd..23702e2 100644 --- a/plasmoid/plugin/olliedbusclient.cpp +++ b/plasmoid/plugin/olliedbusclient.cpp @@ -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) diff --git a/plasmoid/plugin/olliedbusclient.h b/plasmoid/plugin/olliedbusclient.h index 192a4d9..f718aef 100644 --- a/plasmoid/plugin/olliedbusclient.h +++ b/plasmoid/plugin/olliedbusclient.h @@ -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);