From 0759652ceead9d834edd92576ca7ad8527b4ad87 Mon Sep 17 00:00:00 2001 From: jp9000 Date: Thu, 8 Aug 2019 03:27:45 -0700 Subject: [PATCH] UI: Add the ability to create custom browser docks Allows the ability for users to add custom browser widget docks that they can use for their third party services if they feel the need, mostly as a convenience tool so they don't have to open extra browsers alongside the program. --- UI/CMakeLists.txt | 3 + UI/data/locale/en-US.ini | 6 + UI/forms/OBSExtraBrowsers.ui | 98 ++++++ UI/window-basic-main.cpp | 24 ++ UI/window-basic-main.hpp | 16 + UI/window-extra-browsers.cpp | 575 +++++++++++++++++++++++++++++++++++ UI/window-extra-browsers.hpp | 95 ++++++ 7 files changed, 817 insertions(+) create mode 100644 UI/forms/OBSExtraBrowsers.ui create mode 100644 UI/window-extra-browsers.cpp create mode 100644 UI/window-extra-browsers.hpp diff --git a/UI/CMakeLists.txt b/UI/CMakeLists.txt index 1ed137860..62a8a9503 100644 --- a/UI/CMakeLists.txt +++ b/UI/CMakeLists.txt @@ -146,10 +146,12 @@ if(BROWSER_AVAILABLE_INTERNAL) list(APPEND obs_PLATFORM_SOURCES obf.c auth-oauth.cpp + window-extra-browsers.cpp ) list(APPEND obs_PLATFORM_HEADERS obf.h auth-oauth.hpp + window-extra-browsers.hpp ) if(TWITCH_ENABLED) @@ -324,6 +326,7 @@ set(obs_UI forms/OBSBasicSettings.ui forms/OBSBasicSourceSelect.ui forms/OBSBasicInteraction.ui + forms/OBSExtraBrowsers.ui forms/OBSUpdate.ui forms/OBSRemux.ui forms/OBSAbout.ui) diff --git a/UI/data/locale/en-US.ini b/UI/data/locale/en-US.ini index f029ce3bb..b526efedc 100644 --- a/UI/data/locale/en-US.ini +++ b/UI/data/locale/en-US.ini @@ -98,6 +98,11 @@ AlreadyRunning.LaunchAnyway="Launch Anyway" DockCloseWarning.Title="Closing Dockable Window" DockCloseWarning.Text="You just closed a dockable window. If you'd like to show it again, use the View → Docks menu on the menu bar." +# extra browser panels dialog +ExtraBrowsers="Custom Browser Docks" +ExtraBrowsers.Info="Add a dock by giving it a name and URL, then click Apply or Close to configure where it is on your screen. You can add or remove docks at any time." +ExtraBrowsers.DockName="Dock Name" + # Auth Auth.Authing.Title="Authenticating..." Auth.Authing.Text="Authenticating with %1, please wait..." @@ -572,6 +577,7 @@ Basic.MainMenu.View.Toolbars="&Toolbars" Basic.MainMenu.View.Docks="Docks" Basic.MainMenu.View.Docks.ResetUI="Reset UI" Basic.MainMenu.View.Docks.LockUI="Lock UI" +Basic.MainMenu.View.Docks.CustomBrowserDocks="Custom Browser Docks..." Basic.MainMenu.View.Toolbars.Listboxes="&Listboxes" Basic.MainMenu.View.SceneTransitions="S&cene Transitions" Basic.MainMenu.View.StatusBar="&Status Bar" diff --git a/UI/forms/OBSExtraBrowsers.ui b/UI/forms/OBSExtraBrowsers.ui new file mode 100644 index 000000000..a7d3a27a5 --- /dev/null +++ b/UI/forms/OBSExtraBrowsers.ui @@ -0,0 +1,98 @@ + + + OBSExtraBrowsers + + + + 0 + 0 + 785 + 353 + + + + ExtraBrowsers + + + + + + ExtraBrowsers.Info + + + true + + + + + + + QAbstractItemView::NoSelection + + + 23 + + + 23 + + + false + + + 23 + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Apply + + + + + + + Close + + + + + + + + + + + close + clicked() + OBSExtraBrowsers + close() + + + 520 + 286 + + + 435 + -19 + + + + + diff --git a/UI/window-basic-main.cpp b/UI/window-basic-main.cpp index 10817c132..8437bdc3b 100644 --- a/UI/window-basic-main.cpp +++ b/UI/window-basic-main.cpp @@ -1689,6 +1689,23 @@ void OBSBasic::OBSInit() OBSBasicStats *statsDlg = new OBSBasicStats(statsDock, false); statsDock->setWidget(statsDlg); + /* ----------------------------- */ + /* add custom browser docks */ + +#ifdef BROWSER_AVAILABLE + if (cef) { + QAction *action = new QAction(QTStr("Basic.MainMenu." + "View.Docks." + "CustomBrowserDocks")); + ui->viewMenuDocks->insertAction(ui->toggleScenes, action); + connect(action, &QAction::triggered, this, + &OBSBasic::ManageExtraBrowserDocks); + ui->viewMenuDocks->insertSeparator(ui->toggleScenes); + + LoadExtraBrowserDocks(); + } +#endif + const char *dockStateStr = config_get_string( App()->GlobalConfig(), "BasicWindow", "DockState"); if (!dockStateStr) { @@ -3794,9 +3811,16 @@ void OBSBasic::closeEvent(QCloseEvent *event) SaveProjectNow(); auth.reset(); + delete extraBrowsers; + config_set_string(App()->GlobalConfig(), "BasicWindow", "DockState", saveState().toBase64().constData()); +#ifdef BROWSER_AVAILABLE + SaveExtraBrowserDocks(); + ClearExtraBrowserDocks(); +#endif + if (api) api->on_event(OBS_FRONTEND_EVENT_EXIT); diff --git a/UI/window-basic-main.hpp b/UI/window-basic-main.hpp index ff4ddb1ce..419c11eac 100644 --- a/UI/window-basic-main.hpp +++ b/UI/window-basic-main.hpp @@ -124,6 +124,8 @@ class OBSBasic : public OBSMainWindow { friend class AutoConfig; friend class AutoConfigStreamPage; friend class RecordButton; + friend class ExtraBrowsersModel; + friend class ExtraBrowsersDelegate; friend struct OBSStudioAPI; enum class MoveDir { Up, Down, Left, Right }; @@ -200,6 +202,7 @@ private: QPointer stats; QPointer remux; + QPointer extraBrowsers; QPointer startStreamMenu; @@ -420,6 +423,19 @@ private: bool NoSourcesConfirmation(); +#ifdef BROWSER_AVAILABLE + QList> extraBrowserDocks; + QList> extraBrowserDockActions; + QStringList extraBrowserDockTargets; + + void ClearExtraBrowserDocks(); + void LoadExtraBrowserDocks(); + void SaveExtraBrowserDocks(); + void ManageExtraBrowserDocks(); + void AddExtraBrowserDock(const QString &title, const QString &url, + bool firstCreate); +#endif + public slots: void DeferSaveBegin(); void DeferSaveEnd(); diff --git a/UI/window-extra-browsers.cpp b/UI/window-extra-browsers.cpp new file mode 100644 index 000000000..c1e6f8e4a --- /dev/null +++ b/UI/window-extra-browsers.cpp @@ -0,0 +1,575 @@ +#include "window-extra-browsers.hpp" +#include "window-basic-main.hpp" +#include "qt-wrappers.hpp" +#include "window-dock.hpp" + +#include +#include + +#include + +#include "ui_OBSExtraBrowsers.h" + +#include +extern QCef *cef; +extern QCefCookieManager *panel_cookies; + +using namespace json11; + +#define OBJ_NAME_SUFFIX "_extraBrowser" + +enum class Column : int { + Title, + Url, + Delete, + + Count, +}; + +class ExtraBrowser : public OBSDock { +public: + inline ExtraBrowser() : OBSDock() {} + + QScopedPointer cefWidget; + + inline void SetWidget(QCefWidget *widget_) + { + setWidget(widget_); + cefWidget.reset(widget_); + } +}; + +/* ------------------------------------------------------------------------- */ + +void ExtraBrowsersModel::Reset() +{ + items.clear(); + + OBSBasic *main = OBSBasic::Get(); + + for (int i = 0; i < main->extraBrowserDocks.size(); i++) { + ExtraBrowser *dock = reinterpret_cast( + main->extraBrowserDocks[i].data()); + + Item item; + item.prevIdx = i; + item.title = dock->windowTitle(); + item.url = main->extraBrowserDockTargets[i]; + items.push_back(item); + } +} + +int ExtraBrowsersModel::rowCount(const QModelIndex &) const +{ + int count = items.size() + 1; + return count; +} + +int ExtraBrowsersModel::columnCount(const QModelIndex &) const +{ + return (int)Column::Count; +} + +QVariant ExtraBrowsersModel::data(const QModelIndex &index, int role) const +{ + int column = index.column(); + int idx = index.row(); + int count = items.size(); + bool validRole = role == Qt::DisplayRole || + role == Qt::AccessibleTextRole; + + if (!validRole) + return QVariant(); + + if (idx >= 0 && idx < count) { + switch (column) { + case (int)Column::Title: + return items[idx].title; + case (int)Column::Url: + return items[idx].url; + } + } else if (idx == count) { + switch (column) { + case (int)Column::Title: + return newTitle; + case (int)Column::Url: + return newURL; + } + } + + return QVariant(); +} + +QVariant ExtraBrowsersModel::headerData(int section, + Qt::Orientation orientation, + int role) const +{ + bool validRole = role == Qt::DisplayRole || + role == Qt::AccessibleTextRole; + + if (validRole && orientation == Qt::Orientation::Horizontal) { + switch (section) { + case (int)Column::Title: + return QTStr("ExtraBrowsers.DockName"); + case (int)Column::Url: + return QStringLiteral("URL"); + } + } + + return QVariant(); +} + +Qt::ItemFlags ExtraBrowsersModel::flags(const QModelIndex &index) const +{ + Qt::ItemFlags flags = QAbstractTableModel::flags(index); + + if (index.column() != (int)Column::Delete) + flags |= Qt::ItemIsEditable; + + return flags; +} + +class DelButton : public QPushButton { +public: + inline DelButton(QModelIndex index_) : QPushButton(), index(index_) {} + + QPersistentModelIndex index; +}; + +class EditWidget : public QLineEdit { +public: + inline EditWidget(QWidget *parent, QModelIndex index_) + : QLineEdit(parent), index(index_) + { + } + + QPersistentModelIndex index; +}; + +void ExtraBrowsersModel::AddDeleteButton(int idx) +{ + QTableView *widget = reinterpret_cast(parent()); + + QSizePolicy policy(QSizePolicy::Expanding, QSizePolicy::Expanding, + QSizePolicy::PushButton); + policy.setWidthForHeight(true); + + QModelIndex index = createIndex(idx, (int)Column::Delete, nullptr); + + QPushButton *del = new DelButton(index); + del->setProperty("themeID", "trashIcon"); + del->setSizePolicy(policy); + del->setFlat(true); + connect(del, &QPushButton::clicked, this, + &ExtraBrowsersModel::DeleteItem); + + widget->setIndexWidget(index, del); +} + +void ExtraBrowsersModel::CheckToAdd() +{ + if (newTitle.isEmpty() || newURL.isEmpty()) + return; + + int idx = items.size() + 1; + beginInsertRows(QModelIndex(), idx, idx); + + Item item; + item.prevIdx = -1; + item.title = newTitle; + item.url = newURL; + items.push_back(item); + + newTitle = ""; + newURL = ""; + + endInsertRows(); + + AddDeleteButton(idx - 1); +} + +void ExtraBrowsersModel::UpdateItem(Item &item) +{ + int idx = item.prevIdx; + + OBSBasic *main = OBSBasic::Get(); + ExtraBrowser *dock = reinterpret_cast( + main->extraBrowserDocks[idx].data()); + dock->setWindowTitle(item.title); + dock->setObjectName(item.title + OBJ_NAME_SUFFIX); + main->extraBrowserDockActions[idx]->setText(item.title); + + if (main->extraBrowserDockTargets[idx] != item.url) { + dock->cefWidget->setURL(QT_TO_UTF8(item.url)); + main->extraBrowserDockTargets[idx] = item.url; + } +} + +void ExtraBrowsersModel::DeleteItem() +{ + QTableView *widget = reinterpret_cast(parent()); + + DelButton *del = reinterpret_cast(sender()); + int row = del->index.row(); + + /* there's some sort of internal bug in Qt and deleting certain index + * widgets or "editors" that can cause a crash inside Qt if the widget + * is not manually removed, at least on 5.7 */ + widget->setIndexWidget(del->index, nullptr); + del->deleteLater(); + + /* --------- */ + + beginRemoveRows(QModelIndex(), row, row); + + int prevIdx = items[row].prevIdx; + items.removeAt(row); + + if (prevIdx != -1) { + int i = 0; + for (; i < deleted.size() && deleted[i] < prevIdx; i++) + ; + deleted.insert(i, prevIdx); + } + + endRemoveRows(); +} + +void ExtraBrowsersModel::Apply() +{ + OBSBasic *main = OBSBasic::Get(); + + for (Item &item : items) { + if (item.prevIdx != -1) { + UpdateItem(item); + } else { + main->AddExtraBrowserDock(item.title, item.url, true); + } + } + + for (int i = deleted.size() - 1; i >= 0; i--) { + int idx = deleted[i]; + main->extraBrowserDockActions.removeAt(idx); + main->extraBrowserDockTargets.removeAt(idx); + main->extraBrowserDocks.removeAt(idx); + } + + deleted.clear(); + + Reset(); +} + +void ExtraBrowsersModel::TabSelection(bool forward) +{ + QListView *widget = reinterpret_cast(parent()); + QItemSelectionModel *selModel = widget->selectionModel(); + + QModelIndex sel = selModel->currentIndex(); + int row = sel.row(); + int col = sel.column(); + + switch (sel.column()) { + case (int)Column::Title: + if (!forward) { + if (row == 0) { + return; + } + + row -= 1; + } + + col += 1; + break; + + case (int)Column::Url: + if (forward) { + if (row == items.size()) { + return; + } + + row += 1; + } + + col -= 1; + } + + sel = createIndex(row, col, nullptr); + selModel->setCurrentIndex(sel, QItemSelectionModel::Clear); +} + +void ExtraBrowsersModel::Init() +{ + for (int i = 0; i < items.count(); i++) + AddDeleteButton(i); +} + +/* ------------------------------------------------------------------------- */ + +QWidget *ExtraBrowsersDelegate::createEditor(QWidget *parent, + const QStyleOptionViewItem &, + const QModelIndex &index) const +{ + QLineEdit *text = new EditWidget(parent, index); + text->installEventFilter(const_cast(this)); + text->setSizePolicy(QSizePolicy(QSizePolicy::Policy::Expanding, + QSizePolicy::Policy::Expanding, + QSizePolicy::ControlType::LineEdit)); + return text; +} + +void ExtraBrowsersDelegate::setEditorData(QWidget *editor, + const QModelIndex &index) const +{ + QLineEdit *text = reinterpret_cast(editor); + text->blockSignals(true); + text->setText(index.data().toString()); + text->blockSignals(false); +} + +bool ExtraBrowsersDelegate::eventFilter(QObject *object, QEvent *event) +{ + QLineEdit *edit = qobject_cast(object); + if (!edit) + return false; + + if (LineEditCanceled(event)) { + RevertText(edit); + } + if (LineEditChanged(event)) { + UpdateText(edit); + + if (event->type() == QEvent::KeyPress) { + QKeyEvent *keyEvent = static_cast(event); + if (keyEvent->key() == Qt::Key_Tab) { + model->TabSelection(true); + } else if (keyEvent->key() == Qt::Key_Backtab) { + model->TabSelection(false); + } + } + return true; + } + + return false; +} + +bool ExtraBrowsersDelegate::ValidName(const QString &name) const +{ + for (auto &item : model->items) { + if (name.compare(item.title, Qt::CaseInsensitive) == 0) { + return false; + } + } + return true; +} + +void ExtraBrowsersDelegate::RevertText(QLineEdit *edit_) +{ + EditWidget *edit = reinterpret_cast(edit_); + int row = edit->index.row(); + int col = edit->index.column(); + bool newItem = (row == model->items.size()); + + QString oldText; + if (col == (int)Column::Title) { + oldText = newItem ? model->newTitle : model->items[row].title; + } else { + oldText = newItem ? model->newURL : model->items[row].url; + } + + edit->setText(oldText); +} + +bool ExtraBrowsersDelegate::UpdateText(QLineEdit *edit_) +{ + EditWidget *edit = reinterpret_cast(edit_); + int row = edit->index.row(); + int col = edit->index.column(); + bool newItem = (row == model->items.size()); + + QString text = edit->text().trimmed(); + + if (!newItem && text.isEmpty()) { + return false; + } + + if (col == (int)Column::Title) { + QString oldText = newItem ? model->newTitle + : model->items[row].title; + bool same = oldText.compare(text, Qt::CaseInsensitive) == 0; + + if (!same && !ValidName(text)) { + edit->setText(oldText); + return false; + } + } + + if (!newItem) { + /* if edited existing item, update it*/ + switch (col) { + case (int)Column::Title: + model->items[row].title = text; + break; + case (int)Column::Url: + model->items[row].url = text; + break; + } + } else { + /* if both new values filled out, create new one */ + switch (col) { + case (int)Column::Title: + model->newTitle = text; + break; + case (int)Column::Url: + model->newURL = text; + break; + } + + model->CheckToAdd(); + } + + emit commitData(edit); + return true; +} + +/* ------------------------------------------------------------------------- */ + +OBSExtraBrowsers::OBSExtraBrowsers(QWidget *parent) + : QDialog(parent), ui(new Ui::OBSExtraBrowsers) +{ + ui->setupUi(this); + + setAttribute(Qt::WA_DeleteOnClose, true); + setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); + + model = new ExtraBrowsersModel(ui->table); + + ui->table->setModel(model); + ui->table->setItemDelegateForColumn((int)Column::Title, + new ExtraBrowsersDelegate(model)); + ui->table->setItemDelegateForColumn((int)Column::Url, + new ExtraBrowsersDelegate(model)); + ui->table->horizontalHeader()->setSectionResizeMode( + QHeaderView::ResizeMode::Stretch); + ui->table->horizontalHeader()->setSectionResizeMode( + (int)Column::Delete, QHeaderView::ResizeMode::Fixed); + ui->table->setEditTriggers( + QAbstractItemView::EditTrigger::CurrentChanged); +} + +OBSExtraBrowsers::~OBSExtraBrowsers() +{ + delete ui; +} + +void OBSExtraBrowsers::closeEvent(QCloseEvent *event) +{ + QDialog::closeEvent(event); + model->Apply(); +} + +void OBSExtraBrowsers::on_apply_clicked() +{ + model->Apply(); +} + +/* ------------------------------------------------------------------------- */ + +void OBSBasic::ClearExtraBrowserDocks() +{ + extraBrowserDockTargets.clear(); + extraBrowserDockActions.clear(); + extraBrowserDocks.clear(); +} + +void OBSBasic::LoadExtraBrowserDocks() +{ + const char *jsonStr = config_get_string( + App()->GlobalConfig(), "BasicWindow", "ExtraBrowserDocks"); + + std::string err; + Json json = Json::parse(jsonStr, err); + if (!err.empty()) + return; + + Json::array array = json.array_items(); + for (Json &item : array) { + std::string title = item["title"].string_value(); + std::string url = item["url"].string_value(); + + AddExtraBrowserDock(title.c_str(), url.c_str(), false); + } +} + +void OBSBasic::SaveExtraBrowserDocks() +{ + Json::array array; + for (int i = 0; i < extraBrowserDocks.size(); i++) { + QAction *action = extraBrowserDockActions[i].data(); + QString url = extraBrowserDockTargets[i]; + Json::object obj{ + {"title", QT_TO_UTF8(action->text())}, + {"url", QT_TO_UTF8(url)}, + }; + array.push_back(obj); + } + + std::string output = Json(array).dump(); + config_set_string(App()->GlobalConfig(), "BasicWindow", + "ExtraBrowserDocks", output.c_str()); +} + +void OBSBasic::ManageExtraBrowserDocks() +{ + if (!extraBrowsers.isNull()) { + extraBrowsers->show(); + extraBrowsers->raise(); + return; + } + + extraBrowsers = new OBSExtraBrowsers(this); + extraBrowsers->show(); +} + +void OBSBasic::AddExtraBrowserDock(const QString &title, const QString &url, + bool firstCreate) +{ + static int panel_version = -1; + if (panel_version == -1) { + panel_version = obs_browser_qcef_version(); + } + + ExtraBrowser *dock = new ExtraBrowser(); + dock->setObjectName(title + OBJ_NAME_SUFFIX); + dock->resize(460, 600); + dock->setMinimumSize(150, 150); + dock->setWindowTitle(title); + dock->setAllowedAreas(Qt::AllDockWidgetAreas); + + QCefWidget *browser = + cef->create_widget(nullptr, QT_TO_UTF8(url), nullptr); + if (browser && panel_version >= 1) + browser->allowAllPopups(true); + + dock->SetWidget(browser); + + addDockWidget(Qt::RightDockWidgetArea, dock); + + if (firstCreate) { + dock->setFloating(true); + + QPoint curPos = pos(); + QSize wSizeD2 = size() / 2; + QSize dSizeD2 = dock->size() / 2; + + curPos.setX(curPos.x() + wSizeD2.width() - dSizeD2.width()); + curPos.setY(curPos.y() + wSizeD2.height() - dSizeD2.height()); + + dock->move(curPos); + dock->setVisible(true); + } + + extraBrowserDocks.push_back(QSharedPointer(dock)); + extraBrowserDockActions.push_back( + QSharedPointer(AddDockWidget(dock))); + extraBrowserDockTargets.push_back(url); +} diff --git a/UI/window-extra-browsers.hpp b/UI/window-extra-browsers.hpp new file mode 100644 index 000000000..cdfb14b05 --- /dev/null +++ b/UI/window-extra-browsers.hpp @@ -0,0 +1,95 @@ +#pragma once + +#include +#include +#include +#include + +class Ui_OBSExtraBrowsers; +class ExtraBrowsersModel; + +class QCefWidget; + +class OBSExtraBrowsers : public QDialog { + Q_OBJECT + + Ui_OBSExtraBrowsers *ui; + ExtraBrowsersModel *model; + +public: + OBSExtraBrowsers(QWidget *parent); + ~OBSExtraBrowsers(); + + void closeEvent(QCloseEvent *event) override; + +public slots: + void on_apply_clicked(); +}; + +class ExtraBrowsersModel : public QAbstractTableModel { + Q_OBJECT + +public: + inline ExtraBrowsersModel(QObject *parent = nullptr) + : QAbstractTableModel(parent) + { + Reset(); + QMetaObject::invokeMethod(this, "Init", Qt::QueuedConnection); + } + + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + int + columnCount(const QModelIndex &parent = QModelIndex()) const override; + QVariant data(const QModelIndex &index, int role) const override; + QVariant headerData(int section, Qt::Orientation orientation, + int role = Qt::DisplayRole) const override; + Qt::ItemFlags flags(const QModelIndex &index) const override; + + struct Item { + int prevIdx; + QString title; + QString url; + }; + + void TabSelection(bool forward); + + void AddDeleteButton(int idx); + void Reset(); + void CheckToAdd(); + void UpdateItem(Item &item); + void DeleteItem(); + void Apply(); + + QVector items; + QVector deleted; + + QString newTitle; + QString newURL; + +public slots: + void Init(); +}; + +class ExtraBrowsersDelegate : public QStyledItemDelegate { + Q_OBJECT + +public: + inline ExtraBrowsersDelegate(ExtraBrowsersModel *model_) + : QStyledItemDelegate(nullptr), model(model_) + { + } + + QWidget *createEditor(QWidget *parent, + const QStyleOptionViewItem &option, + const QModelIndex &index) const override; + + void setEditorData(QWidget *editor, + const QModelIndex &index) const override; + + bool eventFilter(QObject *object, QEvent *event) override; + void RevertText(QLineEdit *edit); + bool UpdateText(QLineEdit *edit); + bool ValidName(const QString &text) const; + + ExtraBrowsersModel *model; +};