June 16th, 2007 @ 22:04

As part of a still unpublished (or unadvertised, whatever) project, I was looking for how to do a simple window on top of the screen that would not steal focus but would still be able to receive keyboard events. Furthermore I was also looking for how to easily make this window partly transparent. It’s just all simple code-wise, but when you have no clue on how to do it, it’s not that easy.

I learnt that popup windows aren’t stealing focus, that I could grab the keyboard only (but that I can not grab it while a key is pressed because there is already another active grab then), and that using Composite extension was just about setting the right colormap and using Cairo to draw on the window surface by hooking in the expose events.

Sadly I also learnt that it wasn’t currently possible to draw transparent widgets since they own their region so that what’s under it (in the same window) can not be used for compositing (which results into an awful dark background behind the widget). The only workaround I found was drawing the widget on its own Cairo surface and draw that surface at the right place after painting the window background.

drawing sample - thumbnail

I have included the full sample code featuring all this but the workaround for transparent widgets.

#!/usr/bin/python
 
"""
Author : Guillaume "iXce" Seguin
Email  : guillaume@segu.in
 
Copyright (C) 2007 Guillaume Seguin
 
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.
 
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.
 
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
"""
 
import gtk, gobject, cairo
from random import randint, random
from math import pi
 
class drawingWindow (gtk.Window):
 
    __gsignals__ = {
            "expose-event"  : "override",
            "screen-changed": "override"
            }
 
    count = 0
    timeout = 1000
 
    def __init__ (self, filename, timeout = 400):
        self.timeout = timeout
        # Create a popup window which won't take focus
        gtk.Window.__init__ (self, gtk.WINDOW_POPUP)
        # Required to paint the background
        self.set_app_paintable (True)
        # Load image file
        self.image_surface = cairo.ImageSurface.create_from_png (filename)
        self.image_width = self.image_surface.get_width ()
        self.image_height = self.image_surface.get_height ()
        # Make window as large as possible
        screen = self.get_screen ()
        width = min (1280, screen.get_width ())
        height = min (1024, screen.get_height ())
        self.set_default_size (width, height)
        # Set the colormap
        self.do_screen_changed ()
        width, height = self.get_size ()
        self.surface = cairo.ImageSurface (cairo.FORMAT_ARGB32, width, height)
        # Plug signals
        self.connect ("delete-event", gtk.main_quit)
        self.connect ("key-press-event", self.on_key_press_event)
 
    # Show the window and plug required stuff
    def show (self):
        # Show the window
        gtk.Window.show (self)
        # Start timer
        gobject.timeout_add (self.timeout, self.add_image)
        # Grab keyboard
        while gtk.gdk.keyboard_grab (self.window) != gtk.gdk.GRAB_SUCCESS:
            sleep (0.1)
 
    # Add a new rotated version of the image to the global surface
    def add_image (self):
        cr = cairo.Context (self.surface)
        width, height = self.get_size ()
        x = randint (0, width - self.image_width)
        y = randint (0, height - self.image_height)
        cr.set_operator (cairo.OPERATOR_OVER)
        cr.set_source_surface (self.rotate_image (self.image_surface), x, y)
        cr.paint ()
        self.queue_draw ()
        return True
 
    # Create a new surface containing a rotated version of the source surface
    def rotate_image (self, source_surface):
        width = int (1.5 * source_surface.get_width ())
        height = int (1.5 * source_surface.get_height ())
        surface = cairo.ImageSurface (cairo.FORMAT_ARGB32, width, height)
        cr = cairo.Context (surface)
        cr.scale (width, height)
        cr.translate (0.5, 0.5)
        cr.rotate (random () * 2.0 * pi)
        cr.translate (- 0.5, - 0.5)
        cr.scale (1.0 / width, 1.0 / height)
        cr.set_source_surface (source_surface, 1.0 / 6 * width, 1.0 / 6 * height)
        cr.paint ()
        return surface 
 
    # Copy the surface to the current window pixmap
    def do_expose_event (self, event):
        cr = self.window.cairo_create ()
        cr.set_operator (cairo.OPERATOR_SOURCE)
        cr.set_source_rgba (0, 0, 0, 0)
        cr.paint ()
        cr.set_source_surface (self.surface)
        cr.paint ()
 
    # Update colormap
    def do_screen_changed (self, old_screen = None):
        screen = self.get_screen ()
        colormap = screen.get_rgba_colormap ()
        if not colormap:
            colormap = screen.get_rbg_colormap ()
        self.set_colormap (colormap)
 
    # Quit when Escape key is pressed
    def on_key_press_event (self, widget, event):
        if event.hardware_keycode == 9:
            gtk.main_quit ()
 
dW = drawingWindow ("heart.png")
dW.show ()
try:
    gtk.main ()
except KeyboardInterrupt:
    raise SystemExit

The heart.png file originally used is :
Heart PNG

(but you can obviously replace it by whatever you want :p)