├── .gitattributes ├── jitpack.yml ├── .gitignore ├── preview-for-github.png ├── tutorials └── load-an-image.png ├── .mvn └── wrapper │ ├── maven-wrapper.jar │ └── maven-wrapper.properties ├── src ├── main │ ├── resources │ │ ├── META-INF │ │ │ ├── MANIFEST.MF │ │ │ └── services │ │ │ │ ├── com.marginallyclever.nodegraphcore.DAORegistry │ │ │ │ └── com.marginallyclever.nodegraphcore.NodeRegistry │ │ ├── com │ │ │ └── marginallyclever │ │ │ │ └── donatello │ │ │ │ ├── icons8-add-16.png │ │ │ │ ├── icons8-bug-16.png │ │ │ │ ├── icons8-copy-16.png │ │ │ │ ├── icons8-cut-16.png │ │ │ │ ├── icons8-edit-16.png │ │ │ │ ├── icons8-end-16.png │ │ │ │ ├── icons8-fold-16.png │ │ │ │ ├── icons8-load-16.png │ │ │ │ ├── icons8-move-16.png │ │ │ │ ├── icons8-new-16.png │ │ │ │ ├── icons8-path-16.png │ │ │ │ ├── icons8-pin-16.png │ │ │ │ ├── icons8-play-16.png │ │ │ │ ├── icons8-plug-16.png │ │ │ │ ├── icons8-redo-16.png │ │ │ │ ├── icons8-save-16.png │ │ │ │ ├── icons8-undo-16.png │ │ │ │ ├── icons8-blank-16.png │ │ │ │ ├── icons8-delete-16.png │ │ │ │ ├── icons8-expand-16.png │ │ │ │ ├── icons8-invert-16.png │ │ │ │ ├── icons8-paste-16.png │ │ │ │ ├── icons8-pause-16.png │ │ │ │ ├── icons8-print-16.png │ │ │ │ ├── icons8-reset-16.png │ │ │ │ ├── icons8-rewind-16.png │ │ │ │ ├── icons8-search-16.png │ │ │ │ ├── icons8-unfold-16.png │ │ │ │ ├── icons8-update-16.png │ │ │ │ ├── icons8-cocktail-16.png │ │ │ │ ├── icons8-collapse-16.png │ │ │ │ ├── icons8-discord-16.png │ │ │ │ ├── icons8-hamburger-16.png │ │ │ │ ├── icons8-handshake-16.png │ │ │ │ ├── icons8-install-16.png │ │ │ │ ├── icons8-lightning-16.png │ │ │ │ ├── icons8-newspaper-16.png │ │ │ │ ├── icons8-rectangle-16.png │ │ │ │ ├── icons8-separate-16.png │ │ │ │ ├── icons8-settings-16.png │ │ │ │ ├── icons8-sorting-16.png │ │ │ │ ├── icons8-telephone-16.png │ │ │ │ ├── icons8-light-bulb-16.png │ │ │ │ ├── icons8-select-all-16.png │ │ │ │ ├── icons8-zoom-to-fit-16.png │ │ │ │ ├── icons8-square-ruler-16.png │ │ │ │ ├── icons8-thought-bubble-16.png │ │ │ │ └── nodes │ │ │ │ ├── color │ │ │ │ ├── icons8-cmyk-48.png │ │ │ │ ├── icons8-rgb-48.png │ │ │ │ └── icons8-color-48.png │ │ │ │ ├── icons8-calculate-48.png │ │ │ │ └── images │ │ │ │ ├── icons8-blur-48.png │ │ │ │ ├── icons8-image-48.png │ │ │ │ ├── icons8-print-48.png │ │ │ │ ├── icons8-resize-48.png │ │ │ │ ├── icons8-eye-dropper-48.png │ │ │ │ └── icons8-invert-colors-48.png │ │ └── logback.xml │ ├── java │ │ ├── com │ │ │ └── marginallyclever │ │ │ │ └── donatello │ │ │ │ ├── select │ │ │ │ ├── SelectListener.java │ │ │ │ ├── BackgroundPaintedButton.java │ │ │ │ ├── SelectEvent.java │ │ │ │ ├── SelectPanel.java │ │ │ │ ├── SelectSpinner.java │ │ │ │ ├── SelectToggleButton.java │ │ │ │ ├── Select.java │ │ │ │ ├── SelectBoolean.java │ │ │ │ ├── SelectSlider.java │ │ │ │ ├── SelectColor.java │ │ │ │ ├── SelectButton.java │ │ │ │ ├── SelectPassword.java │ │ │ │ ├── DelayedDocumentValidator.java │ │ │ │ ├── SelectTextField.java │ │ │ │ ├── SelectOneOfMany.java │ │ │ │ ├── SelectInteger.java │ │ │ │ └── SelectTextArea.java │ │ │ │ ├── ports │ │ │ │ ├── OutputInt.java │ │ │ │ ├── OutputColor.java │ │ │ │ ├── OutputDouble.java │ │ │ │ ├── OutputNumber.java │ │ │ │ ├── Filename.java │ │ │ │ ├── InputImage.java │ │ │ │ ├── FilenameDAO4JSON.java │ │ │ │ ├── InputBoolean.java │ │ │ │ ├── InputString.java │ │ │ │ ├── InputNumber.java │ │ │ │ ├── InputColor.java │ │ │ │ ├── InputInt.java │ │ │ │ ├── InputDouble.java │ │ │ │ ├── OutputImage.java │ │ │ │ ├── InputRange.java │ │ │ │ ├── InputOneOfMany.java │ │ │ │ └── InputFilename.java │ │ │ │ ├── nodefactorypanel │ │ │ │ ├── AddNodeListener.java │ │ │ │ └── FactoryCategoryCellRenderer.java │ │ │ │ ├── UpdateClockListener.java │ │ │ │ ├── SwingProvider.java │ │ │ │ ├── curveeditor │ │ │ │ └── CurveChangedListener.java │ │ │ │ ├── UnifiedUndoManager.java │ │ │ │ ├── DonatelloSelectionListener.java │ │ │ │ ├── graphview │ │ │ │ ├── GraphViewProvider.java │ │ │ │ └── GraphViewListener.java │ │ │ │ ├── edits │ │ │ │ ├── SignificantUndoableEdit.java │ │ │ │ ├── ConnectionRemoveEdit.java │ │ │ │ ├── MoveNodesEdit.java │ │ │ │ ├── NodeAddEdit.java │ │ │ │ ├── GraphIsolateEdit.java │ │ │ │ ├── ReorderEdit.java │ │ │ │ ├── GraphPasteEdit.java │ │ │ │ └── ConnectionAddEdit.java │ │ │ │ ├── package-info.java │ │ │ │ ├── actions │ │ │ │ ├── EditorAction.java │ │ │ │ ├── SwapToolsAction.java │ │ │ │ ├── GraphOrganizeAction.java │ │ │ │ ├── GraphNewAction.java │ │ │ │ ├── SelectAllAction.java │ │ │ │ ├── GraphUpdateAction.java │ │ │ │ ├── SelectionInvertAction.java │ │ │ │ ├── RedoAction.java │ │ │ │ ├── UndoAction.java │ │ │ │ ├── BrowseURLAction.java │ │ │ │ ├── undoable │ │ │ │ │ ├── NodeDeleteAction.java │ │ │ │ │ ├── NodeIsolateAction.java │ │ │ │ │ ├── NodeCutAction.java │ │ │ │ │ ├── NodePasteAction.java │ │ │ │ │ └── NodeAddAction.java │ │ │ │ ├── ForciblyUpdateNodesAction.java │ │ │ │ ├── ZoomToFitSelectedAction.java │ │ │ │ ├── NodeEditAction.java │ │ │ │ ├── SelectionGrowAction.java │ │ │ │ ├── PlayAction.java │ │ │ │ ├── GraphFoldAction.java │ │ │ │ ├── NodeCopyAction.java │ │ │ │ ├── GraphPrintAction.java │ │ │ │ ├── GraphStraightenAction.java │ │ │ │ └── SelectionShrinkAction.java │ │ │ │ ├── KeyStateMemory.java │ │ │ │ ├── nodes │ │ │ │ ├── TimeInSeconds.java │ │ │ │ ├── color │ │ │ │ │ ├── ColorDAO4JSON.java │ │ │ │ │ ├── LoadColor.java │ │ │ │ │ ├── ColorToRGBA.java │ │ │ │ │ └── ColorToCMYK.java │ │ │ │ └── images │ │ │ │ │ ├── BufferedImageDAO4JSON.java │ │ │ │ │ ├── PrintImage.java │ │ │ │ │ ├── ColorHelper.java │ │ │ │ │ ├── Invert.java │ │ │ │ │ └── ScaleImage.java │ │ │ │ ├── RollOncePerSessionTriggeringPolicy.java │ │ │ │ ├── organize │ │ │ │ ├── NodeListCellRenderer.java │ │ │ │ └── TransferableNode.java │ │ │ │ ├── NumberFormatHelper.java │ │ │ │ ├── UndoHandler.java │ │ │ │ ├── contextsensitivetools │ │ │ │ ├── RememberKeyStateAction.java │ │ │ │ └── ContextSensitiveTool.java │ │ │ │ ├── IconHelper.java │ │ │ │ ├── UnicodeIcon.java │ │ │ │ ├── simpleparser │ │ │ │ └── SimpleParser.java │ │ │ │ ├── UpdateClock.java │ │ │ │ ├── DonatelloRegistry.java │ │ │ │ ├── PropertiesHelper.java │ │ │ │ ├── NodeHelper.java │ │ │ │ └── FileHelper.java │ │ └── module-info.java │ └── assembly │ │ └── application.xml └── test │ ├── resources │ └── com │ │ └── marginallyclever │ │ └── donatello │ │ ├── test.png │ │ ├── david2.jpg │ │ ├── bill-murray.jpg │ │ ├── mona-lisa-full.jpg │ │ └── logback-test.xml │ └── java │ └── com │ └── marginallyclever │ └── donatello │ ├── EditNodePanelTest.java │ ├── select │ ├── SelectIntegerTest.java │ └── SelectDoubleTest.java │ ├── port │ ├── InputIntTest.java │ └── InputDoubleTest.java │ ├── simpleparser │ └── SimpleParserTest.java │ ├── nodes │ └── CalculateTest.java │ ├── TestDAO4JSON.java │ ├── actions │ └── ActionRegistryTest.java │ ├── TestGraphSwing.java │ ├── ClassLoadingTest.java │ └── GraphViewSettingsTest.java ├── graphViewSettings.json ├── .github ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md └── workflows │ ├── updater.yaml │ ├── main.yml │ └── publish-javadoc.yml ├── CONTRIBUTING.md └── README.md /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /jitpack.yml: -------------------------------------------------------------------------------- 1 | install: 2 | - mvn wrapper:wrapper -Dmaven=3.8.1 3 | - ./mvnw install -DskipTests -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.class 2 | target/* 3 | .idea 4 | GraphCore.iml 5 | output.svg 6 | saved.png 7 | log.txt 8 | -------------------------------------------------------------------------------- /preview-for-github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarginallyClever/Donatello/HEAD/preview-for-github.png -------------------------------------------------------------------------------- /tutorials/load-an-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarginallyClever/Donatello/HEAD/tutorials/load-an-image.png -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarginallyClever/Donatello/HEAD/.mvn/wrapper/maven-wrapper.jar -------------------------------------------------------------------------------- /src/main/resources/META-INF/MANIFEST.MF: -------------------------------------------------------------------------------- 1 | Manifest-Version: 1.0 2 | Main-Class: com.marginallyclever.donatello.Donatello 3 | 4 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/services/com.marginallyclever.nodegraphcore.DAORegistry: -------------------------------------------------------------------------------- 1 | com.marginallyclever.donatello.DonatelloRegistry -------------------------------------------------------------------------------- /src/main/resources/META-INF/services/com.marginallyclever.nodegraphcore.NodeRegistry: -------------------------------------------------------------------------------- 1 | com.marginallyclever.donatello.DonatelloRegistry -------------------------------------------------------------------------------- /src/test/resources/com/marginallyclever/donatello/test.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarginallyClever/Donatello/HEAD/src/test/resources/com/marginallyclever/donatello/test.png -------------------------------------------------------------------------------- /src/test/resources/com/marginallyclever/donatello/david2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarginallyClever/Donatello/HEAD/src/test/resources/com/marginallyclever/donatello/david2.jpg -------------------------------------------------------------------------------- /src/test/resources/com/marginallyclever/donatello/bill-murray.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarginallyClever/Donatello/HEAD/src/test/resources/com/marginallyclever/donatello/bill-murray.jpg -------------------------------------------------------------------------------- /src/main/resources/com/marginallyclever/donatello/icons8-add-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarginallyClever/Donatello/HEAD/src/main/resources/com/marginallyclever/donatello/icons8-add-16.png -------------------------------------------------------------------------------- /src/main/resources/com/marginallyclever/donatello/icons8-bug-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarginallyClever/Donatello/HEAD/src/main/resources/com/marginallyclever/donatello/icons8-bug-16.png -------------------------------------------------------------------------------- /src/main/resources/com/marginallyclever/donatello/icons8-copy-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarginallyClever/Donatello/HEAD/src/main/resources/com/marginallyclever/donatello/icons8-copy-16.png -------------------------------------------------------------------------------- /src/main/resources/com/marginallyclever/donatello/icons8-cut-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarginallyClever/Donatello/HEAD/src/main/resources/com/marginallyclever/donatello/icons8-cut-16.png -------------------------------------------------------------------------------- /src/main/resources/com/marginallyclever/donatello/icons8-edit-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarginallyClever/Donatello/HEAD/src/main/resources/com/marginallyclever/donatello/icons8-edit-16.png -------------------------------------------------------------------------------- /src/main/resources/com/marginallyclever/donatello/icons8-end-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarginallyClever/Donatello/HEAD/src/main/resources/com/marginallyclever/donatello/icons8-end-16.png -------------------------------------------------------------------------------- /src/main/resources/com/marginallyclever/donatello/icons8-fold-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarginallyClever/Donatello/HEAD/src/main/resources/com/marginallyclever/donatello/icons8-fold-16.png -------------------------------------------------------------------------------- /src/main/resources/com/marginallyclever/donatello/icons8-load-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarginallyClever/Donatello/HEAD/src/main/resources/com/marginallyclever/donatello/icons8-load-16.png -------------------------------------------------------------------------------- /src/main/resources/com/marginallyclever/donatello/icons8-move-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarginallyClever/Donatello/HEAD/src/main/resources/com/marginallyclever/donatello/icons8-move-16.png -------------------------------------------------------------------------------- /src/main/resources/com/marginallyclever/donatello/icons8-new-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarginallyClever/Donatello/HEAD/src/main/resources/com/marginallyclever/donatello/icons8-new-16.png -------------------------------------------------------------------------------- /src/main/resources/com/marginallyclever/donatello/icons8-path-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarginallyClever/Donatello/HEAD/src/main/resources/com/marginallyclever/donatello/icons8-path-16.png -------------------------------------------------------------------------------- /src/main/resources/com/marginallyclever/donatello/icons8-pin-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarginallyClever/Donatello/HEAD/src/main/resources/com/marginallyclever/donatello/icons8-pin-16.png -------------------------------------------------------------------------------- /src/main/resources/com/marginallyclever/donatello/icons8-play-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarginallyClever/Donatello/HEAD/src/main/resources/com/marginallyclever/donatello/icons8-play-16.png -------------------------------------------------------------------------------- /src/main/resources/com/marginallyclever/donatello/icons8-plug-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarginallyClever/Donatello/HEAD/src/main/resources/com/marginallyclever/donatello/icons8-plug-16.png -------------------------------------------------------------------------------- /src/main/resources/com/marginallyclever/donatello/icons8-redo-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarginallyClever/Donatello/HEAD/src/main/resources/com/marginallyclever/donatello/icons8-redo-16.png -------------------------------------------------------------------------------- /src/main/resources/com/marginallyclever/donatello/icons8-save-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarginallyClever/Donatello/HEAD/src/main/resources/com/marginallyclever/donatello/icons8-save-16.png -------------------------------------------------------------------------------- /src/main/resources/com/marginallyclever/donatello/icons8-undo-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarginallyClever/Donatello/HEAD/src/main/resources/com/marginallyclever/donatello/icons8-undo-16.png -------------------------------------------------------------------------------- /src/test/resources/com/marginallyclever/donatello/mona-lisa-full.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarginallyClever/Donatello/HEAD/src/test/resources/com/marginallyclever/donatello/mona-lisa-full.jpg -------------------------------------------------------------------------------- /src/main/resources/com/marginallyclever/donatello/icons8-blank-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarginallyClever/Donatello/HEAD/src/main/resources/com/marginallyclever/donatello/icons8-blank-16.png -------------------------------------------------------------------------------- /src/main/resources/com/marginallyclever/donatello/icons8-delete-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarginallyClever/Donatello/HEAD/src/main/resources/com/marginallyclever/donatello/icons8-delete-16.png -------------------------------------------------------------------------------- /src/main/resources/com/marginallyclever/donatello/icons8-expand-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarginallyClever/Donatello/HEAD/src/main/resources/com/marginallyclever/donatello/icons8-expand-16.png -------------------------------------------------------------------------------- /src/main/resources/com/marginallyclever/donatello/icons8-invert-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarginallyClever/Donatello/HEAD/src/main/resources/com/marginallyclever/donatello/icons8-invert-16.png -------------------------------------------------------------------------------- /src/main/resources/com/marginallyclever/donatello/icons8-paste-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarginallyClever/Donatello/HEAD/src/main/resources/com/marginallyclever/donatello/icons8-paste-16.png -------------------------------------------------------------------------------- /src/main/resources/com/marginallyclever/donatello/icons8-pause-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarginallyClever/Donatello/HEAD/src/main/resources/com/marginallyclever/donatello/icons8-pause-16.png -------------------------------------------------------------------------------- /src/main/resources/com/marginallyclever/donatello/icons8-print-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarginallyClever/Donatello/HEAD/src/main/resources/com/marginallyclever/donatello/icons8-print-16.png -------------------------------------------------------------------------------- /src/main/resources/com/marginallyclever/donatello/icons8-reset-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarginallyClever/Donatello/HEAD/src/main/resources/com/marginallyclever/donatello/icons8-reset-16.png -------------------------------------------------------------------------------- /src/main/resources/com/marginallyclever/donatello/icons8-rewind-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarginallyClever/Donatello/HEAD/src/main/resources/com/marginallyclever/donatello/icons8-rewind-16.png -------------------------------------------------------------------------------- /src/main/resources/com/marginallyclever/donatello/icons8-search-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarginallyClever/Donatello/HEAD/src/main/resources/com/marginallyclever/donatello/icons8-search-16.png -------------------------------------------------------------------------------- /src/main/resources/com/marginallyclever/donatello/icons8-unfold-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarginallyClever/Donatello/HEAD/src/main/resources/com/marginallyclever/donatello/icons8-unfold-16.png -------------------------------------------------------------------------------- /src/main/resources/com/marginallyclever/donatello/icons8-update-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarginallyClever/Donatello/HEAD/src/main/resources/com/marginallyclever/donatello/icons8-update-16.png -------------------------------------------------------------------------------- /src/main/resources/com/marginallyclever/donatello/icons8-cocktail-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarginallyClever/Donatello/HEAD/src/main/resources/com/marginallyclever/donatello/icons8-cocktail-16.png -------------------------------------------------------------------------------- /src/main/resources/com/marginallyclever/donatello/icons8-collapse-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarginallyClever/Donatello/HEAD/src/main/resources/com/marginallyclever/donatello/icons8-collapse-16.png -------------------------------------------------------------------------------- /src/main/resources/com/marginallyclever/donatello/icons8-discord-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarginallyClever/Donatello/HEAD/src/main/resources/com/marginallyclever/donatello/icons8-discord-16.png -------------------------------------------------------------------------------- /src/main/resources/com/marginallyclever/donatello/icons8-hamburger-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarginallyClever/Donatello/HEAD/src/main/resources/com/marginallyclever/donatello/icons8-hamburger-16.png -------------------------------------------------------------------------------- /src/main/resources/com/marginallyclever/donatello/icons8-handshake-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarginallyClever/Donatello/HEAD/src/main/resources/com/marginallyclever/donatello/icons8-handshake-16.png -------------------------------------------------------------------------------- /src/main/resources/com/marginallyclever/donatello/icons8-install-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarginallyClever/Donatello/HEAD/src/main/resources/com/marginallyclever/donatello/icons8-install-16.png -------------------------------------------------------------------------------- /src/main/resources/com/marginallyclever/donatello/icons8-lightning-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarginallyClever/Donatello/HEAD/src/main/resources/com/marginallyclever/donatello/icons8-lightning-16.png -------------------------------------------------------------------------------- /src/main/resources/com/marginallyclever/donatello/icons8-newspaper-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarginallyClever/Donatello/HEAD/src/main/resources/com/marginallyclever/donatello/icons8-newspaper-16.png -------------------------------------------------------------------------------- /src/main/resources/com/marginallyclever/donatello/icons8-rectangle-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarginallyClever/Donatello/HEAD/src/main/resources/com/marginallyclever/donatello/icons8-rectangle-16.png -------------------------------------------------------------------------------- /src/main/resources/com/marginallyclever/donatello/icons8-separate-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarginallyClever/Donatello/HEAD/src/main/resources/com/marginallyclever/donatello/icons8-separate-16.png -------------------------------------------------------------------------------- /src/main/resources/com/marginallyclever/donatello/icons8-settings-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarginallyClever/Donatello/HEAD/src/main/resources/com/marginallyclever/donatello/icons8-settings-16.png -------------------------------------------------------------------------------- /src/main/resources/com/marginallyclever/donatello/icons8-sorting-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarginallyClever/Donatello/HEAD/src/main/resources/com/marginallyclever/donatello/icons8-sorting-16.png -------------------------------------------------------------------------------- /src/main/resources/com/marginallyclever/donatello/icons8-telephone-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarginallyClever/Donatello/HEAD/src/main/resources/com/marginallyclever/donatello/icons8-telephone-16.png -------------------------------------------------------------------------------- /src/main/resources/com/marginallyclever/donatello/icons8-light-bulb-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarginallyClever/Donatello/HEAD/src/main/resources/com/marginallyclever/donatello/icons8-light-bulb-16.png -------------------------------------------------------------------------------- /src/main/resources/com/marginallyclever/donatello/icons8-select-all-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarginallyClever/Donatello/HEAD/src/main/resources/com/marginallyclever/donatello/icons8-select-all-16.png -------------------------------------------------------------------------------- /src/main/resources/com/marginallyclever/donatello/icons8-zoom-to-fit-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarginallyClever/Donatello/HEAD/src/main/resources/com/marginallyclever/donatello/icons8-zoom-to-fit-16.png -------------------------------------------------------------------------------- /src/main/resources/com/marginallyclever/donatello/icons8-square-ruler-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarginallyClever/Donatello/HEAD/src/main/resources/com/marginallyclever/donatello/icons8-square-ruler-16.png -------------------------------------------------------------------------------- /src/main/resources/com/marginallyclever/donatello/icons8-thought-bubble-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarginallyClever/Donatello/HEAD/src/main/resources/com/marginallyclever/donatello/icons8-thought-bubble-16.png -------------------------------------------------------------------------------- /src/main/resources/com/marginallyclever/donatello/nodes/color/icons8-cmyk-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarginallyClever/Donatello/HEAD/src/main/resources/com/marginallyclever/donatello/nodes/color/icons8-cmyk-48.png -------------------------------------------------------------------------------- /src/main/resources/com/marginallyclever/donatello/nodes/color/icons8-rgb-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarginallyClever/Donatello/HEAD/src/main/resources/com/marginallyclever/donatello/nodes/color/icons8-rgb-48.png -------------------------------------------------------------------------------- /src/main/resources/com/marginallyclever/donatello/nodes/icons8-calculate-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarginallyClever/Donatello/HEAD/src/main/resources/com/marginallyclever/donatello/nodes/icons8-calculate-48.png -------------------------------------------------------------------------------- /src/main/resources/com/marginallyclever/donatello/nodes/color/icons8-color-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarginallyClever/Donatello/HEAD/src/main/resources/com/marginallyclever/donatello/nodes/color/icons8-color-48.png -------------------------------------------------------------------------------- /src/main/resources/com/marginallyclever/donatello/nodes/images/icons8-blur-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarginallyClever/Donatello/HEAD/src/main/resources/com/marginallyclever/donatello/nodes/images/icons8-blur-48.png -------------------------------------------------------------------------------- /src/main/resources/com/marginallyclever/donatello/nodes/images/icons8-image-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarginallyClever/Donatello/HEAD/src/main/resources/com/marginallyclever/donatello/nodes/images/icons8-image-48.png -------------------------------------------------------------------------------- /src/main/resources/com/marginallyclever/donatello/nodes/images/icons8-print-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarginallyClever/Donatello/HEAD/src/main/resources/com/marginallyclever/donatello/nodes/images/icons8-print-48.png -------------------------------------------------------------------------------- /src/main/resources/com/marginallyclever/donatello/nodes/images/icons8-resize-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarginallyClever/Donatello/HEAD/src/main/resources/com/marginallyclever/donatello/nodes/images/icons8-resize-48.png -------------------------------------------------------------------------------- /src/main/resources/com/marginallyclever/donatello/nodes/images/icons8-eye-dropper-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarginallyClever/Donatello/HEAD/src/main/resources/com/marginallyclever/donatello/nodes/images/icons8-eye-dropper-48.png -------------------------------------------------------------------------------- /src/main/resources/com/marginallyclever/donatello/nodes/images/icons8-invert-colors-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarginallyClever/Donatello/HEAD/src/main/resources/com/marginallyclever/donatello/nodes/images/icons8-invert-colors-48.png -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.1/apache-maven-3.8.1-bin.zip 2 | wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar 3 | -------------------------------------------------------------------------------- /src/main/java/com/marginallyclever/donatello/select/SelectListener.java: -------------------------------------------------------------------------------- 1 | package com.marginallyclever.donatello.select; 2 | 3 | import java.util.EventListener; 4 | 5 | /** 6 | * A SelectEvent is fired when a Select changes value. 7 | * @author Dan Royer 8 | * @since 7.50.2 9 | */ 10 | public interface SelectListener extends EventListener { 11 | void selectEvent(SelectEvent evt); 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/com/marginallyclever/donatello/ports/OutputInt.java: -------------------------------------------------------------------------------- 1 | package com.marginallyclever.donatello.ports; 2 | 3 | import com.marginallyclever.nodegraphcore.port.Output; 4 | 5 | public class OutputInt extends Output { 6 | public OutputInt(String _name, Integer startingValue) throws IllegalArgumentException { 7 | super(_name, Integer.class, startingValue); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/com/marginallyclever/donatello/nodefactorypanel/AddNodeListener.java: -------------------------------------------------------------------------------- 1 | package com.marginallyclever.donatello.nodefactorypanel; 2 | 3 | import com.marginallyclever.nodegraphcore.Node; 4 | 5 | import java.util.EventListener; 6 | 7 | /** 8 | * For listening to the {@link NodeFactoryPanel}. 9 | */ 10 | public interface AddNodeListener extends EventListener { 11 | void addNode(Node node); 12 | } 13 | -------------------------------------------------------------------------------- /graphViewSettings.json: -------------------------------------------------------------------------------- 1 | {"connectionPointColor":-4144960,"drawCursor":true,"panelGridColor":-4934476,"nodeColorTitleFont":-1,"nodeColorBackground":-1,"nodeColorInternalBorder":-12566464,"panelColorBackground":-4144960,"drawOrigin":true,"nodeColorBorder":-16777216,"cornerRadius":10,"nodeColorFontClean":-16777216,"gridSize":20,"nodeColorTitleBackground":-16777216,"drawBackground":true,"nodeColorFontDirty":-65536,"connectionColor":-16776961} -------------------------------------------------------------------------------- /src/main/java/com/marginallyclever/donatello/ports/OutputColor.java: -------------------------------------------------------------------------------- 1 | package com.marginallyclever.donatello.ports; 2 | 3 | import com.marginallyclever.nodegraphcore.port.Output; 4 | 5 | import java.awt.*; 6 | 7 | public class OutputColor extends Output { 8 | public OutputColor(String _name, Color startingValue) throws IllegalArgumentException { 9 | super(_name, Color.class, startingValue); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/com/marginallyclever/donatello/UpdateClockListener.java: -------------------------------------------------------------------------------- 1 | package com.marginallyclever.donatello; 2 | 3 | /** 4 | * Classes which implement {@link UpdateClockListener} can subscribe to an {@link UpdateClock} to receive 5 | * {@link #updateClockEvent()} at the scheduled interval. 6 | * @author Dan Royer 7 | * @since 2022-03-19 8 | */ 9 | public interface UpdateClockListener { 10 | void updateClockEvent(); 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/com/marginallyclever/donatello/SwingProvider.java: -------------------------------------------------------------------------------- 1 | package com.marginallyclever.donatello; 2 | 3 | import com.marginallyclever.donatello.select.Select; 4 | 5 | import java.awt.*; 6 | 7 | /** 8 | * Interface for classes that provide a Swing component. 9 | */ 10 | public interface SwingProvider { 11 | /** 12 | * @return the Select component or null. 13 | */ 14 | Select getSwingComponent(Component parent); 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/marginallyclever/donatello/curveeditor/CurveChangedListener.java: -------------------------------------------------------------------------------- 1 | package com.marginallyclever.donatello.curveeditor; 2 | 3 | import java.util.EventListener; 4 | 5 | public interface CurveChangedListener extends EventListener { 6 | /** 7 | * Called when the curve has been changed. 8 | * @param curveEditor the curve editor that has changed. 9 | */ 10 | void curveChanged(CurveEditor curveEditor); 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/com/marginallyclever/donatello/UnifiedUndoManager.java: -------------------------------------------------------------------------------- 1 | package com.marginallyclever.donatello; 2 | 3 | import javax.swing.undo.*; 4 | 5 | /** 6 | * A singleton class that provides a single instance of {@link UndoManager} for the entire application. 7 | */ 8 | public class UnifiedUndoManager { 9 | private static final UndoManager undoManager = new UndoManager(); 10 | 11 | public static UndoManager getInstance() { 12 | return undoManager; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/com/marginallyclever/donatello/ports/OutputDouble.java: -------------------------------------------------------------------------------- 1 | package com.marginallyclever.donatello.ports; 2 | 3 | import com.marginallyclever.nodegraphcore.port.Output; 4 | 5 | public class OutputDouble extends Output { 6 | public OutputDouble(String name) { 7 | this(name, 0d); 8 | } 9 | 10 | public OutputDouble(String name, Double startingValue) throws IllegalArgumentException { 11 | super(name, Double.class, startingValue); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/marginallyclever/donatello/ports/OutputNumber.java: -------------------------------------------------------------------------------- 1 | package com.marginallyclever.donatello.ports; 2 | 3 | import com.marginallyclever.nodegraphcore.port.Output; 4 | 5 | public class OutputNumber extends Output { 6 | public OutputNumber(String name) { 7 | this(name, 0); 8 | } 9 | 10 | public OutputNumber(String name, Number startingValue) throws IllegalArgumentException { 11 | super(name, Number.class, startingValue); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/marginallyclever/donatello/DonatelloSelectionListener.java: -------------------------------------------------------------------------------- 1 | package com.marginallyclever.donatello; 2 | 3 | import java.util.EventListener; 4 | 5 | /** 6 | * Interface for listening to changes in the selected nodes of a {@link Donatello}. 7 | */ 8 | public interface DonatelloSelectionListener extends EventListener { 9 | /** 10 | * Called when the selection of a {@link Donatello} changes. 11 | * @param e the {@link Donatello} that changed. 12 | */ 13 | void selectionChanged(Donatello e); 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/com/marginallyclever/donatello/graphview/GraphViewProvider.java: -------------------------------------------------------------------------------- 1 | package com.marginallyclever.donatello.graphview; 2 | 3 | import java.awt.*; 4 | 5 | /** 6 | * Ports which implement this interface will provide custom displays in the graph view. 7 | */ 8 | public interface GraphViewProvider { 9 | /** 10 | * Display the port in the graph view. 11 | * @param g The graphics context to draw on. 12 | * @param box The bounding box to draw in. 13 | */ 14 | void paint(Graphics g, Rectangle box); 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/marginallyclever/donatello/edits/SignificantUndoableEdit.java: -------------------------------------------------------------------------------- 1 | package com.marginallyclever.donatello.edits; 2 | 3 | import javax.swing.undo.AbstractUndoableEdit; 4 | 5 | public abstract class SignificantUndoableEdit extends AbstractUndoableEdit { 6 | private boolean isSignificant = true; 7 | 8 | @Override 9 | public boolean isSignificant() { 10 | return isSignificant; 11 | } 12 | 13 | public void setSignificant(boolean significant) { 14 | isSignificant = significant; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/test/java/com/marginallyclever/donatello/EditNodePanelTest.java: -------------------------------------------------------------------------------- 1 | package com.marginallyclever.donatello; 2 | 3 | import com.marginallyclever.donatello.nodes.Calculate; 4 | import com.marginallyclever.nodegraphcore.Node; 5 | 6 | public class EditNodePanelTest { 7 | /** 8 | * main entry point. Good for independent test. 9 | * @param args command line arguments. 10 | */ 11 | public static void main(String[] args) { 12 | // a test case 13 | Node node = new Calculate(); 14 | EditNodePanel.runAsDialog(node,null,null); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/com/marginallyclever/donatello/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * donatello contains Swing-based {@link com.marginallyclever.nodegraphcore.Node}s and all Swing-based editing 3 | * tools for nodegraphcore elements. 4 | * 5 | * The main entry point is the {@link com.marginallyclever.donatello.Donatello}, which launches a GUI. 6 | * The Swing nodes are registered by calling {@link com.marginallyclever.donatello.DonatelloRegistry#registerNodes()}. 7 | * The Swing DAO are registered by calling {@link com.marginallyclever.donatello.DonatelloRegistry#registerDAO()}. 8 | */ 9 | package com.marginallyclever.donatello; -------------------------------------------------------------------------------- /src/main/java/com/marginallyclever/donatello/actions/EditorAction.java: -------------------------------------------------------------------------------- 1 | package com.marginallyclever.donatello.actions; 2 | 3 | import com.marginallyclever.donatello.Donatello; 4 | 5 | import javax.swing.*; 6 | 7 | /** 8 | * {@link AbstractAction}s implement this interface to update their own 9 | * {@link AbstractAction#setEnabled(boolean)} status. 10 | * @author Dan Royer 11 | * @since 2022-02-23 12 | */ 13 | public interface EditorAction { 14 | /** 15 | * Called by the {@link Donatello} when the editor believes it is time to confirm enable status. 16 | */ 17 | void updateEnableStatus(); 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/com/marginallyclever/donatello/graphview/GraphViewListener.java: -------------------------------------------------------------------------------- 1 | package com.marginallyclever.donatello.graphview; 2 | 3 | import java.awt.*; 4 | 5 | /** 6 | * Used by any class wanting to add decorations to a {@link GraphViewPanel}. 7 | * @author Dan Royer 8 | * @since 2022-02-11 9 | */ 10 | public interface GraphViewListener { 11 | /** 12 | * Called when the {@link GraphViewPanel} has completed painting itself. 13 | * Useful for then adding highlights and extra annotation. 14 | * @param g the graphics context used to paint the panel 15 | * @param panel the caller 16 | */ 17 | void paint(Graphics g, GraphViewPanel panel); 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/com/marginallyclever/donatello/KeyStateMemory.java: -------------------------------------------------------------------------------- 1 | package com.marginallyclever.donatello; 2 | 3 | import java.awt.event.KeyEvent; 4 | 5 | /** 6 | * Remembers the state of important keys. 7 | * @author Dan Royer 8 | * @since 2022-03-08 9 | */ 10 | public class KeyStateMemory { 11 | private boolean shiftKeyDown = false; 12 | 13 | public void keyPressed(int keyCode) { 14 | if(keyCode==KeyEvent.VK_SHIFT) shiftKeyDown = true; 15 | } 16 | 17 | public void keyReleased(int keyCode) { 18 | if(keyCode==KeyEvent.VK_SHIFT) shiftKeyDown = false; 19 | } 20 | 21 | public boolean isShiftKeyDown() { 22 | return shiftKeyDown; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/com/marginallyclever/donatello/ports/Filename.java: -------------------------------------------------------------------------------- 1 | package com.marginallyclever.donatello.ports; 2 | 3 | /** 4 | * This is container for a String. EditNodePanel looks for Filename and then knows to use a file chooser. 5 | */ 6 | public class Filename { 7 | private String value; 8 | 9 | public Filename(String value) { 10 | super(); 11 | set(value); 12 | } 13 | 14 | public Filename() {} 15 | 16 | public String get() { 17 | return value; 18 | } 19 | 20 | public void set(String value) { 21 | this.value = value; 22 | } 23 | 24 | @Override 25 | public String toString() { 26 | return value; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/updater.yaml: -------------------------------------------------------------------------------- 1 | name: GitHub Actions Version Updater 2 | 3 | # Controls when the action will run. 4 | on: 5 | schedule: 6 | # Automatically run on every Sunday 7 | - cron: '0 0 * * 0' 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v4 15 | with: 16 | # [Required] Access token with `workflow` scope. 17 | token: ${{ secrets.WORKFLOW_SECRET }} 18 | 19 | - name: Run GitHub Actions Version Updater 20 | uses: saadmk11/github-actions-version-updater@v0.8.1 21 | with: 22 | # [Required] Access token with `workflow` scope. 23 | token: ${{ secrets.WORKFLOW_SECRET }} -------------------------------------------------------------------------------- /src/main/java/com/marginallyclever/donatello/nodes/TimeInSeconds.java: -------------------------------------------------------------------------------- 1 | package com.marginallyclever.donatello.nodes; 2 | 3 | import com.marginallyclever.donatello.ports.OutputInt; 4 | import com.marginallyclever.nodegraphcore.Node; 5 | 6 | /** 7 | * Publishes the time in seconds continuously. 8 | */ 9 | public class TimeInSeconds extends Node { 10 | private final OutputInt seconds = new OutputInt("seconds", 0); 11 | private final double startTime = System.currentTimeMillis(); 12 | 13 | public TimeInSeconds() { 14 | super("TimeInSeconds"); 15 | addPort(seconds); 16 | } 17 | 18 | @Override 19 | public void update() { 20 | double t = (System.currentTimeMillis()-startTime)/1000.0; 21 | seconds.setValue((int)t); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/com/marginallyclever/donatello/select/BackgroundPaintedButton.java: -------------------------------------------------------------------------------- 1 | package com.marginallyclever.donatello.select; 2 | 3 | import javax.swing.*; 4 | import java.awt.*; 5 | 6 | /** 7 | * A {@link JButton} filled with the background color. Especially useful for color selection dialogs. 8 | * @author Dan Royer 9 | * @since 7.31.0 10 | * 11 | */ 12 | public class BackgroundPaintedButton extends JButton { 13 | public BackgroundPaintedButton(String label) { 14 | super(label); 15 | } 16 | 17 | @Override 18 | protected void paintComponent(Graphics g) { 19 | super.paintComponent(g); 20 | Graphics2D g2 = (Graphics2D)g; 21 | 22 | Color c = getBackground(); 23 | g2.setPaint(c); 24 | g2.fillRoundRect(0,0,getWidth()-1,getHeight()-1,0,0); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/test/resources/com/marginallyclever/donatello/logback-test.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 8 | 9 | %d %boldCyan(%-5level) %boldGreen(%-15.-15logger{0}) - %msg %n 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/main/java/com/marginallyclever/donatello/actions/SwapToolsAction.java: -------------------------------------------------------------------------------- 1 | package com.marginallyclever.donatello.actions; 2 | 3 | import com.marginallyclever.donatello.Donatello; 4 | import com.marginallyclever.donatello.contextsensitivetools.ContextSensitiveTool; 5 | 6 | import javax.swing.*; 7 | import java.awt.event.ActionEvent; 8 | 9 | public class SwapToolsAction extends AbstractAction { 10 | private final Donatello editor; 11 | private final ContextSensitiveTool tool; 12 | 13 | public SwapToolsAction(Donatello editor, ContextSensitiveTool tool) { 14 | super(tool.getName()); 15 | this.editor = editor; 16 | this.tool = tool; 17 | } 18 | 19 | @Override 20 | public void actionPerformed(ActionEvent e) { 21 | editor.swapTool(tool); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/com/marginallyclever/donatello/RollOncePerSessionTriggeringPolicy.java: -------------------------------------------------------------------------------- 1 | package com.marginallyclever.donatello; 2 | 3 | import ch.qos.logback.core.rolling.TriggeringPolicyBase; 4 | 5 | import java.io.File; 6 | 7 | /** 8 | * Logback policy referenced in logback.xml in order to get a new file at each start of the application 9 | * @param 10 | */ 11 | public class RollOncePerSessionTriggeringPolicy extends TriggeringPolicyBase { 12 | private static boolean doRolling = true; 13 | 14 | @Override 15 | public boolean isTriggeringEvent(File activeFile, E event) { 16 | // roll the first time when the event gets called 17 | if (doRolling) { 18 | doRolling = false; 19 | return true; 20 | } 21 | return false; 22 | } 23 | } 24 | 25 | -------------------------------------------------------------------------------- /src/main/java/com/marginallyclever/donatello/ports/InputImage.java: -------------------------------------------------------------------------------- 1 | package com.marginallyclever.donatello.ports; 2 | 3 | import com.marginallyclever.nodegraphcore.port.Input; 4 | import com.marginallyclever.nodegraphcore.port.Port; 5 | 6 | import java.awt.*; 7 | import java.awt.image.BufferedImage; 8 | 9 | /** 10 | * An input port that accepts a {@link BufferedImage}. 11 | */ 12 | public class InputImage extends Input { 13 | public InputImage(String name) throws IllegalArgumentException { 14 | super(name, BufferedImage.class, new BufferedImage(1,1,BufferedImage.TYPE_INT_ARGB)); 15 | } 16 | 17 | @Override 18 | public Rectangle getRectangle() { 19 | rectangle.setSize(Port.DEFAULT_WIDTH,Port.DEFAULT_HEIGHT); 20 | return super.getRectangle(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/com/marginallyclever/donatello/select/SelectEvent.java: -------------------------------------------------------------------------------- 1 | package com.marginallyclever.donatello.select; 2 | 3 | /** 4 | * A SelectEvent is fired when a Select changes value. 5 | * @author Dan Royer 6 | * @since 7.50.2 7 | */ 8 | public class SelectEvent { 9 | private final Select source; 10 | private final Object oldValue; 11 | private final Object newValue; 12 | 13 | public SelectEvent(Select source,Object oldValue,Object newValue) { 14 | this.source = source; 15 | this.oldValue = oldValue; 16 | this.newValue = newValue; 17 | } 18 | 19 | public Select getSource() { 20 | return source; 21 | } 22 | 23 | public Object getOldValue() { 24 | return oldValue; 25 | } 26 | 27 | public Object getNewValue() { 28 | return newValue; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/test/java/com/marginallyclever/donatello/select/SelectIntegerTest.java: -------------------------------------------------------------------------------- 1 | package com.marginallyclever.donatello.select; 2 | 3 | import org.junit.jupiter.api.Assertions; 4 | import org.junit.jupiter.api.Test; 5 | 6 | import java.util.Locale; 7 | 8 | public class SelectIntegerTest { 9 | private int result = 5; 10 | 11 | @Test 12 | public void test() throws InterruptedException { 13 | Locale locale = Locale.getDefault(); 14 | SelectInteger selectInteger = new SelectInteger("internalName", "labelKey", locale, 0); 15 | selectInteger.addSelectListener(evt->{ 16 | result = (Integer)evt.getNewValue(); 17 | }); 18 | selectInteger.setValue(1); 19 | // the event fires ~100ms after setValue is called. 20 | Thread.sleep(200); 21 | Assertions.assertEquals(1,result); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/com/marginallyclever/donatello/organize/NodeListCellRenderer.java: -------------------------------------------------------------------------------- 1 | package com.marginallyclever.donatello.organize; 2 | 3 | import com.marginallyclever.nodegraphcore.Node; 4 | 5 | import javax.swing.*; 6 | import java.awt.*; 7 | 8 | /** 9 | * A custom {@link ListCellRenderer} for displaying {@link Node} objects in a {@link JList}. 10 | */ 11 | class NodeListCellRenderer implements ListCellRenderer { 12 | protected DefaultListCellRenderer defaultRenderer = new DefaultListCellRenderer(); 13 | 14 | @Override 15 | public Component getListCellRendererComponent(JList list, Node node, int index, boolean isSelected, boolean cellHasFocus) { 16 | String label = node.getUniqueID() + " " + node.getLabel(); 17 | return defaultRenderer.getListCellRendererComponent(list, label, index, isSelected, cellHasFocus); 18 | } 19 | } -------------------------------------------------------------------------------- /src/main/java/com/marginallyclever/donatello/ports/FilenameDAO4JSON.java: -------------------------------------------------------------------------------- 1 | package com.marginallyclever.donatello.ports; 2 | 3 | import com.marginallyclever.nodegraphcore.AbstractDAO4JSON; 4 | import org.json.JSONException; 5 | import org.json.JSONObject; 6 | 7 | public class FilenameDAO4JSON extends AbstractDAO4JSON { 8 | public FilenameDAO4JSON() { 9 | super(Filename.class); 10 | } 11 | 12 | @Override 13 | public Object toJSON(Object value) throws JSONException { 14 | Filename f = (Filename)value; 15 | JSONObject v = new JSONObject(); 16 | v.put("filename",f.get()); 17 | return v; 18 | } 19 | 20 | @Override 21 | public Filename fromJSON(Object object) throws JSONException { 22 | JSONObject v = (JSONObject)object; 23 | return new Filename(v.optString("filename","")); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/com/marginallyclever/donatello/NumberFormatHelper.java: -------------------------------------------------------------------------------- 1 | package com.marginallyclever.donatello; 2 | 3 | import javax.swing.text.NumberFormatter; 4 | import java.text.NumberFormat; 5 | 6 | public class NumberFormatHelper { 7 | static public NumberFormatter getNumberFormatterDouble() { 8 | NumberFormat format = NumberFormat.getNumberInstance(); 9 | NumberFormatter formatter = new NumberFormatter(format); 10 | formatter.setAllowsInvalid(true); 11 | formatter.setCommitsOnValidEdit(true); 12 | return formatter; 13 | } 14 | 15 | static public NumberFormatter getNumberFormatterInt() { 16 | NumberFormat format = NumberFormat.getIntegerInstance(); 17 | NumberFormatter formatter = new NumberFormatter(format); 18 | formatter.setAllowsInvalid(true); 19 | formatter.setCommitsOnValidEdit(true); 20 | return formatter; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/com/marginallyclever/donatello/actions/GraphOrganizeAction.java: -------------------------------------------------------------------------------- 1 | package com.marginallyclever.donatello.actions; 2 | 3 | import com.marginallyclever.donatello.Donatello; 4 | import com.marginallyclever.donatello.organize.OrganizeGraphPanel; 5 | 6 | import javax.swing.*; 7 | import java.awt.event.ActionEvent; 8 | 9 | public class GraphOrganizeAction extends AbstractAction { 10 | /** 11 | * The editor being affected. 12 | */ 13 | private final Donatello editor; 14 | 15 | /** 16 | * Constructor for subclasses to call. 17 | * @param name the name of this action visible on buttons and menu items. 18 | * @param editor the editor affected by this Action. 19 | */ 20 | public GraphOrganizeAction(String name, Donatello editor) { 21 | super(name); 22 | this.editor = editor; 23 | } 24 | 25 | @Override 26 | public void actionPerformed(ActionEvent e) { 27 | OrganizeGraphPanel.runAsDialog((String)this.getValue(Action.NAME),editor); 28 | } 29 | } -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /src/main/java/com/marginallyclever/donatello/ports/InputBoolean.java: -------------------------------------------------------------------------------- 1 | package com.marginallyclever.donatello.ports; 2 | 3 | import com.marginallyclever.donatello.SwingProvider; 4 | import com.marginallyclever.donatello.select.Select; 5 | import com.marginallyclever.donatello.select.SelectBoolean; 6 | import com.marginallyclever.nodegraphcore.port.Input; 7 | 8 | import java.awt.*; 9 | 10 | public class InputBoolean extends Input implements SwingProvider { 11 | private SelectBoolean selectBoolean; 12 | 13 | public InputBoolean(String name, Boolean startingValue) throws IllegalArgumentException { 14 | super(name, Boolean.class, startingValue); 15 | } 16 | 17 | @Override 18 | public Select getSwingComponent(Component parent) { 19 | if(selectBoolean==null) { 20 | selectBoolean = new SelectBoolean(name,name,value); 21 | selectBoolean.addSelectListener( evt -> { 22 | setValue(evt.getNewValue()); 23 | }); 24 | } 25 | return selectBoolean; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/com/marginallyclever/donatello/ports/InputString.java: -------------------------------------------------------------------------------- 1 | package com.marginallyclever.donatello.ports; 2 | 3 | import com.marginallyclever.donatello.SwingProvider; 4 | import com.marginallyclever.donatello.select.Select; 5 | import com.marginallyclever.donatello.select.SelectTextField; 6 | import com.marginallyclever.nodegraphcore.port.Input; 7 | 8 | import java.awt.*; 9 | 10 | public class InputString extends Input implements SwingProvider { 11 | private SelectTextField selectTextField; 12 | 13 | public InputString(String name, String startingValue) throws IllegalArgumentException { 14 | super(name, String.class, startingValue); 15 | } 16 | 17 | @Override 18 | public Select getSwingComponent(Component parent) { 19 | if(selectTextField==null) { 20 | selectTextField = new SelectTextField(name,name,value); 21 | selectTextField.addSelectListener( evt -> { 22 | setValue(evt.getNewValue()); 23 | }); 24 | } 25 | return selectTextField; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/com/marginallyclever/donatello/select/SelectPanel.java: -------------------------------------------------------------------------------- 1 | package com.marginallyclever.donatello.select; 2 | 3 | import javax.swing.*; 4 | import javax.swing.border.EmptyBorder; 5 | import java.awt.*; 6 | 7 | /** 8 | * A container for all Select elements, to facilitate formatting as a group. 9 | * @author Dan Royer 10 | * @since 7.24.0 11 | */ 12 | public class SelectPanel extends JPanel { 13 | private final JPanel interiorPanel = new JPanel(); 14 | private final GridBagConstraints gbc = new GridBagConstraints(); 15 | 16 | public SelectPanel() { 17 | super(new BorderLayout()); 18 | add(interiorPanel,BorderLayout.PAGE_START); 19 | 20 | interiorPanel.setBorder(new EmptyBorder(5,5,5,5)); 21 | interiorPanel.setLayout(new GridBagLayout()); 22 | gbc.gridx=0; 23 | gbc.gridy=0; 24 | gbc.fill = GridBagConstraints.HORIZONTAL; 25 | gbc.weightx=1; 26 | gbc.weighty=0; 27 | } 28 | 29 | public void add(Select c) { 30 | c.attach(interiorPanel,gbc); 31 | gbc.gridy++; 32 | } 33 | 34 | public void clear() { 35 | interiorPanel.removeAll(); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/com/marginallyclever/donatello/actions/GraphNewAction.java: -------------------------------------------------------------------------------- 1 | package com.marginallyclever.donatello.actions; 2 | 3 | import com.marginallyclever.donatello.Donatello; 4 | 5 | import javax.swing.*; 6 | import java.awt.event.ActionEvent; 7 | 8 | /** 9 | * Clears the editor's current {@link com.marginallyclever.nodegraphcore.Graph}. 10 | * @author Dan Royer 11 | * @since 2022-02-21 12 | */ 13 | public class GraphNewAction extends AbstractAction { 14 | /** 15 | * The editor being affected. 16 | */ 17 | private final Donatello editor; 18 | 19 | /** 20 | * Constructor for subclasses to call. 21 | * @param name the name of this action visible on buttons and menu items. 22 | * @param editor the editor affected by this Action. 23 | */ 24 | public GraphNewAction(String name, Donatello editor) { 25 | super(name); 26 | this.editor = editor; 27 | this.putValue(SHORT_DESCRIPTION, "Clear the current graph."); 28 | } 29 | 30 | @Override 31 | public void actionPerformed(ActionEvent e) { 32 | editor.clear(); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/com/marginallyclever/donatello/UndoHandler.java: -------------------------------------------------------------------------------- 1 | package com.marginallyclever.donatello; 2 | 3 | import com.marginallyclever.donatello.actions.RedoAction; 4 | import com.marginallyclever.donatello.actions.UndoAction; 5 | 6 | import javax.swing.event.UndoableEditEvent; 7 | import javax.swing.event.UndoableEditListener; 8 | import javax.swing.undo.UndoManager; 9 | 10 | public class UndoHandler implements UndoableEditListener { 11 | private final UndoManager undoManager; 12 | private final UndoAction actionUndo; 13 | private final RedoAction actionRedo; 14 | 15 | public UndoHandler(UndoManager undoManager, UndoAction actionUndo, RedoAction actionRedo) { 16 | super(); 17 | this.undoManager = undoManager; 18 | this.actionUndo = actionUndo; 19 | this.actionRedo = actionRedo; 20 | } 21 | 22 | @Override 23 | public void undoableEditHappened(UndoableEditEvent e) { 24 | System.out.println("Undoable edit "+e.getEdit().getPresentationName()); 25 | undoManager.addEdit(e.getEdit()); 26 | actionUndo.update(); 27 | actionRedo.update(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/com/marginallyclever/donatello/contextsensitivetools/RememberKeyStateAction.java: -------------------------------------------------------------------------------- 1 | package com.marginallyclever.donatello.contextsensitivetools; 2 | 3 | import com.marginallyclever.donatello.KeyStateMemory; 4 | 5 | import javax.swing.*; 6 | import java.awt.event.ActionEvent; 7 | 8 | /** 9 | * Remembers the state of a key for other actions, such as "is shift being held down?" for selection tools. 10 | * @author Dan Royer 11 | * @since 2022-03-08 12 | */ 13 | public class RememberKeyStateAction extends AbstractAction { 14 | private final KeyStateMemory memory; 15 | private final boolean onRelease; 16 | private final int keyCode; 17 | 18 | public RememberKeyStateAction(KeyStateMemory memory, int keyCode, boolean onRelease) { 19 | super(); 20 | this.memory=memory; 21 | this.onRelease = onRelease; 22 | this.keyCode = keyCode; 23 | } 24 | 25 | @Override 26 | public void actionPerformed(ActionEvent e) { 27 | if(onRelease) { 28 | memory.keyReleased(keyCode); 29 | } else { 30 | memory.keyPressed(keyCode); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/com/marginallyclever/donatello/IconHelper.java: -------------------------------------------------------------------------------- 1 | package com.marginallyclever.donatello; 2 | 3 | import javax.swing.*; 4 | import java.awt.*; 5 | import java.awt.image.BufferedImage; 6 | 7 | public class IconHelper { 8 | public static final int ICON_SIZE = 24; 9 | 10 | public static Icon scaleIcon(Icon icon, int width, int height) { 11 | BufferedImage img = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); 12 | Graphics2D g2 = img.createGraphics(); 13 | g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR); 14 | 15 | // Paint the icon into a temp image at its original size 16 | BufferedImage iconImage = new BufferedImage(icon.getIconWidth(), icon.getIconHeight(), BufferedImage.TYPE_INT_ARGB); 17 | Graphics2D gIcon = iconImage.createGraphics(); 18 | icon.paintIcon(null, gIcon, 0, 0); 19 | gIcon.dispose(); 20 | 21 | // Draw the temp image scaled to the target size 22 | g2.drawImage(iconImage, 0, 0, width, height, null); 23 | g2.dispose(); 24 | return new ImageIcon(img); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/assembly/application.xml: -------------------------------------------------------------------------------- 1 | 4 | with-dependencies 5 | 6 | jar 7 | 8 | false 9 | 10 | 11 | / 12 | true 13 | true 14 | runtime 15 | 16 | 17 | 18 | 19 | ${project.build.outputDirectory} 20 | / 21 | 22 | **/* 23 | 24 | 25 | 26 | 27 | 28 | metaInf-services 29 | 30 | 31 | -------------------------------------------------------------------------------- /src/main/java/com/marginallyclever/donatello/actions/SelectAllAction.java: -------------------------------------------------------------------------------- 1 | package com.marginallyclever.donatello.actions; 2 | 3 | import com.marginallyclever.donatello.Donatello; 4 | 5 | import javax.swing.*; 6 | import java.awt.event.ActionEvent; 7 | 8 | /** 9 | * Select all the {@link com.marginallyclever.nodegraphcore.Node}s in the {@link com.marginallyclever.nodegraphcore.Graph}. 10 | */ 11 | public class SelectAllAction extends AbstractAction { 12 | /** 13 | * The editor being affected. 14 | */ 15 | private final Donatello editor; 16 | 17 | /** 18 | * Constructor for subclasses to call. 19 | * @param name the name of this action visible on buttons and menu items. 20 | * @param editor the editor affected by this Action. 21 | */ 22 | public SelectAllAction(String name, Donatello editor) { 23 | super(name); 24 | this.editor = editor; 25 | } 26 | 27 | /** 28 | * Updates the model and repaints the panel. 29 | */ 30 | @Override 31 | public void actionPerformed(ActionEvent e) { 32 | editor.setSelectedNodes(editor.getGraph().getNodes()); 33 | editor.repaint(); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/com/marginallyclever/donatello/UnicodeIcon.java: -------------------------------------------------------------------------------- 1 | package com.marginallyclever.donatello; 2 | 3 | import javax.swing.*; 4 | import java.awt.*; 5 | 6 | /** 7 | * Creates a single color {@link Icon} based on a Unicode symbol. 8 | * @author Dan Royer 9 | * @since 2022-03-15 10 | */ 11 | public class UnicodeIcon implements Icon { 12 | private final static int HEIGHT = 24; 13 | private final static int WIDTH = 24; 14 | private final String unicode; 15 | 16 | public UnicodeIcon(String unicode) { 17 | super(); 18 | this.unicode = unicode; 19 | } 20 | 21 | @Override 22 | public void paintIcon(Component c, Graphics g, int x, int y) { 23 | Graphics g2 = g.create(); 24 | g2.setFont(new Font("SansSerif",Font.PLAIN,(int)((double)HEIGHT/1.25))); 25 | FontMetrics fm = g.getFontMetrics(); 26 | int x2 = (WIDTH - fm.stringWidth(unicode)) / 2; 27 | int y2 = y + (fm.getAscent() + (HEIGHT - (fm.getAscent() + fm.getDescent())) / 2); 28 | g2.setColor(Color.GRAY); 29 | g2.drawString(unicode, x2, y2); 30 | } 31 | 32 | @Override 33 | public int getIconWidth() { 34 | return WIDTH; 35 | } 36 | 37 | @Override 38 | public int getIconHeight() { 39 | return HEIGHT; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/com/marginallyclever/donatello/simpleparser/SimpleParser.java: -------------------------------------------------------------------------------- 1 | package com.marginallyclever.donatello.simpleparser; 2 | 3 | import net.objecthunter.exp4j.Expression; 4 | import net.objecthunter.exp4j.ExpressionBuilder; 5 | import net.objecthunter.exp4j.function.Function; 6 | 7 | /** 8 | * A simple parser that evaluates a mathematical expression. Supports the use of the constant PI and the hypot function. 9 | */ 10 | public class SimpleParser { 11 | private static final Function hypot = new Function("hypot",2) { 12 | @Override 13 | public double apply(double... doubles) { 14 | return Math.hypot(doubles[0], doubles[1]); 15 | } 16 | }; 17 | private static final Function atan2 = new Function("atan2",2) { 18 | @Override 19 | public double apply(double... doubles) { 20 | return Math.atan2(doubles[0], doubles[1]); 21 | } 22 | }; 23 | 24 | public static double evaluate(String expression) { 25 | Expression e = new ExpressionBuilder(expression) 26 | .variables("PI") // Declare the variable 27 | .function(hypot) 28 | .function(atan2) 29 | .build() 30 | .setVariable("PI", Math.PI); // Assign value to variable 31 | return e.evaluate(); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/com/marginallyclever/donatello/nodes/color/ColorDAO4JSON.java: -------------------------------------------------------------------------------- 1 | package com.marginallyclever.donatello.nodes.color; 2 | 3 | import com.marginallyclever.nodegraphcore.AbstractDAO4JSON; 4 | import org.json.JSONException; 5 | import org.json.JSONObject; 6 | 7 | import java.awt.*; 8 | 9 | /** 10 | * For dealing with {@link Color} 11 | * @author Dan Royer 12 | * @since 2022-03-19 13 | */ 14 | public class ColorDAO4JSON extends AbstractDAO4JSON { 15 | public ColorDAO4JSON() { 16 | super(Color.class); 17 | } 18 | 19 | @Override 20 | public Object toJSON(Object value) throws JSONException { 21 | Color image = (Color)value; 22 | JSONObject v = new JSONObject(); 23 | v.put("r",image.getRed()); 24 | v.put("g",image.getGreen()); 25 | v.put("b",image.getBlue()); 26 | v.put("a",image.getAlpha()); 27 | return v; 28 | } 29 | 30 | @Override 31 | public Color fromJSON(Object object) throws JSONException { 32 | JSONObject v = (JSONObject)object; 33 | int r = v.getInt("r"); 34 | int g = v.getInt("g"); 35 | int b = v.getInt("b"); 36 | int a = v.getInt("a"); 37 | // for a complete snapshot, restore all the instance details, too. 38 | return new Color(r,g,b,a); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/com/marginallyclever/donatello/actions/GraphUpdateAction.java: -------------------------------------------------------------------------------- 1 | package com.marginallyclever.donatello.actions; 2 | 3 | import com.marginallyclever.donatello.Donatello; 4 | 5 | import javax.swing.*; 6 | import java.awt.event.ActionEvent; 7 | 8 | /** 9 | * Updates all dirty {@link com.marginallyclever.nodegraphcore.Node}s in the editor's graph. It cannot be undone. 10 | */ 11 | public class GraphUpdateAction extends AbstractAction { 12 | /** 13 | * The editor being affected. 14 | */ 15 | private final Donatello editor; 16 | 17 | /** 18 | * Constructor for subclasses to call. 19 | * @param name the name of this action visible on buttons and menu items. 20 | * @param editor the editor affected by this Action. 21 | */ 22 | public GraphUpdateAction(String name, Donatello editor) { 23 | super(name); 24 | this.editor = editor; 25 | putValue(SHORT_DESCRIPTION, "Update the graph once."); 26 | } 27 | 28 | /** 29 | * Updates the model and repaints the panel. 30 | */ 31 | @Override 32 | public void actionPerformed(ActionEvent e) { 33 | try { 34 | editor.getGraph().update(); 35 | } catch (Exception ex) { 36 | ex.printStackTrace(); 37 | } 38 | editor.repaint(); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/com/marginallyclever/donatello/actions/SelectionInvertAction.java: -------------------------------------------------------------------------------- 1 | package com.marginallyclever.donatello.actions; 2 | 3 | import com.marginallyclever.nodegraphcore.Node; 4 | import com.marginallyclever.donatello.Donatello; 5 | 6 | import javax.swing.*; 7 | import java.awt.event.ActionEvent; 8 | import java.util.ArrayList; 9 | import java.util.List; 10 | 11 | /** 12 | * Invert the selection. 13 | */ 14 | public class SelectionInvertAction extends AbstractAction { 15 | /** 16 | * The editor being affected. 17 | */ 18 | private final Donatello editor; 19 | 20 | /** 21 | * Constructor for subclasses to call. 22 | * @param name the name of this action visible on buttons and menu items. 23 | * @param editor the editor affected by this Action. 24 | */ 25 | public SelectionInvertAction(String name, Donatello editor) { 26 | super(name); 27 | this.editor = editor; 28 | } 29 | 30 | /** 31 | * Updates the model and repaints the panel. 32 | */ 33 | @Override 34 | public void actionPerformed(ActionEvent e) { 35 | List difference = new ArrayList<>(editor.getGraph().getNodes()); 36 | difference.removeAll(editor.getSelectedNodes()); 37 | editor.setSelectedNodes(difference); 38 | editor.repaint(); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/com/marginallyclever/donatello/nodes/images/BufferedImageDAO4JSON.java: -------------------------------------------------------------------------------- 1 | package com.marginallyclever.donatello.nodes.images; 2 | 3 | import com.marginallyclever.nodegraphcore.AbstractDAO4JSON; 4 | import org.json.JSONException; 5 | import org.json.JSONObject; 6 | 7 | import java.awt.image.BufferedImage; 8 | 9 | public class BufferedImageDAO4JSON extends AbstractDAO4JSON { 10 | public BufferedImageDAO4JSON() { 11 | super(BufferedImage.class); 12 | } 13 | 14 | @Override 15 | public Object toJSON(Object value) throws JSONException { 16 | BufferedImage image = (BufferedImage)value; 17 | JSONObject v = new JSONObject(); 18 | v.put("width",image.getWidth()); 19 | v.put("height",image.getHeight()); 20 | v.put("type",image.getType()); 21 | // for a complete snapshot, capture all the instance details, too. 22 | return v; 23 | } 24 | 25 | @Override 26 | public BufferedImage fromJSON(Object object) throws JSONException { 27 | JSONObject v = (JSONObject)object; 28 | int w = v.getInt("width"); 29 | int h = v.getInt("height"); 30 | int type = v.getInt("type"); 31 | // for a complete snapshot, restore all the instance details, too. 32 | return new BufferedImage(w,h,type); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/com/marginallyclever/donatello/organize/TransferableNode.java: -------------------------------------------------------------------------------- 1 | package com.marginallyclever.donatello.organize; 2 | 3 | import com.marginallyclever.nodegraphcore.Node; 4 | 5 | import java.awt.datatransfer.DataFlavor; 6 | import java.awt.datatransfer.Transferable; 7 | import java.awt.datatransfer.UnsupportedFlavorException; 8 | 9 | public class TransferableNode implements Transferable { 10 | protected static DataFlavor nodeFlavor = new DataFlavor(Node.class, "A Node Object"); 11 | protected static DataFlavor[] supportedFlavors = { nodeFlavor }; 12 | 13 | Node node; 14 | 15 | public TransferableNode(Node node) { 16 | this.node = node; 17 | } 18 | 19 | public DataFlavor[] getTransferDataFlavors() { 20 | return supportedFlavors; 21 | } 22 | 23 | public boolean isDataFlavorSupported(DataFlavor flavor) { 24 | if (flavor.equals(nodeFlavor) || flavor.equals(DataFlavor.stringFlavor)) 25 | return true; 26 | return false; 27 | } 28 | 29 | public Object getTransferData(DataFlavor flavor) throws UnsupportedFlavorException { 30 | if (flavor.equals(nodeFlavor)) 31 | return node; 32 | else if (flavor.equals(DataFlavor.stringFlavor)) 33 | return node.toString(); 34 | else 35 | throw new UnsupportedFlavorException(flavor); 36 | } 37 | } -------------------------------------------------------------------------------- /src/main/java/com/marginallyclever/donatello/ports/InputNumber.java: -------------------------------------------------------------------------------- 1 | package com.marginallyclever.donatello.ports; 2 | 3 | import com.marginallyclever.donatello.SwingProvider; 4 | import com.marginallyclever.donatello.select.Select; 5 | import com.marginallyclever.donatello.select.SelectDouble; 6 | import com.marginallyclever.nodegraphcore.port.Input; 7 | 8 | import java.awt.*; 9 | 10 | /** 11 | * {@link InputNumber} exists only so that OutputInteger and OutputDouble can both connect to it. 12 | * It is problematic because it's the exact same as an InputDouble, but with a different parameter class. 13 | */ 14 | public class InputNumber extends Input implements SwingProvider { 15 | private SelectDouble selectDouble; 16 | 17 | public InputNumber(String name) { 18 | this(name, 0); 19 | } 20 | 21 | public InputNumber(String name, Number startingValue) throws IllegalArgumentException { 22 | super(name, Number.class, startingValue); 23 | } 24 | 25 | @Override 26 | public Select getSwingComponent(Component parent) { 27 | if (selectDouble == null) { 28 | selectDouble = new SelectDouble(name, name, value.doubleValue()); 29 | selectDouble.addSelectListener(evt -> { 30 | setValue(evt.getNewValue()); 31 | }); 32 | } 33 | return selectDouble; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/com/marginallyclever/donatello/actions/RedoAction.java: -------------------------------------------------------------------------------- 1 | package com.marginallyclever.donatello.actions; 2 | 3 | import javax.swing.*; 4 | import javax.swing.undo.CannotRedoException; 5 | import javax.swing.undo.UndoManager; 6 | import java.awt.event.ActionEvent; 7 | 8 | public class RedoAction extends AbstractAction { 9 | private final UndoManager undoManager; 10 | private UndoAction actionUndo; 11 | 12 | public RedoAction(UndoManager undoManager) { 13 | super("Redo"); 14 | setEnabled(false); 15 | this.undoManager = undoManager; 16 | } 17 | 18 | @Override 19 | public void actionPerformed(ActionEvent e) { 20 | try { 21 | undoManager.redo(); 22 | } 23 | catch (CannotRedoException ex) { 24 | // TODO deal with this 25 | ex.printStackTrace(); 26 | } 27 | update(); 28 | actionUndo.update(); 29 | } 30 | 31 | public void update() { 32 | if (undoManager.canRedo()) { 33 | setEnabled(true); 34 | putValue(Action.NAME, undoManager.getRedoPresentationName()); 35 | } else { 36 | setEnabled(false); 37 | putValue(Action.NAME, "Redo"); 38 | } 39 | } 40 | 41 | public void setActionUndo(UndoAction actionUndo) { 42 | this.actionUndo = actionUndo; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/com/marginallyclever/donatello/actions/UndoAction.java: -------------------------------------------------------------------------------- 1 | package com.marginallyclever.donatello.actions; 2 | 3 | import javax.swing.*; 4 | import javax.swing.undo.CannotUndoException; 5 | import javax.swing.undo.UndoManager; 6 | import java.awt.event.ActionEvent; 7 | 8 | public class UndoAction extends AbstractAction { 9 | private final UndoManager undoManager; 10 | private RedoAction actionRedo; 11 | 12 | public UndoAction(UndoManager undoManager) { 13 | super("Undo"); 14 | setEnabled(false); 15 | this.undoManager = undoManager; 16 | } 17 | 18 | @Override 19 | public void actionPerformed(ActionEvent e) { 20 | try { 21 | undoManager.undo(); 22 | } 23 | catch (CannotUndoException ex) { 24 | // TODO deal with this 25 | //ex.printStackTrace(); 26 | } 27 | update(); 28 | actionRedo.update(); 29 | } 30 | 31 | public void update() { 32 | if (undoManager.canUndo()) { 33 | setEnabled(true); 34 | putValue(Action.NAME, undoManager.getUndoPresentationName()); 35 | } else { 36 | setEnabled(false); 37 | putValue(Action.NAME, "Undo"); 38 | } 39 | } 40 | 41 | public void setActionRedo(RedoAction actionRedo) { 42 | this.actionRedo = actionRedo; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/com/marginallyclever/donatello/actions/BrowseURLAction.java: -------------------------------------------------------------------------------- 1 | package com.marginallyclever.donatello.actions; 2 | 3 | import com.marginallyclever.donatello.Donatello; 4 | import org.slf4j.Logger; 5 | import org.slf4j.LoggerFactory; 6 | 7 | import javax.swing.*; 8 | import java.awt.*; 9 | import java.awt.event.ActionEvent; 10 | import java.io.IOException; 11 | import java.net.URI; 12 | 13 | /** 14 | * Use the desktop browser to open a URL. 15 | * @author Dan Royer 16 | * @since 2022-03-14 17 | */ 18 | public class BrowseURLAction extends AbstractAction { 19 | private static final Logger logger = LoggerFactory.getLogger(BrowseURLAction.class); 20 | 21 | private final String address; 22 | 23 | public BrowseURLAction(String label, String address) { 24 | super(label); 25 | this.address = address; 26 | } 27 | 28 | @Override 29 | public void actionPerformed(ActionEvent e) { 30 | try { 31 | if (Desktop.isDesktopSupported() && Desktop.getDesktop().isSupported(Desktop.Action.BROWSE)) { 32 | java.awt.Desktop.getDesktop().browse(URI.create(address)); 33 | } 34 | } catch (IOException ex) { 35 | logger.warn("Could not open browser.", ex); 36 | JOptionPane.showMessageDialog((Component)e.getSource(), "Could not open "+address, "Error", JOptionPane.ERROR_MESSAGE); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/com/marginallyclever/donatello/actions/undoable/NodeDeleteAction.java: -------------------------------------------------------------------------------- 1 | package com.marginallyclever.donatello.actions.undoable; 2 | 3 | import com.marginallyclever.nodegraphcore.Node; 4 | import com.marginallyclever.donatello.Donatello; 5 | import com.marginallyclever.donatello.actions.EditorAction; 6 | import com.marginallyclever.donatello.edits.GraphDeleteEdit; 7 | 8 | import javax.swing.*; 9 | import java.awt.event.ActionEvent; 10 | 11 | /** 12 | * Deletes the editor's selected {@link Node}s and sundry. 13 | * @author Dan Royer 14 | * @since 2022-02-21 15 | */ 16 | public class NodeDeleteAction extends AbstractAction implements EditorAction { 17 | /** 18 | * The editor being affected. 19 | */ 20 | private final Donatello editor; 21 | 22 | /** 23 | * Constructor for subclasses to call. 24 | * @param name the name of this action visible on buttons and menu items. 25 | * @param editor the editor affected by this Action. 26 | */ 27 | public NodeDeleteAction(String name, Donatello editor) { 28 | super(name); 29 | this.editor = editor; 30 | } 31 | 32 | @Override 33 | public void actionPerformed(ActionEvent e) { 34 | editor.addEdit(new GraphDeleteEdit((String)this.getValue(Action.NAME),editor,editor.getSelectedNodes())); 35 | } 36 | 37 | @Override 38 | public void updateEnableStatus() { 39 | setEnabled(!editor.getSelectedNodes().isEmpty()); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/test/java/com/marginallyclever/donatello/port/InputIntTest.java: -------------------------------------------------------------------------------- 1 | package com.marginallyclever.donatello.port; 2 | 3 | import com.marginallyclever.donatello.ports.InputInt; 4 | import com.marginallyclever.donatello.ports.OutputDouble; 5 | import com.marginallyclever.nodegraphcore.Connection; 6 | import com.marginallyclever.nodegraphcore.Node; 7 | import org.junit.jupiter.api.Test; 8 | 9 | public class InputIntTest { 10 | // test that InputDouble can receive from an OutputInt 11 | @Test 12 | public void testDoubleToInt() { 13 | // Create an instance of InputDouble 14 | Node outputNode = new Node("output") { 15 | final OutputDouble outputDouble = new OutputDouble("TestOutput", 1.0); 16 | { 17 | addPort(outputDouble); 18 | } 19 | 20 | @Override 21 | public void update() {} 22 | }; 23 | 24 | Node inputNode = new Node("input") { 25 | final InputInt inputInt = new InputInt("TestInput", 0); 26 | { 27 | addPort(inputInt); 28 | } 29 | @Override 30 | public void update() {} 31 | }; 32 | 33 | // make a Connection 34 | Connection connection = new Connection(); 35 | connection.setFrom(outputNode,0); 36 | connection.setTo(inputNode,0); 37 | // check that the connection is valid 38 | assert connection.isValidDataType(); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/test/java/com/marginallyclever/donatello/port/InputDoubleTest.java: -------------------------------------------------------------------------------- 1 | package com.marginallyclever.donatello.port; 2 | 3 | import com.marginallyclever.donatello.ports.InputDouble; 4 | import com.marginallyclever.donatello.ports.OutputInt; 5 | import com.marginallyclever.nodegraphcore.Connection; 6 | import com.marginallyclever.nodegraphcore.Node; 7 | import org.junit.jupiter.api.Test; 8 | 9 | public class InputDoubleTest { 10 | // test that InputDouble can receive from an OutputInt 11 | @Test 12 | public void testIntToDouble() { 13 | // Create an instance of InputDouble 14 | Node outputNode = new Node("output") { 15 | final OutputInt outputInt = new OutputInt("TestOutput", 1); 16 | { 17 | addPort(outputInt); 18 | } 19 | 20 | @Override 21 | public void update() {} 22 | }; 23 | 24 | Node inputNode = new Node("input") { 25 | final InputDouble inputDouble = new InputDouble("TestInput", 0.0); 26 | { 27 | addPort(inputDouble); 28 | } 29 | @Override 30 | public void update() {} 31 | }; 32 | 33 | // make a Connection 34 | Connection connection = new Connection(); 35 | connection.setFrom(outputNode,0); 36 | connection.setTo(inputNode,0); 37 | // check that the connection is valid 38 | assert connection.isValidDataType(); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/com/marginallyclever/donatello/ports/InputColor.java: -------------------------------------------------------------------------------- 1 | package com.marginallyclever.donatello.ports; 2 | 3 | import com.marginallyclever.donatello.SwingProvider; 4 | import com.marginallyclever.donatello.graphview.GraphViewProvider; 5 | import com.marginallyclever.donatello.select.Select; 6 | import com.marginallyclever.donatello.select.SelectColor; 7 | import com.marginallyclever.nodegraphcore.port.Input; 8 | 9 | import java.awt.*; 10 | 11 | public class InputColor extends Input implements SwingProvider, GraphViewProvider { 12 | private SelectColor selectColor; 13 | 14 | public InputColor(String name, Color startingValue) throws IllegalArgumentException { 15 | super(name, Color.class, startingValue); 16 | } 17 | 18 | @Override 19 | public Select getSwingComponent(Component parent) { 20 | if(selectColor==null) { 21 | selectColor = new SelectColor(name,name,value,parent); 22 | selectColor.addSelectListener( evt -> { 23 | setValue(evt.getNewValue()); 24 | }); 25 | } 26 | return selectColor; 27 | } 28 | 29 | @Override 30 | public void paint(Graphics g, Rectangle box) { 31 | int w = (int)box.getWidth(); 32 | int h = (int)box.getHeight(); 33 | int x = (int)box.getX(); 34 | int y = (int)box.getY(); 35 | Color prev = g.getColor(); 36 | g.setColor(getValue()); 37 | g.fillRect(x, y, w, h); 38 | g.setColor(prev); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/com/marginallyclever/donatello/actions/ForciblyUpdateNodesAction.java: -------------------------------------------------------------------------------- 1 | package com.marginallyclever.donatello.actions; 2 | 3 | import com.marginallyclever.nodegraphcore.Node; 4 | import com.marginallyclever.donatello.Donatello; 5 | 6 | import javax.swing.*; 7 | import java.awt.event.ActionEvent; 8 | 9 | /** 10 | * Forces all of the editor's selected {@link Node}s to {@link Node#update()}. 11 | * status. 12 | * @author Dan Royer 13 | * @since 2022-02-21 14 | */ 15 | public class ForciblyUpdateNodesAction extends AbstractAction implements EditorAction { 16 | /** 17 | * The editor being affected. 18 | */ 19 | private final Donatello editor; 20 | 21 | /** 22 | * Constructor for subclasses to call. 23 | * @param name the name of this action visible on buttons and menu items. 24 | * @param editor the editor affected by this Action. 25 | */ 26 | public ForciblyUpdateNodesAction(String name, Donatello editor) { 27 | super(name); 28 | this.editor = editor; 29 | } 30 | 31 | @Override 32 | public void actionPerformed(ActionEvent e) { 33 | for(Node n : editor.getSelectedNodes()) { 34 | try { 35 | //n.update(); 36 | editor.submit(n); 37 | } catch(Exception e1) { 38 | // TODO report? 39 | } 40 | } 41 | } 42 | 43 | @Override 44 | public void updateEnableStatus() { 45 | setEnabled(!editor.getSelectedNodes().isEmpty()); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/com/marginallyclever/donatello/ports/InputInt.java: -------------------------------------------------------------------------------- 1 | package com.marginallyclever.donatello.ports; 2 | 3 | import com.marginallyclever.donatello.SwingProvider; 4 | import com.marginallyclever.donatello.select.Select; 5 | import com.marginallyclever.donatello.select.SelectInteger; 6 | import com.marginallyclever.nodegraphcore.port.Input; 7 | 8 | import java.awt.*; 9 | 10 | public class InputInt extends Input implements SwingProvider { 11 | private SelectInteger selectInteger; 12 | 13 | public InputInt(String name) { 14 | this(name, 0); 15 | } 16 | 17 | public InputInt(String name, Integer startingValue) throws IllegalArgumentException { 18 | super(name, Integer.class, startingValue); 19 | } 20 | 21 | @Override 22 | public Select getSwingComponent(Component parent) { 23 | if(selectInteger==null) { 24 | selectInteger = new SelectInteger(name,name,value); 25 | selectInteger.addSelectListener( evt -> setValue(evt.getNewValue()) ); 26 | } 27 | return selectInteger; 28 | } 29 | 30 | @Override 31 | public boolean isValidType(Class arg0) { 32 | if(Number.class.isAssignableFrom(arg0)) return true; 33 | return super.isValidType(arg0); 34 | } 35 | 36 | @Override 37 | public void setValue(Object arg0) { 38 | if(arg0 instanceof Number number) { 39 | super.setValue(number.intValue()); 40 | return; 41 | } 42 | super.setValue(arg0); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/com/marginallyclever/donatello/UpdateClock.java: -------------------------------------------------------------------------------- 1 | package com.marginallyclever.donatello; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | import java.util.Timer; 6 | import java.util.TimerTask; 7 | import java.util.concurrent.locks.ReentrantLock; 8 | 9 | /** 10 | * UpdateClock notifies all listeners at the scheduled interval. 11 | * @author Dan Royer 12 | * @since 2022-03-19 13 | */ 14 | public class UpdateClock extends TimerTask { 15 | private final Timer myTimer = new Timer("UpdateClock"); 16 | private final List listeners = new ArrayList<>(); 17 | private final int frequency; 18 | 19 | private final ReentrantLock myLock = new ReentrantLock(); 20 | 21 | public UpdateClock(int hz) { 22 | super(); 23 | this.frequency = hz; 24 | myTimer.schedule(this,0,frequency); 25 | } 26 | 27 | public void addListener(UpdateClockListener ear) { 28 | listeners.add(ear); 29 | } 30 | 31 | public void removeListener(UpdateClockListener ear) { 32 | listeners.remove(ear); 33 | } 34 | 35 | @Override 36 | public void run() { 37 | lock(); 38 | for( UpdateClockListener ear : listeners ) { 39 | ear.updateClockEvent(); 40 | } 41 | unlock(); 42 | } 43 | 44 | public void stop() { 45 | myTimer.cancel(); 46 | } 47 | 48 | public void lock() { 49 | myLock.lock(); 50 | } 51 | 52 | public void unlock() { 53 | myLock.unlock(); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/main/java/com/marginallyclever/donatello/actions/undoable/NodeIsolateAction.java: -------------------------------------------------------------------------------- 1 | package com.marginallyclever.donatello.actions.undoable; 2 | 3 | import com.marginallyclever.nodegraphcore.Node; 4 | import com.marginallyclever.nodegraphcore.Graph; 5 | import com.marginallyclever.donatello.Donatello; 6 | import com.marginallyclever.donatello.actions.EditorAction; 7 | import com.marginallyclever.donatello.edits.GraphIsolateEdit; 8 | 9 | import javax.swing.*; 10 | import java.awt.event.ActionEvent; 11 | 12 | /** 13 | * Disconnects the selected {@link Node}s from any non-selected {@link Node}s of a {@link Graph}. 14 | * @author Dan Royer 15 | * @since 2022-03-10 16 | */ 17 | public class NodeIsolateAction extends AbstractAction implements EditorAction { 18 | /** 19 | * The editor being affected. 20 | */ 21 | private final Donatello editor; 22 | 23 | /** 24 | * Constructor for subclasses to call. 25 | * @param name the name of this action visible on buttons and menu items. 26 | * @param editor the editor affected by this Action. 27 | */ 28 | public NodeIsolateAction(String name, Donatello editor) { 29 | super(name); 30 | this.editor = editor; 31 | } 32 | 33 | @Override 34 | public void actionPerformed(ActionEvent e) { 35 | editor.addEdit(new GraphIsolateEdit((String)this.getValue(Action.NAME),editor,editor.getSelectedNodes())); 36 | } 37 | 38 | @Override 39 | public void updateEnableStatus() { 40 | setEnabled(!editor.getSelectedNodes().isEmpty()); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/com/marginallyclever/donatello/contextsensitivetools/ContextSensitiveTool.java: -------------------------------------------------------------------------------- 1 | package com.marginallyclever.donatello.contextsensitivetools; 2 | 3 | import javax.swing.*; 4 | import java.awt.*; 5 | import java.awt.event.MouseAdapter; 6 | 7 | /** 8 | * A tool that is context-sensitive. It can be activated by a key press or by clicking on the graph. 9 | */ 10 | public abstract class ContextSensitiveTool extends MouseAdapter { 11 | private boolean isActive=false; 12 | 13 | public abstract void paint(Graphics g); 14 | 15 | public void attachKeyboardAdapter() {} 16 | public void detachKeyboardAdapter() {} 17 | 18 | public void attachMouseAdapter() {} 19 | public void detachMouseAdapter() {} 20 | 21 | public void restart() {} 22 | 23 | public abstract String getName(); 24 | 25 | /** 26 | * Returns the {@link KeyStroke} associated with activating this tool 27 | * @return the {@link KeyStroke} associated with activating this tool 28 | */ 29 | public abstract KeyStroke getAcceleratorKey(); 30 | 31 | public abstract Icon getSmallIcon(); 32 | 33 | protected void setActive(boolean isActive) { 34 | this.isActive = isActive; 35 | } 36 | 37 | public boolean isActive() { 38 | return isActive; 39 | } 40 | 41 | /** 42 | * Returns true if the tool should activate at this point. 43 | * @param p the point on the graph to check 44 | * @return true if the tool should activate at this point. 45 | */ 46 | public abstract boolean isCorrectContext(Point p); 47 | } 48 | -------------------------------------------------------------------------------- /src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | %d %boldCyan(%-5level) %boldGreen(%-15.-15logger{0}) - %msg %n 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | ${user.home}/Donatello/donatello.log 22 | 23 | ${user.home}/Donatello/donatello.%i.log 24 | 1 25 | 1 26 | 27 | 28 | 29 | 30 | %d{HH:mm:ss.SSS} [%thread] %-5level %logger{32} - %msg%n 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /src/main/java/com/marginallyclever/donatello/nodes/images/PrintImage.java: -------------------------------------------------------------------------------- 1 | package com.marginallyclever.donatello.nodes.images; 2 | 3 | import com.marginallyclever.donatello.ports.InputImage; 4 | import com.marginallyclever.donatello.ports.InputInt; 5 | import com.marginallyclever.nodegraphcore.Node; 6 | import com.marginallyclever.nodegraphcore.PrintWithGraphics; 7 | 8 | import javax.swing.*; 9 | import java.awt.*; 10 | import java.awt.image.BufferedImage; 11 | import java.util.Objects; 12 | 13 | /** 14 | * This {@link Node} can print a {@link BufferedImage} using the {@link Graphics} context. 15 | * @author Dan Royer 16 | * @since 2022-02-23 17 | */ 18 | public class PrintImage extends Node implements PrintWithGraphics { 19 | private final InputImage image = new InputImage("image"); 20 | private final InputInt layer = new InputInt("layer",2); 21 | 22 | /** 23 | * Constructor for subclasses to call. 24 | */ 25 | public PrintImage() { 26 | super("PrintImage"); 27 | addPort(image); 28 | addPort(layer); 29 | } 30 | 31 | @Override 32 | public void update() {} 33 | 34 | @Override 35 | public void print(Graphics g) { 36 | var img = image.getValue(); 37 | g.drawImage(img,-img.getWidth()/2,-img.getHeight()/2,null); 38 | } 39 | 40 | @Override 41 | public int getLayer() { 42 | return layer.getValue(); 43 | } 44 | 45 | @Override 46 | public Icon getIcon() { 47 | return new ImageIcon(Objects.requireNonNull(PrintImage.class.getResource("icons8-print-48.png"))); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/com/marginallyclever/donatello/DonatelloRegistry.java: -------------------------------------------------------------------------------- 1 | package com.marginallyclever.donatello; 2 | 3 | import com.marginallyclever.nodegraphcore.DAO4JSONFactory; 4 | import com.marginallyclever.nodegraphcore.DAORegistry; 5 | import com.marginallyclever.nodegraphcore.NodeFactory; 6 | import com.marginallyclever.nodegraphcore.NodeRegistry; 7 | import org.slf4j.Logger; 8 | import org.slf4j.LoggerFactory; 9 | 10 | /** 11 | *

Registers Swing {@link com.marginallyclever.nodegraphcore.Node}s to the {@link NodeFactory}. 12 | * Registers Swing types with the JSON DAO factory.

13 | *

Do not instantiate this class or call these directly. Instead call NodeFactory.loadRegistries() and DAO4JSONFactory.loadRegistries()

14 | * @author Dan Royer 15 | * @since 2022-02-11 16 | */ 17 | public class DonatelloRegistry implements NodeRegistry, DAORegistry { 18 | private static final Logger logger = LoggerFactory.getLogger(DonatelloRegistry.class); 19 | 20 | public String getName() { 21 | return "Donatello"; 22 | } 23 | 24 | @Override 25 | public void registerNodes() { 26 | logger.info("Registering donatello nodes"); 27 | NodeFactory.registerAllNodesInPackage("com.marginallyclever.donatello.nodes"); 28 | } 29 | 30 | @Override 31 | public void registerDAO() { 32 | logger.info("Registering donatello DAOs"); 33 | DAO4JSONFactory.registerAllDAOInPackage("com.marginallyclever.donatello.nodes"); 34 | DAO4JSONFactory.registerAllDAOInPackage("com.marginallyclever.donatello.ports"); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/com/marginallyclever/donatello/nodes/color/LoadColor.java: -------------------------------------------------------------------------------- 1 | package com.marginallyclever.donatello.nodes.color; 2 | 3 | import com.marginallyclever.donatello.nodes.Calculate; 4 | import com.marginallyclever.donatello.ports.InputInt; 5 | import com.marginallyclever.donatello.ports.OutputColor; 6 | import com.marginallyclever.nodegraphcore.Node; 7 | 8 | import javax.swing.*; 9 | import java.awt.*; 10 | import java.util.Objects; 11 | 12 | /** 13 | * Loads a {@link Color} into the graph. 14 | * @author Dan Royer 15 | * @since 2022-03-19 16 | */ 17 | public class LoadColor extends Node { 18 | private final InputInt r = new InputInt("r", 0); 19 | private final InputInt g = new InputInt("g", 0); 20 | private final InputInt b = new InputInt("b", 0); 21 | private final InputInt a = new InputInt("a", 0); 22 | private final OutputColor output = new OutputColor("output", Color.BLACK); 23 | 24 | /** 25 | * Constructor for subclasses to call. 26 | */ 27 | public LoadColor() { 28 | super("LoadColor"); 29 | addPort(r); 30 | addPort(g); 31 | addPort(b); 32 | addPort(a); 33 | addPort(output); 34 | } 35 | 36 | @Override 37 | public void update() { 38 | int rr = r.getValue(); 39 | int gg = g.getValue(); 40 | int bb = b.getValue(); 41 | int aa = a.getValue(); 42 | output.setValue(new Color(rr,gg,bb,aa)); 43 | } 44 | 45 | @Override 46 | public Icon getIcon() { 47 | return new ImageIcon(Objects.requireNonNull(LoadColor.class.getResource("icons8-color-48.png"))); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/com/marginallyclever/donatello/ports/InputDouble.java: -------------------------------------------------------------------------------- 1 | package com.marginallyclever.donatello.ports; 2 | 3 | import com.marginallyclever.donatello.SwingProvider; 4 | import com.marginallyclever.donatello.select.Select; 5 | import com.marginallyclever.donatello.select.SelectDouble; 6 | import com.marginallyclever.nodegraphcore.port.Input; 7 | 8 | import java.awt.*; 9 | 10 | public class InputDouble extends Input implements SwingProvider { 11 | private SelectDouble selectDouble; 12 | 13 | public InputDouble(String name) throws IllegalArgumentException { 14 | super(name, Double.class, 0.0); 15 | } 16 | 17 | public InputDouble(String name, Double startingValue) throws IllegalArgumentException { 18 | super(name, Double.class, startingValue); 19 | } 20 | 21 | @Override 22 | public Select getSwingComponent(Component parent) { 23 | if (selectDouble == null) { 24 | selectDouble = new SelectDouble(name, name, value); 25 | selectDouble.addSelectListener(evt -> { 26 | setValue(evt.getNewValue()); 27 | }); 28 | } 29 | return selectDouble; 30 | } 31 | 32 | @Override 33 | public boolean isValidType(Class arg0) { 34 | if(Number.class.isAssignableFrom(arg0)) return true; 35 | return super.isValidType(arg0); 36 | } 37 | 38 | @Override 39 | public void setValue(Object arg0) { 40 | if(arg0 instanceof Number number) { 41 | super.setValue(number.doubleValue()); 42 | return; 43 | } 44 | super.setValue(arg0); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/com/marginallyclever/donatello/nodes/images/ColorHelper.java: -------------------------------------------------------------------------------- 1 | package com.marginallyclever.donatello.nodes.images; 2 | 3 | import java.awt.*; 4 | 5 | public class ColorHelper { 6 | public static double[] IntToCMYK(int pixel) { 7 | //double a = 255-((pixel>>24) & 0xff); 8 | double r = 1.0-(double)((pixel >> 16) & 0xff) / 255.0; 9 | double g = 1.0-(double)((pixel >> 8) & 0xff) / 255.0; 10 | double b = 1.0-(double)((pixel ) & 0xff) / 255.0; 11 | // now convert to cmyk 12 | double k = Math.min(Math.min(r,g),b); // should be Math.max(Math.max(r,g),b) but colors are inverted. 13 | double ik = 1.0 - k; 14 | 15 | //if(ik<1.0/255.0) { 16 | // c=m=y=0; 17 | //} else { 18 | int c = (int)Math.max(0,Math.min(255, 255 * (r-k) / k )); 19 | int m = (int)Math.max(0,Math.min(255, 255 * (g-k) / k )); 20 | int y = (int)Math.max(0,Math.min(255, 255 * (b-k) / k )); 21 | int k2 = (int)Math.max(0,Math.min(255, 255 * ik )); 22 | //} 23 | return new double[]{c,m,y,k2}; 24 | } 25 | 26 | public static int RGBToInt(int r, int g, int b, int a) { 27 | a = ( a & 0xff ) << 24; 28 | r = ( r & 0xff ) << 16; 29 | g = ( g & 0xff ) << 8; 30 | b = ( b & 0xff ); 31 | return a|r|g|b; 32 | } 33 | 34 | public static int ColorToInt(Color color) { 35 | int a = ( color.getAlpha() & 0xff ) << 24; 36 | int r = ( color.getRed() & 0xff ) << 16; 37 | int g = ( color.getGreen() & 0xff ) << 8; 38 | int b = ( color.getBlue() & 0xff ); 39 | return a|r|g|b; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/com/marginallyclever/donatello/actions/ZoomToFitSelectedAction.java: -------------------------------------------------------------------------------- 1 | package com.marginallyclever.donatello.actions; 2 | 3 | import com.marginallyclever.donatello.Donatello; 4 | import com.marginallyclever.nodegraphcore.Node; 5 | 6 | import javax.swing.*; 7 | import java.awt.*; 8 | import java.awt.event.ActionEvent; 9 | import java.util.List; 10 | 11 | /** 12 | * Moves the camera to try and see all selected nodes at once. 13 | * @author Dan Royer 14 | * @since 2022-04-14 15 | */ 16 | public class ZoomToFitSelectedAction extends AbstractAction implements EditorAction { 17 | /** 18 | * The editor being affected. 19 | */ 20 | private final Donatello editor; 21 | 22 | /** 23 | * Constructor for subclasses to call. 24 | * @param name the name of this action visible on buttons and menu items. 25 | * @param editor the editor affected by this Action. 26 | */ 27 | public ZoomToFitSelectedAction(String name, Donatello editor) { 28 | super(name); 29 | this.editor = editor; 30 | } 31 | 32 | @Override 33 | public void actionPerformed(ActionEvent e) { 34 | List list = editor.getSelectedNodes(); 35 | if(list.isEmpty()) return; 36 | 37 | Rectangle rect = new Rectangle(list.get(0).getRectangle()); 38 | for( Node n : list ) { 39 | Rectangle r = n.getRectangle(); 40 | rect.add(r); 41 | } 42 | editor.getPaintArea().moveAndZoomToFit(rect); 43 | editor.repaint(); 44 | } 45 | 46 | @Override 47 | public void updateEnableStatus() { 48 | setEnabled(!editor.getSelectedNodes().isEmpty()); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/com/marginallyclever/donatello/actions/NodeEditAction.java: -------------------------------------------------------------------------------- 1 | package com.marginallyclever.donatello.actions; 2 | 3 | import com.marginallyclever.nodegraphcore.Node; 4 | import com.marginallyclever.donatello.Donatello; 5 | import com.marginallyclever.donatello.EditNodePanel; 6 | 7 | import javax.swing.*; 8 | import java.awt.event.ActionEvent; 9 | import java.util.List; 10 | 11 | /** 12 | * Launches the "edit node" dialog. 13 | * @author Dan Royer 14 | * @since 2022-02-21 15 | */ 16 | public class NodeEditAction extends AbstractAction implements EditorAction { 17 | /** 18 | * The editor being affected. 19 | */ 20 | private final Donatello editor; 21 | 22 | /** 23 | * Constructor for subclasses to call. 24 | * @param name the name of this action visible on buttons and menu items. 25 | * @param editor the editor affected by this Action. 26 | */ 27 | public NodeEditAction(String name, Donatello editor) { 28 | super(name); 29 | this.editor = editor; 30 | } 31 | 32 | @Override 33 | public void actionPerformed(ActionEvent e) { 34 | List nodes = editor.getSelectedNodes(); 35 | if(nodes.isEmpty()) return; 36 | Node firstNode = nodes.getFirst(); 37 | EditNodePanel.runAsDialog(firstNode,(JFrame)SwingUtilities.getWindowAncestor(editor),editor.getGraph()); 38 | if(firstNode.isDirty()) { 39 | editor.repaint(firstNode.getRectangle()); 40 | editor.submit(firstNode); 41 | } 42 | } 43 | 44 | @Override 45 | public void updateEnableStatus() { 46 | setEnabled(!editor.getSelectedNodes().isEmpty()); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/com/marginallyclever/donatello/actions/SelectionGrowAction.java: -------------------------------------------------------------------------------- 1 | package com.marginallyclever.donatello.actions; 2 | 3 | import com.marginallyclever.donatello.Donatello; 4 | import com.marginallyclever.donatello.NodeHelper; 5 | import com.marginallyclever.nodegraphcore.Node; 6 | 7 | import javax.swing.*; 8 | import java.awt.event.ActionEvent; 9 | import java.util.ArrayList; 10 | import java.util.List; 11 | 12 | /** 13 | * Select all the {@link com.marginallyclever.nodegraphcore.Node}s directly connected to the already selected nodes. 14 | */ 15 | public class SelectionGrowAction extends AbstractAction implements EditorAction { 16 | /** 17 | * The editor being affected. 18 | */ 19 | private final Donatello editor; 20 | 21 | /** 22 | * Constructor for subclasses to call. 23 | * @param name the name of this action visible on buttons and menu items. 24 | * @param editor the editor affected by this Action. 25 | */ 26 | public SelectionGrowAction(String name, Donatello editor) { 27 | super(name); 28 | this.editor = editor; 29 | } 30 | 31 | /** 32 | * Updates the model and repaints the panel. 33 | */ 34 | @Override 35 | public void actionPerformed(ActionEvent e) { 36 | List list = new ArrayList<>(editor.getSelectedNodes()); 37 | List adjacent = NodeHelper.getNeighbors(editor.getGraph(),list); 38 | list.addAll(adjacent); 39 | editor.setSelectedNodes(list); 40 | editor.repaint(); 41 | } 42 | 43 | @Override 44 | public void updateEnableStatus() { 45 | setEnabled(editor.getSelectedNodes().size() > 0); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/test/java/com/marginallyclever/donatello/select/SelectDoubleTest.java: -------------------------------------------------------------------------------- 1 | package com.marginallyclever.donatello.select; 2 | 3 | import org.junit.jupiter.api.Disabled; 4 | import org.junit.jupiter.api.Test; 5 | import org.slf4j.Logger; 6 | import org.slf4j.LoggerFactory; 7 | 8 | import java.util.Arrays; 9 | import java.util.Locale; 10 | 11 | import static org.junit.jupiter.api.Assertions.assertEquals; 12 | 13 | public class SelectDoubleTest { 14 | private static final Logger logger = LoggerFactory.getLogger(SelectDoubleTest.class); 15 | protected int testObservation; 16 | 17 | protected void testFloatField() { 18 | // test contructor(s) 19 | SelectDouble b = new SelectDouble("test","test",0); 20 | assertEquals(0.0f,b.getValue(),1e-6); 21 | b = new SelectDouble("test2","test2",0.1f); 22 | assertEquals(0.1f,b.getValue(),1e-6); 23 | b.setValue(2000.34f); 24 | 25 | try { 26 | Thread.sleep(200); 27 | } catch (InterruptedException e) { 28 | e.printStackTrace(); 29 | } 30 | assertEquals(2000.34f,b.getValue(),1e-3); 31 | } 32 | 33 | @Disabled("Very slow test, only run when needed") 34 | @Test 35 | public void testAllFloatFields() throws Exception { 36 | logger.debug("testAllFloatFields() start"); 37 | Locale original = Locale.getDefault(); 38 | Locale [] list = Locale.getAvailableLocales(); 39 | logger.info("Available Locales: {}", list.length); 40 | Arrays.stream(list).parallel().forEach(loc ->{ 41 | logger.debug("Locale={} {}", loc.toString(), loc.getDisplayLanguage()); 42 | Locale.setDefault(loc); 43 | testFloatField(); 44 | }); 45 | Locale.setDefault(original); 46 | logger.debug("testAllFloatFields() end"); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/com/marginallyclever/donatello/PropertiesHelper.java: -------------------------------------------------------------------------------- 1 | package com.marginallyclever.donatello; 2 | 3 | import com.marginallyclever.nodegraphcore.DAO4JSONFactory; 4 | import com.marginallyclever.nodegraphcore.NodeFactory; 5 | import org.slf4j.Logger; 6 | import org.slf4j.LoggerFactory; 7 | 8 | import java.util.*; 9 | import java.util.List; 10 | 11 | /** 12 | * Convenient methods for dealing with system properties. 13 | * @author Dan Royer 14 | * @since 2022-02-01 15 | */ 16 | public class PropertiesHelper { 17 | private static final Logger logger = LoggerFactory.getLogger(PropertiesHelper.class); 18 | 19 | public static void showProperties() { 20 | logger.debug("------------------------------------------------"); 21 | Properties p = System.getProperties(); 22 | List names = new ArrayList<>(p.stringPropertyNames()); 23 | Collections.sort(names); 24 | for (String name : names) { 25 | logger.debug("{} = {}", name, p.get(name)); 26 | } 27 | logger.debug("------------------------------------------------"); 28 | } 29 | 30 | public static void listAllNodes() { 31 | String sum = ""; 32 | String add = ""; 33 | for (String n : NodeFactory.getNames()) { 34 | sum += add + n; 35 | add = ", "; 36 | } 37 | logger.debug("Nodes: {}.",sum); 38 | } 39 | 40 | public static void listAllDAO() { 41 | String sum = ""; 42 | String add = ""; 43 | for(String n : DAO4JSONFactory.getNames()) { 44 | sum += add + n; 45 | add = ", "; 46 | } 47 | logger.debug("DAOs: {}.",sum); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/com/marginallyclever/donatello/actions/undoable/NodeCutAction.java: -------------------------------------------------------------------------------- 1 | package com.marginallyclever.donatello.actions.undoable; 2 | 3 | import com.marginallyclever.donatello.actions.EditorAction; 4 | import com.marginallyclever.donatello.actions.NodeCopyAction; 5 | 6 | import javax.swing.*; 7 | import java.awt.event.ActionEvent; 8 | 9 | /** 10 | * Performs an {@link NodeCopyAction} and then an {@link NodeDeleteAction}. 11 | * @author Dan Royer 12 | * @since 2022-02-21 13 | */ 14 | public class NodeCutAction extends AbstractAction implements EditorAction { 15 | /** 16 | * The delete action on which this action depends. 17 | */ 18 | private final NodeDeleteAction actionDeleteGraph; 19 | /** 20 | * The copy action on which this action depends. 21 | */ 22 | private final NodeCopyAction actionCopyGraph; 23 | 24 | /** 25 | * Constructor for subclasses to call. 26 | * @param name the name of this action visible on buttons and menu items. 27 | * @param actionDeleteGraph the delete action to use 28 | * @param actionCopyGraph the copy action to use 29 | */ 30 | public NodeCutAction(String name, NodeDeleteAction actionDeleteGraph, NodeCopyAction actionCopyGraph) { 31 | super(name); 32 | this.actionDeleteGraph = actionDeleteGraph; 33 | this.actionCopyGraph = actionCopyGraph; 34 | } 35 | 36 | @Override 37 | public void actionPerformed(ActionEvent e) { 38 | actionCopyGraph.actionPerformed(e); 39 | actionDeleteGraph.actionPerformed(e); 40 | } 41 | 42 | @Override 43 | public void updateEnableStatus() { 44 | setEnabled(actionDeleteGraph.isEnabled()); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/com/marginallyclever/donatello/ports/OutputImage.java: -------------------------------------------------------------------------------- 1 | package com.marginallyclever.donatello.ports; 2 | 3 | import com.marginallyclever.donatello.graphview.GraphViewProvider; 4 | import com.marginallyclever.nodegraphcore.port.Output; 5 | import com.marginallyclever.nodegraphcore.port.Port; 6 | 7 | import java.awt.*; 8 | import java.awt.image.BufferedImage; 9 | 10 | public class OutputImage extends Output implements GraphViewProvider { 11 | public OutputImage(String _name) throws IllegalArgumentException { 12 | super(_name, BufferedImage.class, new BufferedImage(1,1,BufferedImage.TYPE_INT_ARGB)); 13 | } 14 | 15 | @Override 16 | public Rectangle getRectangle() { 17 | double w = value.getWidth(); 18 | double h = value.getHeight(); 19 | if(w maxW) { 38 | h = h * maxW / w; 39 | w = maxW; 40 | } 41 | int x = (int)box.getX(); 42 | int y = (int)box.getY(); 43 | g.drawImage(img, x, y, w, h, null); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/com/marginallyclever/donatello/select/SelectSpinner.java: -------------------------------------------------------------------------------- 1 | package com.marginallyclever.donatello.select; 2 | 3 | import javax.swing.*; 4 | import java.awt.*; 5 | import java.util.ArrayList; 6 | import java.util.List; 7 | 8 | /** 9 | * A {@link SelectSpinner} is a {@link Select} that uses a {@link JSpinner} to select a value. 10 | */ 11 | public class SelectSpinner extends Select { 12 | private final JSpinner field; 13 | private final JLabel label; 14 | 15 | public SelectSpinner(String internalName, String labelKey, int min, int max, int value) { 16 | super(internalName); 17 | 18 | List list = new ArrayList<>(); 19 | for (int i = min; i<= max; i++) { 20 | list.add(i); 21 | } 22 | field = new JSpinner(new SpinnerListModel(list)); 23 | field.setName(internalName+".field"); 24 | 25 | Dimension d = field.getPreferredSize(); 26 | d.width = 50; 27 | field.setPreferredSize(d); 28 | field.setValue(value); 29 | 30 | label = createLabel(labelKey); 31 | } 32 | 33 | @Override 34 | public void attach(JComponent panel, GridBagConstraints gbc) { 35 | gbc.anchor = GridBagConstraints.LINE_START; 36 | gbc.gridx=0; 37 | panel.add(label,gbc); 38 | gbc.gridx=1; 39 | gbc.anchor = GridBagConstraints.LINE_END; 40 | panel.add(field,gbc); 41 | } 42 | 43 | @Override 44 | public void setReadOnly(boolean state) { 45 | field.setEnabled(!state); 46 | } 47 | 48 | public int getValue() { 49 | return (int) field.getValue(); 50 | } 51 | 52 | public void setValue(int v) { 53 | field.setValue(v); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/test/java/com/marginallyclever/donatello/simpleparser/SimpleParserTest.java: -------------------------------------------------------------------------------- 1 | package com.marginallyclever.donatello.simpleparser; 2 | 3 | import org.junit.jupiter.api.Assertions; 4 | import org.junit.jupiter.api.Test; 5 | 6 | public class SimpleParserTest { 7 | @Test 8 | public void test() { 9 | Assertions.assertEquals(51,SimpleParser.evaluate("51")); 10 | Assertions.assertEquals(-12,SimpleParser.evaluate("3 + 5 * (2 - 8) / 2.0")); 11 | Assertions.assertEquals(1,SimpleParser.evaluate("sin(PI / 2)")); // 1.0 12 | Assertions.assertEquals(1,SimpleParser.evaluate("cos(0)")); // 1.0 13 | Assertions.assertEquals(0.999999,SimpleParser.evaluate("tan(PI / 4)"),1e-6); 14 | Assertions.assertEquals(0.7615941559557649,SimpleParser.evaluate("tanh(1)"),1e-6); // Hyperbolic tangent 15 | Assertions.assertEquals(5,SimpleParser.evaluate("hypot(3, 4)")); // Pythagorean theorem 16 | Assertions.assertEquals(5,SimpleParser.evaluate("sqrt(25)")); 17 | Assertions.assertEquals(-2,SimpleParser.evaluate("-5 + 3")); 18 | Assertions.assertEquals(1,SimpleParser.evaluate("10 % 3")); 19 | Assertions.assertEquals(57.29577951308232,SimpleParser.evaluate("(180 / PI)"),1e-6); // Convert to degrees 20 | Assertions.assertEquals(0.017453292519943295,SimpleParser.evaluate("(PI / 180)"),1e-6); // Convert to radians 21 | Assertions.assertEquals(3,SimpleParser.evaluate("floor(PI)")); 22 | Assertions.assertEquals(4,SimpleParser.evaluate("ceil(PI)")); 23 | Assertions.assertEquals(0,SimpleParser.evaluate("atan2(0, 1)")); 24 | Assertions.assertThrows(IllegalArgumentException.class,()->SimpleParser.evaluate("random")); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/com/marginallyclever/donatello/select/SelectToggleButton.java: -------------------------------------------------------------------------------- 1 | package com.marginallyclever.donatello.select; 2 | 3 | import javax.swing.*; 4 | import java.awt.*; 5 | 6 | /** 7 | * A toggle button that does nothing until you attach an observer. 8 | * @author Dan Royer 9 | * @since 7.24.0 10 | */ 11 | public class SelectToggleButton extends Select { 12 | private final JToggleButton button; 13 | 14 | public SelectToggleButton(String internalName, AbstractAction action, GridBagConstraints gbc) { 15 | super(internalName); 16 | button = new JToggleButton(action); 17 | button.setName(internalName+".button"); 18 | } 19 | 20 | public SelectToggleButton(String internalName, String labelText) { 21 | super(internalName); 22 | 23 | button = new JToggleButton(labelText); 24 | 25 | button.addActionListener((e) -> { 26 | fireSelectEvent(!button.isSelected(),button.isSelected()); 27 | }); 28 | } 29 | 30 | @Override 31 | public void attach(JComponent panel, GridBagConstraints gbc) { 32 | gbc.anchor = GridBagConstraints.CENTER; 33 | gbc.fill = GridBagConstraints.HORIZONTAL; 34 | panel.add(button,gbc); 35 | } 36 | 37 | @Override 38 | public void setReadOnly(boolean state) { 39 | button.setEnabled(!state); 40 | } 41 | 42 | public void doClick() { 43 | if(button!=null) button.doClick(); 44 | } 45 | 46 | public void setText(String label) { 47 | if(button!=null) button.setText(label); 48 | } 49 | 50 | public void setEnabled(boolean b) { 51 | if(button!=null) button.setEnabled(b); 52 | } 53 | 54 | public void setForeground(Color fg) { 55 | if(button!=null) button.setForeground(fg); 56 | } 57 | 58 | public boolean isSelected() { 59 | return button.isSelected(); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/main/java/com/marginallyclever/donatello/edits/ConnectionRemoveEdit.java: -------------------------------------------------------------------------------- 1 | package com.marginallyclever.donatello.edits; 2 | 3 | import com.marginallyclever.nodegraphcore.Connection; 4 | import com.marginallyclever.donatello.Donatello; 5 | 6 | import javax.swing.undo.CannotRedoException; 7 | import javax.swing.undo.CannotUndoException; 8 | 9 | public class ConnectionRemoveEdit extends SignificantUndoableEdit { 10 | private final String name; 11 | private final Donatello editor; 12 | private final Connection connection; 13 | 14 | public ConnectionRemoveEdit(String name, Donatello editor, Connection connection) { 15 | super(); 16 | this.name = name; 17 | this.editor = editor; 18 | this.connection = connection; 19 | doIt(); 20 | } 21 | 22 | @Override 23 | public String getPresentationName() { 24 | return name; 25 | } 26 | 27 | @Override 28 | public void undo() throws CannotUndoException { 29 | editor.lockClock(); 30 | try { 31 | editor.getGraph().add(connection); 32 | connection.apply(); 33 | editor.submit(connection.getTo()); 34 | } 35 | finally { 36 | editor.unlockClock(); 37 | } 38 | super.undo(); 39 | } 40 | 41 | private void doIt() { 42 | editor.lockClock(); 43 | try { 44 | editor.getGraph().remove(connection); 45 | connection.reset(); 46 | editor.submit(connection.getTo()); 47 | } 48 | finally { 49 | editor.unlockClock(); 50 | } 51 | } 52 | 53 | @Override 54 | public void redo() throws CannotRedoException { 55 | doIt(); 56 | super.redo(); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/main/java/com/marginallyclever/donatello/actions/undoable/NodePasteAction.java: -------------------------------------------------------------------------------- 1 | package com.marginallyclever.donatello.actions.undoable; 2 | 3 | import com.marginallyclever.nodegraphcore.Graph; 4 | import com.marginallyclever.donatello.Donatello; 5 | import com.marginallyclever.donatello.actions.EditorAction; 6 | import com.marginallyclever.donatello.edits.GraphPasteEdit; 7 | 8 | import javax.swing.*; 9 | import java.awt.*; 10 | import java.awt.event.ActionEvent; 11 | 12 | /** 13 | * Duplicates the editor's copy buffer, inserts the contents into the editor's current {@link Graph}, and sets the 14 | * new content as the editor's selected items. 15 | * @author Dan Royer 16 | * @since 2022-02-21 17 | */ 18 | public class NodePasteAction extends AbstractAction implements EditorAction { 19 | /** 20 | * The editor being affected. 21 | */ 22 | private final Donatello editor; 23 | 24 | /** 25 | * Constructor for subclasses to call. 26 | * @param name the name of this action visible on buttons and menu items. 27 | * @param editor the editor affected by this Action. 28 | */ 29 | public NodePasteAction(String name, Donatello editor) { 30 | super(name); 31 | this.editor = editor; 32 | } 33 | 34 | @Override 35 | public void actionPerformed(ActionEvent e) { 36 | var mp = editor.getPaintArea().getPreviousMousePosition(); 37 | Point np = (mp==null) ? new Point() : editor.getPaintArea().transformScreenToWorldPoint(mp); 38 | editor.addEdit(new GraphPasteEdit((String)this.getValue(Action.NAME),editor,editor.getCopiedGraph(),np)); 39 | } 40 | 41 | @Override 42 | public void updateEnableStatus() { 43 | setEnabled(!editor.getCopiedGraph().isEmpty()); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/com/marginallyclever/donatello/NodeHelper.java: -------------------------------------------------------------------------------- 1 | package com.marginallyclever.donatello; 2 | 3 | import com.marginallyclever.nodegraphcore.Node; 4 | import com.marginallyclever.nodegraphcore.Connection; 5 | import com.marginallyclever.nodegraphcore.Graph; 6 | 7 | import java.util.ArrayList; 8 | import java.util.List; 9 | 10 | /** 11 | * Helper class for working with {@link Node} objects in a {@link Graph}. 12 | * Provides methods to retrieve outgoing connections and neighbors of nodes. 13 | */ 14 | public class NodeHelper { 15 | public static List getAllOutgoingConnections(Graph graph, Node source) { 16 | List adjacent = new ArrayList<>(); 17 | 18 | for( Connection c : graph.getConnections() ) { 19 | if (c.isConnectedTo(source) && c.getFrom()==source) { 20 | adjacent.add(c.getTo()); 21 | } 22 | } 23 | 24 | return adjacent; 25 | } 26 | 27 | public static List getNeighbors(Graph graph, List startingNodes) { 28 | List adjacent = new ArrayList<>(); 29 | List potential = new ArrayList<>(); 30 | 31 | for( Connection c : graph.getConnections() ) { 32 | for( Node n : startingNodes ) { 33 | if (c.isConnectedTo(n)) { 34 | potential.add(c); 35 | break; 36 | } 37 | } 38 | } 39 | 40 | for( Node n : graph.getNodes() ) { 41 | if(startingNodes.contains(n)) continue; 42 | for( Connection c : potential ) { 43 | if (c.isConnectedTo(n)) { 44 | adjacent.add(n); 45 | } 46 | } 47 | } 48 | 49 | return adjacent; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/com/marginallyclever/donatello/nodes/color/ColorToRGBA.java: -------------------------------------------------------------------------------- 1 | package com.marginallyclever.donatello.nodes.color; 2 | 3 | import com.marginallyclever.nodegraphcore.*; 4 | import com.marginallyclever.nodegraphcore.port.Input; 5 | import com.marginallyclever.nodegraphcore.port.Output; 6 | 7 | import javax.swing.*; 8 | import java.awt.*; 9 | import java.util.Objects; 10 | 11 | /** 12 | * Separates a color into its four component RGBA channels. Each channel is a value 0...1. 13 | * @author Dan Royer 14 | * @since 2022-03-19 15 | */ 16 | public class ColorToRGBA extends Node { 17 | private final Input color = new Input<>("color", Color.class, new Color(0,0,0,0)); 18 | private final Output red = new Output<>("red", Number.class, 0); 19 | private final Output green = new Output<>("green", Number.class, 0); 20 | private final Output blue = new Output<>("blue", Number.class, 0); 21 | private final Output alpha = new Output<>("alpha", Number.class, 0); 22 | 23 | /** 24 | * Constructor for subclasses to call. 25 | */ 26 | public ColorToRGBA() { 27 | super("ColorToRGBA"); 28 | addPort(color); 29 | addPort(red ); 30 | addPort(green); 31 | addPort(blue ); 32 | addPort(alpha); 33 | } 34 | 35 | @Override 36 | public void update() { 37 | Color c = color.getValue(); 38 | red .setValue(c.getRed() /255.0); 39 | green.setValue(c.getGreen()/255.0); 40 | blue .setValue(c.getBlue() /255.0); 41 | alpha.setValue(c.getAlpha()/255.0); 42 | } 43 | 44 | @Override 45 | public Icon getIcon() { 46 | return new ImageIcon(Objects.requireNonNull(LoadColor.class.getResource("icons8-rgb-48.png"))); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/com/marginallyclever/donatello/actions/PlayAction.java: -------------------------------------------------------------------------------- 1 | package com.marginallyclever.donatello.actions; 2 | 3 | import com.marginallyclever.donatello.Donatello; 4 | 5 | import javax.swing.*; 6 | import java.awt.event.ActionEvent; 7 | import java.util.Objects; 8 | 9 | public class PlayAction extends AbstractAction implements EditorAction { 10 | private final Donatello editor; 11 | private final GraphUpdateAction graphUpdateAction; 12 | 13 | public PlayAction(String name, Donatello editor, GraphUpdateAction graphUpdateAction) { 14 | super(name); 15 | this.editor = editor; 16 | this.graphUpdateAction = graphUpdateAction; 17 | putValue(SHORT_DESCRIPTION, "Update the graph automatically."); 18 | updateButtonState(); 19 | } 20 | 21 | @Override 22 | public void actionPerformed(ActionEvent e) { 23 | boolean keepGoing = !editor.getKeepGoing(); 24 | editor.setKeepGoing(keepGoing); 25 | graphUpdateAction.setEnabled(keepGoing); 26 | updateButtonState(); 27 | } 28 | 29 | private void updateButtonState() { 30 | boolean keepGoing = editor.getKeepGoing(); 31 | this.putValue(Action.SMALL_ICON, keepGoing 32 | ? new ImageIcon(Objects.requireNonNull(getClass().getResource("/com/marginallyclever/donatello/icons8-pause-16.png"))) 33 | : new ImageIcon(Objects.requireNonNull(getClass().getResource("/com/marginallyclever/donatello/icons8-play-16.png"))) 34 | ); 35 | this.putValue(Action.NAME, keepGoing ? "Pause" : "Play"); 36 | } 37 | 38 | /** 39 | * Called by the {@link Donatello} when the editor believes it is time to confirm enable status. 40 | */ 41 | @Override 42 | public void updateEnableStatus() { 43 | setEnabled(!editor.getGraph().isEmpty()); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/com/marginallyclever/donatello/edits/MoveNodesEdit.java: -------------------------------------------------------------------------------- 1 | package com.marginallyclever.donatello.edits; 2 | 3 | import com.marginallyclever.nodegraphcore.Node; 4 | import com.marginallyclever.donatello.Donatello; 5 | 6 | import javax.swing.undo.CannotRedoException; 7 | import javax.swing.undo.CannotUndoException; 8 | import java.util.ArrayList; 9 | import java.util.List; 10 | 11 | /** 12 | * Created by the {@link com.marginallyclever.donatello.contextsensitivetools.NodeMoveTool} so that reorganizations 13 | * can be undone. This edit is created when a move is complete and should record only the significant relative change 14 | * in position. Since the tool already completed the move there is no need to apply the move in the constructor. 15 | * @author Dan Royer 16 | * @since 2022-03-11 17 | */ 18 | public class MoveNodesEdit extends SignificantUndoableEdit { 19 | private final String name; 20 | private final Donatello editor; 21 | private final int dx,dy; 22 | private final List selected = new ArrayList<>(); 23 | 24 | public MoveNodesEdit(String name, Donatello editor, int dx, int dy) { 25 | super(); 26 | this.name = name; 27 | this.editor = editor; 28 | this.dx = dx; 29 | this.dy = dy; 30 | this.selected.addAll(editor.getSelectedNodes()); 31 | } 32 | 33 | @Override 34 | public String getPresentationName() { 35 | return name; 36 | } 37 | 38 | @Override 39 | public void undo() throws CannotUndoException { 40 | editor.setSelectedNodes(selected); 41 | editor.moveSelectedNodes(-dx,-dy); 42 | super.undo(); 43 | } 44 | 45 | @Override 46 | public void redo() throws CannotRedoException { 47 | editor.setSelectedNodes(selected); 48 | editor.moveSelectedNodes(dx,dy); 49 | super.redo(); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/test/java/com/marginallyclever/donatello/nodes/CalculateTest.java: -------------------------------------------------------------------------------- 1 | package com.marginallyclever.donatello.nodes; 2 | 3 | import com.marginallyclever.nodegraphcore.DAO4JSONFactory; 4 | import org.json.JSONObject; 5 | import org.junit.jupiter.api.AfterAll; 6 | import org.junit.jupiter.api.Assertions; 7 | import org.junit.jupiter.api.BeforeAll; 8 | import org.junit.jupiter.api.Test; 9 | 10 | public class CalculateTest { 11 | @BeforeAll 12 | public static void beforeAll() { 13 | DAO4JSONFactory.loadRegistries(); 14 | } 15 | 16 | @AfterAll 17 | public static void afterAll() { 18 | DAO4JSONFactory.clear(); 19 | } 20 | 21 | /** 22 | * Create a Calculate node. set the inputCount to 2. set the inputExpression to "a+b". 23 | * set the inputA to 1.0. set the inputB to 2.0. update the node. check that the output is 3.0. 24 | * save the Calculate node to a JSONObject. load the Calculate node from the JSONObject. 25 | * check that the inputCount is 2. 26 | */ 27 | @Test 28 | public void testCalculate() { 29 | Calculate before = new Calculate(); 30 | before.getPort("count").setValue(2); 31 | before.getPort("expression").setValue("a+b"); 32 | before.update(); 33 | before.getPort("a").setValue(1.0); 34 | before.getPort("b").setValue(2.0); 35 | before.update(); 36 | Assertions.assertEquals(3.0, (double)before.getPort("result").getValue(), 0.0001); 37 | 38 | JSONObject json = before.toJSON(); 39 | Calculate after = new Calculate(); 40 | after.fromJSON(json); 41 | Assertions.assertEquals(5, after.getPorts().size()); 42 | Assertions.assertEquals(2, after.getPort("count").getValue()); 43 | after.update(); 44 | Assertions.assertEquals(3.0, after.getPort("result").getValue()); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/com/marginallyclever/donatello/edits/NodeAddEdit.java: -------------------------------------------------------------------------------- 1 | package com.marginallyclever.donatello.edits; 2 | 3 | import com.marginallyclever.nodegraphcore.Node; 4 | import com.marginallyclever.donatello.Donatello; 5 | 6 | import javax.swing.undo.CannotRedoException; 7 | import javax.swing.undo.CannotUndoException; 8 | import java.util.ArrayList; 9 | import java.util.List; 10 | 11 | public class NodeAddEdit extends SignificantUndoableEdit { 12 | private final String name; 13 | private final Donatello editor; 14 | private final Node node; 15 | 16 | public NodeAddEdit(String name, Donatello editor, Node node) { 17 | super(); 18 | this.name = name; 19 | this.editor = editor; 20 | this.node = node; 21 | doIt(); 22 | } 23 | 24 | @Override 25 | public String getPresentationName() { 26 | return name; 27 | } 28 | 29 | public void doIt() { 30 | editor.lockClock(); 31 | try { 32 | editor.getGraph().add(node); 33 | editor.setSelectedNode(node); 34 | editor.repaint(node.getRectangle()); 35 | } 36 | finally { 37 | editor.unlockClock(); 38 | } 39 | } 40 | 41 | @Override 42 | public void undo() throws CannotUndoException { 43 | editor.lockClock(); 44 | try { 45 | editor.getGraph().remove(node); 46 | List nodes = new ArrayList<>(editor.getSelectedNodes()); 47 | nodes.remove(node); 48 | editor.setSelectedNodes(nodes); 49 | } 50 | finally { 51 | editor.unlockClock(); 52 | } 53 | 54 | editor.repaint(node.getRectangle()); 55 | 56 | super.undo(); 57 | } 58 | 59 | @Override 60 | public void redo() throws CannotRedoException { 61 | doIt(); 62 | super.redo(); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/main/java/com/marginallyclever/donatello/select/Select.java: -------------------------------------------------------------------------------- 1 | package com.marginallyclever.donatello.select; 2 | 3 | import javax.swing.*; 4 | import javax.swing.event.EventListenerList; 5 | import java.awt.*; 6 | 7 | /** 8 | * Base class for all Select. A Select is a UI panel item the user can control. 9 | * This system provides consistent look and behavior across all elements in the app. 10 | * @author Dan Royer 11 | * @since 7.24.0 12 | */ 13 | public abstract class Select { 14 | private final EventListenerList listeners = new EventListenerList(); 15 | private String name; 16 | 17 | protected Select(String name) { 18 | super(); 19 | setName(name); 20 | } 21 | 22 | public String getName() { 23 | return name; 24 | } 25 | 26 | public void setName(String name) { 27 | this.name = name; 28 | } 29 | 30 | public void addSelectListener(SelectListener listener) { 31 | listeners.add(SelectListener.class,listener); 32 | } 33 | 34 | public void removeSelectListener(SelectListener listener) { 35 | listeners.remove(SelectListener.class,listener); 36 | } 37 | 38 | protected void fireSelectEvent(Object oldValue,Object newValue) { 39 | try { 40 | SelectEvent evt = new SelectEvent(this, oldValue, newValue); 41 | for (SelectListener listener : listeners.getListeners(SelectListener.class)) { 42 | listener.selectEvent(evt); 43 | } 44 | } catch (Exception e) { 45 | e.printStackTrace(); 46 | } 47 | } 48 | 49 | protected JLabel createLabel(String labelKey) { 50 | JLabel label = new JLabel(labelKey, JLabel.LEADING); 51 | label.setName(getName()+".label"); 52 | return label; 53 | } 54 | 55 | /** 56 | * 57 | * @param panel the parent component 58 | * @param gbc the GridBagConstraints for this component 59 | */ 60 | abstract public void attach(JComponent panel, GridBagConstraints gbc); 61 | 62 | abstract public void setReadOnly(boolean b); 63 | } 64 | -------------------------------------------------------------------------------- /src/main/java/com/marginallyclever/donatello/nodes/color/ColorToCMYK.java: -------------------------------------------------------------------------------- 1 | package com.marginallyclever.donatello.nodes.color; 2 | 3 | import com.marginallyclever.donatello.nodes.images.ColorHelper; 4 | import com.marginallyclever.nodegraphcore.*; 5 | import com.marginallyclever.nodegraphcore.port.Input; 6 | import com.marginallyclever.nodegraphcore.port.Output; 7 | 8 | import javax.swing.*; 9 | import java.awt.*; 10 | import java.util.Objects; 11 | 12 | /** 13 | * Separates a color into its four component CMYK channels. Each channel is a value 0...1. 14 | * @author Dan Royer 15 | * @since 2022-03-19 16 | */ 17 | public class ColorToCMYK extends Node { 18 | private final Input color = new Input<>("color", Color.class, new Color(0,0,0,0)); 19 | private final Output cyan = new Output<>("cyan", Number.class, 0); 20 | private final Output magenta = new Output<>("magenta", Number.class, 0); 21 | private final Output yellow = new Output<>("yellow", Number.class, 0); 22 | private final Output black = new Output<>("black", Number.class, 0); 23 | 24 | /** 25 | * Constructor for subclasses to call. 26 | */ 27 | public ColorToCMYK() { 28 | super("ColorToCMYK"); 29 | addPort(color); 30 | addPort(cyan); 31 | addPort(magenta); 32 | addPort(yellow); 33 | addPort(black); 34 | } 35 | 36 | @Override 37 | public void update() { 38 | Color c = color.getValue(); 39 | double [] cmyk = ColorHelper.IntToCMYK(ColorHelper.ColorToInt(c)); 40 | cyan.setValue( cmyk[0]/255.0); 41 | magenta.setValue(cmyk[1]/255.0); 42 | yellow.setValue( cmyk[2]/255.0); 43 | black.setValue( cmyk[3]/255.0); 44 | } 45 | 46 | @Override 47 | public Icon getIcon() { 48 | return new ImageIcon(Objects.requireNonNull(LoadColor.class.getResource("icons8-cmyk-48.png"))); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/com/marginallyclever/donatello/select/SelectBoolean.java: -------------------------------------------------------------------------------- 1 | package com.marginallyclever.donatello.select; 2 | 3 | import javax.swing.*; 4 | import javax.swing.border.EmptyBorder; 5 | import java.awt.*; 6 | 7 | /** 8 | * A JCheckBox that sets itself up to format true/false. 9 | * @author Dan Royer 10 | * @since 7.24.0 11 | */ 12 | public class SelectBoolean extends Select { 13 | private final JLabel label; 14 | private final JCheckBox field; 15 | 16 | /** 17 | * Create a new SelectBoolean 18 | * @param internalName the name of this SelectBoolean, used for debugging 19 | * @param labelKey the key to use for the label, visible to users 20 | * @param defaultValue the default value 21 | */ 22 | public SelectBoolean(String internalName,String labelKey,boolean defaultValue) { 23 | super(internalName); 24 | label = createLabel(labelKey); 25 | field = createField(defaultValue); 26 | field.setName(internalName+".field"); 27 | } 28 | 29 | @Override 30 | public void attach(JComponent panel, GridBagConstraints gbc) { 31 | gbc.anchor = GridBagConstraints.LINE_START; 32 | gbc.gridx=0; 33 | panel.add(label,gbc); 34 | gbc.gridx=1; 35 | gbc.anchor = GridBagConstraints.LINE_END; 36 | panel.add(field,gbc); 37 | } 38 | 39 | @Override 40 | public void setReadOnly(boolean state) { 41 | field.setEnabled(!state); 42 | } 43 | 44 | private JCheckBox createField(boolean arg0) { 45 | JCheckBox field = new JCheckBox(); 46 | field.setName(getName()+".field"); 47 | field.setSelected(arg0); 48 | field.setBorder(new EmptyBorder(0,0,0,0)); 49 | field.addItemListener((e)-> { 50 | boolean newValue = field.isSelected(); 51 | boolean oldValue = !newValue; 52 | fireSelectEvent(oldValue, newValue); 53 | }); 54 | return field; 55 | } 56 | 57 | public boolean isSelected() { 58 | return field.isSelected(); 59 | } 60 | 61 | public void setSelected(boolean b) { 62 | field.setSelected(b); 63 | } 64 | 65 | } 66 | -------------------------------------------------------------------------------- /src/main/java/com/marginallyclever/donatello/nodes/images/Invert.java: -------------------------------------------------------------------------------- 1 | package com.marginallyclever.donatello.nodes.images; 2 | 3 | import com.marginallyclever.donatello.ports.InputImage; 4 | import com.marginallyclever.donatello.ports.OutputImage; 5 | import com.marginallyclever.nodegraphcore.Node; 6 | 7 | import javax.swing.*; 8 | import java.awt.image.BufferedImage; 9 | import java.util.Objects; 10 | import java.util.stream.IntStream; 11 | 12 | /** 13 | * Invert the colors in an image. 14 | */ 15 | public class Invert extends Node { 16 | private final InputImage before = new InputImage("before"); 17 | private final OutputImage after = new OutputImage("after"); 18 | 19 | public Invert() { 20 | super("Invert"); 21 | addPort(before); 22 | addPort(after); 23 | } 24 | 25 | @Override 26 | public void update() { 27 | var img = before.getValue(); 28 | if (img == null) { 29 | after.setValue(new BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB)); 30 | return; 31 | } 32 | 33 | int width = img.getWidth(); 34 | int height = img.getHeight(); 35 | var dst = new BufferedImage(width, height, img.getType()); 36 | 37 | IntStream.range(0,height).parallel().forEach(y->{ 38 | IntStream.range(0,width).parallel().forEach(x->{ 39 | int rgb = img.getRGB(x, y); 40 | int r = 255 - ((rgb >> 16) & 0xFF); 41 | int g = 255 - ((rgb >> 8) & 0xFF); 42 | int b = 255 - (rgb & 0xFF); 43 | int invertedRGB = (rgb & 0xFF000000) | (r << 16) | (g << 8) | b; 44 | dst.setRGB(x, y, invertedRGB); 45 | }); 46 | }); 47 | 48 | after.setValue(dst); 49 | } 50 | 51 | @Override 52 | public Icon getIcon() { 53 | return new ImageIcon(Objects.requireNonNull(Invert.class.getResource("icons8-invert-colors-48.png"))); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/main/java/com/marginallyclever/donatello/actions/GraphFoldAction.java: -------------------------------------------------------------------------------- 1 | package com.marginallyclever.donatello.actions; 2 | 3 | import com.marginallyclever.nodegraphcore.Node; 4 | import com.marginallyclever.nodegraphcore.Graph; 5 | import com.marginallyclever.donatello.Donatello; 6 | import com.marginallyclever.donatello.actions.undoable.NodeCutAction; 7 | 8 | import javax.swing.*; 9 | import java.awt.event.ActionEvent; 10 | 11 | /** 12 | * Collapses the editor's selected {@link Node}s into a new sub-graph. 13 | * @author Dan Royer 14 | * @since 2022-02-21 15 | */ 16 | public class GraphFoldAction extends AbstractAction implements EditorAction { 17 | /** 18 | * The editor being affected. 19 | */ 20 | private final Donatello editor; 21 | /** 22 | * The cut action on which this action depends. 23 | */ 24 | private final NodeCutAction nodeCutAction; 25 | 26 | /** 27 | * Constructor for subclasses to call. 28 | * @param name the name of this action visible on buttons and menu items. 29 | * @param editor the editor affected by this Action. 30 | * @param NodeCutAction the cut action to use with this Action. 31 | */ 32 | public GraphFoldAction(String name, Donatello editor, NodeCutAction NodeCutAction) { 33 | super(name); 34 | this.editor = editor; 35 | this.nodeCutAction = NodeCutAction; 36 | } 37 | 38 | @Override 39 | public void actionPerformed(ActionEvent e) { 40 | Graph preserveCopyBehaviour = editor.getCopiedGraph().deepCopy(); 41 | 42 | nodeCutAction.actionPerformed(e); 43 | Graph justCut = editor.getCopiedGraph().deepCopy(); 44 | editor.getGraph().add(justCut); 45 | justCut.setPosition(editor.getPopupPoint()); 46 | 47 | editor.setCopiedGraph(preserveCopyBehaviour); 48 | } 49 | 50 | @Override 51 | public void updateEnableStatus() { 52 | setEnabled(!editor.getSelectedNodes().isEmpty()); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | :+1::tada: First off, thanks for taking the time to contribute! :tada::+1: 4 | 5 | The following is a set of guidelines for contributing. These are just guidelines, not rules, use your best judgment and feel free to propose changes. 6 | 7 | - Reporting bugs 8 | - Writing code 9 | - Contributing new code to the project 10 | - Discussing features 11 | - Code of conduct 12 | 13 | ## Reporting bugs 14 | 15 | Please use the Github [Issues](https://github.com/MarginallyClever/NodeGraphCore/issues) and fill out the template to make good bug reports. 16 | 17 | ## Contributing new code 18 | 19 | Please see the [Getting Started Guide](https://github.com/MarginallyClever/NodeGraphCore/wiki/Getting-Started) to get setup and running. 20 | 21 | We use github to host code, to track issues and feature requests, as well as accept pull requests. 22 | 23 | We Use [Github Flow](https://guides.github.com/introduction/flow/index.html), So All Code Changes Happen Through Pull Requests 24 | Pull requests are the best way to propose changes to the codebase (we use [Github Flow](https://guides.github.com/introduction/flow/index.html)). We actively welcome your pull requests: 25 | 26 | 1. Fork the repo and create your branch from `master`. 27 | 2. Write tests for your code. 28 | 3. Write code that passes your tests. 29 | 3. If you've changed APIs, update the documentation. 30 | 4. Ensure the test suite passes. 31 | 5. Send in your [pull request](https://github.com/MarginallyClever/NodeGraphCore/pulls). 32 | 6. Discuss the changes with the team. 33 | 7. Get the code approved and merged into the `master` branch. 34 | 35 | ### License 36 | 37 | By sending in new code you agree that it will be covered by the existing license - GNU General Public License v2.0. 38 | 39 | ## Discussing features 40 | 41 | Please use the [forums](https://discord.gg/Q5TZFmB) to discuss future features and direction for the project. 42 | 43 | ## Code of conduct 44 | 45 | It's not hard: Don't be a jerk. 46 | -------------------------------------------------------------------------------- /src/main/java/com/marginallyclever/donatello/edits/GraphIsolateEdit.java: -------------------------------------------------------------------------------- 1 | package com.marginallyclever.donatello.edits; 2 | 3 | import com.marginallyclever.nodegraphcore.Node; 4 | import com.marginallyclever.nodegraphcore.Connection; 5 | import com.marginallyclever.nodegraphcore.Graph; 6 | import com.marginallyclever.donatello.Donatello; 7 | 8 | import javax.swing.undo.CannotRedoException; 9 | import javax.swing.undo.CannotUndoException; 10 | import java.util.ArrayList; 11 | import java.util.List; 12 | 13 | public class GraphIsolateEdit extends SignificantUndoableEdit { 14 | private final String name; 15 | private final Donatello editor; 16 | private final List connections = new ArrayList<>(); 17 | 18 | public GraphIsolateEdit(String name, Donatello editor, List selectedNodes) { 19 | super(); 20 | this.name = name; 21 | this.editor = editor; 22 | connections.addAll(editor.getGraph().getExteriorConnections(selectedNodes)); 23 | doIt(); 24 | } 25 | 26 | @Override 27 | public String getPresentationName() { 28 | return name; 29 | } 30 | 31 | private void doIt() { 32 | editor.lockClock(); 33 | try { 34 | Graph graph = editor.getGraph(); 35 | graph.getConnections().removeAll(connections); 36 | editor.repaint(); 37 | } 38 | finally { 39 | editor.unlockClock(); 40 | } 41 | } 42 | 43 | @Override 44 | public void undo() throws CannotUndoException { 45 | editor.lockClock(); 46 | try { 47 | Graph g = editor.getGraph(); 48 | for(Connection c : connections) g.add(c); 49 | editor.repaint(); 50 | } 51 | finally { 52 | editor.unlockClock(); 53 | } 54 | super.undo(); 55 | } 56 | 57 | @Override 58 | public void redo() throws CannotRedoException { 59 | doIt(); 60 | super.redo(); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/main/java/com/marginallyclever/donatello/ports/InputRange.java: -------------------------------------------------------------------------------- 1 | package com.marginallyclever.donatello.ports; 2 | 3 | import com.marginallyclever.donatello.SwingProvider; 4 | import com.marginallyclever.donatello.select.Select; 5 | import com.marginallyclever.donatello.select.SelectSlider; 6 | import com.marginallyclever.nodegraphcore.port.Input; 7 | 8 | import java.awt.*; 9 | 10 | /** 11 | * An input port that allows the user to select a range of values using a slider. 12 | */ 13 | public class InputRange extends Input implements SwingProvider { 14 | private SelectSlider selectSlider; 15 | private int top, bottom; 16 | 17 | public InputRange(String name) { 18 | this(name,0); 19 | } 20 | 21 | public InputRange(String name, Integer startingValue) throws IllegalArgumentException { 22 | this(name, startingValue,100,0); 23 | } 24 | 25 | public InputRange(String name, Integer startingValue, int top, int bottom) throws IllegalArgumentException { 26 | super(name, Integer.class, startingValue); 27 | this.top = top; 28 | this.bottom = bottom; 29 | } 30 | 31 | @Override 32 | public Select getSwingComponent(Component parent) { 33 | if(selectSlider==null) { 34 | selectSlider = new SelectSlider(name,name,top,bottom,value); 35 | selectSlider.addSelectListener( evt -> setValue(evt.getNewValue()) ); 36 | } 37 | return selectSlider; 38 | } 39 | 40 | public int getTop() { 41 | return top; 42 | } 43 | 44 | public int getBottom() { 45 | return bottom; 46 | } 47 | 48 | public void setTop(int top) { 49 | this.top = top; 50 | if(selectSlider!=null) { 51 | selectSlider.setTop(top); 52 | } 53 | } 54 | 55 | public void setBottom(int bottom) { 56 | this.bottom = bottom; 57 | if(selectSlider!=null) { 58 | selectSlider.setBottom(bottom); 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/main/java/com/marginallyclever/donatello/edits/ReorderEdit.java: -------------------------------------------------------------------------------- 1 | package com.marginallyclever.donatello.edits; 2 | 3 | import com.marginallyclever.nodegraphcore.Node; 4 | import com.marginallyclever.donatello.Donatello; 5 | 6 | import javax.swing.undo.CannotRedoException; 7 | import javax.swing.undo.CannotUndoException; 8 | import java.util.List; 9 | 10 | /** 11 | * Reorder nodes in a graph. Presumably this will change the rendering order to put the later nodes on top. 12 | */ 13 | public class ReorderEdit extends SignificantUndoableEdit { 14 | private final String name; 15 | private final Donatello editor; 16 | private final Node node; 17 | private final int from, to; 18 | 19 | public ReorderEdit(String name, Donatello editor, Node node, int to) { 20 | super(); 21 | this.name = name; 22 | this.editor = editor; 23 | this.node = node; 24 | this.to = to; 25 | this.from = editor.getGraph().getNodes().indexOf(node); 26 | } 27 | 28 | @Override 29 | public String getPresentationName() { 30 | return name; 31 | } 32 | 33 | public void doIt() { 34 | editor.lockClock(); 35 | try { 36 | List list = editor.getGraph().getNodes(); 37 | list.remove(node); 38 | list.add(to,node); 39 | editor.repaint(); 40 | } 41 | finally { 42 | editor.unlockClock(); 43 | } 44 | } 45 | 46 | @Override 47 | public void undo() throws CannotUndoException { 48 | editor.lockClock(); 49 | try { 50 | List list = editor.getGraph().getNodes(); 51 | list.remove(node); 52 | list.add(from,node); 53 | editor.repaint(); 54 | super.undo(); 55 | } 56 | finally { 57 | editor.unlockClock(); 58 | } 59 | } 60 | 61 | @Override 62 | public void redo() throws CannotRedoException { 63 | doIt(); 64 | super.redo(); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/main/java/module-info.java: -------------------------------------------------------------------------------- 1 | module com.marginallyclever.donatello { 2 | requires java.desktop; 3 | requires org.json; 4 | requires org.slf4j; 5 | requires ch.qos.logback.core; 6 | requires com.marginallyclever.nodegraphcore; 7 | requires io.github.classgraph; 8 | requires org.reflections; 9 | requires com.formdev.flatlaf; 10 | requires java.prefs; 11 | requires exp4j; 12 | requires vecmath; 13 | 14 | exports com.marginallyclever.donatello; 15 | exports com.marginallyclever.donatello.actions; 16 | exports com.marginallyclever.donatello.actions.undoable; 17 | exports com.marginallyclever.donatello.bezier; 18 | exports com.marginallyclever.donatello.graphview; 19 | exports com.marginallyclever.donatello.nodes; 20 | exports com.marginallyclever.donatello.nodes.color; 21 | exports com.marginallyclever.donatello.nodes.images; 22 | exports com.marginallyclever.donatello.ports; 23 | exports com.marginallyclever.donatello.search; 24 | exports com.marginallyclever.donatello.select; 25 | exports com.marginallyclever.donatello.nodefactorypanel; 26 | 27 | // A Java module that wants to implement a service interface from a service interface module must: 28 | // - Require the service interface module in its own module descriptor. 29 | // - Implement the service interface with a Java class. 30 | // - Declare the service interface implementation in its module descriptor. 31 | // In order to use the service, the client module must declare in its module descriptor that it uses the service. 32 | // http://tutorials.jenkov.com/java/modules.html 33 | uses com.marginallyclever.nodegraphcore.NodeRegistry; 34 | provides com.marginallyclever.nodegraphcore.NodeRegistry with 35 | com.marginallyclever.donatello.DonatelloRegistry; 36 | 37 | uses com.marginallyclever.nodegraphcore.DAORegistry; 38 | provides com.marginallyclever.nodegraphcore.DAORegistry with 39 | com.marginallyclever.donatello.DonatelloRegistry; 40 | } -------------------------------------------------------------------------------- /src/main/java/com/marginallyclever/donatello/actions/NodeCopyAction.java: -------------------------------------------------------------------------------- 1 | package com.marginallyclever.donatello.actions; 2 | 3 | import com.marginallyclever.nodegraphcore.Node; 4 | import com.marginallyclever.nodegraphcore.Connection; 5 | import com.marginallyclever.nodegraphcore.Graph; 6 | import com.marginallyclever.donatello.Donatello; 7 | 8 | import javax.swing.*; 9 | import java.awt.event.ActionEvent; 10 | import java.util.List; 11 | 12 | /** 13 | * Copies the editor's selected {@link Node}s and their shared {@link Connection}s to the copy buffer. The copy 14 | * buffer ensures the items so that they can be pasted even after they are deleted.
15 | *
16 | * It cannot be undone. It does not affect the {@link Graph}. Furthermore, this lets the user undo several 17 | * commands, copy something, redo back to where they were, and then paste the copy. 18 | * @author Dan Royer 19 | * @since 2022-02-21 20 | */ 21 | public class NodeCopyAction extends AbstractAction implements EditorAction { 22 | /** 23 | * The editor being affected. 24 | */ 25 | private final Donatello editor; 26 | 27 | /** 28 | * Constructor for subclasses to call. 29 | * @param name the name of this action visible on buttons and menu items. 30 | * @param editor the editor affected by this Action. 31 | */ 32 | public NodeCopyAction(String name, Donatello editor) { 33 | super(name); 34 | this.editor = editor; 35 | } 36 | 37 | @Override 38 | public void actionPerformed(ActionEvent e) { 39 | Graph graph = editor.getGraph(); 40 | 41 | Graph modelB = new Graph(); 42 | List selectedNodes = editor.getSelectedNodes(); 43 | for(Node n : selectedNodes) modelB.add(n); 44 | List selectedConnections = graph.getInteriorConnections(selectedNodes); 45 | for(Connection c : selectedConnections) modelB.add(c); 46 | editor.setCopiedGraph(modelB.deepCopy()); 47 | } 48 | 49 | @Override 50 | public void updateEnableStatus() { 51 | setEnabled(!editor.getSelectedNodes().isEmpty()); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/main/java/com/marginallyclever/donatello/nodes/images/ScaleImage.java: -------------------------------------------------------------------------------- 1 | package com.marginallyclever.donatello.nodes.images; 2 | 3 | import com.marginallyclever.donatello.nodes.color.LoadColor; 4 | import com.marginallyclever.donatello.ports.InputImage; 5 | import com.marginallyclever.donatello.ports.InputInt; 6 | import com.marginallyclever.donatello.ports.OutputImage; 7 | import com.marginallyclever.nodegraphcore.Node; 8 | 9 | import javax.swing.*; 10 | import java.awt.geom.AffineTransform; 11 | import java.awt.image.AffineTransformOp; 12 | import java.awt.image.BufferedImage; 13 | import java.util.Objects; 14 | 15 | /** 16 | * Resize an {@link BufferedImage} to the new desired size 17 | * @author Dan Royer 18 | * @since 2022-02-23 19 | */ 20 | public class ScaleImage extends Node { 21 | private final InputImage image = new InputImage("image"); 22 | private final InputInt width = new InputInt("width",1); 23 | private final InputInt height = new InputInt("height",1); 24 | private final OutputImage output = new OutputImage("output"); 25 | 26 | /** 27 | * Constructor for subclasses to call. 28 | */ 29 | public ScaleImage() { 30 | super("ScaleImage"); 31 | addPort(image); 32 | addPort(width); 33 | addPort(height); 34 | addPort(output); 35 | } 36 | 37 | @Override 38 | public void update() { 39 | int w = Math.max(1,width.getValue()); 40 | int h = Math.max(1,height.getValue()); 41 | 42 | BufferedImage input = image.getValue(); 43 | BufferedImage result = new BufferedImage(w,h,input.getType()); 44 | 45 | AffineTransform at = new AffineTransform(); 46 | at.scale((double)w/(double)input.getWidth(), (double)h/(double)input.getHeight()); 47 | AffineTransformOp scaleOp = new AffineTransformOp(at, AffineTransformOp.TYPE_BICUBIC); 48 | scaleOp.filter(input, result); 49 | output.setValue(result); 50 | } 51 | 52 | @Override 53 | public Icon getIcon() { 54 | return new ImageIcon(Objects.requireNonNull(ScaleImage.class.getResource("icons8-resize-48.png"))); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/main/java/com/marginallyclever/donatello/select/SelectSlider.java: -------------------------------------------------------------------------------- 1 | package com.marginallyclever.donatello.select; 2 | 3 | import javax.swing.*; 4 | import java.awt.*; 5 | 6 | public class SelectSlider extends Select { 7 | private final JLabel label; 8 | private final JSlider field = new JSlider(); 9 | private final JLabel value; 10 | private final JPanel panel2 = new JPanel(new BorderLayout()); 11 | 12 | public SelectSlider(String internalName,String labelKey,int top,int bottom,int defaultValue) { 13 | super(internalName); 14 | 15 | value = new JLabel("0",JLabel.TRAILING); 16 | Dimension dim = new Dimension(30,1); 17 | value.setMinimumSize(dim); 18 | value.setPreferredSize(dim); 19 | value.setMaximumSize(dim); 20 | 21 | field.setName(internalName+".field"); 22 | field.setMaximum(top); 23 | field.setMinimum(bottom); 24 | field.setMinorTickSpacing(1); 25 | field.addChangeListener(e -> { 26 | int n = field.getValue(); 27 | value.setText(Integer.toString(n)); 28 | if(field.getValueIsAdjusting()) return; 29 | fireSelectEvent(null,n); 30 | }); 31 | field.setValue(defaultValue); 32 | 33 | label = createLabel(labelKey); 34 | 35 | panel2.add(field,BorderLayout.CENTER); 36 | panel2.add(value,BorderLayout.LINE_END); 37 | } 38 | 39 | @Override 40 | public void attach(JComponent panel, GridBagConstraints gbc) { 41 | gbc.anchor = GridBagConstraints.LINE_START; 42 | gbc.gridx=0; 43 | panel.add(label,gbc); 44 | gbc.gridx=1; 45 | gbc.anchor = GridBagConstraints.LINE_END; 46 | panel.add(panel2,gbc); 47 | } 48 | 49 | @Override 50 | public void setReadOnly(boolean state) { 51 | field.setEnabled(!state); 52 | } 53 | 54 | public int getValue() { 55 | return field.getValue(); 56 | } 57 | 58 | public void setValue(int v) { 59 | field.setValue(v); 60 | } 61 | 62 | public void setTop(int top) { 63 | field.setMaximum(top); 64 | } 65 | 66 | public void setBottom(int bottom) { 67 | field.setMinimum(bottom); 68 | } 69 | 70 | public int getTop() { 71 | return field.getMaximum(); 72 | } 73 | 74 | public int getBottom() { 75 | return field.getMinimum(); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/main/java/com/marginallyclever/donatello/select/SelectColor.java: -------------------------------------------------------------------------------- 1 | package com.marginallyclever.donatello.select; 2 | 3 | import javax.swing.*; 4 | import javax.swing.border.LineBorder; 5 | import java.awt.*; 6 | 7 | /** 8 | * A color selection dialog 9 | * @author Dan Royer 10 | * @since 7.24.0 11 | */ 12 | public class SelectColor extends Select { 13 | private final JLabel label; 14 | private final BackgroundPaintedButton chooseButton; 15 | 16 | /** 17 | * @param parentComponent a component (JFrame, JPanel) that owns the color selection dialog 18 | * @param labelValue 19 | * @param defaultValue 20 | */ 21 | public SelectColor(String internalName,String labelValue,Color defaultValue,final Component parentComponent) { 22 | super(internalName); 23 | 24 | chooseButton = new BackgroundPaintedButton(""); 25 | chooseButton.setOpaque(true); 26 | chooseButton.setMinimumSize(new Dimension(80,20)); 27 | chooseButton.setMaximumSize(chooseButton.getMinimumSize()); 28 | chooseButton.setPreferredSize(chooseButton.getMinimumSize()); 29 | chooseButton.setSize(chooseButton.getMinimumSize()); 30 | chooseButton.setBackground(defaultValue); 31 | chooseButton.setBorder(new LineBorder(Color.BLACK)); 32 | chooseButton.addActionListener(e -> { 33 | Color c = JColorChooser.showDialog(parentComponent, labelValue, chooseButton.getBackground()); 34 | if ( c != null ){ 35 | chooseButton.setBackground(c); 36 | fireSelectEvent(null,c); 37 | } 38 | }); 39 | chooseButton.setName(internalName+".button"); 40 | 41 | label = createLabel(labelValue); 42 | } 43 | 44 | @Override 45 | public void setReadOnly(boolean state) { 46 | chooseButton.setEnabled(!state); 47 | } 48 | 49 | @Override 50 | public void attach(JComponent panel, GridBagConstraints gbc) { 51 | gbc.anchor = GridBagConstraints.LINE_START; 52 | gbc.gridx=0; 53 | panel.add(label,gbc); 54 | gbc.gridx=1; 55 | gbc.anchor = GridBagConstraints.LINE_END; 56 | panel.add(chooseButton,gbc); 57 | } 58 | 59 | public Color getColor() { 60 | return chooseButton.getBackground(); 61 | } 62 | 63 | public void setColor(Color c) { 64 | chooseButton.setBackground(c); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/main/java/com/marginallyclever/donatello/actions/GraphPrintAction.java: -------------------------------------------------------------------------------- 1 | package com.marginallyclever.donatello.actions; 2 | 3 | import com.marginallyclever.donatello.Donatello; 4 | 5 | import javax.imageio.ImageIO; 6 | import javax.swing.*; 7 | import java.awt.*; 8 | import java.awt.event.ActionEvent; 9 | import java.awt.image.BufferedImage; 10 | import java.io.File; 11 | import java.io.IOException; 12 | 13 | /** 14 | * Uses the {@link Donatello#printAll(Graphics)} to generate a {@link BufferedImage} and then saves that to 15 | * a default path. 16 | * TODO add a file selection dialog? 17 | * @author Dan Royer 18 | * @since 2022-02-21 19 | */ 20 | public class GraphPrintAction extends AbstractAction { 21 | /** 22 | * The editor being affected. 23 | */ 24 | private final Donatello editor; 25 | 26 | /** 27 | * The default save file. 28 | */ 29 | public static final String SAVE_PATH = "saved.png"; 30 | 31 | 32 | /** 33 | * Constructor for subclasses to call. 34 | * @param name the name of this action visible on buttons and menu items. 35 | * @param editor the editor affected by this Action. 36 | */ 37 | public GraphPrintAction(String name, Donatello editor) { 38 | super(name); 39 | this.editor = editor; 40 | } 41 | 42 | @Override 43 | public void actionPerformed(ActionEvent e) { 44 | BufferedImage awtImage = new BufferedImage(editor.getWidth(), editor.getHeight(), BufferedImage.TYPE_INT_ARGB); 45 | Graphics g = awtImage.getGraphics(); 46 | editor.printAll(g); 47 | /* 48 | if(popupBar.isVisible()) { 49 | g.translate(popupPoint.x, popupPoint.y); 50 | popupBar.printAll(g); 51 | g.translate(-popupPoint.x, -popupPoint.y); 52 | } 53 | */ 54 | // TODO file selection dialog here 55 | File outputFile = new File(SAVE_PATH); 56 | String extension = SAVE_PATH.substring(SAVE_PATH.lastIndexOf(".")+1); 57 | 58 | try { 59 | ImageIO.write(awtImage, extension, outputFile); 60 | } catch (IOException e1) { 61 | e1.printStackTrace(); 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/test/java/com/marginallyclever/donatello/TestDAO4JSON.java: -------------------------------------------------------------------------------- 1 | package com.marginallyclever.donatello; 2 | 3 | import com.marginallyclever.nodegraphcore.DAO4JSONFactory; 4 | import com.marginallyclever.nodegraphcore.NodeFactory; 5 | import com.marginallyclever.donatello.nodes.images.BufferedImageDAO4JSON; 6 | import org.junit.jupiter.api.AfterAll; 7 | import org.junit.jupiter.api.BeforeAll; 8 | import org.junit.jupiter.api.Disabled; 9 | import org.junit.jupiter.api.Test; 10 | 11 | import java.awt.image.BufferedImage; 12 | 13 | 14 | /** 15 | * Test JSON Data Access Objects. 16 | * @author Dan Royer 17 | * @since 2022-03-07 18 | */ 19 | public class TestDAO4JSON { 20 | @BeforeAll 21 | public static void beforeAll() { 22 | NodeFactory.loadRegistries(); 23 | DAO4JSONFactory.loadRegistries(); 24 | } 25 | 26 | @AfterAll 27 | public static void afterAll() { 28 | NodeFactory.clear(); 29 | DAO4JSONFactory.clear(); 30 | } 31 | 32 | private boolean bufferedImagesEqual(BufferedImage img1, BufferedImage img2) { 33 | if(img1.getType() != img2.getType() 34 | || img1.getWidth() != img2.getWidth() 35 | || img1.getHeight() != img2.getHeight()) { 36 | return false; 37 | } 38 | 39 | for (int x = 0; x < img1.getWidth(); x++) { 40 | for (int y = 0; y < img1.getHeight(); y++) { 41 | if (img1.getRGB(x, y) != img2.getRGB(x, y)) 42 | return false; 43 | } 44 | } 45 | 46 | return true; 47 | } 48 | 49 | // TODO figure out why this test fails in Maven. 50 | @Disabled("Work in IntelliJ, not in Maven. Investigate!") 51 | @Test 52 | public void isRegistered() { 53 | assert(DAO4JSONFactory.isRegistered(BufferedImage.class)); 54 | } 55 | 56 | /** 57 | * Test {@link BufferedImage}. 58 | */ 59 | @Test 60 | public void testBufferedImageDAO() { 61 | BufferedImageDAO4JSON dao = new BufferedImageDAO4JSON(); 62 | BufferedImage r1 = new BufferedImage(2,3,BufferedImage.TYPE_INT_ARGB); 63 | BufferedImage r2=dao.fromJSON(dao.toJSON(r1)); 64 | assert(bufferedImagesEqual(r1,r2)); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/main/java/com/marginallyclever/donatello/ports/InputOneOfMany.java: -------------------------------------------------------------------------------- 1 | package com.marginallyclever.donatello.ports; 2 | 3 | import com.marginallyclever.donatello.SwingProvider; 4 | import com.marginallyclever.donatello.graphview.GraphViewPanel; 5 | import com.marginallyclever.donatello.graphview.GraphViewProvider; 6 | import com.marginallyclever.donatello.select.Select; 7 | import com.marginallyclever.donatello.select.SelectOneOfMany; 8 | 9 | import java.awt.*; 10 | 11 | public class InputOneOfMany extends InputInt implements SwingProvider, GraphViewProvider { 12 | private SelectOneOfMany selectOneOfMany; 13 | private String [] options; 14 | 15 | public InputOneOfMany(String name) { 16 | this(name,0); 17 | } 18 | 19 | public InputOneOfMany(String name, Integer startingValue) throws IllegalArgumentException { 20 | super(name, startingValue); 21 | } 22 | 23 | @Override 24 | public Select getSwingComponent(Component parent) { 25 | if(selectOneOfMany == null) { 26 | selectOneOfMany = new SelectOneOfMany(name,name); 27 | if(options!=null) setOptions(options); 28 | selectOneOfMany.addSelectListener(evt-> setValue(evt.getNewValue()) ); 29 | } 30 | return selectOneOfMany; 31 | } 32 | 33 | public void setOptions(String[] options) { 34 | this.options = options; 35 | if(selectOneOfMany!=null) { 36 | selectOneOfMany.setNewList(options); 37 | selectOneOfMany.setSelectedIndex(getValue()); 38 | } 39 | } 40 | 41 | @Override 42 | public void setValue(Object arg0) { 43 | super.setValue(arg0); 44 | if(selectOneOfMany!=null) { 45 | selectOneOfMany.setSelectedIndex(getValue()); 46 | } 47 | } 48 | 49 | @Override 50 | public void paint(Graphics g, Rectangle box) { 51 | String val = options[getValue()]; 52 | Rectangle insideBox = GraphViewPanel.getNodeInternalBounds(box); 53 | if (val.length() > GraphViewPanel.MAX_CHARS_PER_RORT) val = val.substring(0, GraphViewPanel.MAX_CHARS_PER_RORT) + "..."; 54 | GraphViewPanel.paintText(g, val, box, GraphViewPanel.ALIGN_RIGHT, GraphViewPanel.ALIGN_TOP); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/main/java/com/marginallyclever/donatello/select/SelectButton.java: -------------------------------------------------------------------------------- 1 | package com.marginallyclever.donatello.select; 2 | 3 | import javax.swing.*; 4 | import java.awt.event.ActionEvent; 5 | import java.util.List; 6 | import java.util.ArrayList; 7 | import java.awt.*; 8 | import java.awt.event.ActionListener; 9 | 10 | 11 | 12 | /** 13 | * A button that does nothing until you attach an observer. 14 | * @author Dan Royer 15 | * @since 7.24.0 16 | */ 17 | public class SelectButton extends Select { 18 | private final List actionListenerList = new ArrayList<>(); 19 | private final JButton button; 20 | 21 | public SelectButton(String internalName,AbstractAction action) { 22 | super(internalName); 23 | button = new JButton(action); 24 | button.setName(internalName+".button"); 25 | } 26 | 27 | public SelectButton(String internalName, String labelText) { 28 | super(internalName); 29 | 30 | button = new JButton(labelText); 31 | button.addActionListener((e) -> { 32 | fireActionEvent(); 33 | }); 34 | } 35 | 36 | @Override 37 | public void attach(JComponent panel, GridBagConstraints gbc) { 38 | gbc.fill = GridBagConstraints.HORIZONTAL; 39 | gbc.gridx=0; 40 | gbc.gridwidth=2; 41 | panel.add(button,gbc); 42 | gbc.gridwidth=1; 43 | } 44 | 45 | @Override 46 | public void setReadOnly(boolean state) { 47 | button.setEnabled(!state); 48 | } 49 | 50 | public void doClick() { 51 | if(button!=null) button.doClick(); 52 | } 53 | 54 | public void setText(String label) { 55 | if(button!=null) button.setText(label); 56 | } 57 | 58 | public void setEnabled(boolean b) { 59 | if(button!=null) button.setEnabled(b); 60 | } 61 | 62 | public void setForeground(Color fg) { 63 | if(button!=null) button.setForeground(fg); 64 | } 65 | 66 | public void addActionListener(ActionListener l) { 67 | actionListenerList.add(l); 68 | } 69 | 70 | public void removeActionListener(ActionListener l) { 71 | actionListenerList.remove(l); 72 | } 73 | 74 | public void fireActionEvent() { 75 | for(ActionListener l : actionListenerList) { 76 | l.actionPerformed(new ActionEvent(this,ActionEvent.ACTION_PERFORMED,getName())); 77 | } 78 | } 79 | 80 | public JButton getButton() { 81 | return button; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/test/java/com/marginallyclever/donatello/actions/ActionRegistryTest.java: -------------------------------------------------------------------------------- 1 | package com.marginallyclever.donatello.actions; 2 | 3 | import com.marginallyclever.donatello.Donatello; 4 | import org.junit.jupiter.api.Assertions; 5 | import org.junit.jupiter.api.Test; 6 | 7 | import javax.swing.*; 8 | 9 | public class ActionRegistryTest { 10 | // test that action accelerators can be saved, loaded, and reset 11 | @Test 12 | public void testActionAccelerators() { 13 | String name = "toolbar > graphNew"; 14 | // This test will check if the actions can be registered, their accelerators can be set, 15 | var donatello = new Donatello(); 16 | var action = new GraphNewAction(name,donatello); 17 | KeyStroke defaultKs = (KeyStroke)action.getValue(Action.ACCELERATOR_KEY); 18 | ActionRegistry.register(name, action); 19 | KeyStroke currentKs = (KeyStroke) action.getValue(Action.ACCELERATOR_KEY); 20 | 21 | // Assert that the action can be retrieved and its accelerator is set correctly. 22 | Action retrievedAction = ActionRegistry.get(name); 23 | Assertions.assertEquals(action, retrievedAction); 24 | Assertions.assertEquals(defaultKs, ActionRegistry.getDefaultAccelerator(action)); 25 | 26 | // Change the accelerator to a new value. 27 | KeyStroke newKs = KeyStroke.getKeyStroke("control N"); 28 | retrievedAction.putValue(Action.ACCELERATOR_KEY, newKs); 29 | ActionRegistry.saveUserAcceleratorForAction(name, newKs); 30 | 31 | // Assert that the action's accelerator has been updated. 32 | var middle = ActionRegistry.get(name); 33 | Assertions.assertEquals(newKs, middle.getValue(Action.ACCELERATOR_KEY)); 34 | Assertions.assertNotEquals(newKs,ActionRegistry.getDefaultAccelerator(action)); 35 | 36 | // Reset the action's accelerator to the current. 37 | retrievedAction.putValue(Action.ACCELERATOR_KEY, currentKs); 38 | ActionRegistry.saveUserAcceleratorForAction(name, newKs); 39 | 40 | // Assert that the action's accelerator is back to the current (test should not change settings). 41 | Assertions.assertEquals(currentKs, retrievedAction.getValue(Action.ACCELERATOR_KEY)); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/com/marginallyclever/donatello/select/SelectPassword.java: -------------------------------------------------------------------------------- 1 | package com.marginallyclever.donatello.select; 2 | 3 | import javax.swing.*; 4 | import javax.swing.event.DocumentEvent; 5 | import javax.swing.event.DocumentListener; 6 | import java.awt.*; 7 | 8 | /** 9 | * A short text input. 10 | * @author Dan Royer 11 | * @since 7.24.0 12 | */ 13 | public class SelectPassword extends Select { 14 | private final JLabel label; 15 | private final JPasswordField field; 16 | 17 | public SelectPassword(String internalName, String labelKey, String defaultText) { 18 | super(internalName); 19 | 20 | field = new JPasswordField(defaultText); 21 | field.setName(internalName+".field"); 22 | field.setEchoChar('*'); 23 | Dimension d = field.getPreferredSize(); 24 | d.width = 100; 25 | field.setPreferredSize(d); 26 | field.setMinimumSize(d); 27 | 28 | field.getDocument().addDocumentListener(new DocumentListener() { 29 | @Override 30 | public void insertUpdate(DocumentEvent e) { 31 | validate(); 32 | } 33 | 34 | @Override 35 | public void removeUpdate(DocumentEvent e) { 36 | validate(); 37 | } 38 | 39 | @Override 40 | public void changedUpdate(DocumentEvent e) { 41 | validate(); 42 | } 43 | 44 | void validate() { 45 | fireSelectEvent(null,field.getPassword()); 46 | } 47 | }); 48 | 49 | label = createLabel(labelKey); 50 | } 51 | 52 | @Override 53 | public void attach(JComponent panel, GridBagConstraints gbc) { 54 | gbc.anchor = GridBagConstraints.LINE_START; 55 | panel.add(label,gbc); 56 | gbc.anchor = GridBagConstraints.LINE_END; 57 | panel.add(field,gbc); 58 | } 59 | 60 | @Override 61 | public void setReadOnly(boolean state) { 62 | field.setEnabled(!state); 63 | } 64 | 65 | public String getPassword() { 66 | return new String(field.getPassword()); 67 | } 68 | 69 | public void setPassword(String str) { 70 | field.setText(str); 71 | } 72 | 73 | public boolean isEditable() { 74 | return field.isEditable(); 75 | } 76 | 77 | public void setEditable(boolean b) { 78 | field.setEditable(b); 79 | } 80 | 81 | public boolean getDragEnabled() { 82 | return field.getDragEnabled(); 83 | } 84 | 85 | public void setDragEnabled(boolean b) { 86 | field.setDragEnabled(b); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/main/java/com/marginallyclever/donatello/FileHelper.java: -------------------------------------------------------------------------------- 1 | package com.marginallyclever.donatello; 2 | 3 | import java.io.File; 4 | import java.nio.file.FileSystems; 5 | import java.util.ArrayList; 6 | import java.util.List; 7 | 8 | /** 9 | * Convenient methods for searching files in a directory and finding the user's donatello/extensions path. 10 | * @author Dan Royer 11 | * @since 2022-03-?? 12 | */ 13 | public class FileHelper { 14 | /** 15 | * Attempts to create a directory if it does not exist. 16 | * @param dir the desired path 17 | */ 18 | public static void createDirectoryIfMissing(String dir) { 19 | File directory = new File(dir); 20 | if(!directory.exists()) directory.mkdirs(); 21 | } 22 | 23 | /** 24 | * Returns the extension path ~/Donatello/extensions/ 25 | * @return the extension path ~/Donatello/extensions/ 26 | */ 27 | public static String getExtensionPath() { 28 | String sep = FileSystems.getDefault().getSeparator(); 29 | return getWorkPath() + "extensions" + sep; 30 | } 31 | 32 | /** 33 | * Returns the path ~/Donatello/ 34 | * @return the path ~/Donatello/ 35 | */ 36 | public static String getWorkPath() { 37 | String sep = FileSystems.getDefault().getSeparator(); 38 | return System.getProperty("user.home") + sep + "Donatello" + sep; 39 | } 40 | 41 | /** 42 | * Returns the path ~/Donatello/Donatello.log 43 | * @return the path ~/Donatello/Donatello.log 44 | */ 45 | public static String getLogFile() { 46 | String sep = FileSystems.getDefault().getSeparator(); 47 | return getWorkPath() + sep + "Donatello.log"; 48 | } 49 | 50 | /** 51 | * Convert from a filename to a file URL. 52 | */ 53 | public static String convertToFileURL( String filename ) { 54 | // On JDK 1.2 and later, simplify this to: 55 | // "path = file.toURL().toString()". 56 | String path = new File( filename ).getAbsolutePath(); 57 | if ( File.separatorChar != '/' ) { 58 | path = path.replace ( File.separatorChar, '/' ); 59 | } 60 | if ( !path.startsWith ( "/" ) ) { 61 | path = "/" + path; 62 | } 63 | return "file:" + path; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/main/java/com/marginallyclever/donatello/actions/GraphStraightenAction.java: -------------------------------------------------------------------------------- 1 | package com.marginallyclever.donatello.actions; 2 | 3 | import com.marginallyclever.donatello.Donatello; 4 | import com.marginallyclever.donatello.graphview.GraphViewSettings; 5 | import com.marginallyclever.nodegraphcore.Node; 6 | import org.slf4j.Logger; 7 | import org.slf4j.LoggerFactory; 8 | 9 | import javax.swing.*; 10 | import java.awt.*; 11 | import java.awt.event.ActionEvent; 12 | 13 | /** 14 | * Straightens the editor's {@link Node}s by rounding their top-left corner x and y values to the nearest 15 | * {@link GraphViewSettings#getGridSize()}. 16 | * @author Dan Royer 17 | * @since 2022-02-21 18 | */ 19 | public class GraphStraightenAction extends AbstractAction implements EditorAction { 20 | private static final Logger logger = LoggerFactory.getLogger(GraphStraightenAction.class); 21 | 22 | /** 23 | * The editor being affected. 24 | */ 25 | private final Donatello editor; 26 | 27 | /** 28 | * Constructor for subclasses to call. 29 | * @param name the name of this action visible on buttons and menu items. 30 | * @param editor the editor affected by this Action. 31 | */ 32 | public GraphStraightenAction(String name, Donatello editor) { 33 | super(name); 34 | this.editor = editor; 35 | } 36 | 37 | @Override 38 | public void actionPerformed(ActionEvent e) { 39 | var g = editor.getGraph(); 40 | GraphViewSettings settings = editor.getPaintArea().getSettings(); 41 | var gs = settings.getGridSize(); 42 | for(Node n : g.getNodes()) { 43 | Rectangle r = n.getRectangle(); 44 | var x = round(r.x,gs); 45 | var y = round(r.y,gs); 46 | if(r.x!=x) logger.debug("x round({},{})={}", r.x, gs, x); 47 | if(r.y!=y) logger.debug("y round({},{})={}", r.y, gs, y); 48 | r.x = x; 49 | r.y = y; 50 | n.updateBounds(); 51 | } 52 | editor.repaint(); 53 | } 54 | 55 | private int round(int v, int step) { 56 | return Math.round((float) v / step) * step; 57 | } 58 | 59 | @Override 60 | public void updateEnableStatus() { 61 | setEnabled(!editor.getSelectedNodes().isEmpty()); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/main/java/com/marginallyclever/donatello/select/DelayedDocumentValidator.java: -------------------------------------------------------------------------------- 1 | package com.marginallyclever.donatello.select; 2 | 3 | import com.marginallyclever.donatello.simpleparser.SimpleParser; 4 | 5 | import javax.swing.*; 6 | import javax.swing.event.DocumentEvent; 7 | import javax.swing.event.DocumentListener; 8 | import javax.swing.text.JTextComponent; 9 | import java.awt.*; 10 | import java.util.Timer; 11 | import java.util.TimerTask; 12 | import java.util.function.Consumer; 13 | 14 | /** 15 | * {@link DelayedDocumentValidator} validates the text in a number field using a {@link SimpleParser} so it can 16 | * handle simple math expressions. 17 | */ 18 | public class DelayedDocumentValidator implements DocumentListener { 19 | private final JTextComponent field; 20 | private Timer timer=null; 21 | private final Consumer consumer; 22 | 23 | public DelayedDocumentValidator(JTextComponent field, Consumer consumer) { 24 | super(); 25 | this.field = field; 26 | this.consumer = consumer; 27 | } 28 | 29 | @Override 30 | public void changedUpdate(DocumentEvent arg0) { 31 | if(arg0.getLength()==0) return; 32 | validate(); 33 | } 34 | 35 | @Override 36 | public void insertUpdate(DocumentEvent arg0) { 37 | if(arg0.getLength()==0) return; 38 | validate(); 39 | } 40 | 41 | @Override 42 | public void removeUpdate(DocumentEvent arg0) { 43 | if(arg0.getLength()==0) return; 44 | validate(); 45 | } 46 | 47 | public void validate() { 48 | try { 49 | double newValue = SimpleParser.evaluate(field.getText()); 50 | field.setForeground(UIManager.getColor("Textfield.foreground")); 51 | field.setToolTipText(null); 52 | 53 | if(timer!=null) timer.cancel(); 54 | timer = new Timer("Delayed response"); 55 | timer.schedule(new TimerTask() { 56 | public void run() { 57 | consumer.accept(newValue); 58 | } 59 | }, 100L); // brief delay in case someone is typing fast 60 | } catch (IllegalArgumentException e) { 61 | field.setForeground(Color.RED); 62 | field.setToolTipText(e.getMessage()); 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/main/java/com/marginallyclever/donatello/edits/GraphPasteEdit.java: -------------------------------------------------------------------------------- 1 | package com.marginallyclever.donatello.edits; 2 | 3 | import com.marginallyclever.donatello.Donatello; 4 | import com.marginallyclever.donatello.QueueByDepth; 5 | import com.marginallyclever.nodegraphcore.Graph; 6 | import com.marginallyclever.nodegraphcore.Node; 7 | 8 | import javax.swing.undo.CannotRedoException; 9 | import javax.swing.undo.CannotUndoException; 10 | import java.awt.*; 11 | 12 | public class GraphPasteEdit extends SignificantUndoableEdit { 13 | private final String name; 14 | private final Donatello editor; 15 | private final Graph copiedGraph; 16 | 17 | public GraphPasteEdit(String name, Donatello editor, Graph graph, Point position) { 18 | super(); 19 | this.name = name; 20 | this.editor = editor; 21 | this.copiedGraph = graph.deepCopy(); 22 | copiedGraph.updateBounds(); 23 | Rectangle r = copiedGraph.getBounds(); 24 | int dx = position.x - r.x - r.width/2; 25 | int dy = position.y - r.y - r.height/2; 26 | 27 | for(Node n : copiedGraph.getNodes()) { 28 | n.moveRelative(dx, dy); 29 | } 30 | System.out.println("test "+position.x+" "+position.y); 31 | 32 | doIt(); 33 | } 34 | 35 | @Override 36 | public String getPresentationName() { 37 | return name; 38 | } 39 | 40 | private void doIt() { 41 | editor.lockClock(); 42 | try { 43 | editor.getGraph().add(copiedGraph); 44 | editor.setSelectedNodes(copiedGraph.getNodes()); 45 | editor.repaint(); 46 | new QueueByDepth(editor, copiedGraph,0); 47 | } 48 | finally { 49 | editor.unlockClock(); 50 | } 51 | } 52 | 53 | @Override 54 | public void undo() throws CannotUndoException { 55 | editor.lockClock(); 56 | try { 57 | editor.getGraph().remove(copiedGraph); 58 | editor.setSelectedNodes(null); 59 | editor.repaint(); 60 | } 61 | finally { 62 | editor.unlockClock(); 63 | } 64 | super.undo(); 65 | } 66 | 67 | @Override 68 | public void redo() throws CannotRedoException { 69 | doIt(); 70 | super.redo(); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/main/java/com/marginallyclever/donatello/select/SelectTextField.java: -------------------------------------------------------------------------------- 1 | package com.marginallyclever.donatello.select; 2 | 3 | import javax.swing.*; 4 | import javax.swing.event.DocumentEvent; 5 | import javax.swing.event.DocumentListener; 6 | import java.awt.*; 7 | 8 | /** 9 | * A short text input. 10 | * @author Dan Royer 11 | * @since 7.24.0 12 | */ 13 | public class SelectTextField extends Select { 14 | private final JTextField field; 15 | private final JLabel label; 16 | 17 | public SelectTextField(String internalName, String labelKey, String defaultText) { 18 | super(internalName); 19 | //this.setBorder(BorderFactory.createLineBorder(Color.RED)); 20 | 21 | field = new JTextField(defaultText,20); 22 | field.setName(internalName+".field"); 23 | Dimension d = field.getPreferredSize(); 24 | d.width = 100; 25 | field.setPreferredSize(d); 26 | field.setMinimumSize(d); 27 | field.getDocument().addDocumentListener(new DocumentListener() { 28 | @Override 29 | public void insertUpdate(DocumentEvent e) { 30 | validate(); 31 | } 32 | 33 | @Override 34 | public void removeUpdate(DocumentEvent e) { 35 | validate(); 36 | } 37 | 38 | @Override 39 | public void changedUpdate(DocumentEvent e) { 40 | validate(); 41 | } 42 | 43 | void validate() { 44 | fireSelectEvent(null,field.getText()); 45 | } 46 | }); 47 | 48 | label = createLabel(labelKey); 49 | } 50 | 51 | @Override 52 | public void setReadOnly(boolean state) { 53 | field.setEnabled(!state); 54 | } 55 | 56 | @Override 57 | public void attach(JComponent panel, GridBagConstraints gbc) { 58 | gbc.anchor = GridBagConstraints.LINE_START; 59 | gbc.gridx=0; 60 | panel.add(label,gbc); 61 | gbc.gridx=1; 62 | gbc.anchor = GridBagConstraints.LINE_END; 63 | panel.add(field,gbc); 64 | } 65 | 66 | public String getText() { 67 | return field.getText(); 68 | } 69 | 70 | public void setText(String str) { 71 | field.setText(str); 72 | } 73 | 74 | public boolean isEditable() { 75 | return field.isEditable(); 76 | } 77 | 78 | public void setEditable(boolean b) { 79 | field.setEditable(b); 80 | } 81 | 82 | public boolean getDragEnabled() { 83 | return field.getDragEnabled(); 84 | } 85 | 86 | public void setDragEnabled(boolean b) { 87 | field.setDragEnabled(b); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/main/java/com/marginallyclever/donatello/select/SelectOneOfMany.java: -------------------------------------------------------------------------------- 1 | package com.marginallyclever.donatello.select; 2 | 3 | import javax.swing.*; 4 | import java.awt.*; 5 | import java.util.Arrays; 6 | 7 | /** 8 | * @author Dan Royer 9 | * @since 7.24.0 10 | */ 11 | public class SelectOneOfMany extends Select { 12 | private final JComboBox field = new JComboBox<>(); 13 | private final DefaultComboBoxModel model = (DefaultComboBoxModel)field.getModel(); 14 | private final JLabel label; 15 | 16 | public SelectOneOfMany(String internalName,String labelKey) { 17 | super(internalName); 18 | field.setName(internalName+".field"); 19 | field.addActionListener((e)-> fireSelectEvent(null, field.getSelectedIndex()) ); 20 | label = createLabel(labelKey); 21 | } 22 | 23 | public SelectOneOfMany(String internalName,String labelKey,String[] options,int defaultValue) { 24 | this(internalName,labelKey); 25 | 26 | setNewList(options); 27 | field.setSelectedIndex(defaultValue); 28 | } 29 | 30 | @Override 31 | public void attach(JComponent panel, GridBagConstraints gbc) { 32 | gbc.anchor = GridBagConstraints.LINE_START; 33 | gbc.gridx=0; 34 | panel.add(label,gbc); 35 | gbc.gridx=1; 36 | gbc.anchor = GridBagConstraints.LINE_END; 37 | panel.add(field,gbc); 38 | } 39 | 40 | @Override 41 | public void setReadOnly(boolean state) { 42 | field.setEnabled(!state); 43 | } 44 | 45 | public void removeAll() { 46 | model.removeAllElements(); 47 | } 48 | 49 | public void addItem(String s) { 50 | model.addElement(s); 51 | } 52 | 53 | public void removeItem(String s) { 54 | model.removeElement(s); 55 | } 56 | 57 | public void setReadOnly() { 58 | field.setEditable(false); 59 | } 60 | 61 | public String getSelectedItem() { 62 | return (String)field.getSelectedItem(); 63 | } 64 | 65 | public int getSelectedIndex() { 66 | return field.getSelectedIndex(); 67 | } 68 | 69 | public void setSelectedIndex(int index) { 70 | field.setSelectedIndex(index); 71 | field.repaint();// Some times it need it ! but why ? normaly the swing events listener take care of that ... 72 | } 73 | 74 | public void setNewList(String[] list) { 75 | model.removeAllElements(); 76 | model.addAll(Arrays.asList(list)); 77 | } 78 | 79 | public JLabel getLabel() { 80 | return label; 81 | } 82 | 83 | public JComboBox getField() { 84 | return field; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/main/java/com/marginallyclever/donatello/ports/InputFilename.java: -------------------------------------------------------------------------------- 1 | package com.marginallyclever.donatello.ports; 2 | 3 | import com.marginallyclever.donatello.SwingProvider; 4 | import com.marginallyclever.donatello.select.Select; 5 | import com.marginallyclever.donatello.select.SelectFile; 6 | import com.marginallyclever.nodegraphcore.port.Input; 7 | 8 | import javax.swing.*; 9 | import java.awt.*; 10 | 11 | /** 12 | * A port that accepts a filename. 13 | */ 14 | public class InputFilename extends Input implements SwingProvider { 15 | private SelectFile selectFile; 16 | private JFileChooser fileChooser; 17 | private boolean isSave=false; 18 | 19 | public InputFilename(String name) { 20 | this(name,""); 21 | } 22 | 23 | public InputFilename(String name, String startingValue) throws IllegalArgumentException { 24 | super(name, Filename.class, new Filename(startingValue)); 25 | } 26 | 27 | @Override 28 | public Select getSwingComponent(Component parent) { 29 | if(selectFile==null) { 30 | selectFile = new SelectFile(name,name,getValue().get(),parent); 31 | selectFile.addSelectListener( evt -> { 32 | setValue(evt.getNewValue()); 33 | }); 34 | } 35 | if(fileChooser!=null) { 36 | selectFile.setFileChooser(fileChooser); 37 | } 38 | selectFile.setDialogType(isSave); 39 | return selectFile; 40 | } 41 | 42 | /** 43 | * Set the file chooser to use when selecting a file. 44 | * @param fileChooser the file chooser to use. cannot be null. 45 | */ 46 | public void setFileChooser(JFileChooser fileChooser) { 47 | this.fileChooser = fileChooser; 48 | if(selectFile!=null) { 49 | selectFile.setFileChooser(fileChooser); 50 | } 51 | } 52 | 53 | /** 54 | * @param isSave true for save dialog, false for load dialog. Default is false. 55 | */ 56 | public void setDialogType(boolean isSave) { 57 | this.isSave = isSave; 58 | if(selectFile!=null) { 59 | selectFile.setDialogType(isSave); 60 | } 61 | } 62 | 63 | @Override 64 | public void setValue(Object arg0) { 65 | if(arg0 instanceof String str) { 66 | setDirtyOnValueChange(arg0); 67 | value.set(str); 68 | return; 69 | } 70 | super.setValue(arg0); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/main/java/com/marginallyclever/donatello/nodefactorypanel/FactoryCategoryCellRenderer.java: -------------------------------------------------------------------------------- 1 | package com.marginallyclever.donatello.nodefactorypanel; 2 | 3 | import com.marginallyclever.donatello.IconHelper; 4 | import com.marginallyclever.nodegraphcore.Node; 5 | import com.marginallyclever.nodegraphcore.NodeCategory; 6 | 7 | import javax.swing.*; 8 | import javax.swing.tree.DefaultMutableTreeNode; 9 | import javax.swing.tree.DefaultTreeCellRenderer; 10 | import java.awt.*; 11 | 12 | import static com.marginallyclever.donatello.IconHelper.scaleIcon; 13 | 14 | /** 15 | * Custom cell renderer for displaying node categories in a tree. 16 | * It sets the text and color based on the category's supplier. 17 | */ 18 | class FactoryCategoryCellRenderer extends DefaultTreeCellRenderer { 19 | @Override 20 | public Component getTreeCellRendererComponent(JTree tree, Object value, boolean sel, boolean expanded, boolean leaf, int row, boolean hasFocus) { 21 | JLabel label = (JLabel) super.getTreeCellRendererComponent(tree, value, sel, expanded, leaf, row, hasFocus); 22 | 23 | DefaultMutableTreeNode node = (DefaultMutableTreeNode) value; 24 | Object userObject = node.getUserObject(); 25 | if (userObject instanceof NodeCategory category) { 26 | label.setText(category.getName()); 27 | if (category.getSupplier() != null) { 28 | try { 29 | Node n = category.getSupplier().get(); 30 | if (n != null) { 31 | var icon = n.getIcon(); 32 | if(icon != null) { 33 | label.setIcon(scaleIcon(icon, IconHelper.ICON_SIZE,IconHelper.ICON_SIZE)); 34 | } else { 35 | label.setIcon(scaleIcon(getDefaultLeafIcon(),IconHelper.ICON_SIZE,IconHelper.ICON_SIZE)); 36 | } 37 | } else { 38 | label.setIcon(scaleIcon(getDefaultLeafIcon(),IconHelper.ICON_SIZE,IconHelper.ICON_SIZE)); 39 | } 40 | } catch (Exception e) { 41 | label.setIcon(scaleIcon(getDefaultLeafIcon(),IconHelper.ICON_SIZE,IconHelper.ICON_SIZE)); 42 | } 43 | } else { 44 | label.setIcon(scaleIcon(getDefaultOpenIcon(),IconHelper.ICON_SIZE,IconHelper.ICON_SIZE)); 45 | } 46 | } 47 | return label; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/com/marginallyclever/donatello/edits/ConnectionAddEdit.java: -------------------------------------------------------------------------------- 1 | package com.marginallyclever.donatello.edits; 2 | 3 | import com.marginallyclever.nodegraphcore.Connection; 4 | import com.marginallyclever.nodegraphcore.Graph; 5 | import com.marginallyclever.donatello.Donatello; 6 | 7 | import javax.swing.undo.CannotRedoException; 8 | import javax.swing.undo.CannotUndoException; 9 | import java.util.List; 10 | 11 | /** 12 | * Adds a {@link Connection} to a {@link com.marginallyclever.nodegraphcore.port.Port}. 13 | * Since the inbound node can only have one connection at a time, this edit also preserves any connection that has 14 | * to be removed. 15 | */ 16 | public class ConnectionAddEdit extends SignificantUndoableEdit { 17 | private final String name; 18 | private final Donatello editor; 19 | private final Connection connection; 20 | private final List connectionsInto; 21 | 22 | public ConnectionAddEdit(String name, Donatello editor, Connection connection) { 23 | super(); 24 | this.name = name; 25 | this.editor = editor; 26 | this.connection = connection; 27 | this.connectionsInto = editor.getGraph().getAllConnectionsInto(connection.getInput()); 28 | doIt(); 29 | } 30 | 31 | @Override 32 | public String getPresentationName() { 33 | return name; 34 | } 35 | 36 | private void doIt() { 37 | editor.lockClock(); 38 | try { 39 | Graph graph = editor.getGraph(); 40 | for(Connection c : connectionsInto) { 41 | graph.remove(c); 42 | } 43 | graph.add(connection); 44 | connection.apply(); 45 | editor.submit(connection.getTo()); 46 | } 47 | finally { 48 | editor.unlockClock(); 49 | } 50 | } 51 | 52 | @Override 53 | public void undo() throws CannotUndoException { 54 | editor.lockClock(); 55 | try { 56 | Graph graph = editor.getGraph(); 57 | graph.remove(connection); 58 | for(Connection c : connectionsInto) { 59 | graph.add(c); 60 | } 61 | connection.reset(); 62 | editor.submit(connection.getTo()); 63 | } 64 | finally { 65 | editor.unlockClock(); 66 | } 67 | super.undo(); 68 | } 69 | 70 | @Override 71 | public void redo() throws CannotRedoException { 72 | doIt(); 73 | super.redo(); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Donatello: Flow-based Programming IDE 2 | 3 | [![Release](https://jitpack.io/v/MarginallyClever/Donatello.svg)](https://jitpack.io/#MarginallyClever/Donatello) 4 | [![Discord](https://img.shields.io/discord/521753686238560256?label=Discord)](https://discord.gg/QtvHqAv8yp) 5 | [![Javadoc](https://img.shields.io/badge/JavaDoc-Online-green)](https://marginallyclever.github.io/Donatello/) 6 | 7 | ## What is it? 8 | 9 | Donatello is a Java-based Integrated Development Environment (IDE) designed for 10 | [Flow-Based Programming (FBP)](https://en.wikipedia.org/wiki/Flow-based_programming). 11 | It offers a graphical interface that simplifies the creation and management of complex 12 | data flows, making it accessible for both novice and experienced developers. 13 | 14 | ## Features 15 | 16 | - No-code style programming with a graphical interface. 17 | - Syntax errors are impossible to make. The editor will not let you connect nodes in a way that doesn't make sense. 18 | - Nodes can run in parallel using virtual threads (JEP 425). 19 | 20 | ## Screenshots 21 | 22 | ![img](preview-for-github.png) 23 | 24 | ## Getting started (Developers) 25 | 26 | 1. Clone [Donatello](https://github.com/MarginallyClever/Donatello/). 27 | 2. Use your favorite IDE to import the Maven project. 28 | 3. Donatello can be built to run on its own, or as a plugin in your project. 29 | 30 | `./src/test/java/com/marginallyclever/donatello` has unit tests, which are also examples of how to use the API. 31 | 32 | ## Use it, Discuss it, Love it. 33 | 34 | - Please see the [Javadoc with the full node graph API](https://marginallyclever.github.io/Donatello/). 35 | - Please see guide for [how to Contribute](https://github.com/MarginallyClever/Donatello/blob/main/CONTRIBUTING.md) 36 | - The [Official webpage](https://github.com/MarginallyClever/Donatello/)! 37 | - Join [the Discord channel](https://discord.gg/Q5TZFmB) and make new friends. 38 | 39 | ## Based on work by 40 | 41 | - https://github.com/otto-link/GNode/ 42 | - https://github.com/jpaulm/javafbp/ 43 | - https://nodes.io/story/ 44 | - https://github.com/janbijster/cobble 45 | - https://github.com/kenk42292/shoyu 46 | - https://github.com/paceholder/nodeeditor 47 | - https://github.com/miho/VWorkflows 48 | - https://nodered.org/ 49 | - Maya, Unity, Blender 50 | - NoFlo, Flowhub 51 | - and others 52 | 53 | ## Learn more 54 | 55 | - [Flow based programming Discord](https://discord.com/invite/YBQj6UsD5H) 56 | - https://jpaulm.github.io/fbp/ 57 | 58 | ## Icons 59 | 60 | Many app icons provided by http://icons8.com. 61 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Java CI with Maven 2 | 3 | env: 4 | # find out this value by opening https://api.github.com/repos///releases 5 | # in your browser and copy the full "upload_url" value including the {?name,label} part 6 | UPLOAD_URL: https://uploads.github.com/repos/MarginallyClever/Donatello/releases/197988121/assets{?name,label} 7 | RELEASE_ID: 197988121 # same as above (id can just be taken out the upload_url, it's used to find old releases) 8 | 9 | on: 10 | push: 11 | branches: 12 | - main 13 | pull_request: 14 | 15 | jobs: 16 | build: 17 | runs-on: windows-latest 18 | steps: 19 | - uses: actions/checkout@v4.2.2 20 | 21 | - uses: actions/setup-java@v4.7.0 22 | with: 23 | distribution: 'temurin' 24 | java-version: '22' 25 | cache: 'maven' 26 | 27 | # from https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-maven 28 | # runs for all pushes and pull requests 29 | - name: Build and Test with Maven 30 | timeout-minutes: 15 31 | run: mvn --batch-mode --update-snapshots package 32 | 33 | - name: Prepare nightly package 34 | # only run on pushes to master or main 35 | if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/main' || github.ref == 'refs/heads/dev' 36 | shell: bash 37 | run: | 38 | mkdir -p target/universal-package && 39 | cp CONTRIBUTING.md LICENSE README.md target/universal-package/ && 40 | cp target/donatello-*.jar target/universal-package/ && 41 | cd target/universal-package/ && 42 | 7z a -tzip Donatello.zip . && 43 | mv Donatello.zip ../.. 44 | 45 | - name: Deploy nightly release 46 | # only run on pushes to master or main 47 | if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/main' || github.ref == 'refs/heads/dev' 48 | uses: WebFreak001/deploy-nightly@v1.1.0 49 | env: 50 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # automatically provided by github actions 51 | with: 52 | upload_url: ${{ env.UPLOAD_URL }} 53 | release_id: ${{ env.RELEASE_ID }}{ 54 | asset_path: Donatello.zip # path to archive to upload 55 | asset_name: Donatello-nightly-$$.zip # name to upload the release as, use $$ to insert date (YYYYMMDD) and 6 letter commit hash 56 | asset_content_type: application/zip # required by GitHub API 57 | max_releases: 1 # optional, if there are more releases than this matching the asset_name, the oldest ones are going to be deleted 58 | -------------------------------------------------------------------------------- /.github/workflows/publish-javadoc.yml: -------------------------------------------------------------------------------- 1 | name: Deploy Javadoc 2 | 3 | on: 4 | push: 5 | branches: 6 | - gh-pages 7 | 8 | jobs: 9 | publish: 10 | runs-on: ubuntu-latest 11 | steps: 12 | # Step 1: Checkout the repository 13 | - name: Checkout code 14 | uses: actions/checkout@v3 15 | 16 | # Step 2: Set up Java (required for building and generating Javadoc) 17 | - name: Set up Java 18 | uses: actions/setup-java@v3 19 | with: 20 | distribution: 'temurin' 21 | java-version: '22' 22 | 23 | # Step 0: Get project version 24 | - name: Get project version 25 | id: get_version 26 | run: echo "version=$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout)" >> $GITHUB_OUTPUT 27 | 28 | # Step 3: Build and generate Javadoc 29 | - name: Build project and generate Javadoc 30 | run: | 31 | mvn clean install # Clean and build the project 32 | mvn javadoc:javadoc # Generate Javadoc 33 | 34 | # Step 5: Deploy Javadoc to GitHub Pages 35 | - name: Deploy JavaDoc 🚀 36 | uses: MathieuSoysal/Javadoc-publisher.yml@v2.4.0 37 | with: 38 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 39 | java-version: 22 40 | javadoc-branch: javadoc 41 | java-distribution: temurin 42 | project: maven 43 | # url will be https://.github.io// 44 | # update this to the project version number 45 | target-folder: ${{ steps.get_version.outputs.version }} 46 | 47 | # Step 6: Generate index.html in the javadoc branch root 48 | - name: Generate index.html for all versions 49 | run: | 50 | git fetch origin javadoc:javadoc 51 | git checkout javadoc 52 | # Generate index.html listing all version folders 53 | echo ' 54 | Donatello Documentation Versions 55 |

Available Donatello Documentation Versions

    ' > index.html 56 | for dir in $(find . -maxdepth 1 -type d ! -name '.' ! -name 'assets' ! -name 'static'); do 57 | version=$(basename "$dir") 58 | [[ "$version" =~ ^[0-9]+\.[0-9]+(\.[0-9]+)?$ ]] || continue 59 | echo "
  • $version
  • " >> index.html 60 | done 61 | echo '
' >> index.html 62 | git add index.html 63 | git commit -m "Update index.html for Javadoc versions" || echo "No changes to commit" 64 | git push origin javadoc -------------------------------------------------------------------------------- /src/main/java/com/marginallyclever/donatello/actions/undoable/NodeAddAction.java: -------------------------------------------------------------------------------- 1 | package com.marginallyclever.donatello.actions.undoable; 2 | 3 | import com.marginallyclever.donatello.Donatello; 4 | import com.marginallyclever.donatello.nodefactorypanel.NodeFactoryPanel; 5 | import com.marginallyclever.donatello.edits.NodeAddEdit; 6 | import com.marginallyclever.nodegraphcore.Graph; 7 | import com.marginallyclever.nodegraphcore.Node; 8 | import org.slf4j.Logger; 9 | import org.slf4j.LoggerFactory; 10 | 11 | import javax.swing.*; 12 | import java.awt.*; 13 | import java.awt.event.ActionEvent; 14 | import java.security.InvalidParameterException; 15 | 16 | /** 17 | * Launches the "Add Node" dialog. If the user clicks "Ok" then the selected {@link Node} type is added to the 18 | * current editor {@link Graph}. 19 | * @author Dan Royer 20 | * @since 2022-02-21 21 | */ 22 | public class NodeAddAction extends AbstractAction { 23 | private static final Logger logger = LoggerFactory.getLogger(NodeAddAction.class); 24 | 25 | private final Donatello editor; 26 | private final NodeFactoryPanel nodeFactoryPanel; 27 | 28 | /** 29 | * Constructor for subclasses to call. 30 | * @param name the name of this action visible on buttons and menu items. 31 | */ 32 | public NodeAddAction(String name, Donatello editor, NodeFactoryPanel nodeFactoryPanel) { 33 | super(name); 34 | this.editor = editor; 35 | this.nodeFactoryPanel = nodeFactoryPanel; 36 | } 37 | 38 | /** 39 | * Launches the "Add Node" dialog. 40 | * @param e the event to be processed 41 | */ 42 | @Override 43 | public void actionPerformed(ActionEvent e) { 44 | JFrame parentFrame = (JFrame)SwingUtilities.getWindowAncestor((Component)e.getSource()); 45 | JDialog dialog = new JDialog(parentFrame, "Add Node", true); 46 | dialog.add(nodeFactoryPanel); 47 | dialog.pack(); 48 | dialog.setLocationRelativeTo(parentFrame); 49 | dialog.setVisible(true); 50 | } 51 | 52 | /** 53 | * Adds the given node to the editor's graph and creates an undoable edit. 54 | * @param n the node to add. 55 | */ 56 | public void commitAdd(Node n, Point p) { 57 | if(n==null) throw new InvalidParameterException("NodeAddAction.commitAdd(null)"); 58 | 59 | System.out.println("Adding "+n.getName() + " ("+n.getUniqueID()+")"); 60 | n.setPosition(p); 61 | n.updateBounds(); 62 | editor.addEdit(new NodeAddEdit((String)this.getValue(Action.NAME),editor,n)); 63 | } 64 | 65 | public NodeFactoryPanel getNodeFactoryPanel() { 66 | return nodeFactoryPanel; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/main/java/com/marginallyclever/donatello/actions/SelectionShrinkAction.java: -------------------------------------------------------------------------------- 1 | package com.marginallyclever.donatello.actions; 2 | 3 | import com.marginallyclever.donatello.Donatello; 4 | import com.marginallyclever.nodegraphcore.Node; 5 | import com.marginallyclever.nodegraphcore.Connection; 6 | 7 | import javax.swing.*; 8 | import java.awt.event.ActionEvent; 9 | import java.util.ArrayList; 10 | import java.util.List; 11 | 12 | /** 13 | * Find and de-select the outer-most {@link Node}s from the list of selected nodes. 14 | */ 15 | public class SelectionShrinkAction extends AbstractAction implements EditorAction { 16 | /** 17 | * The editor being affected. 18 | */ 19 | private final Donatello editor; 20 | 21 | /** 22 | * Constructor for subclasses to call. 23 | * @param name the name of this action visible on buttons and menu items. 24 | * @param editor the editor affected by this Action. 25 | */ 26 | public SelectionShrinkAction(String name, Donatello editor) { 27 | super(name); 28 | this.editor = editor; 29 | } 30 | 31 | /** 32 | * Find all selected nodes that do not have an input to another selected node OR do not have an output to another 33 | * selected node. Then de-select all of the found nodes. 34 | */ 35 | @Override 36 | public void actionPerformed(ActionEvent e) { 37 | List connections = editor.getGraph().getConnections(); 38 | List selectedNodes = new ArrayList<>(editor.getSelectedNodes()); 39 | List edgeNodes = new ArrayList<>(); 40 | 41 | for( Node n : selectedNodes ) { 42 | int fromCount=0; 43 | int toCount=0; 44 | 45 | for( Connection c : connections ) { 46 | if (c.isConnectedTo(n)) { 47 | if(c.getFrom()==n) fromCount++; 48 | if(c.getTo()==n) toCount++; 49 | 50 | // node has a connection to another selected node? 51 | if( !selectedNodes.contains(c.getOtherNode(n)) ) { 52 | edgeNodes.add(n); 53 | break; 54 | } 55 | } 56 | } 57 | if(fromCount==0 || toCount==0) { 58 | edgeNodes.add(n); 59 | } 60 | } 61 | 62 | System.out.println("edge nodes: "+edgeNodes.size()); 63 | 64 | selectedNodes.removeAll(edgeNodes); 65 | editor.setSelectedNodes(selectedNodes); 66 | editor.repaint(); 67 | } 68 | 69 | @Override 70 | public void updateEnableStatus() { 71 | setEnabled(editor.getSelectedNodes().size()>0); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/test/java/com/marginallyclever/donatello/TestGraphSwing.java: -------------------------------------------------------------------------------- 1 | package com.marginallyclever.donatello; 2 | 3 | import com.marginallyclever.donatello.actions.GraphSaveAsAction; 4 | import com.marginallyclever.donatello.nodes.images.LoadImage; 5 | import com.marginallyclever.donatello.nodes.images.PrintImage; 6 | import com.marginallyclever.donatello.ports.Filename; 7 | import com.marginallyclever.nodegraphcore.Connection; 8 | import com.marginallyclever.nodegraphcore.DAO4JSONFactory; 9 | import com.marginallyclever.nodegraphcore.NodeFactory; 10 | import org.junit.jupiter.api.AfterAll; 11 | import org.junit.jupiter.api.BeforeAll; 12 | import org.junit.jupiter.api.Test; 13 | 14 | import java.util.Objects; 15 | 16 | import static org.junit.jupiter.api.Assertions.*; 17 | 18 | /** 19 | * Test the GraphSwing elements. 20 | * @author Dan Royer 21 | * @since 2022-02-21 22 | */ 23 | public class TestGraphSwing { 24 | @BeforeAll 25 | public static void beforeAll() { 26 | NodeFactory.loadRegistries(); 27 | DAO4JSONFactory.loadRegistries(); 28 | } 29 | 30 | @AfterAll 31 | public static void afterAll() { 32 | NodeFactory.clear(); 33 | DAO4JSONFactory.clear(); 34 | } 35 | 36 | /** 37 | * Make sure all nodes introduced in this package can be created. 38 | */ 39 | @Test 40 | public void testFactoryCreatesAllSwingTypes() { 41 | assertNotEquals(0,NodeFactory.getNames().length); 42 | System.out.print("Create all Swing types: "); 43 | String add = ""; 44 | for(String s : NodeFactory.getNames()) { 45 | System.out.print(add+s); 46 | add=", "; 47 | assertNotNull(NodeFactory.createNode(s)); 48 | } 49 | System.out.println(); 50 | } 51 | 52 | @Test 53 | public void testImages() { 54 | LoadImage img2 = new LoadImage(); 55 | img2.getPort(0).setValue(new Filename("doesNotExist.png")); 56 | img2.update(); 57 | 58 | LoadImage img = new LoadImage(); 59 | // get filename of test image 60 | String fname = Objects.requireNonNull(TestGraphSwing.class.getResource("test.png")).getPath(); 61 | img.getPort(0).setValue(new Filename(fname)); 62 | img.update(); 63 | 64 | PrintImage printer = new PrintImage(); 65 | Connection c = new Connection(img,1,printer,0); 66 | } 67 | 68 | @Test 69 | public void testAddExtension() { 70 | GraphSaveAsAction actionSaveGraph = new GraphSaveAsAction(null,"Save",null); 71 | assertEquals("test.graph",actionSaveGraph.addExtensionIfNeeded("test")); 72 | assertEquals("test.graph",actionSaveGraph.addExtensionIfNeeded("test.graph")); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/test/java/com/marginallyclever/donatello/ClassLoadingTest.java: -------------------------------------------------------------------------------- 1 | package com.marginallyclever.donatello; 2 | 3 | import com.marginallyclever.nodegraphcore.*; 4 | import org.junit.jupiter.api.Test; 5 | import org.reflections.Reflections; 6 | import org.reflections.scanners.ResourcesScanner; 7 | import org.reflections.scanners.Scanner; 8 | 9 | import java.io.File; 10 | import java.lang.reflect.Constructor; 11 | import java.lang.reflect.InvocationTargetException; 12 | import java.net.MalformedURLException; 13 | import java.net.URL; 14 | import java.net.URLClassLoader; 15 | import java.nio.file.FileSystems; 16 | import java.util.Arrays; 17 | import java.util.ServiceLoader; 18 | 19 | import static org.junit.jupiter.api.Assertions.assertEquals; 20 | 21 | public class ClassLoadingTest { 22 | @Test 23 | public void listAllNodes() throws Exception { 24 | NodeFactory.loadRegistries(); 25 | System.out.println("List all nodes: "+ Arrays.toString(NodeFactory.getNames())); 26 | NodeFactory.clear(); 27 | } 28 | 29 | @Test 30 | public void listAllDAOs() { 31 | ServiceLoaderHelper helper = new ServiceLoaderHelper(); 32 | ClassLoader classLoader = helper.getExtensionClassLoader(); 33 | ServiceLoader loader = ServiceLoader.load(DAORegistry.class, classLoader); 34 | for (DAORegistry registry : loader) { 35 | registry.registerDAO(); 36 | } 37 | System.out.println("List all Donatello DAOs: "+Arrays.toString(DAO4JSONFactory.getNames())); 38 | DAO4JSONFactory.clear(); 39 | } 40 | 41 | @Test 42 | public void whoIsMyClassLoader() throws Exception { 43 | ServiceLoaderHelper helper = new ServiceLoaderHelper(); 44 | ClassLoader classLoader = helper.getExtensionClassLoader(); 45 | NodeFactory.loadRegistries(); 46 | if(NodeFactory.knowsAbout("PrintTurtle")) { 47 | Node pt = NodeFactory.createNode("PrintTurtle"); 48 | ClassLoader addLoader = pt.getClass().getClassLoader(); 49 | NodeFactory.clear(); 50 | assertEquals(addLoader, classLoader); 51 | } else { 52 | System.out.println("Did not thoroughly run this test"); 53 | } 54 | NodeFactory.clear(); 55 | } 56 | 57 | @Test 58 | public void testLoadingDonatelloExtensionsIfAvailable() throws Exception { 59 | System.out.println("Loading Donatello extensions: "+FileHelper.getExtensionPath()); 60 | 61 | ServiceLoaderHelper.addAllPathFiles(FileHelper.getExtensionPath()); 62 | 63 | NodeFactory.loadRegistries(); 64 | System.out.println("All Donatello nodes: "+Arrays.toString(NodeFactory.getNames())); 65 | NodeFactory.clear(); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/test/java/com/marginallyclever/donatello/GraphViewSettingsTest.java: -------------------------------------------------------------------------------- 1 | package com.marginallyclever.donatello; 2 | 3 | import com.marginallyclever.donatello.graphview.GraphViewSettings; 4 | import org.junit.jupiter.api.Assertions; 5 | import org.junit.jupiter.api.Test; 6 | 7 | import java.awt.Color; 8 | import java.io.IOException; 9 | 10 | public class GraphViewSettingsTest { 11 | @Test 12 | public void testToFromJSON() throws IOException { 13 | GraphViewSettings a = new GraphViewSettings(); 14 | a.setNodeColorBackground(Color.CYAN); 15 | a.setNodeColorBorder(Color.MAGENTA); 16 | a.setNodeColorInternalBorder(Color.DARK_GRAY); 17 | a.setPanelColorBackground(Color.LIGHT_GRAY); 18 | a.setPanelGridColor(Color.GRAY); 19 | a.setGridSize(10); 20 | a.setNodeColorFontClean(Color.BLACK); 21 | a.setNodeColorFontDirty(Color.RED); 22 | a.setNodeColorTitleFont(Color.WHITE); 23 | a.setNodeColorTitleBackground(Color.BLACK); 24 | a.setConnectionPointColor(Color.LIGHT_GRAY); 25 | a.setConnectionColor(Color.BLUE); 26 | a.setCornerRadius(5); 27 | a.setDrawBackground(true); 28 | a.setDrawCursor(true); 29 | a.setDrawOrigin(true); 30 | 31 | GraphViewSettings b = new GraphViewSettings(); 32 | b.fromJSON(a.toJSON()); 33 | 34 | Assertions.assertEquals(a.getNodeColorBackground(), b.getNodeColorBackground()); 35 | Assertions.assertEquals(a.getNodeColorBorder(), b.getNodeColorBorder()); 36 | Assertions.assertEquals(a.getNodeColorInternalBorder(), b.getNodeColorInternalBorder()); 37 | Assertions.assertEquals(a.getPanelColorBackground(), b.getPanelColorBackground()); 38 | Assertions.assertEquals(a.getPanelGridColor(), b.getPanelGridColor()); 39 | Assertions.assertEquals(a.getGridSize(), b.getGridSize()); 40 | Assertions.assertEquals(a.getNodeColorFontClean(), b.getNodeColorFontClean()); 41 | Assertions.assertEquals(a.getNodeColorFontDirty(), b.getNodeColorFontDirty()); 42 | Assertions.assertEquals(a.getNodeColorTitleFont(), b.getNodeColorTitleFont()); 43 | Assertions.assertEquals(a.getNodeColorTitleBackground(), b.getNodeColorTitleBackground()); 44 | Assertions.assertEquals(a.getConnectionPointColor(), b.getConnectionPointColor()); 45 | Assertions.assertEquals(a.getConnectionColor(), b.getConnectionColor()); 46 | Assertions.assertEquals(a.getCornerRadius(), b.getCornerRadius()); 47 | Assertions.assertEquals(a.getDrawBackgroundGrid(), b.getDrawBackgroundGrid()); 48 | Assertions.assertEquals(a.getDrawCursor(), b.getDrawCursor()); 49 | Assertions.assertEquals(a.getDrawOrigin(), b.getDrawOrigin()); 50 | } 51 | } -------------------------------------------------------------------------------- /src/main/java/com/marginallyclever/donatello/select/SelectInteger.java: -------------------------------------------------------------------------------- 1 | package com.marginallyclever.donatello.select; 2 | 3 | import javax.swing.*; 4 | import javax.swing.text.DefaultFormatterFactory; 5 | import javax.swing.text.NumberFormatter; 6 | import java.awt.*; 7 | import java.text.NumberFormat; 8 | import java.util.Locale; 9 | 10 | 11 | /** 12 | * A JFormattedTextField that sets itself up to format integers. 13 | * @author Dan Royer 14 | * @since 7.24.0 15 | */ 16 | public class SelectInteger extends Select { 17 | private final JFormattedTextField field = new JFormattedTextField(); 18 | private final JLabel label; 19 | private int value; 20 | 21 | public SelectInteger(String internalName,String labelKey,Locale locale,int defaultValue) { 22 | super(internalName); 23 | 24 | value = defaultValue; 25 | 26 | field.setName(internalName+".field"); 27 | createAndAttachFormatter(locale); 28 | Dimension d = field.getPreferredSize(); 29 | d.width = 100; 30 | field.setPreferredSize(d); 31 | field.setMinimumSize(d); 32 | field.setHorizontalAlignment(JTextField.RIGHT); 33 | field.setValue(defaultValue); 34 | field.setColumns(20); 35 | field.getDocument().addDocumentListener(new DelayedDocumentValidator(field,newValue-> { 36 | if (value != newValue) { 37 | int oldValue = value; 38 | value = newValue.intValue(); 39 | fireSelectEvent(oldValue, value); 40 | } 41 | })); 42 | 43 | label = createLabel(labelKey); 44 | } 45 | 46 | public SelectInteger(String internalName,String labelKey,Locale locale) { 47 | this(internalName,labelKey,locale,0); 48 | } 49 | 50 | public SelectInteger(String internalName,String labelKey,int defaultValue) { 51 | this(internalName,labelKey,Locale.getDefault(),defaultValue); 52 | } 53 | 54 | @Override 55 | public void attach(JComponent panel, GridBagConstraints gbc) { 56 | gbc.anchor = GridBagConstraints.LINE_START; 57 | gbc.gridx=0; 58 | panel.add(label,gbc); 59 | gbc.gridx=1; 60 | gbc.anchor = GridBagConstraints.LINE_END; 61 | panel.add(field,gbc); 62 | } 63 | 64 | @Override 65 | public void setReadOnly(boolean state) { 66 | field.setEnabled(!state); 67 | } 68 | 69 | protected void createAndAttachFormatter(Locale locale) { 70 | NumberFormat numberFormat = NumberFormat.getIntegerInstance(locale); 71 | numberFormat.setGroupingUsed(false); 72 | 73 | field.setFormatterFactory(new DefaultFormatterFactory(new NumberFormatter(numberFormat))); 74 | } 75 | 76 | public void setReadOnly() { 77 | field.setEditable(false); 78 | } 79 | 80 | /** 81 | * @return last valid integer typed into field. 82 | */ 83 | public int getValue() { 84 | return value; 85 | } 86 | 87 | public void setValue(int arg0) { 88 | field.setText(Integer.toString(arg0)); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/main/java/com/marginallyclever/donatello/select/SelectTextArea.java: -------------------------------------------------------------------------------- 1 | package com.marginallyclever.donatello.select; 2 | 3 | import javax.swing.*; 4 | import javax.swing.event.DocumentEvent; 5 | import javax.swing.event.DocumentListener; 6 | import java.awt.*; 7 | 8 | /** 9 | * A text input dialog with some limited formatting options. 10 | * @author Dan Royer 11 | * @since 7.24.0 12 | */ 13 | public class SelectTextArea extends Select { 14 | private final JTextArea field; 15 | private final JLabel label; 16 | private final JScrollPane pane; 17 | 18 | public SelectTextArea(String internalName,String labelKey,String defaultText) { 19 | super(internalName); 20 | //this.setBorder(BorderFactory.createLineBorder(Color.RED)); 21 | 22 | field = new JTextArea(defaultText, 4, 20); 23 | field.setName(internalName+".field"); 24 | field.setLineWrap(true); 25 | field.setWrapStyleWord(true); 26 | field.setBorder(BorderFactory.createLoweredBevelBorder()); 27 | field.setFont(UIManager.getFont("Label.font")); 28 | field.getDocument().addDocumentListener(new DocumentListener() { 29 | @Override 30 | public void insertUpdate(DocumentEvent e) { 31 | validate(); 32 | } 33 | 34 | @Override 35 | public void removeUpdate(DocumentEvent e) { 36 | validate(); 37 | } 38 | 39 | @Override 40 | public void changedUpdate(DocumentEvent e) { 41 | validate(); 42 | } 43 | 44 | void validate() { 45 | fireSelectEvent(null, field.getText()); 46 | } 47 | }); 48 | 49 | pane = new JScrollPane(field); 50 | pane.setPreferredSize(new Dimension(200, 150)); 51 | 52 | label = createLabel(labelKey); 53 | } 54 | 55 | @Override 56 | public void attach(JComponent panel, GridBagConstraints gbc) { 57 | gbc.fill = GridBagConstraints.HORIZONTAL; 58 | gbc.anchor = GridBagConstraints.PAGE_START; 59 | gbc.gridx=0; 60 | gbc.gridwidth=2; 61 | panel.add(label,gbc); 62 | gbc.gridy++; 63 | gbc.anchor = GridBagConstraints.CENTER; 64 | panel.add(pane,gbc); 65 | gbc.gridwidth=1; 66 | } 67 | 68 | @Override 69 | public void setReadOnly(boolean state) { 70 | field.setEnabled(!state); 71 | } 72 | 73 | public String getText() { 74 | return field.getText(); 75 | } 76 | 77 | public void setText(String str) { 78 | field.setText(str); 79 | } 80 | 81 | public void setLineWrap(boolean wrap) { 82 | field.setLineWrap(wrap); 83 | } 84 | 85 | public boolean getLineWrap() { 86 | return field.getLineWrap(); 87 | } 88 | 89 | public boolean isEditable() { 90 | return field.isEditable(); 91 | } 92 | 93 | public void setEditable(boolean b) { 94 | field.setEditable(b); 95 | } 96 | 97 | public boolean getDragEnabled() { 98 | return field.getDragEnabled(); 99 | } 100 | 101 | public void setDragEnabled(boolean b) { 102 | field.setDragEnabled(b); 103 | } 104 | } 105 | --------------------------------------------------------------------------------