823 lines
23 KiB
C++
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;
|
|
}
|