#include "widget.h"
#include "main.h"
#include "xx.h"
#include "bus.h"
#include "ui_widget.h"
#include "timeoutdialog.h"
#include <QDebug>
#include <QtWidgets/QAction>
#include <QAbstractItemView>
#include <QScreen>
#include <QThread>
#include <QMessageBox>
#include <QResizeEvent>
/*
* 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<int>::of(&QComboBox::currentIndexChanged), [=](int index) {
combo->setFont(qvariant_cast<QFont>(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) :
QDialog(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);
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<QComboBox*>();
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;
ScreenMode mode;
auto ret = ScreenSetup::inst()->setDefaultMode(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) {
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();
if (!CommandLine::backgroundMode()) {
qApp->quit();
}
}
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<QFont>(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<QComboBox*>(QObject::sender());
if (cbo != nullptr) {
cbo->setFont(qvariant_cast<QFont>(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(true);
auto screenList = screenMap.values();
qSort(screenList.begin(), screenList.end(), dualHeadLess);
// Clone
// Determine preferred resolution for cloning: If no screen is detected as projector,
// use the smallest prefered resolution across all screens
// Otherwise, use the prefered resolution of the first projector we encounter
QSize preferredClone;
for (const 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;
}
}
if (screenList.empty()) {
fillCombo(_ui->cboCloneResolution, modes, QSize(), preferredClone);
} else {
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<QComboBox*>({_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<QLabel*>();
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<int>::of(&QComboBox::currentIndexChanged), [a, this](int index) {
if (!_ignoreResolutionChange) {
a->desiredResolution = a->cboResolution->itemData(index).toSize();
}
});
addBoldListener(a->cboResolution);
connect(a->cboResolution, QOverload<int>::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<int>::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<QSize> 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<TimeOutDialog*> 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<QSize, QList<QString>> left = { _ui->cboDualLeft->currentData().toSize(), { _ui->cboDualLeft->property("output").toString() } };
QPair<QSize, QList<QString>> 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<QPair<QSize, QList<QString>>> list;
for (auto e : _advancedScreens) {
list.append({ e->cboResolution->currentData().toSize(), QList<QString>() });
}
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);
}
}
////////////////////////////////////////////////////////////////////////////////