From 99eb158bca51252321d39e86b5dd89ba73397dee Mon Sep 17 00:00:00 2001 From: kekh Date: Sun, 13 Mar 2016 02:52:42 +0100 Subject: [PATCH 1/4] Add a "Sticky mouse pointer" feature Ability to make the mouse pointer following the currently manipulated window. --- quicktile.py | 45 +++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 43 insertions(+), 2 deletions(-) diff --git a/quicktile.py b/quicktile.py index a4aff85..bf315f7 100755 --- a/quicktile.py +++ b/quicktile.py @@ -160,6 +160,7 @@ def __call__(self, w, h, gravity='top-left', x=None, y=None): # Use Ctrl+Alt as the default base for key combinations 'ModMask': '', 'UseWorkarea': True, + 'StickyPointer': False, }, 'keys': { "KP_0" : "maximize", @@ -466,7 +467,7 @@ class WindowManager(object): # Prevent these temporary variables from showing up in the apidocs del _name, key, val - def __init__(self, screen=None, ignore_workarea=False): + def __init__(self, screen=None, ignore_workarea=False, sticky_pointer=False): """ Initializes C{WindowManager}. @@ -488,6 +489,7 @@ def __init__(self, screen=None, ignore_workarea=False): self.screen = wnck.screen_get(self.gdk_screen.get_number()) self.ignore_workarea = ignore_workarea + self.sticky_pointer = sticky_pointer @classmethod def calc_win_gravity(cls, geom, gravity): @@ -671,6 +673,35 @@ def get_workspace(self, window=None, direction=None): return nxt + @staticmethod + def pointer_follow(win_geom, center=True): + """Position the mouse pointer at topleft or center of the current manipulated window + + @param win_geom: The window geometry to which calculate pointer coordinates. + @param center: Center or not the pointer relative to window. + @type win_geom: C{gtk.gdk.Rectangle} + @type center: C{bool} + + @returns: Nothing. + @rtype: void + """ + + if center: + # position pointer at center of the window + new_x = win_geom.x + (win_geom.width / 2) + new_y = win_geom.y + (win_geom.height / 2) + else: + # add some space (here 10px) from TopLeft of window to ensure pointer is really inside of it + new_x = win_geom.x + 10 + new_y = win_geom.y + 10 + + logging.debug(" Stick mouse pointer to current window at: x=%d, y=%d\n", + new_x, new_y) + + xdisp = Display() + xdisp.screen().root.warp_pointer(int(new_x), int(new_y)) + xdisp.sync() + @classmethod def reposition(cls, win, geom=None, monitor=gtk.gdk.Rectangle(0, 0, 0, 0), keep_maximize=False, gravity=wnck.WINDOW_GRAVITY_NORTHWEST, @@ -748,6 +779,11 @@ def reposition(cls, win, geom=None, monitor=gtk.gdk.Rectangle(0, 0, 0, 0), # gravities have no effect. I'm guessing something's just broken. win.set_geometry(wnck.WINDOW_GRAVITY_STATIC, geometry_mask, new_x, new_y, geom.width, geom.height) + + if wm.sticky_pointer: + geom.x = new_x + geom.y = new_y + wm.pointer_follow(geom) # Restore maximization if asked if maxed and keep_maximize: @@ -1213,6 +1249,9 @@ def workspace_send_window(wm, win, state, motion): # pylint: disable=W0613 parser.add_option('--no-workarea', action="store_true", dest="no_workarea", default=False, help="Overlap panels but work better with " "non-rectangular desktops") + parser.add_option('--sticky-pointer', action="store_true", dest="sticky_pointer", + default=False, help="Make mouse pointer following the " + "currently manipulated window") help_group = OptionGroup(parser, "Additional Help") help_group.add_option('--show-bindings', action="store_true", @@ -1288,9 +1327,11 @@ def workspace_send_window(wm, win, state, motion): # pylint: disable=W0613 ignore_workarea = ((not config.getboolean('general', 'UseWorkarea')) or opts.no_workarea) + + sticky_pointer = (config.getboolean('general', 'StickyPointer') or opts.sticky_pointer) try: - wm = WindowManager(ignore_workarea=ignore_workarea) + wm = WindowManager(ignore_workarea=ignore_workarea, sticky_pointer=sticky_pointer) except XInitError as err: logging.critical(err) sys.exit(1) From 3f85d2428e7e8ab1da9735a13313a84ac4f93574 Mon Sep 17 00:00:00 2001 From: jean Date: Sun, 13 Mar 2016 13:19:12 +0100 Subject: [PATCH 2/4] "Sticky mouse pointer" feature refactoring --- quicktile.py | 55 +++++++++++++++++++++++++++++++++++----------------- 1 file changed, 37 insertions(+), 18 deletions(-) diff --git a/quicktile.py b/quicktile.py index bf315f7..59df585 100755 --- a/quicktile.py +++ b/quicktile.py @@ -259,6 +259,25 @@ def fmt_row(row, pad=' ', indent=0, min_width=0): output.extend(fmt_row(row, indent=1)) return ''.join(output) + +def get_xdisplay(xdisplay=None): + """ + Initializes C{WindowManager}. + + @param xdisplay: A C{python-xlib} display handle. + @type xdisplay: C{Xlib.display.Display} + @rtype: Xlib.display.Display + """ + try: + xdisp = xdisplay or Display() + return xdisp + except (UnicodeDecodeError, DisplayConnectionError), err: + raise XInitError("python-xlib failed with %s when asked to open" + " a connection to the X server. Cannot bind keys." + "\n\tIt's unclear why this happens, but it is" + " usually fixed by deleting your ~/.Xauthority" + " file and rebooting." + % err.__class__.__name__) class EnumSafeDict(DictMixin): """A dict-like object which avoids comparing objects of different types @@ -467,10 +486,12 @@ class WindowManager(object): # Prevent these temporary variables from showing up in the apidocs del _name, key, val - def __init__(self, screen=None, ignore_workarea=False, sticky_pointer=False): + def __init__(self, xdisplay=None, screen=None, ignore_workarea=False, sticky_pointer=False): """ Initializes C{WindowManager}. + @param xdisplay: A C{python-xlib} display handle. + @type xdisplay: C{Xlib.display.Display} @param screen: The X11 screen to operate on. If C{None}, the default screen as retrieved by C{gtk.gdk.screen_get_default} will be used. @type screen: C{gtk.gdk.Screen} @@ -482,6 +503,10 @@ def __init__(self, screen=None, ignore_workarea=False, sticky_pointer=False): It could possibly change while toggling "allow desktop icons" in KDE 3.x. (Not sure what would be equivalent elsewhere) """ + + self.xdisp = get_xdisplay(xdisplay) + self.xroot = self.xdisp.screen().root + self.gdk_screen = screen or gtk.gdk.screen_get_default() if self.gdk_screen is None: raise XInitError("GTK+ could not open a connection to the X server" @@ -673,8 +698,7 @@ def get_workspace(self, window=None, direction=None): return nxt - @staticmethod - def pointer_follow(win_geom, center=True): + def pointer_follow(self, win_geom, center=True): """Position the mouse pointer at topleft or center of the current manipulated window @param win_geom: The window geometry to which calculate pointer coordinates. @@ -698,9 +722,12 @@ def pointer_follow(win_geom, center=True): logging.debug(" Stick mouse pointer to current window at: x=%d, y=%d\n", new_x, new_y) - xdisp = Display() - xdisp.screen().root.warp_pointer(int(new_x), int(new_y)) - xdisp.sync() + #xdisp = Display() + #xdisp.screen().root.warp_pointer(int(new_x), int(new_y)) + #xdisp.sync() + + self.xroot.warp_pointer(int(new_x), int(new_y)) + self.xdisp.sync() @classmethod def reposition(cls, win, geom=None, monitor=gtk.gdk.Rectangle(0, 0, 0, 0), @@ -806,16 +833,8 @@ def __init__(self, xdisplay=None): @param xdisplay: A C{python-xlib} display handle. @type xdisplay: C{Xlib.display.Display} """ - try: - self.xdisp = xdisplay or Display() - except (UnicodeDecodeError, DisplayConnectionError), err: - raise XInitError("python-xlib failed with %s when asked to open" - " a connection to the X server. Cannot bind keys." - "\n\tIt's unclear why this happens, but it is" - " usually fixed by deleting your ~/.Xauthority" - " file and rebooting." - % err.__class__.__name__) + self.xdisp = get_xdisplay(xdisplay) self.xroot = self.xdisp.screen().root self._keys = {} @@ -983,7 +1002,7 @@ def run(self): if XLIB_PRESENT: try: - self.keybinder = KeyBinder() + self.keybinder = KeyBinder(xdisplay=wm.xdisp) except XInitError as err: logging.error(err) else: @@ -1329,9 +1348,9 @@ def workspace_send_window(wm, win, state, motion): # pylint: disable=W0613 or opts.no_workarea) sticky_pointer = (config.getboolean('general', 'StickyPointer') or opts.sticky_pointer) - + try: - wm = WindowManager(ignore_workarea=ignore_workarea, sticky_pointer=sticky_pointer) + wm = WindowManager(xdisplay=get_xdisplay(), ignore_workarea=ignore_workarea, sticky_pointer=sticky_pointer) except XInitError as err: logging.critical(err) sys.exit(1) From 1d73d0e66557f7d3a32fef97065123a225212d0a Mon Sep 17 00:00:00 2001 From: jean Date: Sun, 13 Mar 2016 15:00:09 +0100 Subject: [PATCH 3/4] "Sticky mouse pointer" feature refactoring finalization (to preserve original classes declaration then because it seems that less code is better...) --- quicktile.py | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/quicktile.py b/quicktile.py index 59df585..861c29c 100755 --- a/quicktile.py +++ b/quicktile.py @@ -260,17 +260,18 @@ def fmt_row(row, pad=' ', indent=0, min_width=0): return ''.join(output) -def get_xdisplay(xdisplay=None): +def get_xdisplay_xroot(xdisplay=None): """ - Initializes C{WindowManager}. + Get a C{python-xlib} display handle with its Screen root. @param xdisplay: A C{python-xlib} display handle. @type xdisplay: C{Xlib.display.Display} - @rtype: Xlib.display.Display + @rtype: C{(Xlib.display.Display, Xlib.display.Display.Screen.root)} """ try: xdisp = xdisplay or Display() - return xdisp + xroot = xdisp.screen().root + return xdisp, xroot except (UnicodeDecodeError, DisplayConnectionError), err: raise XInitError("python-xlib failed with %s when asked to open" " a connection to the X server. Cannot bind keys." @@ -486,12 +487,10 @@ class WindowManager(object): # Prevent these temporary variables from showing up in the apidocs del _name, key, val - def __init__(self, xdisplay=None, screen=None, ignore_workarea=False, sticky_pointer=False): + def __init__(self, screen=None, ignore_workarea=False, sticky_pointer=False): """ Initializes C{WindowManager}. - @param xdisplay: A C{python-xlib} display handle. - @type xdisplay: C{Xlib.display.Display} @param screen: The X11 screen to operate on. If C{None}, the default screen as retrieved by C{gtk.gdk.screen_get_default} will be used. @type screen: C{gtk.gdk.Screen} @@ -503,9 +502,8 @@ def __init__(self, xdisplay=None, screen=None, ignore_workarea=False, sticky_poi It could possibly change while toggling "allow desktop icons" in KDE 3.x. (Not sure what would be equivalent elsewhere) """ - - self.xdisp = get_xdisplay(xdisplay) - self.xroot = self.xdisp.screen().root + + self.xdisp, self.xroot = get_xdisplay_xroot() self.gdk_screen = screen or gtk.gdk.screen_get_default() if self.gdk_screen is None: @@ -834,8 +832,7 @@ def __init__(self, xdisplay=None): @type xdisplay: C{Xlib.display.Display} """ - self.xdisp = get_xdisplay(xdisplay) - self.xroot = self.xdisp.screen().root + self.xdisp, self.xroot = get_xdisplay_xroot(xdisplay) self._keys = {} # Resolve these at runtime to avoid NameErrors @@ -1350,7 +1347,7 @@ def workspace_send_window(wm, win, state, motion): # pylint: disable=W0613 sticky_pointer = (config.getboolean('general', 'StickyPointer') or opts.sticky_pointer) try: - wm = WindowManager(xdisplay=get_xdisplay(), ignore_workarea=ignore_workarea, sticky_pointer=sticky_pointer) + wm = WindowManager(ignore_workarea=ignore_workarea, sticky_pointer=sticky_pointer) except XInitError as err: logging.critical(err) sys.exit(1) From 88a330dbff5ce6162cba6f002a2beb68407a822b Mon Sep 17 00:00:00 2001 From: jean Date: Sun, 13 Mar 2016 16:48:09 +0100 Subject: [PATCH 4/4] Minor config populating refactorization, allow for unlimited/non statically coded sections --- quicktile.py | 47 +++++++++++++++++++++++++---------------------- 1 file changed, 25 insertions(+), 22 deletions(-) diff --git a/quicktile.py b/quicktile.py index 861c29c..06ffb9c 100755 --- a/quicktile.py +++ b/quicktile.py @@ -160,7 +160,6 @@ def __call__(self, w, h, gravity='top-left', x=None, y=None): # Use Ctrl+Alt as the default base for key combinations 'ModMask': '', 'UseWorkarea': True, - 'StickyPointer': False, }, 'keys': { "KP_0" : "maximize", @@ -177,6 +176,12 @@ def __call__(self, w, h, gravity='top-left', x=None, y=None): "V" : "vertical-maximize", "H" : "horizontal-maximize", "C" : "move-to-center", + }, + 'misc': { + # Keep mouse pointer within currently handled window + 'StickyPointer': False, + # Keep currently handled window above the others + 'KeepAbove': False, } } #: Default content for the config file @@ -1294,17 +1299,25 @@ def workspace_send_window(wm, win, state, motion): # pylint: disable=W0613 config.read(cfg_path) dirty = False - if not config.has_section('general'): - config.add_section('general') - # Change this if you make backwards-incompatible changes to the - # section and key naming in the config file. - config.set('general', 'cfg_schema', 1) - dirty = True - - for key, val in DEFAULTS['general'].items(): - if not config.has_option('general', key): - config.set('general', key, str(val)) + for key, val in sorted(DEFAULTS.items()): + if config.has_section(key): + # Either load the keybindings or use and save the defaults + if key == 'keys': + keymap = dict(config.items('keys')) + else: + config.add_section(key) dirty = True + if key == 'general': + # Change this if you make backwards-incompatible changes to the + # section and key naming in the config file. + config.set('general', 'cfg_schema', 1) + elif key == 'keys': + keymap = DEFAULTS['keys'] + + for k, v in DEFAULTS[key].items(): + if not config.has_option(key, k): + config.set(key, k, str(v)) + dirty = True mk_raw = modkeys = config.get('general', 'ModMask') if ' ' in modkeys.strip() and '<' not in modkeys: @@ -1313,16 +1326,6 @@ def workspace_send_window(wm, win, state, motion): # pylint: disable=W0613 config.set('general', 'ModMask', modkeys) dirty = True - # Either load the keybindings or use and save the defaults - if config.has_section('keys'): - keymap = dict(config.items('keys')) - else: - keymap = DEFAULTS['keys'] - config.add_section('keys') - for row in keymap.items(): - config.set('keys', row[0], row[1]) - dirty = True - # Migrate from the deprecated syntax for punctuation keysyms for key in keymap: # Look up unrecognized shortkeys in a hardcoded dict and @@ -1344,7 +1347,7 @@ def workspace_send_window(wm, win, state, motion): # pylint: disable=W0613 ignore_workarea = ((not config.getboolean('general', 'UseWorkarea')) or opts.no_workarea) - sticky_pointer = (config.getboolean('general', 'StickyPointer') or opts.sticky_pointer) + sticky_pointer = (config.getboolean('misc', 'StickyPointer') or opts.sticky_pointer) try: wm = WindowManager(ignore_workarea=ignore_workarea, sticky_pointer=sticky_pointer)