diff --git a/src/pygetwindow/__init__.py b/src/pygetwindow/__init__.py index ad952dc..deaf63a 100644 --- a/src/pygetwindow/__init__.py +++ b/src/pygetwindow/__init__.py @@ -1,26 +1,12 @@ # PyGetWindow # A cross-platform module to find information about the windows on the screen. - # Work in progress -# Useful info: -# https://stackoverflow.com/questions/373020/finding-the-current-active-window-in-mac-os-x-using-python -# https://stackoverflow.com/questions/7142342/get-window-position-size-with-python - - -# win32 api and ctypes on Windows -# cocoa api and pyobjc on Mac -# Xlib on linux - - -# Possible Future Features: -# get/click menu (win32: GetMenuItemCount, GetMenuItemInfo, GetMenuItemID, GetMenu, GetMenuItemRect) - - __version__ = "0.0.9" -import sys, collections, pyrect - +import sys +import collections +import pyrect class PyGetWindowException(Exception): """ @@ -96,7 +82,7 @@ def maximize(self): raise NotImplementedError def restore(self): - """If maximized or minimized, restores the window to it's normal size.""" + """If maximized or minimized, restores the window to its normal size.""" raise NotImplementedError def activate(self): @@ -150,7 +136,6 @@ def left(self): @left.setter def left(self, value): - # import pdb; pdb.set_trace() self._rect.left # Run rect's onRead to update the Rect object. self._rect.left = value @@ -326,12 +311,14 @@ def box(self, value): self._rect.box = value +# Platform-specific imports if sys.platform == "darwin": - # raise NotImplementedError('PyGetWindow currently does not support macOS. If you have Appkit/Cocoa knowledge, please contribute! https://github.com/asweigart/pygetwindow') # TODO - implement mac + # macOS support from ._pygetwindow_macos import * - Window = MacOSWindow + elif sys.platform == "win32": + # Windows support from ._pygetwindow_win import ( Win32Window, getActiveWindow, @@ -341,9 +328,22 @@ def box(self, value): getAllWindows, getAllTitles, ) - Window = Win32Window + +elif sys.platform.startswith("linux"): + # Linux support + from ._pygetwindow_linux import ( + LinuxWindow, + getActiveWindow, + getWindowsWithTitle, + getAllWindows, + getAllTitles, + ) + Window = LinuxWindow + else: raise NotImplementedError( - "PyGetWindow currently does not support Linux. If you have Xlib knowledge, please contribute! https://github.com/asweigart/pygetwindow" - ) + "PyGetWindow currently does not support this platform. " + "If you have knowledge of the platform's windowing system, please contribute! " + "https://github.com/asweigart/pygetwindow" + ) \ No newline at end of file diff --git a/src/pygetwindow/_base.py b/src/pygetwindow/_base.py new file mode 100644 index 0000000..a2fd3ef --- /dev/null +++ b/src/pygetwindow/_base.py @@ -0,0 +1,105 @@ +# _base.py +import collections + +Rect = collections.namedtuple("Rect", "left top right bottom") +Point = collections.namedtuple("Point", "x y") +Size = collections.namedtuple("Size", "width height") + + +class BaseWindow: + def __init__(self): + pass + + def _setupRectProperties(self): + def _onRead(attrName): + r = self._getWindowRect() + self._rect._left = r.left # Setting _left directly to skip the onRead. + self._rect._top = r.top # Setting _top directly to skip the onRead. + self._rect._width = r.right - r.left # Setting _width directly to skip the onRead. + self._rect._height = r.bottom - r.top # Setting _height directly to skip the onRead. + + def _onChange(oldBox, newBox): + self.moveTo(newBox.left, newBox.top) + self.resizeTo(newBox.width, newBox.height) + + r = self._getWindowRect() + self._rect = pyrect.Rect(r.left, r.top, r.right - r.left, r.bottom - r.top, onChange=_onChange, onRead=_onRead) + + def _getWindowRect(self): + raise NotImplementedError + + def __str__(self): + r = self._getWindowRect() + width = r.right - r.left + height = r.bottom - r.top + return '<%s left="%s", top="%s", width="%s", height="%s", title="%s">' % ( + self.__class__.__qualname__, + r.left, + r.top, + width, + height, + self.title, + ) + + def close(self): + """Closes this window. This may trigger "Are you sure you want to + quit?" dialogs or other actions that prevent the window from + actually closing. This is identical to clicking the X button on the + window.""" + raise NotImplementedError + + def minimize(self): + """Minimizes this window.""" + raise NotImplementedError + + def maximize(self): + """Maximizes this window.""" + raise NotImplementedError + + def restore(self): + """If maximized or minimized, restores the window to its normal size.""" + raise NotImplementedError + + def activate(self): + """Activate this window and make it the foreground window.""" + raise NotImplementedError + + def resizeRel(self, widthOffset, heightOffset): + """Resizes the window relative to its current size.""" + raise NotImplementedError + + def resizeTo(self, newWidth, newHeight): + """Resizes the window to a new width and height.""" + raise NotImplementedError + + def moveRel(self, xOffset, yOffset): + """Moves the window relative to its current position.""" + raise NotImplementedError + + def moveTo(self, newLeft, newTop): + """Moves the window to new coordinates on the screen.""" + raise NotImplementedError + + @property + def isMinimized(self): + """Returns True if the window is currently minimized.""" + raise NotImplementedError + + @property + def isMaximized(self): + """Returns True if the window is currently maximized.""" + raise NotImplementedError + + @property + def isActive(self): + """Returns True if the window is currently the active, foreground window.""" + raise NotImplementedError + + @property + def title(self): + """Returns the window title as a string.""" + raise NotImplementedError + + @property + def visible(self): + raise NotImplementedError \ No newline at end of file diff --git a/src/pygetwindow/_pygetwindow_linux.py b/src/pygetwindow/_pygetwindow_linux.py new file mode 100644 index 0000000..ed65c3b --- /dev/null +++ b/src/pygetwindow/_pygetwindow_linux.py @@ -0,0 +1,135 @@ +import sys +from collections import namedtuple +from Xlib import X, display, protocol +from Xlib.ext import randr +from ._base import BaseWindow, Rect, Point, Size + +# Definition of basic data structures +Rect = namedtuple("Rect", "left top right bottom") +Point = namedtuple("Point", "x y") +Size = namedtuple("Size", "width height") + +class LinuxWindow(BaseWindow): + def __init__(self, hwnd): + self._hwnd = hwnd # Window identifier (Window ID) + self._display = display.Display() # Connection to the X server + self._setupRectProperties() + + def _getWindowRect(self): + """Gets the window rectangle (left, top, right, bottom).""" + geom = self._display.create_resource_object('window', self._hwnd).get_geometry() + return Rect(geom.x, geom.y, geom.x + geom.width, geom.y + geom.height) + + def close(self): + """Closes the window.""" + self._send_event(self._hwnd, protocol.event.ClientMessage, + data=(32, [self._display.intern_atom('WM_DELETE_WINDOW'), X.CurrentTime, 0])) + + def minimize(self): + """Minimizes the window.""" + self._change_state(self._display.intern_atom('_NET_WM_STATE_HIDDEN')) + + def maximize(self): + """Maximizes the window.""" + self._change_state(self._display.intern_atom('_NET_WM_STATE_MAXIMIZED_VERT'), + self._display.intern_atom('_NET_WM_STATE_MAXIMIZED_HORZ')) + + def restore(self): + """Restores the window from minimized/maximized state.""" + self._change_state(self._display.intern_atom('_NET_WM_STATE_NORMAL')) + + def activate(self): + """Activates the window.""" + self._send_event(self._hwnd, protocol.event.ClientMessage, + data=(32, [self._display.intern_atom('_NET_ACTIVE_WINDOW'), X.CurrentTime, 0])) + + def resizeRel(self, widthOffset, heightOffset): + """Resizes the window relative to its current size.""" + rect = self._getWindowRect() + self.resizeTo(rect.width + widthOffset, rect.height + heightOffset) + + def resizeTo(self, newWidth, newHeight): + """Resizes the window.""" + self._send_event(self._hwnd, protocol.event.ConfigureNotify, + data=(newWidth, newHeight)) + + def moveRel(self, xOffset, yOffset): + """Moves the window relative to its current position.""" + rect = self._getWindowRect() + self.moveTo(rect.left + xOffset, rect.top + yOffset) + + def moveTo(self, newLeft, newTop): + """Moves the window.""" + self._send_event(self._hwnd, protocol.event.ConfigureNotify, + data=(newLeft, newTop)) + + @property + def isMinimized(self): + """Checks if the window is minimized.""" + return '_NET_WM_STATE_HIDDEN' in self._get_window_states() + + @property + def isMaximized(self): + """Checks if the window is maximized.""" + states = self._get_window_states() + return ('_NET_WM_STATE_MAXIMIZED_VERT' in states and + '_NET_WM_STATE_MAXIMIZED_HORZ' in states) + + @property + def isActive(self): + """Checks if the window is active.""" + active_window = self._display.get_input_focus().focus + return active_window == self._hwnd + + @property + def title(self): + """Returns the window title.""" + return self._display.create_resource_object('window', self._hwnd).get_wm_name() + + @property + def visible(self): + """Checks if the window is visible.""" + return self._display.create_resource_object('window', self._hwnd).get_attributes().map_state == X.IsViewable + + def _send_event(self, window, event_type, data): + """Sends an event to the window.""" + event = event_type( + window=window, + client_type=self._display.intern_atom('_NET_WM_STATE'), + data=data + ) + self._display.send_event(window, event, event_mask=X.SubstructureRedirectMask | X.SubstructureNotifyMask) + self._display.sync() + + def _change_state(self, *states): + """Changes the window state.""" + data = (32, list(states)) + self._send_event(self._hwnd, protocol.event.ClientMessage, data) + + def _get_window_states(self): + """Returns the current states of the window.""" + atom = self._display.intern_atom('_NET_WM_STATE') + prop = self._display.create_resource_object('window', self._hwnd).get_property(atom, X.AnyPropertyType) + return [self._display.get_atom_name(state) for state in prop.value] + +def getActiveWindow(): + """Returns the active window.""" + d = display.Display() + root = d.screen().root + active_window_id = root.get_full_property(d.intern_atom('_NET_ACTIVE_WINDOW'), X.AnyPropertyType).value[0] + return LinuxWindow(active_window_id) + +def getAllWindows(): + """Returns a list of all windows.""" + d = display.Display() + root = d.screen().root + window_ids = root.query_tree().children + return [LinuxWindow(wid) for wid in window_ids] + +def getWindowsWithTitle(title): + """Returns windows with the specified title.""" + return [win for win in getAllWindows() if title in win.title] + +def getAllTitles(): + """Returns titles of all windows.""" + return [win.title for win in getAllWindows()] \ No newline at end of file