Blob Blame History Raw
// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-

const UI = imports.testcommon.ui;

const { Clutter, GObject, Gtk, Shell, St } = imports.gi;

// This is an interactive test of the sizing behavior of StScrollView. It
// may be interesting in the future to split out the two classes at the
// top into utility classes for testing the sizing behavior of other
// containers and actors.

/****************************************************************************/

// FlowedBoxes: This is a simple actor that demonstrates an interesting
// height-for-width behavior. A set of boxes of different sizes are line-wrapped
// horizontally with the minimum horizontal size being determined by the
// largest box. It would be easy to extend this to allow doing vertical
// wrapping instead, if you wanted to see just how badly our width-for-height
// implementation is or work on fixing it.

const BOX_HEIGHT = 20;
const BOX_WIDTHS = [
    10, 40, 100, 20, 60, 30, 70, 10, 20, 200, 50, 70, 90, 20, 40,
    10, 40, 100, 20, 60, 30, 70, 10, 20, 200, 50, 70, 90, 20, 40,
    10, 40, 100, 20, 60, 30, 70, 10, 20, 200, 50, 70, 90, 20, 40,
    10, 40, 100, 20, 60, 30, 70, 10, 20, 200, 50, 70, 90, 20, 40,
];

const SPACING = 10;

var FlowedBoxes = GObject.registerClass(
class FlowedBoxes extends St.Widget {
    _init() {
        super._init();

	for (let i = 0; i < BOX_WIDTHS.length; i++) {
	    let child = new St.Bin({ width: BOX_WIDTHS[i], height: BOX_HEIGHT,
	                             style: 'border: 1px solid #444444; background: #00aa44' })
	    this.add_actor(child);
	}
    }

    vfunc_get_preferred_width(forHeight) {
        let children = this.get_children();

	let maxMinWidth = 0;
	let totalNaturalWidth = 0;

	for (let i = 0; i < children.length; i++) {
	    let child = children[i];
	    let [minWidth, naturalWidth] = child.get_preferred_width(-1);
	    maxMinWidth = Math.max(maxMinWidth, minWidth);
	    if (i != 0)
		totalNaturalWidth += SPACING;
	    totalNaturalWidth += naturalWidth;
	}

        return [maxMinWidth, totalNaturalWidth];
    }

    _layoutChildren(forWidth, callback) {
        let children = this.get_children();

	let x = 0;
	let y = 0;
	for (let i = 0; i < children.length; i++) {
	    let child = children[i];
	    let [minWidth, naturalWidth] = child.get_preferred_width(-1);
	    let [minHeight, naturalHeight] = child.get_preferred_height(naturalWidth);

	    let x1 = x;
	    if (x != 0)
		x1 += SPACING;
	    let x2 = x1 + naturalWidth;

	    if (x2 > forWidth) {
		if (x > 0) {
	            x1 = 0;
		    y += BOX_HEIGHT + SPACING;
                }

                x2 = naturalWidth;
	    }

	    callback(child, x1, y, x2, y + naturalHeight);
	    x = x2;
	}

    }

    vfunc_get_preferred_height(forWidth) {
	let height = 0;
	this._layoutChildren(forWidth,
           function(child, x1, y1, x2, y2) {
	       height = Math.max(height, y2);
	   });

        return [height, height];
    }

    vfunc_allocate(box, flags) {
        this.set_allocation(box, flags);

	this._layoutChildren(box.x2 - box.x1,
           function(child, x1, y1, x2, y2) {
	       child.allocate(new Clutter.ActorBox({ x1: x1, y1: y1, x2: x2, y2: y2 }),
			      flags);
	   });
    }
});

/****************************************************************************/

// SizingIllustrator: this is a container that allows interactively exploring
// the sizing behavior of the child. Lines are drawn to indicate the minimum
// and natural size of the child, and a drag handle allows the user to resize
// the child interactively and see how that affects it.
//
// This is currently only written for the case where the child is height-for-width

var SizingIllustrator = GObject.registerClass(
class SizingIllustrator extends St.Widget {
    _init() {
        super._init();

	this.minWidthLine = new St.Bin({ style: 'background: red' });
        this.add_actor(this.minWidthLine);
	this.minHeightLine = new St.Bin({ style: 'background: red' });
        this.add_actor(this.minHeightLine);

	this.naturalWidthLine = new St.Bin({ style: 'background: #4444ff' });
        this.add_actor(this.naturalWidthLine);
	this.naturalHeightLine = new St.Bin({ style: 'background: #4444ff' });
        this.add_actor(this.naturalHeightLine);

	this.currentWidthLine = new St.Bin({ style: 'background: #aaaaaa' });
        this.add_actor(this.currentWidthLine);
	this.currentHeightLine = new St.Bin({ style: 'background: #aaaaaa' });
        this.add_actor(this.currentHeightLine);

	this.handle = new St.Bin({ style: 'background: yellow; border: 1px solid black;',
				   reactive: true });
	this.handle.connect('button-press-event', this._handlePressed.bind(this));
	this.handle.connect('button-release-event', this._handleReleased.bind(this));
	this.handle.connect('motion-event', this._handleMotion.bind(this));
        this.add_actor(this.handle);

	this._inDrag = false;

	this.width = 300;
	this.height = 300;
    }

    add(child) {
        this.child = child;
        this.add_child(child);
        this.child.lower_bottom();
    }

    vfunc_get_preferred_width(forHeight) {
        let children = this.get_children();
	for (let i = 0; i < children.length; i++) {
	    let child = children[i];
	    let [minWidth, naturalWidth] = child.get_preferred_width(-1);
	    if (child == this.child) {
		this.minWidth = minWidth;
		this.naturalWidth = naturalWidth;
	    }
	}

        return [0, 400];
    }

    vfunc_get_preferred_height(forWidth) {
        let children = this.get_children();
	for (let i = 0; i < children.length; i++) {
	    let child = children[i];
	    if (child == this.child) {
		[this.minHeight, this.naturalHeight] = child.get_preferred_height(this.width);
	    } else {
		let [minWidth, naturalWidth] = child.get_preferred_height(naturalWidth);
	    }
	}

        return [0, 400];
    }

    vfunc_allocate(box, flags) {
        this.set_allocation(box, flags);

        box = this.get_theme_node().get_content_box(box);

	let allocWidth = box.x2 - box.x1;
	let allocHeight = box.y2 - box.y1;

	function alloc(child, x1, y1, x2, y2) {
	    child.allocate(new Clutter.ActorBox({ x1: x1, y1: y1, x2: x2, y2: y2 }),
	                   flags);
	}

	alloc(this.child, 0, 0, this.width, this.height);
	alloc(this.minWidthLine, this.minWidth, 0, this.minWidth + 1, allocHeight);
	alloc(this.naturalWidthLine, this.naturalWidth, 0, this.naturalWidth + 1, allocHeight);
	alloc(this.currentWidthLine, this.width, 0, this.width + 1, allocHeight);
	alloc(this.minHeightLine, 0, this.minHeight, allocWidth, this.minHeight + 1);
	alloc(this.naturalHeightLine, 0, this.naturalHeight, allocWidth, this.naturalHeight + 1);
	alloc(this.currentHeightLine, 0, this.height, allocWidth, this.height + 1);
	alloc(this.handle, this.width, this.height, this.width + 10, this.height + 10);
    }

    _handlePressed(handle, event) {
	if (event.get_button() == 1) {
	    this._inDrag = true;
	    let [handleX, handleY] = handle.get_transformed_position();
	    let [x, y] = event.get_coords();
	    this._dragX = x - handleX;
	    this._dragY = y - handleY;
	    Clutter.grab_pointer(handle);
	}
    }

    _handleReleased(handle, event) {
	if (event.get_button() == 1) {
	    this._inDrag = false;
	    Clutter.ungrab_pointer(handle);
	}
    }

    _handleMotion(handle, event) {
	if (this._inDrag) {
	    let [x, y] = event.get_coords();
            let [actorX, actorY] = this.get_transformed_position();
	    this.width = x - this._dragX - actorX;
	    this.height = y - this._dragY - actorY;
            this.queue_relayout();
	}
    }
});

/****************************************************************************/

function test() {
    let stage = new Clutter.Stage({ width: 600, height: 600 });
    UI.init(stage);

    let mainBox = new St.BoxLayout({ width: stage.width,
				     height: stage.height,
				     vertical: true,
			             style: 'padding: 10px;'
                                     + 'spacing: 5px;'
                                     + 'font: 16px sans-serif;'
                                     + 'background: black;'
                                     + 'color: white;' });
    stage.add_actor(mainBox);

    const DOCS = 'Red lines represent minimum size, blue lines natural size. Drag yellow handle to resize ScrollView. Click on options to change.';

    let docsLabel = new St.Label({ text: DOCS });
    docsLabel.clutter_text.line_wrap = true;
    mainBox.add(docsLabel);

    let bin = new St.Bin({ x_fill: true, y_fill: true, style: 'border: 2px solid #666666;' });
    mainBox.add(bin, { x_fill: true, y_fill: true, expand: true });

    let illustrator = new SizingIllustrator();
    bin.add_actor(illustrator);

    let scrollView = new St.ScrollView();
    illustrator.add(scrollView);

    let box = new St.BoxLayout({ vertical: true });
    scrollView.add_actor(box);

    let flowedBoxes = new FlowedBoxes();
    box.add(flowedBoxes, { expand: false, x_fill: true, y_fill: true });

    let policyBox = new St.BoxLayout({ vertical: false });
    mainBox.add(policyBox);

    policyBox.add(new St.Label({ text: 'Horizontal Policy: ' }));
    let hpolicy = new St.Button({ label: 'AUTOMATIC', style: 'text-decoration: underline; color: #4444ff;' });
    policyBox.add(hpolicy);

    let spacer = new St.Bin();
    policyBox.add(spacer, { expand: true });

    policyBox.add(new St.Label({ text: 'Vertical Policy: '}));
    let vpolicy = new St.Button({ label: 'AUTOMATIC', style: 'text-decoration: underline; color: #4444ff;' });
    policyBox.add(vpolicy);

    function togglePolicy(button) {
        switch(button.label) {
        case 'AUTOMATIC':
	    button.label = 'ALWAYS';
	    break;
        case 'ALWAYS':
	    button.label = 'NEVER';
	    break;
        case 'NEVER':
	    button.label = 'EXTERNAL';
	    break;
        case 'EXTERNAL':
	    button.label = 'AUTOMATIC';
	    break;
        }
        scrollView.set_policy(Gtk.PolicyType[hpolicy.label], Gtk.PolicyType[vpolicy.label]);
    }

    hpolicy.connect('clicked', () => { togglePolicy(hpolicy); });
    vpolicy.connect('clicked', () => { togglePolicy(vpolicy); });

    let fadeBox = new St.BoxLayout({ vertical: false });
    mainBox.add(fadeBox);

    spacer = new St.Bin();
    fadeBox.add(spacer, { expand: true });

    fadeBox.add(new St.Label({ text: 'Padding: '}));
    let paddingButton = new St.Button({ label: 'No', style: 'text-decoration: underline; color: #4444ff;padding-right:3px;' });
    fadeBox.add(paddingButton);

    fadeBox.add(new St.Label({ text: 'Borders: '}));
    let borderButton = new St.Button({ label: 'No', style: 'text-decoration: underline; color: #4444ff;padding-right:3px;' });
    fadeBox.add(borderButton);

    fadeBox.add(new St.Label({ text: 'Vertical Fade: '}));
    let vfade = new St.Button({ label: 'No', style: 'text-decoration: underline; color: #4444ff;' });
    fadeBox.add(vfade);

    fadeBox.add(new St.Label({ text: 'Overlay scrollbars: '}));
    let overlay = new St.Button({ label: 'No', style: 'text-decoration: underline; color: #4444ff;' });
    fadeBox.add(overlay);

    function togglePadding(button) {
        switch(button.label) {
        case 'No':
	    button.label = 'Yes';
	    break;
        case 'Yes':
	    button.label = 'No';
	    break;
        }
        if (scrollView.style == null)
            scrollView.style = (button.label == 'Yes' ? 'padding: 10px;' : 'padding: 0;');
        else
            scrollView.style += (button.label == 'Yes' ? 'padding: 10px;' : 'padding: 0;');
    }

    paddingButton.connect('clicked', () => { togglePadding(paddingButton); });

    function toggleBorders(button) {
        switch(button.label) {
        case 'No':
	    button.label = 'Yes';
	    break;
        case 'Yes':
	    button.label = 'No';
	    break;
        }
        if (scrollView.style == null)
            scrollView.style = (button.label == 'Yes' ? 'border: 2px solid red;' : 'border: 0;');
        else
            scrollView.style += (button.label == 'Yes' ? 'border: 2px solid red;' : 'border: 0;');
    }

    borderButton.connect('clicked', () => { toggleBorders(borderButton); });

    function toggleFade(button) {
        switch(button.label) {
        case 'No':
	    button.label = 'Yes';
	    break;
        case 'Yes':
	    button.label = 'No';
	    break;
        }
        scrollView.set_style_class_name(button.label == 'Yes' ? 'vfade' : '');
    }

    vfade.connect('clicked', () => { toggleFade(vfade); });
    toggleFade(vfade);

    function toggleOverlay(button) {
        switch(button.label) {
        case 'No':
	    button.label = 'Yes';
	    break;
        case 'Yes':
	    button.label = 'No';
	    break;
        }
        scrollView.overlay_scrollbars = (button.label == 'Yes');
    }

    overlay.connect('clicked', () => { toggleOverlay(overlay); });

    UI.main(stage);
}
test();