# Tutorial

Here's a quick tutorial to give you an idea of how it all fits together in practice. It takes the form of an annotated listing of blobedit.py, a simple example application included with the source.

## What does BlobEdit do?

BlobEdit edits Blob Documents. Blob Documents are documents containing Blobs. Blobs are red squares that you place by clicking and move around by dragging.

BlobEdit demonstrates how to:

1. Define a Document class for holding a data structure, and an Application class that deals with it.
2. Give your Documents the ability to be saved in files, and define a file type for those files.
3. Define a View class for displaying your data structure, and ensure that the View is updated whenever the data structure changes.
4. Write a mouse tracking loop to handle dragging within a View.
5. On MacOSX, build a stand-alone, double-clickable application with file and application icons and the ability to launch the application by opening its files.

This tutorial may be extended in the future to cover more features of the framework.

## Imports

We'll start by importing the modules and classes that we'll need (using clairvoyance to determine what they are):

    import pickle
    from GUI import Application, ScrollableView, Document, Window, \
      FileType, Cursor, rgb
    from GUI.Geometry import pt_in_rect, offset_rect, rects_intersect
    from GUI.StdColors import black, red

## The Application class

Because we want to work with a custom Document, we'll have to define our own subclass of Application.

    class BlobApp(Application):

The initialisation method will first initialise Application, and then set a few things up.

      def __init__(self):
        Application.__init__(self)

Let's define a file type for our application's files. Doing this will allow the application to recognise the files it can open, and on MacOSX it will also allow us to give our files distinctive icons if we build an application using py2app. Assigning our file type to the file_type property will cause it to be used automatically by the Open and Save commands.

        self.file_type = FileType(name = "Blob Document", suffix = "blob")

Also, just for fun, let's create a custom cursor to use in our views. Here we're creating a cursor from the image file "blob.tiff", which will be looked for in the Resources directory alongside our main .py file.

        self.blob_cursor = Cursor("blob.tiff")

We're not doing anything with the cursor yet, just storing it away for future use.

Next, we'll define what our application is to do when it's started up without being given any files to open. We do this by overriding the open_app method and having it invoke the New command to create a new, empty document.

      def open_app(self):
        self.new_cmd()

The `new_cmd` method is the method that's invoked by the standard New menu command. There's also an open_cmd method that implements the Open... command. The default implementations of these methods know almost everything about what to do, but there are a few things we need to tell them. First, we need to define how to create a Document object of the appropriate kind. We do this by providing a `make_document` method:

      def make_document(self, fileref):

When a new document is being created, this method is called with fileref = None, and when an existing file is being opened, it is passed a FileRef. Since our application only deals with one type of file, we can ignore the fileref argument. All we have to do is create an instance of our document class and return it. All further initialization will be done by `new_cmd` or open_cmd.

        return BlobDoc()

Finally, we need to tell our Application how to create a window for viewing our document. We do this by providing a `make_window` method. This method is passed the document object for which a window is to be made. Since our application only deals with one type of document, we know what class it will be. If we had defined more than one Document class, we would have to do some testing to find out which kind it was and construct a window accordingly.

      def make_window(self, document):

First, we'll create a top-level Window object and associate it with the Document. The purpose of associating the Window with the Document is so that, when the Window is closed, the Document will get the chance to ask the user whether to save changes.

        win = Window(size = (400, 400), document = document)

Next, we'll create a view for displaying our data structure. We'll call our view class BlobView here and define it later. We make our document the view's model, so that the view will be notified of any changes that require it to be redrawn.

        view = BlobView(model = document,
            extent = (1000, 1000), scrolling = 'hv',
            cursor = self.blob_cursor)

We're intending to make our view class a subclass of ScrollableView so we can scroll it. Here we establish the extent of our view, which is the size of the area that the user will be able to scroll around in, and indicate with the scrolling parameter that it will have both horizontal and vertical scroll bars. We also give it the cursor that we created earlier.

Next we place the view inside the window with options that determine its position, size and resizing behaviour. Without going deeply into details, we're saying that the edges of the scroll frame are to initially have offsets of 0 from the corresponding edges of the window, and that when the window is resized, the view is to be resized along with it.

        win.place(view, left = 0, top = 0, right = 0, bottom = 0,
             sticky = 'nsew')

(The options to the `place` method are very flexible, and there's much more you can do with it than is demonstrated here. See the documentation for it in the Container class for details.)

Finally, we make the window visible on the screen. (It's easy to forget this step. If you leave it out, you won't see anything!)

        win.show()

## The Document class

We'll represent the data structure within our document by means of a `blobs` attribute which will hold a list of Blobs.

    class BlobDoc(Document):

      blobs = None

We won't define an `__init__` method for the document, because there are two different ways that a Document object can get initialised. If it was created by a "New" command, it gets initialised by calling `new_contents`, whereas if it was created by an "Open..." command, it gets initialised by calling `read_contents`. So, we'll put our initialisation in those methods. The `new_contents` method will create a new empty list of blobs, and the `read_contents` method will use `pickle` to read a list of blobs from the supplied file.

      def new_contents(self):
        self.blobs = []

      def read_contents(self, file):
        self.blobs = pickle.load(file)

The counterpart to `read_contents` is `write_contents`, which gets called during the processing of a "Save" or "Save As..." command.

      def write_contents(self, file):
        pickle.dump(self.blobs, file)

We'll also define some methods for modifying our data structure. Later we'll call these from our View in response to user input. After each modification, we call `self.changed()` to mark the document as needing to be saved, and `self.notify_views()` to notify any attached views that they need to be redrawn.

      def add_blob(self, blob):
        self.blobs.append(blob)
        self.changed()
        self.notify_views()

      def move_blob(self, blob, dx, dy):
        blob.move(dx, dy)
        self.changed()
        self.notify_views()

      def delete_blob(self, blob):
        self.blobs.remove(blob)
        self.changed()
        self.notify_views()

We'll also find it useful to have a method that searches for a blob given a pair of coordinates.

      def find_blob(self, x, y):
        for blob in self.blobs:
          if blob.contains(x, y):
            return blob
        return None

## The View class

Our view class will have two responsibilities: (1) drawing the blobs on the screen; (2) handling user input actions.

    class BlobView(ScrollableView):

Drawing is done by the `draw` method. It is passed a Canvas object on which the drawing should be done. First, we'll select some colours for drawing our blobs. We're going to fill them with red and draw a line around them in black. Then we'll traverse the list of blobs and tell each one to draw itself on the canvas.

      def draw(self, canvas, update_rect):
        canvas.fillcolor = red
        canvas.pencolor = black
        for blob in self.model.blobs:
          if blob.intersects(update_rect):
            blob.draw(canvas)

The update_rect parameter is a rectangle that bounds the region needing to be drawn. Here we've shown one way in which it can be used, by only drawing blobs which intersect it. We don't strictly need to do this, since drawing is clipped to the update_rect anyway, but it can make the drawing process more efficient. (In this case it may actually make things worse, since testing for intersection in Python could be slower than letting the underlying graphics library do the clipping, but the technique is shown here for illustration purposes.)

Mouse clicks are handled by the `mouse_down` method. There are three things we want the user to be able to do with the mouse. If the click is in empty space, a new blob should be created; if the click is within an existing blob, it should be dragged, or if the shift key is held down, it should be deleted. So the first thing we will do is search the blob list to find out whether the clicked coordinates are within an existing blob.

      def mouse_down(self, event):
        x, y = event.position
        blob = self.model.find_blob(x, y)

If we find a blob, we either drag it around or delete it depending on the state of the shift key.

        if blob:
          if not event.shift:
            self.drag_blob(blob, x, y)
          else:
            self.model.delete_blob(blob)

If not, we add a new blob to the data structure:

        else:
            self.model.add_blob(Blob(x, y))

If we're dragging a blob, we need to track the movements of the mouse until the mouse button is released. To do this we use the `track_mouse` method of class View.

The `track_mouse` method returns an iterator which produces a series of mouse events as long as the mouse is dragged around with the button held down. It's designed to be used in a for-loop like this:

      def drag_blob(self, blob, x0, y0):
        for event in self.track_mouse():
          x, y = event.position
          self.model.move_blob(blob, x - x0, y - y0)
          x0 = x
          y0 = y

## The Blob class

Here's the implementation of the Blob class, representing a single blob.

    class Blob:

      def __init__(self, x, y):
        self.rect = (x - 20, y - 20, x + 20, y + 20)

      def contains(self, x, y):
        return pt_in_rect((x, y), self.rect)

      def intersects(self, rect):
        return rects_intersect(rect, self.rect)

      def move(self, dx, dy):
        self.rect = offset_rect(self.rect, (dx, dy))

      def draw(self, canvas):
        l, t, r, b = self.rect
        canvas.newpath()
        canvas.moveto(l, t)
        canvas.lineto(r, t)
        canvas.lineto(r, b)
        canvas.lineto(l, b)
        canvas.closepath()
        canvas.fill_stroke()

## Instantiating the application

Finally, to start everything off, we create an instance of our application class and call its `run` method. The run method runs the event loop, and retains control until the application is quit.

    BlobApp().run()

## Using py2app

On MacOSX, while you can quite successfully run BlobEdit as a normal Python script, you may want to go a step further and use py2app to create a stand-alone, double-clickable MacOSX application bundle, complete with all the trimmings. There is a setup.py script in the Demos/BlobEdit directory of the PyGUI distribution that shows you how to do this. You invoke it with

    python setup.py py2app

and your application will appear in the dist directory.

## The End

This tutorial will be expanded in the future, but for now, that's all, folks. Happy blobbing!