xpra icon
Bug tracker and wiki

source: xpra/trunk/src/xpra/client.py @ 925

Last change on this file since 925 was 925, checked in by antoine, 3 years ago

don't bother trying GL with gtk3, also fix pydev warnings and whitespace

File size: 32.6 KB
Line 
1# This file is part of Parti.
2# Copyright (C) 2011 Serviware (Arthur Huillet, <ahuillet@serviware.com>)
3# Copyright (C) 2010-2012 Antoine Martin <antoine@devloop.org.uk>
4# Copyright (C) 2008, 2010 Nathaniel Smith <njs@pobox.com>
5# Parti is released under the terms of the GNU GPL v2, or, at your option, any
6# later version. See the file COPYING for details.
7
8#pygtk3 vs pygtk2 (sigh)
9from wimpiggy.gobject_compat import import_gobject, import_gtk, import_gdk, is_gtk3
10gobject = import_gobject()
11gtk = import_gtk()
12gdk = import_gdk()
13if is_gtk3():
14    def get_root_size():
15        return gdk.get_default_root_window().get_geometry()[2:]
16    def set_windows_cursor(gtkwindows, new_cursor):
17        pass
18        #window.override_cursor(cursor, None)
19else:
20    def get_root_size():
21        return gdk.get_default_root_window().get_size()
22    def set_windows_cursor(gtkwindows, new_cursor):
23        cursor = None
24        if len(new_cursor)>0:
25            (_, _, w, h, xhot, yhot, serial, pixels) = new_cursor
26            log.debug("new cursor at %s,%s with serial=%s, dimensions: %sx%s, len(pixels)=%s" % (xhot,yhot, serial, w,h, len(pixels)))
27            pixbuf = gdk.pixbuf_new_from_data(pixels, gdk.COLORSPACE_RGB, True, 8, w, h, w * 4)
28            x = max(0, min(xhot, w-1))
29            y = max(0, min(yhot, h-1))
30            size = gdk.display_get_default().get_default_cursor_size()
31            if size>0 and (size<w or size<h):
32                ratio = float(max(w,h))/size
33                pixbuf = pixbuf.scale_simple(int(w/ratio), int(h/ratio), gdk.INTERP_BILINEAR)
34                x = int(x/ratio)
35                y = int(y/ratio)
36            cursor = gdk.Cursor(gdk.display_get_default(), pixbuf, x, y)
37        for gtkwindow in gtkwindows:
38            gtkwindow.get_window().set_cursor(cursor)
39
40
41import os
42import time
43import ctypes
44
45from wimpiggy.util import (n_arg_signal,
46                           gtk_main_quit_really,
47                           gtk_main_quit_on_fatal_exceptions_enable)
48
49from wimpiggy.log import Logger
50log = Logger()
51
52from xpra.deque import maxdeque
53from xpra.client_base import XpraClientBase
54from xpra.keys import mask_to_names, DEFAULT_MODIFIER_MEANINGS, DEFAULT_MODIFIER_NUISANCE, DEFAULT_MODIFIER_IGNORE_KEYNAMES
55from xpra.platform.gui import ClientExtras
56from xpra.scripts.main import ENCODINGS
57from xpra.version_util import is_compatible_with
58
59from xpra.client_window import ClientWindow
60ClientWindowClass = ClientWindow
61#the GL backend only works with gtk2 (and is disabled by default)
62USE_OPENGL = False
63if USE_OPENGL and not is_gtk3():
64    try:
65        from xpra.gl_client_window import GLClientWindow
66        ClientWindowClass = GLClientWindow
67    except ImportError, e:
68        pass    #GL not available
69
70def nn(x):
71    if x is None:
72        return  ""
73    return x
74
75
76
77class XpraClient(XpraClientBase):
78    __gsignals__ = {
79        "clipboard-toggled": n_arg_signal(0),
80        }
81
82    def __init__(self, conn, opts):
83        XpraClientBase.__init__(self, opts)
84        self.start_time = time.time()
85        self._window_to_id = {}
86        self._id_to_window = {}
87        self.title = opts.title
88        self.readonly = opts.readonly
89        self.session_name = opts.session_name
90        self.compression_level = opts.compression_level
91        self.auto_refresh_delay = opts.auto_refresh_delay
92        self.max_bandwidth = opts.max_bandwidth
93        if self.max_bandwidth>0.0 and self.jpegquality==0:
94            """ jpegquality was not set, use a better start value """
95            self.jpegquality = 50
96
97        self.server_capabilities = {}
98
99        self.mmap_enabled = False
100        self.server_start_time = -1
101        self.server_platform = ""
102        self.server_actual_desktop_size = None
103        self.server_desktop_size = None
104        self.server_randr = False
105        self.pixel_counter = maxdeque(maxlen=100)
106        self.server_latency = maxdeque(maxlen=100)
107        self.server_load = None
108        self.client_latency = maxdeque(maxlen=100)
109        self.toggle_cursors_bell_notify = False
110        self.bell_enabled = True
111        self.cursors_enabled = True
112        self.notifications_enabled = True
113        self.clipboard_enabled = False
114        self.window_configure = False
115        self.mmap = None
116        self.mmap_token = None
117        self.mmap_file = None
118        self.mmap_size = 0
119
120        self._client_extras = ClientExtras(self, opts)
121        self.clipboard_enabled = not self.readonly and opts.clipboard and self._client_extras.supports_clipboard()
122        self.supports_mmap = opts.mmap and ("rgb24" in ENCODINGS) and self._client_extras.supports_mmap()
123        if self.supports_mmap:
124            try:
125                import mmap
126                import tempfile
127                import uuid
128                from stat import S_IRUSR,S_IWUSR,S_IRGRP,S_IWGRP
129                mmap_dir = os.getenv("TMPDIR", "/tmp")
130                if not os.path.exists(mmap_dir):
131                    raise Exception("TMPDIR %s does not exist!" % mmap_dir)
132                #create the mmap file, the mkstemp that is called via NamedTemporaryFile ensures
133                #that the file is readable and writable only by the creating user ID
134                temp = tempfile.NamedTemporaryFile(prefix="xpra.", suffix=".mmap", dir=mmap_dir)
135                #keep a reference to it so it does not disappear!
136                self._mmap_temp_file = temp
137                self.mmap_file = temp.name
138                fd = temp.file.fileno()
139                #set the group permissions and gid if the mmap-group option is specified
140                if opts.mmap_group and type(conn.target)==str and os.path.exists(conn.target):
141                    s = os.stat(conn.target)
142                    os.fchown(fd, -1, s.st_gid)
143                    os.fchmod(fd, S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP)
144                self.mmap_size = max(4096, mmap.PAGESIZE)*32*1024   #generally 128MB
145                log("using mmap file %s, fd=%s, size=%s", self.mmap_file, fd, self.mmap_size)
146                os.lseek(fd, self.mmap_size-1, os.SEEK_SET)
147                assert os.write(fd, '\x00')
148                os.lseek(fd, 0, os.SEEK_SET)
149                self.mmap = mmap.mmap(fd, length=self.mmap_size)
150                #write the 16 byte token one byte at a time - no endianness
151                self.mmap_token = uuid.uuid4().int
152                log.debug("mmap_token=%s", self.mmap_token)
153                v = self.mmap_token
154                for i in range(0,16):
155                    poke = ctypes.c_ubyte.from_buffer(self.mmap, 512+i)
156                    poke.value = v % 256
157                    v = v>>8
158                assert v==0
159            except Exception, e:
160                log.error("failed to setup mmap: %s", e)
161                self.supports_mmap = False
162                self.clean_mmap()
163                self.mmap = None
164                self.mmap_file = None
165                self.mmap_size = 0
166
167        self.init_packet_handlers()
168        self.ready(conn)
169
170        self.keyboard_sync = opts.keyboard_sync
171        self.key_repeat_delay = -1
172        self.key_repeat_interval = -1
173        self.keys_pressed = {}
174        self._remote_version = None
175        self._keymap_changing = False
176        try:
177            self._keymap = gdk.keymap_get_default()
178        except:
179            self._keymap = None
180        self._do_keys_changed()
181        self.key_shortcuts = self.parse_shortcuts(opts.key_shortcuts)
182        self.send_hello()
183
184        if self._keymap:
185            self._keymap.connect("keys-changed", self._keys_changed)
186        self._xsettings_watcher = None
187        self._root_props_watcher = None
188
189        self._focused = None
190        def compute_receive_bandwidth(delay):
191            bytecount = self._protocol.input_bytecount
192            bw = ((bytecount - self.last_input_bytecount) / 1024) * 1000 / delay
193            self.last_input_bytecount = bytecount;
194            log.debug("Bandwidth is ", bw, "kB/s, max ", self.max_bandwidth, "kB/s")
195            q = self.jpegquality
196            if bw > self.max_bandwidth:
197                q -= 10
198            elif bw < self.max_bandwidth:
199                q += 5
200            q = max(10, min(95 ,q))
201            self.send_jpeg_quality(q)
202            return True
203        if (self.max_bandwidth):
204            self.last_input_bytecount = 0
205            gobject.timeout_add(2000, compute_receive_bandwidth, 2000)
206        if opts.send_pings:
207            gobject.timeout_add(1000, self.send_ping)
208
209    def init_packet_handlers(self):
210        XpraClientBase.init_packet_handlers(self)
211        for k,v in {
212            "hello":                self._process_hello,
213            "new-window":           self._process_new_window,
214            "new-override-redirect":self._process_new_override_redirect,
215            "draw":                 self._process_draw,
216            "cursor":               self._process_cursor,
217            "bell":                 self._process_bell,
218            "notify_show":          self._process_notify_show,
219            "notify_close":         self._process_notify_close,
220            "ping":                 self._process_ping,
221            "ping_echo":            self._process_ping_echo,
222            "window-metadata":      self._process_window_metadata,
223            "configure-override-redirect":  self._process_configure_override_redirect,
224            "lost-window":          self._process_lost_window,
225            # "clipboard-*" packets are handled by a special case below.
226            }.items():
227            self._packet_handlers[k] = v
228
229    def run(self):
230        gtk_main_quit_on_fatal_exceptions_enable()
231        gtk.main()
232        return  self.exit_code
233
234    def quit(self, *args):
235        gtk_main_quit_really()
236
237    def cleanup(self):
238        if self._client_extras:
239            self._client_extras.exit()
240            self._client_extras = None
241        XpraClientBase.cleanup(self)
242        self.clean_mmap()
243
244    def clean_mmap(self):
245        if self.mmap_file and os.path.exists(self.mmap_file):
246            os.unlink(self.mmap_file)
247            self.mmap_file = None
248
249    def parse_shortcuts(self, strs):
250        #TODO: maybe parse with re instead?
251        if len(strs)==0:
252            """ if none are defined, add this as default
253            it would be nicer to specify it via OptionParser in main
254            but then it would always have to be there with no way of removing it
255            whereas now it is enough to define one (any shortcut)
256            """
257            strs = ["meta+shift+F4:quit"]
258        log.debug("parse_shortcuts(%s)" % str(strs))
259        shortcuts = {}
260        #modifier names contains the internal modifiers list, ie: "mod1", "control", ...
261        #but the user expects the name of the key to be used, ie: "alt" or "super"
262        #whereas at best, we keep "Alt_L" : "mod1" mappings... (xposix)
263        #so generate a map from one to the other:
264        modifier_names = {}
265        meanings = self.xkbmap_mod_meanings or DEFAULT_MODIFIER_MEANINGS
266        for pub_name,mod_name in meanings.items():
267            if mod_name in DEFAULT_MODIFIER_NUISANCE or pub_name in DEFAULT_MODIFIER_IGNORE_KEYNAMES:
268                continue
269            #just hope that xxx_L is mapped to the same modifier as xxx_R!
270            if pub_name.endswith("_L") or pub_name.endswith("_R"):
271                pub_name = pub_name[:-2]
272            elif pub_name=="ISO_Level3_Shift":
273                pub_name = "AltGr"
274            if pub_name not in modifier_names:
275                modifier_names[pub_name.lower()] = mod_name
276
277        for s in strs:
278            #example for s: Control+F8:some_action()
279            parts = s.split(":", 1)
280            if len(parts)!=2:
281                log.error("invalid shortcut: %s" % s)
282                continue
283            #example for action: "quit"
284            action = parts[1]
285            #example for keyspec: ["Alt", "F8"]
286            keyspec = parts[0].split("+")
287            modifiers = []
288            if len(keyspec)>1:
289                valid = True
290                #ie: ["Alt"]
291                for mod in keyspec[:len(keyspec)-1]:
292                    #ie: "alt_l" -> "mod1"
293                    imod = modifier_names.get(mod.lower())
294                    if not imod:
295                        log.error("invalid modifier: %s, valid modifiers are: %s", mod, modifier_names.keys())
296                        valid = False
297                        break
298                    modifiers.append(imod)
299                if not valid:
300                    continue
301            keyname = keyspec[len(keyspec)-1]
302            shortcuts[keyname] = (modifiers, action)
303        log.debug("parse_shortcuts(%s)=%s" % (str(strs), shortcuts))
304        return  shortcuts
305
306    def key_handled_as_shortcut(self, window, key_name, modifiers, depressed):
307        shortcut = self.key_shortcuts.get(key_name)
308        if not shortcut:
309            return  False
310        (req_mods, action) = shortcut
311        for rm in req_mods:
312            if rm not in modifiers:
313                #modifier is missing, bail out
314                return False
315        if not depressed:
316            """ when the key is released, just ignore it - do NOT send it to the server! """
317            return  True
318        try:
319            method = getattr(window, action)
320            log.info("key_handled_as_shortcut(%s,%s,%s,%s) has been handled by shortcut=%s", window, key_name, modifiers, depressed, shortcut)
321        except AttributeError, e:
322            log.error("key dropped, invalid method name in shortcut %s: %s", action, e)
323            return  True
324        try:
325            method()
326        except Exception, e:
327            log.error("key_handled_as_shortcut(%s,%s,%s,%s) failed to execute shortcut=%s: %s", window, key_name, modifiers, depressed, shortcut, e)
328        return  True
329
330    def handle_key_action(self, event, window, depressed):
331        if self.readonly:
332            return
333        log.debug("handle_key_action(%s,%s,%s)", event, window, depressed)
334        modifiers = self.mask_to_names(event.state)
335        name = gdk.keyval_name(event.keyval)
336        keyval = nn(event.keyval)
337        keycode = event.hardware_keycode
338        group = event.group
339        #meant to be in PyGTK since 2.10, not used yet so just return False if we don't have it:
340        is_modifier = hasattr(event, "is_modifier") and event.is_modifier
341        translated = self._client_extras.translate_key(depressed, keyval, name, keycode, group, is_modifier, modifiers)
342        if translated is None:
343            return
344        depressed, keyval, name, keycode, group, is_modifier, modifiers = translated
345        if self.key_handled_as_shortcut(window, name, modifiers, depressed):
346            return
347        if keycode<0:
348            log.debug("key_action(%s,%s,%s) translated keycode is %s, ignoring it", event, window, depressed, keycode)
349            return
350        log.debug("key_action(%s,%s,%s) modifiers=%s, name=%s, state=%s, keyval=%s, string=%s, keycode=%s", event, window, depressed, modifiers, name, event.state, event.keyval, event.string, keycode)
351        wid = self._window_to_id[window]
352        self.send(["key-action", wid, nn(name), depressed, modifiers, keyval, nn(event.string), nn(keycode), group, is_modifier])
353        if self.keyboard_sync and self.key_repeat_delay>0 and self.key_repeat_interval>0:
354            self._key_repeat(wid, depressed, name, keyval, keycode)
355
356    def _key_repeat(self, wid, depressed, name, keyval, keycode):
357        """ this method takes care of scheduling the sending of
358            "key-repeat" packets to the server so that it can
359            maintain a consistent keyboard state.
360        """
361        #we keep track of which keys are still pressed in a dict,
362        if keycode==0:
363            key = name
364        else:
365            key = keycode
366        if not depressed and key in self.keys_pressed:
367            """ stop the timer and clear this keycode: """
368            log.debug("key repeat: clearing timer for %s / %s", name, keycode)
369            gobject.source_remove(self.keys_pressed[key])
370            del self.keys_pressed[key]
371        elif depressed and key not in self.keys_pressed:
372            """ we must ping the server regularly for as long as the key is still pressed: """
373            #TODO: we can have latency measurements (see ping).. use them?
374            LATENCY_JITTER = 100
375            MIN_DELAY = 5
376            delay = max(self.key_repeat_delay-LATENCY_JITTER, MIN_DELAY)
377            interval = max(self.key_repeat_interval-LATENCY_JITTER, MIN_DELAY)
378            log.debug("scheduling key repeat for %s: delay=%s, interval=%s (from %s and %s)", name, delay, interval, self.key_repeat_delay, self.key_repeat_interval)
379            def send_key_repeat():
380                modifiers = self.get_current_modifiers()
381                self.send_now(["key-repeat", wid, name, keyval, keycode, modifiers])
382            def continue_key_repeat(*args):
383                #if the key is still pressed (redundant check?)
384                #confirm it and continue, otherwise stop
385                log.debug("continue_key_repeat for %s / %s", name, keycode)
386                if key in self.keys_pressed:
387                    send_key_repeat()
388                    return  True
389                else:
390                    del self.keys_pressed[key]
391                    return  False
392            def start_key_repeat(*args):
393                #if the key is still pressed (redundant check?)
394                #confirm it and start repeat:
395                log.debug("start_key_repeat for %s / %s", name, keycode)
396                if key in self.keys_pressed:
397                    send_key_repeat()
398                    self.keys_pressed[key] = gobject.timeout_add(interval, continue_key_repeat)
399                else:
400                    del self.keys_pressed[key]
401                return  False   #never run this timer again
402            log.debug("key repeat: starting timer for %s / %s with delay %s and interval %s", name, keycode, delay, interval)
403            self.keys_pressed[key] = gobject.timeout_add(delay, start_key_repeat)
404
405    def clear_repeat(self):
406        for timer in self.keys_pressed.values():
407            gobject.source_remove(timer)
408        self.keys_pressed = {}
409
410    def query_xkbmap(self):
411        if self.readonly:
412            self.xkbmap_layout, self.xkbmap_variant, self.xkbmap_variants = "", "", []
413            self.xkbmap_print, self.xkbmap_query = "", ""
414        else:
415            self.xkbmap_layout, self.xkbmap_variant, self.xkbmap_variants = self._client_extras.get_layout_spec()
416            self.xkbmap_print, self.xkbmap_query = self._client_extras.get_keymap_spec()
417        self.xkbmap_keycodes = self._client_extras.get_gtk_keymap()
418        self.xkbmap_mod_meanings, self.xkbmap_mod_managed, self.xkbmap_mod_pointermissing = self._client_extras.get_keymap_modifiers()
419        log.debug("layout=%s, variant=%s", self.xkbmap_layout, self.xkbmap_variant)
420        log.debug("print=%s, query=%s", self.xkbmap_print, self.xkbmap_query)
421        log.debug("keycodes=%s", str(self.xkbmap_keycodes)[:80]+"...")
422        log.debug("xkbmap_mod_meanings: %s", self.xkbmap_mod_meanings)
423
424    def _keys_changed(self, *args):
425        log.debug("keys_changed")
426        self._keymap = gdk.keymap_get_default()
427        if not self._keymap_changing:
428            self._keymap_changing = True
429            gobject.timeout_add(500, self._do_keys_changed, True)
430
431    def _do_keys_changed(self, sendkeymap=False):
432        self._keymap_changing = False
433        self.query_xkbmap()
434        try:
435            self._modifier_map = self._client_extras.grok_modifier_map(gdk.display_get_default(), self.xkbmap_mod_meanings)
436        except:
437            self._modifier_map = {}
438        log.debug("do_keys_changed() modifier_map=%s" % self._modifier_map)
439        if sendkeymap and not self.readonly:
440            if self.xkbmap_layout:
441                self.send_layout()
442            self.send_keymap()
443
444    def send_layout(self):
445        self.send(["layout-changed", nn(self.xkbmap_layout), nn(self.xkbmap_variant)])
446
447    def send_keymap(self):
448        self.send(["keymap-changed", self.get_keymap_properties()])
449
450    def get_keymap_properties(self):
451        props = {"modifiers" : self.get_current_modifiers()}
452        for x in ["xkbmap_print", "xkbmap_query", "xkbmap_mod_meanings",
453              "xkbmap_mod_managed", "xkbmap_mod_pointermissing", "xkbmap_keycodes"]:
454            props[x] = nn(getattr(self, x))
455        return  props
456
457    def send_focus(self, wid):
458        self.send(["focus", wid, self.get_current_modifiers()])
459
460    def update_focus(self, wid, gotit):
461        log("update_focus(%s,%s) _focused=%s", wid, gotit, self._focused)
462        if gotit and self._focused is not wid:
463            self.clear_repeat()
464            self.send_focus(wid)
465            self._focused = wid
466        if not gotit and self._focused is wid:
467            self.clear_repeat()
468            self.send_focus(0)
469            self._focused = None
470
471    def get_current_modifiers(self):
472        modifiers_mask = gdk.get_default_root_window().get_pointer()[-1]
473        return self.mask_to_names(modifiers_mask)
474
475    def mask_to_names(self, mask):
476        mn = mask_to_names(mask, self._modifier_map)
477        names = self._client_extras.current_modifiers(mn)
478        return  names
479
480    def send_positional(self, packet):
481        self._protocol.source.queue_positional_packet(packet)
482
483    def send_mouse_position(self, packet):
484        self._protocol.source.queue_mouse_position_packet(packet)
485
486    def make_hello(self, challenge_response=None):
487        capabilities = XpraClientBase.make_hello(self, challenge_response)
488        for k,v in self.get_keymap_properties().items():
489            capabilities[k] = v
490        if self.readonly:
491            #don't bother sending keyboard info, as it won't be used
492            capabilities["keyboard"] = False
493        else:
494            capabilities["xkbmap_layout"] = nn(self.xkbmap_layout)
495            capabilities["xkbmap_variant"] = nn(self.xkbmap_variant)
496        capabilities["clipboard"] = self.clipboard_enabled
497        capabilities["notifications"] = self._client_extras.can_notify()
498        capabilities["modifiers"] = self.get_current_modifiers()
499        root_w, root_h = get_root_size()
500        capabilities["desktop_size"] = [root_w, root_h]
501        key_repeat = self._client_extras.get_keyboard_repeat()
502        if key_repeat:
503            delay_ms,interval_ms = key_repeat
504            capabilities["key_repeat"] = (delay_ms,interval_ms)
505        capabilities["keyboard_sync"] = self.keyboard_sync and (key_repeat is not None)
506        if self.mmap_file:
507            capabilities["mmap_file"] = self.mmap_file
508            capabilities["mmap_token"] = self.mmap_token
509        #these should be turned into options:
510        capabilities["cursors"] = True
511        capabilities["bell"] = True
512        capabilities["png_window_icons"] = "png" in ENCODINGS
513        return capabilities
514
515    def send_ping(self):
516        self.send(["ping", int(1000*time.time())])
517        return True
518
519    def _process_ping_echo(self, packet):
520        (echoedtime, l1, l2, l3, cl) = packet[1:6]
521        diff = int(1000*time.time()-echoedtime)
522        self.server_latency.append(diff)
523        self.server_load = (l1, l2, l3)
524        if cl>=0:
525            self.client_latency.append(cl)
526        log("ping echo server load=%s, measured client latency=%s", self.server_load, cl)
527
528    def _process_ping(self, packet):
529        echotime = packet[1]
530        try:
531            (fl1, fl2, fl3) = os.getloadavg()
532            l1,l2,l3 = int(fl1*1000), int(fl2*1000), int(fl3*1000)
533        except:
534            l1,l2,l3 = 0,0,0
535        sl = -1
536        if len(self.server_latency)>0:
537            sl = self.server_latency[-1]
538        self.send(["ping_echo", echotime, l1, l2, l3, sl])
539
540    def send_jpeg_quality(self, q):
541        assert q>0 and q<100
542        self.jpegquality = q
543        self.send(["jpeg-quality", self.jpegquality])
544
545    def send_refresh(self, wid):
546        self.send(["buffer-refresh", wid, True, 95])
547        self._refresh_requested = True
548
549    def send_refresh_all(self):
550        log.debug("Automatic refresh for all windows ")
551        self.send_refresh(-1)
552
553    def _process_hello(self, packet):
554        capabilities = packet[1]
555        self.server_capabilities = capabilities
556        if not self.session_name:
557            self.session_name = capabilities.get("session_name", "Xpra")
558        try:
559            import glib
560            glib.set_application_name(self.session_name)
561        except ImportError, e:
562            log.warn("glib is missing, cannot set the application name, please install glib's python bindings: %s", e)
563        self._remote_version = capabilities.get("version") or capabilities.get("__prerelease_version")
564        if not is_compatible_with(self._remote_version):
565            self.quit()
566            return
567        #figure out the maximum actual desktop size and use to
568        #calculate the maximum size of a packet (a full screen update packet)
569        root_w, root_h = get_root_size()
570        self.server_actual_desktop_size = capabilities.get("actual_desktop_size")
571        maxw, maxh = root_w, root_h
572        try:
573            server_w, server_h = self.server_actual_desktop_size
574            maxw = max(root_w, server_w)
575            maxh = max(root_h, server_h)
576        except:
577            pass
578        assert maxw>0 and maxh>0 and maxw<32768 and maxh<32768, "problems calculating maximum desktop size: %sx%s" % (maxw, maxh)
579        #full screen at 32bits times 4 for safety
580        self._protocol.max_packet_size = maxw*maxh*4*4
581        self._protocol.raw_packets = bool(capabilities.get("raw_packets", False))
582        log("set maximum packet size to %s", self._protocol.max_packet_size)
583        self.server_desktop_size = capabilities.get("desktop_size")
584        assert self.server_desktop_size
585        avail_w, avail_h = self.server_desktop_size
586        if avail_w<root_w or avail_h<root_h:
587            log.warn("Server's virtual screen is too small -- "
588                     "(server: %sx%s vs. client: %sx%s)\n"
589                     "You may see strange behavior.\n"
590                     "Please see "
591                     "https://www.xpra.org/trac/ticket/10"
592                     % (avail_w, avail_h, root_w, root_h))
593        self.server_randr = capabilities.get("resize_screen", False)
594        log.debug("server has randr: %s", self.server_randr)
595        if self.server_randr and not is_gtk3():
596            display = gdk.display_get_default()
597            i=0
598            while i<display.get_n_screens():
599                screen = display.get_screen(i)
600                screen.connect("size-changed", self._screen_size_changed)
601                i += 1
602        e = capabilities.get("encoding")
603        if e and e!=self.encoding:
604            log.debug("server is using %s encoding" % e)
605            self.encoding = e
606        self.window_configure = capabilities.get("window_configure", False)
607        self.notifications_enabled = capabilities.get("notifications", False)
608        clipboard_server_support = capabilities.get("clipboard", True)
609        self.clipboard_enabled = clipboard_server_support and self._client_extras.supports_clipboard()
610        self.mmap_enabled = self.supports_mmap and self.mmap_file and capabilities.get("mmap_enabled")
611        if self.mmap_enabled:
612            log.info("mmap enabled using %s", self.mmap_file)
613        #the server will have a handle on the mmap file by now, safe to delete:
614        self.clean_mmap()
615        self.send_deflate_level()
616        self.server_start_time = capabilities.get("start_time", -1)
617        self.server_platform = capabilities.get("platform")
618        self.toggle_cursors_bell_notify = capabilities.get("toggle_cursors_bell_notify")
619        #ui may want to know this is now set:
620        self.emit("clipboard-toggled")
621        self.key_repeat_delay, self.key_repeat_interval = capabilities.get("key_repeat", (-1,-1))
622        self.emit("handshake-complete")
623        if clipboard_server_support:
624            #from now on, we will send a message to the server whenever the clipboard flag changes:
625            self.connect("clipboard-toggled", self.send_clipboard_enabled_status)
626
627    def send_notify_enabled(self):
628        if self.toggle_cursors_bell_notify:
629            self.send(["set-notify", self.notifications_enabled])
630
631    def send_bell_enabled(self):
632        if self.toggle_cursors_bell_notify:
633            self.send(["set-bell", self.bell_enabled])
634
635    def send_cursors_enabled(self):
636        if self.toggle_cursors_bell_notify:
637            self.send(["set-cursors", self.cursors_enabled])
638
639    def send_deflate_level(self):
640        self.send(["set_deflate", self.compression_level])
641
642    def send_clipboard_enabled_status(self, *args):
643        self.send(["set-clipboard-enabled", self.clipboard_enabled])
644
645    def set_encoding(self, encoding):
646        assert encoding in ENCODINGS
647        assert encoding in self.server_capabilities.get("encodings", [])
648        self.encoding = encoding
649        self.send(["encoding", encoding])
650
651    def _screen_size_changed(self, *args):
652        root_w, root_h = get_root_size()
653        log.debug("sending updated screen size to server: %sx%s", root_w, root_h)
654        self.send(["desktop_size", root_w, root_h])
655
656    def _process_new_common(self, packet, override_redirect):
657        (wid, x, y, w, h, metadata) = packet[1:7]
658        if w<=0 or h<=0:
659            log.error("window dimensions are wrong: %sx%s", w, h)
660            w = 10
661            h = 5
662        window = ClientWindowClass(self, wid, x, y, w, h, metadata, override_redirect)
663        self._id_to_window[wid] = window
664        self._window_to_id[window] = wid
665        window.show_all()
666
667    def _process_new_window(self, packet):
668        self._process_new_common(packet, False)
669
670    def _process_new_override_redirect(self, packet):
671        self._process_new_common(packet, True)
672
673    def _process_draw(self, packet):
674        (wid, x, y, width, height, coding, data, packet_sequence, rowstride) = packet[1:10]
675        window = self._id_to_window.get(wid)
676        if window:
677            start = time.time()
678            window.draw_region(x, y, width, height, coding, data, rowstride)
679            end = time.time()
680            self.pixel_counter.append((end, width*height))
681            decode_time = int(end*1000*1000-start*1000*1000)
682        else:
683            decode_time = 0
684            #window is gone
685            if coding=="mmap":
686                #we need to ack the data to free the space!
687                assert self.mmap_enabled
688                data_start = ctypes.c_uint.from_buffer(self.mmap, 0)
689                offset, length = data[-1]
690                data_start.value = offset+length
691        if packet_sequence:
692            self.send_now(["damage-sequence", packet_sequence, wid, width, height, decode_time])
693
694    def _process_cursor(self, packet):
695        (_, new_cursor) = packet
696        set_windows_cursor(self._id_to_window.values(), new_cursor)
697
698    def _process_bell(self, packet):
699        if not self.bell_enabled:
700            return
701        (wid, device, percent, pitch, duration, bell_class, bell_id, bell_name) = packet[1:9]
702        gdkwindow = None
703        if wid!=0:
704            try:
705                gdkwindow = self._id_to_window[wid].get_window()
706            except:
707                pass
708        if gdkwindow is None:
709            gdkwindow = gdk.get_default_root_window()
710        log("_process_bell(%s) gdkwindow=%s", packet, gdkwindow)
711        self._client_extras.system_bell(gdkwindow, device, percent, pitch, duration, bell_class, bell_id, bell_name)
712
713    def _process_notify_show(self, packet):
714        if not self.notifications_enabled:
715            return
716        (dbus_id, nid, app_name, replaces_nid, app_icon, summary, body, expire_timeout) = packet[1:9]
717        log("_process_notify_show(%s)", packet)
718        self._client_extras.show_notify(dbus_id, nid, app_name, replaces_nid, app_icon, summary, body, expire_timeout)
719
720    def _process_notify_close(self, packet):
721        if not self.notifications_enabled:
722            return
723        nid = packet[1]
724        log("_process_notify_close(%s)", nid)
725        self._client_extras.close_notify(nid)
726
727    def _process_window_metadata(self, packet):
728        (wid, metadata) = packet[1:3]
729        window = self._id_to_window[wid]
730        window.update_metadata(metadata)
731
732    def _process_configure_override_redirect(self, packet):
733        (wid, x, y, w, h) = packet[1:6]
734        window = self._id_to_window[wid]
735        window.move_resize(x, y, w, h)
736
737    def _process_lost_window(self, packet):
738        wid = packet[1]
739        window = self._id_to_window.get(wid)
740        if window:
741            del self._id_to_window[wid]
742            del self._window_to_id[window]
743            if window._refresh_timer:
744                gobject.source_remove(window._refresh_timer)
745            window.destroy()
746        if len(self._id_to_window)==0:
747            log.debug("last window gone, clearing key repeat")
748            self.clear_repeat()
749
750    def process_packet(self, proto, packet):
751        packet_type = str(packet[0])
752        if packet_type.startswith("clipboard-"):
753            if self.clipboard_enabled:
754                self._client_extras.process_clipboard_packet(packet)
755        else:
756            XpraClientBase.process_packet(self, proto, packet)
757
758gobject.type_register(XpraClient)
Note: See TracBrowser for help on using the repository browser.