/****************************************************************************** Copyright (C) 2023 by Lain Bailey Zachary Lund Philippe Groarke This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . ******************************************************************************/ #include "OBSBasic.hpp" #include "OBSBasicStats.hpp" #include #include #include #include #include #include #include #include #ifdef __APPLE__ #include #endif #include #include #ifdef _WIN32 #include #endif #include #if defined(_WIN32) || defined(WHATSNEW_ENABLED) #include #endif #include #include #include #ifdef _WIN32 #include #endif extern bool restart; extern bool restart_safe; extern volatile long insideEventLoop; extern bool safe_mode; struct QCef; struct QCefCookieManager; extern QCef *cef; extern QCefCookieManager *panel_cookies; using namespace std; void OBSBasic::CreateInteractionWindow(obs_source_t *source) { bool closed = true; if (interaction) closed = interaction->close(); if (!closed) return; interaction = new OBSBasicInteraction(this, source); interaction->Init(); interaction->setAttribute(Qt::WA_DeleteOnClose, true); } void OBSBasic::CreatePropertiesWindow(obs_source_t *source) { bool closed = true; if (properties) closed = properties->close(); if (!closed) return; properties = new OBSBasicProperties(this, source); properties->Init(); properties->setAttribute(Qt::WA_DeleteOnClose, true); } void OBSBasic::CreateFiltersWindow(obs_source_t *source) { bool closed = true; if (filters) closed = filters->close(); if (!closed) return; filters = new OBSBasicFilters(this, source); filters->Init(); filters->setAttribute(Qt::WA_DeleteOnClose, true); } void OBSBasic::updateCheckFinished() { ui->actionCheckForUpdates->setEnabled(true); ui->actionRepair->setEnabled(true); } void OBSBasic::ResetUI() { bool studioPortraitLayout = config_get_bool(App()->GetUserConfig(), "BasicWindow", "StudioPortraitLayout"); if (studioPortraitLayout) ui->previewLayout->setDirection(QBoxLayout::BottomToTop); else ui->previewLayout->setDirection(QBoxLayout::LeftToRight); UpdatePreviewProgramIndicators(); } void OBSBasic::CloseDialogs() { QList childDialogs = this->findChildren(); if (!childDialogs.isEmpty()) { for (int i = 0; i < childDialogs.size(); ++i) { childDialogs.at(i)->close(); } } if (!stats.isNull()) stats->close(); //call close to save Stats geometry if (!remux.isNull()) remux->close(); } void OBSBasic::EnumDialogs() { visDialogs.clear(); modalDialogs.clear(); visMsgBoxes.clear(); /* fill list of Visible dialogs and Modal dialogs */ QList dialogs = findChildren(); for (QDialog *dialog : dialogs) { if (dialog->isVisible()) visDialogs.append(dialog); if (dialog->isModal()) modalDialogs.append(dialog); } /* fill list of Visible message boxes */ QList msgBoxes = findChildren(); for (QMessageBox *msgbox : msgBoxes) { if (msgbox->isVisible()) visMsgBoxes.append(msgbox); } } void OBSBasic::on_actionRemux_triggered() { if (!remux.isNull()) { remux->show(); remux->raise(); return; } const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); const char *path = strcmp(mode, "Advanced") ? config_get_string(activeConfiguration, "SimpleOutput", "FilePath") : config_get_string(activeConfiguration, "AdvOut", "RecFilePath"); OBSRemux *remuxDlg; remuxDlg = new OBSRemux(path, this); remuxDlg->show(); remux = remuxDlg; } void OBSBasic::on_action_Settings_triggered() { static bool settings_already_executing = false; /* Do not load settings window if inside of a temporary event loop * because we could be inside of an Auth::LoadUI call. Keep trying * once per second until we've exit any known sub-loops. */ if (os_atomic_load_long(&insideEventLoop) != 0) { QTimer::singleShot(1000, this, &OBSBasic::on_action_Settings_triggered); return; } if (settings_already_executing) { return; } settings_already_executing = true; { OBSBasicSettings settings(this); settings.exec(); } settings_already_executing = false; if (restart) { QMessageBox::StandardButton button = OBSMessageBox::question(this, QTStr("Restart"), QTStr("NeedsRestart")); if (button == QMessageBox::Yes) close(); else restart = false; } } void OBSBasic::on_actionShowMacPermissions_triggered() { #ifdef __APPLE__ OBSPermissions check(this, CheckPermission(kScreenCapture), CheckPermission(kVideoDeviceAccess), CheckPermission(kAudioDeviceAccess), CheckPermission(kInputMonitoring)); check.exec(); #endif } void OBSBasic::on_actionAdvAudioProperties_triggered() { if (advAudioWindow != nullptr) { advAudioWindow->raise(); return; } bool iconsVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons"); advAudioWindow = new OBSBasicAdvAudio(this); advAudioWindow->show(); advAudioWindow->setAttribute(Qt::WA_DeleteOnClose, true); advAudioWindow->SetIconsVisible(iconsVisible); } static BPtr ReadLogFile(const char *subdir, const char *log) { char logDir[512]; if (GetAppConfigPath(logDir, sizeof(logDir), subdir) <= 0) return nullptr; string path = logDir; path += "/"; path += log; BPtr file = os_quick_read_utf8_file(path.c_str()); if (!file) blog(LOG_WARNING, "Failed to read log file %s", path.c_str()); return file; } void OBSBasic::UploadLog(const char *subdir, const char *file, const bool crash) { BPtr fileString{ReadLogFile(subdir, file)}; if (!fileString) return; if (!*fileString) return; ui->menuLogFiles->setEnabled(false); #if defined(_WIN32) ui->menuCrashLogs->setEnabled(false); #endif stringstream ss; ss << "OBS " << App()->GetVersionString(false) << " log file uploaded at " << CurrentDateTimeString() << "\n\n" << fileString; if (logUploadThread) { logUploadThread->wait(); } RemoteTextThread *thread = new RemoteTextThread("https://obsproject.com/logs/upload", "text/plain", ss.str()); logUploadThread.reset(thread); if (crash) { connect(thread, &RemoteTextThread::Result, this, &OBSBasic::crashUploadFinished); } else { connect(thread, &RemoteTextThread::Result, this, &OBSBasic::logUploadFinished); } logUploadThread->start(); } void OBSBasic::on_actionShowLogs_triggered() { char logDir[512]; if (GetAppConfigPath(logDir, sizeof(logDir), "obs-studio/logs") <= 0) return; QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir)); QDesktopServices::openUrl(url); } void OBSBasic::on_actionUploadCurrentLog_triggered() { UploadLog("obs-studio/logs", App()->GetCurrentLog(), false); } void OBSBasic::on_actionUploadLastLog_triggered() { UploadLog("obs-studio/logs", App()->GetLastLog(), false); } void OBSBasic::on_actionViewCurrentLog_triggered() { if (!logView) logView = new OBSLogViewer(); logView->show(); logView->setWindowState((logView->windowState() & ~Qt::WindowMinimized) | Qt::WindowActive); logView->activateWindow(); logView->raise(); } void OBSBasic::on_actionShowCrashLogs_triggered() { char logDir[512]; if (GetAppConfigPath(logDir, sizeof(logDir), "obs-studio/crashes") <= 0) return; QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir)); QDesktopServices::openUrl(url); } void OBSBasic::on_actionUploadLastCrashLog_triggered() { UploadLog("obs-studio/crashes", App()->GetLastCrashLog(), true); } void OBSBasic::on_actionCheckForUpdates_triggered() { CheckForUpdates(true); } void OBSBasic::on_actionRepair_triggered() { #if defined(_WIN32) ui->actionCheckForUpdates->setEnabled(false); ui->actionRepair->setEnabled(false); if (updateCheckThread && updateCheckThread->isRunning()) return; updateCheckThread.reset(new AutoUpdateThread(false, true)); updateCheckThread->start(); #endif } void OBSBasic::on_actionRestartSafe_triggered() { QMessageBox::StandardButton button = OBSMessageBox::question( this, QTStr("Restart"), safe_mode ? QTStr("SafeMode.RestartNormal") : QTStr("SafeMode.Restart")); if (button == QMessageBox::Yes) { restart = safe_mode; restart_safe = !safe_mode; close(); } } void OBSBasic::logUploadFinished(const QString &text, const QString &error) { ui->menuLogFiles->setEnabled(true); #if defined(_WIN32) ui->menuCrashLogs->setEnabled(true); #endif if (text.isEmpty()) { OBSMessageBox::critical(this, QTStr("LogReturnDialog.ErrorUploadingLog"), error); return; } openLogDialog(text, false); } void OBSBasic::crashUploadFinished(const QString &text, const QString &error) { ui->menuLogFiles->setEnabled(true); #if defined(_WIN32) ui->menuCrashLogs->setEnabled(true); #endif if (text.isEmpty()) { OBSMessageBox::critical(this, QTStr("LogReturnDialog.ErrorUploadingLog"), error); return; } openLogDialog(text, true); } void OBSBasic::openLogDialog(const QString &text, const bool crash) { OBSDataAutoRelease returnData = obs_data_create_from_json(QT_TO_UTF8(text)); string resURL = obs_data_get_string(returnData, "url"); QString logURL = resURL.c_str(); OBSLogReply logDialog(this, logURL, crash); logDialog.exec(); } void OBSBasic::on_actionHelpPortal_triggered() { QUrl url = QUrl("https://obsproject.com/help", QUrl::TolerantMode); QDesktopServices::openUrl(url); } void OBSBasic::on_actionWebsite_triggered() { QUrl url = QUrl("https://obsproject.com", QUrl::TolerantMode); QDesktopServices::openUrl(url); } void OBSBasic::on_actionDiscord_triggered() { QUrl url = QUrl("https://obsproject.com/discord", QUrl::TolerantMode); QDesktopServices::openUrl(url); } void OBSBasic::on_actionShowWhatsNew_triggered() { #ifdef WHATSNEW_ENABLED if (introCheckThread && introCheckThread->isRunning()) return; if (!cef) return; config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", -1); WhatsNewInfoThread *wnit = new WhatsNewInfoThread(); connect(wnit, &WhatsNewInfoThread::Result, this, &OBSBasic::ReceivedIntroJson, Qt::QueuedConnection); introCheckThread.reset(wnit); introCheckThread->start(); #endif } void OBSBasic::on_actionReleaseNotes_triggered() { QString addr("https://github.com/obsproject/obs-studio/releases"); QUrl url(QString("%1/%2").arg(addr, obs_get_version_string()), QUrl::TolerantMode); QDesktopServices::openUrl(url); } void OBSBasic::on_actionShowSettingsFolder_triggered() { const std::string userConfigPath = App()->userConfigLocation.u8string() + "/obs-studio"; const QString userConfigLocation = QString::fromStdString(userConfigPath); QDesktopServices::openUrl(QUrl::fromLocalFile(userConfigLocation)); } void OBSBasic::on_actionShowProfileFolder_triggered() { try { const OBSProfile ¤tProfile = GetCurrentProfile(); QString currentProfileLocation = QString::fromStdString(currentProfile.path.u8string()); QDesktopServices::openUrl(QUrl::fromLocalFile(currentProfileLocation)); } catch (const std::invalid_argument &error) { blog(LOG_ERROR, "%s", error.what()); } } void OBSBasic::on_actionAlwaysOnTop_triggered() { #ifndef _WIN32 /* Make sure all dialogs are safely and successfully closed before * switching the always on top mode due to the fact that windows all * have to be recreated, so queue the actual toggle to happen after * all events related to closing the dialogs have finished */ CloseDialogs(); #endif QMetaObject::invokeMethod(this, "ToggleAlwaysOnTop", Qt::QueuedConnection); } void OBSBasic::ToggleAlwaysOnTop() { bool isAlwaysOnTop = IsAlwaysOnTop(this); ui->actionAlwaysOnTop->setChecked(!isAlwaysOnTop); SetAlwaysOnTop(this, !isAlwaysOnTop); show(); } void OBSBasic::CreateEditTransformWindow(obs_sceneitem_t *item) { if (transformWindow) transformWindow->close(); transformWindow = new OBSBasicTransform(item, this); connect(ui->scenes, &QListWidget::currentItemChanged, transformWindow, &OBSBasicTransform::OnSceneChanged); transformWindow->show(); transformWindow->setAttribute(Qt::WA_DeleteOnClose, true); } void OBSBasic::on_actionFullscreenInterface_triggered() { if (!isFullScreen()) showFullScreen(); else showNormal(); } void OBSBasic::on_resetUI_triggered() { on_resetDocks_triggered(); ui->toggleListboxToolbars->setChecked(true); ui->toggleContextBar->setChecked(true); ui->toggleSourceIcons->setChecked(true); ui->toggleStatusBar->setChecked(true); ui->scenes->SetGridMode(false); ui->actionSceneListMode->setChecked(true); config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", false); } void OBSBasic::on_toggleListboxToolbars_toggled(bool visible) { ui->sourcesToolbar->setVisible(visible); ui->scenesToolbar->setVisible(visible); ui->mixerToolbar->setVisible(visible); config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowListboxToolbars", visible); } void OBSBasic::on_toggleStatusBar_toggled(bool visible) { ui->statusbar->setVisible(visible); config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowStatusBar", visible); } void OBSBasic::SetShowing(bool showing) { if (!showing && isVisible()) { config_set_string(App()->GetUserConfig(), "BasicWindow", "geometry", saveGeometry().toBase64().constData()); /* hide all visible child dialogs */ visDlgPositions.clear(); if (!visDialogs.isEmpty()) { for (QDialog *dlg : visDialogs) { visDlgPositions.append(dlg->pos()); dlg->hide(); } } if (showHide) showHide->setText(QTStr("Basic.SystemTray.Show")); QTimer::singleShot(0, this, &OBSBasic::hide); if (previewEnabled) EnablePreviewDisplay(false); #ifdef __APPLE__ EnableOSXDockIcon(false); #endif } else if (showing && !isVisible()) { if (showHide) showHide->setText(QTStr("Basic.SystemTray.Hide")); QTimer::singleShot(0, this, &OBSBasic::show); if (previewEnabled) EnablePreviewDisplay(true); #ifdef __APPLE__ EnableOSXDockIcon(true); #endif /* raise and activate window to ensure it is on top */ raise(); activateWindow(); /* show all child dialogs that was visible earlier */ if (!visDialogs.isEmpty()) { for (int i = 0; i < visDialogs.size(); ++i) { QDialog *dlg = visDialogs[i]; dlg->move(visDlgPositions[i]); dlg->show(); } } /* Unminimize window if it was hidden to tray instead of task * bar. */ if (sysTrayMinimizeToTray()) { Qt::WindowStates state; state = windowState() & ~Qt::WindowMinimized; state |= Qt::WindowActive; setWindowState(state); } } } void OBSBasic::ToggleShowHide() { bool showing = isVisible(); if (showing) { /* check for modal dialogs */ EnumDialogs(); if (!modalDialogs.isEmpty() || !visMsgBoxes.isEmpty()) return; } SetShowing(!showing); } void OBSBasic::on_actionMainUndo_triggered() { undo_s.undo(); } void OBSBasic::on_actionMainRedo_triggered() { undo_s.redo(); } void OBSBasic::on_autoConfigure_triggered() { AutoConfig test(this); test.setModal(true); test.show(); test.exec(); } void OBSBasic::on_stats_triggered() { if (!stats.isNull()) { stats->show(); stats->raise(); return; } OBSBasicStats *statsDlg; statsDlg = new OBSBasicStats(nullptr); statsDlg->show(); stats = statsDlg; } void OBSBasic::on_actionShowAbout_triggered() { if (about) about->close(); about = new OBSAbout(this); about->show(); about->setAttribute(Qt::WA_DeleteOnClose, true); } void OBSBasic::on_OBSBasic_customContextMenuRequested(const QPoint &pos) { QWidget *widget = childAt(pos); const char *className = nullptr; QString objName; if (widget != nullptr) { className = widget->metaObject()->className(); objName = widget->objectName(); } QPoint globalPos = mapToGlobal(pos); if (className && strstr(className, "Dock") != nullptr && !objName.isEmpty()) { if (objName.compare("scenesDock") == 0) { ui->scenes->customContextMenuRequested(globalPos); } else if (objName.compare("sourcesDock") == 0) { ui->sources->customContextMenuRequested(globalPos); } else if (objName.compare("mixerDock") == 0) { StackedMixerAreaContextMenuRequested(); } } else if (!className) { ui->menuDocks->exec(globalPos); } }