Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a "Sticky mouse pointer" feature #62

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
130 changes: 95 additions & 35 deletions quicktile.py
Original file line number Diff line number Diff line change
Expand Up @@ -176,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

Expand Down Expand Up @@ -258,6 +264,26 @@ def fmt_row(row, pad=' ', indent=0, min_width=0):
output.extend(fmt_row(row, indent=1))

return ''.join(output)

def get_xdisplay_xroot(xdisplay=None):
"""
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: C{(Xlib.display.Display, Xlib.display.Display.Screen.root)}
"""
try:
xdisp = xdisplay or Display()
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."
"\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
Expand Down Expand Up @@ -466,7 +492,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}.

Expand All @@ -481,13 +507,17 @@ def __init__(self, screen=None, ignore_workarea=False):
It could possibly change while toggling "allow desktop icons"
in KDE 3.x. (Not sure what would be equivalent elsewhere)
"""

self.xdisp, self.xroot = get_xdisplay_xroot()

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"
" (bad DISPLAY value?)")

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):
Expand Down Expand Up @@ -671,6 +701,37 @@ def get_workspace(self, window=None, direction=None):

return nxt

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.
@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()

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),
keep_maximize=False, gravity=wnck.WINDOW_GRAVITY_NORTHWEST,
Expand Down Expand Up @@ -748,6 +809,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:
Expand All @@ -770,17 +836,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.xroot = self.xdisp.screen().root

self.xdisp, self.xroot = get_xdisplay_xroot(xdisplay)
self._keys = {}

# Resolve these at runtime to avoid NameErrors
Expand Down Expand Up @@ -947,7 +1004,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:
Expand Down Expand Up @@ -1213,6 +1270,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",
Expand All @@ -1239,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:
Expand All @@ -1258,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
Expand All @@ -1288,9 +1346,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('misc', '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)
Expand Down