2011-11-28

Email reminder

Oftentimes while on the road, I think of something I need to do and would like to remind myself by sending an email to myself with the task name as the email subject.

Doing that with my Android phone is not difficult, but I prefer to write a Python script to streamline it, so that I don't have to go through the hassle of selecting myself as the recipient, etc.  Here is the script:

import android
droid = android.Android()
task = droid.dialogGetInput("ToDo", "Task?", "").result
if task:
    droid.sendEmail('myself@example.com', '[ToDo] ' + task, task)

A simple five lines script can boost my productivity and also bring me back the good old feeling of programming my calculator!



2011-11-18

Analog Clock in PyGTK

I was searching the web for some PyGTK tutorial and came across this one to build an analog clock.  However only the clock face is done.  The hour, minute and second hands are yet to be implemented.


Then I came across this tutorial which implements the clock hands but it is done in gtkmm (C++).  So I translated the C++ code to PyGTK in the draw() method:


# compute the angles in radians of the hands of the clock
now = datetime.datetime.now()
hours = now.hour * math.pi / 6
minutes = now.minute * math.pi / 30
seconds = now.second * math.pi / 30

context.set_line_cap(cairo.LINE_CAP_ROUND)

# draw the hours hand
context.save()
context.set_source_rgba(0.337, 0.612, 0.117, 0.9) # green
context.set_line_width(7)
context.move_to(x, y)
context.line_to(x + math.sin(hours + minutes/12) * (radius * 0.5),
                y - math.cos(hours + minutes/12) * (radius * 0.5))
context.stroke()
context.restore()

# draw the minutes hand
context.save()
context.set_source_rgba(0.117, 0.337, 0.612, 0.9) # blue
context.set_line_width(5)
context.move_to(x, y)
context.line_to(x + math.sin(minutes + seconds/60) * (radius * 0.8),
                y - math.cos(minutes + seconds/60) * (radius * 0.8))
context.stroke()
context.restore()

# draw the seconds hand
context.save()
context.set_source_rgba(0.7, 0.7, 0.7, 0.8) # gray
context.set_line_width(3)
context.move_to(x, y)
context.line_to(x + math.sin(seconds) * (radius * 0.9),
                y - math.cos(seconds) * (radius * 0.9))
context.stroke()
context.restore()



At this point the clock hands are not moving yet.  We need a timer to move the clock hands every second.  I need some extra help to translate the gtkmm timer code to PyGTK, and this tutorial just did that.  What it does is to set up a timer in __init__():

gobject.timeout_add_seconds(1, self.on_timeout)

to invalidate the clock face every second:

def on_timeout(self):
    win = self.get_window()
    rect = self.get_allocation()
    win.invalidate_rect(rect, False)
    return True


Now we have an analog clock written in PyGTK.  Here is the complete source code:

import cairo
import datetime
import gobject
import gtk
import math

class ClockFace(gtk.DrawingArea):
    def __init__(self):
        gtk.DrawingArea.__init__(self)
        self.connect("expose_event", self.expose)
        gobject.timeout_add_seconds(1, self.on_timeout)
       
    def expose(self, widget, event):
        self.context = widget.window.cairo_create()
        self.context.rectangle(event.area.x, event.area.y,
                               event.area.width, event.area.height)
        self.context.clip()
        self.draw(self.context)
        return False

    def on_timeout(self):
        win = self.get_window()
        rect = self.get_allocation()
        win.invalidate_rect(rect, False)
        return True

    def draw(self, context):
        rect = self.get_allocation()
        x = rect.x + rect.width / 2
        y = rect.y + rect.height / 2
        radius = min(rect.width / 2, rect.height / 2) - 5

        # clock background
        context.arc(x, y, radius, 0, 2 * math.pi)
        context.set_source_rgb(1, 1, 1)
        context.fill_preserve()
        context.set_source_rgb(0, 0, 0)
        context.stroke()

        # clock ticks
        for i in range(12):
            context.save()
            if i % 3 == 0:
                inset = 0.2 * radius
            else:
                inset = 0.1 * radius
                context.set_line_width(0.5 * context.get_line_width())

            context.move_to(x + (radius-inset) * math.cos(i * math.pi/6),
                                 y + (radius-inset) * math.sin(i * math.pi/6))
            context.line_to(x + radius * math.cos(i * math.pi/6),
                                 y + radius * math.sin(i * math.pi/6))
            context.stroke()
            context.restore()

        # compute the angles in radians of the hands of the clock
        now = datetime.datetime.now()
        hours = now.hour * math.pi / 6
        minutes = now.minute * math.pi / 30
        seconds = now.second * math.pi / 30

        context.set_line_cap(cairo.LINE_CAP_ROUND)

        # draw the hours hand
        context.save()
        context.set_source_rgba(0.337, 0.612, 0.117, 0.9) # green
        context.set_line_width(7)
        context.move_to(x, y)
        context.line_to(x + math.sin(hours + minutes/12) * (radius * 0.5),
                        y - math.cos(hours + minutes/12) * (radius * 0.5))
        context.stroke()
        context.restore()

        # draw the minutes hand
        context.save()
        context.set_source_rgba(0.117, 0.337, 0.612, 0.9) # blue
        context.set_line_width(5)
        context.move_to(x, y)
        context.line_to(x + math.sin(minutes + seconds/60) * (radius * 0.8),
                        y - math.cos(minutes + seconds/60) * (radius * 0.8))
        context.stroke()
        context.restore()

        # draw the seconds hand
        context.save()
        context.set_source_rgba(0.7, 0.7, 0.7, 0.8) # gray
        context.set_line_width(3)
        context.move_to(x, y)
        context.line_to(x + math.sin(seconds) * (radius * 0.9),
                        y - math.cos(seconds) * (radius * 0.9))
        context.stroke()
        context.restore()

def main():
    window = gtk.Window()
    clock = ClockFace()

    window.add(clock)
    window.connect("destroy", gtk.main_quit)
    window.show_all()

    gtk.main()

if __name__ == '__main__':
    main()

2011-11-06

Kasio - Find Button

Continue from the last post, we are going to implement the Find button and wrap up the project.

We are going to use regular expression, so let's add an import statement:

import re

Next we implement findnext() as below:

def findnext(self, pattern):
    '''search pattern from cursor position onward.'''
    if not pattern: # ask if no pattern specified
        dlg = wx.TextEntryDialog(self, "Find?", "Attention", "",
                                 style=wx.OK|wx.CANCEL)
        if dlg.ShowModal() == wx.ID_OK:
            pattern = dlg.GetValue()
    if pattern:
        self.lastfind = pattern
        curpos = self.textbox.GetSelection()[1] # end of current selection
        endpos = self.textbox.GetLastPosition() # eof
        # retrieve the content in the text area from current position onward
        content = self.textbox.GetRange(curpos, endpos)
        # find the next match using regexp & case insensitive
        mo = re.search(pattern, content, re.I)
        # select the match and scroll to it
        if mo:
            self.textbox.SetSelection(curpos + mo.start(), curpos + mo.end())
            self.textbox.ShowPosition(curpos + mo.start())
        else:
            dlg = wx.MessageDialog(None, 'Not found.', 'Attention', wx.OK)
            dlg.ShowModal()

What it does is to ask the user for the search pattern if it is not provided, and then search the text area from the current position onward until it reaches the end.  If a match is found, select it and scroll to it.  It will also save the search pattern to an instance variable lastfind for later use.

Next we write the Find button event handler, which simply calls findnext() and then set the focus back to the text area:

def onFind(self, event):
    self.findnext('')
    self.textbox.SetFocus()


Next we implement Find Next (F3).  First we need to initialize the lastfind instance variable to an empty string, as well as binding the Key Press event in __init__():

    self.lastfind = ''
    textbox.Bind(wx.EVT_KEY_DOWN, self.onKeyPress)

and implement the event handler as:

def onKeyPress(self, event):
    keycode = event.GetKeyCode()
    if keycode == wx.WXK_F3:
        self.findnext(self.lastfind)
    else:
        event.Skip()

It simply calls findnext() with the instance variable lastfind.  That means if this is the first time doing a find, the user will be asked to enter a search pattern because the instance variable lastfind is empty.

That's it.  Both Find and Find Next are now implemented!

To wrap up the project, I add a feature to change the default directory on startup if a particular environmental variable is set with the following code in __init__():

    # change to default directory
    if os.environ.has_key('KASIO_PATH'):
        os.chdir(os.environ['KASIO_PATH'])


Below is the complete source code for the GUI part of this project, with additional keyboard shortcuts for Open, Save, Find and Quit.  Also in order to make it runs seamlessly, focus is switched back to the text area whenever a button is clicked.

# kasio.py

import re
import os
import sys

import wx

import filecrypto

class MainFrame(wx.Frame):
    def __init__(self, parent=None, id=(-1), title='kasio', size=(800,600)):
        wx.Frame.__init__(self, parent, id, title, size=size)
        self.panel = wx.Panel(self, wx.ID_ANY)

        textbox = wx.TextCtrl(self.panel, -1, style=wx.TE_MULTILINE)
        font = wx.Font(10, wx.FONTFAMILY_MODERN, wx.NORMAL, wx.NORMAL)
        textbox.SetFont(font)
        btnOpen = wx.Button(self.panel, -1, 'Open')
        btnSave = wx.Button(self.panel, -1, 'Save')
        btnFind = wx.Button(self.panel, -1, 'Find')
        btnExit = wx.Button(self.panel, -1, 'Exit')

        self.Bind(wx.EVT_BUTTON, self.onOpen, btnOpen)
        self.Bind(wx.EVT_BUTTON, self.onSave, btnSave)
        self.Bind(wx.EVT_BUTTON, self.onFind, btnFind)
        self.Bind(wx.EVT_BUTTON, self.onExit, btnExit)

        topSizer = wx.BoxSizer(wx.VERTICAL)
        btnSizer = wx.BoxSizer(wx.HORIZONTAL)

        btnSizer.Add(btnOpen, 0, wx.ALL, 1)
        btnSizer.Add(btnSave, 0, wx.ALL, 1)
        btnSizer.Add(btnFind, 0, wx.ALL, 1)
        btnSizer.Add(btnExit, 0, wx.ALL, 1)

        topSizer.Add(textbox, 1, wx.ALL|wx.EXPAND, 1)
        topSizer.Add(wx.StaticLine(self.panel), 0, wx.ALL|wx.EXPAND, 3)
        topSizer.Add(btnSizer, 0, wx.ALL|wx.ALIGN_RIGHT, 3)
      
        self.panel.SetSizer(topSizer)
        #topSizer.Fit(self) # minimum size to show all controls
        self.Centre() # center the frame in desktop
        btnOpen.SetDefault()

        self.filename = ''
        self.password = ''
        self.textbox = textbox

        self.lastfind = ''
        textbox.Bind(wx.EVT_KEY_DOWN, self.onKeyPress)
        textbox.SetFocus()

        # change to default directory
        if os.environ.has_key('KASIO_PATH'):
            os.chdir(os.environ['KASIO_PATH'])

    def onOpen(self, event):
        try:
            # ask for file
            dlg = wx.FileDialog(self, "Choose a file", os.getcwd(), "", "*",
                wx.OPEN|wx.CHANGE_DIR)
            if dlg.ShowModal() == wx.ID_OK:
                self.filename = dlg.GetPath()
            else:
                return
            # ask for password
            dlg = wx.TextEntryDialog(self, "Password?", "Attention", "",
                style=wx.OK|wx.CANCEL|wx.TE_PASSWORD)
            if dlg.ShowModal() == wx.ID_OK:
                self.password = dlg.GetValue()
            else:
                return
            # create file cipher
            fc = filecrypto.FileCrypto()
            # read & decrypt file
            plaintext = fc.load(self.filename, self.password)
            # convert 8-bit string in utf-8 encoding to unicode string
            plaintext = plaintext.decode('utf-8')
            # populate text control
            self.textbox.SetValue(plaintext)
        except:
            self.filename = ''
            self.password = ''
            self.textbox.SetValue('')
            errmsg = str(sys.exc_info()[1])
            print(errmsg)
            dlg = wx.MessageDialog(None, 'Password Incorrect!', 'Attention', wx.OK | wx.ICON_EXCLAMATION)
            dlg.ShowModal()
        finally:
            self.textbox.SetFocus()

    def onSave(self, event):
        try:
            # if no filename, ask filename and password
            if len(self.filename) == 0:
                # ask for filename
                dlg = wx.FileDialog(self, "Choose a file", os.getcwd(), "", "*",
                    wx.SAVE|wx.OVERWRITE_PROMPT|wx.CHANGE_DIR)
                if dlg.ShowModal() == wx.ID_OK:
                    self.filename = dlg.GetPath()
                else:
                    return
                # ask for password
                dlg = wx.TextEntryDialog(self, "Password?", "Attention", "",
                    style=wx.OK|wx.CANCEL|wx.TE_PASSWORD)
                if dlg.ShowModal() == wx.ID_OK:
                    self.password = dlg.GetValue()
                else:
                    return
            # create file cipher
            fc = filecrypto.FileCrypto()
            # get the text
            plaintext = self.textbox.GetValue()
            # convert unicode string to 8-bit string in utf-8 encoding
            plaintext = plaintext.encode('utf-8')
            # encrypt and write to file
            fc.save(self.filename, self.password, plaintext)
        except:
            errmsg = str(sys.exc_info()[1])
            dlg = wx.MessageDialog(None, errmsg, 'Attention', wx.OK | wx.ICON_EXCLAMATION)
            dlg.ShowModal()
            print(errmsg)
        finally:
            self.textbox.SetFocus()

    def onExit(self, event):
        self.Close()

    def onFind(self, event):
        self.findnext('')
        self.textbox.SetFocus()

    def onKeyPress(self, event):
        keycode = event.GetKeyCode()
        if keycode == wx.WXK_F3:
            self.findnext(self.lastfind)
        elif keycode == ord('O') and event.ControlDown(): # Control-O
            self.onOpen(None)
        elif keycode == ord('S') and event.ControlDown(): # Control-S
            self.onSave(None)
        elif keycode == ord('F') and event.ControlDown(): # Control-F
            self.onFind(None)
        elif keycode == ord('Q') and event.ControlDown(): # Control-Q
            self.onExit(None)
        else:
            event.Skip()
       
    def findnext(self, pattern):
        '''search pattern from cursor position onward.'''
        if not pattern: # ask if no pattern specified
            dlg = wx.TextEntryDialog(self, "Find?", "Attention", "", style=wx.OK|wx.CANCEL)
            if dlg.ShowModal() == wx.ID_OK:
                pattern = dlg.GetValue()
        if pattern:
            self.lastfind = pattern
            curpos = self.textbox.GetSelection()[1] # end of current selection
            endpos = self.textbox.GetLastPosition() # eof
            # retrieve the content in the text area from current position onward
            content = self.textbox.GetRange(curpos, endpos)
            # find the next match using regexp & case insensitive
            mo = re.search(pattern, content, re.I)
            # select the match and scroll to it
            if mo:
                self.textbox.SetSelection(curpos + mo.start(), curpos + mo.end())
                self.textbox.ShowPosition(curpos + mo.start())
            else:
                dlg = wx.MessageDialog(None, 'Not found.', 'Attention', wx.OK)
                dlg.ShowModal()

class MainApp(wx.App):
    def OnInit(self):
        frame = MainFrame()
        frame.Show(True)
        self.SetTopWindow(frame)
        return True

def main():
    app = MainApp()
    app.MainLoop()

if __name__=='__main__':
    main()