Allow client-logic in component type interface property name editing
[simantics/platform.git] / bundles / org.simantics.modeling.ui / src / org / simantics / modeling / ui / componentTypeEditor / ComponentTypeViewerData.java
1 package org.simantics.modeling.ui.componentTypeEditor;
2
3 import java.util.ArrayList;
4 import java.util.Collections;
5 import java.util.List;
6 import java.util.regex.Matcher;
7 import java.util.regex.Pattern;
8
9 import org.eclipse.jface.dialogs.IMessageProvider;
10 import org.eclipse.jface.layout.GridDataFactory;
11 import org.eclipse.jface.layout.GridLayoutFactory;
12 import org.eclipse.swt.SWT;
13 import org.eclipse.swt.custom.StyledText;
14 import org.eclipse.swt.custom.TableEditor;
15 import org.eclipse.swt.events.SelectionAdapter;
16 import org.eclipse.swt.events.SelectionEvent;
17 import org.eclipse.swt.graphics.Point;
18 import org.eclipse.swt.graphics.Rectangle;
19 import org.eclipse.swt.widgets.Button;
20 import org.eclipse.swt.widgets.Combo;
21 import org.eclipse.swt.widgets.Composite;
22 import org.eclipse.swt.widgets.Display;
23 import org.eclipse.swt.widgets.Event;
24 import org.eclipse.swt.widgets.Label;
25 import org.eclipse.swt.widgets.Shell;
26 import org.eclipse.swt.widgets.Table;
27 import org.eclipse.swt.widgets.TableItem;
28 import org.eclipse.swt.widgets.Text;
29 import org.eclipse.ui.forms.widgets.Form;
30 import org.eclipse.ui.forms.widgets.FormToolkit;
31 import org.simantics.Simantics;
32 import org.simantics.databoard.type.NumberType;
33 import org.simantics.databoard.units.internal.library.UnitLibrary;
34 import org.simantics.databoard.util.Limit;
35 import org.simantics.databoard.util.Range;
36 import org.simantics.databoard.util.RangeException;
37 import org.simantics.db.RequestProcessor;
38 import org.simantics.db.Resource;
39 import org.simantics.db.WriteGraph;
40 import org.simantics.db.common.NamedResource;
41 import org.simantics.db.common.request.WriteRequest;
42 import org.simantics.db.exception.DatabaseException;
43 import org.simantics.db.function.DbConsumer;
44 import org.simantics.layer0.Layer0;
45 import org.simantics.modeling.userComponent.ComponentTypeCommands;
46 import org.simantics.scl.runtime.function.Function2;
47 import org.simantics.scl.runtime.function.Function4;
48 import org.simantics.utils.ui.ErrorLogger;
49
50 public class ComponentTypeViewerData {
51     /**
52      * Used to validate property names.
53      */
54     public static final Pattern PROPERTY_NAME_PATTERN =
55             Pattern.compile("([a-z]|_[0-9a-zA-Z_])[0-9a-zA-Z_]*");
56
57     public static final String[] PROPERTY_TYPE_SUGGESTIONS = new String[] {
58         "Double",
59         "Integer",
60         "Float",
61         "String",
62         "Boolean",
63         "Long",
64         "[Double]",
65         "[Integer]",
66         "[Float]",
67         "[String]",
68         "[Boolean]",
69         "[Long]",
70         "Vector Double",
71         "Vector Integer",
72         "Vector Float",
73         "Vector String",
74         "Vector Boolean",
75         "Vector Long"        
76     };
77     
78     public Resource componentType;
79     public FormToolkit tk;
80     public Form form;
81     public UnitLibrary unitLibrary = UnitLibrary.createDefault();
82     public boolean readOnly;
83     public NamedResource[] connectionPoints;
84     public ComponentTypeViewerPropertyInfo[] properties;
85
86     public ComponentTypeViewerData(FormToolkit tk, Resource componentType, Form form) {
87         this.tk = tk;
88         this.componentType = componentType;
89         this.form = form;
90     }
91
92     public void editName(Table table, TableEditor editor, final ComponentTypeViewerPropertyInfo propertyInfo, TableItem selectedItem, int column,
93             Pattern namePattern) {
94         editName(table, editor, propertyInfo, selectedItem, column, namePattern, null);
95     }
96
97     public void editName(Table table, TableEditor editor, final ComponentTypeViewerPropertyInfo propertyInfo, TableItem selectedItem, int column,
98             Pattern namePattern, DbConsumer<WriteGraph> extraWriter) {
99         editName(table, editor, propertyInfo, selectedItem, column,
100                 (pInfo, name) -> validatePropertyName(pInfo, name, namePattern),
101                 extraWriter);
102     }
103
104     public void editName(Table table, TableEditor editor, ComponentTypeViewerPropertyInfo propertyInfo, TableItem selectedItem, int column,
105             Function2<ComponentTypeViewerPropertyInfo, String, String> nameValidator)
106     {
107         editName(table, editor, propertyInfo, selectedItem, column, nameValidator, null);
108     }
109
110     public void editName(Table table, TableEditor editor, final ComponentTypeViewerPropertyInfo propertyInfo, TableItem selectedItem, int column,
111             Function2<ComponentTypeViewerPropertyInfo, String, String> nameValidator, DbConsumer<WriteGraph> extraWriter) {
112         int extraStyle = propertyInfo.immutable ? SWT.READ_ONLY : 0;
113         final Text text = new Text(table, SWT.NONE | extraStyle);
114         org.eclipse.swt.widgets.Listener listener = 
115                 new org.eclipse.swt.widgets.Listener() {
116             @Override
117             public void handleEvent(Event e) {
118                 if (e.type == SWT.Dispose) {
119                     form.setMessage(null);
120                     return;
121                 }
122
123                 if (e.type == SWT.Modify) {
124                     // validate current name
125                     String error = nameValidator.apply(propertyInfo, text.getText());
126                     if (error != null) {
127                         text.setBackground(text.getDisplay().getSystemColor(SWT.COLOR_RED));
128                         form.setMessage(error, IMessageProvider.ERROR);
129                     } else {
130                         text.setBackground(null);
131                         form.setMessage(null);
132                     }
133                     return;
134                 }
135
136                 if (e.type == SWT.Traverse) {
137                     if (e.detail == SWT.TRAVERSE_ESCAPE) {
138                         text.dispose();
139                         e.doit = false;
140                         return;
141                     }
142                     if (e.detail == SWT.TRAVERSE_ARROW_NEXT || e.detail == SWT.TRAVERSE_ARROW_PREVIOUS || e.detail == SWT.TRAVERSE_MNEMONIC)
143                         return;
144                     e.doit = false;
145                 }
146                 final String newValue = text.getText();
147                 text.dispose();
148
149                 String error = nameValidator.apply(propertyInfo, newValue);
150                 if (error != null)
151                     return;
152
153                 if (propertyInfo.immutable)
154                     return;
155
156                 Simantics.getSession().async(new WriteRequest() {
157                     @Override
158                     public void perform(WriteGraph graph)
159                             throws DatabaseException {
160                         graph.markUndoPoint();
161                         Layer0 L0 = Layer0.getInstance(graph);
162                         String prevName = graph.getPossibleRelatedValue2(propertyInfo.resource, L0.HasName);
163                         String oldCamelCasedLabel = prevName != null ? ComponentTypeCommands.camelCaseNameToLabel(prevName) : "";
164                         String oldLabel = graph.getPossibleRelatedValue(propertyInfo.resource, L0.HasLabel);
165                         boolean setLabel = oldLabel == null
166                                 || oldLabel.isEmpty()
167                                 || oldCamelCasedLabel.isEmpty()
168                                 || oldCamelCasedLabel.equals(oldLabel);
169
170                         ComponentTypeCommands.rename(graph, propertyInfo.resource, newValue);
171                         if (setLabel)
172                             ComponentTypeCommands.setLabel(graph, propertyInfo.resource, ComponentTypeCommands.camelCaseNameToLabel(newValue));
173
174                         if (extraWriter != null)
175                             extraWriter.accept(graph);
176                     }
177                 });
178             }
179         };
180         text.addListener(SWT.Modify, listener);
181         text.addListener(SWT.Deactivate, listener);
182         text.addListener(SWT.Traverse, listener);
183         text.addListener(SWT.Dispose, listener);
184
185         text.setText(selectedItem.getText(column));
186         text.selectAll();
187         text.setFocus();
188
189         editor.setEditor(text, selectedItem, column);
190     }
191
192     public void editType(Table table, TableEditor editor, final ComponentTypeViewerPropertyInfo propertyInfo, TableItem selectedItem, int column, final boolean convertDefaultValue) {
193         int extraStyle = propertyInfo.immutable ? SWT.READ_ONLY : 0;
194         final Combo combo = new Combo(table, SWT.NONE | extraStyle);
195         combo.setText(selectedItem.getText(column));
196         for(String suggestion : PROPERTY_TYPE_SUGGESTIONS)
197             combo.add(suggestion);
198         org.eclipse.swt.widgets.Listener listener = 
199                 new org.eclipse.swt.widgets.Listener() {
200             @Override
201             public void handleEvent(Event e) {
202                 if(e.type == SWT.Traverse) {
203                     if (e.detail == SWT.TRAVERSE_ESCAPE) {
204                         combo.dispose();
205                         e.doit = false;
206                         return;
207                     }
208                     if (e.detail == SWT.TRAVERSE_ARROW_NEXT
209                             || e.detail == SWT.TRAVERSE_ARROW_PREVIOUS
210                             || e.detail == SWT.TRAVERSE_MNEMONIC)
211                         return;
212                 }
213                 final String newValue = combo.getText();
214                 if (e.type == SWT.Traverse) {
215                     e.doit = false;
216                 }
217                 combo.dispose();
218
219                 if (propertyInfo.immutable)
220                     return;
221
222                 Simantics.getSession().async(new WriteRequest() {
223                     @Override
224                     public void perform(WriteGraph graph)
225                             throws DatabaseException {
226                         graph.markUndoPoint();
227                         ComponentTypeCommands.editType(graph, componentType, propertyInfo.resource, convertDefaultValue, newValue);
228                     }
229                 });
230             }
231         };
232         combo.setFocus();
233         editor.setEditor(combo, selectedItem, column);
234         combo.addListener(SWT.FocusOut, listener);
235         combo.addListener(SWT.Traverse, listener);
236     }
237
238     protected void editUnit(Table table, TableEditor editor, final ComponentTypeViewerPropertyInfo propertyInfo, TableItem selectedItem, int column) {
239         // Disallow unit editing for non-numeric configuration properties
240         if (propertyInfo.numberType == null && propertyInfo.sectionSpecificData == null)
241             return;
242
243         int extraStyle = propertyInfo.immutable ? SWT.READ_ONLY : 0;
244         final Combo combo = new Combo(table, SWT.NONE | extraStyle);
245         String initialValue = selectedItem.getText(column);
246         List<String> units = new ArrayList<>( unitLibrary.getUnits() );
247         Collections.sort(units, String.CASE_INSENSITIVE_ORDER);
248         int i = -1;
249         int selected = -1;
250         for (String unit : units) {
251             combo.add(unit);
252             if (unit.equals(initialValue))
253                 combo.select(i);
254         }
255         if (selected == -1)
256             combo.setText(initialValue);
257
258         org.eclipse.swt.widgets.Listener listener = 
259                 new org.eclipse.swt.widgets.Listener() {
260             @Override
261             public void handleEvent(Event e) {
262                 if(e.type == SWT.Traverse) {
263                     if (e.detail == SWT.TRAVERSE_ESCAPE) {
264                         combo.dispose();
265                         e.doit = false;
266                         return;
267                     }
268                     if (e.detail == SWT.TRAVERSE_ARROW_NEXT
269                             || e.detail == SWT.TRAVERSE_ARROW_PREVIOUS
270                             || e.detail == SWT.TRAVERSE_MNEMONIC)
271                         return;
272                 }
273                 final String newValue = combo.getText();
274                 if(e.type == SWT.Traverse) {
275                     e.doit = false;
276                 }
277                 combo.dispose();
278
279                 if (propertyInfo.immutable)
280                     return;
281
282                 Simantics.getSession().async(new WriteRequest() {
283                     @Override
284                     public void perform(WriteGraph graph)
285                             throws DatabaseException {
286                         graph.markUndoPoint();
287                         ComponentTypeCommands.setUnit(graph, componentType, propertyInfo.resource, newValue);
288                     }
289                 });
290             }
291         };
292         combo.setFocus();
293         editor.setEditor(combo, selectedItem, column);
294         combo.addListener(SWT.Deactivate, listener);
295         combo.addListener(SWT.Traverse, listener);
296     }
297
298     public void editValue(Table table, TableEditor editor,
299             final ComponentTypeViewerPropertyInfo propertyInfo,
300             TableItem selectedItem, int column,
301             final StringWriter writer,
302             final Function4<RequestProcessor, Resource, Resource, String, String> validator)
303     {
304         int extraStyle = writer == null ? SWT.READ_ONLY : 0;
305         final Text text = new Text(table, SWT.NONE | extraStyle);
306         text.setText(selectedItem.getText(column));
307         org.eclipse.swt.widgets.Listener listener = 
308                 new org.eclipse.swt.widgets.Listener() {
309             @Override
310             public void handleEvent(Event e) {
311                 if(e.type == SWT.Traverse) {
312                     if (e.detail == SWT.TRAVERSE_ESCAPE) {
313                         text.dispose();
314                         e.doit = false;
315                         return;
316                     }
317                     if (e.detail == SWT.TRAVERSE_ARROW_NEXT
318                             || e.detail == SWT.TRAVERSE_ARROW_PREVIOUS
319                             || e.detail == SWT.TRAVERSE_MNEMONIC)
320                         return;
321                 }
322                 final String newValue = text.getText();
323                 if(e.type == SWT.Traverse) {
324                     e.doit = false;
325                 }
326                 text.dispose();
327
328                 if (validator != null) {
329                     String error = validator.apply(Simantics.getSession(), componentType, propertyInfo.resource, newValue);
330                     if (error != null)
331                         return;
332                 }
333
334                 if (writer != null) {
335                     Simantics.getSession().async(new WriteRequest() {
336                         @Override
337                         public void perform(WriteGraph graph) throws DatabaseException {
338                             writer.perform(graph, newValue);
339                         }
340                     });
341                 }
342             }
343         };
344         text.selectAll();
345         text.setFocus();
346         editor.setEditor(text, selectedItem, column);
347         text.addListener(SWT.FocusOut, listener);
348         text.addListener(SWT.Traverse, listener);
349
350         if (validator != null) {
351             org.eclipse.swt.widgets.Listener validationListener = new org.eclipse.swt.widgets.Listener() {
352                 @Override
353                 public void handleEvent(Event e) {
354                     final String newValue = text.getText();
355                     String error = validator.apply(Simantics.getSession(), componentType, propertyInfo.resource, newValue);
356                     if (error != null) {
357                         text.setBackground(text.getDisplay().getSystemColor(SWT.COLOR_RED));
358                         text.setToolTipText(error);
359                         return;
360                     } else {
361                         text.setBackground(null);
362                         text.setToolTipText(null);
363                     }
364                 }
365             };
366             text.addListener(SWT.Modify, validationListener);
367         }
368     }
369
370     private Range parseRange(NumberType numberType, String minStr, String maxStr, boolean lowInclusive, boolean highInclusive) throws RangeException {
371         try {
372             String rangeStr = (lowInclusive ? "[" : "(")  + minStr + ".." + maxStr + (highInclusive ? "]" : ")");
373             return Range.valueOf(rangeStr);
374         } catch (IllegalArgumentException e) {
375             // Limits are invalid
376             throw new RangeException(e.getMessage(), e);
377         }
378     }
379     
380     private static Combo createRangeInclusionCombo(Composite parent, boolean inclusive) {
381         Combo rng = new Combo(parent, SWT.READ_ONLY);
382         rng.add("Inclusive");
383         rng.add("Exclusive");
384         rng.select(inclusive ? 0 : 1);
385         return rng;
386     }
387     
388     protected void editRange(Table table, TableEditor editor, final ComponentTypeViewerPropertyInfo propertyInfo, TableItem selectedItem, Rectangle selectedItemBounds, int column) {
389         // Disallow range editing when the property is not numeric
390         if (propertyInfo.numberType == null)
391             return;
392
393         int extraTextStyle = propertyInfo.immutable ? SWT.READ_ONLY : 0;
394
395         // Parse initial range value
396         Range range = null;
397         String rangeStr = selectedItem.getText(column);
398         try {
399             range = Range.valueOf(rangeStr);
400         } catch (RangeException ex) {
401             range = new Range(Limit.nolimit(), Limit.nolimit());
402         }
403
404         final Shell shell = new Shell(table.getShell(), SWT.ON_TOP);
405         GridLayoutFactory.fillDefaults().applyTo(shell);
406
407         Composite composite = new Composite(shell, SWT.NONE);
408         GridDataFactory.fillDefaults().grab(true, true).applyTo(composite);
409         GridLayoutFactory.swtDefaults().numColumns(3).applyTo(composite);
410
411         Label low = new Label(composite, SWT.NONE);
412         low.setText("Minimum Value:");
413         final Text lowText = new Text(composite, SWT.BORDER | extraTextStyle);
414         GridDataFactory.fillDefaults().grab(true, false).hint(100, SWT.DEFAULT).applyTo(lowText);
415         final Combo lowSelector = createRangeInclusionCombo(composite, !range.getLower().isExclusive());
416         Label high = new Label(composite, SWT.NONE);
417         high.setText("Maximum Value:");
418         final Text highText = new Text(composite, SWT.BORDER | extraTextStyle);
419         GridDataFactory.fillDefaults().grab(true, false).hint(100, SWT.DEFAULT).applyTo(highText);
420         final Combo highSelector = createRangeInclusionCombo(composite, !range.getUpper().isExclusive());
421
422         Composite buttonComposite = new Composite(shell, SWT.NONE);
423         GridDataFactory.fillDefaults().grab(true, false).align(SWT.TRAIL, SWT.FILL).applyTo(buttonComposite);
424         GridLayoutFactory.swtDefaults().numColumns(2).equalWidth(true).applyTo(buttonComposite);
425
426         Button ok = new Button(buttonComposite, SWT.NONE);
427         ok.setText("&OK");
428         GridDataFactory.swtDefaults().align(SWT.FILL, SWT.CENTER).applyTo(ok);
429         Button cancel = new Button(buttonComposite, SWT.NONE);
430         cancel.setText("&Cancel");
431         GridDataFactory.swtDefaults().align(SWT.FILL, SWT.CENTER).applyTo(ok);
432
433         if (range.getLower().getValue() != null)
434             lowText.setText(range.getLower().getValue().toString());
435         if (range.getUpper().getValue() != null)
436             highText.setText(range.getUpper().getValue().toString());
437
438         shell.addListener(SWT.Deactivate, new org.eclipse.swt.widgets.Listener() {
439             @Override
440             public void handleEvent(Event event) {
441                 shell.dispose();
442             }
443         });
444
445         ok.addSelectionListener(new SelectionAdapter() {
446             public void widgetSelected(SelectionEvent e) {
447                 try {
448                     final Range newRange = parseRange(propertyInfo.numberType,
449                             lowText.getText().trim(),
450                             highText.getText().trim(),
451                             lowSelector.getSelectionIndex() == 0 ? true : false,
452                             highSelector.getSelectionIndex() == 0 ? true : false);
453
454                     shell.dispose();
455
456                     if (propertyInfo.immutable)
457                         return;
458
459                     Simantics.getSession().async(new WriteRequest() {
460                         @Override
461                         public void perform(WriteGraph graph)
462                                 throws DatabaseException {
463                             graph.markUndoPoint();
464                             ComponentTypeCommands.setRange(graph, componentType, propertyInfo.resource, newRange == null ? null : newRange.toString());
465                         }
466                     });
467                 } catch (RangeException ex) {
468                     ErrorLogger.defaultLogError(ex);
469                 }
470             }
471         });
472         cancel.addSelectionListener(new SelectionAdapter() {
473             public void widgetSelected(SelectionEvent e) {
474                 shell.dispose();
475             }
476         });
477
478         shell.pack();
479         Point size = shell.getSize();
480
481         Display display = table.getDisplay();
482         Rectangle clientArea = display.getClientArea();
483         Point bt = table.toDisplay(selectedItemBounds.x, selectedItemBounds.y);
484         Rectangle b = selectedItemBounds;
485         b.x = bt.x;
486         b.y = bt.y;
487         b.width = size.x;
488         b.height = size.y;
489         if ((b.x + b.width) > clientArea.width)
490             b.x -= b.x + b.width - clientArea.width;
491         if (b.height > clientArea.height)
492             b.height = clientArea.height;
493         if ((b.y + b.height) > clientArea.height)
494             b.y -= b.y + b.height - clientArea.height;
495
496         shell.setBounds(selectedItemBounds);
497         shell.open();
498     }
499
500     protected void editMultilineText(Table table, TableEditor editor,
501             final ComponentTypeViewerPropertyInfo propertyInfo, TableItem selectedItem,
502             Rectangle selectedItemBounds, int column, final StringWriter writer)
503     {
504         final Shell shell = new Shell(table.getShell(), SWT.ON_TOP);
505         GridLayoutFactory.fillDefaults().spacing(0, 0).applyTo(shell);
506         final StyledText text = new StyledText(shell, SWT.MULTI | SWT.WRAP | SWT.V_SCROLL | (propertyInfo.immutable ? SWT.READ_ONLY : 0));
507         GridDataFactory.fillDefaults().grab(true, true).applyTo(text);
508         text.setText(selectedItem.getText(column));
509         org.eclipse.swt.widgets.Listener listener = 
510                 new org.eclipse.swt.widgets.Listener() {
511             @Override
512             public void handleEvent(Event e) {
513                 final String newValue = text.getText();
514
515                 if (e.type == SWT.Traverse) {
516                     if (e.detail == SWT.TRAVERSE_ESCAPE) {
517                         shell.dispose();
518                         e.doit = false;
519                         return;
520                     }
521                     if (e.detail == SWT.TRAVERSE_ARROW_NEXT
522                             || e.detail == SWT.TRAVERSE_ARROW_PREVIOUS
523                             || e.detail == SWT.TRAVERSE_MNEMONIC)
524                         return;
525                     if ((e.stateMask & SWT.CTRL) == 0)
526                         return;
527                     e.doit = false;
528                 }
529
530                 shell.dispose();
531
532                 if (propertyInfo.immutable)
533                     return;
534
535                 if (writer != null) {
536                     Simantics.getSession().async(new WriteRequest() {
537                         @Override
538                         public void perform(WriteGraph graph) throws DatabaseException {
539                             writer.perform(graph, newValue);
540                         }
541                     });
542                 }
543             }
544         };
545
546         String helpText = propertyInfo.immutable ? "ESC to close." : "Ctrl+Enter to apply changes, ESC to cancel.";
547         Label help = tk.createLabel(shell, helpText, SWT.BORDER | SWT.FLAT);
548         GridDataFactory.fillDefaults().grab(true, false).align(SWT.FILL, SWT.CENTER).applyTo(help);
549         help.setForeground( tk.getColors().createColor( "fg", tk.getColors().getSystemColor(SWT.COLOR_LIST_SELECTION) ) );
550
551         Display display = table.getDisplay();
552         Rectangle clientArea = display.getClientArea();
553         Point bt = table.toDisplay(selectedItemBounds.x, selectedItemBounds.y);
554         Rectangle b = selectedItemBounds;
555         b.x = bt.x;
556         b.y = bt.y;
557         b.height = 200;
558         if ((b.x + b.width) > clientArea.width)
559             b.x -= b.x + b.width - clientArea.width;
560         if (b.height > clientArea.height)
561             b.height = clientArea.height;
562         if ((b.y + b.height) > clientArea.height)
563             b.y -= b.y + b.height - clientArea.height;
564
565         shell.setBounds(selectedItemBounds);
566         shell.open();
567
568         text.selectAll();
569         text.setFocus();
570
571         text.addListener(SWT.Traverse, listener);
572         shell.addListener(SWT.Deactivate, listener);
573     }
574
575     private String validatePropertyName(ComponentTypeViewerPropertyInfo propertyInfo, String propertyName, Pattern namePattern) {
576         if (propertyName.equals(propertyInfo.name))
577             return null;
578         for (ComponentTypeViewerPropertyInfo info : properties) {
579             if (propertyName.equals(info.name))
580                 return "Property name '" + propertyName + "' is already in use.";
581         }
582         for (NamedResource cp : connectionPoints) {
583             if (propertyName.equals(cp.getName()))
584                 return "Name '" + propertyName + "' is already used for a terminal.";
585         }
586         Matcher m = namePattern.matcher(propertyName);
587         if (!m.matches())
588             return "Property name '" + propertyName + "' contains invalid characters, does not match pattern "
589                     + namePattern.pattern() + ".";
590         return null;
591     }
592
593 }