Screenshot

Screenshot

Calendar 1.06

Simple PyS60 script which accesses the calendar and displays all entries of the next two weeks on a scrollable (touch) canvas. Pretty cool how easy one can create a Python application on a symbian mobile which can even be distributed as an SIS file.

Download

Download python script
Download SIS package

Known issues:
- Quit button not works (binding events on display-coordinates not works as expected)
- Time displayed in the header gets not updated (have to implemtent an timer here)

Python code

__package__ = "calendar"
__version__ = "1.06"
__author__ = "Kai Winter"
__url__ = "http://www.vorlesungsfrei.de"
__copyright__ = "Copyright (c) 2010 Kai Winter"

import appuifw
import key_codes
import e32
import e32calendar

import time
from datetime import datetime
from graphics import *

HEADER_HEIGHT = 30
FOOTER_HEIGHT = 30

class Painter():
    """ Class handling the painting of the calender events on a dragable canvas. """
    def __init__(self, calender_events):
        # 1D-scrolling will do
        self.down_coordinate_y = 0
        self.move_pixels_y = HEADER_HEIGHT
        self.canvas = appuifw.Canvas(redraw_callback=self.redraw_callback)
        self.canvas_width = self.canvas.size[0]

        # Bind handlers
        self.canvas.bind(key_codes.EButton1Down, self.button_down)
        self.canvas.bind(key_codes.EDrag, self.drag_canvas)

        # Create content
        self.create_timesheet(calender_events)
        self.create_header()
        self.create_footer()

        appuifw.app.body = self.canvas

        self.canvas.blit(self.imgInfo, target=(0, 0))
        self.canvas.blit(self.imgFooter, target=(0, self.canvas.size[1] - FOOTER_HEIGHT))

    def create_header(self):
        """ Creates the header image to display the current time. """
        # Create header with current time
        self.imgInfo = Image.new((self.canvas_width, HEADER_HEIGHT))
        # Display current time
        current_time = unicode(render_datetime(datetime.now(), True, False))
        text_width = self.imgInfo.measure_text(current_time)[1]
        self.imgInfo.rectangle(((0, 0), (self.canvas_width, HEADER_HEIGHT)), fill=0xddddff)
        self.imgInfo.text(((self.canvas_width / 2) - (text_width / 2), 20), current_time)
        self.imgInfo.line(((0, HEADER_HEIGHT - 1), (self.canvas_width, HEADER_HEIGHT - 1)), outline=0x000000)

    def create_footer(self):
        """ Creates the footer image to display buttons. """
        self.imgFooter = Image.new((self.canvas_width, FOOTER_HEIGHT))
        self.imgFooter.rectangle(((0, 0), (self.canvas_width, FOOTER_HEIGHT)), fill=0xddddff)
        self.imgFooter.line(((0, 0), (self.canvas_width, 0)), outline=0x000000)
        self.imgFooter.text((10, 20), u"Quit")

        #self.canvas.bind(key_codes.EButton1Down, quit, ((0, 520), (400, 800)))

    def create_timesheet(self, calendar_events):
        text_height = self.canvas.measure_text(u"test")[0]
        text_height2 = self.canvas.measure_text(u"test", font='annotation')[0]
        text_height = text_height[2] - text_height[0]
        text_height2 = text_height2[2] - text_height2[0]
        whole_text_height = text_height + text_height2 - 4
        offset_top = 20
        offset_left = 10
        flip_color = False
        canvas_height = len(calendar_events) * (whole_text_height + 1)
        self.img = Image.new((self.canvas_width, canvas_height))

        self.img.rectangle([(0, 0), self.img.size], fill=0x000000)
        for entry in calendar_events:
            color = 0xeeeeff
            if flip_color:
                color = 0xf7f7ff
            flip_color = not flip_color
            self.img.rectangle([(0, -20 + offset_top), (self.canvas_width, -20 + offset_top + (whole_text_height))], fill=color)
            entry_time = entry[1]
            entry_text = entry[0]
            self.img.text((offset_left, offset_top + 5), unicode(entry_text), font='annotation')
            offset_top += 20
            self.img.text((offset_left, offset_top + 5), unicode(entry_time))
            offset_top += 40

    def drag_canvas(self, event):
        img_size_y = self.img.size[1]
        canvas_size_y = self.canvas.size[1]
        previous_move_pixels = self.move_pixels_y
        # save last, to skip rendering, if no not needed
        result_scroll = -self.down_coordinate_y + event[1]

        if result_scroll > 0 + HEADER_HEIGHT:
            # scrolled to high
            self.move_pixels_y = HEADER_HEIGHT
            self.button_down(event)
        elif result_scroll + img_size_y < canvas_size_y - FOOTER_HEIGHT + 1:
            # scrolled too far down
            self.move_pixels_y = -img_size_y + canvas_size_y - FOOTER_HEIGHT + 1
            self.button_down(event)
        elif img_size_y > canvas_size_y - HEADER_HEIGHT - FOOTER_HEIGHT:
            #scroll
            self.move_pixels_y = result_scroll
        # only draw if canvas was scrolled
        if (previous_move_pixels != self.move_pixels_y):
            self.redraw_callback(event)

    def button_down(self, event):
        self.down_coordinate_y = event[1] - self.move_pixels_y

    def redraw_callback(self, rect):
        self.canvas.blit(self.img, source=((0, -self.move_pixels_y + HEADER_HEIGHT), (self.canvas_width, -self.move_pixels_y + self.canvas.size[1] - FOOTER_HEIGHT)), target=(0, HEADER_HEIGHT))
        #self.canvas.blit(self.imgInfo, target=(0, 0))
        #self.canvas.blit(self.imgFooter, target=(0, self.canvas.size[1] - FOOTER_HEIGHT))
        #self.canvas.rectangle(((0, self.canvas.size[1] - FOOTER_HEIGHT), (self.canvas_width, self.canvas.size[1])), fill=0x000000)

    def quit(self):
        app_lock.signal()

    def redraw(self):
        self.canvas.blit(self.imgInfo, target=(0, 0))
        self.canvas.blit(self.imgFooter, target=(0, self.canvas.size[1] - FOOTER_HEIGHT))

def render_datetime(timestamp, with_time=True, skipMidnight=True):
    """ Creates a string representation of a timestamp """
    return_string = timestamp.strftime("%a %d.%m.")
    if with_time:
        if skipMidnight and timestamp.hour == 0 and timestamp.minute == 0 and timestamp.second == 0:
            pass
        else:
            return_string += timestamp.strftime(" / %H:%M")
    return return_string

def get_calender_events():
    """ Returns a tuple (title, subtitle) """
    cal = e32calendar.open()

    current_datetime = datetime.now()
    current_time = time.time()
    seconds_a_day = 24 * 60 * 60

    entries = []
    seen_ids = []
    for i in range (0, 51):
        day_to_fetch = current_time + (i * seconds_a_day)
        day_entries = cal.daily_instances(day_to_fetch)
        for entry in day_entries:
            entry = cal[entry["id"]]
            entry_id = entry.id
            if entry_id in seen_ids:
                continue

            entry_time = datetime.fromtimestamp(entry.start_time)
            entry_time_end = datetime.fromtimestamp(entry.end_time)
            entry_time_end_check = datetime.fromtimestamp(entry.end_time - 1)
            entry_text = entry.content

            # Change timestamp to current year
            entry_time = entry_time.replace(year=current_datetime.year)

            if entry_time.day == entry_time_end_check.day:
                entries.append((entry_text, render_datetime(entry_time)))
            else:
                entries.append((entry_text, render_datetime(entry_time) + " - " + render_datetime(entry_time_end)))
            seen_ids.append(entry_id)
    return entries

def quit():
    app_lock.signal()

def is_visible(visible):
    if visible:
        p.redraw()

appuifw.app.exit_key_handler = quit
appuifw.app.directional_pad = False

calender_events = get_calender_events()
app_lock = appuifw.e32.Ao_lock()
p = Painter(calender_events)
appuifw.app.screen = 'large'
appuifw.app.orientation = 'portrait'
appuifw.app.focus = is_visible

app_lock.wait()