Skip to content

feat: mini workspace bubble #7096

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
May 19, 2023
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions core/blockly.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,7 @@ import {Menu} from './menu.js';
import {MenuItem} from './menuitem.js';
import {MetricsManager} from './metrics_manager.js';
import {Msg, setLocale} from './msg.js';
import {MiniWorkspaceBubble} from './bubbles/mini_workspace_bubble.js';
import {Mutator} from './mutator.js';
import {Names} from './names.js';
import {Options} from './options.js';
Expand Down Expand Up @@ -443,6 +444,12 @@ Mutator.prototype.newWorkspaceSvg = function (options: Options): WorkspaceSvg {
return new WorkspaceSvg(options);
};

MiniWorkspaceBubble.prototype.newWorkspaceSvg = function (
options: Options
): WorkspaceSvg {
return new WorkspaceSvg(options);
};

Names.prototype.populateProcedures = function (
this: Names,
workspace: Workspace
Expand Down
10 changes: 5 additions & 5 deletions core/bubbles/bubble.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,11 @@ export abstract class Bubble implements IBubble {
/** The width of the border around the bubble. */
static BORDER_WIDTH = 6;

/** Double the width of the border around the bubble. */
static DOUBLE_BORDER = this.BORDER_WIDTH * 2;

/** The minimum size the bubble can have. */
static MIN_SIZE = this.BORDER_WIDTH * 2;
static MIN_SIZE = this.DOUBLE_BORDER;

/**
* The thickness of the base of the tail in relation to the size of the
Expand Down Expand Up @@ -548,11 +551,8 @@ export abstract class Bubble implements IBubble {
}

/**
* Move this bubble during a drag, taking into account whether or not there is
* a drag surface.
* Move this bubble during a drag.
*
* @param dragSurface The surface that carries rendered items during a drag,
* or null if no drag surface is in use.
* @param newLoc The location to translate to, in workspace coordinates.
* @internal
*/
Expand Down
199 changes: 199 additions & 0 deletions core/bubbles/mini_workspace_bubble.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
/**
* @license
* Copyright 2023 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/

import {Abstract as AbstractEvent} from '../events/events_abstract.js';
import type {BlocklyOptions} from '../blockly_options.js';
import {Bubble} from './bubble.js';
import type {Coordinate} from '../utils/coordinate.js';
import * as dom from '../utils/dom.js';
import {Options} from '../options.js';
import {Svg} from '../utils/svg.js';
import type {Rect} from '../utils/rect.js';
import {Size} from '../utils/size.js';
import {WorkspaceSvg} from '../workspace_svg.js';

export class MiniWorkspaceBubble extends Bubble {
private svgDialog: SVGElement;
private miniWorkspace: WorkspaceSvg;
private autoLayout = true;

constructor(
workspaceOptions: BlocklyOptions,
protected readonly workspace: WorkspaceSvg,
protected anchor: Coordinate,
protected ownerRect?: Rect
) {
super(workspace, anchor, ownerRect);
const options = new Options(workspaceOptions);
this.validateWorkspaceOptions(options);

this.svgDialog = dom.createSvgElement(
Svg.SVG,
{
'x': Bubble.BORDER_WIDTH,
'y': Bubble.BORDER_WIDTH,
},
this.contentContainer
);
workspaceOptions.parentWorkspace = this.workspace;
this.miniWorkspace = this.newWorkspaceSvg(new Options(workspaceOptions));
const background = this.miniWorkspace.createDom('blocklyMutatorBackground');
this.svgDialog.appendChild(background);
if (options.languageTree) {
background.insertBefore(
this.miniWorkspace.addFlyout(Svg.G),
this.miniWorkspace.getCanvas()
);
const flyout = this.miniWorkspace.getFlyout();
flyout?.init(this.miniWorkspace);
flyout?.show(options.languageTree);
}

this.miniWorkspace.addChangeListener(this.updateBubbleSize.bind(this));
this.miniWorkspace
.getFlyout()
?.getWorkspace()
?.addChangeListener(this.updateBubbleSize.bind(this));
this.updateBubbleSize();
}

dispose() {
this.miniWorkspace.dispose();
super.dispose();
}

/** @internal */
getWorkspace(): WorkspaceSvg {
return this.miniWorkspace;
}

/** Adds a change listener to the mini workspace. */
addWorkspaceChangeListener(listener: (e: AbstractEvent) => void) {
this.miniWorkspace.addChangeListener(listener);
}

/**
* Validates the workspace options to make sure folks aren't trying to
* enable things the miniworkspace doesn't support.
*/
private validateWorkspaceOptions(options: Options) {
if (options.hasCategories) {
throw new Error(
'The miniworkspace bubble does not support toolboxes with categories'
);
}
if (options.hasTrashcan) {
throw new Error('The miniworkspace bubble does not support trashcans');
}
if (
options.zoomOptions.controls ||
options.zoomOptions.wheel ||
options.zoomOptions.pinch
) {
throw new Error('The miniworkspace bubble does not support zooming');
}
if (
options.moveOptions.scrollbars ||
options.moveOptions.wheel ||
options.moveOptions.drag
) {
throw new Error(
'The miniworkspace bubble does not scrolling/moving the workspace'
);
}
if (options.horizontalLayout) {
throw new Error(
'The miniworkspace bubble does not support horizontal layouts'
);
}
}

/**
* Updates the size of this bubble to account for the size of the
* mini workspace.
*/
private updateBubbleSize() {
if (this.miniWorkspace.isDragging()) return;

const currSize = this.getSize();
const newSize = this.calculateWorkspaceSize();
if (
Math.abs(currSize.width - newSize.width) < 10 &&
Math.abs(currSize.height - newSize.height) < 10
) {
// Only resize if the size has noticeably changed.
return;
}
this.svgDialog.setAttribute('width', `${newSize.width}px`);
this.svgDialog.setAttribute('height', `${newSize.height}px`);
this.miniWorkspace.setCachedParentSvgSize(newSize.width, newSize.height);
if (this.miniWorkspace.RTL) {
this.miniWorkspace
.getCanvas()
.setAttribute('transform', `translate(${newSize.width}, 0)`);
}
this.setSize(
new Size(
newSize.width + Bubble.DOUBLE_BORDER,
newSize.height + Bubble.DOUBLE_BORDER
),
this.autoLayout
);
this.miniWorkspace.resize();
this.miniWorkspace.recordDragTargets();
}

/**
* Calculates the size of the mini workspace for use in resizing the bubble.
*/
private calculateWorkspaceSize(): Size {
const canvas = this.miniWorkspace.getCanvas();
const bbox = canvas.getBBox();
let width = this.miniWorkspace.RTL ? -bbox.x : bbox.width;
let height = bbox.height + bbox.y + Bubble.DOUBLE_BORDER * 3;
const flyout = this.miniWorkspace.getFlyout();
if (flyout) {
const flyoutScrollMetrics = flyout
.getWorkspace()
.getMetricsManager()
.getScrollMetrics();
height = Math.max(height, flyoutScrollMetrics.height + 20);
if (this.miniWorkspace.RTL) {
width = Math.max(width, flyout.getWidth());
} else {
width += flyout.getWidth();
}
}
width += Bubble.DOUBLE_BORDER * 3;
return new Size(width, height);
}

/**
* Move this bubble during a drag.
*
* @param newLoc The location to translate to, in workspace coordinates.
* @internal
*/
moveDuringDrag(newLoc: Coordinate): void {
super.moveDuringDrag(newLoc);
this.autoLayout = false;
}

/** @internal */
moveTo(x: number, y: number): void {
super.moveTo(x, y);
this.miniWorkspace.recordDragTargets();
}

/** @internal */
// eslint-disable-next-line @typescript-eslint/no-unused-vars
newWorkspaceSvg(options: Options): WorkspaceSvg {
throw new Error(
'The implementation of newWorkspaceSvg should be ' +
'monkey-patched in by blockly.ts'
);
}
}
5 changes: 3 additions & 2 deletions core/flyout_base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import * as goog from '../closure/goog/goog.js';
goog.declareModuleId('Blockly.Flyout');

import type {Abstract as AbstractEvent} from './events/events_abstract.js';
import type {Block} from './block.js';
import type {BlockSvg} from './block_svg.js';
import * as browserEvents from './browser_events.js';
Expand Down Expand Up @@ -136,14 +137,14 @@ export abstract class Flyout extends DeleteArea implements IFlyout {
* Function that will be registered as a change listener on the workspace
* to reflow when blocks in the flyout workspace change.
*/
private reflowWrapper: Function | null = null;
private reflowWrapper: ((e: AbstractEvent) => void) | null = null;

/**
* Function that disables blocks in the flyout based on max block counts
* allowed in the target workspace. Registered as a change listener on the
* target workspace.
*/
private filterWrapper: Function | null = null;
private filterWrapper: ((e: AbstractEvent) => void) | null = null;

/**
* List of background mats that lurk behind each block to catch clicks
Expand Down
4 changes: 2 additions & 2 deletions core/mutator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ export class Mutator extends Icon {
* Function registered on the main workspace to update the mutator contents
* when the main workspace changes.
*/
private sourceListener: Function | null = null;
private sourceListener: (() => void) | null = null;

/**
* The PID associated with the updateWorkpace_ timeout, or null if no timeout
Expand Down Expand Up @@ -265,7 +265,7 @@ export class Mutator extends Icon {
const canvas = this.workspace_.getCanvas();
const workspaceSize = canvas.getBBox();
let width = workspaceSize.width + workspaceSize.x;
let height = workspaceSize.height + doubleBorderWidth * 3;
let height = workspaceSize.height + doubleBorderWidth * 2;
const flyout = this.workspace_.getFlyout();
if (flyout) {
const flyoutScrollMetrics = flyout
Expand Down
2 changes: 1 addition & 1 deletion core/workspace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -668,7 +668,7 @@ export class Workspace implements IASTNodeLocation {
* @param func Function to call.
* @returns Obsolete return value, ignore.
*/
addChangeListener(func: Function): Function {
addChangeListener(func: (e: Abstract) => void): Function {
this.listeners.push(func);
return func;
}
Expand Down