0
0
mirror of https://github.com/obsproject/obs-studio.git synced 2024-09-20 13:08:50 +02:00
obs-studio/UI/window-extra-browsers.cpp
jp9000 0759652cee 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.
2019-08-08 03:31:31 -07:00

576 lines
13 KiB
C++

#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);
}