1# -*- coding: utf-8 -*- 2# This file is part of Xpra. 3# Copyright (C) 2013-2020 Antoine Martin <antoine@xpra.org> 4# Xpra is released under the terms of the GNU GPL v2, or, at your option, any 5# later version. See the file COPYING for details. 6 7import Quartz.CoreGraphics as CG #@UnresolvedImport 8 9from xpra.util import envbool 10from xpra.os_util import memoryview_to_bytes 11from xpra.scripts.config import InitExit 12from xpra.scripts.main import check_display 13from xpra.exit_codes import EXIT_FAILURE 14from xpra.server.gtk_server_base import GTKServerBase 15from xpra.server.shadow.gtk_shadow_server_base import GTKShadowServerBase 16from xpra.platform.darwin.keyboard_config import KeyboardConfig 17from xpra.platform.darwin.gui import get_CG_imagewrapper, take_screenshot 18from xpra.log import Logger 19 20log = Logger("shadow", "osx") 21 22 23USE_TIMER = envbool("XPRA_OSX_SHADOW_USE_TIMER", False) 24 25ALPHA = { 26 CG.kCGImageAlphaNone : "AlphaNone", 27 CG.kCGImageAlphaPremultipliedLast : "PremultipliedLast", 28 CG.kCGImageAlphaPremultipliedFirst : "PremultipliedFirst", 29 CG.kCGImageAlphaLast : "Last", 30 CG.kCGImageAlphaFirst : "First", 31 CG.kCGImageAlphaNoneSkipLast : "SkipLast", 32 CG.kCGImageAlphaNoneSkipFirst : "SkipFirst", 33 } 34 35BTYPES = tuple((str, bytes, memoryview, bytearray)) 36 37#ensure that picture_encode can deal with pixels as NSCFData: 38def patch_pixels_to_bytes(): 39 from CoreFoundation import CFDataGetBytes, CFDataGetLength #@UnresolvedImport 40 def pixels_to_bytes(v): 41 if isinstance(v, BTYPES): 42 return memoryview_to_bytes(v) 43 l = CFDataGetLength(v) 44 return CFDataGetBytes(v, (0, l), None) 45 from xpra.codecs import rgb_transform 46 rgb_transform.pixels_to_bytes = pixels_to_bytes 47 48 49class OSXRootCapture: 50 51 def __repr__(self): 52 return "OSXRootCapture" 53 54 def refresh(self): 55 return True 56 57 def clean(self): 58 """ nothing specific to do here on MacOS """ 59 60 def get_image(self, x, y, width, height): 61 rect = (x, y, width, height) 62 return get_CG_imagewrapper(rect) 63 64 def get_info(self) -> dict: 65 return {} 66 67 def take_screenshot(self): 68 log("grabbing screenshot") 69 return take_screenshot() 70 71 72class ShadowServer(GTKShadowServerBase): 73 74 def __init__(self): 75 #sanity check: 76 check_display() 77 image = CG.CGWindowListCreateImage(CG.CGRectInfinite, 78 CG.kCGWindowListOptionOnScreenOnly, 79 CG.kCGNullWindowID, 80 CG.kCGWindowImageDefault) 81 if image is None: 82 log("cannot grab test screenshot - maybe you need to run this command whilst logged in via the UI") 83 raise InitExit(EXIT_FAILURE, "cannot grab pixels from the screen, make sure this command is launched from a GUI session") 84 patch_pixels_to_bytes() 85 self.refresh_count = 0 86 self.refresh_rectangle_count = 0 87 self.refresh_registered = False 88 super().__init__() 89 90 def init(self, opts): 91 super().init(opts) 92 self.keycodes = {} 93 #printing fails silently on OSX 94 self.printing = False 95 96 def get_keyboard_config(self, _props=None): 97 return KeyboardConfig() 98 99 def make_tray_widget(self): 100 from xpra.client.gtk_base.statusicon_tray import GTKStatusIconTray 101 return GTKStatusIconTray(self, 0, self.tray, "Xpra Shadow Server", None, None, 102 self.tray_click_callback, mouseover_cb=None, exit_cb=self.tray_exit_callback) 103 104 105 def setup_capture(self): 106 return OSXRootCapture() 107 108 def screen_refresh_callback(self, count, rects, info): 109 log("screen_refresh_callback%s mapped=%s", (count, rects, info), self.mapped) 110 self.refresh_count += 1 111 rlist = [] 112 for r in rects: 113 if not isinstance(r, CG.CGRect): 114 log.error("Error: invalid rectangle in refresh list: %s", r) 115 continue 116 self.refresh_rectangle_count += 1 117 rlist.append((int(r.origin.x), int(r.origin.y), int(r.size.width), int(r.size.height))) 118 #return quickly, and process the list copy via idle add: 119 self.idle_add(self.do_screen_refresh, rlist) 120 121 def do_screen_refresh(self, rlist): 122 #TODO: improve damage method to handle lists directly: 123 from xpra.rectangle import rectangle #@UnresolvedImport 124 model_rects = {} 125 for model in self._id_to_window.values(): 126 model_rects[model] = rectangle(*model.geometry) 127 for x, y, w, h in rlist: 128 for model, rect in model_rects.items(): 129 mrect = rect.intersection(x, y, w, h) 130 #log("screen refresh intersection of %s and %24s: %s", model, (x, y, w, h), mrect) 131 if mrect: 132 rx = mrect.x-rect.x 133 ry = mrect.y-rect.y 134 self.refresh_window_area(model, rx, ry, mrect.width, mrect.height, {"damage" : True}) 135 136 def start_refresh(self, wid): 137 #don't use the timer, get damage notifications: 138 if wid not in self.mapped: 139 self.mapped.append(wid) 140 if self.refresh_registered: 141 return 142 if not USE_TIMER: 143 err = CG.CGRegisterScreenRefreshCallback(self.screen_refresh_callback, None) 144 log("CGRegisterScreenRefreshCallback(%s)=%s", self.screen_refresh_callback, err) 145 if err==0: 146 self.refresh_registered = True 147 return 148 log.warn("Warning: CGRegisterScreenRefreshCallback failed with error %i", err) 149 log.warn(" using fallback timer method") 150 super().start_refresh(wid) 151 152 def stop_refresh(self, wid): 153 log("stop_refresh(%i) mapped=%s, timer=%s", wid, self.mapped, self.refresh_timer) 154 #may stop the timer fallback: 155 super().stop_refresh(wid) 156 if self.refresh_registered and not self.mapped: 157 try: 158 err = CG.CGUnregisterScreenRefreshCallback(self.screen_refresh_callback, None) 159 log("CGUnregisterScreenRefreshCallback(%s)=%s", self.screen_refresh_callback, err) 160 if err: 161 log.warn(" unregistering the existing one returned %s", {0 : "OK"}.get(err, err)) 162 except ValueError as e: 163 log.warn("Error unregistering screen refresh callback:") 164 log.warn(" %s", e) 165 self.refresh_registered = False 166 167 168 def do_process_mouse_common(self, proto, wid, pointer, *args): 169 if proto not in self._server_sources: 170 return False 171 assert wid in self._id_to_window 172 CG.CGWarpMouseCursorPosition(pointer[:2]) 173 return True 174 175 def fake_key(self, keycode, press): 176 e = CG.CGEventCreateKeyboardEvent(None, keycode, press) 177 log("fake_key(%s, %s)", keycode, press) 178 #CGEventSetFlags(keyPress, modifierFlags) 179 #modifierFlags: kCGEventFlagMaskShift, ... 180 CG.CGEventPost(CG.kCGSessionEventTap, e) 181 #this causes crashes, don't do it! 182 #CG.CFRelease(e) 183 184 def do_process_button_action(self, proto, wid, button, pressed, pointer, modifiers, *args): 185 self._update_modifiers(proto, wid, modifiers) 186 pointer = self._process_mouse_common(proto, wid, pointer) 187 if pointer: 188 self.button_action(pointer, button, pressed, -1, *args) 189 190 def button_action(self, pointer, button, pressed, _deviceid=-1, *_args): 191 if button<=3: 192 #we should be using CGEventCreateMouseEvent 193 #instead we clear previous clicks when a "higher" button is pressed... oh well 194 event = [pointer[:2], 1, button] 195 for i in range(button): 196 event.append(i==(button-1) and pressed) 197 r = CG.CGPostMouseEvent(*event) 198 log("CG.CGPostMouseEvent%s=%s", event, r) 199 return 200 if not pressed: 201 #we don't simulate press/unpress 202 #so just ignore unpressed events 203 return 204 wheel = (button-2)//2 205 direction = 1-(((button-2) % 2)*2) 206 event = [wheel] 207 for i in range(wheel): 208 if i!=(wheel-1): 209 event.append(0) 210 else: 211 event.append(direction) 212 r = CG.CGPostScrollWheelEvent(*event) 213 log("CG.CGPostScrollWheelEvent%s=%s", event, r) 214 215 def make_hello(self, source): 216 capabilities = GTKServerBase.make_hello(self, source) 217 capabilities["shadow"] = True 218 capabilities["server_type"] = "Python/gtk2/osx-shadow" 219 return capabilities 220 221 def get_info(self, proto, *_args): 222 info = GTKServerBase.get_info(self, proto) 223 info.setdefault("features", {})["shadow"] = True 224 info.setdefault("server", {})["type"] = "Python/gtk2/osx-shadow" 225 info.setdefault("damage", {}).update({ 226 "use-timer" : USE_TIMER, 227 "notifications" : self.refresh_registered, 228 "count" : self.refresh_count, 229 "rectangles" : self.refresh_rectangle_count, 230 }) 231 return info 232 233 234def main(): 235 import sys 236 from xpra.platform import program_context 237 with program_context("MacOS Shadow Capture"): 238 log.enable_debug() 239 c = OSXRootCapture() 240 x, y, w, h = list(int(sys.argv[x]) for x in range(1,5)) 241 img = c.get_image(x, y, w, h) 242 from PIL import Image 243 i = Image.frombuffer("RGBA", (w, h), img.get_pixels(), "raw", "BGRA", img.get_rowstride()) 244 import time 245 t = time.time() 246 tstr = time.strftime("%H-%M-%S", time.localtime(t)) 247 filename = "./Capture-%s-%s.png" % ((x, y, w, h),tstr,) 248 i.save(filename, "png") 249 print("saved to %s" % (filename,)) 250 251 252if __name__ == "__main__": 253 main() 254