chat: system tray icon and close to tray (#3109)

Signed-off-by: bgallois <benjamin@gallois.cc>
Signed-off-by: Adam Treat <treat.adam@gmail.com>
Co-authored-by: Adam Treat <treat.adam@gmail.com>
This commit is contained in:
Benjamin Gallois
2024-10-25 18:20:55 +02:00
committed by GitHub
parent 62f90ff7d5
commit 57c0974f4a
15 changed files with 154 additions and 19 deletions

View File

@@ -6,6 +6,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
## [Unreleased] ## [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 ### Changed
- Implement Qt 6.8 compatibility ([#3121](https://github.com/nomic-ai/gpt4all/pull/3121)) - Implement Qt 6.8 compatibility ([#3121](https://github.com/nomic-ai/gpt4all/pull/3121))

View File

@@ -75,6 +75,7 @@ configure_file(
"${CMAKE_CURRENT_BINARY_DIR}/config.h" "${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) find_package(Qt6 6.5 COMPONENTS Core HttpServer LinguistTools Pdf Quick QuickDialogs2 Sql Svg REQUIRED)
if (QT_KNOWN_POLICY_QTP0004) if (QT_KNOWN_POLICY_QTP0004)
@@ -152,6 +153,12 @@ if (APPLE)
set_source_files_properties(${CHAT_EXE_RESOURCES} PROPERTIES MACOSX_PACKAGE_LOCATION Resources) set_source_files_properties(${CHAT_EXE_RESOURCES} PROPERTIES MACOSX_PACKAGE_LOCATION Resources)
endif() 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 qt_add_executable(chat
src/main.cpp src/main.cpp
src/chat.cpp src/chat.h src/chat.cpp src/chat.h
@@ -173,6 +180,7 @@ qt_add_executable(chat
src/server.cpp src/server.h src/server.cpp src/server.h
src/xlsxtomd.cpp src/xlsxtomd.h src/xlsxtomd.cpp src/xlsxtomd.h
${CHAT_EXE_RESOURCES} ${CHAT_EXE_RESOURCES}
${MACOS_SOURCES}
) )
gpt4all_add_warning_options(chat) gpt4all_add_warning_options(chat)
@@ -357,6 +365,9 @@ target_link_libraries(chat
target_link_libraries(chat target_link_libraries(chat
PRIVATE llmodel SingleApplication fmt::fmt duckx::duckx QXlsx) PRIVATE llmodel SingleApplication fmt::fmt duckx::duckx QXlsx)
if (APPLE)
target_link_libraries(chat PRIVATE ${COCOA_LIBRARY})
endif()
# -- install -- # -- install --

View File

@@ -3,7 +3,7 @@ set(BUILD_SHARED_LIBS OFF)
set(FMT_INSTALL OFF) set(FMT_INSTALL OFF)
add_subdirectory(fmt) add_subdirectory(fmt)
set(QAPPLICATION_CLASS QGuiApplication) set(QAPPLICATION_CLASS QApplication)
add_subdirectory(SingleApplication) add_subdirectory(SingleApplication)
set(DUCKX_INSTALL OFF) set(DUCKX_INSTALL OFF)

View File

@@ -12,6 +12,7 @@ import network
import gpt4all import gpt4all
import localdocs import localdocs
import mysettings import mysettings
import Qt.labs.platform
Window { Window {
id: window id: window
@@ -22,6 +23,43 @@ Window {
visible: true visible: true
title: qsTr("GPT4All v%1").arg(Qt.application.version) 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 { Settings {
property alias x: window.x property alias x: window.x
property alias y: window.y property alias y: window.y
@@ -156,7 +194,7 @@ Window {
font.pixelSize: theme.fontSizeLarge font.pixelSize: theme.fontSizeLarge
} }
property bool hasSaved: false property bool shouldClose: false
PopupDialog { PopupDialog {
id: savingPopup id: savingPopup
@@ -180,9 +218,18 @@ Window {
} }
onClosing: function(close) { onClosing: function(close) {
if (window.hasSaved) if (systemTrayIcon.visible) {
LLM.hideDockIcon();
window.visible = false;
ChatListModel.saveChats();
close.accepted = false;
return;
}
if (window.shouldClose)
return; return;
window.shouldClose = true;
savingPopup.open(); savingPopup.open();
ChatListModel.saveChats(); ChatListModel.saveChats();
close.accepted = false close.accepted = false
@@ -191,8 +238,8 @@ Window {
Connections { Connections {
target: ChatListModel target: ChatListModel
function onSaveChatsFinished() { function onSaveChatsFinished() {
window.hasSaved = true;
savingPopup.close(); savingPopup.close();
if (window.shouldClose)
window.close() window.close()
} }
} }

View File

@@ -504,17 +504,34 @@ MySettingsTab {
} }
} }
MySettingsLabel { MySettingsLabel {
id: serverChatLabel id: trayLabel
text: qsTr("Enable Local API Server") text: qsTr("Enable System Tray")
helpText: qsTr("Expose an OpenAI-Compatible server to localhost. WARNING: Results in increased resource usage.") helpText: qsTr("The application will minimize to the system tray when the window is closed.")
Layout.row: 13 Layout.row: 13
Layout.column: 0 Layout.column: 0
} }
MyCheckBox { MyCheckBox {
id: serverChatBox id: trayBox
Layout.row: 13 Layout.row: 13
Layout.column: 2 Layout.column: 2
Layout.alignment: Qt.AlignRight 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 checked: MySettings.serverChat
onClicked: { onClicked: {
MySettings.serverChat = !MySettings.serverChat MySettings.serverChat = !MySettings.serverChat
@@ -524,7 +541,7 @@ MySettingsTab {
id: serverPortLabel id: serverPortLabel
text: qsTr("API Server Port") text: qsTr("API Server Port")
helpText: qsTr("The port to use for the local server. Requires restart.") helpText: qsTr("The port to use for the local server. Requires restart.")
Layout.row: 14 Layout.row: 15
Layout.column: 0 Layout.column: 0
} }
MyTextField { MyTextField {
@@ -532,7 +549,7 @@ MySettingsTab {
text: MySettings.networkPort text: MySettings.networkPort
color: theme.textColor color: theme.textColor
font.pixelSize: theme.fontSizeLarge font.pixelSize: theme.fontSizeLarge
Layout.row: 14 Layout.row: 15
Layout.column: 2 Layout.column: 2
Layout.minimumWidth: 200 Layout.minimumWidth: 200
Layout.maximumWidth: 200 Layout.maximumWidth: 200
@@ -577,12 +594,12 @@ MySettingsTab {
id: updatesLabel id: updatesLabel
text: qsTr("Check For Updates") text: qsTr("Check For Updates")
helpText: qsTr("Manually check for an update to GPT4All."); helpText: qsTr("Manually check for an update to GPT4All.");
Layout.row: 15 Layout.row: 16
Layout.column: 0 Layout.column: 0
} }
MySettingsButton { MySettingsButton {
Layout.row: 15 Layout.row: 16
Layout.column: 2 Layout.column: 2
Layout.alignment: Qt.AlignRight Layout.alignment: Qt.AlignRight
text: qsTr("Updates"); text: qsTr("Updates");
@@ -593,7 +610,7 @@ MySettingsTab {
} }
Rectangle { Rectangle {
Layout.row: 16 Layout.row: 17
Layout.column: 0 Layout.column: 0
Layout.columnSpan: 3 Layout.columnSpan: 3
Layout.fillWidth: true Layout.fillWidth: true

View File

@@ -130,6 +130,7 @@ public:
QList<QString> generatedQuestions() const { return m_generatedQuestions; } QList<QString> generatedQuestions() const { return m_generatedQuestions; }
bool needsSave() const { return m_needsSave; } bool needsSave() const { return m_needsSave; }
void setNeedsSave(bool n) { m_needsSave = n; }
public Q_SLOTS: public Q_SLOTS:
void serverNewPromptResponsePair(const QString &prompt, const QList<PromptAttachment> &attachments = {}); void serverNewPromptResponsePair(const QString &prompt, const QList<PromptAttachment> &attachments = {});

View File

@@ -51,6 +51,10 @@ void ChatListModel::loadChats()
connect(thread, &ChatsRestoreThread::finished, thread, &QObject::deleteLater); connect(thread, &ChatsRestoreThread::finished, thread, &QObject::deleteLater);
thread->start(); 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); connect(MySettings::globalInstance(), &MySettings::serverChatChanged, this, &ChatListModel::handleServerEnabledChanged);
} }
@@ -88,9 +92,6 @@ void ChatListModel::saveChats()
return; 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); emit requestSaveChats(toSave);
} }
@@ -128,6 +129,7 @@ void ChatSaver::saveChats(const QVector<Chat *> &chats)
continue; continue;
} }
chat->setNeedsSave(false);
if (originalFile.exists()) if (originalFile.exists())
originalFile.remove(); originalFile.remove();
tempFile.rename(filePath); tempFile.rename(filePath);

View File

@@ -33,7 +33,6 @@ class ChatSaver : public QObject
Q_OBJECT Q_OBJECT
public: public:
explicit ChatSaver(); explicit ChatSaver();
void stop();
Q_SIGNALS: Q_SIGNALS:
void saveChatsFinished(); void saveChatsFinished();
@@ -238,7 +237,6 @@ public Q_SLOTS:
Q_SIGNALS: Q_SIGNALS:
void countChanged(); void countChanged();
void currentChatChanged(); void currentChatChanged();
void chatsSavedFinished();
void requestSaveChats(const QVector<Chat*> &); void requestSaveChats(const QVector<Chat*> &);
void saveChatsFinished(); void saveChatsFinished();

View File

@@ -19,6 +19,10 @@
# include "network.h" # include "network.h"
#endif #endif
#ifdef Q_OS_MAC
#include "macosdock.h"
#endif
using namespace Qt::Literals::StringLiterals; using namespace Qt::Literals::StringLiterals;
class MyLLM: public LLM { }; class MyLLM: public LLM { };
@@ -105,3 +109,21 @@ bool LLM::isNetworkOnline() const
auto * netinfo = QNetworkInformation::instance(); auto * netinfo = QNetworkInformation::instance();
return !netinfo || netinfo->reachability() == QNetworkInformation::Reachability::Online; 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
}

View File

@@ -23,6 +23,9 @@ public:
Q_INVOKABLE QString systemTotalRAMInGBString() const; Q_INVOKABLE QString systemTotalRAMInGBString() const;
Q_INVOKABLE bool isNetworkOnline() const; Q_INVOKABLE bool isNetworkOnline() const;
Q_INVOKABLE void showDockIcon() const;
Q_INVOKABLE void hideDockIcon() const;
Q_SIGNALS: Q_SIGNALS:
void isNetworkOnlineChanged(); void isNetworkOnlineChanged();

View File

@@ -0,0 +1,9 @@
#ifndef MACOSDOCK_H
#define MACOSDOCK_H
struct MacOSDock {
static void showIcon();
static void hideIcon();
};
#endif // MACOSDOCK_H

View File

@@ -0,0 +1,13 @@
#include "macosdock.h"
#include <Cocoa/Cocoa.h>
void MacOSDock::showIcon()
{
[[NSApplication sharedApplication] setActivationPolicy:NSApplicationActivationPolicyRegular];
}
void MacOSDock::hideIcon()
{
[[NSApplication sharedApplication] setActivationPolicy:NSApplicationActivationPolicyProhibited];
}

View File

@@ -44,6 +44,7 @@ static void raiseWindow(QWindow *window)
SetForegroundWindow(hwnd); SetForegroundWindow(hwnd);
#else #else
LLM::globalInstance()->showDockIcon();
window->show(); window->show();
window->raise(); window->raise();
window->requestActivate(); window->requestActivate();

View File

@@ -49,6 +49,7 @@ static const QVariantMap basicDefaults {
{ "lastVersionStarted", "" }, { "lastVersionStarted", "" },
{ "networkPort", 4891, }, { "networkPort", 4891, },
{ "saveChatsContext", false }, { "saveChatsContext", false },
{ "systemTray", false },
{ "serverChat", false }, { "serverChat", false },
{ "userDefaultModel", "Application default" }, { "userDefaultModel", "Application default" },
{ "suggestionMode", QVariant::fromValue(SuggestionMode::LocalDocsOnly) }, { "suggestionMode", QVariant::fromValue(SuggestionMode::LocalDocsOnly) },
@@ -206,6 +207,7 @@ void MySettings::restoreApplicationDefaults()
setDevice(defaults::device); setDevice(defaults::device);
setThreadCount(defaults::threadCount); setThreadCount(defaults::threadCount);
setSaveChatsContext(basicDefaults.value("saveChatsContext").toBool()); setSaveChatsContext(basicDefaults.value("saveChatsContext").toBool());
setSystemTray(basicDefaults.value("saveTrayContext").toBool());
setServerChat(basicDefaults.value("serverChat").toBool()); setServerChat(basicDefaults.value("serverChat").toBool());
setNetworkPort(basicDefaults.value("networkPort").toInt()); setNetworkPort(basicDefaults.value("networkPort").toInt());
setModelPath(defaultLocalModelsPath()); setModelPath(defaultLocalModelsPath());
@@ -444,6 +446,7 @@ void MySettings::setThreadCount(int value)
} }
bool MySettings::saveChatsContext() const { return getBasicSetting("saveChatsContext" ).toBool(); } bool MySettings::saveChatsContext() const { return getBasicSetting("saveChatsContext" ).toBool(); }
bool MySettings::systemTray() const { return getBasicSetting("systemTray" ).toBool(); }
bool MySettings::serverChat() const { return getBasicSetting("serverChat" ).toBool(); } bool MySettings::serverChat() const { return getBasicSetting("serverChat" ).toBool(); }
int MySettings::networkPort() const { return getBasicSetting("networkPort" ).toInt(); } int MySettings::networkPort() const { return getBasicSetting("networkPort" ).toInt(); }
QString MySettings::userDefaultModel() const { return getBasicSetting("userDefaultModel" ).toString(); } 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)); } SuggestionMode MySettings::suggestionMode() const { return SuggestionMode(getEnumSetting("suggestionMode", suggestionModeNames)); }
void MySettings::setSaveChatsContext(bool value) { setBasicSetting("saveChatsContext", value); } 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::setServerChat(bool value) { setBasicSetting("serverChat", value); }
void MySettings::setNetworkPort(int value) { setBasicSetting("networkPort", value); } void MySettings::setNetworkPort(int value) { setBasicSetting("networkPort", value); }
void MySettings::setUserDefaultModel(const QString &value) { setBasicSetting("userDefaultModel", value); } void MySettings::setUserDefaultModel(const QString &value) { setBasicSetting("userDefaultModel", value); }

View File

@@ -49,6 +49,7 @@ class MySettings : public QObject
Q_OBJECT Q_OBJECT
Q_PROPERTY(int threadCount READ threadCount WRITE setThreadCount NOTIFY threadCountChanged) Q_PROPERTY(int threadCount READ threadCount WRITE setThreadCount NOTIFY threadCountChanged)
Q_PROPERTY(bool saveChatsContext READ saveChatsContext WRITE setSaveChatsContext NOTIFY saveChatsContextChanged) 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(bool serverChat READ serverChat WRITE setServerChat NOTIFY serverChatChanged)
Q_PROPERTY(QString modelPath READ modelPath WRITE setModelPath NOTIFY modelPathChanged) Q_PROPERTY(QString modelPath READ modelPath WRITE setModelPath NOTIFY modelPathChanged)
Q_PROPERTY(QString userDefaultModel READ userDefaultModel WRITE setUserDefaultModel NOTIFY userDefaultModelChanged) Q_PROPERTY(QString userDefaultModel READ userDefaultModel WRITE setUserDefaultModel NOTIFY userDefaultModelChanged)
@@ -142,6 +143,8 @@ public:
void setThreadCount(int value); void setThreadCount(int value);
bool saveChatsContext() const; bool saveChatsContext() const;
void setSaveChatsContext(bool value); void setSaveChatsContext(bool value);
bool systemTray() const;
void setSystemTray(bool value);
bool serverChat() const; bool serverChat() const;
void setServerChat(bool value); void setServerChat(bool value);
QString modelPath(); QString modelPath();
@@ -218,6 +221,7 @@ Q_SIGNALS:
void suggestedFollowUpPromptChanged(const ModelInfo &info); void suggestedFollowUpPromptChanged(const ModelInfo &info);
void threadCountChanged(); void threadCountChanged();
void saveChatsContextChanged(); void saveChatsContextChanged();
void systemTrayChanged();
void serverChatChanged(); void serverChatChanged();
void modelPathChanged(); void modelPathChanged();
void userDefaultModelChanged(); void userDefaultModelChanged();