diff --git a/configure.ac b/configure.ac index 96277426..2063f6d2 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 20acee0f..e305f995 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 81b5113b..ed658158 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 d013d0a2..460c9166 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 45282057..35d13b0b 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 2f752ca8..7796e934 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 a62586b7..e5422ea4 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 +#include "randr.h" #endif #include @@ -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 00000000..bacfa4a7 --- /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 +#include + +#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/randr.h b/src/randr.h new file mode 100644 index 00000000..4799a43b --- /dev/null +++ b/src/randr.h @@ -0,0 +1,36 @@ +/* randr.h - 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. + */ + +#ifndef _WMRANDR_H_ +#define _WMRANDR_H_ + +#ifdef USE_RANDR + +#include +#include "screen.h" + +void wRandRInit(WScreen *scr); +void wRandRTeardown(WScreen *scr); +void wRandRHandleNotify(WScreen *scr, XEvent *event); + +#endif /* USE_RANDR */ + +#endif /* _WMRANDR_H_ */ diff --git a/src/screen.c b/src/screen.c index a737c095..8cb27487 100644 --- a/src/screen.c +++ b/src/screen.c @@ -34,7 +34,7 @@ #include #include #ifdef USE_RANDR -#include +#include "randr.h" #endif #include @@ -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 dac0be56..c3100b0b 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 300a2692..99116678 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 7b86746b..ed992901 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 10949aa3..6e80dd68 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 5da5d897..8b14564e 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);