"""Interface with RISC OS window manager (wimp) and related functions.

Class hierarchy:

window             -- base window class
  iconbar_icon       -- iconbar icon
  info_window        -- info box
  save_window        -- save box
menu               -- menu class
content            -- window contents class
  rectangle          -- example content
font               -- font class

Functions:

task() -- initialise the task with the wimp
quit() -- tidy up and exit

poll() -- get a wimp event
handle() -- handle a wimp event

screen_size() -- return screen mode dimensions
mem_string() -- read or write a string to a buffer

hourglass_on() -- switch on the hourglass
hourglass_off() -- switch off the hourglass
hourglass_percentage() -- display a percentage underneath

warning() -- display a wimp error box
lookup() -- convert a message token to a message

send_message() -- send a wimp message
close_menus() -- close any open menus
set_colour() -- set the colour for plotting
plot() -- plot to the screen

Variables:

task_handle -- wimp task handle
task_name -- wimp task name
poll_mask -- default wimp poll event mask

Comments:

All window coordinates behave as follows:
 - the origin is assumed to be in the top left, so ensure that
   your templates have this to avoid problems
 - x coordinates are 0 at the origin and increase across the window
 - y coordinates are 0 at the origin and increase down the window
 - scroll offsets are also always positive

Coordinates are usually in os units. However, the document classes
and fonts use millipoints. At 100%, 400 millipoints = 1 os unit.
Font sizes are in 1/16 points.

"""

import os
import swi
import string
import sys
import types
import rufl

task_handle = 0
task_name = 'Application'
poll_mask = 1
null_reason_handler = None
user_drag_handler = None
message_handler = {}
windows = {}
fonts = {}

_depth = 0

def task(name, dir, task, messages = (), debug = 0, profile = 0, mask = None):
    "Initialise the task with the wimp and load the messages file"
    global task_name, directory, b, wimp_version, task_handle, territory, m, task_module, _trace

    hourglass_on()
    task_name = name
    directory = dir
    task_module = task
    if mask:
        poll_mask = mask

    # set up debugging stuff
    if debug:
        import traceback
        sys.stderr = sys.stdout = open(directory + '.^.Output', 'w')
    if debug == 2:
        def _trace(frame, event, arg):
            global _depth
            if event == 'call':
                print _depth * ' ', frame.f_code.co_name + '(', arg, ')'
                _depth = _depth + 1
            elif event == 'return':
                print _depth * ' ', 'return', arg
                _depth = _depth - 1
            return _trace
        sys.settrace(_trace)
    elif debug == 3:
        def _trace(frame, event, arg):
            global _depth
            if event == 'line':
                print _depth * ' ', frame.f_lineno
            elif event == 'call':
                print _depth * ' ', frame.f_code.co_name + '(', arg, ')'
                _depth = _depth + 1
            elif event == 'return':
                print _depth * ' ', 'return', arg
                _depth = _depth - 1
            else:
                print _depth * ' ', event + ':', arg
            return _trace
        sys.settrace(_trace)

    # buffer for various stuff
    b = swi.block(64)

    # initialise with wimp
    # place the message list in the buffer
    b[0] = 0x502    # Message_HelpRequest
    b[1] = 0x400c9  # Message_MenusDeleted
    index = 2
    for message in messages:
        b[index] = message
        index = index + 1
    b[index] = 0
    # Wimp_Initialise
    wimp_version, task_handle = swi.swi(0x400c0, 'iisb;ii', 310, 0x4b534154, name, b)

    # load messages
    # Territory_NumberToName, Territory_Number
    territory = swi.swi(0x43043, 'ibi;.s', swi.swi(0x43040, ';i'), b, 256)

    try:
        message_file = open(directory + '.Resources.' + territory + '.Messages', 'r')
    except IOError:
        message_file = open(directory + '.Resources.UK.Messages', 'r')
    m = {}
    for line in message_file.readlines():
        if line[0] != '#' and ':' in line:
	    message = string.split(line, ':', 1)
            m[message[0]] = message[1][:-1]
    message_file.close()

    try:
        if profile:
            import profile
            profile.run('wimp._go()', directory + '.^.Profile')
        else:
            _go()
    except SystemExit:
        hourglass_on()
        task_module.quit()
    except RuntimeError, value:
        swi.swi(0x400df, 'sis', '    ' + str(value), 1, task_name)
    except:
        if debug > 1:
            sys.settrace(None)

        type, value, traceb = sys.exc_info()
        swi.swi(0x400df, 'sis',
                         '    Internal error - must exit (' + str(type) + ': ' + str(value) + ')',
                         1, task_name)

        if debug:
            traceback.print_exc()
            trace = traceback.extract_tb(traceb)
            try:
                swi.swi(0x42587, '')
                swi.swi(0x42588, '0.s', trace[-1][0])
                swi.swi(0x42588, '1.si1s', trace[-1][0], trace[-1][1],
                                           str(type) + ': ' + trace[-1][2] + ': ' + trace[-1][3])
                swi.swi(0x42589, '')
            except:
                pass

            if sys.stderr.tell() > 0:
                sys.stderr.close()
                os.system('Filer_Run ' + directory + '.^.Output')
            else:
                sys.stderr.close()

    # Wimp_CloseDown
    swi.swi(0x400dd, 'ii', task_handle, 0x4b534154)
    hourglass_off()


def _go():
    "Run the program"
    task_module.init()
    hourglass_off()
    while 1:
        handle(poll(poll_mask))


def quit():
    "Trigger quitting"
    for f in fonts.values():
        f.use = 1
        f.lose()
    raise SystemExit


def poll(mask = None):
    "Poll wimp once and return data"
    # Wimp_Poll
    if mask is None:
        mask = poll_mask
    event = swi.swi(0x400c7, 'ib;i', mask, b)
    if event == 2:		# Open_Window_Request
        return 2, (windows[b[0]], b[1], b[2], b[3], b[4], b[5], b[6], b[7])
    elif event == 3:		# Close_Window_Request
        return 3, windows[b[0]]
    elif event == 6:		# Mouse_Click
        x, y, z, w, i = b[0], b[1], b[2], b[3], b[4]
        b[0] = w
        # Wimp_GetWindowState
        swi.swi(0x400cb, '.b', b)
        return 6, (windows[w], i, z, x - (b[1] - b[5]), (b[4] - b[6]) - y, x, y)
    elif event == 7:		# User_Drag_Box
        return 7, (b[0], b[1], b[2], b[3])
    elif event == 8:		# Key_Pressed
        return 8, (windows[b[0]], b[1], b[6])
	    			# User_Message, User_Message_Recorded, User_Message_Acknowledge
    elif event == 17 or event == 18 or event == 19:
        return event, (b[4])
    return event, b


def handle((event, details)):
    "Handle events as returned by poll()"
#    try:
    if event == 0:			# Null_Reason_Code
        if null_reason_handler:
            null_reason_handler()
    elif event == 1:		# Redraw_Window_Request
        windows[b[0]].do_redraw()
    elif event == 2:		# Open_Window_Request
        details[0].open(details[1], details[2], details[3], details[4], details[5], details[6], details[7])
    elif event == 3:		# Close_Window_Request
        details.close()
    elif event == 4:		# Pointer_Leaving_Window
        if windows.has_key(b[0]):
            windows[b[0]].leaving()
    elif event == 5:		# Pointer_Entering_Window
        windows[b[0]].entering()
    elif event == 6:		# Mouse_Click
        details[0].click(details[1], details[2], details[3], details[4], details[5], details[6])
    elif event == 7:		# User_Drag_Box
        if user_drag_handler:
            user_drag_handler(details[0], details[1], details[2], details[3])
    elif event == 8:		# Key_Pressed
        details[0].key(details[1], details[2])
 				# User_Message, User_Message_Recorded, User_Message_Acknowledge
    elif event == 17 or event == 18 or event == 19:
        if details == 0:			# Message_Quit
            quit()
        elif details == 0x502:		# Message_HelpRequest
            _help()
        elif message_handler.has_key(details):
            message_handler[details]()
#    except KeyError:
#        pass


def _help():
    "Supply interactive help"
    help = None
    try:
        help = windows[b[8]].help(b[9])
    except KeyError:
        # must be a menu
        bb = swi.block(20)
        # Wimp_GetMenuState
        swi.swi(0x400f4, '0b', bb)
        submenus = ''
        for selection in bb:
            if selection == -1:
                break
            submenus = submenus + '.' + str(selection)
        if m.has_key('help.menu.' + current_menu.name + submenus):
            help = m['help.menu.' + current_menu.name + submenus]
        elif m.has_key('help.menu.' + current_menu.name):
            help = m['help.menu.' + current_menu.name]
    if help is None:
        help = m['help.default']
    b[0] = 256
    b[3] = b[2]
    b[4] = 0x503
    b.padstring(help, '\0', 20, 256)
    # Wimp_SendMessage
    swi.swi(0x400e7, 'ibi', 17, b, b[1])


def screen_size():
    "Return the screen width and height"
    bb = swi.block(20)
    bb[0] = 130
    bb[1] = 131
    bb[2] = -1
    # OS_ReadVduVariables
    swi.swi(0x31, 'bb', bb, bb)
    # OS_ReadModeVariable
    width = (bb[0] + 1) * 2 ** swi.swi(0x35, '-4;..i')
    height = (bb[1] + 1) * 2 ** swi.swi(0x35, '-5;..i')
    return width, height


def mem_string(address, write = None):
    "Read or write a string in memory - dangerous"
    if write is None:
        bb = swi.register(256, address)
        return bb.ctrlstring()
    else:
        bb = swi.register((len(write) + 4) / 4, address)
        bb.padstring(write, '\0', 0, len(write) + 1)


def hourglass_on():
    "Start the hourglass"
    # Hourglass_On
    swi.swi(0x406c0, '')

def hourglass_off():
    "Stop the hourglass"
    # Hourglass_Off
    swi.swi(0x406c1, '')

def hourglass_percentage(value):
    "Show a percentage under the hourglass"
    # Hourglass_Percentage
    swi.swi(0x406c4, 'i', value)


def warning(message):
    "Give a warning error box"
    # Wimp_ReportError
    swi.swi(0x400df, 's1s', '    ' + message, task_name)


def lookup(message):
    "Lookup message if possible"
    try:
        return m[message]
    except KeyError:
        return message


def send_message(event, action, receiver, data, icon = -1, ref = 0):
    "Send a wimp message"
    b[3] = ref
    b[4] = action
    offset = 5
    for thing in data:
        if isinstance(thing, types.IntType):
            b[offset] = thing
            offset = offset + 1
        else:
            length = int((len(thing) + 4) / 4)
            b.padstring(thing, '\0', offset * 4, (offset + length) * 4)
            offset = offset + length
            break
    b[0] = offset * 4
    # Wimp_SendMessage
    swi.swi(0x400e7, 'ibii', event, b, receiver, icon)


def close_menus():
    # Wimp_CreateMenu
    swi.swi(0x400d4, '.-')


def set_colour(colour):
    swi.swi('ColourTrans_SetGCOL', 'i..00', colour << 8)


def plot(type, x, y):
    # OS_Plot
    swi.swi(0x45, 'iii', type, x, y)


def pointer():
    bb = swi.block(20)
    # Wimp_GetPointerInfo
    swi.swi(0x400cf, '.b', bb)
    return bb[0], bb[1], bb[2], bb[3], bb[4]


class window:
    "Wimp window class"

    def __init__(self, id):
        "Load window from template file"
        # Wimp_OpenTemplate
        try:
            swi.swi(0x400d9, '.s', directory + '.Resources.' + territory + '.Templates')
        except swi.error:
            swi.swi(0x400d9, '.s', directory + '.Resources.UK.Templates')
        # Wimp_LoadTemplate
        buffer_size, workspace_size, found = swi.swi(0x400db, '.0..-s0;.ii...i', id)
        if not found:
            raise RuntimeError, m['error.template'] % id
        buffer = swi.block(buffer_size / 4 + 1)
        workspace = swi.block(workspace_size / 4 + 1)
        swi.swi(0x400db, '.bbe-s0', buffer, workspace, workspace, id)
        # Wimp_CreateWindow
        self.handle = swi.swi(0x400c1, '.b;i', buffer)
        self.workspace = workspace
        self.name = id
        self.title_pointer = buffer[18]
        self.icons = buffer[21]
        self.is_open = 0
        # Wimp_CloseTemplate
        swi.swi(0x400da, '')
        windows[self.handle] = self
        self.contents = []

    def remove(self):
        del windows[self.handle]
        bb = swi.block(20)
        bb[0] = self.handle
        # Wimp_DeleteWindow
        swi.swi(0x400c3, '.b', bb)

    def do_redraw(self):
        # Wimp_RedrawWindow
        more = swi.swi(0x400c8, '.b;i', b)
        self.ox = b[1] - b[5]
        self.oy = b[4] - b[6]
        self.redraw_start()
        while more:
            self.gx0 = b[7] - self.ox
            self.gy0 = self.oy - b[10]
            self.gx1 = b[9] - self.ox
            self.gy1 = self.oy - b[8]
            self.redraw()
            # Wimp_GetRectangle
            more = swi.swi(0x400ca, '.b;i', b)
        self.redraw_stop()

    def redraw_start(self):
        "Get ready to redraw the window"

    def redraw(self):
        "Redraw a rectangle of the window"
        if self.contents:
            for content in self.contents:
                if not ((content.pos[2] < self.gx0) or (content.pos[0] > self.gx1) or (content.pos[3] < self.gy0) or (content.pos[1] > self.gy1)):
                    content.redraw(self.ox, self.oy)

    def redraw_stop(self):
        "Tidy up after redrawing the window"

    def open(self, minx = None, miny = None, maxx = None, maxy = None,
                   scrollx = None, scrolly = None, behind = None):
        "Open the window"
        bb = swi.block(20)
        bb[0] = self.handle
        if minx is None:
            # Wimp_GetWindowState
            swi.swi(0x400cb, '.b', bb)
        else:
            bb[1] = minx
            bb[2] = miny
            bb[3] = maxx
            bb[4] = maxy
            bb[5] = scrollx
            bb[6] = scrolly
            bb[7] = behind
        # Wimp_OpenWindow
        swi.swi(0x400c5, '.b', bb)
        self.is_open = 1

    def open_centred(self, behind = None):
        "Open the window centred in the screen"
        bb = swi.block(20)
        bb[0] = self.handle
        # Wimp_GetWindowState
        swi.swi(0x400cb, '.b', bb)
        screen_width, screen_height = screen_size()
        width = bb[3] - bb[1]
        height = bb[4] - bb[2]
        if behind is None:
            behind = bb[7]
        self.open((screen_width - width) / 2, (screen_height - height) / 2,
                  (screen_width + width) / 2, (screen_height + height) / 2,
                   bb[5], bb[6], behind)

    def open_full_size(self, behind = None):
        bb = swi.block(20)
        bb[0] = self.handle
        swi.swi('Wimp_GetWindowState', '.b', bb)
        bb[3] = 10000
        bb[2] = -10000
        if behind:
            bb[7] = behind
        swi.swi('Wimp_OpenWindow', '.b', bb)
        self.is_open = 1

    def open_as_menu(self):
        "Open the window as a menu centred on the pointer"
        bb = swi.block(20)
        bb[0] = self.handle
        # Wimp_GetWindowState
        swi.swi(0x400cb, '.b', bb)
        width = bb[3] - bb[1]
        height = bb[4] - bb[2]
        # Wimp_GetPointerInfo
        swi.swi(0x400cf, '.b', bb)
        # Wimp_CreateMenu
        swi.swi(0x400d4, '.iii', self.handle, bb[0] - width / 2, bb[1] + height / 2)

    def close(self):
        "Close the window"
        bb = swi.block(20)
        bb[0] = self.handle
        # Wimp_CloseWindow
        swi.swi(0x400c6, '.b', bb)
        self.is_open = 0

    def entering(self):
        "Pointer entering window"

    def leaving(self):
        "Pointer leaving window"

    def click(self, icon, buttons, x, y, sx, sy):
        "Mouse click in window"
        self.contents.reverse()
        if self.contents:
            for content in self.contents:
                if content.pos[0] <= x <= content.pos[2] and content.pos[1] <= y <= content.pos[3]:
                    if content.click(buttons, x - content.pos[0], y - content.pos[1]):
                        self.contents.reverse()
                        return 1
        self.contents.reverse()
        return 0

    def help(self, icon):
        "Return interactive help message"
        if m.has_key('help.' + self.name + '.' + str(icon)):
            return m['help.' + self.name + '.' + str(icon)]
        elif m.has_key('help.' + self.name):
            return m['help.' + self.name]
        return None

    def key(self, icon, key):
        "Handle key presses"
        # Wimp_ProcessKey
        swi.swi(0x400dc, 'i', key)

    def icon_text(self, icon, text = None):
        "Read or write an icon's text contents"
        if icon >= self.icons:
            raise RuntimeError, 'Icon does not exist in icon_text()'
        bb = swi.block(20)
        bb[0] = self.handle
        bb[1] = icon
        # Wimp_GetIconState
        swi.swi(0x400ce, '.b', bb)
        indirected = bb[6] & 0x100
        if text is None:
            if indirected:
                return mem_string(bb[7])
            else:
                return mem_string(bb.start + 28)
        else:
            if not indirected:
                raise RuntimeError, 'Icon not indirected in icon_text()'
            mem_string(bb[7], text)
            # Wimp_SetIconState
            bb[2] = bb[3] = 0
            swi.swi(0x400cd, '.b', bb)
            # Wimp_GetCaretPosition
            swi.swi(0x400d3, '.b', bb)
            if bb[0] == self.handle and bb[1] == icon:
                self.put_caret(icon)

    def icon_crement(self, icon, min, max, step = 1):
        "Increment or decrement a numerical icon"
        number = self.icon_text(icon)
        if number == '':
            number = (step < 0) * min + (step > 0) * max
        else:
            number = eval(number)
            if number == (step < 0) * min + (step > 0) * max:
                return
            if number > max:
                number = max
            elif number < min:
                number = min
            else:
                number = number + step
        self.icon_text(0, str(number))

    def icon_set(self, icon, state = None):
        "Return the state or set the state of an icon"
        if icon >= self.icons:
            raise RuntimeError, 'Icon does not exist in icon_set()'
        bb = swi.block(20)
        bb[0] = self.handle
        bb[1] = icon
        # Wimp_GetIconState
        swi.swi(0x400ce, '.b', bb)
        if state is None:
            return (bb[6] & 0x200000) == 0x200000
        else:
            bb[6] = bb[6] & ~0x200000
            if state:
                bb[6] = bb[6] | 0x200000
            # Wimp_SetIconState
            bb[2] = bb[3] = bb[6]
            swi.swi(0x400cd, '.b', bb)

    def put_caret(self, icon = -1, x = 0, y = 0, height = 0x2000000):
        "Place the caret in the window"
        if icon == -1:
            # Wimp_SetCaretPosition
            swi.swi(0x400d2, 'iiiii0', self.handle, icon, x, y, height)
        else:
            # Wimp_SetCaretPosition
            swi.swi(0x400d2, 'ii00-i', self.handle, icon, len(self.icon_text(icon)))

    def icon_grey(self, icon, grey = 1):
        "(Un)grey out an icon"
        if icon >= self.icons:
            raise RuntimeError, 'Icon does not exist in icon_grey()'
        bb = swi.block(20)
        bb[0] = self.handle
        bb[1] = icon
        bb[2] = 0x400000 * grey
        bb[3] = 0x400000
        swi.swi(0x400cd, '.b', bb)

    def icon_ungrey(self, icon):
        "Ungrey an icon"
        self.icon_grey(icon, 0)

    def add_content(self, content):
        "Add a content class to the window"
        self.contents.append(content)

    def remove_content(self, content):
        "Remove a content class from the window"
        self.contents.remove(content)

    def refresh(self, x0, y0, x1, y1):
        "Force a redraw of an area of the window"
        swi.swi('Wimp_ForceRedraw', 'iiiii', self.handle, x0, -y1, x1, -y0)

    def update(self, x0, y0, x1, y1):
        "Update an area of the window"
        bb = swi.block(20)
        bb[0] = self.handle
        bb[1] = x0
        bb[2] = -y1
        bb[3] = x1
        bb[4] = -y0
        # Wimp_UpdateWindow
        more = swi.swi(0x400c9, '.b;i', bb)
        self.ox = bb[1] - bb[5]
        self.oy = bb[4] - bb[6]
        self.redraw_start()
        while more:
            self.gx0 = bb[7] - self.ox
            self.gy0 = self.oy - bb[10]
            self.gx1 = bb[9] - self.ox
            self.gy1 = self.oy - bb[8]
            self.redraw()
            # Wimp_GetRectangle
            more = swi.swi(0x400ca, '.b;i', bb)
        self.redraw_stop()

    def title(self, title = None):
        "Change or return the window title"
        if title is None:
            return mem_string(self.title_pointer, None)
        else:
            mem_string(self.title_pointer, title)
            if self.is_open:
                bb = swi.block(20)
                # Wimp_GetCaretPosition
                swi.swi(0x400d3, '.b', bb)
                if bb[0] == self.handle:
                    # Wimp_SetCaretPosition
                    swi.swi(0x400d2, '-000--')
                    swi.swi(0x400d2, 'iiiiii', bb[0], bb[1], bb[2], bb[3], bb[4], bb[5])
                else:
                    # Wimp_SetCaretPosition
                    swi.swi(0x400d2, 'i-00i-', self.handle, 1<<25)
                    swi.swi(0x400d2, 'iiiiii', bb[0], bb[1], bb[2], bb[3], bb[4], bb[5])

    def set_extent(self, x0, y0, x1, y1):
        "Set the work area extent in os units"
        bb = swi.block(20)
        bb[0] = x0
        bb[1] = -y1
        bb[2] = x1
        bb[3] = -y0
        # Wimp_SetExtent
        swi.swi(0x400d7, 'ib', self.handle, bb)

    def origin(self):
        "Return the window origin position on the screen"
        bb = swi.block(20)
        bb[0] = self.handle
        swi.swi('Wimp_GetWindowState', '.b', bb)
        self.ox = b[1] - b[5]
        self.oy = b[4] - b[6]
        return self.ox, self.oy

    def icon_box(self, icon):
        "Return the bbox of an icon relative to the window"
        bb = swi.block(20)
        bb[0] = self.handle
        bb[1] = icon
        swi.swi('Wimp_GetIconState', '.b', bb)
        return bb[2], -bb[5], bb[4], -bb[3]

    def icon_drag(self, icon):
        "Start to drag the specified icon"
        self.origin()
        x0, y0, x1, y1 = self.icon_box(icon)
        bb = swi.block(20)
        bb[0] = self.ox + x0
        bb[1] = self.oy - y1
        bb[2] = self.ox + x1
        bb[3] = self.oy - y0
        swi.swi('DragASprite_Start', 'i1sb', 0xc5, self.icon_text(icon), bb)


class iconbar_icon(window):

    def __init__(self, sprite, handler, indirected = 0):
        "Create an iconbar icon with the specified sprite"
        bb = swi.block(20)
        bb[0] = -1
        bb[1] = bb[2] = 0
        bb[3] = bb[4] = 68
        if indirected:
            bb[5] = 0x311a
            self.buffer = swi.block(5)
            self.buffer.padstring(sprite, '\0')
            bb[6] = self.buffer.start
            bb[7] = 1
            bb[8] = len(sprite)
        else:
            bb[5] = 0x301a
            bb.padstring(sprite, '\0', 24, 36)
        # Wimp_CreateIcon
        self.icon = swi.swi(0x400c2, '0b;i', bb)
        self.icons = self.icon + 1
        self.handle = -2
        self.name = 'iconbar'
        self.handler = handler
        windows[-2] = self

    def click(self, icon, buttons, x, y, sx, sy):
        "Handle a click on the iconbar"
        self.handler(buttons, sx)

    def change(self, sprite):
        self.icon_text(self.icon, sprite)


class info_window(window):
    "Info box window including email/web buttons"

    def __init__(self):
        "Load the window template"
        window.__init__(self, 'info')

    def click(self, icon, buttons, x, y, sx, sy):
        "Handle mouse clicks"
        try:
            if icon == 8 or icon == 9:
                if os.path.exists('System:Modules.Network.URI'):
                    # Wimp_StartTask
                    swi.swi(0x400de, 's', 'RMEnsure AcornURI 0.00 RMRun System:Modules.Network.URI')
            if icon == 8:
                # URI_Dispatch
                swi.swi(0x4e381, '0s0', m['email'])
            elif icon == 9:
                # URI_Dispatch
                swi.swi(0x4e381, '0s0', m['www'])
        except swi.error:
            swi.swi(0x107, '')


class save_window(window):
    "Standard RISC OS save window"

    def __init__(self, handler, type):
        "Load the window template"
        window.__init__(self, 'save')
        self.handler = handler
        self.type = type

    def click(self, icon, buttons = 0, x = 0, y = 0, sx = 0, sy = 0):
        "Handle mouse clicks"
        if icon == 0 and buttons > 15:
            if self.icon_text(1) == '':
                warning(m['error.save.empty'])
            else:
                self.drag_save()
        elif icon == 2:
            close_menus()
            self.close()
        elif icon == 3:
            path = self.icon_text(1)
            if os.path.isabs(path):
                self.handler(path, 0)
                close_menus()
                self.close()
            else:
                warning(m['error.save.tosave'])

    def key(self, icon, key):
        "Handle key presses"
        if key == 0xd:
            self.click(3)
        elif key == 0x1b:
            self.click(2)
        else:
            # Wimp_ProcessKey
            swi.swi(0x400dc, 'i', key)

    def path(self, path):
        "Set the save path"
        self.icon_text(1, path)

    def drag_save(self):
        "Save the object by dragging the icon"
        self.icon_drag(0)
        while 1:
            event, details = poll()
            if event == 7:
                swi.swi('DragASprite_Stop', '')
                mouse = pointer()
                path = self.icon_text(1)
                # Message_DataSave
                send_message(18, 1, mouse[3],
                  (mouse[3], mouse[4], mouse[0], mouse[1], 0, self.type, os.path.basename(path)),
                  mouse[4])
                break
            else:
                handle((event, details))
        event, details = poll()
        if event == 19 and details == 1:
            # Message_DataSave bounced
            pass
        elif event in (17, 18) and details == 2:
            path = b.nullstring(44, b[0])
            # Message_DataSaveAck
            if self.handler(path, b[9] == -1):
                # Message_DataLoad
                send_message(17, 3, b[1], (b[5], b[6], b[7], b[8], b[9], b[10], path), ref = b[2])
                close_menus()
                self.close()
        else:
            handle((event, details))


class menu:
    "Wimp menu class"

    def __init__(self, name, items):
        "Load menu"
        self.blocks = []
        self.menu_block = self._load(items)
        self.name = name

    def menu(self, x, y):
        "Display menu and return the selection"
        global current_menu
        current_menu = self
        swi.swi(0x400d4, '.bii', self.menu_block, x, y)
        while 1:
            event, details = poll(1)
            # Menu_Selection
            if event == 9:
                bb = swi.block(20)
                # Wimp_GetPointerInfo
                swi.swi(0x400cf, '.b', bb)
                return details, (bb[2] == 1)
            # User_Message, User_Message_Recorded: Message_MenusDeleted
            elif (event == 17 or event == 18) and details == 0x400c9:
                return None, 0
            else:
                handle((event, details))

    def popup(self, window, icon):
        "Popup the menu next to the icon"
        bb = swi.block(20)
        bb[0] = window.handle
        bb[1] = icon
        # Wimp_GetIconState
        swi.swi(0x400ce, '.b', bb)
        x = bb[4]
        y = bb[5]
        # Wimp_GetWindowState
        swi.swi(0x400cb, '.b', bb)
        return self.menu(bb[1] - bb[5] + x, bb[4] - bb[6] + y)

    def _load(self, items):
        "Create a wimp menu structure (recursively)"
        menu_block = swi.block(7 + 6 * (len(items) - 1))
        self.blocks.append(menu_block)

        title_block = swi.block(len(lookup(items[0])) / 4 + 1)
        self.blocks.append(title_block)
        title_block.padstring(lookup(items[0]), '\0')
        menu_block[0] = title_block.start

        menu_block[1] = menu_block[2] = -1
        menu_block[3] = 0x00070207
        menu_block[4] = 300
        menu_block[5] = 44
        menu_block[6] = 0

        offset = 7

        for item in items[1:]:
            if isinstance(item, types.StringType):
                text = item
                submenu = -1
            else:
                text = item[0]
                if isinstance(item[1], window):
                    submenu = item[1].handle
                else:
                    submenu = self._load(item[1]).start
            grey = False
            if text[0] == '-':
                grey = True
                text = text[1:]

            item_block = swi.block(len(lookup(text)) / 4 + 1)
            self.blocks.append(item_block)
            item_block.padstring(lookup(text), '\0')
            menu_block[offset] = (0x100 * (offset == 7))
            menu_block[offset + 1] = submenu
            menu_block[offset + 2] = 0x07000111
            if grey:
                menu_block[offset + 2] |= 1 << 22
            menu_block[offset + 3] = item_block.start
            menu_block[offset + 4] = menu_block[offset + 5] = -1

            offset = offset + 6

        menu_block[offset - 6] = menu_block[offset - 6] | 0x80

        return menu_block


class content:
    "Base window content class"

    def __init__(self, w, x0, y0, x1, y1):
        "Initialise a window content"
        self.window = w
        self.pos = [x0, y0, x1, y1]

    def redraw(self, ox, oy):
        "Redraw content"
        pass

    def click(self, buttons, x, y):
        "Handle clicks in the content"
        return 0

    def refresh(self):
        "Force redraw of the content"
        self.window.refresh(self.pos[0] - 4, self.pos[1] - 4,
                self.pos[2] + 4, self.pos[3] + 4)

    def update(self):
        "Update the content"
        self.window.update(self.pos[0] - 4, self.pos[1] - 4,
                self.pos[2] + 4, self.pos[3] + 4)


class rectangle(content):
    "Example window content"

    def __init__(self, w, x0, y0, x1, y1, fill = None, border = None,
            text = None, font = None, text_colour = 0x000000):
        content.__init__(self, w, x0, y0, x1, y1)
        self.fill = fill
        self.border = border
        self.text = text
        self.font = font
        self.text_colour = text_colour

    def redraw(self, ox, oy):
        if not self.fill is None:
            # ColourTrans_SetGCOL
            swi.swi(0x40743, 'i..i0', self.fill << 8, 1 << 8)
            # OS_Plot
            swi.swi(0x45, '4ii', ox + self.pos[0], oy - self.pos[3])
            swi.swi(0x45, 'iii', 96 + 5, ox + self.pos[2], oy - self.pos[1])
        if not self.border is None:
            # ColourTrans_SetGCOL
            swi.swi(0x40743, 'i..i0', self.border << 8, 1 << 8)
            # OS_Plot
            swi.swi(0x45, '4ii', ox + self.pos[0], oy - self.pos[3])
            swi.swi(0x45, '5ii', ox + self.pos[2], oy - self.pos[3])
            swi.swi(0x45, '5ii', ox + self.pos[2], oy - self.pos[1])
            swi.swi(0x45, '5ii', ox + self.pos[0], oy - self.pos[1])
            swi.swi(0x45, '5ii', ox + self.pos[0], oy - self.pos[3])
        if not self.text is None and not self.font is None:
            self.font.colour(0xdddddd, self.text_colour)
            self.font.paint(self.text, 400 * (ox + self.pos[0]),
                    400 * (oy - (self.pos[1] + self.pos[3] * 3) / 4))

    def click(self, buttons, x, y):
        #self.window.remove_content(self)
        #self.refresh()
        return 0


class paragraph(content):
    "A paragraph of text"

    def __init__(self, w, x0, y0, x1, font_family, font_style, font_size,
            text, text_colour = 0x000000, fill = None, padding = 10):
        self.font_family = font_family
        self.font_style = font_style
        self.font_size = font_size
        self.line_height = int(font_size * 0.2)
        self.text = []
        self.text_colour = text_colour
        self.fill = fill
        self.padding = padding
        text = text.encode('utf8')
        width = x1 - x0 - padding - padding
        while text:
            (c, x) = rufl.split(font_family, font_style, font_size, text,
                    width)
            s = text[:c]
            space = s.rfind(' ')
            if c != len(text) and space != -1:
                c = space
            s = text[:c + 1]
            text = text[c + 1:]
            self.text.append(s)
        y1 = y0 + len(self.text) * self.line_height + padding + padding
        content.__init__(self, w, x0, y0, x1, y1)
        self.y1 = y1

    def redraw(self, ox, oy):
        if not self.fill is None:
            swi.swi('ColourTrans_SetGCOL', 'i..i0', self.fill << 8, 1 << 8)
            swi.swi('OS_Plot', '4ii', ox + self.pos[0], oy - self.pos[3])
            swi.swi('OS_Plot', 'iii', 96 + 5, ox + self.pos[2],
                    oy - self.pos[1])
        swi.swi('ColourTrans_SetFontColours', 'iiii', 0,
                0xffffff << 8, self.text_colour << 8, 14)
        y = oy - self.pos[1] - self.padding - self.line_height * 0.75
        for line in self.text:
            rufl.paint(self.font_family, self.font_style, self.font_size,
                    line, ox + self.pos[0] + self.padding, y, rufl.blend)
            y -= self.line_height


def font(name, size_x, size_y, no_encoding = False):
    f = (name, size_x, size_y)
    if fonts.has_key(f):
        font = fonts[f]
        font.use = font.use + 1
        return font
    else:
        fonts[f] = _font(name, size_x, size_y, no_encoding)
        return fonts[f]

class _font:

    def __init__(self, name, size_x, size_y, no_encoding):
        self.name = name
        self.size_x = size_x
        self.size_y = size_y
        self.use = 1
        if no_encoding:
            self.handle = swi.swi('XFont_FindFont', '.sii00;i',
                    self.name, self.size_x, self.size_y)
            self.utf8 = False
        else:
            try:
                self.handle = swi.swi('XFont_FindFont', '.sii00;i',
                        self.name + '\EUTF8', self.size_x, self.size_y)
                self.utf8 = True
            except swi.error:
                self.handle = swi.swi('XFont_FindFont', '.sii00;i',
                        self.name + '\ELatin1', self.size_x, self.size_y)
                self.utf8 = False

    def lose(self):
        self.use = self.use - 1
        if self.use == 0:
            # Font_LoseFont
            swi.swi(0x40082, 'i', self.handle)

    def paint(self, string, x, y, spacex = 0, spacey = 0, offx = 0, offy = 0,
              x0 = 0, y0 = 0, x1 = 0, y1 = 0):
        if self.utf8:
            s = string.encode('utf8')
        else:
            s = string.encode('latin1', 'replace')
        if x0:
            bb = swi.block(20)
            bb[0] = spacex
            bb[1] = spacey
            bb[2] = offx
            bb[3] = offy
            bb[4] = int(x0)
            bb[5] = int(y0)
            bb[6] = int(x1)
            bb[7] = int(y1)
            swi.swi('Font_Paint', 'isiiib', self.handle, s, 0x322, x, y, bb)
        else:
            swi.swi('Font_Paint', 'isiii', self.handle, s, 0x300, x, y)

    def charbbox(self, char):
        return swi.swi('Font_CharBBox', 'ii0;.iiii', self.handle, ord(char))

    def colour(self, back, fore):
        # ColourTrans_SetFontColours
        swi.swi(0x4074f, 'iiii', self.handle, back << 8, fore << 8, 14)

    def stringbox(self, string):
        if string == '':
            return (0, 0, 0, 0)
        if string[-1] == ' ':
            string = string + ' '
        if self.utf8:
            s = string.encode('utf8')
        else:
            s = string.encode('latin1', 'replace')
        bb = swi.block(20)
        bb[0] = bb[1] = bb[2] = bb[3] = 0
        bb[4] = -1
        # Font_ScanString
        swi.swi(0x400a1, 'isiiib', self.handle, s, 0x40320, 0x7fffffff,
                0x7fffffff, bb)
        return bb[5], bb[6], bb[7], bb[8]

    def caretpos(self, string, x, y):
        if self.utf8:
            s = string.encode('utf8')
        else:
            s = string.encode('latin1', 'replace')
        bb = swi.block(len(s) / 4 + 10)
        bb.padstring(s, chr(0))
        # Font_ScanString
        pos, cx, cy = swi.swi(0x400a1, 'ibiii;.i.ii', self.handle, bb,
                0x20300, x, y)
        return pos - bb.start, cx, cy

    def caretxy(self, string, pos):
        if self.utf8:
            s = string.encode('utf8')
        else:
            s = string.encode('latin1', 'replace')
        # Font_ScanString
        return swi.swi(0x400a1, 'isiii..i;...ii', self.handle, s,
                0x20380, 0x7fffffff, 0x7fffffff, pos)

    def fontbox(self):
        return swi.swi('Font_ReadInfo', 'i;.iiii', self.handle)

    def split(self, string, x, y):
        if self.utf8:
            s = string.encode('utf8')
        else:
            s = string.encode('latin1', 'replace')
        bb = swi.block(len(s) / 4 + 10)
        bb.padstring(s, chr(0))
        # Font_ScanString
        pos = swi.swi(0x400a1, 'ibiii;.i', self.handle, bb, 0x300, x, y)
        return pos - bb.start


class document:

    def __init__(self, width, height, margin):
        self.contents = []
        self.views = []
        self.extent = [width, height]
        self.margin = margin
        self.fonts = {}
        self.fonts_use = {}

    def add_content(self, content):
        self.contents.append(content)
        for view in self.views:
            content.add_view(view)

    def remove_content(self, content):
        self.contents.remove(content)

    def add_view(self, view):
        self.views.append(view)
        for content in self.contents:
            content.add_view(view)

    def remove_view(self, view):
        self.views.remove(view)
        for content in self.contents:
            content.remove_view(view)

    def rescaled(self, view):
        for content in self.contents:
            content.remove_view(view)
            content.add_view(view)

    def redraw_start(self, view, scale):
        pass

    def redraw(self, view, scale):
        if self.margin:
            self.redraw_margin(view)
        for content in self.contents:
            content.scaled_pos = map(lambda z, scale = scale: scale * z / 400, content.pos)
            if not ((content.scaled_pos[2] < view.gx0) or (content.scaled_pos[0] > view.gx1) or (content.scaled_pos[3] < view.gy0) or (content.scaled_pos[1] > view.gy1)):
                content.redraw_box = [view.gx0 * 400 / scale - content.pos[0],
                                      view.gy0 * 400 / scale - content.pos[1],
                                      view.gx1 * 400 / scale - content.pos[0],
                                      view.gy1 * 400 / scale - content.pos[1]]
                content.redraw(view, view.ox, view.oy, scale)

    def redraw_stop(self, view, scale):
        pass

    def redraw_margin(self, view):
        set_colour(0x777777)
        plot(4,  view.ox + view.gx0, view.oy)
        plot(97, view.gx1 - view.gx0, self.margin)
        plot(4,  view.ox + view.gx0, view.oy - view.extent[1])
        plot(97, view.gx1 - view.gx0, -self.margin)
        plot(4,  view.ox, view.oy - view.gy1)
        plot(97, -self.margin, view.gy1 - view.gy0)
        plot(4,  view.ox + view.extent[0], view.oy - view.gy1)
        plot(97, self.margin, view.gy1 - view.gy0)

    def click(self, view, buttons, x, y):
        for content in self.contents:
            if content.pos[0] <= x <= content.pos[2] and content.pos[1] <= y <= content.pos[3]:
                content.click(view, buttons, x - content.pos[0], y - content.pos[1])

    def refresh(self, x0, y0, x1, y1):
        for view in self.views:
            view.update(int(view.scale * x0 / 400 - 4), int(view.scale * y0 / 400 - 4),
                        int(view.scale * x1 / 400 + 4), int(view.scale * y1 / 400 + 4))

    def add_font(self, name, size_x, size_y):
        f = (name, size_x, size_y)
        if self.fonts.has_key(f):
            self.fonts_use[f] = self.fonts_use[f] + 1
        else:
            self.fonts[f] = font(name, size_x, size_y)
            self.fonts_use[f] = 1

    def remove_font(self, name, size_x, size_y):
        f = (name, size_x, size_y)
        self.fonts_use[f] = self.fonts_use[f] - 1
        if self.fonts_use[f] == 0:
            self.fonts[f].lose()
            del self.fonts[f]
            del self.fonts_use[f]


class document_view(window):

    def __init__(self, document, id, scale):
        window.__init__(self, id)
        self.document = document
        self.fonts = {}
        self.rescale(scale)
        document.add_view(self)
        self.caret = None

    def redraw_start(self):
        self.document.redraw_start(self, self.scale)

    def redraw(self):
        self.document.redraw(self, self.scale)

    def redraw_stop(self):
        self.document.redraw_stop(self, self.scale)

    def click(self, icon, buttons, x, y, sx, sy):
        if 0 <= x <= self.extent[0] and 0 <= y <= self.extent[1]:
            self.document.click(self, buttons, 400 * x / self.scale, 400 * y / self.scale)

    def key(self, icon, key):
        if self.caret:
            self.caret[0].key(self, key)
        else:
            swi.swi(0x400dc, 'i', key)

    def rescale(self, scale):
        for f in self.fonts.values():
            f.lose()
        self.fonts = {}
        for f in self.document.fonts.keys():
            self.fonts[f] = font(f[0], f[1] * scale, f[2] * scale)
        self.scale = scale
        self.extent = (int(scale * self.document.extent[0] / 400), int(scale * self.document.extent[1] / 400))
        self.set_extent(-self.document.margin, -self.document.margin,
                        self.extent[0] + self.document.margin, self.extent[1] + self.document.margin)
        self.document.rescaled(self)


class document_content:

    def __init__(self, document, x0, y0, x1, y1):
        self.pos = [x0, y0, x1, y1]
        self.colour = 0x0000ff
        self.document = document
        document.add_content(self)

    def add_view(self, view):
        pass

    def remove_view(self, view):
        pass

    def redraw(self, view, x, y, scale):
        set_colour(self.colour)
        plot(4, x + self.scaled_pos[0], y - self.scaled_pos[1])
        plot(21, x + self.scaled_pos[2], y - self.scaled_pos[1])
        plot(21, x + self.scaled_pos[2], y - self.scaled_pos[3])
        plot(21, x + self.scaled_pos[0], y - self.scaled_pos[3])
        plot(21, x + self.scaled_pos[0], y - self.scaled_pos[1])
        plot(21, x + self.scaled_pos[2], y - self.scaled_pos[3])
        plot(4, x + self.scaled_pos[2], y - self.scaled_pos[1])
        plot(21, x + self.scaled_pos[0], y - self.scaled_pos[3])

    def click(self, view, buttons, x, y):
        self.colour = 0xffffff - self.colour
        self.document.refresh(self.pos[0], self.pos[1], self.pos[2], self.pos[3])
