View Javadoc
1   /*
2    * Copyright 2013-2023 Medical Information Systems Research Group (https://medical.zcu.cz),
3    * Department of Computer Science and Engineering, University of West Bohemia.
4    * Address: Univerzitni 8, 306 14 Plzen, Czech Republic.
5    *
6    * This file is part of Sparkle project.
7    *
8    * Sparkle is free software: you can redistribute it and/or modify
9    * it under the terms of the GNU General Public License as published by
10   * the Free Software Foundation, either version 3 of the License.
11   *
12   * Sparkle is distributed in the hope that it will be useful,
13   * but WITHOUT ANY WARRANTY; without even the implied warranty of
14   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15   * GNU General Public License for more details.
16   *
17   * You should have received a copy of the GNU General Public License
18   * along with Sparkle. If not, see <http://www.gnu.org/licenses/>.
19   */
20  package cz.zcu.mre.sparkle.gui.query.other;
21  
22  import cz.zcu.mre.sparkle.data.PrefixesStorage;
23  import cz.zcu.mre.sparkle.gui.query.helpers.FieldType;
24  import cz.zcu.mre.sparkle.gui.query.helpers.RegionPopup;
25  import cz.zcu.mre.sparkle.gui.query.helpers.RegionPopup.PopupAlignment;
26  import cz.zcu.mre.sparkle.gui.query.helpers.RegionPopup.PopupPlacement;
27  import cz.zcu.mre.sparkle.gui.tools.Components;
28  import cz.zcu.mre.sparkle.gui.tools.ReferenceKeeper;
29  import cz.zcu.mre.sparkle.tools.Utils;
30  import cz.zcu.mre.sparkle.tools.sparqlValidation.SparqlValidationUtils;
31  import javafx.beans.Observable;
32  import javafx.beans.property.*;
33  import javafx.collections.ObservableList;
34  import javafx.event.Event;
35  import javafx.fxml.FXML;
36  import javafx.scene.Node;
37  import javafx.scene.control.TextField;
38  import javafx.scene.control.Toggle;
39  import javafx.scene.control.ToggleButton;
40  import javafx.scene.control.ToggleGroup;
41  import javafx.scene.input.KeyEvent;
42  import javafx.scene.layout.HBox;
43  import org.apache.jena.sparql.util.ExprUtils;
44  import java.util.*;
45  
46  /**
47   * Wrapper pro textová pole dodávající možnost výběru jejich typu. Navíc
48   * umožňuje kontrolovat, zda jejich obsah vybranému typu odpovídá.
49   *
50   * @author Jan Smucr
51   * @author Klara Hlavacova
52   * @author Petr Vcelak (vcelak@kiv.zcu.cz)
53   */
54  final class FieldTypeWrapper
55          extends HBox {
56  
57      @FXML
58      private ToggleButton variableToggleButton, prefixedNameToggleButton, iriToggleButton, aToggleButton,
59              literalToggleButton, expressionToggleButton, modifierToggleButton, blankNodeToggleButton, nilToggleButton,
60              undefToggleButton, booleanToggleButton, numericToggleButton;
61      @FXML
62      private ToggleGroup fieldTypeToggleGroup;
63  
64      private TextField textField;
65      private final Set<FieldType> allowedOptions = new HashSet<>();
66      private final RegionPopup popup;
67      private final ObjectProperty<FieldType> fieldTypeProperty = new SimpleObjectProperty<>();
68      private final BooleanProperty fieldContentIsValidProperty = new SimpleBooleanProperty(true);
69      private final ReadOnlyBooleanProperty fieldContentIsValidReadOnlyProperty = ReadOnlyBooleanProperty.readOnlyBooleanProperty(fieldContentIsValidProperty);
70      private final PrefixesStorage prefixesStorage;
71      private final ReferenceKeeper keeper = new ReferenceKeeper();
72      private boolean checkValue;
73  
74      //==============================
75      public FieldTypeWrapper(final TextField textField, final PrefixesStorage prefixesStorage,
76              final FieldType initialType, boolean checkValue, final FieldType... otherOptions) {
77          Components.load(this);
78  
79          this.textField = textField;
80          this.prefixesStorage = prefixesStorage;
81          this.checkValue = checkValue;
82  
83          // Přiřazení typu každému z tlačítek
84          for (final FieldType fieldType : FieldType.values()) {
85              getToggle(fieldType).setUserData(fieldType);
86          }
87  
88          // Programové stisknutí správného tlačítka při programové změně typu
89          fieldTypeProperty.addListener(keeper.toWeak((observable, oldValue, newValue) -> {
90              getToggle(newValue).setSelected(true);
91              updateFieldContentIsValid();
92          }));
93  
94          // Seznam povolených typů
95          allowedOptions.add(initialType);
96          Collections.addAll(allowedOptions, otherOptions);
97  
98          // Odstranění tlačítek s nepovolenými typy
99          final List<FieldType> allOptionsList = new LinkedList<>(Arrays.asList(FieldType.values()));
100         allOptionsList.removeAll(allowedOptions);
101         allOptionsList.stream().map(this::getToggle).filter((toggle) -> (toggle instanceof Node))
102                 .peek((toggle) -> ((Node) toggle).setVisible(false)).forEach((toggle) -> ((Node) toggle).setManaged(false));
103 
104         // Nastavení plovoucího okna
105         popup = new RegionPopup(textField, PopupPlacement.TOP, PopupAlignment.LEFT_OR_TOP);
106         popup.getContent().add(this);
107         popup.setAutoFix(false);
108         popup.setConsumeAutoHidingEvents(false);
109         popup.setAutoHide(true);
110         popup.setHideOnEscape(false);
111 
112         // Zobrazení plovoucího okna po zaměření pole
113         textField.focusedProperty().addListener(keeper.toWeak((observable, oldValue, newValue) -> {
114             if (newValue && !popup.isVisible()) {
115                 popup.showPositioned();
116             } else if (popup.isVisible()) {
117                 popup.hide();
118             }
119         }));
120 
121         // Zobrazení plovoucího okna po kliknutí do pole
122         textField.setOnMouseClicked(keeper.toWeak((final Event event) -> {
123             if (textField.isFocused() && !popup.isVisible()) {
124                 popup.showPositioned();
125             }
126         }));
127 
128         // Reakce na změnu typu tlačítky
129         fieldTypeToggleGroup.selectedToggleProperty().addListener(keeper.toWeak((observable, oldValue, newValue) -> {
130             if (newValue == null) {
131                 if (oldValue != null) {
132                     setFieldType((FieldType) oldValue.getUserData());
133                 }
134             } else if (!fieldTypeProperty.isBound()) {
135                 fieldTypeProperty.set((FieldType) newValue.getUserData());
136             }
137         }));
138 
139         // Cyklická změna typu šipkami + alt
140         textField.addEventFilter(KeyEvent.KEY_PRESSED, keeper.toWeak((final KeyEvent event) -> {
141             if (event.isAltDown()) {
142                 switch (event.getCode()) {
143                     case LEFT:
144                         selectPrevious();
145                         event.consume();
146                         break;
147                     case RIGHT:
148                         selectNext();
149                         event.consume();
150                         break;
151                     default:
152                 }
153             }
154         }));
155 
156         // Kontrola obsahu po každé změně
157         textField.textProperty()
158                 .addListener(keeper.toWeak((final Observable observable) -> updateFieldContentIsValid()));
159 
160         // Podbarvení pole s obsahem nevyhovujícím typu
161         fieldContentIsValidProperty.addListener(keeper.toWeak((observable, oldValue, newValue) -> {
162             if (newValue) {
163                 textField.getStyleClass().remove("invalid-field"); //$NON-NLS-1$
164             } else {
165                 textField.getStyleClass().add("invalid-field"); //$NON-NLS-1$
166             }
167         }));
168 
169         setFieldType(initialType);
170     }
171 
172     /**
173      * Zkontroluje správnost obsahu přiřazeného pole a nastaví flag
174      * {@link #fieldContentIsValidProperty()}.
175      */
176     private void updateFieldContentIsValid() {
177         // TODO: Improve
178         switch (fieldTypeProperty.get()) {
179             case A:
180                 fieldContentIsValidProperty.set(true);
181                 break;
182             case Expression:
183                 try {
184                     ExprUtils.parse(textField.getText(), prefixesStorage);
185                     fieldContentIsValidProperty.set(true);
186                 } catch (final Exception e) {
187                     fieldContentIsValidProperty.set(false);
188                 }
189                 break;
190             case IRI:
191                 fieldContentIsValidProperty.set(SparqlValidationUtils.isIRIValid(textField.getText(), false));
192                 break;
193             case Literal:
194                 fieldContentIsValidProperty.set(true);
195                 break;
196             case PrefixedName:
197                 fieldContentIsValidProperty.set(SparqlValidationUtils.isPrefixedNameValid(textField.getText(), this.checkValue));
198                 String prefix = Utils.extractPrefix(textField.getText());
199                 fieldContentIsValidProperty.set(prefixesStorage.getPrefixToIri().containsKey(prefix));
200                 break;
201             case Variable:
202                 fieldContentIsValidProperty.set(SparqlValidationUtils.isVariableNameValid(textField.getText()));
203                 break;
204             case BlankNode:
205                 fieldContentIsValidProperty.set(Utils.isFieldNotEmpty(textField.getText()));
206                 break;
207             case Nil:
208                 fieldContentIsValidProperty.set(SparqlValidationUtils.isNilValid(textField.getText()));
209                 break;
210             case Undef:
211                 fieldContentIsValidProperty.set(true);
212                 break;
213             case Boolean:
214                 fieldContentIsValidProperty.set(SparqlValidationUtils.isBooleanValid(textField.getText()));
215                 break;
216             case Numeric:
217                 fieldContentIsValidProperty.set(SparqlValidationUtils.isNumericValid(textField.getText()));
218                 break;
219             default:
220                 fieldContentIsValidProperty.set(true);
221                 break;
222         }
223     }
224 
225     public void setCheckValue(boolean state) {
226         this.checkValue = state;
227     }
228 
229     /**
230      * K typu pole vrátí tlačítko, které jej nastavuje.
231      *
232      * @param fieldType Typ.
233      *
234      * @return Tlačítko.
235      */
236     private Toggle getToggle(final FieldType fieldType) {
237         switch (fieldType) {
238             case A:
239                 return aToggleButton;
240             case IRI:
241                 return iriToggleButton;
242             case Literal:
243                 return literalToggleButton;
244             case PrefixedName:
245                 return prefixedNameToggleButton;
246             case Variable:
247                 return variableToggleButton;
248             case Modifier:
249                 return modifierToggleButton;
250             case BlankNode:
251                 return blankNodeToggleButton;
252             case Nil:
253                 return nilToggleButton;
254             case Undef:
255                 return undefToggleButton;
256             case Boolean:
257                 return booleanToggleButton;
258             case Numeric:
259                 return numericToggleButton;
260             case Expression:
261                 return expressionToggleButton;
262             default:
263                 return null;
264         }
265     }
266 
267     public final FieldType getFieldType() {
268         return fieldTypeProperty.get();
269     }
270 
271     public final void setFieldType(final FieldType fieldType) {
272         if (allowedOptions.contains(fieldType)) {
273             fieldTypeToggleGroup.selectToggle(getToggle(fieldType));
274         } else {
275             throw new IllegalArgumentException(
276                     "Field type " + fieldType + " is not allowed here."); //$NON-NLS-1$ //$NON-NLS-2$
277         }
278     }
279 
280     public final ObjectProperty<FieldType> fieldTypeProperty() {
281         return fieldTypeProperty;
282     }
283 
284     public final TextField getTextField() {
285         return textField;
286     }
287 
288     public final void setTextField(final TextField textField) {
289         if (this.textField != textField) {
290             if (popup.isVisible()) {
291                 popup.hide();
292             }
293 
294             this.textField = textField;
295 
296             popup.showPositioned();
297         }
298     }
299 
300     private ObservableList<Node> getVisibleToggles() {
301         return getChildren().filtered((t) -> {
302             final Object o = t.getUserData();
303             return (t.isVisible()) && (t instanceof Toggle) && (o instanceof FieldType);
304         });
305     }
306 
307     /**
308      * Programově klikne na předchozí nebo následující tlačítko. Umožňuje tak
309      * cyklickou změnu typu.
310      *
311      * @param next <code>true</code> pro následující, <code>false</code> pro
312      * předchozí.
313      */
314     private void selectPreviousOrNext(final boolean next) {
315         final ObservableList<Node> visibleToggles = getVisibleToggles();
316         if (visibleToggles.isEmpty() || (visibleToggles.size() == 1)) {
317             return;
318         }
319         final Toggle currentToggle = getToggle(fieldTypeProperty.get());
320         int currentToggleIndex = visibleToggles.indexOf(currentToggle);
321 
322         if (currentToggleIndex == -1) {
323             setFieldType((FieldType) visibleToggles.get(0).getUserData());
324             return;
325         }
326 
327         if (next) {
328             currentToggleIndex++;
329             currentToggleIndex %= visibleToggles.size();
330         } else {
331             currentToggleIndex = (currentToggleIndex == 0) ? visibleToggles.size() - 1 : currentToggleIndex - 1;
332         }
333 
334         setFieldType((FieldType) visibleToggles.get(currentToggleIndex).getUserData());
335     }
336 
337     /**
338      * Vybere následující typ v cyklu.
339      */
340     private void selectNext() {
341         selectPreviousOrNext(true);
342     }
343 
344     /**
345      * Vybere předchozí typ v cyklu.
346      */
347     private void selectPrevious() {
348         selectPreviousOrNext(false);
349     }
350 
351     /**
352      * @return Flag určující, zda obsah pole odpovídá nastavenému typu.
353      */
354     public final ReadOnlyBooleanProperty fieldContentIsValidProperty() {
355         return fieldContentIsValidReadOnlyProperty;
356     }
357 
358     /**
359      * @return Povolené typy pole.
360      */
361     public final FieldType[] getAllowedFieldTypes() {
362         return allowedOptions.toArray(new FieldType[0]);
363     }
364 }