vulcast-vertical/projector.cpp
Vulcast 0e83ab9eef Initial import from Aitum/obs-vertical-canvas v1.6.4
Forked for Vulcast branding changes.
Licensed under GPL-2.0.
2026-06-29 22:58:49 +01:00

435 lines
10 KiB
C++

#include <QAction>
#include <QGuiApplication>
#include <QMouseEvent>
#include <QMenu>
#include <QScreen>
#include <QWindow>
#include "projector.hpp"
#include <obs-module.h>
#include <obs-frontend-api.h>
#include <util/config-file.h>
#include <util/platform.h>
#ifdef _WIN32
#include <windows.h>
#endif
static inline void GetScaleAndCenterPos(int baseCX, int baseCY, int windowCX, int windowCY, int &x, int &y, float &scale)
{
double windowAspect, baseAspect;
int newCX, newCY;
windowAspect = double(windowCX) / double(windowCY);
baseAspect = double(baseCX) / double(baseCY);
if (windowAspect > baseAspect) {
scale = float(windowCY) / float(baseCY);
newCX = int(double(windowCY) * baseAspect);
newCY = windowCY;
} else {
scale = float(windowCX) / float(baseCX);
newCX = windowCX;
newCY = int(float(windowCX) / baseAspect);
}
x = windowCX / 2 - newCX / 2;
y = windowCY / 2 - newCY / 2;
}
static inline void startRegion(int vX, int vY, int vCX, int vCY, float oL, float oR, float oT, float oB)
{
gs_projection_push();
gs_viewport_push();
gs_set_viewport(vX, vY, vCX, vCY);
gs_ortho(oL, oR, oT, oB, -100.0f, 100.0f);
}
static inline void endRegion()
{
gs_viewport_pop();
gs_projection_pop();
}
config_t *get_user_config(void);
OBSProjector::OBSProjector(CanvasDock *canvas_, int monitor) : OBSQTDisplay(nullptr, Qt::Window), canvas(canvas_)
{
isAlwaysOnTop = config_get_bool(get_user_config(), "BasicWindow", "ProjectorAlwaysOnTop");
if (isAlwaysOnTop)
setWindowFlags(Qt::WindowStaysOnTopHint);
// Mark the window as a projector so SetDisplayAffinity
// can skip it
windowHandle()->setProperty("isOBSProjectorWindow", true);
#if defined(__linux__) || defined(__FreeBSD__) || defined(__DragonFly__)
// Prevents resizing of projector windows
setAttribute(Qt::WA_PaintOnScreen, false);
#endif
#ifdef __APPLE__
setWindowIcon(QIcon::fromTheme("obs", QIcon(":/res/images/obs_256x256.png")));
#else
setWindowIcon(QIcon::fromTheme("obs", QIcon(":/res/images/obs.png")));
#endif
if (monitor == -1)
resize(480, 270);
else
SetMonitor(monitor);
UpdateProjectorTitle();
QAction *action = new QAction(this);
action->setShortcut(Qt::Key_Escape);
addAction(action);
connect(action, SIGNAL(triggered()), this, SLOT(EscapeTriggered()));
setAttribute(Qt::WA_DeleteOnClose, true);
//disable application quit when last window closed
setAttribute(Qt::WA_QuitOnClose, false);
//installEventFilter(CreateShortcutFilter());
auto addDrawCallback = [this]() {
obs_display_add_draw_callback(GetDisplay(), OBSRender, this);
obs_display_set_background_color(GetDisplay(), 0x000000);
};
connect(this, &OBSQTDisplay::DisplayCreated, addDrawCallback);
//connect(App(), &QGuiApplication::screenRemoved, this, &OBSProjector::ScreenRemoved);
//App()->IncrementSleepInhibition();
ready = true;
show();
// We need it here to allow keyboard input in X11 to listen to Escape
activateWindow();
}
OBSProjector::~OBSProjector()
{
obs_display_remove_draw_callback(GetDisplay(), OBSRender, this);
//App()->DecrementSleepInhibition();
screen = nullptr;
}
void OBSProjector::SetMonitor(int monitor)
{
savedMonitor = monitor;
auto screens = QGuiApplication::screens();
if (monitor < 0 || monitor >= screens.size())
screen = nullptr;
else
screen = screens[monitor];
if (screen)
setGeometry(screen->geometry());
showFullScreen();
SetHideCursor();
}
void OBSProjector::SetHideCursor()
{
if (savedMonitor == -1)
return;
bool hideCursor = config_get_bool(get_user_config(), "BasicWindow", "HideProjectorCursor");
if (hideCursor)
setCursor(Qt::BlankCursor);
else
setCursor(Qt::ArrowCursor);
}
void OBSProjector::OBSRender(void *data, uint32_t cx, uint32_t cy)
{
OBSProjector *window = reinterpret_cast<OBSProjector *>(data);
if (!window->ready)
return;
obs_canvas_t *canvas = window->canvas->canvas;
uint32_t targetCX;
uint32_t targetCY;
int x, y;
int newCX, newCY;
float scale;
targetCX = window->canvas->canvas_width;
targetCY = window->canvas->canvas_height;
GetScaleAndCenterPos(targetCX, targetCY, cx, cy, x, y, scale);
newCX = int(scale * float(targetCX));
newCY = int(scale * float(targetCY));
startRegion(x, y, newCX, newCY, 0.0f, float(targetCX), 0.0f, float(targetCY));
obs_canvas_render(canvas);
endRegion();
}
void OBSProjector::mousePressEvent(QMouseEvent *event)
{
OBSQTDisplay::mousePressEvent(event);
if (event->button() == Qt::RightButton) {
QMenu popup(this);
QMenu *projectorMenu = new QMenu(QString::fromUtf8(obs_frontend_get_locale_string("Fullscreen")));
canvas->AddProjectorMenuMonitors(projectorMenu, this, SLOT(OpenFullScreenProjector()));
popup.addMenu(projectorMenu);
if (GetMonitor() > -1) {
popup.addAction(QString::fromUtf8(obs_frontend_get_locale_string("Windowed")), this,
SLOT(OpenWindowedProjector()));
} else if (!this->isMaximized()) {
popup.addAction(QString::fromUtf8(obs_frontend_get_locale_string("ResizeProjectorWindowToContent")), this,
SLOT(ResizeToContent()));
}
QAction *alwaysOnTopButton =
new QAction(QString::fromUtf8(obs_frontend_get_locale_string("Basic.MainMenu.View.AlwaysOnTop")), this);
alwaysOnTopButton->setCheckable(true);
alwaysOnTopButton->setChecked(isAlwaysOnTop);
connect(alwaysOnTopButton, &QAction::toggled, this, &OBSProjector::AlwaysOnTopToggled);
popup.addAction(alwaysOnTopButton);
popup.addAction(QString::fromUtf8(obs_frontend_get_locale_string("Close")), this, SLOT(EscapeTriggered()));
popup.exec(QCursor::pos());
}
}
void OBSProjector::EscapeTriggered()
{
canvas->DeleteProjector(this);
}
void OBSProjector::UpdateProjectorTitle(QString name)
{
UNUSED_PARAMETER(name);
bool window = (GetMonitor() == -1);
QString title = QString::fromUtf8(obs_frontend_get_locale_string(window ? "PreviewWindow" : "PreviewProjector"));
setWindowTitle(title);
}
int OBSProjector::GetMonitor()
{
return savedMonitor;
}
void OBSProjector::RenameProjector(QString oldName, QString newName)
{
if (oldName == newName)
return;
UpdateProjectorTitle(newName);
}
void OBSProjector::OpenFullScreenProjector()
{
if (!isFullScreen())
prevGeometry = geometry();
int monitor = sender()->property("monitor").toInt();
SetMonitor(monitor);
UpdateProjectorTitle();
}
void OBSProjector::OpenWindowedProjector()
{
showFullScreen();
showNormal();
setCursor(Qt::ArrowCursor);
if (!prevGeometry.isNull())
setGeometry(prevGeometry);
else
resize(480, 270);
savedMonitor = -1;
UpdateProjectorTitle();
screen = nullptr;
}
void OBSProjector::ResizeToContent()
{
uint32_t targetCX = canvas->canvas_width;
uint32_t targetCY = canvas->canvas_height;
int x, y, newX, newY;
float scale;
QSize size = this->size();
GetScaleAndCenterPos(targetCX, targetCY, size.width(), size.height(), x, y, scale);
newX = size.width() - (x * 2);
newY = size.height() - (y * 2);
resize(newX, newY);
}
void OBSProjector::AlwaysOnTopToggled(bool onTop)
{
SetIsAlwaysOnTop(onTop, true);
}
void OBSProjector::closeEvent(QCloseEvent *event)
{
EscapeTriggered();
event->accept();
}
bool OBSProjector::IsAlwaysOnTop() const
{
return isAlwaysOnTop;
}
bool OBSProjector::IsAlwaysOnTopOverridden() const
{
return isAlwaysOnTopOverridden;
}
void OBSProjector::SetIsAlwaysOnTop(bool onTop, bool isOverridden)
{
isAlwaysOnTop = onTop;
isAlwaysOnTopOverridden = isOverridden;
SetAlwaysOnTop(this, onTop);
}
void OBSProjector::ScreenRemoved(QScreen *screen_)
{
if (GetMonitor() < 0 || !screen)
return;
if (screen == screen_)
EscapeTriggered();
}
#ifdef _WIN32
bool IsAlwaysOnTop(QWidget *window)
{
DWORD exStyle = GetWindowLong((HWND)window->winId(), GWL_EXSTYLE);
return (exStyle & WS_EX_TOPMOST) != 0;
}
#else
bool IsAlwaysOnTop(QWidget *window)
{
return (window->windowFlags() & Qt::WindowStaysOnTopHint) != 0;
}
#endif
#ifdef _WIN32
void SetAlwaysOnTop(QWidget *window, bool enable)
{
HWND hwnd = (HWND)window->winId();
SetWindowPos(hwnd, enable ? HWND_TOPMOST : HWND_NOTOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE);
}
#else
void SetAlwaysOnTop(QWidget *window, bool enable)
{
Qt::WindowFlags flags = window->windowFlags();
if (enable) {
flags |= Qt::WindowStaysOnTopHint;
} else {
flags &= ~Qt::WindowStaysOnTopHint;
}
window->setWindowFlags(flags);
window->show();
}
#endif
#ifdef _WIN32
#if QT_VERSION < QT_VERSION_CHECK(6, 4, 0)
#define GENERIC_MONITOR_NAME QStringLiteral("Generic PnP Monitor")
struct MonitorData {
const wchar_t *id;
MONITORINFOEX info;
bool found;
};
static BOOL CALLBACK GetMonitorCallback(HMONITOR monitor, HDC, LPRECT, LPARAM param)
{
MonitorData *data = (MonitorData *)param;
if (GetMonitorInfoW(monitor, &data->info)) {
if (wcscmp(data->info.szDevice, data->id) == 0) {
data->found = true;
return false;
}
}
return true;
}
QString GetMonitorName(const QString &id)
{
MonitorData data = {};
data.id = (const wchar_t *)id.utf16();
data.info.cbSize = sizeof(data.info);
EnumDisplayMonitors(nullptr, nullptr, GetMonitorCallback, (LPARAM)&data);
if (!data.found) {
return GENERIC_MONITOR_NAME;
}
UINT32 numPath, numMode;
if (GetDisplayConfigBufferSizes(QDC_ONLY_ACTIVE_PATHS, &numPath, &numMode) != ERROR_SUCCESS) {
return GENERIC_MONITOR_NAME;
}
std::vector<DISPLAYCONFIG_PATH_INFO> paths(numPath);
std::vector<DISPLAYCONFIG_MODE_INFO> modes(numMode);
if (QueryDisplayConfig(QDC_ONLY_ACTIVE_PATHS, &numPath, paths.data(), &numMode, modes.data(), nullptr) != ERROR_SUCCESS) {
return GENERIC_MONITOR_NAME;
}
DISPLAYCONFIG_TARGET_DEVICE_NAME target;
bool found = false;
paths.resize(numPath);
for (size_t i = 0; i < numPath; ++i) {
const DISPLAYCONFIG_PATH_INFO &path = paths[i];
DISPLAYCONFIG_SOURCE_DEVICE_NAME s;
s.header.type = DISPLAYCONFIG_DEVICE_INFO_GET_SOURCE_NAME;
s.header.size = sizeof(s);
s.header.adapterId = path.sourceInfo.adapterId;
s.header.id = path.sourceInfo.id;
if (DisplayConfigGetDeviceInfo(&s.header) == ERROR_SUCCESS &&
wcscmp(data.info.szDevice, s.viewGdiDeviceName) == 0) {
target.header.type = DISPLAYCONFIG_DEVICE_INFO_GET_TARGET_NAME;
target.header.size = sizeof(target);
target.header.adapterId = path.sourceInfo.adapterId;
target.header.id = path.targetInfo.id;
found = DisplayConfigGetDeviceInfo(&target.header) == ERROR_SUCCESS;
break;
}
}
if (!found) {
return GENERIC_MONITOR_NAME;
}
return QString::fromWCharArray(target.monitorFriendlyDeviceName);
}
#endif
#endif