System tray icon?

192 views
Skip to first unread message

Pat LeSmithe

unread,
Mar 15, 2009, 9:23:51 AM3/15/09
to sage-...@googlegroups.com

For a Linux system with (Py)GTK installed, here's a simple way to set up
a system tray icon which indicates, roughly, whether the notebook server
is running:

1. Save the attached script as $SAGE_ROOT/local/bin/sage-tray-icon .
2. Make it executable.
3. Insert "sage-tray-icon &" between the sage-cleaner and sage-notebook
commands in the sage-sage script.

There's probably a much better way. Does Sage have an equivalent of
rc.local?

I'm not sure how to get information between the notebook and the icon,
e.g., to have the latter blink when a long computation is finished. If
anyone wants try, these might help:

http://www.pygtk.org/docs/pygtk/class-gtkstatusicon.html
http://marcin.af.gliwice.pl/if-then-else-20070121143245


Sincerely,
Pat LeSmithe


sage-tray-icon

Pat LeSmithe

unread,
Mar 29, 2009, 10:15:29 PM3/29/09
to sage-...@googlegroups.com

Thanks to an unexpected grant of round tuits [1] from the Back Burner
Foundation for Eventual Development, here's a somewhat improved tray
icon for Sage.

Besides PyGTK, the main script now uses Python's bindings to
freedesktop.org's D-Bus to export several methods for manipulating the
icon. These are accessible from any local Sage instance that sets up
some variables, e.g., in init.sage. In particular, it's possible to
change or animate the icon from a worksheet. Though I haven't yet
displayed an image made by Sage itself, this includes the ability to
send an "arbitrary" image over the bus to the icon's process.

Some assembly is required. I've attached commented scripts --- still
sans doctests of some sort --- and a demo txt worksheet. You may need
to make some tweaks for distributions other than 64-bit Fedora 9. As
before, to start the icon process, I suggest adding
"/path/to/sage-tray-icon-gtk7 &" to the notebook section of the
sage-sage script [2]. The setup should be similar for command-line
Sage, but be sure to check the top of init.sage.

I'll try to submit a Trac ticket soon, so that anyone who's interested
can make changes. But the not-quite-prestigious Foundation informs me
that it would also be great just to get some input on possible further
improvements, such as

* Refreshing the tooltip with useful statistics?
* Right-click menu options?
* Optional notification bubbles?
* More and/or better animations? [3]
* More and/or more appropriate icons?
* Deploying a Sage plot or animation as the icon (tiny!), or in a
notification?
* Displaying some sort of help in a notification?
* Exploiting Sage's image-processing capabilities?
* Making pickle-over-D-Bus transport more efficient.
* Setting up callbacks so the icon dances, say, upon completing
computations that exceed some duration?
* The same, but not as dramatically, for making measurable progress?
* Better ways to start the icon process?
* Cross-platform support?
* Qt toolkit support?
* Actually creative applications of D-Bus in Sage?
* What else?

Most of these are mutually exclusive, in the sense that there's no way
they're all happening. Nevertheless, the BBFED works, occasionally.

Thanks!

[1] http://en.wiktionary.org/wiki/round_tuit

[2] My previous message:
http://groups.google.com/group/sage-devel/browse_thread/thread/f9aeba22ac171082#

[3] I do hope there will be NO Sage equivalent of Clippy! ("It looks
like you're dividing by zero..." :)

Sincerely,
Pat LeSmithe

sage-tray-icon-gtk7
tray_icon.txt
init.sage

William Stein

unread,
Mar 29, 2009, 10:20:38 PM3/29/09
to sage-...@googlegroups.com
On Sun, Mar 29, 2009 at 7:15 PM, Pat LeSmithe <qed...@gmail.com> wrote:
>
> Thanks to an unexpected grant of round tuits [1] from the Back Burner
> Foundation for Eventual Development, here's a somewhat improved tray
> icon for Sage.
>
> Besides PyGTK, the main script now uses Python's bindings to
> freedesktop.org's D-Bus to export several methods for manipulating the

Could you post some screenshots?
> #!/usr/bin/env python
> """
> A Sage system tray / notification area / status icon built on PyGTK
> and freedesktop.org's D-Bus.
> """
> import math, os, sys
>
> # We pickle only when actually sending pixbuf'd icons from a Sage
> # worksheet, via the bus.
> import cPickle as pickle
>
> # Register for automatic clean-up.
> import sage.interfaces.cleaner as the_wolf
> the_wolf.cleaner(os.getpid())
>
> # We use system-wide installations of PyGTK and D-Bus Python bindings.
> # The package Numeric is used only when pickling icons to transmit
> # over D-Bus.  In this case, PyGTK needs to be built with Numeric
> # support.  There's probably a way to do it with NumPy, instead.  The
> # following additions seem to be sufficient on a x86_64 Fedora 9
> # system.  Perhaps it's possible to auto-detect and configure for
> # these dependencies.
> sys.path.append('/usr/lib/python2.5/site-packages/')
> sys.path.append('/usr/lib/python2.5/site-packages/dbus')
> sys.path.append('/usr/lib64/python2.5/site-packages/')
> sys.path.append('/usr/lib64/python2.5/site-packages/gtk-2.0')
> sys.path.append('/usr/lib64/python2.5/site-packages/Numeric')
>
> import Numeric
> import gobject, gtk
> import dbus, dbus.service, dbus.mainloop.glib
>
> # Local dictionary of some available icons.
> icon_home = os.environ['SAGE_ROOT'] + '/data/extcode/notebook/images/'
> sage_icons = { 'default' : 'icon32x32.png',
>               'email'   : 'icon_email.gif',
>               'preview' : 'icon_preview.gif',
>               'print'   : 'icon_print.gif' }
> for name in sage_icons:
>    sage_icons[name] = icon_home + sage_icons[name]
>
> # We'll export a SageTrayIcon object, including several methods for
> # asynchronously "signalling" the tray icon, over the session bus.
> #
> #  dbus-python tutorial:
> #      http://dbus.freedesktop.org/doc/dbus-python/doc/tutorial.html
> #  gtk.StatusIcon doc:
> #      http://library.gnome.org/devel/pygtk/stable/class-gtkstatusicon.html
> #  gtk.gdk.Pixbuf doc:
> #      http://library.gnome.org/devel/pygtk/stable/class-gdkpixbuf.html
> class SageTrayIcon(dbus.service.Object):
>    def __init__(self, conn, object_path):
>        dbus.service.Object.__init__(self, conn, object_path)
>        self.icon = gtk.StatusIcon()
>        self.icon.set_from_file(sage_icons['default'])
>        self.icon.set_tooltip('Sage Notebook started')
>
>        # Local prep for icon animations.
>        self.update_pixbuf_props()
>        self.update_anim_params(5.0)
>
>    # Store the current icon as a GDK pixbuf and get its parameters.
>    # Used to to prepare for animations.
>    def update_pixbuf_props(self):
>        self.pixbuf = self.icon.get_pixbuf()
>        self.w = self.pixbuf.get_width()
>        self.h = self.pixbuf.get_height()
>        self.pixbuf_mod = gtk.gdk.Pixbuf(gtk.gdk.COLORSPACE_RGB, True, 8,
>                                         self.w, self.h)
>
>    # (re)Set various animation parameters.
>    def update_anim_params(self, seconds):
>        # Duration of an animation, in seconds.
>        self.t = seconds
>        # Frames per second, in theory.
>        self.fps = 24.0
>        # Time between frames, in milliseconds.
>        self.dt_ms = int(1000 / self.fps)
>        # Number of frames.
>        self.frames = int(self.t * self.fps)
>        # Number of animation periods.
>        self.periods = self.t
>        # Period = a * pi.  We convert the unit of time from seconds
>        # to frames.  See below.
>        self.a = self.frames / self.periods / math.pi
>
>    # The dbus.service.method decorator exports a method in the
>    # indicated "interface" via D-Bus.  The in/out signatures may not
>    # be required, but D-Bus' introspection doesn't always work.
>    # Default and variable-length arguments also don't appear to work,
>    # unfortunately.  These issues may be surmountable.
>
>    # Return a list of names of available icons.
>    @dbus.service.method('org.sagemath.TrayIconInterface',
>                         in_signature='', out_signature='as')
>    def get_icon_names(self):
>        return [name for name in sage_icons]
>
>    # Set the icon by keying into the local dictionary.
>    @dbus.service.method('org.sagemath.TrayIconInterface',
>                         in_signature='s', out_signature='')
>    def set_icon_by_name(self, name):
>        self.icon.set_from_file(sage_icons[name])
>
>    # Set the icon using the given pickled pixbuf.  This currently
>    # requires PyGTK to be built with Numeric support.
>    @dbus.service.method('org.sagemath.TrayIconInterface',
>                         in_signature='s', out_signature='')
>    def send_pickled_icon(self, pickled_icon):
>        new_icon_pixbuf = gtk.gdk.pixbuf_new_from_array(
>            pickle.loads(str(pickled_icon)), gtk.gdk.COLORSPACE_RGB, 8 )
>        self.icon.set_from_pixbuf(new_icon_pixbuf)
>
>    # Set the icon tooltip.
>    @dbus.service.method('org.sagemath.TrayIconInterface',
>                         in_signature='s', out_signature='')
>    def set_icon_tooltip(self, text):
>        self.icon.set_tooltip(text)
>
>    # Toggle icon blinking.  The period seems to be one second.
>    @dbus.service.method('org.sagemath.TrayIconInterface',
>                         in_signature='', out_signature='')
>    def toggle_blinking(self):
>        if self.icon.get_blinking():
>            self.icon.set_blinking(False)
>        else:
>            self.icon.set_blinking(True)
>
>    # Blink the icon for an interval, unless it's already blinking.
>    @dbus.service.method('org.sagemath.TrayIconInterface',
>                         in_signature='d', out_signature='')
>    def blink(self, seconds):
>        if not self.icon.get_blinking():
>            self.icon.set_blinking(True)
>            gobject.timeout_add(int(seconds * 1000), self.toggle_blinking)
>
>    # Transform the icon via self.* and a given function.  Afterwards,
>    # restore the original icon.  Note: This is not re-entrant, so
>    # multiple simultaneous calls may be entertaining.
>    def transform_shape(self, func):
>        self.update_pixbuf_props()
>        self.i = 0
>        def advance_frame():
>            self.icon.set_from_pixbuf(func(self.i))
>            if self.i < self.frames:
>                self.i += 1
>                return True
>            self.icon.set_from_pixbuf(self.pixbuf)
>        # Note: time.sleep() doesn't suffice here, since it won't
>        # properly queue GTK events.  Also, gobject.timeout_add()
>        # doesn't seem to work with Python generators.
>        gobject.timeout_add(self.dt_ms, advance_frame)
>
>    # (un)Squeeze the icon horizontally --- make it wink.
>    @dbus.service.method('org.sagemath.TrayIconInterface',
>                         in_signature='d', out_signature='')
>    def squeeze_horizontally(self, seconds):
>        self.update_anim_params(seconds)
>        def func(t):
>            h = max( 1, int( self.h * ( 1.0 - math.sin( t / self.a )**2 ) ) )
>            return self.pixbuf.scale_simple(self.w, h, gtk.gdk.INTERP_HYPER)
>        self.transform_shape(func)
>
>    # (un)Squeeze the icon vertically --- make it wink, sideways.
>    @dbus.service.method('org.sagemath.TrayIconInterface',
>                         in_signature='d', out_signature='')
>    def squeeze_vertically(self, seconds):
>        self.update_anim_params(seconds)
>        def func(t):
>            w = max( 1, int( self.w * ( 1.0 - math.sin( t / self.a )**2 ) ) )
>            return self.pixbuf.scale_simple(w, self.h, gtk.gdk.INTERP_HYPER)
>        self.transform_shape(func)
>
>    # (un)Shrink the icon.
>    @dbus.service.method('org.sagemath.TrayIconInterface',
>                         in_signature='d', out_signature='')
>    def shrink(self, seconds):
>        self.update_anim_params(seconds)
>        def func(t):
>            x = max( 1, int( self.w * ( 1.0 - math.sin( t / self.a )**2 ) ) )
>            return self.pixbuf.scale_simple(x, x, gtk.gdk.INTERP_HYPER)
>        self.transform_shape(func)
>
>    # Modulate the icon's saturation.
>    @dbus.service.method('org.sagemath.TrayIconInterface',
>                         in_signature='dd', out_signature='')
>    def saturate(self, seconds, amplitude):
>        self.update_anim_params(seconds)
>        phase = math.asin( 1.0 / amplitude - 1.0 )
>        def func(t):
>            sat = amplitude * ( 1.0 + math.sin( 2.0 * t / self.a + phase ) )
>            self.pixbuf.saturate_and_pixelate(self.pixbuf_mod, sat, False)
>            return self.pixbuf_mod
>        self.transform_shape(func)
>
>    # Kill the tray icon by quitting the GTK event loop.  This is
>    # currently irreversible.
>    @dbus.service.method('org.sagemath.TrayIconInterface',
>                         in_signature='', out_signature='')
>    def quit_icon(self):
>        loop.quit()
>
> # If we're called as shell script, we make a new Sage tray icon,
> # export selected methods over the session bus, and enter a GTK event
> # loop.  Note: A running loop is required for a functioning (e.g.,
> # visible) icon.
> if __name__ == '__main__':
>    dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
>
>    bus = dbus.SessionBus()
>    name = dbus.service.BusName('org.sagemath.TrayIconService', bus)
>    exported_icon = SageTrayIcon(bus, '/SageTrayIcon')
>
>    loop = gobject.MainLoop()
>    loop.run()
>
> tray icon
> system:sage
>
> <p>Use D-Bus' introspection to get an ugly list of methods exported by the tray icon process:</p>
>
> {{{id=41|
> tray_icon.dbus_introspect()
> ///
> }}}
>
> <p>Get the names of the remote icons:</p>
>
> {{{id=44|
> map(str,tray_icon.get_icon_names())
> ///
> }}}
>
> <p>Change the icon a couple of times:</p>
>
> {{{id=36|
> tray_icon.set_icon_by_name('email')
> ///
> }}}
>
> {{{id=10|
> tray_icon.set_icon_by_name('default')
> ///
> }}}
>
> <p>Update the tooltip:</p>
>
> {{{id=46|
> tray_icon.set_icon_tooltip('Sage Notebook is running...')
> ///
> }}}
>
> <p>Sample some effects, not all at once:</p>
>
> {{{id=48|
> tray_icon.toggle_blinking()
> ///
> }}}
>
> {{{id=49|
> tray_icon.toggle_blinking()
> ///
> }}}
>
> {{{id=35|
> tray_icon.blink(3)
> ///
> }}}
>
> {{{id=33|
> tray_icon.squeeze_vertically(3)
> ///
> }}}
>
> {{{id=29|
> tray_icon.squeeze_horizontally(3)
> ///
> }}}
>
> {{{id=31|
> tray_icon.shrink(3)
> ///
> }}}
>
> {{{id=30|
> tray_icon.saturate(3.0, 2.0)
> ///
> }}}
>
> <p>Get the names of our local icons:</p>
>
> {{{id=53|
> tray_icon.get_sendable_icon_names()
> ///
> }}}
>
> <p>Send a local icon to the tray icon process via D-Bus:</p>
>
> {{{id=52|
> tray_icon.send_icon_by_name('alert')
> ///
> }}}
>
> <p>Compose a simple effect, which blocks until finished:</p>
>
> {{{id=54|
> import time
> for i in xrange(10):
>    tray_icon.send_icon_by_name('smile')
>    time.sleep(0.75)
>    tray_icon.send_icon_by_name('tongue')
>    time.sleep(0.25)
> ///
> }}}
>
> <p>Quit the tray icon process:</p>
>
> {{{id=47|
> tray_icon.quit_icon()
> ///
> }}}
>
> {{{id=55|
>
> ///
> }}}
> # Change this to access an icon process from command-line Sage.  Be
> # sure the icon process is running.  See that script for more detail.
> if sage_mode == 'notebook':
>   import sys
>
>   # Use the system's installation of dbus-python and PyGTK.
>   sys.path.append('/usr/lib/python2.5/site-packages/')
>   sys.path.append('/usr/lib/python2.5/site-packages/dbus')
>   sys.path.append('/usr/lib64/python2.5/site-packages/')
>   sys.path.append('/usr/lib64/python2.5/site-packages/gtk-2.0')
>
>   import gtk
>   import dbus, dbus.mainloop.glib
>
>   # Find the exported tray icon and specify an interface.
>   dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
>   bus = dbus.SessionBus()
>   proxy_icon = bus.get_object('org.sagemath.TrayIconService','/SageTrayIcon')
>   tray_icon = dbus.Interface(proxy_icon,
>                              dbus_interface='org.sagemath.TrayIconInterface')
>
>   # D-Bus' introspection tells us what's been exported.  Perhaps it's
>   # possible to get this to work transparently with Sage's
>   # intropection.  Note: proxy_icon.Introspect() gives the same
>   # results.
>   def dbus_introspect():
>      print str( tray_icon.Introspect(
>            dbus_interface='org.freedesktop.DBus.Introspectable' ) )
>
>   # We mock up a convenience method for tray_icon.
>   tray_icon.dbus_introspect = dbus_introspect
>
>   # If Numeric is installed, and PyGTK uses it, we can actually send
>   # pickled pixbufs, via D-Bus, to the tray icon process.
>   sys.path.append('/usr/lib64/python2.5/site-packages/Numeric')
>
>   import os
>   import Numeric
>   import cPickle as pickle
>
>   # The tray icon process has its own dictionary of icons.  We make
>   # our own dictionary of icons.
>   new_icon_home = SAGE_ROOT + '/local/share/moin/htdocs/modern/img/'
>   new_sage_icons = { 'alert'     : 'alert.png',
>                      'attention' : 'attention.png',
>                      'smile'     : 'smile.png',
>                      'tongue'    : 'tongue.png' }
>   for name in new_sage_icons:
>      new_sage_icons[name] = new_icon_home + new_sage_icons[name]
>
>   # More convenience methods, analogous to those exported via D-Bus.
>   def get_sendable_icon_names():
>      return [name for name in new_sage_icons]
>
>   tray_icon.get_sendable_icon_names = get_sendable_icon_names
>
>   def send_icon_by_name(name):
>      pixbuf = gtk.gdk.pixbuf_new_from_file(new_sage_icons[name])
>      tray_icon.send_pickled_icon(pickle.dumps(pixbuf.get_pixels_array()))
>   tray_icon.send_icon_by_name = send_icon_by_name
>
>



--
William Stein
Associate Professor of Mathematics
University of Washington
http://wstein.org

William Stein

unread,
Mar 29, 2009, 11:22:33 PM3/29/09
to sage-devel
On Sun, Mar 29, 2009 at 7:20 PM, William Stein <wst...@gmail.com> wrote:
> On Sun, Mar 29, 2009 at 7:15 PM, Pat LeSmithe <qed...@gmail.com> wrote:
>>
>> Thanks to an unexpected grant of round tuits [1] from the Back Burner
>> Foundation for Eventual Development, here's a somewhat improved tray
>> icon for Sage.
>>
>> Besides PyGTK, the main script now uses Python's bindings to
>> freedesktop.org's D-Bus to export several methods for manipulating the
>
> Could you post some screenshots?

For people wondering what the fuss is, Pat sent me a video of what
he's doing in action:

http://sage.math.washington.edu/home/wstein/tmp/tray_icon.mpeg

You can watch the above using, e.g., VLC.

William

Pat LeSmithe

unread,
Mar 29, 2009, 11:31:36 PM3/29/09
to sage-...@googlegroups.com

William Stein wrote:
> On Sun, Mar 29, 2009 at 7:15 PM, Pat LeSmithe <qed...@gmail.com> wrote:
>> Thanks to an unexpected grant of round tuits [1] from the Back Burner
>> Foundation for Eventual Development, here's a somewhat improved tray
>> icon for Sage.
>>
>> Besides PyGTK, the main script now uses Python's bindings to
>> freedesktop.org's D-Bus to export several methods for manipulating the
>
> Could you post some screenshots?

Good idea! I've attached a couple of boring PNGS. These show the Sage
logo and an email icon bundled with Sage, the latter for no particular
reason.

I've also made a short MPEG2 movie. Dr. Stein has very kindly hosted it
and provided a link in an earlier message.

In the movie, xvidcap, not the animated icon or Sage, is taxing
[one-half of] the CPU. I'm pretty sure the red lines in the icon are an
artifact of compression.

tray_icon1.png
tray_icon2.png
Reply all
Reply to author
Forward
0 new messages