linny-r 1.7.4 → 1.8.1

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.
@@ -501,6 +501,9 @@ class GUIController extends Controller {
501
501
 
502
502
  // Visible draggable dialogs are sorted by their z-index.
503
503
  this.dr_dialog_order = [];
504
+
505
+ // Record of message that was overridden by more important message.
506
+ this.old_info_line = null;
504
507
  }
505
508
 
506
509
  get color() {
@@ -617,7 +620,8 @@ class GUIController extends Controller {
617
620
  UI.updateButtons();
618
621
  }
619
622
  });
620
- this.buttons.solve.addEventListener('click', () => VM.solveModel());
623
+ this.buttons.solve.addEventListener('click',
624
+ (event) => VM.solveModel(event.altKey));
621
625
  this.buttons.stop.addEventListener('click', () => VM.halt());
622
626
  this.buttons.reset.addEventListener('click', () => UI.resetModel());
623
627
 
@@ -905,7 +909,7 @@ class GUIController extends Controller {
905
909
  // Reset the Virtual Machine.
906
910
  VM.reset();
907
911
  this.updateIssuePanel();
908
- this.setMessage('');
912
+ this.clearStatusLine();
909
913
  this.updateButtons();
910
914
  // Undoable operations no longer apply!
911
915
  UNDO_STACK.clear();
@@ -1729,8 +1733,8 @@ class GUIController extends Controller {
1729
1733
  }
1730
1734
  for(let i = fc.sub_clusters.length-1; i >= 0; i--) {
1731
1735
  const obj = fc.sub_clusters[i];
1732
- // NOTE: ignore cluster that is being dragged, so that a cluster it is
1733
- // being dragged over will be detected instead
1736
+ // NOTE: Ignore cluster that is being dragged, so that a cluster
1737
+ // it is being dragged over will be detected instead.
1734
1738
  if(obj != this.dragged_node &&
1735
1739
  obj.containsPoint(this.mouse_x, this.mouse_y)) {
1736
1740
  this.on_cluster = obj;
@@ -1738,12 +1742,13 @@ class GUIController extends Controller {
1738
1742
  break;
1739
1743
  }
1740
1744
  }
1741
- // unset and redraw target cluster if cursor no longer over it
1745
+ // Unset and redraw target cluster if cursor no longer over it.
1742
1746
  if(!this.on_cluster && this.target_cluster) {
1743
1747
  const c = this.target_cluster;
1744
1748
  this.target_cluster = null;
1745
1749
  UI.paper.drawCluster(c);
1746
- // NOTE: element is persistent, so semi-transparency must also be undone
1750
+ // NOTE: Element is persistent, so semi-transparency must also be
1751
+ // undone.
1747
1752
  c.shape.element.setAttribute('opacity', 1);
1748
1753
  }
1749
1754
  for(let i = fc.notes.length-1; i >= 0; i--) {
@@ -1754,15 +1759,15 @@ class GUIController extends Controller {
1754
1759
  }
1755
1760
  }
1756
1761
  if(this.active_button === this.buttons.link && this.linking_node) {
1757
- // Draw red dotted line from linking node to cursor
1762
+ // Draw red dotted line from linking node to cursor.
1758
1763
  this.paper.dragLineToCursor(this.linking_node, this.mouse_x, this.mouse_y);
1759
1764
  } else if(this.start_sel_x >= 0 && this.start_sel_y >= 0) {
1760
- // Draw selecting rectangle in red dotted lines
1765
+ // Draw selecting rectangle in red dotted lines.
1761
1766
  this.paper.dragRectToCursor(this.start_sel_x, this.start_sel_y,
1762
1767
  this.mouse_x, this.mouse_y);
1763
1768
  } else if(this.active_button === this.buttons.constraint &&
1764
1769
  this.constraining_node) {
1765
- // Draw red dotted line from constraining node to cursor
1770
+ // Draw red dotted line from constraining node to cursor.
1766
1771
  this.paper.dragLineToCursor(this.constraining_node,
1767
1772
  this.mouse_x, this.mouse_y);
1768
1773
  } else if(this.dragged_node) {
@@ -1770,14 +1775,14 @@ class GUIController extends Controller {
1770
1775
  this.mouse_y - this.move_dy - this.dragged_node.y);
1771
1776
  }
1772
1777
  let cr = 'pointer';
1773
- // NOTE: first check ON_CONSTRAINT because constraint thumbnails overlap
1774
- // with nodes
1778
+ // NOTE: First check ON_CONSTRAINT because constraint thumbnails overlap
1779
+ // with nodes.
1775
1780
  if(this.on_constraint) {
1776
1781
  DOCUMENTATION_MANAGER.update(this.on_constraint, e.shiftKey);
1777
- // NOTE: skip the "on node" check if the node is being dragged
1782
+ // NOTE: Skip the "on node" check if the node is being dragged.
1778
1783
  } else if(this.on_node && this.on_node !== this.dragged_node) {
1779
1784
  if((this.active_button === this.buttons.link) && this.linking_node) {
1780
- // Cannot link process to process
1785
+ // Cannot link process to process.
1781
1786
  cr = (MODEL.canLink(this.linking_node, this.on_node) ?
1782
1787
  'crosshair' : 'not-allowed');
1783
1788
  } else if(this.active_button === this.buttons.constraint) {
@@ -1785,21 +1790,21 @@ class GUIController extends Controller {
1785
1790
  cr = (this.constraining_node.canConstrain(this.on_node) ?
1786
1791
  'crosshair' : 'not-allowed');
1787
1792
  } else if(!this.on_node.hasBounds) {
1788
- // Products can only constrain when they have bounds
1793
+ // Products can only constrain when they have bounds.
1789
1794
  cr = 'not-allowed';
1790
1795
  }
1791
1796
  }
1792
- // NOTE: do not overwite status line when cursor is on a block arrow
1797
+ // NOTE: Do not overwite status line when cursor is on a block arrow.
1793
1798
  if(!this.on_block_arrow) {
1794
1799
  DOCUMENTATION_MANAGER.update(this.on_node, e.shiftKey);
1795
1800
  }
1796
1801
  } else if(this.on_note) {
1797
- // When shift-moving over a note, show the model's documentation
1802
+ // When shift-moving over a note, show the model's documentation.
1798
1803
  DOCUMENTATION_MANAGER.update(MODEL, e.shiftKey);
1799
1804
  } else {
1800
1805
  if((this.active_button === this.buttons.link && this.linking_node) ||
1801
1806
  (this.active_button === this.buttons.constraint && this.constraining_node)) {
1802
- // Cannot link to clusters or notes
1807
+ // Cannot link to clusters or notes.
1803
1808
  cr = (this.on_cluster || this.on_note ? 'not-allowed' : 'crosshair');
1804
1809
  } else if(!this.on_note && !this.on_constraint && !this.on_link &&
1805
1810
  !this.on_cluster_edge) {
@@ -1815,38 +1820,42 @@ class GUIController extends Controller {
1815
1820
  }
1816
1821
  }
1817
1822
  // When dragging selection that contains a process, change cursor to
1818
- // indicate that selected process(es) will be moved into the cluster
1819
- if(this.dragged_node && this.on_cluster) {
1820
- cr = 'cell';
1821
- this.target_cluster = this.on_cluster;
1822
- // Redraw the target cluster so it will appear on top (and highlighted)
1823
- UI.paper.drawCluster(this.target_cluster);
1823
+ // indicate that selected process(es) will be moved into the cluster.
1824
+ if(this.dragged_node) {
1825
+ if(this.on_cluster) {
1826
+ cr = 'cell';
1827
+ this.target_cluster = this.on_cluster;
1828
+ // Redraw the target cluster so it will appear on top (and highlighted).
1829
+ UI.paper.drawCluster(this.target_cluster);
1830
+ } else {
1831
+ cr = 'grab';
1832
+ }
1824
1833
  }
1825
1834
  }
1826
1835
  this.paper.container.style.cursor = cr;
1827
1836
  }
1828
1837
 
1829
1838
  mouseDown(e) {
1830
- // Responds to mousedown event in model diagram area
1839
+ // Responds to mousedown event in model diagram area.
1831
1840
  // In case mouseup event occurred outside drawing area,ignore this
1832
- // mousedown event, so that only the mouseup will be processed
1841
+ // mousedown event, so that only the mouseup will be processed.
1833
1842
  if(this.start_sel_x >= 0 && this.start_sel_y >= 0) return;
1834
1843
  const cp = this.paper.cursorPosition(e.pageX, e.pageY);
1835
1844
  this.mouse_down_x = cp[0];
1836
1845
  this.mouse_down_y = cp[1];
1837
1846
  // De-activate "stay active" buttons if dysfunctional, or if SHIFT,
1838
- // ALT or CTRL is pressed
1847
+ // ALT or CTRL is pressed.
1839
1848
  if((e.shiftKey || e.altKey || e.ctrlKey ||
1840
1849
  this.on_note || this.on_cluster || this.on_link || this.on_constraint ||
1841
1850
  (this.on_node && this.active_button !== this.buttons.link &&
1842
1851
  this.active_button !== this.buttons.constraint)) && this.stayActive) {
1843
1852
  resetActiveButton();
1844
1853
  }
1845
- // NOTE: only left button is detected (browser catches right menu button)
1854
+ // NOTE: Only left button is detected (browser catches right menu button).
1846
1855
  if(e.ctrlKey) {
1847
1856
  // Remove clicked item from selection
1848
1857
  if(MODEL.selection) {
1849
- // NOTE: first check constraints -- see mouseMove() for motivation
1858
+ // NOTE: First check constraints -- see mouseMove() for motivation.
1850
1859
  if(this.on_constraint) {
1851
1860
  if(MODEL.selection.indexOf(this.on_constraint) >= 0) {
1852
1861
  MODEL.deselect(this.on_constraint);
@@ -1885,7 +1894,7 @@ class GUIController extends Controller {
1885
1894
  } // END IF Ctrl
1886
1895
 
1887
1896
  // Clear selection unless SHIFT pressed or mouseDown while hovering
1888
- // over a SELECTED node or link
1897
+ // over a SELECTED node or link.
1889
1898
  if(!(e.shiftKey ||
1890
1899
  (this.on_node && MODEL.selection.indexOf(this.on_node) >= 0) ||
1891
1900
  (this.on_cluster && MODEL.selection.indexOf(this.on_cluster) >= 0) ||
@@ -1897,7 +1906,7 @@ class GUIController extends Controller {
1897
1906
  }
1898
1907
 
1899
1908
  // If one of the top six sidebar buttons is active, prompt for new node
1900
- // (not link or constraint)
1909
+ // (not link or constraint).
1901
1910
  if(this.active_button && this.active_button !== this.buttons.link &&
1902
1911
  this.active_button !== this.buttons.constraint) {
1903
1912
  this.add_x = this.mouse_x;
@@ -1939,16 +1948,16 @@ class GUIController extends Controller {
1939
1948
  }
1940
1949
 
1941
1950
  // ALT key pressed => open properties dialog if cursor hovers over
1942
- // some element
1951
+ // some element.
1943
1952
  if(e.altKey) {
1944
- // NOTE: first check constraints -- see mouseMove() for motivation
1953
+ // NOTE: First check constraints -- see mouseMove() for motivation.
1945
1954
  if(this.on_constraint) {
1946
1955
  this.showConstraintPropertiesDialog(this.on_constraint);
1947
1956
  } else if(this.on_node) {
1948
1957
  if(this.on_node instanceof Process) {
1949
1958
  this.showProcessPropertiesDialog(this.on_node);
1950
1959
  } else if(e.shiftKey) {
1951
- // Shift-Alt on product is like Shift-Double-click
1960
+ // Shift-Alt on product is like Shift-Double-click.
1952
1961
  this.showReplaceProductDialog(this.on_node);
1953
1962
  } else {
1954
1963
  this.showProductPropertiesDialog(this.on_node);
@@ -1960,7 +1969,7 @@ class GUIController extends Controller {
1960
1969
  } else if(this.on_link) {
1961
1970
  this.showLinkPropertiesDialog(this.on_link);
1962
1971
  }
1963
- // NOTE: first check constraints -- see mouseMove() for motivation
1972
+ // NOTE: First check constraints -- see mouseMove() for motivation.
1964
1973
  } else if(this.on_constraint) {
1965
1974
  MODEL.select(this.on_constraint);
1966
1975
  } else if(this.on_note) {
@@ -1969,17 +1978,17 @@ class GUIController extends Controller {
1969
1978
  this.move_dy = this.mouse_y - this.on_note.y;
1970
1979
  MODEL.select(this.on_note);
1971
1980
  UNDO_STACK.push('move', this.dragged_node, true);
1972
- // Cursor on node => add link or constraint, or start moving
1981
+ // Cursor on node => add link or constraint, or start moving.
1973
1982
  } else if(this.on_node) {
1974
1983
  if(this.active_button === this.buttons.link) {
1975
1984
  this.linking_node = this.on_node;
1976
1985
  // NOTE: return without updating buttons
1977
1986
  return;
1978
1987
  } else if(this.active_button === this.buttons.constraint) {
1979
- // Allow constraints only on nodes having upper bounds defined
1988
+ // Allow constraints only on nodes having upper bounds defined.
1980
1989
  if(this.on_node.upper_bound.defined) {
1981
1990
  this.constraining_node = this.on_node;
1982
- // NOTE: here, too, return without updating buttons
1991
+ // NOTE: Here, too, return without updating buttons.
1983
1992
  return;
1984
1993
  }
1985
1994
  } else {
@@ -1987,7 +1996,7 @@ class GUIController extends Controller {
1987
1996
  this.move_dx = this.mouse_x - this.on_node.x;
1988
1997
  this.move_dy = this.mouse_y - this.on_node.y;
1989
1998
  if(MODEL.selection.indexOf(this.on_node) < 0) MODEL.select(this.on_node);
1990
- // Pass dragged node for UNDO
1999
+ // Pass dragged node for UNDO.
1991
2000
  UNDO_STACK.push('move', this.dragged_node, true);
1992
2001
  }
1993
2002
  } else if(this.on_cluster) {
@@ -2006,23 +2015,23 @@ class GUIController extends Controller {
2006
2015
  }
2007
2016
 
2008
2017
  mouseUp(e) {
2009
- // Responds to mouseup event
2018
+ // Responds to mouseup event.
2010
2019
  const cp = this.paper.cursorPosition(e.pageX, e.pageY);
2011
2020
  this.mouse_up_x = cp[0];
2012
2021
  this.mouse_up_y = cp[1];
2013
- // First check whether user is selecting a rectangle
2022
+ // First check whether user is selecting a rectangle.
2014
2023
  if(this.start_sel_x >= 0 && this.start_sel_y >= 0) {
2015
2024
  // Clear previous selection unless user is adding to it (by still
2016
- // holding SHIFT button down)
2025
+ // holding SHIFT button down).
2017
2026
  if(!e.shiftKey) MODEL.clearSelection();
2018
- // Compute defining points of rectangle (top left and bottom right)
2027
+ // Compute defining points of rectangle (top left and bottom right).
2019
2028
  const
2020
2029
  tlx = Math.min(this.start_sel_x, this.mouse_up_x),
2021
2030
  tly = Math.min(this.start_sel_y, this.mouse_up_y),
2022
2031
  brx = Math.max(this.start_sel_x, this.mouse_up_x),
2023
2032
  bry = Math.max(this.start_sel_y, this.mouse_up_y);
2024
2033
  // If rectangle has size greater than 2x2 pixels, select all elements
2025
- // having their center inside the selection rectangle
2034
+ // having their center inside the selection rectangle.
2026
2035
  if(brx - tlx > 2 && bry - tly > 2) {
2027
2036
  const ol = [], fc = MODEL.focal_cluster;
2028
2037
  for(let i = 0; i < fc.processes.length; i++) {
@@ -2051,19 +2060,19 @@ class GUIController extends Controller {
2051
2060
  }
2052
2061
  for(let i in MODEL.links) if(MODEL.links.hasOwnProperty(i)) {
2053
2062
  const obj = MODEL.links[i];
2054
- // Only add a link if both its nodes are selected as well
2063
+ // Only add a link if both its nodes are selected as well.
2055
2064
  if(fc.linkInList(obj, ol)) {
2056
2065
  ol.push(obj);
2057
2066
  }
2058
2067
  }
2059
2068
  for(let i in MODEL.constraints) if(MODEL.constraints.hasOwnProperty(i)) {
2060
2069
  const obj = MODEL.constraints[i];
2061
- // Only add a constraint if both its nodes are selected as well
2070
+ // Only add a constraint if both its nodes are selected as well.
2062
2071
  if(fc.linkInList(obj, ol)) {
2063
2072
  ol.push(obj);
2064
2073
  }
2065
2074
  }
2066
- // Having compiled the object list, actually select them
2075
+ // Having compiled the object list, actually select them.
2067
2076
  MODEL.selectList(ol);
2068
2077
  this.paper.drawSelection(MODEL);
2069
2078
  }
@@ -2071,10 +2080,10 @@ class GUIController extends Controller {
2071
2080
  this.start_sel_y = -1;
2072
2081
  this.paper.hideDragRect();
2073
2082
 
2074
- // Then check whether user is drawing a flow link
2075
- // (by dragging its endpoint)
2083
+ // Then check whether user is drawing a flow link (by dragging its
2084
+ // endpoint).
2076
2085
  } else if(this.linking_node) {
2077
- // If so, check whether the cursor is over a node of the appropriate type
2086
+ // If so, check whether the cursor is over a node of the appropriate type.
2078
2087
  if(this.on_node && MODEL.canLink(this.linking_node, this.on_node)) {
2079
2088
  const obj = MODEL.addLink(this.linking_node, this.on_node);
2080
2089
  UNDO_STACK.push('add', obj);
@@ -2085,8 +2094,8 @@ class GUIController extends Controller {
2085
2094
  if(!this.stayActive) this.resetActiveButton();
2086
2095
  this.paper.hideDragLine();
2087
2096
 
2088
- // Then check whether user is drawing a constraint link
2089
- // (again: by dragging its endpoint)
2097
+ // Then check whether user is drawing a constraint link (again: by
2098
+ // dragging its endpoint).
2090
2099
  } else if(this.constraining_node) {
2091
2100
  if(this.on_node && this.constraining_node.canConstrain(this.on_node)) {
2092
2101
  // display constraint editor
@@ -2100,35 +2109,37 @@ class GUIController extends Controller {
2100
2109
  UI.drawDiagram(MODEL);
2101
2110
 
2102
2111
  // Then check whether the user is moving a node (possibly part of a
2103
- // larger selection)
2112
+ // larger selection).
2104
2113
  } else if(this.dragged_node) {
2105
2114
  // Always perform the move operation (this will do nothing if the
2106
- // cursor did not move)
2115
+ // cursor did not move).
2107
2116
  MODEL.moveSelection(
2108
2117
  this.mouse_up_x - this.mouse_x, this.mouse_up_y - this.mouse_y);
2109
- // @@TO DO: if on top of a cluster, move it there
2110
- // NOTE: cursor will always be over the selected cluster (while dragging)
2118
+ // Set cursor to pointer, as it should be on some node while dragging.
2119
+ this.paper.container.style.cursor = 'pointer';
2120
+ // @@TO DO: if on top of a cluster, move it there.
2121
+ // NOTE: Cursor will always be over the selected cluster (while dragging).
2111
2122
  if(this.on_cluster && !this.on_cluster.selected) {
2112
2123
  UNDO_STACK.push('drop', this.on_cluster);
2113
2124
  MODEL.dropSelectionIntoCluster(this.on_cluster);
2114
2125
  this.on_node = null;
2115
2126
  this.on_note = null;
2116
2127
  this.target_cluster = null;
2117
- // Redraw cluster to erase its "target corona"
2128
+ // Redraw cluster to erase its orange "target corona".
2118
2129
  UI.paper.drawCluster(this.on_cluster);
2119
2130
  }
2120
2131
 
2121
- // Check wether the cursor has been moved
2132
+ // Check wether the cursor has been moved.
2122
2133
  const
2123
2134
  absdx = Math.abs(this.mouse_down_x - this.mouse_x),
2124
2135
  absdy = Math.abs(this.mouse_down_y - this.mouse_y);
2125
- // If no *significant* move made, remove the move undo
2136
+ // If no *significant* move made, remove the move undo.
2126
2137
  if(absdx + absdy === 0) UNDO_STACK.pop('move');
2127
2138
  if(this.doubleClicked && absdx + absdy < 3) {
2128
2139
  // Double-clicking opens properties dialog, except for clusters;
2129
- // then "drill down", i.e., make the double-clicked cluster focal
2140
+ // then "drill down", i.e., make the double-clicked cluster focal.
2130
2141
  if(this.dragged_node instanceof Cluster) {
2131
- // NOTE: bottom & right cluster edges remain sensitive!
2142
+ // NOTE: Bottom & right cluster edges remain sensitive!
2132
2143
  if(this.on_cluster_edge) {
2133
2144
  this.showClusterPropertiesDialog(this.dragged_node);
2134
2145
  } else {
@@ -2138,7 +2149,7 @@ class GUIController extends Controller {
2138
2149
  if(e.shiftKey) {
2139
2150
  // Shift-double-clicking on a *product* prompts for "remapping"
2140
2151
  // the product position to another product (and potentially
2141
- // deleting the original one if it has no more occurrences)
2152
+ // deleting the original one if it has no more occurrences).
2142
2153
  this.showReplaceProductDialog(this.dragged_node);
2143
2154
  } else {
2144
2155
  this.showProductPropertiesDialog(this.dragged_node);
@@ -2151,7 +2162,7 @@ class GUIController extends Controller {
2151
2162
  }
2152
2163
  this.dragged_node = null;
2153
2164
 
2154
- // Then check whether the user is clicking on a link
2165
+ // Then check whether the user is clicking on a link.
2155
2166
  } else if(this.on_link) {
2156
2167
  if(this.doubleClicked) {
2157
2168
  this.showLinkPropertiesDialog(this.on_link);
@@ -2167,8 +2178,8 @@ class GUIController extends Controller {
2167
2178
  }
2168
2179
 
2169
2180
  dragOver(e) {
2170
- // Accepts products that are dragged from the Finder and do not have
2171
- // a placeholder in the focal cluster
2181
+ // Accept products that are dragged from the Finder and do not have
2182
+ // a placeholder in the focal cluster.
2172
2183
  this.updateCursorPosition(e);
2173
2184
  const p = MODEL.products[e.dataTransfer.getData('text')];
2174
2185
  if(p && MODEL.focal_cluster.indexOfProduct(p) < 0) e.preventDefault();
@@ -2176,7 +2187,7 @@ class GUIController extends Controller {
2176
2187
 
2177
2188
  drop(e) {
2178
2189
  // Adds a product that is dragged from the Finder to the focal cluster
2179
- // at the cursor position if it does not have a placeholder yet
2190
+ // at the cursor position if it does not have a placeholder yet.
2180
2191
  const p = MODEL.products[e.dataTransfer.getData('text')];
2181
2192
  if(p && MODEL.focal_cluster.indexOfProduct(p) < 0) {
2182
2193
  e.preventDefault();
@@ -2185,7 +2196,7 @@ class GUIController extends Controller {
2185
2196
  this.selectNode(p);
2186
2197
  this.drawDiagram(MODEL);
2187
2198
  }
2188
- // NOTE: update afterwards, as the modeler may target a precise (X, Y)
2199
+ // NOTE: Update afterwards, as the modeler may target a precise (X, Y).
2189
2200
  this.updateCursorPosition(e);
2190
2201
  }
2191
2202
 
@@ -2194,14 +2205,16 @@ class GUIController extends Controller {
2194
2205
  //
2195
2206
 
2196
2207
  checkModals(e) {
2197
- // Respond to Escape, Enter and shortcut keys
2208
+ // Respond to Escape, Enter and shortcut keys.
2198
2209
  const
2199
2210
  ttype = e.target.type,
2200
2211
  ttag = e.target.tagName,
2201
2212
  modals = document.getElementsByClassName('modal');
2202
- // Modal dialogs: hide on ESC and move to next input on ENTER
2213
+ // Modal dialogs: hide on ESC and move to next input on ENTER.
2203
2214
  let maxz = 0,
2204
- topmod = null;
2215
+ topmod = null,
2216
+ code = e.code,
2217
+ alt = e.altKey;
2205
2218
  for(let i = 0; i < modals.length; i++) {
2206
2219
  const
2207
2220
  m = modals[i],
@@ -2212,11 +2225,11 @@ class GUIController extends Controller {
2212
2225
  maxz = z;
2213
2226
  }
2214
2227
  }
2215
- // NOTE: consider only the top modal (if any)
2216
- if(e.keyCode === 27) {
2228
+ // NOTE: Consider only the top modal (if any is showing).
2229
+ if(code === 'Escape') {
2217
2230
  e.stopImmediatePropagation();
2218
2231
  if(topmod) topmod.style.display = 'none';
2219
- } else if(e.keyCode === 13 && ttype !== 'textarea') {
2232
+ } else if(code === 'Enter' && ttype !== 'textarea') {
2220
2233
  e.preventDefault();
2221
2234
  if(topmod) {
2222
2235
  const inp = Array.from(topmod.getElementsByTagName('input'));
@@ -2225,73 +2238,78 @@ class GUIController extends Controller {
2225
2238
  if(i < inp.length) {
2226
2239
  inp[i].focus();
2227
2240
  } else if('constraint-modal xp-clusters-modal'.indexOf(topmod.id) >= 0) {
2228
- // NOTE: constraint modal and "ignore clusters" modal must NOT close
2229
- // when Enter is pressed; just de-focus the input field
2241
+ // NOTE: Constraint modal and "ignore clusters" modal must NOT close
2242
+ // when Enter is pressed, but only de-focus the input field.
2230
2243
  e.target.blur();
2231
2244
  } else {
2232
2245
  const btns = topmod.getElementsByClassName('ok-btn');
2233
2246
  if(btns.length > 0) btns[0].dispatchEvent(new Event('click'));
2234
2247
  }
2235
2248
  } else if(this.dr_dialog_order.length > 0) {
2236
- // Send ENTER key event to the top draggable dialog
2249
+ // Send ENTER key event to the top draggable dialog.
2237
2250
  const last = this.dr_dialog_order.length - 1;
2238
2251
  if(last >= 0) {
2239
2252
  const mgr = window[this.dr_dialog_order[last].dataset.manager];
2240
2253
  if(mgr && 'enterKey' in mgr) mgr.enterKey();
2241
2254
  }
2242
2255
  }
2243
- } else if(e.keyCode === 8 &&
2256
+ } else if(code === 'Backspace' &&
2244
2257
  ttype !== 'text' && ttype !== 'password' && ttype !== 'textarea') {
2245
- // Prevent backspace to be interpreted (by FireFox) as "go back in browser"
2258
+ // Prevent backspace to be interpreted (by FireFox) as "go back in browser".
2246
2259
  e.preventDefault();
2247
2260
  } else if(ttag === 'BODY') {
2248
- // Constraint Editor accepts arrow keys
2261
+ // Constraint Editor accepts arrow keys.
2249
2262
  if(topmod && topmod.id === 'constraint-modal') {
2250
- if([37, 38, 39, 40].indexOf(e.keyCode) >= 0) {
2263
+ if(code.startsWith('Arrow')) {
2251
2264
  e.preventDefault();
2252
2265
  CONSTRAINT_EDITOR.arrowKey(e);
2253
2266
  return;
2254
2267
  }
2255
2268
  }
2256
- // Up and down arrow keys
2257
- if([38, 40].indexOf(e.keyCode) >= 0) {
2269
+ // Up and down arrow keys.
2270
+ if(code === 'ArrowUp' || code === 'ArrowDown') {
2258
2271
  e.preventDefault();
2259
- // Send event to the top draggable dialog
2272
+ // Send event to the top draggable dialog.
2260
2273
  const last = this.dr_dialog_order.length - 1;
2261
2274
  if(last >= 0) {
2262
2275
  const mgr = window[this.dr_dialog_order[last].dataset.manager];
2263
- // NOTE: pass key direction as -1 for UP and +1 for DOWN
2276
+ // NOTE: Pass key direction as -1 for UP and +1 for DOWN.
2264
2277
  if(mgr && 'upDownKey' in mgr) mgr.upDownKey(e.keyCode - 39);
2265
2278
  }
2266
2279
  }
2267
- // end, home, Left and right arrow keys
2268
- if([35, 36, 37, 39].indexOf(e.keyCode) >= 0) e.preventDefault();
2269
- if(e.keyCode === 35) {
2280
+ // End, Home, and left and right arrow keys.
2281
+ if(code === 'End') {
2282
+ e.preventDefault();
2270
2283
  MODEL.t = MODEL.end_period - MODEL.start_period + 1;
2271
2284
  UI.updateTimeStep();
2272
2285
  UI.drawDiagram(MODEL);
2273
- } else if(e.keyCode === 36) {
2286
+ } else if(code === 'Home') {
2287
+ e.preventDefault();
2274
2288
  MODEL.t = 1;
2275
2289
  UI.updateTimeStep();
2276
2290
  UI.drawDiagram(MODEL);
2277
- } else if(e.keyCode === 37) {
2291
+ } else if(code === 'ArrowLeft') {
2292
+ e.preventDefault();
2278
2293
  this.stepBack(e);
2279
- } else if(e.keyCode === 39) {
2294
+ } else if(code === 'ArrowRight') {
2295
+ e.preventDefault();
2280
2296
  this.stepForward(e);
2281
- } else if(e.altKey && [67, 77].indexOf(e.keyCode) >= 0) {
2282
- // Special shortcut keys for "clone selection" and "model settings"
2297
+ } else if(alt && code === 'KeyR') {
2298
+ // Alt-R means: run to diagnose infeasible/unbounded problem.
2299
+ VM.solveModel(true);
2300
+ } else if(alt && ['KeyC', 'KeyM'].indexOf(code) >= 0) {
2301
+ // Special shortcut keys for "clone selection" and "model settings".
2283
2302
  const be = new Event('click');
2284
- be.altKey = true;
2285
- if(e.keyCode === 67) {
2303
+ if(code === 'KeyC') {
2286
2304
  this.buttons.clone.dispatchEvent(be);
2287
2305
  } else {
2288
2306
  this.buttons.settings.dispatchEvent(be);
2289
2307
  }
2290
- } else if(!e.shiftKey && !e.altKey &&
2291
- (!topmod || [65, 67, 86].indexOf(e.keyCode) < 0)) {
2292
- // Interpret special keys as shortcuts unless a modal dialog is open
2293
- if(e.keyCode === 46) {
2294
- // DEL button => delete selection
2308
+ } else if(!e.shiftKey && !alt &&
2309
+ (!topmod || ['KeyA', 'KeyC', 'KeyV'].indexOf(code) < 0)) {
2310
+ // Interpret special keys as shortcuts unless a modal dialog is open.
2311
+ if(code === 'Delete') {
2312
+ // DEL button => delete selection.
2295
2313
  e.preventDefault();
2296
2314
  if(!this.hidden('constraint-modal')) {
2297
2315
  CONSTRAINT_EDITOR.deleteBoundLine();
@@ -2300,18 +2318,18 @@ class GUIController extends Controller {
2300
2318
  } else {
2301
2319
  this.buttons['delete'].dispatchEvent(new Event('click'));
2302
2320
  }
2303
- } else if (e.keyCode === 190 && (e.ctrlKey || e.metaKey)) {
2304
- // Ctrl-. (dot) moves entire diagram to upper-left corner
2321
+ } else if (code === 'Period' && (e.ctrlKey || e.metaKey)) {
2322
+ // Ctrl-. (dot) moves entire diagram to upper-left corner.
2305
2323
  e.preventDefault();
2306
2324
  this.paper.fitToSize();
2307
2325
  MODEL.alignToGrid();
2308
- } else if (e.keyCode >= 65 && e.keyCode <= 90 && (e.ctrlKey || e.metaKey)) {
2309
- // ALWAYS prevent browser to do respond to Ctrl-letter commands
2310
- // NOTE: this cannot prevent a new tab from opening on Ctrl-T
2326
+ } else if (code >= 'KeyA' && code <= 'KeyZ' && (e.ctrlKey || e.metaKey)) {
2327
+ // ALWAYS prevent browser to do respond to Ctrl-letter commands.
2328
+ // NOTE: This cannot prevent a new tab from opening on Ctrl-T.
2311
2329
  e.preventDefault();
2312
- let shortcut = String.fromCharCode(e.keyCode);
2330
+ let shortcut = code.substring(3);
2313
2331
  if(shortcut === 'Z' && e.shiftKey) {
2314
- // Interpret Shift-Ctrl-Z as Ctrl-Y (redo last undone operation)
2332
+ // Interpret Shift-Ctrl-Z as Ctrl-Y (redo last undone operation).
2315
2333
  shortcut = 'Y';
2316
2334
  }
2317
2335
  if(this.shortcuts.hasOwnProperty(shortcut)) {
@@ -2662,15 +2680,22 @@ class GUIController extends Controller {
2662
2680
  //
2663
2681
  // Informing the modeler via the status line
2664
2682
  //
2665
-
2683
+
2684
+ clearStatusLine() {
2685
+ // Clear message on the status line.
2686
+ this.info_line.innerHTML = '';
2687
+ UI.info_line.classList.remove(...UI.info_line.classList);
2688
+ }
2689
+
2666
2690
  setMessage(msg, type=null) {
2667
- // Displays message on infoline unless no type (= plain text) and some
2668
- // info, warning or error message is already displayed
2691
+ // Display `msg` on infoline unless no type (= plain text) and some
2692
+ // info, warning or error message is already displayed.
2669
2693
  super.setMessage(msg, type);
2670
2694
  const types = ['notification', 'warning', 'error'];
2671
2695
  let d = new Date(),
2672
2696
  t = d.getTime(),
2673
- dt = t - this.time_last_message,
2697
+ dt = t - this.time_last_message, // Time since display
2698
+ rt = this.message_display_time - dt, // Time remaining
2674
2699
  mti = types.indexOf(type),
2675
2700
  lmti = types.indexOf(this.last_message_type);
2676
2701
  if(type) {
@@ -2683,18 +2708,36 @@ class GUIController extends Controller {
2683
2708
  // When receiver is active, add message to its log.
2684
2709
  if(RECEIVER.active) RECEIVER.log(`[${now}] ${msg}`);
2685
2710
  }
2686
- // Display text only if previous message has "timed out" or was less
2687
- // urgent than this one.
2688
- if(lmti < 0 || mti > lmti || dt >= this.message_display_time) {
2711
+ if(mti === 1 && lmti === 2 && rt > 0) {
2712
+ // Queue warnings if an error message is still being displayed.
2713
+ setTimeout(() => {
2714
+ UI.info_line.innerHTML = msg;
2715
+ UI.info_line.classList.remove(...UI.info_line.classList);
2716
+ if(type) UI.info_line.classList.add(type);
2717
+ UI.updateIssuePanel();
2718
+ }, rt);
2719
+ } else if(lmti < 0 || mti > lmti || rt <= 0) {
2720
+ // Display text only if previous message has "timed out" or was less
2721
+ // urgent than this one.
2722
+ const override = mti === 2 && lmti === 1 && rt > 0;
2689
2723
  this.time_last_message = t;
2690
2724
  this.last_message_type = type;
2691
2725
  if(type) SOUNDS[type].play().catch(() => {
2692
2726
  console.log('NOTICE: Sounds will only play after first user action');
2693
2727
  });
2694
- const il = document.getElementById('info-line');
2695
- il.classList.remove(...types);
2696
- il.classList.add(type);
2697
- il.innerHTML = msg;
2728
+ if(override && !this.old_info_line) {
2729
+ // Set time-out to restore overridden warning.
2730
+ this.old_info_line = {msg: this.info_line.innerHTML, status: types[lmti]};
2731
+ setTimeout(() => {
2732
+ UI.info_line.innerHTML = UI.old_info_line.msg;
2733
+ UI.info_line.classList.add(UI.old_info_line.status);
2734
+ UI.old_info_line = null;
2735
+ UI.updateIssuePanel();
2736
+ }, this.message_display_time);
2737
+ }
2738
+ UI.info_line.classList.remove(...UI.info_line.classList);
2739
+ if(type) UI.info_line.classList.add(type);
2740
+ UI.info_line.innerHTML = msg;
2698
2741
  }
2699
2742
  }
2700
2743
 
@@ -2846,11 +2889,15 @@ class GUIController extends Controller {
2846
2889
  }
2847
2890
  }
2848
2891
  } else if(type === 'process' || type === 'product') {
2892
+ /* NOT CLEAR WHAT THIS CODE DOES, SO DISABLE IT
2849
2893
  if(this.dbl_clicked_node) {
2850
2894
  n = this.dbl_clicked_node;
2851
2895
  md = this.modals['add-' + type];
2852
2896
  this.dbl_clicked_node = null;
2853
2897
  } else {
2898
+ */
2899
+ if(true) { // added line
2900
+ this.dbl_clicked_node = null; // added line
2854
2901
  if(type === 'process') {
2855
2902
  md = this.modals['add-process'];
2856
2903
  nn = md.element('name').value;
@@ -2904,7 +2951,7 @@ class GUIController extends Controller {
2904
2951
  UNDO_STACK.pop();
2905
2952
  return false;
2906
2953
  }
2907
- // NOTE: pre-check if product exists
2954
+ // NOTE: Pre-check if product exists.
2908
2955
  const pp = MODEL.objectByName(nn);
2909
2956
  n = MODEL.addProduct(nn);
2910
2957
  if(n) {
@@ -2925,7 +2972,7 @@ class GUIController extends Controller {
2925
2972
  }
2926
2973
  if(n) {
2927
2974
  // If process, and X and Y are set, it exists; then if not in the
2928
- // focal cluster, ask whether to move it there
2975
+ // focal cluster, ask whether to move it there.
2929
2976
  if(n instanceof Process && (n.x !== 0 || n.y !== 0)) {
2930
2977
  if(n.cluster !== MODEL.focal_cluster) {
2931
2978
  this.confirmToMoveNode(n);
@@ -2943,9 +2990,9 @@ class GUIController extends Controller {
2943
2990
  MODEL.inferIgnoredEntities();
2944
2991
  if(n) {
2945
2992
  md.hide();
2946
- // Select the newly added entity
2947
- // NOTE: If the focal cluster was selected (via the top tool bar), it
2948
- // cannot be selected
2993
+ // Select the newly added entity.
2994
+ // NOTE: If the focal cluster was selected (via the top tool bar),
2995
+ // it cannot be selected.
2949
2996
  if(n !== MODEL.focal_cluster) this.selectNode(n);
2950
2997
  }
2951
2998
  }
@@ -3503,12 +3550,13 @@ console.log('HERE name conflicts', name_conflicts, mapping);
3503
3550
  md.element('block-length').value = model.block_length;
3504
3551
  md.element('look-ahead').value = model.look_ahead;
3505
3552
  md.element('time-limit').value = model.timeout_period;
3506
- this.setBox('settings-encrypt', model.encrypt);
3507
3553
  this.setBox('settings-decimal-comma', model.decimal_comma);
3508
3554
  this.setBox('settings-align-to-grid', model.align_to_grid);
3555
+ this.setBox('settings-block-arrows', model.show_block_arrows);
3556
+ this.setBox('settings-diagnose', MODEL.always_diagnose);
3509
3557
  this.setBox('settings-cost-prices', model.infer_cost_prices);
3510
3558
  this.setBox('settings-report-results', model.report_results);
3511
- this.setBox('settings-block-arrows', model.show_block_arrows);
3559
+ this.setBox('settings-encrypt', model.encrypt);
3512
3560
  md.show('name');
3513
3561
  }
3514
3562
 
@@ -3563,6 +3611,12 @@ console.log('HERE name conflicts', name_conflicts, mapping);
3563
3611
  model.report_results = UI.boxChecked('settings-report-results');
3564
3612
  model.encrypt = UI.boxChecked('settings-encrypt');
3565
3613
  model.decimal_comma = UI.boxChecked('settings-decimal-comma');
3614
+ MODEL.always_diagnose = this.boxChecked('settings-diagnose');
3615
+ // Notify modeler that diagnosis changes the value of +INF.
3616
+ if(MODEL.always_diagnose) {
3617
+ UI.notify('To diagnose unbounded problems, values beyond 1e+10 ' +
3618
+ 'are considered as infinite (\u221E)');
3619
+ }
3566
3620
  // Some changes may necessitate redrawing the diagram
3567
3621
  let cb = UI.boxChecked('settings-align-to-grid'),
3568
3622
  redraw = !model.align_to_grid && cb;