From ef0a61233a08363691e523f009e8132319e3582b Mon Sep 17 00:00:00 2001 From: Simon Rettberg Date: Tue, 28 Aug 2018 15:27:40 +0200 Subject: Make wakeup work --- CMakeLists.txt | 3 +- dbus/de.bwlehrpool.beamergui.conf | 14 + src/bus.cpp | 40 ++ src/bus.h | 26 ++ src/i18n/de.ts | 47 ++- src/main.cpp | 14 +- src/widget.cpp | 62 ++- src/widget.ui | 37 +- src/x.cpp | 796 -------------------------------------- src/x.h | 97 ----- src/xprivate.cpp | 452 ++++++++++++++++++++++ src/xprivate.h | 70 ++++ src/xx.cpp | 348 +++++++++++++++++ src/xx.h | 66 ++++ 14 files changed, 1160 insertions(+), 912 deletions(-) create mode 100644 dbus/de.bwlehrpool.beamergui.conf create mode 100644 src/bus.cpp create mode 100644 src/bus.h delete mode 100644 src/x.cpp delete mode 100644 src/x.h create mode 100644 src/xprivate.cpp create mode 100644 src/xprivate.h create mode 100644 src/xx.cpp create mode 100644 src/xx.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 448676c..78e9c7c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -16,7 +16,7 @@ SET( CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11" ) # # Qt5 # -FIND_PACKAGE(Qt5 COMPONENTS Widgets LinguistTools REQUIRED) +FIND_PACKAGE(Qt5 COMPONENTS Widgets LinguistTools DBus REQUIRED) FIND_PACKAGE(X11 REQUIRED) if(NOT X11_Xrandr_FOUND) @@ -77,6 +77,7 @@ add_executable(beamergui target_link_libraries(beamergui Qt5::Widgets + Qt5::DBus ${X11_LIBRARIES} ${X11_Xrandr_LIB} ) diff --git a/dbus/de.bwlehrpool.beamergui.conf b/dbus/de.bwlehrpool.beamergui.conf new file mode 100644 index 0000000..d3adf9b --- /dev/null +++ b/dbus/de.bwlehrpool.beamergui.conf @@ -0,0 +1,14 @@ + + + + + + + + + + + + diff --git a/src/bus.cpp b/src/bus.cpp new file mode 100644 index 0000000..1dadedb --- /dev/null +++ b/src/bus.cpp @@ -0,0 +1,40 @@ +#include "bus.h" + +#include + +static const QString SERVICE_NAME("de.bwlehrpool.beamergui"); + +Bus* Bus::_instance = nullptr; + +Bus::Bus(QObject *parent) : QObject(parent), _hasListener(false) +{ + +} + +bool Bus::registerListener() +{ + if (!QDBusConnection::systemBus().isConnected()) { + qDebug() << "Cannot connect to system bus"; + return false; + } + QDBusServiceWatcher *w = new QDBusServiceWatcher("de.bwlehrpool.beamergui", QDBusConnection::systemBus()); + w->setParent(this); + connect(w, &QDBusServiceWatcher::serviceRegistered, [=](const QString &service) { + qDebug() << "Registered Service" << service; + emit serviceConnected(); + }); + return true; +} + +bool Bus::registerService() +{ + if (!QDBusConnection::systemBus().isConnected()) { + qDebug() << "Cannot connect to system bus"; + return false; + } + if (!QDBusConnection::systemBus().registerService(SERVICE_NAME)) { + qDebug() << QDBusConnection::systemBus().lastError().message(); + return false; + } + return true; +} diff --git a/src/bus.h b/src/bus.h new file mode 100644 index 0000000..f98ee65 --- /dev/null +++ b/src/bus.h @@ -0,0 +1,26 @@ +#ifndef BUS_H +#define BUS_H + +#include + +class Bus : public QObject +{ + Q_OBJECT +public: + bool registerListener(); + bool registerService(); + inline static Bus* inst() { + if (_instance == nullptr) _instance = new Bus(); + return _instance; + } + +signals: + void serviceConnected(); + +private: + explicit Bus(QObject *parent = nullptr); + bool _hasListener; + static Bus *_instance; +}; + +#endif // BUS_H diff --git a/src/i18n/de.ts b/src/i18n/de.ts index a9ade8d..b8537b6 100644 --- a/src/i18n/de.ts +++ b/src/i18n/de.ts @@ -4,7 +4,7 @@ QCoreApplication - + %1x%2 @@ -57,52 +57,79 @@ - + + Exit + + + + + Close + + + + (off) - + Output - + Position - + Do you want to keep this resolution? - + Keep + + + Confirm + + + + + This terminates the GUI. +It will not pop up again if further screens are connected. +Are you sure? + + main - + Automatically configure modes and set up screens. - + Show config GUI if more than one screen is connected. - + Keep running in background and show GUI again when number of screens changes. - + Test mode, don't actually apply any changes. + + + Connect to system bus to trigger wakeup of wainting beamergui. + + diff --git a/src/main.cpp b/src/main.cpp index 1493bcd..2a85617 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,6 +1,7 @@ #include "widget.h" #include "main.h" -#include "x.h" +#include "xx.h" +#include "bus.h" #include #include @@ -8,7 +9,7 @@ #include namespace { -bool _testMode, _autoSetup, _showGui, _backgroundMode; +bool _testMode, _autoSetup, _showGui, _backgroundMode, _wakeup; } namespace CommandLine @@ -37,6 +38,10 @@ int main(int argc, char *argv[]) parseCommandLine(a); + if (_wakeup) { + return Bus::inst()->registerService() ? 0 : 1; + } + ScreenMode currentMode; if (CommandLine::autoSetup()) { currentMode = ScreenSetup::inst()->setDefaultMode(CommandLine::testMode()); @@ -81,10 +86,15 @@ static void parseCommandLine(const QApplication &a) QCommandLineOption oTest(QStringList() << "t" << "test", QCoreApplication::translate("main", "Test mode, don't actually apply any changes.")); parser.addOption(oTest); + // Wakeup beamergui daemon via DBus connect + QCommandLineOption oWakeup(QStringList() << "w" << "wakeup", + QCoreApplication::translate("main", "Connect to system bus to trigger wakeup of wainting beamergui.")); + parser.addOption(oWakeup); // PARSE parser.process(a); _testMode = parser.isSet(oTest); _autoSetup = parser.isSet(oAutoSetup); _showGui = parser.isSet(oShowGui); _backgroundMode = parser.isSet(oBackground); + _wakeup = parser.isSet(oWakeup); } diff --git a/src/widget.cpp b/src/widget.cpp index d0f0312..bc4869f 100644 --- a/src/widget.cpp +++ b/src/widget.cpp @@ -1,14 +1,20 @@ #include "widget.h" +#include "main.h" +#include "xx.h" +#include "bus.h" #include "ui_widget.h" #include "timeoutdialog.h" -#include "x.h" -#include "main.h" #include #include #include #include #include +#include + +/* + * Helper and static stuff + */ class ScreenWidget : public QWidget { @@ -62,7 +68,10 @@ static void addBoldListener(QComboBox *combo) }); } -//______________________________________________________________________________ +/* + * Main widget + */ + Widget::Widget(QWidget *parent) : QWidget(parent), _ui(new Ui::Widget), @@ -128,6 +137,36 @@ Widget::Widget(QWidget *parent) : connect(qApp, &QGuiApplication::screenRemoved, [this](const QScreen *scrn) { _qtScreens.removeAll(scrn); }); + _ui->btnExit->setVisible(CommandLine::backgroundMode()); + if (CommandLine::backgroundMode()) { + // Listener + if (!Bus::inst()->registerListener()) { + qDebug() << "WARNING: CANNOT CONNECT TO DBUS FOR LISTENING"; + // TODO: GUI feedback + } else { + // Worked fine + // Timer + QTimer *t = new QTimer(this); + t->setSingleShot(true); + connect(t, &QTimer::timeout, [=]() { + if (this->isHidden()) { + ScreenSetup::inst()->initModes(); + this->show(); + } else { + // TODO: Flash button + } + }); + // GUI popup logic + connect(Bus::inst(), &Bus::serviceConnected, [=]() { + qDebug() << "\\o/ Received DBus connect notification \\o/"; + if (this->isHidden()) { + t->start(1500); + } else { + // TODO: Flash button + } + }); + } + } } //______________________________________________________________________________ @@ -516,6 +555,23 @@ void Widget::connectButtons() { ScreenSetup::inst()->updateScreenResources(); initControls(); }); + // Close + connect(_ui->btnClose, &QPushButton::clicked, [=](bool) { + if (CommandLine::backgroundMode()) { + this->hide(); + } else { + qApp->exit(0); + } + }); + // Exit + connect(_ui->btnExit, &QPushButton::clicked, [=](bool) { + if (QMessageBox::question(this, tr("Confirm"), + tr("This terminates the GUI.\n" + "It will not pop up again if further screens are connected.\n" + "Are you sure?")) == QMessageBox::Yes) { + qApp->exit(0); + } + }); } diff --git a/src/widget.ui b/src/widget.ui index 7a09c4c..a0be031 100644 --- a/src/widget.ui +++ b/src/widget.ui @@ -6,8 +6,8 @@ 0 0 - 796 - 309 + 687 + 340 @@ -22,7 +22,7 @@ - + @@ -257,6 +257,37 @@ border: 2px solid black; + + + + + + Exit + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Close + + + + + diff --git a/src/x.cpp b/src/x.cpp deleted file mode 100644 index ef43f08..0000000 --- a/src/x.cpp +++ /dev/null @@ -1,796 +0,0 @@ -#include "x.h" -#include "cvt.h" -#include - - -/////////////////////////////////////////////////////////////////////////////// -/////////////////////////////////////////////////////////////////////////////// -/////////////////////////////////////////////////////////////////////////////// - -enum class Projector { - No, - Yes, - Maybe -}; - -struct OutputInfo { - OutputInfo(RROutput id, XRROutputInfo *output, XRRCrtcInfo *crtc, XRRModeInfo *mode) - : output(output), crtc(crtc), mode(mode), id(id), position(-1), outputType(Projector::No) {} - ~OutputInfo() { - XRRFreeOutputInfo(output); - } - XRROutputInfo* output; - XRRCrtcInfo* crtc; - XRRModeInfo* mode; - RROutput id; - QString modelName; - QString outputName; - int position; - Projector outputType; -}; - -ScreenInfo::ScreenInfo(const OutputInfo *oi, const ModeMap &om) - : position(oi->position), name(oi->modelName), output(oi->outputName), - isProjector(oi->outputType == Projector::Yes) -{ - if (oi->mode != nullptr) { - currentResolution = QSize(QSize(int(oi->mode->width), int(oi->mode->height))); - } - for (int i = 0; i < oi->output->nmode; ++i) { - if (om.contains(oi->output->modes[i])) { - auto m = om.value(oi->output->modes[i]); - const QSize size(int(m->width), int(m->height)); - if (!modes.contains(size)) { - modes.append(size); - } - if (i == 0 && oi->output->npreferred > 0) { - preferredResolution = size; - } - } - } -} - -ScreenSetup * ScreenSetup::_instance = nullptr; - -ScreenSetup::ScreenSetup() : _screenResources(nullptr) -{ - // Get initial data (to be freed) - _display = XOpenDisplay(nullptr); - if (_display == nullptr) { - qFatal("Cannot open display"); - ::exit(1); - } - _EDID_ATOM = XInternAtom(_display, RR_PROPERTY_RANDR_EDID, False); - /* Get informations about Xserver */ - updateScreenResources(); - -} - -void ScreenSetup::freeResources() -{ - freeCrtcBackup(); - // Clear the modemap (nothing to be freed, stored in screenResources) - _modeMap.clear(); - _resolutions.clear(); - - // Clear output info - for (OutputInfo *output : _outputMap.values()) { - delete output; - } - _outputMap.clear(); - - // Clear crtc info - for (XRRCrtcInfo *info : _crtcMap.values()) { - XRRFreeCrtcInfo(info); - } - _crtcMap.clear(); - - // Release resources - if (_screenResources != nullptr) { - XRRFreeScreenResources(_screenResources); - _screenResources = nullptr; - } - XRRFreeScreenResources(_screenResources); -} - -static bool xRectLessThan(const OutputInfo* const &a, const OutputInfo* const &b) -{ - return a->crtc->x < b->crtc->x; -} - -static double toVertRefresh(const XRRModeInfo *mode) -{ - if (mode->hTotal > 0 && mode->vTotal > 0) - return (double(mode->dotClock) / (double(mode->hTotal) * double(mode->vTotal))); - return 0; -} - -/** - * Check list of known model names that falsely report a screen size or similar - */ -static QStringList initProjectorList() -{ - QStringList list; - list << "AT-HDVS-RX"; // Switchbox - // TODO: Load from file - return list; -} - -static bool isProjectorName(const QString &name) -{ - static QStringList projectors = initProjectorList(); - return projectors.contains(name); -} - -//___________________________________________________________________________ -void ScreenSetup::updateScreenResources() -{ - freeResources(); - _screenResources = XRRGetScreenResourcesCurrent(_display, DefaultRootWindow(_display)); - // Create the modemap - qDebug() << "Modes"; - for (int i = 0; i < _screenResources->nmode; ++i) { - _modeMap.insert( - _screenResources->modes[i].id, - &_screenResources->modes[i]); - qDebug() << _screenResources->modes[i].id << "\t" - << _screenResources->modes[i].width << "x" - << _screenResources->modes[i].height; - } - - /* - int num = 0; - XRRMonitorInfo *mi = XRRGetMonitors(_display, DefaultRootWindow(_display), True, &num); - for (int i = 0; i < num; ++i) { - char *name = XGetAtomName(_display, mi [i].name); - if (name == nullptr) - continue; - qDebug() << "some monitor with name" << name << mi[i].primary; - XFree(name); - } - */ - - // Create crtcMap - qDebug() << "CRTCs"; - for (int i = 0; i < _screenResources->ncrtc; ++i) { - XRRCrtcInfo * info = XRRGetCrtcInfo( - _display, - _screenResources, - _screenResources->crtcs[i]); - if (info == nullptr) { - qDebug() << "Error getting CRTC info for crtc" << i; - continue; - } - qDebug() << _screenResources->crtcs[i] << "-- Outputs:" << info->noutput << "Possible:" << info->npossible; - _crtcMap.insert(_screenResources->crtcs[i], info); - } - - // Create outputmap and connectedOutputMap - qDebug() << "Outputs"; - QHash tempMap; - QMap typeCount; - for (int i = 0; i < _screenResources->noutput; ++i) { - XRROutputInfo* info = XRRGetOutputInfo( - _display, - _screenResources, - _screenResources->outputs[i]); - if (info == nullptr) { - qDebug() << "Error getting info for output" << i; - continue; - } - const QString outputName = QString::fromLocal8Bit(info->name, info->nameLen); - tempMap.insert(_screenResources->outputs[i], outputName); - if (info->connection == RR_Disconnected) { - qDebug() << "Ignoring disconnected output" << outputName; - XRRFreeOutputInfo(info); - continue; - } - bool disconnected = false; - if (info->crtc == None) { - disconnected = true; - qDebug() << "Connected output" << outputName << "has no CRTC -- trying to find free one"; - info->crtc = getFreeCrtc(info); - } - if (!_crtcMap.contains(info->crtc)) { - qDebug() << "Have output" << outputName << "with no known crtc"; - XRRFreeOutputInfo(info); - continue; - } - XRRCrtcInfo *crtc = _crtcMap.value(info->crtc); - XRRModeInfo *mode = nullptr; - if (!disconnected) { - if (!_modeMap.contains(crtc->mode)) { - qDebug() << "Have output" << outputName << " with crtc with no known mode -- offline?"; - } else { - mode = _modeMap.value(crtc->mode); - } - } - OutputInfo *oi = new OutputInfo(_screenResources->outputs[i], info, crtc, mode); - oi->outputName = outputName; - if (!this->readEdid(oi)) { // We have no EDID - take it as an indicator that there's a dumb output split/switch box - oi->outputType = Projector::Maybe; - } else if ((info->mm_height == 0 && info->mm_width == 0) || isProjectorName(oi->modelName)) { - oi->outputType = Projector::Yes; // Screens with size 0x0 are projectors by convention - } else if (info->mm_width > 500 && info->mm_height > 500) { // Big screen - probably should be handled like a projector - oi->outputType = Projector::Yes; - } else if (info->mm_width > 350 && info->mm_height > 350) { // Somewhere in-between - oi->outputType = Projector::Maybe; - } - typeCount[oi->outputType]++; - _outputMap.insert(_screenResources->outputs[i], oi); - qDebug() << "Connected" << outputName << "-- Clones:" << info->nclone << "Modes:" << info->nmode << "Crtcs:" << info->ncrtc << "Preferred:" << info->npreferred; - } - // Final checks for projector - if (typeCount[Projector::Yes] == 0) { - // No definitive projector - if (typeCount[Projector::Maybe] > 0 && typeCount[Projector::No] > 0) { - // Potential projector(s) and normal screen, promote - for (OutputInfo *info : _outputMap) { - if (info->outputType == Projector::Maybe) { - info->outputType = Projector::Yes; - break; - } - } - } - } - // Print mappings - for (XRRCrtcInfo *info : _crtcMap) { - qDebug() << info << ":"; - for (int i = 0; i < info->npossible; ++i) { - qDebug() << "Possible:" << tempMap[info->possible[i]]; - } - } - // Determine each screen's position - QList screens = _outputMap.values(); - // Nothing connected? - if (screens.isEmpty()) - return; - qSort(screens.begin(), screens.end(), xRectLessThan); - int endX = -0xffff; - qDebug() << "From left to right"; - for (OutputInfo* output : screens) { - if (output->mode == nullptr) { - qDebug() << "(Ignoring" << output->outputName << "since it's disconnected)"; - } - if (output->crtc->x >= endX) { - QSize res(0, 0); - if (_modeMap.contains(output->crtc->mode)) { - auto mode = _modeMap.value(output->crtc->mode); - res = QSize(int(mode->width), int(mode->height)); - } - _resolutions.append(res); - endX = -0xffff; // Reset - } - output->position = _resolutions.size() - 1; - qDebug() << "Screen (" << output->crtc->x << "," << output->crtc->y << ") @" << output->crtc->width << "x" << output->crtc->height << "as screen" << output->position; - if (output->crtc->x + int(output->crtc->width) > endX) { - endX = output->crtc->x + int(output->crtc->width); - } - } - qDebug() << "Loaded."; -} - -QMap ScreenSetup::getScreenPositions() const -{ - QMap ret; - for (auto oi : _outputMap) { - ret.insert(oi->outputName, ScreenInfo(oi, _modeMap)); - } - return ret; -} - - - -bool ScreenSetup::readEdid(OutputInfo* output) -{ - int numProps = 0; - bool found = false; - Atom* properties = XRRListOutputProperties(_display, output->id, &numProps); - for (int i = 0; i < numProps; ++i) { - if (properties[i] == _EDID_ATOM) { - found = true; - break; - } - } - XFree(properties); - if (!found) - return false; - - unsigned long nitems, bytes_after; - unsigned char *prop; - int actual_format; - Atom actual_type; - bool valid; - - XRRGetOutputProperty(_display, output->id, _EDID_ATOM, - 0, 128, False, False, - AnyPropertyType, - &actual_type, &actual_format, - &nitems, &bytes_after, &prop); - valid = (actual_format == 8 && nitems >= 128); - - if (valid) { - int idx; - for (unsigned char *byte = prop + 54; byte < prop + 126; byte += 18) { - if (byte[0] != 0 || byte[1] != 0 || byte[2] != 0) - continue; // Not a text block - if (byte[3] == 0xfc) { // Serial number - output->modelName = QString::fromLatin1(reinterpret_cast(byte) + 5, 13); // It's actually CP-437 but meh - if ((idx = output->modelName.indexOf('\r')) != -1) { - output->modelName.truncate(idx); - } - output->modelName = output->modelName.trimmed(); - qDebug() << "Display name:" << output->modelName; - } - } - } - XFree(prop); - return valid; -} - -/** - * Create common modes and add them to all outputs. - * Make sure every output has some variant of the most commonly - * used modes. - */ -void ScreenSetup::initModes() -{ - // First copy typical resolutions to all outputs -#define RES(x,y) (((y) << 16) | (x)) - QSet wanted; - wanted << RES(1280, 720) << RES(1280, 800) << RES(1920, 1080); - for (XRRModeInfo *mode : _modeMap) { - if (toVertRefresh(mode) < 58 || toVertRefresh(mode) > 61) - continue; // Play it safe and consider only those for copying that are 60Hz - wanted.remove(RES(mode->width, mode->height)); - // Make sure all outputs got it - for (OutputInfo *info : _outputMap) { - if (getOutputModeForResolution(info->output, mode->width, mode->height)) - continue; - XRRAddOutputMode(_display, info->id, mode->id); - } - } -#undef RES - // Create those that no output supported - for (auto res : wanted) { - unsigned int x = res & 0xffff; - unsigned int y = res >> 16; - createMode(x, y, 60, QString::asprintf("%ux%u", x, y)); - } - if (!wanted.isEmpty()) { - updateScreenResources(); - } - // Finally copy all those the projector supports to other outputs - for (auto key : _outputMap.keys()) { - OutputInfo *oi = _outputMap[key]; - if (oi->outputType == Projector::Yes) { - copyModesToAll(key, oi->output->nmode); - } - } - updateScreenResources(); -} - -XRRModeInfo* ScreenSetup::getPreferredMode(OutputInfo *oi) const -{ - if (oi->output->nmode == 0) { - qDebug() << "getPreferredMode: Output" << oi->outputName << "has no modes!?"; - return nullptr; // WTF!? - } - RRMode mode; - if (oi->output->npreferred > 0) { - mode = oi->output->modes[0]; - } else { - mode = getOutputModeForResolution(oi->output, 1920, 1080); - if (mode == None) { - mode = getOutputModeForResolution(oi->output, 1280, 720); - } - if (mode == None) { - mode = getOutputModeForResolution(oi->output, 1280, 800); - } - if (mode == None) { - mode = oi->output->modes[0]; - } - } - if (!_modeMap.contains(mode)) - return nullptr; - return _modeMap[mode]; -} - -QList ScreenSetup::getTotalSize(const QList &projectors, const QList &screens) const -{ - const int max = qMax(screens.size(), projectors.size()); - QList modes; - for (int i = 0; i < max; ++i) { - XRRModeInfo *mode = nullptr; - if (i < projectors.size()) { - mode = getPreferredMode(projectors.at(i)); - } - if (mode == nullptr && i < screens.size()) { - mode = getPreferredMode(screens.at(i)); - } - if (mode != nullptr) { - modes.append(QSize(int(mode->width), int(mode->height))); - } - } - return modes; -} - -static QSize getTotalSizeHorz(const QList &list) -{ - QSize ret(0, 0); - for (auto e : list) { - ret.rwidth() += e.width(); - ret.rheight() = qMax(ret.height(), e.height()); - } - return ret; -} - -bool ScreenSetup::setOutputResolution(OutputInfo *oi, int x, int y, const QSize &size, bool dryRun) -{ - RRMode mode = getOutputModeForResolution(oi->output, size); - if (mode == None) { - qDebug() << "Cannot set" << oi->outputName << "to" << size << " since it's not supported"; - if (oi->output->nmode == 0) - return false; - qDebug() << "falling back to its default"; - mode = oi->output->modes[0]; - } - if (!dryRun) { - XRRSetCrtcConfig(_display, - _screenResources, - oi->output->crtc, - CurrentTime, - x, y, mode, - RR_Rotate_0, - &oi->id, 1); - } - qDebug() << "Set" << oi->outputName << "to" << _modeMap[mode]->width << "x" << _modeMap[mode]->height << "-- offset" << x << "/" << y; - return true; -} - -ScreenMode ScreenSetup::getCurrentMode() -{ - bool notAtOrigin = false; - for (auto oi : _outputMap) { - if (oi->mode != nullptr) { - if (oi->crtc->x != 0 || oi->crtc->y != 0) { - notAtOrigin = true; - } - } - } - if (_outputMap.size() == 1) - return ScreenMode::Single; - if (_outputMap.size() > 2) - return ScreenMode::Advanced; - if (notAtOrigin) - return ScreenMode::Dual; - return ScreenMode::Clone; -} - -ScreenMode ScreenSetup::setDefaultMode(bool dryRun) -{ - if (_outputMap.size() == 1) // Only one output exists, do nothing - return ScreenMode::Single; - QMap screenMap; - QMap projectorMap; - for (auto o : _outputMap) { - qDebug() << o->outputName << quint32(o->outputType); - if (o->outputType == Projector::Yes) { - projectorMap.insert(o->outputName, o); - } else { - screenMap.insert(o->outputName, o); - } - } - auto projectors = projectorMap.values(); - auto screens = screenMap.values(); - qDebug() << projectors.size() << "projectors," << screens.size() << "screens."; - QList outputSizes = getTotalSize(projectors, screens); - if (outputSizes.isEmpty()) - return ScreenMode::Advanced; // Dunno lol - QSize screenSize = getTotalSizeHorz(outputSizes); - if (!dryRun) { - XGrabServer(_display); - disconnectAllCrtcs(); - // Set new screen size - setScreenSize(screenSize); - } - - qDebug() << "Virtual size:" << screenSize << "with" << outputSizes.size() << "different screens."; - - int offset = 0; - for (int i = 0; i < outputSizes.size(); ++i) { - const QSize &size = outputSizes.at(i); - if (i < projectors.size()) { - setOutputResolution(projectors.at(i), offset, 0, size, dryRun); - } - if (i < screens.size()) { - setOutputResolution(screens.at(i), offset, 0, size, dryRun); - } - offset += size.width(); - } - if (!dryRun) { - XUngrabServer(_display); - XSync(_display, False); - } - updateScreenResources(); // Re-Read - if (outputSizes.size() == 1) // One output size, at least 2 outputs in total -- clone mode - return ScreenMode::Clone; - if (outputSizes.size() == 2 && _outputMap.size() == 2) // Two outputs, two sizes -- extended - return ScreenMode::Dual; - return ScreenMode::Advanced; // Must be more than 2 outputs -> something more involved -} - -/** - * Copy first "num" modes from output to all other outputs if they - * dont have a suitable mode yet. - */ -void ScreenSetup::copyModesToAll(RROutput sourceId, int num) -{ - const XRROutputInfo *outputInfo = _outputMap[sourceId]->output; - for (int i = 0; i < num; ++i) { - if (!_modeMap.contains(outputInfo->modes[i])) { - qDebug() << "BUG: Mode" << outputInfo->modes[i] << "not found in copyModesToAll"; - continue; - } - const XRRModeInfo *mode = _modeMap[outputInfo->modes[i]]; - for (auto other : _outputMap.keys()) { - if (other == sourceId) - continue; - const XRROutputInfo *otherInfo = _outputMap[other]->output; - if (getOutputModeForResolution(otherInfo, mode->width, mode->height) != None) - continue; - XRRAddOutputMode(_display, other, outputInfo->modes[i]); - } - } -} - -/** - * Get the RRMode for the specified output matching the given resolution. - * If multiple modes match the given resolution, the function will - * return the first matching preferred mode. If no preferred - * mode matches, the non-preferred mode with the highest refresh rate - * will be returned. - * Otherwise, None will be returned. - */ -RRMode ScreenSetup::getOutputModeForResolution(const XRROutputInfo *output, unsigned int width, unsigned int height) const -{ - XRRModeInfo *retval = nullptr; - for (int i = 0; i < output->nmode; ++i) { - if (!_modeMap.contains(output->modes[i])) - continue; - XRRModeInfo *info = _modeMap[output->modes[i]]; - if (info->width == width && info->height == height) { - if (i < output->npreferred) - return output->modes[i]; - if (retval == nullptr || retval->dotClock < info->dotClock) { - retval = info; - } - } - } - return retval == nullptr ? None : retval->id; -} - -RRMode ScreenSetup::getOutputModeForResolution(const XRROutputInfo *output, const QSize &resolution) const -{ - return getOutputModeForResolution(output, static_cast(resolution.width()), static_cast(resolution.height())); -} - -RRCrtc ScreenSetup::getFreeCrtc(const XRROutputInfo* output) const -{ - for (int i = 0; i < output->ncrtc; ++i) { - RRCrtc c = output->crtcs[i]; - if (!_crtcMap.contains(c)) { - // Unknown CRTC!? Scan already known outputs - qDebug() << "BUG: Output has unknown possible CRTC"; - for (auto oi : _outputMap) { - if (oi->output->crtc == c) - goto next; - } - } else { - // Known CRTC -- see if free - if (_crtcMap[c]->noutput > 0) - goto next; - } - return c; -next:; - } - return None; -} - -//___________________________________________________________________________ -bool ScreenSetup::createMode(unsigned int resX, unsigned int resY, float refresh, QString name) -{ - QByteArray ba = name.toLocal8Bit(); - mode *mode = vert_refresh(int(resX), int(resY), refresh, 0, 0, 0); - if (mode == nullptr) - return false; - XRRModeInfo m; - m.width = static_cast(mode->hr); - m.height = static_cast(mode->vr); - m.dotClock = static_cast(mode->pclk) * 1000ul * 1000ul; - m.hSyncStart= static_cast(mode->hss); - m.hSyncEnd = static_cast(mode->hse); - m.hTotal = static_cast(mode->hfl); - m.hSkew = 0; - m.vSyncStart= static_cast(mode->vss); - m.vSyncEnd = static_cast(mode->vse); - m.vTotal = static_cast(mode->vfl); - m.id = 0; - m.name = ba.data(); - m.nameLength = static_cast(ba.length()); - free(mode); - - for (XRRModeInfo *mode : _modeMap) { - if (mode->width == m.width && mode->height == m.height && mode->dotClock == m.dotClock) - return true; // Already exists, return true? - } - - RRMode xid = XRRCreateMode(_display, DefaultRootWindow(_display), &m); - qDebug() << "Return value of create was" << xid; - // Immediately add to all screens - for (OutputInfo *info : _outputMap) { - XRRAddOutputMode(_display, info->id, xid); - } - return true; -} - -//___________________________________________________________________________ -ScreenSetup::~ScreenSetup() -{ - freeResources(); - XCloseDisplay(_display); -} - -void ScreenSetup::setScreenSize(const QSize &size) -{ - XRRSetScreenSize(_display, DefaultRootWindow(_display), - size.width(), size.height(), - int(25.4 * size.width() / 96.0), // standard dpi that X uses - int(25.4 * size.height() / 96.0)); // standard dpi that X uses -} - -void ScreenSetup::disconnectAllCrtcs() -{ - // Disconnect everything - for (auto crtc : _crtcMap.keys()) { - XRRSetCrtcConfig(_display, _screenResources, crtc, CurrentTime, - 0, 0, None, RR_Rotate_0, nullptr, 0); - } -} - -void ScreenSetup::freeCrtcBackup() -{ - for (auto entry : _crtcBackup) { - free(entry->outputs); - free(entry); - } - _crtcBackup.clear(); - qDebug() << "CRTC freed"; -} - -void ScreenSetup::createCrtcBackup() -{ - freeCrtcBackup(); - for (CrtcMap::iterator it = _crtcMap.begin(); it != _crtcMap.end(); ++it) { - const auto src = it.value(); - XRRCrtcInfo *copy = static_cast(calloc(1, sizeof(XRRCrtcInfo))); - copy->outputs = static_cast(calloc(size_t(src->noutput), sizeof(RROutput))); - copy->x = src->x; - copy->y = src->y; - copy->mode = src->mode; - copy->rotation = src->rotation; - copy->noutput = src->noutput; - for (int i = 0; i < src->noutput; ++i) { - copy->outputs[i] = src->outputs[i]; - } - _crtcBackup[it.key()] = copy; - } - qDebug() << "Created CRTC backup with entries:" << _crtcBackup.size(); -} - -bool ScreenSetup::setClone(const QSize &resolution) -{ - createCrtcBackup(); - XGrabServer(_display); - disconnectAllCrtcs(); - setScreenSize(resolution); - bool ok = false; - for (auto oi : _outputMap) { - ok = setOutputResolution(oi, 0, 0, resolution) || ok; - } - XUngrabServer(_display); - XSync(_display, False); - return ok; -} - -bool ScreenSetup::setCustom(const QList>> &list) -{ - QList sizes; - for (auto e : list) { - if (e.second.isEmpty()) - continue; - sizes.append(e.first); - } - if (sizes.isEmpty()) - return false; - createCrtcBackup(); - auto screenSize = getTotalSizeHorz(sizes); - if (screenSize.isEmpty()) - return false; - XGrabServer(_display); - disconnectAllCrtcs(); - setScreenSize(screenSize); - int x = 0; - bool ok = false; - for (auto e : list) { - if (e.second.isEmpty()) - continue; - const QSize &res = e.first; - for (auto outputName : e.second) { - for (auto oi : _outputMap) { - if (oi->outputName != outputName) - continue; - ok = setOutputResolution(oi, x, 0, res) || ok; - } - } - x += res.width(); - } - XUngrabServer(_display); - XSync(_display, False); - return ok; -} - -void ScreenSetup::revertChanges() -{ - if (_crtcBackup.isEmpty()) - return; - qDebug() << "Starting revert"; - XGrabServer(_display); - disconnectAllCrtcs(); - QSize screenSize; - for (auto e : _crtcBackup) { - if (e->mode == None || !_modeMap.contains(e->mode)) - continue; - screenSize = screenSize.expandedTo(QSize(e->x + int(_modeMap[e->mode]->width), e->y + int(_modeMap[e->mode]->height))); - } - for (CrtcMap::iterator it = _crtcBackup.begin(); it != _crtcBackup.end(); ++it) { - auto e = it.value(); - if (e->mode == None) - continue; - XRRSetCrtcConfig(_display, _screenResources, it.key(), CurrentTime, - e->x, e->y, e->mode, e->rotation, e->outputs, e->noutput); - } - XUngrabServer(_display); - XSync(_display, False); -} - -static bool modeBiggerThan(const QSize &a, const QSize &b) -{ - if (a.width() > b.width()) - return true; - return a.width() == b.width() && a.height() > b.height(); -} - -ResolutionVector ScreenSetup::getCommonModes() const -{ - QHash, QSet> matches; - for (auto oi : _outputMap) { - for (int i = 0; i < oi->output->nmode; ++i) { - if (!_modeMap.contains(oi->output->modes[i])) - continue; - const auto mode = _modeMap[oi->output->modes[i]]; - const QPair pair = qMakePair(mode->width, mode->height); - matches[pair].insert(oi->id); - } - } - ResolutionVector ret; - for (auto it = matches.begin(); it != matches.end(); ++it) { - if (it.value().size() == _outputMap.size()) { - ret.append(QSize(int(it.key().first), int(it.key().second))); - } - } - qSort(ret.begin(), ret.end(), modeBiggerThan); - return ret; -} - - diff --git a/src/x.h b/src/x.h deleted file mode 100644 index b5df6e0..0000000 --- a/src/x.h +++ /dev/null @@ -1,97 +0,0 @@ -#ifndef XRANDR_H -#define XRANDR_H - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -struct OutputInfo; - -typedef QHash ModeMap; -typedef QHash CrtcMap; -typedef QHash OutputMap; -typedef QVector ResolutionVector; - -/////////////////////////////////////////////////////////////////////////// - -enum class ScreenMode -{ - Single, - Clone, - Dual, - Advanced, -}; - -class ScreenInfo -{ -public: - ScreenInfo(const OutputInfo *oi, const ModeMap &om); - int position; - QString name; - QString output; - QSize currentResolution; - QSize preferredResolution; - bool isProjector; - ResolutionVector modes; -}; - -class ScreenSetup -{ -public: - void updateScreenResources(); - void initModes(); - XRRModeInfo* getPreferredMode(OutputInfo *oi) const; - QList getTotalSize(const QList &projectors, const QList &screens) const; - ScreenMode getCurrentMode(); - ScreenMode setDefaultMode(bool dryRun = false); - void copyModesToAll(RROutput id, int num); - bool createMode(unsigned int resX, unsigned int resY, float refresh, QString name); - void revertChanges(); - bool setClone(const QSize &resolution); - bool setCustom(const QList>> &list); - ResolutionVector getCommonModes() const; - int getOutputCount() const { return _outputMap.size(); } - QMap getScreenPositions() const; - const ResolutionVector &getVirtualResolutions() const { return _resolutions; } - - // Singleton - inline static ScreenSetup* inst() { - if (_instance == nullptr) _instance = new ScreenSetup(); - return _instance; - } - -private: - ScreenSetup(); - ~ScreenSetup(); - - void freeResources(); - bool readEdid(OutputInfo* output); - void createCrtcBackup(); - void freeCrtcBackup(); - void disconnectAllCrtcs(); - void setScreenSize(const QSize &size); - RRMode getOutputModeForResolution(const XRROutputInfo *output, unsigned int width, unsigned int height) const; - RRMode getOutputModeForResolution(const XRROutputInfo *output, const QSize &resolution) const; - RRCrtc getFreeCrtc(const XRROutputInfo* output) const; - bool setOutputResolution(OutputInfo *oi, int x, int y, const QSize &size, bool dryRun = false); - - static ScreenSetup * _instance; - Display* _display; - Atom _EDID_ATOM; - XRRScreenResources* _screenResources; - ModeMap _modeMap; - CrtcMap _crtcMap; - CrtcMap _crtcBackup; - OutputMap _outputMap; - ResolutionVector _resolutions; -}; - -/////////////////////////////////////////////////////////////////////////// - -#endif // XRANDR_H diff --git a/src/xprivate.cpp b/src/xprivate.cpp new file mode 100644 index 0000000..120fb66 --- /dev/null +++ b/src/xprivate.cpp @@ -0,0 +1,452 @@ +#include "xprivate.h" + +#include + +/** + * Check list of known model names that falsely report a screen size or similar + */ +static QStringList initProjectorList() +{ + QStringList list; + list << "AT-HDVS-RX"; // Switchbox + // TODO: Load from file + return list; +} + +static bool isProjectorName(const QString &name) +{ + static QStringList projectors = initProjectorList(); + return projectors.contains(name); +} + +static bool xRectLessThan(const OutputInfo* const &a, const OutputInfo* const &b) +{ + return a->crtc->x < b->crtc->x; +} + +/* + * Class members + */ + +XPrivate::XPrivate() + : _screenResources(nullptr) +{ + // Get initial data (to be freed) + _display = XOpenDisplay(nullptr); + if (_display == nullptr) { + qFatal("Cannot open display"); + ::exit(1); + } + _EDID_ATOM = XInternAtom(_display, RR_PROPERTY_RANDR_EDID, False); +} + +XPrivate::~XPrivate() +{ + XCloseDisplay(_display); +} + +void XPrivate::freeResources() +{ + freeCrtcBackup(); + // Clear the modemap (nothing to be freed, stored in screenResources) + _modeMap.clear(); + _resolutions.clear(); + + // Clear output info + for (OutputInfo *output : _outputMap.values()) { + delete output; + } + _outputMap.clear(); + + // Clear crtc info + for (XRRCrtcInfo *info : _crtcMap.values()) { + XRRFreeCrtcInfo(info); + } + _crtcMap.clear(); + + // Release resources + if (_screenResources != nullptr) { + XRRFreeScreenResources(_screenResources); + _screenResources = nullptr; + } + XRRFreeScreenResources(_screenResources); +} + +void XPrivate::updateScreenResources() +{ + freeResources(); + _screenResources = XRRGetScreenResourcesCurrent(_display, DefaultRootWindow(_display)); + // Create the modemap + qDebug() << "Modes"; + for (int i = 0; i < _screenResources->nmode; ++i) { + _modeMap.insert( + _screenResources->modes[i].id, + &_screenResources->modes[i]); + qDebug() << _screenResources->modes[i].id << "\t" + << _screenResources->modes[i].width << "x" + << _screenResources->modes[i].height; + } + + /* + int num = 0; + XRRMonitorInfo *mi = XRRGetMonitors(_display, DefaultRootWindow(_display), True, &num); + for (int i = 0; i < num; ++i) { + char *name = XGetAtomName(_display, mi [i].name); + if (name == nullptr) + continue; + qDebug() << "some monitor with name" << name << mi[i].primary; + XFree(name); + } + */ + + // Create crtcMap + qDebug() << "CRTCs"; + for (int i = 0; i < _screenResources->ncrtc; ++i) { + XRRCrtcInfo * info = XRRGetCrtcInfo( + _display, + _screenResources, + _screenResources->crtcs[i]); + if (info == nullptr) { + qDebug() << "Error getting CRTC info for crtc" << i; + continue; + } + qDebug() << _screenResources->crtcs[i] << "-- Outputs:" << info->noutput << "Possible:" << info->npossible; + _crtcMap.insert(_screenResources->crtcs[i], info); + } + + // Create outputmap and connectedOutputMap + qDebug() << "Outputs"; + QHash tempMap; + QMap typeCount; + for (int i = 0; i < _screenResources->noutput; ++i) { + XRROutputInfo* info = XRRGetOutputInfo( + _display, + _screenResources, + _screenResources->outputs[i]); + if (info == nullptr) { + qDebug() << "Error getting info for output" << i; + continue; + } + const QString outputName = QString::fromLocal8Bit(info->name, info->nameLen); + tempMap.insert(_screenResources->outputs[i], outputName); + if (info->connection == RR_Disconnected) { + qDebug() << "Ignoring disconnected output" << outputName; + XRRFreeOutputInfo(info); + continue; + } + bool disconnected = false; + if (info->crtc == None) { + disconnected = true; + qDebug() << "Connected output" << outputName << "has no CRTC -- trying to find free one"; + info->crtc = getFreeCrtc(info); + } + if (!_crtcMap.contains(info->crtc)) { + qDebug() << "Have output" << outputName << "with no known crtc"; + XRRFreeOutputInfo(info); + continue; + } + XRRCrtcInfo *crtc = _crtcMap.value(info->crtc); + XRRModeInfo *mode = nullptr; + if (!disconnected) { + if (!_modeMap.contains(crtc->mode)) { + qDebug() << "Have output" << outputName << " with crtc with no known mode -- offline?"; + } else { + mode = _modeMap.value(crtc->mode); + } + } + OutputInfo *oi = new OutputInfo(_screenResources->outputs[i], info, crtc, mode); + oi->outputName = outputName; + if (!this->readEdid(oi)) { // We have no EDID - take it as an indicator that there's a dumb output split/switch box + oi->outputType = Projector::Maybe; + } else if ((info->mm_height == 0 && info->mm_width == 0) || isProjectorName(oi->modelName)) { + oi->outputType = Projector::Yes; // Screens with size 0x0 are projectors by convention + } else if (info->mm_width > 500 && info->mm_height > 500) { // Big screen - probably should be handled like a projector + oi->outputType = Projector::Yes; + } else if (info->mm_width > 350 && info->mm_height > 350) { // Somewhere in-between + oi->outputType = Projector::Maybe; + } + typeCount[oi->outputType]++; + _outputMap.insert(_screenResources->outputs[i], oi); + qDebug() << "Connected" << outputName << "-- Clones:" << info->nclone << "Modes:" << info->nmode << "Crtcs:" << info->ncrtc << "Preferred:" << info->npreferred; + } + // Final checks for projector + if (typeCount[Projector::Yes] == 0) { + // No definitive projector + if (typeCount[Projector::Maybe] > 0 && typeCount[Projector::No] > 0) { + // Potential projector(s) and normal screen, promote + for (OutputInfo *info : _outputMap) { + if (info->outputType == Projector::Maybe) { + info->outputType = Projector::Yes; + break; + } + } + } + } + // Determine each screen's position + QList screens = _outputMap.values(); + // Nothing connected? + if (screens.isEmpty()) + return; + qSort(screens.begin(), screens.end(), xRectLessThan); + int endX = -0xffff; + qDebug() << "From left to right"; + for (OutputInfo* output : screens) { + if (output->mode == nullptr) { + qDebug() << "(Ignoring" << output->outputName << "since it's disconnected)"; + } + if (output->crtc->x >= endX) { + QSize res(0, 0); + if (_modeMap.contains(output->crtc->mode)) { + auto mode = _modeMap.value(output->crtc->mode); + res = QSize(int(mode->width), int(mode->height)); + } + _resolutions.append(res); + endX = -0xffff; // Reset + } + output->position = _resolutions.size() - 1; + qDebug() << "Screen (" << output->crtc->x << "," << output->crtc->y << ") @" << output->crtc->width << "x" << output->crtc->height << "as screen" << output->position; + if (output->crtc->x + int(output->crtc->width) > endX) { + endX = output->crtc->x + int(output->crtc->width); + } + } + qDebug() << "Loaded."; +} + +bool XPrivate::readEdid(OutputInfo* output) +{ + int numProps = 0; + bool found = false; + Atom* properties = XRRListOutputProperties(_display, output->id, &numProps); + for (int i = 0; i < numProps; ++i) { + if (properties[i] == _EDID_ATOM) { + found = true; + break; + } + } + XFree(properties); + if (!found) + return false; + + unsigned long nitems, bytes_after; + unsigned char *prop; + int actual_format; + Atom actual_type; + bool valid; + + XRRGetOutputProperty(_display, output->id, _EDID_ATOM, + 0, 128, False, False, + AnyPropertyType, + &actual_type, &actual_format, + &nitems, &bytes_after, &prop); + valid = (actual_format == 8 && nitems >= 128); + + if (valid) { + int idx; + for (unsigned char *byte = prop + 54; byte < prop + 126; byte += 18) { + if (byte[0] != 0 || byte[1] != 0 || byte[2] != 0) + continue; // Not a text block + if (byte[3] == 0xfc) { // Serial number + output->modelName = QString::fromLatin1(reinterpret_cast(byte) + 5, 13); // It's actually CP-437 but meh + if ((idx = output->modelName.indexOf('\r')) != -1) { + output->modelName.truncate(idx); + } + output->modelName = output->modelName.trimmed(); + qDebug() << "Display name:" << output->modelName; + } + } + } + XFree(prop); + return valid; +} + +void XPrivate::setScreenSize(const QSize &size) +{ + XRRSetScreenSize(_display, DefaultRootWindow(_display), + size.width(), size.height(), + int(25.4 * size.width() / 96.0), // standard dpi that X uses + int(25.4 * size.height() / 96.0)); // standard dpi that X uses +} + +void XPrivate::disconnectAllCrtcs() +{ + // Disconnect everything + for (auto crtc : _crtcMap.keys()) { + XRRSetCrtcConfig(_display, _screenResources, crtc, CurrentTime, + 0, 0, None, RR_Rotate_0, nullptr, 0); + } +} + +void XPrivate::freeCrtcBackup() +{ + for (auto entry : _crtcBackup) { + free(entry->outputs); + free(entry); + } + _crtcBackup.clear(); + qDebug() << "CRTC freed"; +} + +void XPrivate::createCrtcBackup() +{ + freeCrtcBackup(); + for (CrtcMap::iterator it = _crtcMap.begin(); it != _crtcMap.end(); ++it) { + const auto src = it.value(); + XRRCrtcInfo *copy = static_cast(calloc(1, sizeof(XRRCrtcInfo))); + copy->outputs = static_cast(calloc(size_t(src->noutput), sizeof(RROutput))); + copy->x = src->x; + copy->y = src->y; + copy->mode = src->mode; + copy->rotation = src->rotation; + copy->noutput = src->noutput; + for (int i = 0; i < src->noutput; ++i) { + copy->outputs[i] = src->outputs[i]; + } + _crtcBackup[it.key()] = copy; + } + qDebug() << "Created CRTC backup with entries:" << _crtcBackup.size(); +} + +/** + * Copy first "num" modes from output to all other outputs if they + * dont have a suitable mode yet. + */ +void XPrivate::copyModesToAll(RROutput sourceId, int num) +{ + const XRROutputInfo *outputInfo = _outputMap[sourceId]->output; + for (int i = 0; i < num; ++i) { + if (!_modeMap.contains(outputInfo->modes[i])) { + qDebug() << "BUG: Mode" << outputInfo->modes[i] << "not found in copyModesToAll"; + continue; + } + const XRRModeInfo *mode = _modeMap[outputInfo->modes[i]]; + for (auto other : _outputMap.keys()) { + if (other == sourceId) + continue; + const XRROutputInfo *otherInfo = _outputMap[other]->output; + if (getOutputModeForResolution(otherInfo, mode->width, mode->height) != None) + continue; + XRRAddOutputMode(_display, other, outputInfo->modes[i]); + } + } +} + +/** + * Get the RRMode for the specified output matching the given resolution. + * If multiple modes match the given resolution, the function will + * return the first matching preferred mode. If no preferred + * mode matches, the non-preferred mode with the highest refresh rate + * will be returned. + * Otherwise, None will be returned. + */ +RRMode XPrivate::getOutputModeForResolution(const XRROutputInfo *output, unsigned int width, unsigned int height) const +{ + XRRModeInfo *retval = nullptr; + for (int i = 0; i < output->nmode; ++i) { + if (!_modeMap.contains(output->modes[i])) + continue; + XRRModeInfo *info = _modeMap[output->modes[i]]; + if (info->width == width && info->height == height) { + if (i < output->npreferred) + return output->modes[i]; + if (retval == nullptr || retval->dotClock < info->dotClock) { + retval = info; + } + } + } + return retval == nullptr ? None : retval->id; +} + +RRMode XPrivate::getOutputModeForResolution(const XRROutputInfo *output, const QSize &resolution) const +{ + return getOutputModeForResolution(output, static_cast(resolution.width()), static_cast(resolution.height())); +} + +RRCrtc XPrivate::getFreeCrtc(const XRROutputInfo* output) const +{ + for (int i = 0; i < output->ncrtc; ++i) { + RRCrtc c = output->crtcs[i]; + if (!_crtcMap.contains(c)) { + // Unknown CRTC!? Scan already known outputs + qDebug() << "BUG: Output has unknown possible CRTC"; + for (auto oi : _outputMap) { + if (oi->output->crtc == c) + goto next; + } + } else { + // Known CRTC -- see if free + if (_crtcMap[c]->noutput > 0) + goto next; + } + return c; +next:; + } + return None; +} + +XRRModeInfo* XPrivate::getPreferredMode(OutputInfo *oi) const +{ + if (oi->output->nmode == 0) { + qDebug() << "getPreferredMode: Output" << oi->outputName << "has no modes!?"; + return nullptr; // WTF!? + } + RRMode mode; + if (oi->output->npreferred > 0) { + mode = oi->output->modes[0]; + } else { + mode = getOutputModeForResolution(oi->output, 1920, 1080); + if (mode == None) { + mode = getOutputModeForResolution(oi->output, 1280, 720); + } + if (mode == None) { + mode = getOutputModeForResolution(oi->output, 1280, 800); + } + if (mode == None) { + mode = oi->output->modes[0]; + } + } + if (!_modeMap.contains(mode)) + return nullptr; + return _modeMap[mode]; +} + +QList XPrivate::getTotalSize(const QList &projectors, const QList &screens) const +{ + const int max = qMax(screens.size(), projectors.size()); + QList modes; + for (int i = 0; i < max; ++i) { + XRRModeInfo *mode = nullptr; + if (i < projectors.size()) { + mode = getPreferredMode(projectors.at(i)); + } + if (mode == nullptr && i < screens.size()) { + mode = getPreferredMode(screens.at(i)); + } + if (mode != nullptr) { + modes.append(QSize(int(mode->width), int(mode->height))); + } + } + return modes; +} + +bool XPrivate::setOutputResolution(OutputInfo *oi, int x, int y, const QSize &size, bool dryRun) +{ + RRMode mode = getOutputModeForResolution(oi->output, size); + if (mode == None) { + qDebug() << "Cannot set" << oi->outputName << "to" << size << " since it's not supported"; + if (oi->output->nmode == 0) + return false; + qDebug() << "falling back to its default"; + mode = oi->output->modes[0]; + } + if (!dryRun) { + XRRSetCrtcConfig(_display, + _screenResources, + oi->output->crtc, + CurrentTime, + x, y, mode, + RR_Rotate_0, + &oi->id, 1); + } + qDebug() << "Set" << oi->outputName << "to" << _modeMap[mode]->width << "x" << _modeMap[mode]->height << "-- offset" << x << "/" << y; + return true; +} diff --git a/src/xprivate.h b/src/xprivate.h new file mode 100644 index 0000000..6725273 --- /dev/null +++ b/src/xprivate.h @@ -0,0 +1,70 @@ +#ifndef XPRIVATE_H +#define XPRIVATE_H + +#include "xx.h" + +#include +#include +#include + +#include +#include + +enum class Projector { + No, + Yes, + Maybe +}; + +struct OutputInfo { + OutputInfo(RROutput id, XRROutputInfo *output, XRRCrtcInfo *crtc, XRRModeInfo *mode) + : output(output), crtc(crtc), mode(mode), id(id), position(-1), outputType(Projector::No) {} + ~OutputInfo() { + XRRFreeOutputInfo(output); + } + XRROutputInfo* output; + XRRCrtcInfo* crtc; + XRRModeInfo* mode; + RROutput id; + QString modelName; + QString outputName; + int position; + Projector outputType; +}; + +typedef QHash ModeMap; +typedef QHash CrtcMap; +typedef QHash OutputMap; + +class XPrivate +{ +public: + explicit XPrivate(); + virtual ~XPrivate(); + + void freeResources(); + void updateScreenResources(); + bool readEdid(OutputInfo* output); + void createCrtcBackup(); + void freeCrtcBackup(); + void disconnectAllCrtcs(); + XRRModeInfo* getPreferredMode(OutputInfo *oi) const; + void setScreenSize(const QSize &size); + RRMode getOutputModeForResolution(const XRROutputInfo *output, unsigned int width, unsigned int height) const; + RRMode getOutputModeForResolution(const XRROutputInfo *output, const QSize &resolution) const; + RRCrtc getFreeCrtc(const XRROutputInfo* output) const; + bool setOutputResolution(OutputInfo *oi, int x, int y, const QSize &size, bool dryRun = false); + QList getTotalSize(const QList &projectors, const QList &screens) const; + void copyModesToAll(RROutput id, int num); + + Display* _display; + Atom _EDID_ATOM; + XRRScreenResources* _screenResources; + ModeMap _modeMap; + CrtcMap _crtcMap; + CrtcMap _crtcBackup; + OutputMap _outputMap; + ResolutionVector _resolutions; +}; + +#endif // XPRIVATE_H diff --git a/src/xx.cpp b/src/xx.cpp new file mode 100644 index 0000000..2a8cdc5 --- /dev/null +++ b/src/xx.cpp @@ -0,0 +1,348 @@ +#include "xx.h" +#include "xprivate.h" +#include "cvt.h" +#include + + +/////////////////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////// + + +static ScreenInfo initScreenInfo(const OutputInfo *oi, const ModeMap &om) +{ + ScreenInfo si; + si.position = oi->position; + si.name = oi->modelName; + si.output = oi->outputName; + si.isProjector = (oi->outputType == Projector::Yes); + if (oi->mode != nullptr) { + si.currentResolution = QSize(QSize(int(oi->mode->width), int(oi->mode->height))); + } + for (int i = 0; i < oi->output->nmode; ++i) { + if (om.contains(oi->output->modes[i])) { + auto m = om.value(oi->output->modes[i]); + const QSize size(int(m->width), int(m->height)); + if (!si.modes.contains(size)) { + si.modes.append(size); + } + if (i == 0 && oi->output->npreferred > 0) { + si.preferredResolution = size; + } + } + } + return si; +} + +ScreenSetup * ScreenSetup::_instance = nullptr; + +ScreenSetup::ScreenSetup() : a(new XPrivate()) +{ + /* Get informations about Xserver */ + updateScreenResources(); + +} + +static double toVertRefresh(const XRRModeInfo *mode) +{ + if (mode->hTotal > 0 && mode->vTotal > 0) + return (double(mode->dotClock) / (double(mode->hTotal) * double(mode->vTotal))); + return 0; +} + +//___________________________________________________________________________ +void ScreenSetup::updateScreenResources() +{ + a->updateScreenResources(); +} + +QMap ScreenSetup::getScreenPositions() const +{ + QMap ret; + for (auto oi : a->_outputMap) { + ret.insert(oi->outputName, initScreenInfo(oi, a->_modeMap)); + } + return ret; +} + +/** + * Create common modes and add them to all outputs. + * Make sure every output has some variant of the most commonly + * used modes. + */ +void ScreenSetup::initModes() +{ + // First copy typical resolutions to all outputs +#define RES(x,y) (((y) << 16) | (x)) + QSet wanted; + wanted << RES(1280, 720) << RES(1280, 800) << RES(1920, 1080); + for (XRRModeInfo *mode : a->_modeMap) { + if (toVertRefresh(mode) < 58 || toVertRefresh(mode) > 61) + continue; // Play it safe and consider only those for copying that are 60Hz + wanted.remove(RES(mode->width, mode->height)); + // Make sure all outputs got it + for (OutputInfo *info : a->_outputMap) { + if (a->getOutputModeForResolution(info->output, mode->width, mode->height)) + continue; + XRRAddOutputMode(a->_display, info->id, mode->id); + } + } +#undef RES + // Create those that no output supported + for (auto res : wanted) { + unsigned int x = res & 0xffff; + unsigned int y = res >> 16; + createMode(x, y, 60, QString::asprintf("%ux%u", x, y)); + } + if (!wanted.isEmpty()) { + updateScreenResources(); + } + // Finally copy all those the projector supports to other outputs + for (auto key : a->_outputMap.keys()) { + OutputInfo *oi = a->_outputMap[key]; + if (oi->outputType == Projector::Yes) { + a->copyModesToAll(key, oi->output->nmode); + } + } + updateScreenResources(); +} + +static QSize getTotalSizeHorz(const QList &list) +{ + QSize ret(0, 0); + for (auto e : list) { + ret.rwidth() += e.width(); + ret.rheight() = qMax(ret.height(), e.height()); + } + return ret; +} + +ScreenMode ScreenSetup::getCurrentMode() +{ + bool notAtOrigin = false; + for (auto oi : a->_outputMap) { + if (oi->mode != nullptr) { + if (oi->crtc->x != 0 || oi->crtc->y != 0) { + notAtOrigin = true; + } + } + } + if (a->_outputMap.size() == 1) + return ScreenMode::Single; + if (a->_outputMap.size() > 2) + return ScreenMode::Advanced; + if (notAtOrigin) + return ScreenMode::Dual; + return ScreenMode::Clone; +} + +ScreenMode ScreenSetup::setDefaultMode(bool dryRun) +{ + if (a->_outputMap.size() == 1) // Only one output exists, do nothing + return ScreenMode::Single; + QMap screenMap; + QMap projectorMap; + for (auto o : a->_outputMap) { + qDebug() << o->outputName << quint32(o->outputType); + if (o->outputType == Projector::Yes) { + projectorMap.insert(o->outputName, o); + } else { + screenMap.insert(o->outputName, o); + } + } + auto projectors = projectorMap.values(); + auto screens = screenMap.values(); + qDebug() << projectors.size() << "projectors," << screens.size() << "screens."; + QList outputSizes = a->getTotalSize(projectors, screens); + if (outputSizes.isEmpty()) + return ScreenMode::Advanced; // Dunno lol + QSize screenSize = getTotalSizeHorz(outputSizes); + if (!dryRun) { + XGrabServer(a->_display); + a->disconnectAllCrtcs(); + // Set new screen size + a->setScreenSize(screenSize); + } + + qDebug() << "Virtual screen size:" << screenSize << "with" << outputSizes.size() << "different screens."; + + int offset = 0; + for (int i = 0; i < outputSizes.size(); ++i) { + const QSize &size = outputSizes.at(i); + if (i < projectors.size()) { + a->setOutputResolution(projectors.at(i), offset, 0, size, dryRun); + } + if (i < screens.size()) { + a->setOutputResolution(screens.at(i), offset, 0, size, dryRun); + } + offset += size.width(); + } + if (!dryRun) { + XUngrabServer(a->_display); + XSync(a->_display, False); + } + updateScreenResources(); // Re-Read + if (outputSizes.size() == 1) // One output size, at least 2 outputs in total -- clone mode + return ScreenMode::Clone; + if (outputSizes.size() == 2 && a->_outputMap.size() == 2) // Two outputs, two sizes -- extended + return ScreenMode::Dual; + return ScreenMode::Advanced; // Must be more than 2 outputs -> something more involved +} + +//___________________________________________________________________________ +bool ScreenSetup::createMode(unsigned int resX, unsigned int resY, float refresh, QString name) +{ + QByteArray ba = name.toLocal8Bit(); + mode *mode = vert_refresh(int(resX), int(resY), refresh, 0, 0, 0); + if (mode == nullptr) + return false; + XRRModeInfo m; + m.width = static_cast(mode->hr); + m.height = static_cast(mode->vr); + m.dotClock = static_cast(mode->pclk) * 1000ul * 1000ul; + m.hSyncStart= static_cast(mode->hss); + m.hSyncEnd = static_cast(mode->hse); + m.hTotal = static_cast(mode->hfl); + m.hSkew = 0; + m.vSyncStart= static_cast(mode->vss); + m.vSyncEnd = static_cast(mode->vse); + m.vTotal = static_cast(mode->vfl); + m.id = 0; + m.name = ba.data(); + m.nameLength = static_cast(ba.length()); + free(mode); + + for (XRRModeInfo *mode : a->_modeMap) { + if (mode->width == m.width && mode->height == m.height && mode->dotClock == m.dotClock) + return true; // Already exists, return true? + } + + RRMode xid = XRRCreateMode(a->_display, DefaultRootWindow(a->_display), &m); + qDebug() << "Return value of create was" << xid; + // Immediately add to all screens + for (OutputInfo *info : a->_outputMap) { + XRRAddOutputMode(a->_display, info->id, xid); + } + return true; +} + +//___________________________________________________________________________ +ScreenSetup::~ScreenSetup() +{ + delete a; +} + +bool ScreenSetup::setClone(const QSize &resolution) +{ + a->createCrtcBackup(); + XGrabServer(a->_display); + a->disconnectAllCrtcs(); + a->setScreenSize(resolution); + bool ok = false; + for (auto oi : a->_outputMap) { + ok = a->setOutputResolution(oi, 0, 0, resolution) || ok; + } + XUngrabServer(a->_display); + XSync(a->_display, False); + return ok; +} + +bool ScreenSetup::setCustom(const QList>> &list) +{ + QList sizes; + for (auto e : list) { + if (e.second.isEmpty()) + continue; + sizes.append(e.first); + } + if (sizes.isEmpty()) + return false; + a->createCrtcBackup(); + auto screenSize = getTotalSizeHorz(sizes); + if (screenSize.isEmpty()) + return false; + XGrabServer(a->_display); + a->disconnectAllCrtcs(); + a->setScreenSize(screenSize); + int x = 0; + bool ok = false; + for (auto e : list) { + if (e.second.isEmpty()) + continue; + const QSize &res = e.first; + for (auto outputName : e.second) { + for (auto oi : a->_outputMap) { + if (oi->outputName != outputName) + continue; + ok = a->setOutputResolution(oi, x, 0, res) || ok; + } + } + x += res.width(); + } + XUngrabServer(a->_display); + XSync(a->_display, False); + return ok; +} + +void ScreenSetup::revertChanges() +{ + if (a->_crtcBackup.isEmpty()) + return; + qDebug() << "Starting revert"; + XGrabServer(a->_display); + a->disconnectAllCrtcs(); + QSize screenSize; + for (auto e : a->_crtcBackup) { + if (e->mode == None || !a->_modeMap.contains(e->mode)) + continue; + screenSize = screenSize.expandedTo(QSize(e->x + int(a->_modeMap[e->mode]->width), e->y + int(a->_modeMap[e->mode]->height))); + } + for (CrtcMap::iterator it = a->_crtcBackup.begin(); it != a->_crtcBackup.end(); ++it) { + auto e = it.value(); + if (e->mode == None) + continue; + XRRSetCrtcConfig(a->_display, a->_screenResources, it.key(), CurrentTime, + e->x, e->y, e->mode, e->rotation, e->outputs, e->noutput); + } + XUngrabServer(a->_display); + XSync(a->_display, False); +} + +static bool modeBiggerThan(const QSize &a, const QSize &b) +{ + if (a.width() > b.width()) + return true; + return a.width() == b.width() && a.height() > b.height(); +} + +ResolutionVector ScreenSetup::getCommonModes() const +{ + QHash, QSet> matches; + for (auto oi : a->_outputMap) { + for (int i = 0; i < oi->output->nmode; ++i) { + if (!a->_modeMap.contains(oi->output->modes[i])) + continue; + const auto mode = a->_modeMap[oi->output->modes[i]]; + const QPair pair = qMakePair(mode->width, mode->height); + matches[pair].insert(oi->id); + } + } + ResolutionVector ret; + for (auto it = matches.begin(); it != matches.end(); ++it) { + if (it.value().size() == a->_outputMap.size()) { + ret.append(QSize(int(it.key().first), int(it.key().second))); + } + } + qSort(ret.begin(), ret.end(), modeBiggerThan); + return ret; +} + +int ScreenSetup::getOutputCount() const +{ + return a->_outputMap.size(); +} + +const ResolutionVector &ScreenSetup::getVirtualResolutions() const +{ + return a->_resolutions; +} diff --git a/src/xx.h b/src/xx.h new file mode 100644 index 0000000..a17d4b7 --- /dev/null +++ b/src/xx.h @@ -0,0 +1,66 @@ +#ifndef XRANDR_H +#define XRANDR_H + +#include +#include +#include + +struct ScreenInfo; +class XPrivate; + +typedef QVector ResolutionVector; + +/////////////////////////////////////////////////////////////////////////// + +enum class ScreenMode +{ + Single, + Clone, + Dual, + Advanced, +}; + +struct ScreenInfo +{ + int position; + QString name; + QString output; + QSize currentResolution; + QSize preferredResolution; + bool isProjector; + ResolutionVector modes; +}; + +class ScreenSetup +{ +public: + void updateScreenResources(); + void initModes(); + ScreenMode getCurrentMode(); + ScreenMode setDefaultMode(bool dryRun = false); + bool createMode(unsigned int resX, unsigned int resY, float refresh, QString name); + void revertChanges(); + bool setClone(const QSize &resolution); + bool setCustom(const QList>> &list); + ResolutionVector getCommonModes() const; + int getOutputCount() const; + QMap getScreenPositions() const; + const ResolutionVector &getVirtualResolutions() const; + + // Singleton + inline static ScreenSetup* inst() { + if (_instance == nullptr) _instance = new ScreenSetup(); + return _instance; + } + +private: + ScreenSetup(); + ~ScreenSetup(); + + static ScreenSetup * _instance; + XPrivate *a; +}; + +/////////////////////////////////////////////////////////////////////////// + +#endif // XRANDR_H -- cgit v1.2.3-55-g7522