"""
Window management for Windows.
"""
# This file is part of Echofilter.
#
# Copyright (C) 2020-2022 Scott C. Lowe and Offshore Energy Research Association (OERA)
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, version 3.
#
# 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
import re
from contextlib import contextmanager
from .. import ui
try:
import pywintypes
import win32com.client
import win32con
import win32gui
except ImportError:
from ..path import check_if_windows
if check_if_windows():
raise
import warnings
msg = "The Windows management module is only for Windows operating systems."
with ui.style.warning_message(msg) as msg:
warnings.warn(msg, category=RuntimeWarning)
__all__ = ["opencom", "WindowManager"]
[docs]class WindowManager:
"""
Encapsulates calls to window management using the Windows api.
Notes
-----
Based on: https://stackoverflow.com/a/2091530 and https://stackoverflow.com/a/4440622
"""
def __init__(self, title=None, class_name=None, title_pattern=None):
self.reset()
if title is not None:
self.find_window(class_name=class_name, title=title)
elif title_pattern is not None:
self.find_window_regex(title_pattern)
[docs] def reset(self):
"""Clear handle attribute state."""
self.handle = None
self.handles = []
self.handle_was_visible = {}
[docs] def check_handles_visible(self):
"""Check and remember which of self.handles are currently visible."""
for hwnd in self.handles:
self.handle_was_visible[hwnd] = win32gui.IsWindowVisible(hwnd)
[docs] def find_window(self, class_name=None, title=None):
"""Find a window by its exact title."""
self.reset()
handle = win32gui.FindWindow(class_name, title)
if handle == 0:
raise EnvironmentError(
"Couldn't find window with class={}, title={}.".format(
class_name, title
)
)
else:
self.handle = handle
self.handles = [handle]
self.check_handles_visible()
return self.handle
def _window_enum_callback(self, hwnd, pattern):
"""Pass to win32gui.EnumWindows() to check all the opened windows."""
if re.match(pattern, str(win32gui.GetWindowText(hwnd))) is not None:
self.handle = hwnd
self.handles.append(hwnd)
[docs] def find_window_regex(self, pattern):
"""Find a window whose title matches a regular expression."""
self.reset()
win32gui.EnumWindows(self._window_enum_callback, pattern)
if self.handle is None:
raise EnvironmentError(
"Couldn't find a window with title matching pattern {}.".format(pattern)
)
self.check_handles_visible()
return self.handles
[docs] def set_foreground(self):
"""Bring the window to the foreground."""
win32gui.SetForegroundWindow(self.handle)
[docs] def hide(self):
"""Hide the window."""
win32gui.ShowWindow(self.handle, win32con.SW_HIDE)
[docs] def hide_all(self):
"""Hide all the windows."""
self.handles_hidden = []
for hwnd in self.handles:
win32gui.ShowWindow(hwnd, win32con.SW_HIDE)
[docs] def show(self):
"""Show the window."""
win32gui.ShowWindow(self.handle, win32con.SW_SHOW)
[docs] def show_all(self, only_hidden=True):
"""Show all the windows."""
had_error = False
for hwnd in self.handles:
if only_hidden and not self.handle_was_visible[hwnd]:
# Handle was initially hidden, don't show this one
continue
try:
win32gui.ShowWindow(hwnd, win32con.SW_SHOW)
except Exception:
# Using invalid window handles doesn't give an error, so
# the try block should silently do nothing if the window
# was closed.
# But just in case, let's use a try/except block to catch
# anything that may come up.
print(
ui.style.warning_fmt(
"Could not unhide the '{}' window with handle {}.".format(
win32gui.GetWindowText(hwnd), hwnd
)
)
)
had_error = True
if had_error:
raise
[docs]@contextmanager
def opencom(
com_name,
can_make_anew=False,
title=None,
title_pattern=None,
minimize=False,
hide="never",
):
"""
Open a connection to an application with a COM object.
The application may or may not be open before this context begins. If it
was not already open, the application is closed when leaving the context.
Parameters
----------
com_name : str
Name of COM object to dispatch.
can_make_anew : bool, optional
Whether arbitrarily many sessions of the COM object can be created, and
if so whether they should be. Default is ``False``, in which case the
context manager will check to see if the application is already running
before connecting to it. If it was already running, it will not be
closed when this context closes.
title : str, optional
Exact title of window. If the title can not be determined exactly, use
``title_pattern`` instead.
title_pattern : str, optional
Regular expression for the window title.
minimize : bool, optional
If ``True``, the application will be minimized while the code runs.
Default is ``False``.
hide : {"never", "new", "always"}, optional
Whether to hide the application window entirely. Default is ``"never"``.
If this is enabled, at least one of ``title`` and ``title_pattern`` must
be specified. If ``hide="new"``, the application is only hidden if it
was created by this context, and not if it was already running.
If ``hide="always"``, the application is hidden even if it was already
running. In the latter case, the window will be revealed again when
leaving this context.
Yields
------
win32com.gen_py
Interface to COM object.
"""
HIDE_OPTIONS = {"never", "new", "always"}
if hide not in HIDE_OPTIONS:
raise ValueError(
"Unsupported hide value: {}. Must be one of {}".format(hide, HIDE_OPTIONS)
)
make_anew = can_make_anew
if not make_anew:
# Try to fetch existing session
try:
app = win32com.client.GetActiveObject(com_name)
existing_session = True
except pywintypes.com_error:
# No existing session, make a new session
make_anew = True
if make_anew:
# Create a new session
existing_session = False
app = win32com.client.Dispatch(com_name)
was_minimized = False
if minimize:
# Tell the app to minimize itself
try:
app.Minimize()
was_minimized = True
except Exception:
print(ui.style.warning_fmt("Could not minimize {} window".format(com_name)))
was_hidden = False
if hide == "always" or (hide == "new" and not existing_session):
# Find handle to the app based on the window title, and hide it
winman = WindowManager()
if title is None and title_pattern is None:
raise ValueError(
"One of the arguments title or title_pattern must be set in"
" order to hide the window."
)
if title is not None:
try:
winman.find_window(title=title)
except Exception:
pass
if winman.handle is None and title_pattern is not None:
try:
winman.find_window_regex(title_pattern)
except Exception:
pass
if len(winman.handles) == 0:
print(
ui.style.warning_fmt(
"Could not hide {} window with title {}".format(
com_name, title if title_pattern is None else title_pattern
)
)
)
else:
winman.hide_all()
was_hidden = True
try:
yield app
finally:
# As we leave the context, fix the state of the app to how it was
# before we started
if was_hidden:
# Try to show all windows that we hid before
try:
winman.show_all(only_hidden=True)
except Exception:
print(ui.style.warning_fmt("Error unhiding window(s)."))
try:
command = ""
if not existing_session:
# If we opened it, tell the application to quit
command = "exit"
app.Quit()
elif was_minimized:
# Restore the window from being minimised
command = "restore"
app.Restore()
except Exception:
# We'll get an error if the application was already closed, etc
print(
ui.style.warning_fmt(
"Could not {} the {} window with handle {}.".format(
command, com_name, app
)
)
)