xpra icon
Bug tracker and wiki

Ticket #907: split-windowmodel-v2.patch

File split-windowmodel-v2.patch, 637.7 KB (added by Antoine Martin, 4 years ago)

much cleaner patch

  • xpra/x11/gtk2/composite.py

     
    1 # This file is part of Xpra.
    2 # Copyright (C) 2008, 2009 Nathaniel Smith <njs@pobox.com>
    3 # Copyright (C) 2012-2014 Antoine Martin <antoine@devloop.org.uk>
    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 
    7 import gobject
    8 from xpra.gtk_common.gobject_util import one_arg_signal, AutoPropGObjectMixin
    9 from xpra.x11.gtk2.gdk_bindings import (
    10             add_event_receiver,             #@UnresolvedImport
    11             remove_event_receiver,          #@UnresolvedImport
    12             get_parent)                     #@UnresolvedImport
    13 from xpra.gtk_common.error import trap
    14 
    15 from xpra.x11.gtk2.world_window import get_world_window
    16 from xpra.x11.bindings.ximage import XImageBindings #@UnresolvedImport
    17 XImage = XImageBindings()
    18 from xpra.x11.bindings.window_bindings import constants, X11WindowBindings #@UnresolvedImport
    19 X11Window = X11WindowBindings()
    20 X11Window.ensure_XComposite_support()
    21 X11Window.ensure_XDamage_support()
    22 
    23 from xpra.log import Logger
    24 log = Logger("x11", "window")
    25 
    26 
    27 StructureNotifyMask = constants["StructureNotifyMask"]
    28 
    29 
    30 class CompositeHelper(AutoPropGObjectMixin, gobject.GObject):
    31 
    32     XShmEnabled = True
    33 
    34     __gsignals__ = {
    35         "contents-changed": one_arg_signal,
    36 
    37         "xpra-damage-event": one_arg_signal,
    38         "xpra-unmap-event": one_arg_signal,
    39         "xpra-configure-event": one_arg_signal,
    40         "xpra-reparent-event": one_arg_signal,
    41         }
    42 
    43     __gproperties__ = {
    44         "contents-handle": (gobject.TYPE_PYOBJECT,
    45                             "", "", gobject.PARAM_READABLE),
    46         "shm-handle": (gobject.TYPE_PYOBJECT,
    47                             "", "", gobject.PARAM_READABLE),
    48         }
    49 
    50     # This may raise XError.
    51     def __init__(self, window, already_composited, use_shm=False):
    52         super(CompositeHelper, self).__init__()
    53         log("CompositeHelper.__init__(%#x, %s)", window.xid, already_composited)
    54         self._window = window
    55         self._already_composited = already_composited
    56         self._listening_to = None
    57         self._damage_handle = None
    58         self._use_shm = use_shm
    59         self._shm_handle = None
    60         self._contents_handle = None
    61 
    62     def __repr__(self):
    63         xid = None
    64         if self._window:
    65             xid = self._window.xid
    66         return "CompositeHelper(%#x)" % xid
    67 
    68     def setup(self):
    69         xwin = self._window.xid
    70         if not self._already_composited:
    71             X11Window.XCompositeRedirectWindow(xwin)
    72         _, _, _, _, self._border_width = X11Window.geometry_with_border(xwin)
    73         self.invalidate_pixmap()
    74         self._damage_handle = X11Window.XDamageCreate(xwin)
    75         log("CompositeHelper.setup() damage handle(%#x)=%#x", xwin, self._damage_handle)
    76         add_event_receiver(self._window, self)
    77 
    78     def destroy(self):
    79         if self._window is None:
    80             log.warn("composite window %s already destroyed!", self)
    81             return
    82         #clear the reference to the window early:
    83         win = self._window
    84         xwin = self._window.xid
    85         #Note: invalidate_pixmap()/_cleanup_listening() use self._window, but won't care if it's None
    86         self._window = None
    87         remove_event_receiver(win, self)
    88         self.invalidate_pixmap()
    89         if not self._already_composited:
    90             trap.swallow_synced(X11Window.XCompositeUnredirectWindow, xwin)
    91         if self._damage_handle:
    92             trap.swallow_synced(X11Window.XDamageDestroy, self._damage_handle)
    93             self._damage_handle = None
    94         if self._shm_handle:
    95             self._shm_handle.cleanup()
    96             self._shm_handle = None
    97         #note: this should be redundant since we cleared the
    98         #reference to self._window and shortcut out in do_get_property_contents_handle
    99         #but it's cheap anyway
    100         self.invalidate_pixmap()
    101 
    102     def acknowledge_changes(self):
    103         if self._shm_handle:
    104             self._shm_handle.discard()
    105         if self._damage_handle is not None and self._window is not None:
    106             #"Synchronously modifies the regions..." so unsynced?
    107             if not trap.swallow_synced(X11Window.XDamageSubtract, self._damage_handle):
    108                 self.invalidate_pixmap()
    109 
    110     def invalidate_pixmap(self):
    111         log("invalidating named pixmap")
    112         if self._listening_to is not None:
    113             self._cleanup_listening(self._listening_to)
    114             self._listening_to = None
    115         if self._contents_handle:
    116             self._contents_handle.cleanup()
    117             self._contents_handle = None
    118 
    119     def _cleanup_listening(self, listening):
    120         if listening:
    121             # Don't want to stop listening to self._window!:
    122             assert self._window is None or self._window not in listening
    123             for w in listening:
    124                 remove_event_receiver(w, self)
    125 
    126     def do_get_property_shm_handle(self, name):
    127         if not self._use_shm or not CompositeHelper.XShmEnabled:
    128             return None
    129         if self._shm_handle and self._shm_handle.get_size()!=self._window.get_size():
    130             #size has changed!
    131             #make sure the current wrapper gets garbage collected:
    132             self._shm_handle.cleanup()
    133             self._shm_handle = None
    134         if self._shm_handle is None:
    135             #make a new one:
    136             self._shm_handle = XImage.get_XShmWrapper(self._window.xid)
    137             if self._shm_handle is None:
    138                 #failed (may retry)
    139                 return None
    140             init_ok, retry_window, xshm_failed = self._shm_handle.setup()
    141             if not init_ok:
    142                 #this handle is not valid, clear it:
    143                 self._shm_handle = None
    144             if not retry_window:
    145                 #and it looks like it is not worth re-trying this window:
    146                 self._use_shm = False
    147             if xshm_failed:
    148                 log.warn("disabling XShm support following irrecoverable error")
    149                 CompositeHelper.XShmEnabled = False
    150         return self._shm_handle
    151 
    152     def do_get_property_contents_handle(self, name):
    153         if self._window is None:
    154             #shortcut out
    155             return  None
    156         if self._contents_handle is None:
    157             log("refreshing named pixmap")
    158             assert self._listening_to is None
    159             def set_pixmap():
    160                 # The tricky part here is that the pixmap returned by
    161                 # NameWindowPixmap gets invalidated every time the window's
    162                 # viewable state changes.  ("viewable" here is the X term that
    163                 # means "mapped, and all ancestors are also mapped".)  But
    164                 # there is no X event that will tell you when a window's
    165                 # viewability changes!  Instead we have to find all ancestors,
    166                 # and watch all of them for unmap and reparent events.  But
    167                 # what about races?  I hear you cry.  By doing things in the
    168                 # exact order:
    169                 #   1) select for StructureNotify
    170                 #   2) QueryTree to get parent
    171                 #   3) repeat 1 & 2 up to the root
    172                 #   4) call NameWindowPixmap
    173                 # we are safe.  (I think.)
    174                 listening = []
    175                 e = None
    176                 try:
    177                     root = self._window.get_screen().get_root_window()
    178                     world = get_world_window().window
    179                     win = get_parent(self._window)
    180                     while win not in (None, root, world) and win.get_parent() is not None:
    181                         # We have to use a lowlevel function to manipulate the
    182                         # event selection here, because SubstructureRedirectMask
    183                         # does not roundtrip through the GDK event mask
    184                         # functions.  So if we used them, here, we would clobber
    185                         # corral window selection masks, and those don't deserve
    186                         # clobbering.  They are our friends!  X is driving me
    187                         # slowly mad.
    188                         X11Window.addXSelectInput(win.xid, StructureNotifyMask)
    189                         add_event_receiver(win, self, max_receivers=-1)
    190                         listening.append(win)
    191                         win = get_parent(win)
    192                     handle = XImage.get_xcomposite_pixmap(self._window.xid)
    193                 except Exception as e:
    194                     try:
    195                         self._cleanup_listening(listening)
    196                     except:
    197                         pass
    198                     raise
    199                 if handle is None:
    200                     #avoid race during signal exit, which will clear self._window:
    201                     win = self._window
    202                     xid = 0
    203                     if win:
    204                         xid = win.xid
    205                     log("failed to name a window pixmap for %#x: %s", xid, e)
    206                     self._cleanup_listening(listening)
    207                 else:
    208                     self._contents_handle = handle
    209                     # Don't save the listening set until after
    210                     # NameWindowPixmap has succeeded, to maintain our
    211                     # invariant:
    212                     self._listening_to = listening
    213             trap.swallow_synced(set_pixmap)
    214         return self._contents_handle
    215 
    216     def do_xpra_unmap_event(self, event):
    217         self.invalidate_pixmap()
    218 
    219     def do_xpra_configure_event(self, event):
    220         self._border_width = event.border_width
    221         self.invalidate_pixmap()
    222 
    223     def do_xpra_reparent_event(self, event):
    224         self.invalidate_pixmap()
    225 
    226     def do_xpra_damage_event(self, event):
    227         event.x += self._border_width
    228         event.y += self._border_width
    229         self.emit("contents-changed", event)
    230 
    231 gobject.type_register(CompositeHelper)
  • xpra/x11/gtk2/models/__init__.py

     
     1# This file is part of Xpra.
     2# Copyright (C) 2015 Antoine Martin <antoine@devloop.org.uk>
     3# Xpra is released under the terms of the GNU GPL v2, or, at your option, any
     4# later version. See the file COPYING for details.
     5
     6class Unmanageable(Exception):
     7    pass
     8
     9#if you want to use a virtual screen bigger than 32767x32767
     10#you will need to change those values, but some broken toolkits
     11#will then misbehave (they use signed shorts instead of signed ints..)
     12MAX_WINDOW_SIZE = 2**15-1
  • xpra/x11/gtk2/models/base.py

     
    11# This file is part of Xpra.
    22# Copyright (C) 2008, 2009 Nathaniel Smith <njs@pobox.com>
    3 # Copyright (C) 2011-2014 Antoine Martin <antoine@devloop.org.uk>
     3# Copyright (C) 2011-2015 Antoine Martin <antoine@devloop.org.uk>
    44# Xpra is released under the terms of the GNU GPL v2, or, at your option, any
    55# later version. See the file COPYING for details.
    66
    7 """The magic GTK widget that represents a client window.
    87
    9 Most of the gunk required to be a valid window manager (reparenting, synthetic
    10 events, mucking about with properties, etc. etc.) is wrapped up in here."""
    11 
    12 # Maintain compatibility with old versions of Python, while avoiding a
    13 # deprecation warning on new versions:
    14 import os
    15 from socket import gethostname
    16 
    17 import gobject
    18 import glib
    19 import gtk.gdk
    20 import cairo
    21 import signal
    22 
    23 from xpra.util import nonl, WORKSPACE_UNSET, WORKSPACE_ALL
    24 from xpra.x11.bindings.window_bindings import constants, X11WindowBindings, SHAPE_KIND #@UnresolvedImport
    25 X11Window = X11WindowBindings()
    26 
    27 from xpra.x11.gtk_x11.send_wm import (
    28                 send_wm_take_focus,
    29                 send_wm_delete_window)
    30 from xpra.gtk_common.gobject_util import (AutoPropGObjectMixin,
    31                            one_arg_signal,
    32                            non_none_list_accumulator)
     8from xpra.util import WORKSPACE_UNSET, WORKSPACE_ALL
     9from xpra.gtk_common.gobject_util import one_arg_signal
     10from xpra.x11.gtk_x11.prop import prop_set
     11from xpra.x11.gtk2.models.core import CoreX11WindowModel, gobject, xsync, xswallow, gdk
     12from xpra.x11.bindings.window_bindings import X11WindowBindings, constants, SHAPE_KIND #@UnresolvedImport
    3313from xpra.x11.gtk2.gdk_bindings import (
    34                 get_pyatom,                                 #@UnresolvedImport
    3514                get_pywindow,                               #@UnresolvedImport
    36                 add_event_receiver,                         #@UnresolvedImport
    37                 remove_event_receiver,                      #@UnresolvedImport
    38                 get_display_for,                            #@UnresolvedImport
    39                 calc_constrained_size,                      #@UnresolvedImport
    40                )
    41 from xpra.gtk_common.error import XError, xsync, xswallow
    42 from xpra.x11.gtk_x11.prop import prop_get, prop_set, MotifWMHints
    43 from xpra.x11.gtk2.composite import CompositeHelper
     15                )
    4416
    4517from xpra.log import Logger
    4618log = Logger("x11", "window")
    47 focuslog = Logger("x11", "window", "focus")
    48 grablog = Logger("x11", "window", "grab")
    49 iconlog = Logger("x11", "window", "icon")
    5019workspacelog = Logger("x11", "window", "workspace")
    5120shapelog = Logger("x11", "window", "shape")
     21grablog = Logger("x11", "window", "grab")
     22metalog = Logger("x11", "window", "metadata")
    5223
    5324
    54 _NET_WM_STATE_REMOVE = 0
    55 _NET_WM_STATE_ADD    = 1
    56 _NET_WM_STATE_TOGGLE = 2
    57 STATE_STRING = {
    58             _NET_WM_STATE_REMOVE    : "REMOVE",
    59             _NET_WM_STATE_ADD       : "ADD",
    60             _NET_WM_STATE_TOGGLE    : "TOGGLE",
    61                 }
     25X11Window = X11WindowBindings()
    6226
    63 XNone = constants["XNone"]
    64 
    65 CWX             = constants["CWX"]
    66 CWY             = constants["CWY"]
    67 CWWidth         = constants["CWWidth"]
    68 CWHeight        = constants["CWHeight"]
    69 CWBorderWidth   = constants["CWBorderWidth"]
    70 CWSibling       = constants["CWSibling"]
    71 CWStackMode     = constants["CWStackMode"]
    72 CONFIGURE_GEOMETRY_MASK = CWX | CWY | CWWidth | CWHeight
    73 CW_MASK_TO_NAME = {
    74                    CWX              : "X",
    75                    CWY              : "Y",
    76                    CWWidth          : "Width",
    77                    CWHeight         : "Height",
    78                    CWBorderWidth    : "BorderWidth",
    79                    CWSibling        : "Sibling",
    80                    CWStackMode      : "StackMode",
    81                    CWBorderWidth    : "BorderWidth",
    82                    }
    83 def configure_bits(value_mask):
    84     return "|".join((v for k,v in CW_MASK_TO_NAME.items() if (k&value_mask)))
    85 
    86 
    87 IconicState = constants["IconicState"]
    88 NormalState = constants["NormalState"]
    89 
    9027# grab stuff:
    9128NotifyNormal        = constants["NotifyNormal"]
    9229NotifyGrab          = constants["NotifyGrab"]
     
    10643    DETAIL_CONSTANTS[constants[x]] = x
    10744grablog("pointer grab constants: %s", GRAB_CONSTANTS)
    10845grablog("detail constants: %s", DETAIL_CONSTANTS)
    109 
    110 
    111 #if you want to use a virtual screen bigger than 32767x32767
    112 #you will need to change those values, but some broken toolkits
    113 #will then misbehave (they use signed shorts instead of signed ints..)
    114 MAX_WINDOW_SIZE = 2**15-1
    115 MAX_ASPECT = 2**15-1
    116 USE_XSHM = os.environ.get("XPRA_XSHM", "1")=="1"
    117 
    118 #these properties are not handled, and we don't want to spam the log file
    119 #whenever an app decides to change them:
    120 PROPERTIES_IGNORED = ("_NET_WM_OPAQUE_REGION", )
    121 #make it easier to debug property changes, just add them here:
    122 PROPERTIES_DEBUG = {}   #ie: {"WM_PROTOCOLS" : ["atom"]}
    123 
    124 
    12546#add user friendly workspace logging:
    12647WORKSPACE_STR = {WORKSPACE_UNSET    : "UNSET",
    12748                 WORKSPACE_ALL      : "ALL"}
     
    12950    return WORKSPACE_STR.get(w, w)
    13051
    13152
    132 def sanestr(s):
    133     return (s or "").strip("\0").replace("\0", " ")
    134 
    135 
    136 # Todo:
    137 #   client focus hints
    138 #   _NET_WM_SYNC_REQUEST
    139 #   root window requests (pagers, etc. requesting to change client states)
    140 #   _NET_WM_PING/detect window not responding (also a root window message)
    141 
    142 # Okay, we need a block comment to explain the window arrangement that this
    143 # file is working with.
    144 #
    145 #                +--------+
    146 #                | widget |
    147 #                +--------+
    148 #                  /    \
    149 #  <- top         /     -\-        bottom ->
    150 #                /        \
    151 #          +-------+       |
    152 #          | image |  +---------+
    153 #          +-------+  | corral  |
    154 #                     +---------+
    155 #                          |
    156 #                     +---------+
    157 #                     | client  |
    158 #                     +---------+
    159 #
    160 # Each box in this diagram represents one X/GDK window.  In the common case,
    161 # every window here takes up exactly the same space on the screen (!).  In
    162 # fact, the two windows on the right *always* have exactly the same size and
    163 # location, and the window on the left and the top window also always have
    164 # exactly the same size and position.  However, each window in the diagram
    165 # plays a subtly different role.
    166 #
    167 # The client window is obvious -- this is the window owned by the client,
    168 # which they created and which we have various ICCCM/EWMH-mandated
    169 # responsibilities towards.  It is also composited.
    170 #
    171 # The purpose of the 'corral' is to keep the client window managed -- we
    172 # select for SubstructureRedirect on it, so that the client cannot resize
    173 # etc. without going through the WM.
    174 #
    175 # These two windows are always managed together, as a unit; an invariant of
    176 # the code is that they always take up exactly the same space on the screen.
    177 # They get reparented back and forth between widgets, and when there are no
    178 # widgets, they get reparented to a "parking area".  For now, we're just using
    179 # the root window as a parking area, so we also map/unmap the corral window
    180 # depending on whether we are parked or not; the corral and window is left
    181 # mapped at all times.
    182 #
    183 # When a particular WindowView controls the underlying client window, then two
    184 # things happen:
    185 #   -- Its size determines the size of the client window.  Ideally they are
    186 #      the same size -- but this is not always the case, because the client
    187 #      may have specified sizing constraints, in which case the client window
    188 #      is the "best fit" to the controlling widget window.
    189 #   -- The client window and its corral are reparented under the widget
    190 #      window, as in the diagram above.  This is necessary to allow mouse
    191 #      events to work -- a WindowView widget can always *look* like the client
    192 #      window is there, through the magic of Composite, but in order for it to
    193 #      *act* like the client window is there in terms of receiving mouse
    194 #      events, it has to actually be there.
    195 #
    196 # We should also have a block comment describing how to create a
    197 # view/"controller" for a WindowModel.
    198 #
    199 # Viewing a (Base)WindowModel is easy.  Connect to the client-contents-changed
    200 # signal.  Every time the window contents is updated, you'll get a message.
    201 # This message is passed a single object e, which has useful members:
    202 #   e.x, e.y, e.width, e.height:
    203 #      The part of the client window that was modified, and needs to be
    204 #      redrawn.
    205 # To get the actual contents of the window to draw, there is a "handle"
    206 # available as the "contents-handle" property on the Composite window.
    207 #
    208 # But what if you'd like to do more than just look at your pretty composited
    209 # windows?  Maybe you'd like to, say, *interact* with them?  Then life is a
    210 # little more complicated.  To make a view "live", we have to move the actual
    211 # client window to be a child of your view window and position it correctly.
    212 # Obviously, only one view can be live at any given time, so we have to figure
    213 # out which one that is.  Supposing we have a WindowModel called "model" and
    214 # a view called "view", then the following pieces come into play:
    215 #   The "ownership-election" signal on window:
    216 #     If a view wants the chance to become live, it must connect to this
    217 #     signal.  When the signal is emitted, its handler should return a tuple
    218 #     of the form:
    219 #       (votes, my_view)
    220 #     Just like a real election, everyone votes for themselves.  The view that
    221 #     gives the highest value to 'votes' becomes the new owner.  However, a
    222 #     view with a negative (< 0) votes value will never become the owner.
    223 #   model.ownership_election():
    224 #     This method (distinct from the ownership-election signal!) triggers an
    225 #     election.  All views MUST call this method whenever they decide their
    226 #     number of votes has changed.  All views MUST call this method when they
    227 #     are destructing themselves (ideally after disconnecting from the
    228 #     ownership-election signal).
    229 #   The "owner" property on window:
    230 #     This records the view that currently owns the window (i.e., the winner
    231 #     of the last election), or None if no view is live.
    232 #   view.take_window(model, window):
    233 #     This method is called on 'view' when it becomes owner of 'model'.  It
    234 #     should reparent 'window' into the appropriate place, and put it at the
    235 #     appropriate place in its window stack.  (The x,y position, however, does
    236 #     not matter.)
    237 #   view.window_size(model):
    238 #     This method is called when the model needs to know how much space it is
    239 #     allocated.  It should return the maximum (width, height) allowed.
    240 #     (However, the model may choose to use less than this.)
    241 #   view.window_position(mode, width, height):
    242 #     This method is called when the model needs to know where it should be
    243 #     located (relative to the parent window the view placed it in).  'width'
    244 #     and 'height' are the size the model window will actually be.  It should
    245 #     return the (x, y) position desired.
    246 #   model.maybe_recalculate_geometry_for(view):
    247 #     This method (potentially) triggers a resize/move of the client window
    248 #     within the view.  If 'view' is not the current owner, is a no-op, which
    249 #     means that views can call it without worrying about whether they are in
    250 #     fact the current owner.
    251 #
    252 # The actual method for choosing 'votes' is not really determined yet.
    253 # Probably it should take into account at least the following factors:
    254 #   -- has focus (or has mouse-over?)
    255 #   -- is visible in a tray/other window, and the tray/other window is visible
    256 #      -- and is focusable
    257 #      -- and is not focusable
    258 #   -- is visible in a tray, and the tray/other window is not visible
    259 #      -- and is focusable
    260 #      -- and is not focusable
    261 #      (NB: Widget.get_ancestor(my.Tray) will give us the nearest ancestor
    262 #      that isinstance(my.Tray), if any.)
    263 #   -- is not visible
    264 #   -- the size of the widget (as a final tie-breaker)
    265 
    266 class Unmanageable(Exception):
    267     pass
    268 
    269 class BaseWindowModel(AutoPropGObjectMixin, gobject.GObject):
    270     __gproperties__ = {
    271         "client-window": (gobject.TYPE_PYOBJECT,
    272                           "gtk.gdk.Window representing the client toplevel", "",
    273                           gobject.PARAM_READABLE),
    274         "geometry": (gobject.TYPE_PYOBJECT,
    275                      "current (border-corrected, relative to parent) coordinates (x, y, w, h) for the window", "",
    276                      gobject.PARAM_READABLE),
     53class BaseWindowModel(CoreX11WindowModel):
     54    """
     55        Extends CoreX11WindowModel to add properties
     56        that we find on real X11 windows
     57        (as opposed to trays).
     58        Also wraps access to WM_STATE using simpler gobject booleans.
     59    """
     60    __common_properties__ = CoreX11WindowModel.__common_properties__.copy()
     61    __common_properties__.update({
    27762        "transient-for": (gobject.TYPE_PYOBJECT,
    27863                          "Transient for (or None)", "",
    27964                          gobject.PARAM_READABLE),
    280         "pid": (gobject.TYPE_INT,
    281                 "PID of owning process", "",
    282                 -1, 65535, -1,
    283                 gobject.PARAM_READABLE),
    28465        "opacity": (gobject.TYPE_INT64,
    28566                "Opacity", "",
    28667                -1, 0xffffffff, -1,
     
    28869        "shape": (gobject.TYPE_PYOBJECT,
    28970                          "Window XShape data", "",
    29071                          gobject.PARAM_READABLE),
    291         "xid": (gobject.TYPE_INT,
    292                 "X11 window id", "",
    293                 -1, 65535, -1,
    294                 gobject.PARAM_READABLE),
    295         "title": (gobject.TYPE_PYOBJECT,
    296                   "Window title (unicode or None)", "",
    297                   gobject.PARAM_READABLE),
    29872        "group-leader": (gobject.TYPE_PYOBJECT,
    29973                         "Window group leader as a pair: (xid, gdk window)", "",
    30074                         gobject.PARAM_READABLE),
     
    30680                      "Does this window ever accept keyboard input?", "",
    30781                      True,
    30882                      gobject.PARAM_READWRITE),
    309         "has-alpha": (gobject.TYPE_BOOLEAN,
    310                        "Does the window use transparency", "",
    311                        False,
    312                        gobject.PARAM_READABLE),
    31383        "bypass-compositor": (gobject.TYPE_INT,
    31484                       "hint that the window would benefit from running uncomposited ", "",
    31585                       0, 2, 0,
     
    31787        "fullscreen-monitors": (gobject.TYPE_PYOBJECT,
    31888                         "List of 4 monitor indices indicating the top, bottom, left, and right edges of the window when the fullscreen state is enabled", "",
    31989                         gobject.PARAM_READABLE),
     90        "strut": (gobject.TYPE_PYOBJECT,
     91                  "Struts requested by window, or None", "",
     92                  gobject.PARAM_READABLE),
     93        "workspace": (gobject.TYPE_UINT,
     94                "The workspace this window is on", "",
     95                0, 2**32-1, WORKSPACE_UNSET,
     96                gobject.PARAM_READWRITE),
     97        "override-redirect": (gobject.TYPE_BOOLEAN,
     98                       "Is the window of type override-redirect", "",
     99                       False,
     100                       gobject.PARAM_READABLE),
     101        "modal": (gobject.TYPE_PYOBJECT,
     102                          "Modal (boolean)", "",
     103                          gobject.PARAM_READWRITE),
     104        "window-type": (gobject.TYPE_PYOBJECT,
     105                        "Window type",
     106                        "NB, most preferred comes first, then fallbacks",
     107                        gobject.PARAM_READABLE),
     108        "state": (gobject.TYPE_PYOBJECT,
     109                  "State, as per _NET_WM_STATE", "",
     110                  gobject.PARAM_READABLE),
     111        #all the attributes below are virtual attributes from WM_STATE:
    320112        "fullscreen": (gobject.TYPE_BOOLEAN,
    321113                       "Fullscreen-ness of window", "",
    322114                       False,
     
    353145                       "Is the window's position fixed on the screen", "",
    354146                       False,
    355147                       gobject.PARAM_READWRITE),
    356         "strut": (gobject.TYPE_PYOBJECT,
    357                   "Struts requested by window, or None", "",
    358                   gobject.PARAM_READABLE),
    359         "workspace": (gobject.TYPE_UINT,
    360                 "The workspace this window is on", "",
    361                 0, 2**32-1, WORKSPACE_UNSET,
    362                 gobject.PARAM_READWRITE),
    363         "override-redirect": (gobject.TYPE_BOOLEAN,
    364                        "Is the window of type override-redirect", "",
    365                        False,
    366                        gobject.PARAM_READABLE),
    367         "tray": (gobject.TYPE_BOOLEAN,
    368                        "Is the window a system tray icon", "",
    369                        False,
    370                        gobject.PARAM_READABLE),
    371         "role" : (gobject.TYPE_PYOBJECT,
    372                           "The window's role (ICCCM session management)", "",
    373                           gobject.PARAM_READABLE),
    374         "modal": (gobject.TYPE_PYOBJECT,
    375                           "Modal (boolean)", "",
    376                           gobject.PARAM_READWRITE),
    377         "window-type": (gobject.TYPE_PYOBJECT,
    378                         "Window type",
    379                         "NB, most preferred comes first, then fallbacks",
    380                         gobject.PARAM_READABLE),
    381         }
    382     __gsignals__ = {
    383         "client-contents-changed": one_arg_signal,
    384         "raised"                : one_arg_signal,
    385         "unmanaged"             : one_arg_signal,
    386         "initiate-moveresize"   : one_arg_signal,
    387 
    388         "grab"                  : one_arg_signal,
    389         "ungrab"                : one_arg_signal,
    390         }
    391     __common_signals__ = {
    392         "bell"                      : one_arg_signal,   #out
     148        })
     149    __common_signals__ = CoreX11WindowModel.__common_signals__.copy()
     150    __common_signals__.update({
     151        #signals we emit:
     152        "raised"                    : one_arg_signal,
     153        "initiate-moveresize"       : one_arg_signal,
     154        "grab"                      : one_arg_signal,
     155        "ungrab"                    : one_arg_signal,
     156        "bell"                      : one_arg_signal,
     157        #x11 events we catch (and often re-emit):
    393158        "xpra-xkb-event"            : one_arg_signal,
    394159        "xpra-shape-event"          : one_arg_signal,
    395160        "xpra-configure-event"      : one_arg_signal,
    396161        "xpra-unmap-event"          : one_arg_signal,
    397162        "xpra-client-message-event" : one_arg_signal,
    398         "xpra-property-notify-event": one_arg_signal,
    399163        "xpra-focus-in-event"       : one_arg_signal,
    400164        "xpra-focus-out-event"      : one_arg_signal,
    401         }
     165        })
    402166
     167    _property_names = CoreX11WindowModel._property_names + [
     168        "transient-for", "fullscreen-monitors", "bypass-compositor", "group-leader", "window-type", "workspace", "strut", "shape",
     169        "fullscreen", "focused", "maximized", "above", "below", "shaded", "skip-taskbar", "skip-pager", "sticky"
     170        ]
     171    _dynamic_property_names = CoreX11WindowModel._dynamic_property_names + [
     172            "attention-requested", "fullscreen", #etc..
     173            ]
     174    _initial_x11_properties = CoreX11WindowModel._initial_x11_properties + [
     175        "WM_TRANSIENT_FOR",
     176        "_NET_WM_WINDOW_TYPE",
     177        "_NET_WM_DESKTOP",
     178        "_NET_WM_FULLSCREEN_MONITORS",
     179        "_NET_WM_BYPASS_COMPOSITOR",
     180        "_NET_WM_STRUT",
     181        "_NET_WM_STRUT_PARTIAL",
     182        "_NET_WM_WINDOW_OPACITY",
     183        "WM_HINTS",
     184        ]
     185
    403186    def __init__(self, client_window):
    404         log("new window %#x", client_window.xid)
    405         super(BaseWindowModel, self).__init__()
    406         self.client_window = client_window
    407         self.client_window_saved_events = self.client_window.get_events()
    408         self._managed = False
    409         self._managed_handlers = []
    410         self._setup_done = False
     187        super(BaseWindowModel, self).__init__(client_window)
    411188        self._input_field = True            # The WM_HINTS input field
    412         self._geometry = None
    413         self._damage_forward_handle = None
    414         self._internal_set_property("client-window", client_window)
    415         use_xshm = USE_XSHM and not self.is_tray()
    416         self._composite = CompositeHelper(self.client_window, False, use_xshm)
    417189        if X11Window.displayHasXShape():
    418190            X11Window.XShapeSelectInput(self.client_window.xid)
    419         self.property_names = ["pid", "transient-for", "fullscreen", "fullscreen-monitors", "bypass-compositor", "maximized", "window-type", "role", "group-leader",
    420                                "xid", "workspace", "has-alpha", "opacity", "strut", "shape"]
    421191
    422     def get_property_names(self):
    423         return self.property_names
    424192
    425     def get_dynamic_property_names(self):
    426         return ("title", "size-hints", "fullscreen", "fullscreen-monitors", "bypass-compositor", "maximized", "opacity", "workspace", "strut", "shape")
     193    def _read_initial_X11_properties(self):
     194        CoreX11WindowModel._read_initial_X11_properties(self)
     195        self._internal_set_property("shape", self._read_xshape())
     196        self._internal_set_property("state", self._read_wm_state())
    427197
     198    def _guess_window_type(self):
     199        #query the property in case it isn't set yet
     200        transient_for = self.prop_get("WM_TRANSIENT_FOR", "window")
     201        if transient_for is not None:
     202            # EWMH says that even if it's transient-for, we MUST check to
     203            # see if it's override-redirect (and if so treat as NORMAL).
     204            # But we wouldn't be here if this was override-redirect.
     205            # (OverrideRedirectWindowModel overrides this method)
     206            return "_NET_WM_WINDOW_TYPE_DIALOG"
     207        return "_NET_WM_WINDOW_TYPE_NORMAL"
    428208
    429     def managed_connect(self, detailed_signal, handler, *args):
    430         """ connects a signal handler and makes sure we will clean it up on unmanage() """
    431         handler_id = self.connect(detailed_signal, handler, *args)
    432         self._managed_handlers.append(handler_id)
    433         return handler_id
    434209
    435     def managed_disconnect(self):
    436         for handler_id in self._managed_handlers:
    437             self.disconnect(handler_id)
    438         self._managed_handlers = []
     210    ################################
     211    # Actions
     212    ################################
    439213
    440     def call_setup(self):
    441         try:
    442             with xsync:
    443                 self._geometry = X11Window.geometry_with_border(self.client_window.xid)
    444                 self._read_initial_X11_properties()
    445         except XError as e:
    446             raise Unmanageable(e)
    447         add_event_receiver(self.client_window, self)
    448         # Keith Packard says that composite state is undefined following a
    449         # reparent, so I'm not sure doing this here in the superclass,
    450         # before we reparent, actually works... let's wait and see.
    451         try:
    452             with xsync:
    453                 self._composite.setup()
    454         except XError as e:
    455             remove_event_receiver(self.client_window, self)
    456             log("window %#x does not support compositing: %s", self.client_window.xid, e)
    457             with xswallow:
    458                 self._composite.destroy()
    459             self._composite = None
    460             raise Unmanageable(e)
    461         #compositing is now enabled, from now on we need to call setup_failed to clean things up
    462         self._managed = True
    463         try:
    464             with xsync:
    465                 self.setup()
    466         except XError as e:
    467             try:
    468                 with xsync:
    469                     self.setup_failed(e)
    470             except Exception as ex:
    471                 log.error("error in cleanup handler: %s", ex)
    472             raise Unmanageable(e)
    473         self._setup_done = True
     214    def move_to_workspace(self, workspace):
     215        #we send a message to ourselves, we could also just update the property
     216        current = self.get_property("workspace")
     217        if current==workspace:
     218            workspacelog("move_to_workspace(%s) unchanged", workspacestr(workspace))
     219            return
     220        workspacelog("move_to_workspace(%s) current=%s", workspacestr(workspace), workspacestr(current))
     221        with xswallow:
     222            if workspace==WORKSPACE_UNSET:
     223                workspacelog("removing _NET_WM_DESKTOP property from window %#x", self.client_window.xid)
     224                X11Window.XDeleteProperty(self.client_window.xid, "_NET_WM_DESKTOP")
     225            else:
     226                workspacelog("setting _NET_WM_DESKTOP=%s on window %#x", workspacestr(workspace), self.client_window.xid)
     227                prop_set(self.client_window, "_NET_WM_DESKTOP", "u32", workspace)
    474228
    475     def setup_failed(self, e):
    476         log("cannot manage %#x: %s", self.client_window.xid, e)
    477         self.do_unmanaged(False)
    478229
    479     def setup(self):
    480         # Start listening for important events.
    481         self.client_window.set_events(self.client_window_saved_events
    482                                       | gtk.gdk.STRUCTURE_MASK
    483                                       | gtk.gdk.PROPERTY_CHANGE_MASK
    484                                       | gtk.gdk.FOCUS_CHANGE_MASK)
    485         h = self._composite.connect("contents-changed", self._forward_contents_changed)
    486         self._damage_forward_handle = h
     230    def set_active(self):
     231        root = self.client_window.get_screen().get_root_window()
     232        prop_set(root, "_NET_ACTIVE_WINDOW", "u32", self.client_window.xid)
    487233
    488     def prop_get(self, key, ptype, ignore_errors=None, raise_xerrors=False):
    489         # Utility wrapper for prop_get on the client_window
    490         # also allows us to ignore property errors during setup_client
    491         if ignore_errors is None and (not self._setup_done or not self._managed):
    492             ignore_errors = True
    493         return prop_get(self.client_window, key, ptype, ignore_errors=bool(ignore_errors), raise_xerrors=raise_xerrors)
    494 
    495     def is_managed(self):
    496         return self._managed
    497 
    498     def is_shadow(self):
    499         return False
    500 
    501     def get_default_window_icon(self):
    502         return None
    503 
    504 
    505     def _forward_contents_changed(self, obj, event):
    506         if self._managed:
    507             self.emit("client-contents-changed", event)
    508 
    509     def acknowledge_changes(self):
    510         assert self._composite, "composite window destroyed outside the UI thread?"
    511         self._composite.acknowledge_changes()
    512 
    513234    ################################
    514235    # Property reading
    515236    ################################
    516237
    517     def do_xpra_property_notify_event(self, event):
    518         assert event.window is self.client_window
    519         self._handle_property_change(str(event.atom))
    520 
    521     _property_handlers = {}
    522 
    523     def _handle_property_change(self, name):
    524         log("Property changed on %#x: %s", self.client_window.xid, name)
    525         if name in PROPERTIES_DEBUG:
    526             log.info("%s=%s", name, self.prop_get(name, PROPERTIES_DEBUG[name], True, False))
    527         if name in PROPERTIES_IGNORED:
    528             return
    529         self._call_property_handler(name)
    530 
    531     def _call_property_handler(self, name):
    532         if name in self._property_handlers:
    533             self._property_handlers[name](self)
    534 
    535     def do_xpra_configure_event(self, event):
    536         if self.client_window is None or not self._managed:
    537             return
    538         #shouldn't the border width always be 0?
    539         geom = (event.x, event.y, event.width, event.height, event.border_width)
    540         log("BaseWindowModel.do_xpra_configure_event(%s) client_window=%#x, old geometry=%s, new geometry=%s", event, self.client_window.xid, self._geometry, geom)
    541         if geom!=self._geometry:
    542             self._geometry = geom
    543             #X11Window.MoveResizeWindow(self.client_window.xid, )
    544             self.notify("geometry")
    545 
    546238    def do_get_property_geometry(self, pspec):
    547239        if self._geometry is None:
    548240            with xsync:
     
    552244        x, y, w, h, b = self._geometry
    553245        return (x, y, w + 2*b, h + 2*b)
    554246
     247    def do_get_property_can_focus(self, name):
     248        assert name == "can-focus"
     249        return bool(self._input_field) or "WM_TAKE_FOCUS" in self.get_property("protocols")
     250
    555251    def get_position(self):
    556252        return self.do_get_property_geometry(None)[:2]
    557253
    558     def unmanage(self, exiting=False):
    559         if self._managed:
    560             self.emit("unmanaged", exiting)
    561254
    562     def do_unmanaged(self, wm_exiting):
    563         if not self._managed:
    564             return
    565         self._managed = False
    566         log("do_unmanaged(%s) damage_forward_handle=%s, composite=%s", wm_exiting, self._damage_forward_handle, self._composite)
    567         remove_event_receiver(self.client_window, self)
    568         glib.idle_add(self.managed_disconnect)
    569         if self._composite:
    570             if self._damage_forward_handle:
    571                 self._composite.disconnect(self._damage_forward_handle)
    572                 self._damage_forward_handle = None
    573             self._composite.destroy()
    574             self._composite = None
     255    #########################################
     256    # X11 properties
     257    #########################################
    575258
    576     def _read_initial_properties(self):
     259    def _handle_transient_for_change(self):
    577260        transient_for = self.prop_get("WM_TRANSIENT_FOR", "window")
     261        metalog("WM_TRANSIENT_FOR=%s", transient_for)
    578262        # May be None
    579263        self._internal_set_property("transient-for", transient_for)
    580264
     265    def _handle_window_type_change(self):
    581266        window_types = self.prop_get("_NET_WM_WINDOW_TYPE", ["atom"])
     267        metalog("_NET_WM_WINDOW_TYPE=%s", window_types)
    582268        if not window_types:
    583             window_type = self._guess_window_type(transient_for)
    584             window_types = [gtk.gdk.atom_intern(window_type)]
     269            window_type = self._guess_window_type()
     270            metalog("guessed window type=%s", window_type)
     271            window_types = [gdk.atom_intern(window_type)]
    585272        #normalize them (hide _NET_WM_WINDOW_TYPE prefix):
    586273        window_types = [str(wt).replace("_NET_WM_WINDOW_TYPE_", "").replace("_NET_WM_TYPE_", "") for wt in window_types]
    587274        self._internal_set_property("window-type", window_types)
    588         self._internal_set_property("xid", self.client_window.xid)
    589         self._internal_set_property("pid", self.prop_get("_NET_WM_PID", "u32") or -1)
    590         self._internal_set_property("role", self.prop_get("WM_WINDOW_ROLE", "latin1"))
    591         for mutable in ["WM_NAME", "_NET_WM_NAME", "_NET_WM_WINDOW_OPACITY", "_NET_WM_DESKTOP",
    592                         "_NET_WM_BYPASS_COMPOSITOR", "_NET_WM_FULLSCREEN_MONITORS",
    593                         "_NET_WM_STRUT", "_NET_WM_STRUT_PARTIAL"]:
    594             self._call_property_handler(mutable)
    595275
    596     def _read_initial_X11_properties(self):
    597         self._internal_set_property("has-alpha", X11Window.get_depth(self.client_window.xid)==32)
    598         self._internal_set_property("shape", self._read_xshape())
    599 
    600     def _read_xshape(self):
    601         xid = self.client_window.xid
    602         extents = X11Window.XShapeQueryExtents(xid)
    603         if not extents:
    604             shapelog("read_shape for window %#x: no extents", xid)
    605             return {}
    606         v = {}
    607         #w,h = X11Window.getGeometry(xid)[2:4]
    608         bextents = extents[0]
    609         cextents = extents[1]
    610         if bextents[0]==0 and cextents[0]==0:
    611             shapelog("read_shape for window %#x: none enabled", xid)
    612             return {}
    613         v["Bounding.extents"] = bextents
    614         v["Clip.extents"] = cextents
    615         for kind in SHAPE_KIND.keys():
    616             kind_name = SHAPE_KIND[kind]
    617             rectangles = X11Window.XShapeGetRectangles(xid, kind)
    618             v[kind_name+".rectangles"] = rectangles
    619         shapelog("_read_shape()=%s", v)
    620         return v
    621 
    622 
    623276    def _handle_workspace_change(self):
    624277        workspace = self.prop_get("_NET_WM_DESKTOP", "u32", True)
    625278        if workspace is None:
     
    626279            workspace = WORKSPACE_UNSET
    627280        workspacelog("_NET_WM_DESKTOP=%s for window %#x", workspacestr(workspace), self.client_window.xid)
    628281        self._internal_set_property("workspace", workspace)
    629     _property_handlers["_NET_WM_DESKTOP"] = _handle_workspace_change
    630282
    631     def move_to_workspace(self, workspace):
    632         #we send a message to ourselves, we could also just update the property
    633         current = self.get_property("workspace")
    634         if current==workspace:
    635             workspacelog("move_to_workspace(%s) unchanged", workspacestr(workspace))
    636             return
    637         workspacelog("move_to_workspace(%s) current=%s", workspacestr(workspace), workspacestr(current))
    638         with xswallow:
    639             if workspace==WORKSPACE_UNSET:
    640                 workspacelog("removing _NET_WM_DESKTOP property from window %#x", self.client_window.xid)
    641                 X11Window.XDeleteProperty(self.client_window.xid, "_NET_WM_DESKTOP")
    642             else:
    643                 workspacelog("setting _NET_WM_DESKTOP=%s on window %#x", workspacestr(workspace), self.client_window.xid)
    644                 prop_set(self.client_window, "_NET_WM_DESKTOP", "u32", workspace)
    645 
    646 
    647283    def _handle_fullscreen_monitors_change(self):
    648284        fsm = self.prop_get("_NET_WM_FULLSCREEN_MONITORS", ["u32"], True)
     285        metalog("_NET_WM_FULLSCREEN_MONITORS=%s", fsm)
    649286        self._internal_set_property("fullscreen-monitors", fsm)
    650         log("fullscreen-monitors=%s", fsm)
    651     _property_handlers["_NET_WM_FULLSCREEN_MONITORS"] = _handle_fullscreen_monitors_change
    652287
    653 
    654288    def _handle_bypass_compositor_change(self):
    655289        bypass = self.prop_get("_NET_WM_BYPASS_COMPOSITOR", "u32", True) or 0
     290        metalog("_NET_WM_BYPASS_COMPOSITOR=%s", bypass)
    656291        self._internal_set_property("bypass-compositor", bypass)
    657         log("bypass-compositor=%s", bypass)
    658     _property_handlers["_NET_WM_BYPASS_COMPOSITOR"] = _handle_bypass_compositor_change
    659292
    660 
    661     def _handle_wm_strut(self):
    662         partial = self.prop_get("_NET_WM_STRUT_PARTIAL", "strut-partial")
    663         if partial is not None:
    664             self._internal_set_property("strut", partial)
    665             return
    666         full = self.prop_get("_NET_WM_STRUT", "strut")
     293    def _handle_wm_strut_change(self):
     294        strut = self.prop_get("_NET_WM_STRUT_PARTIAL", "strut-partial")
     295        metalog("_NET_WM_STRUT_PARTIAL=%s", strut)
     296        if strut is None:
     297            strut = self.prop_get("_NET_WM_STRUT", "strut")
     298            metalog("_NET_WM_STRUT=%s", strut)
    667299        # Might be None:
    668         self._internal_set_property("strut", full)
     300        self._internal_set_property("strut", strut)
    669301
    670     _property_handlers["_NET_WM_STRUT"] = _handle_wm_strut
    671     _property_handlers["_NET_WM_STRUT_PARTIAL"] = _handle_wm_strut
    672 
    673 
    674302    def _handle_opacity_change(self):
    675303        opacity = self.prop_get("_NET_WM_WINDOW_OPACITY", "u32", True) or -1
     304        metalog("_NET_WM_WINDOW_OPACITY=%s", opacity)
    676305        self._internal_set_property("opacity", opacity)
    677     _property_handlers["_NET_WM_WINDOW_OPACITY"] = _handle_opacity_change
    678306
    679     def _handle_title_change(self):
    680         name = self.prop_get("_NET_WM_NAME", "utf8", True)
    681         if name is None:
    682             name = self.prop_get("WM_NAME", "latin1", True)
    683         self._internal_set_property("title", sanestr(name))
    684 
    685     _property_handlers["WM_NAME"] = _handle_title_change
    686     _property_handlers["_NET_WM_NAME"] = _handle_title_change
    687 
    688     def _handle_wm_hints(self):
     307    def _handle_wm_hints_change(self):
    689308        with xswallow:
    690309            wm_hints = X11Window.getWMHints(self.client_window.xid)
    691310        if wm_hints is None:
     
    699318            except:
    700319                group_leader = xid, None
    701320        self._internal_set_property("group-leader", group_leader)
     321        self._internal_set_property("attention-requested", wm_hints.get("urgency", False))
    702322
    703         if "urgency" in wm_hints:
    704             self.set_property("attention-requested", True)
    705 
    706323        _input = wm_hints.get("input")
    707         log("wm_hints.input = %s", _input)
     324        metalog("wm_hints.input = %s", _input)
    708325        #we only set this value once:
    709326        #(input_field always starts as True, and we then set it to an int)
    710327        if self._input_field is True and _input is not None:
     
    713330            if bool(self._input_field):
    714331                self.notify("can-focus")
    715332
    716     _property_handlers["WM_HINTS"] = _handle_wm_hints
    717 
    718     def _guess_window_type(self, transient_for):
    719         if transient_for is not None:
    720             # EWMH says that even if it's transient-for, we MUST check to
    721             # see if it's override-redirect (and if so treat as NORMAL).
    722             # But we wouldn't be here if this was override-redirect.
    723             # (OverrideRedirectWindowModel overrides this method)
    724             return "_NET_WM_TYPE_DIALOG"
    725         return "_NET_WM_WINDOW_TYPE_NORMAL"
    726 
    727     def is_tray(self):
    728         return False
    729 
    730     def uses_XShm(self):
    731         return self._composite and self._composite.get_property("shm-handle") is not None
    732 
    733     def has_alpha(self):
    734         return self.get_property("has-alpha")
    735 
    736     def get_image(self, x, y, width, height, logger=log.debug):
    737         handle = self._composite.get_property("contents-handle")
    738         if handle is None:
    739             logger("get_image(..) pixmap is None for window %#x", self.client_window.xid)
    740             return  None
    741 
    742         #try XShm:
    743         try:
    744             #logger("get_image(%s, %s, %s, %s) geometry=%s", x, y, width, height, self._geometry[:4])
    745             shm = self._composite.get_property("shm-handle")
    746             #logger("get_image(..) XShm handle: %s, handle=%s, pixmap=%s", shm, handle, handle.get_pixmap())
    747             if shm is not None:
    748                 with xsync:
    749                     shm_image = shm.get_image(handle.get_pixmap(), x, y, width, height)
    750                 #logger("get_image(..) XShm image: %s", shm_image)
    751                 if shm_image:
    752                     return shm_image
    753         except Exception as e:
    754             if type(e)==XError and e.msg=="BadMatch":
    755                 logger("get_image(%s, %s, %s, %s) get_image BadMatch ignored (window already gone?)", x, y, width, height)
    756             else:
    757                 log.warn("get_image(%s, %s, %s, %s) get_image %s", x, y, width, height, e, exc_info=True)
    758 
    759         try:
    760             w = min(handle.get_width(), width)
    761             h = min(handle.get_height(), height)
    762             if w!=width or h!=height:
    763                 logger("get_image(%s, %s, %s, %s) clamped to pixmap dimensions: %sx%s", x, y, width, height, w, h)
    764             with xsync:
    765                 return handle.get_image(x, y, w, h)
    766         except Exception as e:
    767             if type(e)==XError and e.msg=="BadMatch":
    768                 logger("get_image(%s, %s, %s, %s) get_image BadMatch ignored (window already gone?)", x, y, width, height)
    769             else:
    770                 log.warn("get_image(%s, %s, %s, %s) get_image %s", x, y, width, height, e, exc_info=True)
    771             return None
    772 
    773 
    774     def do_xpra_shape_event(self, event):
    775         shapelog("shape event: %s, kind=%s", event, SHAPE_KIND.get(event.kind, event.kind))
    776         cur_shape = self.get_property("shape")
    777         if cur_shape and cur_shape.get("serial", 0)>=event.serial:
    778             shapelog("same or older xshape serial no: %#x", event.serial)
    779             return
    780         #remove serial before comparing dicts:
    781         try:
    782             cur_shape["serial"]
    783         except:
    784             pass
    785         #read new xshape:
    786         v = self._read_xshape()
    787         if cur_shape==v:
    788             shapelog("xshape unchanged")
    789             return
    790         v["serial"] = int(event.serial)
    791         shapelog("xshape updated with serial %#x", event.serial)
    792         self._internal_set_property("shape", v)
    793 
    794 
    795     def do_xpra_xkb_event(self, event):
    796         log("WindowModel.do_xpra_xkb_event(%r)" % event)
    797         if event.type!="bell":
    798             log.error("WindowModel.do_xpra_xkb_event(%r) unknown event type: %s" % (event, event.type))
    799             return
    800         event.window_model = self
    801         self.emit("bell", event)
    802 
    803     def do_xpra_client_message_event(self, event):
    804         log("do_xpra_client_message_event(%s)", event)
    805         if not event.data or len(event.data)!=5:
    806             log.warn("invalid event data: %s", event.data)
    807             return
    808         if not self.process_client_message_event(event):
    809             log.warn("do_xpra_client_message_event(%s) not handled", event)
    810 
    811     def process_client_message_event(self, event):
    812         #most messages are only handled in WindowModel,
    813         #OR windows and trays should not be receiving any other message
    814         if event.message_type=="_NET_CLOSE_WINDOW":
    815             log.info("_NET_CLOSE_WINDOW received by %s", self)
    816             self.request_close()
    817             return True
    818         return False
    819 
    820 
    821     def set_active(self):
    822         prop_set(self.client_window.get_screen().get_root_window(), "_NET_ACTIVE_WINDOW", "u32", self.client_window.xid)
    823 
    824 
    825     def do_xpra_focus_in_event(self, event):
    826         grablog("focus_in_event(%s) mode=%s, detail=%s",
    827             event, GRAB_CONSTANTS.get(event.mode), DETAIL_CONSTANTS.get(event.detail, event.detail))
    828         if event.mode==NotifyNormal and event.detail==NotifyNonlinearVirtual:
    829             self.emit("raised", event)
    830         else:
    831             self.may_emit_grab(event)
    832 
    833     def do_xpra_focus_out_event(self, event):
    834         grablog("focus_out_event(%s) mode=%s, detail=%s",
    835             event, GRAB_CONSTANTS.get(event.mode), DETAIL_CONSTANTS.get(event.detail, event.detail))
    836         self.may_emit_grab(event)
    837 
    838     def may_emit_grab(self, event):
    839         if event.mode==NotifyGrab:
    840             grablog("emitting grab on %s", self)
    841             self.emit("grab", event)
    842         if event.mode==NotifyUngrab:
    843             grablog("emitting ungrab on %s", self)
    844             self.emit("ungrab", event)
    845 
    846 
    847 gobject.type_register(BaseWindowModel)
    848 
    849 
    850 # FIXME: EWMH says that O-R windows should set various properties just like
    851 # ordinary managed windows; so some of that code should get pushed up into the
    852 # superclass sooner or later.  When someone cares, presumably.
    853 class OverrideRedirectWindowModel(BaseWindowModel):
    854     __gsignals__ = BaseWindowModel.__common_signals__.copy()
    855 
    856     def __init__(self, client_window):
    857         super(OverrideRedirectWindowModel, self).__init__(client_window)
    858         self.property_names.append("override-redirect")
    859 
    860     def setup(self):
    861         self._read_initial_properties()
    862         BaseWindowModel.setup(self)
    863         # So now if the window becomes unmapped in the future then we will
    864         # notice... but it might be unmapped already, and any event
    865         # already generated, and our request for that event is too late!
    866         # So double check now, *after* putting in our request:
    867         if not X11Window.is_mapped(self.client_window.xid):
    868             raise Unmanageable("window already unmapped")
    869         ch = self._composite.get_property("contents-handle")
    870         if ch is None:
    871             raise Unmanageable("failed to get damage handle")
    872 
    873     def _read_initial_properties(self):
    874         BaseWindowModel._read_initial_properties(self)
    875         self._internal_set_property("override-redirect", True)
    876 
    877     def _guess_window_type(self, transient_for):
    878         return "_NET_WM_WINDOW_TYPE_NORMAL"
    879 
    880     def do_xpra_unmap_event(self, event):
    881         self.unmanage()
    882 
    883     def get_dimensions(self):
    884         ww, wh = self._geometry[2:4]
    885         return ww, wh
    886 
    887     def is_OR(self):
    888         return  True
    889 
    890     def raise_window(self):
    891         self.client_window.raise_()
    892 
    893     def __repr__(self):
    894         return "OverrideRedirectWindowModel(%#x)" % self.client_window.xid
    895 
    896 
    897 gobject.type_register(OverrideRedirectWindowModel)
    898 
    899 
    900 class SystemTrayWindowModel(OverrideRedirectWindowModel):
    901 
    902     def __init__(self, client_window):
    903         OverrideRedirectWindowModel.__init__(self, client_window)
    904         self.property_names = ["pid", "role", "xid", "has-alpha", "tray", "title"]
    905 
    906     def is_tray(self):
    907         return  True
    908 
    909     def has_alpha(self):
    910         return  True
    911 
    912     def _read_initial_properties(self):
    913         BaseWindowModel._read_initial_properties(self)
    914         self._internal_set_property("tray", True)
    915         self._internal_set_property("has-alpha", True)
    916 
    917     def move_resize(self, x, y, width, height):
    918         #Used by clients to tell us where the tray is located on screen
    919         log("SystemTrayWindowModel.move_resize(%s, %s, %s, %s)", x, y, width, height)
    920         self.client_window.move_resize(x, y, width, height)
    921         border = self._geometry[4]
    922         self._geometry = (x, y, width, height, border)
    923 
    924     def __repr__(self):
    925         return "SystemTrayWindowModel(%#x)" % self.client_window.xid
    926 
    927 
    928 class WindowModel(BaseWindowModel):
    929     """This represents a managed client window.  It allows one to produce
    930     widgets that view that client window in various ways."""
    931 
    932     _NET_WM_ALLOWED_ACTIONS = [
    933         "_NET_WM_ACTION_CLOSE",
    934         "_NET_WM_ACTION_MOVE",
    935         "_NET_WM_ACTION_RESIZE",
    936         "_NET_WM_ACTION_FULLSCREEN",
    937         "_NET_WM_ACTION_MINIMIZE",
    938         "_NET_WM_ACTION_SHADE",
    939         "_NET_WM_ACTION_STICK",
    940         "_NET_WM_ACTION_MAXIMIZE_HORZ",
    941         "_NET_WM_ACTION_MAXIMIZE_VERT",
    942         "_NET_WM_ACTION_CHANGE_DESKTOP",
    943         "_NET_WM_ACTION_ABOVE",
    944         "_NET_WM_ACTION_BELOW",
    945         ]
    946 
    947     __gproperties__ = {
    948         # Interesting properties of the client window, that will be
    949         # automatically kept up to date:
    950         "actual-size": (gobject.TYPE_PYOBJECT,
    951                         "Size of client window (actual (width,height))", "",
    952                         gobject.PARAM_READABLE),
    953         "user-friendly-size": (gobject.TYPE_PYOBJECT,
    954                                "Description of client window size for user", "",
    955                                gobject.PARAM_READABLE),
    956         "requested-position": (gobject.TYPE_PYOBJECT,
    957                                "Client-requested position on screen", "",
    958                                gobject.PARAM_READABLE),
    959         "requested-size": (gobject.TYPE_PYOBJECT,
    960                            "Client-requested size on screen", "",
    961                            gobject.PARAM_READABLE),
    962         "size-hints": (gobject.TYPE_PYOBJECT,
    963                        "Client hints on constraining its size", "",
    964                        gobject.PARAM_READABLE),
    965         "class-instance": (gobject.TYPE_PYOBJECT,
    966                            "Classic X 'class' and 'instance'", "",
    967                            gobject.PARAM_READABLE),
    968         "protocols": (gobject.TYPE_PYOBJECT,
    969                       "Supported WM protocols", "",
    970                       gobject.PARAM_READABLE),
    971         "frame": (gobject.TYPE_PYOBJECT,
    972                       "Size of the window frame", "",
    973                       gobject.PARAM_READWRITE),
    974         "client-machine": (gobject.TYPE_PYOBJECT,
    975                            "Host where client process is running", "",
    976                            gobject.PARAM_READABLE),
    977         "command": (gobject.TYPE_PYOBJECT,
    978                            "Command used to start or restart the client", "",
    979                            gobject.PARAM_READABLE),
    980         # Toggling this property does not actually make the window iconified,
    981         # i.e. make it appear or disappear from the screen -- it merely
    982         # updates the various window manager properties that inform the world
    983         # whether or not the window is iconified.
    984         "iconic": (gobject.TYPE_BOOLEAN,
    985                    "ICCCM 'iconic' state -- any sort of 'not on desktop'.", "",
    986                    False,
    987                    gobject.PARAM_READWRITE),
    988         "state": (gobject.TYPE_PYOBJECT,
    989                   "State, as per _NET_WM_STATE", "",
    990                   gobject.PARAM_READABLE),
    991         "icon-title": (gobject.TYPE_PYOBJECT,
    992                        "Icon title (unicode or None)", "",
    993                        gobject.PARAM_READABLE),
    994         "icon": (gobject.TYPE_PYOBJECT,
    995                  "Icon (local Cairo surface)", "",
    996                  gobject.PARAM_READABLE),
    997         "icon-pixmap": (gobject.TYPE_PYOBJECT,
    998                         "Icon (server Pixmap)", "",
    999                         gobject.PARAM_READABLE),
    1000 
    1001         "owner": (gobject.TYPE_PYOBJECT,
    1002                   "Owner", "",
    1003                   gobject.PARAM_READABLE),
    1004         "decorations": (gobject.TYPE_BOOLEAN,
    1005                        "Should the window decorations be shown", "",
    1006                        True,
    1007                        gobject.PARAM_READABLE),
    1008         }
    1009     __gsignals__ = BaseWindowModel.__common_signals__.copy()
    1010     __gsignals__.update({
    1011         "ownership-election"            : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_PYOBJECT, (), non_none_list_accumulator),
    1012         "child-map-request-event"       : one_arg_signal,
    1013         "child-configure-request-event" : one_arg_signal,
    1014         "xpra-destroy-event"            : one_arg_signal,
     333    _property_handlers = CoreX11WindowModel._property_handlers.copy()
     334    _property_handlers.update({
     335        "_NET_WM_DESKTOP"               : _handle_workspace_change,
     336        "_NET_WM_FULLSCREEN_MONITORS"   : _handle_fullscreen_monitors_change,
     337        "_NET_WM_BYPASS_COMPOSITOR"     : _handle_bypass_compositor_change,
     338        "_NET_WM_STRUT"                 : _handle_wm_strut_change,
     339        "_NET_WM_STRUT_PARTIAL"         : _handle_wm_strut_change,
     340        "_NET_WM_WINDOW_OPACITY"        : _handle_opacity_change,
     341        "WM_HINTS"                      : _handle_wm_hints_change,
    1015342        })
    1016343
    1017     def __init__(self, parking_window, client_window):
    1018         """Register a new client window with the WM.
    1019344
    1020         Raises an Unmanageable exception if this window should not be
    1021         managed, for whatever reason.  ATM, this mostly means that the window
    1022         died somehow before we could do anything with it."""
    1023 
    1024         super(WindowModel, self).__init__(client_window)
    1025         self.parking_window = parking_window
    1026         self.corral_window = None
    1027         self.in_save_set = False
    1028         self.client_reparented = False
    1029         self.last_unmap_serial = 0
    1030         self.kill_count = 0
    1031 
    1032         self.connect("notify::iconic", self._handle_iconic_update)
    1033 
    1034         self.property_names += ["title", "icon-title", "size-hints", "class-instance", "icon", "client-machine", "command",
    1035                                 "modal", "decorations",
    1036                                 "above", "below", "shaded", "sticky", "skip-taskbar", "skip-pager"]
    1037         self.call_setup()
    1038 
    1039     def setup(self):
    1040         BaseWindowModel.setup(self)
    1041 
    1042         x, y, w, h, _ = self.client_window.get_geometry()
    1043         # We enable PROPERTY_CHANGE_MASK so that we can call
    1044         # x11_get_server_time on this window.
    1045         self.corral_window = gtk.gdk.Window(self.parking_window,
    1046                                             x = x, y = y, width =w, height= h,
    1047                                             window_type=gtk.gdk.WINDOW_CHILD,
    1048                                             wclass=gtk.gdk.INPUT_OUTPUT,
    1049                                             event_mask=gtk.gdk.PROPERTY_CHANGE_MASK,
    1050                                             title = "CorralWindow-%#x" % self.client_window.xid)
    1051         log("setup() corral_window=%#x", self.corral_window.xid)
    1052         prop_set(self.corral_window, "_NET_WM_NAME", "utf8", u"Xpra-CorralWindow-%#x" % self.client_window.xid)
    1053         X11Window.substructureRedirect(self.corral_window.xid)
    1054         add_event_receiver(self.corral_window, self)
    1055 
    1056         # The child might already be mapped, in case we inherited it from
    1057         # a previous window manager.  If so, we unmap it now, and save the
    1058         # serial number of the request -- this way, when we get an
    1059         # UnmapNotify later, we'll know that it's just from us unmapping
    1060         # the window, not from the client withdrawing the window.
    1061         if X11Window.is_mapped(self.client_window.xid):
    1062             log("hiding inherited window")
    1063             self.last_unmap_serial = X11Window.Unmap(self.client_window.xid)
    1064 
    1065         # Process properties
    1066         self._read_initial_properties()
    1067         self._write_initial_properties_and_setup()
    1068 
    1069         log("setup() adding to save set")
    1070         X11Window.XAddToSaveSet(self.client_window.xid)
    1071         self.in_save_set = True
    1072 
    1073         log("setup() reparenting")
    1074         X11Window.Reparent(self.client_window.xid, self.corral_window.xid, 0, 0)
    1075         self.client_reparented = True
    1076 
    1077         log("setup() geometry")
    1078         w,h = X11Window.getGeometry(self.client_window.xid)[2:4]
    1079         hints = self.get_property("size-hints")
    1080         log("setup() hints=%s size=%ix%i", hints, w, h)
    1081         self._sanitize_size_hints(hints)
    1082         nw, nh = calc_constrained_size(w, h, hints)[:2]
    1083         if nw>=MAX_WINDOW_SIZE or nh>=MAX_WINDOW_SIZE:
    1084             #we can't handle windows that big!
    1085             raise Unmanageable("window constrained size is too large: %sx%s (from client geometry: %s,%s with size hints=%s)" % (nw, nh, w, h, hints))
    1086         log("setup() resizing windows to %sx%s", nw, nh)
    1087         self.corral_window.resize(nw, nh)
    1088         self.client_window.resize(nw, nh)
    1089         self.client_window.show_unraised()
    1090         #this is here to trigger X11 errors if any are pending
    1091         #or if the window is deleted already:
    1092         self.client_window.get_geometry()
    1093         self._internal_set_property("actual-size", (nw, nh))
    1094 
    1095     def get_dynamic_property_names(self):
    1096         return list(BaseWindowModel.get_dynamic_property_names(self))+["icon", "icon-title", "size-hints", "iconic", "decorations", "modal",
    1097                                                                        "above", "below", "shaded", "sticky", "skip-taskbar", "skip-pager"]
    1098 
    1099 
    1100     def is_OR(self):
    1101         return  False
    1102 
    1103     def raise_window(self):
    1104         self.corral_window.raise_()
    1105 
    1106     def get_dimensions(self):
    1107         return  self.get_property("actual-size")
    1108 
    1109 
    1110     def process_client_message_event(self, event):
    1111         if BaseWindowModel.process_client_message_event(self, event):
    1112             #already handled
    1113             return True
    1114         # FIXME
    1115         # Need to listen for:
    1116         #   _NET_CURRENT_DESKTOP
    1117         #   _NET_WM_PING responses
    1118         # and maybe:
    1119         #   _NET_RESTACK_WINDOW
    1120         #   _NET_WM_STATE (more fully)
    1121         def update_wm_state(prop):
    1122             current = self.get_property(prop)
    1123             mode = event.data[0]
    1124             if mode==_NET_WM_STATE_ADD:
    1125                 v = True
    1126             elif mode==_NET_WM_STATE_REMOVE:
    1127                 v = False
    1128             elif mode==_NET_WM_STATE_TOGGLE:
    1129                 v = not bool(current)
    1130             else:
    1131                 log.warn("invalid mode for _NET_WM_STATE: %s", mode)
    1132                 return
    1133             log("do_xpra_client_message_event(%s) window %s=%s after %s (current state=%s)", event, prop, v, STATE_STRING.get(mode, mode), current)
    1134             if v!=current:
    1135                 self.set_property(prop, v)
    1136 
    1137         if event.message_type=="_NET_WM_STATE":
    1138             atom1 = get_pyatom(event.window, event.data[1])
    1139             log("_NET_WM_STATE: %s", atom1)
    1140             if atom1=="_NET_WM_STATE_FULLSCREEN":
    1141                 update_wm_state("fullscreen")
    1142             elif atom1=="_NET_WM_STATE_ABOVE":
    1143                 update_wm_state("above")
    1144             elif atom1=="_NET_WM_STATE_BELOW":
    1145                 update_wm_state("below")
    1146             elif atom1=="_NET_WM_STATE_SHADED":
    1147                 update_wm_state("shaded")
    1148             elif atom1=="_NET_WM_STATE_STICKY":
    1149                 update_wm_state("sticky")
    1150             elif atom1=="_NET_WM_STATE_SKIP_TASKBAR":
    1151                 update_wm_state("skip-taskbar")
    1152             elif atom1=="_NET_WM_STATE_SKIP_PAGER":
    1153                 update_wm_state("skip-pager")
    1154                 get_pyatom(event.window, event.data[2])
    1155             elif atom1 in ("_NET_WM_STATE_MAXIMIZED_VERT", "_NET_WM_STATE_MAXIMIZED_HORZ"):
    1156                 atom2 = get_pyatom(event.window, event.data[2])
    1157                 #we only have one state for both, so we require both to be set:
    1158                 if atom1!=atom2 and atom2 in ("_NET_WM_STATE_MAXIMIZED_VERT", "_NET_WM_STATE_MAXIMIZED_HORZ"):
    1159                     update_wm_state("maximized")
    1160             elif atom1=="_NET_WM_STATE_HIDDEN":
    1161                 log("ignoring 'HIDDEN' _NET_WM_STATE: %s", event)
    1162                 #we don't honour those because they make little sense, see:
    1163                 #https://mail.gnome.org/archives/wm-spec-list/2005-May/msg00004.html
    1164                 pass
    1165             elif atom1=="_NET_WM_STATE_MODAL":
    1166                 update_wm_state("modal")
    1167             else:
    1168                 log.info("do_xpra_client_message_event(%s) unhandled atom=%s", event, atom1)
    1169             return True
    1170         elif event.message_type=="WM_CHANGE_STATE":
    1171             log("WM_CHANGE_STATE: %s", event.data[0])
    1172             if event.data[0]==IconicState and event.serial>self.last_unmap_serial:
    1173                 self._internal_set_property("iconic", True)
    1174             return True
    1175         elif event.message_type=="_NET_WM_MOVERESIZE":
    1176             log("_NET_WM_MOVERESIZE: %s", event)
    1177             self.emit("initiate-moveresize", event)
    1178             return True
    1179         elif event.message_type=="_NET_ACTIVE_WINDOW" and event.data[0] in (0, 1):
    1180             log("_NET_ACTIVE_WINDOW: %s", event)
    1181             self.set_active()
    1182             self.emit("raised", event)
    1183             return True
    1184         elif event.message_type=="_NET_WM_DESKTOP":
    1185             workspace = int(event.data[0])
    1186             #query the workspace count on the root window
    1187             #since we cannot access Wm from here..
    1188             root = self.client_window.get_screen().get_root_window()
    1189             ndesktops = prop_get(root, "_NET_NUMBER_OF_DESKTOPS", "u32", ignore_errors=True)
    1190             workspacelog("received _NET_WM_DESKTOP: workspace=%s, number of desktops=%s", workspacestr(workspace), ndesktops)
    1191             if ndesktops>0 and (workspace in (WORKSPACE_UNSET, WORKSPACE_ALL) or (workspace>=0 and workspace<ndesktops)):
    1192                 self.move_to_workspace(workspace)
    1193             else:
    1194                 workspacelog.warn("invalid _NET_WM_DESKTOP request: workspace=%s, number of desktops=%s", workspacestr(workspace), ndesktops)
    1195             return True
    1196         elif event.message_type=="_NET_WM_FULLSCREEN_MONITORS":
    1197             log("_NET_WM_FULLSCREEN_MONITORS: %s", event)
    1198             #TODO: we should validate the indexes instead of copying them blindly!
    1199             m1, m2, m3, m4 = event.data[0], event.data[1], event.data[2], event.data[3]
    1200             N = 16      #FIXME: arbitrary limit
    1201             if m1<0 or m1>=N or m2<0 or m2>=N or m3<0 or m3>=N or m4<0 or m4>=N:
    1202                 log.warn("invalid list of _NET_WM_FULLSCREEN_MONITORS - ignored")
    1203                 return
    1204             monitors = [m1, m2, m3, m4]
    1205             log("_NET_WM_FULLSCREEN_MONITORS: monitors=%s", monitors)
    1206             prop_set(self.client_window, "_NET_WM_FULLSCREEN_MONITORS", ["u32"], monitors)
    1207             return True
    1208         elif event.message_type=="_NET_MOVERESIZE_WINDOW":
    1209             #TODO: honour gravity, show source indication
    1210             geom = self.corral_window.get_geometry()
    1211             x, y, w, h, _ = geom
    1212             if event.data[0] & 0x100:
    1213                 x = event.data[1]
    1214             if event.data[0] & 0x200:
    1215                 y = event.data[2]
    1216             if event.data[0] & 0x400:
    1217                 w = event.data[3]
    1218             if event.data[0] & 0x800:
    1219                 h = event.data[4]
    1220             #honour hints:
    1221             hints = self.get_property("size-hints")
    1222             w, h, _, _ = calc_constrained_size(w, h, hints)
    1223             log("_NET_MOVERESIZE_WINDOW on %s (data=%s, current geometry=%s, new geometry=%s)", self, event.data, geom, (x,y,w,h))
    1224             with xswallow:
    1225                 X11Window.configureAndNotify(self.client_window.xid, x, y, w, h)
    1226             return True
    1227         elif event.message_type=="_NET_REQUEST_FRAME_EXTENTS":
    1228             log("_NET_REQUEST_FRAME_EXTENTS")
    1229             self._handle_frame_changed()
    1230             return True
    1231         #not handled:
    1232         return False
    1233 
    1234 
    1235     def do_xpra_property_notify_event(self, event):
    1236         if event.delivered_to is self.corral_window:
    1237             return
    1238         BaseWindowModel.do_xpra_property_notify_event(self, event)
    1239 
    1240     def do_child_map_request_event(self, event):
    1241         # If we get a MapRequest then it might mean that someone tried to map
    1242         # this window multiple times in quick succession, before we actually
    1243         # mapped it (so that several MapRequests ended up queued up; FSF Emacs
    1244         # 22.1.50.1 does this, at least).  It alternatively might mean that
    1245         # the client is naughty and tried to map their window which is
    1246         # currently not displayed.  In either case, we should just ignore the
    1247         # request.
    1248         log("do_child_map_request_event(%s)", event)
    1249 
    1250     def do_xpra_unmap_event(self, event):
    1251         if event.delivered_to is self.corral_window or self.corral_window is None:
    1252             return
    1253         assert event.window is self.client_window
    1254         # The client window got unmapped.  The question is, though, was that
    1255         # because it was withdrawn/destroyed, or was it because we unmapped it
    1256         # going into IconicState?
    1257         #
    1258         # At the moment, we never actually put windows into IconicState
    1259         # (i.e. unmap them), except in the special case when we start up and
    1260         # find windows that are already mapped.  So we only need to check
    1261         # against that one serial number.
    1262         #
    1263         # Also, if we receive a *synthetic* UnmapNotify event, that always
    1264         # means that the client has withdrawn the window (even if it was not
    1265         # mapped in the first place) -- ICCCM section 4.1.4.
    1266         log("do_xpra_unmap_event(%s) client window unmapped", event)
    1267         if event.send_event or event.serial>self.last_unmap_serial:
    1268             self.unmanage()
    1269 
    1270     def do_xpra_destroy_event(self, event):
    1271         if event.delivered_to is self.corral_window or self.corral_window is None:
    1272             return
    1273         assert event.window is self.client_window
    1274         # This is somewhat redundant with the unmap signal, because if you
    1275         # destroy a mapped window, then a UnmapNotify is always generated.
    1276         # However, this allows us to catch the destruction of unmapped
    1277         # ("iconified") windows, and also catch any mistakes we might have
    1278         # made with unmap heuristics.  I love the smell of XDestroyWindow in
    1279         # the morning.  It makes for simple code:
    1280         self.unmanage()
    1281 
    1282     SCRUB_PROPERTIES = ["WM_STATE",
    1283                         "_NET_WM_STATE",
    1284                         "_NET_FRAME_EXTENTS",
    1285                         "_NET_WM_ALLOWED_ACTIONS",
    1286                         ]
    1287 
    1288     def do_unmanaged(self, wm_exiting):
    1289         log("unmanaging window: %s (%s - %s)", self, self.corral_window, self.client_window)
    1290         self._internal_set_property("owner", None)
    1291         if self.corral_window:
    1292             remove_event_receiver(self.corral_window, self)
    1293             with xswallow:
    1294                 for prop in WindowModel.SCRUB_PROPERTIES:
    1295                     X11Window.XDeleteProperty(self.client_window.xid, prop)
    1296             if self.client_reparented:
    1297                 self.client_window.reparent(gtk.gdk.get_default_root_window(), 0, 0)
    1298                 self.client_reparented = False
    1299             self.client_window.set_events(self.client_window_saved_events)
    1300             #it is now safe to destroy the corral window:
    1301             self.corral_window.destroy()
    1302             self.corral_window = None
    1303             # It is important to remove from our save set, even after
    1304             # reparenting, because according to the X spec, windows that are
    1305             # in our save set are always Mapped when we exit, *even if those
    1306             # windows are no longer inferior to any of our windows!* (see
    1307             # section 10. Connection Close).  This causes "ghost windows", see
    1308             # bug #27:
    1309             if self.in_save_set:
    1310                 with xswallow:
    1311                     X11Window.XRemoveFromSaveSet(self.client_window.xid)
    1312                 self.in_save_set = False
    1313             with xswallow:
    1314                 X11Window.sendConfigureNotify(self.client_window.xid)
    1315             if wm_exiting:
    1316                 self.client_window.show_unraised()
    1317         BaseWindowModel.do_unmanaged(self, wm_exiting)
    1318 
    1319     def ownership_election(self):
    1320         #returns True if we have updated the geometry
    1321         candidates = self.emit("ownership-election")
    1322         if candidates:
    1323             rating, winner = sorted(candidates)[-1]
    1324             if rating < 0:
    1325                 winner = None
    1326         else:
    1327             winner = None
    1328         old_owner = self.get_property("owner")
    1329         if old_owner is winner:
    1330             return False
    1331         if old_owner is not None:
    1332             self.corral_window.hide()
    1333             self.corral_window.reparent(self.parking_window, 0, 0)
    1334         self._internal_set_property("owner", winner)
    1335         if winner is not None:
    1336             winner.take_window(self, self.corral_window)
    1337             self._update_client_geometry()
    1338             self.corral_window.show_unraised()
    1339             return True
    1340         with xswallow:
    1341             X11Window.sendConfigureNotify(self.client_window.xid)
    1342         return False
    1343 
    1344     def maybe_recalculate_geometry_for(self, maybe_owner):
    1345         if maybe_owner and self.get_property("owner") is maybe_owner:
    1346             self._update_client_geometry()
    1347 
    1348     def _sanitize_size_hints(self, size_hints):
    1349         if size_hints is None:
    1350             return
    1351         for attr in ["min_aspect", "max_aspect"]:
    1352             v = size_hints.get(attr)
    1353             if v is not None:
    1354                 try:
    1355                     f = float(v)
    1356                 except:
    1357                     f = None
    1358                 if f is None or f>=MAX_ASPECT:
    1359                     log.warn("clearing invalid aspect hint value for %s: %s", attr, v)
    1360                     size_hints[attr] = -1.0
    1361         for attr in ["max_size", "min_size", "base_size", "resize_inc",
    1362                      "min_aspect_ratio", "max_aspect_ratio"]:
    1363             v = size_hints.get(attr)
    1364             if v is not None:
    1365                 try:
    1366                     w,h = v
    1367                 except:
    1368                     w,h = None,None
    1369                 if (w is None or h is None) or w>=MAX_WINDOW_SIZE or h>=MAX_WINDOW_SIZE:
    1370                     log("clearing invalid size hint value for %s: %s", attr, v)
    1371                     del size_hints[attr]
    1372         #if max-size is smaller than min-size (bogus), clamp it..
    1373         mins = size_hints.get("min_size")
    1374         maxs = size_hints.get("max_size")
    1375         if mins is not None and maxs is not None:
    1376             minw,minh = mins
    1377             maxw,maxh = maxs
    1378             if minw<=0 and minh<=0:
    1379                 #doesn't do anything
    1380                 size_hints["min_size"] = None
    1381             if maxw<=0 or maxh<=0:
    1382                 #doesn't mak sense!
    1383                 size_hints["max_size"] = None
    1384             if maxw<minw or maxh<minh:
    1385                 size_hints["min_size"] = max(minw, maxw), max(minh, maxh)
    1386                 size_hints["max_size"] = size_hints.min_size
    1387                 log.warn("invalid min_size=%s / max_size=%s changed to: %s / %s",
    1388                          mins, maxs, size_hints["min_size"], size_hints["max_size"])
    1389 
    1390     def _update_client_geometry(self):
    1391         owner = self.get_property("owner")
    1392         log("_update_client_geometry: owner=%s, setup_done=%s", owner, self._setup_done)
    1393         if owner is not None:
    1394             log("_update_client_geometry: using owner=%s", owner)
    1395             def window_size():
    1396                 return  owner.window_size(self)
    1397             def window_position(w, h):
    1398                 return  owner.window_position(self, w, h)
    1399             self._do_update_client_geometry(window_size, window_position)
    1400         elif not self._setup_done:
    1401             #try to honour initial size and position requests during setup:
    1402             def window_size():
    1403                 return self.get_property("requested-size")
    1404             def window_position(w=0, h=0):
    1405                 return self.get_property("requested-position")
    1406             log("_update_client_geometry: using initial size=%s and position=%s", window_size(), window_position())
    1407             self._do_update_client_geometry(window_size, window_position)
    1408 
    1409     def _do_update_client_geometry(self, window_size_cb, window_position_cb):
    1410         allocated_w, allocated_h = window_size_cb()
    1411         log("_do_update_client_geometry: %sx%s", allocated_w, allocated_h)
    1412         hints = self.get_property("size-hints")
    1413         log("_do_update_client_geometry: hints=%s", hints)
    1414         size = calc_constrained_size(allocated_w, allocated_h, hints)
    1415         log("_do_update_client_geometry: size=%s", size)
    1416         w, h, wvis, hvis = size
    1417         x, y = window_position_cb(w, h)
    1418         log("_do_update_client_geometry: position=%s", (x,y))
    1419         self.corral_window.move_resize(x, y, w, h)
    1420         self._internal_set_property("actual-size", (w, h))
    1421         self._internal_set_property("user-friendly-size", (wvis, hvis))
    1422         with xswallow:
    1423             X11Window.configureAndNotify(self.client_window.xid, 0, 0, w, h)
    1424 
    1425     def do_xpra_configure_event(self, event):
    1426         log("WindowModel.do_xpra_configure_event(%s) corral=%#x, client=%#x, managed=%s", event, self.corral_window.xid, self.client_window.xid, self._managed)
    1427         if not self._managed:
    1428             return
    1429         if event.window!=self.client_window:
    1430             #we only care about events on the client window
    1431             log("WindowModel.do_xpra_configure_event: event is not on the client window")
    1432             return
    1433         if self.corral_window is None or not self.corral_window.is_visible():
    1434             log("WindowModel.do_xpra_configure_event: corral window is not visible")
    1435             return
    1436         if self.client_window is None or not self.client_window.is_visible():
    1437             log("WindowModel.do_xpra_configure_event: client window is not visible")
    1438             return
    1439         try:
    1440             #workaround applications whose windows disappear from underneath us:
    1441             with xsync:
    1442                 if self.resize_corral_window(event.x, event.y, event.width, event.height, event.border_width):
    1443                     self.notify("geometry")
    1444         except XError as e:
    1445             log.warn("failed to resize corral window: %s", e)
    1446 
    1447     def resize_corral_window(self, x, y, w, h, border):
    1448         #the client window may have been resized or moved (generally programmatically)
    1449         #so we may need to update the corral_window to match
    1450         cox, coy, cow, coh = self.corral_window.get_geometry()[:4]
    1451         modded = False
    1452         if self._geometry[4]!=border:
    1453             modded = True
    1454         #size changes (and position if any):
    1455         hints = self.get_property("size-hints")
    1456         size = calc_constrained_size(w, h, hints)
    1457         log("resize_corral_window() new constrained size=%s", size)
    1458         w, h, wvis, hvis = size
    1459         if cow!=w or coh!=h:
    1460             if (x, y) != (0, 0):
    1461                 log("resize_corral_window() move and resize from %s to %s", (cox, coy, cow, coh), (x, y, w, h))
    1462                 self.corral_window.move_resize(x, y, w, h)
    1463                 self.client_window.move(0, 0)
    1464                 cox, coy, cow, coh = x, y, w, h
    1465             else:
    1466                 #just resize:
    1467                 log("resize_corral_window() resize from %s to %s", (cow, coh), (w, h))
    1468                 self.corral_window.resize(w, h)
    1469                 cow, coh = w, h
    1470             modded = True
    1471         #just position change:
    1472         elif (x, y) != (0, 0):
    1473             log("resize_corral_window() moving corral window from %s to %s", (cox, coy), (x, y))
    1474             self.corral_window.move(x, y)
    1475             self.client_window.move(0, 0)
    1476             cox, coy = x, y
    1477             modded = True
    1478 
    1479         #these two should be using geometry rather than duplicating it?
    1480         if self.get_property("actual-size")!=(w, h):
    1481             self._internal_set_property("actual-size", (w, h))
    1482             modded = True
    1483         if self.get_property("user-friendly-size")!=(wvis, hvis):
    1484             self._internal_set_property("user-friendly-size", (wvis, hvis))
    1485             modded = True
    1486 
    1487         if modded:
    1488             self._geometry = (cox, coy, cow, coh, border)
    1489         log("resize_corral_window() modified=%s, geometry=%s", modded, self._geometry)
    1490         return modded
    1491 
    1492     def do_child_configure_request_event(self, event):
    1493         log("do_child_configure_request_event(%s) client=%#x, corral=%#x, value_mask=%s", event, self.client_window.xid, self.corral_window.xid, configure_bits(event.value_mask))
    1494         if event.value_mask & CWStackMode:
    1495             log(" restack above=%s, detail=%s", event.above, event.detail)
    1496         # Also potentially update our record of what the app has requested:
    1497         (x, y) = self.get_property("requested-position")
    1498         if event.value_mask & CWX:
    1499             x = event.x
    1500         if event.value_mask & CWY:
    1501             y = event.y
    1502         self._internal_set_property("requested-position", (x, y))
    1503 
    1504         (w, h) = self.get_property("requested-size")
    1505         if event.value_mask & CWWidth:
    1506             w = event.width
    1507         if event.value_mask & CWHeight:
    1508             h = event.height
    1509         self._internal_set_property("requested-size", (w, h))
    1510         # As per ICCCM 4.1.5, even if we ignore the request
    1511         # send back a synthetic ConfigureNotify telling the client that nothing has happened.
    1512         log("do_child_configure_request_event updated requested geometry: %s", (x, y, w, h))
    1513         self._update_client_geometry()
    1514 
    1515         # FIXME: consider handling attempts to change stacking order here.
    1516         # (In particular, I believe that a request to jump to the top is
    1517         # meaningful and should perhaps even be respected.)
    1518 
    1519     _property_handlers = BaseWindowModel._property_handlers.copy()
    1520 
    1521     def _handle_wm_normal_hints(self):
    1522         with xswallow:
    1523             size_hints = X11Window.getSizeHints(self.client_window.xid)
    1524         #getSizeHints exports fields using their X11 names as defined in the "XSizeHints" structure,
    1525         #but we use a different naming (for historical reason and backwards compatibility)
    1526         #so rename the fields:
    1527         hints = {}
    1528         if size_hints:
    1529             for k,v in size_hints.items():
    1530                 hints[{"min_size"       : "minimum-size",
    1531                        "max_size"       : "maximum-size",
    1532                        "base_size"      : "base-size",
    1533                        "resize_inc"     : "increment",
    1534                        "min_aspect"     : "minimum-aspect-ratio",
    1535                        "max_aspect"     : "maximum-aspect-ratio",
    1536                        "win_gravity"    : "gravity",
    1537                        }.get(k, k)] = v
    1538         self._sanitize_size_hints(hints)
    1539         # Don't send out notify and ConfigureNotify events when this property
    1540         # gets no-op updated -- some apps like FSF Emacs 21 like to update
    1541         # their properties every time they see a ConfigureNotify, and this
    1542         # reduces the chance for us to get caught in loops:
    1543         old_hints = self.get_property("size-hints")
    1544         if hints and hints!=old_hints:
    1545             self._internal_set_property("size-hints", hints)
    1546             self._update_client_geometry()
    1547 
    1548     _property_handlers["WM_NORMAL_HINTS"] = _handle_wm_normal_hints
    1549 
    1550     def _handle_icon_title_change(self):
    1551         icon_name = self.prop_get("_NET_WM_ICON_NAME", "utf8", True)
    1552         iconlog("_NET_WM_ICON_NAME=%s", icon_name)
    1553         if icon_name is None:
    1554             icon_name = self.prop_get("WM_ICON_NAME", "latin1", True)
    1555         self._internal_set_property("icon-title", sanestr(icon_name))
    1556 
    1557     _property_handlers["WM_ICON_NAME"] = _handle_icon_title_change
    1558     _property_handlers["_NET_WM_ICON_NAME"] = _handle_icon_title_change
    1559 
    1560     def _handle_motif_wm_hints(self):
    1561         #motif_hints = self.prop_get("_MOTIF_WM_HINTS", "motif-hints")
    1562         motif_hints = prop_get(self.client_window, "_MOTIF_WM_HINTS", "motif-hints", ignore_errors=False, raise_xerrors=True)
    1563         log("_handle_motif_wm_hints() motif_hints=%s", motif_hints)
    1564         if motif_hints and motif_hints.flags&(2**MotifWMHints.DECORATIONS_BIT):
    1565             self._internal_set_property("decorations", motif_hints.decorations)
    1566     _property_handlers["_MOTIF_WM_HINTS"] = _handle_motif_wm_hints
    1567 
    1568     def _handle_net_wm_icon(self):
    1569         iconlog("_NET_WM_ICON changed on %#x, re-reading", self.client_window.xid)
    1570         surf = self.prop_get("_NET_WM_ICON", "icon")
    1571         if surf is not None:
    1572             # FIXME: There is no Pixmap.new_for_display(), so this isn't
    1573             # actually display-clean.  Oh well.
    1574             pixmap = gtk.gdk.Pixmap(None, surf.get_width(), surf.get_height(), 32)
    1575             screen = get_display_for(pixmap).get_default_screen()
    1576             pixmap.set_colormap(screen.get_rgba_colormap())
    1577             cr = pixmap.cairo_create()
    1578             cr.set_source_surface(surf)
    1579             # Important to use SOURCE, because a newly created Pixmap can have
    1580             # random trash as its contents, and otherwise that will show
    1581             # through any alpha in the icon:
    1582             cr.set_operator(cairo.OPERATOR_SOURCE)
    1583             cr.paint()
    1584         else:
    1585             pixmap = None
    1586         #FIXME: it would be more efficient to notify first,
    1587         #then get the icon pixels on demand and cache them..
    1588         self._internal_set_property("icon", surf)
    1589         self._internal_set_property("icon-pixmap", pixmap)
    1590         iconlog("icon is now %r", surf)
    1591     _property_handlers["_NET_WM_ICON"] = _handle_net_wm_icon
    1592 
    1593     def _read_initial_properties(self):
    1594         # Things that don't change:
    1595         BaseWindowModel._read_initial_properties(self)
    1596         def pget(key, ptype):
    1597             return self.prop_get(key, ptype, raise_xerrors=True)
    1598 
    1599         geometry = self.client_window.get_geometry()
    1600         self._internal_set_property("requested-position", (geometry[0], geometry[1]))
    1601         self._internal_set_property("requested-size", (geometry[2], geometry[3]))
    1602 
    1603         #try using XGetClassHint:
    1604         class_instance = X11Window.getClassHint(self.client_window.xid)
    1605         if class_instance is None:
    1606             #fallback to reading WM_CLASS:
    1607             def get_wm_class_prop(ptype):
    1608                 class_instance = pget("WM_CLASS", ptype)
    1609                 if not class_instance:
    1610                     return None
    1611                 try:
    1612                     parts = class_instance.split("\0")
    1613                     if len(parts)!=3:
    1614                         return None
    1615                     (c, i, _) = parts
    1616                     return  (c, i)
    1617                 except ValueError:
    1618                     log.warn("Malformed WM_CLASS: %s, ignoring", class_instance)
    1619                     return None
    1620             class_instance = get_wm_class_prop("latin1") or get_wm_class_prop("utf8")
    1621         self._internal_set_property("class-instance", class_instance)
    1622 
    1623         protocols = pget("WM_PROTOCOLS", ["atom"])
    1624         if protocols is None:
    1625             protocols = []
    1626         self._internal_set_property("protocols", protocols)
    1627         self.notify("can-focus")
    1628 
    1629         client_machine = pget("WM_CLIENT_MACHINE", "latin1")
    1630         # May be None
    1631         self._internal_set_property("client-machine", client_machine)
    1632 
    1633         command = pget("WM_COMMAND", "latin1")
    1634         if command:
    1635             command = command.strip("\0")
    1636         self._internal_set_property("command", command)
    1637 
    1638         # WARNING: have to handle _NET_WM_STATE before we look at WM_HINTS;
    1639         # WM_HINTS assumes that our "state" property is already set.  This is
    1640         # because there are four ways a window can get its urgency
    1641         # ("attention-requested") bit set:
    1642         #   1) _NET_WM_STATE_DEMANDS_ATTENTION in the _initial_ state hints
    1643         #   2) setting the bit WM_HINTS, at _any_ time
    1644         #   3) sending a request to the root window to add
    1645         #      _NET_WM_STATE_DEMANDS_ATTENTION to their state hints
    1646         #   4) if we (the wm) decide they should be and set it
    1647         # To implement this, we generally track the urgency bit via
    1648         # _NET_WM_STATE (since that is under our sole control during normal
    1649         # operation).  Then (1) is accomplished through the normal rule that
    1650         # initial states are read off from the client, and (2) is accomplished
    1651         # by having WM_HINTS affect _NET_WM_STATE.  But this means that
    1652         # WM_HINTS and _NET_WM_STATE handling become intertangled.
    1653         net_wm_state = pget("_NET_WM_STATE", ["atom"])
    1654         if net_wm_state:
    1655             self._internal_set_property("state", frozenset(net_wm_state))
    1656         else:
    1657             self._internal_set_property("state", frozenset())
    1658         modal = (net_wm_state is not None) and ("_NET_WM_STATE_MODAL" in net_wm_state)
    1659         self._internal_set_property("modal", modal)
    1660 
    1661         #the default value is True, but is not being honoured! (so we force it here)
    1662         self._internal_set_property("decorations", True)
    1663 
    1664         for mutable in ["WM_HINTS", "WM_NORMAL_HINTS",
    1665                         "WM_ICON_NAME", "_NET_WM_ICON_NAME",
    1666                         "_NET_WM_STRUT", "_NET_WM_STRUT_PARTIAL", "_MOTIF_WM_HINTS"]:
    1667             self._call_property_handler(mutable)
    1668         for mutable in ["_NET_WM_ICON"]:
    1669             try:
    1670                 self._call_property_handler(mutable)
    1671             except:
    1672                 log.error("error reading initial property %s", mutable, exc_info=True)
    1673 
    1674     def get_default_window_icon(self):
    1675         #return the icon which would be used from the wmclass
    1676         c_i = self.get_property("class-instance")
    1677         if not c_i or len(c_i)!=2:
    1678             return None
    1679         wmclass_name, wmclass_class = [x.encode("utf-8") for x in c_i]
    1680         iconlog("get_default_window_icon() using %s", (wmclass_name, wmclass_class))
    1681         if not wmclass_name:
    1682             return None
    1683         it = gtk.icon_theme_get_default()
    1684         i = it.lookup_icon(wmclass_name, 48, 0)
    1685         iconlog("%s.lookup_icon(%s)=%s", it, wmclass_name, i)
    1686         if not i:
    1687             return None
    1688         p = i.load_icon()
    1689         iconlog("%s.load_icon()=%s", i, p)
    1690         if not p:
    1691             return None
    1692         #to make it consistent with the "icon" property,
    1693         #return a cairo surface..
    1694         surf = cairo.ImageSurface(cairo.FORMAT_ARGB32, p.get_width(), p.get_height())
    1695         gc = gtk.gdk.CairoContext(cairo.Context(surf))
    1696         gc.set_source_pixbuf(p, 0, 0)
    1697         gc.paint()
    1698         iconlog("get_default_window_icon()=%s", surf)
    1699         return surf
    1700 
    1701     ################################
    1702     # Property setting
    1703     ################################
    1704 
     345    #########################################
     346    # _NET_WM_STATE
     347    #########################################
    1705348    # A few words about _NET_WM_STATE are in order.  Basically, it is a set of
    1706349    # flags.  Clients are allowed to set the initial value of this X property
    1707350    # to anything they like, when their window is first mapped; after that,
     
    1731374        "modal"                 : ("_NET_WM_STATE_MODAL", ),
    1732375        "focused"               : ("_NET_WM_STATE_FOCUSED", ),
    1733376        }
    1734 
    1735377    _state_properties_reversed = {}
    1736378    for k, states in _state_properties.iteritems():
    1737379        for x in states:
     
    1766408    def _state_isset(self, state_name):
    1767409        return state_name in self.get_property("state")
    1768410
    1769     def _handle_state_changed(self, *args):
     411    def _read_wm_state(self):
     412        wm_state = self.prop_get("_NET_WM_STATE", ["atom"])
     413        metalog("_NET_WM_STATE=%s", wm_state)
     414        return wm_state
     415
    1770416        # Sync changes to "state" property out to X property.
    1771417        with xswallow:
    1772418            wm_state = list(self.get_property("state"))
     
    1773419            prop_set(self.client_window, "_NET_WM_STATE", ["atom"], wm_state)
    1774420            log("_handle_state_changed: _NET_WM_STATE=%s", wm_state)
    1775421
    1776     def _handle_frame_changed(self, *args):
    1777         v = self.get_property("frame")
    1778         if not v and (not self.is_OR() and not self.is_tray()):
    1779             root = self.client_window.get_screen().get_root_window()
    1780             v = prop_get(root, "DEFAULT_NET_FRAME_EXTENTS", ["u32"], ignore_errors=True)
    1781         if not v:
    1782             #default for OR, or if we don't have any other value:
    1783             v = (0, 0, 0, 0)
    1784         log("handle_frame_changed: setting _NET_FRAME_EXTENTS=%s on %#x", v, self.client_window.xid)
    1785         with xswallow:
    1786             prop_set(self.client_window, "_NET_FRAME_EXTENTS", ["u32"], v)           
    1787422
    1788423    def do_set_property(self, pspec, value):
     424        #intercept state properties to route via update_state()
    1789425        if pspec.name in self._state_properties:
    1790426            #virtual property for WM_STATE:
    1791427            self.update_state(pspec.name, value)
    1792428            return
    1793         AutoPropGObjectMixin.do_set_property(self, pspec, value)
     429        super(BaseWindowModel, self).do_set_property(pspec, value)
    1794430
     431    def do_get_property(self, pspec):
     432        #intercept state properties to route via get_wm_state()
     433        if pspec.name in self._state_properties:
     434            #virtual property for WM_STATE:
     435            return self.get_wm_state(pspec.name)
     436        return super(BaseWindowModel, self).do_get_property(pspec)
     437
     438
    1795439    def update_wm_state(self, prop, b):
    1796440        state_names = self._state_properties.get(prop)
    1797441        assert state_names, "invalid window state %s" % prop
     
    1801445        else:
    1802446            self._state_remove(*state_names)
    1803447
    1804 
    1805     def do_get_property_can_focus(self, name):
    1806         assert name == "can-focus"
    1807         return bool(self._input_field) or "WM_TAKE_FOCUS" in self.get_property("protocols")
    1808 
    1809     def do_get_property(self, pspec):
    1810         if pspec.name in self._state_properties:
    1811             #virtual property for WM_STATE:
    1812             return self.get_wm_state(pspec.name)
    1813         return AutoPropGObjectMixin.do_get_property(self, pspec)
    1814 
    1815448    def get_wm_state(self, prop):
    1816449        state_names = self._state_properties.get(prop)
    1817450        assert state_names, "invalid window state %s" % prop
     
    1824457        return False
    1825458
    1826459
    1827     def unmap(self):
    1828         with xsync:
    1829             if X11Window.is_mapped(self.client_window.xid):
    1830                 self.last_unmap_serial = X11Window.Unmap(self.client_window.xid)
    1831                 log("client window %#x unmapped, serial=%s", self.client_window.xid, self.last_unmap_serial)
     460    #########################################
     461    # XShape
     462    #########################################
    1832463
    1833     def map(self):
    1834         with xsync:
    1835             if not X11Window.is_mapped(self.client_window.xid):
    1836                 X11Window.MapWindow(self.client_window.xid)
    1837                 log("client window %#x mapped", self.client_window.xid)
     464    def _read_xshape(self):
     465        if not X11Window.displayHasXShape():
     466            return {}
     467        xid = self.client_window.xid
     468        extents = X11Window.XShapeQueryExtents(xid)
     469        if not extents:
     470            shapelog("read_shape for window %#x: no extents", xid)
     471            return {}
     472        v = {}
     473        #w,h = X11Window.getGeometry(xid)[2:4]
     474        bextents = extents[0]
     475        cextents = extents[1]
     476        if bextents[0]==0 and cextents[0]==0:
     477            shapelog("read_shape for window %#x: none enabled", xid)
     478            return {}
     479        v["Bounding.extents"] = bextents
     480        v["Clip.extents"] = cextents
     481        for kind in SHAPE_KIND.keys():
     482            kind_name = SHAPE_KIND[kind]
     483            rectangles = X11Window.XShapeGetRectangles(xid, kind)
     484            v[kind_name+".rectangles"] = rectangles
     485        shapelog("_read_shape()=%s", v)
     486        return v
    1838487
    1839488
    1840     def _handle_iconic_update(self, *args):
    1841         def set_state(state):
    1842             log("_handle_iconic_update: set_state(%s)", state)
    1843             with xswallow:
    1844                 prop_set(self.client_window, "WM_STATE", "state", state)
     489    def process_client_message_event(self, event):
     490        #most messages are only handled in WindowModel,
     491        #OR windows and trays should not be receiving any other message
     492        if event.message_type=="_NET_CLOSE_WINDOW":
     493            log.info("_NET_CLOSE_WINDOW received by %s", self)
     494            self.request_close()
     495            return True
     496        return False
    1845497
    1846         if self.get_property("iconic"):
    1847             set_state(IconicState)
    1848             self._state_add("_NET_WM_STATE_HIDDEN")
    1849         else:
    1850             set_state(NormalState)
    1851             self._state_remove("_NET_WM_STATE_HIDDEN")
    1852498
    1853     def _write_initial_properties_and_setup(self):
    1854         # Things that don't change:
    1855         prop_set(self.client_window, "_NET_WM_ALLOWED_ACTIONS",
    1856                  ["atom"], self._NET_WM_ALLOWED_ACTIONS)
    1857         self.connect("notify::state", self._handle_state_changed)
    1858         self.connect("notify::frame", self._handle_frame_changed)
    1859         # Flush things:
    1860         self._handle_state_changed()
    1861499
     500    #########################################
     501    # X11 Events
     502    #########################################
    1862503
    1863     ################################
    1864     # Focus handling:
    1865     ################################
     504    def do_xpra_configure_event(self, event):
     505        if self.client_window is None or not self._managed:
     506            return
     507        #shouldn't the border width always be 0?
     508        geom = (event.x, event.y, event.width, event.height, event.border_width)
     509        log("BaseWindowModel.do_xpra_configure_event(%s) client_window=%#x, old geometry=%s, new geometry=%s", event, self.client_window.xid, self._geometry, geom)
     510        if geom!=self._geometry:
     511            self._geometry = geom
     512            #X11Window.MoveResizeWindow(self.client_window.xid, )
     513            self.notify("geometry")
    1866514
    1867     def give_client_focus(self):
    1868         """The focus manager has decided that our client should receive X
    1869         focus.  See world_window.py for details."""
    1870         if self.corral_window:
    1871             with xswallow:
    1872                 self.do_give_client_focus()
    1873515
    1874     def do_give_client_focus(self):
    1875         focuslog("Giving focus to %#x", self.client_window.xid)
    1876         # Have to fetch the time, not just use CurrentTime, both because ICCCM
    1877         # says that WM_TAKE_FOCUS must use a real time and because there are
    1878         # genuine race conditions here (e.g. suppose the client does not
    1879         # actually get around to requesting the focus until after we have
    1880         # already changed our mind and decided to give it to someone else).
    1881         now = gtk.gdk.x11_get_server_time(self.corral_window)
    1882         # ICCCM 4.1.7 *claims* to describe how we are supposed to give focus
    1883         # to a window, but it is completely opaque.  From reading the
    1884         # metacity, kwin, gtk+, and qt code, it appears that the actual rules
    1885         # for giving focus are:
    1886         #   -- the WM_HINTS input field determines whether the WM should call
    1887         #      XSetInputFocus
    1888         #   -- independently, the WM_TAKE_FOCUS protocol determines whether
    1889         #      the WM should send a WM_TAKE_FOCUS ClientMessage.
    1890         # If both are set, both methods MUST be used together. For example,
    1891         # GTK+ apps respect WM_TAKE_FOCUS alone but I'm not sure they handle
    1892         # XSetInputFocus well, while Qt apps ignore (!!!) WM_TAKE_FOCUS
    1893         # (unless they have a modal window), and just expect to get focus from
    1894         # the WM's XSetInputFocus.
    1895         if bool(self._input_field):
    1896             focuslog("... using XSetInputFocus")
    1897             X11Window.XSetInputFocus(self.client_window.xid, now)
    1898         if "WM_TAKE_FOCUS" in self.get_property("protocols"):
    1899             focuslog("... using WM_TAKE_FOCUS")
    1900             send_wm_take_focus(self.client_window, now)
    1901         self.set_active()
     516    def do_xpra_shape_event(self, event):
     517        shapelog("shape event: %s, kind=%s", event, SHAPE_KIND.get(event.kind, event.kind))
     518        cur_shape = self.get_property("shape")
     519        if cur_shape and cur_shape.get("serial", 0)>=event.serial:
     520            shapelog("same or older xshape serial no: %#x", event.serial)
     521            return
     522        #remove serial before comparing dicts:
     523        try:
     524            cur_shape["serial"]
     525        except:
     526            pass
     527        #read new xshape:
     528        v = self._read_xshape()
     529        if cur_shape==v:
     530            shapelog("xshape unchanged")
     531            return
     532        v["serial"] = int(event.serial)
     533        shapelog("xshape updated with serial %#x", event.serial)
     534        self._internal_set_property("shape", v)
    1902535
    1903     ################################
    1904     # Killing clients:
    1905     ################################
    1906536
    1907     def request_close(self):
    1908         if "WM_DELETE_WINDOW" in self.get_property("protocols"):
    1909             with xswallow:
    1910                 send_wm_delete_window(self.client_window)
    1911         else:
    1912             title = self.get_property("title")
    1913             xid = self.get_property("xid")
    1914             log.warn("window %#x ('%s') does not support WM_DELETE_WINDOW... using force_quit()", xid, title)
    1915             # You don't wanna play ball?  Then no more Mr. Nice Guy!
    1916             self.force_quit()
     537    def do_xpra_xkb_event(self, event):
     538        #X11: XKBNotify
     539        log("WindowModel.do_xpra_xkb_event(%r)" % event)
     540        if event.type!="bell":
     541            log.error("WindowModel.do_xpra_xkb_event(%r) unknown event type: %s" % (event, event.type))
     542            return
     543        event.window_model = self
     544        self.emit("bell", event)
    1917545
    1918     def force_quit(self):
    1919         pid = self.get_property("pid")
    1920         machine = self.get_property("client-machine")
    1921         localhost = gethostname()
    1922         log("force_quit() pid=%s, machine=%s, localhost=%s", pid, machine, localhost)
    1923         xid = self.client_window.xid
    1924         def XKill():
    1925             with xswallow:
    1926                 X11Window.XKillClient(xid)
    1927         if pid > 0 and machine is not None and machine == localhost:
    1928             if pid==os.getpid():
    1929                 log.warn("force_quit() refusing to kill ourselves!")
    1930                 return
    1931             if self.kill_count==0:
    1932                 #first time around: just send a SIGINT and hope for the best
    1933                 try:
    1934                     os.kill(pid, signal.SIGINT)
    1935                 except OSError:
    1936                     log.warn("failed to kill(SIGINT) client with pid %s", pid)
    1937             else:
    1938                 #the more brutal way: SIGKILL + XKill
    1939                 try:
    1940                     os.kill(pid, signal.SIGKILL)
    1941                 except OSError:
    1942                     log.warn("failed to kill(SIGKILL) client with pid %s", pid)
    1943                 XKill()
    1944             self.kill_count += 1
     546    def do_xpra_client_message_event(self, event):
     547        #X11: ClientMessage
     548        log("do_xpra_client_message_event(%s)", event)
     549        if not event.data or len(event.data)!=5:
     550            log.warn("invalid event data: %s", event.data)
    1945551            return
    1946         XKill()
     552        if not self.process_client_message_event(event):
     553            log.warn("do_xpra_client_message_event(%s) not handled", event)
    1947554
    1948     def __repr__(self):
    1949         xid = 0
    1950         if self.client_window:
    1951             xid = self.client_window.xid
    1952         title = self.get_property("title")
    1953         if title:
    1954             return "WindowModel(%#x - \"%s\")" % (xid, nonl(title))
    1955         return "WindowModel(%#x)" % xid
    1956555
     556    def do_xpra_focus_in_event(self, event):
     557        #X11: FocusIn
     558        grablog("focus_in_event(%s) mode=%s, detail=%s",
     559            event, GRAB_CONSTANTS.get(event.mode), DETAIL_CONSTANTS.get(event.detail, event.detail))
     560        if event.mode==NotifyNormal and event.detail==NotifyNonlinearVirtual:
     561            self.emit("raised", event)
     562        else:
     563            self.may_emit_grab(event)
    1957564
    1958 gobject.type_register(WindowModel)
     565    def do_xpra_focus_out_event(self, event):
     566        #X11: FocusOut
     567        grablog("focus_out_event(%s) mode=%s, detail=%s",
     568            event, GRAB_CONSTANTS.get(event.mode), DETAIL_CONSTANTS.get(event.detail, event.detail))
     569        self.may_emit_grab(event)
     570
     571    def may_emit_grab(self, event):
     572        if event.mode==NotifyGrab:
     573            grablog("emitting grab on %s", self)
     574            self.emit("grab", event)
     575        if event.mode==NotifyUngrab:
     576            grablog("emitting ungrab on %s", self)
     577            self.emit("ungrab", event)
  • xpra/x11/gtk2/models/core.py

     
    11# This file is part of Xpra.
    22# Copyright (C) 2008, 2009 Nathaniel Smith <njs@pobox.com>
    3 # Copyright (C) 2011-2014 Antoine Martin <antoine@devloop.org.uk>
     3# Copyright (C) 2011-2015 Antoine Martin <antoine@devloop.org.uk>
    44# Xpra is released under the terms of the GNU GPL v2, or, at your option, any
    55# later version. See the file COPYING for details.
    66
    7 """The magic GTK widget that represents a client window.
    8 
    9 Most of the gunk required to be a valid window manager (reparenting, synthetic
    10 events, mucking about with properties, etc. etc.) is wrapped up in here."""
    11 
    12 # Maintain compatibility with old versions of Python, while avoiding a
    13 # deprecation warning on new versions:
    147import os
    15 from socket import gethostname
    16 
     8import glib
    179import gobject
    18 import glib
    19 import gtk.gdk
    20 import cairo
     10from gtk import gdk
    2111import signal
    2212
    23 from xpra.util import nonl, WORKSPACE_UNSET, WORKSPACE_ALL
    24 from xpra.x11.bindings.window_bindings import constants, X11WindowBindings, SHAPE_KIND #@UnresolvedImport
    25 X11Window = X11WindowBindings()
    26 
    27 from xpra.x11.gtk_x11.send_wm import (
    28                 send_wm_take_focus,
    29                 send_wm_delete_window)
    30 from xpra.gtk_common.gobject_util import (AutoPropGObjectMixin,
    31                            one_arg_signal,
    32                            non_none_list_accumulator)
     13from xpra.x11.gtk2.models import Unmanageable
     14from xpra.gtk_common.gobject_util import AutoPropGObjectMixin, one_arg_signal
     15from xpra.gtk_common.error import XError, xsync, xswallow
     16from xpra.x11.bindings.window_bindings import X11WindowBindings #@UnresolvedImport
     17from xpra.x11.gtk_x11.prop import prop_get
     18from xpra.x11.gtk_x11.send_wm import send_wm_delete_window
     19from xpra.x11.gtk2.models.composite import CompositeHelper
    3320from xpra.x11.gtk2.gdk_bindings import (
    34                 get_pyatom,                                 #@UnresolvedImport
    35                 get_pywindow,                               #@UnresolvedImport
    3621                add_event_receiver,                         #@UnresolvedImport
    3722                remove_event_receiver,                      #@UnresolvedImport
    38                 get_display_for,                            #@UnresolvedImport
    39                 calc_constrained_size,                      #@UnresolvedImport
    4023               )
    41 from xpra.gtk_common.error import XError, xsync, xswallow
    42 from xpra.x11.gtk_x11.prop import prop_get, prop_set, MotifWMHints
    43 from xpra.x11.gtk2.composite import CompositeHelper
    4424
    4525from xpra.log import Logger
    4626log = Logger("x11", "window")
    47 focuslog = Logger("x11", "window", "focus")
    48 grablog = Logger("x11", "window", "grab")
    49 iconlog = Logger("x11", "window", "icon")
    50 workspacelog = Logger("x11", "window", "workspace")
    51 shapelog = Logger("x11", "window", "shape")
     27metalog = Logger("x11", "window", "metadata")
    5228
    5329
    54 _NET_WM_STATE_REMOVE = 0
    55 _NET_WM_STATE_ADD    = 1
    56 _NET_WM_STATE_TOGGLE = 2
    57 STATE_STRING = {
    58             _NET_WM_STATE_REMOVE    : "REMOVE",
    59             _NET_WM_STATE_ADD       : "ADD",
    60             _NET_WM_STATE_TOGGLE    : "TOGGLE",
    61                 }
    62 
    63 XNone = constants["XNone"]
    64 
    65 CWX             = constants["CWX"]
    66 CWY             = constants["CWY"]
    67 CWWidth         = constants["CWWidth"]
    68 CWHeight        = constants["CWHeight"]
    69 CWBorderWidth   = constants["CWBorderWidth"]
    70 CWSibling       = constants["CWSibling"]
    71 CWStackMode     = constants["CWStackMode"]
    72 CONFIGURE_GEOMETRY_MASK = CWX | CWY | CWWidth | CWHeight
    73 CW_MASK_TO_NAME = {
    74                    CWX              : "X",
    75                    CWY              : "Y",
    76                    CWWidth          : "Width",
    77                    CWHeight         : "Height",
    78                    CWBorderWidth    : "BorderWidth",
    79                    CWSibling        : "Sibling",
    80                    CWStackMode      : "StackMode",
    81                    CWBorderWidth    : "BorderWidth",
    82                    }
    83 def configure_bits(value_mask):
    84     return "|".join((v for k,v in CW_MASK_TO_NAME.items() if (k&value_mask)))
    85 
    86 
    87 IconicState = constants["IconicState"]
    88 NormalState = constants["NormalState"]
    89 
    90 # grab stuff:
    91 NotifyNormal        = constants["NotifyNormal"]
    92 NotifyGrab          = constants["NotifyGrab"]
    93 NotifyUngrab        = constants["NotifyUngrab"]
    94 NotifyWhileGrabbed  = constants["NotifyWhileGrabbed"]
    95 NotifyNonlinearVirtual = constants["NotifyNonlinearVirtual"]
    96 GRAB_CONSTANTS = {
    97                   NotifyNormal          : "NotifyNormal",
    98                   NotifyGrab            : "NotifyGrab",
    99                   NotifyUngrab          : "NotifyUngrab",
    100                   NotifyWhileGrabbed    : "NotifyWhileGrabbed",
    101                  }
    102 DETAIL_CONSTANTS    = {}
    103 for x in ("NotifyAncestor", "NotifyVirtual", "NotifyInferior",
    104           "NotifyNonlinear", "NotifyNonlinearVirtual", "NotifyPointer",
    105           "NotifyPointerRoot", "NotifyDetailNone"):
    106     DETAIL_CONSTANTS[constants[x]] = x
    107 grablog("pointer grab constants: %s", GRAB_CONSTANTS)
    108 grablog("detail constants: %s", DETAIL_CONSTANTS)
    109 
    110 
    111 #if you want to use a virtual screen bigger than 32767x32767
    112 #you will need to change those values, but some broken toolkits
    113 #will then misbehave (they use signed shorts instead of signed ints..)
    114 MAX_WINDOW_SIZE = 2**15-1
    115 MAX_ASPECT = 2**15-1
     30X11Window = X11WindowBindings()
     31ADDMASK = gdk.STRUCTURE_MASK | gdk.PROPERTY_CHANGE_MASK | gdk.FOCUS_CHANGE_MASK
    11632USE_XSHM = os.environ.get("XPRA_XSHM", "1")=="1"
    11733
    11834#these properties are not handled, and we don't want to spam the log file
    11935#whenever an app decides to change them:
    120 PROPERTIES_IGNORED = ("_NET_WM_OPAQUE_REGION", )
     36PROPERTIES_IGNORED = os.environ.get("XPRA_X11_PROPERTIES_IGNORED", "_NET_WM_OPAQUE_REGION").split(",")
    12137#make it easier to debug property changes, just add them here:
    122 PROPERTIES_DEBUG = {}   #ie: {"WM_PROTOCOLS" : ["atom"]}
     38#ie: {"WM_PROTOCOLS" : ["atom"]}
     39PROPERTIES_DEBUG = {}
    12340
    12441
    125 #add user friendly workspace logging:
    126 WORKSPACE_STR = {WORKSPACE_UNSET    : "UNSET",
    127                  WORKSPACE_ALL      : "ALL"}
    128 def workspacestr(w):
    129     return WORKSPACE_STR.get(w, w)
    130 
    131 
    13242def sanestr(s):
    13343    return (s or "").strip("\0").replace("\0", " ")
    13444
    13545
    136 # Todo:
    137 #   client focus hints
    138 #   _NET_WM_SYNC_REQUEST
    139 #   root window requests (pagers, etc. requesting to change client states)
    140 #   _NET_WM_PING/detect window not responding (also a root window message)
    141 
    142 # Okay, we need a block comment to explain the window arrangement that this
    143 # file is working with.
    144 #
    145 #                +--------+
    146 #                | widget |
    147 #                +--------+
    148 #                  /    \
    149 #  <- top         /     -\-        bottom ->
    150 #                /        \
    151 #          +-------+       |
    152 #          | image |  +---------+
    153 #          +-------+  | corral  |
    154 #                     +---------+
    155 #                          |
    156 #                     +---------+
    157 #                     | client  |
    158 #                     +---------+
    159 #
    160 # Each box in this diagram represents one X/GDK window.  In the common case,
    161 # every window here takes up exactly the same space on the screen (!).  In
    162 # fact, the two windows on the right *always* have exactly the same size and
    163 # location, and the window on the left and the top window also always have
    164 # exactly the same size and position.  However, each window in the diagram
    165 # plays a subtly different role.
    166 #
    167 # The client window is obvious -- this is the window owned by the client,
    168 # which they created and which we have various ICCCM/EWMH-mandated
    169 # responsibilities towards.  It is also composited.
    170 #
    171 # The purpose of the 'corral' is to keep the client window managed -- we
    172 # select for SubstructureRedirect on it, so that the client cannot resize
    173 # etc. without going through the WM.
    174 #
    175 # These two windows are always managed together, as a unit; an invariant of
    176 # the code is that they always take up exactly the same space on the screen.
    177 # They get reparented back and forth between widgets, and when there are no
    178 # widgets, they get reparented to a "parking area".  For now, we're just using
    179 # the root window as a parking area, so we also map/unmap the corral window
    180 # depending on whether we are parked or not; the corral and window is left
    181 # mapped at all times.
    182 #
    183 # When a particular WindowView controls the underlying client window, then two
    184 # things happen:
    185 #   -- Its size determines the size of the client window.  Ideally they are
    186 #      the same size -- but this is not always the case, because the client
    187 #      may have specified sizing constraints, in which case the client window
    188 #      is the "best fit" to the controlling widget window.
    189 #   -- The client window and its corral are reparented under the widget
    190 #      window, as in the diagram above.  This is necessary to allow mouse
    191 #      events to work -- a WindowView widget can always *look* like the client
    192 #      window is there, through the magic of Composite, but in order for it to
    193 #      *act* like the client window is there in terms of receiving mouse
    194 #      events, it has to actually be there.
    195 #
    196 # We should also have a block comment describing how to create a
    197 # view/"controller" for a WindowModel.
    198 #
    199 # Viewing a (Base)WindowModel is easy.  Connect to the client-contents-changed
    200 # signal.  Every time the window contents is updated, you'll get a message.
    201 # This message is passed a single object e, which has useful members:
    202 #   e.x, e.y, e.width, e.height:
    203 #      The part of the client window that was modified, and needs to be
    204 #      redrawn.
    205 # To get the actual contents of the window to draw, there is a "handle"
    206 # available as the "contents-handle" property on the Composite window.
    207 #
    208 # But what if you'd like to do more than just look at your pretty composited
    209 # windows?  Maybe you'd like to, say, *interact* with them?  Then life is a
    210 # little more complicated.  To make a view "live", we have to move the actual
    211 # client window to be a child of your view window and position it correctly.
    212 # Obviously, only one view can be live at any given time, so we have to figure
    213 # out which one that is.  Supposing we have a WindowModel called "model" and
    214 # a view called "view", then the following pieces come into play:
    215 #   The "ownership-election" signal on window:
    216 #     If a view wants the chance to become live, it must connect to this
    217 #     signal.  When the signal is emitted, its handler should return a tuple
    218 #     of the form:
    219 #       (votes, my_view)
    220 #     Just like a real election, everyone votes for themselves.  The view that
    221 #     gives the highest value to 'votes' becomes the new owner.  However, a
    222 #     view with a negative (< 0) votes value will never become the owner.
    223 #   model.ownership_election():
    224 #     This method (distinct from the ownership-election signal!) triggers an
    225 #     election.  All views MUST call this method whenever they decide their
    226 #     number of votes has changed.  All views MUST call this method when they
    227 #     are destructing themselves (ideally after disconnecting from the
    228 #     ownership-election signal).
    229 #   The "owner" property on window:
    230 #     This records the view that currently owns the window (i.e., the winner
    231 #     of the last election), or None if no view is live.
    232 #   view.take_window(model, window):
    233 #     This method is called on 'view' when it becomes owner of 'model'.  It
    234 #     should reparent 'window' into the appropriate place, and put it at the
    235 #     appropriate place in its window stack.  (The x,y position, however, does
    236 #     not matter.)
    237 #   view.window_size(model):
    238 #     This method is called when the model needs to know how much space it is
    239 #     allocated.  It should return the maximum (width, height) allowed.
    240 #     (However, the model may choose to use less than this.)
    241 #   view.window_position(mode, width, height):
    242 #     This method is called when the model needs to know where it should be
    243 #     located (relative to the parent window the view placed it in).  'width'
    244 #     and 'height' are the size the model window will actually be.  It should
    245 #     return the (x, y) position desired.
    246 #   model.maybe_recalculate_geometry_for(view):
    247 #     This method (potentially) triggers a resize/move of the client window
    248 #     within the view.  If 'view' is not the current owner, is a no-op, which
    249 #     means that views can call it without worrying about whether they are in
    250 #     fact the current owner.
    251 #
    252 # The actual method for choosing 'votes' is not really determined yet.
    253 # Probably it should take into account at least the following factors:
    254 #   -- has focus (or has mouse-over?)
    255 #   -- is visible in a tray/other window, and the tray/other window is visible
    256 #      -- and is focusable
    257 #      -- and is not focusable
    258 #   -- is visible in a tray, and the tray/other window is not visible
    259 #      -- and is focusable
    260 #      -- and is not focusable
    261 #      (NB: Widget.get_ancestor(my.Tray) will give us the nearest ancestor
    262 #      that isinstance(my.Tray), if any.)
    263 #   -- is not visible
    264 #   -- the size of the widget (as a final tie-breaker)
    265 
    266 class Unmanageable(Exception):
    267     pass
    268 
    269 class BaseWindowModel(AutoPropGObjectMixin, gobject.GObject):
    270     __gproperties__ = {
     46class CoreX11WindowModel(AutoPropGObjectMixin, gobject.GObject):
     47    """
     48        The utility superclass for all GTK2 / X11 window models,
     49        it wraps an X11 window (the "client-window").
     50        Defines the common properties and signals,
     51        sets up the composite helper so we get the damage events.
     52    """
     53    __common_properties__ = {
    27154        "client-window": (gobject.TYPE_PYOBJECT,
    272                           "gtk.gdk.Window representing the client toplevel", "",
    273                           gobject.PARAM_READABLE),
     55                "gtk.gdk.Window representing the client toplevel", "",
     56                gobject.PARAM_READABLE),
     57        "xid": (gobject.TYPE_INT,
     58                "X11 window id", "",
     59                -1, 65535, -1,
     60                gobject.PARAM_READABLE),
    27461        "geometry": (gobject.TYPE_PYOBJECT,
    275                      "current (border-corrected, relative to parent) coordinates (x, y, w, h) for the window", "",
    276                      gobject.PARAM_READABLE),
    277         "transient-for": (gobject.TYPE_PYOBJECT,
    278                           "Transient for (or None)", "",
    279                           gobject.PARAM_READABLE),
     62                "current (border-corrected, relative to parent) coordinates (x, y, w, h) for the window", "",
     63                gobject.PARAM_READABLE),
     64        "has-alpha": (gobject.TYPE_BOOLEAN,
     65                "Does the window use transparency", "",
     66                False,
     67                gobject.PARAM_READABLE),
     68        "client-machine": (gobject.TYPE_PYOBJECT,
     69                           "Host where client process is running", "",
     70                           gobject.PARAM_READABLE),
    28071        "pid": (gobject.TYPE_INT,
    28172                "PID of owning process", "",
    28273                -1, 65535, -1,
    28374                gobject.PARAM_READABLE),
    284         "opacity": (gobject.TYPE_INT64,
    285                 "Opacity", "",
    286                 -1, 0xffffffff, -1,
     75        "title": (gobject.TYPE_PYOBJECT,
     76                "Window title (unicode or None)", "",
    28777                gobject.PARAM_READABLE),
    288         "shape": (gobject.TYPE_PYOBJECT,
    289                           "Window XShape data", "",
    290                           gobject.PARAM_READABLE),
    291         "xid": (gobject.TYPE_INT,
    292                 "X11 window id", "",
    293                 -1, 65535, -1,
     78        "role" : (gobject.TYPE_PYOBJECT,
     79                "The window's role (ICCCM session management)", "",
    29480                gobject.PARAM_READABLE),
    295         "title": (gobject.TYPE_PYOBJECT,
    296                   "Window title (unicode or None)", "",
    297                   gobject.PARAM_READABLE),
    298         "group-leader": (gobject.TYPE_PYOBJECT,
    299                          "Window group leader as a pair: (xid, gdk window)", "",
    300                          gobject.PARAM_READABLE),
    301         "attention-requested": (gobject.TYPE_BOOLEAN,
    302                                 "Urgency hint from client, or us", "",
    303                                 False,
    304                                 gobject.PARAM_READWRITE),
    305         "can-focus": (gobject.TYPE_BOOLEAN,
    306                       "Does this window ever accept keyboard input?", "",
    307                       True,
    308                       gobject.PARAM_READWRITE),
    309         "has-alpha": (gobject.TYPE_BOOLEAN,
    310                        "Does the window use transparency", "",
    311                        False,
    312                        gobject.PARAM_READABLE),
    313         "bypass-compositor": (gobject.TYPE_INT,
    314                        "hint that the window would benefit from running uncomposited ", "",
    315                        0, 2, 0,
    316                        gobject.PARAM_READABLE),
    317         "fullscreen-monitors": (gobject.TYPE_PYOBJECT,
    318                          "List of 4 monitor indices indicating the top, bottom, left, and right edges of the window when the fullscreen state is enabled", "",
    319                          gobject.PARAM_READABLE),
    320         "fullscreen": (gobject.TYPE_BOOLEAN,
    321                        "Fullscreen-ness of window", "",
    322                        False,
    323                        gobject.PARAM_READWRITE),
    324         "focused": (gobject.TYPE_BOOLEAN,
    325                        "Is the window focused", "",
    326                        False,
    327                        gobject.PARAM_READWRITE),
    328         "maximized": (gobject.TYPE_BOOLEAN,
    329                        "Is the window maximized", "",
    330                        False,
    331                        gobject.PARAM_READWRITE),
    332         "above": (gobject.TYPE_BOOLEAN,
    333                        "Is the window on top of most windows", "",
    334                        False,
    335                        gobject.PARAM_READWRITE),
    336         "below": (gobject.TYPE_BOOLEAN,
    337                        "Is the window below most windows", "",
    338                        False,
    339                        gobject.PARAM_READWRITE),
    340         "shaded": (gobject.TYPE_BOOLEAN,
    341                        "Is the window shaded", "",
    342                        False,
    343                        gobject.PARAM_READWRITE),
    344         "skip-taskbar": (gobject.TYPE_BOOLEAN,
    345                        "Should the window be included on a taskbar", "",
    346                        False,
    347                        gobject.PARAM_READWRITE),
    348         "skip-pager": (gobject.TYPE_BOOLEAN,
    349                        "Should the window be included on a pager", "",
    350                        False,
    351                        gobject.PARAM_READWRITE),
    352         "sticky": (gobject.TYPE_BOOLEAN,
    353                        "Is the window's position fixed on the screen", "",
    354                        False,
    355                        gobject.PARAM_READWRITE),
    356         "strut": (gobject.TYPE_PYOBJECT,
    357                   "Struts requested by window, or None", "",
    358                   gobject.PARAM_READABLE),
    359         "workspace": (gobject.TYPE_UINT,
    360                 "The workspace this window is on", "",
    361                 0, 2**32-1, WORKSPACE_UNSET,
    362                 gobject.PARAM_READWRITE),
    363         "override-redirect": (gobject.TYPE_BOOLEAN,
    364                        "Is the window of type override-redirect", "",
    365                        False,
    366                        gobject.PARAM_READABLE),
    367         "tray": (gobject.TYPE_BOOLEAN,
    368                        "Is the window a system tray icon", "",
    369                        False,
    370                        gobject.PARAM_READABLE),
    371         "role" : (gobject.TYPE_PYOBJECT,
    372                           "The window's role (ICCCM session management)", "",
    373                           gobject.PARAM_READABLE),
    374         "modal": (gobject.TYPE_PYOBJECT,
    375                           "Modal (boolean)", "",
    376                           gobject.PARAM_READWRITE),
    377         "window-type": (gobject.TYPE_PYOBJECT,
    378                         "Window type",
    379                         "NB, most preferred comes first, then fallbacks",
    380                         gobject.PARAM_READABLE),
    381         }
    382     __gsignals__ = {
    383         "client-contents-changed": one_arg_signal,
    384         "raised"                : one_arg_signal,
    385         "unmanaged"             : one_arg_signal,
    386         "initiate-moveresize"   : one_arg_signal,
     81        "protocols": (gobject.TYPE_PYOBJECT,
     82                "Supported WM protocols", "",
     83                gobject.PARAM_READABLE),
     84        "command": (gobject.TYPE_PYOBJECT,
     85                "Command used to start or restart the client", "",
     86                gobject.PARAM_READABLE),
     87        "class-instance": (gobject.TYPE_PYOBJECT,
     88                           "Classic X 'class' and 'instance'", "",
     89                           gobject.PARAM_READABLE),
     90           }
    38791
    388         "grab"                  : one_arg_signal,
    389         "ungrab"                : one_arg_signal,
    390         }
    39192    __common_signals__ = {
    392         "bell"                      : one_arg_signal,   #out
    393         "xpra-xkb-event"            : one_arg_signal,
    394         "xpra-shape-event"          : one_arg_signal,
    395         "xpra-configure-event"      : one_arg_signal,
    396         "xpra-unmap-event"          : one_arg_signal,
    397         "xpra-client-message-event" : one_arg_signal,
    398         "xpra-property-notify-event": one_arg_signal,
    399         "xpra-focus-in-event"       : one_arg_signal,
    400         "xpra-focus-out-event"      : one_arg_signal,
     93        "xpra-property-notify-event"    : one_arg_signal,
     94        "client-contents-changed"       : one_arg_signal,
     95        "unmanaged"                     : one_arg_signal,
    40196        }
    40297
     98    _property_names         = ["xid", "has-alpha", "client-machine", "pid", "title", "role", "command", "class-instance"]
     99    _dynamic_property_names = ["title", "command"]
     100    _initial_x11_properties = ["_NET_WM_PID", "WM_CLIENT_MACHINE", "WM_NAME", "_NET_WM_NAME", "WM_PROTOCOLS", "WM_CLASS", "WM_WINDOW_ROLE"]
     101    _scrub_properties       = []
     102
    403103    def __init__(self, client_window):
    404104        log("new window %#x", client_window.xid)
    405         super(BaseWindowModel, self).__init__()
     105        super(CoreX11WindowModel, self).__init__()
    406106        self.client_window = client_window
    407107        self.client_window_saved_events = self.client_window.get_events()
    408108        self._managed = False
    409109        self._managed_handlers = []
    410110        self._setup_done = False
    411         self._input_field = True            # The WM_HINTS input field
    412111        self._geometry = None
     112        self._composite = None
    413113        self._damage_forward_handle = None
     114        self._kill_count = 0
    414115        self._internal_set_property("client-window", client_window)
    415         use_xshm = USE_XSHM and not self.is_tray()
    416         self._composite = CompositeHelper(self.client_window, False, use_xshm)
    417         if X11Window.displayHasXShape():
    418             X11Window.XShapeSelectInput(self.client_window.xid)
    419         self.property_names = ["pid", "transient-for", "fullscreen", "fullscreen-monitors", "bypass-compositor", "maximized", "window-type", "role", "group-leader",
    420                                "xid", "workspace", "has-alpha", "opacity", "strut", "shape"]
    421116
    422     def get_property_names(self):
    423         return self.property_names
     117    #########################################
     118    # Setup and teardown
     119    #########################################
    424120
    425     def get_dynamic_property_names(self):
    426         return ("title", "size-hints", "fullscreen", "fullscreen-monitors", "bypass-compositor", "maximized", "opacity", "workspace", "strut", "shape")
     121    def is_managed(self):
     122        return self._managed
    427123
    428124
    429     def managed_connect(self, detailed_signal, handler, *args):
    430         """ connects a signal handler and makes sure we will clean it up on unmanage() """
    431         handler_id = self.connect(detailed_signal, handler, *args)
    432         self._managed_handlers.append(handler_id)
    433         return handler_id
    434 
    435     def managed_disconnect(self):
    436         for handler_id in self._managed_handlers:
    437             self.disconnect(handler_id)
    438         self._managed_handlers = []
    439 
    440125    def call_setup(self):
     126        """
     127            Call this method to prepare the window:
     128            * makes sure it still exists (query its geometry)
     129            * setup composite redirection
     130            * calls setup
     131            The difficulty comes from X11 errors and synchronization:
     132            we want to catch errors and undo what we've done.
     133            The mix of GTK and pure-X11 calls is not helping.
     134        """
    441135        try:
    442136            with xsync:
    443137                self._geometry = X11Window.geometry_with_border(self.client_window.xid)
     
    449143        # reparent, so I'm not sure doing this here in the superclass,
    450144        # before we reparent, actually works... let's wait and see.
    451145        try:
     146            self._composite = CompositeHelper(self.client_window, False, USE_XSHM)
    452147            with xsync:
    453148                self._composite.setup()
    454149        except XError as e:
     
    478173
    479174    def setup(self):
    480175        # Start listening for important events.
    481         self.client_window.set_events(self.client_window_saved_events
    482                                       | gtk.gdk.STRUCTURE_MASK
    483                                       | gtk.gdk.PROPERTY_CHANGE_MASK
    484                                       | gtk.gdk.FOCUS_CHANGE_MASK)
    485         h = self._composite.connect("contents-changed", self._forward_contents_changed)
    486         self._damage_forward_handle = h
     176        self.client_window.set_events(self.client_window_saved_events | ADDMASK)
     177        self._damage_forward_handle = self._composite.connect("contents-changed", self._forward_contents_changed)
    487178
    488     def prop_get(self, key, ptype, ignore_errors=None, raise_xerrors=False):
    489         # Utility wrapper for prop_get on the client_window
    490         # also allows us to ignore property errors during setup_client
    491         if ignore_errors is None and (not self._setup_done or not self._managed):
    492             ignore_errors = True
    493         return prop_get(self.client_window, key, ptype, ignore_errors=bool(ignore_errors), raise_xerrors=raise_xerrors)
    494179
    495     def is_managed(self):
    496         return self._managed
    497 
    498     def is_shadow(self):
    499         return False
    500 
    501     def get_default_window_icon(self):
    502         return None
    503 
    504 
    505     def _forward_contents_changed(self, obj, event):
    506         if self._managed:
    507             self.emit("client-contents-changed", event)
    508 
    509     def acknowledge_changes(self):
    510         assert self._composite, "composite window destroyed outside the UI thread?"
    511         self._composite.acknowledge_changes()
    512 
    513     ################################
    514     # Property reading
    515     ################################
    516 
    517     def do_xpra_property_notify_event(self, event):
    518         assert event.window is self.client_window
    519         self._handle_property_change(str(event.atom))
    520 
    521     _property_handlers = {}
    522 
    523     def _handle_property_change(self, name):
    524         log("Property changed on %#x: %s", self.client_window.xid, name)
    525         if name in PROPERTIES_DEBUG:
    526             log.info("%s=%s", name, self.prop_get(name, PROPERTIES_DEBUG[name], True, False))
    527         if name in PROPERTIES_IGNORED:
    528             return
    529         self._call_property_handler(name)
    530 
    531     def _call_property_handler(self, name):
    532         if name in self._property_handlers:
    533             self._property_handlers[name](self)
    534 
    535     def do_xpra_configure_event(self, event):
    536         if self.client_window is None or not self._managed:
    537             return
    538         #shouldn't the border width always be 0?
    539         geom = (event.x, event.y, event.width, event.height, event.border_width)
    540         log("BaseWindowModel.do_xpra_configure_event(%s) client_window=%#x, old geometry=%s, new geometry=%s", event, self.client_window.xid, self._geometry, geom)
    541         if geom!=self._geometry:
    542             self._geometry = geom
    543             #X11Window.MoveResizeWindow(self.client_window.xid, )
    544             self.notify("geometry")
    545 
    546     def do_get_property_geometry(self, pspec):
    547         if self._geometry is None:
    548             with xsync:
    549                 xwin = self.client_window.xid
    550                 self._geometry = X11Window.geometry_with_border(xwin)
    551                 log("BaseWindowModel.do_get_property_geometry() synced update: geometry(%#x)=%s", xwin, self._geometry)
    552         x, y, w, h, b = self._geometry
    553         return (x, y, w + 2*b, h + 2*b)
    554 
    555     def get_position(self):
    556         return self.do_get_property_geometry(None)[:2]
    557 
    558180    def unmanage(self, exiting=False):
    559181        if self._managed:
    560182            self.emit("unmanaged", exiting)
     
    572194                self._damage_forward_handle = None
    573195            self._composite.destroy()
    574196            self._composite = None
     197            self._scrub_properties()
    575198
    576     def _read_initial_properties(self):
    577         transient_for = self.prop_get("WM_TRANSIENT_FOR", "window")
    578         # May be None
    579         self._internal_set_property("transient-for", transient_for)
    580199
    581         window_types = self.prop_get("_NET_WM_WINDOW_TYPE", ["atom"])
    582         if not window_types:
    583             window_type = self._guess_window_type(transient_for)
    584             window_types = [gtk.gdk.atom_intern(window_type)]
    585         #normalize them (hide _NET_WM_WINDOW_TYPE prefix):
    586         window_types = [str(wt).replace("_NET_WM_WINDOW_TYPE_", "").replace("_NET_WM_TYPE_", "") for wt in window_types]
    587         self._internal_set_property("window-type", window_types)
    588         self._internal_set_property("xid", self.client_window.xid)
    589         self._internal_set_property("pid", self.prop_get("_NET_WM_PID", "u32") or -1)
    590         self._internal_set_property("role", self.prop_get("WM_WINDOW_ROLE", "latin1"))
    591         for mutable in ["WM_NAME", "_NET_WM_NAME", "_NET_WM_WINDOW_OPACITY", "_NET_WM_DESKTOP",
    592                         "_NET_WM_BYPASS_COMPOSITOR", "_NET_WM_FULLSCREEN_MONITORS",
    593                         "_NET_WM_STRUT", "_NET_WM_STRUT_PARTIAL"]:
    594             self._call_property_handler(mutable)
    595 
    596     def _read_initial_X11_properties(self):
    597         self._internal_set_property("has-alpha", X11Window.get_depth(self.client_window.xid)==32)
    598         self._internal_set_property("shape", self._read_xshape())
    599 
    600     def _read_xshape(self):
    601         xid = self.client_window.xid
    602         extents = X11Window.XShapeQueryExtents(xid)
    603         if not extents:
    604             shapelog("read_shape for window %#x: no extents", xid)
    605             return {}
    606         v = {}
    607         #w,h = X11Window.getGeometry(xid)[2:4]
    608         bextents = extents[0]
    609         cextents = extents[1]
    610         if bextents[0]==0 and cextents[0]==0:
    611             shapelog("read_shape for window %#x: none enabled", xid)
    612             return {}
    613         v["Bounding.extents"] = bextents
    614         v["Clip.extents"] = cextents
    615         for kind in SHAPE_KIND.keys():
    616             kind_name = SHAPE_KIND[kind]
    617             rectangles = X11Window.XShapeGetRectangles(xid, kind)
    618             v[kind_name+".rectangles"] = rectangles
    619         shapelog("_read_shape()=%s", v)
    620         return v
    621 
    622 
    623     def _handle_workspace_change(self):
    624         workspace = self.prop_get("_NET_WM_DESKTOP", "u32", True)
    625         if workspace is None:
    626             workspace = WORKSPACE_UNSET
    627         workspacelog("_NET_WM_DESKTOP=%s for window %#x", workspacestr(workspace), self.client_window.xid)
    628         self._internal_set_property("workspace", workspace)
    629     _property_handlers["_NET_WM_DESKTOP"] = _handle_workspace_change
    630 
    631     def move_to_workspace(self, workspace):
    632         #we send a message to ourselves, we could also just update the property
    633         current = self.get_property("workspace")
    634         if current==workspace:
    635             workspacelog("move_to_workspace(%s) unchanged", workspacestr(workspace))
     200    def _scrub_properties(self):
     201        if not self._scrub_properties:
    636202            return
    637         workspacelog("move_to_workspace(%s) current=%s", workspacestr(workspace), workspacestr(current))
    638203        with xswallow:
    639             if workspace==WORKSPACE_UNSET:
    640                 workspacelog("removing _NET_WM_DESKTOP property from window %#x", self.client_window.xid)
    641                 X11Window.XDeleteProperty(self.client_window.xid, "_NET_WM_DESKTOP")
    642             else:
    643                 workspacelog("setting _NET_WM_DESKTOP=%s on window %#x", workspacestr(workspace), self.client_window.xid)
    644                 prop_set(self.client_window, "_NET_WM_DESKTOP", "u32", workspace)
     204            for prop in self._scrub_properties:
     205                X11Window.XDeleteProperty(self.client_window.xid, prop)
    645206
     207    def _read_initial_properties(self):
     208        #immutable ones:
     209        xid = self.client_window.xid
     210        has_alpha = X11Window.get_depth(self.client_window.xid)==32
     211        metalog("xid=%#x, has-alpha=%s", xid, has_alpha)
     212        self._updateprop("xid", xid)
     213        self._updateprop("has-alpha", has_alpha)
    646214
    647     def _handle_fullscreen_monitors_change(self):
    648         fsm = self.prop_get("_NET_WM_FULLSCREEN_MONITORS", ["u32"], True)
    649         self._internal_set_property("fullscreen-monitors", fsm)
    650         log("fullscreen-monitors=%s", fsm)
    651     _property_handlers["_NET_WM_FULLSCREEN_MONITORS"] = _handle_fullscreen_monitors_change
     215    def _read_initial_X11_properties(self):
     216        #note: some of those are technically mutable,
     217        #but we don't export them as "dynamic" properties, so this won't be propagated
     218        for mutable in self._initial_x11_properties:
     219            self._call_property_handler(mutable)
    652220
     221    #########################################
     222    # Composite
     223    #########################################
    653224
    654     def _handle_bypass_compositor_change(self):
    655         bypass = self.prop_get("_NET_WM_BYPASS_COMPOSITOR", "u32", True) or 0
    656         self._internal_set_property("bypass-compositor", bypass)
    657         log("bypass-compositor=%s", bypass)
    658     _property_handlers["_NET_WM_BYPASS_COMPOSITOR"] = _handle_bypass_compositor_change
     225    def _forward_contents_changed(self, obj, event):
     226        if self._managed:
     227            self.emit("client-contents-changed", event)
    659228
     229    def acknowledge_changes(self):
     230        c = self._composite
     231        assert c, "composite window destroyed outside the UI thread?"
     232        c.acknowledge_changes()
    660233
    661     def _handle_wm_strut(self):
    662         partial = self.prop_get("_NET_WM_STRUT_PARTIAL", "strut-partial")
    663         if partial is not None:
    664             self._internal_set_property("strut", partial)
    665             return
    666         full = self.prop_get("_NET_WM_STRUT", "strut")
    667         # Might be None:
    668         self._internal_set_property("strut", full)
    669 
    670     _property_handlers["_NET_WM_STRUT"] = _handle_wm_strut
    671     _property_handlers["_NET_WM_STRUT_PARTIAL"] = _handle_wm_strut
    672 
    673 
    674     def _handle_opacity_change(self):
    675         opacity = self.prop_get("_NET_WM_WINDOW_OPACITY", "u32", True) or -1
    676         self._internal_set_property("opacity", opacity)
    677     _property_handlers["_NET_WM_WINDOW_OPACITY"] = _handle_opacity_change
    678 
    679     def _handle_title_change(self):
    680         name = self.prop_get("_NET_WM_NAME", "utf8", True)
    681         if name is None:
    682             name = self.prop_get("WM_NAME", "latin1", True)
    683         self._internal_set_property("title", sanestr(name))
    684 
    685     _property_handlers["WM_NAME"] = _handle_title_change
    686     _property_handlers["_NET_WM_NAME"] = _handle_title_change
    687 
    688     def _handle_wm_hints(self):
    689         with xswallow:
    690             wm_hints = X11Window.getWMHints(self.client_window.xid)
    691         if wm_hints is None:
    692             return
    693         # GdkWindow or None
    694         group_leader = None
    695         if "window_group" in wm_hints:
    696             xid = wm_hints.get("window_group")
    697             try:
    698                 group_leader = xid, get_pywindow(xid)
    699             except:
    700                 group_leader = xid, None
    701         self._internal_set_property("group-leader", group_leader)
    702 
    703         if "urgency" in wm_hints:
    704             self.set_property("attention-requested", True)
    705 
    706         _input = wm_hints.get("input")
    707         log("wm_hints.input = %s", _input)
    708         #we only set this value once:
    709         #(input_field always starts as True, and we then set it to an int)
    710         if self._input_field is True and _input is not None:
    711             #keep the value as an int to differentiate from the start value:
    712             self._input_field = int(_input)
    713             if bool(self._input_field):
    714                 self.notify("can-focus")
    715 
    716     _property_handlers["WM_HINTS"] = _handle_wm_hints
    717 
    718     def _guess_window_type(self, transient_for):
    719         if transient_for is not None:
    720             # EWMH says that even if it's transient-for, we MUST check to
    721             # see if it's override-redirect (and if so treat as NORMAL).
    722             # But we wouldn't be here if this was override-redirect.
    723             # (OverrideRedirectWindowModel overrides this method)
    724             return "_NET_WM_TYPE_DIALOG"
    725         return "_NET_WM_WINDOW_TYPE_NORMAL"
    726 
    727     def is_tray(self):
    728         return False
    729 
    730234    def uses_XShm(self):
    731         return self._composite and self._composite.get_property("shm-handle") is not None
     235        c = self._composite
     236        return c and c.get_property("shm-handle") is not None
    732237
    733     def has_alpha(self):
    734         return self.get_property("has-alpha")
    735 
    736238    def get_image(self, x, y, width, height, logger=log.debug):
    737239        handle = self._composite.get_property("contents-handle")
    738240        if handle is None:
     
    771273            return None
    772274
    773275
    774     def do_xpra_shape_event(self, event):
    775         shapelog("shape event: %s, kind=%s", event, SHAPE_KIND.get(event.kind, event.kind))
    776         cur_shape = self.get_property("shape")
    777         if cur_shape and cur_shape.get("serial", 0)>=event.serial:
    778             shapelog("same or older xshape serial no: %#x", event.serial)
    779             return
    780         #remove serial before comparing dicts:
    781         try:
    782             cur_shape["serial"]
    783         except:
    784             pass
    785         #read new xshape:
    786         v = self._read_xshape()
    787         if cur_shape==v:
    788             shapelog("xshape unchanged")
    789             return
    790         v["serial"] = int(event.serial)
    791         shapelog("xshape updated with serial %#x", event.serial)
    792         self._internal_set_property("shape", v)
    793276
     277    #########################################
     278    # Connect to signals in a "managed" way
     279    #########################################
    794280
    795     def do_xpra_xkb_event(self, event):
    796         log("WindowModel.do_xpra_xkb_event(%r)" % event)
    797         if event.type!="bell":
    798             log.error("WindowModel.do_xpra_xkb_event(%r) unknown event type: %s" % (event, event.type))
    799             return
    800         event.window_model = self
    801         self.emit("bell", event)
     281    def managed_connect(self, detailed_signal, handler, *args):
     282        """ connects a signal handler and makes sure we will clean it up on unmanage() """
     283        handler_id = self.connect(detailed_signal, handler, *args)
     284        self._managed_handlers.append(handler_id)
     285        return handler_id
    802286
    803     def do_xpra_client_message_event(self, event):
    804         log("do_xpra_client_message_event(%s)", event)
    805         if not event.data or len(event.data)!=5:
    806             log.warn("invalid event data: %s", event.data)
    807             return
    808         if not self.process_client_message_event(event):
    809             log.warn("do_xpra_client_message_event(%s) not handled", event)
     287    def managed_disconnect(self):
     288        for handler_id in self._managed_handlers:
     289            self.disconnect(handler_id)
     290        self._managed_handlers = []
    810291
    811     def process_client_message_event(self, event):
    812         #most messages are only handled in WindowModel,
    813         #OR windows and trays should not be receiving any other message
    814         if event.message_type=="_NET_CLOSE_WINDOW":
    815             log.info("_NET_CLOSE_WINDOW received by %s", self)
    816             self.request_close()
    817             return True
    818         return False
    819292
     293    #########################################
     294    # Properties we choose to expose
     295    #########################################
    820296
    821     def set_active(self):
    822         prop_set(self.client_window.get_screen().get_root_window(), "_NET_ACTIVE_WINDOW", "u32", self.client_window.xid)
     297    def get_property_names(self):
     298        """ The properties that should be exposed to clients """
     299        return self._property_names
    823300
     301    def get_dynamic_property_names(self):
     302        """ The properties that may change over time """
     303        return self._dynamic_property_names
    824304
    825     def do_xpra_focus_in_event(self, event):
    826         grablog("focus_in_event(%s) mode=%s, detail=%s",
    827             event, GRAB_CONSTANTS.get(event.mode), DETAIL_CONSTANTS.get(event.detail, event.detail))
    828         if event.mode==NotifyNormal and event.detail==NotifyNonlinearVirtual:
    829             self.emit("raised", event)
     305    def get(self, name, default_value=None):
     306        """ Allows us the avoid defining all the attributes we may ever query,
     307            returns the default value if the property does not exist.
     308        """
     309        if name in self._property_names:
     310            return self.get_property(name)
     311        return default_value
     312
     313    def _updateprop(self, name, value):
     314        """ Updates the property and fires notify(),
     315            but only if the value has changed
     316            and if the window is setup and still managed.
     317            Can only be used for AutoPropGObjectMixin properties.
     318        """
     319        cur = self._gproperties.get(name, None)
     320        if name not in self._gproperties or cur!=value:
     321            metalog("_updateprop(%s, %s) current value=%s (setup done=%s, managed=%s)", name, value, cur, self._setup_done, self._managed)
     322            self._gproperties[name] = value
     323            if self._setup_done and self._managed:
     324                self.notify(name)
    830325        else:
    831             self.may_emit_grab(event)
     326            metalog("_updateprop(%s, %s) unchanged", name, value)
    832327
    833     def do_xpra_focus_out_event(self, event):
    834         grablog("focus_out_event(%s) mode=%s, detail=%s",
    835             event, GRAB_CONSTANTS.get(event.mode), DETAIL_CONSTANTS.get(event.detail, event.detail))
    836         self.may_emit_grab(event)
    837 
    838     def may_emit_grab(self, event):
    839         if event.mode==NotifyGrab:
    840             grablog("emitting grab on %s", self)
    841             self.emit("grab", event)
    842         if event.mode==NotifyUngrab:
    843             grablog("emitting ungrab on %s", self)
    844             self.emit("ungrab", event)
    845 
    846 
    847 gobject.type_register(BaseWindowModel)
    848 
    849 
    850 # FIXME: EWMH says that O-R windows should set various properties just like
    851 # ordinary managed windows; so some of that code should get pushed up into the
    852 # superclass sooner or later.  When someone cares, presumably.
    853 class OverrideRedirectWindowModel(BaseWindowModel):
    854     __gsignals__ = BaseWindowModel.__common_signals__.copy()
    855 
    856     def __init__(self, client_window):
    857         super(OverrideRedirectWindowModel, self).__init__(client_window)
    858         self.property_names.append("override-redirect")
    859 
    860     def setup(self):
    861         self._read_initial_properties()
    862         BaseWindowModel.setup(self)
    863         # So now if the window becomes unmapped in the future then we will
    864         # notice... but it might be unmapped already, and any event
    865         # already generated, and our request for that event is too late!
    866         # So double check now, *after* putting in our request:
    867         if not X11Window.is_mapped(self.client_window.xid):
    868             raise Unmanageable("window already unmapped")
    869         ch = self._composite.get_property("contents-handle")
    870         if ch is None:
    871             raise Unmanageable("failed to get damage handle")
    872 
    873     def _read_initial_properties(self):
    874         BaseWindowModel._read_initial_properties(self)
    875         self._internal_set_property("override-redirect", True)
    876 
    877     def _guess_window_type(self, transient_for):
    878         return "_NET_WM_WINDOW_TYPE_NORMAL"
    879 
    880     def do_xpra_unmap_event(self, event):
    881         self.unmanage()
    882 
    883     def get_dimensions(self):
    884         ww, wh = self._geometry[2:4]
    885         return ww, wh
    886 
     328    #temporary? access methods:
    887329    def is_OR(self):
    888         return  True
     330        """ Is this an override-redirect window? """
     331        return self.get("override-redirect", False)
    889332
    890     def raise_window(self):
    891         self.client_window.raise_()
     333    def is_tray(self):
     334        """ Is this a tray window? """
     335        return self.get("tray", False)
    892336
    893     def __repr__(self):
    894         return "OverrideRedirectWindowModel(%#x)" % self.client_window.xid
     337    def is_shadow(self):
     338        """ Is this the shadow of a real window? """
     339        return False
    895340
    896 
    897 gobject.type_register(OverrideRedirectWindowModel)
    898 
    899 
    900 class SystemTrayWindowModel(OverrideRedirectWindowModel):
    901 
    902     def __init__(self, client_window):
    903         OverrideRedirectWindowModel.__init__(self, client_window)
    904         self.property_names = ["pid", "role", "xid", "has-alpha", "tray", "title"]
    905 
    906     def is_tray(self):
    907         return  True
    908 
    909341    def has_alpha(self):
    910         return  True
     342        """ Does the pixel data have an alpha channel? """
     343        return self.get("has-alpha", False)
    911344
    912     def _read_initial_properties(self):
    913         BaseWindowModel._read_initial_properties(self)
    914         self._internal_set_property("tray", True)
    915         self._internal_set_property("has-alpha", True)
    916345
    917     def move_resize(self, x, y, width, height):
    918         #Used by clients to tell us where the tray is located on screen
    919         log("SystemTrayWindowModel.move_resize(%s, %s, %s, %s)", x, y, width, height)
    920         self.client_window.move_resize(x, y, width, height)
    921         border = self._geometry[4]
    922         self._geometry = (x, y, width, height, border)
     346    #########################################
     347    # X11 properties
     348    #########################################
    923349
    924     def __repr__(self):
    925         return "SystemTrayWindowModel(%#x)" % self.client_window.xid
     350    def prop_get(self, key, ptype, ignore_errors=None, raise_xerrors=False):
     351        # Utility wrapper for prop_get on the client_window
     352        # also allows us to ignore property errors during setup_client
     353        if ignore_errors is None and (not self._setup_done or not self._managed):
     354            ignore_errors = True
     355        return prop_get(self.client_window, key, ptype, ignore_errors=bool(ignore_errors), raise_xerrors=raise_xerrors)
    926356
    927357
    928 class WindowModel(BaseWindowModel):
    929     """This represents a managed client window.  It allows one to produce
    930     widgets that view that client window in various ways."""
    931 
    932     _NET_WM_ALLOWED_ACTIONS = [
    933         "_NET_WM_ACTION_CLOSE",
    934         "_NET_WM_ACTION_MOVE",
    935         "_NET_WM_ACTION_RESIZE",
    936         "_NET_WM_ACTION_FULLSCREEN",
    937         "_NET_WM_ACTION_MINIMIZE",
    938         "_NET_WM_ACTION_SHADE",
    939         "_NET_WM_ACTION_STICK",
    940         "_NET_WM_ACTION_MAXIMIZE_HORZ",
    941         "_NET_WM_ACTION_MAXIMIZE_VERT",
    942         "_NET_WM_ACTION_CHANGE_DESKTOP",
    943         "_NET_WM_ACTION_ABOVE",
    944         "_NET_WM_ACTION_BELOW",
    945         ]
    946 
    947     __gproperties__ = {
    948         # Interesting properties of the client window, that will be
    949         # automatically kept up to date:
    950         "actual-size": (gobject.TYPE_PYOBJECT,
    951                         "Size of client window (actual (width,height))", "",
    952                         gobject.PARAM_READABLE),
    953         "user-friendly-size": (gobject.TYPE_PYOBJECT,
    954                                "Description of client window size for user", "",
    955                                gobject.PARAM_READABLE),
    956         "requested-position": (gobject.TYPE_PYOBJECT,
    957                                "Client-requested position on screen", "",
    958                                gobject.PARAM_READABLE),
    959         "requested-size": (gobject.TYPE_PYOBJECT,
    960                            "Client-requested size on screen", "",
    961                            gobject.PARAM_READABLE),
    962         "size-hints": (gobject.TYPE_PYOBJECT,
    963                        "Client hints on constraining its size", "",
    964                        gobject.PARAM_READABLE),
    965         "class-instance": (gobject.TYPE_PYOBJECT,
    966                            "Classic X 'class' and 'instance'", "",
    967                            gobject.PARAM_READABLE),
    968         "protocols": (gobject.TYPE_PYOBJECT,
    969                       "Supported WM protocols", "",
    970                       gobject.PARAM_READABLE),
    971         "frame": (gobject.TYPE_PYOBJECT,
    972                       "Size of the window frame", "",
    973                       gobject.PARAM_READWRITE),
    974         "client-machine": (gobject.TYPE_PYOBJECT,
    975                            "Host where client process is running", "",
    976                            gobject.PARAM_READABLE),
    977         "command": (gobject.TYPE_PYOBJECT,
    978                            "Command used to start or restart the client", "",
    979                            gobject.PARAM_READABLE),
    980         # Toggling this property does not actually make the window iconified,
    981         # i.e. make it appear or disappear from the screen -- it merely
    982         # updates the various window manager properties that inform the world
    983         # whether or not the window is iconified.
    984         "iconic": (gobject.TYPE_BOOLEAN,
    985                    "ICCCM 'iconic' state -- any sort of 'not on desktop'.", "",
    986                    False,
    987                    gobject.PARAM_READWRITE),
    988         "state": (gobject.TYPE_PYOBJECT,
    989                   "State, as per _NET_WM_STATE", "",
    990                   gobject.PARAM_READABLE),
    991         "icon-title": (gobject.TYPE_PYOBJECT,
    992                        "Icon title (unicode or None)", "",
    993                        gobject.PARAM_READABLE),
    994         "icon": (gobject.TYPE_PYOBJECT,
    995                  "Icon (local Cairo surface)", "",
    996                  gobject.PARAM_READABLE),
    997         "icon-pixmap": (gobject.TYPE_PYOBJECT,
    998                         "Icon (server Pixmap)", "",
    999                         gobject.PARAM_READABLE),
    1000 
    1001         "owner": (gobject.TYPE_PYOBJECT,
    1002                   "Owner", "",
    1003                   gobject.PARAM_READABLE),
    1004         "decorations": (gobject.TYPE_BOOLEAN,
    1005                        "Should the window decorations be shown", "",
    1006                        True,
    1007                        gobject.PARAM_READABLE),
    1008         }
    1009     __gsignals__ = BaseWindowModel.__common_signals__.copy()
    1010     __gsignals__.update({
    1011         "ownership-election"            : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_PYOBJECT, (), non_none_list_accumulator),
    1012         "child-map-request-event"       : one_arg_signal,
    1013         "child-configure-request-event" : one_arg_signal,
    1014         "xpra-destroy-event"            : one_arg_signal,
    1015         })
    1016 
    1017     def __init__(self, parking_window, client_window):
    1018         """Register a new client window with the WM.
    1019 
    1020         Raises an Unmanageable exception if this window should not be
    1021         managed, for whatever reason.  ATM, this mostly means that the window
    1022         died somehow before we could do anything with it."""
    1023 
    1024         super(WindowModel, self).__init__(client_window)
    1025         self.parking_window = parking_window
    1026         self.corral_window = None
    1027         self.in_save_set = False
    1028         self.client_reparented = False
    1029         self.last_unmap_serial = 0
    1030         self.kill_count = 0
    1031 
    1032         self.connect("notify::iconic", self._handle_iconic_update)
    1033 
    1034         self.property_names += ["title", "icon-title", "size-hints", "class-instance", "icon", "client-machine", "command",
    1035                                 "modal", "decorations",
    1036                                 "above", "below", "shaded", "sticky", "skip-taskbar", "skip-pager"]
    1037         self.call_setup()
    1038 
    1039     def setup(self):
    1040         BaseWindowModel.setup(self)
    1041 
    1042         x, y, w, h, _ = self.client_window.get_geometry()
    1043         # We enable PROPERTY_CHANGE_MASK so that we can call
    1044         # x11_get_server_time on this window.
    1045         self.corral_window = gtk.gdk.Window(self.parking_window,
    1046                                             x = x, y = y, width =w, height= h,
    1047                                             window_type=gtk.gdk.WINDOW_CHILD,
    1048                                             wclass=gtk.gdk.INPUT_OUTPUT,
    1049                                             event_mask=gtk.gdk.PROPERTY_CHANGE_MASK,
    1050                                             title = "CorralWindow-%#x" % self.client_window.xid)
    1051         log("setup() corral_window=%#x", self.corral_window.xid)
    1052         prop_set(self.corral_window, "_NET_WM_NAME", "utf8", u"Xpra-CorralWindow-%#x" % self.client_window.xid)
    1053         X11Window.substructureRedirect(self.corral_window.xid)
    1054         add_event_receiver(self.corral_window, self)
    1055 
    1056         # The child might already be mapped, in case we inherited it from
    1057         # a previous window manager.  If so, we unmap it now, and save the
    1058         # serial number of the request -- this way, when we get an
    1059         # UnmapNotify later, we'll know that it's just from us unmapping
    1060         # the window, not from the client withdrawing the window.
    1061         if X11Window.is_mapped(self.client_window.xid):
    1062             log("hiding inherited window")
    1063             self.last_unmap_serial = X11Window.Unmap(self.client_window.xid)
    1064 
    1065         # Process properties
    1066         self._read_initial_properties()
    1067         self._write_initial_properties_and_setup()
    1068 
    1069         log("setup() adding to save set")
    1070         X11Window.XAddToSaveSet(self.client_window.xid)
    1071         self.in_save_set = True
    1072 
    1073         log("setup() reparenting")
    1074         X11Window.Reparent(self.client_window.xid, self.corral_window.xid, 0, 0)
    1075         self.client_reparented = True
    1076 
    1077         log("setup() geometry")
    1078         w,h = X11Window.getGeometry(self.client_window.xid)[2:4]
    1079         hints = self.get_property("size-hints")
    1080         log("setup() hints=%s size=%ix%i", hints, w, h)
    1081         self._sanitize_size_hints(hints)
    1082         nw, nh = calc_constrained_size(w, h, hints)[:2]
    1083         if nw>=MAX_WINDOW_SIZE or nh>=MAX_WINDOW_SIZE:
    1084             #we can't handle windows that big!
    1085             raise Unmanageable("window constrained size is too large: %sx%s (from client geometry: %s,%s with size hints=%s)" % (nw, nh, w, h, hints))
    1086         log("setup() resizing windows to %sx%s", nw, nh)
    1087         self.corral_window.resize(nw, nh)
    1088         self.client_window.resize(nw, nh)
    1089         self.client_window.show_unraised()
    1090         #this is here to trigger X11 errors if any are pending
    1091         #or if the window is deleted already:
    1092         self.client_window.get_geometry()
    1093         self._internal_set_property("actual-size", (nw, nh))
    1094 
    1095     def get_dynamic_property_names(self):
    1096         return list(BaseWindowModel.get_dynamic_property_names(self))+["icon", "icon-title", "size-hints", "iconic", "decorations", "modal",
    1097                                                                        "above", "below", "shaded", "sticky", "skip-taskbar", "skip-pager"]
    1098 
    1099 
    1100     def is_OR(self):
    1101         return  False
    1102 
    1103     def raise_window(self):
    1104         self.corral_window.raise_()
    1105 
    1106     def get_dimensions(self):
    1107         return  self.get_property("actual-size")
    1108 
    1109 
    1110     def process_client_message_event(self, event):
    1111         if BaseWindowModel.process_client_message_event(self, event):
    1112             #already handled
    1113             return True
    1114         # FIXME
    1115         # Need to listen for:
    1116         #   _NET_CURRENT_DESKTOP
    1117         #   _NET_WM_PING responses
    1118         # and maybe:
    1119         #   _NET_RESTACK_WINDOW
    1120         #   _NET_WM_STATE (more fully)
    1121         def update_wm_state(prop):
    1122             current = self.get_property(prop)
    1123             mode = event.data[0]
    1124             if mode==_NET_WM_STATE_ADD:
    1125                 v = True
    1126             elif mode==_NET_WM_STATE_REMOVE:
    1127                 v = False
    1128             elif mode==_NET_WM_STATE_TOGGLE:
    1129                 v = not bool(current)
    1130             else:
    1131                 log.warn("invalid mode for _NET_WM_STATE: %s", mode)
    1132                 return
    1133             log("do_xpra_client_message_event(%s) window %s=%s after %s (current state=%s)", event, prop, v, STATE_STRING.get(mode, mode), current)
    1134             if v!=current:
    1135                 self.set_property(prop, v)
    1136 
    1137         if event.message_type=="_NET_WM_STATE":
    1138             atom1 = get_pyatom(event.window, event.data[1])
    1139             log("_NET_WM_STATE: %s", atom1)
    1140             if atom1=="_NET_WM_STATE_FULLSCREEN":
    1141                 update_wm_state("fullscreen")
    1142             elif atom1=="_NET_WM_STATE_ABOVE":
    1143                 update_wm_state("above")
    1144             elif atom1=="_NET_WM_STATE_BELOW":
    1145                 update_wm_state("below")
    1146             elif atom1=="_NET_WM_STATE_SHADED":
    1147                 update_wm_state("shaded")
    1148             elif atom1=="_NET_WM_STATE_STICKY":
    1149                 update_wm_state("sticky")
    1150             elif atom1=="_NET_WM_STATE_SKIP_TASKBAR":
    1151                 update_wm_state("skip-taskbar")
    1152             elif atom1=="_NET_WM_STATE_SKIP_PAGER":
    1153                 update_wm_state("skip-pager")
    1154                 get_pyatom(event.window, event.data[2])
    1155             elif atom1 in ("_NET_WM_STATE_MAXIMIZED_VERT", "_NET_WM_STATE_MAXIMIZED_HORZ"):
    1156                 atom2 = get_pyatom(event.window, event.data[2])
    1157                 #we only have one state for both, so we require both to be set:
    1158                 if atom1!=atom2 and atom2 in ("_NET_WM_STATE_MAXIMIZED_VERT", "_NET_WM_STATE_MAXIMIZED_HORZ"):
    1159                     update_wm_state("maximized")
    1160             elif atom1=="_NET_WM_STATE_HIDDEN":
    1161                 log("ignoring 'HIDDEN' _NET_WM_STATE: %s", event)
    1162                 #we don't honour those because they make little sense, see:
    1163                 #https://mail.gnome.org/archives/wm-spec-list/2005-May/msg00004.html
    1164                 pass
    1165             elif atom1=="_NET_WM_STATE_MODAL":
    1166                 update_wm_state("modal")
    1167             else:
    1168                 log.info("do_xpra_client_message_event(%s) unhandled atom=%s", event, atom1)
    1169             return True
    1170         elif event.message_type=="WM_CHANGE_STATE":
    1171             log("WM_CHANGE_STATE: %s", event.data[0])
    1172             if event.data[0]==IconicState and event.serial>self.last_unmap_serial:
    1173                 self._internal_set_property("iconic", True)
    1174             return True
    1175         elif event.message_type=="_NET_WM_MOVERESIZE":
    1176             log("_NET_WM_MOVERESIZE: %s", event)
    1177             self.emit("initiate-moveresize", event)
    1178             return True
    1179         elif event.message_type=="_NET_ACTIVE_WINDOW" and event.data[0] in (0, 1):
    1180             log("_NET_ACTIVE_WINDOW: %s", event)
    1181             self.set_active()
    1182             self.emit("raised", event)
    1183             return True
    1184         elif event.message_type=="_NET_WM_DESKTOP":
    1185             workspace = int(event.data[0])
    1186             #query the workspace count on the root window
    1187             #since we cannot access Wm from here..
    1188             root = self.client_window.get_screen().get_root_window()
    1189             ndesktops = prop_get(root, "_NET_NUMBER_OF_DESKTOPS", "u32", ignore_errors=True)
    1190             workspacelog("received _NET_WM_DESKTOP: workspace=%s, number of desktops=%s", workspacestr(workspace), ndesktops)
    1191             if ndesktops>0 and (workspace in (WORKSPACE_UNSET, WORKSPACE_ALL) or (workspace>=0 and workspace<ndesktops)):
    1192                 self.move_to_workspace(workspace)
    1193             else:
    1194                 workspacelog.warn("invalid _NET_WM_DESKTOP request: workspace=%s, number of desktops=%s", workspacestr(workspace), ndesktops)
    1195             return True
    1196         elif event.message_type=="_NET_WM_FULLSCREEN_MONITORS":
    1197             log("_NET_WM_FULLSCREEN_MONITORS: %s", event)
    1198             #TODO: we should validate the indexes instead of copying them blindly!
    1199             m1, m2, m3, m4 = event.data[0], event.data[1], event.data[2], event.data[3]
    1200             N = 16      #FIXME: arbitrary limit
    1201             if m1<0 or m1>=N or m2<0 or m2>=N or m3<0 or m3>=N or m4<0 or m4>=N:
    1202                 log.warn("invalid list of _NET_WM_FULLSCREEN_MONITORS - ignored")
    1203                 return
    1204             monitors = [m1, m2, m3, m4]
    1205             log("_NET_WM_FULLSCREEN_MONITORS: monitors=%s", monitors)
    1206             prop_set(self.client_window, "_NET_WM_FULLSCREEN_MONITORS", ["u32"], monitors)
    1207             return True
    1208         elif event.message_type=="_NET_MOVERESIZE_WINDOW":
    1209             #TODO: honour gravity, show source indication
    1210             geom = self.corral_window.get_geometry()
    1211             x, y, w, h, _ = geom
    1212             if event.data[0] & 0x100:
    1213                 x = event.data[1]
    1214             if event.data[0] & 0x200:
    1215                 y = event.data[2]
    1216             if event.data[0] & 0x400:
    1217                 w = event.data[3]
    1218             if event.data[0] & 0x800:
    1219                 h = event.data[4]
    1220             #honour hints:
    1221             hints = self.get_property("size-hints")
    1222             w, h, _, _ = calc_constrained_size(w, h, hints)
    1223             log("_NET_MOVERESIZE_WINDOW on %s (data=%s, current geometry=%s, new geometry=%s)", self, event.data, geom, (x,y,w,h))
    1224             with xswallow:
    1225                 X11Window.configureAndNotify(self.client_window.xid, x, y, w, h)
    1226             return True
    1227         elif event.message_type=="_NET_REQUEST_FRAME_EXTENTS":
    1228             log("_NET_REQUEST_FRAME_EXTENTS")
    1229             self._handle_frame_changed()
    1230             return True
    1231         #not handled:
    1232         return False
    1233 
    1234 
    1235358    def do_xpra_property_notify_event(self, event):
    1236         if event.delivered_to is self.corral_window:
    1237             return
    1238         BaseWindowModel.do_xpra_property_notify_event(self, event)
    1239 
    1240     def do_child_map_request_event(self, event):
    1241         # If we get a MapRequest then it might mean that someone tried to map
    1242         # this window multiple times in quick succession, before we actually
    1243         # mapped it (so that several MapRequests ended up queued up; FSF Emacs
    1244         # 22.1.50.1 does this, at least).  It alternatively might mean that
    1245         # the client is naughty and tried to map their window which is
    1246         # currently not displayed.  In either case, we should just ignore the
    1247         # request.
    1248         log("do_child_map_request_event(%s)", event)
    1249 
    1250     def do_xpra_unmap_event(self, event):
    1251         if event.delivered_to is self.corral_window or self.corral_window is None:
    1252             return
     359        #X11: PropertyNotify
    1253360        assert event.window is self.client_window
    1254         # The client window got unmapped.  The question is, though, was that
    1255         # because it was withdrawn/destroyed, or was it because we unmapped it
    1256         # going into IconicState?
    1257         #
    1258         # At the moment, we never actually put windows into IconicState
    1259         # (i.e. unmap them), except in the special case when we start up and
    1260         # find windows that are already mapped.  So we only need to check
    1261         # against that one serial number.
    1262         #
    1263         # Also, if we receive a *synthetic* UnmapNotify event, that always
    1264         # means that the client has withdrawn the window (even if it was not
    1265         # mapped in the first place) -- ICCCM section 4.1.4.
    1266         log("do_xpra_unmap_event(%s) client window unmapped", event)
    1267         if event.send_event or event.serial>self.last_unmap_serial:
    1268             self.unmanage()
     361        self._handle_property_change(str(event.atom))
    1269362
    1270     def do_xpra_destroy_event(self, event):
    1271         if event.delivered_to is self.corral_window or self.corral_window is None:
     363    def _handle_property_change(self, name):
     364        #ie: _handle_property_change("_NET_WM_NAME")
     365        metalog("Property changed on %#x: %s", self.client_window.xid, name)
     366        if name in PROPERTIES_DEBUG:
     367            metalog.info("%s=%s", name, self.prop_get(name, PROPERTIES_DEBUG[name], True, False))
     368        if name in PROPERTIES_IGNORED:
    1272369            return
    1273         assert event.window is self.client_window
    1274         # This is somewhat redundant with the unmap signal, because if you
    1275         # destroy a mapped window, then a UnmapNotify is always generated.
    1276         # However, this allows us to catch the destruction of unmapped
    1277         # ("iconified") windows, and also catch any mistakes we might have
    1278         # made with unmap heuristics.  I love the smell of XDestroyWindow in
    1279         # the morning.  It makes for simple code:
    1280         self.unmanage()
     370        self._call_property_handler(name)
    1281371
    1282     SCRUB_PROPERTIES = ["WM_STATE",
    1283                         "_NET_WM_STATE",
    1284                         "_NET_FRAME_EXTENTS",
    1285                         "_NET_WM_ALLOWED_ACTIONS",
    1286                         ]
     372    def _call_property_handler(self, name):
     373        if name in self._property_handlers:
     374            self._property_handlers[name](self)
    1287375
    1288     def do_unmanaged(self, wm_exiting):
    1289         log("unmanaging window: %s (%s - %s)", self, self.corral_window, self.client_window)
    1290         self._internal_set_property("owner", None)
    1291         if self.corral_window:
    1292             remove_event_receiver(self.corral_window, self)
    1293             with xswallow:
    1294                 for prop in WindowModel.SCRUB_PROPERTIES:
    1295                     X11Window.XDeleteProperty(self.client_window.xid, prop)
    1296             if self.client_reparented:
    1297                 self.client_window.reparent(gtk.gdk.get_default_root_window(), 0, 0)
    1298                 self.client_reparented = False
    1299             self.client_window.set_events(self.client_window_saved_events)
    1300             #it is now safe to destroy the corral window:
    1301             self.corral_window.destroy()
    1302             self.corral_window = None
    1303             # It is important to remove from our save set, even after
    1304             # reparenting, because according to the X spec, windows that are
    1305             # in our save set are always Mapped when we exit, *even if those
    1306             # windows are no longer inferior to any of our windows!* (see
    1307             # section 10. Connection Close).  This causes "ghost windows", see
    1308             # bug #27:
    1309             if self.in_save_set:
    1310                 with xswallow:
    1311                     X11Window.XRemoveFromSaveSet(self.client_window.xid)
    1312                 self.in_save_set = False
    1313             with xswallow:
    1314                 X11Window.sendConfigureNotify(self.client_window.xid)
    1315             if wm_exiting:
    1316                 self.client_window.show_unraised()
    1317         BaseWindowModel.do_unmanaged(self, wm_exiting)
     376    #specific properties:
    1318377
    1319     def ownership_election(self):
    1320         #returns True if we have updated the geometry
    1321         candidates = self.emit("ownership-election")
    1322         if candidates:
    1323             rating, winner = sorted(candidates)[-1]
    1324             if rating < 0:
    1325                 winner = None
    1326         else:
    1327             winner = None
    1328         old_owner = self.get_property("owner")
    1329         if old_owner is winner:
    1330             return False
    1331         if old_owner is not None:
    1332             self.corral_window.hide()
    1333             self.corral_window.reparent(self.parking_window, 0, 0)
    1334         self._internal_set_property("owner", winner)
    1335         if winner is not None:
    1336             winner.take_window(self, self.corral_window)
    1337             self._update_client_geometry()
    1338             self.corral_window.show_unraised()
    1339             return True
    1340         with xswallow:
    1341             X11Window.sendConfigureNotify(self.client_window.xid)
    1342         return False
     378    def _handle_pid_change(self):
     379        pid = self.prop_get("_NET_WM_PID", "u32") or -1
     380        metalog("_NET_WM_PID=%s", pid)
     381        self._updateprop("pid", pid)
    1343382
    1344     def maybe_recalculate_geometry_for(self, maybe_owner):
    1345         if maybe_owner and self.get_property("owner") is maybe_owner:
    1346             self._update_client_geometry()
     383    def _handle_client_machine_change(self):
     384        client_machine = self.prop_get("WM_CLIENT_MACHINE", "latin1")
     385        metalog("WM_CLIENT_MACHINE=%s", client_machine)
     386        self._updateprop("client-machine", client_machine)
    1347387
    1348     def _sanitize_size_hints(self, size_hints):
    1349         if size_hints is None:
    1350             return
    1351         for attr in ["min_aspect", "max_aspect"]:
    1352             v = size_hints.get(attr)
    1353             if v is not None:
    1354                 try:
    1355                     f = float(v)
    1356                 except:
    1357                     f = None
    1358                 if f is None or f>=MAX_ASPECT:
    1359                     log.warn("clearing invalid aspect hint value for %s: %s", attr, v)
    1360                     size_hints[attr] = -1.0
    1361         for attr in ["max_size", "min_size", "base_size", "resize_inc",
    1362                      "min_aspect_ratio", "max_aspect_ratio"]:
    1363             v = size_hints.get(attr)
    1364             if v is not None:
    1365                 try:
    1366                     w,h = v
    1367                 except:
    1368                     w,h = None,None
    1369                 if (w is None or h is None) or w>=MAX_WINDOW_SIZE or h>=MAX_WINDOW_SIZE:
    1370                     log("clearing invalid size hint value for %s: %s", attr, v)
    1371                     del size_hints[attr]
    1372         #if max-size is smaller than min-size (bogus), clamp it..
    1373         mins = size_hints.get("min_size")
    1374         maxs = size_hints.get("max_size")
    1375         if mins is not None and maxs is not None:
    1376             minw,minh = mins
    1377             maxw,maxh = maxs
    1378             if minw<=0 and minh<=0:
    1379                 #doesn't do anything
    1380                 size_hints["min_size"] = None
    1381             if maxw<=0 or maxh<=0:
    1382                 #doesn't mak sense!
    1383                 size_hints["max_size"] = None
    1384             if maxw<minw or maxh<minh:
    1385                 size_hints["min_size"] = max(minw, maxw), max(minh, maxh)
    1386                 size_hints["max_size"] = size_hints.min_size
    1387                 log.warn("invalid min_size=%s / max_size=%s changed to: %s / %s",
    1388                          mins, maxs, size_hints["min_size"], size_hints["max_size"])
     388    def _handle_wm_name_change(self):
     389        name = self.prop_get("_NET_WM_NAME", "utf8", True)
     390        metalog("_NET_WM_NAME=%s", name)
     391        if name is None:
     392            name = self.prop_get("WM_NAME", "latin1", True)
     393            metalog("WM_NAME=%s", name)
     394        self._updateprop("title", sanestr(name))
    1389395
    1390     def _update_client_geometry(self):
    1391         owner = self.get_property("owner")
    1392         log("_update_client_geometry: owner=%s, setup_done=%s", owner, self._setup_done)
    1393         if owner is not None:
    1394             log("_update_client_geometry: using owner=%s", owner)
    1395             def window_size():
    1396                 return  owner.window_size(self)
    1397             def window_position(w, h):
    1398                 return  owner.window_position(self, w, h)
    1399             self._do_update_client_geometry(window_size, window_position)
    1400         elif not self._setup_done:
    1401             #try to honour initial size and position requests during setup:
    1402             def window_size():
    1403                 return self.get_property("requested-size")
    1404             def window_position(w=0, h=0):
    1405                 return self.get_property("requested-position")
    1406             log("_update_client_geometry: using initial size=%s and position=%s", window_size(), window_position())
    1407             self._do_update_client_geometry(window_size, window_position)
     396    def _handle_role_change(self):
     397        role = self.prop_get("WM_WINDOW_ROLE", "latin1")
     398        metalog("WM_WINDOW_ROLE=%s", role)
     399        self._updateprop("role", role)
    1408400
    1409     def _do_update_client_geometry(self, window_size_cb, window_position_cb):
    1410         allocated_w, allocated_h = window_size_cb()
    1411         log("_do_update_client_geometry: %sx%s", allocated_w, allocated_h)
    1412         hints = self.get_property("size-hints")
    1413         log("_do_update_client_geometry: hints=%s", hints)
    1414         size = calc_constrained_size(allocated_w, allocated_h, hints)
    1415         log("_do_update_client_geometry: size=%s", size)
    1416         w, h, wvis, hvis = size
    1417         x, y = window_position_cb(w, h)
    1418         log("_do_update_client_geometry: position=%s", (x,y))
    1419         self.corral_window.move_resize(x, y, w, h)
    1420         self._internal_set_property("actual-size", (w, h))
    1421         self._internal_set_property("user-friendly-size", (wvis, hvis))
    1422         with xswallow:
    1423             X11Window.configureAndNotify(self.client_window.xid, 0, 0, w, h)
     401    def _handle_protocols_change(self):
     402        protocols = self.prop_get("WM_PROTOCOLS", ["atom"]) or []
     403        metalog("WM_PROTOCOLS=%s", protocols)
     404        self._updateprop("protocols", protocols)
    1424405
    1425     def do_xpra_configure_event(self, event):
    1426         log("WindowModel.do_xpra_configure_event(%s) corral=%#x, client=%#x, managed=%s", event, self.corral_window.xid, self.client_window.xid, self._managed)
    1427         if not self._managed:
    1428             return
    1429         if event.window!=self.client_window:
    1430             #we only care about events on the client window
    1431             log("WindowModel.do_xpra_configure_event: event is not on the client window")
    1432             return
    1433         if self.corral_window is None or not self.corral_window.is_visible():
    1434             log("WindowModel.do_xpra_configure_event: corral window is not visible")
    1435             return
    1436         if self.client_window is None or not self.client_window.is_visible():
    1437             log("WindowModel.do_xpra_configure_event: client window is not visible")
    1438             return
    1439         try:
    1440             #workaround applications whose windows disappear from underneath us:
    1441             with xsync:
    1442                 if self.resize_corral_window(event.x, event.y, event.width, event.height, event.border_width):
    1443                     self.notify("geometry")
    1444         except XError as e:
    1445             log.warn("failed to resize corral window: %s", e)
    1446 
    1447     def resize_corral_window(self, x, y, w, h, border):
    1448         #the client window may have been resized or moved (generally programmatically)
    1449         #so we may need to update the corral_window to match
    1450         cox, coy, cow, coh = self.corral_window.get_geometry()[:4]
    1451         modded = False
    1452         if self._geometry[4]!=border:
    1453             modded = True
    1454         #size changes (and position if any):
    1455         hints = self.get_property("size-hints")
    1456         size = calc_constrained_size(w, h, hints)
    1457         log("resize_corral_window() new constrained size=%s", size)
    1458         w, h, wvis, hvis = size
    1459         if cow!=w or coh!=h:
    1460             if (x, y) != (0, 0):
    1461                 log("resize_corral_window() move and resize from %s to %s", (cox, coy, cow, coh), (x, y, w, h))
    1462                 self.corral_window.move_resize(x, y, w, h)
    1463                 self.client_window.move(0, 0)
    1464                 cox, coy, cow, coh = x, y, w, h
    1465             else:
    1466                 #just resize:
    1467                 log("resize_corral_window() resize from %s to %s", (cow, coh), (w, h))
    1468                 self.corral_window.resize(w, h)
    1469                 cow, coh = w, h
    1470             modded = True
    1471         #just position change:
    1472         elif (x, y) != (0, 0):
    1473             log("resize_corral_window() moving corral window from %s to %s", (cox, coy), (x, y))
    1474             self.corral_window.move(x, y)
    1475             self.client_window.move(0, 0)
    1476             cox, coy = x, y
    1477             modded = True
    1478 
    1479         #these two should be using geometry rather than duplicating it?
    1480         if self.get_property("actual-size")!=(w, h):
    1481             self._internal_set_property("actual-size", (w, h))
    1482             modded = True
    1483         if self.get_property("user-friendly-size")!=(wvis, hvis):
    1484             self._internal_set_property("user-friendly-size", (wvis, hvis))
    1485             modded = True
    1486 
    1487         if modded:
    1488             self._geometry = (cox, coy, cow, coh, border)
    1489         log("resize_corral_window() modified=%s, geometry=%s", modded, self._geometry)
    1490         return modded
    1491 
    1492     def do_child_configure_request_event(self, event):
    1493         log("do_child_configure_request_event(%s) client=%#x, corral=%#x, value_mask=%s", event, self.client_window.xid, self.corral_window.xid, configure_bits(event.value_mask))
    1494         if event.value_mask & CWStackMode:
    1495             log(" restack above=%s, detail=%s", event.above, event.detail)
    1496         # Also potentially update our record of what the app has requested:
    1497         (x, y) = self.get_property("requested-position")
    1498         if event.value_mask & CWX:
    1499             x = event.x
    1500         if event.value_mask & CWY:
    1501             y = event.y
    1502         self._internal_set_property("requested-position", (x, y))
    1503 
    1504         (w, h) = self.get_property("requested-size")
    1505         if event.value_mask & CWWidth:
    1506             w = event.width
    1507         if event.value_mask & CWHeight:
    1508             h = event.height
    1509         self._internal_set_property("requested-size", (w, h))
    1510         # As per ICCCM 4.1.5, even if we ignore the request
    1511         # send back a synthetic ConfigureNotify telling the client that nothing has happened.
    1512         log("do_child_configure_request_event updated requested geometry: %s", (x, y, w, h))
    1513         self._update_client_geometry()
    1514 
    1515         # FIXME: consider handling attempts to change stacking order here.
    1516         # (In particular, I believe that a request to jump to the top is
    1517         # meaningful and should perhaps even be respected.)
    1518 
    1519     _property_handlers = BaseWindowModel._property_handlers.copy()
    1520 
    1521     def _handle_wm_normal_hints(self):
    1522         with xswallow:
    1523             size_hints = X11Window.getSizeHints(self.client_window.xid)
    1524         #getSizeHints exports fields using their X11 names as defined in the "XSizeHints" structure,
    1525         #but we use a different naming (for historical reason and backwards compatibility)
    1526         #so rename the fields:
    1527         hints = {}
    1528         if size_hints:
    1529             for k,v in size_hints.items():
    1530                 hints[{"min_size"       : "minimum-size",
    1531                        "max_size"       : "maximum-size",
    1532                        "base_size"      : "base-size",
    1533                        "resize_inc"     : "increment",
    1534                        "min_aspect"     : "minimum-aspect-ratio",
    1535                        "max_aspect"     : "maximum-aspect-ratio",
    1536                        "win_gravity"    : "gravity",
    1537                        }.get(k, k)] = v
    1538         self._sanitize_size_hints(hints)
    1539         # Don't send out notify and ConfigureNotify events when this property
    1540         # gets no-op updated -- some apps like FSF Emacs 21 like to update
    1541         # their properties every time they see a ConfigureNotify, and this
    1542         # reduces the chance for us to get caught in loops:
    1543         old_hints = self.get_property("size-hints")
    1544         if hints and hints!=old_hints:
    1545             self._internal_set_property("size-hints", hints)
    1546             self._update_client_geometry()
    1547 
    1548     _property_handlers["WM_NORMAL_HINTS"] = _handle_wm_normal_hints
    1549 
    1550     def _handle_icon_title_change(self):
    1551         icon_name = self.prop_get("_NET_WM_ICON_NAME", "utf8", True)
    1552         iconlog("_NET_WM_ICON_NAME=%s", icon_name)
    1553         if icon_name is None:
    1554             icon_name = self.prop_get("WM_ICON_NAME", "latin1", True)
    1555         self._internal_set_property("icon-title", sanestr(icon_name))
    1556 
    1557     _property_handlers["WM_ICON_NAME"] = _handle_icon_title_change
    1558     _property_handlers["_NET_WM_ICON_NAME"] = _handle_icon_title_change
    1559 
    1560     def _handle_motif_wm_hints(self):
    1561         #motif_hints = self.prop_get("_MOTIF_WM_HINTS", "motif-hints")
    1562         motif_hints = prop_get(self.client_window, "_MOTIF_WM_HINTS", "motif-hints", ignore_errors=False, raise_xerrors=True)
    1563         log("_handle_motif_wm_hints() motif_hints=%s", motif_hints)
    1564         if motif_hints and motif_hints.flags&(2**MotifWMHints.DECORATIONS_BIT):
    1565             self._internal_set_property("decorations", motif_hints.decorations)
    1566     _property_handlers["_MOTIF_WM_HINTS"] = _handle_motif_wm_hints
    1567 
    1568     def _handle_net_wm_icon(self):
    1569         iconlog("_NET_WM_ICON changed on %#x, re-reading", self.client_window.xid)
    1570         surf = self.prop_get("_NET_WM_ICON", "icon")
    1571         if surf is not None:
    1572             # FIXME: There is no Pixmap.new_for_display(), so this isn't
    1573             # actually display-clean.  Oh well.
    1574             pixmap = gtk.gdk.Pixmap(None, surf.get_width(), surf.get_height(), 32)
    1575             screen = get_display_for(pixmap).get_default_screen()
    1576             pixmap.set_colormap(screen.get_rgba_colormap())
    1577             cr = pixmap.cairo_create()
    1578             cr.set_source_surface(surf)
    1579             # Important to use SOURCE, because a newly created Pixmap can have
    1580             # random trash as its contents, and otherwise that will show
    1581             # through any alpha in the icon:
    1582             cr.set_operator(cairo.OPERATOR_SOURCE)
    1583             cr.paint()
    1584         else:
    1585             pixmap = None
    1586         #FIXME: it would be more efficient to notify first,
    1587         #then get the icon pixels on demand and cache them..
    1588         self._internal_set_property("icon", surf)
    1589         self._internal_set_property("icon-pixmap", pixmap)
    1590         iconlog("icon is now %r", surf)
    1591     _property_handlers["_NET_WM_ICON"] = _handle_net_wm_icon
    1592 
    1593     def _read_initial_properties(self):
    1594         # Things that don't change:
    1595         BaseWindowModel._read_initial_properties(self)
    1596         def pget(key, ptype):
    1597             return self.prop_get(key, ptype, raise_xerrors=True)
    1598 
    1599         geometry = self.client_window.get_geometry()
    1600         self._internal_set_property("requested-position", (geometry[0], geometry[1]))
    1601         self._internal_set_property("requested-size", (geometry[2], geometry[3]))
    1602 
    1603         #try using XGetClassHint:
    1604         class_instance = X11Window.getClassHint(self.client_window.xid)
    1605         if class_instance is None:
    1606             #fallback to reading WM_CLASS:
    1607             def get_wm_class_prop(ptype):
    1608                 class_instance = pget("WM_CLASS", ptype)
    1609                 if not class_instance:
    1610                     return None
    1611                 try:
    1612                     parts = class_instance.split("\0")
    1613                     if len(parts)!=3:
    1614                         return None
    1615                     (c, i, _) = parts
    1616                     return  (c, i)
    1617                 except ValueError:
    1618                     log.warn("Malformed WM_CLASS: %s, ignoring", class_instance)
    1619                     return None
    1620             class_instance = get_wm_class_prop("latin1") or get_wm_class_prop("utf8")
    1621         self._internal_set_property("class-instance", class_instance)
    1622 
    1623         protocols = pget("WM_PROTOCOLS", ["atom"])
    1624         if protocols is None:
    1625             protocols = []
    1626         self._internal_set_property("protocols", protocols)
    1627         self.notify("can-focus")
    1628 
    1629         client_machine = pget("WM_CLIENT_MACHINE", "latin1")
    1630         # May be None
    1631         self._internal_set_property("client-machine", client_machine)
    1632 
    1633         command = pget("WM_COMMAND", "latin1")
     406    def _handle_command_change(self):
     407        command = self.prop_get("WM_COMMAND", "latin1")
     408        metalog("WM_COMMAND=%s", command)
    1634409        if command:
    1635410            command = command.strip("\0")
    1636         self._internal_set_property("command", command)
     411        self._updateprop("command", command)
    1637412
    1638         # WARNING: have to handle _NET_WM_STATE before we look at WM_HINTS;
    1639         # WM_HINTS assumes that our "state" property is already set.  This is
    1640         # because there are four ways a window can get its urgency
    1641         # ("attention-requested") bit set:
    1642         #   1) _NET_WM_STATE_DEMANDS_ATTENTION in the _initial_ state hints
    1643         #   2) setting the bit WM_HINTS, at _any_ time
    1644         #   3) sending a request to the root window to add
    1645         #      _NET_WM_STATE_DEMANDS_ATTENTION to their state hints
    1646         #   4) if we (the wm) decide they should be and set it
    1647         # To implement this, we generally track the urgency bit via
    1648         # _NET_WM_STATE (since that is under our sole control during normal
    1649         # operation).  Then (1) is accomplished through the normal rule that
    1650         # initial states are read off from the client, and (2) is accomplished
    1651         # by having WM_HINTS affect _NET_WM_STATE.  But this means that
    1652         # WM_HINTS and _NET_WM_STATE handling become intertangled.
    1653         net_wm_state = pget("_NET_WM_STATE", ["atom"])
    1654         if net_wm_state:
    1655             self._internal_set_property("state", frozenset(net_wm_state))
    1656         else:
    1657             self._internal_set_property("state", frozenset())
    1658         modal = (net_wm_state is not None) and ("_NET_WM_STATE_MODAL" in net_wm_state)
    1659         self._internal_set_property("modal", modal)
     413    def _handle_class_change(self):
     414        #try using XGetClassHint:
     415        with xswallow:
     416            class_instance = X11Window.getClassHint(self.client_window.xid)
     417            metalog("WM_CLASS=%s", class_instance)
     418            self._updateprop("class-instance", class_instance)
    1660419
    1661         #the default value is True, but is not being honoured! (so we force it here)
    1662         self._internal_set_property("decorations", True)
    1663 
    1664         for mutable in ["WM_HINTS", "WM_NORMAL_HINTS",
    1665                         "WM_ICON_NAME", "_NET_WM_ICON_NAME",
    1666                         "_NET_WM_STRUT", "_NET_WM_STRUT_PARTIAL", "_MOTIF_WM_HINTS"]:
    1667             self._call_property_handler(mutable)
    1668         for mutable in ["_NET_WM_ICON"]:
    1669             try:
    1670                 self._call_property_handler(mutable)
    1671             except:
    1672                 log.error("error reading initial property %s", mutable, exc_info=True)
    1673 
    1674     def get_default_window_icon(self):
    1675         #return the icon which would be used from the wmclass
    1676         c_i = self.get_property("class-instance")
    1677         if not c_i or len(c_i)!=2:
    1678             return None
    1679         wmclass_name, wmclass_class = [x.encode("utf-8") for x in c_i]
    1680         iconlog("get_default_window_icon() using %s", (wmclass_name, wmclass_class))
    1681         if not wmclass_name:
    1682             return None
    1683         it = gtk.icon_theme_get_default()
    1684         i = it.lookup_icon(wmclass_name, 48, 0)
    1685         iconlog("%s.lookup_icon(%s)=%s", it, wmclass_name, i)
    1686         if not i:
    1687             return None
    1688         p = i.load_icon()
    1689         iconlog("%s.load_icon()=%s", i, p)
    1690         if not p:
    1691             return None
    1692         #to make it consistent with the "icon" property,
    1693         #return a cairo surface..
    1694         surf = cairo.ImageSurface(cairo.FORMAT_ARGB32, p.get_width(), p.get_height())
    1695         gc = gtk.gdk.CairoContext(cairo.Context(surf))
    1696         gc.set_source_pixbuf(p, 0, 0)
    1697         gc.paint()
    1698         iconlog("get_default_window_icon()=%s", surf)
    1699         return surf
    1700 
    1701     ################################
    1702     # Property setting
    1703     ################################
    1704 
    1705     # A few words about _NET_WM_STATE are in order.  Basically, it is a set of
    1706     # flags.  Clients are allowed to set the initial value of this X property
    1707     # to anything they like, when their window is first mapped; after that,
    1708     # though, only the window manager is allowed to touch this property.  So
    1709     # we store its value (or at least, our idea as to its value, the X server
    1710     # in principle could disagree) as the "state" property.  There are
    1711     # basically two things we need to accomplish:
    1712     #   1) Whenever our property is modified, we mirror that modification into
    1713     #      the X server.  This is done by connecting to our own notify::state
    1714     #      signal.
    1715     #   2) As a more user-friendly interface to these state flags, we provide
    1716     #      several boolean properties like "attention-requested".
    1717     #      These are virtual boolean variables; they are actually backed
    1718     #      directly by the "state" property, and reading/writing them in fact
    1719     #      accesses the "state" set directly.  This is done by overriding
    1720     #      do_set_property and do_get_property.
    1721     _state_properties = {
    1722         "attention-requested"   : ("_NET_WM_STATE_DEMANDS_ATTENTION", ),
    1723         "fullscreen"            : ("_NET_WM_STATE_FULLSCREEN", ),
    1724         "maximized"             : ("_NET_WM_STATE_MAXIMIZED_VERT", "_NET_WM_STATE_MAXIMIZED_HORZ"),
    1725         "shaded"                : ("_NET_WM_STATE_SHADED", ),
    1726         "above"                 : ("_NET_WM_STATE_ABOVE", ),
    1727         "below"                 : ("_NET_WM_STATE_BELOW", ),
    1728         "sticky"                : ("_NET_WM_STATE_STICKY", ),
    1729         "skip-taskbar"          : ("_NET_WM_STATE_SKIP_TASKBAR", ),
    1730         "skip-pager"            : ("_NET_WM_STATE_SKIP_PAGER", ),
    1731         "modal"                 : ("_NET_WM_STATE_MODAL", ),
    1732         "focused"               : ("_NET_WM_STATE_FOCUSED", ),
     420    _property_handlers = {
     421        "_NET_WM_PID"       : _handle_pid_change,
     422        "WM_NAME"           : _handle_wm_name_change,
     423        "_NET_WM_NAME"      : _handle_wm_name_change,
     424        "WM_WINDOW_ROLE"    : _handle_role_change,
     425        "WM_PROTOCOLS"      : _handle_protocols_change,
     426        "WM_COMMAND"        : _handle_command_change,
     427        "WM_CLASS"          : _handle_class_change,
    1733428        }
    1734429
    1735     _state_properties_reversed = {}
    1736     for k, states in _state_properties.iteritems():
    1737         for x in states:
    1738             _state_properties_reversed[x] = k
    1739430
    1740     def _state_add(self, *state_names):
    1741         curr = set(self.get_property("state"))
    1742         add = [s for s in state_names if s not in curr]
    1743         if add:
    1744             for x in add:
    1745                 curr.add(x)
    1746             self._internal_set_property("state", frozenset(curr))
    1747             self._state_notify(add)
    1748 
    1749     def _state_remove(self, *state_names):
    1750         curr = set(self.get_property("state"))
    1751         discard = [s for s in state_names if s in curr]
    1752         if discard:
    1753             for x in discard:
    1754                 curr.discard(x)
    1755             self._internal_set_property("state", frozenset(curr))
    1756             self._state_notify(discard)
    1757 
    1758     def _state_notify(self, state_names):
    1759         notify_props = set()
    1760         for x in state_names:
    1761             if x in self._state_properties_reversed:
    1762                 notify_props.add(self._state_properties_reversed[x])
    1763         for x in list(notify_props):
    1764             self.notify(x)
    1765 
    1766     def _state_isset(self, state_name):
    1767         return state_name in self.get_property("state")
    1768 
    1769     def _handle_state_changed(self, *args):
    1770         # Sync changes to "state" property out to X property.
    1771         with xswallow:
    1772             wm_state = list(self.get_property("state"))
    1773             prop_set(self.client_window, "_NET_WM_STATE", ["atom"], wm_state)
    1774             log("_handle_state_changed: _NET_WM_STATE=%s", wm_state)
    1775 
    1776     def _handle_frame_changed(self, *args):
    1777         v = self.get_property("frame")
    1778         if not v and (not self.is_OR() and not self.is_tray()):
    1779             root = self.client_window.get_screen().get_root_window()
    1780             v = prop_get(root, "DEFAULT_NET_FRAME_EXTENTS", ["u32"], ignore_errors=True)
    1781         if not v:
    1782             #default for OR, or if we don't have any other value:
    1783             v = (0, 0, 0, 0)
    1784         log("handle_frame_changed: setting _NET_FRAME_EXTENTS=%s on %#x", v, self.client_window.xid)
    1785         with xswallow:
    1786             prop_set(self.client_window, "_NET_FRAME_EXTENTS", ["u32"], v)           
    1787 
    1788     def do_set_property(self, pspec, value):
    1789         if pspec.name in self._state_properties:
    1790             #virtual property for WM_STATE:
    1791             self.update_state(pspec.name, value)
    1792             return
    1793         AutoPropGObjectMixin.do_set_property(self, pspec, value)
    1794 
    1795     def update_wm_state(self, prop, b):
    1796         state_names = self._state_properties.get(prop)
    1797         assert state_names, "invalid window state %s" % prop
    1798         log("update_wm_state(%s, %s) state_names=%s", prop, b, state_names)
    1799         if b:
    1800             self._state_add(*state_names)
    1801         else:
    1802             self._state_remove(*state_names)
    1803 
    1804 
    1805     def do_get_property_can_focus(self, name):
    1806         assert name == "can-focus"
    1807         return bool(self._input_field) or "WM_TAKE_FOCUS" in self.get_property("protocols")
    1808 
    1809     def do_get_property(self, pspec):
    1810         if pspec.name in self._state_properties:
    1811             #virtual property for WM_STATE:
    1812             return self.get_wm_state(pspec.name)
    1813         return AutoPropGObjectMixin.do_get_property(self, pspec)
    1814 
    1815     def get_wm_state(self, prop):
    1816         state_names = self._state_properties.get(prop)
    1817         assert state_names, "invalid window state %s" % prop
    1818         log("get_wm_state(%s) state_names=%s", prop, state_names)
    1819         #this is a virtual property for WM_STATE:
    1820         #return True if any is set (only relevant for maximized)
    1821         for x in state_names:
    1822             if self._state_isset(x):
    1823                 return True
    1824         return False
    1825 
    1826 
    1827     def unmap(self):
    1828         with xsync:
    1829             if X11Window.is_mapped(self.client_window.xid):
    1830                 self.last_unmap_serial = X11Window.Unmap(self.client_window.xid)
    1831                 log("client window %#x unmapped, serial=%s", self.client_window.xid, self.last_unmap_serial)
    1832 
    1833     def map(self):
    1834         with xsync:
    1835             if not X11Window.is_mapped(self.client_window.xid):
    1836                 X11Window.MapWindow(self.client_window.xid)
    1837                 log("client window %#x mapped", self.client_window.xid)
    1838 
    1839 
    1840     def _handle_iconic_update(self, *args):
    1841         def set_state(state):
    1842             log("_handle_iconic_update: set_state(%s)", state)
    1843             with xswallow:
    1844                 prop_set(self.client_window, "WM_STATE", "state", state)
    1845 
    1846         if self.get_property("iconic"):
    1847             set_state(IconicState)
    1848             self._state_add("_NET_WM_STATE_HIDDEN")
    1849         else:
    1850             set_state(NormalState)
    1851             self._state_remove("_NET_WM_STATE_HIDDEN")
    1852 
    1853     def _write_initial_properties_and_setup(self):
    1854         # Things that don't change:
    1855         prop_set(self.client_window, "_NET_WM_ALLOWED_ACTIONS",
    1856                  ["atom"], self._NET_WM_ALLOWED_ACTIONS)
    1857         self.connect("notify::state", self._handle_state_changed)
    1858         self.connect("notify::frame", self._handle_frame_changed)
    1859         # Flush things:
    1860         self._handle_state_changed()
    1861 
    1862 
    1863431    ################################
    1864     # Focus handling:
    1865     ################################
    1866 
    1867     def give_client_focus(self):
    1868         """The focus manager has decided that our client should receive X
    1869         focus.  See world_window.py for details."""
    1870         if self.corral_window:
    1871             with xswallow:
    1872                 self.do_give_client_focus()
    1873 
    1874     def do_give_client_focus(self):
    1875         focuslog("Giving focus to %#x", self.client_window.xid)
    1876         # Have to fetch the time, not just use CurrentTime, both because ICCCM
    1877         # says that WM_TAKE_FOCUS must use a real time and because there are
    1878         # genuine race conditions here (e.g. suppose the client does not
    1879         # actually get around to requesting the focus until after we have
    1880         # already changed our mind and decided to give it to someone else).
    1881         now = gtk.gdk.x11_get_server_time(self.corral_window)
    1882         # ICCCM 4.1.7 *claims* to describe how we are supposed to give focus
    1883         # to a window, but it is completely opaque.  From reading the
    1884         # metacity, kwin, gtk+, and qt code, it appears that the actual rules
    1885         # for giving focus are:
    1886         #   -- the WM_HINTS input field determines whether the WM should call
    1887         #      XSetInputFocus
    1888         #   -- independently, the WM_TAKE_FOCUS protocol determines whether
    1889         #      the WM should send a WM_TAKE_FOCUS ClientMessage.
    1890         # If both are set, both methods MUST be used together. For example,
    1891         # GTK+ apps respect WM_TAKE_FOCUS alone but I'm not sure they handle
    1892         # XSetInputFocus well, while Qt apps ignore (!!!) WM_TAKE_FOCUS
    1893         # (unless they have a modal window), and just expect to get focus from
    1894         # the WM's XSetInputFocus.
    1895         if bool(self._input_field):
    1896             focuslog("... using XSetInputFocus")
    1897             X11Window.XSetInputFocus(self.client_window.xid, now)
    1898         if "WM_TAKE_FOCUS" in self.get_property("protocols"):
    1899             focuslog("... using WM_TAKE_FOCUS")
    1900             send_wm_take_focus(self.client_window, now)
    1901         self.set_active()
    1902 
    1903     ################################
    1904432    # Killing clients:
    1905433    ################################
    1906434
     
    1911439        else:
    1912440            title = self.get_property("title")
    1913441            xid = self.get_property("xid")
    1914             log.warn("window %#x ('%s') does not support WM_DELETE_WINDOW... using force_quit()", xid, title)
     442            log.warn("window %#x ('%s') does not support WM_DELETE_WINDOW... using force_quit", xid, title)
    1915443            # You don't wanna play ball?  Then no more Mr. Nice Guy!
    1916444            self.force_quit()
    1917445
     
    1918446    def force_quit(self):
    1919447        pid = self.get_property("pid")
    1920448        machine = self.get_property("client-machine")
     449        from socket import gethostname
    1921450        localhost = gethostname()
    1922451        log("force_quit() pid=%s, machine=%s, localhost=%s", pid, machine, localhost)
    1923452        xid = self.client_window.xid
     
    1928457            if pid==os.getpid():
    1929458                log.warn("force_quit() refusing to kill ourselves!")
    1930459                return
    1931             if self.kill_count==0:
     460            if self._kill_count==0:
    1932461                #first time around: just send a SIGINT and hope for the best
    1933462                try:
    1934463                    os.kill(pid, signal.SIGINT)
     
    1941470                except OSError:
    1942471                    log.warn("failed to kill(SIGKILL) client with pid %s", pid)
    1943472                XKill()
    1944             self.kill_count += 1
     473            self._kill_count += 1
    1945474            return
    1946475        XKill()
    1947 
    1948     def __repr__(self):
    1949         xid = 0
    1950         if self.client_window:
    1951             xid = self.client_window.xid
    1952         title = self.get_property("title")
    1953         if title:
    1954             return "WindowModel(%#x - \"%s\")" % (xid, nonl(title))
    1955         return "WindowModel(%#x)" % xid
    1956 
    1957 
    1958 gobject.type_register(WindowModel)
  • xpra/x11/gtk2/models/or_window.py

     
    11# This file is part of Xpra.
    22# Copyright (C) 2008, 2009 Nathaniel Smith <njs@pobox.com>
    3 # Copyright (C) 2011-2014 Antoine Martin <antoine@devloop.org.uk>
     3# Copyright (C) 2011-2015 Antoine Martin <antoine@devloop.org.uk>
    44# Xpra is released under the terms of the GNU GPL v2, or, at your option, any
    55# later version. See the file COPYING for details.
    66
    7 """The magic GTK widget that represents a client window.
    87
    9 Most of the gunk required to be a valid window manager (reparenting, synthetic
    10 events, mucking about with properties, etc. etc.) is wrapped up in here."""
     8from xpra.x11.gtk2.models import Unmanageable
     9from xpra.x11.gtk2.models.base import BaseWindowModel, gobject
     10from xpra.x11.bindings.window_bindings import X11WindowBindings #@UnresolvedImport
    1111
    12 # Maintain compatibility with old versions of Python, while avoiding a
    13 # deprecation warning on new versions:
    14 import os
    15 from socket import gethostname
    16 
    17 import gobject
    18 import glib
    19 import gtk.gdk
    20 import cairo
    21 import signal
    22 
    23 from xpra.util import nonl, WORKSPACE_UNSET, WORKSPACE_ALL
    24 from xpra.x11.bindings.window_bindings import constants, X11WindowBindings, SHAPE_KIND #@UnresolvedImport
    2512X11Window = X11WindowBindings()
    2613
    27 from xpra.x11.gtk_x11.send_wm import (
    28                 send_wm_take_focus,
    29                 send_wm_delete_window)
    30 from xpra.gtk_common.gobject_util import (AutoPropGObjectMixin,
    31                            one_arg_signal,
    32                            non_none_list_accumulator)
    33 from xpra.x11.gtk2.gdk_bindings import (
    34                 get_pyatom,                                 #@UnresolvedImport
    35                 get_pywindow,                               #@UnresolvedImport
    36                 add_event_receiver,                         #@UnresolvedImport
    37                 remove_event_receiver,                      #@UnresolvedImport
    38                 get_display_for,                            #@UnresolvedImport
    39                 calc_constrained_size,                      #@UnresolvedImport
    40                )
    41 from xpra.gtk_common.error import XError, xsync, xswallow
    42 from xpra.x11.gtk_x11.prop import prop_get, prop_set, MotifWMHints
    43 from xpra.x11.gtk2.composite import CompositeHelper
    4414
    45 from xpra.log import Logger
    46 log = Logger("x11", "window")
    47 focuslog = Logger("x11", "window", "focus")
    48 grablog = Logger("x11", "window", "grab")
    49 iconlog = Logger("x11", "window", "icon")
    50 workspacelog = Logger("x11", "window", "workspace")
    51 shapelog = Logger("x11", "window", "shape")
    52 
    53 
    54 _NET_WM_STATE_REMOVE = 0
    55 _NET_WM_STATE_ADD    = 1
    56 _NET_WM_STATE_TOGGLE = 2
    57 STATE_STRING = {
    58             _NET_WM_STATE_REMOVE    : "REMOVE",
    59             _NET_WM_STATE_ADD       : "ADD",
    60             _NET_WM_STATE_TOGGLE    : "TOGGLE",
    61                 }
    62 
    63 XNone = constants["XNone"]
    64 
    65 CWX             = constants["CWX"]
    66 CWY             = constants["CWY"]
    67 CWWidth         = constants["CWWidth"]
    68 CWHeight        = constants["CWHeight"]
    69 CWBorderWidth   = constants["CWBorderWidth"]
    70 CWSibling       = constants["CWSibling"]
    71 CWStackMode     = constants["CWStackMode"]
    72 CONFIGURE_GEOMETRY_MASK = CWX | CWY | CWWidth | CWHeight
    73 CW_MASK_TO_NAME = {
    74                    CWX              : "X",
    75                    CWY              : "Y",
    76                    CWWidth          : "Width",
    77                    CWHeight         : "Height",
    78                    CWBorderWidth    : "BorderWidth",
    79                    CWSibling        : "Sibling",
    80                    CWStackMode      : "StackMode",
    81                    CWBorderWidth    : "BorderWidth",
    82                    }
    83 def configure_bits(value_mask):
    84     return "|".join((v for k,v in CW_MASK_TO_NAME.items() if (k&value_mask)))
    85 
    86 
    87 IconicState = constants["IconicState"]
    88 NormalState = constants["NormalState"]
    89 
    90 # grab stuff:
    91 NotifyNormal        = constants["NotifyNormal"]
    92 NotifyGrab          = constants["NotifyGrab"]
    93 NotifyUngrab        = constants["NotifyUngrab"]
    94 NotifyWhileGrabbed  = constants["NotifyWhileGrabbed"]
    95 NotifyNonlinearVirtual = constants["NotifyNonlinearVirtual"]
    96 GRAB_CONSTANTS = {
    97                   NotifyNormal          : "NotifyNormal",
    98                   NotifyGrab            : "NotifyGrab",
    99                   NotifyUngrab          : "NotifyUngrab",
    100                   NotifyWhileGrabbed    : "NotifyWhileGrabbed",
    101                  }
    102 DETAIL_CONSTANTS    = {}
    103 for x in ("NotifyAncestor", "NotifyVirtual", "NotifyInferior",
    104           "NotifyNonlinear", "NotifyNonlinearVirtual", "NotifyPointer",
    105           "NotifyPointerRoot", "NotifyDetailNone"):
    106     DETAIL_CONSTANTS[constants[x]] = x
    107 grablog("pointer grab constants: %s", GRAB_CONSTANTS)
    108 grablog("detail constants: %s", DETAIL_CONSTANTS)
    109 
    110 
    111 #if you want to use a virtual screen bigger than 32767x32767
    112 #you will need to change those values, but some broken toolkits
    113 #will then misbehave (they use signed shorts instead of signed ints..)
    114 MAX_WINDOW_SIZE = 2**15-1
    115 MAX_ASPECT = 2**15-1
    116 USE_XSHM = os.environ.get("XPRA_XSHM", "1")=="1"
    117 
    118 #these properties are not handled, and we don't want to spam the log file
    119 #whenever an app decides to change them:
    120 PROPERTIES_IGNORED = ("_NET_WM_OPAQUE_REGION", )
    121 #make it easier to debug property changes, just add them here:
    122 PROPERTIES_DEBUG = {}   #ie: {"WM_PROTOCOLS" : ["atom"]}
    123 
    124 
    125 #add user friendly workspace logging:
    126 WORKSPACE_STR = {WORKSPACE_UNSET    : "UNSET",
    127                  WORKSPACE_ALL      : "ALL"}
    128 def workspacestr(w):
    129     return WORKSPACE_STR.get(w, w)
    130 
    131 
    132 def sanestr(s):
    133     return (s or "").strip("\0").replace("\0", " ")
    134 
    135 
    136 # Todo:
    137 #   client focus hints
    138 #   _NET_WM_SYNC_REQUEST
    139 #   root window requests (pagers, etc. requesting to change client states)
    140 #   _NET_WM_PING/detect window not responding (also a root window message)
    141 
    142 # Okay, we need a block comment to explain the window arrangement that this
    143 # file is working with.
    144 #
    145 #                +--------+
    146 #                | widget |
    147 #                +--------+
    148 #                  /    \
    149 #  <- top         /     -\-        bottom ->
    150 #                /        \
    151 #          +-------+       |
    152 #          | image |  +---------+
    153 #          +-------+  | corral  |
    154 #                     +---------+
    155 #                          |
    156 #                     +---------+
    157 #                     | client  |
    158 #                     +---------+
    159 #
    160 # Each box in this diagram represents one X/GDK window.  In the common case,
    161 # every window here takes up exactly the same space on the screen (!).  In
    162 # fact, the two windows on the right *always* have exactly the same size and
    163 # location, and the window on the left and the top window also always have
    164 # exactly the same size and position.  However, each window in the diagram
    165 # plays a subtly different role.
    166 #
    167 # The client window is obvious -- this is the window owned by the client,
    168 # which they created and which we have various ICCCM/EWMH-mandated
    169 # responsibilities towards.  It is also composited.
    170 #
    171 # The purpose of the 'corral' is to keep the client window managed -- we
    172 # select for SubstructureRedirect on it, so that the client cannot resize
    173 # etc. without going through the WM.
    174 #
    175 # These two windows are always managed together, as a unit; an invariant of
    176 # the code is that they always take up exactly the same space on the screen.
    177 # They get reparented back and forth between widgets, and when there are no
    178 # widgets, they get reparented to a "parking area".  For now, we're just using
    179 # the root window as a parking area, so we also map/unmap the corral window
    180 # depending on whether we are parked or not; the corral and window is left
    181 # mapped at all times.
    182 #
    183 # When a particular WindowView controls the underlying client window, then two
    184 # things happen:
    185 #   -- Its size determines the size of the client window.  Ideally they are
    186 #      the same size -- but this is not always the case, because the client
    187 #      may have specified sizing constraints, in which case the client window
    188 #      is the "best fit" to the controlling widget window.
    189 #   -- The client window and its corral are reparented under the widget
    190 #      window, as in the diagram above.  This is necessary to allow mouse
    191 #      events to work -- a WindowView widget can always *look* like the client
    192 #      window is there, through the magic of Composite, but in order for it to
    193 #      *act* like the client window is there in terms of receiving mouse
    194 #      events, it has to actually be there.
    195 #
    196 # We should also have a block comment describing how to create a
    197 # view/"controller" for a WindowModel.
    198 #
    199 # Viewing a (Base)WindowModel is easy.  Connect to the client-contents-changed
    200 # signal.  Every time the window contents is updated, you'll get a message.
    201 # This message is passed a single object e, which has useful members:
    202 #   e.x, e.y, e.width, e.height:
    203 #      The part of the client window that was modified, and needs to be
    204 #      redrawn.
    205 # To get the actual contents of the window to draw, there is a "handle"
    206 # available as the "contents-handle" property on the Composite window.
    207 #
    208 # But what if you'd like to do more than just look at your pretty composited
    209 # windows?  Maybe you'd like to, say, *interact* with them?  Then life is a
    210 # little more complicated.  To make a view "live", we have to move the actual
    211 # client window to be a child of your view window and position it correctly.
    212 # Obviously, only one view can be live at any given time, so we have to figure
    213 # out which one that is.  Supposing we have a WindowModel called "model" and
    214 # a view called "view", then the following pieces come into play:
    215 #   The "ownership-election" signal on window:
    216 #     If a view wants the chance to become live, it must connect to this
    217 #     signal.  When the signal is emitted, its handler should return a tuple
    218 #     of the form:
    219 #       (votes, my_view)
    220 #     Just like a real election, everyone votes for themselves.  The view that
    221 #     gives the highest value to 'votes' becomes the new owner.  However, a
    222 #     view with a negative (< 0) votes value will never become the owner.
    223 #   model.ownership_election():
    224 #     This method (distinct from the ownership-election signal!) triggers an
    225 #     election.  All views MUST call this method whenever they decide their
    226 #     number of votes has changed.  All views MUST call this method when they
    227 #     are destructing themselves (ideally after disconnecting from the
    228 #     ownership-election signal).
    229 #   The "owner" property on window:
    230 #     This records the view that currently owns the window (i.e., the winner
    231 #     of the last election), or None if no view is live.
    232 #   view.take_window(model, window):
    233 #     This method is called on 'view' when it becomes owner of 'model'.  It
    234 #     should reparent 'window' into the appropriate place, and put it at the
    235 #     appropriate place in its window stack.  (The x,y position, however, does
    236 #     not matter.)
    237 #   view.window_size(model):
    238 #     This method is called when the model needs to know how much space it is
    239 #     allocated.  It should return the maximum (width, height) allowed.
    240 #     (However, the model may choose to use less than this.)
    241 #   view.window_position(mode, width, height):
    242 #     This method is called when the model needs to know where it should be
    243 #     located (relative to the parent window the view placed it in).  'width'
    244 #     and 'height' are the size the model window will actually be.  It should
    245 #     return the (x, y) position desired.
    246 #   model.maybe_recalculate_geometry_for(view):
    247 #     This method (potentially) triggers a resize/move of the client window
    248 #     within the view.  If 'view' is not the current owner, is a no-op, which
    249 #     means that views can call it without worrying about whether they are in
    250 #     fact the current owner.
    251 #
    252 # The actual method for choosing 'votes' is not really determined yet.
    253 # Probably it should take into account at least the following factors:
    254 #   -- has focus (or has mouse-over?)
    255 #   -- is visible in a tray/other window, and the tray/other window is visible
    256 #      -- and is focusable
    257 #      -- and is not focusable
    258 #   -- is visible in a tray, and the tray/other window is not visible
    259 #      -- and is focusable
    260 #      -- and is not focusable
    261 #      (NB: Widget.get_ancestor(my.Tray) will give us the nearest ancestor
    262 #      that isinstance(my.Tray), if any.)
    263 #   -- is not visible
    264 #   -- the size of the widget (as a final tie-breaker)
    265 
    266 class Unmanageable(Exception):
    267     pass
    268 
    269 class BaseWindowModel(AutoPropGObjectMixin, gobject.GObject):
    270     __gproperties__ = {
    271         "client-window": (gobject.TYPE_PYOBJECT,
    272                           "gtk.gdk.Window representing the client toplevel", "",
    273                           gobject.PARAM_READABLE),
    274         "geometry": (gobject.TYPE_PYOBJECT,
    275                      "current (border-corrected, relative to parent) coordinates (x, y, w, h) for the window", "",
    276                      gobject.PARAM_READABLE),
    277         "transient-for": (gobject.TYPE_PYOBJECT,
    278                           "Transient for (or None)", "",
    279                           gobject.PARAM_READABLE),
    280         "pid": (gobject.TYPE_INT,
    281                 "PID of owning process", "",
    282                 -1, 65535, -1,
    283                 gobject.PARAM_READABLE),
    284         "opacity": (gobject.TYPE_INT64,
    285                 "Opacity", "",
    286                 -1, 0xffffffff, -1,
    287                 gobject.PARAM_READABLE),
    288         "shape": (gobject.TYPE_PYOBJECT,
    289                           "Window XShape data", "",
    290                           gobject.PARAM_READABLE),
    291         "xid": (gobject.TYPE_INT,
    292                 "X11 window id", "",
    293                 -1, 65535, -1,
    294                 gobject.PARAM_READABLE),
    295         "title": (gobject.TYPE_PYOBJECT,
    296                   "Window title (unicode or None)", "",
    297                   gobject.PARAM_READABLE),
    298         "group-leader": (gobject.TYPE_PYOBJECT,
    299                          "Window group leader as a pair: (xid, gdk window)", "",
    300                          gobject.PARAM_READABLE),
    301         "attention-requested": (gobject.TYPE_BOOLEAN,
    302                                 "Urgency hint from client, or us", "",
    303                                 False,
    304                                 gobject.PARAM_READWRITE),
    305         "can-focus": (gobject.TYPE_BOOLEAN,
    306                       "Does this window ever accept keyboard input?", "",
    307                       True,
    308                       gobject.PARAM_READWRITE),
    309         "has-alpha": (gobject.TYPE_BOOLEAN,
    310                        "Does the window use transparency", "",
    311                        False,
    312                        gobject.PARAM_READABLE),
    313         "bypass-compositor": (gobject.TYPE_INT,
    314                        "hint that the window would benefit from running uncomposited ", "",
    315                        0, 2, 0,
    316                        gobject.PARAM_READABLE),
    317         "fullscreen-monitors": (gobject.TYPE_PYOBJECT,
    318                          "List of 4 monitor indices indicating the top, bottom, left, and right edges of the window when the fullscreen state is enabled", "",
    319                          gobject.PARAM_READABLE),
    320         "fullscreen": (gobject.TYPE_BOOLEAN,
    321                        "Fullscreen-ness of window", "",
    322                        False,
    323                        gobject.PARAM_READWRITE),
    324         "focused": (gobject.TYPE_BOOLEAN,
    325                        "Is the window focused", "",
    326                        False,
    327                        gobject.PARAM_READWRITE),
    328         "maximized": (gobject.TYPE_BOOLEAN,
    329                        "Is the window maximized", "",
    330                        False,
    331                        gobject.PARAM_READWRITE),
    332         "above": (gobject.TYPE_BOOLEAN,
    333                        "Is the window on top of most windows", "",
    334                        False,
    335                        gobject.PARAM_READWRITE),
    336         "below": (gobject.TYPE_BOOLEAN,
    337                        "Is the window below most windows", "",
    338                        False,
    339                        gobject.PARAM_READWRITE),
    340         "shaded": (gobject.TYPE_BOOLEAN,
    341                        "Is the window shaded", "",
    342                        False,
    343                        gobject.PARAM_READWRITE),
    344         "skip-taskbar": (gobject.TYPE_BOOLEAN,
    345                        "Should the window be included on a taskbar", "",
    346                        False,
    347                        gobject.PARAM_READWRITE),
    348         "skip-pager": (gobject.TYPE_BOOLEAN,
    349                        "Should the window be included on a pager", "",
    350                        False,
    351                        gobject.PARAM_READWRITE),
    352         "sticky": (gobject.TYPE_BOOLEAN,
    353                        "Is the window's position fixed on the screen", "",
    354                        False,
    355                        gobject.PARAM_READWRITE),
    356         "strut": (gobject.TYPE_PYOBJECT,
    357                   "Struts requested by window, or None", "",
    358                   gobject.PARAM_READABLE),
    359         "workspace": (gobject.TYPE_UINT,
    360                 "The workspace this window is on", "",
    361                 0, 2**32-1, WORKSPACE_UNSET,
    362                 gobject.PARAM_READWRITE),
     15class OverrideRedirectWindowModel(BaseWindowModel):
     16    __gsignals__ = dict(BaseWindowModel.__common_signals__)
     17    __gproperties__ = dict(BaseWindowModel.__common_properties__)
     18    __gproperties__.update({
    36319        "override-redirect": (gobject.TYPE_BOOLEAN,
    36420                       "Is the window of type override-redirect", "",
    365                        False,
     21                       True,
    36622                       gobject.PARAM_READABLE),
    367         "tray": (gobject.TYPE_BOOLEAN,
    368                        "Is the window a system tray icon", "",
    369                        False,
    370                        gobject.PARAM_READABLE),
    371         "role" : (gobject.TYPE_PYOBJECT,
    372                           "The window's role (ICCCM session management)", "",
    373                           gobject.PARAM_READABLE),
    374         "modal": (gobject.TYPE_PYOBJECT,
    375                           "Modal (boolean)", "",
    376                           gobject.PARAM_READWRITE),
    377         "window-type": (gobject.TYPE_PYOBJECT,
    378                         "Window type",
    379                         "NB, most preferred comes first, then fallbacks",
    380                         gobject.PARAM_READABLE),
    381         }
    382     __gsignals__ = {
    383         "client-contents-changed": one_arg_signal,
    384         "raised"                : one_arg_signal,
    385         "unmanaged"             : one_arg_signal,
    386         "initiate-moveresize"   : one_arg_signal,
     23                        })
     24    _property_names = BaseWindowModel._property_names + ["override-redirect"]
    38725
    388         "grab"                  : one_arg_signal,
    389         "ungrab"                : one_arg_signal,
    390         }
    391     __common_signals__ = {
    392         "bell"                      : one_arg_signal,   #out
    393         "xpra-xkb-event"            : one_arg_signal,
    394         "xpra-shape-event"          : one_arg_signal,
    395         "xpra-configure-event"      : one_arg_signal,
    396         "xpra-unmap-event"          : one_arg_signal,
    397         "xpra-client-message-event" : one_arg_signal,
    398         "xpra-property-notify-event": one_arg_signal,
    399         "xpra-focus-in-event"       : one_arg_signal,
    400         "xpra-focus-out-event"      : one_arg_signal,
    401         }
    402 
    40326    def __init__(self, client_window):
    404         log("new window %#x", client_window.xid)
    405         super(BaseWindowModel, self).__init__()
    406         self.client_window = client_window
    407         self.client_window_saved_events = self.client_window.get_events()
    408         self._managed = False
    409         self._managed_handlers = []
    410         self._setup_done = False
    411         self._input_field = True            # The WM_HINTS input field
    412         self._geometry = None
    413         self._damage_forward_handle = None
    414         self._internal_set_property("client-window", client_window)
    415         use_xshm = USE_XSHM and not self.is_tray()
    416         self._composite = CompositeHelper(self.client_window, False, use_xshm)
    417         if X11Window.displayHasXShape():
    418             X11Window.XShapeSelectInput(self.client_window.xid)
    419         self.property_names = ["pid", "transient-for", "fullscreen", "fullscreen-monitors", "bypass-compositor", "maximized", "window-type", "role", "group-leader",
    420                                "xid", "workspace", "has-alpha", "opacity", "strut", "shape"]
    421 
    422     def get_property_names(self):
    423         return self.property_names
    424 
    425     def get_dynamic_property_names(self):
    426         return ("title", "size-hints", "fullscreen", "fullscreen-monitors", "bypass-compositor", "maximized", "opacity", "workspace", "strut", "shape")
    427 
    428 
    429     def managed_connect(self, detailed_signal, handler, *args):
    430         """ connects a signal handler and makes sure we will clean it up on unmanage() """
    431         handler_id = self.connect(detailed_signal, handler, *args)
    432         self._managed_handlers.append(handler_id)
    433         return handler_id
    434 
    435     def managed_disconnect(self):
    436         for handler_id in self._managed_handlers:
    437             self.disconnect(handler_id)
    438         self._managed_handlers = []
    439 
    440     def call_setup(self):
    441         try:
    442             with xsync:
    443                 self._geometry = X11Window.geometry_with_border(self.client_window.xid)
    444                 self._read_initial_X11_properties()
    445         except XError as e:
    446             raise Unmanageable(e)
    447         add_event_receiver(self.client_window, self)
    448         # Keith Packard says that composite state is undefined following a
    449         # reparent, so I'm not sure doing this here in the superclass,
    450         # before we reparent, actually works... let's wait and see.
    451         try:
    452             with xsync:
    453                 self._composite.setup()
    454         except XError as e:
    455             remove_event_receiver(self.client_window, self)
    456             log("window %#x does not support compositing: %s", self.client_window.xid, e)
    457             with xswallow:
    458                 self._composite.destroy()
    459             self._composite = None
    460             raise Unmanageable(e)
    461         #compositing is now enabled, from now on we need to call setup_failed to clean things up
    462         self._managed = True
    463         try:
    464             with xsync:
    465                 self.setup()
    466         except XError as e:
    467             try:
    468                 with xsync:
    469                     self.setup_failed(e)
    470             except Exception as ex:
    471                 log.error("error in cleanup handler: %s", ex)
    472             raise Unmanageable(e)
    473         self._setup_done = True
    474 
    475     def setup_failed(self, e):
    476         log("cannot manage %#x: %s", self.client_window.xid, e)
    477         self.do_unmanaged(False)
    478 
    479     def setup(self):
    480         # Start listening for important events.
    481         self.client_window.set_events(self.client_window_saved_events
    482                                       | gtk.gdk.STRUCTURE_MASK
    483                                       | gtk.gdk.PROPERTY_CHANGE_MASK
    484                                       | gtk.gdk.FOCUS_CHANGE_MASK)
    485         h = self._composite.connect("contents-changed", self._forward_contents_changed)
    486         self._damage_forward_handle = h
    487 
    488     def prop_get(self, key, ptype, ignore_errors=None, raise_xerrors=False):
    489         # Utility wrapper for prop_get on the client_window
    490         # also allows us to ignore property errors during setup_client
    491         if ignore_errors is None and (not self._setup_done or not self._managed):
    492             ignore_errors = True
    493         return prop_get(self.client_window, key, ptype, ignore_errors=bool(ignore_errors), raise_xerrors=raise_xerrors)
    494 
    495     def is_managed(self):
    496         return self._managed
    497 
    498     def is_shadow(self):
    499         return False
    500 
    501     def get_default_window_icon(self):
    502         return None
    503 
    504 
    505     def _forward_contents_changed(self, obj, event):
    506         if self._managed:
    507             self.emit("client-contents-changed", event)
    508 
    509     def acknowledge_changes(self):
    510         assert self._composite, "composite window destroyed outside the UI thread?"
    511         self._composite.acknowledge_changes()
    512 
    513     ################################
    514     # Property reading
    515     ################################
    516 
    517     def do_xpra_property_notify_event(self, event):
    518         assert event.window is self.client_window
    519         self._handle_property_change(str(event.atom))
    520 
    521     _property_handlers = {}
    522 
    523     def _handle_property_change(self, name):
    524         log("Property changed on %#x: %s", self.client_window.xid, name)
    525         if name in PROPERTIES_DEBUG:
    526             log.info("%s=%s", name, self.prop_get(name, PROPERTIES_DEBUG[name], True, False))
    527         if name in PROPERTIES_IGNORED:
    528             return
    529         self._call_property_handler(name)
    530 
    531     def _call_property_handler(self, name):
    532         if name in self._property_handlers:
    533             self._property_handlers[name](self)
    534 
    535     def do_xpra_configure_event(self, event):
    536         if self.client_window is None or not self._managed:
    537             return
    538         #shouldn't the border width always be 0?
    539         geom = (event.x, event.y, event.width, event.height, event.border_width)
    540         log("BaseWindowModel.do_xpra_configure_event(%s) client_window=%#x, old geometry=%s, new geometry=%s", event, self.client_window.xid, self._geometry, geom)
    541         if geom!=self._geometry:
    542             self._geometry = geom
    543             #X11Window.MoveResizeWindow(self.client_window.xid, )
    544             self.notify("geometry")
    545 
    546     def do_get_property_geometry(self, pspec):
    547         if self._geometry is None:
    548             with xsync:
    549                 xwin = self.client_window.xid
    550                 self._geometry = X11Window.geometry_with_border(xwin)
    551                 log("BaseWindowModel.do_get_property_geometry() synced update: geometry(%#x)=%s", xwin, self._geometry)
    552         x, y, w, h, b = self._geometry
    553         return (x, y, w + 2*b, h + 2*b)
    554 
    555     def get_position(self):
    556         return self.do_get_property_geometry(None)[:2]
    557 
    558     def unmanage(self, exiting=False):
    559         if self._managed:
    560             self.emit("unmanaged", exiting)
    561 
    562     def do_unmanaged(self, wm_exiting):
    563         if not self._managed:
    564             return
    565         self._managed = False
    566         log("do_unmanaged(%s) damage_forward_handle=%s, composite=%s", wm_exiting, self._damage_forward_handle, self._composite)
    567         remove_event_receiver(self.client_window, self)
    568         glib.idle_add(self.managed_disconnect)
    569         if self._composite:
    570             if self._damage_forward_handle:
    571                 self._composite.disconnect(self._damage_forward_handle)
    572                 self._damage_forward_handle = None
    573             self._composite.destroy()
    574             self._composite = None
    575 
    576     def _read_initial_properties(self):
    577         transient_for = self.prop_get("WM_TRANSIENT_FOR", "window")
    578         # May be None
    579         self._internal_set_property("transient-for", transient_for)
    580 
    581         window_types = self.prop_get("_NET_WM_WINDOW_TYPE", ["atom"])
    582         if not window_types:
    583             window_type = self._guess_window_type(transient_for)
    584             window_types = [gtk.gdk.atom_intern(window_type)]
    585         #normalize them (hide _NET_WM_WINDOW_TYPE prefix):
    586         window_types = [str(wt).replace("_NET_WM_WINDOW_TYPE_", "").replace("_NET_WM_TYPE_", "") for wt in window_types]
    587         self._internal_set_property("window-type", window_types)
    588         self._internal_set_property("xid", self.client_window.xid)
    589         self._internal_set_property("pid", self.prop_get("_NET_WM_PID", "u32") or -1)
    590         self._internal_set_property("role", self.prop_get("WM_WINDOW_ROLE", "latin1"))
    591         for mutable in ["WM_NAME", "_NET_WM_NAME", "_NET_WM_WINDOW_OPACITY", "_NET_WM_DESKTOP",
    592                         "_NET_WM_BYPASS_COMPOSITOR", "_NET_WM_FULLSCREEN_MONITORS",
    593                         "_NET_WM_STRUT", "_NET_WM_STRUT_PARTIAL"]:
    594             self._call_property_handler(mutable)
    595 
    596     def _read_initial_X11_properties(self):
    597         self._internal_set_property("has-alpha", X11Window.get_depth(self.client_window.xid)==32)
    598         self._internal_set_property("shape", self._read_xshape())
    599 
    600     def _read_xshape(self):
    601         xid = self.client_window.xid
    602         extents = X11Window.XShapeQueryExtents(xid)
    603         if not extents:
    604             shapelog("read_shape for window %#x: no extents", xid)
    605             return {}
    606         v = {}
    607         #w,h = X11Window.getGeometry(xid)[2:4]
    608         bextents = extents[0]
    609         cextents = extents[1]
    610         if bextents[0]==0 and cextents[0]==0:
    611             shapelog("read_shape for window %#x: none enabled", xid)
    612             return {}
    613         v["Bounding.extents"] = bextents
    614         v["Clip.extents"] = cextents
    615         for kind in SHAPE_KIND.keys():
    616             kind_name = SHAPE_KIND[kind]
    617             rectangles = X11Window.XShapeGetRectangles(xid, kind)
    618             v[kind_name+".rectangles"] = rectangles
    619         shapelog("_read_shape()=%s", v)
    620         return v
    621 
    622 
    623     def _handle_workspace_change(self):
    624         workspace = self.prop_get("_NET_WM_DESKTOP", "u32", True)
    625         if workspace is None:
    626             workspace = WORKSPACE_UNSET
    627         workspacelog("_NET_WM_DESKTOP=%s for window %#x", workspacestr(workspace), self.client_window.xid)
    628         self._internal_set_property("workspace", workspace)
    629     _property_handlers["_NET_WM_DESKTOP"] = _handle_workspace_change
    630 
    631     def move_to_workspace(self, workspace):
    632         #we send a message to ourselves, we could also just update the property
    633         current = self.get_property("workspace")
    634         if current==workspace:
    635             workspacelog("move_to_workspace(%s) unchanged", workspacestr(workspace))
    636             return
    637         workspacelog("move_to_workspace(%s) current=%s", workspacestr(workspace), workspacestr(current))
    638         with xswallow:
    639             if workspace==WORKSPACE_UNSET:
    640                 workspacelog("removing _NET_WM_DESKTOP property from window %#x", self.client_window.xid)
    641                 X11Window.XDeleteProperty(self.client_window.xid, "_NET_WM_DESKTOP")
    642             else:
    643                 workspacelog("setting _NET_WM_DESKTOP=%s on window %#x", workspacestr(workspace), self.client_window.xid)
    644                 prop_set(self.client_window, "_NET_WM_DESKTOP", "u32", workspace)
    645 
    646 
    647     def _handle_fullscreen_monitors_change(self):
    648         fsm = self.prop_get("_NET_WM_FULLSCREEN_MONITORS", ["u32"], True)
    649         self._internal_set_property("fullscreen-monitors", fsm)
    650         log("fullscreen-monitors=%s", fsm)
    651     _property_handlers["_NET_WM_FULLSCREEN_MONITORS"] = _handle_fullscreen_monitors_change
    652 
    653 
    654     def _handle_bypass_compositor_change(self):
    655         bypass = self.prop_get("_NET_WM_BYPASS_COMPOSITOR", "u32", True) or 0
    656         self._internal_set_property("bypass-compositor", bypass)
    657         log("bypass-compositor=%s", bypass)
    658     _property_handlers["_NET_WM_BYPASS_COMPOSITOR"] = _handle_bypass_compositor_change
    659 
    660 
    661     def _handle_wm_strut(self):
    662         partial = self.prop_get("_NET_WM_STRUT_PARTIAL", "strut-partial")
    663         if partial is not None:
    664             self._internal_set_property("strut", partial)
    665             return
    666         full = self.prop_get("_NET_WM_STRUT", "strut")
    667         # Might be None:
    668         self._internal_set_property("strut", full)
    669 
    670     _property_handlers["_NET_WM_STRUT"] = _handle_wm_strut
    671     _property_handlers["_NET_WM_STRUT_PARTIAL"] = _handle_wm_strut
    672 
    673 
    674     def _handle_opacity_change(self):
    675         opacity = self.prop_get("_NET_WM_WINDOW_OPACITY", "u32", True) or -1
    676         self._internal_set_property("opacity", opacity)
    677     _property_handlers["_NET_WM_WINDOW_OPACITY"] = _handle_opacity_change
    678 
    679     def _handle_title_change(self):
    680         name = self.prop_get("_NET_WM_NAME", "utf8", True)
    681         if name is None:
    682             name = self.prop_get("WM_NAME", "latin1", True)
    683         self._internal_set_property("title", sanestr(name))
    684 
    685     _property_handlers["WM_NAME"] = _handle_title_change
    686     _property_handlers["_NET_WM_NAME"] = _handle_title_change
    687 
    688     def _handle_wm_hints(self):
    689         with xswallow:
    690             wm_hints = X11Window.getWMHints(self.client_window.xid)
    691         if wm_hints is None:
    692             return
    693         # GdkWindow or None
    694         group_leader = None
    695         if "window_group" in wm_hints:
    696             xid = wm_hints.get("window_group")
    697             try:
    698                 group_leader = xid, get_pywindow(xid)
    699             except:
    700                 group_leader = xid, None
    701         self._internal_set_property("group-leader", group_leader)
    702 
    703         if "urgency" in wm_hints:
    704             self.set_property("attention-requested", True)
    705 
    706         _input = wm_hints.get("input")
    707         log("wm_hints.input = %s", _input)
    708         #we only set this value once:
    709         #(input_field always starts as True, and we then set it to an int)
    710         if self._input_field is True and _input is not None:
    711             #keep the value as an int to differentiate from the start value:
    712             self._input_field = int(_input)
    713             if bool(self._input_field):
    714                 self.notify("can-focus")
    715 
    716     _property_handlers["WM_HINTS"] = _handle_wm_hints
    717 
    718     def _guess_window_type(self, transient_for):
    719         if transient_for is not None:
    720             # EWMH says that even if it's transient-for, we MUST check to
    721             # see if it's override-redirect (and if so treat as NORMAL).
    722             # But we wouldn't be here if this was override-redirect.
    723             # (OverrideRedirectWindowModel overrides this method)
    724             return "_NET_WM_TYPE_DIALOG"
    725         return "_NET_WM_WINDOW_TYPE_NORMAL"
    726 
    727     def is_tray(self):
    728         return False
    729 
    730     def uses_XShm(self):
    731         return self._composite and self._composite.get_property("shm-handle") is not None
    732 
    733     def has_alpha(self):
    734         return self.get_property("has-alpha")
    735 
    736     def get_image(self, x, y, width, height, logger=log.debug):
    737         handle = self._composite.get_property("contents-handle")
    738         if handle is None:
    739             logger("get_image(..) pixmap is None for window %#x", self.client_window.xid)
    740             return  None
    741 
    742         #try XShm:
    743         try:
    744             #logger("get_image(%s, %s, %s, %s) geometry=%s", x, y, width, height, self._geometry[:4])
    745             shm = self._composite.get_property("shm-handle")
    746             #logger("get_image(..) XShm handle: %s, handle=%s, pixmap=%s", shm, handle, handle.get_pixmap())
    747             if shm is not None:
    748                 with xsync:
    749                     shm_image = shm.get_image(handle.get_pixmap(), x, y, width, height)
    750                 #logger("get_image(..) XShm image: %s", shm_image)
    751                 if shm_image:
    752                     return shm_image
    753         except Exception as e:
    754             if type(e)==XError and e.msg=="BadMatch":
    755                 logger("get_image(%s, %s, %s, %s) get_image BadMatch ignored (window already gone?)", x, y, width, height)
    756             else:
    757                 log.warn("get_image(%s, %s, %s, %s) get_image %s", x, y, width, height, e, exc_info=True)
    758 
    759         try:
    760             w = min(handle.get_width(), width)
    761             h = min(handle.get_height(), height)
    762             if w!=width or h!=height:
    763                 logger("get_image(%s, %s, %s, %s) clamped to pixmap dimensions: %sx%s", x, y, width, height, w, h)
    764             with xsync:
    765                 return handle.get_image(x, y, w, h)
    766         except Exception as e:
    767             if type(e)==XError and e.msg=="BadMatch":
    768                 logger("get_image(%s, %s, %s, %s) get_image BadMatch ignored (window already gone?)", x, y, width, height)
    769             else:
    770                 log.warn("get_image(%s, %s, %s, %s) get_image %s", x, y, width, height, e, exc_info=True)
    771             return None
    772 
    773 
    774     def do_xpra_shape_event(self, event):
    775         shapelog("shape event: %s, kind=%s", event, SHAPE_KIND.get(event.kind, event.kind))
    776         cur_shape = self.get_property("shape")
    777         if cur_shape and cur_shape.get("serial", 0)>=event.serial:
    778             shapelog("same or older xshape serial no: %#x", event.serial)
    779             return
    780         #remove serial before comparing dicts:
    781         try:
    782             cur_shape["serial"]
    783         except:
    784             pass
    785         #read new xshape:
    786         v = self._read_xshape()
    787         if cur_shape==v:
    788             shapelog("xshape unchanged")
    789             return
    790         v["serial"] = int(event.serial)
    791         shapelog("xshape updated with serial %#x", event.serial)
    792         self._internal_set_property("shape", v)
    793 
    794 
    795     def do_xpra_xkb_event(self, event):
    796         log("WindowModel.do_xpra_xkb_event(%r)" % event)
    797         if event.type!="bell":
    798             log.error("WindowModel.do_xpra_xkb_event(%r) unknown event type: %s" % (event, event.type))
    799             return
    800         event.window_model = self
    801         self.emit("bell", event)
    802 
    803     def do_xpra_client_message_event(self, event):
    804         log("do_xpra_client_message_event(%s)", event)
    805         if not event.data or len(event.data)!=5:
    806             log.warn("invalid event data: %s", event.data)
    807             return
    808         if not self.process_client_message_event(event):
    809             log.warn("do_xpra_client_message_event(%s) not handled", event)
    810 
    811     def process_client_message_event(self, event):
    812         #most messages are only handled in WindowModel,
    813         #OR windows and trays should not be receiving any other message
    814         if event.message_type=="_NET_CLOSE_WINDOW":
    815             log.info("_NET_CLOSE_WINDOW received by %s", self)
    816             self.request_close()
    817             return True
    818         return False
    819 
    820 
    821     def set_active(self):
    822         prop_set(self.client_window.get_screen().get_root_window(), "_NET_ACTIVE_WINDOW", "u32", self.client_window.xid)
    823 
    824 
    825     def do_xpra_focus_in_event(self, event):
    826         grablog("focus_in_event(%s) mode=%s, detail=%s",
    827             event, GRAB_CONSTANTS.get(event.mode), DETAIL_CONSTANTS.get(event.detail, event.detail))
    828         if event.mode==NotifyNormal and event.detail==NotifyNonlinearVirtual:
    829             self.emit("raised", event)
    830         else:
    831             self.may_emit_grab(event)
    832 
    833     def do_xpra_focus_out_event(self, event):
    834         grablog("focus_out_event(%s) mode=%s, detail=%s",
    835             event, GRAB_CONSTANTS.get(event.mode), DETAIL_CONSTANTS.get(event.detail, event.detail))
    836         self.may_emit_grab(event)
    837 
    838     def may_emit_grab(self, event):
    839         if event.mode==NotifyGrab:
    840             grablog("emitting grab on %s", self)
    841             self.emit("grab", event)
    842         if event.mode==NotifyUngrab:
    843             grablog("emitting ungrab on %s", self)
    844             self.emit("ungrab", event)
    845 
    846 
    847 gobject.type_register(BaseWindowModel)
    848 
    849 
    850 # FIXME: EWMH says that O-R windows should set various properties just like
    851 # ordinary managed windows; so some of that code should get pushed up into the
    852 # superclass sooner or later.  When someone cares, presumably.
    853 class OverrideRedirectWindowModel(BaseWindowModel):
    854     __gsignals__ = BaseWindowModel.__common_signals__.copy()
    855 
    856     def __init__(self, client_window):
    85727        super(OverrideRedirectWindowModel, self).__init__(client_window)
    858         self.property_names.append("override-redirect")
    85928
    86029    def setup(self):
    86130        self._read_initial_properties()
    862         BaseWindowModel.setup(self)
     31        super(OverrideRedirectWindowModel, self).setup()
    86332        # So now if the window becomes unmapped in the future then we will
    86433        # notice... but it might be unmapped already, and any event
    86534        # already generated, and our request for that event is too late!
     
    87140            raise Unmanageable("failed to get damage handle")
    87241
    87342    def _read_initial_properties(self):
    874         BaseWindowModel._read_initial_properties(self)
     43        super(BaseWindowModel, self)._read_initial_properties()
    87544        self._internal_set_property("override-redirect", True)
    87645
    87746    def _guess_window_type(self, transient_for):
     
    88453        ww, wh = self._geometry[2:4]
    88554        return ww, wh
    88655
    887     def is_OR(self):
    888         return  True
    889 
    89056    def raise_window(self):
    89157        self.client_window.raise_()
    89258
     
    89359    def __repr__(self):
    89460        return "OverrideRedirectWindowModel(%#x)" % self.client_window.xid
    89561
    896 
    89762gobject.type_register(OverrideRedirectWindowModel)
    898 
    899 
    900 class SystemTrayWindowModel(OverrideRedirectWindowModel):
    901 
    902     def __init__(self, client_window):
    903         OverrideRedirectWindowModel.__init__(self, client_window)
    904         self.property_names = ["pid", "role", "xid", "has-alpha", "tray", "title"]
    905 
    906     def is_tray(self):
    907         return  True
    908 
    909     def has_alpha(self):
    910         return  True
    911 
    912     def _read_initial_properties(self):
    913         BaseWindowModel._read_initial_properties(self)
    914         self._internal_set_property("tray", True)
    915         self._internal_set_property("has-alpha", True)
    916 
    917     def move_resize(self, x, y, width, height):
    918         #Used by clients to tell us where the tray is located on screen
    919         log("SystemTrayWindowModel.move_resize(%s, %s, %s, %s)", x, y, width, height)
    920         self.client_window.move_resize(x, y, width, height)
    921         border = self._geometry[4]
    922         self._geometry = (x, y, width, height, border)
    923 
    924     def __repr__(self):
    925         return "SystemTrayWindowModel(%#x)" % self.client_window.xid
    926 
    927 
    928 class WindowModel(BaseWindowModel):
    929     """This represents a managed client window.  It allows one to produce
    930     widgets that view that client window in various ways."""
    931 
    932     _NET_WM_ALLOWED_ACTIONS = [
    933         "_NET_WM_ACTION_CLOSE",
    934         "_NET_WM_ACTION_MOVE",
    935         "_NET_WM_ACTION_RESIZE",
    936         "_NET_WM_ACTION_FULLSCREEN",
    937         "_NET_WM_ACTION_MINIMIZE",
    938         "_NET_WM_ACTION_SHADE",
    939         "_NET_WM_ACTION_STICK",
    940         "_NET_WM_ACTION_MAXIMIZE_HORZ",
    941         "_NET_WM_ACTION_MAXIMIZE_VERT",
    942         "_NET_WM_ACTION_CHANGE_DESKTOP",
    943         "_NET_WM_ACTION_ABOVE",
    944         "_NET_WM_ACTION_BELOW",
    945         ]
    946 
    947     __gproperties__ = {
    948         # Interesting properties of the client window, that will be
    949         # automatically kept up to date:
    950         "actual-size": (gobject.TYPE_PYOBJECT,
    951                         "Size of client window (actual (width,height))", "",
    952                         gobject.PARAM_READABLE),
    953         "user-friendly-size": (gobject.TYPE_PYOBJECT,
    954                                "Description of client window size for user", "",
    955                                gobject.PARAM_READABLE),
    956         "requested-position": (gobject.TYPE_PYOBJECT,
    957                                "Client-requested position on screen", "",
    958                                gobject.PARAM_READABLE),
    959         "requested-size": (gobject.TYPE_PYOBJECT,
    960                            "Client-requested size on screen", "",
    961                            gobject.PARAM_READABLE),
    962         "size-hints": (gobject.TYPE_PYOBJECT,
    963                        "Client hints on constraining its size", "",
    964                        gobject.PARAM_READABLE),
    965         "class-instance": (gobject.TYPE_PYOBJECT,
    966                            "Classic X 'class' and 'instance'", "",
    967                            gobject.PARAM_READABLE),
    968         "protocols": (gobject.TYPE_PYOBJECT,
    969                       "Supported WM protocols", "",
    970                       gobject.PARAM_READABLE),
    971         "frame": (gobject.TYPE_PYOBJECT,
    972                       "Size of the window frame", "",
    973                       gobject.PARAM_READWRITE),
    974         "client-machine": (gobject.TYPE_PYOBJECT,
    975                            "Host where client process is running", "",
    976                            gobject.PARAM_READABLE),
    977         "command": (gobject.TYPE_PYOBJECT,
    978                            "Command used to start or restart the client", "",
    979                            gobject.PARAM_READABLE),
    980         # Toggling this property does not actually make the window iconified,
    981         # i.e. make it appear or disappear from the screen -- it merely
    982         # updates the various window manager properties that inform the world
    983         # whether or not the window is iconified.
    984         "iconic": (gobject.TYPE_BOOLEAN,
    985                    "ICCCM 'iconic' state -- any sort of 'not on desktop'.", "",
    986                    False,
    987                    gobject.PARAM_READWRITE),
    988         "state": (gobject.TYPE_PYOBJECT,
    989                   "State, as per _NET_WM_STATE", "",
    990                   gobject.PARAM_READABLE),
    991         "icon-title": (gobject.TYPE_PYOBJECT,
    992                        "Icon title (unicode or None)", "",
    993                        gobject.PARAM_READABLE),
    994         "icon": (gobject.TYPE_PYOBJECT,
    995                  "Icon (local Cairo surface)", "",
    996                  gobject.PARAM_READABLE),
    997         "icon-pixmap": (gobject.TYPE_PYOBJECT,
    998                         "Icon (server Pixmap)", "",
    999                         gobject.PARAM_READABLE),
    1000 
    1001         "owner": (gobject.TYPE_PYOBJECT,
    1002                   "Owner", "",
    1003                   gobject.PARAM_READABLE),
    1004         "decorations": (gobject.TYPE_BOOLEAN,
    1005                        "Should the window decorations be shown", "",
    1006                        True,
    1007                        gobject.PARAM_READABLE),
    1008         }
    1009     __gsignals__ = BaseWindowModel.__common_signals__.copy()
    1010     __gsignals__.update({
    1011         "ownership-election"            : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_PYOBJECT, (), non_none_list_accumulator),
    1012         "child-map-request-event"       : one_arg_signal,
    1013         "child-configure-request-event" : one_arg_signal,
    1014         "xpra-destroy-event"            : one_arg_signal,
    1015         })
    1016 
    1017     def __init__(self, parking_window, client_window):
    1018         """Register a new client window with the WM.
    1019 
    1020         Raises an Unmanageable exception if this window should not be
    1021         managed, for whatever reason.  ATM, this mostly means that the window
    1022         died somehow before we could do anything with it."""
    1023 
    1024         super(WindowModel, self).__init__(client_window)
    1025         self.parking_window = parking_window
    1026         self.corral_window = None
    1027         self.in_save_set = False
    1028         self.client_reparented = False
    1029         self.last_unmap_serial = 0
    1030         self.kill_count = 0
    1031 
    1032         self.connect("notify::iconic", self._handle_iconic_update)
    1033 
    1034         self.property_names += ["title", "icon-title", "size-hints", "class-instance", "icon", "client-machine", "command",
    1035                                 "modal", "decorations",
    1036                                 "above", "below", "shaded", "sticky", "skip-taskbar", "skip-pager"]
    1037         self.call_setup()
    1038 
    1039     def setup(self):
    1040         BaseWindowModel.setup(self)
    1041 
    1042         x, y, w, h, _ = self.client_window.get_geometry()
    1043         # We enable PROPERTY_CHANGE_MASK so that we can call
    1044         # x11_get_server_time on this window.
    1045         self.corral_window = gtk.gdk.Window(self.parking_window,
    1046                                             x = x, y = y, width =w, height= h,
    1047                                             window_type=gtk.gdk.WINDOW_CHILD,
    1048                                             wclass=gtk.gdk.INPUT_OUTPUT,
    1049                                             event_mask=gtk.gdk.PROPERTY_CHANGE_MASK,
    1050                                             title = "CorralWindow-%#x" % self.client_window.xid)
    1051         log("setup() corral_window=%#x", self.corral_window.xid)
    1052         prop_set(self.corral_window, "_NET_WM_NAME", "utf8", u"Xpra-CorralWindow-%#x" % self.client_window.xid)
    1053         X11Window.substructureRedirect(self.corral_window.xid)
    1054         add_event_receiver(self.corral_window, self)
    1055 
    1056         # The child might already be mapped, in case we inherited it from
    1057         # a previous window manager.  If so, we unmap it now, and save the
    1058         # serial number of the request -- this way, when we get an
    1059         # UnmapNotify later, we'll know that it's just from us unmapping
    1060         # the window, not from the client withdrawing the window.
    1061         if X11Window.is_mapped(self.client_window.xid):
    1062             log("hiding inherited window")
    1063             self.last_unmap_serial = X11Window.Unmap(self.client_window.xid)
    1064 
    1065         # Process properties
    1066         self._read_initial_properties()
    1067         self._write_initial_properties_and_setup()
    1068 
    1069         log("setup() adding to save set")
    1070         X11Window.XAddToSaveSet(self.client_window.xid)
    1071         self.in_save_set = True
    1072 
    1073         log("setup() reparenting")
    1074         X11Window.Reparent(self.client_window.xid, self.corral_window.xid, 0, 0)
    1075         self.client_reparented = True
    1076 
    1077         log("setup() geometry")
    1078         w,h = X11Window.getGeometry(self.client_window.xid)[2:4]
    1079         hints = self.get_property("size-hints")
    1080         log("setup() hints=%s size=%ix%i", hints, w, h)
    1081         self._sanitize_size_hints(hints)
    1082         nw, nh = calc_constrained_size(w, h, hints)[:2]
    1083         if nw>=MAX_WINDOW_SIZE or nh>=MAX_WINDOW_SIZE:
    1084             #we can't handle windows that big!
    1085             raise Unmanageable("window constrained size is too large: %sx%s (from client geometry: %s,%s with size hints=%s)" % (nw, nh, w, h, hints))
    1086         log("setup() resizing windows to %sx%s", nw, nh)
    1087         self.corral_window.resize(nw, nh)
    1088         self.client_window.resize(nw, nh)
    1089         self.client_window.show_unraised()
    1090         #this is here to trigger X11 errors if any are pending
    1091         #or if the window is deleted already:
    1092         self.client_window.get_geometry()
    1093         self._internal_set_property("actual-size", (nw, nh))
    1094 
    1095     def get_dynamic_property_names(self):
    1096         return list(BaseWindowModel.get_dynamic_property_names(self))+["icon", "icon-title", "size-hints", "iconic", "decorations", "modal",
    1097                                                                        "above", "below", "shaded", "sticky", "skip-taskbar", "skip-pager"]
    1098 
    1099 
    1100     def is_OR(self):
    1101         return  False
    1102 
    1103     def raise_window(self):
    1104         self.corral_window.raise_()
    1105 
    1106     def get_dimensions(self):
    1107         return  self.get_property("actual-size")
    1108 
    1109 
    1110     def process_client_message_event(self, event):
    1111         if BaseWindowModel.process_client_message_event(self, event):
    1112             #already handled
    1113             return True
    1114         # FIXME
    1115         # Need to listen for:
    1116         #   _NET_CURRENT_DESKTOP
    1117         #   _NET_WM_PING responses
    1118         # and maybe:
    1119         #   _NET_RESTACK_WINDOW
    1120         #   _NET_WM_STATE (more fully)
    1121         def update_wm_state(prop):
    1122             current = self.get_property(prop)
    1123             mode = event.data[0]
    1124             if mode==_NET_WM_STATE_ADD:
    1125                 v = True
    1126             elif mode==_NET_WM_STATE_REMOVE:
    1127                 v = False
    1128             elif mode==_NET_WM_STATE_TOGGLE:
    1129                 v = not bool(current)
    1130             else:
    1131                 log.warn("invalid mode for _NET_WM_STATE: %s", mode)
    1132                 return
    1133             log("do_xpra_client_message_event(%s) window %s=%s after %s (current state=%s)", event, prop, v, STATE_STRING.get(mode, mode), current)
    1134             if v!=current:
    1135                 self.set_property(prop, v)
    1136 
    1137         if event.message_type=="_NET_WM_STATE":
    1138             atom1 = get_pyatom(event.window, event.data[1])
    1139             log("_NET_WM_STATE: %s", atom1)
    1140             if atom1=="_NET_WM_STATE_FULLSCREEN":
    1141                 update_wm_state("fullscreen")
    1142             elif atom1=="_NET_WM_STATE_ABOVE":
    1143                 update_wm_state("above")
    1144             elif atom1=="_NET_WM_STATE_BELOW":
    1145                 update_wm_state("below")
    1146             elif atom1=="_NET_WM_STATE_SHADED":
    1147                 update_wm_state("shaded")
    1148             elif atom1=="_NET_WM_STATE_STICKY":
    1149                 update_wm_state("sticky")
    1150             elif atom1=="_NET_WM_STATE_SKIP_TASKBAR":
    1151                 update_wm_state("skip-taskbar")
    1152             elif atom1=="_NET_WM_STATE_SKIP_PAGER":
    1153                 update_wm_state("skip-pager")
    1154                 get_pyatom(event.window, event.data[2])
    1155             elif atom1 in ("_NET_WM_STATE_MAXIMIZED_VERT", "_NET_WM_STATE_MAXIMIZED_HORZ"):
    1156                 atom2 = get_pyatom(event.window, event.data[2])
    1157                 #we only have one state for both, so we require both to be set:
    1158                 if atom1!=atom2 and atom2 in ("_NET_WM_STATE_MAXIMIZED_VERT", "_NET_WM_STATE_MAXIMIZED_HORZ"):
    1159                     update_wm_state("maximized")
    1160             elif atom1=="_NET_WM_STATE_HIDDEN":