view doodle/gtk/canvas.d @ 58:c63719604adb

Beginnings of creating a rectangle...
author "David Bryant <bagnose@gmail.com>"
date Mon, 09 Aug 2010 21:43:24 +0930
parents 9960c4fbd0dd
children 6c3993f4c3eb
line wrap: on
line source

module doodle.gtk.canvas;

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

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

    import cairo.Surface;

    import std.math;
    import std.stdio;

    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;
}

// x and y run right and up respectively
//
// Model units are millimetres.
//
// _zoom         -> pixels-per-model-unit
// _viewSize     -> size of view window in pixels
// _viewCentre   -> location in model corresponding to centre of view
// _canvasBounds -> size of the virtual canvas in model coordinates
//
// User operations:
//   pan (middle click and drag)
//   zoom about a point (hold control and move scroll wheel)
//   resize the widget

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

        _damage = Rectangle.DEFAULT;

        _layers = layers.dup;
        _eventHandler = eventHandler;
        _grid = grid;
        _ppi = ppi;

        /*
           writefln("Layer bounds: %s", layerBounds);
           writefln("Canvas bounds: %s", _canvasBounds);
           writefln("View centre: %s", _viewCentre);
         */

        // 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);
        _drawingArea.addOnButtonRelease(&onButtonRelease);
        _drawingArea.addOnKeyPress(&onKeyEvent);
        _drawingArea.addOnKeyRelease(&onKeyEvent);
        _drawingArea.addOnMotionNotify(&onMotionNotify);
        _drawingArea.addOnScroll(&onScroll);
        _drawingArea.addOnEnterNotify(&onEnterNotify);
        _drawingArea.addOnLeaveNotify(&onLeaveNotify);
        _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);

        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(&onValueChanged);
        _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(&onValueChanged);
        _vScrollbar = new VScrollbar(_vAdjustment);
        _vScrollbar.setInverted(true);
        attach(_vScrollbar,
               2, 3,
               1, 2,
               AttachOptions.SHRINK,
               AttachOptions.FILL | AttachOptions.EXPAND,
               0, 0);
    }

    override void zoomRelative(in Point pixelDatum, in double factor) {
        // Work out pixel distance from current centre to datum,
        // Do the zoom, then work out the new centre that keeps the
        // pixel distance the same

        Point oldModelDatum = pixelToModel(pixelDatum);
        Vector pixelDistance = modelToPixel(oldModelDatum - _viewCentre);
        _zoom = clampZoom(factor * _zoom);
        _viewCentre = oldModelDatum - pixelToModel(pixelDistance);

        updateAdjustments;
        updateRulers;
        _grid.zoomChanged(_zoom);
        queueDraw;
    }

    override void panRelative(in Vector pixelDisplacement) {
        _viewCentre = _viewCentre + pixelToModel(pixelDisplacement);

        updateAdjustments;
        updateRulers;
        queueDraw;
    }

    override void setCursor(in Cursor cursor) {
        CursorType cursor_type;

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

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

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

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

    private {

        void update_bounds() {
        }


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

            _viewSize = Vector(cast(double)event.width, cast(double)event.height);

            Rectangle layerBounds = Rectangle.DEFAULT;

            foreach (ref layer; _layers) {
                layerBounds = layerBounds | layer.bounds;
            }

            assert(layerBounds.valid);

            Rectangle paddedLayerBounds = expand(move(layerBounds, - layerBounds.size), 2.0 * layerBounds.size);

            if (!_hadConfigure) {
                const double MM_PER_INCH = 25.4;
                _zoom = 0.25 * _ppi / MM_PER_INCH;

                _canvasBounds = paddedLayerBounds;
                _viewCentre = _canvasBounds.centre;

                _grid.zoomChanged(_zoom);

                _hadConfigure = true;
            }
            else {
                // Use configure events as an opportunity
                // to consolidate the canvas-bounds
                // XXX nasty code.
                Vector z = _viewSize / _zoom;
                Rectangle r = Rectangle(_viewCentre - z / 2.0, z);
                _canvasBounds = r | paddedLayerBounds;
            }

            updateAdjustments;
            updateRulers;

            //writefln("Canvas bounds: %s", _canvasBounds);
            //writefln("View centre: %s", _viewCentre);

            return true;
        }

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

            Drawable dr = _drawingArea.getWindow;

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

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

            Rectangle pixel_damage =
                event is null ?
                Rectangle(Point(0.0, 0.0), _viewSize) :
                Rectangle(Point(cast(double)event.area.x, _viewSize.y - cast(double)(event.area.y + event.area.height)),
                          Vector(cast(double)event.area.width, cast(double)event.area.height));

            Rectangle model_damage = pixelToModel(pixel_damage);

            //writefln("Pixel damage: %s, model damage: %s", pixel_damage, model_damage);

            modelCr.save; pixelCr.save; {
                // Setup model context and clip

                GtkAdjustment * h_gtkAdjustment = _hAdjustment.getAdjustmentStruct;
                GtkAdjustment * v_gtkAdjustment = _vAdjustment.getAdjustmentStruct;

                modelCr.scale(_zoom, -_zoom);
                modelCr.translate(-gtk_adjustment_get_value(h_gtkAdjustment),
                                  -gtk_adjustment_get_value(v_gtkAdjustment) - gtk_adjustment_get_page_size(v_gtkAdjustment));

                rectangle(modelCr, model_damage);
                modelCr.clip;

                // Setup pixel context and clip

                pixelCr.translate(0.0, _viewSize.y);
                pixelCr.scale(1.0, -1.0);

                rectangle(pixelCr, pixel_damage);
                pixelCr.clip;

                // Fill the background

                pixelCr.save; {
                    // Make the window light grey
                    pixelCr.setSourceRgba(0.9, 0.9, 0.9, 1.0);
                    rectangle(pixelCr, pixel_damage);
                    pixelCr.fill;
                } pixelCr.restore;

                // Draw each layer

                foreach(ref layer; _layers) {
                    modelCr.save; pixelCr.save; {
                        layer.draw(this, pixel_damage, pixelCr, model_damage, modelCr);
                    } pixelCr.restore; modelCr.restore;
                }
            } pixelCr.restore; modelCr.restore;

            return true;
        }

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

            Point pixelPoint = Point(event.x + 0.5, _viewSize.y - (event.y + 0.5));
            Point modelPoint = 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);
            //writefln("Got button event\n");

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

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

            _eventHandler.handleButtonRelease(this, buttonEvent);

            fixDamage;

            return true;
        }

        bool onKeyEvent(GdkEventKey * event, Widget widget) {
            assert(widget is _drawingArea);
            //writefln("Got key event\n");

            //auto key_event = new KeyEvent("",
            // mEventHandle.handle_key(key_event);

            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, _viewSize.y - (event.y + 0.5));
            Point modelPoint = pixelToModel(pixelPoint);

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

            _eventHandler.handleMotion(this, motion_event);

            fixDamage;

            return true;
        }

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

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

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

            _eventHandler.handleScroll(this, scroll_event);

            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);
            //writefln("Enter %d %d %d", cast(int)event.mode, event.focus, event.state);
            // TODO
            return true;
        }

        bool onLeaveNotify(GdkEventCrossing * event, Widget widget) {
            assert(widget is _drawingArea);
            //writefln("Leave %d %d %d", cast(int)event.mode, event.focus, event.state);
            // TODO
            return true;
        }

        void onValueChanged(Adjustment adjustment) {
            GtkAdjustment * h_gtkAdjustment = _hAdjustment.getAdjustmentStruct;
            GtkAdjustment * v_gtkAdjustment = _vAdjustment.getAdjustmentStruct;

            Point viewLeftTop = Point(gtk_adjustment_get_value(h_gtkAdjustment),
                                      gtk_adjustment_get_value(v_gtkAdjustment));

            Vector modelSize = pixelToModel(_viewSize);

            //writefln("%s", viewLeftBottom);
            _viewCentre = viewLeftTop + modelSize / 2.0;
            //writefln("onValueChanged _viewCentre: %s", _viewCentre);

            updateRulers;

            queueDraw;
        }

        void updateRulers() {
            immutable Vector modelSize = pixelToModel(_viewSize);

            immutable Point viewLeftBottom = _viewCentre - modelSize / 2.0;
            immutable Point viewRightTop = _viewCentre + modelSize / 2.0;

            // 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,
                             _zoom * 50.0);

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

        void updateAdjustments() {
            immutable Vector modelSize = pixelToModel(_viewSize);

            immutable Point viewLeftBottom = _viewCentre - modelSize / 2.0;
            immutable Point viewRightTop = _viewCentre + modelSize / 2.0;

            // Adjust the canvas size if necessary
            _canvasBounds = Rectangle(minExtents(_canvasBounds.minCorner, viewLeftBottom),
                                      maxExtents(_canvasBounds.maxCorner, viewRightTop));

            // Update the adjustments

            GtkAdjustment * h_gtkAdjustment = _hAdjustment.getAdjustmentStruct;
            GtkAdjustment * v_gtkAdjustment = _vAdjustment.getAdjustmentStruct;

            gtk_adjustment_set_lower(h_gtkAdjustment, _canvasBounds.minCorner.x);
            gtk_adjustment_set_upper(h_gtkAdjustment, _canvasBounds.maxCorner.x);
            gtk_adjustment_set_value(h_gtkAdjustment, viewLeftBottom.x);
            gtk_adjustment_set_step_increment(h_gtkAdjustment, _canvasBounds.size.x / 16.0);
            gtk_adjustment_set_page_increment(h_gtkAdjustment, _canvasBounds.size.x / 4.0);
            gtk_adjustment_set_page_size(h_gtkAdjustment, modelSize.x);

            gtk_adjustment_set_lower(v_gtkAdjustment, _canvasBounds.minCorner.y);
            gtk_adjustment_set_upper(v_gtkAdjustment, _canvasBounds.maxCorner.y);
            gtk_adjustment_set_value(v_gtkAdjustment, viewLeftBottom.y);
            gtk_adjustment_set_step_increment(v_gtkAdjustment, _canvasBounds.size.y / 16.0);
            gtk_adjustment_set_page_increment(v_gtkAdjustment, _canvasBounds.size.y / 4.0);
            gtk_adjustment_set_page_size(v_gtkAdjustment, modelSize.y);

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

        void fixDamage() {
            if (_damage.valid) {
                //writefln("Damage: %s", _damage);
                int x, y, w, h;
                _damage.getQuantised(x, y, w, h);
                _drawingArea.queueDrawArea(x, cast(int)_viewSize.y - (y + h), w, h);
                _damage = Rectangle.DEFAULT;
            }
            else {
                //writefln("No damage");
            }
        }

        double clampZoom(in double zoom) { return clamp(zoom, 0.2, 10.0); }

        Point modelToPixel(in Point model) const {
            return Point.DEFAULT + _viewSize / 2.0 + _zoom * (model - _viewCentre);
        }

        Point pixelToModel(in Point pixel) const {
            return _viewCentre + (pixel - _viewSize / 2.0 - Point.DEFAULT) / _zoom;
        }

        Vector modelToPixel(in Vector model) const {
            return _zoom * model;
        }

        Vector pixelToModel(in Vector pixel) const {
            return pixel / _zoom;
        }

        Rectangle modelToPixel(in Rectangle model) const {
            return Rectangle(modelToPixel(model.position), modelToPixel(model.size));
        }

        Rectangle pixelToModel(in Rectangle model) const {
            return Rectangle(pixelToModel(model.position), pixelToModel(model.size));
        }

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

        bool _hadConfigure;
        Rectangle _damage;          // pixels

        // Model units are in millimetres
        // Screen units are in pixels
        double _zoom;               // pixels-per-model-unit
        Vector _viewSize;           // pixel: size of view window in pixels
        Point _viewCentre;          // model: where in the model is the centre of our view
        Rectangle _canvasBounds;    // model:

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

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