mirror of
https://github.com/enso-org/enso.git
synced 2024-09-11 13:15:52 +03:00
Visualization Preview (#10310)
Fixes #8533 Visualization is shown when pressing ctrl (cmd on mac) and hovering output port. [Screencast from 2024-06-18 17-06-59.webm](https://github.com/enso-org/enso/assets/3919101/4963eaa7-8b96-4fdb-af09-70de68c6df57)
This commit is contained in:
parent
7eeea8e1c8
commit
ef2f5f993d
@ -11,6 +11,8 @@
|
||||
- The documentation editor supports [rendering images][10205].
|
||||
- [Fixed a bug where drop-down were not displayed for some arguments][10297].
|
||||
For example, `locale` parameter of `Equal_Ignore_Case` kind in join component.
|
||||
- [Node previews][10310]: Node may be previewed by hovering output port while
|
||||
pressing <kbd>Ctrl</kbd> key (<kbd>Cmd</kbd> on macOS).
|
||||
|
||||
[10064]: https://github.com/enso-org/enso/pull/10064
|
||||
[10179]: https://github.com/enso-org/enso/pull/10179
|
||||
@ -18,6 +20,7 @@
|
||||
[10198]: https://github.com/enso-org/enso/pull/10198
|
||||
[10205]: https://github.com/enso-org/enso/pull/10205
|
||||
[10297]: https://github.com/enso-org/enso/pull/10297
|
||||
[10310]: https://github.com/enso-org/enso/pull/10310
|
||||
|
||||
#### Enso Standard Library
|
||||
|
||||
|
@ -5,7 +5,7 @@ import { computedContent } from './css'
|
||||
import { expect } from './customExpect'
|
||||
import * as locate from './locate'
|
||||
|
||||
test('node can open and load visualization', async ({ page }) => {
|
||||
test('Node can open and load visualization', async ({ page }) => {
|
||||
await actions.goToGraph(page)
|
||||
const node = locate.graphNode(page).last()
|
||||
await node.click({ position: { x: 8, y: 8 } })
|
||||
@ -25,6 +25,29 @@ test('node can open and load visualization', async ({ page }) => {
|
||||
expect(typeof jsonContent).toBe('object')
|
||||
})
|
||||
|
||||
test('Previewing visualization', async ({ page }) => {
|
||||
await actions.goToGraph(page)
|
||||
const node = locate.graphNode(page).last()
|
||||
const port = await locate.outputPortCoordinates(node)
|
||||
await page.keyboard.down('Meta')
|
||||
await page.keyboard.down('Control')
|
||||
await expect(locate.anyVisualization(page)).not.toBeVisible()
|
||||
await page.mouse.move(port.x, port.y)
|
||||
await expect(locate.anyVisualization(node)).toBeVisible()
|
||||
await page.keyboard.up('Meta')
|
||||
await page.keyboard.up('Control')
|
||||
await expect(locate.anyVisualization(page)).not.toBeVisible()
|
||||
await page.keyboard.down('Meta')
|
||||
await page.keyboard.down('Control')
|
||||
await expect(locate.anyVisualization(node)).toBeVisible()
|
||||
await page.mouse.move(1, 1)
|
||||
await expect(locate.anyVisualization(page)).not.toBeVisible()
|
||||
await page.keyboard.up('Meta')
|
||||
await page.keyboard.up('Control')
|
||||
await page.mouse.move(port.x, port.y)
|
||||
await expect(locate.anyVisualization(page)).not.toBeVisible()
|
||||
})
|
||||
|
||||
test('Warnings visualization', async ({ page }) => {
|
||||
await actions.goToGraph(page)
|
||||
|
||||
|
@ -11,14 +11,14 @@ const props = defineProps<{
|
||||
isRecordingEnabledGlobally: boolean
|
||||
isRecordingOverridden: boolean
|
||||
isDocsVisible: boolean
|
||||
isVisualizationVisible: boolean
|
||||
isVisualizationEnabled: boolean
|
||||
isFullMenuVisible: boolean
|
||||
matchableNodeColors: Set<string>
|
||||
}>()
|
||||
const emit = defineEmits<{
|
||||
'update:isRecordingOverridden': [isRecordingOverridden: boolean]
|
||||
'update:isDocsVisible': [isDocsVisible: boolean]
|
||||
'update:isVisualizationVisible': [isVisualizationVisible: boolean]
|
||||
'update:isVisualizationEnabled': [isVisualizationEnabled: boolean]
|
||||
startEditing: []
|
||||
startEditingComment: []
|
||||
openFullMenu: []
|
||||
@ -62,8 +62,8 @@ const showColorPicker = ref(false)
|
||||
icon="eye"
|
||||
class="slot5"
|
||||
title="Visualization"
|
||||
:modelValue="props.isVisualizationVisible"
|
||||
@update:modelValue="emit('update:isVisualizationVisible', $event)"
|
||||
:modelValue="props.isVisualizationEnabled"
|
||||
@update:modelValue="emit('update:isVisualizationEnabled', $event)"
|
||||
/>
|
||||
<SvgButton
|
||||
name="edit"
|
||||
@ -90,7 +90,7 @@ const showColorPicker = ref(false)
|
||||
/>
|
||||
</div>
|
||||
<SmallPlusButton
|
||||
v-if="!isVisualizationVisible"
|
||||
v-if="!isVisualizationEnabled"
|
||||
class="below-slot5"
|
||||
@createNodes="emit('createNodes', $event)"
|
||||
/>
|
||||
|
@ -19,6 +19,7 @@ import NodeWidgetTree, {
|
||||
import SvgIcon from '@/components/SvgIcon.vue'
|
||||
import { useDoubleClick } from '@/composables/doubleClick'
|
||||
import { usePointer, useResizeObserver } from '@/composables/events'
|
||||
import { useKeyboard } from '@/composables/keyboard'
|
||||
import { injectGraphNavigator } from '@/providers/graphNavigator'
|
||||
import { injectNodeColors } from '@/providers/graphNodeColors'
|
||||
import { injectGraphSelection } from '@/providers/graphSelection'
|
||||
@ -69,7 +70,7 @@ const emit = defineEmits<{
|
||||
'update:hoverAnim': [progress: number]
|
||||
'update:visualizationId': [id: Opt<VisualizationIdentifier>]
|
||||
'update:visualizationRect': [rect: Rect | undefined]
|
||||
'update:visualizationVisible': [visible: boolean]
|
||||
'update:visualizationEnabled': [enabled: boolean]
|
||||
'update:visualizationFullscreen': [fullscreen: boolean]
|
||||
'update:visualizationWidth': [width: number]
|
||||
'update:visualizationHeight': [height: number]
|
||||
@ -220,9 +221,23 @@ function openFullMenu() {
|
||||
}
|
||||
|
||||
const isDocsVisible = ref(false)
|
||||
const outputHovered = ref(false)
|
||||
const keyboard = useKeyboard()
|
||||
const visualizationWidth = computed(() => props.node.vis?.width ?? null)
|
||||
const visualizationHeight = computed(() => props.node.vis?.height ?? null)
|
||||
const isVisualizationVisible = computed(() => props.node.vis?.visible ?? false)
|
||||
const isVisualizationEnabled = computed(() => props.node.vis?.visible ?? false)
|
||||
const isVisualizationPreviewed = computed(
|
||||
() => keyboard.mod && outputHovered.value && !isVisualizationEnabled.value,
|
||||
)
|
||||
const isVisualizationVisible = computed(
|
||||
() => isVisualizationEnabled.value || isVisualizationPreviewed.value,
|
||||
)
|
||||
watch(isVisualizationPreviewed, (newVal, oldVal) => {
|
||||
if (newVal && !oldVal) {
|
||||
graph.db.moveNodeToTop(nodeId.value)
|
||||
}
|
||||
})
|
||||
|
||||
const isVisualizationFullscreen = computed(() => props.node.vis?.fullscreen ?? false)
|
||||
|
||||
const bgStyleVariables = computed(() => {
|
||||
@ -384,12 +399,12 @@ const { getNodeColor, getNodeColors } = injectNodeColors()
|
||||
const matchableNodeColors = getNodeColors((node) => node !== nodeId.value)
|
||||
|
||||
const graphSelectionSize = computed(() =>
|
||||
isVisualizationVisible.value && visRect.value ? visRect.value.size : nodeSize.value,
|
||||
isVisualizationEnabled.value && visRect.value ? visRect.value.size : nodeSize.value,
|
||||
)
|
||||
|
||||
const nodeRect = computed(() => new Rect(props.node.position, nodeSize.value))
|
||||
const nodeOuterRect = computed(() =>
|
||||
isVisualizationVisible.value && visRect.value ? visRect.value : nodeRect.value,
|
||||
isVisualizationEnabled.value && visRect.value ? visRect.value : nodeRect.value,
|
||||
)
|
||||
watchEffect(() => {
|
||||
if (!nodeOuterRect.value.size.isZero()) {
|
||||
@ -405,14 +420,13 @@ watchEffect(() => {
|
||||
class="GraphNode"
|
||||
:style="{
|
||||
transform,
|
||||
minWidth: isVisualizationVisible ? `${visualizationWidth ?? 200}px` : undefined,
|
||||
minWidth: isVisualizationEnabled ? `${visualizationWidth ?? 200}px` : undefined,
|
||||
'--node-group-color': color,
|
||||
...(node.zIndex ? { 'z-index': node.zIndex } : {}),
|
||||
}"
|
||||
:class="{
|
||||
selected,
|
||||
selectionVisible,
|
||||
visualizationVisible: isVisualizationVisible,
|
||||
['executionState-' + executionState]: true,
|
||||
}"
|
||||
:data-node-id="nodeId"
|
||||
@ -452,11 +466,11 @@ watchEffect(() => {
|
||||
v-model:isRecordingOverridden="isRecordingOverridden"
|
||||
v-model:isDocsVisible="isDocsVisible"
|
||||
:isRecordingEnabledGlobally="projectStore.isRecordingEnabled"
|
||||
:isVisualizationVisible="isVisualizationVisible"
|
||||
:isVisualizationEnabled="isVisualizationEnabled"
|
||||
:isFullMenuVisible="menuVisible && menuFull"
|
||||
:nodeColor="getNodeColor(nodeId)"
|
||||
:matchableNodeColors="matchableNodeColors"
|
||||
@update:isVisualizationVisible="emit('update:visualizationVisible', $event)"
|
||||
@update:isVisualizationEnabled="emit('update:visualizationEnabled', $event)"
|
||||
@startEditing="startEditingNode"
|
||||
@startEditingComment="editingComment = true"
|
||||
@openFullMenu="openFullMenu"
|
||||
@ -479,9 +493,10 @@ watchEffect(() => {
|
||||
:width="visualizationWidth"
|
||||
:height="visualizationHeight"
|
||||
:isFocused="isOnlyOneSelected"
|
||||
:isPreview="isVisualizationPreviewed"
|
||||
@update:rect="updateVisualizationRect"
|
||||
@update:id="emit('update:visualizationId', $event)"
|
||||
@update:visible="emit('update:visualizationVisible', $event)"
|
||||
@update:enabled="emit('update:visualizationEnabled', $event)"
|
||||
@update:fullscreen="emit('update:visualizationFullscreen', $event)"
|
||||
@update:width="emit('update:visualizationWidth', $event)"
|
||||
@update:height="emit('update:visualizationHeight', $event)"
|
||||
@ -531,6 +546,7 @@ watchEffect(() => {
|
||||
@portClick="(...args) => emit('outputPortClick', ...args)"
|
||||
@portDoubleClick="(...args) => emit('outputPortDoubleClick', ...args)"
|
||||
@update:hoverAnim="emit('update:hoverAnim', $event)"
|
||||
@update:nodeHovered="outputHovered = $event"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
|
@ -4,7 +4,15 @@ import { useDoubleClick } from '@/composables/doubleClick'
|
||||
import { useGraphStore, type NodeId } from '@/stores/graph'
|
||||
import { setIfUndefined } from 'lib0/map'
|
||||
import type { AstId } from 'shared/ast'
|
||||
import { computed, effectScope, onScopeDispose, ref, watchEffect, type EffectScope } from 'vue'
|
||||
import {
|
||||
computed,
|
||||
effectScope,
|
||||
onScopeDispose,
|
||||
ref,
|
||||
watch,
|
||||
watchEffect,
|
||||
type EffectScope,
|
||||
} from 'vue'
|
||||
|
||||
const props = defineProps<{ nodeId: NodeId; forceVisible: boolean }>()
|
||||
|
||||
@ -12,6 +20,7 @@ const emit = defineEmits<{
|
||||
portClick: [event: PointerEvent, portId: AstId]
|
||||
portDoubleClick: [event: PointerEvent, portId: AstId]
|
||||
'update:hoverAnim': [progress: number]
|
||||
'update:nodeHovered': [hovered: boolean]
|
||||
}>()
|
||||
|
||||
const graph = useGraphStore()
|
||||
@ -47,6 +56,11 @@ const outputPorts = computed((): PortData[] => {
|
||||
const mouseOverOutput = ref<AstId>()
|
||||
|
||||
const outputHovered = computed(() => (graph.mouseEditedEdge ? undefined : mouseOverOutput.value))
|
||||
watch(outputHovered, (newVal, oldVal) => {
|
||||
if ((newVal != null) !== (oldVal != null)) {
|
||||
emit('update:nodeHovered', newVal != null)
|
||||
}
|
||||
})
|
||||
|
||||
const anyPortDisconnected = computed(() => {
|
||||
for (const port of outputPortsSet.value) {
|
||||
|
@ -79,7 +79,7 @@ const uploadingFiles = computed<[FileName, File][]>(() => {
|
||||
graphStore.setNodeVisualization(id, $event != null ? { identifier: $event } : {})
|
||||
"
|
||||
@update:visualizationRect="graphStore.updateVizRect(id, $event)"
|
||||
@update:visualizationVisible="graphStore.setNodeVisualization(id, { visible: $event })"
|
||||
@update:visualizationEnabled="graphStore.setNodeVisualization(id, { visible: $event })"
|
||||
@update:visualizationFullscreen="graphStore.setNodeVisualization(id, { fullscreen: $event })"
|
||||
@update:visualizationWidth="graphStore.setNodeVisualization(id, { width: $event })"
|
||||
@update:visualizationHeight="graphStore.setNodeVisualization(id, { height: $event })"
|
||||
|
@ -51,6 +51,7 @@ type RawDataSource = { type: 'raw'; data: any }
|
||||
const props = defineProps<{
|
||||
currentType?: Opt<VisualizationIdentifier>
|
||||
isCircularMenuVisible: boolean
|
||||
isPreview?: boolean
|
||||
nodePosition: Vec2
|
||||
nodeSize: Vec2
|
||||
width: Opt<number>
|
||||
@ -64,7 +65,7 @@ const props = defineProps<{
|
||||
const emit = defineEmits<{
|
||||
'update:rect': [rect: Rect | undefined]
|
||||
'update:id': [id: VisualizationIdentifier]
|
||||
'update:visible': [visible: boolean]
|
||||
'update:enabled': [visible: boolean]
|
||||
'update:fullscreen': [fullscreen: boolean]
|
||||
'update:width': [width: number]
|
||||
'update:height': [height: number]
|
||||
@ -310,7 +311,10 @@ provideVisualizationConfig({
|
||||
get nodeType() {
|
||||
return props.typename
|
||||
},
|
||||
hide: () => emit('update:visible', false),
|
||||
get isPreview() {
|
||||
return props.isPreview ?? false
|
||||
},
|
||||
hide: () => emit('update:enabled', false),
|
||||
updateType: (id) => emit('update:id', id),
|
||||
createNodes: (...options) => emit('createNodes', options),
|
||||
})
|
||||
|
@ -103,6 +103,7 @@ const contentStyle = computed(() => {
|
||||
:style="{
|
||||
'--color-visualization-bg': config.background,
|
||||
'--node-height': `${config.nodeSize.y}px`,
|
||||
...(config.isPreview ? { pointerEvents: 'none' } : {}),
|
||||
}"
|
||||
>
|
||||
<div
|
||||
@ -115,6 +116,7 @@ const contentStyle = computed(() => {
|
||||
<slot></slot>
|
||||
</div>
|
||||
<ResizeHandles
|
||||
v-if="!config.isPreview"
|
||||
v-model="clientBounds"
|
||||
left
|
||||
right
|
||||
@ -128,6 +130,7 @@ const contentStyle = computed(() => {
|
||||
/>
|
||||
<div class="toolbars">
|
||||
<div
|
||||
v-if="!config.isPreview"
|
||||
:class="{
|
||||
toolbar: true,
|
||||
invisible: config.isCircularMenuVisible,
|
||||
@ -136,7 +139,7 @@ const contentStyle = computed(() => {
|
||||
>
|
||||
<SvgButton name="eye" alt="Hide visualization" @click.stop="config.hide()" />
|
||||
</div>
|
||||
<div class="toolbar">
|
||||
<div v-if="!config.isPreview" class="toolbar">
|
||||
<SvgButton
|
||||
:name="config.fullscreen ? 'exit_fullscreen' : 'fullscreen'"
|
||||
:title="config.fullscreen ? 'Exit Fullscreen' : 'Fullscreen'"
|
||||
@ -162,7 +165,7 @@ const contentStyle = computed(() => {
|
||||
</Suspense>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="$slots.toolbar" class="visualization-defined-toolbars">
|
||||
<div v-if="$slots.toolbar && !config.isPreview" class="visualization-defined-toolbars">
|
||||
<div class="toolbar"><slot name="toolbar"></slot></div>
|
||||
</div>
|
||||
<div
|
||||
|
@ -17,6 +17,7 @@ export interface VisualizationConfig {
|
||||
readonly scale: number
|
||||
readonly isFocused: boolean
|
||||
readonly nodeType: string | undefined
|
||||
readonly isPreview: boolean
|
||||
isBelowToolbar: boolean
|
||||
width: number
|
||||
height: number
|
||||
|
@ -21,7 +21,7 @@ const isVisualizationVisible = ref(false)
|
||||
<CircularMenu
|
||||
v-model:is-auto-evaluation-disabled="isAutoEvaluationDisabled"
|
||||
v-model:is-docs-visible="isDocsVisible"
|
||||
v-model:is-visualization-visible="isVisualizationVisible"
|
||||
v-model:is-visualization-enabled="isVisualizationVisible"
|
||||
@update:isAutoEvaluationDisabled="logEvent('update:isAutoEvaluationDisabled', [$event])"
|
||||
@update:isDocsVisible="logEvent('update:isDocsVisible', [$event])"
|
||||
@update:isVisualizationVisible="logEvent('update:isVisualizationVisible', [$event])"
|
||||
|
20
package-lock.json
generated
20
package-lock.json
generated
@ -486,16 +486,6 @@
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"app/gui2/node_modules/@vue/test-utils": {
|
||||
"version": "2.4.6",
|
||||
"resolved": "https://registry.npmjs.org/@vue/test-utils/-/test-utils-2.4.6.tgz",
|
||||
"integrity": "sha512-FMxEjOpYNYiFe0GkaHsnJPXFHxQ6m4t8vI/ElPGpMWxZKpmRvQ33OIrvRXemy6yha03RxhOlQuy+gZMC3CQSow==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"js-beautify": "^1.14.9",
|
||||
"vue-component-type-helpers": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"app/gui2/node_modules/@lexical/clipboard": {
|
||||
"version": "0.16.0",
|
||||
"resolved": "https://registry.npmjs.org/@lexical/clipboard/-/clipboard-0.16.0.tgz",
|
||||
@ -618,6 +608,16 @@
|
||||
"lexical": "0.16.0"
|
||||
}
|
||||
},
|
||||
"app/gui2/node_modules/@vue/test-utils": {
|
||||
"version": "2.4.6",
|
||||
"resolved": "https://registry.npmjs.org/@vue/test-utils/-/test-utils-2.4.6.tgz",
|
||||
"integrity": "sha512-FMxEjOpYNYiFe0GkaHsnJPXFHxQ6m4t8vI/ElPGpMWxZKpmRvQ33OIrvRXemy6yha03RxhOlQuy+gZMC3CQSow==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"js-beautify": "^1.14.9",
|
||||
"vue-component-type-helpers": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"app/gui2/node_modules/lexical": {
|
||||
"version": "0.16.0",
|
||||
"resolved": "https://registry.npmjs.org/lexical/-/lexical-0.16.0.tgz",
|
||||
|
Loading…
Reference in New Issue
Block a user