Scripting the Linux desktop, Part 1
Put Python and screenlets to work building useful desktop applications
Developing applications for the Linux desktop typically requires some type of graphical user interface (GUI) framework to build on. Options include GTK+ for the GNOME desktop and Qt for the K Desktop Environment (KDE). Both platforms offer everything a developer needs to build a GUI application, including libraries and layout tools to create the windows users see. This article shows you how to build desktop productivity applications based on the screenlets widget toolkit (see Related topics for a link).
A number of existing applications would fit in the desktop productivity category, including GNOME Do and Tomboy. These applications typically allow users to interact with them directly from the desktop through either a special key combination or by dragging and dropping from another application such as Mozilla Firefox. Tomboy functions as a desktop note-taking tool that supports dropping text from other windows.
Getting started with screenlets
You need to install a few things to get started developing screenlets.
First, install the screenlets package using either the Ubuntu Software Center or
the command line. In the Ubuntu Software Center, type
screenlets in the Search box.
should see two options for the main package and a separate installation for the
Next, download the test screenlet's source from the screenlets.org site. The test screenlet resides in the src/share/screenlets/Test folder and uses Cairo and GTK, which you also need to install. The entire source code for the test program is in the TestScreenlet.py file. Open this file in your favorite editor to see the basic structure of a screenlet.
Python is highly object oriented and as such uses
keyword to define an object. In this example, the class is named
TestScreenlet and has a
number of methods defined.
In TestScreenlet.py, note the following code at line 42:
def __init__(self, **keyword_args):
Python uses the leading and trailing double underscore
notation to identify system functions with predefined behaviors. In this case,
__init__ function is for all
intents and purposes
the constructor for the class and contains any number of initialization steps
to be executed on the creation of a new instance of the object. By convention,
the first argument of every class method is a reference to the current instance
of the class and is named
self. This behavior makes
it easy to use
self to reference methods and properties
of the instance it is in:
self.theme_name = "default"
The screenlets framework defines several naming conventions and standards, as outlined on screenlets.org's developer's page (see Related topics for a link). There's a link to the source code for the screenlets package along with the application programming interface (API) documentation. Looking at the code also gives you insight into what each function does with the calling arguments and what it returns.
Writing a simple screenlet
The basic components of a screenlet include an icon file, the source code file, and a themes folder. The themes folder contains additional folders for different themes. You'll find a sample template at screenlets.org with the required files and folders to help you get started.
For this first example, use the template provided to create a basic "Hello World" application. The code for this basic application is shown in Listing 1.
Listing 1. Python code for the Hello World screenlet
#!/usr/bin/env python import screenlets class HelloWorldScreenlet(screenlets.Screenlet): __name__ = 'HelloWorld' __version__ = '0.1' __author__ = 'John Doe' __desc__ = 'Simple Hello World Screenlet' def __init__(self, **kwargs): # Customize the width and height. screenlets.Screenlet.__init__(self, width=180, height=50, **kwargs) def on_draw(self, ctx): # Change the color to white and fill the screenlet. ctx.set_source_rgb(255, 255, 255) self.draw_rectangle(ctx, 0, 0, self.width, self.height) # Change the color to black and write the message. ctx.set_source_rgb(0, 0, 0) text = 'Hello World!' self.draw_text(ctx, text, 10, 10, "Sans 9" , 20, self.width) if __name__ == "__main__": import screenlets.session screenlets.session.create_session(HelloWorldScreenlet)
Each application must import the screenlets framework and create a new session.
There are a few other minimal requirements, including any initialization steps
along with a basic draw function to present the widget on screen. The TestScreenlet.py
example has an
__init__ method that initializes the
object. In this case, you see a single line with a call to the screenlet's
__init__ method, which sets the initial width and height of
the window to be created for this application.
The only other function you need for this application is the
method. This routine sets the background color of the box to white and draws
a rectangle with the dimensions defined earlier. It sets the text color to black
and the source text to "Hello World!" and then draws the text. Figure
1 shows what you should see when you run this screenlet. This basic structure
sticks with you for the rest of this article as you build on these simple blocks to
create more useful applications.
Figure 1. Basic screenlet structure
Reusing code in a more complex screenlet
One nice thing about writing screenlets is the ability to reuse code from other applications. Code reuse opens a world of possibilities with the wide range of open source projects based on the Python language. Every screenlet has the same basic structure but with more methods defined to handle different behaviors. Listing 2 shows a sample application named TimeTrackerScreenlet.
Listing 2. Python code for the Time Tracker screenlet
#!/usr/bin/env python import screenlets import cairo import datetime class TimeTrackerScreenlet(screenlets.Screenlet): __name__ = 'TimeTrackerScreenlet' __version__ = '0.1' __author__ = 'John Doe' __desc__ = 'A basic time tracker screenlet.' theme_dir = 'themes/default' image = 'start.png' def __init__(self, **keyword_args): screenlets.Screenlet.__init__(self, width=250, height=50, **keyword_args) self.add_default_menuitems() self.y = 25 self.theme_name = 'default' self.on = False self.started = None def on_draw(self, ctx): self.draw_scaled_image(ctx, 0, 0, self.theme_dir + '/' + self.image, self.width, self.height) def on_mouse_down(self, event): if self.on: self.started = datetime.datetime.now() self.image = 'stop.png' self.on = False else: if self.started: length = datetime.datetime.now() - self.started screenlets.show_message(None, '%s seconds' % length.seconds, 'Time') self.started = None self.image = 'start.png' self.on = True def on_draw_shape(self, ctx): self.on_draw(ctx) ctx.rectangle(0, 0, self.width, self.height) ctx.fill() if __name__ == "__main__": import screenlets.session screenlets.session.create_session(TimeTrackerScreenlet)
This example introduces a few more concepts that you need to understand before you
start building anything useful. All screenlet applications have the ability to
respond to specific user actions or events such as mouse clicks or drag-and-drop
operations. In this example, the mouse down event is used as a trigger to change
the state of your icon. When the screenlet runs, the start.png image is displayed.
Clicking the image changes it to stop.png and records the time started in
self.started. Clicking the stop image changes the image
back to start.png and displays the amount of time elapsed since the first start
image was clicked.
Responding to events is another key capability that makes it possible to build any
number of different applications. Although this example only uses the
mouse_down event, you can use the same approach
for other events generated either by the screenlets framework or by a system
event such as a timer. The second concept introduced here is persistent state.
Because your application is running continuously, waiting for an event to trigger
some action, it is able to keep track of items in memory, such as the time the
start image was clicked. You could also save information to disk for later retrieval,
Automating tasks with screenlets
Now that you have the general idea behind developing screenlets, let's put all together. Most users these days use a Really Simple Syndication (RSS) reader to read blogs and news feeds. For this last example, you're going to build a configurable screenlet that monitors specific feeds for keywords and displays any hits in a text box. The results will be clickable links to open the post in your default Web browser. Listing 3 shows the source code for the RSS Search screenlet.
Listing 3. Python code for the RSS Search screenlet
#!/usr/bin/env python from screenlets.options import StringOption, IntOption, ListOption import xml.dom.minidom import webbrowser import screenlets import urllib2 import gobject import pango import cairo class RSSSearchScreenlet(screenlets.Screenlet): __name__ = 'RSSSearch' __version__ = '0.1' __author__ = 'John Doe' __desc__ = 'An RSS search screenlet.' topic = 'Windows Phone 7' feeds = ['http://www.engadget.com/rss.xml', 'http://feeds.gawker.com/gizmodo/full'] interval = 10 __items =  __mousesel = 0 __selected = None def __init__(self, **kwargs): # Customize the width and height. screenlets.Screenlet.__init__(self, width=250, height=300, **kwargs) self.y = 25 def on_init(self): # Add options. self.add_options_group('Search Options', 'RSS feeds to search and topic to search for.') self.add_option(StringOption('Search Options', 'topic', self.topic, 'Topic', 'Topic to search feeds for.')) self.add_option(ListOption('Search Options', 'feeds', self.feeds, 'RSS Feeds', 'A list of feeds to search for a topic.')) self.add_option(IntOption('Search Options', 'interval', self.interval, 'Update Interval', 'How frequently to update (in seconds)')) self.update() def update(self): """Search selected feeds and update results.""" self.__items =  # Go through each feed. for feed_url in self.feeds: # Load the raw feed and find all item elements. raw = urllib2.urlopen(feed_url).read() dom = xml.dom.minidom.parseString(raw) items = dom.getElementsByTagName('item') for item in items: # Find the title and make sure it matches the topic. title = item.getElementsByTagName('title').firstChild.data if self.topic.lower() not in title.lower(): continue # Shorten the title to 30 characters. if len(title) > 30: title = title[:27]+'...' # Find the link and save the item. link = item.getElementsByTagName('link').firstChild.data self.__items.append((title, link)) self.redraw_canvas() # Set to update again after self.interval. self.__timeout = gobject.timeout_add(self.interval * 1000, self.update) def on_draw(self, ctx): """Called every time the screenlet is drawn to the screen.""" # Draw the background (a gradient). gradient = cairo.LinearGradient(0, self.height * 2, 0, 0) gradient.add_color_stop_rgba(1, 1, 1, 1, 1) gradient.add_color_stop_rgba(0.7, 1, 1, 1, 0.75) ctx.set_source(gradient) self.draw_rectangle_advanced (ctx, 0, 0, self.width - 20, self.height - 20, rounded_angles=(5, 5, 5, 5), fill=True, border_size=1, border_color=(0, 0, 0, 0.25), shadow_size=10, shadow_color=(0, 0, 0, 0.25)) # Make sure we have a pango layout initialized and updated. if self.p_layout == None : self.p_layout = ctx.create_layout() else: ctx.update_layout(self.p_layout) # Configure fonts. p_fdesc = pango.FontDescription() p_fdesc.set_family("Free Sans") p_fdesc.set_size(10 * pango.SCALE) self.p_layout.set_font_description(p_fdesc) # Display our text. pos = [20, 20] ctx.set_source_rgb(0, 0, 0) x = 0 self.__selected = None for item in self.__items: ctx.save() ctx.translate(*pos) # Find if the current item is under the mouse. if self.__mousesel == x and self.mouse_is_over: ctx.set_source_rgb(0, 0, 0.5) self.__selected = item else: ctx.set_source_rgb(0, 0, 0) self.p_layout.set_markup('%s' % item) ctx.show_layout(self.p_layout) pos += 20 ctx.restore() x += 1 def on_draw_shape(self, ctx): ctx.rectangle(0, 0, self.width, self.height) ctx.fill() def on_mouse_move(self, event): """Called whenever the mouse moves over the screenlet.""" x = event.x / self.scale y = event.y / self.scale self.__mousesel = int((y -10 )/ (20)) -1 self.redraw_canvas() def on_mouse_down(self, event): """Called when the mouse is clicked.""" if self.__selected and self.mouse_is_over: webbrowser.open_new(self.__selected) if __name__ == "__main__": import screenlets.session screenlets.session.create_session(RSSSearchScreenlet)
Building on the concepts of the first two examples, this screenlet uses a number of
new concepts, including the config page. In the
routine, three options are added for the user to specify: a list of RSS feeds to
track, a topic of interest to search for, and an update interval. The update
routine then uses all of these when it runs.
Python is a great language for this type of task. The standard library includes everything you need to load the Extensible Markup Language (XML) from an RSS feed into a searchable list. In Python, this takes just three lines of code:
raw = urllib2.urlopen(feed_url).read() dom = xml.dom.minidom.parseString(raw) items = dom.getElementsByTagName('item')
The libraries used in these three lines include
xml. In the first line, the entire contents found
feed_url address are read into the string raw.
Next, because you know that this string contains XML, you use the Python XML
dom.minidom.parseString method to create a
document object made up of node objects.
Finally, you create a list of element objects corresponding to the individual XML
item. You can then iterate over this
list to search for your target topic. Python has a very elegant way of iterating over
a list of items using the
for keyword, as in this code
for item in items: # Find the title and make sure it matches the topic. title = item.getElementsByTagName('title').firstChild.data if self.topic.lower() not in title.lower(): continue
Each item matching your criteria is added to the currently displayed list, which
is associated with this instance of the screenlet. Using this approach makes it
possible to have multiple instances of the same screenlet running, each configured
to search for different topics. The final part of the update function redraws the
text with the updated list and fires off a new update timer based on the interval
on the config page. By default, the timer fires every 10 seconds, although you
could change that to anything you want. The timer mechanism comes from the
gobject library, which is a part of the GTK framework.
This application expands the
on_draw method quite heavily
to accommodate your new functionality. Both the Cairo and Pango libraries make it
possible to create some of the effects used in the text window. Using a gradient
gives the background of the widget a nice look along with rounded angles and
semi-transparency. Using Pango for layout adds a number of functions for saving
and restoring the current context easily. It also provides a way to generate scalable
fonts based on the current size of the screenlet.
The trickiest part in the
on_draw method is handling when
a user hovers over an item in the list. Using the
keyword, you iterate over the items in the screenlet to see whether the user
is hovering over that particular item. If so, you set the selected property and
change the color to provide visual feedback. You also use a bit of markup to set
the link property to bold—probably not the most elegant or efficient way
to deal with the problem, but it works. When a user clicks one of the links in the
box, a Web browser is launched with the target URL. You can see this functionality
on_mouse_down function. Python and its
libraries make it possible to launch the default web browser to display the desired
page with a single line of code. Figure 2 shows an example of
Figure 2. The example screenlet
Building useful desktop applications is not a difficult task with Python and screenlets. The biggest hurdle is getting comfortable with the screenlets API and the mechanics of passing control between different functions. Although the documentation may not be an easy read, it does contain the information you need to use the different functions. An even better way of getting something working quickly is to modify an existing screenlet that is close to what you want.
- Learn more about the GTK+ windowing toolkit for GNOME.
- Learn more about the Qt windowing toolkit for KDE.
- Learn more about screenlets and find examples, templates, and documentation at the Screenlets Web site.
- Find the Python resources you need at Python.org.
- In the developerWorks Linux zone, find hundreds of how-to articles and tutorials, as well as downloads, discussion forums, and a wealth of other resources for Linux developers and administrators.
- Evaluate IBM products in the way that suits you best: Download a product trial, try a product online, use a product in a cloud environment, or spend a few hours in the SOA Sandbox learning how to implement Service Oriented Architecture efficiently.
- Follow developerWorks on Twitter, or subscribe to a feed of Linux tweets on developerWorks.