@yuneta/gobj-ui 7.23.132 → 7.23.134

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.
@@ -290,7 +290,7 @@ const PORT_LINE_WIDTH = 2;
290
290
  * gobj tree view has: `expanded` is the card with its pills and
291
291
  * its ports; `compact` is a one-line pill with the name inside
292
292
  * and small ports, the half-way house; `shape` is a figure of the
293
- * topic's colour -- a square, a circle, a diamond... chosen in the
293
+ * topic's colour -- a circle, a square, a diamond... chosen in the
294
294
  * node's properties -- no ports, no text, topology and nothing
295
295
  * else. The three tiers keep their order of size in every view,
296
296
  * so a root is still bigger than a leaf when nothing says which
@@ -311,9 +311,11 @@ const SHAPE_SIZE = {
311
311
  child: 22,
312
312
  };
313
313
  /* The figures a `shape` node can be, by G6's own node types
314
- * (`square` is G6's `rect` with rounded corners). */
314
+ * (`square` is G6's `rect` with rounded corners). A circle by
315
+ * default: a figure is a POINT of the topology, and a circle is
316
+ * the one figure that says nothing more than that. */
315
317
  const NODE_SHAPES = ['square', 'circle', 'diamond', 'triangle', 'hexagon', 'star'];
316
- const DEFAULT_NODE_SHAPE = 'square';
318
+ const DEFAULT_NODE_SHAPE = 'circle';
317
319
 
318
320
  /***************************************************************
319
321
  * Internal layout and operation mode definitions
@@ -519,6 +521,7 @@ let PRIVATE_DATA = {
519
521
  _focus_topic: null, // topic currently focused (EV_FOCUS_TOPIC)
520
522
  _focus_ids: [], // node ids carrying the focus 'active' state
521
523
  _on_pointerdown_focus: null, // listener keeping the keyboard on the canvas
524
+ _on_native_contextmenu: null, // listener keeping the browser's menu out
522
525
  _uninstall_long_press: null, // touch door to the context menu
523
526
  toolbar_collapsed: true, // floating toolbars folded (narrow only)
524
527
  _toolbars_could_collapse: null, // last answer, to notice it changed
@@ -704,6 +707,13 @@ function mt_destroy(gobj)
704
707
  priv._on_focusout_restore = null;
705
708
  }
706
709
 
710
+ if(priv._on_native_contextmenu) {
711
+ priv.$container.removeEventListener(
712
+ "contextmenu", priv._on_native_contextmenu, true
713
+ );
714
+ priv._on_native_contextmenu = null;
715
+ }
716
+
707
717
  if(priv.theme_observer) {
708
718
  priv.theme_observer.disconnect();
709
719
  priv.theme_observer = null;
@@ -1141,6 +1151,32 @@ function configure_events(gobj)
1141
1151
  };
1142
1152
  priv.$container.addEventListener("focusout", priv._on_focusout_restore);
1143
1153
 
1154
+ /* The browser's own menu never opens over the graph.
1155
+ *
1156
+ * The context-menu plugin calls preventDefault() on the event
1157
+ * it is handed, and that event is the one @antv/g SYNTHESISES
1158
+ * from `pointerdown` with `button === 2` -- not the DOM's
1159
+ * `contextmenu`, which nobody cancelled. So a right click opened
1160
+ * both menus, and where ours had nothing to show (an edge, the
1161
+ * empty canvas) it opened the browser's alone. A form field of a
1162
+ * popover keeps it: that is where cut and paste live. */
1163
+ if(priv._on_native_contextmenu) {
1164
+ priv.$container.removeEventListener(
1165
+ "contextmenu", priv._on_native_contextmenu, true
1166
+ );
1167
+ }
1168
+ priv._on_native_contextmenu = (ev) => {
1169
+ let target = ev.target;
1170
+ if(target && typeof target.closest === "function" &&
1171
+ target.closest("input, textarea, select, [contenteditable]")) {
1172
+ return;
1173
+ }
1174
+ ev.preventDefault();
1175
+ };
1176
+ priv.$container.addEventListener(
1177
+ "contextmenu", priv._on_native_contextmenu, true
1178
+ );
1179
+
1144
1180
  /* The canvas carries a `tabIndex` of its own, so a keydown reaches
1145
1181
  * us only while the GRAPH has focus. That is what keeps Ctrl+A in
1146
1182
  * the find box a text selection and not a selection of every node:
@@ -1205,7 +1241,12 @@ function configure_plugins(gobj)
1205
1241
  * press below, so the gesture arms itself on exactly what the
1206
1242
  * menu would agree to open on. */
1207
1243
  let menu_enable = (e) => {
1208
- return e.targetType === 'node' || e.targetType === 'edge';
1244
+ if(e.targetType === 'node') {
1245
+ return true;
1246
+ }
1247
+ /* An edge's commands (its properties, unlink) are edition
1248
+ * commands: in reading mode it has nothing to offer. */
1249
+ return e.targetType === 'edge' && !!priv.edit_mode;
1209
1250
  };
1210
1251
 
1211
1252
  graph_add_plugin(
@@ -2400,7 +2441,7 @@ function pill_innerHTML_of(gobj, nd, theme, highlight, selected, anchored)
2400
2441
 
2401
2442
  /* The figure a `shape` node wears: the node's own, then the
2402
2443
  * topic's default (the node properties popover writes both), then
2403
- * a square. */
2444
+ * a circle. */
2404
2445
  function node_figure_of(gobj, desc, geometry)
2405
2446
  {
2406
2447
  let shape = geometry? geometry.node_shape : null;
@@ -2447,13 +2488,14 @@ function figure_shape_of(gobj, desc, record, geometry, flags)
2447
2488
  let fill = dark? mix_colors(paint.fill, 0.25, "#2c3542")
2448
2489
  : mix_colors(paint.fill, 0.10, "#ffffff");
2449
2490
  let stroke = dark? mix_colors(paint.fill, 0.85, "#ffffff") : paint.fill;
2450
- let line_width = 2;
2491
+ /* The card's and the pill's line width, 1 unless somebody chose
2492
+ * another: the three views of a record wear the same outline. */
2493
+ let line_width = paint.lineWidth;
2451
2494
  /* A paint somebody CHOSE (a node's or a topic's own, saved in
2452
2495
  * `__graphs__`) is not ours to reinterpret: it comes through as
2453
2496
  * it was chosen. */
2454
2497
  if(paint.stroke !== getStrokeColor(paint.fill)) {
2455
2498
  stroke = paint.stroke;
2456
- line_width = Math.max(paint.lineWidth, 2);
2457
2499
  }
2458
2500
  if(f.selected) {
2459
2501
  stroke = SELECT_RING;
@@ -2798,57 +2840,17 @@ function draw_link(
2798
2840
  * Create the edge with independent id and semantic data
2799
2841
  * HACK: source/target are interchanged so arrows point parent -> child
2800
2842
  */
2801
- // Doc-style default: neutral grey edges, teal for the
2802
- // containment/tree relation (parent and child same topic, i.e.
2803
- // a self-hierarchy). Saturated topic colour is dropped.
2804
- // `themed_default` marks edges still on the default colour so a
2805
- // theme toggle can re-theme them without touching user-saved ones.
2806
- let dark = (priv.theme === "dark");
2807
2843
  let is_tree = (parent_topic === child_topic);
2808
- let saved_lineWidth = is_tree ? 2 : 1.6;
2809
- let saved_stroke = is_tree
2810
- ? (dark ? '#22a7c2' : '#0e7490')
2811
- : (dark ? '#8b94a3' : '#6b7280');
2812
- let themed_default = true;
2813
- let topic_props = priv._graph_properties[parent_topic];
2814
- if(topic_props) {
2815
- // Per-edge saved style
2816
- if(is_object(topic_props.edges)) {
2817
- let edge_key = hook_name + ":" + parent_id + ":" + child_id;
2818
- let edge_props = topic_props.edges[edge_key];
2819
- if(edge_props) {
2820
- if(edge_props.lineWidth != null) {
2821
- saved_lineWidth = edge_props.lineWidth;
2822
- }
2823
- if(edge_props.stroke) {
2824
- saved_stroke = edge_props.stroke;
2825
- themed_default = false;
2826
- }
2827
- }
2828
- }
2829
- // Fall back to topic defaults (only if not user-overridden)
2830
- if(themed_default && is_object(topic_props.defaults)) {
2831
- let defs = topic_props.defaults;
2832
- if(defs.edge_styles && defs.edge_styles[hook_name]) {
2833
- let hook_style = defs.edge_styles[hook_name];
2834
- if(hook_style.lineWidth != null) {
2835
- saved_lineWidth = hook_style.lineWidth;
2836
- }
2837
- if(hook_style.stroke) {
2838
- saved_stroke = hook_style.stroke;
2839
- themed_default = false;
2840
- }
2841
- } else if(defs.edge_style) {
2842
- if(defs.edge_style.lineWidth != null) {
2843
- saved_lineWidth = defs.edge_style.lineWidth;
2844
- }
2845
- if(defs.edge_style.stroke) {
2846
- saved_stroke = defs.edge_style.stroke;
2847
- themed_default = false;
2848
- }
2849
- }
2850
- }
2851
- }
2844
+ let edge_style = edge_style_of(gobj, {
2845
+ parent_topic: parent_topic,
2846
+ parent_id: parent_id,
2847
+ hook_name: hook_name,
2848
+ child_id: child_id,
2849
+ is_tree: is_tree,
2850
+ });
2851
+ let saved_lineWidth = edge_style.lineWidth;
2852
+ let saved_stroke = edge_style.stroke;
2853
+ let themed_default = edge_style.themed_default;
2852
2854
 
2853
2855
  let edge = {
2854
2856
  id: build_edge_id(gobj),
@@ -6804,6 +6806,7 @@ function show_node_popover(gobj)
6804
6806
  shapeSelect.value = nodeData.data && nodeData.data.desc
6805
6807
  ? node_figure_of(gobj, nodeData.data.desc, nodeData.data.graph_props || {})
6806
6808
  : DEFAULT_NODE_SHAPE;
6809
+ let orig_figure = shapeSelect.value;
6807
6810
 
6808
6811
  // Apply-to scope
6809
6812
  create_form_label(popover, 'apply to');
@@ -6832,7 +6835,10 @@ function show_node_popover(gobj)
6832
6835
  strokeInput.value,
6833
6836
  parseInt(lwInput.value) || 1,
6834
6837
  scopeSelect.value,
6835
- shapeSelect.value
6838
+ /* Only a figure somebody CHOSE is written: the
6839
+ * one the select opened on is today's default,
6840
+ * and saving it would freeze it on the node. */
6841
+ (shapeSelect.value !== orig_figure)? shapeSelect.value : ""
6836
6842
  );
6837
6843
  },
6838
6844
  },
@@ -8256,9 +8262,89 @@ function apply_edge_properties(gobj, edge_id, lineWidth, stroke, scope)
8256
8262
  deselect_edge(gobj);
8257
8263
  }
8258
8264
 
8265
+ /************************************************************
8266
+ * The style an edge INHERITS, before anything saved for the
8267
+ * edge itself: its hook's default in the parent topic, else the
8268
+ * parent topic's default for every edge, else the library's.
8269
+ *
8270
+ * The library's: neutral grey, teal for the containment/tree
8271
+ * relation (parent and child of the same topic, a
8272
+ * self-hierarchy). `themed_default` says the colour is still the
8273
+ * library's, so a theme toggle re-themes it and leaves a chosen
8274
+ * one alone.
8275
+ ************************************************************/
8276
+ function edge_inherited_style_of(gobj, d)
8277
+ {
8278
+ let priv = gobj.priv;
8279
+ let dark = (priv.theme === "dark");
8280
+ let style = {
8281
+ lineWidth: d.is_tree ? 2 : 1.6,
8282
+ stroke: d.is_tree
8283
+ ? (dark ? '#22a7c2' : '#0e7490')
8284
+ : (dark ? '#8b94a3' : '#6b7280'),
8285
+ themed_default: true,
8286
+ };
8287
+
8288
+ let defs = topic_defaults_of(gobj, d.parent_topic);
8289
+ if(!defs) {
8290
+ return style;
8291
+ }
8292
+ let chosen = null;
8293
+ if(is_object(defs.edge_styles) && is_object(defs.edge_styles[d.hook_name])) {
8294
+ chosen = defs.edge_styles[d.hook_name];
8295
+ } else if(is_object(defs.edge_style)) {
8296
+ chosen = defs.edge_style;
8297
+ }
8298
+ if(chosen) {
8299
+ if(chosen.lineWidth != null) {
8300
+ style.lineWidth = chosen.lineWidth;
8301
+ }
8302
+ if(chosen.stroke) {
8303
+ style.stroke = chosen.stroke;
8304
+ style.themed_default = false;
8305
+ }
8306
+ }
8307
+ return style;
8308
+ }
8309
+
8310
+ /* The key of an edge's own entry in its parent topic's `edges`. */
8311
+ function edge_key_of(d)
8312
+ {
8313
+ return d.hook_name + ":" + d.parent_id + ":" + d.child_id;
8314
+ }
8315
+
8316
+ /************************************************************
8317
+ * The style an edge WEARS: what it inherits, with what was saved
8318
+ * for the edge itself on top.
8319
+ ************************************************************/
8320
+ function edge_style_of(gobj, d)
8321
+ {
8322
+ let style = edge_inherited_style_of(gobj, d);
8323
+ let topic_props = gobj.priv._graph_properties[d.parent_topic];
8324
+ let own = (is_object(topic_props) && is_object(topic_props.edges))?
8325
+ topic_props.edges[edge_key_of(d)] : null;
8326
+ if(is_object(own)) {
8327
+ if(own.lineWidth != null) {
8328
+ style.lineWidth = own.lineWidth;
8329
+ }
8330
+ if(own.stroke) {
8331
+ style.stroke = own.stroke;
8332
+ style.themed_default = false;
8333
+ }
8334
+ }
8335
+ return style;
8336
+ }
8337
+
8259
8338
  /************************************************************
8260
8339
  * Save edge styles into _graph_properties (called from
8261
8340
  * save_geometry).
8341
+ *
8342
+ * Only what differs from what the edge INHERITS is written. It
8343
+ * used to be "any line width but 2", and the library's own width
8344
+ * for an edge between two topics is 1.6: every Save wrote every
8345
+ * such edge down with its width and its theme's colour, so the
8346
+ * default was frozen on it -- a theme toggle no longer re-themed
8347
+ * it after a reload, and a topic default never reached it.
8262
8348
  ************************************************************/
8263
8349
  function update_edge_geometry(gobj, edge_id)
8264
8350
  {
@@ -8271,16 +8357,20 @@ function update_edge_geometry(gobj, edge_id)
8271
8357
  }
8272
8358
 
8273
8359
  let style = edgeData.style || {};
8274
- let lineWidth = style.lineWidth;
8275
- let stroke = style.stroke;
8360
+ let d = edgeData.data;
8361
+ let inherited = edge_inherited_style_of(gobj, d);
8362
+ let parent_topic = d.parent_topic;
8363
+ let edge_key = edge_key_of(d);
8276
8364
 
8277
- let parent_topic = edgeData.data.parent_topic;
8278
- let edge_key = edgeData.data.hook_name + ":" +
8279
- edgeData.data.parent_id + ":" + edgeData.data.child_id;
8365
+ let edge_props = {};
8366
+ if(style.lineWidth != null && style.lineWidth !== inherited.lineWidth) {
8367
+ edge_props.lineWidth = style.lineWidth;
8368
+ }
8369
+ if(style.stroke && style.stroke !== inherited.stroke) {
8370
+ edge_props.stroke = style.stroke;
8371
+ }
8280
8372
 
8281
- // If default values, remove any previously saved entry
8282
- let hasCustom = (lineWidth != null && lineWidth !== 2);
8283
- if(!hasCustom) {
8373
+ if(Object.keys(edge_props).length === 0) {
8284
8374
  let topic_props = priv._graph_properties[parent_topic];
8285
8375
  if(topic_props && is_object(topic_props.edges)) {
8286
8376
  delete topic_props.edges[edge_key];
@@ -8298,11 +8388,6 @@ function update_edge_geometry(gobj, edge_id)
8298
8388
  if(!is_object(topic_props.edges)) {
8299
8389
  topic_props.edges = {};
8300
8390
  }
8301
-
8302
- let edge_props = { lineWidth: lineWidth };
8303
- if(stroke) {
8304
- edge_props.stroke = stroke;
8305
- }
8306
8391
  topic_props.edges[edge_key] = edge_props;
8307
8392
  }
8308
8393
 
@@ -8341,7 +8426,9 @@ function build_context_menu_items(gobj, e)
8341
8426
  gobj, node_id, canvasPoint[0], canvasPoint[1]
8342
8427
  );
8343
8428
 
8344
- if(port_key) {
8429
+ /* A port's commands are edition commands; out of edition a
8430
+ * port is part of its node, and gets the node's menu. */
8431
+ if(port_key && priv.edit_mode) {
8345
8432
  priv._context_port_key = port_key;
8346
8433
  return build_port_context_menu(gobj, node_id, port_key);
8347
8434
  } else {
@@ -8387,11 +8474,14 @@ function build_node_context_menu(gobj, node_id)
8387
8474
  if(gobj.priv.edit_mode && !node_is_compact(gobj, node_id)) {
8388
8475
  items.push(ctx_item('g6-icon-resize', t('resize all'), 'resize_all_nodes'));
8389
8476
  items.push(ctx_item('g6-icon-resize', t('resize topic nodes'), 'resize_topic_nodes'));
8390
- /* The way back: the sizes somebody saved -- or that a Save
8391
- * froze before 7.23.80 -- forgotten, the library's defaults
8392
- * back on every card. */
8393
- items.push(ctx_item('g6-icon-undo', t('reset sizes'), 'reset_all_sizes'));
8394
- items.push(ctx_item('g6-icon-undo', t('reset topic sizes'), 'reset_topic_sizes'));
8477
+ }
8478
+ if(gobj.priv.edit_mode) {
8479
+ /* The way back, in every view: what the node properties and
8480
+ * the resizes saved -- or a Save froze before 7.23.80 --
8481
+ * forgotten, the library's defaults back. */
8482
+ items.push(ctx_item('g6-icon-undo', t('reset node'), 'reset_node'));
8483
+ items.push(ctx_item('g6-icon-undo', t('reset topic nodes'), 'reset_topic_nodes'));
8484
+ items.push(ctx_item('g6-icon-undo', t('reset all nodes'), 'reset_all_nodes'));
8395
8485
  }
8396
8486
 
8397
8487
  return items;
@@ -8411,18 +8501,33 @@ function build_port_context_menu(gobj, node_id, port_key)
8411
8501
  items.push(ctx_item('g6-icon-edit', t('port properties'), 'port_properties'));
8412
8502
  items.push(ctx_item('g6-icon-resize', t('resize all ports'), 'resize_all_ports'));
8413
8503
  items.push(ctx_item('g6-icon-resize', t('resize topic ports'), 'resize_topic_ports'));
8504
+ /* The way back from the popover and the two above: the
8505
+ * library's own shape and radius. */
8506
+ items.push(ctx_item('g6-icon-undo', t('reset port'), 'reset_port'));
8507
+ items.push(ctx_item('g6-icon-undo', t('reset topic ports'), 'reset_topic_ports'));
8508
+ items.push(ctx_item('g6-icon-undo', t('reset all ports'), 'reset_all_ports'));
8414
8509
  }
8415
8510
 
8416
8511
  return items;
8417
8512
  }
8418
8513
 
8419
8514
  /************************************************************
8420
- * Build edge context menu items (prepared for expansion).
8515
+ * Build edge context menu items: the two floating icons of a
8516
+ * selected edge, for a right click and for a finger.
8421
8517
  ************************************************************/
8422
8518
  function build_edge_context_menu(gobj)
8423
8519
  {
8424
8520
  let items = [];
8425
- // Future: add edge-specific actions here
8521
+
8522
+ if(gobj.priv.edit_mode) {
8523
+ inject_svg_icons();
8524
+ items.push(ctx_item('g6-icon-edit', t('edge properties'), 'edge_properties'));
8525
+ items.push(ctx_item('g6-icon-delete', t('unlink'), 'unlink_edge'));
8526
+ items.push(ctx_item('g6-icon-undo', t('reset edge'), 'reset_edge'));
8527
+ items.push(ctx_item('g6-icon-undo', t('reset same type edges'), 'reset_same_type_edges'));
8528
+ items.push(ctx_item('g6-icon-undo', t('reset all edges'), 'reset_all_edges'));
8529
+ }
8530
+
8426
8531
  return items;
8427
8532
  }
8428
8533
 
@@ -8445,11 +8550,23 @@ function handle_context_menu_click(gobj, value)
8445
8550
  case 'resize_topic_nodes':
8446
8551
  copy_size_to_nodes(gobj, true);
8447
8552
  break;
8448
- case 'reset_all_sizes':
8449
- reset_sizes(gobj, false);
8553
+ case 'reset_node':
8554
+ reset_nodes(gobj, 'this');
8555
+ break;
8556
+ case 'reset_topic_nodes':
8557
+ reset_nodes(gobj, 'same_topic');
8558
+ break;
8559
+ case 'reset_all_nodes':
8560
+ reset_nodes(gobj, 'all');
8450
8561
  break;
8451
- case 'reset_topic_sizes':
8452
- reset_sizes(gobj, true);
8562
+ case 'reset_edge':
8563
+ reset_edges(gobj, 'this');
8564
+ break;
8565
+ case 'reset_same_type_edges':
8566
+ reset_edges(gobj, 'same_type');
8567
+ break;
8568
+ case 'reset_all_edges':
8569
+ reset_edges(gobj, 'all');
8453
8570
  break;
8454
8571
  case 'port_properties':
8455
8572
  if(priv._context_node_id && priv._context_port_key) {
@@ -8463,6 +8580,32 @@ function handle_context_menu_click(gobj, value)
8463
8580
  case 'resize_topic_ports':
8464
8581
  copy_size_to_ports(gobj, true);
8465
8582
  break;
8583
+ case 'reset_port':
8584
+ reset_ports(gobj, 'this');
8585
+ break;
8586
+ case 'reset_topic_ports':
8587
+ reset_ports(gobj, 'same_topic');
8588
+ break;
8589
+ case 'reset_all_ports':
8590
+ reset_ports(gobj, 'all');
8591
+ break;
8592
+ case 'edge_properties':
8593
+ if(priv._context_edge_id) {
8594
+ deselect_node(gobj);
8595
+ select_edge(gobj, priv._context_edge_id);
8596
+ show_edge_popover(gobj);
8597
+ }
8598
+ break;
8599
+ case 'unlink_edge':
8600
+ if(priv._context_edge_id) {
8601
+ deselect_node(gobj);
8602
+ select_edge(gobj, priv._context_edge_id);
8603
+ request_unlink_edge(gobj);
8604
+ }
8605
+ break;
8606
+ default:
8607
+ log_error(`${gobj_short_name(gobj)}: unknown context menu item: ${value}`);
8608
+ break;
8466
8609
  }
8467
8610
  }
8468
8611
 
@@ -8561,40 +8704,47 @@ function copy_size_to_nodes(gobj, same_topic_only)
8561
8704
  }
8562
8705
  }
8563
8706
 
8707
+ /*
8708
+ * What a reset forgets, by what it resets. A node's own look is
8709
+ * its size, its colours, its line width and its figure; its ports
8710
+ * have their own reset. The POSITION is not a property and no reset
8711
+ * touches it: it is where somebody put the node.
8712
+ */
8713
+ const NODE_STYLE_KEYS = ["size", "fill", "stroke", "lineWidth", "node_shape"];
8714
+ const PORT_STYLE_KEYS = ["portR", "port_sizes", "port_shapes", "port_shape"];
8715
+
8564
8716
  /************************************************************
8565
- * Forget every SAVED size: the card sizes, the port radii and the
8566
- * per-port radii, per node and as topic defaults (`resize all`),
8567
- * for one topic or for all of them. The cards go back to the
8568
- * library's defaults on the spot, and Save writes the cleared
8569
- * entries -- a size nobody chose is not written back (see
8570
- * update_geometry). Positions and edge styles are not touched:
8571
- * they are a different choice.
8717
+ * Forget these saved keys -- on the node of the menu alone
8718
+ * ('this'), or on every node of its topic and in the topic's
8719
+ * defaults ('same_topic'), or everywhere ('all'). Both copies of
8720
+ * an entry are stripped: the one in `_graph_properties` (which
8721
+ * Save writes, folded nodes included) and the one the node
8722
+ * carries. Returns the ids of the nodes on screen to reshape.
8572
8723
  ************************************************************/
8573
- function reset_sizes(gobj, same_topic_only)
8724
+ function forget_saved_node_keys(gobj, nd, scope, keys)
8574
8725
  {
8575
8726
  let priv = gobj.priv;
8576
8727
  let graph = priv.graph;
8577
-
8578
- let topic = "";
8579
- if(same_topic_only) {
8580
- let nd = priv._context_node_id? graph.getNodeData(priv._context_node_id) : null;
8581
- if(!nd || !nd.data || !nd.data.topic_name) {
8582
- log_error(`${gobj_short_name(gobj)}: reset topic sizes without a node`);
8583
- return;
8584
- }
8585
- topic = nd.data.topic_name;
8586
- }
8587
- const SIZE_KEYS = ["size", "portR", "port_sizes"];
8588
8728
  let strip = (props) => {
8589
8729
  if(!is_object(props)) {
8590
8730
  return;
8591
8731
  }
8592
- for(let k of SIZE_KEYS) {
8732
+ for(let k of keys) {
8593
8733
  delete props[k];
8594
8734
  }
8595
8735
  };
8596
8736
 
8597
- /* The saved entries, on screen or folded away. */
8737
+ if(scope === 'this') {
8738
+ strip(node_props_entry(gobj, nd));
8739
+ strip(nd.data.graph_props);
8740
+ if(nd.data.record) {
8741
+ strip(nd.data.record._geometry);
8742
+ }
8743
+ return [nd.id];
8744
+ }
8745
+
8746
+ let topic = (scope === 'same_topic')? nd.data.topic_name : "";
8747
+
8598
8748
  for(const [topic_name, props] of Object.entries(priv._graph_properties || {})) {
8599
8749
  if(topic && topic_name !== topic) {
8600
8750
  continue;
@@ -8607,25 +8757,191 @@ function reset_sizes(gobj, same_topic_only)
8607
8757
  strip(props.defaults);
8608
8758
  }
8609
8759
 
8610
- /* The nodes on screen: their own copy of the entry, then the shape. */
8611
8760
  let ids = [];
8612
- for(let nd of (graph.getNodeData() || [])) {
8613
- if(!nd || !nd.data || !nd.data.desc || is_more_node(nd)) {
8761
+ for(let n of (graph.getNodeData() || [])) {
8762
+ if(!n || !n.data || !n.data.desc || is_more_node(n)) {
8614
8763
  continue;
8615
8764
  }
8616
- if(topic && nd.data.topic_name !== topic) {
8765
+ if(topic && n.data.topic_name !== topic) {
8617
8766
  continue;
8618
8767
  }
8619
- strip(nd.data.graph_props);
8620
- if(nd.data.record) {
8621
- strip(nd.data.record._geometry);
8768
+ strip(n.data.graph_props);
8769
+ if(n.data.record) {
8770
+ strip(n.data.record._geometry);
8622
8771
  }
8623
- ids.push(nd.id);
8772
+ ids.push(n.id);
8624
8773
  }
8774
+ return ids;
8775
+ }
8776
+
8777
+ /* The nodes are reshaped from what is left saved, and Save writes
8778
+ * the cleared entries: a value nobody chose is not written back
8779
+ * (see update_geometry). */
8780
+ function reshape_after_reset(gobj, ids)
8781
+ {
8782
+ let graph = gobj.priv.graph;
8625
8783
  reshape_nodes(gobj, ids);
8626
8784
  graph.draw().then(() => {
8627
8785
  update_resize_handles_position(gobj);
8628
8786
  update_port_resize_handles_position(gobj);
8787
+ update_port_icon_position(gobj);
8788
+ mark_graph_dirty(gobj);
8789
+ });
8790
+ }
8791
+
8792
+ /************************************************************
8793
+ * Forget the saved look of nodes (NODE_STYLE_KEYS), back to the
8794
+ * library's: this node, its topic's, or all of them. A figure
8795
+ * saved as a square before the circle became the default goes
8796
+ * back to the default this way.
8797
+ ************************************************************/
8798
+ function reset_nodes(gobj, scope)
8799
+ {
8800
+ let priv = gobj.priv;
8801
+ let nd = priv._context_node_id? priv.graph.getNodeData(priv._context_node_id) : null;
8802
+ if(!nd || !nd.data || !nd.data.topic_name) {
8803
+ log_error(`${gobj_short_name(gobj)}: reset nodes without a node`);
8804
+ return;
8805
+ }
8806
+ reshape_after_reset(gobj, forget_saved_node_keys(gobj, nd, scope, NODE_STYLE_KEYS));
8807
+ }
8808
+
8809
+ /************************************************************
8810
+ * Forget the saved SHAPE and RADIUS of ports, back to the
8811
+ * library's own:
8812
+ * 'this' the port of the menu, on its node only
8813
+ * 'same_topic' every port of the node's topic, with the
8814
+ * topic's port defaults
8815
+ * 'all' every port of every topic, likewise
8816
+ * The node's own look is not touched: that is `reset node`.
8817
+ ************************************************************/
8818
+ function reset_ports(gobj, scope)
8819
+ {
8820
+ let priv = gobj.priv;
8821
+ let graph = priv.graph;
8822
+
8823
+ let node_id = priv._context_node_id;
8824
+ let port_key = priv._context_port_key;
8825
+ let nd = node_id? graph.getNodeData(node_id) : null;
8826
+ if(!nd || !nd.data || !nd.data.topic_name || !port_key) {
8827
+ log_error(`${gobj_short_name(gobj)}: reset ports without a port`);
8828
+ return;
8829
+ }
8830
+
8831
+ if(scope !== 'this') {
8832
+ reshape_after_reset(gobj, forget_saved_node_keys(gobj, nd, scope, PORT_STYLE_KEYS));
8833
+ return;
8834
+ }
8835
+
8836
+ /* One port: its entries in the per-port maps, not the maps. */
8837
+ let strip_port = (props) => {
8838
+ if(!is_object(props)) {
8839
+ return;
8840
+ }
8841
+ for(let k of ["port_sizes", "port_shapes"]) {
8842
+ if(is_object(props[k])) {
8843
+ delete props[k][port_key];
8844
+ if(Object.keys(props[k]).length === 0) {
8845
+ delete props[k];
8846
+ }
8847
+ }
8848
+ }
8849
+ };
8850
+ strip_port(node_props_entry(gobj, nd));
8851
+ strip_port(nd.data.graph_props);
8852
+ if(nd.data.record) {
8853
+ strip_port(nd.data.record._geometry);
8854
+ }
8855
+ reshape_after_reset(gobj, [node_id]);
8856
+ }
8857
+
8858
+ /************************************************************
8859
+ * Forget the saved style of edges, back to what they inherit:
8860
+ * 'this' the edge of the menu, its own entry
8861
+ * 'same_type' every edge of its hook in its parent topic,
8862
+ * with that hook's default
8863
+ * 'all' every edge, with every edge default
8864
+ * The same three scopes as the edge properties popover.
8865
+ ************************************************************/
8866
+ function reset_edges(gobj, scope)
8867
+ {
8868
+ let priv = gobj.priv;
8869
+ let graph = priv.graph;
8870
+
8871
+ let ed = priv._context_edge_id? graph.getEdgeData(priv._context_edge_id) : null;
8872
+ if(!ed || !ed.data || !ed.data.parent_topic) {
8873
+ log_error(`${gobj_short_name(gobj)}: reset edges without an edge`);
8874
+ return;
8875
+ }
8876
+ let src = ed.data;
8877
+ let hits = (d) => {
8878
+ if(!d || !d.parent_topic) {
8879
+ return false; /* a `+N` chip's line */
8880
+ }
8881
+ if(scope === 'this') {
8882
+ return edge_key_of(d) === edge_key_of(src) && d.parent_topic === src.parent_topic;
8883
+ }
8884
+ if(scope === 'same_type') {
8885
+ return d.parent_topic === src.parent_topic && d.hook_name === src.hook_name;
8886
+ }
8887
+ return true;
8888
+ };
8889
+
8890
+ /* The saved entries, on screen or folded away. */
8891
+ for(const [topic_name, props] of Object.entries(priv._graph_properties || {})) {
8892
+ if(!is_object(props)) {
8893
+ continue;
8894
+ }
8895
+ if(scope !== 'all' && topic_name !== src.parent_topic) {
8896
+ continue;
8897
+ }
8898
+ if(is_object(props.edges)) {
8899
+ for(let key of Object.keys(props.edges)) {
8900
+ let hook_name = key.split(":")[0];
8901
+ if(scope === 'this' && key !== edge_key_of(src)) {
8902
+ continue;
8903
+ }
8904
+ if(scope === 'same_type' && hook_name !== src.hook_name) {
8905
+ continue;
8906
+ }
8907
+ delete props.edges[key];
8908
+ }
8909
+ if(Object.keys(props.edges).length === 0) {
8910
+ delete props.edges;
8911
+ }
8912
+ }
8913
+ if(is_object(props.defaults)) {
8914
+ if(scope === 'same_type' && is_object(props.defaults.edge_styles)) {
8915
+ delete props.defaults.edge_styles[src.hook_name];
8916
+ if(Object.keys(props.defaults.edge_styles).length === 0) {
8917
+ delete props.defaults.edge_styles;
8918
+ }
8919
+ } else if(scope === 'all') {
8920
+ delete props.defaults.edge_styles;
8921
+ delete props.defaults.edge_style;
8922
+ }
8923
+ }
8924
+ }
8925
+
8926
+ /* The edges on screen, restyled from what is left. */
8927
+ let updates = [];
8928
+ for(let e of (graph.getEdgeData() || [])) {
8929
+ if(!e || !hits(e.data)) {
8930
+ continue;
8931
+ }
8932
+ let style = edge_style_of(gobj, e.data);
8933
+ updates.push({
8934
+ id: e.id,
8935
+ style: {lineWidth: style.lineWidth, stroke: style.stroke},
8936
+ data: Object.assign({}, e.data, {themed_default: style.themed_default}),
8937
+ });
8938
+ }
8939
+
8940
+ deselect_edge(gobj);
8941
+ if(updates.length > 0) {
8942
+ graph.updateEdgeData(updates);
8943
+ }
8944
+ graph.draw().then(() => {
8629
8945
  mark_graph_dirty(gobj);
8630
8946
  });
8631
8947
  }