""" Platform-specific code for Darwin is encapsulated in this module. """ import os import re import tempfile import threading import subprocess try: import Tkinter as tk import Queue as queue except ImportError: import tkinter as tk import queue import numpy import AppKit import Quartz from PIL import Image, ImageTk from .SettingsDebug import Debug from .InputEmulation import Keyboard # Python 3 compatibility try: basestring except NameError: basestring = str class PlatformManagerDarwin(object): """ Abstracts Darwin-specific OS-level features """ def __init__(self): # Mapping to `keyboard` names self._SPECIAL_KEYCODES = { "BACKSPACE": "backspace", "TAB": "tab", "CLEAR": "clear", "ENTER": "enter", "SHIFT": "shift", "CTRL": "ctrl", "ALT": "alt", "PAUSE": "pause", "CAPS_LOCK": "caps lock", "ESC": "esc", "SPACE": "spacebar", "PAGE_UP": "page up", "PAGE_DOWN": "page down", "END": "end", "HOME": "home", "LEFT": "left arrow", "UP": "up arrow", "RIGHT": "right arrow", "DOWN": "down arrow", "SELECT": "select", "PRINT": "print", "PRINTSCREEN": "print screen", "INSERT": "ins", "DELETE": "del", "WIN": "win", "CMD": "win", "META": "win", "NUM0": "keypad 0", "NUM1": "keypad 1", "NUM2": "keypad 2", "NUM3": "keypad 3", "NUM4": "keypad 4", "NUM5": "keypad 5", "NUM6": "keypad 6", "NUM7": "keypad 7", "NUM8": "keypad 8", "NUM9": "keypad 9", "NUM9": "keypad 9", "SEPARATOR": 83, "ADD": 78, "MINUS": 74, "MULTIPLY": 55, "DIVIDE": 53, "F1": "f1", "F2": "f2", "F3": "f3", "F4": "f4", "F5": "f5", "F6": "f6", "F7": "f7", "F8": "f8", "F9": "f9", "F10": "f10", "F11": "f11", "F12": "f12", "F13": "f13", "F14": "f14", "F15": "f15", "F16": "f16", "NUM_LOCK": "num lock", "SCROLL_LOCK": "scroll lock", } self._REGULAR_KEYCODES = { "0": "0", "1": "1", "2": "2", "3": "3", "4": "4", "5": "5", "6": "6", "7": "7", "8": "8", "9": "9", "a": "a", "b": "b", "c": "c", "d": "d", "e": "e", "f": "f", "g": "g", "h": "h", "i": "i", "j": "j", "k": "k", "l": "l", "m": "m", "n": "n", "o": "o", "p": "p", "q": "q", "r": "r", "s": "s", "t": "t", "u": "u", "v": "v", "w": "w", "x": "x", "y": "y", "z": "z", ";": ";", "=": "=", ",": ",", "-": "-", ".": ".", "/": "/", "`": "`", "[": "[", "\\": "\\", "]": "]", "'": "'", " ": " ", } self._UPPERCASE_KEYCODES = { "~": "`", "+": "=", ")": "0", "!": "1", "@": "2", "#": "3", "$": "4", "%": "5", "^": "6", "&": "7", "*": "8", "(": "9", "A": "a", "B": "b", "C": "c", "D": "d", "E": "e", "F": "f", "G": "g", "H": "h", "I": "i", "J": "j", "K": "k", "L": "l", "M": "m", "N": "n", "O": "o", "P": "p", "Q": "q", "R": "r", "S": "s", "T": "t", "U": "u", "V": "v", "W": "w", "X": "x", "Y": "y", "Z": "z", ":": ";", "<": ",", "_": "-", ">": ".", "?": "/", "|": "\\", "\"": "'", "{": "[", "}": "]", } ## Screen functions def getBitmapFromRect(self, x, y, w, h): """ Capture the specified area of the (virtual) screen. """ min_x, min_y, screen_width, screen_height = self._getVirtualScreenRect() img = self._getVirtualScreenBitmap() # TODO # Limit the coordinates to the virtual screen # Then offset so 0,0 is the top left corner of the image # (Top left of virtual screen could be negative) x1 = min(max(min_x, x), min_x+screen_width) - min_x y1 = min(max(min_y, y), min_y+screen_height) - min_y x2 = min(max(min_x, x+w), min_x+screen_width) - min_x y2 = min(max(min_y, y+h), min_y+screen_height) - min_y return numpy.array(img.crop((x1, y1, x2, y2))) def getScreenBounds(self, screenId): """ Returns the screen size of the specified monitor (0 being the main monitor). """ screen_details = self.getScreenDetails() if not isinstance(screenId, int) or screenId < -1 or screenId >= len(screen_details): raise ValueError("Invalid screen ID") if screenId == -1: # -1 represents the entire virtual screen return self._getVirtualScreenRect() return screen_details[screenId]["rect"] def _getVirtualScreenRect(self): """ Returns the rect of all attached screens as (x, y, w, h) """ monitors = self.getScreenDetails() x1 = min([s["rect"][0] for s in monitors]) y1 = min([s["rect"][1] for s in monitors]) x2 = max([s["rect"][0]+s["rect"][2] for s in monitors]) y2 = max([s["rect"][1]+s["rect"][3] for s in monitors]) return (x1, y1, x2-x1, y2-y1) def _getVirtualScreenBitmap(self): """ Returns a bitmap of all attached screens """ filenames = [] screen_details = self.getScreenDetails() for screen in screen_details: fh, filepath = tempfile.mkstemp('.png') filenames.append(filepath) os.close(fh) subprocess.call(['screencapture', '-x'] + filenames) min_x, min_y, screen_w, screen_h = self._getVirtualScreenRect() virtual_screen = Image.new("RGB", (screen_w, screen_h)) for filename, screen in zip(filenames, screen_details): # Capture virtscreen coordinates of monitor x, y, w, h = screen["rect"] # Convert image size if needed im = Image.open(filename) im.load() if im.size[0] != w or im.size[1] != h: im = im.resize((int(w), int(h)), Image.ANTIALIAS) # Convert to image-local coordinates x = x - min_x y = y - min_y # Paste on the virtual screen virtual_screen.paste(im, (x, y)) os.unlink(filename) return virtual_screen def getScreenDetails(self): """ Return list of attached monitors For each monitor (as dict), ``monitor["rect"]`` represents the screen as positioned in virtual screen. List is returned in device order, with the first element (0) representing the primary monitor. """ primary_screen = None screens = [] for monitor in AppKit.NSScreen.screens(): # Convert screen rect to Lackey-style rect (x,y,w,h) as position in virtual screen screen = { "rect": ( int(monitor.frame().origin.x), int(monitor.frame().origin.y), int(monitor.frame().size.width), int(monitor.frame().size.height) ) } screens.append(screen) return screens def isPointVisible(self, x, y): """ Checks if a point is visible on any monitor. """ for screen in self.getScreenDetails(): s_x, s_y, s_w, s_h = screen["rect"] if (s_x <= x < (s_x + s_w)) and (s_y <= y < (s_y + s_h)): return True return False ## Clipboard functions def osCopy(self): """ Triggers the OS "copy" keyboard shortcut """ k = Keyboard() k.keyDown("{CTRL}") k.type("c") k.keyUp("{CTRL}") def osPaste(self): """ Triggers the OS "paste" keyboard shortcut """ k = Keyboard() k.keyDown("{CTRL}") k.type("v") k.keyUp("{CTRL}") ## Window functions def getWindowByTitle(self, wildcard, order=0): """ Returns a handle for the first window that matches the provided "wildcard" regex """ for w in self._get_window_list(): if "kCGWindowName" in w and re.search(wildcard, w["kCGWindowName"], flags=re.I): # Matches - make sure we get it in the correct order if order == 0: return w["kCGWindowNumber"] else: order -= 1 def getWindowByPID(self, pid, order=0): """ Returns a handle for the first window that matches the provided PID """ for w in self._get_window_list(): if "kCGWindowOwnerPID" in w and w["kCGWindowOwnerPID"] == pid: # Matches - make sure we get it in the correct order if order == 0: return w["kCGWindowNumber"] else: order -= 1 raise OSError("Could not find window for PID {} at index {}".format(pid, order)) def getWindowRect(self, hwnd): """ Returns a rect (x,y,w,h) for the specified window's area """ for w in self._get_window_list(): if "kCGWindowNumber" in w and w["kCGWindowNumber"] == hwnd: x = w["kCGWindowBounds"]["X"] y = w["kCGWindowBounds"]["Y"] width = w["kCGWindowBounds"]["Width"] height = w["kCGWindowBounds"]["Height"] return (x, y, width, height) raise OSError("Unrecognized window number {}".format(hwnd)) def focusWindow(self, hwnd): """ Brings specified window to the front """ Debug.log(3, "Focusing window: " + str(hwnd)) # TODO pass def getWindowTitle(self, hwnd): """ Gets the title for the specified window """ for w in self._get_window_list(): if "kCGWindowNumber" in w and w["kCGWindowNumber"] == hwnd: return w["kCGWindowName"] def getWindowPID(self, hwnd): """ Gets the process ID that the specified window belongs to """ for w in self._get_window_list(): if "kCGWindowNumber" in w and w["kCGWindowNumber"] == hwnd: return w["kCGWindowOwnerPID"] def getForegroundWindow(self): """ Returns a handle to the window in the foreground """ active_app = NSWorkspace.sharedWorkspace().frontmostApplication().localizedName() for w in self._get_window_list(): if "kCGWindowOwnerName" in w and w["kCGWindowOwnerName"] == active_app: return w["kCGWindowNumber"] def _get_window_list(self): """ Returns a dictionary of details about open windows """ window_list = Quartz.CGWindowListCopyWindowInfo(Quartz.kCGWindowListExcludeDesktopElements, Quartz.kCGNullWindowID) return window_list ## Highlighting functions def highlight(self, rect, color="red", seconds=None): """ Simulates a transparent rectangle over the specified ``rect`` on the screen. Actually takes a screenshot of the region and displays with a rectangle border in a borderless window (due to Tkinter limitations) If a Tkinter root window has already been created somewhere else, uses that instead of creating a new one. """ self.queue = queue.Queue() if seconds == 0: t = threading.Thread(target=self._do_until_timeout, args=(self.queue,(rect,color,seconds))) t.start() q = self.queue control_obj = lambda: None control_obj.close = lambda: q.put(True) return control_obj self._do_until_timeout(self.queue, (rect,color,seconds)) def _do_until_timeout(self, queue, args): rect, color, seconds = args if tk._default_root is None: Debug.log(3, "Creating new temporary Tkinter root") temporary_root = True root = tk.Tk() root.withdraw() else: Debug.log(3, "Borrowing existing Tkinter root") temporary_root = False root = tk._default_root image_to_show = self.getBitmapFromRect(*rect) app = highlightWindow(root, rect, color, image_to_show, queue) app.do_until_timeout(seconds) ## Process functions def isPIDValid(self, pid): """ Checks if a PID is associated with a running process """ try: os.kill(pid, 0) # Does nothing if valid, raises exception otherwise except OSError: return False else: return True def killProcess(self, pid): """ Kills the process with the specified PID (if possible) """ os.kill(pid, 15) def getProcessName(self, pid): """ Searches all processes for the given PID, then returns the originating command """ ps = subprocess.check_output(["ps", "aux"]).decode("ascii") processes = ps.split("\n") cols = len(processes[0].split()) - 1 for row in processes[1:]: if row != "": proc = row.split(None, cols) if proc[1].strip() == str(pid): return proc[-1] ## Helper class for highlighting class highlightWindow(tk.Toplevel): def __init__(self, root, rect, frame_color, screen_cap, queue): """ Accepts rect as (x,y,w,h) """ self.root = root self.root.tk.call('tk', 'scaling', 0.5) tk.Toplevel.__init__(self, self.root, bg="red", bd=0) self.queue = queue self.check_close_after = None ## Set toplevel geometry, remove borders, and push to the front self.geometry("{2}x{3}+{0}+{1}".format(*rect)) self.overrideredirect(1) self.attributes("-topmost", True) ## Create canvas and fill it with the provided image. Then draw rectangle outline self.canvas = tk.Canvas( self, width=rect[2], height=rect[3], bd=0, bg="blue", highlightthickness=0) self.tk_image = ImageTk.PhotoImage(Image.fromarray(screen_cap)) self.canvas.create_image(0, 0, image=self.tk_image, anchor=tk.NW) self.canvas.create_rectangle( 2, 2, rect[2]-2, rect[3]-2, outline=frame_color, width=4) self.canvas.pack(fill=tk.BOTH, expand=tk.YES) ## Lift to front if necessary and refresh. self.lift() self.update() def do_until_timeout(self, seconds=None): self.check_close() if seconds is not None: self.root.after(seconds*1000, self.close) self.root.mainloop() def check_close(self): try: kill = self.queue.get_nowait() if kill == True: self.close() return except queue.Empty: pass self.check_close_after = self.root.after(500, self.check_close) def close(self): if self.check_close_after is not None: self.root.after_cancel(self.check_close_after) self.root.destroy()