-
Notifications
You must be signed in to change notification settings - Fork 3
/
Copy pathSimpleEditorDemo.java
516 lines (462 loc) · 20 KB
/
SimpleEditorDemo.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
/****************************************************************************
**
** This demo file is part of yFiles for JavaFX 3.6.
**
** Copyright (c) 2000-2023 by yWorks GmbH, Vor dem Kreuzberg 28,
** 72070 Tuebingen, Germany. All rights reserved.
**
** yFiles demo files exhibit yFiles for JavaFX functionalities. Any redistribution
** of demo files in source code or binary form, with or without
** modification, is not permitted.
**
** Owners of a valid software license for a yFiles for JavaFX version that this
** demo is shipped with are allowed to use the demo source code as basis
** for their own yFiles for JavaFX powered applications. Use of such programs is
** governed by the rights and conditions as set out in the yFiles for JavaFX
** license agreement.
**
** THIS SOFTWARE IS PROVIDED ''AS IS'' AND ANY EXPRESS OR IMPLIED
** WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
** MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN
** NO EVENT SHALL yWorks BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
** TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
** PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
** LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
** NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
** SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
**
***************************************************************************/
package complete.simpleeditor;
import com.yworks.yfiles.geometry.InsetsD;
import com.yworks.yfiles.graph.FoldingManager;
import com.yworks.yfiles.graph.IFoldingView;
import com.yworks.yfiles.graph.IGraph;
import com.yworks.yfiles.layout.ILayoutAlgorithm;
import com.yworks.yfiles.layout.circular.CircularLayout;
import com.yworks.yfiles.layout.hierarchic.HierarchicLayout;
import com.yworks.yfiles.layout.organic.OrganicLayout;
import com.yworks.yfiles.layout.orthogonal.OrthogonalLayout;
import com.yworks.yfiles.layout.radial.RadialLayout;
import com.yworks.yfiles.layout.router.OrganicEdgeRouter;
import com.yworks.yfiles.layout.router.polyline.EdgeRouter;
import com.yworks.yfiles.layout.router.polyline.EdgeRoutingStyle;
import com.yworks.yfiles.layout.tree.BalloonLayout;
import com.yworks.yfiles.layout.tree.TreeLayout;
import com.yworks.yfiles.layout.tree.TreeReductionStage;
import com.yworks.yfiles.view.CanvasControl;
import com.yworks.yfiles.view.CanvasPrinter;
import com.yworks.yfiles.view.GraphControl;
import com.yworks.yfiles.view.GraphOverviewControl;
import com.yworks.yfiles.view.GridInfo;
import com.yworks.yfiles.view.GridVisualCreator;
import com.yworks.yfiles.view.ICanvasObjectDescriptor;
import com.yworks.yfiles.view.input.GraphEditorInputMode;
import com.yworks.yfiles.view.input.GraphSnapContext;
import com.yworks.yfiles.view.input.GridConstraintProvider;
import com.yworks.yfiles.view.input.GridSnapTypes;
import com.yworks.yfiles.view.input.ICommand;
import com.yworks.yfiles.view.input.IInputMode;
import com.yworks.yfiles.view.input.LabelSnapContext;
import com.yworks.yfiles.view.input.OrthogonalEdgeEditingContext;
import com.yworks.yfiles.view.input.WaitInputMode;
import toolkit.BitmapExportHelper;
import toolkit.CommandMenuItem;
import toolkit.DemoApplication;
import toolkit.DemoStyles;
import toolkit.WebViewUtils;
import javafx.application.Platform;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.print.PrinterJob;
import javafx.scene.control.Alert;
import javafx.scene.control.ButtonType;
import javafx.scene.control.Menu;
import javafx.scene.control.MenuBar;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.SeparatorMenuItem;
import javafx.scene.control.ToggleButton;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyCodeCombination;
import javafx.scene.input.KeyCombination;
import javafx.scene.web.WebView;
import javafx.stage.FileChooser;
import java.io.File;
import java.io.IOException;
import java.time.Duration;
/**
* Simple demo that hosts a {@link GraphControl}
* which enables graph editing via the default {@link GraphEditorInputMode}
* input mode for editing graphs.
* <p>This demo also supports grouped graphs, i.e., selected nodes can be grouped
* in so-called group nodes using Ctrl/Command-G, and again be un-grouped using Ctrl/Command-U.
* To move sets of nodes into and out of group nodes using the mouse, hold down
* the SHIFT key while dragging.</p>
* <p>
* Apart from graph editing, the demo demonstrates various basic features that are already
* present on GraphControl (either as predefined commands or as simple method calls), e.g.
* load/save/export.
* </p>
* <p>
* In addition to the GraphControl itself, the demo also shows how to use the GraphOverviewControl.
</p>
*/
public class SimpleEditorDemo extends DemoApplication {
private static final String[] IMAGE_FILE_EXTENSIONS = {".jpg", ".jpeg", ".jpe", ".png", ".bmp"};
private GridVisualCreator grid;
private GraphSnapContext graphSnapContext;
private LabelSnapContext labelSnapContext;
private static final int GRID_SIZE = 50;
private static final GridInfo GRID_INFO = new GridInfo(GRID_SIZE);
public GraphControl graphControl;
private WaitInputMode waitInputMode;
public GraphOverviewControl overviewControl;
public WebView webView;
public MenuBar menubar;
/**
* Initializes the controller. This is called when the FXMLLoader instantiates the scene graph.
* At the time this method is called, all nodes in the scene graph is available. Most importantly,
* the GraphControl instance is initialized.
*/
public void initialize() {
overviewControl.setGraphControl(graphControl);
overviewControl.setHorizontalScrollBarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
overviewControl.setVerticalScrollBarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
WebViewUtils.initHelp(webView, this);
}
/**
* Initializes the graph and the input mode.
*/
public void onLoaded() {
// initialize the graph
initializeGraph();
// initialize the grid for grid snapping
initializeGrid();
// initialize the snap context
initializeSnapContext();
// initialize the input mode
initializeInputModes();
// initializes the menu that contains the available layout algorithms for this demo
initializeLayoutMenu();
// loads the example graph
loadSampleGraph();
}
/**
* Adds a menu to the menu bar that contains CommandMenuItems. These
* menu items invoke a specific layouting algorithm.
*/
private void initializeLayoutMenu() {
ObservableList<Menu> menus = menubar.getMenus();
Menu layoutMenu = new Menu("Layout");
addCommandMenuItem(layoutMenu, "Hierarchic", new HierarchicLayout());
addCommandMenuItem(layoutMenu, "Organic", new OrganicLayout());
addCommandMenuItem(layoutMenu, "Orthogonal", new OrthogonalLayout());
addCommandMenuItem(layoutMenu, "Circular", new CircularLayout());
// TreeLayout will fail with exception for graphs that are not trees.
// If we prepend a TreeReductionStage, those graphs will be reduced
// to trees and then those layouts can safely be calculated.
TreeLayout treeLayout = new TreeLayout();
treeLayout.prependStage(new TreeReductionStage());
addCommandMenuItem(layoutMenu, "Tree", treeLayout);
// BalloonLayout will fail with exception for graphs that are not trees.
// If we prepend a TreeReductionStage, those graphs will be reduced
// to trees and then those layouts can safely be calculated.
BalloonLayout balloonLayout = new BalloonLayout();
balloonLayout.prependStage(new TreeReductionStage());
addCommandMenuItem(layoutMenu, "Balloon", balloonLayout);
addCommandMenuItem(layoutMenu, "Radial", new RadialLayout());
layoutMenu.getItems().add(new SeparatorMenuItem());
addCommandMenuItem(layoutMenu, "Orthogonal Router", new EdgeRouter());
addCommandMenuItem(layoutMenu, "Organic Router", new OrganicEdgeRouter());
EdgeRouter polylineRouter = new EdgeRouter();
polylineRouter.getDefaultEdgeLayoutDescriptor().setRoutingStyle(EdgeRoutingStyle.OCTILINEAR);
addCommandMenuItem(layoutMenu, "Polyline Router", polylineRouter);
menus.add(layoutMenu);
}
/**
* Creates and initializes a CommandMenuItem using the given command name and parameter and adds it to the given menu.
* @param menu The menu to add the CommandMenuItem to.
* @param commandName The name for the command (will be displayed as text).
* @param commandParameter The parameter for the command.
*/
private void addCommandMenuItem(Menu menu, String commandName, Object commandParameter) {
CommandMenuItem item = new CommandMenuItem();
item.setText(commandName);
item.setCommand(RUN_LAYOUT);
item.setCommandParameter(commandParameter);
item.setCommandTarget(graphControl);
menu.getItems().add(item);
}
/**
* Loads a sample graph from GraphML for this demo.
*/
private void loadSampleGraph() {
try {
graphControl.importFromGraphML(getClass().getResource("resources/example.graphml"));
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* Initializes the grid feature.
*/
private void initializeGrid() {
grid = new GridVisualCreator(GRID_INFO);
graphControl.getBackgroundGroup().addChild(grid, ICanvasObjectDescriptor.ALWAYS_DIRTY_INSTANCE);
// disable the grid by default
grid.setVisible(false);
}
/**
* Initializes the snapping feature.
*/
private void initializeSnapContext() {
graphSnapContext = new GraphSnapContext();
graphSnapContext.setEnabled(false);
// disable grid snapping because grid is disabled by default
graphSnapContext.setGridSnapType(GridSnapTypes.NONE);
// add constraint provider for nodes, bends, and ports
graphSnapContext.setNodeGridConstraintProvider(new GridConstraintProvider<>(GRID_INFO));
graphSnapContext.setBendGridConstraintProvider(new GridConstraintProvider<>(GRID_INFO));
graphSnapContext.setPortGridConstraintProvider(new GridConstraintProvider<>(GRID_INFO));
// initialize label snapping
labelSnapContext = new LabelSnapContext();
// set maximum distance between the current mouse coordinates and the coordinates to which the mouse will snap
labelSnapContext.setSnapDistance(15);
// set the amount by which snap lines that are induced by existing edge segments are being extended
labelSnapContext.setSnapLineExtension(100);
}
/**
* Calls {@link #createEditorMode()} and registers
* the result as the {@link CanvasControl#getInputMode()}.
*/
private void initializeInputModes() {
IInputMode geim = createEditorMode();
graphControl.setInputMode(geim);
}
/**
* Creates the default input mode for the GraphControl,
* @see GraphEditorInputMode
* @return a new GraphEditorInputMode instance and configures snapping and orthogonal edge editing
*/
public IInputMode createEditorMode() {
GraphEditorInputMode mode = new GraphEditorInputMode();
mode.setSnapContext(graphSnapContext);
mode.setLabelSnapContext(labelSnapContext);
OrthogonalEdgeEditingContext orthogonalEdgeEditingContext = new OrthogonalEdgeEditingContext();
orthogonalEdgeEditingContext.setEnabled(false);
mode.setOrthogonalEdgeEditingContext(orthogonalEdgeEditingContext);
waitInputMode = mode.getWaitInputMode();
// make bend creation more important than moving of selected edges
// this has the effect that dragging a selected edge (not its bends)
// will create a new bend instead of moving all bends
// This is especially nicer in conjunction with orthogonal
// edge editing because this creates additional bends every time
// the edge is moved otherwise
mode.getCreateBendInputMode().setPriority(mode.getMoveInputMode().getPriority() - 1);
// enable grouping operations such as grouping selected nodes moving nodes
// into group nodes
mode.setGroupingOperationsAllowed(true);
// add command bindings for 'run layout'
mode.getKeyboardInputMode().addCommandBinding(RUN_LAYOUT, this::executeLayout, this::canExecuteLayout);
mode.getKeyboardInputMode().addKeyBinding(new KeyCodeCombination(KeyCode.ADD, KeyCombination.CONTROL_DOWN), RUN_LAYOUT);
// add command binding for 'new' and 'print'
mode.getKeyboardInputMode().addCommandBinding(ICommand.NEW, this::executeNew, this::canExecuteNew);
mode.getKeyboardInputMode().addCommandBinding(ICommand.PRINT, this::executePrintCommand, this::canExecutePrintCommand);
// The following line triggers a call to the can-execute-method of each registered action/binding. This is normally
// done automatically by yFiles via input modes and on specific structural changes. But we want to have the above
// added actions to be initially in the correct can-execute-state, so we trigger this method manually.
ICommand.invalidateRequerySuggested();
return mode;
}
/**
* Initializes the graph instance setting default styles
*/
public void initializeGraph() {
//Enable folding
IFoldingView view = new FoldingManager().createFoldingView();
IGraph graph = view.getGraph();
// Enable undoability
// Get the master graph instance and enable undoability support.
IGraph masterGraph = view.getManager().getMasterGraph();
masterGraph.setUndoEngineEnabled(true);
DemoStyles.initDemoStyles(masterGraph, true);
graphControl.setGraph(graph);
}
// ======== Button Actions =========
/**
* Stops the demo.
*/
public void exit() {
Platform.exit();
}
/**
* Resets the zoom value of the graph control to 1.
*/
public void resetZoom(){
graphControl.setZoom(1);
}
/**
* Delegates to fitGraphBounds of GraphControl
*/
public void fitGraphBounds(){
graphControl.fitGraphBounds();
}
/**
* Toggles the snapping behavior when placing nodes.
*/
public void toggleSnapping(ActionEvent event) {
if (event.getSource() instanceof ToggleButton) {
ToggleButton snappingButton = (ToggleButton) event.getSource();
GraphEditorInputMode inputMode = (GraphEditorInputMode) graphControl.getInputMode();
inputMode.getSnapContext().setEnabled(snappingButton.isSelected());
inputMode.getLabelSnapContext().setEnabled(snappingButton.isSelected());
}
}
/**
* Toggles the orthogonal editing behavior. When this is enabled, edges are created and updated orthogonal.
*/
public void toggleOrthogonalEditing(ActionEvent event) {
if (event.getSource() instanceof ToggleButton) {
ToggleButton orthogonalEditingButton = (ToggleButton) event.getSource();
boolean selected = orthogonalEditingButton.isSelected();
GraphEditorInputMode inputMode = (GraphEditorInputMode) graphControl.getInputMode();
inputMode.getOrthogonalEdgeEditingContext().setEnabled(selected);
}
}
/**
* Toggles the visibility of the grid.
*/
public void toggleGrid(ActionEvent event) {
if (event.getSource() instanceof ToggleButton) {
ToggleButton gridToggleButton = (ToggleButton) event.getSource();
boolean selected = gridToggleButton.isSelected();
if (selected){
graphSnapContext.setGridSnapType(GridSnapTypes.ALL);
} else {
graphSnapContext.setGridSnapType(GridSnapTypes.NONE);
}
grid.setVisible(selected);
// update GraphControl to force a repaint with the new grid state
graphControl.invalidate();
}
}
/**
* Toggles the selection mode.
*/
public void toggleSelectionMode(ActionEvent event) {
if (event.getSource() instanceof ToggleButton && graphControl.getInputMode() instanceof GraphEditorInputMode) {
ToggleButton selectionModeToggleButton = (ToggleButton) event.getSource();
boolean selected = selectionModeToggleButton.isSelected();
GraphEditorInputMode geim = (GraphEditorInputMode) graphControl.getInputMode();
geim.getLassoSelectionInputMode().setEnabled(selected);
}
}
/**
* A {@link ICommand} that is usable from FXML and layouts the given graph.
*/
private static final ICommand RUN_LAYOUT = ICommand.createCommand("RunLayout");
/**
* Determines whether the {@link #RUN_LAYOUT} can be executed.
*/
private boolean canExecuteLayout(ICommand command, Object param, Object sender) {
// if a layout algorithm is currently running, no other layout algorithm shall be executable for two reasons:
// - the result of the current layout run shall be presented before executing a new layout
// - layout algorithms are not thread safe, so calling applyLayout on a layout algorithm that currently calculates
// a layout may result in errors
if (param instanceof ILayoutAlgorithm && !waitInputMode.isWaiting()) {
// don't allow layouts for empty graphs
IGraph graph = graphControl.getGraph();
return graph != null && graph.getNodes().size() != 0;
} else {
return false;
}
}
/**
* Handles the {@link #RUN_LAYOUT}.
*/
private boolean executeLayout(ICommand command, Object parameter, Object sender) {
if (parameter instanceof ILayoutAlgorithm) {
ILayoutAlgorithm layout = (ILayoutAlgorithm) parameter;
graphControl.morphLayout(layout, Duration.ofMillis(500));
return true;
}
return false;
}
/**
* Helper that determines whether the {@link ICommand#NEW} can be executed.
*/
private boolean canExecuteNew(ICommand command, Object param, Object sender) {
// don't allow layouts for empty graphs
IGraph graph = graphControl.getGraph();
return !waitInputMode.isWaiting() && graph != null && graph.getNodes().size() != 0;
}
/**
* Handler for the {@link ICommand#NEW}
*/
private boolean executeNew(ICommand command, Object param, Object sender) {
graphControl.getGraph().clear();
// update the can-execute-states of the commands since this is not
// triggered by clearing the graph programmatically
ICommand.invalidateRequerySuggested();
return true;
}
/**
* Exports the image to bitmap via GraphControl.
*/
public void exportImage() {
graphControl.updateContentRect(new InsetsD(5, 5, 5, 5));
FileChooser dialog = new FileChooser();
dialog.setTitle("Export image to file");
final ObservableList<FileChooser.ExtensionFilter> extensionFilters = dialog.getExtensionFilters();
extensionFilters.add(new FileChooser.ExtensionFilter("JPEG Files", "*.jpg", "*.jpeg", "*.jpe"));
extensionFilters.add(new FileChooser.ExtensionFilter("PNG Files", "*.png"));
extensionFilters.add(new FileChooser.ExtensionFilter("Bitmap Files", "*.bmp"));
File file = dialog.showSaveDialog(graphControl.getScene().getWindow());
if (file != null) {
// If file extension is missing, add ".png".
String filePath = file.getPath();
boolean hasExtension = false;
for (String extension : IMAGE_FILE_EXTENSIONS) {
if (filePath.toLowerCase().endsWith(extension)) {
hasExtension = true;
break;
}
}
if (!hasExtension) {
filePath = filePath + ".png";
}
try {
BitmapExportHelper.exportToBitmap(graphControl, filePath);
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* Handler for the {@link ICommand#PRINT} that initiates the printing process.
*/
private boolean executePrintCommand(ICommand command, Object param, Object sender) {
CanvasPrinter canvasPrinter = new CanvasPrinter(graphControl);
PrinterJob job = PrinterJob.createPrinterJob();
if (job == null) {
new Alert(Alert.AlertType.WARNING, "Could not print.", ButtonType.CLOSE).show();
return false;
}
boolean success = canvasPrinter.print(job, true);
if (success) {
job.endJob();
}
return success;
}
/**
* Helper that determines whether the {@link ICommand#PRINT} can be executed.
*/
private boolean canExecutePrintCommand(ICommand command, Object param, Object sender) {
// don't allow printing empty graphs
IGraph graph = graphControl.getGraph();
return !waitInputMode.isWaiting() && graph != null && graph.getNodes().size() != 0;
}
public static void main(String[] args) {
launch(args);
}
}