Main index > About new desklets > Calendar desklet by darkliquid

By syfou (Core Developer & Desklet Author), on Sun Feb 27 18:39:27 2005, last edited on Sun Feb 27 19:22:44 2005: Calendar desklet by darkliquid.

Moved here from an original post by darkliquid, copied verbatim::

This probably isn't the place to post this, but I'm writing a really basic calendar aDesklet. Basically its ust the output of calendar.month() (for todays date) draw on the screen.

However, I'm new both to python and to adesklets so I'm inevitaly having problems :P

Essentially I've nabbed a whole bunch of code from the mailer aDesklet and ripped out all the display routines and email routines. I've replaced the email section with a simple call to calendar.month and tried various methods based on what I understood from the mailer to try and get it to draw.

Instead, all I seem to get is this error:

Code:

Traceback (most recent call last):
  File "Calendar.py", line 150, in ?
    Events(dirname(__file__)).pause()
  File "Calendar.py", line 68, in __init__
    adesklets.Events_handler.__init__(self)
  File "usr/lib/python2.4/site-packages/adesklets/events_handler.py", line 159, in __init__
  File "usr/lib/python2.4/site-packages/adesklets/events_handler.py", line 296, in _alarm
  File "Calendar.py", line 103, in alarm
    self._display()
  File "Calendar.py", line 121, in _display
    adesklets.context_set_image(0)
  File "/usr/lib/python2.4/commands.py", line 374, in context_set_image

  File "usr/lib/python2.4/site-packages/adesklets/commands_handler.py", line 103, in out
adesklets.error_handler.ADESKLETSError: adesklets command error - syntax error


I can't seem to track down what the problem is, but here is my code:



Code:

"""
Calendar.py - darkliquid <darkliquid@darkliquid.co.uk>, 2005

Simple, adesklets calendar script:
        Basically, this should simply grab the output
                of the calendar.month9) function and display
        on the screen using the fonts defined from the
        config file. Eventually, I plan to add more
        functionality to this so that different fonts
        for various parts can be altered and then who knows?

"""
import adesklets
from os import getenv, spawnlp, P_NOWAIT
from os.path import join, dirname

class Config(adesklets.ConfigFile):
        """
        This is calendar.py desklet configuration file;
        """

        cfg_default = { 'heading_font': 'Vera',
                                        'heading_font_size': 14,
                                        'heading_color': 'FFFFFF',
                                        'heading_opacity': 255,
                                        'date_font': 'Vera',
                                        'date_font_size': 12,
                                        'date_past_color': 'BBBBBB',
                                        'date_past_opacity': 100,
                                        'date_color': 'FFFFFF',
                                        'date_opacity': 255 }

        def __init__(self,id,filename):
                adesklets.ConfigFile.__init__(self,id,filename)

        def color(self,string):
                return [eval('0x%s' % string[i*2:i*2+2]) for i in range(len(string)/2)]

#------------------------------------------------------------------------------

class CalendarDesklet:
        """
        Class that generates a calendar on the desktop
        """

        import calendar
        import datetime

        def __init__(self, config):
                self.config=config

        def __call__(self):
                now = self.datetime.date.today()
                return self.calendar.month(now.year,now.month)

#------------------------------------------------------------------------------

class Events(adesklets.Events_handler):
        """
        The usual Events handling class
        """

        def __init__(self, basedir):
                if len(basedir)==0:
                        self.basedir='.'
                else:
                        self.basedir=basedir
                adesklets.Events_handler.__init__(self)

        def __del__(self):
                adesklets.Events_handler.__del__(self)

        def ready(self):
                # Do initialisation stuff here
                # Get the config file
                self.config=Config(adesklets.get_id(),
                                                   join(self.basedir,'config.txt'))
                # Send the config data to the Calendar class
                self.calendar_desklet = CalendarDesklet(self.config)

                # Load fonts
                self._heading_font = adesklets.load_font(
                        '%s/%d' % (self.config['heading_font'],
                                           self.config['heading_font_size']))
                if self.config['date_font']:
                        self._date_font = adesklets.load_font(
                                '%s/%d' % (self.config['date_font'],
                                                   self.config['date_font_size']))
                else:
                        self._date_font = self._heading_font

                # Set up window properties
                adesklets.window_set_transparency(True)
                adesklets.menu_add_separator()
                adesklets.menu_add_item('Configure')
                adesklets.window_show()

        def alarm(self):
                """
                Refresh the display as needed
                """
                self.block()
                self._display()
                return max(60,60)       

        def menu_fire(self, delayed, menu_id, item):
                if item=='Configure':
                        editor=getenv('EDITOR')
                        if editor:
                                self._execute('xterm -e %s ' % editor +
                                                          join(self.basedir,'config.txt'))

        def _display(self):
                """
                The main drawing routine
                """
                # Get text dimensions           
                cal = self.calendar_desklet()
                adesklets.context_set_font(self._heading_font)
                w,h = adesklets.get_text_size(cal)
                adesklets.context_set_image(0)

                # Set up a buffer
                buffer = adesklets.create_image(w,h)
                adesklets.context_set_image(buffer)
                adesklets.context_set_blend(False)
                adesklets.context_set_color(0,0,0,0)
                adesklets.image_fill_rectangle(0,0,w,h)
                adesklets.context_set_blend(True)

                # Draw text onto buffer
                adesklets.context_set_font(self._heading_font)
                adesklets.context_set_color(*(self.config.color(
                                                                          self.config['heading_color']) + [255]))
                adesklets.text_draw(0,0,y,cal)
                                
                # Resize window if need be and put everything on foreground
                if w!= adesklets.image_get_width() or h!= adesklets.image_get_height():
                        adesklets.window_resize(w,h)
                adesklets.context_set_image(0)
                adesklets.context_set_blend(False)
                adesklets.blend_image_onto_image(buffer,1,0,0,w,h,0,0,w,h)
                adesklets.free_image(buffer)

        def _execute(self,command):
                spawnlp(P_NOWAIT, command.split()[0], *command.split())

#------------------------------------------------------------------------------
# Start running
Events(dirname(__file__)).pause()


Any help or some basic pointers in the right direction will be most appreciated :)

By syfou (Core Developer & Desklet Author), on Sun Feb 27 18:53:43 2005, last edited on Mon Feb 28 00:45:12 2005.

Not a complex problem here:

the 'cal' string is multi-line, wich make no sence in the context: text_draw and associated routines expect single line strings. Just keep in mind adesklets is basically nothing more than a command line oriented 2D drawing program. All text routines are on the mode: put such string verbatim (hence, no escape characters understood), at given position with such and such attributes.

So, when dealing with such an output, you will have to call get_text_size and associated
multiple time (at least once per line) to get/set the text. You should probably look at quidnovy.py for some pointers.

By syfou (Core Developer & Desklet Author), on Sun Feb 27 19:20:23 2005.

In fact, you should never pass anything that has a multiline representation as an argument to any function belonging to adesklets.commands... :-) Here is why.

The python package was built to be light (as python is made to be used), so there is a few error checking as possible. What really happens here is that, when you use a python function such as:

Code:


w,h = adesklets.get_text_size(cal)



all it does is to convert it to the homonymous adesklets command (get_text_size here), then output the result to the interpreter and wait for it to answer. Suppose that you have, in cal, the corresponding escaped string: " February 2005\nMo Tu We Th Fr Sa Su\n 1 2 3 4 5 6\n 7 8 9 10 11 12 13\n14 15 16 17 18 19 20\n21 22 23 24 25 26 27\n28\n". Since the interpreter expects to have all commands on separated lines, what was just emitted is not considered one, but seven commands:

Code:


1) get_text_size   February 2005
2) Mo Tu We Th Fr Sa Su
3)     1  2  3  4  5  6
4)  7  8  9 10 11 12 13
5) 14 15 16 17 18 19 20
6) 21 22 23 24 25 26 27
7) 28


Of course, only the first one make sense. It would litteraly mean: "give me the text size of the 'February 2005' string (all leading spaces stripped) using the current (i.e. from the context) font". Since the other lines are not understood by the interpreter, it just sees it as invalid commands and start emiting a sequence of syntax error messages.

When you are writing a desklet, here is a few tips that can save you much time:

Now, every time something will not work, you only have to look at the corresponding log output according to ADESKLETS_LOG. If what's not working is still not clear to you, you should try fireing and interactive session:

Code:


adesklets :


Then see how the interpreter reacts to problematic sequences of commands.

By darkliquid (Desklet Author), on Mon Feb 28 02:04:24 2005.

Ahh, excellent, thanks for the help!
Hopefully I'll have this working soon :)

By darkliquid (Desklet Author), on Sun Mar 6 17:43:36 2005.

Okay, this actually works, more or less. It actually displays a calendar on my desktop. There are lots of features to add, and the code is messy, but its not bad for my first ever python app :)

This is what it currently looks like:

http://www.darkliquid.co.uk/media/projects/aDesklets/Calendar-aDesklet-1.png

Code:

"""
Calendar.py - darkliquid <darkliquid@darkliquid.co.uk>, 2005

Simple, adesklets calendar script:
        Basically, this should simply grab the output
                of the calendar.month() function and display
        on the screen using the fonts defined from the
        config file. Eventually, I plan to add more
        functionality to this so that different fonts
        for various parts can be altered and then who knows?

"""
import adesklets
from os import getenv, spawnlp, P_NOWAIT
from os.path import join, dirname

class Config(adesklets.ConfigFile):
        """
        This is calendar.py desklet configuration file;
        """

        cfg_default = { 'heading_font': 'Vera',
                                        'heading_font_size': 14,
                                        'heading_color': 'FFAFAF',
                                        'heading_opacity': 255,
                                        'day_font': 'Vera',
                                        'day_font_size': 12,
                                        'day_colour': 'AFFFAF',
                                        'date_font': 'Vera',
                                        'date_font_size': 12,
                                        'date_past_colour': 'BBBBBB',
                                        'date_past_opacity': 100,
                                        'date_color': 'FFFFFF',
                                        'date_opacity': 255,
                                        'cell_padding': 2 }

        def __init__(self,id,filename):
                adesklets.ConfigFile.__init__(self,id,filename)

        def color(self,string):
                return [eval('0x%s' % string[i*2:i*2+2]) for i in range(len(string)/2)]

#------------------------------------------------------------------------------

class CalendarDesklet:
        """
        Class that generates a calendar on the desktop
        """

        import calendar
        import datetime

        def __init__(self, config):
                self.config=config

        def __call__(self):
                """
                This function grabs the calendar as produced by the calendar
                module and processes it into a more palatable form for later
                manipulation
                """
                now = self.datetime.date.today()
                cal = self.calendar.month(now.year,now.month)
                cal_list = cal.splitlines()
                new_cal = []
                temp_list = []

                for row in xrange(0,7):
                        if (row==0):
                                new_cal.append(cal_list[row:row+1][0].strip())
                        else:
                                for col in xrange(0,21,3):
                                        temp_list.append(cal_list[row:row+1][0][col:col+3].strip())
                                new_cal.append(temp_list)
                                temp_list=[]

                return new_cal

#------------------------------------------------------------------------------

class Events(adesklets.Events_handler):
        """
        The usual Events handling class
        """

        def __init__(self, basedir):
                if len(basedir)==0:
                        self.basedir='.'
                else:
                        self.basedir=basedir
                adesklets.Events_handler.__init__(self)

        def __del__(self):
                adesklets.Events_handler.__del__(self)

        def ready(self):
                # Do initialisation stuff here
                # Get the config file
                self.config=Config(adesklets.get_id(),
                                                   join(self.basedir,'config.txt'))
                # Send the config data to the Calendar class
                self.calendar_desklet = CalendarDesklet(self.config)

                # Load fonts
                self._heading_font = adesklets.load_font(
                        '%s/%d' % (self.config['heading_font'],
                                           self.config['heading_font_size']))
                if self.config['date_font']:
                        self._date_font = adesklets.load_font(
                                '%s/%d' % (self.config['date_font'],
                                                   self.config['date_font_size']))
                else:
                        self._date_font = self._heading_font

                # Set up window properties
                adesklets.window_set_transparency(True)
                adesklets.menu_add_separator()
                adesklets.menu_add_item('Configure')
                adesklets.window_show()

        def alarm(self):
                """
                Refresh the display as needed
                """
                self.block()
                self._display()
                return max(60,60)       

        def menu_fire(self, delayed, menu_id, item):
                if item=='Configure':
                        editor=getenv('EDITOR')
                        if editor:
                                self._execute('xterm -e %s ' % editor +
                                                          join(self.basedir,'config.txt'))

        def cellsize(self, cal):
                """
                This function determines the minimum required cell size to display
                the characters within a cell without overlapping into other cells.
                It determines this by finding the maximum widths and heights in the
                list of cell data (ie: day numbers or days names). It then takes the
                maximum of the final max width and height and returns that (we want
                square cells, right?)
                """
                
                old_w = 0
                old_h = 0
                w = 0
                h = 0

                adesklets.context_set_font(self._heading_font)

                for row in xrange(1,7):
                        for col in xrange(0,7):
                                if (len(cal[row:row+1][0][col:col+1][0]) > 0):
                                        old_w,old_h = adesklets.get_text_size(cal[row:row+1][0][col:col+1][0])
                                        w=max(w,old_w)
                                        h=max(h,old_h)

                return max(w,h)

        def _display(self):
                """
                The main drawing routine
                """
                # Get cell dimensions           
                cal = self.calendar_desklet()
                cellsize = self.cellsize(cal)
                adesklets.context_set_image(0)

                # Calc calendar dimensions
                calsize = (cellsize * 7)
                print calsize

                # Set up a buffer
                buffer = adesklets.create_image(calsize,calsize)
                adesklets.context_set_image(buffer)
                adesklets.context_set_blend(False)
                adesklets.context_set_color(0,0,0,0)
                adesklets.image_fill_rectangle(0,0,calsize,calsize)
                adesklets.context_set_blend(True)

                # Draw text onto buffer
                adesklets.context_set_font(self._heading_font)
                adesklets.context_set_color(*(self.config.color(
                                                                          self.config['heading_color']) + [255]))
                # This bit draws the header
                headersize = adesklets.get_text_size(cal[0:1][0])
                header_position = (calsize / 2) - (headersize[0] / 2)
                adesklets.text_draw(header_position,0,cal[0:1][0])

                # This draws the rest
                for row in xrange(1,7):
                        for col in xrange(0,7):
                                if (len(cal[row:row+1][0][col:col+1][0]) > 0):
                                        adesklets.text_draw((col*cellsize),(row*cellsize),cal[row:row+1][0][col:col+1][0])
                                
                # Resize window and put everything on foreground
                adesklets.window_resize(calsize,calsize)
                adesklets.context_set_image(0)
                adesklets.context_set_blend(False)
                adesklets.blend_image_onto_image(buffer,1,0,0,calsize,calsize,0,0,calsize,calsize)
                adesklets.free_image(buffer)

        def _execute(self,command):
                spawnlp(P_NOWAIT, command.split()[0], *command.split())

#------------------------------------------------------------------------------
# Start running
Events(dirname(__file__)).pause()



Any suggestions on improvements are appreciated, especially if some code hints are thrown in too :)

So far, what i'm planning to add is:
Custom font, colour, background colour for the header (Ie the month/year bit), the day names, the date numbers and the current date
Ability to add cellpadding
Ability to add cell borders and an overall calendar border
Ability to have a background or background image for the whole calendar

By exilejedi (Core Developer & Desklet Author), on Sun Mar 6 18:41:57 2005.

Looks like you are making good progress!

One cool little trick you should do is to make your CalendarDesklet class inherit from the mystical "object" class. This makes it what is referred to as a "new-style class", which, among other things, means that your class gets instantiated a LOT faster.

I think you might also want to look into replacing some of your list-building iterative logic with some list comprehensions, as they can be a lot more efficient as well, and also a little cleaner code-wise. For a trivial example, you could replace something like this:

Code:

lstFoo = []
for strBar in lstBaz:
    lstFoo.append(strBar.strip())



With this:

Code:

lstFoo = [x.strip() for x in lstBaz]



(Apologies for the Hungarian notation; we use it constantly at work. It's maybe not that accurate in a dynamic language like Python, but it's really handy when you're working with someone else's code to have an idea of what the author was thinking.)

I'm looking forward to being able to replace my gdeskcal with this. Keep up the good work! :-)

By exilejedi (Core Developer & Desklet Author), on Sun Mar 6 18:44:22 2005.

Oh, one other thing that I meant to suggest... It would be really cool if you abstracted out your display routine a bit... The core of it could live in fairly abstracted base class, and you could then supply additional subclasses that would allow you to skin/theme the calendar.

By syfou (Core Developer & Desklet Author), on Sun Mar 6 20:24:04 2005.

This is very good - it doesn't show at all it is your first time with the language. :-)

I looked at the code, no allocation problem here, nothing strange in any way I could see. Only one suggestion come to my mind for now: you should propably split the drawing routine in two, one part drawing the background, the other all the forground (text, etc.), since it will avoid you to force a refresh of the whole window every time... Look at the test/bumpmap.py from base adesklets distribution for an example.

By TimSchutte (Fearless Moderator), on Sun Mar 6 21:28:32 2005.

Very nice calendar applet!

Congrats--
Tim

By darkliquid (Desklet Author), on Mon Mar 7 02:41:36 2005.

Thanks for all the positive comments!

I kind of got something working and just rushed to the inish line without taking the much prettier scenic route.

I plan on optimising and generally improving the current codebase to make it easier to extend, but I wanted some instant gratification :P

exilejedi wrote:

think you might also want to look into replacing some of your list-building iterative logic with some list comprehensions


Yes, I was thinking of doing this, but I still haven't quite grasped how they work yet, but now that I have something that works, I'm a lot happier to start playing with such things.

exilejedi wrote:

Oh, one other thing that I meant to suggest... It would be really cool if you abstracted out your display routine a bit... The core of it could live in fairly abstracted base class, and you could then supply additional subclasses that would allow you to skin/theme the calendar.


Yes, I was thinking of this too. Ideally, I'd like to move most of the actual drawing to the Calendar class itself so I have more control of things without having to pass around various things between classes. I'd like to have an abstract base class that allows me to override the common methods.

So far the drawing routine goes: find header cell, find cells to render, find what size they should be, render header cell, render cells.

I plan on making this done via methods so each stage of the drawing routine can be overridden, allowing for such things as using images instead of solid colours.

syfou wrote:

Only one suggestion come to my mind for now: you should propably split the drawing routine in two


Yes, sounds god to me, especially since I wanted to split p the drawig routine anyway.


Anyway, thanks for the advice and support everyone. Now all I need to do is learn OOP with Python, get my head around the list comprehensions and play with it all till I get what I like. Hopefully the next release won't be very different visually, but will have much nicer code to work with so future development should hopefully accelerate

By exilejedi (Core Developer & Desklet Author), on Wed Mar 9 17:12:19 2005.

Darkliquid,

In order to run the Calendar.py directly, the first line needs to have something like this:

Code:

#!/usr/bin/env python


Otherwise it tries to invoke everything as a shell command, and that isn't pretty...

Maybe you've already fixed this, and I just got an old copy from the sourceforge mirror. *shrug*

Still, glad to see it finally released to the world! :-)

Cheers,

Mike

By darkliquid (Desklet Author), on Thu Mar 10 02:56:17 2005.

exilejedi wrote:

Darkliquid,

In order to run the Calendar.py directly, the first line needs to have something like this:

Code:

#!/usr/bin/env python


Otherwise it tries to invoke everything as a shell command, and that isn't pretty...


Doh! If its not one thing after another! I knew I'd forget something simple like that! I'll modify the package on my site. Syfou, do you want me to send it again or will you just copy the new package off my page?

By syfou (Core Developer & Desklet Author), on Thu Mar 10 03:32:28 2005.

darkliquid wrote:


Syfou, do you want me to send it again or will you just copy the new package off my page?

Please submit it again, as an upgraded package (incremented version number), otherwise It will be rejected. It shouldn't take you more than one shot this time, since all your other minor problems are already corrected :-)... Keep in mind I live in UTC-5, and that I need some sleep: it could take a few hours before I pick up your submition. Thanks,


adesklets is proud to be hosted on:

SourceForge.net Logo

Back to adesklets.sf.net.