# Copyright (C) 2011 Chris Dekter
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""Basic window management. Requies C{wmctrl} and C{xrandr} to be installed."""
import re
import subprocess
import time
# this regex extracts the pertinant data from wmctrl output, see test_window.py for more info
WMCTRL_GEOM_REGEX = r"^(0x[0-9a-fA-F]{8})\s{1,}(\d*)\s{1,}(\d*)\s{1,}(\d{1,})\s{1,}(\d{1,})\s{1,}(\d{1,})\s{1,}(.*?)\s{1,}(.*?)$"
XRANDR_MONITOR_REGEX = r" (\d{3,4}).*?x(\d{3,4})\/.*?\+(\d{1,4})\+(\d{1,4})"
#Example Output:
#Monitors: 2
# 0: +*HDMI-0 1920/521x1080/293+0+0 HDMI-0
# 1: +DVI-D-0 1440/408x900/255+1920+0 DVI-D-0
# Regex gets ____ and ____ ____ _
# this translates to monitor x and y size, and x and y offset for each monitor
[docs]
class Window:
"""
Basic window management using wmctrl
Note: in all cases where a window title is required (with the exception of wait_for_focus()),
two special values of window title are permitted:
"""
def __init__(self, mediator):
self.mediator = mediator
[docs]
def wait_for_focus(self, title, timeOut=5):
"""
Wait for window with the given title to have focus
Usage: C{window.wait_for_focus(title, timeOut=5)}
If the window becomes active, returns True. Otherwise, returns False if
the window has not become active by the time the timeout has elapsed.
:param title: title to match against (as a regular expression)
:param timeOut: period (seconds) to wait before giving up
:rtype: boolean
"""
regex = re.compile(title)
waited = 0
while waited <= timeOut:
if regex.match(self.mediator.windowInterface.get_window_title()):
return True
if timeOut == 0:
break # zero length timeout, if not matched go straight to end
time.sleep(0.3)
waited += 0.3
return False
[docs]
def wait_for_exist(self, title, timeOut=5, by_hex=False):
"""
Wait for window with the given title to be created
Usage: C{window.wait_for_exist(title, timeOut=5)}
If the window is in existence, returns True. Otherwise, returns False if the window has not been created by the time the timeout has elapsed.
:param title: title to match against (as a regular expression)
:param timeOut: period (seconds) to wait before giving up
:param by_hex: If true, C{wmctrl} will interpret the C{title} as a hexid
:rtype: boolean
"""
waited = 0
while waited <= timeOut:
windowList = self.get_window_list()
for window in windowList:
if by_hex:
if title == window[0]: return True
else:
if title == window[3]: return True
if timeOut == 0:
break # zero length timeout, if not matched go straight to end
time.sleep(0.3)
waited += 0.3
return False
[docs]
def activate(self, title, switchDesktop=False, matchClass=False, by_hex=False):
"""
Activate the specified window, giving it input focus
Usage: C{window.activate(title, switchDesktop=False, matchClass=False)}
If switchDesktop is False (default), the window will be moved to the current desktop and activated. Otherwise, switch to the window's current desktop and activate it there.
:param title: window title to match against (as case-insensitive substring match)
:param switchDesktop: whether or not to switch to the window's current desktop
:param matchClass: if True, match on the window class instead of the title
:param by_hex: If true, C{wmctrl} will interpret the C{title} as a hexid
"""
if switchDesktop:
xArgs = ["-a", title]
else:
xArgs = ["-R", title]
if by_hex:
xArgs += ["-i"] # use hex id instead of window title
if matchClass:
xArgs += ["-x"]
self._run_wmctrl(xArgs)
[docs]
def close(self, title, matchClass=False, by_hex=False):
"""
Close the specified window gracefully
Usage: C{window.close(title, matchClass=False)}
:param title: window title to match against (as case-insensitive substring match)
:param matchClass: if True, match on the window class instead of the title
:param by_hex: If true, C{wmctrl} will interpret the C{title} as a hexid
"""
xArgs = ["-c", title]
if matchClass:
xArgs += ["-x"]
if by_hex:
xArgs += ["-i"]
self._run_wmctrl(xArgs)
[docs]
def resize_move(self, title, xOrigin=-1, yOrigin=-1, width=-1, height=-1, matchClass=False, by_hex=False):
"""
Resize and/or move the specified window
Usage: C{window.resize_move(title, xOrigin=-1, yOrigin=-1, width=-1, height=-1, matchClass=False)}
Leaving any of the position/dimension values as the default (-1) will cause that
value to be left unmodified.
:param title: window title to match against (as case-insensitive substring match)
:param xOrigin: new x origin of the window (upper left corner)
:param yOrigin: new y origin of the window (upper left corner)
:param width: new width of the window
:param height: new height of the window
:param matchClass: if C{True}, match on the window class instead of the title
:param by_hex: If true, C{wmctrl} will interpret the C{title} as a hexid
"""
mvArgs = ["0", str(xOrigin), str(yOrigin), str(width), str(height)]
xArgs = []
if matchClass:
xArgs += ["-x"]
if by_hex:
xArgs += ["-i"]
self._run_wmctrl(["-r", title, "-e", ','.join(mvArgs)] + xArgs)
[docs]
def move_to_desktop(self, title, deskNum, matchClass=False, by_hex=False):
"""
Move the specified window to the given desktop
Usage: C{window.move_to_desktop(title, deskNum, matchClass=False)}
:param title: window title to match against (as case-insensitive substring match)
:param deskNum: desktop to move the window to (note: zero based)
:param matchClass: if True, match on the window class instead of the title
:param by_hex: If true, C{wmctrl} will interpret the C{title} as a hexid
"""
xArgs = []
if matchClass:
xArgs += ["-x"]
if by_hex:
xArgs += ["-i"]
self._run_wmctrl(["-r", title, "-t", str(deskNum)] + xArgs)
[docs]
def switch_desktop(self, deskNum):
"""
Switch to the specified desktop
Usage: C{window.switch_desktop(deskNum)}
:param deskNum: desktop to switch to (note: zero based)
"""
self._run_wmctrl(["-s", str(deskNum)])
[docs]
def set_property(self, title, action, prop, matchClass=False, by_hex=False):
"""
Set a property on the given window using the specified action
Usage: C{window.set_property(title, action, prop, matchClass=False)}
Allowable actions:
- add
- remove
- toggle
Allowable properties:
- modal
- sticky
- maximized_vert
- maximized_horz
- shaded
- skip_taskbar
- skip_pager
- hidden
- fullscreen
- above
:param title: window title to match against (as case-insensitive substring match)
:param action: one of the actions listed above
:param prop: one of the properties listed above
:param matchClass: if True, match on the window class instead of the title
:param by_hex: If true, C{wmctrl} will interpret the C{title} as a hexid
"""
xArgs = []
if matchClass:
xArgs += ["-x"]
if by_hex:
xArgs += ["-i"]
self._run_wmctrl(["-r", title, "-b" + action + ',' + prop] + xArgs)
[docs]
def get_active_geometry(self):
"""
Get the geometry of the currently active window. Uses the C{:ACTIVE:} function of C{wmctrl}.
Usage: C{window.get_active_geometry()}
:return: a 4-tuple containing the x-origin, y-origin, width and height of the window (in pixels)
:rtype: C{tuple(int, int, int, int)}
"""
#wrapper for get_window_geometry()
return self.get_window_geometry(":ACTIVE:")
[docs]
def get_active_title(self):
"""
Get the visible title of the currently active window
Usage: C{window.get_active_title()}
:return: the visible title of the currentle active window
:rtype: C{str}
"""
return self.mediator.windowInterface.get_window_title()
[docs]
def get_active_class(self):
"""
Get the class of the currently active window
Usage: C{window.get_active_class()}
:return: the class of the currently active window
:rtype: C{str}
"""
return self.mediator.windowInterface.get_window_class()
[docs]
def center_window(self, title=":ACTIVE:", win_width=None, win_height=None, monitor=0, matchClass=False, by_hex=False):
"""
Centers the active (or window selected by title) window. Requires xrandr for getting monitor sizes and offsets.
:param title: Title of the window to center (defaults to using the active window)
:param win_width: Width of the centered window, defaults to screenx/3. Use -1 to center without size change.
:param win_height: Height of the centered window, defaults to screeny/3. Use -1 to center without size change.
:param monitor: Monitor number (0 is primary, listed via C{xrandr --listactivemonitors} etc.)
:param matchClass: if True, match on the window class instead of the title
:raises ValueError: If title or desktop is not found by wmctrl
:param by_hex: If true, C{wmctrl} will interpret the C{title} as a hexid
"""
#could also use Gdk.Display.get_default().get_montiors etc.
#Used xrandr for ease of cross Gtk/Qt use, wayland might require an alternate implementation
returncode, output = self._run_xrandr(["--listactivemonitors"])
matches = re.findall(XRANDR_MONITOR_REGEX, output, re.MULTILINE)
width = int(matches[monitor][0])
height = int(matches[monitor][1])
x_offset = int(matches[monitor][2])
y_offset = int(matches[monitor][3])
#work backwards from the size of the window
if win_width is None:
win_width = round(width/2)
if win_height is None:
win_height = round(height/2)
top_x = round((width-win_width)/2)
top_y = round((height-win_height)/2)
#remove maximized attributes from window
self.set_property(title, "remove", "maximized_vert", matchClass=matchClass, by_hex=by_hex)
self.set_property(title, "remove", "maximized_horz", matchClass=matchClass, by_hex=by_hex)
#resize and move window
self.resize_move(title, x_offset+top_x, y_offset+top_y, win_width, win_height, matchClass=matchClass, by_hex=by_hex)
def _run_xrandr(self, args):
try:
with subprocess.Popen(["xrandr"] + args, stdout=subprocess.PIPE) as p:
output = p.communicate()[0].decode()[:-1]
returncode = p.returncode
except FileNotFoundError:
return 1, "ERROR: Please install xrandr"
return returncode, output
def _run_wmctrl(self, args):
try:
with subprocess.Popen(["wmctrl"] + args, stdout=subprocess.PIPE) as p:
output = p.communicate()[0].decode()[:-1] # Drop trailing newline
returncode = p.returncode
except FileNotFoundError:
return 1, 'ERROR: Please install wmctrl'
return returncode, output
[docs]
def get_window_list(self, filter_desktop=-1):
"""
Returns a list of windows matching an optional desktop filter, requires C{wmctrl}!
Each list item consists of: C{[hexid, desktop, hostname, title]}
Where the C{hexid} is the ID used for some other functions (like L{import -window} from ImageMagick).
C{desktop} is the number of which desktop (sometimes called workspaces) the window appears upon.
C{hostname} is the hostname of your computer.
C{title} is the title that you would usually see in your window manager of choice.
:param filter_desktop: String, (usually 0-n) to filter the windows by. Any window not on the given desktop will not be returned.
:return: C{[[hexid1, desktop1, hostname1, title1], [hexid2,desktop2,hostname2,title2], ...etc]} Returns C{[]} if no windows are found.
"""
returncode, output = self._run_wmctrl(["-lG"])
matches = re.findall(WMCTRL_GEOM_REGEX, output, re.MULTILINE)
output = []
for match in matches:
hexid = match[0]
desktop = match[1]
hostname = match[6]
window_title = match[7]
if filter_desktop==desktop:
continue
output.append((hexid,desktop,hostname, window_title))
return output
[docs]
def get_window_hex(self, title):
"""
Returns the hexid of the first window to match title.
:param title: Window title to match for returning hexid
:return: Returns hexid of the window to be used for other functions See L{get_window_geom}, L{visgrep_by_window_id}
Returns C{None} if no matches are found
"""
windowList = self.get_window_list()
for window in windowList:
if title in window[3]:
return window[0]
return None
[docs]
def get_window_geometry(self, title, by_hex=False):
"""
Uses C{wmctrl} to return the window geometry of the given window title. Returns where the location of the
top left hand corner of the window is and the width/height of the window.
:param title
:return: C{[offsetx, offsety, sizex, sizey]} Returns none if no matches are found
"""
index = -1 # by default use the window title for matching
if by_hex:
index = 0
if title == ":ACTIVE:":
if by_hex:
title = self.get_window_hex(self.get_active_title())
else:
title = self.get_active_title()
returncode, output = self._run_wmctrl(["-lG"])
matches = re.findall(WMCTRL_GEOM_REGEX, output, re.MULTILINE)
for match in matches:
if match[index] == title:
# trim hexid, gravity from front of list and the window title from end of list
# convert to ints and return
return list(map(int, match[2:-2]))
return None