patch: focus follows mouse (rat) and sloppy

Jeff Abrahamson <[email protected]> Mon, 29 Sep 2014 21:15:44 +0200
Newsgroups gmane.comp.window-managers.ratpoison.devel
Message-ID <CAM4Y7zx9LHe4NtXapNEWRY1Lbg8J5M2L6y7WYYVt9KPp2x_Wog@mail.gmail.com>
I've implemented (optional!) mouse-based focus in ratpoison. This is
intended to replace sloppy.c. It has the following advantages:


   - It is implemented within ratpoison, so it is more efficient than
   invoking a new instance of the ratpoison binary on every crossing event.
   - It also implements focus-follows-mouse, which means that empty frames
   can be focused.

This is a relatively complex patch. I don't think I've damaged anything
when the feature is off, but it wouldn't surprise me if I missed some edge
case that I didn't think of when the feature is on. Please let me know if
you are able to test this.

To turn it on, type

:focus_policy
The valid arguments are manual, sloppy, and ffm.

I have not updated the docs yet.

I am suspicious about transient windows, but so far it seems to work
correctly. I just can't shake the feeling that I missed a detail there.

There is a technique for requesting events on a window but not on its
subwindow. I have to find and fix this, because at the moment the root
window requesting motion events in ffm mode means that all windows do. It's
harmless but inefficient. And it makes the log longer.

Jeff Abrahamson
+33 6 24 40 01 57
+44 7920 594 255    <-- only if I'm in the UK

http://jeff.purple.com/
http://blog.purple.com/jeff/

_______________________________________________
Ratpoison-devel mailing list
[email protected]
https://lists.nongnu.org/mailman/listinfo/ratpoison-devel
0001-Implement-mouse-based-focus.patch (text/x-patch, 15.6 KB)
From 4c8982ab096267c41d1fbc46026084349059f906 Mon Sep 17 00:00:00 2001
From: Jeff Abrahamson <[email protected]>
Date: Mon, 29 Sep 2014 20:59:40 +0200
Subject: [PATCH] Implement mouse-based focus.

Implement focus-follows-mouse and sloppy focus within
ratpoison. Unlike the separate program sloppy.c, this also permits
focus-follows-mouse with correct interaction with the root window. The
sloppy.c program does not have access to ratpoison's internal frame
set, and so can't focus frames that do not contain windows.

It is hoped that this will prove more reliable than sloppy.c as well,
though sloppy.c has been reasonably good of late, to be fair.
---
 src/actions.c | 44 ++++++++++++++++++++++++++++++++++++-----
 src/actions.h |  1 +
 src/data.h    |  5 +++++
 src/events.c  | 63 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
 src/globals.h | 25 ++++++++++++++++++++++++
 src/main.c    |  1 +
 src/manage.c  |  4 ++--
 src/screen.c  |  5 +----
 src/split.c   | 33 +++++++++++++++++++++++++++++++
 src/split.h   |  2 +-
 src/window.c  | 28 +++++++++++++++++++++++++-
 src/window.h  |  1 +
 12 files changed, 197 insertions(+), 15 deletions(-)

diff --git a/src/actions.c b/src/actions.c
index 7579101..1c5fc48 100644
--- a/src/actions.c
+++ b/src/actions.c
@@ -246,6 +246,8 @@ init_user_commands(void)
   add_command ("swap",	cmd_swap, 2, 1, 1,
 	       "destination frame: ", arg_FRAME,
 	       "source frame: ", arg_FRAME);
+  add_command ("focus_policy", cmd_focus_policy,  1, 1, 1,
+               "Focus policy [manual,sloppy,ffm]: ", arg_STRING);
   add_command ("focuslast",     cmd_focuslast,  0, 0, 0);
   add_command ("focusleft",     cmd_focusleft,  0, 0, 0);
   add_command ("focusright",    cmd_focusright, 0, 0, 0);
@@ -4493,6 +4495,39 @@ cmd_startup_message (int interactive UNUSED, struct cmdarg **args)
   return cmdret_new (RET_SUCCESS, NULL);
 }
 
+static void sync_wins (rp_screen *s);
+
+cmdret *
+cmd_focus_policy (int interactive UNUSED, struct cmdarg **args)
+{
+  if (args[0] == NULL) {
+    /* TODO([email protected]): Fix error message with array of policy names. And below. */
+    return cmdret_new (RET_SUCCESS, "%s", defaults.focus_policy ? "on":"off");
+  }
+
+  int succeeded = 1;
+  if (!strcasecmp (ARG_STRING(0), "manual")) {
+    defaults.focus_policy = FOCUS_MANUAL;
+  } else if (!strcasecmp (ARG_STRING(0), "sloppy")) {
+    defaults.focus_policy = FOCUS_SLOPPY;
+  } else if (!strcasecmp (ARG_STRING(0), "ffm")) {
+    defaults.focus_policy = FOCUS_FOLLOWS_MOUSE;
+  } else {
+    defaults.focus_policy = FOCUS_MANUAL;
+    succeeded = 0;
+  }
+  init_focus_policy();
+
+  /* Hack. I only need to set the input mask. */
+  int i;
+  for (i=0; i<num_screens; i++)
+    sync_wins(&screens[i]);
+
+  if(succeeded)
+    return cmdret_new (RET_SUCCESS, NULL);
+  return cmdret_new (RET_FAILURE, "focus_policy: invalid argument");
+}
+
 cmdret *
 cmd_focuslast (int interactive UNUSED, struct cmdarg **args UNUSED)
 {
@@ -4664,6 +4699,7 @@ sync_wins (rp_screen *s)
   unsigned int i, nwins;
   Window dw1, dw2, *wins;
   XQueryTree(dpy, s->root, &dw1, &dw2, &wins, &nwins);
+  XSelectInput(dpy, RootWindow(dpy, s->screen_num), root_events);
 
   /* Remove any windows in our cached lists that aren't in the query
      tree. These windows have been destroyed. */
@@ -4759,6 +4795,7 @@ sync_wins (rp_screen *s)
             }
 
           /* We've handled the window. */
+          XSelectInput (dpy, win->w, win_events);
           continue;
         }
 
@@ -4786,6 +4823,7 @@ sync_wins (rp_screen *s)
             }
 
           /* We've handled the window. */
+          XSelectInput (dpy, win->w, win_events);
           continue;
         }
 
@@ -4799,7 +4837,6 @@ sync_wins (rp_screen *s)
               && get_state (win) == IconicState))
         map_window (win);
     }
-
 }
 
 static int tmpwm_error_raised = 0;
@@ -4878,10 +4915,7 @@ cmd_tmpwm (int interactive UNUSED, struct cmdarg **args)
     tmpwm_error_raised = 0;
     for (i=0; i<num_screens; i++)
       {
-        XSelectInput(dpy, RootWindow (dpy, screens[i].screen_num),
-                     PropertyChangeMask | ColormapChangeMask
-                     | SubstructureRedirectMask | SubstructureNotifyMask
-                     | StructureNotifyMask);
+        XSelectInput(dpy, RootWindow (dpy, screens[i].screen_num), root_events);
         XSync (dpy, False);
       }
     if (tmpwm_error_raised)
diff --git a/src/actions.h b/src/actions.h
index 519c1a7..328aefe 100644
--- a/src/actions.h
+++ b/src/actions.h
@@ -129,6 +129,7 @@ RP_CMD (exchangedown);
 RP_CMD (exchangeleft);
 RP_CMD (exchangeright);
 RP_CMD (swap);
+RP_CMD (focus_policy);
 RP_CMD (frestore);
 RP_CMD (fselect);
 RP_CMD (gdelete);
diff --git a/src/data.h b/src/data.h
index f4bd185..49baa89 100644
--- a/src/data.h
+++ b/src/data.h
@@ -259,6 +259,11 @@ struct rp_defaults
   /* Pointer warping toggle. */
   int warp;
 
+  /* Ignore the mouse (FOCUS_MANUAL), sloppy focus policy
+   * (FOCUS_SLOPPY), or change focus as the mouse moves
+   * (FOCUS_FOLLOWS_MOUSE). */
+  int focus_policy;
+
   int history_size;
   /* remove older history when adding the same again */
   int history_compaction;
diff --git a/src/events.c b/src/events.c
index 814e1ef..15b67b5 100644
--- a/src/events.c
+++ b/src/events.c
@@ -38,6 +38,10 @@
 
 #include "ratpoison.h"
 
+/* The events we care about. This depends on the value of the flag
+   focus_policy. */
+unsigned int win_events = 0;
+unsigned int root_events = 0;
 /* The event currently being processed. Mostly used in functions from
    action.c which need to forward events to other windows. */
 XEvent rp_current_event;
@@ -874,15 +878,70 @@ delegate_event (XEvent *ev)
 	
     case MapNotify:
     case Expose:
-    case MotionNotify:
     case KeyRelease:
     case ReparentNotify:
-    case EnterNotify:
     case SelectionNotify:
     case CirculateRequest:
+    case LeaveNotify:
+    case VisibilityNotify:
       /* Ignore these events. */
       break;
 
+    case EnterNotify:
+      PRINT_DEBUG (("--- Handling EnterNotify ---\n"));
+      if (defaults.focus_policy != FOCUS_MANUAL) {
+        /* Only pay attention if focus follows mouse. */
+        XAnyEvent *any_event = (XAnyEvent *)ev;
+        rp_window *win = find_window(any_event->window);
+        PRINT_DEBUG(("Mouse focus request: rp_window=%u,  Xwindow=%u\n", (unsigned int)win, (unsigned int)any_event->window));
+        if (win && current_window() != win) {
+          /* Also check for transient windows. */
+          if(is_rp_window_for_screen(win->w, current_screen())) {
+            /* Don't focus ratpoison screen windows. It's not
+               meaningful, the user should never mean it, and we don't
+               mean to support it. */
+            PRINT_DEBUG(("  Ignoring enter event on screen window.\n"));
+            return;
+          }
+          rp_frame *new_frame = find_frame_number(win->frame_number);
+          if(new_frame)
+            set_active_frame(new_frame, 0);
+          else
+            PRINT_DEBUG(("  Ignoring focus request: frame not found.\n"));
+        } else if (defaults.focus_policy == FOCUS_FOLLOWS_MOUSE && is_a_root_window(any_event->window)) {
+          PRINT_DEBUG(("  focus: root window event\n"));
+          XCrossingEvent *crossing_event = (XCrossingEvent *)ev;
+          int x = crossing_event->x_root;
+          int y = crossing_event->y_root;
+          Window root_win = crossing_event->root;
+          rp_frame *new_frame = find_frame_by_coordinates (root_win, x, y);
+          if(new_frame && new_frame->win_number < 0 && new_frame != current_frame())
+            set_active_frame(new_frame, 0);
+          else {
+            PRINT_DEBUG(("  Ignoring focus request: win=%d, not_current=%d\n", new_frame->win_number, new_frame != current_frame()));
+          }
+        } else {
+          PRINT_DEBUG(("  Ignoring focus request.\n"));
+        }
+      }
+      break;
+
+    case MotionNotify:
+      PRINT_DEBUG (("--- Handling MotionNotify ---\n"));
+      XPointerMovedEvent *motion_event = (XPointerMovedEvent *)ev;
+      if (defaults.focus_policy == FOCUS_FOLLOWS_MOUSE && is_a_root_window(motion_event->window)) {
+          int x = motion_event->x_root;
+          int y = motion_event->y_root;
+          Window root_win = motion_event->root;
+          rp_frame *new_frame = find_frame_by_coordinates (root_win, x, y);
+          if(new_frame && new_frame->win_number < 0 && new_frame != current_frame())
+            set_active_frame(new_frame, 0);
+          else {
+            PRINT_DEBUG(("  Ignoring focus request: win=%d, not_current=%d\n", new_frame->win_number, new_frame != current_frame()));
+          }
+      }
+      break;
+
     default:
       PRINT_DEBUG (("--- Unknown event %d ---\n",- ev->type));
     }
diff --git a/src/globals.h b/src/globals.h
index 1fb7e0f..54b757d 100644
--- a/src/globals.h
+++ b/src/globals.h
@@ -42,12 +42,35 @@
 
 #define MAX_FONT_WIDTH(f) (rp_font_width)
 
+#define ROOT_EVENTS (PropertyChangeMask | ColormapChangeMask | SubstructureRedirectMask | SubstructureNotifyMask | StructureNotifyMask)
+#define ROOT_EVENTS_WITH_MOVEMENT (ROOT_EVENTS | EnterWindowMask | PointerMotionMask)
 #define WIN_EVENTS (StructureNotifyMask | PropertyChangeMask | ColormapChangeMask | FocusChangeMask)
+#define WIN_EVENTS_WITH_MOVEMENT (WIN_EVENTS | EnterWindowMask)
 /* EMPTY is used when a frame doesn't contain a window, or a window
    doesn't have a frame. Any time a field refers to the number of a
    window/frame/screen/etc, Use EMPTY to denote a lack there of. */
 #define EMPTY -1
 
+/* Focus policies */
+/* In manual focus, only the keyboard affects focus. The mouse does not. */
+#define FOCUS_MANUAL 0
+/* In sloppy focus, we focus on user (non-root) windows over which the
+ * mouse moves. This notably means that when the mouse moves to a part
+ * of the screen with no user window (i.e., the root window), the
+ * window the pointer has left remains focused until a new user window
+ * is entered. */
+#define FOCUS_SLOPPY 1
+/* Focus follows mouse causes the focus to follow the mouse
+ * strictly. This is not quite true, because ratpoison will never
+ * focus its own windows, things like the message bar (C-t m) and the
+ * window bar (C-t w).  In particular, if the mouse moves over the
+ * root window, the root window itself is focussed, which the user
+ * perceives as input going to no window. Many applications leave a
+ * pixel or two at the edges where the root window is visible, for
+ * example because they restrict resize to multiples of some font
+ * dimension. */
+#define FOCUS_FOLLOWS_MOUSE 2
+
 /* Possible values for defaults.window_list_style */
 #define STYLE_ROW    0
 #define STYLE_COLUMN 1
@@ -108,6 +131,8 @@ extern int rp_current_screen;
 extern rp_screen *screens;
 extern int num_screens;
 
+extern unsigned int win_events;
+extern unsigned int root_events;
 extern XEvent rp_current_event;
 
 extern Display *dpy;
diff --git a/src/main.c b/src/main.c
index 52dbcce..19d3abc 100644
--- a/src/main.c
+++ b/src/main.c
@@ -592,6 +592,7 @@ init_defaults (void)
   defaults.win_name = WIN_NAME_TITLE;
   defaults.startup_message = 1;
   defaults.warp = 0;
+  defaults.focus_policy = FOCUS_MANUAL;
   defaults.window_list_style = STYLE_COLUMN;
 
   defaults.history_size = 20;
diff --git a/src/manage.c b/src/manage.c
index 412fc29..01c0e68 100644
--- a/src/manage.c
+++ b/src/manage.c
@@ -896,9 +896,9 @@ hide_window (rp_window *win)
   win->frame_number = EMPTY;
 
   /* Ignore the unmap_notify event. */
-  XSelectInput(dpy, win->w, WIN_EVENTS&~(StructureNotifyMask));
+  XSelectInput(dpy, win->w, win_events&~(StructureNotifyMask));
   XUnmapWindow (dpy, win->w);
-  XSelectInput (dpy, win->w, WIN_EVENTS);
+  XSelectInput (dpy, win->w, win_events);
   /* Ensure that the window doesn't have the focused border
      color. This is needed by remove_frame and possibly others. */
   XSetWindowBorder (dpy, win->w, win->scr->bw_color);
diff --git a/src/screen.c b/src/screen.c
index 0afdee0..3178560 100644
--- a/src/screen.c
+++ b/src/screen.c
@@ -266,10 +266,7 @@ init_screen (rp_screen *s, int screen_num)
   /* Select on some events on the root window, if this fails, then
      there is already a WM running and the X Error handler will catch
      it, terminating ratpoison. */
-  XSelectInput(dpy, RootWindow (dpy, screen_num),
-               PropertyChangeMask | ColormapChangeMask
-	       | SubstructureRedirectMask | SubstructureNotifyMask
-	       | StructureNotifyMask);
+  XSelectInput(dpy, RootWindow (dpy, screen_num), root_events);
   XSync (dpy, False);
 
   /* Set the numset for the frames to our global numset. */
diff --git a/src/split.c b/src/split.c
index b36b99e..bf45bc5 100644
--- a/src/split.c
+++ b/src/split.c
@@ -1085,6 +1085,39 @@ find_frame_number (int num)
             return cur;
         }
     }
+  return NULL;
+}
+
+rp_frame *
+find_frame_by_coordinates (Window root, int x, int y)
+{
+  /* This is inefficient.
+
+     It would be better to keep an indexed structure of rectangles so
+     that we can search in sub-linear time. That would require
+     maintaining that structure with every frame operation, which
+     incurs some overhead on all frame operations (at least, if
+     defaults.focus_policy > 0). So the subject should be thought
+     through and measured carefully.
+  */
+
+  int i;
+  rp_frame *cur;
+
+  for (i=0; i<num_screens; i++)
+    {
+      rp_screen *s = &screens[i];
+      list_for_each_entry (cur, &s->frames, node)
+        {
+          if(root == s->root)
+            {
+              if (cur->x <= x && cur->y <= y && x < cur->x + cur->width && y < cur->y + cur->height) {
+                return cur;
+              }
+            }
+        }
+    }
 
+  PRINT_DEBUG(("No frame matches coordinates.\n"));
   return NULL;
 }
diff --git a/src/split.h b/src/split.h
index 87bb660..7ce5ba3 100644
--- a/src/split.h
+++ b/src/split.h
@@ -56,5 +56,5 @@ rp_frame *find_last_frame (void);
 rp_frame * find_frame_number (int num);
 
 rp_frame *current_frame (void);
-
+rp_frame *find_frame_by_coordinates (Window root, int x, int y);
 #endif
diff --git a/src/window.c b/src/window.c
index de9032a..cb94600 100644
--- a/src/window.c
+++ b/src/window.c
@@ -184,7 +184,7 @@ add_to_window_list (rp_screen *s, Window w)
 
   get_mouse_position (new_window, &new_window->mouse_x, &new_window->mouse_y);
 
-  XSelectInput (dpy, new_window->w, WIN_EVENTS);
+  XSelectInput (dpy, new_window->w, win_events);
 
   new_window->user_name = xstrdup ("Unnamed");
 
@@ -649,6 +649,7 @@ void
 init_window_stuff (void)
 {
   rp_window_numset = numset_new ();
+  init_focus_policy();
 }
 
 void
@@ -675,6 +676,31 @@ free_window_stuff (void)
   numset_free (rp_window_numset);
 }
 
+void
+init_focus_policy(void)
+{
+  switch(defaults.focus_policy)
+    {
+    case FOCUS_MANUAL:
+      win_events = WIN_EVENTS;
+      root_events = ROOT_EVENTS;
+      break;
+    case FOCUS_SLOPPY:
+      win_events = WIN_EVENTS_WITH_MOVEMENT;
+      root_events = ROOT_EVENTS;
+      break;
+    case FOCUS_FOLLOWS_MOUSE:
+      win_events = WIN_EVENTS_WITH_MOVEMENT;
+      root_events = ROOT_EVENTS_WITH_MOVEMENT;
+      break;
+    default:
+      PRINT_DEBUG(("Unexpected focus policy (%d)! Defaulting to manual focus.\n", defaults.focus_policy));
+      defaults.focus_policy = FOCUS_MANUAL;
+      win_events = WIN_EVENTS;
+      root_events = ROOT_EVENTS;
+  }
+}
+
 rp_frame *
 win_get_frame (rp_window *win)
 {
diff --git a/src/window.h b/src/window.h
index fdd246c..498ed4e 100644
--- a/src/window.h
+++ b/src/window.h
@@ -54,6 +54,7 @@ void get_window_list (char *fmt, char *delim, struct sbuf *buffer,
                       int *mark_start, int *mark_end);
 void init_window_stuff (void);
 void free_window_stuff (void);
+void init_focus_policy(void);
 
 rp_frame *win_get_frame (rp_window *win);
 
-- 
1.9.1