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