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.
master
jp9000 2019-08-08 03:27:45 -07:00
parent d5d8492bb3
commit 0759652cee
7 changed files with 817 additions and 0 deletions

View File

@ -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)

View File

@ -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"

View File

@ -0,0 +1,98 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>OBSExtraBrowsers</class>
<widget class="QWidget" name="OBSExtraBrowsers">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>785</width>
<height>353</height>
</rect>
</property>
<property name="windowTitle">
<string>ExtraBrowsers</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>ExtraBrowsers.Info</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QTableView" name="table">
<property name="selectionMode">
<enum>QAbstractItemView::NoSelection</enum>
</property>
<attribute name="horizontalHeaderDefaultSectionSize">
<number>23</number>
</attribute>
<attribute name="horizontalHeaderMinimumSectionSize">
<number>23</number>
</attribute>
<attribute name="verticalHeaderVisible">
<bool>false</bool>
</attribute>
<attribute name="verticalHeaderDefaultSectionSize">
<number>23</number>
</attribute>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="apply">
<property name="text">
<string>Apply</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="close">
<property name="text">
<string>Close</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<resources/>
<connections>
<connection>
<sender>close</sender>
<signal>clicked()</signal>
<receiver>OBSExtraBrowsers</receiver>
<slot>close()</slot>
<hints>
<hint type="sourcelabel">
<x>520</x>
<y>286</y>
</hint>
<hint type="destinationlabel">
<x>435</x>
<y>-19</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@ -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);

View File

@ -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<QWidget> stats;
QPointer<QWidget> remux;
QPointer<QWidget> extraBrowsers;
QPointer<QMenu> startStreamMenu;
@ -420,6 +423,19 @@ private:
bool NoSourcesConfirmation();
#ifdef BROWSER_AVAILABLE
QList<QSharedPointer<QDockWidget>> extraBrowserDocks;
QList<QSharedPointer<QAction>> 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();

View File

@ -0,0 +1,575 @@
#include "window-extra-browsers.hpp"
#include "window-basic-main.hpp"
#include "qt-wrappers.hpp"
#include "window-dock.hpp"
#include <QLineEdit>
#include <QHBoxLayout>
#include <json11.hpp>
#include "ui_OBSExtraBrowsers.h"
#include <browser-panel.hpp>
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<QCefWidget> 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<ExtraBrowser *>(
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<QTableView *>(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<ExtraBrowser *>(
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<QTableView *>(parent());
DelButton *del = reinterpret_cast<DelButton *>(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<QListView *>(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<ExtraBrowsersDelegate *>(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<QLineEdit *>(editor);
text->blockSignals(true);
text->setText(index.data().toString());
text->blockSignals(false);
}
bool ExtraBrowsersDelegate::eventFilter(QObject *object, QEvent *event)
{
QLineEdit *edit = qobject_cast<QLineEdit *>(object);
if (!edit)
return false;
if (LineEditCanceled(event)) {
RevertText(edit);
}
if (LineEditChanged(event)) {
UpdateText(edit);
if (event->type() == QEvent::KeyPress) {
QKeyEvent *keyEvent = static_cast<QKeyEvent *>(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<EditWidget *>(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<EditWidget *>(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<QDockWidget>(dock));
extraBrowserDockActions.push_back(
QSharedPointer<QAction>(AddDockWidget(dock)));
extraBrowserDockTargets.push_back(url);
}

View File

@ -0,0 +1,95 @@
#pragma once
#include <QDialog>
#include <QScopedPointer>
#include <QAbstractTableModel>
#include <QStyledItemDelegate>
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<Item> items;
QVector<int> 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;
};