Skip to content

Commit cb60206

Browse files
authored
Feat/add informative tooltips (#178)
* Added TooltipManager to centralize tooltip information and display tooltips dynamically. * Tooltips activate on mouse hover, improving the user experience. * Tooltips are used by passing the component and the corresponding dictionary key for the tooltip content. TooltipManager.getInstance().attachTooltip(button, id); ![image](https://github.com/user-attachments/assets/10b67595-ef33-4aff-b886-f49a49c0b99b) * Added an option in the configuration to enable or disable tooltips. ![image](https://github.com/user-attachments/assets/80395adf-e6b7-42c3-a29f-b98a7db7f1f1) * Updated the UI and modified dropdowns to allow tooltips for each option. ![image](https://github.com/user-attachments/assets/16661c1d-6331-44c3-b40e-470ae860fe9a)
1 parent 0fa3e0c commit cb60206

29 files changed

+775
-167
lines changed

src/config.ts

Lines changed: 48 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { GlobalContext } from "./context";
2+
import { TooltipManager } from "./graphics/renderables/tooltip_manager";
23
import { deselectElement } from "./types/viewportManager";
3-
import { Colors } from "./utils";
4+
import { Colors } from "./utils/utils";
45

56
export class ConfigModal {
67
private ctx: GlobalContext;
@@ -11,6 +12,9 @@ export class ConfigModal {
1112
private colorPicker: HTMLInputElement | null;
1213
private selectedColor: number; // Stores the actual selected color as a number
1314
private tempColor: number; // Temporary color for selection
15+
private enableTooltipsSwitch: HTMLInputElement | null;
16+
private enableTooltips: boolean;
17+
private tempEnableTooltips: boolean;
1418

1519
constructor(ctx: GlobalContext) {
1620
this.ctx = ctx;
@@ -21,6 +25,8 @@ export class ConfigModal {
2125
this.colorPicker = null;
2226
this.selectedColor = Colors.Violet; // Default saved color
2327
this.tempColor = this.selectedColor; // Temporary color for selection
28+
this.enableTooltips = true; // Default saved value
29+
this.tempEnableTooltips = this.enableTooltips; // Temporary value for selection
2430

2531
this.createModal();
2632
this.setupEventListeners();
@@ -71,28 +77,11 @@ export class ConfigModal {
7177
<input type="color" id="colorPicker">
7278
</li>
7379
<li class="setting-item">
74-
<label for="autoConnections">Auto Connections</label>
75-
<input type="checkbox" id="autoConnections" class="switch-input">
76-
</li>
77-
<li class="setting-item">
78-
<label for="autoConnections">Config_1</label>
79-
<input type="checkbox" id="Config_1" class="switch-input">
80-
</li>
81-
<li class="setting-item">
82-
<label for="autoConnections">Config_2</label>
83-
<input type="checkbox" id="Config_2" class="switch-input">
84-
</li>
85-
<li class="setting-item">
86-
<label for="autoConnections">Config_3</label>
87-
<input type="checkbox" id="Config_3" class="switch-input">
88-
</li>
89-
<li class="setting-item">
90-
<label for="autoConnections">Config_4</label>
91-
<input type="checkbox" id="Config_4" class="switch-input">
92-
</li>
93-
<li class="setting-item">
94-
<label for="autoConnections">Config_5</label>
95-
<input type="checkbox" id="Config_5" class="switch-input">
80+
<label for="enableTooltips">Enable Tooltips</label>
81+
<label class="switch">
82+
<input type="checkbox" id="enableTooltips" class="switch-input" checked>
83+
<span class="switch-slider"></span>
84+
</label>
9685
</li>
9786
</ul>
9887
</div>
@@ -119,6 +108,9 @@ export class ConfigModal {
119108
this.colorPicker = document.getElementById(
120109
"colorPicker",
121110
) as HTMLInputElement;
111+
this.enableTooltipsSwitch = document.getElementById(
112+
"enableTooltips",
113+
) as HTMLInputElement;
122114
}
123115

124116
private setupEventListeners() {
@@ -127,7 +119,8 @@ export class ConfigModal {
127119
!this.modalContent ||
128120
!this.closeBtn ||
129121
!this.saveSettingsButton ||
130-
!this.colorPicker
122+
!this.colorPicker ||
123+
!this.enableTooltipsSwitch
131124
) {
132125
console.error("Some modal elements were not found.");
133126
return;
@@ -153,6 +146,12 @@ export class ConfigModal {
153146
this.tempColor = this.hexToNumber(this.colorPicker.value);
154147
}
155148
};
149+
150+
this.enableTooltipsSwitch.onchange = () => {
151+
const isEnabled = this.enableTooltipsSwitch?.checked || false;
152+
this.tempEnableTooltips = isEnabled; // Update the local variable only
153+
console.log("Tooltips setting changed (not saved yet):", isEnabled);
154+
};
156155
}
157156

158157
private setUpShortCuts() {
@@ -187,6 +186,12 @@ export class ConfigModal {
187186
this.colorPicker.value = this.toHex(this.selectedColor);
188187
}
189188

189+
// Update tooltips setting to the saved value
190+
if (this.enableTooltipsSwitch) {
191+
this.tempEnableTooltips = this.enableTooltips;
192+
this.enableTooltipsSwitch.checked = this.tempEnableTooltips; // Reflect the saved value in the UI
193+
}
194+
190195
this.modalOverlay.style.display = "flex"; // Make it visible first
191196
setTimeout(() => {
192197
this.modalOverlay?.classList.add("show");
@@ -212,6 +217,12 @@ export class ConfigModal {
212217
}
213218
}, 300);
214219
}
220+
221+
// Reset tooltips setting to the saved value
222+
if (this.enableTooltipsSwitch) {
223+
this.tempEnableTooltips = this.enableTooltips; // Reset temp value
224+
this.enableTooltipsSwitch.checked = this.enableTooltips; // Reflect saved value in UI
225+
}
215226
}
216227

217228
private saveSettings() {
@@ -221,7 +232,19 @@ export class ConfigModal {
221232
this.ctx.change_select_color(this.selectedColor);
222233
}
223234

224-
console.log("Settings saved. Applied color:", this.selectedColor);
235+
// Save the tooltips setting
236+
if (this.enableTooltipsSwitch) {
237+
this.enableTooltips = this.tempEnableTooltips; // Save the temporary value
238+
this.ctx.change_enable_tooltips(this.enableTooltips); // Update the GlobalContext
239+
TooltipManager.getInstance().updateTooltipsState(); // Update tooltips state in the app
240+
}
241+
242+
console.log(
243+
"Settings saved. Applied color:",
244+
this.selectedColor,
245+
"Tooltips enabled:",
246+
this.enableTooltips,
247+
);
225248
}
226249

227250
// Convert a number (0xRRGGBB) to a hex string ("#RRGGBB")

src/context.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import {
1515
MacAddress,
1616
MacAddressGenerator,
1717
} from "./packets/ethernet";
18-
import { Colors } from "./utils";
18+
import { Colors } from "./utils/utils";
1919

2020
export class GlobalContext {
2121
private viewport: Viewport = null;
@@ -26,9 +26,11 @@ export class GlobalContext {
2626
private ipGenerator: IpAddressGenerator;
2727
private macGenerator: MacAddressGenerator;
2828
private selectColor: number;
29+
private tooltipsEnabled: boolean;
2930

3031
constructor(viewport: Viewport) {
3132
this.selectColor = Colors.Violet;
33+
this.tooltipsEnabled = true;
3234
this.viewport = viewport;
3335

3436
// Sets the initial datagraph and viewgraph
@@ -192,4 +194,12 @@ export class GlobalContext {
192194
public get_select_color() {
193195
return this.selectColor;
194196
}
197+
198+
public change_enable_tooltips(enabled: boolean) {
199+
this.tooltipsEnabled = enabled;
200+
}
201+
202+
public get_enable_tooltips() {
203+
return this.tooltipsEnabled;
204+
}
195205
}

src/graphics/canvas.ts

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import { TOOLTIP_KEYS } from "../utils/constants/tooltips_constants";
2+
import { TooltipManager } from "./renderables/tooltip_manager";
3+
4+
export function createLayerSelector(
5+
options: { value: string; text: string }[],
6+
onchange: (value: string, event: Event) => void = () => undefined,
7+
) {
8+
const container = document.createElement("div");
9+
container.classList.add("layer-selector-container");
10+
11+
const dropdown = document.createElement("div");
12+
dropdown.classList.add("custom-dropdown", "layerselectordropdown");
13+
14+
const selected = document.createElement("div");
15+
// Attach tooltip to the selector
16+
TooltipManager.getInstance().attachTooltip(
17+
selected,
18+
TOOLTIP_KEYS.LAYER_SELECTOR,
19+
);
20+
selected.classList.add("selected-option");
21+
selected.textContent = TOOLTIP_KEYS.LINK_LAYER;
22+
dropdown.appendChild(selected);
23+
24+
const optionsContainer = document.createElement("div");
25+
optionsContainer.classList.add("options-container");
26+
27+
let selectedValue: string | null = "link";
28+
29+
options.forEach((optionData) => {
30+
const option = document.createElement("div");
31+
option.classList.add("dropdown-option");
32+
option.textContent = optionData.text;
33+
34+
TooltipManager.getInstance().attachTooltip(option, optionData.text, true);
35+
36+
option.onclick = (e) => {
37+
selected.textContent = optionData.text;
38+
selectedValue = optionData.value;
39+
optionsContainer.classList.remove("show");
40+
onchange(optionData.value, e);
41+
};
42+
43+
optionsContainer.appendChild(option);
44+
});
45+
46+
selected.onclick = (e) => {
47+
e.stopPropagation();
48+
optionsContainer.classList.toggle("show");
49+
};
50+
51+
document.addEventListener("click", () => {
52+
optionsContainer.classList.remove("show");
53+
});
54+
55+
dropdown.appendChild(optionsContainer);
56+
container.appendChild(dropdown);
57+
58+
return {
59+
container,
60+
getValue: () => selectedValue,
61+
setValue: (value: string) => {
62+
const option = options.find((opt) => opt.value === value);
63+
if (option) {
64+
selected.textContent = option.text;
65+
selectedValue = option.value;
66+
}
67+
},
68+
};
69+
}

src/graphics/left_bar.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { addDevice } from "../types/viewportManager";
55
import { GlobalContext } from "../context";
66
import { DeviceType } from "../types/devices/device";
77
import { Layer, layerFromName } from "../types/devices/layer";
8+
import { TooltipManager } from "./renderables/tooltip_manager";
89

910
export class LeftBar {
1011
private leftBar: HTMLElement;
@@ -25,6 +26,7 @@ export class LeftBar {
2526
button.setAttribute("title", label); // Shows Text
2627

2728
button.onclick = onClick;
29+
TooltipManager.getInstance().attachTooltip(button, label);
2830
this.leftBar.appendChild(button);
2931

3032
const img = document.createElement("img");

src/graphics/renderables/device_info.ts

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { DeviceType } from "../../types/devices/device";
44
import { ViewGraph } from "../../types/graphs/viewgraph";
55
import { RemoveDeviceMove } from "../../types/undo-redo";
66
import { urManager } from "../../types/viewportManager";
7+
import { TOOLTIP_KEYS } from "../../utils/constants/tooltips_constants";
78
import { createRightBarButton, createRoutingTable } from "../right_bar";
89
import { ProgramInfo } from "./program_info";
910
import { ProgramRunnerInfo } from "./program_runner_info";
@@ -27,21 +28,21 @@ export class DeviceInfo extends StyledInfo {
2728
const connections = this.device.viewgraph
2829
.getConnections(id)
2930
.map((edge) => edge.otherEnd(id));
30-
super.addField("ID", id.toString());
31-
super.addField("MacAddress", mac.toString());
32-
super.addListField("Connected Devices", connections);
31+
super.addField(TOOLTIP_KEYS.ID, id.toString());
32+
super.addField(TOOLTIP_KEYS.MAC_ADDRESS, mac.toString());
33+
super.addListField(TOOLTIP_KEYS.CONNECTED_DEVICES, connections);
3334
}
3435

3536
private addCommonButtons() {
3637
this.inputFields.push(
3738
createRightBarButton(
38-
"Connect device",
39+
TOOLTIP_KEYS.CONNECT_DEVICE,
3940
() => this.device.selectToConnect(),
4041
"right-bar-connect-button",
4142
true,
4243
),
4344
createRightBarButton(
44-
"Delete device",
45+
TOOLTIP_KEYS.DELETE_DEVICE,
4546
() => {
4647
const currLayer = this.device.viewgraph.getLayer();
4748
const move = new RemoveDeviceMove(currLayer, this.device.id);
@@ -68,8 +69,8 @@ export class DeviceInfo extends StyledInfo {
6869
]);
6970

7071
const dynamicTable = createRoutingTable(
71-
"Routing Table",
72-
["IP", "Mask", "Interface"],
72+
TOOLTIP_KEYS.ROUTING_TABLE,
73+
[TOOLTIP_KEYS.IP, TOOLTIP_KEYS.MASK, TOOLTIP_KEYS.INTERFACE],
7374
rows,
7475
viewgraph,
7576
deviceId,

src/graphics/renderables/program_info.ts

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,10 @@ import { DeviceId } from "../../types/graphs/datagraph";
22
import { ViewGraph } from "../../types/graphs/viewgraph";
33
import { createDropdown, Renderable } from "../right_bar";
44

5-
interface HasValue {
6-
value: string;
7-
}
8-
95
export class ProgramInfo implements Renderable {
106
readonly name: string;
117
private inputs: Node[] = [];
12-
private inputsValues: HasValue[] = [];
8+
private inputsValues: (() => string)[] = [];
139

1410
constructor(name: string) {
1511
this.name = name;
@@ -20,13 +16,14 @@ export class ProgramInfo implements Renderable {
2016
}
2117

2218
withDropdown(name: string, options: { value: string; text: string }[]) {
23-
const dropdown = createDropdown(name, options);
24-
this.inputs.push(dropdown);
25-
this.inputsValues.push(dropdown.querySelector("select"));
19+
const { container, getValue } = createDropdown(name, options);
20+
this.inputs.push(container);
21+
this.inputsValues.push(getValue);
2622
}
27-
2823
getInputValues() {
29-
return this.inputsValues.map(({ value }) => value);
24+
console.log("getInputValues", this.inputsValues);
25+
26+
return this.inputsValues.map((getValue) => getValue());
3027
}
3128

3229
toHTML() {

src/graphics/renderables/program_runner_info.ts

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { ProgramRunner, RunningProgram } from "../../programs";
2+
import { TOOLTIP_KEYS } from "../../utils/constants/tooltips_constants";
23
import {
34
createDropdown,
45
createRightBarButton,
@@ -33,22 +34,31 @@ export class ProgramRunnerInfo implements Renderable {
3334
const selectProgramDropdown = createDropdown(
3435
"Program",
3536
programOptions,
36-
"program-selector",
3737
(v) => {
3838
selectedProgram = programs[parseInt(v)];
39+
console.log("Selected program: ", selectedProgram.name);
3940
programInputs.replaceChildren(...selectedProgram.toHTML());
4041
},
4142
);
4243
// Button to run program
43-
const startProgramButton = createRightBarButton("Start program", () => {
44-
const { name } = selectedProgram;
45-
console.log("Started program: ", name);
46-
const inputs = selectedProgram.getInputValues();
47-
this.runner.addRunningProgram(name, inputs);
48-
this.refreshTable();
49-
});
44+
const startProgramButton = createRightBarButton(
45+
TOOLTIP_KEYS.START_PROGRAM,
46+
() => {
47+
const { name } = selectedProgram;
48+
console.log("Started program: ", name);
49+
const inputs = selectedProgram.getInputValues();
50+
// Validar que se hayan proporcionado todas las entradas necesarias
51+
if (inputs.some((input) => input === null || input === undefined)) {
52+
console.error("Some inputs are missing or invalid.");
53+
return;
54+
}
55+
this.runner.addRunningProgram(name, inputs);
56+
this.refreshTable();
57+
},
58+
"right-bar-start-button",
59+
);
5060
this.inputFields.push(
51-
selectProgramDropdown,
61+
selectProgramDropdown.container,
5262
programInputs,
5363
startProgramButton,
5464
);
@@ -57,6 +67,7 @@ export class ProgramRunnerInfo implements Renderable {
5767
private addRunningProgramsList() {
5868
this.runningProgramsTable = this.generateProgramsTable();
5969
this.inputFields.push(this.runningProgramsTable);
70+
this.inputFields.push(document.createElement("br"));
6071
}
6172

6273
private createProgramsTable(

0 commit comments

Comments
 (0)