Re: patch: focus follows mouse (rat) and sloppy

Jeff Abrahamson <[email protected]> Tue, 30 Sep 2014 15:51:19 +0200
Newsgroups gmane.comp.window-managers.ratpoison.devel
Message-ID <CAM4Y7zyKH=yP-Q-7xmU-qgXmYOrc8i10AxCb4vwG3qpS+pGhzA@mail.gmail.com>
Added patch for documentation.

I still need to test on my multihead box.

I am tempted to make non-manual focus policy require either warp or
relbanish to be in effect, effectively setting warp if neither is set by
the user. It's a bit counter-intuitive to use keyboard commands to move the
focus, leaving the mouse far away, and then the first mouse movement causes
a focus where the mouse is. If not, I think at least documenting the
recommendation in the man and info pages would be a good service to the
user. Any opinions?


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/

On 29 September 2014 21:15, Jeff Abrahamson <[email protected]> wrote:

> 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 1/2] 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
0002-Document-focus_policy.patch (text/x-patch, 2.2 KB)
From 8d8d325b929252e9d8d4a22b1314b241b06abc8a Mon Sep 17 00:00:00 2001
From: Jeff Abrahamson <[email protected]>
Date: Tue, 30 Sep 2014 15:44:45 +0200
Subject: [PATCH 2/2] Document focus_policy.

---
 doc/ratpoison.mdoc.1 |  3 +++
 doc/ratpoison.texi   | 18 ++++++++++++++++++
 2 files changed, 21 insertions(+)

diff --git a/doc/ratpoison.mdoc.1 b/doc/ratpoison.mdoc.1
index 2e3c356..863228b 100644
--- a/doc/ratpoison.mdoc.1
+++ b/doc/ratpoison.mdoc.1
@@ -371,6 +371,9 @@ Switch to the frame to the right of the current one.
 Focus the previous frame.
 .It Ic focusup Pq Ic C\-t Up
 Switch to the frame above the current one.
+.It Ic focus_policy
+Tell ratpoison whether and how to interpret rodent movements for focusing frames.
+Possible values are manual (the default), sloppy, and ffm (focus follows mouse).
 .It Ic frestore Ar frames
 Replace the current frames with the ones specified in
 .Ar frames
diff --git a/doc/ratpoison.texi b/doc/ratpoison.texi
index d12501f..01ca15b 100644
--- a/doc/ratpoison.texi
+++ b/doc/ratpoison.texi
@@ -807,6 +807,24 @@ Move to the frame right of the current frame.
 Move to the frame above the current frame.
 @end deffn
 
+@deffn Command focus_policy
+Tell ratpoison whether and how to interpret mouse movement for focusing frames.
+The default behavior, ``manual'', is to ignore the rodent. This is, of course,
+the default.
+The other two possibilities are ``sloppy'' and ``ffm'' (focus follows mouse).
+
+In sloppy mode, the mouse focuses a frame when it enters, but leaving a
+frame (for example, because the next frame has no window mapped in it)
+does not change focus. This is useful when you like to push the mouse
+over a nearby empty frame without changing focus. On the other hand, the
+empty frame must be focused by keyboard actions in sloppy mode.
+
+In ffm mode, the focus really does follow the mouse: mousing over an empty frame
+focuses that empty frame. This can be useful for creating new windows in empty frames.
+
+Of course, regardless of focus policy, the keyboard focus commands are available and work correctly.
+@end deffn
+
 @node Saving and Restoring Frame Sets
 @section Saving and Restoring Frame Sets
 ratpoison provides two commands, @command{fdump} and
-- 
1.9.1