view gtk/canvas.d @ 27:f3d91579bb28

Checkpoint
author David Bryant <daveb@acres.com.au>
date Wed, 29 Jul 2009 14:11:35 +0930
parents 06c30d250c0a
children
line wrap: on
line source

module gtk.canvas;

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

private {
    import gtk.conversions;
    import tk.misc;
    import 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

class Canvas : Table, Viewport {
    this(in Layer[] layers, EventHandler event_handler, in double ppi) {
        super(3, 3, 0);

        mDamage = Rectangle.DEFAULT;

        mLayers = layers.dup;
        mEventHandler = event_handler;
        mPPI = ppi;

        /*
        writefln("Layer bounds: %s", layer_bounds);
        writefln("Canvas bounds: %s", mCanvasBounds);
        writefln("View centre: %s", mViewCentre);
        */

        // Create our child widgets and register callbacks

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

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

        mDrawingArea = new DrawingArea;
        mDrawingArea.addOnRealize(&on_realize);
        mDrawingArea.addOnConfigure(&on_configure);
        mDrawingArea.addOnExpose(&on_expose);
        mDrawingArea.addOnButtonPress(&on_button_press);
        mDrawingArea.addOnButtonRelease(&on_button_release);
        mDrawingArea.addOnKeyPress(&on_key_event);
        mDrawingArea.addOnKeyRelease(&on_key_event);
        mDrawingArea.addOnMotionNotify(&on_motion_notify);
        mDrawingArea.addOnScroll(&on_scroll);
        mDrawingArea.addOnEnterNotify(&on_enter_notify);
        mDrawingArea.addOnLeaveNotify(&on_leave_notify);
        mDrawingArea.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(mDrawingArea,
               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
        mHAdjustment = new Adjustment(0.0, 0.0, 1.0, 0.2, 0.5, 0.5);
        mHAdjustment.addOnValueChanged(&onValueChanged);
        mHScrollbar = new HScrollbar(mHAdjustment);
        mHScrollbar.setInverted(false);
        attach(mHScrollbar,
               1, 2,
               2, 3,
               AttachOptions.FILL | AttachOptions.EXPAND, AttachOptions.SHRINK,
               0, 0);

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

    override void zoom_relative(in Point pixel_datum, 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 old_model_datum = pixel_to_model(pixel_datum);
        Vector pixel_distance = model_to_pixel(old_model_datum - mViewCentre);
        mZoom = clamp_zoom(factor * mZoom);
        mViewCentre = old_model_datum - pixel_to_model(pixel_distance);

        update_adjustments;
        update_rulers;
        queueDraw;
    }

    override void pan_relative(in Vector pixel_displacement) {
        mViewCentre = mViewCentre + pixel_to_model(pixel_displacement);

        update_adjustments;
        update_rulers;
        queueDraw;
    }

    override void set_cursor(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;
        }

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

    override void damage_model(in Rectangle area) {
        mDamage = mDamage | model_to_pixel(area);
    }

    override void damage_pixel(in Rectangle area) {
        mDamage = mDamage | area;
    }

    private {

        bool on_configure(GdkEventConfigure * event, Widget widget) {
            assert(widget is mDrawingArea);

            if (!mHadConfigure) {
                const double MM_PER_INCH = 25.4;
                mZoom = 0.25 * mPPI / MM_PER_INCH;

                // Take the union of the bounds of each layer to
                // determine the canvas size

                Rectangle layer_bounds = Rectangle.DEFAULT;

                foreach (ref layer; mLayers) {
                    layer_bounds = layer_bounds | layer.bounds;
                }

                assert(layer_bounds.valid);

                mCanvasBounds = layer_bounds.moved(-layer_bounds.size).expanded(2.0 * layer_bounds.size);
                mViewCentre = mCanvasBounds.centre;

                mHadConfigure = true;
            }

            mViewSize = Vector(cast(double)event.width, cast(double)event.height);
            update_adjustments;
            update_rulers;

            //writefln("Canvas bounds: %s", mCanvasBounds);
            //writefln("View centre: %s", mViewCentre);

            return true;
        }

        bool on_expose(GdkEventExpose * event, Widget widget) {
            assert(widget is mDrawingArea);

            Drawable dr = mDrawingArea.getWindow;

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

            scope model_cr = new Context(dr);
            scope pixel_cr = new Context(dr);

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

            Rectangle model_damage = pixel_to_model(pixel_damage);

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

            model_cr.save; pixel_cr.save; {
                // Setup model context and clip

                GtkAdjustment * h_gtkAdjustment = mHAdjustment.getAdjustmentStruct;
                GtkAdjustment * v_gtkAdjustment = mVAdjustment.getAdjustmentStruct;

                model_cr.scale(mZoom, -mZoom);
                model_cr.translate(-gtk_adjustment_get_value(h_gtkAdjustment),
                                   -gtk_adjustment_get_value(v_gtkAdjustment) - gtk_adjustment_get_page_size(v_gtkAdjustment));

                rectangle(model_cr, model_damage);
                model_cr.clip;

                // Setup pixel context and clip

                pixel_cr.translate(0.0, mViewSize.y);
                pixel_cr.scale(1.0, -1.0);

                rectangle(pixel_cr, pixel_damage);
                pixel_cr.clip;

                // Fill the background

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

                // Draw each layer

                foreach(ref layer; mLayers) {
                    model_cr.save; pixel_cr.save; {
                        layer.draw(this, pixel_damage, pixel_cr, model_damage, model_cr);
                    } pixel_cr.restore; model_cr.restore;
                }
            } pixel_cr.restore; model_cr.restore;

            return true;
        }

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

            Point pixel_point = Point(event.x + 0.5, mViewSize.y - (event.y + 0.5));
            Point model_point = pixel_to_model(pixel_point);

            auto button_event = new ButtonEvent(gtk2tk_button_action(event.type),
                                                gtk2tk_button_name(event.button),
                                                pixel_point,
                                                model_point,
                                                gtk2tk_mask(event.state));

            mEventHandler.handle_button_press(this, button_event);

            process_damage;

            return true;
        }

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

            Point pixel_point = Point(event.x + 0.5, mViewSize.y - (event.y + 0.5));
            Point model_point = pixel_to_model(pixel_point);

            auto button_event = new ButtonEvent(gtk2tk_button_action(event.type),
                                                gtk2tk_button_name(event.button),
                                                pixel_point,
                                                model_point,
                                                gtk2tk_mask(event.state));

            mEventHandler.handle_button_release(this, button_event);

            process_damage;

            return true;
        }

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

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

            process_damage;

            return true;
        }

        bool on_motion_notify(GdkEventMotion * event, Widget widget) {
            assert(widget is mDrawingArea);
            //writefln("Got motion notify\n");
            gtk_widget_event(mHRuler.getWidgetStruct(), cast(GdkEvent *)event);
            gtk_widget_event(mVRuler.getWidgetStruct(), cast(GdkEvent *)event);

            Point pixel_point = Point(event.x + 0.5, mViewSize.y - (event.y + 0.5));
            Point model_point = pixel_to_model(pixel_point);

            auto motion_event = new MotionEvent(pixel_point,
                                                model_point,
                                                gtk2tk_mask(event.state));

            mEventHandler.handle_motion(this, motion_event);

            process_damage;

            return true;
        }

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

            Point pixel_point = Point(event.x + 0.5, mViewSize.y - (event.y + 0.5));
            Point model_point = pixel_to_model(pixel_point);

            auto scroll_event = new ScrollEvent(gtk2tk_direction(event.direction),
                                                pixel_point,
                                                model_point,
                                                gtk2tk_mask(event.state));

            mEventHandler.handle_scroll(this, scroll_event);

            process_damage;

            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 on_enter_notify(GdkEventCrossing * event, Widget widget) {
            assert(widget is mDrawingArea);
            //writefln("Enter %d %d %d", cast(int)event.mode, event.focus, event.state);
            // TODO
            return true;
        }

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

        void onValueChanged(Adjustment adjustment) {
            GtkAdjustment * h_gtkAdjustment = mHAdjustment.getAdjustmentStruct;
            GtkAdjustment * v_gtkAdjustment = mVAdjustment.getAdjustmentStruct;

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

            Vector model_size = pixel_to_model(mViewSize);

            //writefln("%s", view_left_bottom);
            mViewCentre = view_left_top + model_size / 2.0;
            //writefln("onValueChanged mViewCentre: %s", mViewCentre);

            update_rulers;

            queueDraw;
        }

        void update_rulers() {
            invariant Vector model_size = pixel_to_model(mViewSize);

            invariant Point view_left_bottom = mViewCentre - model_size / 2.0;
            invariant Point view_right_top = mViewCentre + model_size / 2.0;

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

            mHRuler.getRange(lower, upper, position, max_size);
            mHRuler.setRange(view_left_bottom.x,
                             view_right_top.x,
                             position,
                             mZoom * 50.0);

            mVRuler.getRange(lower, upper, position, max_size);
            mVRuler.setRange(view_right_top.y,
                             view_left_bottom.y,
                             position,
                             mZoom * 50.0);
        }

        void update_adjustments() {
            invariant Vector model_size = pixel_to_model(mViewSize);

            invariant Point view_left_bottom = mViewCentre - model_size / 2.0;
            invariant Point view_right_top = mViewCentre + model_size / 2.0;

            // Adjust the canvas size if necessary
            mCanvasBounds = Rectangle(min_extents(mCanvasBounds.min_corner, view_left_bottom),
                                      max_extents(mCanvasBounds.max_corner, view_right_top));

            // Update the adjustments

            GtkAdjustment * h_gtkAdjustment = mHAdjustment.getAdjustmentStruct;
            GtkAdjustment * v_gtkAdjustment = mVAdjustment.getAdjustmentStruct;

            gtk_adjustment_set_lower(h_gtkAdjustment, mCanvasBounds.min_corner.x);
            gtk_adjustment_set_upper(h_gtkAdjustment, mCanvasBounds.max_corner.x);
            gtk_adjustment_set_value(h_gtkAdjustment, view_left_bottom.x);
            gtk_adjustment_set_step_increment(h_gtkAdjustment, mCanvasBounds.size.x / 16.0);
            gtk_adjustment_set_page_increment(h_gtkAdjustment, mCanvasBounds.size.x / 4.0);
            gtk_adjustment_set_page_size(h_gtkAdjustment, model_size.x);

            gtk_adjustment_set_lower(v_gtkAdjustment, mCanvasBounds.min_corner.y);
            gtk_adjustment_set_upper(v_gtkAdjustment, mCanvasBounds.max_corner.y);
            gtk_adjustment_set_value(v_gtkAdjustment, view_left_bottom.y);
            gtk_adjustment_set_step_increment(v_gtkAdjustment, mCanvasBounds.size.y / 16.0);
            gtk_adjustment_set_page_increment(v_gtkAdjustment, mCanvasBounds.size.y / 4.0);
            gtk_adjustment_set_page_size(v_gtkAdjustment, model_size.y);

            mHAdjustment.changed;
            mHAdjustment.valueChanged;
            mVAdjustment.changed;
            mVAdjustment.valueChanged;
        }

        void process_damage() {
            if (mDamage.valid) {
                //writefln("Damage: %s", mDamage);
                int x, y, w, h;
                mDamage.get_quantised(x, y, w, h);
                mDrawingArea.queueDrawArea(x, cast(int)mViewSize.y - (y + h), w, h);
                mDamage = Rectangle.DEFAULT;
            }
            else {
                //writefln("No damage");
            }
        }

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

        Point model_to_pixel(in Point model) const {
            return Point.DEFAULT + mViewSize / 2.0 + mZoom * (model - mViewCentre);
        }

        Point pixel_to_model(in Point pixel) const {
            return mViewCentre + (pixel - mViewSize / 2.0 - Point.DEFAULT) / mZoom;
        }

        Vector model_to_pixel(in Vector model) const {
            return mZoom * model;
        }

        Vector pixel_to_model(in Vector pixel) const {
            return pixel / mZoom;
        }

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

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

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

        bool mHadConfigure;
        Rectangle mDamage;          // pixels

        // Model units are in millimetres
        // Screen units are in pixels
        double mZoom;               // pixels-per-model-unit (mm)
        Vector mViewSize;           // pixel: size of view window in pixels
        Point mViewCentre;          // model: where in the model is the centre of our view
        Rectangle mCanvasBounds;    // model:

        // Child widgets:
        HRuler mHRuler;
        VRuler mVRuler;
        DrawingArea mDrawingArea;
        Adjustment mHAdjustment;
        HScrollbar mHScrollbar;
        Adjustment mVAdjustment;
        VScrollbar mVScrollbar;

        // Layers:
        Layer[] mLayers;
        EventHandler mEventHandler;
        double mPPI;
    }
}