diff --git a/WPrefs.app/KeyboardShortcuts.c b/WPrefs.app/KeyboardShortcuts.c index f6750c1b..6e327729 100644 --- a/WPrefs.app/KeyboardShortcuts.c +++ b/WPrefs.app/KeyboardShortcuts.c @@ -103,6 +103,11 @@ static struct keyOption { { "SelectKey", N_("Select active window") }, { "FocusNextKey", N_("Focus next window") }, { "FocusPrevKey", N_("Focus previous window") }, + /* Directional window focus */ + { "FocusWindowLeftKey", N_("Focus the window to the left") }, + { "FocusWindowRightKey", N_("Focus the window to the right") }, + { "FocusWindowUpKey", N_("Focus the window above") }, + { "FocusWindowDownKey", N_("Focus the window below") }, { "GroupNextKey", N_("Focus next group window") }, { "GroupPrevKey", N_("Focus previous group window") }, diff --git a/src/actions.c b/src/actions.c index 460c9166..1205a67f 100644 --- a/src/actions.c +++ b/src/actions.c @@ -240,6 +240,100 @@ void wSetFocusTo(WScreen *scr, WWindow *wwin) old_scr = scr; } +/* + *---------------------------------------------------------------------- + * wSetFocusToDirection-- + * Moves focus to the nearest window in the given cardinal + * direction (DIRECTION_LEFT, DIRECTION_RIGHT, DIRECTION_UP, + * DIRECTION_DOWN from xinerama.h). + * + * Selection algorithm: candidate windows are scored by + * (primary_distance + perpendicular_offset). A large penalty is + * added when the perpendicular offset exceeds the primary distance + * (i.e. the candidate is more than 45 degrees off-axis). The window + * with the lowest score wins. If no candidate exists in the requested + * direction the call is silently ignored. + *---------------------------------------------------------------------- + */ +void wSetFocusToDirection(WScreen *scr, int direction) +{ + WWindow *focused = scr->focused_window; + WWindow *best = NULL; + WWindow *candidate = NULL; + int my_cx, my_cy; + long best_score = -1; + + if (!focused || !focused->flags.mapped) + return; + + /* centre of the focused window */ + my_cx = focused->frame_x + (int)focused->frame->core->width / 2; + my_cy = focused->frame_y + (int)focused->frame->core->height / 2; + + /* Iterate from most-recently-focused to least-recently-focused */ + for (candidate = focused->prev; candidate != NULL; candidate = candidate->prev) { + int his_cx, his_cy, distance, offset; + long score; + + if (!candidate->flags.mapped) + continue; + if (WFLAGP(candidate, no_focusable)) + continue; + if (!candidate->frame || candidate->frame->workspace != scr->current_workspace) + continue; + + /* ignore fully covered windows if cannot raised them */ + if (!wPreferences.circ_raise && wWindowIsFullyCovered(candidate)) + continue; + + /* relative centre of candidate */ + his_cx = (candidate->frame_x - my_cx) + (int)candidate->frame->core->width / 2; + his_cy = (candidate->frame_y - my_cy) + (int)candidate->frame->core->height / 2; + + switch (direction) { + case DIRECTION_RIGHT: + distance = his_cx; + offset = his_cy < 0 ? -his_cy : his_cy; + break; + case DIRECTION_LEFT: + distance = -his_cx; + offset = his_cy < 0 ? -his_cy : his_cy; + break; + case DIRECTION_DOWN: + distance = his_cy; + offset = his_cx < 0 ? -his_cx : his_cx; + break; + case DIRECTION_UP: + distance = -his_cy; + offset = his_cx < 0 ? -his_cx : his_cx; + break; + default: + continue; + } + + /* candidate must be strictly in the requested direction */ + if (distance <= 0) + continue; + + score = distance + offset; + + /* heavy penalty for windows more than 45 degrees off-axis */ + if (offset > distance) + score += 1000000L; + + if (best_score < 0 || score < best_score) { + best = candidate; + best_score = score; + } + } + + if (best) { + if (wPreferences.circ_raise) + wRaiseFrame(best->frame->core); + wSetFocusTo(scr, best); + } +} + void wShadeWindow(WWindow *wwin) { diff --git a/src/actions.h b/src/actions.h index 16e5e3f6..f9aa0efc 100644 --- a/src/actions.h +++ b/src/actions.h @@ -41,6 +41,7 @@ #define SAVE_GEOMETRY_ALL SAVE_GEOMETRY_X | SAVE_GEOMETRY_Y | SAVE_GEOMETRY_WIDTH | SAVE_GEOMETRY_HEIGHT void wSetFocusTo(WScreen *scr, WWindow *wwin); +void wSetFocusToDirection(WScreen *scr, int direction); int wMouseMoveWindow(WWindow *wwin, XEvent *ev); int wKeyboardMoveResizeWindow(WWindow *wwin); diff --git a/src/defaults.c b/src/defaults.c index 0a9da900..42bc3445 100644 --- a/src/defaults.c +++ b/src/defaults.c @@ -721,6 +721,15 @@ WDefaultEntry optionList[] = { NULL, getKeybind, setKeyGrab, NULL, NULL}, {"FocusPrevKey", "Mod1+Shift+Tab", (void *)WKBD_FOCUSPREV, NULL, getKeybind, setKeyGrab, NULL, NULL}, + /* Directional window focus */ + {"FocusWindowLeftKey", "None", (void *)WKBD_FOCUSLEFT, + NULL, getKeybind, setKeyGrab, NULL, NULL}, + {"FocusWindowRightKey", "None", (void *)WKBD_FOCUSRIGHT, + NULL, getKeybind, setKeyGrab, NULL, NULL}, + {"FocusWindowUpKey", "None", (void *)WKBD_FOCUSUP, + NULL, getKeybind, setKeyGrab, NULL, NULL}, + {"FocusWindowDownKey", "None", (void *)WKBD_FOCUSDOWN, + NULL, getKeybind, setKeyGrab, NULL, NULL}, {"GroupNextKey", "None", (void *)WKBD_GROUPNEXT, NULL, getKeybind, setKeyGrab, NULL, NULL}, {"GroupPrevKey", "None", (void *)WKBD_GROUPPREV, diff --git a/src/event.c b/src/event.c index c3063642..93669b1e 100644 --- a/src/event.c +++ b/src/event.c @@ -1732,6 +1732,22 @@ static void dispatchWKBDCommand(int command, WScreen *scr, WWindow *wwin, XEvent StartWindozeCycle(wwin, event, False, False); break; + case WKBD_FOCUSLEFT: + wSetFocusToDirection(scr, DIRECTION_LEFT); + break; + + case WKBD_FOCUSRIGHT: + wSetFocusToDirection(scr, DIRECTION_RIGHT); + break; + + case WKBD_FOCUSUP: + wSetFocusToDirection(scr, DIRECTION_UP); + break; + + case WKBD_FOCUSDOWN: + wSetFocusToDirection(scr, DIRECTION_DOWN); + break; + case WKBD_GROUPNEXT: StartWindozeCycle(wwin, event, True, True); break; diff --git a/src/keybind.h b/src/keybind.h index ae9a5022..25b7aac8 100644 --- a/src/keybind.h +++ b/src/keybind.h @@ -77,6 +77,10 @@ enum { WKBD_WORKSPACEMAP, WKBD_FOCUSNEXT, WKBD_FOCUSPREV, + WKBD_FOCUSLEFT, + WKBD_FOCUSRIGHT, + WKBD_FOCUSUP, + WKBD_FOCUSDOWN, WKBD_GROUPNEXT, WKBD_GROUPPREV, WKBD_CENTRAL, diff --git a/src/window.c b/src/window.c index 470cfcab..857deec8 100644 --- a/src/window.c +++ b/src/window.c @@ -470,6 +470,35 @@ void wWindowSetupInitialAttributes(WWindow *wwin, int *level, int *workspace) wwin->client_flags.no_focusable = 1; } +/* + * Returns True if every pixel of 'wwin' is covered by at least one other + * mapped window on the same workspace, making it invisible to the user + */ +Bool wWindowIsFullyCovered(WWindow *wwin) +{ + WScreen *scr = wwin->screen_ptr; + int cx = wwin->frame_x; + int cy = wwin->frame_y; + int cright = cx + (int)wwin->frame->core->width; + int cbottom = cy + (int)wwin->frame->core->height; + WWindow *w; + + for (w = scr->focused_window; w != NULL; w = w->prev) { + if (w == wwin) + continue; + if (!w->flags.mapped) + continue; + if (!w->frame || w->frame->workspace != scr->current_workspace) + continue; + if (w->frame_x <= cx && + w->frame_y <= cy && + w->frame_x + (int)w->frame->core->width >= cright && + w->frame_y + (int)w->frame->core->height >= cbottom) + return True; + } + return False; +} + Bool wWindowObscuresWindow(WWindow *wwin, WWindow *obscured) { int w1, h1, w2, h2; diff --git a/src/window.h b/src/window.h index 1229eed3..9ccabdeb 100644 --- a/src/window.h +++ b/src/window.h @@ -405,6 +405,7 @@ WMagicNumber wWindowGetSavedState(Window win); void wWindowDeleteSavedState(WMagicNumber id); +Bool wWindowIsFullyCovered(WWindow *wwin); Bool wWindowObscuresWindow(WWindow *wwin, WWindow *obscured); void wWindowSetOmnipresent(WWindow *wwin, Bool flag);