#!/usr/bin/env python

# A progress bar and dialog box, by Peter Corbett
# This file is hereby placed into the Public Domain
#
# It shouldn't be too hard to figure out how to use this - an example of it
# in action is provided at the bottom of the file. Basically:
#
# 1) initialise your ProgressDialog, specifying the parent window and
#    the number of 'work units' to do.
# 2) Your ProgressDialog object will contain two useful functions:
#    do_unit   - call this whenever your process completes a work unit to
#                advance the progress bar along
#    keepalive - call this from time to time, to keep the dialog box
#                responsive, and give the user a chance to cancel
# 3) Wrap your function to run up with its arguaments (hopefully including
#    the functions above) up in a WrappedCommand object.
# 4) Set your ProgressDialog's command (using the .set_command method) to
#    your WrappedCommand object.
# 5) Tell your ProgressDialog to .run(), and maybe look at it's .derail or
#    .result afterward.
# 6) To prematurely stop your process, raise a DerailmentException. This
#    brings the whole process crashing to a halt, stopping the ProgressDialog
#    and letting your program get on with whatever it was that it was doing.
#
# Oh, and other stuff may need tweaking if you want it to be bigger/red/
# whatever.
#
# Peter Corbett, <pcorbett@chiark.greenend.org.uk>

from Tkinter import *  # This is vital for this module
import time            # This is only important for the demo function

class DerailmentException(Exception):
    """Raise this to stop ('derail') the process in progress"""
    pass

class ProgressBar:
    """A progress bar"""

    def __init__(self, parent):
        self.parent = parent
        self.height = 24
        self.width = 240
        self.padding = 8
        self.canvas = Canvas(self.parent, height=self.height+self.padding*2, width=self.width+self.padding*2)
        self.progress = .998
        self.draw()

    def set(self, prog):
        self.progress = prog
        self.redraw()
        
    def progress_string(self):
        return str(int(self.progress * 100)) + "%"

    def draw(self):
        self.box = self.canvas.create_rectangle(2+self.padding, 2+self.padding, self.width+1+self.padding, self.height+1+self.padding, fill="white", outline="black")
        self.bar = self.canvas.create_rectangle(3+self.padding, 3+self.padding, int(self.width * self.progress)+1+self.padding, self.height+1+self.padding, fill="blue", outline="")
        self.text = self.canvas.create_text(2+(self.width/2)+self.padding, 2+(self.height/2)+self.padding, text=self.progress_string(), fill="black")
        self.parent.update()

    def redraw(self):
        self.canvas.coords(self.bar, 3+self.padding, 3+self.padding, int(self.width * self.progress)+1+self.padding, self.height+1+self.padding)
        self.canvas.itemconfigure(self.text, text=self.progress_string())
        self.parent.update()

    def grid(self, *args, **keywords):
        self.canvas.grid(*args, **keywords)

    def __del__(self):
        try: self.canvas.destroy()
        except: pass

class WrappedCommand:
    """A command, wrapped with some arguaments"""

    def __init__(self, command, *args, **keywords):
        self.command = command
        self.args = args
        self.keywords = keywords

    def __call__(self, *a, **k):
        self.command(*self.args, **self.keywords)

class ProgressDialog(Toplevel):
    """A progress-bar dialog box"""

    def __init__(self, parent, title, workunits):
        Toplevel.__init__(self, parent)
        self.parent = parent

        if title:
            self.title(title)
            Label(self, text=title).grid(row=0, column=0)

        self.pb = ProgressBar(self)
        self.pb.set(0)
        self.pb.grid(row=1, column=0)

        self.workunits = workunits
        self.doneunits = 0

        self.cancelbutton = Button(self, text="Cancel", command=self.cancel)
        self.cancelbutton.grid(row=2, column=0)

        self.grab_set()
        self.protocol("WM_DELETE_WINDOW", self.cancel)
        self.update()

        self.derail = 0
        self.result = 0

    def run(self):
        try:
            self.command()
        except DerailmentException:
            self.derail = 1
            self.result = 1

        self.parent.focus_set()
        self.destroy()

    def set_command(self, command):
        self.command = command

    def do_unit(self):
        self.doneunits += 1
        self.keepalive()

    def keepalive(self):
        self.pb.set(float(self.doneunits)/self.workunits)
        if self.derail:
            raise DerailmentException

    def cancel(self, event=None):
        self.derail = 1

def count(du, ka):
    for n in range(100):
        du()
        time.sleep(0.05)
        
if __name__=="__main__":
    root = Tk()
    root.update()
    p = ProgressDialog(root, "Test", 100)
    p.set_command(WrappedCommand(count, p.do_unit, p.keepalive))
    p.run()
    print p.result
    root.mainloop()

    
    
