Skip to content

Commit

Permalink
Start to refactor the workarea-calculation code
Browse files Browse the repository at this point in the history
First step in resolving #65.
  • Loading branch information
Stephan Sokolow authored and Stephan Sokolow committed Aug 21, 2017
1 parent 1f972b3 commit 31b8144
Show file tree
Hide file tree
Showing 2 changed files with 120 additions and 92 deletions.
3 changes: 1 addition & 2 deletions quicktile/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,7 @@ def wrapper(winman, window=None, *args, **kwargs):

monitor_id, monitor_geom = winman.get_monitor(window)

use_area, use_rect = winman.get_workarea(
monitor_geom, winman.ignore_workarea)
use_area, use_rect = winman.workarea.get(monitor_geom)

# TODO: Replace this MPlayer safety hack with a properly
# comprehensive exception catcher.
Expand Down
209 changes: 119 additions & 90 deletions quicktile/wm.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,123 @@

# ---

class WorkArea(object):
"""Helper to calculate and query available workarea on the desktop."""
def __init__(self, gdk_screen, ignore_struts=False):
self.gdk_screen = gdk_screen
self.ignore_struts = ignore_struts

def get_monitor_rect(self, monitor):
"""Helper to normalize various monitor identifiers."""
if isinstance(monitor, int):
usable_rect = self.gdk_screen.get_monitor_geometry(monitor)
logging.debug("Retrieved geometry %s for monitor #%s",
usable_rect, monitor)
elif not isinstance(monitor, gtk.gdk.Rectangle):
logging.debug("Converting geometry %s to gtk.gdk.Rectangle",
monitor)
usable_rect = gtk.gdk.Rectangle(monitor)
else:
usable_rect = monitor

usable_region = gtk.gdk.region_rectangle(usable_rect)
if not usable_region.get_rectangles():
logging.error("WorkArea.get_monitor_rect received "
"an empty monitor region!")
return None, None

return usable_rect, usable_region

def get_struts(self, root_win):
"""Retrieve the struts from the root window if supported."""
if not self.gdk_screen.supports_net_wm_hint("_NET_WM_STRUT_PARTIAL"):
return []

# Gather all struts
struts = [root_win.property_get("_NET_WM_STRUT_PARTIAL")]
if self.gdk_screen.supports_net_wm_hint("_NET_CLIENT_LIST"):
# Source: http://stackoverflow.com/a/11332614/435253
for wid in root_win.property_get('_NET_CLIENT_LIST')[2]:
w = gtk.gdk.window_foreign_new(wid)
struts.append(w.property_get("_NET_WM_STRUT_PARTIAL"))
struts = [x[2] for x in struts if x]

logging.debug("Gathered _NET_WM_STRUT_PARTIAL values:\n\t%s",
struts)
return struts

def subtract_struts(self, usable_region, struts):
"""Subtract the given struts from the given region."""

# Subtract the struts from the usable region
_Sub = lambda *g: usable_region.subtract(
gtk.gdk.region_rectangle(g))
_w, _h = self.gdk_screen.get_width(), self.gdk_screen.get_height()
for g in struts: # pylint: disable=invalid-name
# http://standards.freedesktop.org/wm-spec/1.5/ar01s05.html
# XXX: Must not cache unless watching for notify events.
_Sub(0, g[4], g[0], g[5] - g[4] + 1) # left
_Sub(_w - g[1], g[6], g[1], g[7] - g[6] + 1) # right
_Sub(g[8], 0, g[9] - g[8] + 1, g[2]) # top
_Sub(g[10], _h - g[3], g[11] - g[10] + 1, g[3]) # bottom

# Generate a more restrictive version used as a fallback
usable_rect = usable_region.copy()
_Sub = lambda *g: usable_rect.subtract(gtk.gdk.region_rectangle(g))
for geom in struts:
# http://standards.freedesktop.org/wm-spec/1.5/ar01s05.html
# XXX: Must not cache unless watching for notify events.
_Sub(0, geom[4], geom[0], _h) # left
_Sub(_w - geom[1], geom[6], geom[1], _h) # right
_Sub(0, 0, _w, geom[2]) # top
_Sub(0, _h - geom[3], _w, geom[3]) # bottom
# TODO: The required "+ 1" in certain spots confirms that we're
# going to need unit tests which actually check that the
# WM's code for constraining windows to the usable area
# doesn't cause off-by-one bugs.
# TODO: Share this on http://stackoverflow.com/q/2598580/435253
return usable_rect.get_clipbox(), usable_region

def get(self, monitor, ignore_struts=None):
"""Retrieve the usable area of the specified monitor using
the most expressive method the window manager supports.
@param monitor: The number or dimensions of the desired monitor.
@param ignore_struts: If C{True}, just return the size of the whole
monitor, allowing windows to overlap panels.
@type monitor: C{int} or C{gtk.gdk.Rectangle}
@type ignore_struts: C{bool}
@returns: The usable region and its largest rectangular subset.
@rtype: C{gtk.gdk.Region}, C{gtk.gdk.Rectangle}
"""

usable_rect, usable_region = self.get_monitor_rect(monitor)

if ignore_struts or (ignore_struts is None and self.ignore_struts):
logging.debug("Panels ignored. Reported monitor geometry is:\n%s",
usable_rect)
return usable_region, usable_rect

root_win = self.gdk_screen.get_root_window()

struts = self.get_struts(root_win)
if struts:
usable_rect, usable_region = self.subtract_struts(usable_region,
struts)
elif self.gdk_screen.supports_net_wm_hint("_NET_WORKAREA"):
desktop_geo = tuple(root_win.property_get('_NET_WORKAREA')[2][0:4])
logging.debug("Falling back to _NET_WORKAREA: %s", desktop_geo)
usable_region.intersect(gtk.gdk.region_rectangle(desktop_geo))
usable_rect = usable_region.get_clipbox()

# FIXME: Only call get_rectangles if --debug
logging.debug("Usable region of monitor calculated as:\n"
"\tRegion: %r\n\tRectangle: %r",
usable_region.get_rectangles(), usable_rect)
return usable_region, usable_rect


class WindowManager(object):
"""A simple API-wrapper class for manipulating window positioning."""

Expand All @@ -61,7 +178,8 @@ def __init__(self, screen=None, ignore_workarea=False):

# pylint: disable=no-member
self.screen = wnck.screen_get(self.gdk_screen.get_number())
self.ignore_workarea = ignore_workarea
self.workarea = WorkArea(self.gdk_screen,
ignore_struts=ignore_workarea)

@classmethod
def calc_win_gravity(cls, geom, gravity):
Expand Down Expand Up @@ -120,95 +238,6 @@ def get_monitor(self, win):
monitor_id, monitor_geom)
return monitor_id, monitor_geom

def get_workarea(self, monitor, ignore_struts=False):
"""Retrieve the usable area of the specified monitor using
the most expressive method the window manager supports.
@param monitor: The number or dimensions of the desired monitor.
@param ignore_struts: If C{True}, just return the size of the whole
monitor, allowing windows to overlap panels.
@type monitor: C{int} or C{gtk.gdk.Rectangle}
@type ignore_struts: C{bool}
@returns: The usable region and its largest rectangular subset.
@rtype: C{gtk.gdk.Region}, C{gtk.gdk.Rectangle}
"""
if isinstance(monitor, int):
usable_rect = self.gdk_screen.get_monitor_geometry(monitor)
logging.debug("Retrieved geometry %s for monitor #%s",
usable_rect, monitor)
elif not isinstance(monitor, gtk.gdk.Rectangle):
logging.debug("Converting geometry %s to gtk.gdk.Rectangle",
monitor)
usable_rect = gtk.gdk.Rectangle(monitor)
else:
usable_rect = monitor

usable_region = gtk.gdk.region_rectangle(usable_rect)
if not usable_region.get_rectangles():
logging.error("get_workarea received an empty monitor region!")

if ignore_struts:
logging.debug("Panels ignored. Reported monitor geometry is:\n%s",
usable_rect)
return usable_region, usable_rect

root_win = self.gdk_screen.get_root_window()

struts = []
if self.gdk_screen.supports_net_wm_hint("_NET_WM_STRUT_PARTIAL"):
# Gather all struts
struts.append(root_win.property_get("_NET_WM_STRUT_PARTIAL"))
if self.gdk_screen.supports_net_wm_hint("_NET_CLIENT_LIST"):
# Source: http://stackoverflow.com/a/11332614/435253
for wid in root_win.property_get('_NET_CLIENT_LIST')[2]:
w = gtk.gdk.window_foreign_new(wid)
struts.append(w.property_get("_NET_WM_STRUT_PARTIAL"))
struts = [x[2] for x in struts if x]

logging.debug("Gathered _NET_WM_STRUT_PARTIAL values:\n\t%s",
struts)

# Subtract the struts from the usable region
_Sub = lambda *g: usable_region.subtract(
gtk.gdk.region_rectangle(g))
_w, _h = self.gdk_screen.get_width(), self.gdk_screen.get_height()
for g in struts: # pylint: disable=invalid-name
# http://standards.freedesktop.org/wm-spec/1.5/ar01s05.html
# XXX: Must not cache unless watching for notify events.
_Sub(0, g[4], g[0], g[5] - g[4] + 1) # left
_Sub(_w - g[1], g[6], g[1], g[7] - g[6] + 1) # right
_Sub(g[8], 0, g[9] - g[8] + 1, g[2]) # top
_Sub(g[10], _h - g[3], g[11] - g[10] + 1, g[3]) # bottom

# Generate a more restrictive version used as a fallback
usable_rect = usable_region.copy()
_Sub = lambda *g: usable_rect.subtract(gtk.gdk.region_rectangle(g))
for geom in struts:
# http://standards.freedesktop.org/wm-spec/1.5/ar01s05.html
# XXX: Must not cache unless watching for notify events.
_Sub(0, geom[4], geom[0], _h) # left
_Sub(_w - geom[1], geom[6], geom[1], _h) # right
_Sub(0, 0, _w, geom[2]) # top
_Sub(0, _h - geom[3], _w, geom[3]) # bottom
# TODO: The required "+ 1" in certain spots confirms that we're
# going to need unit tests which actually check that the
# WM's code for constraining windows to the usable area
# doesn't cause off-by-one bugs.
# TODO: Share this on http://stackoverflow.com/q/2598580/435253
usable_rect = usable_rect.get_clipbox()
elif self.gdk_screen.supports_net_wm_hint("_NET_WORKAREA"):
desktop_geo = tuple(root_win.property_get('_NET_WORKAREA')[2][0:4])
logging.debug("Falling back to _NET_WORKAREA: %s", desktop_geo)
usable_region.intersect(gtk.gdk.region_rectangle(desktop_geo))
usable_rect = usable_region.get_clipbox()

# FIXME: Only call get_rectangles if --debug
logging.debug("Usable region of monitor calculated as:\n"
"\tRegion: %r\n\tRectangle: %r",
usable_region.get_rectangles(), usable_rect)
return usable_region, usable_rect

def get_workspace(self, window=None, direction=None):
"""Get a workspace relative to either a window or the active one.
Expand Down

0 comments on commit 31b8144

Please sign in to comment.