@yuneta/gobj-ui 7.23.35 → 7.23.36
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +92 -10
- package/dist/gobj-ui.cjs.js +3296 -654
- package/dist/gobj-ui.es.js +3295 -655
- package/index.js +2 -0
- package/package.json +1 -1
- package/src/c_yui_fsm_graph.css +128 -0
- package/src/c_yui_fsm_graph.js +967 -0
- package/src/c_yui_gclass.css +508 -0
- package/src/c_yui_gclass.js +1522 -0
- package/src/c_yui_gobj_tree_js.js +11 -27
- package/src/gclass_view_model.js +421 -0
- package/src/gclass_view_model.test.js +296 -0
- package/src/yui_gclass_view.js +42 -15
|
@@ -0,0 +1,967 @@
|
|
|
1
|
+
/***********************************************************************
|
|
2
|
+
* c_yui_fsm_graph.js
|
|
3
|
+
*
|
|
4
|
+
* A finite state machine, drawn.
|
|
5
|
+
*
|
|
6
|
+
* The second view of C_YUI_GCLASS's machine zone. The matrix is the
|
|
7
|
+
* first, and it stays the default: it is the shape the FSM is
|
|
8
|
+
* DECLARED in, it reads every gclass the same way, and it never lies
|
|
9
|
+
* about what is missing. A graph answers a different question --
|
|
10
|
+
* where does this machine GO -- and it answers it best exactly where
|
|
11
|
+
* the matrix is worst: many states, few events per state.
|
|
12
|
+
*
|
|
13
|
+
* G6, not a canvas of our own, and for one reason: what is expensive
|
|
14
|
+
* in a graph is not the drawing. It is the layout, the hit testing,
|
|
15
|
+
* the camera and the finger -- and all four are already paid for in
|
|
16
|
+
* this library (`yui_graph_camera.js`, `g6_touch_gestures.js`,
|
|
17
|
+
* `g6_drag_canvas_touch.js`), spoken with one vocabulary by the other
|
|
18
|
+
* three graphs. A fourth vocabulary is what a hand-rolled canvas
|
|
19
|
+
* would cost, and it would go on costing it.
|
|
20
|
+
*
|
|
21
|
+
* WHAT IT DRAWS, and what it refuses to draw:
|
|
22
|
+
*
|
|
23
|
+
* - one card per STATE, with how many events it handles and how
|
|
24
|
+
* many of them leave.
|
|
25
|
+
* - one edge per PAIR of states, labelled with the events that
|
|
26
|
+
* make the jump. Several transitions between the same two
|
|
27
|
+
* states are ONE edge with several labels: G6 draws overlapping
|
|
28
|
+
* edges on top of each other, so a second one is invisible and
|
|
29
|
+
* the reader has no way to know it is there.
|
|
30
|
+
* - an event a state handles WITHOUT leaving is not an edge. Ten
|
|
31
|
+
* self-loops on one card say nothing a number cannot say
|
|
32
|
+
* better, and they bury the transitions that do move.
|
|
33
|
+
* - a state nothing declares a way INTO is marked, never drawn as
|
|
34
|
+
* unreachable: an action may jump with `gobj_change_state()`
|
|
35
|
+
* (C_IEVENT_CLI enters ST_SESSION that way) and no description
|
|
36
|
+
* can see inside an action.
|
|
37
|
+
*
|
|
38
|
+
* It publishes NOTHING. A child that publishes an event forces a
|
|
39
|
+
* declaration into every host's FSM, and the machine of a gclass has
|
|
40
|
+
* nothing to say back to the viewer around it.
|
|
41
|
+
*
|
|
42
|
+
* Copyright (c) 2026, ArtGins.
|
|
43
|
+
* All Rights Reserved.
|
|
44
|
+
***********************************************************************/
|
|
45
|
+
import {
|
|
46
|
+
SDATA,
|
|
47
|
+
SDATA_END,
|
|
48
|
+
data_type_t,
|
|
49
|
+
gclass_create,
|
|
50
|
+
gclass_find_by_name,
|
|
51
|
+
log_error,
|
|
52
|
+
gobj_read_pointer_attr,
|
|
53
|
+
gobj_parent,
|
|
54
|
+
gobj_subscribe_event,
|
|
55
|
+
gobj_read_attr,
|
|
56
|
+
gobj_write_attr,
|
|
57
|
+
gobj_read_str_attr,
|
|
58
|
+
gobj_send_event,
|
|
59
|
+
gobj_short_name,
|
|
60
|
+
gobj_name,
|
|
61
|
+
clean_name,
|
|
62
|
+
createElement2,
|
|
63
|
+
refresh_language,
|
|
64
|
+
} from "@yuneta/gobj-js";
|
|
65
|
+
|
|
66
|
+
import {yui_toolbar} from "./yui_toolbar.js";
|
|
67
|
+
import {
|
|
68
|
+
yui_graph_camera_items,
|
|
69
|
+
yui_graph_update_zoom,
|
|
70
|
+
} from "./yui_graph_camera.js";
|
|
71
|
+
import {yui_is_dark, yui_theme_now, yui_watch_theme} from "./yui_theme.js";
|
|
72
|
+
import {ensure_drag_canvas_patch} from "./g6_drag_canvas_touch.js";
|
|
73
|
+
import {ensure_pinch_zoom_patch} from "./g6_touch_gestures.js";
|
|
74
|
+
|
|
75
|
+
import {t} from "i18next";
|
|
76
|
+
|
|
77
|
+
import {Graph} from "@antv/g6";
|
|
78
|
+
|
|
79
|
+
import "./c_yui_fsm_graph.css";
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
/***************************************************************
|
|
83
|
+
* Constants
|
|
84
|
+
***************************************************************/
|
|
85
|
+
const GCLASS_NAME = "C_YUI_FSM_GRAPH";
|
|
86
|
+
|
|
87
|
+
/*
|
|
88
|
+
* Left-to-right is the house direction for a topology, and a machine
|
|
89
|
+
* reads as one: it starts somewhere and it goes somewhere. Top-down
|
|
90
|
+
* is offered for the machines that are deep rather than wide.
|
|
91
|
+
*/
|
|
92
|
+
const LAYOUTS = {
|
|
93
|
+
"dagre-lr": {
|
|
94
|
+
g6_layout: {type: 'antv-dagre', rankdir: 'LR', nodesep: 28, ranksep: 70},
|
|
95
|
+
},
|
|
96
|
+
"dagre-tb": {
|
|
97
|
+
g6_layout: {type: 'antv-dagre', rankdir: 'TB', nodesep: 28, ranksep: 60},
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const DEFAULT_LAYOUT = "dagre-lr";
|
|
102
|
+
|
|
103
|
+
/*
|
|
104
|
+
* How many event names an edge label prints before it gives up and
|
|
105
|
+
* counts. Three fit beside an edge at the zoom a reader starts at,
|
|
106
|
+
* and the matrix has the full list either way.
|
|
107
|
+
*/
|
|
108
|
+
const MAX_EDGE_LABELS = 3;
|
|
109
|
+
|
|
110
|
+
const CARD_W = 210;
|
|
111
|
+
const CARD_H = 46;
|
|
112
|
+
|
|
113
|
+
const BEHAVIORS = ['zoom-canvas', 'drag-canvas', 'drag-element'];
|
|
114
|
+
|
|
115
|
+
/*
|
|
116
|
+
* Below this a card carries no readable text at all. A six-state
|
|
117
|
+
* machine fits a panel at 38%, which is the whole graph and worth
|
|
118
|
+
* nothing; stopping here and centring shows a part that can be read,
|
|
119
|
+
* and the reader pans for the rest. The same floor `C_G6_NODES_TREE`
|
|
120
|
+
* uses, for the same reason.
|
|
121
|
+
*/
|
|
122
|
+
const MIN_READABLE_ZOOM = 0.5;
|
|
123
|
+
|
|
124
|
+
/***************************************************************
|
|
125
|
+
* Data
|
|
126
|
+
***************************************************************/
|
|
127
|
+
const attrs_table = [
|
|
128
|
+
/*---------------- Public Attributes ----------------*/
|
|
129
|
+
SDATA(data_type_t.DTP_POINTER, "subscriber", 0, null, "Subscriber of output events"),
|
|
130
|
+
|
|
131
|
+
/*---------------- Data ----------------*/
|
|
132
|
+
SDATA(data_type_t.DTP_JSON, "fsm", 0, null, "The `fsm` block of a gclass view model: {entry_state, states, rows, published, unreachable}"),
|
|
133
|
+
SDATA(data_type_t.DTP_STRING, "current_state",0, "", "State of the instance the host is showing, lit in the graph"),
|
|
134
|
+
|
|
135
|
+
/*---------------- UI ----------------*/
|
|
136
|
+
SDATA(data_type_t.DTP_POINTER, "$container", 0, null, "HTMLElement root, mounted by the parent"),
|
|
137
|
+
SDATA(data_type_t.DTP_STRING, "canvas_id", 0, "", "Canvas id"),
|
|
138
|
+
SDATA(data_type_t.DTP_STRING, "wide", 0, "36px", "Height of the toolbar buttons"),
|
|
139
|
+
SDATA(data_type_t.DTP_STRING, "layout", 0, DEFAULT_LAYOUT, "Layout key (see LAYOUTS)"),
|
|
140
|
+
SDATA_END()
|
|
141
|
+
];
|
|
142
|
+
|
|
143
|
+
let PRIVATE_DATA = {
|
|
144
|
+
graph: null,
|
|
145
|
+
theme: null,
|
|
146
|
+
theme_observer: null, // MutationObserver on <html data-theme>
|
|
147
|
+
resize_observer: null, // ResizeObserver on the canvas mount
|
|
148
|
+
_resize_raf: 0, // rAF id debouncing resize -> EV_RESIZE
|
|
149
|
+
$layout_select: null, // the direction select
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
let __gclass__ = null;
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
/******************************
|
|
158
|
+
* Framework Methods
|
|
159
|
+
******************************/
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
/***************************************************************
|
|
165
|
+
* Framework Method: Create
|
|
166
|
+
***************************************************************/
|
|
167
|
+
function mt_create(gobj)
|
|
168
|
+
{
|
|
169
|
+
let priv = gobj.priv;
|
|
170
|
+
|
|
171
|
+
if(!gobj_read_str_attr(gobj, "canvas_id")) {
|
|
172
|
+
gobj_write_attr(gobj, "canvas_id",
|
|
173
|
+
"fsm_graph_" + clean_name(gobj_name(gobj)));
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
priv.theme = yui_theme_now();
|
|
177
|
+
|
|
178
|
+
build_ui(gobj);
|
|
179
|
+
|
|
180
|
+
/*
|
|
181
|
+
* CHILD subscription model
|
|
182
|
+
*/
|
|
183
|
+
let subscriber = gobj_read_pointer_attr(gobj, "subscriber");
|
|
184
|
+
if(!subscriber) {
|
|
185
|
+
subscriber = gobj_parent(gobj);
|
|
186
|
+
}
|
|
187
|
+
gobj_subscribe_event(gobj, null, {}, subscriber);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/***************************************************************
|
|
191
|
+
* Framework Method: Start
|
|
192
|
+
***************************************************************/
|
|
193
|
+
function mt_start(gobj)
|
|
194
|
+
{
|
|
195
|
+
/* The mount has to be IN the document by now: G6 sizes itself
|
|
196
|
+
* from its container, and one behind `is-hidden` measures 0x0. */
|
|
197
|
+
build_graph(gobj);
|
|
198
|
+
load_fsm(gobj);
|
|
199
|
+
|
|
200
|
+
gobj.priv.theme_observer = yui_watch_theme(function() {
|
|
201
|
+
gobj_send_event(gobj, "EV_THEME_CHANGED", {}, gobj);
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/***************************************************************
|
|
206
|
+
* Framework Method: Stop
|
|
207
|
+
***************************************************************/
|
|
208
|
+
function mt_stop(gobj)
|
|
209
|
+
{
|
|
210
|
+
let priv = gobj.priv;
|
|
211
|
+
|
|
212
|
+
if(priv.theme_observer) {
|
|
213
|
+
try {
|
|
214
|
+
priv.theme_observer.disconnect();
|
|
215
|
+
} catch(e) {
|
|
216
|
+
/* it went with its document */
|
|
217
|
+
}
|
|
218
|
+
priv.theme_observer = null;
|
|
219
|
+
}
|
|
220
|
+
if(priv.resize_observer) {
|
|
221
|
+
try {
|
|
222
|
+
priv.resize_observer.disconnect();
|
|
223
|
+
} catch(e) {
|
|
224
|
+
/* it went with its element */
|
|
225
|
+
}
|
|
226
|
+
priv.resize_observer = null;
|
|
227
|
+
}
|
|
228
|
+
if(priv._resize_raf) {
|
|
229
|
+
cancelAnimationFrame(priv._resize_raf);
|
|
230
|
+
priv._resize_raf = 0;
|
|
231
|
+
}
|
|
232
|
+
if(priv.graph) {
|
|
233
|
+
try {
|
|
234
|
+
priv.graph.destroy();
|
|
235
|
+
} catch(e) {
|
|
236
|
+
log_error(`${gobj_short_name(gobj)}: the graph refused to die: ${e}`);
|
|
237
|
+
}
|
|
238
|
+
priv.graph = null;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/***************************************************************
|
|
243
|
+
* Framework Method: Destroy
|
|
244
|
+
***************************************************************/
|
|
245
|
+
function mt_destroy(gobj)
|
|
246
|
+
{
|
|
247
|
+
destroy_ui(gobj);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
/***************************
|
|
254
|
+
* Local Methods
|
|
255
|
+
***************************/
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
/************************************************************
|
|
261
|
+
* Build UI
|
|
262
|
+
************************************************************/
|
|
263
|
+
function build_ui(gobj)
|
|
264
|
+
{
|
|
265
|
+
let priv = gobj.priv;
|
|
266
|
+
let canvas_id = gobj_read_str_attr(gobj, "canvas_id");
|
|
267
|
+
|
|
268
|
+
let $container = createElement2(
|
|
269
|
+
['div', {class: 'C_YUI_FSM_GRAPH FSM_GRAPH',
|
|
270
|
+
style: 'height:100%; display:flex; flex-direction:column;'}, [
|
|
271
|
+
['div', {class: 'FSM_GRAPH_TOOLBAR is-flex-grow-0'}, [make_toolbar(gobj)]],
|
|
272
|
+
['div', {class: 'is-flex-grow-1',
|
|
273
|
+
style: 'flex:1 1 auto; min-height:0; overflow:hidden;'}, [
|
|
274
|
+
['div', {id: canvas_id, class: 'FSM_GRAPH_CANVAS',
|
|
275
|
+
style: 'height:100%; min-height:0;'}, []]
|
|
276
|
+
]]
|
|
277
|
+
]]
|
|
278
|
+
);
|
|
279
|
+
|
|
280
|
+
gobj_write_attr(gobj, "$container", $container);
|
|
281
|
+
priv.$layout_select = $container.querySelector('.FSM_GRAPH_LAYOUT');
|
|
282
|
+
|
|
283
|
+
refresh_language($container, t);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/************************************************************
|
|
287
|
+
* Destroy UI
|
|
288
|
+
************************************************************/
|
|
289
|
+
function destroy_ui(gobj)
|
|
290
|
+
{
|
|
291
|
+
let priv = gobj.priv;
|
|
292
|
+
let $container = gobj_read_attr(gobj, "$container");
|
|
293
|
+
|
|
294
|
+
if($container) {
|
|
295
|
+
if($container.parentNode) {
|
|
296
|
+
$container.parentNode.removeChild($container);
|
|
297
|
+
}
|
|
298
|
+
gobj_write_attr(gobj, "$container", null);
|
|
299
|
+
}
|
|
300
|
+
priv.$layout_select = null;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/************************************************************
|
|
304
|
+
* Toolbar: the direction, and the camera every other graph
|
|
305
|
+
* in this library speaks with.
|
|
306
|
+
************************************************************/
|
|
307
|
+
function make_toolbar(gobj)
|
|
308
|
+
{
|
|
309
|
+
let priv = gobj.priv;
|
|
310
|
+
let wide = gobj_read_str_attr(gobj, "wide");
|
|
311
|
+
let layout = gobj_read_str_attr(gobj, "layout");
|
|
312
|
+
|
|
313
|
+
/* Each option's label goes through t() with the key SPELLED OUT:
|
|
314
|
+
* a key reached through a variable is invisible to the apps'
|
|
315
|
+
* locale validator, and i18next answers an unknown key with the
|
|
316
|
+
* key itself -- which renders in lower-case English and never
|
|
317
|
+
* changes language. */
|
|
318
|
+
let $select = createElement2(
|
|
319
|
+
['select', {class: 'FSM_GRAPH_LAYOUT', style: {height: wide},
|
|
320
|
+
title: t("layout"), 'data-i18n-title': 'layout',
|
|
321
|
+
'aria-label': t("layout"), 'data-i18n-aria-label': 'layout'}, [
|
|
322
|
+
['option', {value: 'dagre-lr', i18n: 'dagre left-right'},
|
|
323
|
+
t("dagre left-right")],
|
|
324
|
+
['option', {value: 'dagre-tb', i18n: 'dagre top-down'},
|
|
325
|
+
t("dagre top-down")]
|
|
326
|
+
], {
|
|
327
|
+
change: function(evt) {
|
|
328
|
+
gobj_send_event(gobj, "EV_CHANGE_LAYOUT",
|
|
329
|
+
{layout: evt.target.value}, gobj);
|
|
330
|
+
}
|
|
331
|
+
}]);
|
|
332
|
+
|
|
333
|
+
/* The value is set on the ELEMENT, never with a `selected`
|
|
334
|
+
* attribute: translating an option means writing its text, and a
|
|
335
|
+
* rebuilt option list would come back pointing at the first one. */
|
|
336
|
+
$select.value = LAYOUTS[layout]? layout: DEFAULT_LAYOUT;
|
|
337
|
+
|
|
338
|
+
const $toolbar = yui_toolbar({}, [
|
|
339
|
+
['div', {class: 'yui-horizontal-toolbar-section left is-flex is-align-items-center',
|
|
340
|
+
style: 'gap:.25rem;'}, [
|
|
341
|
+
['div', {class: 'select', style: {height: wide}}, [$select]]
|
|
342
|
+
]],
|
|
343
|
+
['div', {class: 'yui-horizontal-toolbar-section center'}, []],
|
|
344
|
+
['div', {class: 'yui-horizontal-toolbar-section right is-flex is-align-items-center',
|
|
345
|
+
style: 'gap:.25rem;'},
|
|
346
|
+
yui_graph_camera_items(gobj, priv.graph, wide)],
|
|
347
|
+
]);
|
|
348
|
+
|
|
349
|
+
refresh_language($toolbar, t);
|
|
350
|
+
return $toolbar;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/************************************************************
|
|
354
|
+
* The canvas mount element, or null.
|
|
355
|
+
************************************************************/
|
|
356
|
+
function canvas_mount(gobj)
|
|
357
|
+
{
|
|
358
|
+
let $container = gobj_read_attr(gobj, "$container");
|
|
359
|
+
if(!$container) {
|
|
360
|
+
return null;
|
|
361
|
+
}
|
|
362
|
+
return $container.querySelector("#" + gobj_read_str_attr(gobj, "canvas_id"));
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/************************************************************
|
|
366
|
+
* Build the G6 graph instance.
|
|
367
|
+
************************************************************/
|
|
368
|
+
function build_graph(gobj)
|
|
369
|
+
{
|
|
370
|
+
let priv = gobj.priv;
|
|
371
|
+
|
|
372
|
+
if(priv.graph) {
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
if(!canvas_mount(gobj)) {
|
|
376
|
+
log_error(`${gobj_short_name(gobj)}: no canvas mount, no graph`);
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/* Both patches are what make a graph work under a FINGER: G6
|
|
381
|
+
* re-issues pointer ids mid-gesture, and its drag-canvas needs
|
|
382
|
+
* the touch fix the other three graphs already use. */
|
|
383
|
+
ensure_drag_canvas_patch();
|
|
384
|
+
ensure_pinch_zoom_patch();
|
|
385
|
+
|
|
386
|
+
let cfg = LAYOUTS[gobj_read_str_attr(gobj, "layout")] || LAYOUTS[DEFAULT_LAYOUT];
|
|
387
|
+
|
|
388
|
+
priv.graph = new Graph({
|
|
389
|
+
container: gobj_read_str_attr(gobj, "canvas_id"),
|
|
390
|
+
animation: false,
|
|
391
|
+
autoResize: false,
|
|
392
|
+
zoomRange: [0.2, 3],
|
|
393
|
+
/* Room for the outermost cards: a fit that ends exactly on
|
|
394
|
+
* the bounds leaves the first and last state cut in half by
|
|
395
|
+
* the edge of the box. */
|
|
396
|
+
padding: 24,
|
|
397
|
+
edge: {
|
|
398
|
+
type: 'polyline',
|
|
399
|
+
style: {
|
|
400
|
+
stroke: edge_color(),
|
|
401
|
+
lineWidth: 1.2,
|
|
402
|
+
endArrow: true,
|
|
403
|
+
labelFontSize: 10,
|
|
404
|
+
labelFill: edge_label_color(),
|
|
405
|
+
labelBackground: true,
|
|
406
|
+
labelBackgroundFill: card_bg(),
|
|
407
|
+
labelBackgroundOpacity: 1,
|
|
408
|
+
labelPadding: [1, 3],
|
|
409
|
+
},
|
|
410
|
+
},
|
|
411
|
+
layout: cfg.g6_layout,
|
|
412
|
+
behaviors: BEHAVIORS.slice(),
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
if(priv.theme) {
|
|
416
|
+
try {
|
|
417
|
+
priv.graph.setTheme(priv.theme);
|
|
418
|
+
} catch(e) {
|
|
419
|
+
log_error(`${gobj_short_name(gobj)}: the theme was refused: ${e}`);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
priv.graph.on('aftertransform', () => {
|
|
424
|
+
yui_graph_update_zoom(gobj_read_attr(gobj, "$container"), priv.graph);
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
/*
|
|
428
|
+
* G6 v5's autoResize only listens to the window: a graph inside a
|
|
429
|
+
* panel, a tab, or a window somebody drags never hears about it,
|
|
430
|
+
* and a canvas whose configured size drifts from its on-screen one
|
|
431
|
+
* pans by the wrong amount under a finger.
|
|
432
|
+
*/
|
|
433
|
+
if(typeof ResizeObserver !== "undefined") {
|
|
434
|
+
let ro = new ResizeObserver(() => {
|
|
435
|
+
if(priv._resize_raf) {
|
|
436
|
+
cancelAnimationFrame(priv._resize_raf);
|
|
437
|
+
}
|
|
438
|
+
priv._resize_raf = requestAnimationFrame(() => {
|
|
439
|
+
priv._resize_raf = 0;
|
|
440
|
+
if(priv.graph) {
|
|
441
|
+
gobj_send_event(gobj, "EV_RESIZE", {}, gobj);
|
|
442
|
+
}
|
|
443
|
+
});
|
|
444
|
+
});
|
|
445
|
+
ro.observe(canvas_mount(gobj));
|
|
446
|
+
priv.resize_observer = ro;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/************************************************************
|
|
451
|
+
* Colours for what is drawn on the CANVAS.
|
|
452
|
+
*
|
|
453
|
+
* The cards are HTML and take theirs from the stylesheet;
|
|
454
|
+
* an edge cannot, so its three colours are read here and
|
|
455
|
+
* rewritten when the page changes theme.
|
|
456
|
+
************************************************************/
|
|
457
|
+
function edge_color()
|
|
458
|
+
{
|
|
459
|
+
return yui_is_dark()? "#6d7690": "#a9b0c0";
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
function edge_label_color()
|
|
463
|
+
{
|
|
464
|
+
return yui_is_dark()? "#b3bacb": "#5d6474";
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function card_bg()
|
|
468
|
+
{
|
|
469
|
+
return yui_is_dark()? "#171A21": "#FBFBFD";
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/************************************************************
|
|
473
|
+
* HTML escaping. A card is built as an innerHTML string --
|
|
474
|
+
* that is what a G6 html node takes -- and a state name
|
|
475
|
+
* comes from a description some backend answered.
|
|
476
|
+
************************************************************/
|
|
477
|
+
function esc(text)
|
|
478
|
+
{
|
|
479
|
+
return String((text === null || text === undefined)? "": text)
|
|
480
|
+
.replace(/&/g, "&")
|
|
481
|
+
.replace(/</g, "<")
|
|
482
|
+
.replace(/>/g, ">")
|
|
483
|
+
.replace(/"/g, """);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/************************************************************
|
|
487
|
+
* How many of a state's events move it somewhere else.
|
|
488
|
+
************************************************************/
|
|
489
|
+
function count_leaving(fsm, state_name)
|
|
490
|
+
{
|
|
491
|
+
let i = fsm.states.findIndex((s) => s.name === state_name);
|
|
492
|
+
if(i < 0) {
|
|
493
|
+
return 0;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
let n = 0;
|
|
497
|
+
for(let row of fsm.rows) {
|
|
498
|
+
let cell = row.cells[i];
|
|
499
|
+
if(cell && cell.next_state) {
|
|
500
|
+
n++;
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
return n;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/************************************************************
|
|
507
|
+
* One state, as a card.
|
|
508
|
+
*
|
|
509
|
+
* Three things it can be at once -- where the machine
|
|
510
|
+
* starts, where this instance IS, and a state nothing
|
|
511
|
+
* declares a way into -- so they are classes, not a type.
|
|
512
|
+
************************************************************/
|
|
513
|
+
function state_html(state, fsm, current, unreachable)
|
|
514
|
+
{
|
|
515
|
+
let classes = ["FSM_CARD"];
|
|
516
|
+
let marks = "";
|
|
517
|
+
|
|
518
|
+
if(state.name === current) {
|
|
519
|
+
classes.push("is-current");
|
|
520
|
+
}
|
|
521
|
+
if(state.name === fsm.entry_state) {
|
|
522
|
+
classes.push("is-entry");
|
|
523
|
+
marks += `<span class="FSM_CARD_MARK is-entry" ` +
|
|
524
|
+
`title="${esc(t("entry state"))}">▶</span>`;
|
|
525
|
+
}
|
|
526
|
+
if(unreachable.has(state.name)) {
|
|
527
|
+
classes.push("is-unreachable");
|
|
528
|
+
marks += `<span class="FSM_CARD_MARK is-warn" ` +
|
|
529
|
+
`title="${esc(t("state with no declared entry"))}">⚠</span>`;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/* How many of the events it handles LEAVE. The rest are the ones
|
|
533
|
+
* the matrix shows and this graph deliberately does not draw, so
|
|
534
|
+
* the number is the only place they are counted. */
|
|
535
|
+
let leaving = count_leaving(fsm, state.name);
|
|
536
|
+
let sub = `${state.count} ${esc(t("events"))}`;
|
|
537
|
+
if(leaving) {
|
|
538
|
+
sub += ` · ${leaving} ${esc(t("leave"))}`;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
return `<div class="${classes.join(" ")}">` +
|
|
542
|
+
`<div class="FSM_CARD_HEAD">` +
|
|
543
|
+
`<span class="FSM_CARD_NAME">${esc(state.name)}</span>${marks}` +
|
|
544
|
+
`</div>` +
|
|
545
|
+
`<div class="FSM_CARD_SUB">${sub}</div>` +
|
|
546
|
+
`</div>`;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
/************************************************************
|
|
550
|
+
* The graph data: one node per state, one edge per PAIR.
|
|
551
|
+
************************************************************/
|
|
552
|
+
function build_data(gobj)
|
|
553
|
+
{
|
|
554
|
+
let fsm = gobj_read_attr(gobj, "fsm");
|
|
555
|
+
|
|
556
|
+
if(!fsm || !Array.isArray(fsm.states) || !Array.isArray(fsm.rows)) {
|
|
557
|
+
return {nodes: [], edges: []};
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
let current = gobj_read_str_attr(gobj, "current_state");
|
|
561
|
+
let unreachable = new Set(Array.isArray(fsm.unreachable)? fsm.unreachable: []);
|
|
562
|
+
|
|
563
|
+
let nodes = fsm.states.map(function(state) {
|
|
564
|
+
return {
|
|
565
|
+
id: state.name,
|
|
566
|
+
type: 'html',
|
|
567
|
+
data: {state: state.name},
|
|
568
|
+
style: {
|
|
569
|
+
innerHTML: state_html(state, fsm, current, unreachable),
|
|
570
|
+
size: [CARD_W, CARD_H],
|
|
571
|
+
dx: -(CARD_W / 2),
|
|
572
|
+
dy: -(CARD_H / 2),
|
|
573
|
+
},
|
|
574
|
+
};
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
/*
|
|
578
|
+
* One edge per PAIR of states, not per transition: G6 draws two
|
|
579
|
+
* edges with the same ends on top of each other, so the second is
|
|
580
|
+
* invisible -- and nothing would tell a reader that an edge
|
|
581
|
+
* carrying one event is not an edge carrying four.
|
|
582
|
+
*/
|
|
583
|
+
let by_pair = new Map();
|
|
584
|
+
fsm.states.forEach(function(state, i) {
|
|
585
|
+
for(let row of fsm.rows) {
|
|
586
|
+
let cell = row.cells[i];
|
|
587
|
+
if(!cell || !cell.next_state) {
|
|
588
|
+
continue;
|
|
589
|
+
}
|
|
590
|
+
if(cell.next_state === state.name) {
|
|
591
|
+
continue; /* a jump to itself is not a move */
|
|
592
|
+
}
|
|
593
|
+
let key = state.name + " " + cell.next_state;
|
|
594
|
+
if(!by_pair.has(key)) {
|
|
595
|
+
by_pair.set(key, {
|
|
596
|
+
source: state.name,
|
|
597
|
+
target: cell.next_state,
|
|
598
|
+
events: [],
|
|
599
|
+
});
|
|
600
|
+
}
|
|
601
|
+
by_pair.get(key).events.push(row.event);
|
|
602
|
+
}
|
|
603
|
+
});
|
|
604
|
+
|
|
605
|
+
let edges = [];
|
|
606
|
+
for(let pair of by_pair.values()) {
|
|
607
|
+
let shown = pair.events.slice(0, MAX_EDGE_LABELS);
|
|
608
|
+
let label = shown.join("\n");
|
|
609
|
+
if(pair.events.length > shown.length) {
|
|
610
|
+
label += "\n+" + (pair.events.length - shown.length);
|
|
611
|
+
}
|
|
612
|
+
edges.push({
|
|
613
|
+
id: "e_" + edges.length,
|
|
614
|
+
source: pair.source,
|
|
615
|
+
target: pair.target,
|
|
616
|
+
style: {labelText: label},
|
|
617
|
+
});
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
return {nodes: nodes, edges: edges};
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
/************************************************************
|
|
624
|
+
* Fit the graph, but never past legibility.
|
|
625
|
+
*
|
|
626
|
+
* `fitCenter()` before `fitView()` is the house pair: fitView
|
|
627
|
+
* alone leaves an html-node graph off centre, because the
|
|
628
|
+
* cards are DOM elements over the canvas and the bounds it
|
|
629
|
+
* measures are not where the reader is looking.
|
|
630
|
+
************************************************************/
|
|
631
|
+
async function fit_readable(graph)
|
|
632
|
+
{
|
|
633
|
+
await graph.fitCenter();
|
|
634
|
+
await graph.fitView();
|
|
635
|
+
|
|
636
|
+
if(graph.getZoom() < MIN_READABLE_ZOOM) {
|
|
637
|
+
await graph.zoomTo(MIN_READABLE_ZOOM);
|
|
638
|
+
await graph.fitCenter();
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
/************************************************************
|
|
643
|
+
* Feed the graph and fit it.
|
|
644
|
+
************************************************************/
|
|
645
|
+
function load_fsm(gobj)
|
|
646
|
+
{
|
|
647
|
+
let priv = gobj.priv;
|
|
648
|
+
|
|
649
|
+
if(!priv.graph) {
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
let data = build_data(gobj);
|
|
654
|
+
if(!data.nodes.length) {
|
|
655
|
+
return; /* nothing to draw; the zone said so already */
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
try {
|
|
659
|
+
priv.graph.setData(data);
|
|
660
|
+
priv.graph.render().then(function() {
|
|
661
|
+
return fit_readable(priv.graph);
|
|
662
|
+
}).then(function() {
|
|
663
|
+
yui_graph_update_zoom(gobj_read_attr(gobj, "$container"), priv.graph);
|
|
664
|
+
}).catch(function(e) {
|
|
665
|
+
log_error(`${gobj_short_name(gobj)}: the graph did not render: ${e}`);
|
|
666
|
+
});
|
|
667
|
+
} catch(e) {
|
|
668
|
+
log_error(`${gobj_short_name(gobj)}: cannot load the machine: ${e}`);
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
/************************************************************
|
|
673
|
+
* A camera move, with one place to report a refusal.
|
|
674
|
+
************************************************************/
|
|
675
|
+
function camera(gobj, what, fn)
|
|
676
|
+
{
|
|
677
|
+
let priv = gobj.priv;
|
|
678
|
+
|
|
679
|
+
if(!priv.graph) {
|
|
680
|
+
return 0;
|
|
681
|
+
}
|
|
682
|
+
try {
|
|
683
|
+
fn(priv.graph);
|
|
684
|
+
} catch(e) {
|
|
685
|
+
log_error(`${gobj_short_name(gobj)}: cannot ${what}: ${e}`);
|
|
686
|
+
return -1;
|
|
687
|
+
}
|
|
688
|
+
return 0;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
|
|
692
|
+
|
|
693
|
+
|
|
694
|
+
/***************************
|
|
695
|
+
* Actions
|
|
696
|
+
***************************/
|
|
697
|
+
|
|
698
|
+
|
|
699
|
+
|
|
700
|
+
|
|
701
|
+
/************************************************************
|
|
702
|
+
* EV_SET_FSM {fsm, current_state}
|
|
703
|
+
************************************************************/
|
|
704
|
+
function ac_set_fsm(gobj, event, kw, src)
|
|
705
|
+
{
|
|
706
|
+
if(kw.fsm === undefined) {
|
|
707
|
+
log_error(`${GCLASS_NAME}: EV_SET_FSM without an fsm`);
|
|
708
|
+
return -1;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
gobj_write_attr(gobj, "fsm", kw.fsm);
|
|
712
|
+
if(typeof kw.current_state === "string") {
|
|
713
|
+
gobj_write_attr(gobj, "current_state", kw.current_state);
|
|
714
|
+
}
|
|
715
|
+
load_fsm(gobj);
|
|
716
|
+
return 0;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
/************************************************************
|
|
720
|
+
* EV_CHANGE_LAYOUT {layout}
|
|
721
|
+
************************************************************/
|
|
722
|
+
function ac_change_layout(gobj, event, kw, src)
|
|
723
|
+
{
|
|
724
|
+
let priv = gobj.priv;
|
|
725
|
+
let key = kw.layout;
|
|
726
|
+
|
|
727
|
+
if(!LAYOUTS[key]) {
|
|
728
|
+
log_error(`${GCLASS_NAME}: unknown layout '${key}'`);
|
|
729
|
+
return -1;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
gobj_write_attr(gobj, "layout", key);
|
|
733
|
+
if(!priv.graph) {
|
|
734
|
+
return 0;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
try {
|
|
738
|
+
priv.graph.setLayout(LAYOUTS[key].g6_layout);
|
|
739
|
+
priv.graph.layout().then(function() {
|
|
740
|
+
return fit_readable(priv.graph);
|
|
741
|
+
}).catch(function(e) {
|
|
742
|
+
log_error(`${gobj_short_name(gobj)}: the layout failed: ${e}`);
|
|
743
|
+
});
|
|
744
|
+
} catch(e) {
|
|
745
|
+
log_error(`${gobj_short_name(gobj)}: cannot change the layout: ${e}`);
|
|
746
|
+
}
|
|
747
|
+
return 0;
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
/************************************************************
|
|
751
|
+
* EV_RESIZE -- the container changed size under us.
|
|
752
|
+
************************************************************/
|
|
753
|
+
function ac_resize(gobj, event, kw, src)
|
|
754
|
+
{
|
|
755
|
+
let priv = gobj.priv;
|
|
756
|
+
let $canvas = canvas_mount(gobj);
|
|
757
|
+
|
|
758
|
+
if(!priv.graph || !$canvas) {
|
|
759
|
+
return 0;
|
|
760
|
+
}
|
|
761
|
+
if(!$canvas.offsetWidth || !$canvas.offsetHeight) {
|
|
762
|
+
return 0; /* hidden: measuring now would store a 0x0 canvas */
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
return camera(gobj, "resize the graph", function(graph) {
|
|
766
|
+
graph.setSize($canvas.offsetWidth, $canvas.offsetHeight);
|
|
767
|
+
});
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
/************************************************************
|
|
771
|
+
* EV_THEME_CHANGED -- the page went dark, or light.
|
|
772
|
+
*
|
|
773
|
+
* The cards are HTML and follow the stylesheet on their own;
|
|
774
|
+
* the EDGES are canvas and keep the colours they were built
|
|
775
|
+
* with, so those three are rewritten.
|
|
776
|
+
************************************************************/
|
|
777
|
+
function ac_theme_changed(gobj, event, kw, src)
|
|
778
|
+
{
|
|
779
|
+
let priv = gobj.priv;
|
|
780
|
+
|
|
781
|
+
priv.theme = yui_theme_now();
|
|
782
|
+
if(!priv.graph) {
|
|
783
|
+
return 0;
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
return camera(gobj, "repaint the graph", function(graph) {
|
|
787
|
+
graph.setTheme(priv.theme);
|
|
788
|
+
graph.setOptions({
|
|
789
|
+
edge: {
|
|
790
|
+
style: {
|
|
791
|
+
stroke: edge_color(),
|
|
792
|
+
labelFill: edge_label_color(),
|
|
793
|
+
labelBackgroundFill: card_bg(),
|
|
794
|
+
},
|
|
795
|
+
},
|
|
796
|
+
});
|
|
797
|
+
graph.draw();
|
|
798
|
+
});
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
/************************************************************
|
|
802
|
+
* EV_LANGUAGE_CHANGED
|
|
803
|
+
*
|
|
804
|
+
* A rebuild, not a re-translation: the words of a card live
|
|
805
|
+
* inside an innerHTML string, where refresh_language()
|
|
806
|
+
* cannot reach.
|
|
807
|
+
************************************************************/
|
|
808
|
+
function ac_language_changed(gobj, event, kw, src)
|
|
809
|
+
{
|
|
810
|
+
let $container = gobj_read_attr(gobj, "$container");
|
|
811
|
+
|
|
812
|
+
if($container) {
|
|
813
|
+
refresh_language($container, t);
|
|
814
|
+
}
|
|
815
|
+
load_fsm(gobj);
|
|
816
|
+
return 0;
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
/************************************************************
|
|
820
|
+
* The camera.
|
|
821
|
+
************************************************************/
|
|
822
|
+
function ac_zoom_in(gobj, event, kw, src)
|
|
823
|
+
{
|
|
824
|
+
return camera(gobj, "zoom in", (graph) => graph.zoomBy(1.2));
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
function ac_zoom_out(gobj, event, kw, src)
|
|
828
|
+
{
|
|
829
|
+
return camera(gobj, "zoom out", (graph) => graph.zoomBy(1 / 1.2));
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
function ac_zoom_reset(gobj, event, kw, src)
|
|
833
|
+
{
|
|
834
|
+
return camera(gobj, "reset the zoom", (graph) => graph.zoomTo(1));
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
function ac_center(gobj, event, kw, src)
|
|
838
|
+
{
|
|
839
|
+
return camera(gobj, "fit the graph", (graph) => fit_readable(graph));
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
/************************************************************
|
|
843
|
+
* EV_SHOW / EV_HIDE -- host visibility
|
|
844
|
+
************************************************************/
|
|
845
|
+
function ac_show(gobj, event, kw, src)
|
|
846
|
+
{
|
|
847
|
+
let $container = gobj_read_attr(gobj, "$container");
|
|
848
|
+
|
|
849
|
+
if($container) {
|
|
850
|
+
$container.classList.remove('is-hidden');
|
|
851
|
+
}
|
|
852
|
+
gobj_send_event(gobj, "EV_RESIZE", {}, gobj);
|
|
853
|
+
return 0;
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
function ac_hide(gobj, event, kw, src)
|
|
857
|
+
{
|
|
858
|
+
let $container = gobj_read_attr(gobj, "$container");
|
|
859
|
+
|
|
860
|
+
if($container) {
|
|
861
|
+
$container.classList.add('is-hidden');
|
|
862
|
+
}
|
|
863
|
+
return 0;
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
|
|
867
|
+
|
|
868
|
+
|
|
869
|
+
/***************************
|
|
870
|
+
* FSM
|
|
871
|
+
***************************/
|
|
872
|
+
|
|
873
|
+
|
|
874
|
+
|
|
875
|
+
|
|
876
|
+
/*---------------------------------------------*
|
|
877
|
+
* Global methods table
|
|
878
|
+
*---------------------------------------------*/
|
|
879
|
+
const gmt = {
|
|
880
|
+
mt_create: mt_create,
|
|
881
|
+
mt_start: mt_start,
|
|
882
|
+
mt_stop: mt_stop,
|
|
883
|
+
mt_destroy: mt_destroy,
|
|
884
|
+
};
|
|
885
|
+
|
|
886
|
+
/***************************************************************
|
|
887
|
+
* Create the GClass
|
|
888
|
+
***************************************************************/
|
|
889
|
+
function create_gclass(gclass_name)
|
|
890
|
+
{
|
|
891
|
+
if(__gclass__) {
|
|
892
|
+
log_error(`GClass ALREADY created: ${gclass_name}`);
|
|
893
|
+
return -1;
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
/*---------------------------------------------*
|
|
897
|
+
* States
|
|
898
|
+
*---------------------------------------------*/
|
|
899
|
+
const states = [
|
|
900
|
+
["ST_IDLE", [
|
|
901
|
+
["EV_SET_FSM", ac_set_fsm, null],
|
|
902
|
+
["EV_CHANGE_LAYOUT", ac_change_layout, null],
|
|
903
|
+
["EV_ZOOM_IN", ac_zoom_in, null],
|
|
904
|
+
["EV_ZOOM_OUT", ac_zoom_out, null],
|
|
905
|
+
["EV_ZOOM_RESET", ac_zoom_reset, null],
|
|
906
|
+
["EV_CENTER", ac_center, null],
|
|
907
|
+
["EV_RESIZE", ac_resize, null],
|
|
908
|
+
["EV_THEME_CHANGED", ac_theme_changed, null],
|
|
909
|
+
["EV_LANGUAGE_CHANGED", ac_language_changed, null],
|
|
910
|
+
["EV_SHOW", ac_show, null],
|
|
911
|
+
["EV_HIDE", ac_hide, null]
|
|
912
|
+
]]
|
|
913
|
+
];
|
|
914
|
+
|
|
915
|
+
/*---------------------------------------------*
|
|
916
|
+
* Events
|
|
917
|
+
*---------------------------------------------*/
|
|
918
|
+
const event_types = [
|
|
919
|
+
["EV_SET_FSM", 0],
|
|
920
|
+
["EV_CHANGE_LAYOUT", 0],
|
|
921
|
+
["EV_ZOOM_IN", 0],
|
|
922
|
+
["EV_ZOOM_OUT", 0],
|
|
923
|
+
["EV_ZOOM_RESET", 0],
|
|
924
|
+
["EV_CENTER", 0],
|
|
925
|
+
["EV_RESIZE", 0],
|
|
926
|
+
["EV_THEME_CHANGED", 0],
|
|
927
|
+
["EV_LANGUAGE_CHANGED", 0],
|
|
928
|
+
["EV_SHOW", 0],
|
|
929
|
+
["EV_HIDE", 0]
|
|
930
|
+
];
|
|
931
|
+
|
|
932
|
+
__gclass__ = gclass_create(
|
|
933
|
+
gclass_name,
|
|
934
|
+
event_types,
|
|
935
|
+
states,
|
|
936
|
+
gmt,
|
|
937
|
+
0, // lmt,
|
|
938
|
+
attrs_table,
|
|
939
|
+
PRIVATE_DATA,
|
|
940
|
+
0, // authz_table,
|
|
941
|
+
0, // command_table,
|
|
942
|
+
0, // s_user_trace_level
|
|
943
|
+
0 // gclass_flag
|
|
944
|
+
);
|
|
945
|
+
|
|
946
|
+
if(!__gclass__) {
|
|
947
|
+
return -1;
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
return 0;
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
/***************************************************************
|
|
954
|
+
* Register GClass
|
|
955
|
+
***************************************************************/
|
|
956
|
+
function register_c_yui_fsm_graph()
|
|
957
|
+
{
|
|
958
|
+
/* Idempotent: C_YUI_GCLASS registers it for its machine zone, so
|
|
959
|
+
* an app that also registers it explicitly must not trip
|
|
960
|
+
* "GClass ALREADY created". */
|
|
961
|
+
if(gclass_find_by_name(GCLASS_NAME, false)) {
|
|
962
|
+
return 0;
|
|
963
|
+
}
|
|
964
|
+
return create_gclass(GCLASS_NAME);
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
export { register_c_yui_fsm_graph };
|