Paintbrush-like application using Eagle

Tips to follow this tutorial:

- Images can be collapsed, just click them to toogle.
- Code listing can be collapsed, just click its caption.
- Sections can be collapsed, click their heading.
- Lines added to code are highlighted this way
- Lines removed from code are marked # like this
- Lines that were just changed are highlighted like this.
- Classes and types are typed like `this`.
- Keywords are like `this`.
- Strings, like `"this"`.
- Comments, like `# this`.

Index:

- Introduction
- v01: Creating the User Interface
- v02: Creating The First Tool: Line
- v03: Creating Other Tools
- v04: Implementing The Undo Function
- v05: Extra functionality
- v06: Polish it!
- About This Document

# Introduction

This tutorial will show how to create a simple "paintbrush"-like Eagle application. It will draw free-hand, lines, rectangles and text and even support undo!

Screen shot of final version.

Index

# v01: Creating the User Interface

Let's first create the Graphical User Interface, it will be non-functional, but will already look like the final version! The code is in 01-gui.py.

    #!/usr/bin/env python2

    from eagle import *

    app = App( title="Paint",
               id="paint",
               statusbar=True,
               left=( Color( id="fg",
                             label="Foreground:",
                             color="black",
                             ),
                      Color( id="bg",
                             label="Background:",
                             color=( 255, 0, 0 ),
                             ),
                      Selection( id="tool",
                                 label="Tool:",
                                 ),
                      UIntSpin( id="size",
                                label="Line Size:",
                                min=1,
                                value=1,
                                ),
                      ),
               right=( Group( id="textgroup",
                              label="Text Properties:",
                              children=( Entry( id="text",
                                                label="Contents:",
                                                ),
                                         Font( id="font",
                                               label="Font:",
                                               ),
                                         CheckBox( id="textbgtransp",
                                                   label="Transparent background?",
                                                   ),
                                         ),
                              ),
                       Group( id="rectgroup",
                              label="Rectangle Properties:",
                              children=( CheckBox( id="rectfill",
                                                   label="Fill?",
                                                   ),
                                         ),
                              ),
                       ),
               top=( SaveFileButton(),
                     CloseButton(),
                     Button( id="undo",
                             stock="undo",
                             ),
                     Button( id="clear",
                             stock="clear",
                             ),
                     ),
               center=( Canvas( id="canvas",
                                label="Draw Here:",
                                width=400,
                                height=400,
                                bgcolor="white",
                                ),
                        )
               )

    run()

First version, with non-functional GUI.

First we import symbols from Ealge, then we start to declare our `App`, first we give it a `title`, an `id` (optional) and enable its `statusbar`. Then we define the `left`, `right`, `top` and `center`, those we will explain individually.

## Left Area

This application will feature the main drawing menu at the left side. It will enable user to select background and foreground colors, what tool to use and also the size, that is used by most tools (except "Text"). So there we declare:

``Color`( `id`=`"fg"`, ... )`
  This will be our foreground color component, it will enable users to choose colors and we can get its value with ``app`[ `"fg"` ]`.
``Color`( `id`=`"bg"`, ... )`
  This will be our background color component, similar to the foreground one.
``Selection`( `id`=`"tool"`, ... )`
  Will enable users to select between various tools, right now we don't have any tool, so we don't provide it with any options.
``UIntSpin`( `id`=`"size"`, ... )`
  This is an entry that restrict users to integer values greater than zero. We will use its value to draw the pencil, lines and rectangle border.

## Right Area

Tools that have more specific requirements will have these listed in the right area. Right now we will list every possible option inside its "tool" group, they will be "Text" and "Rectangle" groups. Running this example you will see both visible, but we will change this later to just show the group of the selected tool.

``Group`( `id`=`"textgroup"`, ... )`
  Aggregates text drawing options. It have as children the text that will be used, the font and an option to make the text background transparent.
``Group`( `id`=`"rectgroup"`, ... )`
  Aggregates rectangle drawing options. It have as child one option to select filling rectangles with background color.

### Text Group

``Entry`( `id`=`"text"`, ... )`
  Holds what text to be displayed.
``Font`( `id`=`"font"`, ... )`
  Enable user to select font family, size, weight and style.
``CheckBox`( `id`=`"textbgtransp"`, ... )`
  If checked will make the text background transparent.

### Rectangle Group

``CheckBox`( `id`=`"rectfill"`, ... )`
  If checked will fill rectangles with background color.

## Top Area

Top area will hold some application functionality, like saving the drawing to a file, quit the application, clear the canvas and even the undo feature.

``SaveFileButton`()`
  This pops up a dialog to select a file name to save to. Right now it have no parameters, since it just need a callback to be useful and we will add callbacks later.
``CloseButton`()`
  This button is useful on its own and don't need any parameter.
``Button`( `id`=`"undo"`, ... )`
  This button will use stock (pre-defined) values for text and icon, it uses even internationalized text if available. When it get a callback, it will do the undo function, restoring previous drawings.
``Button`( `id`=`"clear"`, ... )`
  Similar to the undo button. When it get a callback, it will clear the image, making it "virgin" again.

## Center Area

The center area will hold the canvas, that is, the place where we draw.

``Canvas`( `id`=`"canvas"`, ... )`
  This drawing are will have a white background and will be 400 by 400 pixels wide.

Index

# v02: Creating The First Tool: Line

Now lets start to make it functional by adding the line drawing tool. Together with it we will create an extensible architecture to add more tools later. The code is in 02-line.py. The differences between the previous version and the current is given at 01-02.diff.

    #!/usr/bin/env python2

    from eagle import *

    class Tool( object ):
        """Interface to be implemented by tools."""

        def mouse( self, app, canvas, buttons, x, y ):
            """This tool have a user feedback using mouse on canvas."""
            pass
        # mouse()
    # Tool

    class Line( Tool ):
        def __init__( self ):
            self.first_point = None
        # __init__()

        def mouse( self, app, canvas, buttons, x, y ):
            if buttons & Canvas.MOUSE_BUTTON_1:
                if self.first_point is None:
                    self.first_point = ( x, y )
                else:
                    color = app[ "fg" ]
                    size  = app[ "size" ]
                    x0, y0 = self.first_point
                    canvas.draw_line( x0, y0, x, y, color, size )
                    self.first_point = None
        # mouse()
    # Line

    tools = {
        "Line": Line(),
        }
    def_tool="Line"

    def canvas_action( app, canvas, buttons, x, y ):
        tool = app[ "tool" ]
        tools[ tool ].mouse( app, canvas, buttons, x, y )
    # canvas_action()

    app = App( title="Paint",
               id="paint",
               statusbar=True,
               left=( Color( id="fg",
                             label="Foreground:",
                             color="black",
                             ),
                      Color( id="bg",
                             label="Background:",
                             color=( 255, 0, 0 ),
                             ),
                      Selection( id="tool",
                                 label="Tool:",
                                 options=tools.keys(),
                                 active=def_tool,
                                 ),
                      UIntSpin( id="size",
                                label="Line Size:",
                                min=1,
                                value=1,
                                ),
                      ),
               right=( Group( id="textgroup",
                              label="Text Properties:",
                              children=( Entry( id="text",
                                                label="Contents:",
                                                ),
                                         Font( id="font",
                                               label="Font:",
                                               ),
                                         CheckBox( id="textbgtransp",
                                                   label="Transparent background?",
                                                   ),
                                         ),
                              ),
                       Group( id="rectgroup",
                              label="Rectangle Properties:",
                              children=( CheckBox( id="rectfill",
                                                   label="Fill?",
                                                   ),
                                         ),
                              ),
                       ),
               top=( SaveFileButton(),
                     CloseButton(),
                     Button( id="undo",
                             stock="undo",
                             ),
                     Button( id="clear",
                             stock="clear",
                             ),
                     ),
               center=( Canvas( id="canvas",
                                label="Draw Here:",
                                width=400,
                                height=400,
                                bgcolor="white",
                                callback=canvas_action,
                                ),
                        )
               )

    run()

Newer version, with changes highlighted. Now we have the line tool.

Let's start by creating an interface to guide our tool behaviour, this is done with `Tool` and its method ``mouse`( `self`, `app`, `canvas`, `buttons`, `x`, `y` )`, that will be called when user press, release or move the mouse with some button pressed, this data came from `Canvas` callback `canvas_action`.

Then we implement this interface for the line drawing tool. To draw a line we need two points, since we're called just when user click or move the mouse, we need to remember the last point and just draw when we have the second point.

Now we make it usable by the application, we will use a dict to map tools and their names and then change the tools component to make it use this information. Note that we alter the `App` declaration, more specifically the `Selection` declaration to make use of the options.

And to finish this step, create a canvas callback `canvas_action` and alter `App` definition to make use of it in the `Canvas` definition.

Index

# v03: Creating Other Tools

Following the ideas in the last section we add the remaining tools `Pencil`, `Rectangle` and `Text` drawing. This should be no problem if you understood the previous section. The code is in 03-tools.py. The differences between the previous version and the current is given at 02-03.diff.

    #!/usr/bin/env python2

    from eagle import *

    class Tool( object ):
        """Interface to be implemented by tools."""

        def mouse( self, app, canvas, buttons, x, y ):
            """This tool have a user feedback using mouse on canvas."""
            pass
        # mouse()
    # Tool

    class Line( Tool ):
        def __init__( self ):
            self.first_point = None
        # __init__()

        def mouse( self, app, canvas, buttons, x, y ):
            if buttons & Canvas.MOUSE_BUTTON_1:
                if self.first_point is None:
                    self.first_point = ( x, y )
                else:
                    color = app[ "fg" ]
                    size  = app[ "size" ]
                    x0, y0 = self.first_point
                    canvas.draw_line( x0, y0, x, y, color, size )
                    self.first_point = None
        # mouse()
    # Line

    class Pencil( Tool ):
        def __init__( self ):
            self.last_point = None
        # __init__()

        def mouse( self, app, canvas, buttons, x, y ):
            if buttons & Canvas.MOUSE_BUTTON_1:
                color = app[ "fg" ]
                size  = app[ "size" ]
                if self.last_point is not None:
                    x0, y0 = self.last_point
                else:
                    x0 = x + 1
                    y0 = y

                if size == 1:
                    canvas.draw_point( x, y, color )
                else:
                    half = size / 2
                    canvas.draw_rectangle( x-half, y-half, size, size, color, 1,
                                           color, True )
                canvas.draw_line( x0, y0, x, y, color, size )
                self.last_point = ( x, y )
            else:
                # Button 1 was released, reset last point
                self.last_point = None
        # mouse()
    # Pencil

    class Rectangle( Tool ):
        def __init__( self ):
            self.first_point = None
        # __init__()

        def mouse( self, app, canvas, buttons, x, y ):
            if buttons & Canvas.MOUSE_BUTTON_1:
                if self.first_point is None:
                    self.first_point = ( x, y )
                else:
                    fg   = app[ "fg" ]
                    bg   = app[ "bg" ]
                    size = app[ "size" ]
                    fill = app[ "rectfill" ]

                    x0, y0 = self.first_point

                    if x0 > x:
                        x0, x = x, x0
                    if y0 > y:
                        y0, y = y, y0

                    w = x - x0
                    h = y - y0

                    canvas.draw_rectangle( x0, y0, w, h, fg, size, bg, fill )
                    self.first_point = None
        # mouse()
    # Rectangle

    class Text( Tool ):
        def mouse( self, app, canvas, buttons, x, y ):
            if buttons & Canvas.MOUSE_BUTTON_1 and app[ "text" ]:
                text  = app[ "text" ]
                fg    = app[ "fg" ]
                bg    = app[ "bg" ]
                font  = app[ "font" ]

                if app[ "textbgtransp" ]:
                    bg = None

                canvas.draw_text( text, x, y, fg, bg, font )
        # mouse()
    # Text

    tools = {
        "Line": Line(),
        "Pencil": Pencil(),
        "Rectangle": Rectangle(),
        "Text": Text(),
        }
    def_tool="Line"

    def canvas_action( app, canvas, buttons, x, y ):
        tool = app[ "tool" ]
        tools[ tool ].mouse( app, canvas, buttons, x, y )
    # canvas_action()

    app = App( title="Paint",
               id="paint",
               statusbar=True,
               left=( Color( id="fg",
                             label="Foreground:",
                             color="black",
                             ),
                      Color( id="bg",
                             label="Background:",
                             color=( 255, 0, 0 ),
                             ),
                      Selection( id="tool",
                                 label="Tool:",
                                 options=tools.keys(),
                                 active=def_tool,
                                 ),
                      UIntSpin( id="size",
                                label="Line Size:",
                                min=1,
                                value=1,
                                ),
                      ),
               right=( Group( id="textgroup",
                              label="Text Properties:",
                              children=( Entry( id="text",
                                                label="Contents:",
                                                ),
                                         Font( id="font",
                                               label="Font:",
                                               ),
                                         CheckBox( id="textbgtransp",
                                                   label="Transparent background?",
                                                   ),
                                         ),
                              ),
                       Group( id="rectgroup",
                              label="Rectangle Properties:",
                              children=( CheckBox( id="rectfill",
                                                   label="Fill?",
                                                   ),
                                         ),
                              ),
                       ),
               top=( SaveFileButton(),
                     CloseButton(),
                     Button( id="undo",
                             stock="undo",
                             ),
                     Button( id="clear",
                             stock="clear",
                             ),
                     ),
               center=( Canvas( id="canvas",
                                label="Draw Here:",
                                width=400,
                                height=400,
                                bgcolor="white",
                                callback=canvas_action,
                                ),
                        )
               )

    run()

Third version, with changes highlighted. Every tool is in place.

You may find `Pencil`'s `mouse` method a bit weird. Why not just paint the point if the mouse button is pressed? Well, here comes a limitation of the underlying system: it's almost impossible to get all response from user, so you may have skipped points and if just draw points, there may be holes in the drawing. Then we remember the last point and draw a line from it to the current point, avoiding holes. Also, since we just draw when mouse button is clicked, we forget the last point as soon as user releases the mouse button.

Index

# v04: Implementing The Undo Function

In this section we will implement `Undo` the trivial way: by remembering entire images between drawing steps. This wastes memory, but it's really easy to implement. It's done using a stack where we push images before we draw. If user want to undo, we pop the topmost image and paint the canvas with it. The code is in 04-undo.py. The differences between the previous version and the current is given at 03-04.diff.

    #!/usr/bin/env python2

    from eagle import *

    class Undo( object ):
        def __init__( self, app ):
            self.last_images = []
            self.app = app
            app.undo = self
            self.canvas = app[ "canvas" ]
            self.button = app[ "undo" ]
            self.button.set_inactive()
        # __init__()

        def push( self ):
            img = self.canvas.get_image()
            self.last_images.append( img )
            self.button.set_active()
        # push()

        def pop( self ):
            if self.last_images:
                img = self.last_images.pop()
                self.canvas.draw_image( img )

            if not self.last_images:
                self.button.set_inactive()
        # pop()
    # Undo

    class Tool( object ):
        """Interface to be implemented by tools."""

        def mouse( self, app, canvas, buttons, x, y ):
            """This tool have a user feedback using mouse on canvas."""
            pass
        # mouse()
    # Tool

    class Line( Tool ):
        def __init__( self ):
            self.first_point = None
        # __init__()

        def mouse( self, app, canvas, buttons, x, y ):
            if buttons & Canvas.MOUSE_BUTTON_1:
                if self.first_point is None:
                    self.first_point = ( x, y )
                else:
                    color = app[ "fg" ]
                    size  = app[ "size" ]
                    x0, y0 = self.first_point
                    app.undo.push()
                    canvas.draw_line( x0, y0, x, y, color, size )
                    self.first_point = None
        # mouse()
    # Line

    class Pencil( Tool ):
        def __init__( self ):
            self.last_point = None
            self.changed = False
        # __init__()

        def mouse( self, app, canvas, buttons, x, y ):
            if buttons & Canvas.MOUSE_BUTTON_1:
                if not self.changed:
                    app.undo.push()
                self.changed = True

                color = app[ "fg" ]
                size  = app[ "size" ]
                if self.last_point is not None:
                    x0, y0 = self.last_point
                else:
                    x0 = x + 1
                    y0 = y

                if size == 1:
                    canvas.draw_point( x, y, color )
                else:
                    half = size / 2
                    canvas.draw_rectangle( x-half, y-half, size, size, color, 1,
                                           color, True )
                canvas.draw_line( x0, y0, x, y, color, size )
                self.last_point = ( x, y )
            else:
                # Button 1 was released, reset last point
                self.last_point = None
                self.changed = False
        # mouse()
    # Pencil

    class Rectangle( Tool ):
        def __init__( self ):
            self.first_point = None
        # __init__()

        def mouse( self, app, canvas, buttons, x, y ):
            if buttons & Canvas.MOUSE_BUTTON_1:
                if self.first_point is None:
                    self.first_point = ( x, y )
                else:
                    fg   = app[ "fg" ]
                    bg   = app[ "bg" ]
                    size = app[ "size" ]
                    fill = app[ "rectfill" ]

                    x0, y0 = self.first_point

                    if x0 > x:
                        x0, x = x, x0
                    if y0 > y:
                        y0, y = y, y0

                    w = x - x0
                    h = y - y0

                    app.undo.push()
                    canvas.draw_rectangle( x0, y0, w, h, fg, size, bg, fill )
                    self.first_point = None
        # mouse()
    # Rectangle

    class Text( Tool ):
        def mouse( self, app, canvas, buttons, x, y ):
            if buttons & Canvas.MOUSE_BUTTON_1 and app[ "text" ]:
                text  = app[ "text" ]
                fg    = app[ "fg" ]
                bg    = app[ "bg" ]
                font  = app[ "font" ]

                if app[ "textbgtransp" ]:
                    bg = None

                app.undo.push()
                canvas.draw_text( text, x, y, fg, bg, font )
        # mouse()
    # Text

    tools = {
        "Line": Line(),
        "Pencil": Pencil(),
        "Rectangle": Rectangle(),
        "Text": Text(),
        }
    def_tool="Line"

    def canvas_action( app, canvas, buttons, x, y ):
        tool = app[ "tool" ]
        tools[ tool ].mouse( app, canvas, buttons, x, y )
    # canvas_action()

    def do_undo( app, button ):
        app.undo.pop()
    # do_undo()

    app = App( title="Paint",
               id="paint",
               statusbar=True,
               left=( Color( id="fg",
                             label="Foreground:",
                             color="black",
                             ),
                      Color( id="bg",
                             label="Background:",
                             color=( 255, 0, 0 ),
                             ),
                      Selection( id="tool",
                                 label="Tool:",
                                 options=tools.keys(),
                                 active=def_tool,
                                 ),
                      UIntSpin( id="size",
                                label="Line Size:",
                                min=1,
                                value=1,
                                ),
                      ),
               right=( Group( id="textgroup",
                              label="Text Properties:",
                              children=( Entry( id="text",
                                                label="Contents:",
                                                ),
                                         Font( id="font",
                                               label="Font:",
                                               ),
                                         CheckBox( id="textbgtransp",
                                                   label="Transparent background?",
                                                   ),
                                         ),
                              ),
                       Group( id="rectgroup",
                              label="Rectangle Properties:",
                              children=( CheckBox( id="rectfill",
                                                   label="Fill?",
                                                   ),
                                         ),
                              ),
                       ),
               top=( SaveFileButton(),
                     CloseButton(),
                     Button( id="undo",
                             stock="undo",
                             callback=do_undo,
                             ),
                     Button( id="clear",
                             stock="clear",
                             ),
                     ),
               center=( Canvas( id="canvas",
                                label="Draw Here:",
                                width=400,
                                height=400,
                                bgcolor="white",
                                callback=canvas_action,
                                ),
                        )
               )

    Undo( app )

    run()

Fourth version, with changes highlighted. Now we can undo.

`Pencil`, again, is a bit different from other tools. Why? Because we need to discover when it changed a image, but the draw step is not the small line between current and last point, but from the moment user pressed the mouse and when it releases it. So we use the state variable `changed`.

As you may note, we create the `do_undo` callback and associate it with the ``Button`( `id`=`"undo"`, ... )`. Also, don't remember to instantiate the `Undo` class! We don't need to remember any references to it, since it registers itself with the `App` and `Tool`s will use this member.

Index

# v05: Extra functionality

Now we already have a working paint program, we may add some extra functionality, like to clear (reset) the canvas to original state or even save the canvas to a file. Also, we will add a confirmation dialog before quit the application, avoiding mistakes. The code is in 05-extra.py. The differences between the previous version and the current is given at 04-05.diff.

    #!/usr/bin/env python2

    from eagle import *

    class Undo( object ):
        def __init__( self, app ):
            self.last_images = []
            self.app = app
            app.undo = self
            self.canvas = app[ "canvas" ]
            self.button = app[ "undo" ]
            self.button.set_inactive()
        # __init__()

        def push( self ):
            img = self.canvas.get_image()
            self.last_images.append( img )
            self.button.set_active()
        # push()

        def pop( self ):
            if self.last_images:
                img = self.last_images.pop()
                self.canvas.draw_image( img )

            if not self.last_images:
                self.button.set_inactive()
        # pop()
    # Undo

    class Tool( object ):
        """Interface to be implemented by tools."""

        def mouse( self, app, canvas, buttons, x, y ):
            """This tool have a user feedback using mouse on canvas."""
            pass
        # mouse()
    # Tool

    class Line( Tool ):
        def __init__( self ):
            self.first_point = None
        # __init__()

        def mouse( self, app, canvas, buttons, x, y ):
            if buttons & Canvas.MOUSE_BUTTON_1:
                if self.first_point is None:
                    self.first_point = ( x, y )
                else:
                    color = app[ "fg" ]
                    size  = app[ "size" ]
                    x0, y0 = self.first_point
                    app.undo.push()
                    canvas.draw_line( x0, y0, x, y, color, size )
                    self.first_point = None
        # mouse()
    # Line

    class Pencil( Tool ):
        def __init__( self ):
            self.last_point = None
            self.changed = False
        # __init__()

        def mouse( self, app, canvas, buttons, x, y ):
            if buttons & Canvas.MOUSE_BUTTON_1:
                if not self.changed:
                    app.undo.push()
                self.changed = True

                color = app[ "fg" ]
                size  = app[ "size" ]
                if self.last_point is not None:
                    x0, y0 = self.last_point
                else:
                    x0 = x + 1
                    y0 = y

                if size == 1:
                    canvas.draw_point( x, y, color )
                else:
                    half = size / 2
                    canvas.draw_rectangle( x-half, y-half, size, size, color, 1,
                                           color, True )
                canvas.draw_line( x0, y0, x, y, color, size )
                self.last_point = ( x, y )
            else:
                # Button 1 was released, reset last point
                self.last_point = None
                self.changed = False
        # mouse()
    # Pencil

    class Rectangle( Tool ):
        def __init__( self ):
            self.first_point = None
        # __init__()

        def mouse( self, app, canvas, buttons, x, y ):
            if buttons & Canvas.MOUSE_BUTTON_1:
                if self.first_point is None:
                    self.first_point = ( x, y )
                else:
                    fg   = app[ "fg" ]
                    bg   = app[ "bg" ]
                    size = app[ "size" ]
                    fill = app[ "rectfill" ]

                    x0, y0 = self.first_point

                    if x0 > x:
                        x0, x = x, x0
                    if y0 > y:
                        y0, y = y, y0

                    w = x - x0
                    h = y - y0

                    app.undo.push()
                    canvas.draw_rectangle( x0, y0, w, h, fg, size, bg, fill )
                    self.first_point = None
        # mouse()
    # Rectangle

    class Text( Tool ):
        def mouse( self, app, canvas, buttons, x, y ):
            if buttons & Canvas.MOUSE_BUTTON_1 and app[ "text" ]:
                text  = app[ "text" ]
                fg    = app[ "fg" ]
                bg    = app[ "bg" ]
                font  = app[ "font" ]

                if app[ "textbgtransp" ]:
                    bg = None

                app.undo.push()
                canvas.draw_text( text, x, y, fg, bg, font )
        # mouse()
    # Text

    tools = {
        "Line": Line(),
        "Pencil": Pencil(),
        "Rectangle": Rectangle(),
        "Text": Text(),
        }
    def_tool="Line"

    def canvas_action( app, canvas, buttons, x, y ):
        tool = app[ "tool" ]
        tools[ tool ].mouse( app, canvas, buttons, x, y )
    # canvas_action()

    def save( app, button, filename ):
        canvas = app[ "canvas" ]
        img = canvas.get_image()
        try:
            img.save( filename )
        except Exception, e:
            error( str( e ) )
    # save()

    def clear( app, button ):
        app.undo.push()
        app[ "canvas" ].clear()
    # clear()

    def confirm_quit( app ):
        return yesno( "Are you sure you want to close '%s'?" % app.title )
    # confirm_quit()

    def do_undo( app, button ):
        app.undo.pop()
    # do_undo()

    app = App( title="Paint",
               id="paint",
               statusbar=True,
               quit_callback=confirm_quit,
               left=( Color( id="fg",
                             label="Foreground:",
                             color="black",
                             ),
                      Color( id="bg",
                             label="Background:",
                             color=( 255, 0, 0 ),
                             ),
                      Selection( id="tool",
                                 label="Tool:",
                                 options=tools.keys(),
                                 active=def_tool,
                                 ),
                      UIntSpin( id="size",
                                label="Line Size:",
                                min=1,
                                value=1,
                                ),
                      ),
               right=( Group( id="textgroup",
                              label="Text Properties:",
                              children=( Entry( id="text",
                                                label="Contents:",
                                                ),
                                         Font( id="font",
                                               label="Font:",
                                               ),
                                         CheckBox( id="textbgtransp",
                                                   label="Transparent background?",
                                                   ),
                                         ),
                              ),
                       Group( id="rectgroup",
                              label="Rectangle Properties:",
                              children=( CheckBox( id="rectfill",
                                                   label="Fill?",
                                                   ),
                                         ),
                              ),
                       ),
               top=( SaveFileButton( callback=save ),
                     CloseButton(),
                     Button( id="undo",
                             stock="undo",
                             callback=do_undo,
                             ),
                     Button( id="clear",
                             stock="clear",
                             callback=clear,
                             ),
                     ),
               center=( Canvas( id="canvas",
                                label="Draw Here:",
                                width=400,
                                height=400,
                                bgcolor="white",
                                callback=canvas_action,
                                ),
                        )
               )

    Undo( app )

    run()

Fifth version, with changes highlighted. Now we can clear and save.

Changes are really simple, just 3 callbacks more. The First callback, `save`, get the `Image` from `Canvas` and save it to a file, being carefully with exceptions raised by this process (mainly invalid formats, given by wrong file extensions), if an exception is raised, it uses the Eagle's `error` dialog box. Other callbacks are even more straightforward, just note that `clear` also collaborates with `Undo` and that `confirm_quit` uses Eagle's `yesno` question to return to `code` if it may exit or not.

Index

# v06: Polish it!

The last section finished every feature we want in our example application, but it's not that polished. Users have no feedback on actions and may be confused "What do I need to do now?", then we will use the status bar to show these kind of information to them. Also, having Text and Rectangle properties shown while drawing a Line is a bid odd! We fix this with the introduction of `set_active` and `set_inactive` methods, that will be called when tools turn active or inactive. There we will show and remove messages from status bar, together with other relevant places. We also use these methods to hide and show groups from the right area. The code is in 06-polishing.py. The differences between the previous version and the current is given at 05-06.diff.

    #!/usr/bin/env python2

    from eagle import *

    class Undo( object ):
        def __init__( self, app ):
            self.last_images = []
            self.app = app
            app.undo = self
            self.canvas = app[ "canvas" ]
            self.button = app[ "undo" ]
            self.button.set_inactive()
        # __init__()

        def push( self ):
            img = self.canvas.get_image()
            self.last_images.append( img )
            self.button.set_active()
        # push()

        def pop( self ):
            if self.last_images:
                img = self.last_images.pop()
                self.canvas.draw_image( img )

            if not self.last_images:
                self.button.set_inactive()
        # pop()
    # Undo

    class Tool( object ):
        """Interface to be implemented by tools."""

        def set_active( self, app ):
            """This tool is now active."""
            pass
        # set_active()

        def set_inactive( self, app ):
            """This tool is now inactive. """
            pass
        # set_inactive()

        def mouse( self, app, canvas, buttons, x, y ):
            """This tool have a user feedback using mouse on canvas."""
            pass
        # mouse()
    # Tool

    class Line( Tool ):
        def __init__( self ):
            self.first_point = None
            self.message_id = None
        # __init__()

        def set_active( self, app ):
            self.message_id = app.status_message(
                "Press the left mouse button to mark the first point." )
        # set_active()

        def set_inactive( self, app ):
            if self.message_id is not None:
                app.remove_status_message( self.message_id )
        # set_inactive()

        def mouse( self, app, canvas, buttons, x, y ):
            if buttons & Canvas.MOUSE_BUTTON_1:
                if self.first_point is None:
                    self.first_point = ( x, y )
                    self.inner_message_id = app.status_message(
                        ( "First poit at ( %d, %d ). Now mark the second." ) %
                        ( x, y ) )
                else:
                    color = app[ "fg" ]
                    size  = app[ "size" ]
                    x0, y0 = self.first_point
                    app.undo.push()
                    canvas.draw_line( x0, y0, x, y, color, size )
                    self.first_point = None
                    app.remove_status_message( self.inner_message_id )
        # mouse()
    # Line

    class Pencil( Tool ):
        def __init__( self ):
            self.last_point = None
            self.message_id = None
            self.changed = False
        # __init__()

        def set_active( self, app ):
            self.message_id = app.status_message(
                "Press the left mouse button and move your mouse." )
        # set_active()

        def set_inactive( self, app ):
            if self.message_id is not None:
                app.remove_status_message( self.message_id )
        # set_inactive()

        def mouse( self, app, canvas, buttons, x, y ):
            if buttons & Canvas.MOUSE_BUTTON_1:
                if not self.changed:
                    app.undo.push()
                self.changed = True

                color = app[ "fg" ]
                size  = app[ "size" ]
                if self.last_point is not None:
                    x0, y0 = self.last_point
                else:
                    x0 = x + 1
                    y0 = y

                if size == 1:
                    canvas.draw_point( x, y, color )
                else:
                    half = size / 2
                    canvas.draw_rectangle( x-half, y-half, size, size, color, 1,
                                           color, True )
                canvas.draw_line( x0, y0, x, y, color, size )
                self.last_point = ( x, y )
            else:
                # Button 1 was released, reset last point
                self.last_point = None
                self.changed = False
        # mouse()
    # Pencil

    class Rectangle( Tool ):
        def __init__( self ):
            self.first_point = None
            self.message_id = None
        # __init__()

        def set_active( self, app ):
            app[ "rectgroup" ].show()
            self.message_id = app.status_message(
                "Press the left mouse button to mark first point." )
        # set_active()

        def set_inactive( self, app ):
            app[ "rectgroup" ].hide()
            if self.message_id is not None:
                app.remove_status_message( self.message_id )
        # set_inactive()

        def mouse( self, app, canvas, buttons, x, y ):
            if buttons & Canvas.MOUSE_BUTTON_1:
                if self.first_point is None:
                    self.first_point = ( x, y )
                    self.inner_message_id = app.status_message(
                        ( "First poit at ( %d, %d ). Now mark the second." ) %
                        ( x, y ) )
                else:
                    fg   = app[ "fg" ]
                    bg   = app[ "bg" ]
                    size = app[ "size" ]
                    fill = app[ "rectfill" ]

                    x0, y0 = self.first_point

                    if x0 > x:
                        x0, x = x, x0
                    if y0 > y:
                        y0, y = y, y0

                    w = x - x0
                    h = y - y0

                    app.undo.push()
                    canvas.draw_rectangle( x0, y0, w, h, fg, size, bg, fill )
                    self.first_point = None
                    app.remove_status_message( self.inner_message_id )
        # mouse()
    # Rectangle

    class Text( Tool ):
        def __init__( self ):
            self.message_id = None
        # __init__()

        def set_active( self, app ):
            app[ "textgroup" ].show()
            self.message_id = app.status_message(
                "Type your text in 'Contents' and press the left button " \
                "to place it." )
        # set_active()

        def set_inactive( self, app ):
            app[ "textgroup" ].hide()
            if self.message_id is not None:
                app.remove_status_message( self.message_id )
        # set_inactive()

        def mouse( self, app, canvas, buttons, x, y ):
            if buttons & Canvas.MOUSE_BUTTON_1 and app[ "text" ]:
                text  = app[ "text" ]
                fg    = app[ "fg" ]
                bg    = app[ "bg" ]
                font  = app[ "font" ]

                if app[ "textbgtransp" ]:
                    bg = None

                app.undo.push()
                canvas.draw_text( text, x, y, fg, bg, font )
        # mouse()
    # Text

    tools = {
        "Line": Line(),
        "Pencil": Pencil(),
        "Rectangle": Rectangle(),
        "Text": Text(),
        }
    def_tool="Line"

    def tool_changed( app, tool, value ):
        if tool_changed.last_tool:
            tool_changed.last_tool.set_inactive( app )

        t = tools[ value ]
        tool_changed.last_tool = t
        t.set_active( app )
    # tool_changed()
    tool_changed.last_tool = None

    def canvas_action( app, canvas, buttons, x, y ):
        tool = app[ "tool" ]
        tools[ tool ].mouse( app, canvas, buttons, x, y )
    # canvas_action()

    def save( app, button, filename ):
        canvas = app[ "canvas" ]
        img = canvas.get_image()
        try:
            img.save( filename )
        except Exception, e:
            error( str( e ) )
    # save()

    def clear( app, button ):
        app.undo.push()
        app[ "canvas" ].clear()
    # clear()

    def confirm_quit( app ):
        return yesno( "Are you sure you want to close '%s'?" % app.title )
    # confirm_quit()

    def do_undo( app, button ):
        app.undo.pop()
    # do_undo()

    app = App( title="Paint",
               id="paint",
               statusbar=True,
               quit_callback=confirm_quit,
               left=( Color( id="fg",
                             label="Foreground:",
                             color="black",
                             ),
                      Color( id="bg",
                             label="Background:",
                             color=( 255, 0, 0 ),
                             ),
                      Selection( id="tool",
                                 label="Tool:",
                                 options=tools.keys(),
                                 active=def_tool,
                                 callback=tool_changed,
                                 ),
                      UIntSpin( id="size",
                                label="Line Size:",
                                min=1,
                                value=1,
                                ),
                      ),
               right=( Group( id="textgroup",
                              label="Text Properties:",
                              children=( Entry( id="text",
                                                label="Contents:",
                                                ),
                                         Font( id="font",
                                               label="Font:",
                                               ),
                                         CheckBox( id="textbgtransp",
                                                   label="Transparent background?",
                                                   ),
                                         ),
                              ),
                       Group( id="rectgroup",
                              label="Rectangle Properties:",
                              children=( CheckBox( id="rectfill",
                                                   label="Fill?",
                                                   ),
                                         ),
                              ),
                       ),
               top=( SaveFileButton( callback=save ),
                     CloseButton(),
                     Button( id="undo",
                             stock="undo",
                             callback=do_undo,
                             ),
                     Button( id="clear",
                             stock="clear",
                             callback=clear,
                             ),
                     ),
               center=( Canvas( id="canvas",
                                label="Draw Here:",
                                width=400,
                                height=400,
                                bgcolor="white",
                                callback=canvas_action,
                                ),
                        )
               )

    for tool in tools.itervalues():
        tool.set_inactive( app )

    tool_changed( app, app.get_widget_by_id( "tool" ), def_tool )

    Undo( app )

    run()

Sixth version, with changes highlighted. Now we have messages in status bar and just relevant info in the right.

That's it folks! Now you should try to implement more tools and more options to the current tools.

Index

# About This Document

This document was written by Gustavo Sverzut Barbieri and is licensed under Creative Commons - Attribution/Share Alike.

Grab most recent version at:

http://www.gustavobarbieri.com.br/eagle/docs/tutorials/paint/