#include "widget.h" #include "main.h" #include "xx.h" #include "bus.h" #include "ui_widget.h" #include "timeoutdialog.h" #include #include #include #include #include #include /* * Helper and static stuff */ class ScreenWidget : public QWidget { public: ScreenWidget(float scale, QWidget *parent = nullptr) : QWidget(parent), scale(scale) { QWidget::setStyleSheet(".QWidget { border-radius: 3px;border: 2px solid black; }"); QWidget::setLayout(new QVBoxLayout(this)); } void setScaling(float scale) { this->scale = scale; } protected: float scale; void resizeEvent(QResizeEvent *event) override { event->accept(); float diff = (float(event->size().width()) / float(event->size().height())) - scale; if(diff > .01f) { QWidget::resize(int(float(event->size().height()) * scale), event->size().height()); } else if (diff < -.01f) { QWidget::resize(event->size().width(), int(float(event->size().width()) / scale)); } } }; class AdvancedScreen { public: ScreenWidget *screen; QComboBox *cboResolution; QSize desiredResolution; }; class AdvancedOutput { public: AdvancedOutput(const ScreenInfo& si) : info(si) {} ScreenInfo info; QLabel *assignmentLabel; QLabel *rowLabel; QComboBox *cboPosition; }; Q_DECLARE_METATYPE(AdvancedOutput*) static void addBoldListener(QComboBox *combo) { combo->connect(combo, QOverload::of(&QComboBox::currentIndexChanged), [=](int index) { combo->setFont(qvariant_cast(combo->itemData(index, Qt::FontRole))); }); } static const QString STYLE_RELOAD_BUTTON_NORMAL("padding: 2px; margin: 0 0 1px 1px;"); static const QString STYLE_RELOAD_BUTTON_HOT(STYLE_RELOAD_BUTTON_NORMAL + "background-color: #f99;"); /* * Main widget */ Widget::Widget(QWidget *parent) : QWidget(parent), _ui(new Ui::Widget), _popupCount(0), _iProjector(QIcon(":projector")), _iScreen(QIcon(":screen")), _addEventDbus(false), _addEventOther(false) { _ui->setupUi(this); // Add refresh button _ui->btnReload->setStyleSheet(STYLE_RELOAD_BUTTON_NORMAL); _ui->tabWidget->setCornerWidget(_ui->btnReload); connect(_ui->btnReload, &QPushButton::clicked, [=](bool) { _ui->btnReload->setStyleSheet(STYLE_RELOAD_BUTTON_NORMAL); ScreenSetup::inst()->initModes(); initControls(); }); setWindowFlags(Qt::Tool | Qt::WindowStaysOnTopHint); QTimer *top = new QTimer(this); connect(top, &QTimer::timeout, [=]() { if (this->isHidden()) return; // Move window to current screen if (qApp->mouseButtons() == Qt::NoButton) { updateWindowPlacement(); } // Raise window if appropriate if (_popupCount == 0) { bool ok = true; auto combos = this->findChildren(); for (auto combo : combos) { if (combo->view()->isVisible()) { ok = false; break; } } if (ok) { raise(); } } }); top->start(1000); addBoldListener(_ui->cboCloneResolution); addBoldListener(_ui->cboDualLeft); addBoldListener(_ui->cboDualRight); connectButtons(); // Handle screens on Qt level // Timer QTimer *t = new QTimer(this); t->setSingleShot(true); // Timeout after screen setup changed -- handle connect(t, &QTimer::timeout, [=]() { // Query how many screens there are now int currentCount = ScreenSetup::inst()->queryCurrentOutputCount(); qDebug() << "Timeout. Dbus:" << _addEventDbus << "X:" << _addEventOther << "old count:" << _lastScreenCount << "new count:" << currentCount; if (_addEventDbus && !_addEventOther) { // Only dbus reported a change -- wait some more in case we have link flap (don't make it worse than it already is) t->start(2000); // If no other DBus event fires within 2 secs, we'll continue below next time } else if (this->isHidden()) { // GUI is currently hidden if (currentCount == _lastScreenCount) { // Nothing seems to have changed. This might happen when another tool changes the screen config. Let's not interfere. } else if (currentCount > _lastScreenCount) { // Screen count increased - auto setup _lastScreenCount = currentCount; ScreenSetup::inst()->initModes(); ScreenMode mode; auto ret = ScreenSetup::inst()->setDefaultMode(CommandLine::testMode(), mode); if (!ret.ok() || !keepResolution()) { ret.revert(); } } else { // Screen count decreased - pop up GUI and don't just deconfig the screen setWindowFlag(Qt::WindowStaysOnTopHint, false); this->show(); setWindowFlag(Qt::WindowStaysOnTopHint, true); this->raise(); this->showNormal(); } } else { // GUI currently visible -- highlight refresh button if (currentCount > _lastScreenCount) { ScreenSetup::inst()->initModes(); initControls(); } else { _ui->btnReload->setStyleSheet(STYLE_RELOAD_BUTTON_HOT); } } // Reset flags (don't use return above!) _addEventDbus = false; _addEventOther = false; }); auto popupGui = [=]() { if (this->isHidden()) { t->start(1500); } else { _ui->btnReload->setStyleSheet(STYLE_RELOAD_BUTTON_HOT); } }; for (auto scrn : QGuiApplication::screens()) { _qtScreens.append(scrn); } connect(qApp, &QGuiApplication::screenAdded, [=](const QScreen *scrn) { qDebug() << "QT SEES SCREEN" << scrn->geometry(); _qtScreens.append(scrn); if (CommandLine::backgroundMode()) { _addEventOther = true; popupGui(); } }); connect(qApp, &QGuiApplication::screenRemoved, [=](const QScreen *scrn) { qDebug() << "Qt lost screen" << scrn->geometry(); _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 { // GUI popup logic connect(Bus::inst(), &Bus::serviceConnected, [=](bool isSession) { qDebug() << "\\o/ Received DBus connect notification \\o/"; if (isSession) { // Session bus means user triggered, show immediately // Try to be very aggressive to get on top, since we don't want the // window to hang around behind the full screen VMplayer setWindowFlag(Qt::WindowStaysOnTopHint, false); this->hide(); QTimer::singleShot(100, [=]() { this->showNormal(); this->setWindowFlag(Qt::WindowStaysOnTopHint, true); this->raise(); this->show(); }); } else { // Otherwise, systembus means udev event -- take normal route popupGui(); } }); } // Xlib connect(ScreenSetup::inst(), &ScreenSetup::outputConfigChanged, [=](ConnectionEvent type) { if (type == ConnectionEvent::Disconnected) { if (!this->isHidden()) { _ui->btnReload->setStyleSheet(STYLE_RELOAD_BUTTON_HOT); } } else { _addEventOther = true; popupGui(); } }); } _lastScreenCount = ScreenSetup::inst()->queryCurrentOutputCount(); } //______________________________________________________________________________ Widget::~Widget() { } void Widget::showEvent(QShowEvent *event) { QWidget::showEvent(event); initControls(true); updateWindowPlacement(true); raise(); } void Widget::hideEvent(QHideEvent *event) { QWidget::hideEvent(event); _lastScreenCount = ScreenSetup::inst()->getOutputCount(); } static void fillCombo(QComboBox *combo, const ResolutionVector &resolutions, const QSize &preselected, const QSize &preferred = QSize()) { combo->clear(); QFont font = combo->font(); font.setBold(false); combo->setFont(font); bool foundPreselected = false; for (auto res : resolutions) { combo->addItem(QCoreApplication::tr("%1x%2").arg(res.width()).arg(res.height()), QVariant(res)); if (res == preferred) { QFont boldFont(font); boldFont.setBold(true); combo->setItemData(combo->count() - 1, boldFont, Qt::FontRole); } else { combo->setItemData(combo->count() - 1, font, Qt::FontRole); } if (res == preselected) { combo->setCurrentIndex(combo->count() - 1); foundPreselected = true; } else if (!foundPreselected && res == preferred) { combo->setCurrentIndex(combo->count() - 1); } } combo->setFont(qvariant_cast(combo->currentData(Qt::FontRole))); } static bool dualHeadLess(const ScreenInfo &a, const ScreenInfo &b) { if (a.position != -1 && a.position < b.position) return true; return a.position == b.position && a.output < b.output; } void Widget::comboBold(int index) { QComboBox *cbo = qobject_cast(QObject::sender()); if (cbo != nullptr) { cbo->setFont(qvariant_cast(cbo->itemData(index, Qt::FontRole))); } } void Widget::initControls(bool jumpToTab) { _lastScreenCount = ScreenSetup::inst()->getOutputCount(); _ui->btnReload->setStyleSheet(STYLE_RELOAD_BUTTON_NORMAL); // ScreenMode currentOpMode = ScreenSetup::inst()->getCurrentMode(); _ui->tabWidget->setTabEnabled(1, CommandLine::testMode() || ScreenSetup::inst()->getOutputCount() == 2 || currentOpMode == ScreenMode::Dual); QWidget *current = nullptr; switch (currentOpMode) { case ScreenMode::Single: case ScreenMode::Clone: current = _ui->tabClone; break; case ScreenMode::Dual: current = _ui->tabDual; break; case ScreenMode::Advanced: current = _ui->tabAdvanced; break; } if (current != nullptr) { if (jumpToTab) { _ui->tabWidget->setCurrentWidget(current); } for (int i = 0; i < _ui->tabWidget->count(); ++i) { auto w = _ui->tabWidget->widget(i); if (w == current) { _ui->tabWidget->setTabIcon(i, QIcon(":check")); } else { _ui->tabWidget->setTabIcon(i, QIcon()); } } } // if (_ui->btnDualSwap->isChecked()) { _ui->btnDualSwap->toggle(); } auto resolutions = ScreenSetup::inst()->getVirtualResolutions(); auto screenMap = ScreenSetup::inst()->getScreenPositions(); auto modes = ScreenSetup::inst()->getCommonModes(); auto screenList = screenMap.values(); qSort(screenList.begin(), screenList.end(), dualHeadLess); // Clone QSize preferredClone; for (auto screen : screenList) { if (!screen.preferredResolution.isEmpty()) { if (screen.isProjector // Projector always overrides || preferredClone.isEmpty() // Have no preferred yet || screen.preferredResolution.width() < preferredClone.width() // For normal screens, || screen.preferredResolution.height() < preferredClone.height()) { // smallest wins preferredClone = screen.preferredResolution; } if (screen.isProjector) break; } } fillCombo(_ui->cboCloneResolution, modes, screenList[0].currentResolution, preferredClone); // Dual _ui->dualContainer->takeAt(0); _ui->dualContainer->takeAt(1); _ui->dualContainer->takeAt(2); _ui->dualContainer->addLayout(_ui->dualLeft); _ui->dualContainer->addWidget(_ui->btnDualSwap); _ui->dualContainer->addLayout(_ui->dualRight); if (screenList.size() >= 2) { auto lists = QList({_ui->cboDualLeft, _ui->cboDualRight}); int j = 0; for (int i = 0; i < screenList.size() && j < 2; ++i) { QSize selected; if (currentOpMode == ScreenMode::Dual) { // When we're not in dualhead mode, pre-select the preferred solution, so in case the user wants // to switch to dualhead, they just need to switch to the "dual" tab and hit apply to get // each screen configured to its preferred resolution. selected = screenList[i].currentResolution; } fillCombo(lists[j], screenList[i].modes, selected, screenList[i].preferredResolution); lists[j]->setProperty("output", screenList[i].output); QWidget *sl = ( j == 0 ? _ui->wDualLeft : _ui->wDualRight ); const QIcon &icon = screenList[i].isProjector ? _iProjector : _iScreen; auto labels = sl->findChildren(); labels[0]->setPixmap(icon.pixmap(QSize(32, 32))); labels[1]->setText(screenList[i].output + "\n" + screenList[i].name); if (screenList[i].currentResolution.isEmpty()) continue; ++j; } } // // Clear advanced controls for (auto e : _advancedScreens) { delete e; } for (auto e : _advancedOutput) { if (e->assignmentLabel->layout() == nullptr) { e->assignmentLabel->deleteLater(); } delete e; } _advancedScreens.clear(); _advancedOutput.clear(); QLayoutItem *w; while ((w = _ui->advancedCombos->takeAt(0)) != nullptr) { if (w->widget() != nullptr) { w->widget()->deleteLater(); } delete w; } while ((w = _ui->advancedContainer->takeAt(0)) != nullptr) { if (w->widget() != nullptr) { w->widget()->deleteLater(); } delete w; } // Create new // Screens QStringList positionList(tr("(off)")); for (int i = 0; i < screenMap.size(); ++i) { QSize res; if (i < resolutions.size()) { res = resolutions[i]; } else { res = QSize(16, 9); } AdvancedScreen *a = new AdvancedScreen(); a->screen = new ScreenWidget(res.isEmpty() ? 1.33f : float(res.width()) / float(res.height())); a->desiredResolution = res; a->cboResolution = new QComboBox(); _ui->advancedContainer->addWidget(a->screen); _advancedScreens.append(a); a->screen->layout()->addWidget(a->cboResolution); a->screen->layout()->addItem(new QSpacerItem(0, 0, QSizePolicy::Minimum, QSizePolicy::Expanding)); positionList.append(QString::number(_advancedScreens.size())); connect(a->cboResolution, QOverload::of(&QComboBox::currentIndexChanged), [a, this](int index) { if (!_ignoreResolutionChange) { a->desiredResolution = a->cboResolution->itemData(index).toSize(); } }); addBoldListener(a->cboResolution); connect(a->cboResolution, QOverload::of(&QComboBox::currentIndexChanged), [a, this](int index) { if (index < 0) return; QSize res = a->cboResolution->itemData(index).toSize(); a->screen->setScaling(res.isEmpty() ? 1.33f : float(res.width()) / float(res.height())); _ui->advancedContainer->update(); }); } // Header _ui->advancedCombos->addWidget(new QLabel(tr("Output")), 0, 0, 1, 2); _ui->advancedCombos->addWidget(new QLabel(tr("Position")), 0, 3); // List int row = 1; for (auto it = screenMap.begin(); it != screenMap.end(); ++it) { int col = 0; AdvancedOutput *a = new AdvancedOutput(it.value()); a->assignmentLabel = new QLabel(it.key(), this); a->assignmentLabel->hide(); a->rowLabel = new QLabel(it.key()); auto ico = new QLabel(); const QIcon &icon = (a->info.isProjector ? _iProjector : _iScreen); auto h = a->assignmentLabel->fontMetrics().height(); ico->setPixmap(icon.pixmap(QSize(h + 3, h))); _ui->advancedCombos->addWidget(ico, row, col++); _ui->advancedCombos->addWidget(a->rowLabel, row, col++); _ui->advancedCombos->addWidget(new QLabel(a->info.name), row, col++); QComboBox *cbo = new QComboBox(); a->cboPosition = cbo; _ui->advancedCombos->addWidget(cbo, row, col++); _ui->advancedCombos->addItem(new QSpacerItem(0, 0, QSizePolicy::Expanding, QSizePolicy::Minimum), row, col++); _advancedOutput.append(a); row++; // Logic cbo->addItems(positionList); // TODO Signal connect(cbo, QOverload::of(&QComboBox::currentIndexChanged), [a, this](int index) { a->info.position = index - 1; if (index > 0) { _advancedScreens[index - 1]->screen->layout()->addWidget(a->assignmentLabel); a->assignmentLabel->show(); } else { a->assignmentLabel->hide(); } // Refill boxes _ignoreResolutionChange = true; for (int i = 0; i < _advancedScreens.size(); ++i) { QSize preferred, fallbackPreferred; // Iterate over all virtual screens AdvancedScreen *screen = _advancedScreens[i]; bool first = true; ResolutionVector common; for (auto output : _advancedOutput) { if (output->info.position != i) continue; // Output not mapped to this virtual screen if (first) { common = output->info.modes; first = false; } else { QMutableVectorIterator it(common); while (it.hasNext()) { if (!output->info.modes.contains(it.next())) { it.remove(); } } } if (!output->info.preferredResolution.isEmpty()) { QSize &p = output->info.isProjector ? preferred : fallbackPreferred; if (p.isEmpty() || !common.contains(p)) { p = output->info.preferredResolution; } } } // Set list // TODO: Highlight preferred mode if (preferred.isEmpty()) { preferred = fallbackPreferred; } qDebug() << "Preferred:" << preferred; fillCombo(screen->cboResolution, common, screen->desiredResolution, preferred); } _ignoreResolutionChange = false; }); cbo->setCurrentIndex(a->info.position + 1); } // Apply Spacing QLayoutItem *control; int i = 0; while ((control = _ui->advancedCombos->itemAt(i++)) != nullptr) { if (control->widget() != nullptr) { control->widget()->setStyleSheet(".QLabel {padding: 1px 7px;}"); } } } bool Widget::keepResolution() { _popupCount += 1; // Qt needs some time to notice the screen setup has changed QCoreApplication::processEvents(); QThread::msleep(10); QCoreApplication::processEvents(); QList list; this->setDisabled(true); for (auto screen : _qtScreens) { QRect geo = screen->geometry(); bool skip = false; // See if it overlaps with an existing screen for (auto other : _qtScreens) { if (other == screen) break; if (other->geometry().intersects(geo)) { skip = true; break; } } if (skip) continue; // Show a dialog asking if the res should be kept TimeOutDialog *keepDialog = new TimeOutDialog(15); QSize s = (geo.size() - keepDialog->size()) / 2; QPoint tl = geo.topLeft(); tl.rx() += s.width(); tl.ry() += s.height(); keepDialog->move(tl); keepDialog->show(); keepDialog->move(tl); list.append(keepDialog); } bool wasCanceled = false; bool active; do { active = true; QCoreApplication::processEvents(); for (auto win : list) { if (!win->isActive()) { active = false; wasCanceled = win->wasCanceled(); } } } while (active); for (auto win : list) { win->deleteLater(); } this->setDisabled(false); _popupCount -= 1; return wasCanceled; } //______________________________________________________________________________ void Widget::connectButtons() { // Swap dualscreen connect(_ui->btnDualSwap, &QPushButton::clicked, [=](bool) { _ui->dualContainer->addItem(_ui->dualContainer->takeAt(1)); _ui->dualContainer->addItem(_ui->dualContainer->takeAt(0)); }); // Apply CLONE connect(_ui->btnCloneApply, &QPushButton::clicked, [=](bool) { auto ret = ScreenSetup::inst()->setClone(_ui->cboCloneResolution->currentData().toSize()); if (!ret.ok() || !keepResolution()) { qDebug() << "reverting clone"; ret.revert(); } ScreenSetup::inst()->updateScreenResources(); initControls(); }); // Apply DUALHEAD connect(_ui->btnDualApply, &QPushButton::clicked, [=](bool) { QPair> left = { _ui->cboDualLeft->currentData().toSize(), { _ui->cboDualLeft->property("output").toString() } }; QPair> right = { _ui->cboDualRight->currentData().toSize(), { _ui->cboDualRight->property("output").toString() } }; if (_ui->btnDualSwap->isChecked()) { qSwap(left, right); } auto ret = ScreenSetup::inst()->setCustom({left, right}); if (!ret.ok() || !keepResolution()) { qDebug() << "reverting dualhead"; ret.revert(); } ScreenSetup::inst()->updateScreenResources(); initControls(); }); // Apply custom connect(_ui->btnAdvancedApply, &QPushButton::clicked, [=](bool) { QList>> list; for (auto e : _advancedScreens) { list.append({ e->cboResolution->currentData().toSize(), QList() }); } int numConf = 0; for (auto e : _advancedOutput) { int index = e->cboPosition->currentIndex() - 1; if (index < 0 || index >= list.size()) continue; list[index].second.append(e->info.output); numConf++; } if (numConf == 0) return; auto ret = ScreenSetup::inst()->setCustom(list); if (!ret.ok() || !keepResolution()) { qDebug() << "reverting custom"; ret.revert(); } 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); } }); } void Widget::updateWindowPlacement(bool force) { QRect win = this->geometry(); QPoint cursor = QCursor::pos(); const QScreen *winScreen = nullptr, *mouseScreen = nullptr; //qDebug() << "Mouse at" << cursor << " window at" << win; for (auto screen : _qtScreens) { QRect geo = screen->geometry(); if (geo.contains(win)) { winScreen = screen; //qDebug() << "Window on screen" << geo; } if (geo.contains(cursor)) { mouseScreen = screen; //qDebug() << "Mouse on screen" << geo; } } if (mouseScreen != nullptr && (force || mouseScreen != winScreen)) { QPoint offset = mouseScreen->geometry().topLeft(); QSize spacing = (mouseScreen->size() - this->size()) / 2; offset.rx() += spacing.width(); offset.ry() += spacing.height(); this->move(offset); } } ////////////////////////////////////////////////////////////////////////////////