This repository has been archived by the owner on Nov 18, 2017. It is now read-only.
/
window.py
1385 lines (1224 loc) · 61.9 KB
/
window.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# This file is part of Xpra.
# Copyright (C) 2008, 2009 Nathaniel Smith <njs@pobox.com>
# Copyright (C) 2011-2013 Antoine Martin <antoine@devloop.org.uk>
# Xpra is released under the terms of the GNU GPL v2, or, at your option, any
# later version. See the file COPYING for details.
"""The magic GTK widget that represents a client window.
Most of the gunk required to be a valid window manager (reparenting, synthetic
events, mucking about with properties, etc. etc.) is wrapped up in here."""
# Maintain compatibility with old versions of Python, while avoiding a
# deprecation warning on new versions:
import sys
if sys.version_info < (2, 6):
from sets import ImmutableSet
else:
ImmutableSet = frozenset
import gobject
import gtk.gdk
import cairo
import os
from socket import gethostname
from xpra.x11.bindings.window_bindings import constants, X11WindowBindings #@UnresolvedImport
X11Window = X11WindowBindings()
from xpra.x11.gtk_x11.gdk_bindings import (
get_pyatom, #@UnresolvedImport
add_event_receiver, #@UnresolvedImport
remove_event_receiver, #@UnresolvedImport
get_display_for, #@UnresolvedImport
calc_constrained_size, #@UnresolvedImport
get_xwindow, #@UnresolvedImport
)
from xpra.x11.gtk_x11.send_wm import (
send_wm_take_focus, #@UnresolvedImport
send_wm_delete_window) #@UnresolvedImport
from xpra.gtk_common.gobject_util import (AutoPropGObjectMixin,
one_arg_signal,
non_none_list_accumulator)
from xpra.x11.gtk_x11.error import trap, XError
from xpra.x11.gtk_x11.prop import prop_get, prop_set
from xpra.x11.gtk_x11.composite import CompositeHelper
from xpra.log import Logger
log = Logger()
if gtk.pygtk_version<(2,17):
log.error("your version of PyGTK is too old - expect some bugs")
#if you want to use a virtual screen bigger than 32767x32767
#you will need to change those values, but some broken toolkits
#will then misbehave (they use signed shorts instead of signed ints..)
MAX_WINDOW_SIZE = 2**15-1
MAX_ASPECT = 2**15-1
USE_XSHM = os.environ.get("XPRA_XSHM", "1")=="1"
# Todo:
# client focus hints
# _NET_WM_SYNC_REQUEST
# root window requests (pagers, etc. requesting to change client states)
# _NET_WM_PING/detect window not responding (also a root window message)
# Okay, we need a block comment to explain the window arrangement that this
# file is working with.
#
# +--------+
# | widget |
# +--------+
# / \
# <- top / -\- bottom ->
# / \
# +-------+ |
# | image | +---------+
# +-------+ | corral |
# +---------+
# |
# +---------+
# | client |
# +---------+
#
# Each box in this diagram represents one X/GDK window. In the common case,
# every window here takes up exactly the same space on the screen (!). In
# fact, the two windows on the right *always* have exactly the same size and
# location, and the window on the left and the top window also always have
# exactly the same size and position. However, each window in the diagram
# plays a subtly different role.
#
# The client window is obvious -- this is the window owned by the client,
# which they created and which we have various ICCCM/EWMH-mandated
# responsibilities towards. It is also composited.
#
# The purpose of the 'corral' is to keep the client window managed -- we
# select for SubstructureRedirect on it, so that the client cannot resize
# etc. without going through the WM.
#
# These two windows are always managed together, as a unit; an invariant of
# the code is that they always take up exactly the same space on the screen.
# They get reparented back and forth between widgets, and when there are no
# widgets, they get reparented to a "parking area". For now, we're just using
# the root window as a parking area, so we also map/unmap the corral window
# depending on whether we are parked or not; the corral and window is left
# mapped at all times.
#
# When a particular WindowView controls the underlying client window, then two
# things happen:
# -- Its size determines the size of the client window. Ideally they are
# the same size -- but this is not always the case, because the client
# may have specified sizing constraints, in which case the client window
# is the "best fit" to the controlling widget window.
# -- The client window and its corral are reparented under the widget
# window, as in the diagram above. This is necessary to allow mouse
# events to work -- a WindowView widget can always *look* like the client
# window is there, through the magic of Composite, but in order for it to
# *act* like the client window is there in terms of receiving mouse
# events, it has to actually be there.
#
# We should also have a block comment describing how to create a
# view/"controller" for a WindowModel.
#
# Viewing a (Base)WindowModel is easy. Connect to the client-contents-changed
# signal. Every time the window contents is updated, you'll get a message.
# This message is passed a single object e, which has useful members:
# e.x, e.y, e.width, e.height:
# The part of the client window that was modified, and needs to be
# redrawn.
# To get the actual contents of the window to draw, there is a "handle"
# available as the "contents-handle" property on the Composite window.
#
# But what if you'd like to do more than just look at your pretty composited
# windows? Maybe you'd like to, say, *interact* with them? Then life is a
# little more complicated. To make a view "live", we have to move the actual
# client window to be a child of your view window and position it correctly.
# Obviously, only one view can be live at any given time, so we have to figure
# out which one that is. Supposing we have a WindowModel called "model" and
# a view called "view", then the following pieces come into play:
# The "ownership-election" signal on window:
# If a view wants the chance to become live, it must connect to this
# signal. When the signal is emitted, its handler should return a tuple
# of the form:
# (votes, my_view)
# Just like a real election, everyone votes for themselves. The view that
# gives the highest value to 'votes' becomes the new owner. However, a
# view with a negative (< 0) votes value will never become the owner.
# model.ownership_election():
# This method (distinct from the ownership-election signal!) triggers an
# election. All views MUST call this method whenever they decide their
# number of votes has changed. All views MUST call this method when they
# are destructing themselves (ideally after disconnecting from the
# ownership-election signal).
# The "owner" property on window:
# This records the view that currently owns the window (i.e., the winner
# of the last election), or None if no view is live.
# view.take_window(model, window):
# This method is called on 'view' when it becomes owner of 'model'. It
# should reparent 'window' into the appropriate place, and put it at the
# appropriate place in its window stack. (The x,y position, however, does
# not matter.)
# view.window_size(model):
# This method is called when the model needs to know how much space it is
# allocated. It should return the maximum (width, height) allowed.
# (However, the model may choose to use less than this.)
# view.window_position(mode, width, height):
# This method is called when the model needs to know where it should be
# located (relative to the parent window the view placed it in). 'width'
# and 'height' are the size the model window will actually be. It should
# return the (x, y) position desired.
# model.maybe_recalculate_geometry_for(view):
# This method (potentially) triggers a resize/move of the client window
# within the view. If 'view' is not the current owner, is a no-op, which
# means that views can call it without worrying about whether they are in
# fact the current owner.
#
# The actual method for choosing 'votes' is not really determined yet.
# Probably it should take into account at least the following factors:
# -- has focus (or has mouse-over?)
# -- is visible in a tray/other window, and the tray/other window is visible
# -- and is focusable
# -- and is not focusable
# -- is visible in a tray, and the tray/other window is not visible
# -- and is focusable
# -- and is not focusable
# (NB: Widget.get_ancestor(my.Tray) will give us the nearest ancestor
# that isinstance(my.Tray), if any.)
# -- is not visible
# -- the size of the widget (as a final tie-breaker)
class Unmanageable(Exception):
pass
class BaseWindowModel(AutoPropGObjectMixin, gobject.GObject):
__gproperties__ = {
"client-window": (gobject.TYPE_PYOBJECT,
"gtk.gdk.Window representing the client toplevel", "",
gobject.PARAM_READABLE),
"geometry": (gobject.TYPE_PYOBJECT,
"current (border-corrected, relative to parent) coordinates (x, y, w, h) for the window", "",
gobject.PARAM_READABLE),
"transient-for": (gobject.TYPE_PYOBJECT,
"Transient for (or None)", "",
gobject.PARAM_READABLE),
"pid": (gobject.TYPE_INT,
"PID of owning process", "",
-1, 65535, -1,
gobject.PARAM_READABLE),
"xid": (gobject.TYPE_INT,
"X11 window id", "",
-1, 65535, -1,
gobject.PARAM_READABLE),
"title": (gobject.TYPE_PYOBJECT,
"Window title (unicode or None)", "",
gobject.PARAM_READABLE),
"group-leader": (gobject.TYPE_PYOBJECT,
"Window group leader as a pair: (xid, gdk window)", "",
gobject.PARAM_READABLE),
"attention-requested": (gobject.TYPE_BOOLEAN,
"Urgency hint from client, or us", "",
False,
gobject.PARAM_READWRITE),
"can-focus": (gobject.TYPE_BOOLEAN,
"Does this window ever accept keyboard input?", "",
True,
gobject.PARAM_READWRITE),
"has-alpha": (gobject.TYPE_BOOLEAN,
"Does the window use transparency", "",
False,
gobject.PARAM_READABLE),
"fullscreen": (gobject.TYPE_BOOLEAN,
"Fullscreen-ness of window", "",
False,
gobject.PARAM_READWRITE),
"maximized": (gobject.TYPE_BOOLEAN,
"Is the window maximized", "",
False,
gobject.PARAM_READWRITE),
"override-redirect": (gobject.TYPE_BOOLEAN,
"Is the window of type override-redirect", "",
False,
gobject.PARAM_READABLE),
"tray": (gobject.TYPE_BOOLEAN,
"Is the window a system tray icon", "",
False,
gobject.PARAM_READABLE),
"scaling" : (gobject.TYPE_PYOBJECT,
"Application requested scaling as a fraction (pair of numbers)", "",
gobject.PARAM_READWRITE),
"role" : (gobject.TYPE_PYOBJECT,
"The window's role (ICCCM session management)", "",
gobject.PARAM_READABLE),
"modal": (gobject.TYPE_PYOBJECT,
"Modal (boolean)", "",
gobject.PARAM_READABLE),
"window-type": (gobject.TYPE_PYOBJECT,
"Window type",
"NB, most preferred comes first, then fallbacks",
gobject.PARAM_READABLE),
}
__gsignals__ = {
"client-contents-changed": one_arg_signal,
"raised": one_arg_signal,
"unmanaged": one_arg_signal,
# this signal must be defined in the subclasses to be seen by the event stuff:
# "xpra-configure-event": one_arg_signal,
}
def __init__(self, client_window):
log("new window %s - %s", hex(client_window.xid), hex(get_xwindow(client_window)))
super(BaseWindowModel, self).__init__()
self.client_window = client_window
self.client_window_saved_events = self.client_window.get_events()
self._managed = False
self._managed_handlers = []
self._setup_done = False
self._input_field = True # The WM_HINTS input field
self._geometry = None
self._damage_forward_handle = None
self._last_wm_state_serial = 0
self._internal_set_property("client-window", client_window)
use_xshm = USE_XSHM and (not self.is_OR() and not self.is_tray())
self._composite = CompositeHelper(self.client_window, False, use_xshm)
self.property_names = ["pid", "transient-for", "fullscreen", "maximized", "window-type", "role", "group-leader", "xid", "has-alpha"]
def get_property_names(self):
return self.property_names
def managed_connect(self, detailed_signal, handler, *args):
""" connects a signal handler and makes sure we will clean it up on unmanage() """
handler_id = self.connect(detailed_signal, handler, *args)
self._managed_handlers.append(handler_id)
return handler_id
def managed_disconnect(self):
for handler_id in self._managed_handlers:
self.disconnect(handler_id)
def call_setup(self):
log("call_setup()")
try:
self._geometry = trap.call_synced(X11Window.geometry_with_border, get_xwindow(self.client_window))
except XError, e:
raise Unmanageable(e)
log("call_setup() adding event receiver")
add_event_receiver(self.client_window, self)
# Keith Packard says that composite state is undefined following a
# reparent, so I'm not sure doing this here in the superclass,
# before we reparent, actually works... let's wait and see.
log("call_setup() composite setup")
try:
trap.call_synced(self._composite.setup)
except XError, e:
remove_event_receiver(self.client_window, self)
log("window %s does not support compositing: %s", hex(get_xwindow(self.client_window)), e)
trap.swallow_synced(self._composite.destroy)
self._composite = None
raise Unmanageable(e)
#compositing is now enabled, from now on we need to call setup_failed to clean things up
self._managed = True
try:
trap.call_synced(self.setup)
except XError, e:
try:
trap.call_synced(self.setup_failed, e)
except Exception, ex:
log.error("error in cleanup handler: %s", ex)
raise Unmanageable(e)
self._setup_done = True
log("call_setup() ended, property_handlers=%s", self._property_handlers)
def setup_failed(self, e):
log("cannot manage %s: %s", hex(get_xwindow(self.client_window)), e)
self.do_unmanaged(False)
def setup(self):
h = self._composite.connect("contents-changed", self._forward_contents_changed)
self._damage_forward_handle = h
def prop_get(self, key, ptype, ignore_errors=False, raise_xerrors=False):
# Utility wrapper for prop_get on the client_window
# also allows us to ignore property errors during setup_client
if not self._setup_done:
ignore_errors = True
return prop_get(self.client_window, key, ptype, ignore_errors=ignore_errors, raise_xerrors=raise_xerrors)
def is_managed(self):
return self._managed
def _forward_contents_changed(self, obj, event):
if self._managed:
self.emit("client-contents-changed", event)
def acknowledge_changes(self):
self._composite.acknowledge_changes()
################################
# Property reading
################################
def do_xpra_property_notify_event(self, event):
assert event.window is self.client_window
self._handle_property_change(str(event.atom))
_property_handlers = {}
def _handle_property_change(self, name):
log("Property changed on %s: %s", self.client_window.xid, name)
if name in self._property_handlers:
self._property_handlers[name](self)
def do_xpra_configure_event(self, event):
if self.client_window is None or not self._managed:
return
oldgeom = self._geometry
self._geometry = (event.x, event.y, event.width, event.height,
event.border_width)
log("BaseWindowModel.do_xpra_configure_event(%s) old geometry=%s, new geometry=%s", event, oldgeom, self._geometry)
if oldgeom!=self._geometry:
self.notify("geometry")
def do_get_property_geometry(self, pspec):
if self._geometry is None:
def synced_update():
xwin = get_xwindow(self.client_window)
self._geometry = X11Window.geometry_with_border(xwin)
log("BaseWindowModel.synced_update() geometry(%s)=%s", hex(xwin), self._geometry)
try:
trap.call_unsynced(synced_update)
except XError:
log.error("failed to retrieve updated window geometry - maybe it's gone?", exc_info=True)
x, y, w, h, b = self._geometry
return (x, y, w + 2*b, h + 2*b)
def get_position(self):
return self.do_get_property_geometry(None)[:2]
def unmanage(self, exiting=False):
if self._managed:
self.emit("unmanaged", exiting)
def do_unmanaged(self, wm_exiting):
if not self._managed:
return
self._managed = False
log("do_unmanaged(%s) damage_forward_handle=%s, composite=%s", wm_exiting, self._damage_forward_handle, self._composite)
remove_event_receiver(self.client_window, self)
gobject.idle_add(self.managed_disconnect)
if self._composite:
if self._damage_forward_handle:
self._composite.disconnect(self._damage_forward_handle)
self._damage_forward_handle = None
self._composite.destroy()
self._composite = None
def _read_initial_properties(self):
def pget(key, ptype, raise_xerrors=True):
return self.prop_get(key, ptype, raise_xerrors=raise_xerrors)
transient_for = pget("WM_TRANSIENT_FOR", "window")
# May be None
self._internal_set_property("transient-for", transient_for)
window_types = pget("_NET_WM_WINDOW_TYPE", ["atom"])
if not window_types:
window_type = self._guess_window_type(transient_for)
window_types = [gtk.gdk.atom_intern(window_type)]
self._internal_set_property("window-type", window_types)
self._handle_scaling()
self._internal_set_property("has-alpha", self.client_window.get_depth()==32)
self._internal_set_property("xid", get_xwindow(self.client_window))
self._internal_set_property("pid", pget("_NET_WM_PID", "u32") or -1)
self._internal_set_property("role", pget("WM_WINDOW_ROLE", "latin1"))
for mutable in ["WM_NAME", "_NET_WM_NAME"]:
log("reading initial value for %s", mutable)
self._handle_property_change(mutable)
def _handle_scaling(self):
scaling = self.prop_get("_XPRA_SCALING", "u32", raise_xerrors=False)
scaling_v, scaling_u = 1, 1
if scaling>0:
scaling_v = scaling & 0xFFFF
scaling_u = (scaling >> 16) & 0xFFFF
self._internal_set_property("scaling", (scaling_v, scaling_u))
else:
self._internal_set_property("scaling", None)
_property_handlers["_XPRA_SCALING"] = _handle_scaling
def _handle_title_change(self):
net_wm_name = self.prop_get("_NET_WM_NAME", "utf8", True)
if net_wm_name is not None:
self._internal_set_property("title", net_wm_name)
else:
# may be None
wm_name = self.prop_get("WM_NAME", "latin1", True)
self._internal_set_property("title", wm_name)
_property_handlers["WM_NAME"] = _handle_title_change
_property_handlers["_NET_WM_NAME"] = _handle_title_change
def _handle_wm_hints(self):
wm_hints = self.prop_get("WM_HINTS", "wm-hints", True)
if wm_hints is not None:
# GdkWindow or None
self._internal_set_property("group-leader", wm_hints.group_leader)
# FIXME: extract state and input hint
if wm_hints.urgency:
self.set_property("attention-requested", True)
log("wm_hints.input = %s", wm_hints.input)
#we only set this value once:
#(input_field always starts as True, and we then set it to an int)
if self._input_field is True and wm_hints.input is not None:
#keep the value as an int to differentiate from the start value:
self._input_field = int(wm_hints.input)
if bool(self._input_field):
self.notify("can-focus")
_property_handlers["WM_HINTS"] = _handle_wm_hints
def _guess_window_type(self, transient_for):
if transient_for is not None:
# EWMH says that even if it's transient-for, we MUST check to
# see if it's override-redirect (and if so treat as NORMAL).
# But we wouldn't be here if this was override-redirect.
# (OverrideRedirectWindowModel overrides this method)
return "_NET_WM_TYPE_DIALOG"
return "_NET_WM_WINDOW_TYPE_NORMAL"
def is_tray(self):
return False
def uses_XShm(self):
return self._composite and self._composite.get_property("shm-handle") is not None
def has_alpha(self):
return self.get_property("has-alpha")
def get_image(self, x, y, width, height, logger=log.debug):
handle = self._composite.get_property("contents-handle")
if handle is None:
logger("get_image(..) pixmap is None for window %s", hex(get_xwindow(self.client_window)))
return None
#try XShm:
try:
logger("get_image(%s, %s, %s, %s) geometry=%s", x, y, width, height, self._geometry[:4])
shm = self._composite.get_property("shm-handle")
logger("get_image(..) XShm handle: %s, handle=%s, pixmap=%s", shm, handle, handle.get_pixmap())
if shm is not None:
shm_image = trap.call_synced(shm.get_image, handle.get_pixmap(), x, y, width, height)
logger("get_image(..) XShm image: %s", shm_image)
if shm_image:
return shm_image
except Exception, e:
if type(e)==XError and e.msg=="BadMatch":
logger("get_image(%s, %s, %s, %s) get_image BadMatch ignored (window already gone?)", x, y, width, height)
else:
log.warn("get_image(%s, %s, %s, %s) get_image %s", x, y, width, height, e, exc_info=True)
try:
w = min(handle.get_width(), width)
h = min(handle.get_height(), height)
if w!=width or h!=height:
logger("get_image(%s, %s, %s, %s) clamped to pixmap dimensions: %sx%s", x, y, width, height, w, h)
return trap.call_synced(handle.get_image, x, y, w, h)
except Exception, e:
if type(e)==XError and e.msg=="BadMatch":
logger("get_image(%s, %s, %s, %s) get_image BadMatch ignored (window already gone?)", x, y, width, height)
else:
log.warn("get_image(%s, %s, %s, %s) get_image %s", x, y, width, height, e, exc_info=True)
return None
def do_xpra_client_message_event(self, event):
# FIXME
# Need to listen for:
# _NET_CLOSE_WINDOW
# _NET_CURRENT_DESKTOP
# _NET_REQUEST_FRAME_EXTENTS
# _NET_WM_PING responses
# and maybe:
# _NET_RESTACK_WINDOW
# _NET_WM_DESKTOP
# _NET_WM_STATE (more fully)
if event.message_type=="_NET_WM_STATE" and event.data and len(event.data)==5:
atom1 = get_pyatom(event.window, event.data[1])
atom2 = get_pyatom(event.window, event.data[2])
_NET_WM_STATE_REMOVE = 0
_NET_WM_STATE_ADD = 1
if atom1=="_NET_WM_STATE_FULLSCREEN":
fullscreen = event.data[0]==_NET_WM_STATE_ADD
log("do_xpra_client_message_event(..) setting fullscreen=%s", fullscreen)
self.set_property("fullscreen", fullscreen)
elif atom1 in ("_NET_WM_STATE_MAXIMIZED_VERT", "_NET_WM_STATE_MAXIMIZED_HORZ") and \
atom2 in ("_NET_WM_STATE_MAXIMIZED_VERT", "_NET_WM_STATE_MAXIMIZED_HORZ"):
if self._last_wm_state_serial == event.serial:
#already seen!
return
maximized = event.data[0]==_NET_WM_STATE_ADD
log("do_xpra_client_message_event(%s) window maximized=%s", event, maximized)
self.set_property("maximized", maximized)
else:
log("do_xpra_client_message_event(%s) atom=%s", event, atom1)
if event.message_type=="_NET_ACTIVE_WINDOW" and event.data and len(event.data)==5 and event.data[0]==1:
self.emit("raised", event)
else:
log("do_xpra_client_message_event(%s)", event)
self._last_wm_state_serial = event.serial
gobject.type_register(BaseWindowModel)
# FIXME: EWMH says that O-R windows should set various properties just like
# ordinary managed windows; so some of that code should get pushed up into the
# superclass sooner or later. When someone cares, presumably.
class OverrideRedirectWindowModel(BaseWindowModel):
__gsignals__ = {
"xpra-configure-event": one_arg_signal,
"xpra-unmap-event": one_arg_signal,
"xpra-client-message-event" : one_arg_signal,
"xpra-property-notify-event": one_arg_signal,
}
def __init__(self, client_window):
super(OverrideRedirectWindowModel, self).__init__(client_window)
self.property_names.append("override-redirect")
def call_setup(self):
self._read_initial_properties()
BaseWindowModel.call_setup(self)
def setup(self):
BaseWindowModel.setup(self)
self.client_window.set_events(self.client_window_saved_events
| gtk.gdk.STRUCTURE_MASK)
# So now if the window becomes unmapped in the future then we will
# notice... but it might be unmapped already, and any event
# already generated, and our request for that event is too late!
# So double check now, *after* putting in our request:
if not X11Window.is_mapped(get_xwindow(self.client_window)):
raise Unmanageable("window already unmapped")
ch = self._composite.get_property("contents-handle")
if ch is None:
raise Unmanageable("failed to get damage handle")
def _read_initial_properties(self):
BaseWindowModel._read_initial_properties(self)
self._internal_set_property("override-redirect", True)
def _guess_window_type(self, transient_for):
return "_NET_WM_WINDOW_TYPE_NORMAL"
def do_xpra_unmap_event(self, event):
self.unmanage()
def get_dimensions(self):
ww, wh = self._geometry[2:4]
return ww, wh
def is_OR(self):
return True
def raise_window(self):
self.client_window.raise_()
def __repr__(self):
return "OverrideRedirectWindowModel(%s)" % self.client_window
gobject.type_register(OverrideRedirectWindowModel)
class SystemTrayWindowModel(OverrideRedirectWindowModel):
def __init__(self, client_window):
OverrideRedirectWindowModel.__init__(self, client_window)
self.property_names = ["pid", "role", "xid", "has-alpha", "tray", "title"]
def is_tray(self):
return True
def _read_initial_properties(self):
BaseWindowModel._read_initial_properties(self)
self._internal_set_property("tray", True)
def composite_configure_event(self, composite_window, event):
BaseWindowModel.composite_configure_event(self, composite_window, event)
log("SystemTrayWindowModel.composite_configure_event(%s, %s) client window geometry=%s", composite_window, event, self.client_window.get_geometry())
def move_resize(self, x, y, width, height):
#Used by clients to tell us where the tray is located on screen
log("SystemTrayWindowModel.move_resize(%s, %s, %s, %s)", x, y, width, height)
self.client_window.move_resize(x, y, width, height)
border = self._geometry[4]
self._geometry = (x, y, width, height, border)
def __repr__(self):
return "SystemTrayWindowModel(%s)" % self.client_window
class WindowModel(BaseWindowModel):
"""This represents a managed client window. It allows one to produce
widgets that view that client window in various ways."""
_NET_WM_ALLOWED_ACTIONS = [
"_NET_WM_ACTION_CLOSE",
]
__gproperties__ = {
# Interesting properties of the client window, that will be
# automatically kept up to date:
"actual-size": (gobject.TYPE_PYOBJECT,
"Size of client window (actual (width,height))", "",
gobject.PARAM_READABLE),
"user-friendly-size": (gobject.TYPE_PYOBJECT,
"Description of client window size for user", "",
gobject.PARAM_READABLE),
"requested-position": (gobject.TYPE_PYOBJECT,
"Client-requested position on screen", "",
gobject.PARAM_READABLE),
"requested-size": (gobject.TYPE_PYOBJECT,
"Client-requested size on screen", "",
gobject.PARAM_READABLE),
"size-hints": (gobject.TYPE_PYOBJECT,
"Client hints on constraining its size", "",
gobject.PARAM_READABLE),
"strut": (gobject.TYPE_PYOBJECT,
"Struts requested by window, or None", "",
gobject.PARAM_READABLE),
"class-instance": (gobject.TYPE_PYOBJECT,
"Classic X 'class' and 'instance'", "",
gobject.PARAM_READABLE),
"protocols": (gobject.TYPE_PYOBJECT,
"Supported WM protocols", "",
gobject.PARAM_READABLE),
"client-machine": (gobject.TYPE_PYOBJECT,
"Host where client process is running", "",
gobject.PARAM_READABLE),
# Toggling this property does not actually make the window iconified,
# i.e. make it appear or disappear from the screen -- it merely
# updates the various window manager properties that inform the world
# whether or not the window is iconified.
"iconic": (gobject.TYPE_BOOLEAN,
"ICCCM 'iconic' state -- any sort of 'not on desktop'.", "",
False,
gobject.PARAM_READWRITE),
"state": (gobject.TYPE_PYOBJECT,
"State, as per _NET_WM_STATE", "",
gobject.PARAM_READABLE),
"icon-title": (gobject.TYPE_PYOBJECT,
"Icon title (unicode or None)", "",
gobject.PARAM_READABLE),
"icon": (gobject.TYPE_PYOBJECT,
"Icon (local Cairo surface)", "",
gobject.PARAM_READABLE),
"icon-pixmap": (gobject.TYPE_PYOBJECT,
"Icon (server Pixmap)", "",
gobject.PARAM_READABLE),
"owner": (gobject.TYPE_PYOBJECT,
"Owner", "",
gobject.PARAM_READABLE),
}
__gsignals__ = {
# X11 bell event:
"bell": one_arg_signal,
"ownership-election": (gobject.SIGNAL_RUN_LAST,
gobject.TYPE_PYOBJECT, (),
non_none_list_accumulator),
"xpra-property-notify-event": one_arg_signal,
"xpra-configure-event": one_arg_signal,
"child-map-request-event": one_arg_signal,
"child-configure-request-event": one_arg_signal,
"xpra-client-message-event" : one_arg_signal,
"xpra-unmap-event": one_arg_signal,
"xpra-destroy-event": one_arg_signal,
"xpra-xkb-event": one_arg_signal,
}
def __init__(self, parking_window, client_window):
"""Register a new client window with the WM.
Raises an Unmanageable exception if this window should not be
managed, for whatever reason. ATM, this mostly means that the window
died somehow before we could do anything with it."""
super(WindowModel, self).__init__(client_window)
self.parking_window = parking_window
self.corral_window = None
self.in_save_set = False
self.client_reparented = False
self.startup_unmap_serial = None
self.connect("notify::iconic", self._handle_iconic_update)
self.property_names += ["title", "icon-title", "size-hints", "class-instance", "icon", "client-machine", "modal"]
self.call_setup()
def setup(self):
BaseWindowModel.setup(self)
x, y, w, h, _ = self.client_window.get_geometry()
# We enable PROPERTY_CHANGE_MASK so that we can call
# x11_get_server_time on this window.
self.corral_window = gtk.gdk.Window(self.parking_window,
x = x, y = y, width =w, height= h,
window_type=gtk.gdk.WINDOW_CHILD,
wclass=gtk.gdk.INPUT_OUTPUT,
event_mask=gtk.gdk.PROPERTY_CHANGE_MASK,
title = "CorralWindow-0x%s" % self.client_window.xid)
log("setup() corral_window=%s", self.corral_window)
X11Window.substructureRedirect(get_xwindow(self.corral_window))
add_event_receiver(self.corral_window, self)
# Start listening for important events.
self.client_window.set_events(self.client_window_saved_events
| gtk.gdk.STRUCTURE_MASK
| gtk.gdk.PROPERTY_CHANGE_MASK)
# The child might already be mapped, in case we inherited it from
# a previous window manager. If so, we unmap it now, and save the
# serial number of the request -- this way, when we get an
# UnmapNotify later, we'll know that it's just from us unmapping
# the window, not from the client withdrawing the window.
if X11Window.is_mapped(get_xwindow(self.client_window)):
log("hiding inherited window")
self.startup_unmap_serial = X11Window.Unmap(get_xwindow(self.client_window))
# Process properties
self._read_initial_properties()
self._write_initial_properties_and_setup()
# For now, we never use the Iconic state at all.
self._internal_set_property("iconic", False)
log("setup() adding to save set")
X11Window.XAddToSaveSet(get_xwindow(self.client_window))
self.in_save_set = True
log("setup() reparenting")
self.client_window.reparent(self.corral_window, 0, 0)
self.client_reparented = True
log("setup() geometry")
w,h = self.client_window.get_geometry()[2:4]
hints = self.get_property("size-hints")
self._sanitize_size_hints(hints)
nw, nh = calc_constrained_size(w, h, hints)[:2]
if nw>=MAX_WINDOW_SIZE or nh>=MAX_WINDOW_SIZE:
#we can't handle windows that big!
raise Unmanageable("window constrained size is too large: %sx%s (from client geometry: %s,%s with size hints=%s)" % (nw, nh, w, h, hints))
log("setup() resizing windows to %sx%s", nw, nh)
self.client_window.resize(nw, nh)
self.corral_window.resize(nw, nh)
self.client_window.show_unraised()
self.client_window.get_geometry()
def is_OR(self):
return False
def raise_window(self):
self.corral_window.raise_()
def get_dimensions(self):
return self.get_property("actual-size")
def do_xpra_xkb_event(self, event):
log("WindowModel.do_xpra_xkb_event(%r)" % event)
if event.type!="bell":
log.error("WindowModel.do_xpra_xkb_event(%r) unknown event type: %s" % (event, event.type))
return
event.window_model = self
self.emit("bell", event)
def do_xpra_property_notify_event(self, event):
if event.delivered_to is self.corral_window:
return
BaseWindowModel.do_xpra_property_notify_event(self, event)
def do_child_map_request_event(self, event):
# If we get a MapRequest then it might mean that someone tried to map
# this window multiple times in quick succession, before we actually
# mapped it (so that several MapRequests ended up queued up; FSF Emacs
# 22.1.50.1 does this, at least). It alternatively might mean that
# the client is naughty and tried to map their window which is
# currently not displayed. In either case, we should just ignore the
# request.
pass
def do_xpra_unmap_event(self, event):
if event.delivered_to is self.corral_window or self.corral_window is None:
return
assert event.window is self.client_window
# The client window got unmapped. The question is, though, was that
# because it was withdrawn/destroyed, or was it because we unmapped it
# going into IconicState?
#
# At the moment, we never actually put windows into IconicState
# (i.e. unmap them), except in the special case when we start up and
# find windows that are already mapped. So we only need to check
# against that one serial number.
#
# Also, if we receive a *synthetic* UnmapNotify event, that always
# means that the client has withdrawn the window (even if it was not
# mapped in the first place) -- ICCCM section 4.1.4.
log("Client window unmapped")
if event.send_event or event.serial != self.startup_unmap_serial:
self.unmanage()
def do_xpra_destroy_event(self, event):
if event.delivered_to is self.corral_window or self.corral_window is None:
return
assert event.window is self.client_window
# This is somewhat redundant with the unmap signal, because if you
# destroy a mapped window, then a UnmapNotify is always generated.
# However, this allows us to catch the destruction of unmapped
# ("iconified") windows, and also catch any mistakes we might have
# made with unmap heuristics. I love the smell of XDestroyWindow in
# the morning. It makes for simple code:
self.unmanage()
SCRUB_PROPERTIES = ["WM_STATE",
"_NET_WM_STATE",
"_NET_FRAME_EXTENTS",
"_NET_WM_ALLOWED_ACTIONS",
]
def do_unmanaged(self, wm_exiting):
log("unmanaging window: %s (%s - %s)", self, self.corral_window, self.client_window)
self._internal_set_property("owner", None)
if self.corral_window:
remove_event_receiver(self.corral_window, self)
for prop in WindowModel.SCRUB_PROPERTIES:
trap.swallow_synced(X11Window.XDeleteProperty, get_xwindow(self.client_window), prop)
if self.client_reparented:
self.client_window.reparent(gtk.gdk.get_default_root_window(), 0, 0)
self.client_reparented = False
self.client_window.set_events(self.client_window_saved_events)
#it is now safe to destroy the corral window:
self.corral_window.destroy()
self.corral_window = None
# It is important to remove from our save set, even after
# reparenting, because according to the X spec, windows that are
# in our save set are always Mapped when we exit, *even if those
# windows are no longer inferior to any of our windows!* (see
# section 10. Connection Close). This causes "ghost windows", see
# bug #27:
if self.in_save_set:
trap.swallow_synced(X11Window.XRemoveFromSaveSet, get_xwindow(self.client_window))
self.in_save_set = False
trap.swallow_synced(X11Window.sendConfigureNotify, get_xwindow(self.client_window))
if wm_exiting:
self.client_window.show_unraised()
BaseWindowModel.do_unmanaged(self, wm_exiting)
def ownership_election(self):
candidates = self.emit("ownership-election")
if candidates:
rating, winner = sorted(candidates)[-1]
if rating < 0:
winner = None
else:
winner = None
old_owner = self.get_property("owner")
if old_owner is winner:
return
if old_owner is not None:
self.corral_window.hide()
self.corral_window.reparent(self.parking_window, 0, 0)
self._internal_set_property("owner", winner)
if winner is not None:
winner.take_window(self, self.corral_window)
self._update_client_geometry()
self.corral_window.show_unraised()
trap.swallow_synced(X11Window.sendConfigureNotify, get_xwindow(self.client_window))
def maybe_recalculate_geometry_for(self, maybe_owner):
if maybe_owner and self.get_property("owner") is maybe_owner:
self._update_client_geometry()
def _sanitize_size_hints(self, size_hints):
if size_hints is None:
return
for attr in ["min_aspect", "max_aspect"]:
v = getattr(size_hints, attr)
if v is not None:
try:
f = float(v)
except:
f = None
if f is None or f>=MAX_ASPECT:
log.warn("clearing invalid aspect hint value for %s: %s", attr, v)
setattr(size_hints, attr, -1.0)
for attr in ["max_size", "min_size", "base_size", "resize_inc",
"min_aspect_ratio", "max_aspect_ratio"]:
v = getattr(size_hints, attr)
if v is not None:
try:
w,h = v
except:
w,h = None,None
if (w is None or h is None) or w>=MAX_WINDOW_SIZE or h>=MAX_WINDOW_SIZE:
log("clearing invalid size hint value for %s: %s", attr, v)
setattr(size_hints, attr, None)
#if max-size is smaller than min-size (bogus), clamp it..
mins = size_hints.min_size
maxs = size_hints.max_size
if mins is not None and maxs is not None:
minw,minh = mins
maxw,maxh = maxs
if maxw<minw or maxh<minh:
size_hints.max_size = None
log.warn("invalid max_size=%s for min_size=%s has now been cleared",
maxs, mins)
def _update_client_geometry(self):
owner = self.get_property("owner")
if owner is not None:
log("_update_client_geometry: owner()=%s", owner)
def window_size():
return owner.window_size(self)
def window_position(w, h):
return owner.window_position(self, w, h)
self._do_update_client_geometry(window_size, window_position)
elif not self._setup_done:
log("_update_client_geometry: using initial size=%s and position=%s",
self.get_property("requested-size"), self.get_property("requested-position"))
#try to honour initial size and position requests during setup:
def window_size():
return self.get_property("requested-size")
def window_position(w, h):
return self.get_property("requested-position")
self._do_update_client_geometry(window_size, window_position)