diff --git a/.classpath b/.classpath index 0e443c19..1a41b89a 100644 --- a/.classpath +++ b/.classpath @@ -1,7 +1,11 @@ - - + + + + + + diff --git a/buildapp.xml b/buildapp.xml index a3a996a8..d5ddd0c8 100644 --- a/buildapp.xml +++ b/buildapp.xml @@ -15,8 +15,8 @@ name="Structorizer" displayname="Structorizer" identifier="lu.fisch.Structorizer" - shortversion="3.30-11" - version="3.30-11" + shortversion="3.30-12" + version="3.30-12" icon="icons/Structorizer.icns" mainclassname="Structorizer" copyright="Bob Fisch" diff --git a/src/lu/fisch/diagrcontrol/DiagramController.java b/src/lu/fisch/diagrcontrol/DiagramController.java index 684ae8af..d7460b01 100644 --- a/src/lu/fisch/diagrcontrol/DiagramController.java +++ b/src/lu/fisch/diagrcontrol/DiagramController.java @@ -38,6 +38,8 @@ * Kay Gürtzig 2018-03-21 Issue #463: console output replaced by standard JDK4 (= j.u.l.) logging * Kay Gürtzig 2018-10-12 Issue #622: Logging of API calls introduced (level CONFIG) * Kay Gürtzig 2019-03-02 Issue #366: New methods isFocused() and requestFocus() in analogy to Window + * Kay Gürtzig 2020-12-11 Enh. #443: deprecated methods removed + * Kay Gürtzig 2020-12-14 References to lu.fisch.util.StringList and java.awt.Color eliminated * ****************************************************************************************************** * @@ -51,14 +53,11 @@ * ******************************************************************************************************/// -import java.awt.Color; import java.lang.reflect.Method; import java.util.HashMap; import java.util.logging.Level; import java.util.logging.Logger; -import lu.fisch.utils.StringList; - /** * Interface for classes that provide an API for being controlled e.g. by executed Structorizer * diagrams.
@@ -245,11 +244,25 @@ public default Object execute(String name, Object[] arguments) throws FunctionEx try { // START KGU#597 2018-10-12: Issue #622 - better monitoring of controller activity if (logger.isLoggable(Level.CONFIG)) { - StringList argStrings = new StringList(); - for (Object arg: arguments) { - argStrings.add(String.valueOf(arg)); + // START KGU#591/KGU#685 2020-12-14: Issues #622, #704: StringList replaced + //StringList argStrings = new StringList(); + //for (Object arg: arguments) { + // argStrings.add(String.valueOf(arg)); + //} + //logger.config("Executing " + name + "(" + argStrings.concatenate(",") + ")"); + StringBuilder sb = new StringBuilder(); + sb.append("Executing "); + sb.append(name); + sb.append("("); + for (int i = 0; i < arguments.length; i++) { + if (i > 0) { + sb.append(","); + } + sb.append(String.valueOf(arguments[i])); } - logger.config("Executing " + name + "(" + argStrings.concatenate(",") + ")"); + sb.append(")"); + logger.config(sb.toString()); + // END KGU#597/KGU#685 2020-12-14 } // END KGU#597 2018-10-12 result = method.invoke(this, arguments); @@ -324,8 +337,10 @@ public default boolean isFocused() public default void requestFocus() {} // END KGU#356 2019-03-02 - @Deprecated - public String execute(String message); - @Deprecated - public String execute(String message, Color color); +// START KGU#448/KGU#673 2020-12-11: Enh. #443, eventually deleted +// @Deprecated +// public String execute(String message); +// @Deprecated +// public String execute(String message, Color color); +// END KGU#448/KGU#673 2020-12-11 } diff --git a/src/lu/fisch/structorizer/arranger/Arranger.java b/src/lu/fisch/structorizer/arranger/Arranger.java index de706d9b..68e1909b 100644 --- a/src/lu/fisch/structorizer/arranger/Arranger.java +++ b/src/lu/fisch/structorizer/arranger/Arranger.java @@ -77,6 +77,7 @@ * Kay Gürtzig 2019-03-28 Enh. #657: New argument for subdiagram retrieval methods * Kay Gürtzig 2019-10-05 Bugfix #759: Exception catch in routinePoolChanged() as emergency workaround * Kay Gürtzig 2020-01-20 Enh. #801: Key F1 now tries to open the PDF help file if offline + * Kay Gürtzig 2020-12-14 Adapted to the no longer reverted meaning of surface.getZoom() * ****************************************************************************************************** * @@ -141,10 +142,6 @@ * one and the same drawing area. * @author robertfisch */ -/** - * @author kay - * - */ @SuppressWarnings("serial") public class Arranger extends LangFrame implements WindowListener, KeyListener, IRoutinePool, IRoutinePoolListener, LangEventListener { @@ -728,6 +725,7 @@ private void initPopupMenu() { public void actionPerformed(ActionEvent e) { expandRootSetOrSelection(null, Arranger.this, null); }}); + // This doesn't work directly but shows the key binding handled via keyPressed() popupExpandSelection.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_F11, 0)); // START KGU#626 2019-01-03: Enh. #657 @@ -738,6 +736,7 @@ public void actionPerformed(ActionEvent e) { public void actionPerformed(ActionEvent e) { makeGroup(Arranger.this); }}); + // This doesn't work directly but shows the key binding handled via keyPressed() popupGroup.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_G, java.awt.event.InputEvent.CTRL_DOWN_MASK)); popupExpandGroup = new javax.swing.JMenuItem("Expand and group ...", IconLoader.getIcon(117)); @@ -748,6 +747,7 @@ public void actionPerformed(ActionEvent e) { expandRootSetOrSelection(null, null, null); makeGroup(Arranger.this); }}); + // This doesn't work directly but shows the key binding handled via keyPressed() popupExpandGroup.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_G, java.awt.event.InputEvent.CTRL_DOWN_MASK | java.awt.event.InputEvent.SHIFT_DOWN_MASK)); // END KGU#626 201-01-03 @@ -761,6 +761,7 @@ public void actionPerformed(ActionEvent e) { inspectAttributes(root); } }}); + // This doesn't work directly but shows the key binding handled via keyPressed() popupAttributes.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, java.awt.event.InputEvent.ALT_DOWN_MASK)); popupRemove = new javax.swing.JMenuItem("Remove selected diagrams", IconLoader.getIcon(100)); @@ -770,6 +771,7 @@ public void actionPerformed(ActionEvent e) { public void actionPerformed(ActionEvent e) { btnRemoveDiagramActionPerformed(e, false); }}); + // This doesn't work directly but shows the key binding handled via keyPressed() popupRemove.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, 0)); popupMenu.addSeparator(); @@ -783,6 +785,7 @@ public void actionPerformed(ActionEvent e) { rearrange(); } }); + // This doesn't work directly but shows the key binding handled via keyPressed() popupRearrange.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_R, java.awt.event.InputEvent.CTRL_DOWN_MASK)); popupMenu.addSeparator(); @@ -806,6 +809,7 @@ public void actionPerformed(ActionEvent e) { public void actionPerformed(ActionEvent e) { helpArranger(false); }}); + // This doesn't work directly but shows the key binding handled via keyPressed() popupHelp.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_F1, 0)); popupKeyBindings = new javax.swing.JMenuItem("Show key bindings ...", IconLoader.getIcon(89)); @@ -815,6 +819,7 @@ public void actionPerformed(ActionEvent e) { public void actionPerformed(ActionEvent e) { helpArranger(true); }}); + // This doesn't work directly but shows the key binding handled via keyPressed() popupKeyBindings.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_F1, java.awt.event.InputEvent.ALT_DOWN_MASK)); } @@ -832,14 +837,15 @@ protected void updateStatusSize() { // statusSize.setText(surface.getWidth() + " x " + surface.getHeight()); // statusViewport.setText(vRect.x + ".." + (vRect.x + vRect.width) + " : " + // vRect.y + ".." + (vRect.y + vRect.height)); - double width = surface.getWidth() * surface.getZoom(); - double height = surface.getHeight() * surface.getZoom(); - Rect visRect = (new Rect(vRect)).scale(surface.getZoom()); + double zoom = surface.getZoom(); + double width = surface.getWidth() / zoom; + double height = surface.getHeight() / zoom; + Rect visRect = (new Rect(vRect)).scale(1/zoom); statusSize.setText((int)width + " x " + (int)height); statusViewport.setText(visRect.left + ".." + visRect.right + " : " + visRect.top + ".." + visRect.bottom); // END KGU#624 2019-03-13 - statusZoom.setText(String.format("%.1f %%", 100 / surface.getZoom())); + statusZoom.setText(String.format("%.1f %%", 100 * zoom)); } protected void updateStatusSelection() { @@ -947,7 +953,7 @@ private void btnSetCoveredActionPerformed(java.awt.event.ActionEvent evt) { // START KGU#497 2018-02-17: Enh. #513 protected void btnZoomActionPerformed(ActionEvent evt) { surface.zoom((evt.getModifiers() & ActionEvent.SHIFT_MASK) != 0); - if (surface.getZoom() <= 1 && this.isShiftPressed) { + if (surface.getZoom() >= 1 && this.isShiftPressed) { btnZoom.setEnabled(false); } } @@ -1316,7 +1322,7 @@ public void keyPressed(KeyEvent ev) { this.routinePoolChanged(this, IRoutinePoolListener.RPC_POSITIONS_CHANGED); } else { - scrollarea.getVerticalScrollBar().setValue(newValue); + scrollarea.getVerticalScrollBar().setValue(newValue); } } break; @@ -1637,7 +1643,7 @@ private void setShiftPressed(boolean isPressed) { this.btnRemoveDiagram.setText(btnRemoveAllDiagrams.getText()); // END KGU#534 2018-06-27 this.isShiftPressed = true; - if (surface.getZoom() <= 1) { + if (surface.getZoom() >= 1) { btnZoom.setEnabled(false); } } @@ -2043,7 +2049,8 @@ public boolean saveAll(Component initiator, boolean goingToClose) { */ public void updateProperties(Ini ini) { - ini.setProperty("arrangerZoom", Float.toString(surface.getZoom())); + // For historical reasons, the ini property has still reverse meaning + ini.setProperty("arrangerZoom", Float.toString(1/surface.getZoom())); // START KGU#623 2018-12-20: Enh. #654 ini.setProperty("arrangerDirectory", surface.currentDirectory.getAbsolutePath()); // END KGU#623 2018-12-20 diff --git a/src/lu/fisch/structorizer/arranger/Surface.java b/src/lu/fisch/structorizer/arranger/Surface.java index e2395e17..ed7243ba 100644 --- a/src/lu/fisch/structorizer/arranger/Surface.java +++ b/src/lu/fisch/structorizer/arranger/Surface.java @@ -125,6 +125,7 @@ * Kay Gürtzig 2019-11-28 Bugfix #788: Offered arrz extraction to user-chosen folder was ignored * Kay Gürtzig 2020-02-16 Issue #815: Combined ArrangerFilter introduced for convenience * Kay Gürtzig 2020-02-17 Bugfix #818: Strong inconsistencies by outdated method replace() mended. + * Kay Gürtzig 2020-12-14 Zoom scale reverted, i.e. zomeFactor is no longer inverse: 0.5 means 50% now * ****************************************************************************************************** * @@ -323,7 +324,7 @@ public class Surface extends LangPanel implements MouseListener, MouseMotionList // END KGU#88 2015-11-24 // START KGU#497 2018-02-17: Enh. # - new zoom facility /** The factor by which the drawing is currently downscaled */ - private float zoomFactor = 2.0f; + private float zoomFactor = 0.5f; // END KGU#497 2018-02-17 // START KGU#110 2015-12-21: Enh. #62, also supports PNG export public File currentDirectory = new File(System.getProperty("user.home")); @@ -474,24 +475,24 @@ else if (contention <= 10 && wasContented) { } // END KGU#572 2018-09-09 // START KGU#497 2018-03-19: Enh. #512 - //g2d.scale(1/zoomFactor, 1/zoomFactor); + //g2d.scale(zoomFactor, zoomFactor); // END KGU#497 2018-02-17 if (compensateZoom) { // In zoom-compensated drawing the background filled by super.paint(g) - // is too small (virtually scaled don), therefore we must draw a + // is too small (virtually scaled down), therefore we must draw a // white rectangle covering the enlarged image area g2d.setColor(Color.WHITE); g2d.fillRect(0, 0, // START KGU#624 2018-12-24: Enh. #655 - //Math.round(this.getWidth() * zoomFactor), - //Math.round(this.getHeight() * zoomFactor) - Math.round(this.getWidth() * zoomFactor - offsetX), - Math.round(this.getHeight() * zoomFactor - offsetY) + //Math.round(this.getWidth() / zoomFactor), + //Math.round(this.getHeight() / zoomFactor) + Math.round(this.getWidth() / zoomFactor - offsetX), + Math.round(this.getHeight() / zoomFactor - offsetY) // END KGU#624 2018-12-24 ); } else { - g2d.scale(1/zoomFactor, 1/zoomFactor); + g2d.scale(zoomFactor, zoomFactor); } // END KGU#497 2018-03-19 @@ -503,7 +504,7 @@ else if (contention <= 10 && wasContented) { /* The clip bounds of the graphics object are usually smaller than the entire * viewport, due to window bit blit */ //Rect visRect = new Rect(((JViewport)getParent()).getViewRect()); - //visRect = visRect.scale(zoomFactor); + //visRect = visRect.scale(1/zoomFactor); //visibleRect = visRect.getRectangle(); // START KGU#706 2019-05-14: Bugfix #722 This failed with the graphics object of a buffered image //Rect clipRect = new Rect(g.getClipBounds()); @@ -577,7 +578,7 @@ else if (contention <= 10 && wasContented) { // END KGU#624 2018-12-23 // START KGU#497 2018-03-19: Enh. #512 if (!compensateZoom) { - g2d.scale(zoomFactor, zoomFactor); + g2d.scale(1/zoomFactor, 1/zoomFactor); } // END KGU#497 2018-03-19 } @@ -586,8 +587,8 @@ else if (contention <= 10 && wasContented) { area.height += DEFAULT_GAP; // END KGU#645 2019-02-03S // START KGU#85 2017-10-23: Enh. #35 - now make sure the scrolling area is up to date - area.width = Math.round(Math.min(area.width, Short.MAX_VALUE) / this.zoomFactor); - area.height = Math.round(Math.min(area.height, Short.MAX_VALUE) / this.zoomFactor); + area.width = Math.round(Math.min(area.width, Short.MAX_VALUE) * this.zoomFactor); + area.height = Math.round(Math.min(area.height, Short.MAX_VALUE) * this.zoomFactor); Dimension oldArea = this.getPreferredSize(); // This check isn't only to improve performance but also to avoid endless recursion if (area.width != oldArea.width || area.height != oldArea.height) { @@ -1996,17 +1997,17 @@ public void exportPNG(Frame frame) rect.right -= offsetX; rect.bottom -= offsetY; } - int width = this.getWidth() - Math.round(offsetX / zoomFactor); - int height = this.getHeight() - Math.round(offsetY / zoomFactor); + int width = this.getWidth() - Math.round(offsetX * zoomFactor); + int height = this.getHeight() - Math.round(offsetY * zoomFactor); // END KGU#624 2018-12-24 if (logger.isLoggable(Level.CONFIG)) { logger.log(Level.CONFIG, "{0} x {1}", new Object[]{width, height}); logger.log(Level.CONFIG, "Drawing Rect: {0}", rect); - logger.log(Level.CONFIG, "zoomed: {0} x {1}", new Object[]{width*this.zoomFactor, height*this.zoomFactor}); + logger.log(Level.CONFIG, "zoomed: {0} x {1}", new Object[]{width/this.zoomFactor, height/this.zoomFactor}); } BufferedImage bi = new BufferedImage( - Math.round(width * this.zoomFactor), - Math.round(height * this.zoomFactor), + Math.round(width / this.zoomFactor), + Math.round(height / this.zoomFactor), BufferedImage.TYPE_4BYTE_ABGR); paintComponent(bi.getGraphics(), true, !this.diagramsSelected.isEmpty(), offsetX, offsetY); // END KGU#497 2018-03-19 @@ -2260,8 +2261,8 @@ protected Rect adaptLayout() // this.setPreferredSize(new Dimension(rect.right, rect.bottom)); // this.revalidate(); // } - Dimension newZoomedDim = new Dimension(Math.round(Math.min(rect.right, Short.MAX_VALUE) / this.zoomFactor), - Math.round(Math.min(rect.bottom, Short.MAX_VALUE) / this.zoomFactor)); + Dimension newZoomedDim = new Dimension(Math.round(Math.min(rect.right, Short.MAX_VALUE) * this.zoomFactor), + Math.round(Math.min(rect.bottom, Short.MAX_VALUE) * this.zoomFactor)); if (newZoomedDim.width != oldDim.width || newZoomedDim.height != oldDim.height) { this.setPreferredSize(newZoomedDim); this.revalidate(); @@ -2467,7 +2468,7 @@ private void addDiagram(Root root, Mainform form, Point point, Group owningGroup // START KGU#497 2018-02-17: Enh. #512 - zooming must be considered //if (left > this.getWidth()) - if (left > this.getWidth() * this.zoomFactor) + if (left > this.getWidth() / this.zoomFactor) // END KGU#497 2018-02-17 { // FIXME (KGU 2015-11-19) This isn't really sensible - might find a free space by means of a quadtree? @@ -2541,7 +2542,7 @@ private void addDiagram(Root root, Mainform form, Point point, Group owningGroup adaptLayout(); // END KGU#85 2015-11-18 // START KGU#497 2018-12-23: Bugfix for enh. #512 - rec = rec.scale(1/this.zoomFactor); + rec = rec.scale(this.zoomFactor); // END KGU#497 2018-12-23 this.scrollRectToVisible(rec.getRectangle()); // START KGU#88 2015-12-20: It ought to be pinned if form wasn't null (KGU#804 2020-02-17: now done in both cases) @@ -2679,7 +2680,7 @@ private Point findPreferredLocation(LinkedList silhouette, Rectangle rec) Point optimum = null; while (iter.hasNext()) { Point leap = iter.next(); - if (leap.x > this.getWidth() * this.zoomFactor) { + if (leap.x > this.getWidth() / this.zoomFactor) { break; } ListIterator iter1 = candidates.listIterator(); @@ -2707,7 +2708,7 @@ private Point findPreferredLocation(LinkedList silhouette, Rectangle rec) // If we didn't find anything better, then we will just adhere to the bounds approach result // But first have a look whether some incompletely analysed breaches (those remaining open at // end) are wide enough to be accepted. We will allow a diagram if it fits at least by half. - float windowWidth = this.getWidth() * this.zoomFactor - rec.width/2; + float windowWidth = this.getWidth() / this.zoomFactor - rec.width/2; for (Point cand: candidates) { if (cand.x < windowWidth && (optimum == null || cand.y < optimum.y)) { optimum = cand; @@ -3054,7 +3055,8 @@ private void initComponents() { this.setPreferredSize(new Dimension(400, 300)); // START KGU#497 2018-02-17: Enh. #512 try { - this.zoomFactor = Float.parseFloat(Ini.getInstance().getProperty("arrangerZoom", "2.0f")); + // For historical reasons, the zoom scale in ini remains reverse + this.zoomFactor = 1/Float.parseFloat(Ini.getInstance().getProperty("arrangerZoom", "2.0f")); } catch (NumberFormatException ex) { logger.log(Level.WARNING, "Corrupt zoom factor in ini", ex); @@ -3468,8 +3470,8 @@ public void mouseClicked(MouseEvent e) this.unselectAll(); } // With zooming we need the virtual mouse coordinates (Enh. #512) - int mouseX = Math.round(e.getX() * this.zoomFactor); - int mouseY = Math.round(e.getY() * this.zoomFactor); + int mouseX = Math.round(e.getX() / this.zoomFactor); + int mouseY = Math.round(e.getY() / this.zoomFactor); // START KGU#633 2019-01-09: Enh. #662/2 if (!(drawGroups && selectGroups)) { Diagram diagram = getHitDiagram(mouseX, mouseY); @@ -3572,8 +3574,8 @@ public void mouseDragged(MouseEvent e) scrollRectToVisible(rect); // Now we need the virtual coordinates - mouseX = Math.round(mouseX * zoomFactor); - mouseY = Math.round(mouseY * zoomFactor); + mouseX = Math.round(mouseX / zoomFactor); + mouseY = Math.round(mouseY / zoomFactor); Diagram diagram = getHitDiagram(mouseX, mouseY); Point oldMousePoint = this.dragPoint; @@ -3632,8 +3634,8 @@ public void mouseMoved(MouseEvent e) { if (this.drawGroups) { pop.setVisible(false); - int x = (int)(e.getX() * this.zoomFactor); - int y = (int)(e.getY() * this.zoomFactor); + int x = (int)(e.getX() / this.zoomFactor); + int y = (int)(e.getY() / this.zoomFactor); Set hitGroups = this.getHitGroups(x, y); if (!hitGroups.isEmpty()) { StringList groupNames = new StringList(); @@ -3673,7 +3675,7 @@ private void showPopupMenuIfTriggered(MouseEvent e) // START KGU#630 2019-01-09: Enh #662/2 if (!(drawGroups && selectGroups)) { // END KGU#630 2019-01-09 - List hitDiagrs = this.getHitDiagrams(Math.round(e.getX() * zoomFactor), Math.round(e.getY() * zoomFactor)); + List hitDiagrs = this.getHitDiagrams(Math.round(e.getX() / zoomFactor), Math.round(e.getY() / zoomFactor)); for (Diagram diagr: hitDiagrs) { String description = diagr.root.getSignatureString(false); javax.swing.JMenuItem menuItem = new javax.swing.JMenuItem(description, diagr.root.getIcon()); @@ -3698,7 +3700,7 @@ public void actionPerformed(ActionEvent evt) { // START KGU#630 2019-01-09: Enh #662/2 } if (drawGroups) { - Set hitGroups = this.getHitGroups(Math.round(e.getX() * zoomFactor), Math.round(e.getY() * zoomFactor)); + Set hitGroups = this.getHitGroups(Math.round(e.getX() / zoomFactor), Math.round(e.getY() / zoomFactor)); String tooltipText = msgTooltipSelectThis.getText(); int pos2 = tooltipText.indexOf("%2"); if (pos2 > 0) { @@ -4507,7 +4509,7 @@ public void scrollToDiagram(Root aRoot, boolean setAtTop) { } Rect rect = aRoot.getRect(diagr.point); // START KGU#497 2018-02-17: Enh. #512 - rect = rect.scale(1/this.zoomFactor); + rect = rect.scale(this.zoomFactor); // END KGU#497 2018-02-17 this.scrollRectToVisible(rect.getRectangle()); } @@ -4523,7 +4525,7 @@ public void scrollToDiagram(Root aRoot, boolean setAtTop) { public void scrollToGroup(Group aGroup) { Collection members = aGroup.getDiagrams(); Rect rect = this.getDrawingRect(members, null); - rect = rect.scale(1/this.zoomFactor); + rect = rect.scale(this.zoomFactor); unselectAll(); this.selectSet(members); this.scrollRectToVisible(rect.getRectangle()); @@ -4551,7 +4553,7 @@ private Rectangle getSelectionBounds(boolean unzoomed) Rectangle bounds = null; Rect rect = this.getDrawingRect(this.diagramsSelected, null); if (!unzoomed) { - rect = rect.scale(1/zoomFactor); + rect = rect.scale(zoomFactor); } if (rect.right > rect.left && rect.bottom > rect.top) { bounds = rect.getRectangle(); @@ -4741,10 +4743,10 @@ private void notifyChangeListeners(int _flags) */ public void zoom(boolean zoomIn) { if (zoomIn) { - this.zoomFactor = Math.max(this.zoomFactor * 0.9f, 1); + this.zoomFactor = Math.min(this.zoomFactor / 0.9f, 1); } else { - this.zoomFactor /= 0.9f; + this.zoomFactor = Math.max(this.zoomFactor * 0.9f, 0.01f); } repaint(); // START KGU#624 2018-12-21: Enh. #655 @@ -4754,7 +4756,7 @@ public void zoom(boolean zoomIn) { /** * Return the current zoom factor - * @return zoom factor (1 = 100%, 2 = 50%, 3 = 33% etc.) + * @return zoom factor (1 = 100%, 0.5 = 50% etc.) */ public float getZoom() { return this.zoomFactor; @@ -4769,16 +4771,12 @@ public float getZoom() { public void mouseWheelMoved(MouseWheelEvent mwEvt) { if (mwEvt.isControlDown()) { int rotation = mwEvt.getWheelRotation(); - if (Element.E_WHEEL_REVERSE_ZOOM) { - rotation *= -1; - } - if (rotation >= 1) { - mwEvt.consume(); - this.zoom(false); - } - else if (rotation <= -1) { + if (Math.abs(rotation) >= 1) { + if (Element.E_WHEEL_REVERSE_ZOOM) { + rotation *= -1; + } mwEvt.consume(); - this.zoom(true); + this.zoom(rotation < 0); } } } diff --git a/src/lu/fisch/structorizer/elements/Element.java b/src/lu/fisch/structorizer/elements/Element.java index 9b746f97..3b06e38e 100644 --- a/src/lu/fisch/structorizer/elements/Element.java +++ b/src/lu/fisch/structorizer/elements/Element.java @@ -114,6 +114,7 @@ * Kay Gürtzig 2020-02-04 Bugfix #805 - method saveToINI decomposed * Kay Gürtzig 2020-04-12 Bugfix #847 inconsistent handling of upper and lowercase in operator names (esp. DIV) * Kay Gürtzig 2020-10-17/19 Enh. #872: New mode to display operators in C style + * Kay Gürtzig 2020-11-01 Issue #881: Highlighting of bit operators and Boolean literals * ****************************************************************************************************** * @@ -269,9 +270,11 @@ public String toString() // END KGU#563 2018-007-26 // START KGU#791 2020-01-20: Enh. #801 - support for offline help public static final String E_HELP_FILE = "structorizer_user_guide.pdf"; + /** Estimated size of the User Guide PDF file (to be adapted when User Guide significantly grows) */ + public static final long E_HELP_FILE_SIZE = 10000000; public static final String E_DOWNLOAD_PAGE = "https://www.fisch.lu/Php/download.php"; // END KGU#791 2020-01-20 - public static final String E_VERSION = "3.30-11"; + public static final String E_VERSION = "3.30-12"; public static final String E_THANKS = "Developed and maintained by\n"+ " - Robert Fisch \n"+ @@ -570,12 +573,12 @@ public static int getPadding() { // reset for any new string which is likely to cause severe concurrency trouble as the patterns are used on drawing etc. private static final Pattern STRING_PATTERN = Pattern.compile("(^\\\".*\\\"$)|(^\\\'.*\\\'$)"); private static final Pattern INC_PATTERN1 = Pattern.compile(BString.breakup("inc", true)+"[(](.*?)[,](.*?)[)](.*?)"); - private static final Pattern INC_PATTERN2 = Pattern.compile(BString.breakup("inc", true)+"[(](.*?)[)](.*?)"); - private static final Pattern DEC_PATTERN1 = Pattern.compile(BString.breakup("dec", true)+"[(](.*?)[,](.*?)[)](.*?)"); - private static final Pattern DEC_PATTERN2 = Pattern.compile(BString.breakup("dec", true)+"[(](.*?)[)](.*?)"); + private static final Pattern INC_PATTERN2 = Pattern.compile(BString.breakup("inc", true)+"[(](.*?)[)](.*?)"); + private static final Pattern DEC_PATTERN1 = Pattern.compile(BString.breakup("dec", true)+"[(](.*?)[,](.*?)[)](.*?)"); + private static final Pattern DEC_PATTERN2 = Pattern.compile(BString.breakup("dec", true)+"[(](.*?)[)](.*?)"); // END KGU#575 2018-09-17 - // START KGU#425 2017-09-29: Lexical core mechanisms revised + // START KGU#425 2017-09-29: Lexical core mechanisms revised private static final String[] LEXICAL_DELIMITERS = new String[] { " ", "\t", @@ -604,6 +607,10 @@ public static int getPadding() { "\"", "\\", "%", + // START KGU#790 2020-11-01: Enh. #800 unary C operators must also split + "&", + "~", + // END KGU#790 2020-11-01 // START KGU#331 2017-01-13: Enh. #333 Precaution against unicode comparison operators "\u2260", "\u2264", @@ -1397,12 +1404,13 @@ public StringList getComment(boolean _alwaysTrueComment) } } - // START KGU#172 2016-04-01: Issue #145: Make it easier to obtain this information, 2019-03-16 made static + /* START KGU#172 2016-04-01: Issue #145: Make it easier to obtain this information, + * 2019-03-16 made static, 2020-12-15 made public */ /** * Checks whether texts and comments are to be swapped for display. * @return true iff the swichTextAndComments flag is on and commentsPlusText mode is not */ - protected static boolean isSwitchTextCommentMode() + public static boolean isSwitchTextCommentMode() { // Root root = getRoot(this); // return (root != null && root.isSwitchTextAndComments()); @@ -1411,7 +1419,7 @@ protected static boolean isSwitchTextCommentMode() return !Element.E_COMMENTSPLUSTEXT && Element.E_TOGGLETC; // END KGU#227 2016-07-31 } - // END KGU#172 2916-04-01 + /* END KGU#172 2916-04-01 */ /** * Returns whether this element appears as selected in the standard {@link DrawingContext}. @@ -2957,7 +2965,7 @@ public static StringList splitExpressionList(String _text, String _listSeparator * If the last result element is empty in mode {@code _appendTail} then the expression list was syntactically * "clean".
* FIXME If the expression was given without some parentheses as delimiters then a tail won't be added. - * @param _text - string containing one or more expressions + * @param _tokens - tokenized text, supposed to contain one or more expressions * @param _listSeparator - a character sequence serving as separator among the expressions (default: ",") * @param _appendTail - if the remaining part of _text from the first unaccepted character on is to be added * @return a StringList consisting of the separated expressions (and the tail if _appendTail was true). @@ -3529,6 +3537,14 @@ private static int writeOutVariables(Canvas _canvas, int _x, int _y, String _tex specialSigns.add("<="); specialSigns.add(">="); // END KGU#872 2020-10-17 + // START KGU#883 2020-11-01: Enh. #881 bit operators and Boolean literal were missing + specialSigns.add("false"); + specialSigns.add("true"); + specialSigns.add("&"); + specialSigns.add("|"); + specialSigns.add("^"); + specialSigns.add("~"); + // END KGU#883 2020-11-01 } // START KGU#611/KGU843 2020-04-12: Issue #643, bugfix #847 if (specialSignsCi == null) { @@ -3544,7 +3560,7 @@ private static int writeOutVariables(Canvas _canvas, int _x, int _y, String _tex // START KGU#115 2015-12-23: Issue #74 - These Pascal operators hadn't been supported specialSignsCi.add("shl"); specialSignsCi.add("shr"); - // END KGU#115 2015-12-23 + // END KGU#115 2015-12-23 } // END KGU#611/KGU#843 2020-04-12 // END KGU#64 2015-11-03 diff --git a/src/lu/fisch/structorizer/elements/TypeMapEntry.java b/src/lu/fisch/structorizer/elements/TypeMapEntry.java index b53640d9..8f63a9b7 100644 --- a/src/lu/fisch/structorizer/elements/TypeMapEntry.java +++ b/src/lu/fisch/structorizer/elements/TypeMapEntry.java @@ -279,10 +279,10 @@ public String getCanonicalType() * possible, i.e. type names like "integer", "real" etc. apparently designating * standard types will be replaced by corresponding Java type names), all * prefixed with as many '@' characters as there are index levels if it - * is an array type. If it is a record type then it will enumerate semicolon- - * separated name:type_name pairs within braces after a '$' prefix. An enumerator - * type will enumerate the value names (constant ids) separated by commas within - * braces following an '€' prefix. + * is an array type. If it is a record type then it will enumerate + * semicolon-separated {@code name:type_name} pairs within braces after a + * '$' prefix. An enumerator type will enumerate the value names (constant + * ids) separated by commas within braces following an '€' prefix. * @param canonicalizeTypeNames - specifies whether type names are to be unified, too * @see TypeMapEntry#getTypes() * @return type string, possibly prefixed with one or more '@' characters. @@ -512,11 +512,15 @@ public HashMap getTypeMap() * possible, i.e. type names like "integer", "real" etc. apparently designating * standard types will be replaced by corresponding Java type names), all * prefixed with as many '@' characters as there are index levels if it - * is an array type or embedded in a "${...}" if it is a record/struct type. + * is an array type, or embedded in a "${...}" if it is a record/struct type + * and {@code _asName} is not {@code true}. An enumerator type will enumerate + * the value names (constant ids) separated by commas within braces following + * to an '€' prefix.
* If the type information is too ambiguous then an empty string is returned. - * @param _canonicalizeTypeNames - if contained element types are to be canonicalized, too. - * @param _asName - set this true if in case of a named type the name is to be returned (otherwise - * the structural description would be returned) + * @param _canonicalizeTypeNames - if contained element types are to be + * canonicalized, too. + * @param _asName - set this true if in case of a named type the name is to be + * returned (otherwise the structural description would be returned) * @return name or structural description */ public String getCanonicalType(boolean _canonicalizeTypeNames, boolean _asName) { @@ -650,10 +654,12 @@ public boolean isConflictFree() /** * Returns a StringList containing the type specifications of all detected - * declarations or assignments in canonicalized form (a prefix "@" stands - * for one array dimension level, a prefix "$" symbolizes a record/struct). - * Type names are preserved as declared. + * declarations or assignments with symbolic structure (a prefix "@" + * stands for one array dimension level, a prefix "${" symbolizes a + * record/struct), a prefix "€{" an enumerator type.
+ * Type names are preserved as declared (non-canonicalized). * @return StringList of differing canonicalized type descriptions + * @see #getTypes(boolean) */ public StringList getTypes() { @@ -664,7 +670,8 @@ public StringList getTypes() /** * Returns a StringList containing the type specifications of all detected * declarations or assignments in canonicalized form (a prefix "@" stands - * for one array dimension level, a prefix "$" symbolizes a record/struct).
+ * for one array dimension level, a prefix "${" symbolizes a record/struct), + * a prefix "€{" an enumerator type.
* If {@code canonicalizeTypeNames} is true then type identifiers apparently * designating standard types (like "integer", "real" etc.) will be replaced * by corresponding Java type names. @@ -742,6 +749,10 @@ public LinkedHashMap getComponentInfo(boolean _merge) // END KGU#388 2017-09-13 // START KGU#542 2019-11-17: Enh. #739 - Support for enum types + /** + * @return null or the list of constant names belonging to the enumeration type + * (the values can be retrieved from {@link Root#constants}) + */ public StringList getEnumerationInfo() { if (this.isEnum()) { @@ -918,11 +929,11 @@ public int getMinIndex(int level) * @see java.lang.Object#toString() */ @Override - public String toString() - { + public String toString() + { String name = typeName == null ? "" : typeName + "="; return getClass().getSimpleName() + "(" + name + this.getTypes().concatenate(" | ") + ")"; - } + } /** * Tries to find a common compatible canonical type for {@code type1} and {@code type2} diff --git a/src/lu/fisch/structorizer/executor/Executor.java b/src/lu/fisch/structorizer/executor/Executor.java index 6ff64400..e74119ed 100644 --- a/src/lu/fisch/structorizer/executor/Executor.java +++ b/src/lu/fisch/structorizer/executor/Executor.java @@ -196,6 +196,7 @@ * Kay Gürtzig 2020-04-23 Bugfix #858: split function in FOR-IN loop was not correctly handled * Kay Gürtzig 2020-04-28 Issue #822: Empty CALL lines should cause more sensible error messages * Kay Gürtzig 2020-10-19 Issue #879: Inappropriate handling of input looking like initializers + * Kay Gürtzig 2020-12-14 Issue #829 revoked (Control will by default close after execution) * ****************************************************************************************************** * @@ -792,11 +793,11 @@ public static Executor getInstance(Diagram diagram, errText = errText.replace("?", " (\"" + root.getMethodName() + "\")?"); } int res = JOptionPane.showOptionDialog(diagram, - errText, - mySelf.control.msgTitleQuestion.getText(), - JOptionPane.YES_NO_OPTION, - JOptionPane.QUESTION_MESSAGE, - null,null,null); + errText, + mySelf.control.msgTitleQuestion.getText(), + JOptionPane.YES_NO_OPTION, + JOptionPane.QUESTION_MESSAGE, + null,null,null); if (res == 0) { // START KGU 2018-08-08: If it had already been stopped make sure new start will be possible @@ -3077,9 +3078,9 @@ public void run() // closes after execution. control.setVisible(false); // START KGU#157 2016-03-16: Bugfix #131 - postponed Control start? - // START KGU#817 2020-04-04: Issue #829 mawa290669 requested to keep control always open - //boolean reopen = false; - boolean reopen = true; + // START KGU#817 2020-04-04: Issue #829, revoked on 2020-12-14 + boolean reopen = false; + //boolean reopen = true; // END KGU#817 2020-04-04 if (this.reopenFor != null) { diff --git a/src/lu/fisch/structorizer/generators/CGenerator.java b/src/lu/fisch/structorizer/generators/CGenerator.java index 7c9b0429..2c655280 100644 --- a/src/lu/fisch/structorizer/generators/CGenerator.java +++ b/src/lu/fisch/structorizer/generators/CGenerator.java @@ -95,7 +95,7 @@ * Kay Gürtzig 2019-11-08 Bugfix #769: Undercomplex selector list splitting in CASE generation mended * Kay Gürtzig 2019-11-12 Bugfix #752: Outcommenting of incomplete declarations ended * Kay Gürtzig 2019-11-17 Enh. #739: Modifications for support of enum type definitions (TODO) - * Kay Gürtzig 2019-11-24 Bugfix #783: Defective record initializers were simpy skipped without trace + * Kay Gürtzig 2019-11-24 Bugfix #783: Defective record initializers were simply skipped without trace * Kay Gürtzig 2019-11-30 Bugfix #782: Handling of global/local declarations mended * Kay Gürtzig 2019-12-02 KGU#784 Type descriptor transformation improved. * Kay Gürtzig 2020-02-10 Bugfix #808: For initialised declarations, operator unification was forgotten. @@ -1342,7 +1342,7 @@ else if (type.isArray() && !this.arrayBracketsAtTypeName()) { if (elemType != null && elemType.startsWith("@")) { elemType = elemType.substring(1); } - // START KGU #784 2019-12-02: varName is only part of the left side, there may be indices, so reduce the type if so + // START KGU#784 2019-12-02: varName is only part of the left side, there may be indices, so reduce the type if so int posIdx = codeLine.indexOf(varName) + varName.length(); String indices = codeLine.substring(posIdx).trim(); while (elemType.startsWith("@") && indices.startsWith("[")) { @@ -1362,7 +1362,7 @@ else if (indexList.get(0).trim().startsWith("]")) { indices = indexList.get(0).substring(1); } } - // END KGU #784 2019-12-02 + // END KGU#784 2019-12-02 } expr = this.transformOrGenerateArrayInit(codeLine, items.subSequence(0, items.count()-1), _indent, isDisabled, elemType, isDecl); if (expr == null) { diff --git a/src/lu/fisch/structorizer/generators/COBOLGenerator.java b/src/lu/fisch/structorizer/generators/COBOLGenerator.java index e3878da6..c5b4112f 100644 --- a/src/lu/fisch/structorizer/generators/COBOLGenerator.java +++ b/src/lu/fisch/structorizer/generators/COBOLGenerator.java @@ -32,26 +32,60 @@ * * Author Date Description * ------ ---- ----------- - * Simon Sobisch 2017.04.14 First Issue + * Simon Sobisch 2017.04.14 First Issue for #357 * Kay Gürtzig 2019-03-30 Issue #696: Type retrieval had to consider an alternative pool + * Kay Gürtzig 2019-10-09 Code for Alternative and Case implemented + * Kay Gürtzig 2020-04-16 Issue #739: Enumeration type export prepared + * Kay Gürtzig 2020-04-19 Declaration part advanced. * Kay Gürtzig 2020-04-22 Bugfix #854: Deterministic topological order of type definitions ensured * ****************************************************************************************************** * * Comment: / + * 2020-04-19 Kay Gürtzig + * - A rather tricky aspect are record and array initializer expressions. Neither does exist in + * COBOL. A COBOL-typical way would be to declare a data item with the initial values and then + * to use MOVE CORRESPONDING TO where it is to be assigned. Unfortunately this will not work if + * some of the "initializers" contains variables as element values because their value cannot be + * known in a data item definition. So the only general way is to decompose the initializer in + * place, i.e. where it is to be assigned. This does not help in case an initializer is used as + * argument value for a subroutine call though. There we would need a temporary variable, which + * must first be filled an thn be passed (by reference in most cases). Where to get this variable + * from it would hav to be declared (with full structure) in advance. + * - Another ugly problem is that there is no general concept of an expression in COBOL. For + * numerical expressions we can at least use COMPUTE (rather than having to decompose it into + * ADD, SUBTRACT, MULTIPLY, DIVIDE, and all that crap. But what to do with mixed expressions + * (containing string concatenation, comparisons, conversions etc.)? This will require a lot of + * auxiliary data items and enormous efforts to decompose the entire expression into whatever. + * Without a syntax tree representation of the Structorizer element content and the possibility + * to insert declarations into some data section afterwards this is simply not feasible. So the + * instruction element is possibly the most complex task. + * - Loop bodies may have to be extracted to some named code section in order to be able to form + * legal PERFORM commands, particularly in fixed format, otherwise inline PERFORM can be chose. * ******************************************************************************************************/// import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; +import java.util.Map; import java.util.Map.Entry; +import lu.fisch.structorizer.elements.Alternative; import lu.fisch.structorizer.elements.Call; +import lu.fisch.structorizer.elements.Case; import lu.fisch.structorizer.elements.Element; +import lu.fisch.structorizer.elements.For; +import lu.fisch.structorizer.elements.Forever; +import lu.fisch.structorizer.elements.Instruction; +import lu.fisch.structorizer.elements.Jump; +import lu.fisch.structorizer.elements.Parallel; +import lu.fisch.structorizer.elements.Repeat; import lu.fisch.structorizer.elements.Root; +import lu.fisch.structorizer.elements.Subqueue; import lu.fisch.structorizer.elements.Try; import lu.fisch.structorizer.elements.TypeMapEntry; +import lu.fisch.structorizer.elements.While; import lu.fisch.structorizer.executor.Function; import lu.fisch.structorizer.generators.Generator.TryCatchSupportLevel; import lu.fisch.utils.StringList; @@ -91,8 +125,12 @@ public static CodePart getByAbbreviation(String abbreviation) { private int lineNumber = 10; private int lineIncrement = 10; private final String[] ext = { "cob", "cbl" }; + /** Maps Roots to the names of all routines they call */ private final HashMap> subMap = new HashMap>(); - private HashMap typeMap; + /** Maps variable and type names to the structural type information */ + private HashMap typeMap; + /** Collects extracted bodies of loops and other structures for procedure generation */ + private final HashMap postponedProcedures = new HashMap(); // START KGU#686 2019-03-18: Enh. #56 /** @@ -309,6 +347,18 @@ protected void addCode(String text, String _indent, boolean asComment) { } } + /* + * (non-Javadoc) + * + * @see lu.fisch.structorizer.generators.Generator#insertCode(java.lang.String, + * boolean) + */ + @Override + protected void insertCode(String text, int atLine) + { + super.insertCode(this.getLineStart(false) + text, atLine); + } + // We need an overridden fundamental comment method here to be able to // switch between fixed-form and free-from reference format. /* @@ -362,24 +412,12 @@ protected Root registerCalled(Call _call, Root _caller) if (!this.subMap.containsKey(_caller)) { this.subMap.put(_caller, new HashSet()); } + // FIXME: How do we cope with overloading? We might modify the name by appending the argument number this.subMap.get(_caller).add(_called.getName()); } return super.registerCalled(_call, _caller); } - /** - * If required by the respective export option, replaces underscores in the given - * name by hyphens. - * @param _identifier - the name to be transformed - * @return transformed identifier - */ - private String transformName(String _identifier) { - if (this.optionUnderscores2Hyphens()) { - _identifier = _identifier.replace("_", "-"); - } - return _identifier; - } - // START KGU#395 2017-05-11: Enh. #357 - source format option for COBOL export /** * Returns the value of the export option whether fixed source file format must @@ -402,20 +440,67 @@ private boolean optionUnderscores2Hyphens() Object optionVal = this.getPluginOption("underscores2hyphens", true); return !(optionVal instanceof Boolean) || ((Boolean)optionVal).booleanValue(); } - // END KGU#395 2017-05-11 + // END KGU#395 2017-05-11 + + // START KGU#395 2020-04-17: Enh. #357 Two new options + /** @return the incrementing step for the level numbers of composed data entries */ + private int optionLevelIncrement() + { + int levelIncr = 2; // The default value + Object optionVal = this.getPluginOption("dataLevelIncrement", levelIncr); + if (optionVal instanceof Integer) { + levelIncr = Math.max(1, ((Integer)optionVal).intValue()); + } + return levelIncr; + } + // END KGU#395 2020-04-17 /************ Code Generation **************/ + /** + * If required by the respective export option, replaces underscores in the given + * name by hyphens. + * @param _identifier - the name to be transformed + * @return transformed identifier + */ + private String transformName(String _identifier) { + if (this.optionUnderscores2Hyphens()) { + _identifier = _identifier.replace("_", "-"); + } + return _identifier; + } + // START KGU#388/KGU#542 2019-12-04: Enh. #423, #739 @Override protected String transformType(String _type, String _default) { - if (_type == null) { + if (_type == null || _type.equals("???")) { _type = _default; } - else if (_type.equals("const")) { - _type = "78 "; + else if (_type.equalsIgnoreCase("string")) { + // Could also be "PIC N(...) USAGE NATIONAL" + _type = "PIC X(" + Integer.toString(this.optionDefaultStringLength()) + ")"; } + else if (_type.equalsIgnoreCase("int")) { + // Could also be "PIC S9(9) USAGE BINARY" + _type = "USAGE BINARY-LONG"; + } + else if (_type.equalsIgnoreCase("long")) { + // Could also be "PIC S9(18) USAGE BINARY" + _type = "USAGE BINARY-DOUBLE"; + } + else if (_type.equalsIgnoreCase("float")) { + // Could also be "USAGE COMP-1" + _type = "USAGE FLOAT-SHORT"; + } + else if (_type.equalsIgnoreCase("double")) { + // Could also be "USAGE COMP-2" + _type = "USAGE FLOAT-LONG"; + } + else if (_type.equalsIgnoreCase("bool") || (_type.equalsIgnoreCase("boolean"))) { + _type = "PIC 9(4) USAGE BINARY"; + } + // TODO return _type; } @@ -449,6 +534,7 @@ protected void generateTypeDef(Root _root, String _typeName, TypeMapEntry _type, String indentPlus1 = _indent + this.getIndent(); appendDeclComment(_root, _indent, typeKey); if (_type.isRecord()) { + // FIXME: This doesn't make sense at all for COBOL! addCode("struct " + _type.typeName + " {", _indent, _asComment); for (Entry compEntry: _type.getComponentInfo(false).entrySet()) { addCode(transformTypeFromEntry(compEntry.getValue(), _type) + "\t" + compEntry.getKey() + ";", @@ -459,9 +545,25 @@ protected void generateTypeDef(Root _root, String _typeName, TypeMapEntry _type, else if (_type.isEnum()) { StringList items = _type.getEnumerationInfo(); appendComment("enum " + _type.typeName, _indent); + int lastVal = -1; for (int i = 0; i < items.count(); i++) { // FIXME: We might have to transform the value... - addCode(items.get(i) + (i < items.count() -1 ? "," : ""), indentPlus1, _asComment); + // START KGU#542 2020-04-16: Enh. #739 + String constName = items.get(i); + String constVal = _root.getConstValueString(constName); + if (constVal == null) { + constVal = Integer.toString(++lastVal); + } + else { + try { + lastVal = Integer.parseInt(constVal); + } + catch (NumberFormatException exc) { + lastVal++; + } + } + addCode("78 " + constName + " VALUE " + constVal, indentPlus1, _asComment); + // END KGU#542 2020-04-16 } } } @@ -480,73 +582,687 @@ protected void appendDeclaration(Root _root, String _name, String _indent) TypeMapEntry typeInfo = typeMap.get(_name); StringList types = null; String constValue = _root.getConstValueString(_name); - String transfConst = transformType("const", ""); + String level = "01 "; if (typeInfo != null) { - types = getTransformedTypes(typeInfo, false); + types = getTransformedTypes(typeInfo, false); } // START KGU#375 2017-04-12: Enh. #388: Might be an imported constant - else if (constValue != null) { + // Enumeration constants will be exported together by generateTypeDef(...); + else if (constValue != null && !_root.constants.get(_name).contains("€")) { String type = Element.identifyExprType(typeMap, constValue, false); if (!type.isEmpty()) { types = StringList.getNew(transformType(type, "int")); // We place a faked workaround entry - typeMap.put(_name, new TypeMapEntry(type, null, null, _root, 0, true, false)); + typeMap.put(_name, typeInfo = new TypeMapEntry(type, null, null, _root, 0, true, false)); } + /* FIXME If it is a numeric constant to be used with "USAGE" + * then "01 " should remain, but "CONSTANT" would have to be inserted + */ + level = "78 "; } - // If the type is unambiguous and has no C-style declaration or may not be - // declared between instructions then add the declaration here - if (types != null && types.count() == 1 && typeInfo != null) { + // If the type is unambiguous then add the declaration here + if (types != null && types.count() == 1 && typeInfo != null) { + // TODO if types.count() > 1 we might think about a REDEFINES clause + if (typeInfo.isArray()) { + generateArrayDeclaration(cobName, typeInfo, 1, _indent); + return; + } + else if (typeInfo.isRecord()) { + generateRecordDeclaration(cobName, typeInfo, 1, _indent); + return; + } String decl = types.get(0).trim(); - // START KGU#375 2017-04-12: Enh. #388 - if (decl.equals(transfConst) && constValue != null) { - // The actual type spec is missing but we try to extract it from the value - decl += " VALUE " + Element.identifyExprType(typeMap, constValue, false); - decl = decl.trim(); + +// // START KGU#375 2017-04-12: Enh. #388 + if (constValue != null) { + decl = " VALUE " + transform(constValue); } + decl = level + cobName + "\t" + decl; // END KGU#375 2017-04-12 - if (decl.startsWith("@")) { - decl = makeArrayDeclaration(decl, _name, typeInfo); + addCode(decl + ".", _indent, false); + } + else if (types != null && types.isEmpty() && _name.startsWith("unstring_")) { + // Apparently a generic name from a COBOL import - is assumed to be a string array + addCode(level + cobName + ".", _indent, false); + addCode(String.format("%02d ", this.optionLevelIncrement()) + "FILLER\t" + + transformType("string", "PIC x(" + this.optionDefaultStringLength() + ")") + + " OCCURS " + Integer.toString(this.optionDefaultArraySize()), + _indent + this.getIndent(), false); + } + // there is no type info + else { + //appendComment(cobName, _indent); + addCode(level + cobName + ".", _indent, false); + } + // END KGU#261/KGU#332 2017-01-16 + } + // END KGU#375 2017-04-12 + + /** + * @param _cobName - the COBOL-transformed name for the record variable or component + * @param _typeInfo - the {@link TypeMapEntry} describing the record type + * @param _level - the structure or declaration level (1 is top, 49 is maximum) + * @param _indent - the current indentation level - may depend on the format + */ + private void generateRecordDeclaration(String _cobName, TypeMapEntry _typeInfo, int _level, String _indent) { + // TODO Auto-generated method stub + addCode(String.format("%02d ", _level) + _cobName + ".", _indent, false); + String subIndent = _indent; + int incr = this.optionLevelIncrement(); + if (!this.optionFixedSourceFormat() || _level == 1) { + subIndent += this.getIndent(); + } + for (Map.Entry comp: _typeInfo.getComponentInfo(true).entrySet()) { + String compName = transformName(comp.getKey()); + TypeMapEntry compType = comp.getValue(); + if (compType.isArray()) { + generateArrayDeclaration(compName, compType, _level + incr, subIndent); + } + else if (compType.isRecord()) { + generateRecordDeclaration(compName, compType, _level + incr, subIndent); + } + else if (compType.isEnum()) { + addCode(String.format("%02d ", _level + 2) + compName + "\tPIC 9(3).", subIndent, false); } else { - // FIXME: handle records - decl = "01 " + cobName + "\t" + decl; + String canonType = compType.getCanonicalType(true, true); + addCode(String.format("%02d ", _level + 2) + compName + "\t" + transformType(canonType, "???") + ".", subIndent, false); + } + } + } + + /** + * @param _cobName - the COBOl-transformed name for the array variable or component + * @param _typeInfo + * @param _level - the structure or declaration level (1 is top, 49 is maximum) + * @param _indent - the current indentation level - may depend on the format + */ + private void generateArrayDeclaration(String _cobName, TypeMapEntry _typeInfo, int _level, String _indent) { + String subIndent = _indent; + int incr = this.optionLevelIncrement(); + StringList types = _typeInfo.getTypes(); + String compName = _cobName; + if (_level == 1) { + // The data item must have a name at level 1 + addCode("01 " + _cobName, _indent, false); + compName = "FILLER"; + _level += incr; + subIndent += this.getIndent(); + } + for (int i = 0; i < types.count(); i++) { + // We look for the first type variant that is definitely an array + String type = types.get(i); + if (type.startsWith("@")) { + int dim = 0; + while (type.startsWith("@")) { + String occursClause = " OCCURS "; + int minIdx = _typeInfo.getMinIndex(dim); + int maxIdx = _typeInfo.getMaxIndex(dim); + if (minIdx > 0) { + occursClause += Integer.toString(minIdx + 1) + " TO "; + } + if (maxIdx < 0) { + maxIdx = Math.max(minIdx, 0) + this.optionDefaultArraySize() - 1; + } + occursClause += Integer.toString(maxIdx + 1); + + type = type.substring(1); + // Check element type + TypeMapEntry elemType = null; + if ((type.startsWith("${") && (elemType = typeMap.get(":" + (type = type.substring(2)))) != null + || !type.startsWith("@") && (elemType = typeMap.get(":" + type)) != null) + && elemType.isRecord()) { + // Seems to be a record type + this.generateRecordDeclaration(compName + occursClause, + elemType, _level + dim * incr, subIndent); + type = ""; + } + else if (type.startsWith("@")) { + addCode(String.format("%02d ", _level + dim * incr) + + compName + occursClause + ".", subIndent, false); + } + else { + if (type.equals("???") && _cobName.startsWith(transformName("unstring_"))) { + type = "string"; + } + addCode(String.format("%02d " , _level + dim * incr) + + compName + "\t" + transformType(type, "???") + occursClause, subIndent, false); + } + if (!this.optionFixedSourceFormat()) { + subIndent += this.getIndent(); + } + dim++; + } + } + if (!type.isEmpty()) { + break; + } + } + } + + /* (non-Javadoc) + * @see lu.fisch.structorizer.generators.Generator#transformTokens(lu.fisch.utils.StringList) + */ + @Override + protected String transformTokens(StringList tokens) + { + // First get rid of superfluous spaces + int pos = -1; + StringList doubleBlank = StringList.explode(" \n ", "\n"); + while ((pos = tokens.indexOf(doubleBlank, 0, true)) >= 0) + { + tokens.delete(pos); // Get rid of one of the blanks + } + // On inserting operator keywords we better make sure them being padded + // (lest neighbouring identifiers be glued to them on concatenating) + // The correct way would of course be to add blank tokens where needed + // but this seemed too expensive here. + tokens.replaceAll("==", "="); + // TODO + tokens.replaceAll("!=", "<>"); // FIXME!!!!! + tokens.replaceAll("%", " mod "); // FIXME + tokens.replaceAll("&&", " AND "); + tokens.replaceAll("||", " OR "); + tokens.replaceAll("!", " NOT "); + tokens.replaceAll("&", " and "); // FIXME + tokens.replaceAll("|", " or "); // FIXME + tokens.replaceAll("~", " not "); // FIXME + tokens.replaceAll("<<", " shl "); // FIXME + tokens.replaceAll(">>", " shr "); // FIXME + tokens.replaceAll("<-", ":="); // FIXME + // START KGU#311 2016-12-26: Enh. #314 - Support for File API + if (this.usesFileAPI) { + tokens.replaceAll("fileWrite", "write"); + tokens.replaceAll("fileWriteLine", "writeln"); + tokens.replaceAll("fileEOF", "eof"); + tokens.replaceAll("fileClose", "closeFile"); + } + // END KGU#311 2016-12-26 + // START KGU#190 2016-04-30: Bugfix #181 - String delimiters must be converted to ' + for (int i = 0; i < tokens.count(); i++) + { + String token = tokens.get(i); + if (token.length() > 1 && token.startsWith("\"") && token.endsWith("\"")) + { + // Seems to be a string, hence modify it + // Replace all internal apostrophes by double apostrophes + token = token.replace("'", "''"); + // Now replace the outer delimiters + tokens.set(i, "'" + token.substring(1, token.length()-1) + "'"); } - if (_root.constants.containsKey(_name)) { - if (!decl.contains(transfConst + " ")) { - decl = transfConst + " " + decl; + } + // END KGU#190 2016-04-30 + String result = tokens.concatenate(); + // We now shrink superfluous padding - this may affect string literals, though! + result = result.replace(" ", " "); + result = result.replace(" ", " "); // twice to catch odd-numbered space sequences, too + return result; + } + + @Override + protected void generateCode(Instruction _inst, String _indent) + { + if (!appendAsComment(_inst, _indent)) { + boolean isDisabled = _inst.isDisabled(); + StringList text = _inst.getUnbrokenText(); + appendComment(_inst, _indent); + for (int i = 0; i < text.count(); i++) { + String line = text.get(i); + if (line.isEmpty() || Instruction.isMereDeclaration(line)) { + continue; } - if (constValue != null) { - decl += " " + transform(constValue); + StringList tokens = Element.splitLexically(line, true); + Element.unifyOperators(tokens, false); + String transfLine = transform(line); + // Input and output should work via standard transformation... + if (Instruction.isAssignment(line)) { + tokens.removeAll(" "); + int posAsgn = tokens.indexOf("<-"); + // FIXME Somehow we must find out what data type is transferred... -> #800 + String varName = transform(Instruction.getAssignedVarname(tokens, false)); + StringList exprTokens = tokens.subSequence(posAsgn+1, tokens.count()); + boolean isVar = isVariable(exprTokens, true, typeMap); + if (varNames.contains(varName)) { + String target = Instruction.getAssignedVarname(tokens, true); + TypeMapEntry varType = typeMap.get(varName); + // FIXME This is all awful without syntax trees (#800) + if (varType != null && (varType.isArray() || varType.isRecord())) { + if (isVar) { + transfLine = "MOVE " + transform(exprTokens.concatenate(null)) + " CORRESPONDING TO " + transform(target); + } + else if (exprTokens.contains("{") && exprTokens.contains("}")) { + // Should be an initializer - so we will have to decompose it + // The code below was basically copied from CGenerator + int posBrace = exprTokens.indexOf("{"); + // FIXME This code is incomllete and not ready + if (posBrace >= 0 && posBrace <= 1 && exprTokens.get(exprTokens.count()-1).equals("}")) { + String transfExpr = null; + if (posBrace == 1 && exprTokens.count() >= 3 && Function.testIdentifier(exprTokens.get(0), true, null)) { + String typeName = exprTokens.get(0); + TypeMapEntry recType = this.typeMap.get(":"+typeName); + if (recType != null && recType.isRecord()) { + // transforms the Structorizer record initializer into a C-conform one + transfExpr = this.transformRecordInit(exprTokens, recType); + } + } + else if (posBrace == 0) { + // Seems to be an array initializer so decompose it to singulary assignments + /* (The alternative would have been to fake an initialized array declaration + * in the data section and to + */ + StringList items = Element.splitExpressionList(exprTokens.subSequence(1, exprTokens.count()), ",", true); + String elemType = null; + if (varType.isArray()) { + elemType = varType.getCanonicalType(true, false); + if (elemType != null && elemType.startsWith("@")) { + elemType = elemType.substring(1); + } + // START KGU #784 2019-12-02: varName is only part of the left side, there may be indices, so reduce the type if so + int posIdx = tokens.indexOf(varName)+1; + StringList indices = tokens.subSequence(posIdx, posAsgn); + while (elemType.startsWith("@") && indices.indexOf("[") == 0) { + elemType = elemType.substring(1); + StringList indexList = Element.splitExpressionList(indices.subSequence(1, indices.count()), ",", true); + indexList.remove(0); // Drop first index expression (has already been handled) + // Are there perhaps more indices within the same bracket pair (comma-separated list)? + while (indexList.count() > 1 && elemType.startsWith("@")) { + indexList.remove(0); + elemType = elemType.substring(1); + } + if (indexList.isEmpty()) { + indices.clear(); + } + else if (indexList.get(0).trim().startsWith("]")) { + // This should be the tail + indices = Element.splitLexically(indexList.get(0).substring(1), true); + } + } + // END KGU #784 2019-12-02 + } + transfExpr = this.transformOrGenerateArrayInit(line, items.subSequence(0, items.count()-1), _indent, isDisabled, elemType); + if (transfExpr == null) { + break; // FIXME FIXME FIXME + } + } + } + + } + } + } } + // TODO + addCode("INSTRUCTION STILL NOT IMPLEMENTED!", _indent, true); } - // END KGU#375 2017-04-12 - if (decl.contains("???")) { - appendComment(decl + ".", _indent); + } + } + + /** + * @param line + * @param subSequence + * @param _indent + * @param isDisabled + * @param elemType + * @return + */ + private String transformOrGenerateArrayInit(String line, StringList subSequence, String _indent, boolean isDisabled, + String elemType) { + // TODO Auto-generated method stub + return null; + } + /** + * @param exprTokens + * @param recType + * @return + */ + private String transformRecordInit(StringList exprTokens, TypeMapEntry recType) { + // TODO Auto-generated method stub + return null; + } + + @Override + protected void generateCode(Alternative _alt, String _indent) + { + boolean isDisabled = _alt.isDisabled(); + + appendComment(_alt, _indent); + + //String condition = BString.replace(transform(_alt.getText().getText()),"\n","").trim(); + String condition = transform(_alt.getUnbrokenText().getLongString()).trim(); + + // TODO: check for File API needs + + addCode("IF " + condition, _indent, isDisabled); + addCode("THEN", _indent, isDisabled); + + generateCode(_alt.qTrue, _indent+this.getIndent()); + + if (_alt.qFalse.getSize() > 0) { + addCode("ELSE", _indent, isDisabled); + generateCode(_alt.qFalse, _indent+this.getIndent()); + } + addCode("END-IF", _indent, isDisabled); + // code.add(_indent+""); + } + + @Override + protected void generateCode(Case _case, String _indent) + { + boolean isDisabled = _case.isDisabled(); + String indent1 = _indent + this.getIndent(); + String indent2 = indent1 + this.getIndent(); + + appendComment(_case, _indent); + + StringList lines = _case.getUnbrokenText(); + + String condition = transform(lines.get(0)); + + addCode("EVALUATE " + condition, _indent, isDisabled); + + for(int i = 0; i < _case.qs.size(); i++) + { + StringList constants = StringList.explode(lines.get(i + 1), ","); + addCode("WHEN = " + constants.concatenate(" OR IS "), indent1, isDisabled); + generateCode((Subqueue) _case.qs.get(i), indent2); + } + + if (!lines.get(_case.qs.size()).trim().equals("%") && _case.qs.get(_case.qs.size()-1).getSize() > 0) + { + addCode("WHEN OTHER", indent1, isDisabled); + generateCode(_case.qs.get(_case.qs.size()-1), indent2); + } + + addCode("END-EVALUATE " + condition, _indent, isDisabled); + } + + @Override + protected void generateCode(For _for, String _indent) + { + boolean isDisabled = _for.isDisabled(); + String bodyName = ""; + if (this.optionFixedSourceFormat()) { + bodyName = transformName("body_" + Integer.toHexString(_for.hashCode())); + } + appendComment(_for, _indent); + if (_for.isForInLoop()) + { + // There aren't many ideas how to implement this here in general, + // but subclasses may have better chances to do so. + if (generateForInCode(_for, _indent)) return; + } + + String varName = transformName(_for.getCounterVar()); + String startVal = transform(_for.getStartValue()); + int step = _for.getStepConst(); + String endVal = transform(_for.getEndValue()); + + addCode("PERFORM " + bodyName + " TEST BEFORE", _indent, isDisabled); + addCode("VARYING " + varName + " FROM " + startVal + + " BY " + step + + " UNTIL " + varName + (step > 0 ? " > " : " < ") + endVal, + _indent + this.getIndent(), isDisabled); + + processProcedure(_for.q, bodyName, _indent, isDisabled); + } + + /** + * @param _for + * @param _indent + * @return + */ + private boolean generateForInCode(For _for, String _indent) { + // TODO + addCode("FOR-IN STILL NOT IMPLEMENTED!", _indent, true); + + return true; + } + + @Override + protected void generateCode(While _while, String _indent) + { + boolean isDisabled = _while.isDisabled(); + String bodyName = ""; + if (this.optionFixedSourceFormat()) { + bodyName = transformName("body_" + Integer.toHexString(_while.hashCode())); + } + appendComment(_while, _indent); + String cond = Element.negateCondition(_while.getUnbrokenText().getLongString()); + addCode("PERFORM " + bodyName + " TEST BEFORE UNTIL " + transform(cond), + _indent, isDisabled); + + processProcedure(_while.q, bodyName, _indent, isDisabled); + } + + @Override + protected void generateCode(Repeat _repeat, String _indent) + { + boolean isDisabled = _repeat.isDisabled(); + String bodyName = ""; + if (this.optionFixedSourceFormat()) { + bodyName = transformName("body_" + Integer.toHexString(_repeat.hashCode())); + } + appendComment(_repeat, _indent); + String cond = _repeat.getUnbrokenText().getLongString(); + addCode("PERFORM " + bodyName + " TEST AFTER UNTIL " + transform(cond), + _indent, isDisabled); + + processProcedure(_repeat.q, bodyName, _indent, isDisabled); + } + + @Override + protected void generateCode(Forever _forever, String _indent) + { + boolean isDisabled = _forever.isDisabled(); + String bodyName = ""; + if (this.optionFixedSourceFormat()) { + bodyName = transformName("body_" + Integer.toHexString(_forever.hashCode())); + } + appendComment(_forever, _indent); + addCode("PERFORM " + bodyName + " FOREVER", _indent, isDisabled); + + processProcedure(_forever.q, bodyName, _indent, isDisabled); + } + + /** + * @param _sq + * @param _bodyName + * @param _indent + * @param _isDisabled + */ + private void processProcedure(Subqueue _sq, String _procName, String _indent, boolean _isDisabled) { + if (_procName.isEmpty()) { + // In free format we will use the inline format as we may indent + generateCode(_sq, _indent + this.getIndent()); + addCode("END-PERFORM", _indent, _isDisabled); + } + else { + // In fixed format it may be better to place the body as procedure + postponedProcedures.put(_procName, _sq); + } + } + + @Override + protected void generateCode(Call _call, String _indent) + { + /* + * Thanks to the decision to confine both procedure and function + * calls within Call elements in Structorizer, we seem to be in + * the comfortable situation to handle all result passing via an + * additional reference parameter without having to distinguish + * between numerical and non-numerical result types. + * But the devil is in the details. As the user is not forced to + * declare a result type in the function header and it is not + * necessary to assign a provided function result to a variable + * (if the function has side effects the user might just ignore + * the returned result such that the Call looks like a procedure + * call). In the event we might miss to append the additional + * reference parameter with the consequence of a wrong signature. + * Moreover it is unclear whether the returning of string result + * may be transformed to filling it as a reference parameter. + * So the code generated here will not be 100 % adequate in some + * cases. + * We prepared a set of auxiliary methods isNumericType(String), + * isNumericType(TypeMapEntry) and hasNumericResult(Root) for + * the potential distinction but they will initially all return + * false. The challenge is to ensure their consistency no matter + * whether we are handling the Root or a Call. + */ + boolean isDisabled = _call.isDisabled(); + StringList lines = _call.getUnbrokenText(); + appendComment(_call, _indent); + Root owningRoot = Element.getRoot(_call); + + //addCode("CALL STILL NOT IMPLEMENTED!", _indent, true); + for (int i = 0; i < lines.count(); i++) { + String line = lines.get(i); + Function called = _call.getCalledRoutine(i); + if (called == null) { + appendComment(line, _indent); } else { - addCode(decl + ".", _indent, false); + StringList paramTypes = new StringList(); + StringList defaults = new StringList(); + boolean hasNumResult = false; + /* The following will only be exact if we assume that a function + * result will always be assigned i.e. the use doesn't call a + * function without being interested in its result (see method + * top comment). + */ + boolean isFct = Call.isAssignment(line); + String target = null; + if (isFct) { + StringList tokens = Element.splitLexically(line, true); + tokens.removeAll(" "); + target = transform(Call.getAssignedVarname(tokens, true)); + } + if (routinePool != null) { + java.util.Vector callCandidates = routinePool.findRoutinesBySignature(called.getName(), called.paramCount(), owningRoot); + if (callCandidates.size() > 0) { + Root sub = callCandidates.get(0); // TODO better selection strategy? + sub.collectParameters(null, paramTypes, defaults); + hasNumResult = hasNumericResult(sub); + } + } + // Procedure call (or function call without getting the result) + String transf = "CALL " + this.transformName(called.getName()); + int nArgs = called.paramCount(); + if (nArgs > 0 || paramTypes != null && !paramTypes.isEmpty() || isFct && !hasNumResult) { + transf += " USING"; + addCode(transf, _indent, isDisabled); + // Now handle the given arguments + for (int j = 0; j < called.paramCount(); j++) { + String arg = called.getParam(j); + boolean isConst = false; + boolean isCompound = false; + String paramType = null; + transf = ""; + if (paramTypes != null && paramTypes.count() > j && (paramType = paramTypes.get(j)) != null) { + isConst = paramType.startsWith("const "); + isCompound = paramType.contains("[") || paramType.toLowerCase().startsWith("array "); + if (Function.testIdentifier(paramType, true, "") && typeMap.containsKey(":" + paramType)) { + TypeMapEntry parType = typeMap.get(":" + paramType); + if (parType != null && (parType.isArray() || parType.isRecord())) { + isCompound = true; + } + } + } + if (this.varNames.contains(arg)) { + TypeMapEntry argType = typeMap.get(arg); + if (argType != null && (argType.isArray() || argType.isRecord())) { + isCompound = true; + } + } + if (isCompound && !isConst) { + transf += "BY REFERENCE "; + } + else { + transf += "BY CONTENT "; + } + /* FIXME: We are in trouble if arg is an expression, since only + * variables or literals may be passed to routines - so we may + * have to assign the value of the expression to a variable in + * advance. But then this variable should already have been declared + * (or we should always declare a set of dummy variables for this + * case). + */ + addCode(transf + transform(arg), _indent + this.getIndent(), isDisabled); + } + // Now handle the omitted arguments + for (int j = nArgs; j < paramTypes.count(); j++) { + String deflt = null; + if (j < defaults.count() && (deflt = defaults.get(j)) != null && !deflt.isEmpty()) { + addCode("BY CONTENT " + transform(deflt), _indent + getIndent(), isDisabled); + } + else { + addCode("OMITTED", _indent + getIndent(), isDisabled); + } + } + // Last but not least ensure the result variable is passed by reference + if (isFct && !hasNumResult) { + addCode("BY REFERENCE " + target, _indent + getIndent(), isDisabled); + } + } + else { + addCode(transf, _indent, isDisabled); + } + if (isFct && hasNumResult) { + addCode("RETURNING " + target, _indent + getIndent(), isDisabled); + } } } - // Add a comment if there is no type info - else if (types == null){ - appendComment(cobName, _indent); + } + + /** + * Tries to decide whether the function {@code sub} has a simple numeric + * result i.e. whether the result may be "RETURNING" or has to be obtained + * via additional reference parameter. + * @param sub - a {@link Root} of type subRoutine + * @return true if the result is primitive-numeric, false otherwise + */ + private boolean hasNumericResult(Root sub) { + boolean isNumeric = false; + /* For consistency between handling a Root and a Call to the same + * Root we must not make use of analysis information that is only + * available while we are processing sub as current Root. + */ + String resultType = sub.getResultType(); + if (resultType != null && !resultType.isEmpty() && !resultType.equals("???")) { + TypeMapEntry typeInfo = typeMap.get(":" + resultType); + if (typeInfo != null) { + isNumeric = isNumericType(typeInfo); + } + else { + isNumeric = isNumericType(transformType(resultType, "???")); + } } - // END KGU#261/KGU#332 2017-01-16 + return isNumeric; } - // END KGU#375 2017-04-12 - // START KGU#332 2017-01-30: Decomposition of generatePreamble to ease sub-classing - private String makeArrayDeclaration(String _elementType, String _varName, TypeMapEntry typeInfo) + @Override + protected void generateCode(Jump _jump, String _indent) { -// int nLevels = _elementType.lastIndexOf('@')+1; -// _elementType = (_elementType.substring(nLevels) + " " + _varName).trim(); -// for (int i = 0; i < nLevels; i++) { -// int maxIndex = typeInfo.getMaxIndex(i); -// _elementType += "[" + (maxIndex >= 0 ? Integer.toString(maxIndex+1) : (i == 0 ? "" : "/*???*/") ) + "]"; -// } - return _elementType; + // TODO + addCode("EXIT STILL NOT IMPLEMENTED!", _indent, true); + + // code.add(_indent+""); } + @Override + protected void generateCode(Parallel _para, String _indent) + { + // TODO + addCode("PARALLEL STILL NOT IMPLEMENTED!", _indent, true); + + // code.add(_indent+""); + for(int i = 0; i < _para.qs.size(); i++) + { + // code.add(_indent+""); + generateCode((Subqueue) _para.qs.get(i), _indent+this.getIndent()); + // code.add(_indent+""); + } + // code.add(_indent+""); + } + protected void generateCode(Try _try, String _indent) { /* FIXME this should somehow be converted to a "declarative procedure" declaration, @@ -571,6 +1287,7 @@ protected void generateCode(Try _try, String _indent) protected String generateHeader(Root _root, String _indent, String _procName, StringList _paramNames, StringList _paramTypes, String _resultType, boolean _public) { + postponedProcedures.clear(); if (topLevel && !this.optionFixedSourceFormat()) { code.add(" >> SOURCE FORMAT IS FREE"); } @@ -653,12 +1370,33 @@ protected String generatePreamble(Root _root, String _indent, StringList varName // Starts the PROCEDURE DIVISION String procdiv = "PROCEDURE DIVISION"; StringList params = _root.getParameterNames(); + boolean hasNumericResult = false; + if (returns || _root.getResultType() != null || isFunctionNameSet || isResultSet) { + // We will enforce that the effective result variable will be named "result" + // But in order to find out the result type we must know which variable to + // check + String resultName = "result"; + String resultType = _root.getResultType(); + if (resultType != null) { + hasNumericResult = isNumericType(resultType); + } + else { + if (isFunctionNameSet && !isResultSet) { + resultName = _root.getMethodName(); + } + TypeMapEntry resType = typeMap.get(resultName); + if (resType != null && isNumericType(resType)) { + hasNumericResult = true; + } + } + if (!hasNumericResult) { + params.add("result"); + } + } if (_root.isSubroutine() || params.count() > 0) { procdiv += " USING " + params.concatenate(" "); } - if (returns || _root.getResultType() != null || isFunctionNameSet || isResultSet) { - // Now the good question is how to guess the name of the return variable and what to do - // if the diagram used return. + if (hasNumericResult) { procdiv += " RETURNING result"; } procdiv += "."; @@ -678,6 +1416,35 @@ protected String generatePreamble(Root _root, String _indent, StringList varName return _indent; } + /** + * Tries to decide whether the given type is primitive numeric (i.e. + * a type that can be used by a COBOL function to RETURN a value). + * @param _typeEntry - a {@link TypeMapEntry} for the type to be + * checked + * @return true if the given {@code _typeEntry} designates a + * primitive numeric type + * @see #isNumericType(String) + * @see #hasNumericResult(Root) + */ + private boolean isNumericType(TypeMapEntry _typeEntry) { + // TODO Auto-generated method stub + // What about strings here? + return !_typeEntry.isArray() && !_typeEntry.isRecord(); + } + /** + * Tries to decide whether the given type is primitive numeric (i.e. + * a type that can be used by a COBOL function to RETURN a value). + * @param _transfTypeName - a type name transformed to COBOL syntax + * @return true if the given {@code _transfTypeName} designates a + * primitive numeric type + * @see #isNumericType(TypeMapEntry) + * @see #hasNumericResult(Root) + */ + private boolean isNumericType(String _transfTypeName) { + // TODO Auto-generated method stub + return false; + } + /** * Creates the appropriate code for returning a required result and adds it * (after the algorithm code of the body) to this.code) @@ -689,27 +1456,19 @@ protected String generatePreamble(Root _root, String _indent, StringList varName @Override protected String generateResult(Root _root, String _indent, boolean alwaysReturns, StringList varNames) { - addCode("CODE SECTION.", _indent, false); // start with any section name - if (_root.isProgram() && !alwaysReturns) - { -// code.add(_indent); -// code.add(_indent + "return 0;"); - } - else if (_root.isSubroutine() && + addCode("CODE SECTION.", _indent, false); // start with any section name (???) + if (_root.isSubroutine() && (returns || _root.getResultType() != null || isFunctionNameSet || isResultSet) && !alwaysReturns) { -// String result = "0"; -// if (isFunctionNameSet) -// { -// result = _root.getMethodName(); -// } -// else if (isResultSet) -// { -// int vx = varNames.indexOf("result", false); -// result = varNames.get(vx); -// } -// code.add(_indent); -// code.add(_indent + "return " + result + ";"); + if (isFunctionNameSet && !isResultSet) + { + String resultName = _root.getMethodName(); + String option = ""; + if (!hasNumericResult(_root)) { + option = "CORRESPONDING "; + } + addCode("MOVE " + option + resultName + " TO result", _indent, false); + } } addCode("GOBACK", _indent, false); // return to the caller (which may be the OS) addCode(".", _indent, false); // Ends the current section and the PROCEDURE DIVISION diff --git a/src/lu/fisch/structorizer/generators/Generator.java b/src/lu/fisch/structorizer/generators/Generator.java index 7f67b1e6..ea9de255 100644 --- a/src/lu/fisch/structorizer/generators/Generator.java +++ b/src/lu/fisch/structorizer/generators/Generator.java @@ -2653,6 +2653,58 @@ else if (_ele instanceof Try) { } // END KGU#236 2016-08-10 + // START KGU#395 2020-04-19: Enh. #357 Introduced for COBOLGenerator but of more general use + /** + * Checks whether the given tokens represent a variable (i.e. some sort of + * "lvalue"). If {@code _mayBeQualified} is {@code true} then a list of all + * sorts of access qualifiers to the right are allowed (i.e. index access + * {@code [...]}, component access {@code .}).
+ * This is a mere syntactic check, i.e. whether the occurring qualifiers meet + * the structure of the variable is not verified! + * @param _tokens - the tokenized expression (without blanks!) + * @param _mayBeQualified - whether qualifiers are allowed (see above) + * @return {@code true} if the expression is a variable + */ + protected boolean isVariable(StringList _tokens, boolean _mayBeQualified, HashMap _typeMap) { + boolean isVar = false; + String token0 = null; + if (!_tokens.isEmpty() && Function.testIdentifier(token0 = _tokens.get(0), true, null) + && (varNames.contains(token0) || _typeMap != null && _typeMap.containsKey(token0))) { + if (_mayBeQualified) { + isVar = true; + _tokens = _tokens.subSequence(1, _tokens.count()); + while (isVar && _tokens.count() > 1 && ".[".contains(token0 = _tokens.get(0))) { + if (token0.equals(".") && Function.testIdentifier(_tokens.get(1), true, null)) { + // Okay, is a component access qualifier + _tokens.remove(0, 2); + } + else if (token0.equals("[") && _tokens.contains("]")) { + // Should be an index access + StringList indices = Element.splitExpressionList(_tokens.subSequence(1, _tokens.count()), ",", true); + String tail = indices.get(indices.count()-1); + // FIXME do we allow more than one index expression? + if (!tail.startsWith("]")) { + isVar = false; + } + else { + _tokens = Element.splitLexically(tail.substring(1), true); + } + } + else { + isVar = false; + } + } + // Okay if completely consumed + isVar = _tokens.isEmpty(); + } + else { + isVar = _tokens.count() == 1; + } + } + return isVar; + } + // END KGU#395 2020-04-19 + /** * This method is responsible for generating the code of an {@code Instruction} element.
* This dummy version is to be overridden by each inheriting generator class. diff --git a/src/lu/fisch/structorizer/generators/PHPGenerator.java b/src/lu/fisch/structorizer/generators/PHPGenerator.java index b3110daf..9b6a43b5 100644 --- a/src/lu/fisch/structorizer/generators/PHPGenerator.java +++ b/src/lu/fisch/structorizer/generators/PHPGenerator.java @@ -71,6 +71,8 @@ * Kay Gürtzig 2020-03-23 Issue #840: Adaptations w.r.t. disabled elements using File API * Kay Gürtzig 2020-04-05 Enh. #828: Preparations for group export (modified include mechanism) * Kay Gürtzig 2020-04-06/07 Bugfixes #843, #844: Global declarations and record/array types + * Kay Gürtzig 2020-11-08/09 Issue #882: Correct translation of random function calls, + * also: randomize -> srand, toDegrees -> rad2deg, toRadians -> deg2rad * ****************************************************************************************************** * @@ -305,7 +307,7 @@ protected String transformTokens(StringList tokens) if (Function.testIdentifier(token, false, null)) { // Check for a preceding dot int k = i; - while (k > 0 && tokens.get(--k).trim().isEmpty()); + while (k > 0 && tokens.get(--k).trim().isEmpty()); // Skip all preceding blanks boolean isComponent = k >= 0 && tokens.get(k).equals("."); if (isComponent) { tokens.set(k++, "["); @@ -314,6 +316,40 @@ protected String transformTokens(StringList tokens) tokens.remove(k, i); i += (k - i) + 1; // This corrects the current index w.r.t. insertions and deletions } + // START KGU#885 2020-11-08: Issue #882 - transform the random function + else { + // Check whether it looks like a function + k = i; // Skip all following blanks + while (k+1 < tokens.count() && tokens.get(++k).trim().isEmpty()); + if (k < tokens.count() && tokens.get(k).equals("(")) { + // It is a function or procedure call, k is the "(" index + if (token.equals("random")) { + StringList exprs = Element.splitExpressionList(tokens.subSequence(k+1, tokens.count()), ",", true); + if (exprs.count() == 2 && exprs.get(1).startsWith(")")) { + // Syntax seems to be okay, so ... + tokens.set(i, "rand"); // Replace "random" by "rand", ... + tokens.remove(k+1, tokens.count()); // clear all that follows the "(" + tokens.add("0"); // ... insert a first argument 0, + tokens.add(","); // ... the argument separator, and ... + tokens.add(" "); + // ... the argument of random, reduced by 1, ... + tokens.add(Element.splitLexically("(" + exprs.get(0) + ") - 1", true)); + // ... and finally the re-tokenized tail + tokens.add(Element.splitLexically(exprs.get(1), true)); + } + } + else if (token.equals("randomize")) { + tokens.set(i, "srand"); + } + else if (token.equals("toDegrees")) { + tokens.set(i, "rad2deg"); + } + else if (token.equals("toRadians")) { + tokens.set(i, "deg2rad"); + } + } + } + // END KGU#885 2020-11-08 } } diff --git a/src/lu/fisch/structorizer/gui/CodeImportMonitor.java b/src/lu/fisch/structorizer/gui/CodeImportMonitor.java index 586747af..5a35fec1 100644 --- a/src/lu/fisch/structorizer/gui/CodeImportMonitor.java +++ b/src/lu/fisch/structorizer/gui/CodeImportMonitor.java @@ -94,16 +94,16 @@ public CodeImportMonitor(Frame _owner, SwingWorker _worker, String _title) { super(_owner, true); this.worker = _worker; - - initComponents(); - - Locales.getInstance().setLocale(this); - - this.setTitle(ttlImporting.getText().replace("%", _title)); - - this.setLocationRelativeTo(_owner); - - this.setVisible(true); + + initComponents(); + + Locales.getInstance().setLocale(this); + + this.setTitle(ttlImporting.getText().replace("%", _title)); + + this.setLocationRelativeTo(_owner); + + this.setVisible(true); } private void initComponents() @@ -166,10 +166,10 @@ private void initComponents() this.getContentPane().add(contentPane); - GUIScaler.rescaleComponents(this); + GUIScaler.rescaleComponents(this); + + this.pack(); - this.pack(); - btnOk.setEnabled(false); this.worker.addPropertyChangeListener(this); this.worker.execute(); diff --git a/src/lu/fisch/structorizer/gui/Diagram.java b/src/lu/fisch/structorizer/gui/Diagram.java index fdb82083..756f96d7 100644 --- a/src/lu/fisch/structorizer/gui/Diagram.java +++ b/src/lu/fisch/structorizer/gui/Diagram.java @@ -204,6 +204,10 @@ * Kay Gürtzig 2020-06-03 Issue #868: Code import via files drop had to be disabled in restricted mode * Kay Gürtzig 2020-10-17 Enh. #872: New display mode for operators (in C style) * Kay Gürtzig 2020-10-18 Issue #875: Direct diagram saving into an archive, group check in canSave(true) + * Kay Gürtzig 2020-10-20/22 Issue #801: Ensured that the User Guide download is done in a background thread + * Kay Gürtzig 2020-12-10 Bugfix #884: Flaws of header inference for virgin diagrams mended + * Kay Gürtzig 2020-12-12 Enh. #704: Adaptations to Turtleizer enhancements + * Kay Gürtzig 2020-12-14 Bugfix #887: TurtleBox must be shared * ****************************************************************************************************** * @@ -229,7 +233,6 @@ import net.iharder.dnd.*; //http://iharder.sourceforge.net/current/java/filedrop/ import java.io.*; -import java.net.HttpURLConnection; import java.net.MalformedURLException; import java.net.SocketTimeoutException; import java.net.URI; @@ -332,7 +335,10 @@ public CancelledException() { //public Root root = new Root(); private Root root = new Root(); // END KGU 2015-10-18 - private TurtleBox turtle = null; // + // START KGU#873 2020-12-14: Bugfix #887 All diagrams must share the same Turtleizer + //private TurtleBox turtle = null; + private static TurtleBox turtle = null; + // END KGU#873 2020-12-14 private Element selected = null; @@ -2070,7 +2076,7 @@ public void openNSD() // END KGU#287 2017-01-09 dlgOpen.setDialogTitle(Menu.msgTitleOpen.getText()); // set directory - if(root.getFile()!=null) + if (root.getFile() != null) { dlgOpen.setCurrentDirectory(root.getFile()); } @@ -2560,13 +2566,63 @@ private void replaceDummyHeader(Root _root, File _file) { // TODO: Infer the argument types and the result type argList = "(" + vars.concatenate(", ") + ")"; vars = _root.getVarNames(); - if (vars.contains(header) || vars.contains("result", false)) { - // TODO also check for return and identify the type + // START KGU#886 2020-12-10: Issue #884 + //if (vars.contains(header) || vars.contains("result", false)) { + IElementVisitor returnFinder = new IElementVisitor() { + private int retLen = CodeParser.getKeyword("preReturn").length(); + @Override + public boolean visitPreOrder(Element _ele) { + if (_ele instanceof Jump) { + StringList lines = _ele.getUnbrokenText(); + for (int i = 0; i < lines.count(); i++) { + String line = lines.get(i); + if (Jump.isReturn(line)) { + // Stops and returns false if a value is returned + // TODO Try to identify the result type + return line.substring(retLen).trim().isEmpty(); + } + } + } + return true; + } + + @Override + public boolean visitPostOrder(Element _ele) { + return true; + } + + }; + boolean lastElReturnsVal = false; + if (root.children.getSize() > 0) { + Element lastEl = root.children.getElement(root.children.getSize()-1); + if (lastEl instanceof Instruction) { + int retLen = CodeParser.getKeyword("preReturn").length(); + StringList lines = lastEl.getUnbrokenText(); + for (int i = 0; i < lines.count(); i++) { + String line = lines.get(i); + if (Jump.isReturn(line)) { + // Stops and detects if a value is returned + lastElReturnsVal = !line.substring(retLen).trim().isEmpty(); + break; + } + } + } + } + if (vars.contains(header) || vars.contains("result", false) + || lastElReturnsVal + || !_root.children.traverse(returnFinder)) { + // END KGU#886 2020-12-10: Issue #884 + // TODO try to identify the type argList += ": ???"; } } if (Function.testIdentifier(header, false, null)) { + // START KGU#886 2020-12-10: Bugfix #884 + //root.setText(header + argList); + root.addUndo(); root.setText(header + argList); + this.analyse(); + // END KGU#886 2020-12-10 this.invalidateAndRedraw(); } } @@ -7282,36 +7338,82 @@ public void helpNSD() // END KGU#250 2016-09-17 else { // Download the current PDF version if there hasn't been any by now. - this.downloadHelpPDF(false); + this.downloadHelpPDF(false, null); } } // END KGU#208 2016-07-22 + // START KGU#791 2010-10-20: Issue #801 - we need a background thread for explicit download + private boolean helpDownloadCancelled = false; + + /** + * Tries to download the most recent user guide as PDF in a backround thread + * with progress bar. Will override a possibly existing file. + * @param title - the menu item caption to be used as window title + */ + public void downloadHelpPDF(String title) + { + SwingWorker worker = new SwingWorker() { + + @Override + protected Boolean doInBackground() throws Exception + { + return downloadHelpPDF(true, this); + } + + public void done() + { + if (isCancelled()) { + // We must tell method downloadHelpPDF that the task was aborted + // (The possibly incompletely transferred file must be deleted.) + helpDownloadCancelled = true; + } + } + + }; + new DownloadMonitor(getFrame(), worker, title, Element.E_HELP_FILE_SIZE); + } + // END KGU#791 2020-10-20 + // START KGU#791 2020-01-20: Enh. #801 support offline help /** * Tries to download the PDF version of the user guide to the ini directory - * @param overrideExisting - if an existing user guide file is to be overriden by the newest one + * @param overrideExisting - if an existing user guide file is to be overriden + * by the newest one + * @param worker - if given then the transfer chunks are chosen smaller and a + * regular progress message will be sent * @return true if the download was done and successful. */ - public boolean downloadHelpPDF(boolean overrideExisting) + public boolean downloadHelpPDF(boolean overrideExisting, SwingWorker worker) { /* See https://stackoverflow.com/questions/921262/how-to-download-and-save-a-file-from-internet-using-java * for technical discussion */ - // FIXME: This might better be done in a performLater environment - boolean done = false; + // KGU#791 2020-10-20 Method revised to allow running in a backround thread + helpDownloadCancelled = false; + String helpFileName = Element.E_HELP_FILE; + File helpDir = Ini.getIniDirectory(true); + File helpFile = new File(helpDir.getAbsolutePath() + File.separator + helpFileName); + String helpFileURI = Element.E_DOWNLOAD_PAGE + "?file=" + helpFileName; + boolean overwritten = false; + long copiedTotal = 0; + long chunk = (worker == null) ? Integer.MAX_VALUE : 1 << 16; try { - String helpFileName = Element.E_HELP_FILE; - String helpFileURI = Element.E_DOWNLOAD_PAGE + "?file=" + helpFileName; URL website = new URL(helpFileURI); - File helpDir = Ini.getIniDirectory(true); - File helpFile = new File(helpDir.getAbsolutePath() + File.separator + helpFileName); if (!helpFile.exists() || overrideExisting) { try (InputStream inputStream = website.openStream(); ReadableByteChannel readableByteChannel = Channels.newChannel(inputStream); FileOutputStream fileOutputStream = new FileOutputStream(helpFile)) { - long copied = fileOutputStream.getChannel().transferFrom(readableByteChannel, 0, 1 << 24); - done = copied > 0; + overwritten = true; + long copied = 0; + do { + copied = fileOutputStream.getChannel(). + transferFrom(readableByteChannel, copiedTotal, chunk); + if (worker != null) { + worker.firePropertyChange("progress", copiedTotal, copiedTotal + copied); + } + copiedTotal += copied; + } while (copied > 0); } catch (IOException ex) { logger.log(Level.INFO, "Failed to download help file!", ex); @@ -7332,9 +7434,14 @@ else if (ex instanceof UnknownHostException) { } } catch (MalformedURLException ex) { - + logger.log(Level.CONFIG, helpFileURI, ex); } - return done; + if (helpDownloadCancelled && overwritten && helpFile.exists()) { + helpFile.delete(); // File is likely to be defective + copiedTotal = 0; + } + //System.out.println("Leaving downloadHelpPDF()"); + return copiedTotal > 0; } /** @@ -7490,7 +7597,7 @@ private String retrieveLatestVersion() try { URL url = new URL(http_url); - HttpURLConnection con = (HttpURLConnection)url.openConnection(); + HttpsURLConnection con = (HttpsURLConnection)url.openConnection(); if (con!=null) { @@ -9132,6 +9239,12 @@ public void toggleCtrlWheelMode() // START KGU#792 2020-02-04: Bugfix #805 Ini.getInstance().setProperty("wheelCtrlReverse", (Element.E_WHEEL_REVERSE_ZOOM ? "1" : "0")); // END KGU#792 2020-02-04 + // START KGU#685 2020-12-12: Enh. #704 + // Behaviour of DiagramControllers should be consistent ... + if (turtle != null) { + turtle.setReverseZoomWheel(Element.E_WHEEL_REVERSE_ZOOM); + } + // END KGU#685 2020-12-12 } // END KGU#503 2018-03-14 @@ -9808,6 +9921,10 @@ public void goTurtle() if (turtle == null) { turtle = new TurtleBox(500,500); + // START KGU#685 2020-12-12: Enh. #704 + Locales.getInstance().register(turtle.getFrame(), true); + turtle.setReverseZoomWheel(Element.E_WHEEL_REVERSE_ZOOM); + // END KGU#685 2020-12-12 } turtle.setVisible(true); // Activate the executor (getInstance() is supposed to do that) @@ -9817,21 +9934,21 @@ public void goTurtle() goRun(); // END KGU#448 2018-01-05 - } - - /** - * Checks for running status of the Root currently held and suggests the user to stop the - * execution if it is running - * @return true if the fostered Root isn't executed (action may proceed), false otherwise - */ - private boolean checkRunning() - { - if (this.root == null || !this.root.isExecuted()) return true; // No problem - // Give the user the chance to kill the execution but don't do anything for now, - // whatever the user may have been decided. - Executor.getInstance(null, null); - return false; - } + } + + /** + * Checks for running status of the Root currently held and suggests the user to stop the + * execution if it is running + * @return true if the fostered Root isn't executed (action may proceed), false otherwise + */ + private boolean checkRunning() + { + if (this.root == null || !this.root.isExecuted()) return true; // No problem + // Give the user the chance to kill the execution but don't do anything for now, + // whatever the user may have been decided. + Executor.getInstance(null, null); + return false; + } // START KGU#448 2018-01-05: Enh. #443 /** @@ -9909,348 +10026,348 @@ private DiagramController[] getEnabledControllers() { } // END KGU#448 2018-01-08 - // START KGU#177 2016-04-07: Enh. #158 - /** - * Tries to shift the selection to the next element in the _direction specified. - * It turned out that on going down and right it's most intuitive to dive into - * the substructure of compound elements (rather than jumping to its successor). - * (For Repeat elements this holds on going up). - * @param _direction - the cursor key orientation (up, down, left, right) - */ - public void moveSelection(Editor.CursorMoveDirection _direction) - { - if (selected != null) - { - Rect selRect = selected.getRectOffDrawPoint(); - // Get center coordinates - int x = (selRect.left + selRect.right) / 2; - int y = (selRect.top + selRect.bottom) / 2; - switch (_direction) - { - case CMD_UP: - // START KGU#495 2018-02-15: Bugfix #511 - we must never dive into collapsed loops! - //if (selected instanceof Repeat) - if (selected instanceof Repeat && !selected.isCollapsed(false)) - // END KGU#495 2018-02-15 - { - // START KGU#292 2016-11-16: Bugfix #291 - //y = ((Repeat)selected).getRectOffDrawPoint().bottom - 2; - y = ((Repeat)selected).getBody().getRectOffDrawPoint().bottom - 2; - // END KGU#292 2016-11-16 - } - else if (selected instanceof Root) - { - y = ((Root)selected).children.getRectOffDrawPoint().bottom - 2; - } - else - { - y = selRect.top - 2; - } - break; - case CMD_DOWN: - // START KGU#495 2018-02-15: Bugfix #511 - we must never dive into collapsed loops! - //if (selected instanceof ILoop && !(selected instanceof Repeat)) - if (selected instanceof ILoop && !selected.isCollapsed(false) && !(selected instanceof Repeat)) - // END KGU#495 2018-02-15 - { - Subqueue body = ((ILoop)selected).getBody(); - y = body.getRectOffDrawPoint().top + 2; - } - // START KGU#346 2017-02-08: Issue #198 - Unification of forking elements - //else if (selected instanceof Alternative) - //{ - // y = ((Alternative)selected).qTrue.getRectOffDrawPoint().top + 2; - //} - //else if (selected instanceof Case) - //{ - // y = ((Case)selected).qs.get(0).getRectOffDrawPoint().top + 2; - //} - // START KGU#498 2018-02-18: Bugfix #511 - cursor was caught when collapsed - //else if (selected instanceof IFork) - else if (selected instanceof IFork && !selected.isCollapsed(false)) - // END KGU#498 2018-02-18 - { - y = selRect.top + ((IFork)selected).getHeadRect().bottom + 2; - } - // END KGU#346 2017-02-08 - // START KGU#498 2018-02-18: Bugfix #511 - cursor was caught when collapsed - //else if (selected instanceof Parallel) - else if (selected instanceof Parallel && !selected.isCollapsed(false)) - // END KGU#498 2018-02-18 - { - y = ((Parallel)selected).qs.get(0).getRectOffDrawPoint().top + 2; - } - else if (selected instanceof Root) - { - y = ((Root)selected).children.getRectOffDrawPoint().top + 2; - } - // START KGU#729 2019-09-24: Bugfix #751 - else if (selected instanceof Try) { - y = ((Try)selected).qTry.getRectOffDrawPoint().top + 2; - } - // END KGU#729 2019-09-24 - else - { - y = selRect.bottom + 2; - } - break; - case CMD_LEFT: - if (selected instanceof Root) - { - Rect bodyRect =((Root)selected).children.getRectOffDrawPoint(); - // The central element of the subqueue isn't the worst choice because from - // here the distances are minimal. The top element, on the other hand, - // is directly reachable by cursor down. - x = bodyRect.right - 2; - y = (bodyRect.top + bodyRect.bottom) / 2; - } - else - { - x = selRect.left - 2; - // START KGU#346 2017-02-08: Bugfix #198: It's more intuitive to stay at header y level - if (selected instanceof IFork) { - y = selRect.top + ((IFork)selected).getHeadRect().bottom/2; - } - // END KGU#346 2017-02-08 - } - break; - case CMD_RIGHT: - // START KGU#495 2018-02-15: Bugfix #511 - we must never dive into collapsed loops! - //if (selected instanceof ILoop) - if (selected instanceof ILoop && !selected.isCollapsed(false)) - // END KGU#495 2018-02-15 - { - Rect bodyRect = ((ILoop)selected).getBody().getRectOffDrawPoint(); - x = bodyRect.left + 2; - // The central element of the subqueue isn't the worst choice because from - // here the distances are minimal. The top element, on the other hand, - // is directly reachable by cursor down. - y = (bodyRect.top + bodyRect.bottom) / 2; - } - else if (selected instanceof Root) - { - Rect bodyRect =((Root)selected).children.getRectOffDrawPoint(); - // The central element of the subqueue isn't the worst choice because from - // here the distances are minimal. The top element, on the other hand, - // is directly reachable by cursor down. - x = bodyRect.left + 2; - y = (bodyRect.top + bodyRect.bottom) / 2; - } - // START KGU#729 2019-09-24: Bugfix #751 - else if (selected instanceof Try) { - Rect catchRect = ((Try)selected).qCatch.getRectOffDrawPoint(); - x = catchRect.left + 2; - y = catchRect.top + 2; - } - // END KGU#729 2019-09-24 - else - { - x = selRect.right + 2; - // START KGU#346 2017-02-08: Bugfix #198: It's more intuitive to stay at header y level - if (selected instanceof IFork) { - y = selRect.top + ((IFork)selected).getHeadRect().bottom/2; - } - // END KGU#346 2017-02-08 - } - break; - } - Element newSel = root.getElementByCoord(x, y, true); - if (newSel != null) - { - // START KGU#177 2016-04-24: Bugfix - couldn't leave Parallel and Forever elements - // Compound elements with a lower bar would catch the selection again when their last - // encorporated element is left downwards. So identify such a situation and leap after - // the enclosing compound... - if (_direction == Editor.CursorMoveDirection.CMD_DOWN && - (newSel instanceof Parallel || newSel instanceof Forever || !Element.E_DIN && newSel instanceof For) && - newSel.getRectOffDrawPoint().top < selRect.top) - { - newSel = root.getElementByCoord(x, newSel.getRectOffDrawPoint().bottom + 2, true); - } - // END KGU#177 2016-04-24 - // START KGU#214 2016-07-25: Improvement of enh. #158 - else if (_direction == Editor.CursorMoveDirection.CMD_UP && - (newSel instanceof Forever || !Element.E_DIN && newSel instanceof For) && - newSel.getRectOffDrawPoint().bottom < selRect.bottom) - { - Subqueue body = ((ILoop)newSel).getBody(); - Element sel = root.getElementByCoord(x, body.getRectOffDrawPoint().bottom - 2, true); - if (sel != null) - { - newSel = sel; - } - } - // END KGU#214 2016-07-25 - // START KGU#214 2016-07-31: Issue #158 - else if (newSel instanceof Root && (_direction == Editor.CursorMoveDirection.CMD_LEFT - || _direction == Editor.CursorMoveDirection.CMD_RIGHT)) - { - newSel = selected; // Stop before the border on boxed diagrams - } - // END KGU#214 2015-07-31 - // START KGU#729 2019-09-24: Bugfix #751 - else if (newSel instanceof Try) { - Element sel = null; - if (_direction == Editor.CursorMoveDirection.CMD_UP) { - // From finally go to catch, from catch to try, from outside to finally - if (selected == ((Try)newSel).qFinally || selected.isDescendantOf(((Try)newSel).qFinally)) { - sel = root.getElementByCoord(x, ((Try)newSel).qCatch.getRectOffDrawPoint().bottom - 2, true); - } - else if (selected == ((Try)newSel).qCatch || selected.isDescendantOf(((Try)newSel).qCatch)) { - sel = root.getElementByCoord(x, ((Try)newSel).qTry.getRectOffDrawPoint().bottom - 2, true); - } - else if (!selected.isDescendantOf(newSel)) { - sel = root.getElementByCoord(x, ((Try)newSel).qFinally.getRectOffDrawPoint().bottom - 2, true); - } - } - else if (_direction == Editor.CursorMoveDirection.CMD_DOWN) { - // From try go to catch, from catch to finally, from finally to subsequent element - if (selected == ((Try)newSel).qTry || selected.isDescendantOf(((Try)newSel).qTry)) { - sel = root.getElementByCoord(x, ((Try)newSel).qCatch.getRectOffDrawPoint().top + 2, true); - } - else if (selected == ((Try)newSel).qCatch || selected.isDescendantOf(((Try)newSel).qCatch)) { - sel = root.getElementByCoord(x, ((Try)newSel).qFinally.getRectOffDrawPoint().top + 2, true); - } - else if (selected == ((Try)newSel).qFinally || selected.isDescendantOf(((Try)newSel).qFinally)) { - sel = root.getElementByCoord(x, newSel.getRectOffDrawPoint().bottom + 2, true); - } - } - if (sel != null) { - newSel = sel; - } - } - // END KGU#729 2019-09-24 - selected = newSel; - } - // START KGU#214 2016-07-25: Bugfix for enh. #158 - un-boxed Roots didn't catch the selection - // This was better than to rush around on horizontal wheel activity! Hence fix withdrawn -// else if (_direction != Editor.CursorMoveDirection.CMD_UP && !root.isNice) -// { -// selected = root; -// } - // END KGU#214 2016-07-25 - selected = selected.setSelected(true); - - // START KGU#177 2016-04-14: Enh. #158 - scroll to the selected element - //redraw(); - redraw(selected); - // END KGU#177 2016-04-14 - - // START KGU#705 2019-09-24: Enh. #738 - highlightCodeForSelection(); - // END KGU#705 2019-09-24 - // START KGU#177 2016-04-24: Bugfix - buttons haven't been updated - this.doButtons(); - // END KGU#177 2016-04-24 - } - } - // END KGU#177 2016-04-07 + // START KGU#177 2016-04-07: Enh. #158 + /** + * Tries to shift the selection to the next element in the _direction specified. + * It turned out that on going down and right it's most intuitive to dive into + * the substructure of compound elements (rather than jumping to its successor). + * (For Repeat elements this holds on going up). + * @param _direction - the cursor key orientation (up, down, left, right) + */ + public void moveSelection(Editor.CursorMoveDirection _direction) + { + if (selected != null) + { + Rect selRect = selected.getRectOffDrawPoint(); + // Get center coordinates + int x = (selRect.left + selRect.right) / 2; + int y = (selRect.top + selRect.bottom) / 2; + switch (_direction) + { + case CMD_UP: + // START KGU#495 2018-02-15: Bugfix #511 - we must never dive into collapsed loops! + //if (selected instanceof Repeat) + if (selected instanceof Repeat && !selected.isCollapsed(false)) + // END KGU#495 2018-02-15 + { + // START KGU#292 2016-11-16: Bugfix #291 + //y = ((Repeat)selected).getRectOffDrawPoint().bottom - 2; + y = ((Repeat)selected).getBody().getRectOffDrawPoint().bottom - 2; + // END KGU#292 2016-11-16 + } + else if (selected instanceof Root) + { + y = ((Root)selected).children.getRectOffDrawPoint().bottom - 2; + } + else + { + y = selRect.top - 2; + } + break; + case CMD_DOWN: + // START KGU#495 2018-02-15: Bugfix #511 - we must never dive into collapsed loops! + //if (selected instanceof ILoop && !(selected instanceof Repeat)) + if (selected instanceof ILoop && !selected.isCollapsed(false) && !(selected instanceof Repeat)) + // END KGU#495 2018-02-15 + { + Subqueue body = ((ILoop)selected).getBody(); + y = body.getRectOffDrawPoint().top + 2; + } + // START KGU#346 2017-02-08: Issue #198 - Unification of forking elements + //else if (selected instanceof Alternative) + //{ + // y = ((Alternative)selected).qTrue.getRectOffDrawPoint().top + 2; + //} + //else if (selected instanceof Case) + //{ + // y = ((Case)selected).qs.get(0).getRectOffDrawPoint().top + 2; + //} + // START KGU#498 2018-02-18: Bugfix #511 - cursor was caught when collapsed + //else if (selected instanceof IFork) + else if (selected instanceof IFork && !selected.isCollapsed(false)) + // END KGU#498 2018-02-18 + { + y = selRect.top + ((IFork)selected).getHeadRect().bottom + 2; + } + // END KGU#346 2017-02-08 + // START KGU#498 2018-02-18: Bugfix #511 - cursor was caught when collapsed + //else if (selected instanceof Parallel) + else if (selected instanceof Parallel && !selected.isCollapsed(false)) + // END KGU#498 2018-02-18 + { + y = ((Parallel)selected).qs.get(0).getRectOffDrawPoint().top + 2; + } + else if (selected instanceof Root) + { + y = ((Root)selected).children.getRectOffDrawPoint().top + 2; + } + // START KGU#729 2019-09-24: Bugfix #751 + else if (selected instanceof Try) { + y = ((Try)selected).qTry.getRectOffDrawPoint().top + 2; + } + // END KGU#729 2019-09-24 + else + { + y = selRect.bottom + 2; + } + break; + case CMD_LEFT: + if (selected instanceof Root) + { + Rect bodyRect =((Root)selected).children.getRectOffDrawPoint(); + // The central element of the subqueue isn't the worst choice because from + // here the distances are minimal. The top element, on the other hand, + // is directly reachable by cursor down. + x = bodyRect.right - 2; + y = (bodyRect.top + bodyRect.bottom) / 2; + } + else + { + x = selRect.left - 2; + // START KGU#346 2017-02-08: Bugfix #198: It's more intuitive to stay at header y level + if (selected instanceof IFork) { + y = selRect.top + ((IFork)selected).getHeadRect().bottom/2; + } + // END KGU#346 2017-02-08 + } + break; + case CMD_RIGHT: + // START KGU#495 2018-02-15: Bugfix #511 - we must never dive into collapsed loops! + //if (selected instanceof ILoop) + if (selected instanceof ILoop && !selected.isCollapsed(false)) + // END KGU#495 2018-02-15 + { + Rect bodyRect = ((ILoop)selected).getBody().getRectOffDrawPoint(); + x = bodyRect.left + 2; + // The central element of the subqueue isn't the worst choice because from + // here the distances are minimal. The top element, on the other hand, + // is directly reachable by cursor down. + y = (bodyRect.top + bodyRect.bottom) / 2; + } + else if (selected instanceof Root) + { + Rect bodyRect =((Root)selected).children.getRectOffDrawPoint(); + // The central element of the subqueue isn't the worst choice because from + // here the distances are minimal. The top element, on the other hand, + // is directly reachable by cursor down. + x = bodyRect.left + 2; + y = (bodyRect.top + bodyRect.bottom) / 2; + } + // START KGU#729 2019-09-24: Bugfix #751 + else if (selected instanceof Try) { + Rect catchRect = ((Try)selected).qCatch.getRectOffDrawPoint(); + x = catchRect.left + 2; + y = catchRect.top + 2; + } + // END KGU#729 2019-09-24 + else + { + x = selRect.right + 2; + // START KGU#346 2017-02-08: Bugfix #198: It's more intuitive to stay at header y level + if (selected instanceof IFork) { + y = selRect.top + ((IFork)selected).getHeadRect().bottom/2; + } + // END KGU#346 2017-02-08 + } + break; + } + Element newSel = root.getElementByCoord(x, y, true); + if (newSel != null) + { + // START KGU#177 2016-04-24: Bugfix - couldn't leave Parallel and Forever elements + // Compound elements with a lower bar would catch the selection again when their last + // encorporated element is left downwards. So identify such a situation and leap after + // the enclosing compound... + if (_direction == Editor.CursorMoveDirection.CMD_DOWN && + (newSel instanceof Parallel || newSel instanceof Forever || !Element.E_DIN && newSel instanceof For) && + newSel.getRectOffDrawPoint().top < selRect.top) + { + newSel = root.getElementByCoord(x, newSel.getRectOffDrawPoint().bottom + 2, true); + } + // END KGU#177 2016-04-24 + // START KGU#214 2016-07-25: Improvement of enh. #158 + else if (_direction == Editor.CursorMoveDirection.CMD_UP && + (newSel instanceof Forever || !Element.E_DIN && newSel instanceof For) && + newSel.getRectOffDrawPoint().bottom < selRect.bottom) + { + Subqueue body = ((ILoop)newSel).getBody(); + Element sel = root.getElementByCoord(x, body.getRectOffDrawPoint().bottom - 2, true); + if (sel != null) + { + newSel = sel; + } + } + // END KGU#214 2016-07-25 + // START KGU#214 2016-07-31: Issue #158 + else if (newSel instanceof Root && (_direction == Editor.CursorMoveDirection.CMD_LEFT + || _direction == Editor.CursorMoveDirection.CMD_RIGHT)) + { + newSel = selected; // Stop before the border on boxed diagrams + } + // END KGU#214 2015-07-31 + // START KGU#729 2019-09-24: Bugfix #751 + else if (newSel instanceof Try) { + Element sel = null; + if (_direction == Editor.CursorMoveDirection.CMD_UP) { + // From finally go to catch, from catch to try, from outside to finally + if (selected == ((Try)newSel).qFinally || selected.isDescendantOf(((Try)newSel).qFinally)) { + sel = root.getElementByCoord(x, ((Try)newSel).qCatch.getRectOffDrawPoint().bottom - 2, true); + } + else if (selected == ((Try)newSel).qCatch || selected.isDescendantOf(((Try)newSel).qCatch)) { + sel = root.getElementByCoord(x, ((Try)newSel).qTry.getRectOffDrawPoint().bottom - 2, true); + } + else if (!selected.isDescendantOf(newSel)) { + sel = root.getElementByCoord(x, ((Try)newSel).qFinally.getRectOffDrawPoint().bottom - 2, true); + } + } + else if (_direction == Editor.CursorMoveDirection.CMD_DOWN) { + // From try go to catch, from catch to finally, from finally to subsequent element + if (selected == ((Try)newSel).qTry || selected.isDescendantOf(((Try)newSel).qTry)) { + sel = root.getElementByCoord(x, ((Try)newSel).qCatch.getRectOffDrawPoint().top + 2, true); + } + else if (selected == ((Try)newSel).qCatch || selected.isDescendantOf(((Try)newSel).qCatch)) { + sel = root.getElementByCoord(x, ((Try)newSel).qFinally.getRectOffDrawPoint().top + 2, true); + } + else if (selected == ((Try)newSel).qFinally || selected.isDescendantOf(((Try)newSel).qFinally)) { + sel = root.getElementByCoord(x, newSel.getRectOffDrawPoint().bottom + 2, true); + } + } + if (sel != null) { + newSel = sel; + } + } + // END KGU#729 2019-09-24 + selected = newSel; + } + // START KGU#214 2016-07-25: Bugfix for enh. #158 - un-boxed Roots didn't catch the selection + // This was better than to rush around on horizontal wheel activity! Hence fix withdrawn +// else if (_direction != Editor.CursorMoveDirection.CMD_UP && !root.isNice) +// { +// selected = root; +// } + // END KGU#214 2016-07-25 + selected = selected.setSelected(true); - // START KGU#206 2016-07-21: Enh. #158 + #197 - /** - * Tries to expand the selection towards the next element in the _direction - * specified. - * This is of course limited to the bounds of the containing Subqueue. - * @param _direction - the cursor key orientation (up, down) - */ - public void expandSelection(Editor.SelectionExpandDirection _direction) - { - if (selected != null - && !(selected instanceof Subqueue) - && !(selected instanceof Root)) - { - boolean newSelection = false; - Subqueue sq = (Subqueue)selected.parent; - Element first = selected; - Element last = selected; - // START KGU#866 2020-05-02: Issue #866 improved expansion / reduction strategy - int anchorOffset = 0; - boolean atUpperEnd = true; // Is the selection to be modified at upper end? - boolean atLowerEnd = true; // Is the selection to be modified at lower end? - // END KGU#866 2020-05-02 - if (selected instanceof SelectedSequence) - { - SelectedSequence sel = (SelectedSequence)selected; - first = sel.getElement(0); - last = sel.getElement(sel.getSize()-1); - // START KGU#866 2020-05-02: Issue #866 improved expansion / reduction strategy - anchorOffset = sel.getAnchorOffset(); - atUpperEnd = !sel.wasModifiedBelowAnchor(); // FIXME offset of anchor? - atLowerEnd = sel.wasModifiedBelowAnchor(); - // END KGU#866 2020-05-02 - } - int index0 = sq.getIndexOf(first); - int index1 = sq.getIndexOf(last); - // START KGU#866 2020-05-02: Issue #866 improved expansion / reduction strategy - //if (_direction == Editor.SelectionExpandDirection.EXPAND_UP && index0 > 0) - if (index0 > 0 && - (_direction == Editor.SelectionExpandDirection.EXPAND_TOP - || _direction == Editor.SelectionExpandDirection.EXPAND_UP && atUpperEnd)) - // END KGU#866 2020-05-02 - { - // START KGU#866 2020-05-02: Issue #866 improved expansion / reduction strategy - //selected = new SelectedSequence(sq, index0-1, index1); - selected = new SelectedSequence(sq, index0-1, index1, anchorOffset+1, false); - // END KGU#866 2020-05-02 - newSelection = true; - } - // START KGU#866 2020-05-02: Issue #866 improved expansion / reduction strategy - //else if (_direction == Editor.SelectionExpandDirection.EXPAND_DOWN && index1 < sq.getSize()-1) - else if (index1 < sq.getSize()-1 && - (_direction == Editor.SelectionExpandDirection.EXPAND_BOTTOM - || _direction == Editor.SelectionExpandDirection.EXPAND_DOWN && atLowerEnd)) - // END KGU#866 2020-05-02 - { - // START KGU#866 2020-05-02: Issue #866 improved expansion / reduction strategy - //selected = new SelectedSequence(sq, index0, index1+1, _index1, true); - selected = new SelectedSequence(sq, index0, index1+1, anchorOffset, true); - // END KGU#866 2020-05-02 - newSelection = true; - } - // START KGU#866 2020-05-02: Issue #866 improved expansion / reduction strategy - else if (_direction == Editor.SelectionExpandDirection.EXPAND_UP && atLowerEnd) { - // Reduce at end - selected.setSelected(false); - redraw(selected); - if (index0+1 >= index1) { - // Selected sequence collapses to a single element - selected = first; - } - else { - selected = new SelectedSequence(sq, index0, index1-1, - (anchorOffset == index1 - index0 ? anchorOffset - 1 : anchorOffset), - true); - } - newSelection = true; - } - else if (_direction == Editor.SelectionExpandDirection.EXPAND_DOWN && atUpperEnd) { - // Reduce at start - selected.setSelected(false); - redraw(selected); - if (index0+1 >= index1) { - // Selected sequence collapses to a single element - selected = last; - } - else { - selected = new SelectedSequence(sq, index0+1, index1, - (anchorOffset == 0 ? 0 : anchorOffset - 1), - false); - } - newSelection = true; - } - // END KGU#866 2020-05-02 - if (newSelection) - { - selected.setSelected(true); - redraw(selected); - this.doButtons(); - } - // START KGU#705 2019-09-24: Enh. #738 - highlightCodeForSelection(); - // END KGU#705 2019-09-24 - } - } - // END KGU#206 2016-07-21 + // START KGU#177 2016-04-14: Enh. #158 - scroll to the selected element + //redraw(); + redraw(selected); + // END KGU#177 2016-04-14 + + // START KGU#705 2019-09-24: Enh. #738 + highlightCodeForSelection(); + // END KGU#705 2019-09-24 + // START KGU#177 2016-04-24: Bugfix - buttons haven't been updated + this.doButtons(); + // END KGU#177 2016-04-24 + } + } + // END KGU#177 2016-04-07 + + // START KGU#206 2016-07-21: Enh. #158 + #197 + /** + * Tries to expand the selection towards the next element in the _direction + * specified. + * This is of course limited to the bounds of the containing Subqueue. + * @param _direction - the cursor key orientation (up, down) + */ + public void expandSelection(Editor.SelectionExpandDirection _direction) + { + if (selected != null + && !(selected instanceof Subqueue) + && !(selected instanceof Root)) + { + boolean newSelection = false; + Subqueue sq = (Subqueue)selected.parent; + Element first = selected; + Element last = selected; + // START KGU#866 2020-05-02: Issue #866 improved expansion / reduction strategy + int anchorOffset = 0; + boolean atUpperEnd = true; // Is the selection to be modified at upper end? + boolean atLowerEnd = true; // Is the selection to be modified at lower end? + // END KGU#866 2020-05-02 + if (selected instanceof SelectedSequence) + { + SelectedSequence sel = (SelectedSequence)selected; + first = sel.getElement(0); + last = sel.getElement(sel.getSize()-1); + // START KGU#866 2020-05-02: Issue #866 improved expansion / reduction strategy + anchorOffset = sel.getAnchorOffset(); + atUpperEnd = !sel.wasModifiedBelowAnchor(); // FIXME offset of anchor? + atLowerEnd = sel.wasModifiedBelowAnchor(); + // END KGU#866 2020-05-02 + } + int index0 = sq.getIndexOf(first); + int index1 = sq.getIndexOf(last); + // START KGU#866 2020-05-02: Issue #866 improved expansion / reduction strategy + //if (_direction == Editor.SelectionExpandDirection.EXPAND_UP && index0 > 0) + if (index0 > 0 && + (_direction == Editor.SelectionExpandDirection.EXPAND_TOP + || _direction == Editor.SelectionExpandDirection.EXPAND_UP && atUpperEnd)) + // END KGU#866 2020-05-02 + { + // START KGU#866 2020-05-02: Issue #866 improved expansion / reduction strategy + //selected = new SelectedSequence(sq, index0-1, index1); + selected = new SelectedSequence(sq, index0-1, index1, anchorOffset+1, false); + // END KGU#866 2020-05-02 + newSelection = true; + } + // START KGU#866 2020-05-02: Issue #866 improved expansion / reduction strategy + //else if (_direction == Editor.SelectionExpandDirection.EXPAND_DOWN && index1 < sq.getSize()-1) + else if (index1 < sq.getSize()-1 && + (_direction == Editor.SelectionExpandDirection.EXPAND_BOTTOM + || _direction == Editor.SelectionExpandDirection.EXPAND_DOWN && atLowerEnd)) + // END KGU#866 2020-05-02 + { + // START KGU#866 2020-05-02: Issue #866 improved expansion / reduction strategy + //selected = new SelectedSequence(sq, index0, index1+1, _index1, true); + selected = new SelectedSequence(sq, index0, index1+1, anchorOffset, true); + // END KGU#866 2020-05-02 + newSelection = true; + } + // START KGU#866 2020-05-02: Issue #866 improved expansion / reduction strategy + else if (_direction == Editor.SelectionExpandDirection.EXPAND_UP && atLowerEnd) { + // Reduce at end + selected.setSelected(false); + redraw(selected); + if (index0+1 >= index1) { + // Selected sequence collapses to a single element + selected = first; + } + else { + selected = new SelectedSequence(sq, index0, index1-1, + (anchorOffset == index1 - index0 ? anchorOffset - 1 : anchorOffset), + true); + } + newSelection = true; + } + else if (_direction == Editor.SelectionExpandDirection.EXPAND_DOWN && atUpperEnd) { + // Reduce at start + selected.setSelected(false); + redraw(selected); + if (index0+1 >= index1) { + // Selected sequence collapses to a single element + selected = last; + } + else { + selected = new SelectedSequence(sq, index0+1, index1, + (anchorOffset == 0 ? 0 : anchorOffset - 1), + false); + } + newSelection = true; + } + // END KGU#866 2020-05-02 + if (newSelection) + { + selected.setSelected(true); + redraw(selected); + this.doButtons(); + } + // START KGU#705 2019-09-24: Enh. #738 + highlightCodeForSelection(); + // END KGU#705 2019-09-24 + } + } + // END KGU#206 2016-07-21 @Override public void lostOwnership(Clipboard arg0, Transferable arg1) { @@ -10351,7 +10468,8 @@ public void findAndReplaceNSD() { } /** - * This only cares for the look and feel update of the Find&Replace dialog if it is open. + * This only cares for the look and feel update of the Find&Replace dialog + * (if it is open) and the Turtleizer. */ protected void updateLookAndFeel() { @@ -10363,6 +10481,11 @@ protected void updateLookAndFeel() } catch (Exception ex) {} } + // START KGU#685 2020-12-12: Enh. #704 + if (turtle != null) { + turtle.updateLookAndFeel(); + } + // END KGU#685 2020-12-12 if (this.codeHighlighter != null) { this.codeHighlighter = codePreview.getHighlighter(); diff --git a/src/lu/fisch/structorizer/gui/DownloadMonitor.java b/src/lu/fisch/structorizer/gui/DownloadMonitor.java new file mode 100644 index 00000000..d3e0d52a --- /dev/null +++ b/src/lu/fisch/structorizer/gui/DownloadMonitor.java @@ -0,0 +1,181 @@ +/* + Structorizer + A little tool which you can use to create Nassi-Shneiderman Diagrams (NSD) + + Copyright (C) 2009, 2020 Bob Fisch + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or any + later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + */ + +package lu.fisch.structorizer.gui; + +/****************************************************************************************************** + * + * Author: Kay Gürtzig + * + * Description: Class Download monitor, e.g. for User Guide download (#801) in background + * + ****************************************************************************************************** + * + * Revision List + * + * Author Date Description + * ------ ---- ----------- + * Kay Gürtzig 2020-10-20 First Issue (for #801 improvement) + * Kay Gürtzig 2020-10-22 Sensible progress bar + * + ****************************************************************************************************** + * + * Comment: + * 2020-10-20 Kay Gürtzig + * Until version 3.30-11, the User Guide download was inexpertly done in the event dispatch + * thread, thus making the GUI unresponsive for even several minutes (because the download takes + * an unreasonable time in comparison with a browser download of the same file - why?). So it + * had to be put in a background thread. Possibly it would be the best just to start it in the + * background and not mention it anymore - whatever time it takes. + * But well, I did the opposite and showed a progress bar in this little window placed at the + * upper left corner. + * + ******************************************************************************************************/// + +import java.awt.Frame; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; + +import javax.swing.BoxLayout; +import javax.swing.JButton; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JProgressBar; +import javax.swing.SwingWorker; + +import lu.fisch.structorizer.locales.LangDialog; + + +/** + * Floating temporary dialog showing a progress bar during the download of a large file + * @author Kay Gürtzig + */ +@SuppressWarnings("serial") +public class DownloadMonitor extends LangDialog implements PropertyChangeListener { + + private SwingWorker worker; + private JProgressBar progrBar; + private JButton btnCancel; + private long size = 0; + + public DownloadMonitor(Frame owner, SwingWorker downloadWorker, String caption, long expectedSize) + { + super(owner); + worker = downloadWorker; + size = expectedSize; + + initComponents(); + + setTitle(caption); + + this.setLocation(0, 0); + this.setModalityType(ModalityType.MODELESS); + this.addWindowListener(new WindowAdapter() { + @Override + public void windowClosing(WindowEvent evt) + { + btnCancel.doClick(); +// worker.cancel(true); +// progrBar.setIndeterminate(false); +// progrBar.setValue(30); +// JOptionPane.showMessageDialog(getOwner(), +// Menu.msgDownloadFailed.getText().replace("%", Menu.lblCancel.getText()), +// getTitle(), +// JOptionPane.WARNING_MESSAGE); +// dispose(); + } + }); + this.pack(); + this.setVisible(true); + } + + /** + * + */ + private void initComponents() { + + JPanel contentPane = new JPanel(); + contentPane.setLayout(new BoxLayout(contentPane, BoxLayout.X_AXIS)); + + progrBar = new JProgressBar(0, 100); + progrBar.setValue(0); + progrBar.setStringPainted(true); + progrBar.setString(""); + progrBar.setIndeterminate(true); + + btnCancel = new JButton("Cancel"); + btnCancel.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent evt) { + worker.cancel(true); + progrBar.setIndeterminate(false); + //progrBar.setValue(30); + btnCancel.setEnabled(false); + setVisible(false); + dispose(); + }}); + + contentPane.add(progrBar); + contentPane.add(btnCancel); + + this.getContentPane().add(contentPane); + + this.worker.addPropertyChangeListener(this); + this.worker.execute(); + } + + @Override + public void propertyChange(PropertyChangeEvent evt) { + if (evt.getSource() == worker) { + if ("state".equals(evt.getPropertyName())) { + //System.out.println("Property Change received: " + // + evt.getPropertyName() + "-->" + evt.getNewValue().toString()); + if (SwingWorker.StateValue.STARTED == evt.getNewValue()) { + progrBar.setIndeterminate(false); + progrBar.setString("0 %"); + } + else if (SwingWorker.StateValue.DONE == evt.getNewValue() + && !worker.isCancelled()) { +// progrBar.setIndeterminate(false); +// progrBar.setValue(100); + progrBar.setString("100 %"); + btnCancel.setEnabled(false); + setVisible(false); + dispose(); + } + } + else if ("progress".equals(evt.getPropertyName()) && size > 0) { + Object value = evt.getNewValue(); + if (value instanceof Long) { + int percentage = (int)Math.min((100 * ((Long)value).intValue() / size), 100); + progrBar.setValue(percentage); + // We better avoid the misconception that the transfer is ready + if (percentage < 100) { + progrBar.setString(percentage + " %"); + } + } + } + } + } +} diff --git a/src/lu/fisch/structorizer/gui/Editor.java b/src/lu/fisch/structorizer/gui/Editor.java index 41653550..a6b43347 100644 --- a/src/lu/fisch/structorizer/gui/Editor.java +++ b/src/lu/fisch/structorizer/gui/Editor.java @@ -85,6 +85,8 @@ * Kay Gürtzig 2020-06-03 Bugfix #868: Suppression of export / import now works without side-effect * Kay Gürtzig 2020-06-06 Bugfix #868/#870: Suppression of group export had been forgotten. * Kay Gürtzig 2020-10-17 Enh. #872: New toolbar with 2 display mode indicators + * Kay Gürtzig 2020-12-11 Bugfix #885: Display mode indicator visibility mended + * Kay Gürtzig 2020-10-15 Bugfix #885 enabling rule for the mode display was still flawed * ****************************************************************************************************** * @@ -1469,11 +1471,18 @@ public void doButtonsLocal() btnInclude.setSelected(diagram.isInclude()); // START KGU#872 2020-10-17: Enh. #872 - lblSwitchComments.setEnabled(Element.E_TOGGLETC); + // START KGU#887 2020-12-11: Bugfix #885 + //lblSwitchComments.setEnabled(Element.E_TOGGLETC); + lblSwitchComments.setEnabled(Element.isSwitchTextCommentMode()); + // END KGU#887 2020-12-11 lblSwitchComments.setToolTipText(buildMessageWithMenuRef( lblSwitchComments.isEnabled() ? ttSwitchComments : msgInactiveMode, Menu.getLocalizedMenuPath(pathSwitchComments, null))); - lblOperatorsC.setEnabled(Element.E_SHOW_C_OPERATORS && !Element.E_TOGGLETC); + // START KGU#887 2020-12-11: Bugfix #885 + //lblOperatorsC.setEnabled(Element.E_SHOW_C_OPERATORS && !Element.E_TOGGLETC); + lblOperatorsC.setEnabled(Element.E_SHOW_C_OPERATORS + && (!Element.isSwitchTextCommentMode())); + // END KGU#887 2020-12-11 lblOperatorsC.setToolTipText(buildMessageWithMenuRef( lblOperatorsC.isEnabled() ? ttOperatorsC : msgInactiveMode, Menu.getLocalizedMenuPath(pathOperatorsC, null))); diff --git a/src/lu/fisch/structorizer/gui/InputBox.java b/src/lu/fisch/structorizer/gui/InputBox.java index 715a6b96..c5473ba9 100644 --- a/src/lu/fisch/structorizer/gui/InputBox.java +++ b/src/lu/fisch/structorizer/gui/InputBox.java @@ -50,6 +50,7 @@ * Kay Gürtzig 2017.01.09 Bugfix #330 (issue #81): Basic scaling outsourced to class GUIScaler * Kay Gürtzig 2017.03.14 Enh. #372: Additional hook for subclass InputBoxRoot. * Kay Gürtzig 2017.10.06 Enh. #430: The scaled TextField font size (#284) is now kept during the session + * Kay Gürtzig 2020-10-15 Bugfix #885 Focus rule was flawed (ignored suppression of switch text/comments mode) * ****************************************************************************************************** * @@ -359,7 +360,10 @@ private void create() { // START KGU#91+KGU#169 2016-07-14: Enh. #180 (also see #39 and #142) this.pack(); // This makes focus control possible but must precede the size setting setPreferredSize(scaleFactor); - if (Element.E_TOGGLETC) { + // START KGU#887 2020-12-15: Bugfix #885 Don't put the focus to comment if TC mode is suppressed + //if (Element.E_TOGGLETC) { + if (Element.isSwitchTextCommentMode()) { + // END KGU#887 2020-12-15 txtComment.requestFocusInWindow(); } else { txtText.requestFocusInWindow(); diff --git a/src/lu/fisch/structorizer/gui/Menu.java b/src/lu/fisch/structorizer/gui/Menu.java index fe1d7cbc..1e3fc773 100644 --- a/src/lu/fisch/structorizer/gui/Menu.java +++ b/src/lu/fisch/structorizer/gui/Menu.java @@ -118,6 +118,7 @@ * Kay Gürtzig 2020-06-06 Issue #440: Submenu items for PapDesigner export now also with icon * Kay Gürtzig 2020-10-16 Bugfix #874: New warning variant error07_5 (non-ascii letters in identifiers) * Kay Gürtzig 2020-10-17 Enh. #872: New display mode "Operators in C style" + * Kay Gürtzig 2020-10-15 Bugfix #885 enabling rule for the C operator mode was flawed * ****************************************************************************************************** * @@ -658,6 +659,9 @@ public enum PluginType { GENERATOR, PARSER, IMPORTER, CONTROLLER }; // START KGU#791 2020-01-20: Enh. #801: Offline help mechanism public static final LangTextHolder msgShowingOfflineGuide = new LangTextHolder("A recently downloaded User Guide is shown by your PDF reader instead."); public static final LangTextHolder msgDownloadFailed = new LangTextHolder("Failed to download the User Guide:\n%"); + // START KGU#791 2020-10-20: Issue #801 Download done in an additional thread now. + public static final LangTextHolder msgCancelled = new LangTextHolder("Cancelled"); + // END KGU#791 2020-10-20 public static final LangTextHolder msgHostNotAvailable = new LangTextHolder("Host \"%\" not accessible."); // END KGU#791 2020-01-20 // START KGU#258 2016-10-03: Enh. #253: Diagram keyword refactoring @@ -801,6 +805,7 @@ public enum PluginType { GENERATOR, PARSER, IMPORTER, CONTROLLER }; private Map importpluginItems = new HashMap(); // END KGU#725 2019-09-13 + // START BOB 2020-05-25: restricted mode // START KGU#868 2020-06-03: Bugfix #868 - renamed for clarity //private boolean restricted = false; @@ -1746,7 +1751,14 @@ public void actionPerformed(ActionEvent evt) { // START KGU#791 2020-01-20: Enh. #791 menuHelp.add(menuHelpDownload); - menuHelpDownload.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent event) {diagram.downloadHelpPDF(true); } } ); + menuHelpDownload.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent event) { + // START KGU#791 2020-10-20: Issue #801 - we need a worker thread... + //diagram.downloadHelpPDF(true); + diagram.downloadHelpPDF(menuHelpDownload.getText()); + // END KGU#791 2020-10-20 + } + }); // END KGU#791 2020-01-20 menuHelp.add(menuHelpAbout); @@ -2013,7 +2025,10 @@ public void doButtonsLocal() // START KGU#872 2020-10-17: Enh. #872 menuDiagramOperatorsC.setSelected(Element.E_SHOW_C_OPERATORS); - menuDiagramOperatorsC.setEnabled(Element.E_VARHIGHLIGHT && !Element.E_TOGGLETC); + // START KGU#887 2020-12-15: Bugfix #885 + //menuDiagramOperatorsC.setEnabled(Element.E_VARHIGHLIGHT && !Element.E_TOGGLETC); + menuDiagramOperatorsC.setEnabled(Element.E_VARHIGHLIGHT && !Element.isSwitchTextCommentMode()); + // END KGU#887 2020-12-15 // END KGU#872 2020-10-17 // show comments? diff --git a/src/lu/fisch/structorizer/gui/changelog.txt b/src/lu/fisch/structorizer/gui/changelog.txt index c9873f57..c862cc0d 100644 --- a/src/lu/fisch/structorizer/gui/changelog.txt +++ b/src/lu/fisch/structorizer/gui/changelog.txt @@ -16,7 +16,7 @@ Known issues: - Shell export copes neither with nested array/record initialisers and component access nor with cleanly handling usual and associative arrays as parameters or results. -Current development version: 3.30-11 (2020-10-20) +Current development version: 3.30-12 (2020-12-16) - 01: Bugfix #759: Dried up another source of stale Mainforms. <2> - 01: Bugfix #761: Code preview defect flooded the log stream. <2> - 01: Precautions against empty error messages on startup <2> @@ -129,13 +129,22 @@ Current development version: 3.30-11 (2020-10-20) - 11: Issue #733: GUI scale factor not to be overwritten by central ini file <2> - 11: Enh. #872: New display mode to present operators in C style [BloodyRain2k]2 - 11: Bugfix #873: Compromised type definition export to C++, C#, Java mended <2> -- 11: Bugfix #874: Error on code export/preview of CALLs with non-ascii names, - identifiers with non-ascii letters now tolerated but warned <2> +- 11: Bugfix #874: Error on code export/preview of CALLs with non-ASCII names, + identifiers with non-ASCII letters now tolerated but warned <2> - 11: Bugfix #875: Saving policy mended for virgin group members and groups, - unnessesary save requests on replacing diagrams in work area avoided <2> -- 11: Bugfix #876: Defective saving and restoring of Ini properties from menu <2> + unnecessary save requests on replacing diagrams in work area avoided <2> +- 11: Bugfix #876: Defective saving and restoring of Ini properties via menu <2> - 11: Bugfix #877: Division by zero error on batch export to StrukTeX <2> - 11: Issue #879: Bad handling of user input looking like a record initializer <2> +- 12: Enh. #704: Turtleizer now scrollable, with status bar, and popup menu <2> +- 12: Issue #801: User Guide download now with progress bar in background <2> +- 12: Issue #829: Experimentally keeping open the debug control now revoked <2> +- 12: Enh. #880: Turtleizer zooming function implemented and accomplished <2> +- 12: Issue #881: Highlighting of bit operators and Boolean literals <2> +- 12: Issue #882: Translation of the random function to PHP added [S. Roschewitz]2 +- 12: Bugfix #884: Flaws of header inference for virgin diagrams (#875) mended <2> +- 12: Bugfix #885: Incorrect display mode indicator state (#872) in some case <2> +- 12: Bugfix #887: Concurrent Turtleizer instances led to dysfunction. <2> Version: 3.30 (2019-10-06) - 01: Issue #657: Spanish message in German locale replaced <2> @@ -144,7 +153,7 @@ Version: 3.30 (2019-10-06) - 01: Enh. #662/3: Arranger popup menu item to rearrange diagrams by groups <2> - 01: Enh. #662/4: Option to save arrangements with relative coordinates <2> - 01: Bugfix #664: Incorrect handling of modified diagrams on closing Arranger <2> -- 01: Bugfix #664: Disambiguated canceling in AUTO_SAVE_ON_CLOSE mode <2> +- 01: Bugfix #664: Disambiguated cancelling in AUTO_SAVE_ON_CLOSE mode <2> - 01: Bugfix #515: Defective diagram positioning in Arranger mended <2> - 02: Bugfix #665: Import of INSPECT statements from COBOL actually works now <2> - 02: Bugfix #655: Arranger popup menu revised (item order, accelerator info) <2> diff --git a/src/lu/fisch/structorizer/gui/generators.xml b/src/lu/fisch/structorizer/gui/generators.xml index a33c52cb..623ee8d0 100644 --- a/src/lu/fisch/structorizer/gui/generators.xml +++ b/src/lu/fisch/structorizer/gui/generators.xml @@ -1,6 +1,6 @@ - + @@ -15,8 +15,10 @@ - - + + diff --git a/src/lu/fisch/structorizer/io/ExtFileFilter.java b/src/lu/fisch/structorizer/io/ExtFileFilter.java index 24e38541..81c320ca 100644 --- a/src/lu/fisch/structorizer/io/ExtFileFilter.java +++ b/src/lu/fisch/structorizer/io/ExtFileFilter.java @@ -31,7 +31,8 @@ * * Author Date Description * ------ ---- ----------- - * Kay Gürtzig 2018.06.08 First Issue + * Kay Gürtzig 2018-06-08 First Issue + * Kay Gürtzig 2020-12-13 Javadoc comments for getExtension methods * ****************************************************************************************************** * @@ -51,6 +52,12 @@ */ public abstract class ExtFileFilter extends FileFilter { + /** + * Extracts the file name extension of the given file path {@code s} + * @param s - a file path as string + * @return the extension as string or {@code ""} if there isn't any. + * @see #getExtension(File) + */ public static String getExtension(String s) { // START KGU#521 2018-06-08: Bugfix #536 We shouldn't return null in any case @@ -69,6 +76,12 @@ public static String getExtension(String s) return ext; } + /** + * Extracts the file name extension of the given file {@code f} + * @param f - a {@link File} object + * @return the extension as string or {@code ""} if there isn't any. + * @see #getExtension(String) + */ public static String getExtension(File f) { return getExtension(f.getName()); diff --git a/src/lu/fisch/structorizer/locales/cz.txt b/src/lu/fisch/structorizer/locales/cz.txt index 52877584..04369af2 100644 --- a/src/lu/fisch/structorizer/locales/cz.txt +++ b/src/lu/fisch/structorizer/locales/cz.txt @@ -618,7 +618,11 @@ FindAndReplace.chkElementTypes.9.text=@{Jump} FindAndReplace.chkElementTypes.10.text=@{Parallel} FindAndReplace.chkElementTypes.11.text=@{Try} +-----[ DownloadMonitor ]----- +DownloadMonitor.btnCancel.text=Storno + -----> Arranger +-----[ Surface ]----- Surface.msgFileLoadError.text= Surface.msgSavePortable.text= Surface.msgSaveDialogTitle.text= diff --git a/src/lu/fisch/structorizer/locales/de.txt b/src/lu/fisch/structorizer/locales/de.txt index 3c36d443..67f7df8a 100644 --- a/src/lu/fisch/structorizer/locales/de.txt +++ b/src/lu/fisch/structorizer/locales/de.txt @@ -128,6 +128,7 @@ * Kay Gürtzig 2019-08-06 Issues #733-#740: New messages for preferences mechanisms * Kay Gürtzig 2020-01-20 Enh. #801: New messages for offline help support * Kay Gürtzig 2020-03-17 New messages for issues #828 (ArrangerIndex) and #837 (ExportOptionDialoge) + * Kay Gürtzig 2020-12-12 Enh. #704: New top-level section "DiagramController" * ****************************************************************************************************** * @@ -389,6 +390,7 @@ Menu.msgErrorNoFile.text=Datei nicht gefunden! Menu.msgBrowseFailed.text=Anzeige von "%" im Browser gescheitert Menu.msgShowingOfflineGuide.text=Das jüngst als PDF heruntergeladene Nutzerhandbuch wird ersatzweise angezeigt (im PDF-Betrachter). Menu.msgDownloadFailed.text=Das Herunterladen des Nutzerhandbuchs als PDF ist gescheitert:\n% +Menu.msgCancelled.text=Abgebrochen Menu.msgHostNotAvailable.text=Die Adresse "%" ist nicht erreichbar. Menu.hdrRefactoringTable.0.text=Referenz Menu.hdrRefactoringTable.1.text=Alt @@ -1230,6 +1232,9 @@ FindAndReplace.btnClose.text=Schließen FindAndReplace.msgRegexCorrupt.text=Der reguläre Ausdruck «%1» scheint inkorrekt: %2 FindAndReplace.ttlSearchError.text=Suchfehler +-----[ DownloadMonitor ]----- +DownloadMonitor.btnCancel.text=Abbrechen + -----> Arranger -----[ Arranger ]----- Arranger.btnAddDiagram.text=Neues Diagramm @@ -1478,6 +1483,31 @@ OutputConsole.msgOverwriteFile.text=Vorhandene Datei überschreiben? OutputConsole.msgErrorFileSave.text=Fehler beim Sichern in Datei: %! OutputConsole.msgTitleError.text=Fehler +-----> DiagramController +-----[ TurtleFrame ]----- +TurtleFrame.popupGotoCoord.text=Zu Koordinate scrollen ... +TurtleFrame.popupGotoTurtle.text=Zur Schildkröte scrollen +TurtleFrame.popupGotoHome.text=Zur Startposition scrollen +TurtleFrame.popupZoom100.text=Zoom auf 100% rücksetzen +TurtleFrame.popupZoomBounds.text=Auf Gesamtbild zoomen +TurtleFrame.popupMoveIn.text=Bild in Zeichenbereich ziehen +TurtleFrame.popupShowOrigin.text=Koordinatenkreuz zeichnen +TurtleFrame.popupShowTurtle.text=Schildkröte zeigen +TurtleFrame.popupShowStatus.text=Statusleiste zeigen +TurtleFrame.popupBackground.text=Hintergrundfarbe ändern ... +TurtleFrame.popupExportCSV.text=Zeichnungselemente als CSV exportieren ... +TurtleFrame.popupExportImage.text=Bildexport +TurtleFrame.popupExportPNG.text=als PNG ... +TurtleFrame.popupExportSVG.text=als SVG ... +TurtleFrame.statusHome.tooltip=Startposition +TurtleFrame.statusTurtle.tooltip=Aktuelle Schildkrötenposition +TurtleFrame.statusSize.tooltip=Ausdehnung des Zeichnungsbereichs +TurtleFrame.statusViewport.tooltip=Momentaner Scrolling-Ausschnitt +TurtleFrame.statusSelection.tooltip=Anzahl der ausgewählten Segmente +TurtleFrame.statusZoom.tooltip=Vergrößerungsfaktor +TurtleFrame.lblOverwrite.text=Datei existiert. Wirklich überschreiben? +TurtleFrame.lblScale.text=Skalierungsfaktor: + -----> Elements ElementNames.localizedNames.0.text=Verarbeitung ElementNames.localizedNames.1.text=WENN diff --git a/src/lu/fisch/structorizer/locales/en.txt b/src/lu/fisch/structorizer/locales/en.txt index acd35046..c056ba10 100644 --- a/src/lu/fisch/structorizer/locales/en.txt +++ b/src/lu/fisch/structorizer/locales/en.txt @@ -124,6 +124,7 @@ * Kay Gürtzig 2020-01-20 Enh. #801: New messages for offline help support * Kay Gürtzig 2020-03-09 Issue #835: New ImportOptionDialog messages * Kay Gürtzig 2020-03-17 New messages for issues #828 (ArrangerIndex) and #837 (ExportOptionDialoge) + * Kay Gürtzig 2020-12-12 Enh. #704: New top-level section "DiagramConroller" * ****************************************************************************************************** * @@ -388,6 +389,7 @@ Menu.msgErrorNoFile.text=File not found! Menu.msgBrowseFailed.text=Failed to show "%" in browser Menu.msgShowingOfflineGuide.text=A recently downloaded User Guide is shown by your PDF reader instead. Menu.msgDownloadFailed.text=Failed to download the User Guide:\n% +Menu.msgCancelled.text=Cancelled Menu.msgHostNotAvailable.text=Host "%" not accessible. Menu.hdrRefactoringTable.0.text=Reference Menu.hdrRefactoringTable.1.text=Old @@ -1230,6 +1232,9 @@ FindAndReplace.btnClose.text=Close FindAndReplace.msgRegexCorrupt.text=Regular expression «%1» seems invalid: %2 FindAndReplace.ttlSearchError.text=Search Error +-----[ DownloadMonitor ]----- +DownloadMonitor.btnCancel.text=Cancel + -----> Arranger -----[ Arranger ]----- Arranger.btnAddDiagram.text=New Diagram @@ -1478,6 +1483,31 @@ OutputConsole.msgOverwriteFile.text=Overwrite existing file? OutputConsole.msgErrorFileSave.text=Error on saving the file: %! OutputConsole.msgTitleError.text=Error +-----> DiagramController +-----[ TurtleFrame ]----- +TurtleFrame.popupGotoCoord.text=Scroll to coordinate ... +TurtleFrame.popupGotoTurtle.text=Scroll to turtle position +TurtleFrame.popupGotoHome.text=Scroll to home position +TurtleFrame.popupZoom100.text=Reset zoom to 100% +TurtleFrame.popupZoomBounds.text=Zoom to fit bounds +TurtleFrame.popupMoveIn.text=Move picture into area +TurtleFrame.popupShowOrigin.text=Show coordinate cross +TurtleFrame.popupShowTurtle.text=Show turtle +TurtleFrame.popupShowStatus.text=Show status bar +TurtleFrame.popupBackground.text=Set background color ... +TurtleFrame.popupExportCSV.text=Export drawing items as CSV ... +TurtleFrame.popupExportImage.text=Export as image +TurtleFrame.popupExportPNG.text=to PNG ... +TurtleFrame.popupExportSVG.text=to SVG ... +TurtleFrame.statusHome.tooltip=Current home position +TurtleFrame.statusTurtle.tooltip=Current turtle position +TurtleFrame.statusSize.tooltip=Extension of drawn area +TurtleFrame.statusViewport.tooltip=Current scrolling viewport +TurtleFrame.statusSelection.tooltip=Number of selected segments +TurtleFrame.statusZoom.tooltip=Zoom factor +TurtleFrame.lblOverwrite.text=File exists. Sure to overwrite? +TurtleFrame.lblScale.text=Scale factor: + -----> Elements ElementNames.localizedNames.0.text=Instruction ElementNames.localizedNames.1.text=IF diff --git a/src/lu/fisch/structorizer/locales/es.txt b/src/lu/fisch/structorizer/locales/es.txt index f994accb..51ccb96c 100644 --- a/src/lu/fisch/structorizer/locales/es.txt +++ b/src/lu/fisch/structorizer/locales/es.txt @@ -130,6 +130,7 @@ * Kay Gürtzig 2020-01-20 Enh. #801: New messages for offline help support * Kay Gürtzig 2020-03-09 Issue #835: New ImportOptionDialog messages * Kay Gürtzig 2020-03-17 New messages for issues #828 (ArrangerIndex) and #837 (ExportOptionDialoge) + * Kay Gürtzig 2020-12-14 Enh. #704: New top-level section "DiagramController" * ****************************************************************************************************** * @@ -392,6 +393,7 @@ Menu.msgErrorNoFile.text=¡Archivo no encontrado! Menu.msgBrowseFailed.text=Mostrar "%" en el browser no logró. Menu.msgShowingOfflineGuide.text=La versión PDF del manual más reciéntemente descargada es presentada en su lugar (PDF reader). Menu.msgDownloadFailed.text=No fue posible descargar el manual:\n% +Menu.msgCancelled.text=Cancelado Menu.msgHostNotAvailable.text=El sitio web "%" no es alcanzable. Menu.hdrRefactoringTable.0.text=Referencia Menu.hdrRefactoringTable.1.text=Anterior @@ -1215,6 +1217,9 @@ FindAndReplace.btnReplaceAll.text=Reemplazar todos FindAndReplace.btnReplaceAll.tooltip=Reemplaza todas aparencias restantes del texto de búsqueda a partir de la posición actual en la puesta dirección. FindAndReplace.btnClose.text=Cerrar +-----[ DownloadMonitor ]----- +DownloadMonitor.btnCancel.text=Cancelar + -----> Arranger -----[ Arranger ]----- Arranger.btnAddDiagram.text=Nuevo diagrama @@ -1463,6 +1468,31 @@ OutputConsole.msgOverwriteFile.text=¿Grabar encima del archivo existente? OutputConsole.msgErrorFileSave.text=¡Error grabando fichero: %! OutputConsole.msgTitleError.text=Error +-----> DiagramController +-----[ TurtleFrame ]----- +TurtleFrame.popupGotoCoord.text=Desplazar a coordenada ... +TurtleFrame.popupGotoTurtle.text=Desplazar hacia la tortuga +TurtleFrame.popupGotoHome.text=Desplazar hacia el inicio +TurtleFrame.popupZoom100.text=Reponer amplificación a 100% +TurtleFrame.popupZoomBounds.text=Ajustar dibujo a la ventana +TurtleFrame.popupMoveIn.text=Mover dibujo adentro del area +TurtleFrame.popupShowOrigin.text=Dibujar cruz del orígen +TurtleFrame.popupShowTurtle.text=Mostrar tortuga +TurtleFrame.popupShowStatus.text=Mostrar barra de estado +TurtleFrame.popupBackground.text=Ajustar color de fondo ... +TurtleFrame.popupExportCSV.text=Exportar elementos del dibujo como CSV ... +TurtleFrame.popupExportImage.text=Exportar imágen +TurtleFrame.popupExportPNG.text=como PNG ... +TurtleFrame.popupExportSVG.text=como SVG ... +TurtleFrame.statusHome.tooltip=Posición inicial +TurtleFrame.statusTurtle.tooltip=Posición de tortuga +TurtleFrame.statusSize.tooltip=Extensión del area de dibujo +TurtleFrame.statusViewport.tooltip=Area del desplazamiento +TurtleFrame.statusSelection.tooltip=Número de segmentos seleccionados +TurtleFrame.statusZoom.tooltip=Factor de amplificación +TurtleFrame.lblOverwrite.text=Archivo existe. ¿Cierto de gravar el nuevo encima del existente? +TurtleFrame.lblScale.text=Factor de ampliación: + -----> Elements ElementNames.localizedNames.0.text=Instrucción ElementNames.localizedNames.1.text=SI diff --git a/src/lu/fisch/structorizer/locales/fr.txt b/src/lu/fisch/structorizer/locales/fr.txt index 73b9be96..01a35b12 100644 --- a/src/lu/fisch/structorizer/locales/fr.txt +++ b/src/lu/fisch/structorizer/locales/fr.txt @@ -662,6 +662,9 @@ FindAndReplace.chkElementTypes.9.text=@{Jump} FindAndReplace.chkElementTypes.10.text=@{Parallel} FindAndReplace.chkElementTypes.11.text=@{Try} +-----[ DownloadMonitor ]----- +DownloadMonitor.btnCancel.text=Annuler + -----> Arranger -----[ Surface ]----- Surface.msgFileLoadError.text= @@ -680,6 +683,7 @@ Surface.msgUnsavedDiagrams.text= Surface.msgUnsavedHint.text= Surface.msgUnsavedContinue.text= Surface.msgNoArrFile.text= + -----> Executor -----[ Control ]----- Control.lblSpeed.text= @@ -755,3 +759,4 @@ ParserKeywords.JumpExit.text=exit ParserKeywords.JumpThrow.text=lancer ParserKeywords.Input.text=lire ParserKeywords.Output.text=écrire + diff --git a/src/lu/fisch/structorizer/locales/it.txt b/src/lu/fisch/structorizer/locales/it.txt index ad2abf5d..b1fd744f 100644 --- a/src/lu/fisch/structorizer/locales/it.txt +++ b/src/lu/fisch/structorizer/locales/it.txt @@ -737,6 +737,9 @@ FindAndReplace.btnClose.text=Chiudere FindAndReplace.msgRegexCorrupt.text=Espressione regolare «%1» sembra invalida: %2 FindAndReplace.ttlSearchError.text=Errore di ricerca +-----[ DownloadMonitor ]----- +DownloadMonitor.btnCancel.text=Annulla + -----> Arranger -----[ Surface ]----- Surface.msgFileLoadError.text= diff --git a/src/lu/fisch/structorizer/locales/lu.txt b/src/lu/fisch/structorizer/locales/lu.txt index dd1ea6ac..8930c583 100644 --- a/src/lu/fisch/structorizer/locales/lu.txt +++ b/src/lu/fisch/structorizer/locales/lu.txt @@ -523,6 +523,9 @@ FindAndReplace.chkElementTypes.9.text=@{Jump} FindAndReplace.chkElementTypes.10.text=@{Parallel} FindAndReplace.chkElementTypes.11.text=@{Try} +-----[ DownloadMonitor ]----- +DownloadMonitor.btnCancel.text=Ofbriechen + -----> Elements ElementNames.localizedNames.0.text=Instruction ElementNames.localizedNames.1.text=IF diff --git a/src/lu/fisch/structorizer/locales/nl.txt b/src/lu/fisch/structorizer/locales/nl.txt index 45d11453..844fe4ee 100644 --- a/src/lu/fisch/structorizer/locales/nl.txt +++ b/src/lu/fisch/structorizer/locales/nl.txt @@ -1141,6 +1141,9 @@ FindAndReplace.btnClose.text=Sluiten FindAndReplace.msgRegexCorrupt.text=Reguliere expressie «%1» lijkt ongeldig: %2 FindAndReplace.ttlSearchError.text=Zoekfout +-----[ DownloadMonitor ]----- +DownloadMonitor.btnCancel.text=Annuleren + -----> Arranger -----[ Arranger ]----- Arranger.btnAddDiagram.text=Nieuw diagram diff --git a/src/lu/fisch/structorizer/locales/pl.txt b/src/lu/fisch/structorizer/locales/pl.txt index 07b6572f..8cba86e0 100644 --- a/src/lu/fisch/structorizer/locales/pl.txt +++ b/src/lu/fisch/structorizer/locales/pl.txt @@ -474,6 +474,9 @@ FindAndReplace.chkElementTypes.9.text=@{Jump} FindAndReplace.chkElementTypes.10.text=@{Parallel} FindAndReplace.chkElementTypes.11.text=@{Try} +-----[ DownloadMonitor ]----- +DownloadMonitor.btnCancel.text=Kasuj + -----> Elements ElementNames.localizedNames.0.text=Instruction ElementNames.localizedNames.1.text=IF diff --git a/src/lu/fisch/structorizer/locales/pt_br.txt b/src/lu/fisch/structorizer/locales/pt_br.txt index 68039303..ba45b1c5 100644 --- a/src/lu/fisch/structorizer/locales/pt_br.txt +++ b/src/lu/fisch/structorizer/locales/pt_br.txt @@ -649,6 +649,9 @@ FindAndReplace.chkElementTypes.9.text=@{Jump} FindAndReplace.chkElementTypes.10.text=@{Parallel} FindAndReplace.chkElementTypes.11.text=@{Try} +-----[ DownloadMonitor ]----- +DownloadMonitor.btnCancel.text=Cancelar + -----> Elements ElementNames.localizedNames.0.text=Instrução ElementNames.localizedNames.1.text=IF diff --git a/src/lu/fisch/structorizer/locales/ru.txt b/src/lu/fisch/structorizer/locales/ru.txt index fd7783ee..bb8ab895 100644 --- a/src/lu/fisch/structorizer/locales/ru.txt +++ b/src/lu/fisch/structorizer/locales/ru.txt @@ -372,7 +372,7 @@ Menu.lblReduced.text=Да, упрощенный вид Menu.lblNormal.text=Нет, нормальный вид Menu.ttlCodeImport.text= Menu.msgSelectParser.text= -Menu.msgImportCancelled.text= +Menu.msgImportCancelled.text=Импорт ​отменен Menu.msgSubroutineName.text=Название новой подпрограммы Menu.msgIncludableName.text= Menu.msgMustBeIdentifier.text= @@ -1162,6 +1162,9 @@ FindAndReplace.btnClose.text= FindAndReplace.msgRegexCorrupt.text= FindAndReplace.ttlSearchError.text= +-----[ DownloadMonitor ]----- +DownloadMonitor.btnCancel.text=Отменить + -----> Arranger -----[ Arranger ]----- Arranger.btnAddDiagram.text= diff --git a/src/lu/fisch/structorizer/parsers/COBOLParser.java b/src/lu/fisch/structorizer/parsers/COBOLParser.java index cbaceb9a..a6e4e59c 100644 --- a/src/lu/fisch/structorizer/parsers/COBOLParser.java +++ b/src/lu/fisch/structorizer/parsers/COBOLParser.java @@ -6668,6 +6668,9 @@ private boolean importString(Reduction _reduction, Subqueue _parentNode) throws // START KGU#847 2020-04-19 Issue #851/1: insert declarations for auxiliary variables insertAuxVarDeclaration(itemId + suffix, "array of string"); // END KGU#847 2020-04-19 + // START KGU#847 2020-04-19 Issue #851/1: insert declarations for auxiliary variables + insertAuxVarDeclaration(itemId + suffix, "array of string"); + // END KGU#847 2020-04-19 } assignments.add(asgnmt + tail); } while (itemlRed != null); diff --git a/src/lu/fisch/turtle/Main.java b/src/lu/fisch/turtle/Main.java index 07c042c1..25b5edc4 100644 --- a/src/lu/fisch/turtle/Main.java +++ b/src/lu/fisch/turtle/Main.java @@ -21,7 +21,7 @@ package lu.fisch.turtle; //import java.awt.Color; -import lu.fisch.structorizer.gui.Mainform; +//import lu.fisch.structorizer.gui.Mainform; /** * @@ -35,7 +35,7 @@ public class Main { public static void main(String[] args) { /*TurtleBox turtle =*/ new TurtleBox(500,500); - /*Mainform structorizer =*/ new Mainform(); + /*Mainform structorizer = new Mainform();*/ //structorizer.getEditor().diagram.setDiagramController(turtle); //structorizer.getEditor().diagram.setAnalyser(false); //structorizer.getEditor().diagram.setHightlightVars(false); diff --git a/src/lu/fisch/turtle/TurtleBox.java b/src/lu/fisch/turtle/TurtleBox.java index a8a5f1a5..1f83e16b 100644 --- a/src/lu/fisch/turtle/TurtleBox.java +++ b/src/lu/fisch/turtle/TurtleBox.java @@ -45,6 +45,9 @@ * Kay Gürtzig 2018-07-30 Enh. #576: New procedure clear() added to the API * Kay Gürtzig 2018-10-12 Issue #622: Modification apparently helping to overcome drawing contention * Kay Gürtzig 2019-03-02 Issue #366: New methods isFocused() and requestFocus() in analogy to Window + * Kay Gürtzig 2020-12-11 Enh. #704: Scrollbars, status bar, and popup menu added + * Enh. #443: Deprecated execute methods disabled + * Kay Gürtzig 2020-12-16 Enh. #704/#880: Zoom and export functions accomplished * ****************************************************************************************************** * @@ -59,21 +62,53 @@ ******************************************************************************************************/// import java.awt.Color; +import java.awt.Dimension; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.Image; import java.awt.Point; +import java.awt.Rectangle; import java.awt.RenderingHints; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.ComponentEvent; +import java.awt.event.KeyEvent; +import java.awt.event.KeyListener; +import java.awt.event.MouseEvent; +import java.awt.event.MouseListener; +import java.awt.event.MouseWheelEvent; +import java.awt.event.MouseWheelListener; +import java.awt.image.BufferedImage; +import java.io.BufferedWriter; +import java.io.File; +import java.io.IOException; +import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.HashMap; +import java.util.Locale; import java.util.Vector; //import java.util.logging.Logger; +import javax.imageio.ImageIO; +import javax.swing.BorderFactory; import javax.swing.ImageIcon; +import javax.swing.JFileChooser; import javax.swing.JFrame; +import javax.swing.JOptionPane; import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.KeyStroke; +import javax.swing.SpinnerNumberModel; +import javax.swing.SwingUtilities; +import javax.swing.event.ChangeEvent; import lu.fisch.diagrcontrol.*; +import lu.fisch.turtle.io.CSVFilter; +import lu.fisch.turtle.io.ExtFileFilter; +import lu.fisch.turtle.io.PNGFilter; +import lu.fisch.turtle.io.SVGFilter; import lu.fisch.turtle.elements.Element; import lu.fisch.turtle.elements.Line; import lu.fisch.turtle.elements.Move; @@ -160,11 +195,1177 @@ public class TurtleBox implements DelayableDiagramController } } // END KGU#417/KGU#448 2017-10-28 - // START KGU#480 2018-01-16: Enh. #490 - frame now as attribute + // START KGU#885 2020-12-12: Enh. #704 + //// START KGU#480 2018-01-16: Enh. #490 - frame now as attribute + ///** The GUI frame - while null, it hasn't been materialized (light-weight instance) */ + //private JFrame frame = null; + //// END KGU#480 2018-01-16 + + + public static final class TurtleFrame extends JFrame implements KeyListener + { + public final class TurtlePanel extends JPanel + { + @Override + public void paint(Graphics graphics) + { + paint(graphics, false); + } + public void paint(Graphics graphics, boolean compensateZoom) + { + synchronized(zoomMutex) { + Graphics2D g = (Graphics2D) graphics; + // set anti-aliasing rendering + ((Graphics2D)g).setRenderingHint(RenderingHints.KEY_ANTIALIASING,RenderingHints.VALUE_ANTIALIAS_ON); + + // clear background + // START KGU#303 2016-12-02: Enh. #302 + //g.setColor(Color.WHITE); + g.setColor(owner.backgroundColor); + // END KGU#303 2016-12-02 + // START KGU#685 2020-12-11: Enh. #704 + //g.fillRect(0, 0, this.getWidth(), this.getHeight()); + if (compensateZoom) { + Dimension prefDim = getPreferredSize(); + g.fillRect(0, 0, + Math.round(prefDim.width / zoomFactor), + Math.round(prefDim.height / zoomFactor)); + } + else { + g.fillRect(0, 0, this.getWidth(), this.getHeight()); + g.scale(zoomFactor, zoomFactor); + } + + Rectangle visibleRect = g.getClipBounds(); + // END KGU#685 2020-12-11 + + // START KGU#685 2020-12-14: Enh. #704 + if (displacement != null && popupShowOrigin.isSelected()) { + int x0 = visibleRect.x; + int y0 = visibleRect.y; + int x1 = x0 + visibleRect.width; + int y1 = y0 + visibleRect.height; + g.setColor(Color.decode("0xffcccc")); + java.awt.Stroke strk = g.getStroke(); + g.setStroke(new java.awt.BasicStroke(1f, + java.awt.BasicStroke.CAP_ROUND, + java.awt.BasicStroke.JOIN_ROUND, 1f, + new float[] {2f, 2f}, 0f)); + if (x0 <= displacement.x && displacement.x <= x1) { + g.drawLine(displacement.x, y0, displacement.x, y1); + } + if (y0 <= displacement.y && displacement.y <= y1) { + g.drawLine(x0, displacement.y, x1, displacement.y); + } + g.setStroke(strk); + } + // END KGU#685 2020-12-14 + + // START KGU#303 2016-12-03: Enh. #302 + //g.setColor(Color.BLACK); + g.setColor(owner.defaultPenColor); + // END KGU#303 2016-12-03 + + // draw all elements + // START KGU#449 2017-10-28: The use of iterators may lead to lots of + //java.util.ConcurrentModificationException errors slowing down all. + // So we better avoid the iterator and loop against a snapshot size + // (which is safe because the elements Vector can't shrink during execution). + //for (Element ele : elements) + //{ + // ele.draw(g); + //} + int nElements = owner.elements.size(); + // START KGU#597 2018-10-12: Issue #622 - Monitoring drawing detention underMac + //logger.config("Painting " + nElements + " elements..."); + // END KGU#597 2018-10-12 + for (int i = 0; i < nElements; i++) { + // START KGU#685 2020-12-11: Enh. #704 + //elements.get(i).draw(g); + owner.elements.get(i).draw(g, visibleRect); + // END KGU#685 2020-12-11 + } + // END KGU#449 2017-10-28 + + if (!owner.turtleHidden) + { + // START #272 2016-10-16 + // fix drawing point + //int x = (int) Math.round(pos.x - (image.getWidth(this)/2)); + //int y = (int) Math.round(pos.y - (image.getHeight(this)/2)); + // fix drawing point + //int xRot = x+image.getWidth(this)/2; + //int yRot = y+image.getHeight(this)/2; + // apply rotation + //g.rotate((270-angle)/180*Math.PI,xRot,yRot); + // fix drawing point + double x = owner.posX - (owner.image.getWidth(this)/2); + double y = owner.posY - (owner.image.getHeight(this)/2); + // apply rotation + g.rotate((270-owner.angle)/180*Math.PI, owner.posX, owner.posY); + // END #272 2016-10-16 + // draw the turtle + g.drawImage(owner.image,(int)Math.round(x),(int)Math.round(y),this); + } + + // START KGU#685 2020-12-11: Enh. #704 + if (!compensateZoom) { + g.scale(1/zoomFactor, 1/zoomFactor); + } + // END KGU#685 2020-12-11 + } + + // START KGU#685 2020-12-16: Enh. #704 + this.updatePreferredSize(!compensateZoom); + // END KGU#685 2020-12-16 + } + + // START KGU#685 2020-12-16: Enh. #704 + public void updatePreferredSize(boolean useZoom) + { + Rectangle bounds = new Rectangle(owner.bounds); + // Make sure the bounds comprise the turtle position - visible or not + bounds.add(new Rectangle(owner.pos.x, owner.pos.y, 1, 1)); + // We need the maximum distances from top and left border + Dimension dim = new Dimension( + // The drawing could lie completely in the negative range + Math.max(bounds.x + bounds.width, 0), + Math.max(bounds.y + bounds.height, 0) + ); + // Add some pixels for the scrollbars + dim.width += MARGIN; + dim.height += MARGIN; + if (useZoom) { + dim.width = Math.round(Math.min(dim.width, Short.MAX_VALUE) * zoomFactor); + dim.height = Math.round(Math.min(dim.height, Short.MAX_VALUE) * zoomFactor); + } + + this.setPreferredSize(dim); + this.revalidate(); + } + // END KGU#685 2020-12-16 + } + private TurtlePanel panel; + // START KGU#685 2020-12-11: Enh. #704 + private JScrollPane scrollarea; + private JPanel statusbar; + protected javax.swing.JLabel statusHome; + protected javax.swing.JLabel statusTurtle; + protected javax.swing.JLabel statusSize; + protected javax.swing.JLabel statusViewport; + protected javax.swing.JLabel statusZoom; + protected javax.swing.JLabel statusSelection; + private float zoomFactor = 1.0f; + protected javax.swing.JPopupMenu popupMenu; + protected javax.swing.JMenuItem popupGotoCoord; + protected javax.swing.JMenuItem popupGotoTurtle; + protected javax.swing.JMenuItem popupGotoHome; + protected javax.swing.JMenuItem popupMoveIn; + protected javax.swing.JMenuItem popupZoom100; + protected javax.swing.JMenuItem popupZoomBounds; + protected javax.swing.JCheckBoxMenuItem popupShowOrigin; + protected javax.swing.JCheckBoxMenuItem popupShowTurtle; + protected javax.swing.JCheckBoxMenuItem popupShowStatus; + protected javax.swing.JMenuItem popupExportCSV; + protected javax.swing.JMenu popupExportImage; + protected javax.swing.JMenuItem popupExportPNG; + protected javax.swing.JMenuItem popupExportSVG; + protected javax.swing.JMenuItem popupBackground; + protected javax.swing.JLabel lblOverwrite = new javax.swing.JLabel("File exists. Sure to overwrite?"); + protected javax.swing.JLabel lblScale = new javax.swing.JLabel("Scale factor:"); + private File currentDirectory = null; + private int[] lastAskedCoords = null; // last explicitly asked coordinates + private double lastAskedScale = 1.0; // last explicitly asked SVG scale + private Point displacement = null; // Origin displacement after moving the drawing + private Object zoomMutex = new Object(); // Sequentialization within the EventQueue + + private TurtleBox owner; + + public TurtleFrame(TurtleBox owner) + { + super(); + this.owner = owner; + initComponents(); + } + + private void initComponents() + { + panel = new TurtlePanel(); + + panel.addMouseListener(new MouseListener() { + @Override + public void mouseClicked(MouseEvent e) { + } + + @Override + public void mousePressed(MouseEvent e) { + if (e.isPopupTrigger() && popupMenu != null) { + popupMenu.show(e.getComponent(), e.getX(), e.getY()); + } + } + + @Override + public void mouseReleased(MouseEvent e) { + if (e.isPopupTrigger() && popupMenu != null) { + popupMenu.show(e.getComponent(), e.getX(), e.getY()); + } + } + + @Override + public void mouseEntered(MouseEvent e) { + } + + @Override + public void mouseExited(MouseEvent e) { + }}); + + scrollarea = new JScrollPane(panel); + scrollarea.getViewport().putClientProperty("EnableWindowBlit", Boolean.TRUE); + scrollarea.setWheelScrollingEnabled(true); + scrollarea.setDoubleBuffered(true); + scrollarea.setBorder(BorderFactory.createEmptyBorder()); + scrollarea.addMouseWheelListener(new MouseWheelListener() { + @Override + public void mouseWheelMoved(MouseWheelEvent mwEvt) { + if (mwEvt.isControlDown()) { + int rotation = mwEvt.getWheelRotation(); + if (Math.abs(rotation) >= 1) { + if (owner.reverseZoomWheel) { + rotation *= -1; + } + mwEvt.consume(); + zoom(rotation < 0); + } + } + }}); + getContentPane().add(scrollarea, java.awt.BorderLayout.CENTER); + panel.setAutoscrolls(true); + statusbar = new JPanel(new java.awt.FlowLayout(java.awt.FlowLayout.LEFT, 0, 0)); + //statusbar.setBorder(new javax.swing.border.CompoundBorder(new javax.swing.border.LineBorder(java.awt.Color.DARK_GRAY), + // new javax.swing.border.EmptyBorder(0, 4, 0, 4))); + statusHome = new javax.swing.JLabel(); + statusTurtle = new javax.swing.JLabel(); + statusSize = new javax.swing.JLabel(); + statusViewport = new javax.swing.JLabel(); + statusZoom = new javax.swing.JLabel(); + statusSelection = new javax.swing.JLabel("0"); + statusHome.setBorder(javax.swing.BorderFactory.createCompoundBorder(javax.swing.BorderFactory.createEtchedBorder(javax.swing.border.EtchedBorder.RAISED), + javax.swing.BorderFactory.createEmptyBorder(0, 4, 0, 4))); + statusTurtle.setBorder(javax.swing.BorderFactory.createCompoundBorder(javax.swing.BorderFactory.createEtchedBorder(javax.swing.border.EtchedBorder.RAISED), + javax.swing.BorderFactory.createEmptyBorder(0, 4, 0, 4))); + statusSize.setBorder(javax.swing.BorderFactory.createCompoundBorder(javax.swing.BorderFactory.createEtchedBorder(javax.swing.border.EtchedBorder.RAISED), + javax.swing.BorderFactory.createEmptyBorder(0, 4, 0, 4))); + statusViewport.setBorder(javax.swing.BorderFactory.createCompoundBorder(javax.swing.BorderFactory.createEtchedBorder(javax.swing.border.EtchedBorder.RAISED), + javax.swing.BorderFactory.createEmptyBorder(0, 4, 0, 4))); + statusZoom.setBorder(javax.swing.BorderFactory.createCompoundBorder(javax.swing.BorderFactory.createEtchedBorder(javax.swing.border.EtchedBorder.RAISED), + javax.swing.BorderFactory.createEmptyBorder(0, 4, 0, 4))); + statusSelection.setBorder(new javax.swing.border.CompoundBorder(javax.swing.BorderFactory.createEtchedBorder(javax.swing.border.EtchedBorder.RAISED), + new javax.swing.border.EmptyBorder(0, 4, 0, 4))); + statusHome.setToolTipText("Current home position"); + statusTurtle.setToolTipText("Current turtle position"); + statusSize.setToolTipText("Extension of the drawn area"); + statusViewport.setToolTipText("Current scrolling viewport"); + statusZoom.setToolTipText("Zoom factor"); + statusSelection.setToolTipText("Number of selected segments"); + statusbar.add(statusHome); + statusbar.add(statusTurtle); + statusbar.add(statusSize); + statusbar.add(statusViewport); + statusbar.add(statusZoom); + //statusbar.add(statusSelection); + statusbar.setFocusable(false); + getContentPane().add(statusbar, java.awt.BorderLayout.SOUTH); + scrollarea.getViewport().addChangeListener(new javax.swing.event.ChangeListener() { + @Override + public void stateChanged(ChangeEvent evt) { + updateStatusBar(); + } + }); + + initPopupMenu(); + + this.addKeyListener(this); + } + + /** + * Updates all status information in the status bar + */ + private void updateStatusBar() { + if (statusbar.isVisible()) { + int homeX = owner.home.x; + int homeY = owner.home.y; + // If the drawing was moved show the virtual moved home coordinate + if (displacement != null) { + homeX += displacement.x; + homeY += displacement.y; + } + statusHome.setText("(" + homeX + ", " + homeY + ")"); + statusTurtle.setText("(" + owner.pos.x + ", " + owner.pos.y + ") " + owner.getOrientation() + "°"); + Rectangle size = new Rectangle(owner.bounds); + size.add(owner.pos); + statusSize.setText( + // rightmost coordinate + Math.max(size.x + size.width, 0) + + " x " + // bottom coordinate + + Math.max(size.y + size.height, 0) + ); + synchronized(zoomMutex) { + Rectangle vRect = scrollarea.getViewport().getViewRect(); + vRect.x /= zoomFactor; + vRect.y /= zoomFactor; + vRect.width /= zoomFactor; + vRect.height /= zoomFactor; + statusViewport.setText(String.format("%d .. %d : %d .. %d", + vRect.x, vRect.x + vRect.width, + vRect.y, vRect.y + vRect.height)); + statusZoom.setText(String.format("%.1f %%", 100 * zoomFactor)); + } + } + popupShowTurtle.setSelected(!owner.turtleHidden); + popupShowOrigin.setEnabled(displacement != null); + if (displacement == null) { + popupShowOrigin.setSelected(false); + } + popupMoveIn.setEnabled(owner.bounds.x < 0 || owner.bounds.y < 0); + popupZoom100.setEnabled(zoomFactor != 1.0f); + } + + // START KGU#685 2020-12-11: Enh. #704 + /** + * Creates the popup menu + */ + private void initPopupMenu() + { + popupMenu = new javax.swing.JPopupMenu(); + popupGotoCoord = new javax.swing.JMenuItem("Scroll to coordinate ..."); + popupGotoTurtle = new javax.swing.JMenuItem("Scroll to turtle position"); + popupGotoHome = new javax.swing.JMenuItem("Scroll to home position"); + popupMoveIn = new javax.swing.JMenuItem("Move picture into area"); + popupZoom100 = new javax.swing.JMenuItem("Reset zoom to 100%"); + popupZoomBounds = new javax.swing.JMenuItem("Zoom to fit bounds"); + popupShowOrigin = new javax.swing.JCheckBoxMenuItem("Show coordinate cross"); + popupShowTurtle = new javax.swing.JCheckBoxMenuItem("Show turtle"); + popupShowStatus = new javax.swing.JCheckBoxMenuItem("Show status bar"); + popupExportCSV = new javax.swing.JMenuItem("Export drawing intems as CSV ..."); + popupExportImage = new javax.swing.JMenu("Export as image"); + popupExportPNG = new javax.swing.JMenuItem("to PNG ..."); + popupExportSVG = new javax.swing.JMenuItem("to SVG ..."); + popupBackground = new javax.swing.JMenuItem("Set background color ..."); + + popupGotoCoord.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + gotoCoordinate(); + }}); + popupMenu.add(popupGotoCoord); + // This doesn't work directly but shows the key binding handled via keyPressed() + popupGotoCoord.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_G, 0)); + + popupGotoTurtle.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + gotoTurtle(); + }}); + popupMenu.add(popupGotoTurtle); + // This doesn't work directly but shows the key binding handled via keyPressed() + popupGotoTurtle.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_END, 0)); + + popupGotoHome.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + gotoHome(); + }}); + popupMenu.add(popupGotoHome); + // This doesn't work directly but shows the key binding handled via keyPressed() + popupGotoHome.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_HOME, 0)); + + popupMenu.addSeparator(); + + popupZoom100.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + zoom(1.0f); + } + }); + popupMenu.add(popupZoom100); + // This doesn't work directly but shows the key binding handled via keyPressed() + popupZoom100.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_1, 0)); + + popupZoomBounds.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + zoomToBounds(); + } + + }); + popupMenu.add(popupZoomBounds); + // This doesn't work directly but shows the key binding handled via keyPressed() + popupZoomBounds.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_Z, 0)); + + popupMenu.addSeparator(); + + popupMoveIn.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + movePictureToCanvas(); + } + + }); + popupMenu.add(popupMoveIn); + // This doesn't work directly but shows the key binding handled via keyPressed() + popupMoveIn.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_M, 0)); + + popupShowOrigin.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + repaint(); + }}); + popupMenu.add(popupShowOrigin); + // This doesn't work directly but shows the key binding handled via keyPressed() + popupShowOrigin.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_O, 0)); + popupShowOrigin.setEnabled(displacement != null); + + popupMenu.addSeparator(); + + popupShowTurtle.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + owner.turtleHidden = !popupShowTurtle.isSelected(); + repaint(); + }}); + popupMenu.add(popupShowTurtle); + // This doesn't work directly but shows the key binding handled via keyPressed() + popupShowTurtle.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_T, 0)); + + popupBackground.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + setBackground(); + }}); + popupMenu.add(popupBackground); + // This doesn't work directly but shows the key binding handled via keyPressed() + popupBackground.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_B, 0)); + + popupMenu.addSeparator(); + + popupShowStatus.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + statusbar.setVisible(popupShowStatus.isSelected()); + updateStatusBar(); + }}); + popupMenu.add(popupShowStatus); + popupShowStatus.setSelected(true); + // This doesn't work directly but shows the key binding handled via keyPressed() + popupShowStatus.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_S, 0)); + + popupMenu.addSeparator(); + + popupExportCSV.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + exportCSV(); + }}); + popupMenu.add(popupExportCSV); + + popupMenu.add(popupExportImage); + + popupExportPNG.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + exportPNG(); + }}); + popupExportImage.add(popupExportPNG); + // This doesn't work directly but shows the key binding handled via keyPressed() + popupExportPNG.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_S, java.awt.event.InputEvent.CTRL_DOWN_MASK)); + + popupExportSVG.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + exportSVG(); + }}); + popupExportImage.add(popupExportSVG); + //popupExportImage.setEnabled(false); + } + + /** + * Adjusts the zoom factor to the next higher or lower step + * @param zoomIn - {@code true} to zoom in, {@code false} to zoom out + */ + private void zoom(boolean zoomIn) + { + if (zoomIn) { + zoom(this.zoomFactor / ZOOM_RATE); + } + else { + zoom(this.zoomFactor * ZOOM_RATE); + } + } + + /** + * Sets the zoom factor to given value {@code newFactor} if it doesn't + * exceed certain limits + * @param newFactor - proposed zoom factor + */ + private void zoom(float newFactor) + { + Point center = null; + synchronized(zoomMutex) { + Rectangle vRect = scrollarea.getViewport().getViewRect(); + // Try to maintain centre coordinate + center = new Point( + Math.round((vRect.x + vRect.width/2) / zoomFactor), + Math.round((vRect.y + vRect.height/2) / zoomFactor) + ); + this.zoomFactor = Math.max(MIN_ZOOM, Math.min(newFactor, MAX_ZOOM)); + panel.updatePreferredSize(true); + } + // Try to maintain centre coordinate + gotoCoordinate(center); + // Is this necessary? + //this.dispatchEvent(new ComponentEvent(this, ComponentEvent.COMPONENT_RESIZED)); + repaintAll(); + } + + /** + * Zooms such that the entire drawing fits into the viewport + */ + private void zoomToBounds() { + Rectangle bounds = new Rectangle(owner.bounds); + if (!owner.turtleHidden) { + bounds.add(owner.pos); + } + // Restrict the considered bounds to the visible part + if (bounds.x < 0) { + bounds.width += bounds.x; + if (bounds.width < 1) bounds.width = 1; + bounds.x = 0; + } + if (bounds.y < 0) { + bounds.height += bounds.y; + if (bounds.height < 1) bounds.height = 1; + bounds.y = 0; + } + // This is the actual viewport size + Rectangle view = scrollarea.getViewport().getViewRect(); + float zoomH = 1.0f * view.width / (bounds.width + MARGIN); + float zoomV = 1.0f * view.height / (bounds.height + MARGIN); + zoom(Math.min(zoomH, zoomV)); + gotoCoordinate(new Point(bounds.x + bounds.width/2, bounds.y + bounds.height/2)); + } + + /** + * Asks the user for a target coordinate and returns the entered coordinate + * as {@link Point} or its nearest point within the occupied area (if the + * coordinate lie outside). + * @return the {@link Point} representing the user input or {@code null} if + * cancelled. + */ + protected Point askForCoordinate() { + Point point = null; + JPanel pnlCoords = new JPanel(); + pnlCoords.setLayout(new java.awt.GridBagLayout()); + java.awt.GridBagConstraints gbc = new java.awt.GridBagConstraints(); + + gbc.gridx = 0; + gbc.gridy = 0; + gbc.gridwidth = 2; + gbc.weightx = 1; + gbc.weighty = 1; + gbc.anchor = java.awt.GridBagConstraints.LINE_START; + gbc.fill = java.awt.GridBagConstraints.HORIZONTAL; + gbc.insets = new java.awt.Insets(1, 2, 2, 1); + javax.swing.JLabel lblPrompt = new javax.swing.JLabel("Target coordinate"); + pnlCoords.add(lblPrompt, gbc); + javax.swing.JTextField[] fields = new javax.swing.JTextField[2]; + + gbc.gridwidth = 1; + for (int row = 0; row < 2; row++) { + gbc.gridy++; + gbc.gridx = 0; + gbc.weightx = 0; + gbc.fill = java.awt.GridBagConstraints.NONE; + pnlCoords.add(new javax.swing.JLabel((char)('x' + row) + ""), gbc); + gbc.gridx++; + gbc.weightx = 1; + gbc.fill = java.awt.GridBagConstraints.HORIZONTAL; + fields[row] = new javax.swing.JTextField(); + pnlCoords.add(fields[row], gbc); + if (lastAskedCoords != null) { + fields[row].setText(Integer.toString(lastAskedCoords[row])); + } + } + + do { + boolean committed = JOptionPane.showConfirmDialog(this, pnlCoords, + popupGotoCoord.getText(), + JOptionPane.OK_CANCEL_OPTION, + JOptionPane.QUESTION_MESSAGE) == JOptionPane.OK_OPTION; + if (!committed) { + return null; + } + point = new Point(); + for (int row = 0; row < fields.length; row++) { + try { + int coord = Integer.parseInt(fields[row].getText()); + if (row == 0) { + point.x = coord; + } + else { + point.y = coord; + } + fields[row].setForeground(null); + if (lastAskedCoords == null) { + lastAskedCoords = new int[] {0, 0}; + } + lastAskedCoords[row] = coord; + } + catch (NumberFormatException ex) { + fields[row].setForeground(Color.RED); + point = null; + break; + } + } + } while (point == null); + return point; + } + + /** + * Dialog method requesting a scale factor for the given + * {@code pixelBounds}, i.e. mm per pixel value + * @param pixelBounds - the bounding box of the drawing in pixels + * @param title - the title for the dialog + * @return width and height in mm as double array + */ + protected double askForScale(Rectangle pixelBounds, String title) + { + JPanel pnl = new JPanel(); + pnl.setLayout(new javax.swing.BoxLayout(pnl, javax.swing.BoxLayout.Y_AXIS)); + pnl.add(new javax.swing.JLabel(statusSize.getToolTipText() + ":")); + pnl.add(new javax.swing.JLabel(String.format("%d x %d pixel", pixelBounds.width, pixelBounds.height))); + javax.swing.JSpinner spnScale = new javax.swing.JSpinner(); + javax.swing.SpinnerModel spnModel = new SpinnerNumberModel(lastAskedScale, 1.0, 10.0, 1.0); + spnScale.setModel(spnModel); + JPanel pnlScale = new JPanel(); + pnlScale.setLayout(new javax.swing.BoxLayout(pnlScale, javax.swing.BoxLayout.X_AXIS)); + pnlScale.add(new javax.swing.JLabel(lblScale.getText())); + pnlScale.add(spnScale); + pnl.add(pnlScale); + int decision = JOptionPane.showConfirmDialog(this, + pnl, + title, JOptionPane.OK_CANCEL_OPTION); + if (decision != JOptionPane.OK_OPTION) { + return 0.0; + } + double scale = (double)spnModel.getValue(); + lastAskedScale = scale; + return scale; + } + + /** + * Opens a colour chooser dialog allowing to specify a colour with the + * given {@code oldColor} as initial setting + * @param oldColor - colour to be presented (white will be the default) + * @return the chosen colour or {@code null} + */ + protected Color chooseBackground(Color oldColor) { + javax.swing.JColorChooser colChooser = new javax.swing.JColorChooser(); + Color bgColor = null; + if (oldColor != null) { + colChooser.setColor(oldColor.getRGB()); + } + if (JOptionPane.showConfirmDialog(this, colChooser, + popupBackground.getText(), + JOptionPane.OK_CANCEL_OPTION) == JOptionPane.OK_OPTION) { + bgColor = colChooser.getColor(); + } + return bgColor; + } + + /** + * Exports the drawing elements and moves to a CSV file. Will open a + * file chooser dialog first. May pop up a message box if something goes wrong. + */ + private void exportCSV() + { + JFileChooser fc = new JFileChooser(currentDirectory); + fc.setDialogTitle(popupExportCSV.getText()); + ExtFileFilter filter = new CSVFilter(); + fc.addChoosableFileFilter(filter); + fc.setFileFilter(filter); + int decision = fc.showSaveDialog(this); + if (decision == JFileChooser.APPROVE_OPTION) { + File chosen = fc.getSelectedFile(); + if (chosen != null) { + // Ensure acceptable file extension + if (!filter.accept(chosen)) { + chosen = new File(chosen.getPath() + ".txt"); + } + Path path = chosen.toPath().toAbsolutePath(); + // Check for overriding + if (Files.exists(path)) { + decision = JOptionPane.showConfirmDialog(this, + lblOverwrite.getText(), + popupExportCSV.getText(), JOptionPane.OK_CANCEL_OPTION); + if (decision != JOptionPane.OK_OPTION) { + return; + } + } + try (BufferedWriter bw = Files.newBufferedWriter(path)) { + bw.append("xFrom,yFrom,xTo,yTo,color\n"); + int nElements = owner.elements.size(); + for (int i = 0; i < nElements; i++) { + bw.append(owner.elements.get(i).toCSV(null)); + bw.newLine(); + } + } catch (IOException exc) { + String message = exc.getMessage(); + if (message == null || message.isEmpty()) { + message = exc.toString(); + } + JOptionPane.showMessageDialog(this, message, + popupExportCSV.getName(), + JOptionPane.ERROR_MESSAGE); + } + if (Files.exists(path) && !chosen.isDirectory()) { + currentDirectory = chosen.getParentFile(); + } + } + } + } + + /** + * Exports the drawing to an SVG file. Will open a file chooser dialog first + * and ask for a scaling factor. + * May pop up a message box if something goes wrong. + */ + private void exportSVG() + { + final int MAX_POINTS_PER_PATH = 800; + JFileChooser fc = new JFileChooser(currentDirectory); + fc.setDialogTitle( + (popupExportImage.getText() + " " + popupExportSVG.getText()) + .replace("...", "")); + ExtFileFilter filter = new SVGFilter(); + fc.addChoosableFileFilter(filter); + fc.setFileFilter(filter); + int decision = fc.showSaveDialog(this); + if (decision == JFileChooser.APPROVE_OPTION) { + File chosen = fc.getSelectedFile(); + if (chosen != null) { + // Ensure acceptable file extension + if (!filter.accept(chosen)) { + chosen = new File(chosen.getPath() + ".svg"); + } + Path path = chosen.toPath().toAbsolutePath(); + // Check for overriding + if (Files.exists(path)) { + decision = JOptionPane.showConfirmDialog(this, + lblOverwrite.getText(), + popupExportCSV.getText(), JOptionPane.OK_CANCEL_OPTION); + if (decision != JOptionPane.OK_OPTION) { + return; + } + } + int offsetX = -owner.bounds.x, offsetY = -owner.bounds.y; + float scale = (float)askForScale(owner.bounds, fc.getDialogTitle()); + if (scale < 1) { + // Cancelled by the user + return; + } + try (BufferedWriter bw = Files.newBufferedWriter(path)) { + bw.append("\n"); + bw.append("\n"); + bw.append("\n"); + String title = chosen.getName(); + if (title.contains(".")) { + title = title.substring(0, title.lastIndexOf('.')-1); + } + bw.append(" " + title + "\n"); + + /* Draw the background: + * The fill colour must not be given as hex code, otherwise the rectangle + * will always be black! */ + bw.append(String.format(" \n"); + + // Now export the elements + bw.append(" \n"); + Point lastPt = null; + Color lastCol = null; + int nPoints = 0; + int nElements = owner.elements.size(); + for (int i = 0; i < nElements; i++) { + Element el = owner.elements.get(i); + if (el instanceof Line) { + Point from = el.getFrom(); + Point to = el.getTo(); + Color col = el.getColor(); + if (lastPt == null || !lastPt.equals(from) + || lastCol == null || !lastCol.equals(col) + || nPoints >= MAX_POINTS_PER_PATH) { + if (i > 0) { + bw.append("\" />\n"); + } + bw.append(" \n"); + } + bw.append(" \n"); + bw.append("\n"); + } catch (IOException exc) { + String message = exc.getMessage(); + if (message == null || message.isEmpty()) { + message = exc.toString(); + } + JOptionPane.showMessageDialog(this, message, + fc.getDialogTitle(), + JOptionPane.ERROR_MESSAGE); + } + if (Files.exists(path) && !chosen.isDirectory()) { + currentDirectory = chosen.getParentFile(); + } + } + } + } + + /** + * Exports the drawing as PNG file. Will open a file chooser dialog first. + * May pop up a message box if something goes wrong. + */ + private void exportPNG() + { + JFileChooser dlgSave = new JFileChooser(currentDirectory); + ExtFileFilter filter = new PNGFilter(); + dlgSave.addChoosableFileFilter(filter); + dlgSave.setFileFilter(filter); + dlgSave.setDialogTitle( + (popupExportImage.getText() + " " + popupExportPNG.getText()) + .replace("...", "")); + int decision = dlgSave.showSaveDialog(this); + if (decision == JFileChooser.APPROVE_OPTION) + { + currentDirectory = dlgSave.getCurrentDirectory(); + while (currentDirectory != null && !currentDirectory.isDirectory()) + { + currentDirectory = currentDirectory.getParentFile(); + } + // correct the filename, if necessary + File chosen = dlgSave.getSelectedFile(); + if (!filter.accept(chosen)) { + chosen = new File(chosen.getPath() + ".png"); + } + // Check for overriding + Path path = chosen.toPath().toAbsolutePath(); + if (Files.exists(path)) { + decision = JOptionPane.showConfirmDialog(this, + lblOverwrite.getText(), + popupExportCSV.getText(), JOptionPane.OK_CANCEL_OPTION); + if (decision != JOptionPane.OK_OPTION) { + return; + } + } + synchronized(zoomMutex) { + int offsetX = 0, offsetY = 0; + Dimension dim = panel.getPreferredSize(); + int width = dim.width - Math.round(offsetX * zoomFactor); + int height = dim.height - Math.round(offsetY * zoomFactor); + BufferedImage bi = new BufferedImage( + Math.round(width / this.zoomFactor), + Math.round(height / this.zoomFactor), + BufferedImage.TYPE_4BYTE_ABGR); + panel.paint(bi.getGraphics(), true); + try + { + ImageIO.write(bi, "png", chosen); + } + catch(Exception e) + { + String message = e.getMessage(); + if (message == null || message.trim().isEmpty()) { + message = e.toString(); + } + JOptionPane.showMessageDialog(this, + message, + dlgSave.getDialogTitle(), + JOptionPane.ERROR_MESSAGE); + } + } + } + } + + /** + * Repaints the turtle drawing and updates the status bar + */ + public void repaintAll() + { + panel.repaint(); + updateStatusBar(); + } + + @Override + public void keyTyped(KeyEvent ev) { + // Nothing to do here + } + + @Override + public void keyPressed(KeyEvent ev) { + if (ev.getSource() == this && !ev.isAltDown() && !ev.isAltGraphDown()) { + switch (ev.getKeyCode()) { + case KeyEvent.VK_HOME: + gotoHome(); + break; + case KeyEvent.VK_END: + gotoTurtle(); + break; + case KeyEvent.VK_G: + gotoCoordinate(); + break; + case KeyEvent.VK_T: + owner.turtleHidden = !owner.turtleHidden; + popupShowTurtle.setSelected(!owner.turtleHidden); + repaint(); + break; + case KeyEvent.VK_B: + setBackground(); + break; + case KeyEvent.VK_S: + if (ev.isControlDown()) { + exportPNG(); + } + else { + statusbar.setVisible(!statusbar.isVisible()); + popupShowStatus.setSelected(statusbar.isVisible()); + updateStatusBar(); + } + break; + case KeyEvent.VK_UP: + handleCursorKey(-1, false, ev.isShiftDown() ? 10 : 1); + break; + case KeyEvent.VK_DOWN: + handleCursorKey(+1, false, ev.isShiftDown() ? 10 : 1); + break; + case KeyEvent.VK_LEFT: + handleCursorKey(-1, true, ev.isShiftDown() ? 10 : 1); + break; + case KeyEvent.VK_RIGHT: + handleCursorKey(+1, true, ev.isShiftDown() ? 10 : 1); + break; + case KeyEvent.VK_PAGE_UP: + handlePageKey(-1, ev.isShiftDown()); + break; + case KeyEvent.VK_PAGE_DOWN: + handlePageKey(1, ev.isShiftDown()); + break; + case KeyEvent.VK_ADD: + zoom(true); + break; + case KeyEvent.VK_SUBTRACT: + zoom(false); + break; + case KeyEvent.VK_1: + zoom(1.0f); + break; + case KeyEvent.VK_Z: + zoomToBounds(); + break; + case KeyEvent.VK_M: + if (owner.bounds.x < 0 || owner.bounds.y < 0) { + movePictureToCanvas(); + } + break; + case KeyEvent.VK_O: + if (displacement != null) { + popupShowOrigin.doClick(); + } + break; + } + } + } + + @Override + public void keyReleased(KeyEvent ev) { + // Nothing to do here + } + + private void gotoHome() + { + Point home = new Point(owner.home); + if (displacement != null) { + home.x += displacement.x; + home.y += displacement.y; + } + gotoCoordinate(home); + } + + private void gotoTurtle() + { + gotoCoordinate(owner.pos); + } + + private void gotoCoordinate() + { + Point coord = askForCoordinate(); + if (coord != null) { + gotoCoordinate(coord); + } + } + + /** + * @param coord + */ + private void gotoCoordinate(Point coord) { + Rectangle vRect = null; + synchronized(zoomMutex) { + vRect = scrollarea.getViewport().getViewRect(); + int marginH = vRect.width/2; // horizontal margin + int marginV = vRect.height/2; // vertical margin + // Estimate the position in the zoomed panel + int posX = Math.round(coord.x * zoomFactor); + int posY = Math.round(coord.y * zoomFactor); + vRect.x = Math.max(posX - marginH, 0); + vRect.y = Math.max(posY - marginV, 0); + } + if (vRect != null) { + panel.scrollRectToVisible(vRect); + } + repaint(); + } + + /** + * Moves all elements into the visible canvas (i.e. the positive quadrant) + */ + private void movePictureToCanvas() + { + if (owner.bounds.x < 0 || owner.bounds.y < 0) { + // We must prevent any repaint activity during this action + synchronized (this.getTreeLock()) { + Dimension shift = new Dimension( + Math.max(-owner.bounds.x, 0), + Math.max(-owner.bounds.y, 0)); + int nElements = owner.elements.size(); + for (int i = 0; i < nElements; i++) { + owner.elements.get(i).move(shift); + } + // Move all important points as well (except home) + owner.pos.x += shift.width; owner.pos.y += shift.height; + owner.posX += shift.width; owner.posY += shift.height; + owner.bounds.x += shift.width; owner.bounds.y += shift.height; + displacement = new Point(shift.width, shift.height); + } + repaintAll(); + } + } + + private void setBackground() + { + Color bgColor = chooseBackground(owner.backgroundColor); + if (bgColor != null) { + owner.setBackgroundColor(bgColor); + } + } + + private void handleCursorKey(int dir, boolean horizontal, int factor) + { + javax.swing.JScrollBar bar; + if (horizontal) { + bar = scrollarea.getHorizontalScrollBar(); + } + else { + bar = scrollarea.getVerticalScrollBar(); + } + int units = bar.getUnitIncrement(dir) * factor; + bar.setValue(Math.max(bar.getValue() + dir * units, 0)); + } + + private void handlePageKey(int dir, boolean horizontal) + { + javax.swing.JScrollBar bar; + if (horizontal) { + bar = scrollarea.getHorizontalScrollBar(); + } + else { + bar = scrollarea.getVerticalScrollBar(); + } + bar.setValue(bar.getValue() + dir * bar.getBlockIncrement(dir)); + } + + } + /** The GUI frame - while null, it hasn't been materialized (light-weight instance) */ - private JFrame frame = null; - // END KGU#480 2018-01-16 + private TurtleFrame frame = null; + /** + * @return the used frame (e.g. for localization purposes), may be {@code null}. + */ + public JFrame getFrame() + { + return frame; + } + + /** + * Adapt the GUI components to the current look and feel + */ + public void updateLookAndFeel() + { + if (frame != null) { + try { + javax.swing.SwingUtilities.updateComponentTreeUI(frame); + javax.swing.SwingUtilities.updateComponentTreeUI(frame.popupMenu); + } + catch (Exception ex) {} + } + } + // END KGU#685 2020-12-11 + private final String TITLE = "Turtleizer"; + + // START KGU#685 2020-12-11: Enh. #704 + /** Width and height margin for the drawn area (regarding scrollbars) */ + private static final int MARGIN = 20; + /** Maximum zoom factor */ + private static final float MAX_ZOOM = 2.0f; + /** Minimum zoom factor */ + private static final float MIN_ZOOM = 0.01f; + /** Zoom change factor */ + private static final float ZOOM_RATE = 0.9f; + /** Flag to specify reverse zoom effect of mouse wheel */ + private boolean reverseZoomWheel = false; + public void setReverseZoomWheel(boolean isReverse) { + reverseZoomWheel = isReverse; + } + // END KGU#685 2020-12-11 private Point pos; // START KGU#282 2016-10-16: Enh. #272 @@ -183,7 +1384,10 @@ public class TurtleBox implements DelayableDiagramController private boolean turtleHidden = false; private int delay = 10; private Vector elements = new Vector(); - private JPanel panel; + // START KGU#685 2020-12-14: Enh. #704 + /** bounding box of all visible elements, to be maintained via {@link #addLine(Point,Point,Color)} */ + private Rectangle bounds = new Rectangle(); + // END KGU#685 2020-12-14 /** * This constructor does NOT realize a GUI, it just creates a light-weight instance @@ -247,26 +1451,26 @@ public double getAngle() * @see #getAngle() */ public double getOrientation() { - double orient = angle + 90.0; - while (orient > 180) { orient -= 360; } - while (orient < -180) { orient += 360; } - return -orient; + double orient = angle + 90.0; + while (orient > 180) { orient -= 360; } + while (orient < -180) { orient += 360; } + return orient == 0.0 ? orient : -orient; } // END KGU#417 2017-06-29 // START #272 2016-10-16 (KGU) private void setPos(Point newPos) { - pos = newPos; - posX = pos.getX(); - posY = pos.getY(); + pos = newPos; + posX = pos.getX(); + posY = pos.getY(); } private void setPos(double x, double y) { - posX = x; - posY = y; - pos = new Point((int)Math.round(x), (int)Math.round(y)); + posX = x; + posY = y; + pos = new Point((int)Math.round(x), (int)Math.round(y)); } // END #272 2016-10-16 @@ -280,70 +1484,12 @@ private void init(int width, int height) { // START KGU#480 2018-01-16: Enh. #490 - care for the existence of a frame if (frame == null) { - frame = new JFrame(); + // START KGU#685 2020-12-12 + //frame = new JFrame(); + frame = new TurtleFrame(this); + // END KGU#685 2020-12-12 } // END KGU#480 2018-01-16 - panel = new JPanel() - { - @Override - public void paint(Graphics graphics) - { - Graphics2D g = (Graphics2D) graphics; - // set anti-aliasing rendering - ((Graphics2D)g).setRenderingHint(RenderingHints.KEY_ANTIALIASING,RenderingHints.VALUE_ANTIALIAS_ON); - - // clear background - // START KGU#303 2016-12-02: Enh. #302 - //g.setColor(Color.WHITE); - g.setColor(backgroundColor); - // END KGU#303 2016-12-02 - g.fillRect(0,0,getWidth(),getHeight()); - // START KGU#303 2016-12-03: Enh. #302 - //g.setColor(Color.BLACK); - g.setColor(defaultPenColor); - // END KGU#303 2016-12-03 - - // draw all elements - // START KGU#449 2017-10-28: The use of iterators may lead to lots of - //java.util.ConcurrentModificationException errors slowing down all. - // So we better avoid the iterator and loop against a snapshot size - // (which is safe because the elements Vector can't shrink during execution). - //for (Element ele : elements) - //{ - // ele.draw(g); - //} - int nElements = elements.size(); - // START KGU#597 2018-10-12: Issue #622 - Monitoring drawing detention underMac - //logger.config("Painting " + nElements + " elements..."); - // END KGU#597 2018-10-12 - for (int i = 0; i < nElements; i++) { - elements.get(i).draw(g); - } - // END KGU#449 2017-10-28 - - if (!turtleHidden) - { - // START #272 2016-10-16 - // fix drawing point - //int x = (int) Math.round(pos.x - (image.getWidth(this)/2)); - //int y = (int) Math.round(pos.y - (image.getHeight(this)/2)); - // fix drawing point - //int xRot = x+image.getWidth(this)/2; - //int yRot = y+image.getHeight(this)/2; - // apply rotation - //g.rotate((270-angle)/180*Math.PI,xRot,yRot); - // fix drawing point - double x = posX - (image.getWidth(this)/2); - double y = posY - (image.getHeight(this)/2); - // apply rotation - g.rotate((270-angle)/180*Math.PI, posX, posY); - // END #272 2016-10-16 - // draw the turtle - g.drawImage(image,(int)Math.round(x),(int)Math.round(y),this); - } - } - }; - frame.setTitle(TITLE); //frame.setIconImage((new ImageIcon(this.getClass().getResource("turtle.png"))).getImage()); frame.setIconImage(image); @@ -351,13 +1497,21 @@ public void paint(Graphics graphics) //this.setDefaultCloseOperation(TurtleBox.EXIT_ON_CLOSE); //this.setDefaultCloseOperation(TurtleBox.DISPOSE_ON_CLOSE); frame.setDefaultCloseOperation(JFrame.HIDE_ON_CLOSE); - frame.setBounds(0,0,width,height); - frame.getContentPane().add(panel); + frame.setBounds(0, 0, width, height); + // START KGU#685 2020-12-11: Enh. #704 + //frame.getContentPane().add(panel); //this.setVisible(true); - setPos(new Point(panel.getWidth()/2,panel.getHeight()/2)); - home = new Point(panel.getWidth()/2,panel.getHeight()/2); - panel.setDoubleBuffered(true); - panel.repaint(); + //setPos(new Point(panel.getWidth()/2,panel.getHeight()/2)); + //home = new Point(panel.getWidth()/2,panel.getHeight()/2); + //panel.setDoubleBuffered(true); + //panel.repaint(); + + setPos(new Point(frame.scrollarea.getWidth()/2, + frame.scrollarea.getHeight()/2)); // FIXME! + home = new Point(pos.x, pos.y); + + frame.repaintAll(); + // END KGU#685 2020-12-11 } //@Override @@ -385,11 +1539,18 @@ public void setVisible(boolean visible) // setPos(new Point(panel.getWidth()/2,panel.getHeight()/2)); // END KGU#303 2016-12-03 if (visible) { - home = new Point(panel.getWidth()/2,panel.getHeight()/2); + // START KGU #685 2020-12-11: Enh. #704 + //home = new Point(panel.getWidth()/2, panel.getHeight()/2); + home = new Point(Math.round(frame.scrollarea.getWidth()/2 / frame.zoomFactor), + Math.round(frame.scrollarea.getHeight()/2 / frame.zoomFactor)); + // END KGU#685 2020-12-11 // START KGU#303 2016-12-03: Issue #302 - replaces disabled code above reinit(); // END KGU#303 2016-12-03 frame.paint(frame.getGraphics()); + // START KGU#685 2020-12-11: Enh. #704 + frame.updateStatusBar(); + // END KGU#685 2020-12-11 } } @@ -404,6 +1565,14 @@ private void reinit() backgroundColor = Color.WHITE; defaultPenColor = Color.BLACK; turtleHidden = false; + // START KGU#685 2020-12-14: Enh. #704 + bounds = new Rectangle(); + bounds.width = -1; + bounds.height = -1; + if (frame != null) { + frame.displacement = null; + } + // END KGU#685 2020-12-14 setPos(home.getLocation()); penDown(); } @@ -415,28 +1584,29 @@ private void delay() // force repaint (not recommended!) // START KGU#480 2018-01-16: Enh. #490 - lazy initialization if (frame == null) { - init(300, 300); + init(300, 300); } // END KGU#480 2018-01-16 - // START KGU#597 2018-10-12: Issue #622 Attempt to fix a drawing contention on some Macbook - //logger.config(panel + " enqueuing repaint()..."); - panel.repaint(); - // END KGU#597 2018-10-12 + // START KGU#685 2020-12-11: Enh. #704 + //panel.repaint(); + frame.repaintAll(); + // END KGU#685 2020-12-11 if (delay!=0) { - // START KGU#597 2018-10-12: Issue #622 Replaced by the queued repaint() above - //panel.paint(panel.getGraphics()); - // END KGU#597 2018-10-12 try { Thread.sleep(delay); } catch (InterruptedException e) { System.out.println(e.getMessage());} } - // START KGU#597 2018-10-12: Issue #622 Now done before the alternative -// else -// { -// panel.repaint(); -// } - // END KGU#597 2018-10-12 } + + // START KGU#685 2020-12-14: Enh. #704 + /** Method to ensure incremental bounds adjustment */ + private void addLine(Point from, Point to, Color color) + { + Line line = new Line(from, to, color); + elements.add(line); + bounds.add(line.getBounds()); + } + // END KGU#685 2020-12-14 public void fd(Integer pixels) { @@ -444,7 +1614,7 @@ public void fd(Integer pixels) pos.y+(int) Math.round(Math.sin(angle/180*Math.PI)*pixels)); if (isPenDown) { - elements.add(new Line(pos,newPos,penColor)); + addLine(pos, newPos, penColor); } else { @@ -467,7 +1637,7 @@ public void forward(Double pixels) Point newPos = new Point((int)Math.round(newX), (int)Math.round(newY)); if (isPenDown) { - elements.add(new Line(pos, newPos, penColor)); + addLine(pos, newPos, penColor); } else { @@ -548,10 +1718,10 @@ public void setBackgroundColor(Color bgColor) // START KGU#480 2018-01-16: Enh. #490 - lazy initialization if (frame == null) { init(300, 300); - } + } // END KGU#480 2018-01-16 backgroundColor = bgColor; - panel.repaint(); + frame.panel.repaint(); } /** Delayed API method to set the background colour from RGB values @@ -578,7 +1748,7 @@ public void setPenColor(Color penColor) } // END KGU#480 2018-01-16 defaultPenColor = penColor; - panel.repaint(); + frame.panel.repaint(); } /** @@ -596,6 +1766,11 @@ public void setPenColor(Integer red, Integer green, Integer blue) // END KGU#303 2016-12-02 // KGU: Where is this method used? + /** + * @return the angle from the turtle home position to its current position + * in integral (!?) degrees + */ + @Deprecated public double getAngleToHome() { // START #272 2016-10-16 (KGU) @@ -617,21 +1792,21 @@ public double getAngleToHome() // rounding (FIXME: what for?) alpha = Math.round(alpha*100)/100; - if (cosAlpha<0) // Q2 & Q3 + if (cosAlpha < 0) // Q2 & Q3 { - alpha=180-alpha; + alpha = 180-alpha; } alpha =- alpha; alpha -= getAngle(); while (alpha < 0) { - alpha+=360; + alpha += 360; } while (alpha>=360) { - alpha-=360; + alpha -= 360; } return alpha; @@ -661,7 +1836,7 @@ public void penDown() public void gotoXY(Integer x, Integer y) { Point newPos = new Point(x,y); - elements.add(new Move(pos,newPos)); + elements.add(new Move(pos, newPos)); setPos(newPos); delay(); } @@ -689,14 +1864,14 @@ public void gotoX(Integer x) /** The turtle icon will no longer be shown, has no impact on the pen */ public void hideTurtle() { - turtleHidden=true; + turtleHidden = true; delay(); } /** The turtle icon will be shown again, has no impact on the pen */ public void showTurtle() { - turtleHidden=false; + turtleHidden = false; delay(); } @@ -748,49 +1923,52 @@ public void requestFocus() } } // END KGU#356 2019-03-02 + - @Deprecated - private String parseFunctionName(String str) - { - if (str.trim().indexOf("(")!=-1) - return str.trim().substring(0,str.trim().indexOf("(")).trim().toLowerCase(); - else - return null; - } - - @Deprecated - private String parseFunctionParam(String str, int count) - { - String res = null; - int posParen1 = (str = str.trim()).indexOf("("); - if (posParen1 > -1) - { - String params = str.substring(posParen1+1, str.indexOf(")")).trim(); - if (!params.isEmpty()) - { - String[] args = params.split(","); - if (count < args.length) { - res = args[count]; - } - } - } - return res; - } - - @Deprecated - private Double parseFunctionParamDouble(String str, int count) - { - String res = parseFunctionParam(str, count); - if( res == null || res.isEmpty() ) { return 0.0; } - return Double.valueOf(res); - } - - @Deprecated - public String execute(String message, Color color) - { - setColorNonWhite(color); - return execute(message); - } +// START KGU#448/KGU#673 2020-12-11: Enh. #443 deprecated stuff disabled +// @Deprecated +// private String parseFunctionName(String str) +// { +// if (str.trim().indexOf("(")!=-1) +// return str.trim().substring(0,str.trim().indexOf("(")).trim().toLowerCase(); +// else +// return null; +// } +// +// @Deprecated +// private String parseFunctionParam(String str, int count) +// { +// String res = null; +// int posParen1 = (str = str.trim()).indexOf("("); +// if (posParen1 > -1) +// { +// String params = str.substring(posParen1+1, str.indexOf(")")).trim(); +// if (!params.isEmpty()) +// { +// String[] args = params.split(","); +// if (count < args.length) { +// res = args[count]; +// } +// } +// } +// return res; +// } +// +// @Deprecated +// private Double parseFunctionParamDouble(String str, int count) +// { +// String res = parseFunctionParam(str, count); +// if( res == null || res.isEmpty() ) { return 0.0; } +// return Double.valueOf(res); +// } +// +// @Deprecated +// public String execute(String message, Color color) +// { +// setColorNonWhite(color); +// return execute(message); +// } +// END KGU#448/KGU#673 2020-12-11 /** * Sets the given color except in case of WHITE where the {@link #defaultPenColor} is used instead. @@ -811,65 +1989,67 @@ private void setColorNonWhite(Color color) { else this.setColor(color); } - @Deprecated - public String execute(String message) - { - String name = parseFunctionName(message); - double param1 = parseFunctionParamDouble(message,0); - double param2 = parseFunctionParamDouble(message,1); - // START KGU#303 2016-12-02: Enh. #302 - double param3 = parseFunctionParamDouble(message,2); - // END KGU#303 2016-12-02 - String res = new String(); - if(name!=null) - { - if (name.equals("init")) { -// START KGU#303 2016-12-03: Issue #302 - replaced by reinit() call -// elements.clear(); -// angle=-90; -// // START KGU#303 2016-12-02: Enh. #302 -// backgroundColor = Color.WHITE; -// // END KGU#3032016-12-02 -// // START KGU#303 2016-12-03: Enh. #302 -// defaultPenColor = Color.BLACK; -// turtleHidden = false; -// // END KGU#3032016-12-03 -// setPos(home.getLocation()); -// penDown(); -// reinit(); -// END KGU#303 2016-12-03 - setAnimationDelay((int) param1, true); - } - // START #272 2016-10-16 (KGU): Now different types (to allow to study rounding behaviour) - //else if (name.equals("forward") || name.equals("fd")) { forward((int) param1); } - //else if (name.equals("backward") || name.equals("bk")) { backward((int) param1); } - else if (name.equals("forward")) { forward(param1); } - else if (name.equals("backward")) { backward(param1); } - else if (name.equals("fd")) { fd((int)param1); } - else if (name.equals("bk")) { bk((int)param1); } - // END #272 2016-10-16 - // START KGU 20141007: Wrong type casting mended (led to rotation biases) - //else if (name.equals("left") || name.equals("rl")) { left((int) param1); } - //else if (name.equals("right") || name.equals("rr")) { right((int) param1); } - else if (name.equals("left") || name.equals("rl")) { left(param1); } - else if (name.equals("right") || name.equals("rr")) { right(param1); } - // END KGU 20141007 - else if (name.equals("penup") || name.equals("up")) { penUp(); } - else if (name.equals("pendown") || name.equals("down")) { penDown(); } - else if (name.equals("gotoxy")) { gotoXY((int) param1, (int) param2); } - else if (name.equals("gotox")) { gotoX((int) param1); } - else if (name.equals("gotoy")) { gotoY((int) param1); } - else if (name.equals("hideturtle")) { hideTurtle(); } - else if (name.equals("showturtle")) { showTurtle(); } - // START KGU#303 2016-12-02: Enh. #302 - A procedure to set the backgroud colour was requested - else if (name.equals("setbackground")) { setBackgroundColor((int)Math.abs(param1),(int)Math.abs(param2),(int)Math.abs(param3)); } - else if (name.equals("setpencolor")) { setPenColor((int)Math.abs(param1),(int)Math.abs(param2),(int)Math.abs(param3)); } - // END KGU#303 2016-12-02 - else { res="Procedure <"+name+"> not implemented!"; } - } - - return res; - } +// START KGU#448/KGU#673 2020-12-11: Enh. #443 deprecated stuff disabled +// @Deprecated +// public String execute(String message) +// { +// String name = parseFunctionName(message); +// double param1 = parseFunctionParamDouble(message,0); +// double param2 = parseFunctionParamDouble(message,1); +// // START KGU#303 2016-12-02: Enh. #302 +// double param3 = parseFunctionParamDouble(message,2); +// // END KGU#303 2016-12-02 +// String res = new String(); +// if(name!=null) +// { +// if (name.equals("init")) { +//// START KGU#303 2016-12-03: Issue #302 - replaced by reinit() call +//// elements.clear(); +//// angle=-90; +//// // START KGU#303 2016-12-02: Enh. #302 +//// backgroundColor = Color.WHITE; +//// // END KGU#3032016-12-02 +//// // START KGU#303 2016-12-03: Enh. #302 +//// defaultPenColor = Color.BLACK; +//// turtleHidden = false; +//// // END KGU#3032016-12-03 +//// setPos(home.getLocation()); +//// penDown(); +//// reinit(); +//// END KGU#303 2016-12-03 +// setAnimationDelay((int) param1, true); +// } +// // START #272 2016-10-16 (KGU): Now different types (to allow to study rounding behaviour) +// //else if (name.equals("forward") || name.equals("fd")) { forward((int) param1); } +// //else if (name.equals("backward") || name.equals("bk")) { backward((int) param1); } +// else if (name.equals("forward")) { forward(param1); } +// else if (name.equals("backward")) { backward(param1); } +// else if (name.equals("fd")) { fd((int)param1); } +// else if (name.equals("bk")) { bk((int)param1); } +// // END #272 2016-10-16 +// // START KGU 20141007: Wrong type casting mended (led to rotation biases) +// //else if (name.equals("left") || name.equals("rl")) { left((int) param1); } +// //else if (name.equals("right") || name.equals("rr")) { right((int) param1); } +// else if (name.equals("left") || name.equals("rl")) { left(param1); } +// else if (name.equals("right") || name.equals("rr")) { right(param1); } +// // END KGU 20141007 +// else if (name.equals("penup") || name.equals("up")) { penUp(); } +// else if (name.equals("pendown") || name.equals("down")) { penDown(); } +// else if (name.equals("gotoxy")) { gotoXY((int) param1, (int) param2); } +// else if (name.equals("gotox")) { gotoX((int) param1); } +// else if (name.equals("gotoy")) { gotoY((int) param1); } +// else if (name.equals("hideturtle")) { hideTurtle(); } +// else if (name.equals("showturtle")) { showTurtle(); } +// // START KGU#303 2016-12-02: Enh. #302 - A procedure to set the backgroud colour was requested +// else if (name.equals("setbackground")) { setBackgroundColor((int)Math.abs(param1),(int)Math.abs(param2),(int)Math.abs(param3)); } +// else if (name.equals("setpencolor")) { setPenColor((int)Math.abs(param1),(int)Math.abs(param2),(int)Math.abs(param3)); } +// // END KGU#303 2016-12-02 +// else { res="Procedure <"+name+"> not implemented!"; } +// } +// +// return res; +// } +// END KGU#448/KGU#673 2020-12-11 // START KGU#448 2017-10-28: Enh. #443 /* (non-Javadoc) @@ -916,6 +2096,12 @@ public double getY() public void clear() { this.elements.clear(); + // START KGU#685 2020-12-14: Enh. #704 + this.bounds = new Rectangle(); + this.bounds.width = -1; + this.bounds.height = -1; + frame.displacement = null; + // END KGU#685 2020-12-14 this.delay(); } // END KGU#566 2018-07-30 diff --git a/src/lu/fisch/turtle/adapters/Turtleizer.java b/src/lu/fisch/turtle/adapters/Turtleizer.java index 6c8ea5b2..14d465cd 100644 --- a/src/lu/fisch/turtle/adapters/Turtleizer.java +++ b/src/lu/fisch/turtle/adapters/Turtleizer.java @@ -98,9 +98,10 @@ public class Turtleizer { }}; /** - * Returns a (heavy-weight) as-if singleton instance of class {@link TurtleBox} (creates it if it - * hadn't been there).
- * Note: For mere API retrieval use a light-weight instance to be obtained via {@link TurtleBox#TurtleBox()}. + * Returns a (heavy-weight) as-if singleton instance of class {@link TurtleBox} + * (creates it if it hadn't been there).
+ * Note: For mere API retrieval use a light-weight instance to be obtained via + * {@link TurtleBox#TurtleBox()}. */ private static TurtleBox getTurtleBox() { diff --git a/src/lu/fisch/turtle/elements/Element.java b/src/lu/fisch/turtle/elements/Element.java index c74ead69..2806a915 100644 --- a/src/lu/fisch/turtle/elements/Element.java +++ b/src/lu/fisch/turtle/elements/Element.java @@ -19,12 +19,37 @@ package lu.fisch.turtle.elements; +/****************************************************************************************************** + * + * Author: Robert Fisch + * + * Description: Move - an invisible move in the Turtle graphics window + * + ****************************************************************************************************** + * + * Revision List + * + * Author Date Description + * ------ ---- ----------- + * Kay Gürtzig 2020-12-11 Enh. #704 API extension: draw(Graphics2D, Rectangle), + * toString(), appendSpecificCSVInfo(StringBuilder, String) + * Kay Gürtzig 2020-12-13 Enh. #704 API extension: getFrom(), getTo(), getColor(), + * 2020-12-14 move(Dimension) + * + ****************************************************************************************************** + * + * Comment: + * + ******************************************************************************************************/// + import java.awt.Color; +import java.awt.Dimension; import java.awt.Graphics2D; import java.awt.Point; +import java.awt.Rectangle; /** - * + * Abstract base class for atomic parts of a TurtleBox drawing * @author robertfisch */ public abstract class Element @@ -35,18 +60,96 @@ public abstract class Element public Element(Point from, Point to) { - this.from=from; - this.to=to; + this.from = from; + this.to = to; } public Element(Point from, Point to, Color color) { - this.from=from; - this.to=to; - this.color=color; + this.from = from; + this.to = to; + this.color = color; } - + /** + * Draws this Element in the given 2D drawing environment {@code graphics} + * @param graphics - the 2D drawing environment + * @see #draw(Graphics2D, Rectangle) + */ public abstract void draw(Graphics2D graphics); + // START KGU#685 2020-12-11: Enh. #704 + /** + * Like {@link #draw(Graphics2D)} but avoids drawing outside the visible area + * {@code viewRect} and expands the given {@link Dimension} {@code dim} if this + * Element is visible and exceeds it. + * @param graphics - the 2D drawing environment + * @param viewRect - visible clip of the graphics system (for acceleration) + */ + public abstract void draw(Graphics2D graphics, Rectangle viewRect); + + @Override + public String toString() + { + return this.getClass().getSimpleName() + "(" + this.from + "," + this.to + "," + this.color + ")"; + } + + public String toCSV(String separator) + { + StringBuilder sb = new StringBuilder(); + if (separator == null || separator.isEmpty()) { + separator = ","; + } + sb.append(from.x); + sb.append(separator); + sb.append(from.y); + sb.append(separator); + sb.append(to.x); + sb.append(separator); + sb.append(to.y); + appendSpecificCSVInfo(sb, separator); + return sb.toString(); + } + /** + * Appends subclass-specific columns to the CSV content given by {@code sb}. + * @param sb - String builder composing the CSV information + * @param separator - column separator to be used + */ + protected void appendSpecificCSVInfo(StringBuilder sb, String separator) + { + } + // END KGU#685 2020-12-11 + + // START KGU#685 2020-12-13: Enh. #704 + /** @return the start point of this element */ + public Point getFrom() + { + return from; + } + + /** @return the end point of this element */ + public Point getTo() + { + return to; + } + + /** @return the end color of this element */ + public Color getColor() + { + return color; + } + // END KGU#685 2020-12-13 + + // START KGU#685 2020-12-14: Enh. #704 + /** + * Moves by the given {@code shift} + * @param shift - width and height specify the horizontal and vertical + * offset to move the element by + */ + public void move(Dimension shift) { + this.from.x += shift.width; this.from.y += shift.height; + this.to.x += shift.width; this.to.y += shift.height; + } + // END KGU#685 2020-12-14 + } diff --git a/src/lu/fisch/turtle/elements/Line.java b/src/lu/fisch/turtle/elements/Line.java index 3be76838..4ce2d8ba 100644 --- a/src/lu/fisch/turtle/elements/Line.java +++ b/src/lu/fisch/turtle/elements/Line.java @@ -19,9 +19,31 @@ package lu.fisch.turtle.elements; +/****************************************************************************************************** + * + * Author: Robert Fisch + * + * Description: Line - a visible line in the Turtle graphics window + * + ****************************************************************************************************** + * + * Revision List + * + * Author Date Description + * ------ ---- ----------- + * Kay Gürtzig 2020-12-11 Enh. #704 API extension: draw(Graphics2D, Rectangle), getBounds() + * appendSpecificCSVInfo(StringBuilder, String) + * + ****************************************************************************************************** + * + * Comment: + * + ******************************************************************************************************/// + import java.awt.Color; import java.awt.Graphics2D; import java.awt.Point; +import java.awt.Rectangle; /** * @@ -47,4 +69,37 @@ public void draw(Graphics2D graphics) graphics.drawLine(from.x, from.y, to.x, to.y); } + // START KGU#685 2020-12-11: Enh. #704 + @Override + public void draw(Graphics2D graphics, Rectangle viewRect) + { + graphics.setColor(color); + if (viewRect == null || viewRect.intersects(getBounds())) { + graphics.drawLine(from.x, from.y, to.x, to.y); + } + } + + protected void appendSpecificCSVInfo(StringBuilder sb, String separator) + { + sb.append(separator); + sb.append(Integer.toHexString(color.getRGB())); + } + + /** + * @return the bounding box of this line, ensuring that no dimensions is 0 + */ + public Rectangle getBounds() + { + Rectangle bounds = new Rectangle(from); + bounds.add(to); + // We must avoid "empty" rectangles for intersection tests + if (bounds.height == 0) { + bounds.height = 1; + } + if (bounds.width == 0) { + bounds.width = 1; + } + return bounds; + } + // END KGU#685 2020-12-11 } diff --git a/src/lu/fisch/turtle/elements/Move.java b/src/lu/fisch/turtle/elements/Move.java index b51139cb..783bd6aa 100644 --- a/src/lu/fisch/turtle/elements/Move.java +++ b/src/lu/fisch/turtle/elements/Move.java @@ -22,6 +22,7 @@ import java.awt.Graphics2D; import java.awt.Point; +import java.awt.Rectangle; /** * @@ -40,4 +41,11 @@ public void draw(Graphics2D graphics) { } + // START KGU#685 2020-12-11: Enh. #704 + @Override + public void draw(Graphics2D graphics, Rectangle viewRect) + { + } + // END KGU#685 2020-12-11 + } diff --git a/src/lu/fisch/turtle/io/CSVFilter.java b/src/lu/fisch/turtle/io/CSVFilter.java new file mode 100644 index 00000000..c9874057 --- /dev/null +++ b/src/lu/fisch/turtle/io/CSVFilter.java @@ -0,0 +1,60 @@ +/* + TurtleBox + A module providing a simple turtle graphics for Java + + Copyright (C) 2009, 2020 Bob Fisch + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or any + later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + */ + +package lu.fisch.turtle.io; + +/****************************************************************************************************** + * + * Author: Kay Gürtzig + * + * Description: File filer for CSV files + * + ****************************************************************************************************** + * + * Revision List + * + * Author Date Description + * ------ ---- ----------- + * Kay Gürtzig 2020-12-13 First Issue for TurtleBox + * + ****************************************************************************************************** + * + * Comment: + * + * + ******************************************************************************************************/// + +/** + * File filter for text files according to the CSV paradigm + * @author Kay Gürtzig + */ +public class CSVFilter extends ExtFileFilter { + + @Override + public String[] getAcceptedExtensions() { + return new String[] {"csv", "txt"}; + } + + @Override + public String getDescription() { + return "Comma-separated values (text) files"; + } + +} diff --git a/src/lu/fisch/turtle/io/ExtFileFilter.java b/src/lu/fisch/turtle/io/ExtFileFilter.java new file mode 100644 index 00000000..60283248 --- /dev/null +++ b/src/lu/fisch/turtle/io/ExtFileFilter.java @@ -0,0 +1,110 @@ +/* + TurtleBox + A module providing a simple turtle graphics for Java + + Copyright (C) 2009, 2020 Bob Fisch + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or any + later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + */ + +package lu.fisch.turtle.io; + +/****************************************************************************************************** + * + * Author: Kay Gürtzig + * + * Description: Abstract extended FileFilter class with file extension support + * + ****************************************************************************************************** + * + * Revision List + * + * Author Date Description + * ------ ---- ----------- + * Kay Gürtzig 2020-12-13 First Issue + * + ****************************************************************************************************** + * + * Comment: + * getExtension() ensures a non-null return value (in case of a missing extension, + * an empty string will be returned) + * + ******************************************************************************************************/// + +import java.io.File; +import javax.swing.filechooser.FileFilter; + +/** + * This is an extended abstract subclass of {@link javax.swing.filechooser.FileFilter} + * providing two basic static methods for file name extension extraction and implementing + * default version of {@link #accept(File)} based on the extensions proposed by the new + * abstract method {@link #getAcceptedExtensions()}. + * @author Kay Gürtzig + */ +public abstract class ExtFileFilter extends FileFilter { + + @Override + public boolean accept(File f) { + if (f.isDirectory()) + { + return true; + } + String ext = getExtension(f); + String[] accExts = getAcceptedExtensions(); + for (int i = 0; i < accExts.length; i++) { + if (ext.equals(accExts[i])) { + return true; + } + } + return false; + } + + /** + * Extracts the file name extension of the given file path {@code s} + * @param s - a file path as string + * @return the extension as string or {@code ""} if there isn't any. + * @see #getExtension(File) + */ + public static String getExtension(String s) + { + String ext = ""; + // We must face cases where entire paths might be passed in + s = (new File(s)).getName(); + int i = s.lastIndexOf('.'); + + if (i > 0) + { + ext = s.substring(i+1).toLowerCase(); + } + return ext; + } + + /** + * Extracts the file name extension of the given file {@code f} + * @param f - a {@link File} object + * @return the extension as string or {@code ""} if there isn't any. + * @see #getExtension(String) + */ + public static String getExtension(File f) + { + return getExtension(f.getName()); + } + + /** + * @return an array of accepted file name extensions (without dot and + * preferably in lower case) + */ + public abstract String[] getAcceptedExtensions(); + +} diff --git a/src/lu/fisch/turtle/io/PNGFilter.java b/src/lu/fisch/turtle/io/PNGFilter.java new file mode 100644 index 00000000..a9b7ef49 --- /dev/null +++ b/src/lu/fisch/turtle/io/PNGFilter.java @@ -0,0 +1,60 @@ +/* + TurtleBox + A module providing a simple turtle graphics for Java + + Copyright (C) 2009, 2020 Bob Fisch + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or any + later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + */ + +package lu.fisch.turtle.io; + +/****************************************************************************************************** + * + * Author: Kay Gürtzig + * + * Description: File filter for image files of PNG type. + * + ****************************************************************************************************** + * + * Revision List + * + * Author Date Description + * ------ ---- ----------- + * Kay Gürtzig 2020-12-13 First Issue for TurtleBox + * + ****************************************************************************************************** + * + * Comment: + * Inspired by lu.fisch.io.PNGFilter + * + ******************************************************************************************************/// + +/** + * File filter for image files of PNG type + * @author Kay Gürtzig + */ +public class PNGFilter extends ExtFileFilter { + + @Override + public String getDescription() { + return "PNG files"; + } + + @Override + public String[] getAcceptedExtensions() { + return new String[] {"png"}; + } + +} diff --git a/src/lu/fisch/turtle/io/SVGFilter.java b/src/lu/fisch/turtle/io/SVGFilter.java new file mode 100644 index 00000000..5820736d --- /dev/null +++ b/src/lu/fisch/turtle/io/SVGFilter.java @@ -0,0 +1,60 @@ +/* + TurtleBox + A module providing a simple turtle graphics for Java + + Copyright (C) 2009, 2020 Bob Fisch + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or any + later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + */ + +package lu.fisch.turtle.io; + +/****************************************************************************************************** +* +* Author: Kay Gürtzig +* +* Description: File filter for XML files of SVG type. +* +****************************************************************************************************** +* +* Revision List +* +* Author Date Description +* ------ ---- ----------- +* Kay Gürtzig 2020-12-13 First Issue for TurtleBox +* +****************************************************************************************************** +* +* Comment: +* Inspired by lu.fisch.io.PNGFilter +* +******************************************************************************************************/// + +/** +* File filter for image files of SVG type +* @author Kay Gürtzig +*/ +public class SVGFilter extends ExtFileFilter { + + @Override + public String[] getAcceptedExtensions() { + return new String[] {"svg"}; + } + + @Override + public String getDescription() { + return "SVG files"; + } + +}