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;
+};