Skip to content

Commit 8213bd9

Browse files
U-UMROOT\chtalbotU-UMROOT\chtalbot
authored andcommitted
Fix window visibility/positioning on Windows; resolves #546
SLiMgui, upon startup, attempts to open in the same position it was last in. However, in the case of multiple screens, this may end up being off-screen (if an external monitor was removed, for example). This commit ensures that, on startup, the main window first attempts to re-open at its last location; however, if that location is off-screen, it adjusts itself to somewhere visible. It also automatically updates its position in the event the available monitors are modified (removed, adjusted, etc.). In the event an app is open in fullscreen in the space needed to open, SLiMgui will attempt to overlay that fullscreen app. If this is not possible, it will trigger an alert on the taskbar/dock, notifying the user that the app has been opened in the background.
1 parent 1b7c724 commit 8213bd9

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)