root/samurai-x2/samuraix/screen.py

Revision 00f96d90c182cf75a5c269c3366c15c65322cf99, 16.8 KB (checked in by Friedrich Weber <fred@…>, 14 months ago)

fixed orphan references to clients, they're now collected properly.

  • Property mode set to 100644
Line 
1# Copyright (c) 2008-2009, samurai-x.org
2# All rights reserved.
3#
4# Redistribution and use in source and binary forms, with or without
5# modification, are permitted provided that the following conditions are met:
6#     * Redistributions of source code must retain the above copyright
7#       notice, this list of conditions and the following disclaimer.
8#     * Redistributions in binary form must reproduce the above copyright
9#       notice, this list of conditions and the following disclaimer in the
10#       documentation and/or other materials provided with the distribution.
11#     * Neither the name of the samurai-x.org nor the
12#       names of its contributors may be used to endorse or promote products
13#       derived from this software without specific prior written permission.
14#
15# THIS SOFTWARE IS PROVIDED BY SAMURAI-X.ORG ``AS IS'' AND ANY
16# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
17# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
18# DISCLAIMED. IN NO EVENT SHALL SAMURAI-X.ORG  BE LIABLE FOR ANY
19# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
20# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
21# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
22# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
23# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
24# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
25
26import logging
27log = logging.getLogger(__name__)
28
29import sys
30import gc
31
32from ooxcb import XNone
33from ooxcb.protocol import xproto
34from ooxcb.protocol.xproto import EventMask
35
36from .client import Client
37from .rect import Rect
38from .base import SXObject
39from .util import ClientMessageHandlers
40
41def configure_request_to_dict(evt):
42    """
43        Convert the :class:`ooxcb.xproto.ConfigureRequestEvent` *evt*
44        to a dictionary containing only the values that are requested
45        to be changed and return it.
46    """
47    cnf = {}
48    mask = evt.value_mask
49    # TODO: get rid of that boilerplate code
50    if mask & xproto.ConfigWindow.X:
51        cnf['x'] = evt.x
52    if mask & xproto.ConfigWindow.Y:
53        cnf['y'] = evt.y
54    if mask & xproto.ConfigWindow.Width:
55        cnf['width'] = evt.width
56    if mask & xproto.ConfigWindow.Height:
57        cnf['height'] = evt.height
58    if mask & xproto.ConfigWindow.BorderWidth:
59        cnf['border_width'] = evt.border_width
60    if mask & xproto.ConfigWindow.Sibling:
61        cnf['sibling'] = evt.sibling # does that work?
62    if mask & xproto.ConfigWindow.StackMode:
63        cnf['stack_mode'] = evt.stack_mode
64    return cnf
65
66class Screen(SXObject):
67    """
68        A wrapper for a physical X screen. For many users, there will
69        only be one Screen. For some others, there will be more.
70
71        The class attribute client_class is the class that will be used
72        to create new clients. You can change it, but that's maybe not
73        such a good idea.
74
75    """
76    client_class = Client
77
78    def __init__(self, app, num):
79        SXObject.__init__(self)
80
81        self.app = app
82        self.conn = app.conn
83
84        self.clients = set()
85        self.client_message_handlers = ClientMessageHandlers()
86        self.focused_client = None
87        # possible states for _NET_WM_STATE.
88        self.possible_states = dict((name, self.conn.atoms[name]) for name in (
89            '_NET_WM_STATE_MODAL', '_NET_WM_STATE_STICKY',
90            '_NET_WM_STATE_MAXIMIZED_VERT', '_NET_WM_STATE_MAXIMIZED_HORZ',
91            '_NET_WM_STATE_SHADED', '_NET_WM_STATE_SKIP_TASKBAR',
92            '_NET_WM_STATE_SKIP_PAGER', '_NET_WM_STATE_HIDDEN',
93            '_NET_WM_STATE_FULLSCREEN', '_NET_WM_STATE_ABOVE',
94            '_NET_WM_STATE_BELOW', '_NET_WM_STATE_DEMANDS_ATTENTION'
95            ))
96
97        # the screen number
98        self.number = num
99        self.info = app.conn.get_setup().roots[num]
100        # the root window
101        self.root = self.info.root
102
103        # set of hints this screen supports
104        self.supported_hints = set()
105
106        # set the check window before setting the MANAGER selection
107        self.check_window = self.create_check_window()
108        self.set_manager_selection()
109        self.check_window.push_handlers(
110                on_selection_clear=self.check_window_on_selection_clear
111                )
112
113        self.root.change_attributes(
114            event_mask=
115                EventMask.SubstructureRedirect |
116                EventMask.SubstructureNotify |
117                EventMask.StructureNotify |
118                EventMask.Exposure |
119                EventMask.PropertyChange,
120            cursor=
121                app.cursors['Normal']
122        )
123
124        self.root.push_handlers(self)
125
126    def set_manager_selection(self):
127        """
128            acquire the WM_Sn selection, where `n` is the screen number
129        """
130        # check ownership first
131        atom = self.conn.atoms["WM_S%d" % self.number]
132        owner = atom.get_selection_owner().reply().owner
133        if owner is not XNone:
134            # there is already a wm running on this screen
135            # should replace?
136            if not self.app.replace_existing_wm:
137                # shouldn't replace
138                log.error('There is already a window manager running on screen %d - ' % self.number +
139                        'Type `sx-wm --replace` if you wish to replace the running wm')
140                # TODO: just quit it?
141                sys.exit(0)
142                return
143        # set the selection owner to the check window
144        atom.set_selection_owner(self.check_window)
145        # send a client message to kick the other window manager
146        evt = xproto.ClientMessageEvent.create(
147                self.conn,
148                self.conn.atoms["MANAGER"],
149                self.root,
150                32,
151                [
152                    xproto.Time.CurrentTime,
153                    atom.get_internal(),
154                    self.check_window.get_internal(),
155                ]
156                )
157        self.root.send_event(
158                EventMask.StructureNotify,
159                evt
160                )
161        self.conn.flush()
162
163    def check_window_on_selection_clear(self, evt):
164        """
165            Event handler: most likely, a window manager wants to replace
166            samurai-x2!
167        """
168        atom = self.conn.atoms["WM_S%d" % self.number]
169        if evt.selection == atom:
170            self.app.stop()
171        else:
172            log.warning('Received an unknown selection clear event: %s' % evt.selection)
173
174    def get_geometry(self):
175        """
176            return a :class:`samuraix.rect.Rect` describing the
177            geometry of the physical screen.
178        """
179        return Rect.from_object(self.root.get_geometry().reply())
180
181    def create_check_window(self):
182        """
183            The 'check window' is the window required by the
184            `_NET_SUPPORTING_WM_CHECK hint`, specified in the
185            netwm standard. It only has a `_NET_WM_NAME` property,
186            set to 'samurai-x2'. It's override-redirected and
187            invisible.
188            This also sets the `_NET_SUPPORTING_WM_CHECK` hint.
189        """
190        win = xproto.Window.create(self.conn,
191                self.root,
192                self.info.root_depth,
193                self.info.root_visual,
194                0,
195                0,
196                1,
197                1,
198                0,
199                override_redirect=True)
200        self.root.change_property('_NET_SUPPORTING_WM_CHECK', 'WINDOW', 32, [win.get_internal()])
201        win.change_property('_NET_SUPPORTING_WM_CHECK', 'WINDOW', 32, [win.get_internal()])
202        win.change_property('_NET_WM_NAME', 'UTF8_STRING', 8, map(ord, 'samurai-x')) # TODO: nicer conversion
203        return win
204
205    def process_netwm_client_message(self, evt):
206        """
207            process the netwm client message *evt*.
208        """
209        return self.client_message_handlers.handle(evt)
210
211    def on_client_message(self, evt):
212        """
213            Root windows' event handler for
214            :class:`ooxcb.xproto.ClientMessageEvent`:
215            pass the client message event *evt* to
216            :meth:`process_netwm_client_message`.
217        """
218        self.process_netwm_client_message(evt)
219
220    def on_configure_request(self, evt):
221        """
222            Root windows' event handler for
223            :class:`ooxcb.xproto.ConfigureRequestEvent`.
224            Fulfill the request, or give a warning if we received a very
225            strange configure request: one that doesn't request to change
226            any values o_O
227
228            Event's parent window is the event target of the
229            configure request because sometimes we have a
230            window that isn't managed yet, but sends a configure
231            request. This configure request would be lost.
232        """
233        cnf = configure_request_to_dict(evt)
234        if cnf:
235            evt.window.configure_checked(**cnf).check()
236        else:
237            log.warning('Strange configure request: No attributes set')
238
239    def on_map_request(self, evt):
240        """
241            Root windows' event handler for
242            :class:`ooxcb.xproto.MapRequestEvent`. That will
243            create a client for the window if there isn't one already.
244            In any case, this will map the window as requested.
245        """
246        #if evt.override_redirect:
247        #    return # TODO: strange override_redirect values (88? oO)
248        client = Client.get_by_window(evt.window)
249        if client is None:
250            # not created yet
251            # NB we still not might manage this window - check manage()
252            if self.manage(evt.window):
253                return
254        # uh, did we forget that?
255        evt.window.map()
256
257    def on_destroy_notify(self, evt):
258        """
259            Root windows' event handler for
260            :class:`ooxcb.xproto.DestroyNotifyEvent`.
261            If there is a client managing *evt*'s window, call
262            :meth:`samuraix.client.Client.remove` and set
263            its window.valid to False.
264        """
265        win = evt.window
266        client = Client.get_by_window(evt.window)
267        log.debug('Root window got destroy notify event for '
268                  'window %s client %s' % (evt.window, client))
269        if client is not None:
270            client.window.valid = False # TODO: shouldn't be here.
271            client.remove()
272
273    def manage(self, window):
274        """
275            manage a new window - this may *not* result in a window
276            being managed if it is unsuitable (ie a dock
277            or override-redirected window)
278        """
279        # we dont want to do this here as we still want to manage these windows
280        # its down to the decorator what to decorate or not
281        # check window type
282        #window_type = map(self.conn.atoms.get_by_id,
283        #    window.get_property('_NET_WM_WINDOW_TYPE', 'ATOM').reply().value)
284        #if self.conn.atoms['_NET_WM_WINDOW_TYPE_DOCK'] in window_type:
285        #    log.debug('%s not managing %s - is a dock.' % (self, window))
286        #    # TODO: ignore other types, too?
287        #    return
288
289        attributes = window.get_attributes().reply()
290        geom = window.get_geometry().reply()
291
292        # override redirect windows need to be ignored - theyre not for us
293        if attributes.override_redirect:
294            log.debug('%s not managing %s override_redirect is set' % \
295                    (self, window))
296            return False
297
298        client = self.client_class(self, window, geom)
299        logging.debug('screen %s is now managing %s' % (self, client))
300        self.clients.add(client)
301
302        self.dispatch_event('on_new_client', self, client)
303        client.push_handlers(on_removed=self.on_client_removed)
304
305        client.init()
306
307        # Don't focus a new client ...
308        #if self.focused_client is None:
309        #    self.focus(client)
310
311        self.dispatch_event('on_after_new_client', self, client)
312
313        # We update the client list after the 'on_after_new_client' event here.
314        # That is because (at least) bbpager reacts to changes of the
315        # _NET_CLIENT_LIST property. If it is updated before the window gets
316        # a _NET_WM_DESKTOP property (it gets one in sx-desktops'
317        # 'on_after_new_client' event handler), bbpager won't display the new
318        # window in its preview. So we update the client list after having
319        # done everything else.
320        # Maybe that's not the best way, but it works.
321        self.update_client_list()
322        return client
323
324    def unmanage(self, client):
325        """
326            Unmanage the client *client*.
327            That means: unban it. If we don't unban it, it is unmapped.
328            If it is unmapped, samurai-x won't manage it if it's restarted.
329        """
330        log.info('Unmanaging %s ...' % client)
331        client.unmanage()
332        self.update_client_list()
333        self.clients.remove(client)
334        self.dispatch_event('on_unmanage_client', self, client)
335        # make the application call `gc.collect()` as soon
336        # as possible to collect `client`.
337        # TODO: solve that more nicely.
338        self.app.add_function_to_call(gc.collect)
339        log.info('Unmanaged %s' % client)
340
341    def on_unmanage_client(self, screen, client):
342        """
343            default handler: if the focused client is
344            unmanaged, a random other client is focused.
345            You May Override This.
346        """
347        if self.focused_client is client:
348            new_client = None
349            try:
350                new_client = iter(self.clients).next() # TODO: expensive?
351            except StopIteration:
352                pass
353            self.focus(new_client)
354
355    def on_client_removed(self, client):
356        """
357            event handler: if a client's window is removed,
358            unmanage the client.
359
360            :todo: is that necessary?
361        """
362        self.unmanage(client)
363
364    def unmanage_all(self):
365        """
366            Unmanage all my clients. That is usually called at
367            the end of samurai-x' lifetime.
368        """
369        while self.clients:
370            self.unmanage(iter(self.clients).next()) # TODO: expensive?
371
372    def update_client_list(self):
373        """
374            update the root window's `_NET_CLIENT_LIST` property as described
375            in the netwm standard.
376        """
377        # re-set _NET_CLIENT_LIST
378        self.root.change_property('_NET_CLIENT_LIST',
379                'WINDOW',
380                32,
381                [c.window.get_internal() for c in self.client_class.all_clients])
382        self.conn.flush()
383        # TODO: calling get_internal() is not that nice. we'll have to change that.
384
385    def update_active_window(self):
386        """
387            Update `_NET_ACTIVE_WINDOW`; set it to *self.focused_client*.
388        """
389        if self.focused_client is not None:
390            self.root.change_property('_NET_ACTIVE_WINDOW', 'WINDOW', 32,
391                    [self.focused_client.window.get_internal()])
392
393    def focus(self, client):
394        """
395            focus the client `client`.
396            It may be None => No focus.
397        """
398        if client:
399            log.debug('Screen. I am focusing %s %s %s' % (client, client.window, client.actor))
400        else:
401            log.debug('Screen. I am focusing nothing.')
402        # set the new focused client before calling `blur`. A client's
403        # "on_blur" event handlers can use `Client.is_focused` then.
404        # Only call `blur` if the client's window is valid (ie not already destroyed)
405        old_client = self.focused_client
406        self.focused_client = client
407        if (old_client is not None and old_client.window.valid):
408            old_client.blur()
409        # set the hint
410        self.update_active_window()
411        if client is not None:
412            client.focus()
413
414    def scan(self):
415        """ scan a screen for windows to manage """
416        children = self.root.query_tree().reply().children
417        for child in children:
418            log.debug('%s found child %s', self, child)
419            try:
420                attr = child.get_attributes().reply()
421            except xproto.BadWindow:
422                log.warning('Window was destroyed while scanning: %s' % child)
423                continue
424
425            log.debug('attr %s', attr)
426
427            # according to awesome we only do this when scanning...
428            # ( not 100% sure why yet... )
429            if attr.map_state != xproto.MapState.Viewable:
430                log.debug('%s not managing %s - not viewable', self, child)
431                continue
432            # TODO: we receive the attributes two times here.
433            self.manage(child)
434
435    def set_supported_hints(self, supported):
436        """
437            set the `_NET_SUPPORTED` atom to the hints we support
438        """
439        self.root.change_property('_NET_SUPPORTED', 'ATOM', 32,
440                [s.get_internal() for s in supported]) # TODO: nicer conversion
441
442Screen.register_event_type('on_new_client')
443Screen.register_event_type('on_after_new_client')
444Screen.register_event_type('on_unmanage_client')
Note: See TracBrowser for help on using the browser.