Adding Positionable interface (#4669)

* Adding IPositionable interface.
This commit is contained in:
Monica Kozbial
2021-03-02 16:17:17 -08:00
committed by GitHub
parent 3e5c17313c
commit f2cec81584
6 changed files with 185 additions and 33 deletions

View File

@@ -93,6 +93,7 @@ goog.addDependency('../../core/interfaces/i_deletearea.js', ['Blockly.IDeleteAre
goog.addDependency('../../core/interfaces/i_flyout.js', ['Blockly.IFlyout'], [], {});
goog.addDependency('../../core/interfaces/i_metrics_manager.js', ['Blockly.IMetricsManager'], [], {});
goog.addDependency('../../core/interfaces/i_movable.js', ['Blockly.IMovable'], [], {});
goog.addDependency('../../core/interfaces/i_positionable.js', ['Blockly.IPositionable'], [], {});
goog.addDependency('../../core/interfaces/i_registrable.js', ['Blockly.IRegistrable'], [], {});
goog.addDependency('../../core/interfaces/i_registrable_field.js', ['Blockly.IRegistrableField'], [], {});
goog.addDependency('../../core/interfaces/i_selectable.js', ['Blockly.ISelectable'], [], {});
@@ -174,7 +175,7 @@ goog.addDependency('../../core/toolbox/toolbox_item.js', ['Blockly.ToolboxItem']
goog.addDependency('../../core/tooltip.js', ['Blockly.Tooltip'], ['Blockly.browserEvents', 'Blockly.utils.string'], {});
goog.addDependency('../../core/touch.js', ['Blockly.Touch'], ['Blockly.constants', 'Blockly.utils', 'Blockly.utils.global', 'Blockly.utils.string'], {});
goog.addDependency('../../core/touch_gesture.js', ['Blockly.TouchGesture'], ['Blockly.Gesture', 'Blockly.browserEvents', 'Blockly.utils', 'Blockly.utils.Coordinate', 'Blockly.utils.object'], {});
goog.addDependency('../../core/trashcan.js', ['Blockly.Trashcan'], ['Blockly.Events', 'Blockly.Events.TrashcanOpen', 'Blockly.Scrollbar', 'Blockly.Xml', 'Blockly.browserEvents', 'Blockly.constants', 'Blockly.utils.Rect', 'Blockly.utils.Svg', 'Blockly.utils.dom', 'Blockly.utils.toolbox'], {});
goog.addDependency('../../core/trashcan.js', ['Blockly.Trashcan'], ['Blockly.Events', 'Blockly.Events.TrashcanOpen', 'Blockly.IPositionable', 'Blockly.Scrollbar', 'Blockly.Xml', 'Blockly.browserEvents', 'Blockly.constants', 'Blockly.utils.Rect', 'Blockly.utils.Svg', 'Blockly.utils.dom', 'Blockly.utils.math', 'Blockly.utils.toolbox'], {});
goog.addDependency('../../core/utils.js', ['Blockly.utils'], ['Blockly.Msg', 'Blockly.constants', 'Blockly.utils.Coordinate', 'Blockly.utils.Rect', 'Blockly.utils.colour', 'Blockly.utils.global', 'Blockly.utils.string', 'Blockly.utils.style', 'Blockly.utils.userAgent'], {});
goog.addDependency('../../core/utils/aria.js', ['Blockly.utils.aria'], [], {});
goog.addDependency('../../core/utils/colour.js', ['Blockly.utils.colour'], [], {});
@@ -209,9 +210,9 @@ goog.addDependency('../../core/workspace_comment_render_svg.js', ['Blockly.Works
goog.addDependency('../../core/workspace_comment_svg.js', ['Blockly.WorkspaceCommentSvg'], ['Blockly.Css', 'Blockly.Events', 'Blockly.Events.CommentCreate', 'Blockly.Events.CommentDelete', 'Blockly.Events.CommentMove', 'Blockly.Events.Selected', 'Blockly.WorkspaceComment', 'Blockly.utils', 'Blockly.utils.Coordinate', 'Blockly.utils.Rect', 'Blockly.utils.Svg', 'Blockly.utils.dom', 'Blockly.utils.object'], {});
goog.addDependency('../../core/workspace_drag_surface_svg.js', ['Blockly.WorkspaceDragSurfaceSvg'], ['Blockly.utils', 'Blockly.utils.Svg', 'Blockly.utils.dom'], {});
goog.addDependency('../../core/workspace_dragger.js', ['Blockly.WorkspaceDragger'], ['Blockly.utils.Coordinate'], {});
goog.addDependency('../../core/workspace_svg.js', ['Blockly.WorkspaceSvg'], ['Blockly.BlockSvg', 'Blockly.ConnectionDB', 'Blockly.ContextMenuRegistry', 'Blockly.Events', 'Blockly.Events.BlockCreate', 'Blockly.Events.ThemeChange', 'Blockly.Events.ViewportChange', 'Blockly.Gesture', 'Blockly.Grid', 'Blockly.MarkerManager', 'Blockly.MetricsManager', 'Blockly.Msg', 'Blockly.Options', 'Blockly.ThemeManager', 'Blockly.Themes.Classic', 'Blockly.TouchGesture', 'Blockly.Workspace', 'Blockly.WorkspaceAudio', 'Blockly.WorkspaceDragSurfaceSvg', 'Blockly.Xml', 'Blockly.blockRendering', 'Blockly.browserEvents', 'Blockly.constants', 'Blockly.registry', 'Blockly.utils', 'Blockly.utils.Coordinate', 'Blockly.utils.Metrics', 'Blockly.utils.Rect', 'Blockly.utils.Svg', 'Blockly.utils.dom', 'Blockly.utils.object', 'Blockly.utils.toolbox'], {});
goog.addDependency('../../core/workspace_svg.js', ['Blockly.WorkspaceSvg'], ['Blockly.BlockSvg', 'Blockly.ConnectionDB', 'Blockly.ContextMenu', 'Blockly.ContextMenuRegistry', 'Blockly.Events', 'Blockly.Events.BlockCreate', 'Blockly.Events.ThemeChange', 'Blockly.Events.ViewportChange', 'Blockly.Gesture', 'Blockly.Grid', 'Blockly.MarkerManager', 'Blockly.MetricsManager', 'Blockly.Msg', 'Blockly.Options', 'Blockly.ThemeManager', 'Blockly.Themes.Classic', 'Blockly.TouchGesture', 'Blockly.Workspace', 'Blockly.WorkspaceAudio', 'Blockly.WorkspaceDragSurfaceSvg', 'Blockly.Xml', 'Blockly.blockRendering', 'Blockly.browserEvents', 'Blockly.constants', 'Blockly.registry', 'Blockly.utils', 'Blockly.utils.Coordinate', 'Blockly.utils.Metrics', 'Blockly.utils.Rect', 'Blockly.utils.Svg', 'Blockly.utils.dom', 'Blockly.utils.object', 'Blockly.utils.toolbox'], {});
goog.addDependency('../../core/xml.js', ['Blockly.Xml'], ['Blockly.Events', 'Blockly.constants', 'Blockly.utils', 'Blockly.utils.Size', 'Blockly.utils.dom', 'Blockly.utils.global', 'Blockly.utils.xml'], {});
goog.addDependency('../../core/zoom_controls.js', ['Blockly.ZoomControls'], ['Blockly.Css', 'Blockly.Events', 'Blockly.Events.Click', 'Blockly.Scrollbar', 'Blockly.Touch', 'Blockly.browserEvents', 'Blockly.constants', 'Blockly.utils.Svg', 'Blockly.utils.dom'], {'lang': 'es5'});
goog.addDependency('../../core/zoom_controls.js', ['Blockly.ZoomControls'], ['Blockly.Css', 'Blockly.Events', 'Blockly.Events.Click', 'Blockly.IPositionable', 'Blockly.Scrollbar', 'Blockly.Touch', 'Blockly.browserEvents', 'Blockly.constants', 'Blockly.utils.Rect', 'Blockly.utils.Svg', 'Blockly.utils.dom'], {'lang': 'es5'});
goog.addDependency("base.js", [], []);
// Load Blockly.

View File

@@ -0,0 +1,43 @@
/**
* @license
* Copyright 2021 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
/**
* @fileoverview The interface for a positionable ui element.
* @author kozbial@google.com (Monica Kozbial)
*/
'use strict';
goog.provide('Blockly.IPositionable');
/**
* Interface for a component that is positioned on top of the workspace.
* @interface
*/
Blockly.IPositionable = function() {};
/**
* Positions the element. Called when the window is resized.
* @param {!Blockly.MetricsManager.ContainerRegion} viewMetrics The workspace
* viewMetrics.
* @param {!Blockly.MetricsManager.AbsoluteMetrics} absoluteMetrics The absolute
* metrics for the workspace.
* @param {!Blockly.MetricsManager.ToolboxMetrics} toolboxMetrics The toolbox
* metrics for the workspace.
* @param {!Array<Blockly.utils.Rect>} savedPositions List of rectangles that
* are already on the workspace.
*/
Blockly.IPositionable.prototype.position;
/**
* Returns the bounding rectangle of the UI element in pixel units relative to
* the Blockly injection div.
* @returns {!Blockly.utils.Rect} The plugins bounding box.
*/
Blockly.IPositionable.prototype.getBoundingRectangle;

View File

@@ -16,8 +16,10 @@ goog.require('Blockly.browserEvents');
goog.require('Blockly.constants');
goog.require('Blockly.Events');
goog.require('Blockly.Events.TrashcanOpen');
goog.require('Blockly.IPositionable');
goog.require('Blockly.Scrollbar');
goog.require('Blockly.utils.dom');
goog.require('Blockly.utils.math');
goog.require('Blockly.utils.Rect');
goog.require('Blockly.utils.Svg');
goog.require('Blockly.utils.toolbox');
@@ -34,6 +36,7 @@ goog.requireType('Blockly.WorkspaceSvg');
* @param {!Blockly.WorkspaceSvg} workspace The workspace to sit in.
* @constructor
* @implements {Blockly.IDeleteArea}
* @implements {Blockly.IPositionable}
*/
Blockly.Trashcan = function(workspace) {
/**
@@ -431,38 +434,74 @@ Blockly.Trashcan.prototype.emptyContents = function() {
* Position the trashcan.
* It is positioned in the opposite corner to the corner the
* categories/toolbox starts at.
* @param {!Blockly.MetricsManager.ContainerRegion} viewMetrics The workspace
* viewMetrics.
* @param {!Blockly.MetricsManager.AbsoluteMetrics} absoluteMetrics The absolute
* metrics for the workspace.
* @param {!Blockly.MetricsManager.ToolboxMetrics} toolboxMetrics The toolbox
* metrics for the workspace.
* @param {!Array<Blockly.utils.Rect>} savedPositions List of rectangles that
* are already on the workspace.
*/
Blockly.Trashcan.prototype.position = function() {
Blockly.Trashcan.prototype.position = function(
viewMetrics, absoluteMetrics, toolboxMetrics, savedPositions) {
// Not yet initialized.
if (!this.verticalSpacing_) {
return;
}
var metrics = this.workspace_.getMetrics();
if (!metrics) {
// There are no metrics available (workspace is probably not visible).
return;
}
if (metrics.toolboxPosition == Blockly.TOOLBOX_AT_LEFT ||
if (toolboxMetrics.position == Blockly.TOOLBOX_AT_LEFT ||
(this.workspace_.horizontalLayout && !this.workspace_.RTL)) {
// Toolbox starts in the left corner.
this.left_ = metrics.viewWidth + metrics.absoluteLeft -
this.left_ = viewMetrics.width + absoluteMetrics.left -
this.WIDTH_ - this.MARGIN_SIDE_ - Blockly.Scrollbar.scrollbarThickness;
} else {
// Toolbox starts in the right corner.
this.left_ = this.MARGIN_SIDE_ + Blockly.Scrollbar.scrollbarThickness;
}
if (metrics.toolboxPosition == Blockly.TOOLBOX_AT_BOTTOM) {
this.top_ = this.verticalSpacing_;
} else {
this.top_ = metrics.viewHeight + metrics.absoluteTop -
(this.BODY_HEIGHT_ + this.LID_HEIGHT_) - this.verticalSpacing_;
var height = this.BODY_HEIGHT_ + this.LID_HEIGHT_;
// Upper corner placement
var minTop = this.top_ = this.verticalSpacing_;
// Bottom corner placement
var maxTop = viewMetrics.height + absoluteMetrics.top - height -
this.verticalSpacing_;
var placeBottom = toolboxMetrics.position !== Blockly.TOOLBOX_AT_BOTTOM;
this.top_ = placeBottom ? maxTop : minTop;
// Check for collision and bump if needed.
var boundingRect = this.getBoundingRectangle();
for (var i = 0, otherEl; (otherEl = savedPositions[i]); i++) {
if (boundingRect.intersects(otherEl)) {
if (placeBottom) {
// Bump up
this.top_ = otherEl.top - height - this.MARGIN_BOTTOM_;
} else {
this.top_ = otherEl.bottom + this.MARGIN_BOTTOM_;
}
// Recheck other savedPositions
boundingRect = this.getBoundingRectangle();
i = -1;
}
}
// Clamp top value within valid range.
this.top_ = Blockly.utils.math.clamp(minTop, this.top_, maxTop);
this.svgGroup_.setAttribute('transform',
'translate(' + this.left_ + ',' + this.top_ + ')');
};
/**
* Returns the bounding rectangle of the UI element in pixel units relative to
* the Blockly injection div.
* @returns {!Blockly.utils.Rect} The plugins bounding box.
*/
Blockly.Trashcan.prototype.getBoundingRectangle = function() {
var bottom = this.top_ + this.BODY_HEIGHT_ + this.LID_HEIGHT_;
var right = this.left_ + this.WIDTH_;
return new Blockly.utils.Rect(this.top_, bottom, this.left_, right);
};
/**
* Return the deletion rectangle for this trash can.
* @return {Blockly.utils.Rect} Rectangle in which to delete.

View File

@@ -52,3 +52,15 @@ Blockly.utils.Rect = function(top, bottom, left, right) {
Blockly.utils.Rect.prototype.contains = function(x, y) {
return x >= this.left && x <= this.right && y >= this.top && y <= this.bottom;
};
/**
* Tests whether this rectangle intersects the provided rectangle.
* Assumes that the coordinate system increases going down and left.
* @param {Blockly.utils.Rect} other The other rectangle to check for
* intersection with.
* @return {boolean} Whether this rectangle intersects the provided rectangle.
*/
Blockly.utils.Rect.prototype.intersects = function(other) {
return !(this.left > other.right || this.right < other.left ||
this.top > other.bottom || this.bottom < other.top);
};

View File

@@ -55,6 +55,7 @@ goog.requireType('Blockly.IASTNodeLocationSvg');
goog.requireType('Blockly.IBoundedElement');
goog.requireType('Blockly.IFlyout');
goog.requireType('Blockly.IMetricsManager');
goog.requireType('Blockly.IPositionable');
goog.requireType('Blockly.IToolbox');
goog.requireType('Blockly.Marker');
goog.requireType('Blockly.ScrollbarPair');
@@ -1077,12 +1078,27 @@ Blockly.WorkspaceSvg.prototype.resize = function() {
if (this.flyout_) {
this.flyout_.position();
}
/** @type {Array<Blockly.IPositionable>} */
var positionableEls = [];
if (this.trashcan) {
this.trashcan.position();
positionableEls.push(this.trashcan);
}
if (this.zoomControls_) {
this.zoomControls_.position();
positionableEls.push(this.zoomControls_);
}
if (positionableEls) {
var metricsManager = this.getMetricsManager();
var viewMetrics = metricsManager.getViewMetrics();
var absoluteMetrics = metricsManager.getAbsoluteMetrics();
var toolboxMetrics = metricsManager.getToolboxMetrics();
var savedPositions = [];
for (var i = 0, uiElement; (uiElement = positionableEls[i]); i++) {
uiElement.position(
viewMetrics, absoluteMetrics, toolboxMetrics, savedPositions);
savedPositions.push(uiElement.getBoundingRectangle());
}
}
if (this.scrollbar) {
this.scrollbar.resize();
}

View File

@@ -20,7 +20,9 @@ goog.require('Blockly.Events.Click');
goog.require('Blockly.Scrollbar');
goog.require('Blockly.Touch');
goog.require('Blockly.utils.dom');
goog.require('Blockly.utils.Rect');
goog.require('Blockly.utils.Svg');
goog.require('Blockly.IPositionable');
goog.requireType('Blockly.WorkspaceSvg');
@@ -29,6 +31,7 @@ goog.requireType('Blockly.WorkspaceSvg');
* Class for a zoom controls.
* @param {!Blockly.WorkspaceSvg} workspace The workspace to sit in.
* @constructor
* @implements {Blockly.IPositionable}
*/
Blockly.ZoomControls = function(workspace) {
/**
@@ -194,44 +197,82 @@ Blockly.ZoomControls.prototype.dispose = function() {
}
};
/**
* Returns the bounding rectangle of the UI element in pixel units relative to
* the Blockly injection div.
* @returns {!Blockly.utils.Rect} The plugins bounding box.
*/
Blockly.ZoomControls.prototype.getBoundingRectangle = function() {
var bottom = this.top_ + this.HEIGHT_;
var right = this.left_ + this.WIDTH_;
return new Blockly.utils.Rect(this.top_, bottom, this.left_, right);
};
/**
* Position the zoom controls.
* It is positioned in the opposite corner to the corner the
* categories/toolbox starts at.
* @param {!Blockly.MetricsManager.ContainerRegion} viewMetrics The workspace
* viewMetrics.
* @param {!Blockly.MetricsManager.AbsoluteMetrics} absoluteMetrics The absolute
* metrics for the workspace.
* @param {!Blockly.MetricsManager.ToolboxMetrics} toolboxMetrics The toolbox
* metrics for the workspace.
* @param {!Array<Blockly.utils.Rect>} savedPositions List of rectangles that
* are already on the workspace.
*/
Blockly.ZoomControls.prototype.position = function() {
Blockly.ZoomControls.prototype.position = function(
viewMetrics, absoluteMetrics, toolboxMetrics, savedPositions) {
// Not yet initialized.
if (!this.verticalSpacing_) {
return;
}
var metrics = this.workspace_.getMetrics();
if (!metrics) {
// There are no metrics available (workspace is probably not visible).
return;
}
if (metrics.toolboxPosition == Blockly.TOOLBOX_AT_LEFT ||
if (toolboxMetrics.position == Blockly.TOOLBOX_AT_LEFT ||
(this.workspace_.horizontalLayout && !this.workspace_.RTL)) {
// Toolbox starts in the left corner.
this.left_ = metrics.viewWidth + metrics.absoluteLeft -
this.left_ = viewMetrics.width + absoluteMetrics.left -
this.WIDTH_ - this.MARGIN_SIDE_ - Blockly.Scrollbar.scrollbarThickness;
} else {
// Toolbox starts in the right corner.
this.left_ = this.MARGIN_SIDE_ + Blockly.Scrollbar.scrollbarThickness;
}
if (metrics.toolboxPosition == Blockly.TOOLBOX_AT_BOTTOM) {
this.top_ = this.verticalSpacing_;
// Upper corner placement
var minTop = this.top_ = this.verticalSpacing_;
// Bottom corner placement
var maxTop = viewMetrics.height + absoluteMetrics.top -
this.HEIGHT_ - this.verticalSpacing_;
var placeBottom = toolboxMetrics.position !== Blockly.TOOLBOX_AT_BOTTOM;
this.top_ = placeBottom ? maxTop : minTop;
if (placeBottom) {
this.zoomInGroup_.setAttribute('transform', 'translate(0, 43)');
this.zoomOutGroup_.setAttribute('transform', 'translate(0, 77)');
} else {
this.zoomInGroup_.setAttribute('transform', 'translate(0, 34)');
if (this.zoomResetGroup_) {
this.zoomResetGroup_.setAttribute('transform', 'translate(0, 77)');
}
} else {
this.top_ = metrics.viewHeight + metrics.absoluteTop -
this.HEIGHT_ - this.verticalSpacing_;
this.zoomInGroup_.setAttribute('transform', 'translate(0, 43)');
this.zoomOutGroup_.setAttribute('transform', 'translate(0, 77)');
}
// Check for collision and bump if needed.
var boundingRect = this.getBoundingRectangle();
for (var i = 0, otherEl; (otherEl = savedPositions[i]); i++) {
if (boundingRect.intersects(otherEl)) {
if (placeBottom) {
// Bump up
this.top_ = otherEl.top - this.HEIGHT_ - this.MARGIN_BOTTOM_;
} else {
this.top_ = otherEl.bottom + this.MARGIN_BOTTOM_;
}
// Recheck other savedPositions
boundingRect = this.getBoundingRectangle();
i = -1;
}
}
// Clamp top value within valid range.
this.top_ = Blockly.utils.math.clamp(minTop, this.top_, maxTop);
this.svgGroup_.setAttribute('transform',
'translate(' + this.left_ + ',' + this.top_ + ')');
};