diff --git a/gpt4all-chat/CHANGELOG.md b/gpt4all-chat/CHANGELOG.md index a6e722d3..4c407096 100644 --- a/gpt4all-chat/CHANGELOG.md +++ b/gpt4all-chat/CHANGELOG.md @@ -6,6 +6,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ## [Unreleased] +### Added +- Add feature to minimize to system tray (by [@bgallois](https://github.com/bgallois) in ([#3109](https://github.com/nomic-ai/gpt4all/pull/3109)) + ### Changed - Implement Qt 6.8 compatibility ([#3121](https://github.com/nomic-ai/gpt4all/pull/3121)) diff --git a/gpt4all-chat/CMakeLists.txt b/gpt4all-chat/CMakeLists.txt index f3a6c775..dcf77a83 100644 --- a/gpt4all-chat/CMakeLists.txt +++ b/gpt4all-chat/CMakeLists.txt @@ -75,6 +75,7 @@ configure_file( "${CMAKE_CURRENT_BINARY_DIR}/config.h" ) +set(CMAKE_FIND_PACKAGE_TARGETS_GLOBAL ON) find_package(Qt6 6.5 COMPONENTS Core HttpServer LinguistTools Pdf Quick QuickDialogs2 Sql Svg REQUIRED) if (QT_KNOWN_POLICY_QTP0004) @@ -152,6 +153,12 @@ if (APPLE) set_source_files_properties(${CHAT_EXE_RESOURCES} PROPERTIES MACOSX_PACKAGE_LOCATION Resources) endif() +set(MACOS_SOURCES) +if (APPLE) + find_library(COCOA_LIBRARY Cocoa) + list(APPEND MACOS_SOURCES src/macosdock.mm src/macosdock.h) +endif() + qt_add_executable(chat src/main.cpp src/chat.cpp src/chat.h @@ -173,6 +180,7 @@ qt_add_executable(chat src/server.cpp src/server.h src/xlsxtomd.cpp src/xlsxtomd.h ${CHAT_EXE_RESOURCES} + ${MACOS_SOURCES} ) gpt4all_add_warning_options(chat) @@ -357,6 +365,9 @@ target_link_libraries(chat target_link_libraries(chat PRIVATE llmodel SingleApplication fmt::fmt duckx::duckx QXlsx) +if (APPLE) + target_link_libraries(chat PRIVATE ${COCOA_LIBRARY}) +endif() # -- install -- diff --git a/gpt4all-chat/deps/CMakeLists.txt b/gpt4all-chat/deps/CMakeLists.txt index d082c38b..a87a9a20 100644 --- a/gpt4all-chat/deps/CMakeLists.txt +++ b/gpt4all-chat/deps/CMakeLists.txt @@ -3,7 +3,7 @@ set(BUILD_SHARED_LIBS OFF) set(FMT_INSTALL OFF) add_subdirectory(fmt) -set(QAPPLICATION_CLASS QGuiApplication) +set(QAPPLICATION_CLASS QApplication) add_subdirectory(SingleApplication) set(DUCKX_INSTALL OFF) diff --git a/gpt4all-chat/main.qml b/gpt4all-chat/main.qml index 1e685385..b035e5d9 100644 --- a/gpt4all-chat/main.qml +++ b/gpt4all-chat/main.qml @@ -12,6 +12,7 @@ import network import gpt4all import localdocs import mysettings +import Qt.labs.platform Window { id: window @@ -22,6 +23,43 @@ Window { visible: true title: qsTr("GPT4All v%1").arg(Qt.application.version) + SystemTrayIcon { + id: systemTrayIcon + property bool shouldClose: false + visible: MySettings.systemTray && !shouldClose + icon.source: "qrc:/gpt4all/icons/gpt4all.svg" + + function restore() { + LLM.showDockIcon(); + window.show(); + window.raise(); + window.requestActivate(); + } + onActivated: function(reason) { + if (reason === SystemTrayIcon.Context && Qt.platform.os !== "osx") + menu.open(); + else if (reason === SystemTrayIcon.Trigger) + restore(); + } + + menu: Menu { + MenuItem { + text: qsTr("Restore") + onTriggered: systemTrayIcon.restore() + } + MenuItem { + text: qsTr("Quit") + onTriggered: { + systemTrayIcon.restore(); + systemTrayIcon.shouldClose = true; + window.shouldClose = true; + savingPopup.open(); + ChatListModel.saveChats(); + } + } + } + } + Settings { property alias x: window.x property alias y: window.y @@ -156,7 +194,7 @@ Window { font.pixelSize: theme.fontSizeLarge } - property bool hasSaved: false + property bool shouldClose: false PopupDialog { id: savingPopup @@ -180,9 +218,18 @@ Window { } onClosing: function(close) { - if (window.hasSaved) + if (systemTrayIcon.visible) { + LLM.hideDockIcon(); + window.visible = false; + ChatListModel.saveChats(); + close.accepted = false; + return; + } + + if (window.shouldClose) return; + window.shouldClose = true; savingPopup.open(); ChatListModel.saveChats(); close.accepted = false @@ -191,9 +238,9 @@ Window { Connections { target: ChatListModel function onSaveChatsFinished() { - window.hasSaved = true; savingPopup.close(); - window.close() + if (window.shouldClose) + window.close() } } diff --git a/gpt4all-chat/qml/ApplicationSettings.qml b/gpt4all-chat/qml/ApplicationSettings.qml index f6902192..e61fc274 100644 --- a/gpt4all-chat/qml/ApplicationSettings.qml +++ b/gpt4all-chat/qml/ApplicationSettings.qml @@ -504,17 +504,34 @@ MySettingsTab { } } MySettingsLabel { - id: serverChatLabel - text: qsTr("Enable Local API Server") - helpText: qsTr("Expose an OpenAI-Compatible server to localhost. WARNING: Results in increased resource usage.") + id: trayLabel + text: qsTr("Enable System Tray") + helpText: qsTr("The application will minimize to the system tray when the window is closed.") Layout.row: 13 Layout.column: 0 } MyCheckBox { - id: serverChatBox + id: trayBox Layout.row: 13 Layout.column: 2 Layout.alignment: Qt.AlignRight + checked: MySettings.systemTray + onClicked: { + MySettings.systemTray = !MySettings.systemTray + } + } + MySettingsLabel { + id: serverChatLabel + text: qsTr("Enable Local API Server") + helpText: qsTr("Expose an OpenAI-Compatible server to localhost. WARNING: Results in increased resource usage.") + Layout.row: 14 + Layout.column: 0 + } + MyCheckBox { + id: serverChatBox + Layout.row: 14 + Layout.column: 2 + Layout.alignment: Qt.AlignRight checked: MySettings.serverChat onClicked: { MySettings.serverChat = !MySettings.serverChat @@ -524,7 +541,7 @@ MySettingsTab { id: serverPortLabel text: qsTr("API Server Port") helpText: qsTr("The port to use for the local server. Requires restart.") - Layout.row: 14 + Layout.row: 15 Layout.column: 0 } MyTextField { @@ -532,7 +549,7 @@ MySettingsTab { text: MySettings.networkPort color: theme.textColor font.pixelSize: theme.fontSizeLarge - Layout.row: 14 + Layout.row: 15 Layout.column: 2 Layout.minimumWidth: 200 Layout.maximumWidth: 200 @@ -577,12 +594,12 @@ MySettingsTab { id: updatesLabel text: qsTr("Check For Updates") helpText: qsTr("Manually check for an update to GPT4All."); - Layout.row: 15 + Layout.row: 16 Layout.column: 0 } MySettingsButton { - Layout.row: 15 + Layout.row: 16 Layout.column: 2 Layout.alignment: Qt.AlignRight text: qsTr("Updates"); @@ -593,7 +610,7 @@ MySettingsTab { } Rectangle { - Layout.row: 16 + Layout.row: 17 Layout.column: 0 Layout.columnSpan: 3 Layout.fillWidth: true diff --git a/gpt4all-chat/src/chat.h b/gpt4all-chat/src/chat.h index 05a6878d..577e5657 100644 --- a/gpt4all-chat/src/chat.h +++ b/gpt4all-chat/src/chat.h @@ -130,6 +130,7 @@ public: QList generatedQuestions() const { return m_generatedQuestions; } bool needsSave() const { return m_needsSave; } + void setNeedsSave(bool n) { m_needsSave = n; } public Q_SLOTS: void serverNewPromptResponsePair(const QString &prompt, const QList &attachments = {}); diff --git a/gpt4all-chat/src/chatlistmodel.cpp b/gpt4all-chat/src/chatlistmodel.cpp index fb5ef68e..207a2b3b 100644 --- a/gpt4all-chat/src/chatlistmodel.cpp +++ b/gpt4all-chat/src/chatlistmodel.cpp @@ -51,6 +51,10 @@ void ChatListModel::loadChats() connect(thread, &ChatsRestoreThread::finished, thread, &QObject::deleteLater); thread->start(); + ChatSaver *saver = new ChatSaver; + connect(this, &ChatListModel::requestSaveChats, saver, &ChatSaver::saveChats, Qt::QueuedConnection); + connect(saver, &ChatSaver::saveChatsFinished, this, &ChatListModel::saveChatsFinished, Qt::QueuedConnection); + connect(MySettings::globalInstance(), &MySettings::serverChatChanged, this, &ChatListModel::handleServerEnabledChanged); } @@ -88,9 +92,6 @@ void ChatListModel::saveChats() return; } - ChatSaver *saver = new ChatSaver; - connect(this, &ChatListModel::requestSaveChats, saver, &ChatSaver::saveChats, Qt::QueuedConnection); - connect(saver, &ChatSaver::saveChatsFinished, this, &ChatListModel::saveChatsFinished, Qt::QueuedConnection); emit requestSaveChats(toSave); } @@ -128,6 +129,7 @@ void ChatSaver::saveChats(const QVector &chats) continue; } + chat->setNeedsSave(false); if (originalFile.exists()) originalFile.remove(); tempFile.rename(filePath); diff --git a/gpt4all-chat/src/chatlistmodel.h b/gpt4all-chat/src/chatlistmodel.h index a30efd5a..4fe1c374 100644 --- a/gpt4all-chat/src/chatlistmodel.h +++ b/gpt4all-chat/src/chatlistmodel.h @@ -33,7 +33,6 @@ class ChatSaver : public QObject Q_OBJECT public: explicit ChatSaver(); - void stop(); Q_SIGNALS: void saveChatsFinished(); @@ -238,7 +237,6 @@ public Q_SLOTS: Q_SIGNALS: void countChanged(); void currentChatChanged(); - void chatsSavedFinished(); void requestSaveChats(const QVector &); void saveChatsFinished(); diff --git a/gpt4all-chat/src/llm.cpp b/gpt4all-chat/src/llm.cpp index aed1a7db..02aa1499 100644 --- a/gpt4all-chat/src/llm.cpp +++ b/gpt4all-chat/src/llm.cpp @@ -19,6 +19,10 @@ # include "network.h" #endif +#ifdef Q_OS_MAC +#include "macosdock.h" +#endif + using namespace Qt::Literals::StringLiterals; class MyLLM: public LLM { }; @@ -105,3 +109,21 @@ bool LLM::isNetworkOnline() const auto * netinfo = QNetworkInformation::instance(); return !netinfo || netinfo->reachability() == QNetworkInformation::Reachability::Online; } + +void LLM::showDockIcon() const +{ +#ifdef Q_OS_MAC + MacOSDock::showIcon(); +#else + qt_noop(); +#endif +} + +void LLM::hideDockIcon() const +{ +#ifdef Q_OS_MAC + MacOSDock::hideIcon(); +#else + qt_noop(); +#endif +} diff --git a/gpt4all-chat/src/llm.h b/gpt4all-chat/src/llm.h index f570c49a..7247b89c 100644 --- a/gpt4all-chat/src/llm.h +++ b/gpt4all-chat/src/llm.h @@ -23,6 +23,9 @@ public: Q_INVOKABLE QString systemTotalRAMInGBString() const; Q_INVOKABLE bool isNetworkOnline() const; + Q_INVOKABLE void showDockIcon() const; + Q_INVOKABLE void hideDockIcon() const; + Q_SIGNALS: void isNetworkOnlineChanged(); diff --git a/gpt4all-chat/src/macosdock.h b/gpt4all-chat/src/macosdock.h new file mode 100644 index 00000000..ac18334f --- /dev/null +++ b/gpt4all-chat/src/macosdock.h @@ -0,0 +1,9 @@ +#ifndef MACOSDOCK_H +#define MACOSDOCK_H + +struct MacOSDock { +static void showIcon(); +static void hideIcon(); +}; + +#endif // MACOSDOCK_H diff --git a/gpt4all-chat/src/macosdock.mm b/gpt4all-chat/src/macosdock.mm new file mode 100644 index 00000000..aba35069 --- /dev/null +++ b/gpt4all-chat/src/macosdock.mm @@ -0,0 +1,13 @@ +#include "macosdock.h" + +#include + +void MacOSDock::showIcon() +{ + [[NSApplication sharedApplication] setActivationPolicy:NSApplicationActivationPolicyRegular]; +} + +void MacOSDock::hideIcon() +{ + [[NSApplication sharedApplication] setActivationPolicy:NSApplicationActivationPolicyProhibited]; +} diff --git a/gpt4all-chat/src/main.cpp b/gpt4all-chat/src/main.cpp index c2bdac1e..b07867c6 100644 --- a/gpt4all-chat/src/main.cpp +++ b/gpt4all-chat/src/main.cpp @@ -44,6 +44,7 @@ static void raiseWindow(QWindow *window) SetForegroundWindow(hwnd); #else + LLM::globalInstance()->showDockIcon(); window->show(); window->raise(); window->requestActivate(); diff --git a/gpt4all-chat/src/mysettings.cpp b/gpt4all-chat/src/mysettings.cpp index 38c8ab68..da1f6c54 100644 --- a/gpt4all-chat/src/mysettings.cpp +++ b/gpt4all-chat/src/mysettings.cpp @@ -49,6 +49,7 @@ static const QVariantMap basicDefaults { { "lastVersionStarted", "" }, { "networkPort", 4891, }, { "saveChatsContext", false }, + { "systemTray", false }, { "serverChat", false }, { "userDefaultModel", "Application default" }, { "suggestionMode", QVariant::fromValue(SuggestionMode::LocalDocsOnly) }, @@ -206,6 +207,7 @@ void MySettings::restoreApplicationDefaults() setDevice(defaults::device); setThreadCount(defaults::threadCount); setSaveChatsContext(basicDefaults.value("saveChatsContext").toBool()); + setSystemTray(basicDefaults.value("saveTrayContext").toBool()); setServerChat(basicDefaults.value("serverChat").toBool()); setNetworkPort(basicDefaults.value("networkPort").toInt()); setModelPath(defaultLocalModelsPath()); @@ -444,6 +446,7 @@ void MySettings::setThreadCount(int value) } bool MySettings::saveChatsContext() const { return getBasicSetting("saveChatsContext" ).toBool(); } +bool MySettings::systemTray() const { return getBasicSetting("systemTray" ).toBool(); } bool MySettings::serverChat() const { return getBasicSetting("serverChat" ).toBool(); } int MySettings::networkPort() const { return getBasicSetting("networkPort" ).toInt(); } QString MySettings::userDefaultModel() const { return getBasicSetting("userDefaultModel" ).toString(); } @@ -462,6 +465,7 @@ FontSize MySettings::fontSize() const { return FontSize (getEnu SuggestionMode MySettings::suggestionMode() const { return SuggestionMode(getEnumSetting("suggestionMode", suggestionModeNames)); } void MySettings::setSaveChatsContext(bool value) { setBasicSetting("saveChatsContext", value); } +void MySettings::setSystemTray(bool value) { setBasicSetting("systemTray", value); } void MySettings::setServerChat(bool value) { setBasicSetting("serverChat", value); } void MySettings::setNetworkPort(int value) { setBasicSetting("networkPort", value); } void MySettings::setUserDefaultModel(const QString &value) { setBasicSetting("userDefaultModel", value); } diff --git a/gpt4all-chat/src/mysettings.h b/gpt4all-chat/src/mysettings.h index 85335f0b..f3d5e5b0 100644 --- a/gpt4all-chat/src/mysettings.h +++ b/gpt4all-chat/src/mysettings.h @@ -49,6 +49,7 @@ class MySettings : public QObject Q_OBJECT Q_PROPERTY(int threadCount READ threadCount WRITE setThreadCount NOTIFY threadCountChanged) Q_PROPERTY(bool saveChatsContext READ saveChatsContext WRITE setSaveChatsContext NOTIFY saveChatsContextChanged) + Q_PROPERTY(bool systemTray READ systemTray WRITE setSystemTray NOTIFY systemTrayChanged) Q_PROPERTY(bool serverChat READ serverChat WRITE setServerChat NOTIFY serverChatChanged) Q_PROPERTY(QString modelPath READ modelPath WRITE setModelPath NOTIFY modelPathChanged) Q_PROPERTY(QString userDefaultModel READ userDefaultModel WRITE setUserDefaultModel NOTIFY userDefaultModelChanged) @@ -142,6 +143,8 @@ public: void setThreadCount(int value); bool saveChatsContext() const; void setSaveChatsContext(bool value); + bool systemTray() const; + void setSystemTray(bool value); bool serverChat() const; void setServerChat(bool value); QString modelPath(); @@ -218,6 +221,7 @@ Q_SIGNALS: void suggestedFollowUpPromptChanged(const ModelInfo &info); void threadCountChanged(); void saveChatsContextChanged(); + void systemTrayChanged(); void serverChatChanged(); void modelPathChanged(); void userDefaultModelChanged();