# 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} to be installed."""
import re
import subprocess
import time
[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:
:ACTIVE: - select the currently active window
:SELECT: - select the desired window by clicking on it
"""
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.interface.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):
"""
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
@rtype: boolean
"""
regex = re.compile(title)
waited = 0
while waited <= timeOut:
retCode, output = self._run_wmctrl(["-l"])
for line in output.split('\n'):
if regex.match(line[14:].split(' ', 1)[-1]):
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):
"""
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
"""
if switchDesktop:
args = ["-a", title]
else:
args = ["-R", title]
if matchClass:
args += ["-x"]
self._run_wmctrl(args)
[docs]
def close(self, title, matchClass=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
"""
if matchClass:
self._run_wmctrl(["-c", title, "-x"])
else:
self._run_wmctrl(["-c", title])
[docs]
def resize_move(self, title, xOrigin=-1, yOrigin=-1, width=-1, height=-1, matchClass=False):
"""
Resize and/or move the specified window
Usage: C{window.close(title, xOrigin=-1, yOrigin=-1, width=-1, height=-1, matchClass=False)}
Leaving and 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 True, match on the window class instead of the title
"""
mvArgs = ["0", str(xOrigin), str(yOrigin), str(width), str(height)]
if matchClass:
xArgs = ["-x"]
else:
xArgs = []
self._run_wmctrl(["-r", title, "-e", ','.join(mvArgs)] + xArgs)
[docs]
def move_to_desktop(self, title, deskNum, matchClass=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
"""
if matchClass:
xArgs = ["-x"]
else:
xArgs = []
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):
"""
Set a property on the given window using the specified action
Usage: C{window.set_property(title, action, prop, matchClass=False)}
Allowable actions: C{add, remove, toggle}
Allowable properties: C{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
"""
if matchClass:
xArgs = ["-x"]
else:
xArgs = []
self._run_wmctrl(["-r", title, "-b" + action + ',' + prop] + xArgs)
[docs]
def get_active_geometry(self):
"""
Get the geometry of the currently active window
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)}
"""
active = self.mediator.interface.get_window_title()
result, output = self._run_wmctrl(["-l", "-G"])
matchingLine = None
for line in output.split('\n'):
if active in line[34:].split(' ', 1)[-1]:
matchingLine = line
if matchingLine is not None:
output = matchingLine.split()[2:6]
# return [int(x) for x in output]
return list(map(int, output))
else:
return None
[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.interface.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 currentle active window
@rtype: C{str}
"""
return self.mediator.interface.get_window_class()
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