Exemple #1
0
    def __init__(self, display=None, screen=None,
                 key_bindings={}, button_bindings={},
                 **kwargs):
        self.conn = xcb.connect(display)
        self.screen_number = (screen
                              if screen is not None
                              else self.conn.pref_screen)
        self.screen = self.conn.get_setup().roots[self.screen_number]
        self.window = self.screen.root
        self.screen_geometry = get_geometry(self.conn, self.window)
        log.debug("Screen geometry: %s.", self.screen_geometry)

        self.events = deque([]) # event queue
        self.window_handlers = {} # event handlers, indexed by window ID
        self.clients = {} # managed clients, indexed by window ID
        self.frames = {} # client frames, indexed by window ID
        self.client_update = None # for move/resize
        self.parents = {self.screen.root: None}
        self.atoms = AtomCache(self.conn)
        self.colors = ColorCache(self.conn, self.screen.default_colormap)
        self.cursors = FontCursor(self.conn)
        self.fonts = FontCache(self.conn)
        self.font_infos = FontInfoCache(self.conn, self.atoms)
        self.modmap = ModifierMap(self.conn)
        self.keymap = KeyboardMap(self.conn, modmap=self.modmap)
        self.key_bindings = KeyBindings(key_bindings,
                                        self.keymap,
                                        self.modmap)
        self.button_bindings = ButtonBindings(button_bindings,
                                              self.keymap,
                                              self.modmap)
        self.heads = HeadManager(self.conn, self.screen, self)
        self.shape = query_extension(self.conn, "SHAPE", xcb.shape.key)

        super(WindowManager, self).__init__(**kwargs)
Exemple #2
0
class WindowManager(EventHandler, PropertyManager):
    """A window manager for one X screen.

    This class provides only the most basic window management functionality.
    Subclasses are expected to override or augment many of its methods and
    add their own management protocols."""

    root_event_mask = (EventMask.ButtonPress |
                       EventMask.ButtonRelease |
                       EventMask.StructureNotify |
                       EventMask.SubstructureNotify |
                       EventMask.SubstructureRedirect |
                       EventMask.PropertyChange)

    wm_command = PropertyDescriptor("WM_COMMAND", WMCommand, [])

    default_client_class = Client
    default_decorator_class = Decorator

    client_selectors = []
    decorator_selectors = []

    def __init__(self, display=None, screen=None,
                 key_bindings={}, button_bindings={},
                 **kwargs):
        self.conn = xcb.connect(display)
        self.screen_number = (screen
                              if screen is not None
                              else self.conn.pref_screen)
        self.screen = self.conn.get_setup().roots[self.screen_number]
        self.window = self.screen.root
        self.screen_geometry = get_window_geometry(self.conn, self.window)
        log.debug("Screen geometry: %s.", self.screen_geometry)

        self.events = deque([]) # event queue
        self.window_handlers = {} # event handlers, indexed by window ID
        self.clients = {} # managed clients, indexed by window ID
        self.frames = {} # client frames, indexed by window ID
        self.client_update = None # for move/resize
        self.parents = {self.screen.root: None}
        self.atoms = AtomCache(self.conn)
        self.colors = ColorCache(self.conn, self.screen.default_colormap)
        self.cursors = FontCursor(self.conn)
        self.fonts = FontCache(self.conn)
        self.font_infos = FontInfoCache(self.conn, self.atoms)
        self.modmap = ModifierMap(self.conn)
        self.keymap = KeyboardMap(self.conn, modmap=self.modmap)
        self.key_bindings = KeyBindings(key_bindings,
                                        self.keymap,
                                        self.modmap)
        self.button_bindings = ButtonBindings(button_bindings,
                                              self.keymap,
                                              self.modmap)
        self.heads = HeadManager(self.conn, self.screen, self)
        self.shape = query_extension(self.conn, "SHAPE", xcb.shape.key)
        self.init_graphics()

        super(WindowManager, self).__init__(**kwargs)

    def init_graphics(self):
        self.black_gc = self.conn.generate_id()
        self.conn.core.CreateGC(self.black_gc, self.screen.root,
                                GC.Foreground | GC.Background,
                                [self.screen.black_pixel,
                                 self.screen.white_pixel])

        self.white_gc = self.conn.generate_id()
        self.conn.core.CreateGC(self.white_gc, self.screen.root,
                                GC.Foreground | GC.Background,
                                [self.screen.white_pixel,
                                 self.screen.black_pixel])

        self.xor_gc = self.conn.generate_id()
        self.conn.core.CreateGC(self.xor_gc, self.screen.root,
                                (GC.Function |
                                 GC.Foreground |
                                 GC.LineStyle |
                                 GC.SubwindowMode |
                                 GC.GraphicsExposures),
                                [GX.xor,
                                 self.colors["#808080"],
                                 LineStyle.OnOffDash,
                                 SubwindowMode.ClipByChildren,
                                 False])

    def start(self):
        """Start the window manager. This method must only be called once."""
        # Stash the argument vector used to start the manager in the
        # WM_COMMAND property on the root window. We'll assume that
        # the arguments are encoded according to the current locale.
        self.wm_command = WMCommand(decode_argv())

        # Make this client a window manager by selecting (at least)
        # SubstructureRedirect events on the root window. If another client
        # has already done so (i.e., there's already a window manager
        # running on this screen), the check will raise an exception.
        event_mask = self.root_event_mask
        assert event_mask & EventMask.SubstructureRedirect, \
            "A window manager must select for SubstructureRedirect."
        try:
            self.conn.core.ChangeWindowAttributesChecked(self.screen.root,
                                                         CW.EventMask,
                                                         [event_mask]).check()
        except BadAccess:
            log.error("Can't select for SubstructureRedirect on screen %d; "
                      "is another window manager already running?",
                      self.screen_number)
            raise

        # Adopt any suitable top-level windows.
        self.adopt(self.conn.core.QueryTree(self.screen.root).reply().children)

        # Process events from the server.
        self.event_loop()

    def shutdown(self, *args):
        """Unmanage all clients and disconnect from the server. If args
        are supplied, treat them as a command with which to replace the
        current process."""
        log.info("Shutting down.")
        if self.conn:
            with grab_server(self.conn):
                for client in self.clients.values():
                    self.unmanage(client)
            self.conn.flush()
            self.conn.disconnect()
            self.conn = None
        assert not self.clients
        assert not self.frames
        if args:
            log.info("Executing command %s.", " ".join(args))
            argv = encode_argv(args)
            execvp(argv[0], argv)

    def adopt(self, windows):
        """Adopt existing top-level windows."""
        for window in windows:
            try:
                attrs = self.conn.core.GetWindowAttributes(window).reply()
            except BadWindow:
                log.warning("Error fetching attributes for window 0x%x.",
                            window)
                continue
            if attrs.override_redirect:
                continue
            log.debug("Adopting window 0x%x.", window)
            client = self.manage(window, True)
            if client and attrs.map_state != MapState.Unmapped:
                self.change_state(client,
                                  WMState.WithdrawnState,
                                  WMState.NormalState)

    def manage(self, window, adopted=False):
        """Manage a window and return a (possibly) new client instance."""
        try:
            return self.clients[window]
        except KeyError:
            pass

        log.debug("Managing client window 0x%x.", window)
        geometry = get_window_geometry(self.conn, window)
        if not geometry:
            return None

        client = self.make_client(window)
        decorator = self.make_decorator(self.select_decorator_class(client),
                                        client)
        try:
            client.frame(decorator, self.place(client, geometry, adopted))
        except BadWindow:
            log.warning("Error framing window 0x%x.", window)
            return
        try:
            client.establish_grabs(key_bindings=self.key_bindings,
                                   button_bindings=self.button_bindings)
        except BadWindow:
            log.warning("Error establishing grabs for window 0x%x.", window)
            return
        self.frames[client.frame] = client
        self.clients[window] = client
        return client

    def unmanage(self, client, **kwargs):
        """Unmanage the given client."""
        log.debug("Unmanaging client window 0x%x.", client.window)
        del self.clients[client.window]
        try:
            del self.frames[client.frame]
        except KeyError:
            # The client could have been reparented.
            pass
        client.unframe(**kwargs)
        return client

    def change_state(self, client, initial, final):
        """Transition a client from initial state to final state."""
        if final == WMState.IconicState:
            client.iconify()
        elif final == WMState.NormalState:
            client.normalize()
        elif final == WMState.WithdrawnState:
            client.withdraw()

    def place(self, client, geometry, adopted=False):
        """Determine and return a suitable geometry for a client window.

        By default, the requested size is always used. If a non-trivial
        position which is on-screen is requested, it is used as well;
        otherwise, the upper left corner of the head currently containing
        the pointer is used."""
        return (geometry
                if (adopted or
                    (geometry.position() and geometry & self.screen_geometry))
                else geometry.move(self.heads.focus_head_geometry.position()))

    def constrain_position(self, client, position):
        """Compute and return a new position for the given client's frame
        based on the requested position."""
        return position

    def constrain_size(self, client, geometry,
                       size=None, border_width=None, gravity=None):
        """Constrain the client geometry using size hints and gravity.
        If a resize is requested, the caller should pass the client's current
        absolute geometry and the requested size and border width; if a move
        with resize is requested, only the requested geometry is required.
        If the gravity argument is supplied, it overrides the win_gravity
        field of the size hints."""
        size_hints = client.wm_normal_hints
        if size is None:
            size = geometry.size()
        if border_width is None:
            border_width = geometry.border_width
        if gravity is None:
            gravity = size_hints.win_gravity
        return geometry.resize(size_hints.constrain_window_size(size),
                               border_width,
                               gravity)

    def fullscreen_geometry(self, client):
        """Compute and return a fullscreen geometry for the given client."""
        return self.heads.client_head_geometry(client)

    @property
    def current_focus(self):
        """Return the client that currently has the input focus."""
        window = get_input_focus(self.conn, self.screen_number)
        while window:
            client = self.get_client(window)
            if client:
                return client
            window = self.conn.core.QueryTree(window).reply().parent

    def ensure_focus(self, client=None, time=Time.CurrentTime):
        """Make a best-effort attempt to ensure that some client has the
        input focus."""
        # Subclasses that deal with focus policy are expected to provide
        # proper implementations.
        if client and client.focus(time):
            return
        self.conn.core.SetInputFocus(InputFocus.PointerRoot,
                                     InputFocus.PointerRoot,
                                     time)

    def make_client(self, window, **kwargs):
        """Construct and return a client instance for the given window."""
        # We'll always start with an instance of the default client class,
        # but we might then change the new client's class based on a selector.
        # The reason we don't just instantiate the selected class in the
        # first place is that it's much easier to write selectors that
        # accept a Client instance instead of (say) a raw window.
        client = self.default_client_class(self.conn, self, window, **kwargs)
        for selector in reversed(self.client_selectors):
            cls = selector(client)
            if cls:
                client.__class__ = cls
                client.update_instance_for_different_class(self.conn, self,
                                                           window, **kwargs)
                break
        return client

    def select_decorator_class(self, client):
        """Return a decorator class for the given client."""
        for selector in reversed(self.decorator_selectors):
            cls = selector(client)
            if cls:
                return cls
        return self.default_decorator_class

    def make_decorator(self, cls, client, **kwargs):
        """Construct and return an instance of the given decorator class
        for the given client."""
        return cls(self.conn, client, **kwargs)

    def event_loop(self):
        """The main event loop of the window manager."""
        # We use a select-based loop instead of XCB's wait_for_event because
        # (a) select handles signals correctly, and (b) wait_for_event blocks
        # the entire interpreter, not just the current thread.
        rlist = [self.conn.get_file_descriptor()]
        wlist = []
        xlist = []
        while True:
            while self.get_pending_events():
                try:
                    self.handle_event(self.events.popleft())
                except ExitWindowManager as e:
                    self.shutdown(*e.args)
                    return
            select(rlist, wlist, xlist)

    def get_pending_events(self):
        """Push all available events onto the event queue and return the queue.
        Flushes all pending requests before returning."""
        while True:
            event = self.conn.poll_for_event()
            if event:
                self.events.append(event)
            else:
                break
        self.conn.flush()
        return self.events

    def put_back_event(self, event):
        """Push an event back onto the head of the event queue."""
        self.events.appendleft(event)

    def check_event(self, test):
        """Search the event queue for an event that satisfies the given test,
        which must be a function of one argument. If a match is found, it
        is removed from the queue and returned."""
        for i, event in enumerate(self.get_pending_events()):
            if test(event):
                del self.events[i]
                return event

    def check_typed_event(self, event_type):
        """Search the event queue for an event of the given type."""
        return self.check_event(lambda event: type(event) == event_type)

    def check_typed_window_event(self, window, event_type):
        """Search the event queue for an event on the given window and
        of the given type."""
        return self.check_event(lambda event: (type(event) == event_type and
                                               event_window(event) == window))

    def handle_event(self, event):
        """Handle an event from the server. If a handler is registered for
        the window the event was reported with respect to, dispatch the
        event to that handler."""
        handler = self.window_handlers.get(event_window(event), None)
        if handler:
            try:
                handler.handle_event(event)
            except UnhandledEvent:
                pass
            except StopPropagation:
                return
        return super(WindowManager, self).handle_event(event)

    def register_window_handler(self, window, handler):
        log.debug("Registering event handler for window 0x%x.", window)
        self.window_handlers[window] = handler

    def unregister_window_handler(self, window):
        if self.window_handlers.pop(window, None):
            log.debug("Removed event handler for window 0x%x.", window)

    def unhandled_event(self, event):
        window = event_window(event)
        log.debug("Ignoring unhandled %s%s.",
                  event.__class__.__name__,
                  (" on window 0x%x" % window) if window else "")

    def get_client(self, window, client_window_only=False):
        """Retrieve the client with the given top-level window, or None if
        there is no such client.

        The second (optional) argument controls whether the window must
        be a top-level client window, or if it is permissible to return
        a client instance from its frame or a subwindow thereof."""
        if not client_window_only:
            # Walk up the window hierarchy until we come to a frame or a root.
            w = window
            while w:
                try:
                    return self.frames[w]
                except KeyError:
                    w = self.parents.get(w, None)
        return self.clients.get(window, None)

    def find_clients(self, test=lambda x: False):
        """Yield clients that satisfy the given test."""
        for client in self.clients.values():
            if test(client):
                yield client

    def update_for_changed_mapping(self):
        """Update for changed keyboard, modifier, or pointer mapping."""
        for client in self.clients.values():
            client.establish_grabs(key_bindings=self.key_bindings,
                                   button_bindings=self.button_bindings)

    @handler(ConfigureNotifyEvent)
    def handle_configure_notify(self, event):
        if event.window == self.screen.root:
            # The root window size may change due to RandR.
            old_geometry = self.screen_geometry
            self.screen_geometry = Geometry(event.x, event.y,
                                            event.width, event.height,
                                            event.border_width)
            log.debug("Root window geometry now %s.", self.screen_geometry)

    @handler(ConfigureRequestEvent)
    def handle_configure_request(self, event,
                                 move_mask=ConfigWindow.X | ConfigWindow.Y):
        """Handle a ConfigureWindow request from a top-level window."""
        client = self.get_client(event.window, True)
        if client:
            attrs = ["x", "y", "width", "height", "border_width",
                     "sibling", "stack_mode"]
            arg_names = select_values(event.value_mask, attrs)
            arg_values = select_values(event.value_mask, [getattr(event, attr)
                                                          for attr in attrs])
            args = zip(arg_names, arg_values)
            log.debug("Client 0x%x requested %s.", client.window,
                      ", ".join(map(lambda arg: "%s=%s" % arg, args)))
            client.configure_request(**dict(args))
        else:
            log.debug("Granting configure request for unmanaged window 0x%x.",
                      event.window)
            value_list = select_values(event.value_mask,
                                       [int16(event.x),
                                        int16(event.y),
                                        card16(event.width),
                                        card16(event.height),
                                        card16(event.border_width),
                                        event.sibling,
                                        event.stack_mode])
            self.conn.core.ConfigureWindow(event.window,
                                           event.value_mask,
                                           value_list)

    @handler(CreateNotifyEvent)
    def handle_create_notify(self, event):
        self.parents[event.window] = event.parent

    @handler(DestroyNotifyEvent)
    def handle_destroy_notify(self, event):
        log.debug("Window 0x%x destroyed.", event.window)
        self.parents.pop(event.window, None)
        self.unregister_window_handler(event.window)

        client = self.get_client(event.window, True)
        if client:
            self.unmanage(client, destroyed=True)

    @handler(ReparentNotifyEvent)
    def handle_reparent_notify(self, event):
        self.parents[event.window] = event.parent
        if event.override_redirect:
            return
        client = self.get_client(event.window, True)
        if client and client.frame != event.parent:
            log.debug("Window 0x%x reparented from its frame.", client.window)
            self.frames.pop(client.frame, None)

    @handler(MapRequestEvent)
    def handle_map_request(self, event):
        """Handle a request to map a top-level window on behalf of a client."""
        client = self.manage(event.window)
        if not client:
            return
        if (client.wm_state == WMState.WithdrawnState and
            client.wm_hints.initial_state == WMState.IconicState):
            # Withdrawn → Iconic state transition (ICCCM §4.1.4).
            self.change_state(client,
                              WMState.WithdrawnState,
                              WMState.IconicState)
        else:
            # {Withdrawn, Iconic} → Normal state transition (ICCCM §4.1.4).
            self.change_state(client,
                              client.wm_state,
                              WMState.NormalState)

    @handler(UnmapNotifyEvent)
    def handle_unmap_notify(self, event):
        """Note the unmapping of a top-level window."""
        if event.from_configure or event.window != event.event:
            return
        client = self.get_client(event.window, True)
        if not client:
            return
        log.debug("Client window 0x%x unmapped.", event.window)
        e = self.check_typed_window_event(event.window, DestroyNotifyEvent)
        if e:
            # Ignore the UnmapNotify.
            self.handle_event(e)
        else:
            # {Normal, Iconic} → Withdrawn state transition (ICCCM §4.1.4).
            self.change_state(client,
                              client.wm_state,
                              WMState.WithdrawnState)
            reparented = self.check_typed_window_event(event.window,
                                                       ReparentNotifyEvent)
            self.unmanage(client, destroyed=False, reparented=reparented)

    @handler(MappingNotifyEvent)
    def handle_mapping_notify(self, event):
        """Note the change of an input device mapping and possibly request
        an update from the server."""
        if event.request == Mapping.Keyboard:
            try:
                log.debug("Refreshing keymap: %d codes starting at %d.",
                          event.count, event.first_keycode)
                self.keymap.refresh(event.first_keycode, event.count)
            except KeymapError as e:
                log.warning("Unable to refresh partial keymap: %s.", e)
                self.keymap.refresh()
        elif event.request == Mapping.Modifier:
            log.debug("Refreshing modifier mapping.")
            self.modmap.refresh()
            self.keymap.scry_modifiers(self.modmap)
        self.update_for_changed_mapping()

    @handler(PropertyNotifyEvent)
    def handle_property_notify(self, event):
        """Note the change of a window property."""
        # Sometimes clients generate little storms of property updates.
        # When that happens, we'll ignore all but the last one available.
        def similar_event(other):
            return (type(other) == PropertyNotifyEvent and
                    other.window == event.window and
                    other.atom == event.atom and
                    other.state == event.state and
                    other.time >= event.time)
        while True:
            next_event = self.check_event(similar_event)
            if next_event:
                event = next_event
            else:
                break

        # Property notifications are dispatched to the appropriate property
        # manager. We act as the property manager for the root window, and
        # each client manages the properties for its window.
        if event.window == self.screen.root:
            propman = self
        else:
            client = self.get_client(event.window)
            if client:
                propman = client
            else:
                log.debug("Got PropertyNotify for unmanaged window 0x%x (%s).",
                          event.window, self.atoms.name(event.atom))
                return
        propman.property_changed(self.atoms.name(event.atom),
                                 event.state == Property.Delete,
                                 event.time)

    @handler((KeyPressEvent, KeyReleaseEvent))
    def handle_key_press(self, event):
        try:
            action = self.key_bindings[event]
        except KeyError:
            return
        action(self, event)

    @handler((ButtonPressEvent, ButtonReleaseEvent))
    def handle_button_press(self, event):
        try:
            action = self.button_bindings[event]
        except KeyError:
            return
        action(self, event)

    @handler(ClientMessageEvent)
    def handle_client_message(self, event):
        """Handle a client message event by dispatching a new ClientMessage
        instance."""
        try:
            event_type = self.atoms.name(event.type)
        except BadAtom:
            return
        log.debug("Received ClientMessage of type %s on window 0x%x.",
                  event_type, event.window)
        try:
            message_type = client_message_type(event_type)
        except KeyError:
            return
        assert issubclass(message_type, ClientMessage)
        self.handle_event(message_type(event.window, event.format, event.data))

    @handler(WMExit)
    def handle_wm_exit(self, client_message):
        log.debug("Received exit message; shutting down.")
        argv = []
        timestamp = client_message.data.data32[0]
        if (timestamp and
            compare_timestamps(self.property_timestamps["WM_COMMAND"],
                               timestamp) <= 0):
            argv = self.wm_command
        raise ExitWindowManager(*argv)