Files
OBS-WebKitGTK/src/docks.cpp
2026-07-18 11:33:47 +05:00

823 lines
23 KiB
C++

#include <obs/obs-frontend-api.h>
#include <obs/obs-interaction.h>
#include <obs/obs-module.h>
#include <QAbstractItemView>
#include <QAction>
#include <QApplication>
#include <QByteArray>
#include <QContextMenuEvent>
#include <QDialog>
#include <QDialogButtonBox>
#include <QDockWidget>
#include <QFocusEvent>
#include <QHash>
#include <QHeaderView>
#include <QImage>
#include <QKeyEvent>
#include <QLabel>
#include <QMainWindow>
#include <QMenu>
#include <QMenuBar>
#include <QMessageBox>
#include <QMouseEvent>
#include <QPainter>
#include <QPointer>
#include <QPushButton>
#include <QResizeEvent>
#include <QSet>
#include <QShowEvent>
#include <QSocketNotifier>
#include <QTableWidget>
#include <QTimer>
#include <QUuid>
#include <QVBoxLayout>
#include <QVector>
#include <QWheelEvent>
#include <QWidget>
#include <algorithm>
#include <cerrno>
#include <csignal>
#include <cstdint>
#include <cstring>
#include <fcntl.h>
#include <spawn.h>
#include <sys/mman.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <utility>
#include "protocol.h"
extern "C" {
extern char **environ;
const char *owb_module_text(const char *key);
}
namespace {
constexpr const char *kSaveKey = "obs-webkit-browser-docks";
constexpr const char *kDockPrefix = "obs-webkit-browser-dock-";
QString text(const char *key)
{
return QString::fromUtf8(owb_module_text(key));
}
bool writeAll(int fd, const void *data, size_t size)
{
auto cursor = static_cast<const uint8_t *>(data);
while (size > 0) {
ssize_t written = send(fd, cursor, size, MSG_NOSIGNAL);
if (written < 0 && errno == EINTR)
continue;
if (written <= 0)
return false;
cursor += written;
size -= static_cast<size_t>(written);
}
return true;
}
void closeFd(int &fd)
{
if (fd >= 0)
close(fd);
fd = -1;
}
void closeOnExec(int fd)
{
int flags = fcntl(fd, F_GETFD);
if (flags >= 0)
fcntl(fd, F_SETFD, flags | FD_CLOEXEC);
}
void nonBlocking(int fd)
{
int flags = fcntl(fd, F_GETFL);
if (flags >= 0)
fcntl(fd, F_SETFL, flags | O_NONBLOCK);
}
struct DockDefinition {
QString id;
QString title;
QString url;
};
class WebPanel final : public QWidget {
public:
explicit WebPanel(QString url, QWidget *parent = nullptr) : QWidget(parent), url_(std::move(url))
{
setFocusPolicy(Qt::StrongFocus);
setMouseTracking(true);
setMinimumSize(80, 60);
resizeTimer_.setSingleShot(true);
resizeTimer_.setInterval(250);
connect(&resizeTimer_, &QTimer::timeout, this, [this] {
if (isVisible())
restartRenderer();
});
}
~WebPanel() override { stopRenderer(); }
void setUrl(const QString &url)
{
if (url_ == url)
return;
url_ = url;
if (isVisible())
restartRenderer();
}
void setDockTitle(const QString &title)
{
for (QWidget *ancestor = parentWidget(); ancestor; ancestor = ancestor->parentWidget()) {
if (auto *dock = qobject_cast<QDockWidget *>(ancestor)) {
dock->setWindowTitle(title);
break;
}
}
}
protected:
bool event(QEvent *event) override
{
const auto dockClosed = static_cast<QEvent::Type>(QEvent::User + QEvent::Close);
if (event->type() == dockClosed)
stopRenderer();
return QWidget::event(event);
}
void paintEvent(QPaintEvent *) override
{
QPainter painter(this);
painter.fillRect(rect(), palette().color(QPalette::Base));
if (!frame_.isNull()) {
painter.setRenderHint(QPainter::SmoothPixmapTransform, true);
painter.drawImage(rect(), frame_);
} else if (!status_.isEmpty()) {
painter.setPen(palette().color(QPalette::Text));
painter.drawText(rect().adjusted(16, 16, -16, -16), Qt::AlignCenter | Qt::TextWordWrap,
status_);
}
}
void showEvent(QShowEvent *event) override
{
QWidget::showEvent(event);
if (pid_ <= 0)
QTimer::singleShot(0, this, [this] { startRenderer(); });
}
void hideEvent(QHideEvent *event) override
{
resizeTimer_.stop();
stopRenderer();
QWidget::hideEvent(event);
}
void resizeEvent(QResizeEvent *event) override
{
QWidget::resizeEvent(event);
if (pid_ > 0)
resizeTimer_.start();
}
void mouseMoveEvent(QMouseEvent *event) override
{
auto [x, y] = rendererPosition(event->position());
owb_command command{};
command.type = OWB_MOUSE_MOVE;
command.x = x;
command.y = y;
command.modifiers = interactionModifiers(event->modifiers(), event->buttons());
sendCommand(command);
}
void leaveEvent(QEvent *event) override
{
owb_command command{};
command.type = OWB_MOUSE_MOVE;
command.value1 = 1;
sendCommand(command);
QWidget::leaveEvent(event);
}
void mousePressEvent(QMouseEvent *event) override
{
setFocus(Qt::MouseFocusReason);
sendMouseButton(event, false);
}
void mouseReleaseEvent(QMouseEvent *event) override { sendMouseButton(event, true); }
void wheelEvent(QWheelEvent *event) override
{
auto [x, y] = rendererPosition(event->position());
owb_command command{};
command.type = OWB_MOUSE_WHEEL;
command.x = x;
command.y = y;
command.value1 = event->angleDelta().x();
command.value2 = event->angleDelta().y();
command.modifiers = interactionModifiers(event->modifiers(), event->buttons());
sendCommand(command);
event->accept();
}
void focusInEvent(QFocusEvent *event) override
{
owb_command command{};
command.type = OWB_FOCUS;
command.value1 = 1;
sendCommand(command);
QWidget::focusInEvent(event);
}
void focusOutEvent(QFocusEvent *event) override
{
owb_command command{};
command.type = OWB_FOCUS;
sendCommand(command);
QWidget::focusOutEvent(event);
}
void keyPressEvent(QKeyEvent *event) override { sendKey(event, false); }
void keyReleaseEvent(QKeyEvent *event) override { sendKey(event, true); }
void contextMenuEvent(QContextMenuEvent *event) override
{
QMenu menu(this);
QAction *reload = menu.addAction(text("Reload"));
if (menu.exec(event->globalPos()) == reload) {
owb_command command{};
command.type = OWB_RELOAD;
sendCommand(command);
}
}
private:
std::pair<int32_t, int32_t> rendererPosition(const QPointF &position) const
{
const double sx = width() > 0 ? static_cast<double>(renderWidth_) / width() : 1.0;
const double sy = height() > 0 ? static_cast<double>(renderHeight_) / height() : 1.0;
int x = std::clamp(static_cast<int>(position.x() * sx), 0, std::max(0, renderWidth_ - 1));
int y = std::clamp(static_cast<int>(position.y() * sy), 0, std::max(0, renderHeight_ - 1));
return {x, y};
}
static uint32_t interactionModifiers(Qt::KeyboardModifiers keys, Qt::MouseButtons buttons)
{
uint32_t result = 0;
if (keys & Qt::ShiftModifier)
result |= INTERACT_SHIFT_KEY;
if (keys & Qt::ControlModifier)
result |= INTERACT_CONTROL_KEY;
if (keys & Qt::AltModifier)
result |= INTERACT_ALT_KEY;
if (keys & Qt::MetaModifier)
result |= INTERACT_COMMAND_KEY;
if (buttons & Qt::LeftButton)
result |= INTERACT_MOUSE_LEFT;
if (buttons & Qt::MiddleButton)
result |= INTERACT_MOUSE_MIDDLE;
if (buttons & Qt::RightButton)
result |= INTERACT_MOUSE_RIGHT;
return result;
}
void sendMouseButton(QMouseEvent *event, bool released)
{
auto [x, y] = rendererPosition(event->position());
owb_command command{};
command.type = OWB_MOUSE_CLICK;
command.x = x;
command.y = y;
command.value1 = event->button() == Qt::MiddleButton ? MOUSE_MIDDLE
: event->button() == Qt::RightButton ? MOUSE_RIGHT
: MOUSE_LEFT;
command.value2 = released;
command.text_length = static_cast<uint32_t>(event->type() == QEvent::MouseButtonDblClick ? 2 : 1);
command.modifiers = interactionModifiers(event->modifiers(), event->buttons());
sendCommand(command);
event->accept();
}
void sendKey(QKeyEvent *event, bool released)
{
owb_command command{};
command.type = OWB_KEY;
command.value1 = released;
command.value2 = static_cast<int32_t>(event->nativeVirtualKey());
command.modifiers = interactionModifiers(event->modifiers(), {});
QByteArray utf8 = event->text().toUtf8();
command.text_length = static_cast<uint32_t>(std::min<size_t>(utf8.size(), sizeof(command.text)));
memcpy(command.text, utf8.constData(), command.text_length);
sendCommand(command);
event->accept();
}
void sendCommand(owb_command &command)
{
if (controlFd_ < 0)
return;
command.magic = OWB_COMMAND_MAGIC;
writeAll(controlFd_, &command, sizeof(command));
}
bool sendConfiguration()
{
QByteArray url = url_.trimmed().toUtf8();
if (url.isEmpty())
url = "about:blank";
owb_config_header header{};
header.magic = OWB_CONFIG_MAGIC;
header.version = OWB_PROTOCOL_VERSION;
header.width = static_cast<uint32_t>(renderWidth_);
header.height = static_cast<uint32_t>(renderHeight_);
header.fps = 30;
header.transparent = 0;
header.hardware_acceleration = 1;
header.url_length = static_cast<uint32_t>(url.size());
return writeAll(controlFd_, &header, sizeof(header)) &&
writeAll(controlFd_, url.constData(), static_cast<size_t>(url.size()));
}
void startRenderer()
{
if (pid_ > 0 || !isVisible())
return;
status_ = text("PanelLoading");
frame_ = QImage();
update();
const double scale = devicePixelRatioF();
renderWidth_ = std::clamp(static_cast<int>(width() * scale), 2, 8192);
renderHeight_ = std::clamp(static_cast<int>(height() * scale), 2, 8192);
sharedSize_ = owb_shared_size(renderWidth_, renderHeight_);
sharedFd_ = memfd_create("obs-webkit-browser-dock", MFD_CLOEXEC);
if (sharedFd_ < 0 || ftruncate(sharedFd_, static_cast<off_t>(sharedSize_)) != 0)
return startFailed(text("PanelRendererError"));
shared_ = static_cast<owb_shared_frames *>(
mmap(nullptr, sharedSize_, PROT_READ | PROT_WRITE, MAP_SHARED, sharedFd_, 0));
if (shared_ == MAP_FAILED) {
shared_ = nullptr;
return startFailed(text("PanelRendererError"));
}
memset(shared_, 0, sizeof(*shared_));
shared_->magic = OWB_SHARED_MAGIC;
shared_->version = OWB_PROTOCOL_VERSION;
shared_->width = static_cast<uint32_t>(renderWidth_);
shared_->height = static_cast<uint32_t>(renderHeight_);
shared_->stride = static_cast<uint32_t>(renderWidth_ * 4);
shared_->slot_count = OWB_SHARED_FRAME_SLOTS;
int control[2] = {-1, -1};
int frames[2] = {-1, -1};
int logs[2] = {-1, -1};
if (socketpair(AF_UNIX, SOCK_STREAM, 0, control) != 0 || pipe(frames) != 0 || pipe(logs) != 0) {
for (int fd : control)
if (fd >= 0)
::close(fd);
for (int fd : frames)
if (fd >= 0)
::close(fd);
for (int fd : logs)
if (fd >= 0)
::close(fd);
return startFailed(text("PanelRendererError"));
}
for (int fd : control)
closeOnExec(fd);
for (int fd : frames)
closeOnExec(fd);
for (int fd : logs)
closeOnExec(fd);
char *helper = obs_module_file("obs-webkit-renderer");
if (!helper || access(helper, X_OK) != 0) {
for (int fd : control)
::close(fd);
for (int fd : frames)
::close(fd);
for (int fd : logs)
::close(fd);
bfree(helper);
return startFailed(text("PanelRendererMissing"));
}
posix_spawn_file_actions_t actions;
posix_spawnattr_t attributes;
posix_spawn_file_actions_init(&actions);
posix_spawnattr_init(&attributes);
sigset_t defaults;
sigemptyset(&defaults);
sigaddset(&defaults, SIGTERM);
sigaddset(&defaults, SIGINT);
sigaddset(&defaults, SIGPIPE);
posix_spawnattr_setsigdefault(&attributes, &defaults);
posix_spawnattr_setpgroup(&attributes, 0);
posix_spawnattr_setflags(&attributes, POSIX_SPAWN_SETSIGDEF | POSIX_SPAWN_SETPGROUP);
posix_spawn_file_actions_adddup2(&actions, control[1], STDIN_FILENO);
posix_spawn_file_actions_adddup2(&actions, frames[1], STDOUT_FILENO);
posix_spawn_file_actions_adddup2(&actions, logs[1], STDERR_FILENO);
posix_spawn_file_actions_adddup2(&actions, sharedFd_, OWB_SHARED_FRAME_FD);
posix_spawn_file_actions_addclose(&actions, control[0]);
posix_spawn_file_actions_addclose(&actions, frames[0]);
posix_spawn_file_actions_addclose(&actions, logs[0]);
char *argv[] = {helper, nullptr};
pid_t child = 0;
int spawnError = posix_spawn(&child, helper, &actions, &attributes, argv, environ);
posix_spawn_file_actions_destroy(&actions);
posix_spawnattr_destroy(&attributes);
bfree(helper);
::close(control[1]);
::close(frames[1]);
::close(logs[1]);
if (spawnError != 0) {
::close(control[0]);
::close(frames[0]);
::close(logs[0]);
return startFailed(text("PanelRendererError"));
}
pid_ = child;
controlFd_ = control[0];
frameFd_ = frames[0];
logFd_ = logs[0];
nonBlocking(frameFd_);
nonBlocking(logFd_);
if (!sendConfiguration()) {
stopRenderer();
return startFailed(text("PanelRendererError"));
}
frameNotifier_ = new QSocketNotifier(frameFd_, QSocketNotifier::Read, this);
connect(frameNotifier_, &QSocketNotifier::activated, this, [this] { readFrames(); });
logNotifier_ = new QSocketNotifier(logFd_, QSocketNotifier::Read, this);
connect(logNotifier_, &QSocketNotifier::activated, this, [this] { readLogs(); });
}
void startFailed(const QString &message)
{
status_ = message;
stopRenderer();
update();
}
void restartRenderer()
{
stopRenderer();
startRenderer();
}
void stopRenderer()
{
delete frameNotifier_;
frameNotifier_ = nullptr;
delete logNotifier_;
logNotifier_ = nullptr;
if (controlFd_ >= 0)
shutdown(controlFd_, SHUT_RDWR);
closeFd(controlFd_);
closeFd(frameFd_);
closeFd(logFd_);
if (pid_ > 0) {
kill(-pid_, SIGTERM);
for (unsigned attempt = 0; attempt < 50; ++attempt) {
pid_t result = waitpid(pid_, nullptr, WNOHANG);
if (result == pid_ || (result < 0 && errno == ECHILD)) {
pid_ = 0;
break;
}
usleep(10000);
}
if (pid_ > 0) {
kill(-pid_, SIGKILL);
while (waitpid(pid_, nullptr, 0) < 0 && errno == EINTR) {
}
pid_ = 0;
}
}
if (shared_)
munmap(shared_, sharedSize_);
shared_ = nullptr;
sharedSize_ = 0;
closeFd(sharedFd_);
logBuffer_.clear();
}
void readFrames()
{
uint8_t frameSlots[128];
uint8_t latest = OWB_SHARED_FRAME_SLOTS;
for (;;) {
ssize_t count = read(frameFd_, frameSlots, sizeof(frameSlots));
if (count < 0 && errno == EINTR)
continue;
if (count <= 0)
break;
for (ssize_t i = 0; i < count; ++i)
if (frameSlots[i] < OWB_SHARED_FRAME_SLOTS)
latest = frameSlots[i];
}
if (!shared_ || latest >= OWB_SHARED_FRAME_SLOTS)
return;
for (unsigned attempt = 0; attempt < 3; ++attempt) {
uint64_t before = __atomic_load_n(&shared_->sequence[latest], __ATOMIC_ACQUIRE);
if (before & 1u)
continue;
QImage view(owb_shared_slot(shared_, latest), renderWidth_, renderHeight_, renderWidth_ * 4,
QImage::Format_ARGB32);
QImage copy = view.copy();
uint64_t after = __atomic_load_n(&shared_->sequence[latest], __ATOMIC_ACQUIRE);
if (before == after && !(after & 1u)) {
frame_ = std::move(copy);
status_.clear();
update();
break;
}
}
}
void readLogs()
{
char bytes[1024];
for (;;) {
ssize_t count = read(logFd_, bytes, sizeof(bytes));
if (count < 0 && errno == EINTR)
continue;
if (count <= 0)
break;
logBuffer_.append(bytes, static_cast<qsizetype>(count));
}
for (;;) {
qsizetype newline = logBuffer_.indexOf('\n');
if (newline < 0)
break;
QByteArray line = logBuffer_.left(newline).trimmed();
logBuffer_.remove(0, newline + 1);
if (!line.isEmpty())
blog(LOG_INFO, "[obs-webkit-browser dock] %s", line.constData());
}
}
QString url_;
QString status_;
QImage frame_;
QTimer resizeTimer_;
pid_t pid_ = 0;
int controlFd_ = -1;
int frameFd_ = -1;
int logFd_ = -1;
int sharedFd_ = -1;
owb_shared_frames *shared_ = nullptr;
size_t sharedSize_ = 0;
int renderWidth_ = 2;
int renderHeight_ = 2;
QSocketNotifier *frameNotifier_ = nullptr;
QSocketNotifier *logNotifier_ = nullptr;
QByteArray logBuffer_;
};
class DockManager final {
public:
DockManager()
{
obs_frontend_add_save_callback(saveCallback, this);
obs_frontend_add_preload_callback(preloadCallback, this);
obs_frontend_add_event_callback(eventCallback, this);
setupFrontend();
}
~DockManager()
{
/* OBS clears frontend callbacks and owns the dock wrappers before
* unloading modules during shutdown. Calling frontend removal APIs
* from obs_module_unload is therefore too late. */
panels_.clear();
if (menuAction_)
delete menuAction_;
}
void setupFrontend()
{
if (menuAction_ || !QApplication::instance())
return;
auto *main = static_cast<QMainWindow *>(obs_frontend_get_main_window());
if (!main)
return;
menuAction_ = new QAction(text("CustomBrowserDocks"), main);
QObject::connect(menuAction_, &QAction::triggered, main, [this] { showEditor(); });
if (QMenu *docksMenu = main->findChild<QMenu *>("menuDocks"))
docksMenu->addAction(menuAction_);
else
main->menuBar()->addAction(menuAction_);
reconcile();
}
private:
static QString dockId(const QString &id) { return QString::fromUtf8(kDockPrefix) + id; }
static void eventCallback(obs_frontend_event event, void *opaque)
{
auto *manager = static_cast<DockManager *>(opaque);
if (event == OBS_FRONTEND_EVENT_FINISHED_LOADING)
manager->setupFrontend();
else if (event == OBS_FRONTEND_EVENT_EXIT) {
manager->panels_.clear();
manager->menuAction_ = nullptr;
}
}
static void saveCallback(obs_data_t *saveData, bool saving, void *opaque)
{
static_cast<DockManager *>(opaque)->saveLoad(saveData, saving);
}
static void preloadCallback(obs_data_t *saveData, bool saving, void *opaque)
{
if (!saving)
static_cast<DockManager *>(opaque)->saveLoad(saveData, false);
}
void saveLoad(obs_data_t *saveData, bool saving)
{
if (saving) {
obs_data_array_t *array = obs_data_array_create();
for (const DockDefinition &definition : definitions_) {
obs_data_t *item = obs_data_create();
obs_data_set_string(item, "id", definition.id.toUtf8().constData());
obs_data_set_string(item, "title", definition.title.toUtf8().constData());
obs_data_set_string(item, "url", definition.url.toUtf8().constData());
obs_data_array_push_back(array, item);
obs_data_release(item);
}
obs_data_set_array(saveData, kSaveKey, array);
obs_data_array_release(array);
return;
}
definitions_.clear();
obs_data_array_t *array = obs_data_get_array(saveData, kSaveKey);
if (array) {
size_t count = obs_data_array_count(array);
for (size_t index = 0; index < count; ++index) {
obs_data_t *item = obs_data_array_item(array, index);
DockDefinition definition{
QString::fromUtf8(obs_data_get_string(item, "id")),
QString::fromUtf8(obs_data_get_string(item, "title")),
QString::fromUtf8(obs_data_get_string(item, "url")),
};
if (definition.id.isEmpty())
definition.id = QUuid::createUuid().toString(QUuid::WithoutBraces);
if (!definition.title.trimmed().isEmpty() && !definition.url.trimmed().isEmpty())
definitions_.push_back(std::move(definition));
obs_data_release(item);
}
obs_data_array_release(array);
}
reconcile();
}
void reconcile()
{
if (!menuAction_)
return;
QHash<QString, DockDefinition> wanted;
for (const DockDefinition &definition : definitions_)
wanted.insert(definition.id, definition);
for (auto it = panels_.begin(); it != panels_.end();) {
if (!wanted.contains(it.key())) {
QByteArray id = dockId(it.key()).toUtf8();
obs_frontend_remove_dock(id.constData());
it = panels_.erase(it);
} else {
++it;
}
}
for (const DockDefinition &definition : definitions_) {
if (panels_.contains(definition.id) && panels_[definition.id]) {
panels_[definition.id]->setUrl(definition.url);
panels_[definition.id]->setDockTitle(definition.title);
continue;
}
auto *panel = new WebPanel(definition.url);
QByteArray id = dockId(definition.id).toUtf8();
QByteArray title = definition.title.toUtf8();
if (obs_frontend_add_dock_by_id(id.constData(), title.constData(), panel))
panels_.insert(definition.id, panel);
else
delete panel;
}
}
void showEditor()
{
auto *main = static_cast<QWidget *>(obs_frontend_get_main_window());
QDialog dialog(main);
dialog.setWindowTitle(text("CustomBrowserDocks"));
dialog.resize(720, 360);
auto *layout = new QVBoxLayout(&dialog);
layout->addWidget(new QLabel(text("CustomBrowserDocksDescription"), &dialog));
auto *table = new QTableWidget(0, 2, &dialog);
table->setHorizontalHeaderLabels({text("PanelName"), text("URL")});
table->horizontalHeader()->setSectionResizeMode(0, QHeaderView::ResizeToContents);
table->horizontalHeader()->setSectionResizeMode(1, QHeaderView::Stretch);
table->setSelectionBehavior(QAbstractItemView::SelectRows);
table->setSelectionMode(QAbstractItemView::ExtendedSelection);
layout->addWidget(table);
auto appendRow = [table](const DockDefinition &definition) {
int row = table->rowCount();
table->insertRow(row);
auto *title = new QTableWidgetItem(definition.title);
title->setData(Qt::UserRole, definition.id);
table->setItem(row, 0, title);
table->setItem(row, 1, new QTableWidgetItem(definition.url));
};
for (const DockDefinition &definition : definitions_)
appendRow(definition);
auto *rowButtons = new QHBoxLayout;
auto *add = new QPushButton(text("AddPanel"), &dialog);
auto *remove = new QPushButton(text("RemovePanel"), &dialog);
rowButtons->addWidget(add);
rowButtons->addWidget(remove);
rowButtons->addStretch();
layout->addLayout(rowButtons);
QObject::connect(add, &QPushButton::clicked, table, [table, appendRow] {
appendRow({QUuid::createUuid().toString(QUuid::WithoutBraces), QString(), "https://"});
int row = table->rowCount() - 1;
table->setCurrentCell(row, 0);
table->editItem(table->item(row, 0));
});
QObject::connect(remove, &QPushButton::clicked, table, [table] {
QSet<int> rows;
for (QTableWidgetItem *item : table->selectedItems())
rows.insert(item->row());
QList<int> sorted = rows.values();
std::sort(sorted.begin(), sorted.end(), std::greater<int>());
for (int row : sorted)
table->removeRow(row);
});
auto *buttons = new QDialogButtonBox(QDialogButtonBox::Apply | QDialogButtonBox::Close,
Qt::Horizontal, &dialog);
layout->addWidget(buttons);
QObject::connect(buttons->button(QDialogButtonBox::Close), &QPushButton::clicked, &dialog,
&QDialog::reject);
QObject::connect(buttons->button(QDialogButtonBox::Apply), &QPushButton::clicked, &dialog,
[this, table, &dialog] {
QVector<DockDefinition> updated;
QSet<QString> titles;
for (int row = 0; row < table->rowCount(); ++row) {
QString title = table->item(row, 0) ? table->item(row, 0)->text().trimmed() : QString();
QString url = table->item(row, 1) ? table->item(row, 1)->text().trimmed() : QString();
if (title.isEmpty() || url.isEmpty()) {
QMessageBox::warning(&dialog, text("CustomBrowserDocks"), text("PanelFieldsRequired"));
return;
}
if (titles.contains(title)) {
QMessageBox::warning(&dialog, text("CustomBrowserDocks"), text("PanelNamesUnique"));
return;
}
titles.insert(title);
QString id = table->item(row, 0)->data(Qt::UserRole).toString();
if (id.isEmpty())
id = QUuid::createUuid().toString(QUuid::WithoutBraces);
updated.push_back({id, title, url});
}
definitions_ = std::move(updated);
reconcile();
obs_frontend_save();
});
dialog.exec();
}
QVector<DockDefinition> definitions_;
QHash<QString, QPointer<WebPanel>> panels_;
QPointer<QAction> menuAction_;
};
DockManager *manager = nullptr;
} // namespace
extern "C" void owb_docks_initialize(void)
{
if (!manager && QApplication::instance())
manager = new DockManager;
}
extern "C" void owb_docks_shutdown(void)
{
delete manager;
manager = nullptr;
}