Scripting the Linux desktop, Part 1

Basics

Put Python and screenlets to work building useful desktop applications

Content series: This content is part # of # in the series: Scripting the Linux desktop, Part 1 Stay tuned for additional content in this series. This content is part of the series: Scripting the Linux desktop, Part 1 Stay tuned for additional content in this series.

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. You should see two options for the main package and a separate installation for the documentation.

Python and Ubuntu You program screenlets using Python. The basic installation of Ubuntu 10.04 has Python version 2.6 installed, as many utilities depend on it. You may need additional libraries depending on your application's requirements. For the purpose of this article, I installed and tested everything on Ubuntu version 10.04.

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 the class 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, the __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 on_draw 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, if necessary.

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')[0].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')[0].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[1] else: ctx.set_source_rgb(0, 0, 0) self.p_layout.set_markup('%s' % item[0]) ctx.show_layout(self.p_layout) pos[1] += 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 on_init 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 urllib2 and xml . In the first line, the entire contents found at the feed_url address are read into the string raw. Next, because you know that this string contains XML, you use the Python XML library 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 elements named 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 snippet:

for item in items: # Find the title and make sure it matches the topic. title = item.getElementsByTagName('title')[0].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 for" 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 in the 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 this screenlet.

Figure 2. The example screenlet

Summary

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.

Downloadable resources

Related topics