Skip to content

Commit 60b0600

Browse files
authored
Merge pull request #561 from chris-a-talbot/fix-windows-position
Fix window visibility/positioning on Windows; resolves #546
2 parents 043dd54 + 8213bd9 commit 60b0600

4 files changed

Lines changed: 146 additions & 19 deletions

File tree

QtSLiM/QtSLiMAppDelegate.cpp

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@
3737
#include "QtSLiMExtras.h"
3838

3939
#include <QApplication>
40+
#include <QGuiApplication>
41+
#include <QScreen>
4042
#include <QOpenGLWidget>
4143
#include <QSurfaceFormat>
4244
#include <QMenu>
@@ -123,6 +125,18 @@ void QtSLiM_MessageHandler(QtMsgType type, const QMessageLogContext &context, co
123125
//if (msg.contains("Using QCharRef with an index"))
124126
// qDebug() << "HIT WATCH MESSAGE; SET A BREAKPOINT HERE!";
125127

128+
// Filter known benign Windows QPA spam when displays change
129+
#ifdef _WIN32
130+
{
131+
QByteArray category = context.category ? QByteArray(context.category) : QByteArray();
132+
if (category == "qwindows")
133+
{
134+
if (msg.startsWith("Unable to get device information for"))
135+
return;
136+
}
137+
}
138+
#endif
139+
126140
// useful behavior, from https://doc.qt.io/qt-5/qtglobal.html#qInstallMessageHandler
127141
{
128142
QByteArray localMsg = msg.toLocal8Bit();
@@ -224,6 +238,49 @@ QtSLiMAppDelegate::QtSLiMAppDelegate(QObject *p_parent) : QObject(p_parent)
224238

225239
// We assume we are the global instance; FIXME singleton pattern would be good
226240
qtSLiMAppDelegate = this;
241+
242+
// Ensure windows stay on-screen when displays change (added, removed, or modified)
243+
#if (QT_VERSION >= QT_VERSION_CHECK(5, 10, 0))
244+
245+
// This lambda (1) ensures that visible top-level windows remain on screen;
246+
// (2) raises the active window to the front on the appropriate screen, if appropriate.
247+
auto ensureOnScreen = [this]() {
248+
const auto topLevels = QApplication::topLevelWidgets();
249+
QWidget *active = qApp->activeWindow();
250+
for (QWidget *w : topLevels)
251+
{
252+
if (!w || !w->isWindow())
253+
continue;
254+
// Do not affect hidden or minimized windows; only adjust currently visible ones
255+
if (!w->isVisible())
256+
continue;
257+
if (w->windowState() & Qt::WindowMinimized)
258+
continue;
259+
260+
// If the window is largely off-screen after a display change, move it without forcing it to the front
261+
if (QtSLiMIsMostlyOnScreen(w))
262+
continue;
263+
264+
if (w == active)
265+
{
266+
// For the active window, fully expose it (bring to front)
267+
QtSLiMMakeWindowVisibleAndExposed(w);
268+
}
269+
else
270+
{
271+
// For other visible windows, adjust quietly without raising
272+
QtSLiMRelocateQuietly(w);
273+
}
274+
}
275+
};
276+
277+
// Connect to the screenAdded, screenRemoved, and geometryChanged signals
278+
// to ensure that all top-level windows are visible and exposed
279+
connect(app, &QGuiApplication::screenAdded, this, [ensureOnScreen](QScreen *) { ensureOnScreen(); });
280+
connect(app, &QGuiApplication::screenRemoved, this, [ensureOnScreen](QScreen *) { ensureOnScreen(); });
281+
for (QScreen *screen : QGuiApplication::screens())
282+
connect(screen, &QScreen::geometryChanged, this, [ensureOnScreen](const QRect &) { ensureOnScreen(); });
283+
#endif
227284

228285
// Cache app-wide icons
229286
slimDocumentIcon_.addFile(":/icons/DocIcon16.png");

QtSLiM/QtSLiMExtras.cpp

Lines changed: 79 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
#include <QSizePolicy>
3030
#include <QGridLayout>
3131
#include <QLabel>
32+
#include <QPointer>
3233
#include <QLineEdit>
3334
#include <QSpacerItem>
3435
#include <QVBoxLayout>
@@ -41,6 +42,8 @@
4142
#include <QApplication>
4243
#include <QDateTime>
4344
#include <QGuiApplication>
45+
#include <QScreen>
46+
#include <QWindow>
4447
#include <QDebug>
4548
#include <cmath>
4649

@@ -53,6 +56,36 @@
5356
#include "eidos_value.h"
5457

5558

59+
bool QtSLiMIsMostlyOnScreen(QWidget *window)
60+
{
61+
#if (QT_VERSION >= QT_VERSION_CHECK(5, 10, 0))
62+
QRect f = window->frameGeometry();
63+
QScreen *screen1 = QGuiApplication::screenAt(f.topLeft());
64+
QScreen *screen2 = QGuiApplication::screenAt(f.topRight());
65+
QScreen *screen3 = QGuiApplication::screenAt(f.bottomLeft());
66+
QScreen *screen4 = QGuiApplication::screenAt(f.bottomRight());
67+
QScreen *screen5 = QGuiApplication::screenAt(f.center());
68+
int cornerCount = (!!screen1) + (!!screen2) + (!!screen3) + (!!screen4);
69+
if (!screen5) cornerCount = 0;
70+
return (cornerCount >= 2);
71+
#else
72+
Q_UNUSED(window);
73+
return true;
74+
#endif
75+
}
76+
77+
void QtSLiMRelocateQuietly(QWidget *window)
78+
{
79+
#if (QT_VERSION >= QT_VERSION_CHECK(5, 10, 0))
80+
QScreen *primary = QGuiApplication::primaryScreen();
81+
QRect avail = primary ? primary->availableGeometry() : QRect(0,0,1024,768);
82+
window->resize(window->size().boundedTo(avail.size()));
83+
window->move(avail.topLeft() + QPoint(50, 50));
84+
#else
85+
Q_UNUSED(window);
86+
#endif
87+
}
88+
5689
void QtSLiMMakeWindowVisibleAndExposed(QWidget *window)
5790
{
5891
// we've had lots of problems in SLiMgui with windows not showing when the user expects them to show; this function
@@ -67,6 +100,14 @@ void QtSLiMMakeWindowVisibleAndExposed(QWidget *window)
67100
// I'm not sure how this is normally dealt with by operating systems, since I never use full-screen mode
68101
// on macOS it seems to work OK; the new window goes full-screen also, in front of the other, which
69102
// I assume is the standard behavior...? I'll wait for reported bugs on this one, I don't know. FIXME
103+
104+
// Fullscreen note: on Windows, "fullscreen" often means a borderless maximized window that can be
105+
// overlaid by another normal window that calls show/raise/activate. We intentionally do that here so
106+
// SLiMgui presents visibly on the current screen if possible, without minimizing/altering other apps.
107+
// If the window still isn't exposed (e.g., truly exclusive fullscreen), we will attempt relocation to
108+
// another monitor after a short delay; if that also fails, we flash the app icon to notify the user. -Chris
109+
110+
// Still unclear how this will behave on Linux systems, hopefuly the same as macOS?
70111

71112
// un-miniaturize the window if it is miniaturized
72113
if (window->windowState() & Qt::WindowMinimized)
@@ -77,26 +118,45 @@ void QtSLiMMakeWindowVisibleAndExposed(QWidget *window)
77118
window->raise();
78119
window->activateWindow();
79120

80-
// check the coordinates of the window and make sure it is actually visible on-screen
81-
// This requires Qt 5.10 or later
121+
// If not sufficiently visible, relocate to a safe point on the primary screen
122+
if (!QtSLiMIsMostlyOnScreen(window))
123+
QtSLiMRelocateQuietly(window);
124+
82125
#if (QT_VERSION >= QT_VERSION_CHECK(5, 10, 0))
83-
QScreen *screen1 = QGuiApplication::screenAt(window->frameGeometry().topLeft());
84-
QScreen *screen2 = QGuiApplication::screenAt(window->frameGeometry().topRight());
85-
QScreen *screen3 = QGuiApplication::screenAt(window->frameGeometry().bottomLeft());
86-
QScreen *screen4 = QGuiApplication::screenAt(window->frameGeometry().bottomRight());
87-
QScreen *screen5 = QGuiApplication::screenAt(window->frameGeometry().center());
88-
int cornerCount = (!!screen1) + (!!screen2) + (!!screen3) + (!!screen4);
89-
90-
if (!screen5)
91-
cornerCount = 0;
92-
93-
if (cornerCount >= 2) // 2 corners plus the center are visible
94-
return;
95-
96-
// we're not very visible on-screen, so move ourselves so that we are; this is obviously ungraceful
97-
// it would be nice to move the window to some concept of a "closest point to the current position
98-
// that is fully visible", but I'm not sure how to do that, for the general case; (100, 100) seems ok
99-
window->move(100, 100);
126+
// If still not actually exposed (e.g., exclusive fullscreen), re-check shortly and then try other screens
127+
if (QWindow *w = window->windowHandle())
128+
{
129+
if (!w->isExposed())
130+
{
131+
QPointer<QWidget> safeWindow(window);
132+
QTimer::singleShot(200, qApp, [safeWindow]() {
133+
if (!safeWindow)
134+
return;
135+
QWidget *win = safeWindow.data();
136+
QWindow *wh = win->windowHandle();
137+
if (!wh)
138+
return;
139+
if (wh->isExposed())
140+
return; // became exposed in the meantime; do nothing
141+
142+
QScreen *currentScreen = QGuiApplication::screenAt(win->frameGeometry().center());
143+
const QList<QScreen*> screens = QGuiApplication::screens();
144+
for (QScreen *screen : screens)
145+
{
146+
if (screen == currentScreen)
147+
continue;
148+
QRect avail = screen->availableGeometry();
149+
win->move(avail.topLeft() + QPoint(50, 50));
150+
win->raise();
151+
win->activateWindow();
152+
if (wh->isExposed())
153+
return;
154+
}
155+
// If we still are not exposed anywhere, alert the user via taskbar/dock
156+
qApp->alert(win);
157+
});
158+
}
159+
}
100160
#endif
101161
}
102162

QtSLiM/QtSLiMExtras.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,10 @@ typedef enum {
5555
kBottomRight
5656
} QtSLiM_LegendPosition;
5757

58+
// Utility helpers for window visibility/relocation
59+
bool QtSLiMIsMostlyOnScreen(QWidget *window);
60+
void QtSLiMRelocateQuietly(QWidget *window);
61+
5862
void QtSLiMMakeWindowVisibleAndExposed(QWidget *window);
5963

6064
void QtSLiMClearLayout(QLayout *layout, bool deleteWidgets = true);

QtSLiM/main.cpp

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#include "QtSLiMAppDelegate.h"
22
#include "QtSLiMWindow.h"
33
#include "QtSLiMPreferences.h"
4+
#include "QtSLiMExtras.h"
45

56
#include <QApplication>
67
#include <QCommandLineParser>
@@ -319,7 +320,12 @@ int main(int argc, char *argv[])
319320
}
320321

321322
if (mainWin)
323+
{
322324
mainWin->show();
325+
326+
// Ensures the main window is visible and exposed on startup
327+
QtSLiMMakeWindowVisibleAndExposed(mainWin);
328+
}
323329

324330
appDelegate.appDidFinishLaunching(mainWin);
325331

0 commit comments

Comments
 (0)