diff --git a/obs/data/locale/en-US.ini b/obs/data/locale/en-US.ini index 5be0dc155..f3467f4d2 100644 --- a/obs/data/locale/en-US.ini +++ b/obs/data/locale/en-US.ini @@ -383,6 +383,8 @@ Basic.Settings.General.SourceSnapping="Snap Sources to other sources" Basic.Settings.General.SnapDistance="Snap Sensitivity" Basic.Settings.General.RecordWhenStreaming="Automatically record when streaming" Basic.Settings.General.KeepRecordingWhenStreamStops="Keep recording when stream stops" +Basic.Settings.General.SysTrayEnabled="Enable system tray icon" +Basic.Settings.General.SysTrayWhenStarted="Minimize to system tray when started" # basic mode 'stream' settings Basic.Settings.Stream="Stream" @@ -550,6 +552,13 @@ Basic.Hotkeys.StartRecording="Start Recording" Basic.Hotkeys.StopRecording="Stop Recording" Basic.Hotkeys.SelectScene="Switch to scene" +# system tray +Basic.SystemTray.Show="Show" +Basic.SystemTray.Hide="Hide" + +# system tray messages +Basic.SystemTray.Message.Reconnecting="Disconnected. Reconnecting..." + # hotkeys that may lack translation on certain operating systems Hotkeys.Insert="Insert" Hotkeys.Delete="Delete" diff --git a/obs/forms/OBSBasicSettings.ui b/obs/forms/OBSBasicSettings.ui index be10b2f20..64ce0fd1c 100644 --- a/obs/forms/OBSBasicSettings.ui +++ b/obs/forms/OBSBasicSettings.ui @@ -192,14 +192,31 @@ - + + + + Basic.Settings.General.SysTrayEnabled + + + + + + + false + + + Basic.Settings.General.SysTrayWhenStarted + + + + Qt::Horizontal - + true @@ -3753,5 +3770,21 @@ + + systemTrayEnabled + toggled(bool) + systemTrayWhenStarted + setEnabled(bool) + + + 404 + 245 + + + 404 + 271 + + + diff --git a/obs/forms/images/tray_active.png b/obs/forms/images/tray_active.png new file mode 100644 index 000000000..d8da1c5f9 Binary files /dev/null and b/obs/forms/images/tray_active.png differ diff --git a/obs/forms/obs.qrc b/obs/forms/obs.qrc index 60ab59a4b..361fef4ae 100644 --- a/obs/forms/obs.qrc +++ b/obs/forms/obs.qrc @@ -13,6 +13,7 @@ images/properties.png images/up.png images/obs.png + images/tray_active.png images/settings/advanced.png diff --git a/obs/obs-app.cpp b/obs/obs-app.cpp index 232f8c85f..0584e18eb 100644 --- a/obs/obs-app.cpp +++ b/obs/obs-app.cpp @@ -360,6 +360,10 @@ bool OBSApp::InitGlobalConfigDefaults() "RecordWhenStreaming", false); config_set_default_bool(globalConfig, "BasicWindow", "KeepRecordingWhenStreamStops", false); + config_set_default_bool(globalConfig, "BasicWindow", + "SysTrayEnabled", true); + config_set_default_bool(globalConfig, "BasicWindow", + "SysTrayWhenStarted", false); config_set_default_bool(globalConfig, "BasicWindow", "ShowTransitions", true); config_set_default_bool(globalConfig, "BasicWindow", diff --git a/obs/window-basic-main-outputs.cpp b/obs/window-basic-main-outputs.cpp index e94503b3b..2415d75d6 100644 --- a/obs/window-basic-main-outputs.cpp +++ b/obs/window-basic-main-outputs.cpp @@ -604,9 +604,13 @@ bool SimpleOutput::StartRecording() os_dir_t *dir = path ? os_opendir(path) : nullptr; if (!dir) { - QMessageBox::information(main, - QTStr("Output.BadPath.Title"), - QTStr("Output.BadPath.Text")); + if (main->isVisible()) + QMessageBox::information(main, + QTStr("Output.BadPath.Title"), + QTStr("Output.BadPath.Text")); + else + main->SysTrayNotify(QTStr("Output.BadPath.Text"), + QSystemTrayIcon::Warning); return false; } @@ -1147,9 +1151,13 @@ bool AdvancedOutput::StartRecording() os_dir_t *dir = path ? os_opendir(path) : nullptr; if (!dir) { - QMessageBox::information(main, - QTStr("Output.BadPath.Title"), - QTStr("Output.BadPath.Text")); + if (main->isVisible()) + QMessageBox::information(main, + QTStr("Output.BadPath.Title"), + QTStr("Output.BadPath.Text")); + else + main->SysTrayNotify(QTStr("Output.BadPath.Text"), + QSystemTrayIcon::Warning); return false; } diff --git a/obs/window-basic-main.cpp b/obs/window-basic-main.cpp index b12246342..d2aecbac3 100644 --- a/obs/window-basic-main.cpp +++ b/obs/window-basic-main.cpp @@ -1181,6 +1181,8 @@ void OBSBasic::OBSInit() } ui->mainSplitter->setSizes(defSizes); + + SystemTray(true); } void OBSBasic::InitHotkeys() @@ -1447,8 +1449,11 @@ OBSBasic::~OBSBasic() QList splitterSizes = ui->mainSplitter->sizes(); bool alwaysOnTop = IsAlwaysOnTop(this); - config_set_string(App()->GlobalConfig(), "BasicWindow", "geometry", - saveGeometry().toBase64().constData()); + if (isVisible()) + config_set_string(App()->GlobalConfig(), + "BasicWindow", "geometry", + saveGeometry().toBase64().constData()); + config_set_int(App()->GlobalConfig(), "BasicWindow", "splitterTop", splitterSizes[0]); config_set_int(App()->GlobalConfig(), "BasicWindow", "splitterBottom", @@ -2610,6 +2615,8 @@ void OBSBasic::closeEvent(QCloseEvent *event) blog(LOG_INFO, SHUTDOWN_SEPARATOR); if (outputHandler && outputHandler->Active()) { + SetShowing(true); + QMessageBox::StandardButton button = QMessageBox::question( this, QTStr("ConfirmExit.Title"), QTStr("ConfirmExit.Text")); @@ -2668,6 +2675,7 @@ void OBSBasic::on_action_Settings_triggered() { OBSBasicSettings settings(this); settings.exec(); + SystemTray(false); } void OBSBasic::on_actionAdvAudioProperties_triggered() @@ -3533,10 +3541,14 @@ void OBSBasic::StartStreaming() ui->streamButton->setEnabled(false); ui->streamButton->setText(QTStr("Basic.Main.Connecting")); + sysTrayStream->setEnabled(false); + sysTrayStream->setText(ui->streamButton->text()); if (!outputHandler->StartStreaming(service)) { ui->streamButton->setText(QTStr("Basic.Main.StartStreaming")); ui->streamButton->setEnabled(true); + sysTrayStream->setText(ui->streamButton->text()); + sysTrayStream->setEnabled(true); } bool recordWhenStreaming = config_get_bool(GetGlobalConfig(), @@ -3572,6 +3584,8 @@ inline void OBSBasic::OnActivate() ui->profileMenu->setEnabled(false); App()->IncrementSleepInhibition(); UpdateProcessPriority(); + + trayIcon->setIcon(QIcon(":/res/images/tray_active.png")); } } @@ -3581,6 +3595,8 @@ inline void OBSBasic::OnDeactivate() ui->profileMenu->setEnabled(true); App()->DecrementSleepInhibition(); ClearProcessPriority(); + + trayIcon->setIcon(QIcon(":/res/images/obs.png")); } } @@ -3622,6 +3638,8 @@ void OBSBasic::StreamDelayStarting(int sec) { ui->streamButton->setText(QTStr("Basic.Main.StopStreaming")); ui->streamButton->setEnabled(true); + sysTrayStream->setText(ui->streamButton->text()); + sysTrayStream->setEnabled(true); if (!startStreamMenu.isNull()) startStreamMenu->deleteLater(); @@ -3642,6 +3660,8 @@ void OBSBasic::StreamDelayStopping(int sec) { ui->streamButton->setText(QTStr("Basic.Main.StartStreaming")); ui->streamButton->setEnabled(true); + sysTrayStream->setText(ui->streamButton->text()); + sysTrayStream->setEnabled(true); if (!startStreamMenu.isNull()) startStreamMenu->deleteLater(); @@ -3661,6 +3681,8 @@ void OBSBasic::StreamingStart() ui->streamButton->setText(QTStr("Basic.Main.StopStreaming")); ui->streamButton->setEnabled(true); ui->statusbar->StreamStarted(outputHandler->streamOutput); + sysTrayStream->setText(ui->streamButton->text()); + sysTrayStream->setEnabled(true); OnActivate(); @@ -3670,6 +3692,7 @@ void OBSBasic::StreamingStart() void OBSBasic::StreamStopping() { ui->streamButton->setText(QTStr("Basic.Main.StoppingStreaming")); + sysTrayStream->setText(ui->streamButton->text()); } void OBSBasic::StreamingStop(int code) @@ -3704,15 +3727,20 @@ void OBSBasic::StreamingStop(int code) ui->streamButton->setText(QTStr("Basic.Main.StartStreaming")); ui->streamButton->setEnabled(true); + sysTrayStream->setText(ui->streamButton->text()); + sysTrayStream->setEnabled(true); OnDeactivate(); blog(LOG_INFO, STREAMING_STOP); - if (code != OBS_OUTPUT_SUCCESS) + if (code != OBS_OUTPUT_SUCCESS && isVisible()) { QMessageBox::information(this, QTStr("Output.ConnectFail.Title"), QT_UTF8(errorMessage)); + } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { + SysTrayNotify(QT_UTF8(errorMessage), QSystemTrayIcon::Warning); + } if (!startStreamMenu.isNull()) { ui->streamButton->setMenu(nullptr); @@ -3733,6 +3761,7 @@ void OBSBasic::StartRecording() void OBSBasic::RecordStopping() { ui->recordButton->setText(QTStr("Basic.Main.StoppingRecording")); + sysTrayRecord->setText(ui->recordButton->text()); } void OBSBasic::StopRecording() @@ -3749,6 +3778,7 @@ void OBSBasic::RecordingStart() { ui->statusbar->RecordingStarted(outputHandler->fileOutput); ui->recordButton->setText(QTStr("Basic.Main.StopRecording")); + sysTrayRecord->setText(ui->recordButton->text()); OnActivate(); @@ -3759,22 +3789,35 @@ void OBSBasic::RecordingStop(int code) { ui->statusbar->RecordingStopped(); ui->recordButton->setText(QTStr("Basic.Main.StartRecording")); + sysTrayRecord->setText(ui->recordButton->text()); blog(LOG_INFO, RECORDING_STOP); - if (code == OBS_OUTPUT_UNSUPPORTED) { + if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) { QMessageBox::information(this, QTStr("Output.RecordFail.Title"), QTStr("Output.RecordFail.Unsupported")); - } else if (code == OBS_OUTPUT_NO_SPACE) { + } else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) { QMessageBox::information(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); - } else if (code != OBS_OUTPUT_SUCCESS) { + } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { QMessageBox::information(this, QTStr("Output.RecordError.Title"), QTStr("Output.RecordError.Msg")); + + } else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordFail.Unsupported"), + QSystemTrayIcon::Warning); + + } else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"), + QSystemTrayIcon::Warning); + + } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordError.Msg"), + QSystemTrayIcon::Warning); } OnDeactivate(); @@ -3786,7 +3829,7 @@ void OBSBasic::on_streamButton_clicked() bool confirm = config_get_bool(GetGlobalConfig(), "BasicWindow", "WarnBeforeStoppingStream"); - if (confirm) { + if (confirm && isVisible()) { QMessageBox::StandardButton button = QMessageBox::question(this, QTStr("ConfirmStop.Title"), @@ -3801,7 +3844,7 @@ void OBSBasic::on_streamButton_clicked() bool confirm = config_get_bool(GetGlobalConfig(), "BasicWindow", "WarnBeforeStartingStream"); - if (confirm) { + if (confirm && isVisible()) { QMessageBox::StandardButton button = QMessageBox::question(this, QTStr("ConfirmStart.Title"), @@ -3827,6 +3870,7 @@ void OBSBasic::on_settingsButton_clicked() { OBSBasicSettings settings(this); settings.exec(); + SystemTray(false); } void OBSBasic::on_actionWebsite_triggered() @@ -4389,3 +4433,115 @@ void OBSBasic::on_actionLockPreview_triggered() ui->preview->ToggleLocked(); ui->actionLockPreview->setChecked(ui->preview->Locked()); } + +void OBSBasic::SetShowing(bool showing) +{ + if (!showing && isVisible()) { + config_set_string(App()->GlobalConfig(), + "BasicWindow", "geometry", + saveGeometry().toBase64().constData()); + + showHide->setText(QTStr("Basic.SystemTray.Show")); + QTimer::singleShot(250, this, SLOT(hide())); + + if (previewEnabled) + EnablePreviewDisplay(false); + + setVisible(false); + + } else if (showing && !isVisible()) { + showHide->setText(QTStr("Basic.SystemTray.Hide")); + QTimer::singleShot(250, this, SLOT(show())); + + if (previewEnabled) + EnablePreviewDisplay(true); + + setVisible(true); + } +} + +void OBSBasic::SystemTrayInit() { + trayIcon = new QSystemTrayIcon(QIcon(":/res/images/obs.png"), + this); + trayIcon->setToolTip("OBS Studio"); + + showHide = new QAction(QTStr("Basic.SystemTray.Show"), + trayIcon); + sysTrayStream = new QAction(QTStr("Basic.Main.StartStreaming"), + trayIcon); + sysTrayRecord = new QAction(QTStr("Basic.Main.StartRecording"), + trayIcon); + exit = new QAction(QTStr("Exit"), + trayIcon); + + connect(trayIcon, SIGNAL(activated(QSystemTrayIcon::ActivationReason)), + this, + SLOT(IconActivated(QSystemTrayIcon::ActivationReason))); + connect(showHide, SIGNAL(triggered()), + this, SLOT(ToggleShowHide())); + connect(sysTrayStream, SIGNAL(triggered()), + this, SLOT(on_streamButton_clicked())); + connect(sysTrayRecord, SIGNAL(triggered()), + this, SLOT(on_recordButton_clicked())); + connect(exit, SIGNAL(triggered()), + this, SLOT(close())); + + trayMenu = new QMenu; + trayMenu->addAction(showHide); + trayMenu->addAction(sysTrayStream); + trayMenu->addAction(sysTrayRecord); + trayMenu->addAction(exit); + trayIcon->setContextMenu(trayMenu); +} + +void OBSBasic::IconActivated(QSystemTrayIcon::ActivationReason reason) +{ + if (reason == QSystemTrayIcon::Trigger) + ToggleShowHide(); +} + +void OBSBasic::SysTrayNotify(const QString &text, + QSystemTrayIcon::MessageIcon n) +{ + if (QSystemTrayIcon::supportsMessages()) { + QSystemTrayIcon::MessageIcon icon = + QSystemTrayIcon::MessageIcon(n); + trayIcon->showMessage("OBS Studio", text, icon, 10000); + } +} + +void OBSBasic::SystemTray(bool firstStarted) +{ + if (!QSystemTrayIcon::isSystemTrayAvailable()) + return; + + bool sysTrayWhenStarted = config_get_bool(GetGlobalConfig(), + "BasicWindow", "SysTrayWhenStarted"); + bool sysTrayEnabled = config_get_bool(GetGlobalConfig(), + "BasicWindow", "SysTrayEnabled"); + + if (firstStarted) + SystemTrayInit(); + + if (!sysTrayWhenStarted && !sysTrayEnabled) { + trayIcon->hide(); + } else if (sysTrayWhenStarted && sysTrayEnabled) { + trayIcon->show(); + if (firstStarted) { + QTimer::singleShot(50, this, SLOT(hide())); + EnablePreviewDisplay(false); + setVisible(false); + } + } else if (sysTrayEnabled) { + trayIcon->show(); + } else if (!sysTrayEnabled) { + trayIcon->hide(); + } else if (!sysTrayWhenStarted && sysTrayEnabled) { + trayIcon->hide(); + } + + if (isVisible()) + showHide->setText(QTStr("Basic.SystemTray.Hide")); + else + showHide->setText(QTStr("Basic.SystemTray.Show")); +} diff --git a/obs/window-basic-main.hpp b/obs/window-basic-main.hpp index 4fedeee6c..c67a762cf 100644 --- a/obs/window-basic-main.hpp +++ b/obs/window-basic-main.hpp @@ -19,6 +19,7 @@ #include #include +#include #include #include #include @@ -135,6 +136,14 @@ private: QPointer startStreamMenu; + QSystemTrayIcon *trayIcon; + QMenu *trayMenu; + QAction *sysTrayStream; + QAction *sysTrayRecord; + QAction *showHide; + QAction *showPreview; + QAction *exit; + void DrawBackdrop(float cx, float cy); void SetupEncoders(); @@ -340,6 +349,14 @@ private slots: void SetScaleFilter(); + void IconActivated(QSystemTrayIcon::ActivationReason reason); + void SetShowing(bool showing); + + inline void ToggleShowHide() + { + SetShowing(!isVisible()); + } + private: /* OBS Callbacks */ static void SceneReordered(void *data, calldata_t *params); @@ -366,6 +383,8 @@ private: public: OBSScene GetCurrentScene(); + void SysTrayNotify(const QString &text, QSystemTrayIcon::MessageIcon n); + inline OBSSource GetCurrentSceneSource() { OBSScene curScene = GetCurrentScene(); @@ -414,6 +433,9 @@ public: void UpdateTitleBar(); void UpdateSceneSelection(OBSSource source); + void SystemTrayInit(); + void SystemTray(bool firstStarted); + protected: virtual void closeEvent(QCloseEvent *event) override; virtual void changeEvent(QEvent *event) override; diff --git a/obs/window-basic-settings.cpp b/obs/window-basic-settings.cpp index fef022bf7..3bd064881 100644 --- a/obs/window-basic-settings.cpp +++ b/obs/window-basic-settings.cpp @@ -274,6 +274,8 @@ OBSBasicSettings::OBSBasicSettings(QWidget *parent) HookWidget(ui->hideProjectorCursor, CHECK_CHANGED, GENERAL_CHANGED); HookWidget(ui->recordWhenStreaming, CHECK_CHANGED, GENERAL_CHANGED); HookWidget(ui->keepRecordStreamStops,CHECK_CHANGED, GENERAL_CHANGED); + HookWidget(ui->systemTrayEnabled, CHECK_CHANGED, GENERAL_CHANGED); + HookWidget(ui->systemTrayWhenStarted,CHECK_CHANGED, GENERAL_CHANGED); HookWidget(ui->snappingEnabled, CHECK_CHANGED, GENERAL_CHANGED); HookWidget(ui->screenSnapping, CHECK_CHANGED, GENERAL_CHANGED); HookWidget(ui->centerSnapping, CHECK_CHANGED, GENERAL_CHANGED); @@ -845,6 +847,14 @@ void OBSBasicSettings::LoadGeneralSettings() "BasicWindow", "KeepRecordingWhenStreamStops"); ui->keepRecordStreamStops->setChecked(keepRecordStreamStops); + bool systemTrayEnabled = config_get_bool(GetGlobalConfig(), + "BasicWindow", "SysTrayEnabled"); + ui->systemTrayEnabled->setChecked(systemTrayEnabled); + + bool systemTrayWhenStarted = config_get_bool(GetGlobalConfig(), + "BasicWindow", "SysTrayWhenStarted"); + ui->systemTrayWhenStarted->setChecked(systemTrayWhenStarted); + bool snappingEnabled = config_get_bool(GetGlobalConfig(), "BasicWindow", "SnappingEnabled"); ui->snappingEnabled->setChecked(snappingEnabled); @@ -2235,6 +2245,16 @@ void OBSBasicSettings::SaveGeneralSettings() config_set_bool(GetGlobalConfig(), "BasicWindow", "KeepRecordingWhenStreamStops", ui->keepRecordStreamStops->isChecked()); + + if (WidgetChanged(ui->systemTrayEnabled)) + config_set_bool(GetGlobalConfig(), "BasicWindow", + "SysTrayEnabled", + ui->systemTrayEnabled->isChecked()); + + if (WidgetChanged(ui->systemTrayWhenStarted)) + config_set_bool(GetGlobalConfig(), "BasicWindow", + "SysTrayWhenStarted", + ui->systemTrayWhenStarted->isChecked()); } void OBSBasicSettings::SaveStream1Settings() diff --git a/obs/window-basic-status-bar.cpp b/obs/window-basic-status-bar.cpp index 64015efca..4c37b0e3f 100644 --- a/obs/window-basic-status-bar.cpp +++ b/obs/window-basic-status-bar.cpp @@ -72,6 +72,7 @@ void OBSBasicStatusBar::Deactivate() delaySecStopping = 0; reconnectTimeout = 0; active = false; + overloadedNotify = true; } } @@ -165,9 +166,10 @@ void OBSBasicStatusBar::UpdateSessionTime() sessionTime->setMinimumWidth(sessionTime->width()); if (reconnectTimeout > 0) { - QString msg = QTStr("Basic.StatusBar.Reconnecting"); - showMessage(msg.arg(QString::number(retries), - QString::number(reconnectTimeout))); + QString msg = QTStr("Basic.StatusBar.Reconnecting") + .arg(QString::number(retries), + QString::number(reconnectTimeout)); + showMessage(msg); reconnectTimeout--; } else if (retries > 0) { @@ -224,12 +226,20 @@ void OBSBasicStatusBar::OBSOutputReconnectSuccess(void *data, calldata_t *params void OBSBasicStatusBar::Reconnect(int seconds) { - retries++; + OBSBasic *main = qobject_cast(parent()); + + if (!retries) + main->SysTrayNotify( + QTStr("Basic.SystemTray.Message.Reconnecting"), + QSystemTrayIcon::Warning); + reconnectTimeout = seconds; if (streamOutput) { delaySecTotal = obs_output_get_active_delay(streamOutput); UpdateDelayMsg(); + + retries++; } } @@ -246,7 +256,11 @@ void OBSBasicStatusBar::ReconnectClear() void OBSBasicStatusBar::ReconnectSuccess() { - showMessage(QTStr("Basic.StatusBar.ReconnectSuccessful"), 4000); + OBSBasic *main = qobject_cast(parent()); + + QString msg = QTStr("Basic.StatusBar.ReconnectSuccessful"); + showMessage(msg, 4000); + main->SysTrayNotify(msg, QSystemTrayIcon::Information); ReconnectClear(); if (streamOutput) { @@ -257,6 +271,8 @@ void OBSBasicStatusBar::ReconnectSuccess() void OBSBasicStatusBar::UpdateStatusBar() { + OBSBasic *main = qobject_cast(parent()); + UpdateBandwidth(); UpdateSessionTime(); UpdateDroppedFrames(); @@ -270,8 +286,14 @@ void OBSBasicStatusBar::UpdateStatusBar() int diff = skipped - lastSkippedFrameCount; double percentage = double(skipped) / double(total) * 100.0; - if (diff > 10 && percentage >= 0.1f) + if (diff > 10 && percentage >= 0.1f) { showMessage(QTStr("HighResourceUsage"), 4000); + if (!main->isVisible() && overloadedNotify) { + main->SysTrayNotify(QTStr("HighResourceUsage"), + QSystemTrayIcon::Warning); + overloadedNotify = false; + } + } lastSkippedFrameCount = skipped; } diff --git a/obs/window-basic-status-bar.hpp b/obs/window-basic-status-bar.hpp index 3f9bc8fa0..e0c7a0d0a 100644 --- a/obs/window-basic-status-bar.hpp +++ b/obs/window-basic-status-bar.hpp @@ -21,6 +21,7 @@ private: obs_output_t *streamOutput = nullptr; obs_output_t *recordOutput = nullptr; bool active = false; + bool overloadedNotify = true; int retries = 0; int totalSeconds = 0;