gnome-shell/tests/interactive/scroll-view-sizing.js

400 lines
13 KiB
JavaScript
Raw Normal View History

// -*- 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();