#!/usr/bin/python
#
# dragnotes Proof-of-Concept
# ~~~~~~~~~~~~~~~~~~~~~~~~~~
# This is just a proof-of-concept, quickly put-together and
# very hackish / non-modular code written in Python to demo
# my idea of a easy-to-use stylus-driven notetaking app for
# Nokia Internet Tablets, like the N800.
#
# Copyright (c) 2008-07-22 Thomas Perl <thpinfo.com>
#


import gtk
from gtk import gdk
import gobject
import math

class Note(object):
    def __init__(self, x=1, y=1, width=100, height=50, area_width=100, area_height=100, r=1, g=.8, b=.5):
        self.x = x
        self.y = y
        self.area_width = area_width
        self.area_height = area_height
        self.width = width
        self.height = height
        self.name = 'note'
        self.grabbed = False
        self.drawing = False
        self.draw_lines = []
        self._draw_points = []
        self._move_start = (0, 0)
        self._draw_start = (0, 0)
        self.color = (r, g, b, .8)
        self.grabsize = 50

    def can_grab(self, x, y):
        xdiff = x - self.x
        ydiff = y - self.y
        return (x > self.x and x < self.x+self.width*self.zoom
                and y > self.y and y < self.y + self.height*self.zoom
                and math.sqrt(xdiff**2 + ydiff**2) < self.grabsize)

    def can_draw(self, x, y):
        return (x > self.x and x < self.x+self.width*self.zoom
                and y > self.y and y < self.y + self.height*self.zoom)

    def can_delete(self, x, y):
        xdiff = x - (self.x+(self.width*self.zoom))
        ydiff = y - self.y
        return (x > self.x and x < self.x+self.width*self.zoom
                and y > self.y and y < self.y + self.height*self.zoom
                and math.sqrt(xdiff**2 + ydiff**2) < self.grabsize)

    def grab(self, x, y):
        self.grabbed = True
        self._move_start = (x, y)

    def draw(self, x, y):
        self.drawing = True
        self._draw_points = [((x-self.x)/self.zoom, (y-self.y)/self.zoom)]
        self.draw_lines.append(self._draw_points)
    
    def ungrab(self):
        self.grabbed = False

    def undraw(self):
        self.drawing = False

    def move(self, x, y):
        if self.grabbed:
            self.x -= self._move_start[0] - x
            self.y -= self._move_start[1] - y
            self.x = max(min(self.x, self.area_width), 1)
            self.y = max(min(self.y, self.area_height), 1)
            self._move_start = (x, y)
        elif self.drawing:
            self._draw_points.append(((x-self.x)/self.zoom, (y-self.y)/self.zoom))

    def rough(self, l, skip=0):
        x=0
        for i in l:
            if x == skip:
                yield i
                x = 0
            x += 1

    def expose(self, cr):
        cr.set_source_rgba(*self.color)
        cr.rectangle(self.x, self.y, self.width*self.zoom, self.height*self.zoom)
        if self.grabbed:
            cr.stroke()
        else:
            cr.fill()
        if len(self.draw_lines):
            cr.set_line_width(self.zoom/2)
            cr.set_source_rgb(0, 0, 1)
            for draw_points in self.draw_lines:
                (x0, y0) = draw_points[0]
                cr.move_to(self.x+x0*self.zoom, self.y+y0*self.zoom)
                roughness = 15
                if self.zoom > 4:
                    roughness = 2
                elif self.zoom > 3:
                    roughness = 5
                elif self.zoom > 2:
                    roughness = 11
                elif self.zoom > 1:
                    roughness = 12
                elif self.zoom > .7:
                    roughness = 13
                elif self.zoom > .5:
                    roughness = 14
                if self.grabbed:
                    roughness = 16
                for x0, y0 in self.rough(draw_points[1:], roughness):
                    cr.line_to(self.x+x0*self.zoom, self.y+y0*self.zoom)
                cr.stroke()
        if self.grabbed:
            return
        cr.set_source_rgba(0, 1, 0, .2)
        r = min(self.height*self.zoom, self.grabsize)
        cr.arc(self.x, self.y, r, 0, math.pi/2)
        cr.move_to(self.x, self.y)
        cr.line_to(self.x+r, self.y)
        cr.line_to(self.x, self.y+r)
        cr.line_to(self.x, self.y)
        cr.fill()
        cr.set_source_rgba(1, 0, 0, .7)
        r = min(self.height*self.zoom, self.grabsize)
        cr.move_to(self.x+self.width*self.zoom, self.y)
        cr.line_to(self.x+self.width*self.zoom-r, self.y)
        cr.line_to(self.x+self.width*self.zoom, self.y+r)
        cr.line_to(self.x+self.width*self.zoom, self.y)
        cr.fill()

    @property
    def zoom(self):
        xpos = max(1, self.x - 100)
        return .5 + 2*math.log(1000.*float(xpos)/float(self.area_width), 10)

    def get_area(self):
        return (self.x, self.y, int(self.x+self.width*self.zoom), int(self.y+self.height*self.zoom))

class DragWidget(gtk.DrawingArea):
    def __init__(self, width=800, height=480):
        super(DragWidget, self).__init__()
        self.connect('expose_event', self.expose)

        self.add_events(gtk.gdk.BUTTON_MOTION_MASK | gtk.gdk.BUTTON_PRESS_MASK | gtk.gdk.BUTTON_RELEASE_MASK | gtk.gdk.POINTER_MOTION_MASK)

        self.connect('motion_notify_event', self.motion_notify_event)
        self.connect('button_press_event', self.button_press_event)
        self.connect('button_release_event', self.button_release_event)

        self.notes = []
        self.width = width
        self.height = height

    def add_note(self, note):
        self.notes.append(note)
        self.redraw()
    
    def do_size_request(self, requisition):
        requisition.height = self.height
        requisition.width = self.width

    def do_size_allocate(self, allocation):
        if self.flags() & gtk.REALIZED:
            self.window.move_resize(*allocation)

    def expose(self, widget, event):
        cr = self.window.cairo_create()
        cr.rectangle(event.area.x, event.area.y,event.area.width, event.area.height)
        cr.clip()
        cr.set_source_rgb(255, 255, 255)
        cr.rectangle(0, 0, self.width, self.height)
        cr.fill()
        for note in self.notes:
            note.expose(cr)
        return False

    def button_press_event(self, widget, event):
        self.redraw()
        for note in reversed(self.notes):
            if note.can_grab(event.x, event.y):
                note.grab(event.x, event.y)
                break
            if note.can_delete(event.x, event.y):
                self.notes.remove(note)
                break
            if note.can_draw(event.x, event.y):
                note.draw(event.x, event.y)
                break

    def button_release_event(self, widget, event):
        for note in self.notes:
            if note.grabbed:
                note.ungrab()
                break
            elif note.drawing:
                note.undraw()
                break
        self.redraw()

    def redraw(self, area=None):
        if area is None:
            self.window.invalidate_rect(gtk.gdk.Rectangle(0, 0, self.width, self.height), True)
            #self.queue_draw_area(0, 0, self.width, self.height)
        else:
            self.window.invalidate_rect(gtk.gdk.Rectangle(*area), True)
            #self.queue_draw_area(*area)

    def motion_notify_event(self, widget, event):
        if event.is_hint:
            x, y, state = event.window.get_pointer()
        else:
            x = event.x
            y = event.y
            state = event.state

        for note in (n for n in self.notes if n.drawing):
            note.move(event.x, event.y)
            self.redraw((event.x-10, event.y-10, 20, 20))
        for note in (n for n in self.notes if n.grabbed):
            self.redraw(note.get_area())
            note.move(event.x, event.y)
            self.redraw(note.get_area())

gobject.type_register(DragWidget)

class DragNotes(gtk.Window):
    def __init__(self):
        gtk.Window.__init__(self)
        self.table = gtk.Table(4, 4)
        self.add(self.table)
        self.drag_widget = DragWidget()
        self.table.attach(self.drag_widget, 0, 3, 1, 2)
        self.btn_add = gtk.Button(stock=gtk.STOCK_ADD)
        self.btn_add.connect('clicked', self.add_note)
        self.table.attach(self.btn_add, 0, 1, 0, 1)
        self.btn_add = gtk.Button(stock=gtk.STOCK_REFRESH)
        self.btn_add.connect('clicked', self.expose_notes)
        self.table.attach(self.btn_add, 1, 2, 0, 1)
        self.show_all()
        self.drag_widget.hide()
        self.drag_widget.show()

    def add_note(self, button):
        self.drag_widget.add_note(Note(area_width=self.drag_widget.width, area_height=self.drag_widget.height))
    
    def expose_notes(self, button):
        self.drag_widget.redraw()
    
    def run(self):
        self.connect('destroy', gtk.main_quit)
        gtk.main()

if __name__ == '__main__':
    drag_notes = DragNotes()
    drag_notes.run()

