Skip to content

Commit 6678b30

Browse files
committed
floating button
1 parent b20e0ef commit 6678b30

File tree

5 files changed

+379
-1
lines changed

5 files changed

+379
-1
lines changed

src/topoViewer/common/htmlTemplateUtils.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,9 @@ export function generateHtmlTemplate(
119119
if (mode === 'editor' && !partials.WIRESHARK_MODAL) {
120120
partials.WIRESHARK_MODAL = '';
121121
}
122+
if (mode === 'viewer' && !partials.FLOATING_ACTION_PANEL) {
123+
partials.FLOATING_ACTION_PANEL = '';
124+
}
122125
template = resolvePartials(template, partials);
123126

124127
const logoFile = params.isDarkTheme ? 'containerlab.svg' : 'containerlab-dark.svg';

src/topoViewer/common/templates/main.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
{{PANEL_LINK}}
3333
{{PANEL_LINK_EDITOR}}
3434
{{WIRESHARK_MODAL}}
35+
{{FLOATING_ACTION_PANEL}}
3536
{{SCRIPTS}}
3637
</div>
3738

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
<!-- Floating Action Button with Radial Menu -->
2+
<div id="floating-action-panel" class="fixed bottom-4 left-4 z-50">
3+
<!-- Main FAB button -->
4+
<div id="fab-container" class="relative">
5+
<!-- Radial menu items (hidden by default) -->
6+
<div id="radial-menu" class="absolute opacity-0 pointer-events-none transition-all duration-300">
7+
<!-- Add Node button -->
8+
<button id="radial-add-node"
9+
class="radial-menu-item absolute w-12 h-12 rounded-full bg-[var(--vscode-button-background)] hover:bg-[var(--vscode-button-hoverBackground)] text-[var(--vscode-button-foreground)] shadow-lg transition-all duration-300 flex items-center justify-center"
10+
title="Add Node">
11+
<i class="fas fa-server"></i>
12+
</button>
13+
14+
<!-- Add Group button -->
15+
<button id="radial-add-group"
16+
class="radial-menu-item absolute w-12 h-12 rounded-full bg-[var(--vscode-button-background)] hover:bg-[var(--vscode-button-hoverBackground)] text-[var(--vscode-button-foreground)] shadow-lg transition-all duration-300 flex items-center justify-center"
17+
title="Add Group">
18+
<i class="fas fa-object-group"></i>
19+
</button>
20+
21+
<!-- Add Text button (future feature) -->
22+
<button id="radial-add-text"
23+
class="radial-menu-item absolute w-12 h-12 rounded-full bg-[var(--vscode-button-background)] hover:bg-[var(--vscode-button-hoverBackground)] text-[var(--vscode-button-foreground)] shadow-lg transition-all duration-300 flex items-center justify-center opacity-50 cursor-not-allowed"
24+
title="Add Text (Coming Soon)"
25+
disabled>
26+
<i class="fas fa-font"></i>
27+
</button>
28+
</div>
29+
30+
<!-- Main + button -->
31+
<button id="fab-main"
32+
class="w-14 h-14 rounded-full bg-[var(--vscode-button-background)] hover:bg-[var(--vscode-button-hoverBackground)] text-[var(--vscode-button-foreground)] shadow-xl transition-all duration-300 flex items-center justify-center relative z-10">
33+
<i class="fas fa-plus text-xl transition-transform duration-300"></i>
34+
</button>
35+
</div>
36+
</div>
37+
38+
<style>
39+
#floating-action-panel {
40+
/* Ensure it stays above other elements but below modals */
41+
z-index: 40;
42+
}
43+
44+
#fab-main {
45+
/* Add subtle animation on hover */
46+
transition: transform 0.2s, box-shadow 0.2s, background-color 0.3s;
47+
}
48+
49+
#fab-main:hover {
50+
transform: scale(1.05);
51+
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.3);
52+
}
53+
54+
#fab-main.active {
55+
transform: rotate(45deg);
56+
}
57+
58+
/* Radial menu positioning when active */
59+
#radial-menu.active {
60+
opacity: 1;
61+
pointer-events: auto;
62+
}
63+
64+
/* Position radial menu items in an arc */
65+
#radial-menu.active #radial-add-node {
66+
transform: translate(70px, -20px);
67+
}
68+
69+
#radial-menu.active #radial-add-group {
70+
transform: translate(60px, -60px);
71+
}
72+
73+
#radial-menu.active #radial-add-text {
74+
transform: translate(20px, -85px);
75+
}
76+
77+
/* Initial hidden state for radial items */
78+
#radial-menu:not(.active) .radial-menu-item {
79+
transform: translate(0, 0) scale(0);
80+
}
81+
82+
#radial-menu.active .radial-menu-item {
83+
transform-origin: center;
84+
animation: fadeInScale 0.3s ease-out forwards;
85+
}
86+
87+
#radial-menu.active #radial-add-node {
88+
animation-delay: 0.05s;
89+
}
90+
91+
#radial-menu.active #radial-add-group {
92+
animation-delay: 0.1s;
93+
}
94+
95+
#radial-menu.active #radial-add-text {
96+
animation-delay: 0.15s;
97+
}
98+
99+
@keyframes fadeInScale {
100+
from {
101+
opacity: 0;
102+
transform: scale(0) translate(0, 0);
103+
}
104+
to {
105+
opacity: 1;
106+
transform: scale(1) var(--final-position);
107+
}
108+
}
109+
110+
/* Set final positions as CSS variables */
111+
#radial-add-node {
112+
--final-position: translate(70px, -20px);
113+
}
114+
115+
#radial-add-group {
116+
--final-position: translate(60px, -60px);
117+
}
118+
119+
#radial-add-text {
120+
--final-position: translate(20px, -85px);
121+
}
122+
123+
/* VS Code theme integration */
124+
#floating-action-panel button {
125+
border: 1px solid var(--vscode-button-border, transparent);
126+
}
127+
128+
/* Subtle pulse animation for main button */
129+
@keyframes pulse {
130+
0% {
131+
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
132+
}
133+
50% {
134+
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
135+
}
136+
100% {
137+
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
138+
}
139+
}
140+
141+
#fab-main {
142+
animation: pulse 2s infinite;
143+
}
144+
145+
#fab-main.active {
146+
animation: none;
147+
}
148+
</style>
Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
// file: managerFloatingActionPanel.ts
2+
3+
import cytoscape from 'cytoscape';
4+
import { ManagerAddContainerlabNode } from './managerAddContainerlabNode';
5+
import { getGroupManager } from '../../common/core/managerRegistry';
6+
import { log } from '../../common/logging/webviewLogger';
7+
8+
/**
9+
* ManagerFloatingActionPanel handles the floating action button (FAB) and radial menu
10+
* for quickly adding nodes, groups, and other elements to the topology.
11+
*/
12+
export class ManagerFloatingActionPanel {
13+
private cy: cytoscape.Core;
14+
private addNodeManager: ManagerAddContainerlabNode;
15+
private isMenuOpen: boolean = false;
16+
private fabMain: HTMLElement | null = null;
17+
private radialMenu: HTMLElement | null = null;
18+
19+
constructor(cy: cytoscape.Core, addNodeManager: ManagerAddContainerlabNode) {
20+
this.cy = cy;
21+
this.addNodeManager = addNodeManager;
22+
this.initializePanel();
23+
}
24+
25+
/**
26+
* Initializes the floating action panel and sets up event listeners
27+
*/
28+
private initializePanel(): void {
29+
// Get DOM elements
30+
this.fabMain = document.getElementById('fab-main');
31+
this.radialMenu = document.getElementById('radial-menu');
32+
33+
if (!this.fabMain || !this.radialMenu) {
34+
log.error('Floating action panel elements not found');
35+
return;
36+
}
37+
38+
// Main FAB click handler
39+
this.fabMain.addEventListener('click', (e) => {
40+
e.stopPropagation();
41+
this.toggleMenu();
42+
});
43+
44+
// Radial menu item handlers
45+
const addNodeBtn = document.getElementById('radial-add-node');
46+
const addGroupBtn = document.getElementById('radial-add-group');
47+
const addTextBtn = document.getElementById('radial-add-text');
48+
49+
if (addNodeBtn) {
50+
addNodeBtn.addEventListener('click', (e) => {
51+
e.stopPropagation();
52+
this.handleAddNode();
53+
});
54+
}
55+
56+
if (addGroupBtn) {
57+
addGroupBtn.addEventListener('click', (e) => {
58+
e.stopPropagation();
59+
this.handleAddGroup();
60+
});
61+
}
62+
63+
if (addTextBtn) {
64+
// Future feature - text annotations
65+
addTextBtn.addEventListener('click', (e) => {
66+
e.stopPropagation();
67+
log.info('Text annotations coming soon');
68+
});
69+
}
70+
71+
// Close menu when clicking outside
72+
document.addEventListener('click', (e) => {
73+
if (this.isMenuOpen && !this.fabMain?.contains(e.target as Node) &&
74+
!this.radialMenu?.contains(e.target as Node)) {
75+
this.closeMenu();
76+
}
77+
});
78+
79+
// Close menu on Escape key
80+
document.addEventListener('keydown', (e) => {
81+
if (e.key === 'Escape' && this.isMenuOpen) {
82+
this.closeMenu();
83+
}
84+
});
85+
}
86+
87+
/**
88+
* Toggles the radial menu open/closed state
89+
*/
90+
private toggleMenu(): void {
91+
if (this.isMenuOpen) {
92+
this.closeMenu();
93+
} else {
94+
this.openMenu();
95+
}
96+
}
97+
98+
/**
99+
* Opens the radial menu
100+
*/
101+
private openMenu(): void {
102+
if (!this.fabMain || !this.radialMenu) return;
103+
104+
this.isMenuOpen = true;
105+
this.fabMain.classList.add('active');
106+
this.radialMenu.classList.add('active');
107+
108+
// Rotate the plus icon to X
109+
const icon = this.fabMain.querySelector('i');
110+
if (icon) {
111+
icon.style.transform = 'rotate(45deg)';
112+
}
113+
114+
log.debug('Floating action menu opened');
115+
}
116+
117+
/**
118+
* Closes the radial menu
119+
*/
120+
private closeMenu(): void {
121+
if (!this.fabMain || !this.radialMenu) return;
122+
123+
this.isMenuOpen = false;
124+
this.fabMain.classList.remove('active');
125+
this.radialMenu.classList.remove('active');
126+
127+
// Rotate icon back to plus
128+
const icon = this.fabMain.querySelector('i');
129+
if (icon) {
130+
icon.style.transform = 'rotate(0deg)';
131+
}
132+
133+
log.debug('Floating action menu closed');
134+
}
135+
136+
/**
137+
* Handles adding a new node to the topology
138+
*/
139+
private handleAddNode(): void {
140+
log.debug('Adding new node via floating action panel');
141+
142+
// Get viewport center for positioning
143+
const extent = this.cy.extent();
144+
const viewportCenterX = (extent.x1 + extent.x2) / 2;
145+
const viewportCenterY = (extent.y1 + extent.y2) / 2;
146+
147+
// Create a synthetic event object for the add node manager
148+
const syntheticEvent: cytoscape.EventObject = {
149+
type: 'click',
150+
target: this.cy,
151+
cy: this.cy,
152+
namespace: '',
153+
timeStamp: Date.now(),
154+
position: {
155+
x: viewportCenterX,
156+
y: viewportCenterY
157+
},
158+
renderedPosition: {
159+
x: viewportCenterX,
160+
y: viewportCenterY
161+
},
162+
originalEvent: new MouseEvent('click')
163+
} as cytoscape.EventObject;
164+
165+
// Add the node
166+
this.addNodeManager.viewportButtonsAddContainerlabNode(this.cy, syntheticEvent);
167+
168+
// Close the menu after adding
169+
this.closeMenu();
170+
171+
// Optionally, open the node editor for the newly added node
172+
const newNode = this.cy.nodes().last();
173+
// Access viewportPanels through topoViewerState
174+
const state = (window as any).topoViewerState;
175+
if (newNode && state?.editorEngine?.viewportPanels) {
176+
setTimeout(() => {
177+
state.editorEngine.viewportPanels.panelNodeEditor(newNode);
178+
}, 100);
179+
}
180+
}
181+
182+
/**
183+
* Handles adding a new group to the topology
184+
*/
185+
private handleAddGroup(): void {
186+
log.debug('Adding new group via floating action panel');
187+
188+
const groupManager = getGroupManager(this.cy, 'edit');
189+
if (!groupManager) {
190+
log.error('Group manager not available');
191+
return;
192+
}
193+
194+
// Use the same method as the navbar button
195+
groupManager.viewportButtonsAddGroup();
196+
197+
// Close the menu after adding
198+
this.closeMenu();
199+
200+
log.info('Added new group via floating action panel');
201+
}
202+
203+
/**
204+
* Shows or hides the floating action panel
205+
*/
206+
public setVisibility(visible: boolean): void {
207+
const panel = document.getElementById('floating-action-panel');
208+
if (panel) {
209+
panel.style.display = visible ? 'block' : 'none';
210+
}
211+
}
212+
213+
/**
214+
* Updates the panel position if needed (e.g., to avoid overlapping with other UI elements)
215+
*/
216+
public updatePosition(bottom: number = 4, left: number = 4): void {
217+
const panel = document.getElementById('floating-action-panel');
218+
if (panel) {
219+
panel.style.bottom = `${bottom}rem`;
220+
panel.style.left = `${left}rem`;
221+
}
222+
}
223+
}

0 commit comments

Comments
 (0)