This is an automated email generated because a ref change occurred in the
git repository for project wmaker-crm.git.
The branch, master has been updated
via 1f03c13f4d3c6e35469b608cac5c6616c4ca1ee7 (commit)
via 8e8426403619d3ab4710fab7c7de9d03c7a7288b (commit)
from 4b4abf4c502bebf8fe97ec64a141d601def71017 (commit)
Those revisions listed above that are new to this repository have
not appeared on any other notification email; so we list those
revisions in full, below.
- Log -----------------------------------------------------------------
commit 1f03c13f4d3c6e35469b608cac5c6616c4ca1ee7
Author: David Maciejak <
david.m...@gmail.com>
Date: Fri, 27 Mar 2026 18:42:52 -0400
URL: <
https://repo.or.cz/wmaker-crm.git/1f03c13f4d3c6e35>
wmaker: improve support for RandR
This patch is improving the support for RandR.
It uses version 1.3 released in March 2009.
Most of the support is done in randr.c/randr.h
It is built on top of the mature Xinerama structure
but Xinerama lib is not required.
Like for Xinerama, RandR is now auto enabled if the
library is found at compiled time.
RandR support can be used in 2 modes:
A static mode (which is the default) is to define manually
your setup with external tools like xrandr or arandr,
like for example what Openbox is doing.
A dynamic mode, which is triggered on hotplug events,
like for example what GNOME is doing.
If a new monitor is detected, it will select the best mode
available and add it to the right on the existing monitors.
The mode can be switched with a new option available in WindowMaker
conf file (or via WPrefs expert panel):
HotplugMonitor = NO;
---
configure.ac | 4 +-
po/Makefile.am | 2 +
src/Makefile.am | 2 +
src/actions.c | 8 +-
src/dock.c | 37 +++
src/dock.h | 1 +
src/event.c | 24 +-
src/randr.c | 629 +++++++++++++++++++++++++++++++++++++
src/{appmenu.h => randr.h} | 22 +-
src/screen.c | 21 +-
src/screen.h | 7 +-
src/shutdown.c | 3 +
src/startup.c | 18 +-
src/window.c | 20 ++
src/window.h | 1 +
15 files changed, 769 insertions(+), 30 deletions(-)
create mode 100644 src/randr.c
copy src/{appmenu.h => randr.h} (68%)
diff --git a/
configure.ac b/
configure.ac
index 96277426634a..2063f6d2c5c0 100644
--- a/
configure.ac
+++ b/
configure.ac
@@ -657,11 +657,11 @@ dnl RandR support
dnl =============
m4_divert_push([INIT_PREPARE])dnl
AC_ARG_ENABLE([randr],
- [AS_HELP_STRING([--enable-randr], [enable RandR extension support (NOT recommended, buggy)])],
+ [AS_HELP_STRING([--enable-randr], [enable RandR extension support for multi-monitor live reconfiguration])],
[AS_CASE(["$enableval"],
[yes|no], [],
[AC_MSG_ERROR([bad value $enableval for --enable-randr]) ]) ],
- [enable_randr=no])
+ [enable_randr=auto])
m4_divert_pop([INIT_PREPARE])dnl
WM_XEXT_CHECK_XRANDR
diff --git a/po/Makefile.am b/po/Makefile.am
index 20acee0fabba..e305f99500a6 100644
--- a/po/Makefile.am
+++ b/po/Makefile.am
@@ -27,6 +27,7 @@ POTFILES = \
$(top_srcdir)/src/framewin.c \
$(top_srcdir)/src/geomview.c \
$(top_srcdir)/src/icon.c \
+ $(top_srcdir)/src/keytree.c \
$(top_srcdir)/src/main.c \
$(top_srcdir)/src/menu.c \
$(top_srcdir)/src/misc.c \
@@ -40,6 +41,7 @@ POTFILES = \
$(top_srcdir)/src/pixmap.c \
$(top_srcdir)/src/placement.c \
$(top_srcdir)/src/properties.c \
+ $(top_srcdir)/src/randr.c \
$(top_srcdir)/src/resources.c \
$(top_srcdir)/src/rootmenu.c \
$(top_srcdir)/src/screen.c \
diff --git a/src/Makefile.am b/src/Makefile.am
index 81b5113be476..ed65815859d2 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -61,6 +61,8 @@ wmaker_SOURCES = \
placement.h \
properties.c \
properties.h \
+ randr.c \
+ randr.h \
resources.c \
resources.h \
rootmenu.c \
diff --git a/src/actions.c b/src/actions.c
index d013d0a2c0fa..460c916624a4 100644
--- a/src/actions.c
+++ b/src/actions.c
@@ -4,7 +4,7 @@
*
* Copyright (c) 1997-2003 Alfredo K. Kojima
* Copyright (c) 1998-2003 Dan Pascu
- * Copyright (c) 2014-2023 Window Maker Team
+ * Copyright (c) 2014-2026 Window Maker Team
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -1608,6 +1608,9 @@ void wDeiconifyWindow(WWindow *wwin)
}
}
+ /* Relocate to an active head if the stored position is in dead space */
+ wWindowSnapToHead(wwin);
+
/* if the window is in another workspace, do it silently */
if (!netwm_hidden) {
#ifdef USE_ANIMATIONS
@@ -1876,6 +1879,9 @@ static void unhideWindow(WIcon *icon, int icon_x, int icon_y, WWindow *wwin, int
wwin->flags.hidden = 0;
+ /* Relocate to an active head if the stored position is in dead space */
+ wWindowSnapToHead(wwin);
+
#ifdef USE_ANIMATIONS
if (!wwin->screen_ptr->flags.startup && !wPreferences.no_animations && animate) {
animateResize(wwin->screen_ptr, icon_x, icon_y,
diff --git a/src/dock.c b/src/dock.c
index 45282057ca46..35d13b0b210e 100644
--- a/src/dock.c
+++ b/src/dock.c
@@ -3054,6 +3054,43 @@ void wDockSwap(WDock *dock)
wScreenUpdateUsableArea(scr);
}
+/* Snap a clip back onto a visible head after a RandR reconfiguration,
+ * preserving its relative position within that head */
+void wClipSnapToHead(WDock *clip)
+{
+ WScreen *scr = clip->screen_ptr;
+ WMRect rect, head;
+ float rel_x, rel_y;
+ int x = clip->x_pos;
+ int y = clip->y_pos;
+
+ /* Already fully on a visible head, nothing to do */
+ if (!wScreenKeepInside(scr, &x, &y, ICON_SIZE, ICON_SIZE))
+ return;
+
+ rect.pos.x = clip->x_pos;
+ rect.pos.y = clip->y_pos;
+ rect.size.width = ICON_SIZE;
+ rect.size.height = ICON_SIZE;
+
+ /* Find the nearest remaining head to the clip's old position */
+ head = wGetRectForHead(scr, wGetHeadForRect(scr, rect));
+
+ /* Compute fractional position within that head and clamp to [0..1] */
+ rel_x = (float)(clip->x_pos - head.pos.x) / (float)head.size.width;
+ rel_y = (float)(clip->y_pos - head.pos.y) / (float)head.size.height;
+
+ if (rel_x < 0.0f) rel_x = 0.0f;
+ else if (rel_x > 1.0f) rel_x = 1.0f;
+ if (rel_y < 0.0f) rel_y = 0.0f;
+ else if (rel_y > 1.0f) rel_y = 1.0f;
+
+ x = head.pos.x + (int)(rel_x * (head.size.width - ICON_SIZE));
+ y = head.pos.y + (int)(rel_y * (head.size.height - ICON_SIZE));
+
+ moveDock(clip, x, y);
+}
+
static pid_t execCommand(WAppIcon *btn, const char *command, WSavedState *state)
{
WScreen *scr = btn->icon->core->screen_ptr;
diff --git a/src/dock.h b/src/dock.h
index 2f752ca8f50e..7796e9340809 100644
--- a/src/dock.h
+++ b/src/dock.h
@@ -92,6 +92,7 @@ WAppIcon *wDockFindIconForWindow(WDock *dock, Window window);
void wDockDoAutoLaunch(WDock *dock, int workspace);
void wDockLaunchWithState(WAppIcon *btn, WSavedState *state);
void wDockSwap(WDock *dock);
+void wClipSnapToHead(WDock *clip);
#ifdef USE_DOCK_XDND
int wDockReceiveDNDDrop(WScreen *scr, XEvent *event);
diff --git a/src/event.c b/src/event.c
index a62586b72591..e5422ea461eb 100644
--- a/src/event.c
+++ b/src/event.c
@@ -3,7 +3,7 @@
* Window Maker window manager
*
* Copyright (c) 1997-2003 Alfredo K. Kojima
- * Copyright (c) 2014-2023 Window Maker Team
+ * Copyright (c) 2014-2026 Window Maker Team
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -46,7 +46,7 @@
#endif
#ifdef USE_RANDR
-#include <X11/extensions/Xrandr.h>
+#include "randr.h"
#endif
#include <X11/XKBlib.h>
@@ -271,10 +271,6 @@ void DispatchEvent(XEvent * event)
break;
case ConfigureNotify:
-#ifdef USE_RANDR
- if (event->xconfigure.window == DefaultRootWindow(dpy))
- XRRUpdateConfiguration(event);
-#endif
break;
case SelectionRequest:
@@ -618,14 +614,14 @@ static void handleExtensions(XEvent * event)
#endif /*KEEP_XKB_LOCK_STATUS */
}
#ifdef USE_RANDR
- if (w_global.xext.randr.supported && event->type == (w_global.xext.randr.event_base + RRScreenChangeNotify)) {
- /* From xrandr man page: "Clients must call back into Xlib using
- * XRRUpdateConfiguration when screen configuration change notify
- * events are generated */
- XRRUpdateConfiguration(event);
- WCHANGE_STATE(WSTATE_RESTARTING);
- Shutdown(WSRestartPreparationMode);
- Restart(NULL,True);
+ if (w_global.xext.randr.supported &&
+ (event->type == (w_global.xext.randr.event_base + RRScreenChangeNotify) ||
+ event->type == (w_global.xext.randr.event_base + RRNotify))) {
+ WScreen *randr_scr;
+
+ randr_scr = wScreenForRootWindow(event->xany.window);
+ if (randr_scr)
+ wRandRHandleNotify(randr_scr, event);
}
#endif
}
diff --git a/src/randr.c b/src/randr.c
new file mode 100644
index 000000000000..bacfa4a70e0f
--- /dev/null
+++ b/src/randr.c
@@ -0,0 +1,629 @@
+/* randr.c - RandR multi-monitor support
+ *
+ * Window Maker window manager
+ *
+ * Copyright (c) 2026 Window Maker Team
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#include "wconfig.h"
+
+#ifdef USE_RANDR
+
+#include <string.h>
+#include <X11/Xlib.h>
+
+#include "randr.h"
+#include "window.h"
+#include "framewin.h"
+#include "xinerama.h"
+#include "wmspec.h"
+#include "dock.h"
+#include "workspace.h"
+#include "actions.h"
+
+#define RANDR_MAX_OUTPUTS 32
+#define RANDR_DEBOUNCE_DELAY 100 /* milliseconds */
+
+typedef struct {
+ RROutput xid;
+ RRCrtc crtc;
+ int x, y, w, h;
+ Rotation rotation; /* current CRTC rotation */
+ Bool connected; /* RR_Connected */
+ Bool stale; /* mark-scan scratch flag */
+ char name[64];
+} WRandROutput;
+
+typedef struct {
+ WRandROutput outputs[RANDR_MAX_OUTPUTS];
+ int n_outputs;
+} WRandRState;
+
+/* Initial output scan to populate state from scratch */
+static void wRandR_Scan(WScreen *scr, WRandRState *state)
+{
+ XRRScreenResources *sr;
+ int i;
+
+ state->n_outputs = 0;
+
+ sr = XRRGetScreenResourcesCurrent(dpy, scr->root_win);
+ if (!sr)
+ return;
+
+ for (i = 0; i < sr->noutput && state->n_outputs < RANDR_MAX_OUTPUTS; i++) {
+ XRROutputInfo *oi;
+ WRandROutput *o;
+
+ oi = XRRGetOutputInfo(dpy, sr, sr->outputs[i]);
+ if (!oi)
+ continue;
+
+ o = &state->outputs[state->n_outputs++];
+ memset(o, 0, sizeof(*o));
+ o->xid = sr->outputs[i];
+ o->connected = (oi->connection == RR_Connected);
+ o->crtc = oi->crtc;
+ strncpy(o->name, oi->name, sizeof(o->name) - 1);
+
+ if (oi->crtc != None) {
+ XRRCrtcInfo *ci = XRRGetCrtcInfo(dpy, sr, oi->crtc);
+ if (ci) {
+ o->x = ci->x;
+ o->y = ci->y;
+ o->w = ci->width;
+ o->h = ci->height;
+ o->rotation = ci->rotation;
+ XRRFreeCrtcInfo(ci);
+ }
+ }
+
+ XRRFreeOutputInfo(oi);
+ }
+
+ XRRFreeScreenResources(sr);
+}
+
+/* Primary head detection */
+static int wRandR_PrimaryHeadIndex(Window root, const RROutput *xids, int count)
+{
+ RROutput primary_xid = XRRGetOutputPrimary(dpy, root);
+ int i;
+
+ if (primary_xid != None) {
+ for (i = 0; i < count; i++) {
+ if (xids[i] == primary_xid)
+ return i;
+ }
+ }
+ return 0;
+}
+
+/* Push RandR geometry into the Xinerama head layer */
+static void wRandR_ApplyToXinerama(WScreen *scr, WRandRState *state)
+{
+ WMRect new_screens[RANDR_MAX_OUTPUTS];
+ RROutput new_xids[RANDR_MAX_OUTPUTS]; /* parallel: XID of each head */
+ int count = 0;
+ int old_count;
+ int i;
+
+ for (i = 0; i < state->n_outputs && count < RANDR_MAX_OUTPUTS; i++) {
+ WRandROutput *o = &state->outputs[i];
+ int j, dup = 0;
+
+ /* Collect outputs that are truly active */
+ if (o->crtc == None || o->w == 0 || o->h == 0)
+ continue;
+
+ /* Deduplicate mirrored outputs that share the same CRTC origin */
+ for (j = 0; j < count; j++) {
+ if (new_screens[j].pos.x == o->x && new_screens[j].pos.y == o->y) {
+ dup = 1;
+ break;
+ }
+ }
+ if (dup)
+ continue;
+
+ new_screens[count].pos.x = o->x;
+ new_screens[count].pos.y = o->y;
+ new_screens[count].size.width = o->w;
+ new_screens[count].size.height = o->h;
+ new_xids[count] = o->xid;
+ count++;
+ }
+
+ /* Fallback: if every output disappeared, use the full virtual screen so
+ * we never end up with zero heads */
+ if (count == 0) {
+ new_screens[0].pos.x = 0;
+ new_screens[0].pos.y = 0;
+ new_screens[0].size.width = scr->scr_width;
+ new_screens[0].size.height = scr->scr_height;
+ new_xids[0] = None;
+ count = 1;
+ }
+
+ old_count = wXineramaHeads(scr);
+
+ /* Replace the screens array */
+ if (scr->xine_info.screens)
+ wfree(scr->xine_info.screens);
+
+ scr->xine_info.screens = wmalloc(sizeof(WMRect) * (count + 1));
+ memcpy(scr->xine_info.screens, new_screens, sizeof(WMRect) * count);
+ scr->xine_info.count = count;
+
+ scr->xine_info.primary_head = wRandR_PrimaryHeadIndex(scr->root_win, new_xids, count);
+
+ /* Refresh cached screen dimensions (updated by XRRUpdateConfiguration) */
+ scr->scr_width = WidthOfScreen(ScreenOfDisplay(dpy, scr->screen));
+ scr->scr_height = HeightOfScreen(ScreenOfDisplay(dpy, scr->screen));
+
+ /* Reallocate per-head usable area arrays if the head count changed */
+ if (old_count != count) {
+ wfree(scr->usableArea);
+ wfree(scr->totalUsableArea);
+ scr->usableArea = wmalloc(sizeof(WArea) * count);
+ scr->totalUsableArea = wmalloc(sizeof(WArea) * count);
+ }
+
+ /* Seed usable areas from new geometry */
+ for (i = 0; i < count; i++) {
+ WMRect *r = &scr->xine_info.screens[i];
+
+ scr->usableArea[i].x1 = scr->totalUsableArea[i].x1 = r->pos.x;
+ scr->usableArea[i].y1 = scr->totalUsableArea[i].y1 = r->pos.y;
+ scr->usableArea[i].x2 = scr->totalUsableArea[i].x2 = r->pos.x + r->size.width;
+ scr->usableArea[i].y2 = scr->totalUsableArea[i].y2 = r->pos.y + r->size.height;
+ }
+}
+
+/* Bring any off-screen windows in */
+static void wRandR_BringAllWindowsInside(WScreen *scr)
+{
+ WWindow *wwin;
+ int i;
+
+ for (wwin = scr->focused_window; wwin != NULL; wwin = wwin->prev) {
+ int bw, wx, wy, ww, wh;
+ int fully_inside = 0;
+
+ if (!wwin->flags.mapped || wwin->flags.hidden)
+ continue;
+
+ bw = HAS_BORDER(wwin) ? scr->frame_border_width : 0;
+ wx = wwin->frame_x - bw;
+ wy = wwin->frame_y - bw;
+ ww = (int)wwin->frame->core->width + 2 * bw;
+ wh = (int)wwin->frame->core->height + 2 * bw;
+
+ /* Skip windows already fully contained within a surviving head */
+ for (i = 0; i < wXineramaHeads(scr); i++) {
+ WMRect r = wGetRectForHead(scr, i);
+ if (wx >= r.pos.x && wy >= r.pos.y &&
+ wx + ww <= r.pos.x + (int)r.size.width &&
+ wy + wh <= r.pos.y + (int)r.size.height) {
+ fully_inside = 1;
+ break;
+ }
+ }
+
+ if (!fully_inside)
+ wWindowSnapToHead(wwin);
+ }
+}
+
+/* Auto-deactivate a physically disconnected output that still holds a CRTC
+ *
+ * When a cable is pulled or monitor turned off, the X server sets
+ * connection=RR_Disconnected but does NOT free the CRTC automatically.
+ * We must call XRRSetCrtcConfig with mode=None to release it,
+ * which causes the server to fire a fresh batch of RandR events.
+ *
+ * Returns True if at least one CRTC was freed */
+static Bool wRandR_AutoDeactivate(WScreen *scr, WRandRState *state, XRRScreenResources *sr)
+{
+ Bool deactivated = False;
+ int i;
+
+ for (i = 0; i < state->n_outputs; i++) {
+ WRandROutput *o = &state->outputs[i];
+ Status ret;
+
+ if (o->connected || o->crtc == None || o->stale)
+ continue;
+
+ /* Release the CRTC: no mode, no outputs */
+ ret = XRRSetCrtcConfig(dpy, sr, o->crtc, sr->timestamp,
+ 0, 0, None, RR_Rotate_0, NULL, 0);
+ if (ret == RRSetConfigSuccess) {
+ wwarning("RandR: released CRTC for disconnected output %s", o->name);
+ o->crtc = None;
+ o->w = o->h = 0;
+ deactivated = True;
+ } else {
+ wwarning("RandR: failed to release CRTC for output %s", o->name);
+ }
+ }
+
+ /* Compact and shrink the virtual framebuffer to the bounding box of remaining active outputs */
+ if (deactivated) {
+ int min_x = scr->scr_width;
+ int max_x = 0, max_y = 0;
+ int j;
+
+ for (i = 0; i < state->n_outputs; i++) {
+ WRandROutput *a = &state->outputs[i];
+
+ if (a->crtc == None || a->w == 0 || a->h == 0)
+ continue;
+
+ if (a->x < min_x) min_x = a->x;
+ if (a->x + a->w > max_x) max_x = a->x + a->w;
+ if (a->y + a->h > max_y) max_y = a->y + a->h;
+ }
+
+ /* Slide surviving outputs left to eliminate dead space at the origin */
+ if (min_x > 0 && max_x > 0) {
+ for (i = 0; i < state->n_outputs; i++) {
+ WRandROutput *a = &state->outputs[i];
+ XRRCrtcInfo *ci;
+
+ if (a->crtc == None || a->w == 0 || a->h == 0)
+ continue;
+
+ ci = XRRGetCrtcInfo(dpy, sr, a->crtc);
+ if (!ci)
+ continue;
+
+ XRRSetCrtcConfig(dpy, sr, a->crtc, sr->timestamp,
+ a->x - min_x, a->y,
+ ci->mode, ci->rotation,
+ ci->outputs, ci->noutput);
+
+ for (j = 0; j < state->n_outputs; j++) {
+ if (state->outputs[j].crtc == a->crtc)
+ state->outputs[j].x -= min_x;
+ }
+
+ XRRFreeCrtcInfo(ci);
+ }
+ max_x -= min_x;
+ }
+
+ /* Shrink the virtual framebuffer down to the compacted bounding box */
+ if (max_x > 0 && max_y > 0 &&
+ (max_x < scr->scr_width || max_y < scr->scr_height)) {
+ int mm_w = (int)((long)WidthMMOfScreen(ScreenOfDisplay(dpy, scr->screen))
+ * max_x / scr->scr_width);
+ int mm_h = (int)((long)HeightMMOfScreen(ScreenOfDisplay(dpy, scr->screen))
+ * max_y / scr->scr_height);
+ XRRSetScreenSize(dpy, scr->root_win, max_x, max_y, mm_w, mm_h);
+ }
+ }
+
+ return deactivated;
+}
+
+/* Auto-activate a newly connected output that has no CRTC assigned yet
+ *
+ * Mirrors "xrandr --auto": find a free CRTC, pick the preferred mode,
+ * place the new head to the right of all active outputs,
+ * expand the virtual framebuffer if needed, then call XRRSetCrtcConfig.
+ *
+ * Returns True if at least one output was activated */
+static Bool wRandR_AutoActivate(WScreen *scr, WRandRState *state, XRRScreenResources *sr)
+{
+ Bool activated = False;
+ int i, j;
+
+ for (i = 0; i < state->n_outputs; i++) {
+ WRandROutput *o = &state->outputs[i];
+ XRROutputInfo *oi;
+ RRCrtc free_crtc = None;
+ Rotation rotation = RR_Rotate_0;
+ RRMode best_mode = None;
+ int mode_w = 0, mode_h = 0;
+ int new_x = 0;
+ int new_vw, new_vh;
+ Status ret;
+
+ /* Only process newly connected outputs that have no CRTC yet */
+ if (!o->connected || o->crtc != None)
+ continue;
+
+ oi = XRRGetOutputInfo(dpy, sr, o->xid);
+ if (!oi)
+ continue;
+
+ if (oi->ncrtc == 0 || oi->nmode == 0) {
+ XRRFreeOutputInfo(oi);
+ continue;
+ }
+
+ /* Find a free CRTC that is idle and capable of driving this output */
+ for (j = 0; j < oi->ncrtc; j++) {
+ XRRCrtcInfo *ci = XRRGetCrtcInfo(dpy, sr, oi->crtcs[j]);
+ if (ci) {
+ if (ci->noutput == 0) {
+ int k;
+
+ for (k = 0; k < (int)ci->npossible; k++) {
+ if (ci->possible[k] == o->xid) {
+ free_crtc = oi->crtcs[j];
+ if (ci->rotations & RR_Rotate_0)
+ rotation = RR_Rotate_0;
+ else if (ci->rotations & RR_Rotate_90)
+ rotation = RR_Rotate_90;
+ else if (ci->rotations & RR_Rotate_180)
+ rotation = RR_Rotate_180;
+ else
+ rotation = RR_Rotate_270;
+ break;
+ }
+ }
+ }
+ XRRFreeCrtcInfo(ci);
+ }
+ if (free_crtc != None)
+ break;
+ }
+
+ if (free_crtc == None) {
+ wwarning("RandR: no free CRTC for output %s", o->name);
+ XRRFreeOutputInfo(oi);
+ continue;
+ }
+
+ /* Preferred mode is first in the list (per RandR spec) */
+ best_mode = oi->modes[0];
+ XRRFreeOutputInfo(oi);
+
+ /* Look up its pixel dimensions in sr->modes[] */
+ for (j = 0; j < sr->nmode; j++) {
+ if (sr->modes[j].id == best_mode) {
+ mode_w = (int)sr->modes[j].width;
+ mode_h = (int)sr->modes[j].height;
+ break;
+ }
+ }
+
+ if (mode_w == 0 || mode_h == 0)
+ continue;
+
+ /* Position it to the right of all currently active outputs, same as GNOME */
+ for (j = 0; j < state->n_outputs; j++) {
+ WRandROutput *a = &state->outputs[j];
+
+ if (a->crtc != None && a->w > 0) {
+ int right = a->x + a->w;
+
+ if (right > new_x)
+ new_x = right;
+ }
+ }
+
+ /* Expand virtual framebuffer if the new head exceeds current size */
+ new_vw = new_x + mode_w;
+ new_vh = mode_h > scr->scr_height ? mode_h : scr->scr_height;
+
+ if (new_vw > scr->scr_width || new_vh > scr->scr_height) {
+ int mm_w = (int)((long)WidthMMOfScreen(ScreenOfDisplay(dpy, scr->screen))
+ * new_vw / scr->scr_width);
+ int mm_h = (int)((long)HeightMMOfScreen(ScreenOfDisplay(dpy, scr->screen))
+ * new_vh / scr->scr_height);
+ XRRSetScreenSize(dpy, scr->root_win, new_vw, new_vh, mm_w, mm_h);
+ }
+
+ ret = XRRSetCrtcConfig(dpy, sr, free_crtc, sr->timestamp,
+ new_x, 0, best_mode, rotation,
+ &o->xid, 1);
+ if (ret == RRSetConfigSuccess) {
+ wwarning("RandR: auto-activated output %s at +%d+0", o->name, new_x);
+ activated = True;
+ } else {
+ wwarning("RandR: failed to auto-activate output %s", o->name);
+ }
+ }
+
+ return activated;
+}
+
+/* Synchronize RandR state with the X server */
+static void wRandR_Update(WScreen *scr)
+{
+ WRandRState *state = (WRandRState *)scr->randr_state;
+ XRRScreenResources *sr;
+ int i, j;
+
+ /* Assume all outputs removed until server confirms */
+ for (i = 0; i < state->n_outputs; i++)
+ state->outputs[i].stale = True;
+
+ /* Re-query server, update or add outputs */
+ sr = XRRGetScreenResourcesCurrent(dpy, scr->root_win);
+ if (!sr) {
+ wwarning("wRandR_Update: XRRGetScreenResourcesCurrent failed");
+ return;
+ }
+
+ for (i = 0; i < sr->noutput; i++) {
+ XRROutputInfo *oi;
+ WRandROutput *found = NULL;
+
+ oi = XRRGetOutputInfo(dpy, sr, sr->outputs[i]);
+ if (!oi)
+ continue;
+
+ /* Match by XID (stable across reconfigures) */
+ for (j = 0; j < state->n_outputs; j++) {
+ if (state->outputs[j].xid == sr->outputs[i]) {
+ found = &state->outputs[j];
+ break;
+ }
+ }
+
+ /* Append new output not seen before */
+ if (!found && state->n_outputs < RANDR_MAX_OUTPUTS) {
+ found = &state->outputs[state->n_outputs++];
+ memset(found, 0, sizeof(*found));
+ found->xid = sr->outputs[i];
+ strncpy(found->name, oi->name, sizeof(found->name) - 1);
+ }
+
+ if (found) {
+ found->stale = False;
+ found->connected = (oi->connection == RR_Connected);
+ found->crtc = oi->crtc;
+ found->w = 0;
+ found->h = 0;
+ found->rotation = RR_Rotate_0;
+
+ if (oi->crtc != None) {
+ XRRCrtcInfo *ci = XRRGetCrtcInfo(dpy, sr, oi->crtc);
+ if (ci) {
+ found->x = ci->x;
+ found->y = ci->y;
+ found->w = ci->width;
+ found->h = ci->height;
+ found->rotation = ci->rotation;
+ XRRFreeCrtcInfo(ci);
+ }
+ }
+ }
+
+ XRRFreeOutputInfo(oi);
+ }
+
+ /* When HotplugMonitor is enabled, actively manage CRTC lifecycle */
+ if (wPreferences.hotplug_monitor) {
+ Bool changed;
+
+ changed = wRandR_AutoDeactivate(scr, state, sr);
+ changed |= wRandR_AutoActivate(scr, state, sr);
+ if (changed) {
+ XRRFreeScreenResources(sr);
+ return;
+ }
+ }
+
+ XRRFreeScreenResources(sr);
+
+ /* Remove outputs not reported by server at all */
+ for (i = 0, j = 0; i < state->n_outputs; i++) {
+ if (!state->outputs[i].stale) {
+ if (j != i)
+ state->outputs[j] = state->outputs[i];
+ j++;
+ }
+ }
+ state->n_outputs = j;
+
+ /* Apply new geometry to the Xinerama head layer */
+ i = scr->xine_info.primary_head;
+ wRandR_ApplyToXinerama(scr, state);
+
+ /* Move the dock if needed */
+ if (scr->dock &&
+ ((scr->xine_info.primary_head != i && wPreferences.keep_dock_on_primary_head) ||
+ scr->dock->x_pos < 0 ||
+ scr->dock->x_pos >= scr->scr_width))
+ wDockSwap(scr->dock);
+
+ /* Snap each workspace clip back onto a visible head if it ended up
+ * outside the (now smaller or rearranged) virtual framebuffer */
+ if (!wPreferences.flags.noclip) {
+ int k;
+
+ for (k = 0; k < scr->workspace_count; k++) {
+ WDock *clip = scr->workspaces[k]->clip;
+
+ if (clip)
+ wClipSnapToHead(clip);
+ }
+ }
+
+ /* Refresh usable areas and EWMH hints */
+ wScreenUpdateUsableArea(scr);
+ wNETWMUpdateWorkarea(scr);
+
+ /* Bring any windows that ended up in dead space to an active head */
+ wRandR_BringAllWindowsInside(scr);
+
+ /* Rearrange miniaturized and appicons into the icon yard */
+ wArrangeIcons(scr, True);
+}
+
+static void wRandRDebounceTimerFired(void *data)
+{
+ WScreen *scr = (WScreen *)data;
+
+ scr->randr_debounce_timer = NULL;
+ wRandR_Update(scr);
+}
+
+/* end of local stuff */
+
+void wRandRInit(WScreen *scr)
+{
+ WRandRState *state;
+
+ if (!w_global.xext.randr.supported)
+ return;
+
+ state = wmalloc(sizeof(WRandRState));
+ memset(state, 0, sizeof(*state));
+ scr->randr_state = state;
+
+ wRandR_Scan(scr, state);
+ wRandR_ApplyToXinerama(scr, state);
+}
+
+void wRandRTeardown(WScreen *scr)
+{
+ if (!scr->randr_state)
+ return;
+
+ if (scr->randr_debounce_timer) {
+ WMDeleteTimerHandler(scr->randr_debounce_timer);
+ scr->randr_debounce_timer = NULL;
+ }
+
+ wfree(scr->randr_state);
+ scr->randr_state = NULL;
+}
+
+void wRandRHandleNotify(WScreen *scr, XEvent *event)
+{
+ if (!scr || !scr->randr_state)
+ return;
+
+ if (event->type == (w_global.xext.randr.event_base + RRScreenChangeNotify))
+ XRRUpdateConfiguration(event);
+
+ /* Debounce: cancel any pending timer, then restart it */
+ if (scr->randr_debounce_timer) {
+ WMDeleteTimerHandler(scr->randr_debounce_timer);
+ scr->randr_debounce_timer = NULL;
+ }
+ scr->randr_debounce_timer =
+ WMAddTimerHandler(RANDR_DEBOUNCE_DELAY, wRandRDebounceTimerFired, scr);
+}
+
+#endif /* USE_RANDR */
diff --git a/src/appmenu.h b/src/randr.h
similarity index 68%
copy from src/appmenu.h
copy to src/randr.h
index cf7b685fa68a..4799a43b3085 100644
--- a/src/appmenu.h
+++ b/src/randr.h
@@ -1,8 +1,8 @@
-/* appmenu.h- application defined menu
+/* randr.h - RandR multi-monitor support
*
* Window Maker window manager
*
- * Copyright (c) 1997-2003 Alfredo K. Kojima
+ * Copyright (c) 2026 Window Maker Team
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -19,14 +19,18 @@
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
-#ifndef _WAPPMENU_H_
-#define _WAPPMENU_H_
+#ifndef _WMRANDR_H_
+#define _WMRANDR_H_
-WMenu *wAppMenuGet(WScreen *scr, Window window);
-void wAppMenuDestroy(WMenu *menu);
+#ifdef USE_RANDR
-void wAppMenuMap(WMenu *menu, WWindow *wwin);
-void wAppMenuUnmap(WMenu *menu);
+#include <X11/extensions/Xrandr.h>
+#include "screen.h"
+void wRandRInit(WScreen *scr);
+void wRandRTeardown(WScreen *scr);
+void wRandRHandleNotify(WScreen *scr, XEvent *event);
-#endif
+#endif /* USE_RANDR */
+
+#endif /* _WMRANDR_H_ */
diff --git a/src/screen.c b/src/screen.c
index a737c0954239..8cb27487c0db 100644
--- a/src/screen.c
+++ b/src/screen.c
@@ -34,7 +34,7 @@
#include <X11/Xatom.h>
#include <X11/XKBlib.h>
#ifdef USE_RANDR
-#include <X11/extensions/Xrandr.h>
+#include "randr.h"
#endif
#include <wraster.h>
@@ -642,6 +642,10 @@ WScreen *wScreenInit(int screen_number)
scr->usableArea[i].y2 = scr->totalUsableArea[i].y2 = rect.pos.y + rect.size.height;
}
+#ifdef USE_RANDR
+ wRandRInit(scr);
+#endif
+
scr->fakeGroupLeaders = WMCreateArray(16);
CantManageScreen = 0;
@@ -669,7 +673,10 @@ WScreen *wScreenInit(int screen_number)
#ifdef USE_RANDR
if (w_global.xext.randr.supported)
- XRRSelectInput(dpy, scr->root_win, RRScreenChangeNotifyMask);
+ XRRSelectInput(dpy, scr->root_win,
+ RRScreenChangeNotifyMask |
+ RRCrtcChangeNotifyMask |
+ RROutputChangeNotifyMask);
#endif
XSync(dpy, False);
@@ -1367,3 +1374,13 @@ void ScreenCapture(WScreen *scr, int mode)
wfree(filepath);
wfree(screenshot_dir);
}
+
+void wScreenDestroy(WScreen *scr)
+{
+#ifdef USE_RANDR
+ wRandRTeardown(scr);
+#else
+ /* Parameter not used, but tell the compiler that it is ok */
+ (void) scr;
+#endif
+}
diff --git a/src/screen.h b/src/screen.h
index dac0be56770d..c3100b0bb2ee 100644
--- a/src/screen.h
+++ b/src/screen.h
@@ -299,6 +299,11 @@ typedef struct _WScreen {
/* for hot-corners delay */
WMHandlerID *hot_corner_timer;
+#ifdef USE_RANDR
+ WMHandlerID *randr_debounce_timer;
+ void *randr_state;
+#endif
+
/* for window shortcuts */
WMArray *shortcutWindows[MAX_WINDOW_SHORTCUTS];
@@ -345,7 +350,7 @@ WScreen *wScreenWithNumber(int i);
WScreen *wScreenForRootWindow(Window window); /* window must be valid */
WScreen *wScreenForWindow(Window window); /* slower than above functions */
-void wScreenFinish(WScreen *scr);
+void wScreenDestroy(WScreen *scr);
void wScreenUpdateUsableArea(WScreen *scr);
void create_logo_image(WScreen *scr);
diff --git a/src/shutdown.c b/src/shutdown.c
index 300a2692b514..99116678c61d 100644
--- a/src/shutdown.c
+++ b/src/shutdown.c
@@ -36,6 +36,7 @@
#include "winspector.h"
#include "wmspec.h"
#include "colormap.h"
+#include "screen.h"
#include "shutdown.h"
@@ -81,6 +82,7 @@ void Shutdown(WShutdownMode mode)
wipeDesktop(scr);
else
RestoreDesktop(scr);
+ wScreenDestroy(scr);
}
}
ExecExitScript();
@@ -103,6 +105,7 @@ void Shutdown(WShutdownMode mode)
kill(scr->helper_pid, SIGKILL);
wScreenSaveState(scr);
RestoreDesktop(scr);
+ wScreenDestroy(scr);
}
}
break;
diff --git a/src/startup.c b/src/startup.c
index 7b86746b77de..ed992901b1b6 100644
--- a/src/startup.c
+++ b/src/startup.c
@@ -600,7 +600,23 @@ void StartUp(Bool defaultScreenOnly)
#endif
#ifdef USE_RANDR
- w_global.xext.randr.supported = XRRQueryExtension(dpy, &w_global.xext.randr.event_base, &j);
+ {
+ int rr_major = 0, rr_minor = 0;
+ Bool rr_ext = XRRQueryExtension(dpy, &w_global.xext.randr.event_base, &j);
+ Bool rr_ver = rr_ext && XRRQueryVersion(dpy, &rr_major, &rr_minor);
+
+ if (rr_ver && (rr_major > 1 || (rr_major == 1 && rr_minor >= 3))) {
+ w_global.xext.randr.supported = 1;
+ } else {
+ w_global.xext.randr.supported = 0;
+ if (!rr_ext)
+ wwarning(_("RandR extension is not available"));
+ else if (!rr_ver)
+ wwarning(_("RandR version check failed, RandR disabled"));
+ else
+ wwarning(_("RandR version %d.%d found but RandR version >=1.3 required"), rr_major, rr_minor);
+ }
+ }
#endif
w_global.xext.xkb.supported = XkbQueryExtension(dpy, NULL, &w_global.xext.xkb.event_base, NULL, NULL, NULL);
diff --git a/src/window.c b/src/window.c
index 10949aa341c9..6e80dd685ab9 100644
--- a/src/window.c
+++ b/src/window.c
@@ -2294,6 +2294,26 @@ void wWindowMove(WWindow *wwin, int req_x, int req_y)
#endif
}
+/* Move the window to the nearest on-screen position if its stored
+ * frame origin falls in dead space (for example when a RandR monitor
+ * was removed while the window was miniaturized or hidden) */
+void wWindowSnapToHead(WWindow *wwin)
+{
+#ifdef USE_RANDR
+ int bw = HAS_BORDER(wwin) ? wwin->screen_ptr->frame_border_width : 0;
+ int rx = wwin->frame_x - bw;
+ int ry = wwin->frame_y - bw;
+
+ if (wScreenBringInside(wwin->screen_ptr, &rx, &ry,
+ wwin->frame->core->width + 2 * bw,
+ wwin->frame->core->height + 2 * bw))
+ wWindowMove(wwin, rx + bw, ry + bw);
+#else
+ /* Parameter not used, but tell the compiler that it is ok */
+ (void) wwin;
+#endif
+}
+
void wWindowUpdateButtonImages(WWindow *wwin)
{
WScreen *scr = wwin->screen_ptr;
diff --git a/src/window.h b/src/window.h
index 5da5d897cab4..8b14564e20d1 100644
--- a/src/window.h
+++ b/src/window.h
@@ -362,6 +362,7 @@ void wWindowConfigure(WWindow *wwin, int req_x, int req_y,
int req_width, int req_height);
void wWindowMove(WWindow *wwin, int req_x, int req_y);
+void wWindowSnapToHead(WWindow *wwin);
void wWindowSynthConfigureNotify(WWindow *wwin);
commit 8e8426403619d3ab4710fab7c7de9d03c7a7288b
Author: David Maciejak <
david.m...@gmail.com>
Date: Fri, 27 Mar 2026 18:48:25 -0400
URL: <
https://repo.or.cz/wmaker-crm.git/8e8426403619d3ab>
WPrefs: add hotplug monitor option in expert panel
This patch is adding a new HotplugMonitor option
to automatically (de)activate monitors on randr hotplug events.
It is disabled by default.
---
WPrefs.app/Expert.c | 5 +++++
src/WindowMaker.h | 3 +++
src/defaults.c | 5 ++++-
3 files changed, 12 insertions(+), 1 deletion(-)
diff --git a/WPrefs.app/Expert.c b/WPrefs.app/Expert.c
index d3731ae49549..21fbcb4ac9ec 100644
--- a/WPrefs.app/Expert.c
+++ b/WPrefs.app/Expert.c
@@ -132,6 +132,11 @@ static struct expert_option {
{ N_("Allow windows to take focus using mouse wheel."),
/* default: */ False, OPTION_WMAKER, "MouseWheelFocus"},
+
+#ifdef USE_RANDR
+ { N_("Automatically (de)activate monitors on hotplug events."),
+ /* default: */ False, OPTION_WMAKER, "HotplugMonitor"},
+#endif
};
diff --git a/src/WindowMaker.h b/src/WindowMaker.h
index cd2c9b48c5d2..f979573f7596 100644
--- a/src/WindowMaker.h
+++ b/src/WindowMaker.h
@@ -410,6 +410,9 @@ extern struct WPreferences {
char dont_blink; /* do not blink icon selection */
char keep_dock_on_primary_head; /* keep dock on primary head */
+#ifdef USE_RANDR
+ char hotplug_monitor; /* auto-(de)activate monitors */
+#endif
/* Appearance options */
char new_style; /* Use newstyle buttons */
diff --git a/src/defaults.c b/src/defaults.c
index d5d81a2d1026..101e4dc1f673 100644
--- a/src/defaults.c
+++ b/src/defaults.c
@@ -5,7 +5,6 @@
* Copyright (c) 1997-2003 Alfredo K. Kojima
* Copyright (c) 1998-2003 Dan Pascu
* Copyright (c) 2014-2026 Window Maker Team
-
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -527,6 +526,10 @@ WDefaultEntry optionList[] = {
{"KeepDockOnPrimaryHead", "NO", NULL,
&wPreferences.keep_dock_on_primary_head, getBool, updateDock,
NULL, NULL},
+#ifdef USE_RANDR
+ {"HotplugMonitor", "NO", NULL,
+ &wPreferences.hotplug_monitor, getBool, NULL, NULL, NULL},
+#endif
{"HotCorners", "NO", NULL,
&wPreferences.hot_corners, getBool, NULL, NULL, NULL},
{"HotCornerDelay", "250", (void *)&wPreferences.hot_corner_delay,
-----------------------------------------------------------------------
Summary of changes:
WPrefs.app/Expert.c | 5 +
configure.ac | 4 +-
po/Makefile.am | 2 +
src/Makefile.am | 2 +
src/WindowMaker.h | 3 +
src/actions.c | 8 +-
src/defaults.c | 5 +-
src/dock.c | 37 +++
src/dock.h | 1 +
src/event.c | 24 +-
src/randr.c | 629 +++++++++++++++++++++++++++++++++++++
src/{appmenu.h => randr.h} | 22 +-
src/screen.c | 21 +-
src/screen.h | 7 +-
src/shutdown.c | 3 +
src/startup.c | 18 +-
src/window.c | 20 ++
src/window.h | 1 +
18 files changed, 781 insertions(+), 31 deletions(-)
create mode 100644 src/randr.c
copy src/{appmenu.h => randr.h} (68%)
repo.or.cz automatic notification. Contact project admin
crm...@gmail.com
if you want to unsubscribe, or site admin
ad...@repo.or.cz if you receive
no reply.
--
wmaker-crm.git ("The Window Maker window manager")