view doodle/gtk/canvas.d @ 71:0f7cf6c6f206

Reimplemented gtk.canvas in terms of tk.pixel_model but needs a lot of consolidation.
author "David Bryant <bagnose@gmail.com>"
date Sat, 14 Aug 2010 20:48:41 +0930
parents 0e61702c6ea6
children 5cc2de64f6d0
line wrap: on
line source

module doodle.gtk.canvas;

public {
    import doodle.dia.icanvas;
    import doodle.tk.events;
}

private {
    import doodle.core.misc;
    import doodle.core.logging;
    import doodle.cairo.routines;
    import doodle.gtk.conversions;
    import doodle.tk.pixel_model;

    import cairo.Surface;

    import gtk.Widget;
    import gtk.Toolbar;
    import gtk.Table;
    import gtk.HRuler;
    import gtk.VRuler;
    import gtk.Range;
    import gtk.HScrollbar;
    import gtk.VScrollbar;
    import gtk.DrawingArea;
    import gtk.Adjustment;

    import gdk.Drawable;

    import gtkc.gtk;
    import gtkc.gtktypes;
    //import gtkc.gdktypes;

    import std.math;
    import std.stdio;

    import core.stdc.string : strlen;
}

final class Canvas : Table, private IViewport {
    this(in Layer[] layers, IEventHandler eventHandler, IGrid grid, in double pixelsPerMillimetre) {
        super(3, 3, 0);

        _damage = Rectangle.DEFAULT;

        _layers = layers.dup;
        _eventHandler = eventHandler;
        _grid = grid;
        _pixelsPerMillimetre = pixelsPerMillimetre;

        // Create our child widgets and register callbacks

        _hRuler = new HRuler;
        attach(_hRuler,
               1, 2,
               0, 1,
               AttachOptions.FILL | AttachOptions.EXPAND, AttachOptions.SHRINK,
               0, 0);
        _hRuler.setMetric(MetricType.PIXELS);

        _vRuler = new VRuler;
        attach(_vRuler,
               0, 1,
               1, 2,
               AttachOptions.SHRINK, AttachOptions.FILL | AttachOptions.EXPAND,
               0, 0);
        _vRuler.setMetric(MetricType.PIXELS);

        _drawingArea = new DrawingArea;
        _drawingArea.addOnRealize(&onRealize);
        _drawingArea.addOnConfigure(&onConfigure);
        _drawingArea.addOnExpose(&onExpose);
        _drawingArea.addOnButtonPress(&onButtonPress);              // FIXME merge delegate with next
        _drawingArea.addOnButtonRelease(&onButtonRelease);
        _drawingArea.addOnKeyPress(&onKeyPressEvent);               // FIXME merge delegate with next
        _drawingArea.addOnKeyRelease(&onKeyReleaseEvent);
        _drawingArea.addOnMotionNotify(&onMotionNotify);
        _drawingArea.addOnScroll(&onScroll);
        _drawingArea.addOnEnterNotify(&onEnterNotify);              // FIXME merge delegate with next
        _drawingArea.addOnLeaveNotify(&onLeaveNotify);

        _drawingArea.addOnFocusIn(&onFocusIn);
        _drawingArea.addOnFocusOut(&onFocusOut);
        _drawingArea.addOnMoveFocus(&onMoveFocus);
        _drawingArea.addOnGrabBroken(&onGrabBroken);
        _drawingArea.addOnGrabFocus(&onGrabFocus);
        _drawingArea.addOnGrabNotify(&onGrabNotify);
        // addOnPopupMenu
        // addOnQueryTooltip
        // addOnSelection*
        _drawingArea.setEvents(EventMask.EXPOSURE_MASK |
                               EventMask.POINTER_MOTION_MASK |
                               EventMask.POINTER_MOTION_HINT_MASK |
                               EventMask.BUTTON_MOTION_MASK |
                               EventMask.BUTTON_PRESS_MASK |
                               EventMask.BUTTON_RELEASE_MASK |
                               EventMask.KEY_PRESS_MASK |
                               EventMask.KEY_RELEASE_MASK |
                               EventMask.ENTER_NOTIFY_MASK |
                               EventMask.LEAVE_NOTIFY_MASK |
                               EventMask.FOCUS_CHANGE_MASK |
                               EventMask.SCROLL_MASK);

        _drawingArea.setCanFocus(true);

        attach(_drawingArea,
               1, 2,
               1, 2, 
               AttachOptions.FILL | AttachOptions.EXPAND, AttachOptions.FILL | AttachOptions.EXPAND,
               0, 0);

        // value, lower, upper, step-inc, page-inc, page-size
        // Give the adjustments dummy values until we receive a configure
        _hAdjustment = new Adjustment(0.0, 0.0, 1.0, 0.2, 0.5, 0.5);
        _hAdjustment.addOnValueChanged(&onAdjustmentValueChanged);
        _hScrollbar = new HScrollbar(_hAdjustment);
        _hScrollbar.setInverted(false);
        attach(_hScrollbar,
               1, 2,
               2, 3,
               AttachOptions.FILL | AttachOptions.EXPAND,
               AttachOptions.SHRINK,
               0, 0);

        _vAdjustment = new Adjustment(0.0, 0.0, 1.0, 0.2, 0.5, 0.5);
        _vAdjustment.addOnValueChanged(&onAdjustmentValueChanged);
        _vScrollbar = new VScrollbar(_vAdjustment);
        _vScrollbar.setInverted(true);
        attach(_vScrollbar,
               2, 3,
               1, 2,
               AttachOptions.SHRINK,
               AttachOptions.FILL | AttachOptions.EXPAND,
               0, 0);
    }

    protected {         // XXX the compiler complains about unimplemented methods if this is private

        // IViewport overrides:

        void zoomRelative(in Point pixelDatum, in double factor) {
            _pixelModel.zoomRelative(factor, pixelDatum);

            consolidateBounds;

            updateAdjustments;
            updateRulers;
            _grid.zoomChanged(_pixelModel.zoom);
            queueDraw;
        }

        void panRelative(in Vector pixelDisplacement) {
            _pixelModel.panRelativePixel(pixelDisplacement);

            consolidateBounds;

            updateAdjustments;
            updateRulers;
            queueDraw;
        }

        void setCursor(in Cursor cursor) {
            CursorType cursorType;

            switch (cursor) {
            case Cursor.DEFAULT:
                cursorType = CursorType.ARROW;
                break;
            case Cursor.HAND:
                cursorType = CursorType.HAND1;
                break;
            case Cursor.CROSSHAIR:
                cursorType = CursorType.CROSSHAIR;
                break;
            case Cursor.PENCIL:
                cursorType = CursorType.PENCIL;
                break;
            default:
                assert(0);
            }

            _drawingArea.setCursor(new gdk.Cursor.Cursor(cursorType));
        }

        void damageModel(in Rectangle area) {
            _damage = _damage | _pixelModel.modelToPixel(area);
        }

        void damagePixel(in Rectangle area) {
            _damage = _damage | area;
        }
    }

    private {

        Rectangle layerBounds() {
            Rectangle bounds = Rectangle.DEFAULT;
            foreach (layer; _layers) { bounds = bounds | layer.bounds; }
            assert(bounds.valid);
            return bounds;
        }

        void initialiseBounds(in Rectangle viewBounds) {
            Rectangle lb = layerBounds;

            // FIXME use a function that grows a rectangle about its centre
            // and change 2.0 to a class-level constant
            Rectangle paddedLayerBounds = expand(move(lb, - lb.size), 2.0 * lb.size);

            _pixelModel = new PixelModel(0.25 * _pixelsPerMillimetre, paddedLayerBounds, viewBounds);

            _grid.zoomChanged(_pixelModel.zoom);

            updateAdjustments;
            updateRulers;
        }

        void consolidateBounds() {
            Rectangle lb = layerBounds;

            // FIXME likewise as above
            Rectangle paddedLayerBounds = expand(move(lb, - lb.size), 2.0 * lb.size);

            _pixelModel.consolidateCanvasBounds(paddedLayerBounds);

            updateAdjustments;
            updateRulers;
        }

        bool onConfigure(GdkEventConfigure * event, Widget widget) {
            assert(widget is _drawingArea);

            Rectangle viewBounds = Rectangle(Point(0.0, 0.0), Vector(cast(double)event.width, cast(double)event.height));

            if (_pixelModel is null) {
                initialiseBounds(viewBounds);
            }
            else {
                consolidateBounds;
            }

            return true;
        }

        bool onExpose(GdkEventExpose * event, Widget widget) {
            assert(widget is _drawingArea);

            Drawable dr = _drawingArea.getWindow;

            int width, height;
            dr.getSize(width, height);
            //trace("Got expose %dx%d\n", width, height);

            scope modelCr = new Context(dr);
            scope pixelCr = new Context(dr);

            Rectangle pixelDamage =
                event is null ? _pixelModel.viewBounds :
                // FIXME next line sucks
                Rectangle(_pixelModel.viewBounds.position + Vector(cast(double)event.area.x, _pixelModel.viewBounds.h - cast(double)(event.area.y + event.area.height)),
                          Vector(cast(double)event.area.width, cast(double)event.area.height));

            Rectangle modelDamage = _pixelModel.pixelToModel(pixelDamage);

            //trace("Pixel damage: %s, model damage: %s", pixelDamage, modelDamage);

            modelCr.save; pixelCr.save; {
                {
                    // Setup model context and clip
                    modelCr.translate(0.0, _pixelModel.viewBounds.h);
                    modelCr.scale(_pixelModel.zoom, -_pixelModel.zoom);

                    immutable Point viewLeftBottom = _pixelModel.pixelToModel(Point(0.0, 0.0));
                    modelCr.translate(-viewLeftBottom.x, -viewLeftBottom.y);

                    rectangle(modelCr, modelDamage);
                    modelCr.clip;
                }

                {
                    // Setup pixel context and clip
                    pixelCr.translate(0.0, _pixelModel.viewBounds.h);
                    pixelCr.scale(1.0, -1.0);

                    rectangle(pixelCr, pixelDamage);
                    pixelCr.clip;
                }

                pixelCr.save; {
                    // Fill the background with light grey
                    pixelCr.setSourceRgba(0.9, 0.9, 0.9, 1.0);
                    rectangle(pixelCr, pixelDamage);
                    pixelCr.fill;
                } pixelCr.restore;

                // Draw each layer
                foreach(layer; _layers) {
                    layer.draw(this, pixelDamage, pixelCr, modelDamage, modelCr);
                }
            } pixelCr.restore; modelCr.restore;

            return true;
        }

        bool onButtonPress(GdkEventButton * event, Widget widget) {
            assert(widget is _drawingArea);
            //trace("Got button event\n");

            Point pixelPoint = Point(event.x + 0.5, _pixelModel.viewBounds.h - (event.y + 0.5));
            Point modelPoint = _pixelModel.pixelToModel(pixelPoint);

            auto buttonEvent = new ButtonEvent(gtk2tkButtonAction(event.type),
                                               gtk2tkButtonName(event.button),
                                               pixelPoint,
                                               modelPoint,
                                               gtk2tkMask(event.state));

            _eventHandler.handleButtonPress(this, buttonEvent);

            fixDamage;

            return true;
        }

        bool onButtonRelease(GdkEventButton * event, Widget widget) {
            assert(widget is _drawingArea);

            Point pixelPoint = Point(event.x + 0.5, _pixelModel.viewBounds.h - (event.y + 0.5));
            Point modelPoint = _pixelModel.pixelToModel(pixelPoint);

            auto buttonEvent = new ButtonEvent(gtk2tkButtonAction(event.type),
                                               gtk2tkButtonName(event.button),
                                               pixelPoint,
                                               modelPoint,
                                               gtk2tkMask(event.state));

            _eventHandler.handleButtonRelease(this, buttonEvent);

            fixDamage;

            return true;
        }

        /*
           public struct GdkEventKey {
           GdkEventType type;
           GdkWindow *window;
           byte sendEvent;
           uint time;
           uint state;
           uint keyval;
           int length;
           char *string;
           ushort hardwareKeycode;
           ubyte group;
           uint bitfield0;
           uint isModifier : 1;
        }
         */
        bool onKeyPressEvent(GdkEventKey * event, Widget widget) {
            assert(widget is _drawingArea);

            auto keyEvent = new KeyEvent(event.string[0..strlen(event.string)].idup,
                                         event.keyval,
                                         gtk2tkMask(event.state));
            message("Got key press %s", keyEvent);
            _eventHandler.handleKeyPress(this, keyEvent);

            fixDamage;

            return true;
        }

        bool onKeyReleaseEvent(GdkEventKey * event, Widget widget) {
            assert(widget is _drawingArea);

            auto keyEvent = new KeyEvent(event.string[0..strlen(event.string)].idup,
                                         event.keyval,
                                         gtk2tkMask(event.state));
            //message("Got key release %s", keyEvent);
            _eventHandler.handleKeyRelease(this, keyEvent);

            fixDamage;

            return true;
        }

        bool onMotionNotify(GdkEventMotion * event, Widget widget) {
            assert(widget is _drawingArea);
            //writefln("Got motion notify\n");
            gtk_widget_event(_hRuler.getWidgetStruct(), cast(GdkEvent *)event);
            gtk_widget_event(_vRuler.getWidgetStruct(), cast(GdkEvent *)event);

            Point pixelPoint = Point(event.x + 0.5, _pixelModel.viewBounds.h - (event.y + 0.5));
            Point modelPoint = _pixelModel.pixelToModel(pixelPoint);

            auto motionEvent = new MotionEvent(pixelPoint,
                                               modelPoint,
                                               gtk2tkMask(event.state));

            _eventHandler.handleMotion(this, motionEvent);

            fixDamage;

            return true;
        }

        bool onScroll(GdkEventScroll * event, Widget widget) {
            assert(widget is _drawingArea);
            //writefln("Got scroll\n");

            Point pixelPoint = Point(event.x + 0.5, _pixelModel.viewBounds.h - (event.y + 0.5));
            Point modelPoint = _pixelModel.pixelToModel(pixelPoint);

            auto scrollEvent = new ScrollEvent(gtk2tkDirection(event.direction),
                                               pixelPoint,
                                               modelPoint,
                                               gtk2tkMask(event.state));

            _eventHandler.handleScroll(this, scrollEvent);

            fixDamage;

            return true;
        }

        /*
           public enum GdkCrossingMode {       
           NORMAL,
           GRAB,
           UNGRAB,
           GTK_GRAB,
           GTK_UNGRAB,
           STATE_CHANGED
           }

           public struct GdkEventCrossing {
           GdkEventType type;
           GdkWindow *window;
           byte sendEvent;
           GdkWindow *subwindow;
           uint time;
           double x;
           double y;
           double xRoot;
           double yRoot;
           GdkCrossingMode mode;
           GdkNotifyType detail;
           int focus;
           uint state;
           }
         */

        bool onEnterNotify(GdkEventCrossing * event, Widget widget) {
            assert(widget is _drawingArea);

            Point pixelPoint = Point(event.x + 0.5, _pixelModel.viewBounds.h - (event.y + 0.5));
            Point modelPoint = _pixelModel.pixelToModel(pixelPoint);

            auto crossingEvent = new CrossingEvent(gtk2tkCrossingMode(event.mode),
                                                   pixelPoint,
                                                   modelPoint,
                                                   gtk2tkMask(event.state));

            _eventHandler.handleEnter(this, crossingEvent);

            fixDamage;

            //message("Enter %d %d %d", cast(int)event.mode, event.focus, event.state);

            return true;
        }

        bool onLeaveNotify(GdkEventCrossing * event, Widget widget) {
            assert(widget is _drawingArea);

            Point pixelPoint = Point(event.x + 0.5, _pixelModel.viewBounds.h - (event.y + 0.5));
            Point modelPoint = _pixelModel.pixelToModel(pixelPoint);

            auto crossingEvent = new CrossingEvent(gtk2tkCrossingMode(event.mode),
                                                   pixelPoint,
                                                   modelPoint,
                                                   gtk2tkMask(event.state));

            _eventHandler.handleLeave(this, crossingEvent);

            fixDamage;

            //message("Leave %d %d %d", cast(int)event.mode, event.focus, event.state);

            return true;
        }

        /*
           public struct GdkEventFocus {
           GdkEventType type;
           GdkWindow *window;
           byte sendEvent;
           short inn;
           }
         */
        bool onFocusIn(GdkEventFocus * event, Widget widget) {
            trace("onFocusIn");
            return true;
        }

        bool onFocusOut(GdkEventFocus * event, Widget widget) {
            trace("onFocusOut");
            return true;
        }

        void onMoveFocus(GtkDirectionType direction, Widget widget) {
            trace("onMoveFocus");
        }

        bool onGrabBroken(gdk.Event.Event event, Widget widget) {
            trace("onGrabBroken");
            return true;
        }

        void onGrabFocus(Widget widget) {
            //trace("onGrabFocus");
        }

        void onGrabNotify(gboolean what, Widget widget){
            trace("onGrabNotify");
        }

        void onAdjustmentValueChanged(Adjustment adjustment) {
            GtkAdjustment * hGtkAdjustment = _hAdjustment.getAdjustmentStruct;
            GtkAdjustment * vGtkAdjustment = _vAdjustment.getAdjustmentStruct;

            Point oldViewLeftBottom = _pixelModel.pixelToModel(Point(0.0, 0.0));
            Point newViewLeftBottom = Point(gtk_adjustment_get_value(hGtkAdjustment),
                                            gtk_adjustment_get_value(vGtkAdjustment));

            _pixelModel.panRelativeModel(newViewLeftBottom - oldViewLeftBottom);

            updateRulers;
            queueDraw;
        }

        void updateRulers() {
            immutable Point viewLeftBottom = _pixelModel.pixelToModel(Point(0.0, 0.0));
            immutable Point viewRightTop = _pixelModel.pixelToModel(_pixelModel.viewBounds.corner1);

            // Define these just to obtain the position
            // below and we can preserve it
            double lower, upper, position, maxSize;

            _hRuler.getRange(lower, upper, position, maxSize);
            _hRuler.setRange(viewLeftBottom.x,
                             viewRightTop.x,
                             position,
                             _pixelModel.zoom * 50.0);

            _vRuler.getRange(lower, upper, position, maxSize);
            _vRuler.setRange(viewRightTop.y,
                             viewLeftBottom.y,
                             position,
                             _pixelModel.zoom * 50.0);
        }

        void updateAdjustments() {
            immutable Point viewLeftBottom = _pixelModel.pixelToModel(Point(0.0, 0.0));
            immutable Point viewRightTop = _pixelModel.pixelToModel(_pixelModel.viewBounds.corner1);

            // Adjust the canvas size if necessary FIXME is this required??
            _pixelModel.canvasAccommodate(Rectangle(viewLeftBottom, viewRightTop));

            // FIXME
            Rectangle modelSize = _pixelModel.pixelToModel(_pixelModel.viewBounds);

            // Update the adjustments

            GtkAdjustment * hGtkAdjustment = _hAdjustment.getAdjustmentStruct;
            GtkAdjustment * vGtkAdjustment = _vAdjustment.getAdjustmentStruct;

            gtk_adjustment_set_lower(hGtkAdjustment, _pixelModel.canvasBounds.x0);
            gtk_adjustment_set_upper(hGtkAdjustment, _pixelModel.canvasBounds.x1);
            gtk_adjustment_set_value(hGtkAdjustment, viewLeftBottom.x);
            gtk_adjustment_set_step_increment(hGtkAdjustment, _pixelModel.canvasBounds.w / 16.0);
            gtk_adjustment_set_page_increment(hGtkAdjustment, _pixelModel.canvasBounds.w / 4.0);
            gtk_adjustment_set_page_size(hGtkAdjustment, modelSize.w);

            gtk_adjustment_set_lower(vGtkAdjustment, _pixelModel.canvasBounds.y0);
            gtk_adjustment_set_upper(vGtkAdjustment, _pixelModel.canvasBounds.y1);
            gtk_adjustment_set_value(vGtkAdjustment, viewLeftBottom.y);
            gtk_adjustment_set_step_increment(vGtkAdjustment, _pixelModel.canvasBounds.h / 16.0);
            gtk_adjustment_set_page_increment(vGtkAdjustment, _pixelModel.canvasBounds.h / 4.0);
            gtk_adjustment_set_page_size(vGtkAdjustment, modelSize.h);

            _hAdjustment.changed;
            _hAdjustment.valueChanged;
            _vAdjustment.changed;
            _vAdjustment.valueChanged;
        }

        void fixDamage() {
            if (_damage.valid) {
                int x, y, w, h;
                _damage.getQuantised(x, y, w, h);
                _drawingArea.queueDrawArea(x, cast(int)_pixelModel.viewBounds.h - (y + h), w, h);
                _damage = Rectangle.DEFAULT;
            }
        }

        void onRealize(Widget widget) {
            assert(widget is _drawingArea);
            //writefln("Got realize\n");
            _drawingArea.grabFocus();
        }

        Rectangle _damage;          // pixels
        PixelModel _pixelModel;

        // Child widgets:
        HRuler _hRuler;
        VRuler _vRuler;
        DrawingArea _drawingArea;
        Adjustment _hAdjustment;
        HScrollbar _hScrollbar;
        Adjustment _vAdjustment;
        VScrollbar _vScrollbar;

        Layer[] _layers;
        IEventHandler _eventHandler;
        IGrid _grid;
        double _pixelsPerMillimetre;
    }
}