]> granicus.if.org Git - python/commitdiff
Checked in some new Tk demos that I wrote a while ago.
authorGuido van Rossum <guido@python.org>
Thu, 3 Apr 1997 00:04:51 +0000 (00:04 +0000)
committerGuido van Rossum <guido@python.org>
Thu, 3 Apr 1997 00:04:51 +0000 (00:04 +0000)
Demo/tkinter/guido/canvasevents.py [new file with mode: 0644]
Demo/tkinter/guido/newmenubardemo.py [new file with mode: 0644]
Demo/tkinter/guido/regexdemo.py [new file with mode: 0644]
Demo/tkinter/guido/sortvisu.py [new file with mode: 0644]

diff --git a/Demo/tkinter/guido/canvasevents.py b/Demo/tkinter/guido/canvasevents.py
new file mode 100644 (file)
index 0000000..60f4096
--- /dev/null
@@ -0,0 +1,244 @@
+#! /usr/bin/env python
+
+from Tkinter import *
+from Canvas import Oval, Group, CanvasText
+
+
+# Fix a bug in Canvas.Group as distributed in Python 1.4.  The
+# distributed bind() method is broken.  This is what should be used:
+
+class Group(Group):
+    def bind(self, sequence=None, command=None):
+       return self.canvas.tag_bind(self.id, sequence, command)
+
+class Object:
+
+    """Base class for composite graphical objects.
+
+    Objects belong to a canvas, and can be moved around on the canvas.
+    They also belong to at most one ``pile'' of objects, and can be
+    transferred between piles (or removed from their pile).
+
+    Objects have a canonical ``x, y'' position which is moved when the
+    object is moved.  Where the object is relative to this position
+    depends on the object; for simple objects, it may be their center.
+
+    Objects have mouse sensitivity.  They can be clicked, dragged and
+    double-clicked.  The behavior may actually determined by the pile
+    they are in.
+
+    All instance attributes are public since the derived class may
+    need them.
+
+    """
+
+    def __init__(self, canvas, x=0, y=0, fill='red', text='object'):
+       self.canvas = canvas
+       self.x = x
+       self.y = y
+       self.pile = None
+       self.group = Group(self.canvas)
+       self.createitems(fill, text)
+
+    def __str__(self):
+       return str(self.group)
+
+    def createitems(self, fill, text):
+       self.__oval = Oval(self.canvas,
+                          self.x-20, self.y-10, self.x+20, self.y+10,
+                          fill=fill, width=3)
+       self.group.addtag_withtag(self.__oval)
+       self.__text = CanvasText(self.canvas,
+                          self.x, self.y, text=text)
+       self.group.addtag_withtag(self.__text)
+
+    def moveby(self, dx, dy):
+       if dx == dy == 0:
+           return
+       self.group.move(dx, dy)
+       self.x = self.x + dx
+       self.y = self.y + dy
+
+    def moveto(self, x, y):
+       self.moveby(x - self.x, y - self.y)
+
+    def transfer(self, pile):
+       if self.pile:
+           self.pile.delete(self)
+           self.pile = None
+       self.pile = pile
+       if self.pile:
+           self.pile.add(self)
+
+    def tkraise(self):
+       self.group.tkraise()
+
+
+class Bottom(Object):
+
+    """An object to serve as the bottom of a pile."""
+
+    def createitems(self, *args):
+       self.__oval = Oval(self.canvas,
+                          self.x-20, self.y-10, self.x+20, self.y+10,
+                          fill='gray', outline='')
+       self.group.addtag_withtag(self.__oval)
+
+
+class Pile:
+
+    """A group of graphical objects."""
+
+    def __init__(self, canvas, x, y, tag=None):
+       self.canvas = canvas
+       self.x = x
+       self.y = y
+       self.objects = []
+       self.bottom = Bottom(self.canvas, self.x, self.y)
+       self.group = Group(self.canvas, tag=tag)
+       self.group.addtag_withtag(self.bottom.group)
+       self.bindhandlers()
+
+    def bindhandlers(self):
+       self.group.bind('<1>', self.clickhandler)
+       self.group.bind('<Double-1>', self.doubleclickhandler)
+
+    def add(self, object):
+       self.objects.append(object)
+       self.group.addtag_withtag(object.group)
+       self.position(object)
+
+    def delete(self, object):
+       object.group.dtag(self.group)
+       self.objects.remove(object)
+
+    def position(self, object):
+       object.tkraise()
+       i = self.objects.index(object)
+       object.moveto(self.x + i*4, self.y + i*8)
+
+    def clickhandler(self, event):
+       pass
+
+    def doubleclickhandler(self, event):
+       pass
+
+
+class MovingPile(Pile):
+
+    def bindhandlers(self):
+       Pile.bindhandlers(self)
+       self.group.bind('<B1-Motion>', self.motionhandler)
+       self.group.bind('<ButtonRelease-1>', self.releasehandler)
+
+    movethis = None
+
+    def clickhandler(self, event):
+       tags = self.canvas.gettags('current')
+       for i in range(len(self.objects)):
+           o = self.objects[i]
+           if o.group.tag in tags:
+               break
+       else:
+           self.movethis = None
+           return
+       self.movethis = self.objects[i:]
+       for o in self.movethis:
+           o.tkraise()
+       self.lastx = event.x
+       self.lasty = event.y
+
+    doubleclickhandler = clickhandler
+
+    def motionhandler(self, event):
+       if not self.movethis:
+           return
+       dx = event.x - self.lastx
+       dy = event.y - self.lasty
+       self.lastx = event.x
+       self.lasty = event.y
+       for o in self.movethis:
+           o.moveby(dx, dy)
+
+    def releasehandler(self, event):
+       objects = self.movethis
+       if not objects:
+           return
+       self.movethis = None
+       self.finishmove(objects)
+
+    def finishmove(self, objects):
+       for o in objects:
+           self.position(o)
+
+
+class Pile1(MovingPile):
+
+    x = 50
+    y = 50
+    tag = 'p1'
+
+    def __init__(self, demo):
+       self.demo = demo
+       MovingPile.__init__(self, self.demo.canvas, self.x, self.y, self.tag)
+
+    def doubleclickhandler(self, event):
+       try:
+           o = self.objects[-1]
+       except IndexError:
+           return
+       o.transfer(self.other())
+       MovingPile.doubleclickhandler(self, event)
+
+    def other(self):
+       return self.demo.p2
+
+    def finishmove(self, objects):
+       o = objects[0]
+       p = self.other()
+       x, y = o.x, o.y
+       if (x-p.x)**2 + (y-p.y)**2 < (x-self.x)**2 + (y-self.y)**2:
+           for o in objects:
+               o.transfer(p)
+       else:
+           MovingPile.finishmove(self, objects)
+
+class Pile2(Pile1):
+
+    x = 150
+    y = 50
+    tag = 'p2'
+
+    def other(self):
+       return self.demo.p1
+
+
+class Demo:
+
+    def __init__(self, master):
+       self.master = master
+       self.canvas = Canvas(master,
+                            width=200, height=200,
+                            background='yellow',
+                            relief=SUNKEN, borderwidth=2)
+       self.canvas.pack(expand=1, fill=BOTH)
+       self.p1 = Pile1(self)
+       self.p2 = Pile2(self)
+       o1 = Object(self.canvas, fill='red', text='o1')
+       o2 = Object(self.canvas, fill='green', text='o2')
+       o3 = Object(self.canvas, fill='light blue', text='o3')
+       o1.transfer(self.p1)
+       o2.transfer(self.p1)
+       o3.transfer(self.p2)
+
+
+# Main function, run when invoked as a stand-alone Python program.
+
+def main():
+    root = Tk()
+    demo = Demo(root)
+    root.protocol('WM_DELETE_WINDOW', root.quit)
+    root.mainloop()
+
+if __name__ == '__main__':
+    main()
diff --git a/Demo/tkinter/guido/newmenubardemo.py b/Demo/tkinter/guido/newmenubardemo.py
new file mode 100644 (file)
index 0000000..7e2d13a
--- /dev/null
@@ -0,0 +1,42 @@
+#! /usr/bin/env python
+
+"""Play with the new Tk 8.0 toplevel menu option."""
+
+from Tkinter import *
+
+class App:
+
+    def __init__(self, master):
+       self.master = master
+
+       self.menubar = Menu(self.master)
+
+       self.filemenu = Menu(self.menubar)
+       
+       self.filemenu.add_command(label="New")
+       self.filemenu.add_command(label="Open...")
+       self.filemenu.add_command(label="Close")
+       self.filemenu.add_separator()
+       self.filemenu.add_command(label="Quit", command=self.master.quit)
+
+       self.editmenu = Menu(self.menubar)
+
+       self.editmenu.add_command(label="Cut")
+       self.editmenu.add_command(label="Copy")
+       self.editmenu.add_command(label="Paste")
+
+       self.menubar.add_cascade(label="File", menu=self.filemenu)
+       self.menubar.add_cascade(label="Edit", menu=self.editmenu)
+
+       self.top = Toplevel(menu=self.menubar)
+
+       # Rest of app goes here...
+
+def main():
+    root = Tk()
+    root.withdraw()
+    app = App(root)
+    root.mainloop()
+
+if __name__ == '__main__':
+    main()
diff --git a/Demo/tkinter/guido/regexdemo.py b/Demo/tkinter/guido/regexdemo.py
new file mode 100644 (file)
index 0000000..89c9ee5
--- /dev/null
@@ -0,0 +1,127 @@
+"""Basic regular expression demostration facility.
+
+This displays a window with two type-in boxes.  In the top box, you enter a 
+regular expression.  In the bottom box, you enter a string.  The first 
+match in the string of the regular expression is highlighted with a yellow 
+background (or red if the match is empty -- then the character at the match 
+is highlighted).  The highlighting is continuously updated.  At the bottom 
+are a number of checkboxes which control the regular expression syntax used 
+(see the regex_syntax module for descriptions).  When there's no match, or 
+when the regular expression is syntactically incorrect, an error message is
+displayed.
+
+"""
+
+from Tkinter import *
+import regex
+import regex_syntax
+
+class RegexDemo:
+       
+       def __init__(self, master):
+               self.master = master
+               self.topframe = Frame(self.master)
+               self.topframe.pack(fill=X)
+               self.promptdisplay = Label(self.topframe, text="Enter a string:")
+               self.promptdisplay.pack(side=LEFT)
+               self.statusdisplay = Label(self.topframe, text="", anchor=W)
+               self.statusdisplay.pack(side=LEFT, fill=X)
+               self.regexdisplay = Entry(self.master)
+               self.regexdisplay.pack(fill=X)
+               self.regexdisplay.focus_set()
+               self.labeldisplay = Label(self.master, anchor=W,
+                       text="Enter a string:")
+               self.labeldisplay.pack(fill=X)
+               self.labeldisplay.pack(fill=X)
+               self.stringdisplay = Text(self.master, width=60, height=4)
+               self.stringdisplay.pack(fill=BOTH, expand=1)
+               self.stringdisplay.tag_configure("hit", background="yellow")
+               self.addoptions()
+               self.regexdisplay.bind('<Key>', self.recompile)
+               self.stringdisplay.bind('<Key>', self.reevaluate)
+               self.compiled = None
+               self.recompile()
+               btags = self.regexdisplay.bindtags()
+               self.regexdisplay.bindtags(btags[1:] + btags[:1])
+               btags = self.stringdisplay.bindtags()
+               self.stringdisplay.bindtags(btags[1:] + btags[:1])
+       
+       def addoptions(self):
+               self.frames = []
+               self.boxes = []
+               self.vars = []
+               for name in (   'RE_NO_BK_PARENS',
+                               'RE_NO_BK_VBAR',
+                               'RE_BK_PLUS_QM',
+                               'RE_TIGHT_VBAR',
+                               'RE_NEWLINE_OR',
+                               'RE_CONTEXT_INDEP_OPS'):
+                       if len(self.boxes) % 3 == 0:
+                               frame = Frame(self.master)
+                               frame.pack(fill=X)
+                               self.frames.append(frame)
+                       val = getattr(regex_syntax, name)
+                       var = IntVar()
+                       box = Checkbutton(frame,
+                               variable=var, text=name,
+                               offvalue=0, onvalue=val,
+                               command=self.newsyntax)
+                       box.pack(side=LEFT)
+                       self.boxes.append(box)
+                       self.vars.append(var)
+       
+       def newsyntax(self):
+               syntax = 0
+               for var in self.vars:
+                       syntax = syntax | var.get()
+               regex.set_syntax(syntax)
+               self.recompile()
+       
+       def recompile(self, event=None):
+               try:
+                       self.compiled = regex.compile(self.regexdisplay.get())
+                       self.statusdisplay.config(text="")
+               except regex.error, msg:
+                       self.compiled = None
+                       self.statusdisplay.config(text="regex.error: %s" % str(msg))
+               self.reevaluate()
+       
+       def reevaluate(self, event=None):
+               try:
+                       self.stringdisplay.tag_remove("hit", "1.0", END)
+               except TclError:
+                       pass
+               if not self.compiled:
+                       return
+               text = self.stringdisplay.get("1.0", END)
+               i = self.compiled.search(text)
+               if i < 0:
+                       self.statusdisplay.config(text="(no match)")
+               else:
+                       self.statusdisplay.config(text="")
+                       regs = self.compiled.regs
+                       first, last = regs[0]
+                       if last == first:
+                               last = first+1
+                               self.stringdisplay.tag_configure("hit",
+                                       background="red")
+                       else:
+                               self.stringdisplay.tag_configure("hit",
+                                       background="yellow")
+                       pfirst = "1.0 + %d chars" % first
+                       plast = "1.0 + %d chars" % last
+                       self.stringdisplay.tag_add("hit", pfirst, plast)
+                       self.stringdisplay.yview_pickplace(pfirst)
+
+
+
+# Main function, run when invoked as a stand-alone Python program.
+
+def main():
+    root = Tk()
+    demo = RegexDemo(root)
+    root.protocol('WM_DELETE_WINDOW', root.quit)
+    root.mainloop()
+
+if __name__ == '__main__':
+    main()
diff --git a/Demo/tkinter/guido/sortvisu.py b/Demo/tkinter/guido/sortvisu.py
new file mode 100644 (file)
index 0000000..6fe4002
--- /dev/null
@@ -0,0 +1,634 @@
+#! /usr/bin/env python
+
+"""Sorting algorithms visualizer using Tkinter.
+
+This module is comprised of three ``components'':
+
+- an array visualizer with methods that implement basic sorting
+operations (compare, swap) as well as methods for ``annotating'' the
+sorting algorithm (e.g. to show the pivot element);
+
+- a number of sorting algorithms (currently quicksort, insertion sort,
+selection sort and bubble sort, as well as a randomization function),
+all using the array visualizer for its basic operations and with calls
+to its annotation methods;
+
+- and a ``driver'' class which can be used as a Grail applet or as a
+stand-alone application.
+
+"""
+
+
+from Tkinter import *
+from Canvas import Line, Rectangle
+import random
+
+
+XGRID = 10
+YGRID = 10
+WIDTH = 6
+
+
+class Array:
+
+    def __init__(self, master, data=None):
+       self.master = master
+       self.frame = Frame(self.master)
+       self.frame.pack(fill=X)
+       self.label = Label(self.frame)
+       self.label.pack()
+       self.canvas = Canvas(self.frame)
+       self.canvas.pack()
+       self.report = Label(self.frame)
+       self.report.pack()
+       self.left = Line(self.canvas, 0, 0, 0, 0)
+       self.right = Line(self.canvas, 0, 0, 0, 0)
+       self.pivot = Line(self.canvas, 0, 0, 0, 0)
+       self.items = []
+       self.size = self.maxvalue = 0
+       if data:
+           self.setdata(data)
+
+    def setdata(self, data):
+       olditems = self.items
+       self.items = []
+       for item in olditems:
+           item.delete()
+       self.size = len(data)
+       self.maxvalue = max(data)
+       self.canvas.config(width=(self.size+1)*XGRID,
+                          height=(self.maxvalue+1)*YGRID)
+       for i in range(self.size):
+           self.items.append(ArrayItem(self, i, data[i]))
+       self.reset("Sort demo, size %d" % self.size)
+
+    speed = "normal"
+
+    def setspeed(self, speed):
+       self.speed = speed
+
+    def destroy(self):
+       self.frame.destroy()
+
+    in_mainloop = 0
+    stop_mainloop = 0
+
+    def cancel(self):
+       self.stop_mainloop = 1
+       if self.in_mainloop:
+           self.master.quit()
+
+    def step(self):
+       if self.in_mainloop:
+           self.master.quit()
+
+    Cancelled = "Array.Cancelled"      # Exception
+
+    def wait(self, msecs):
+       if self.speed == "fastest":
+           msecs = 0
+       elif self.speed == "fast":
+           msecs = msecs/10
+       elif self.speed == "single-step":
+           msecs = 1000000000
+       if not self.stop_mainloop:
+           self.master.update()
+           id = self.master.after(msecs, self.master.quit)
+           self.in_mainloop = 1
+           self.master.mainloop()
+           self.master.after_cancel(id)
+           self.in_mainloop = 0
+       if self.stop_mainloop:
+           self.stop_mainloop = 0
+           self.message("Cancelled")
+           raise Array.Cancelled
+
+    def getsize(self):
+       return self.size
+
+    def show_partition(self, first, last):
+       for i in range(self.size):
+           item = self.items[i]
+           if first <= i < last:
+               item.item.config(fill='red')
+           else:
+               item.item.config(fill='orange')
+       self.hide_left_right_pivot()
+
+    def hide_partition(self):
+       for i in range(self.size):
+           item = self.items[i]
+           item.item.config(fill='red')
+       self.hide_left_right_pivot()
+
+    def show_left(self, left):
+       if not 0 <= left < self.size:
+           self.hide_left()
+           return
+       x1, y1, x2, y2 = self.items[left].position()
+##     top, bot = HIRO
+       self.left.coords([(x1-2, 0), (x1-2, 9999)])
+       self.master.update()
+
+    def show_right(self, right):
+       if not 0 <= right < self.size:
+           self.hide_right()
+           return
+       x1, y1, x2, y2 = self.items[right].position()
+       self.right.coords(((x2+2, 0), (x2+2, 9999)))
+       self.master.update()
+
+    def hide_left_right_pivot(self):
+       self.hide_left()
+       self.hide_right()
+       self.hide_pivot()
+
+    def hide_left(self):
+       self.left.coords(((0, 0), (0, 0)))
+
+    def hide_right(self):
+       self.right.coords(((0, 0), (0, 0)))
+
+    def show_pivot(self, pivot):
+       x1, y1, x2, y2 = self.items[pivot].position()
+       self.pivot.coords(((0, y1-2), (9999, y1-2)))
+
+    def hide_pivot(self):
+       self.pivot.coords(((0, 0), (0, 0)))
+
+    def swap(self, i, j):
+       if i == j: return
+       self.countswap()
+       item = self.items[i]
+       other = self.items[j]
+       self.items[i], self.items[j] = other, item
+       item.swapwith(other)
+
+    def compare(self, i, j):
+       self.countcompare()
+       item = self.items[i]
+       other = self.items[j]
+       return item.compareto(other)
+
+    def reset(self, msg):
+       self.ncompares = 0
+       self.nswaps = 0
+       self.message(msg)
+       self.updatereport()
+       self.hide_partition()
+
+    def message(self, msg):
+       self.label.config(text=msg)
+
+    def countswap(self):
+       self.nswaps = self.nswaps + 1
+       self.updatereport()
+
+    def countcompare(self):
+       self.ncompares = self.ncompares + 1
+       self.updatereport()
+
+    def updatereport(self):
+       text = "%d cmps, %d swaps" % (self.ncompares, self.nswaps)
+       self.report.config(text=text)
+
+
+class ArrayItem:
+
+    def __init__(self, array, index, value):
+       self.array = array
+       self.index = index
+       self.value = value
+       x1, y1, x2, y2 = self.position()
+       self.item = Rectangle(array.canvas, x1, y1, x2, y2,
+                             fill='red', outline='black', width=1)
+       self.item.bind('<Button-1>', self.mouse_down)
+       self.item.bind('<Button1-Motion>', self.mouse_move)
+       self.item.bind('<ButtonRelease-1>', self.mouse_up)
+
+    def delete(self):
+       item = self.item
+       self.array = None
+       self.item = None
+       item.delete()
+
+    def mouse_down(self, event):
+       self.lastx = event.x
+       self.lasty = event.y
+       self.origx = event.x
+       self.origy = event.y
+       self.item.tkraise()
+       
+    def mouse_move(self, event):
+       self.item.move(event.x - self.lastx, event.y - self.lasty)
+       self.lastx = event.x
+       self.lasty = event.y
+
+    def mouse_up(self, event):
+       i = self.nearestindex(event.x)
+       if i >= self.array.getsize():
+           i = self.array.getsize() - 1
+       if i < 0:
+           i = 0
+       other = self.array.items[i]
+       here = self.index
+       self.array.items[here], self.array.items[i] = other, self
+       self.index = i
+       x1, y1, x2, y2 = self.position()
+       self.item.coords(((x1, y1), (x2, y2)))
+       other.setindex(here)
+
+    def setindex(self, index):
+       nsteps = steps(self.index, index)
+       if not nsteps: return
+       if self.array.speed == "fastest":
+           nsteps = 0
+       oldpts = self.position()
+       self.index = index
+       newpts = self.position()
+       trajectory = interpolate(oldpts, newpts, nsteps)
+       self.item.tkraise()
+       for pts in trajectory:
+           self.item.coords((pts[:2], pts[2:]))
+           self.array.wait(50)
+
+    def swapwith(self, other):
+       nsteps = steps(self.index, other.index)
+       if not nsteps: return
+       if self.array.speed == "fastest":
+           nsteps = 0
+       myoldpts = self.position()
+       otheroldpts = other.position()
+       self.index, other.index = other.index, self.index
+       mynewpts = self.position()
+       othernewpts = other.position()
+       myfill = self.item['fill']
+       otherfill = other.item['fill']
+       self.item.config(fill='green')
+       other.item.config(fill='yellow')
+       self.array.master.update()
+       if self.array.speed == "single-step":
+           self.item.coords((mynewpts[:2], mynewpts[2:]))
+           other.item.coords((othernewpts[:2], othernewpts[2:]))
+           self.array.master.update()
+           self.item.config(fill=myfill)
+           other.item.config(fill=otherfill)
+           self.array.wait(0)
+           return
+       mytrajectory = interpolate(myoldpts, mynewpts, nsteps)
+       othertrajectory = interpolate(otheroldpts, othernewpts, nsteps)
+       if self.value > other.value:
+           self.item.tkraise()
+           other.item.tkraise()
+       else:
+           other.item.tkraise()
+           self.item.tkraise()
+       try:
+           for i in range(len(mytrajectory)):
+               mypts = mytrajectory[i]
+               otherpts = othertrajectory[i]
+               self.item.coords((mypts[:2], mypts[2:]))
+               other.item.coords((otherpts[:2], otherpts[2:]))
+               self.array.wait(50)
+       finally:
+           mypts = mytrajectory[-1]
+           otherpts = othertrajectory[-1]
+           self.item.coords((mypts[:2], mypts[2:]))
+           other.item.coords((otherpts[:2], otherpts[2:]))
+           self.item.config(fill=myfill)
+           other.item.config(fill=otherfill)
+
+    def compareto(self, other):
+       myfill = self.item['fill']
+       otherfill = other.item['fill']
+       outcome = cmp(self.value, other.value)
+       if outcome < 0:
+           myflash = 'white'
+           otherflash = 'black'
+       elif outcome > 0:
+           myflash = 'black'
+           otherflash = 'white'
+       else:
+           myflash = otherflash = 'grey'
+       try:
+           self.item.config(fill=myflash)
+           other.item.config(fill=otherflash)
+           self.array.wait(500)
+       finally:
+           self.item.config(fill=myfill)
+           other.item.config(fill=otherfill)
+       return outcome
+
+    def position(self):
+       x1 = (self.index+1)*XGRID - WIDTH/2
+       x2 = x1+WIDTH
+       y2 = (self.array.maxvalue+1)*YGRID
+       y1 = y2 - (self.value)*YGRID
+       return x1, y1, x2, y2
+
+    def nearestindex(self, x):
+       return int(round(float(x)/XGRID)) - 1
+
+
+# Subroutines that don't need an object
+
+def steps(here, there):
+    nsteps = abs(here - there)
+    if nsteps <= 3:
+       nsteps = nsteps * 3
+    elif nsteps <= 5:
+       nsteps = nsteps * 2
+    elif nsteps > 10:
+       nsteps = 10
+    return nsteps
+
+def interpolate(oldpts, newpts, n):
+    if len(oldpts) != len(newpts):
+       raise ValueError, "can't interpolate arrays of different length"
+    pts = [0]*len(oldpts)
+    res = [tuple(oldpts)]
+    for i in range(1, n):
+       for k in range(len(pts)):
+           pts[k] = oldpts[k] + (newpts[k] - oldpts[k])*i/n
+       res.append(tuple(pts))
+    res.append(tuple(newpts))
+    return res
+
+
+# Various (un)sorting algorithms
+
+def uniform(array):
+    size = array.getsize()
+    array.setdata([(size+1)/2] * size)
+    array.reset("Uniform data, size %d" % size)
+
+def distinct(array):
+    size = array.getsize()
+    array.setdata(range(1, size+1))
+    array.reset("Distinct data, size %d" % size)
+
+def randomize(array):
+    array.reset("Randomizing")
+    n = array.getsize()
+    for i in range(n):
+       j = random.randint(0, n-1)
+       array.swap(i, j)
+    array.message("Randomized")
+
+def insertionsort(array):
+    size = array.getsize()
+    array.reset("Insertion sort")
+    for i in range(1, size):
+       j = i-1
+       while j >= 0:
+           if array.compare(j, j+1) <= 0:
+               break
+           array.swap(j, j+1)
+           j = j-1
+    array.message("Sorted")
+
+def selectionsort(array):
+    size = array.getsize()
+    array.reset("Selection sort")
+    try:
+       for i in range(size):
+           array.show_partition(i, size)
+           for j in range(i+1, size):
+               if array.compare(i, j) > 0:
+                   array.swap(i, j)
+       array.message("Sorted")
+    finally:
+       array.hide_partition()
+
+def bubblesort(array):
+    size = array.getsize()
+    array.reset("Bubble sort")
+    for i in range(size):
+       for j in range(1, size):
+           if array.compare(j-1, j) > 0:
+               array.swap(j-1, j)
+    array.message("Sorted")
+
+def quicksort(array):
+    size = array.getsize()
+    array.reset("Quicksort")
+    try:
+       stack = [(0, size)]
+       while stack:
+           first, last = stack[-1]
+           del stack[-1]
+           array.show_partition(first, last)
+           if last-first < 5:
+               array.message("Insertion sort")
+               for i in range(first+1, last):
+                   j = i-1
+                   while j >= first:
+                       if array.compare(j, j+1) <= 0:
+                           break
+                       array.swap(j, j+1)
+                       j = j-1
+               continue
+           array.message("Choosing pivot")
+           j, i, k = first, (first+last)/2, last-1
+           if array.compare(k, i) < 0:
+               array.swap(k, i)
+           if array.compare(k, j) < 0:
+               array.swap(k, j)
+           if array.compare(j, i) < 0:
+               array.swap(j, i)
+           pivot = j
+           array.show_pivot(pivot)
+           array.message("Pivot at left of partition")
+           array.wait(1000)
+           left = first
+           right = last
+           while 1:
+               array.message("Sweep right pointer")
+               right = right-1
+               array.show_right(right)
+               while right > first and array.compare(right, pivot) >= 0:
+                   right = right-1
+                   array.show_right(right)
+               array.message("Sweep left pointer")
+               left = left+1
+               array.show_left(left)
+               while left < last and array.compare(left, pivot) <= 0:
+                   left = left+1
+                   array.show_left(left)
+               if left > right:
+                   array.message("End of partition")
+                   break
+               array.message("Swap items")
+               array.swap(left, right)
+           array.message("Swap pivot back")
+           array.swap(pivot, right)
+           n1 = right-first
+           n2 = last-left
+           if n1 > 1: stack.append((first, right))
+           if n2 > 1: stack.append((left, last))
+       array.message("Sorted")
+    finally:
+       array.hide_partition()
+
+def demosort(array):
+    while 1:
+       for alg in [quicksort, insertionsort, selectionsort, bubblesort]:
+           randomize(array)
+           alg(array)
+
+
+# Sort demo class -- usable as a Grail applet
+
+class SortDemo:
+
+    def __init__(self, master, size=15):
+       self.master = master
+       self.size = size
+       self.busy = 0
+       self.array = Array(self.master)
+
+       self.botframe = Frame(master)
+       self.botframe.pack(side=BOTTOM)
+       self.botleftframe = Frame(self.botframe)
+       self.botleftframe.pack(side=LEFT, fill=Y)
+       self.botrightframe = Frame(self.botframe)
+       self.botrightframe.pack(side=RIGHT, fill=Y)
+
+       self.b_qsort = Button(self.botleftframe,
+                             text="Quicksort", command=self.c_qsort)
+       self.b_qsort.pack(fill=X)
+       self.b_isort = Button(self.botleftframe,
+                             text="Insertion sort", command=self.c_isort)
+       self.b_isort.pack(fill=X)
+       self.b_ssort = Button(self.botleftframe,
+                             text="Selection sort", command=self.c_ssort)
+       self.b_ssort.pack(fill=X)
+       self.b_bsort = Button(self.botleftframe,
+                             text="Bubble sort", command=self.c_bsort)
+       self.b_bsort.pack(fill=X)
+
+       # Terrible hack to overcome limitation of OptionMenu...
+       class MyIntVar(IntVar):
+           def __init__(self, master, demo):
+               self.demo = demo
+               IntVar.__init__(self, master)
+           def set(self, value):
+               IntVar.set(self, value)
+               if str(value) != '0':
+                   self.demo.resize(value)
+
+       self.v_size = MyIntVar(self.master, self)
+       self.v_size.set(size)
+       sizes = [1, 2, 3, 4] + range(5, 55, 5)
+       if self.size not in sizes:
+           sizes.append(self.size)
+           sizes.sort()
+       self.m_size = apply(OptionMenu,
+                           (self.botleftframe, self.v_size) + tuple(sizes))
+       self.m_size.pack(fill=X)
+       
+       self.v_speed = StringVar(self.master)
+       self.v_speed.set("normal")
+       self.m_speed = OptionMenu(self.botleftframe, self.v_speed,
+                                 "single-step", "normal", "fast", "fastest")
+       self.m_speed.pack(fill=X)
+       
+       self.b_step = Button(self.botleftframe,
+                            text="Step", command=self.c_step)
+       self.b_step.pack(fill=X)
+       
+       self.b_randomize = Button(self.botrightframe,
+                                 text="Randomize", command=self.c_randomize)
+       self.b_randomize.pack(fill=X)
+       self.b_uniform = Button(self.botrightframe,
+                                 text="Uniform", command=self.c_uniform)
+       self.b_uniform.pack(fill=X)
+       self.b_distinct = Button(self.botrightframe,
+                                 text="Distinct", command=self.c_distinct)
+       self.b_distinct.pack(fill=X)
+       self.b_demo = Button(self.botrightframe,
+                            text="Demo", command=self.c_demo)
+       self.b_demo.pack(fill=X)
+       self.b_cancel = Button(self.botrightframe,
+                              text="Cancel", command=self.c_cancel)
+       self.b_cancel.pack(fill=X)
+       self.b_cancel.config(state=DISABLED)
+       self.b_quit = Button(self.botrightframe,
+                            text="Quit", command=self.c_quit)
+       self.b_quit.pack(fill=X)
+
+    def resize(self, newsize):
+       if self.busy:
+           self.master.bell()
+           return
+       self.size = newsize
+       self.array.setdata(range(1, self.size+1))
+
+    def c_qsort(self):
+       self.run(quicksort)
+
+    def c_isort(self):
+       self.run(insertionsort)
+
+    def c_ssort(self):
+       self.run(selectionsort)
+
+    def c_bsort(self):
+       self.run(bubblesort)
+
+    def c_demo(self):
+       self.run(demosort)
+
+    def c_randomize(self):
+       self.run(randomize)
+
+    def c_uniform(self):
+       self.run(uniform)
+
+    def c_distinct(self):
+       self.run(distinct)
+
+    def run(self, func):
+       if self.busy:
+           self.master.bell()
+           return
+       self.busy = 1
+       self.array.setspeed(self.v_speed.get())
+       self.b_cancel.config(state=NORMAL)
+       try:
+           func(self.array)
+       except Array.Cancelled:
+           pass
+       self.b_cancel.config(state=DISABLED)
+       self.busy = 0
+
+    def c_cancel(self):
+       if not self.busy:
+           self.master.bell()
+           return
+       self.array.cancel()
+
+    def c_step(self):
+       if not self.busy:
+           self.master.bell()
+           return
+       self.v_speed.set("single-step")
+       self.array.setspeed("single-step")
+       self.array.step()
+
+    def c_quit(self):
+       if self.busy:
+           self.array.cancel()
+       self.master.after_idle(self.master.quit)
+
+
+# Main program -- for stand-alone operation outside Grail
+
+def main():
+    root = Tk()
+    demo = SortDemo(root)
+    root.protocol('WM_DELETE_WINDOW', demo.c_quit)
+    root.mainloop()
+
+if __name__ == '__main__':
+    main()