diff --git a/build.gradle b/build.gradle index ffdcdd9..2df93a1 100644 --- a/build.gradle +++ b/build.gradle @@ -24,4 +24,5 @@ dependencies { compile group: 'com.typesafe', name: 'config', version: '1.3.0' compile group: 'commons-validator', name: 'commons-validator', version: '1.5.0' compile group: 'de.jensd', name: 'fontawesomefx', version: '8.8' + compile group: 'org.controlsfx', name: 'controlsfx', version: '8.40.10' } diff --git a/src/main/java/ninja/javafx/smartcsv/fx/SmartCSVController.java b/src/main/java/ninja/javafx/smartcsv/fx/SmartCSVController.java index f2da08b..a11d6ce 100644 --- a/src/main/java/ninja/javafx/smartcsv/fx/SmartCSVController.java +++ b/src/main/java/ninja/javafx/smartcsv/fx/SmartCSVController.java @@ -43,7 +43,7 @@ import ninja.javafx.smartcsv.FileWriter; import ninja.javafx.smartcsv.csv.CSVFileReader; import ninja.javafx.smartcsv.csv.CSVFileWriter; import ninja.javafx.smartcsv.fx.about.AboutController; -import ninja.javafx.smartcsv.fx.list.ValidationErrorListCell; +import ninja.javafx.smartcsv.fx.list.ErrorSideBar; import ninja.javafx.smartcsv.fx.preferences.PreferencesController; import ninja.javafx.smartcsv.fx.table.ObservableMapValueFactory; import ninja.javafx.smartcsv.fx.table.ValidationCellFactory; @@ -132,9 +132,6 @@ public class SmartCSVController extends FXMLController { @FXML private Label stateName; - @FXML - private ListView errorList; - @FXML private AnchorPane tableWrapper; @@ -162,6 +159,8 @@ public class SmartCSVController extends FXMLController { @FXML private Button addRowButton; + private ErrorSideBar errorSideBar; + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // members //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -185,8 +184,13 @@ public class SmartCSVController extends FXMLController { this.resourceBundle = resourceBundle; setupTableCellFactory(); - setupErrorListCellFactory(); - setupErrorListSelectionListener(); + + errorSideBar = new ErrorSideBar(resourceBundle); + + errorSideBar.selectedValidationErrorProperty().addListener((observable, oldValue, newValue) -> { + scrollToError(newValue); + }); + applicationPane.setRight(errorSideBar); bindMenuItemsToCsvFileExtistence(saveMenuItem, saveAsMenuItem, addRowMenuItem); bindButtonsToCsvFileExistence(saveButton, saveAsButton, addRowButton); @@ -196,14 +200,6 @@ public class SmartCSVController extends FXMLController { loadCsvPreferencesFromFile(); } - private void setupErrorListSelectionListener() { - errorList.getSelectionModel().selectedItemProperty().addListener(observable -> scrollToError()); - } - - private void setupErrorListCellFactory() { - errorList.setCellFactory(param -> new ValidationErrorListCell(resourceBundle)); - } - private void setupTableCellFactory() { cellFactory = new ValidationCellFactory(resourceBundle); } @@ -510,8 +506,7 @@ public class SmartCSVController extends FXMLController { setLeftAnchor(tableView, 0.0); setRightAnchor(tableView, 0.0); tableWrapper.getChildren().setAll(tableView); - - errorList.setItems(model.getValidationError()); + errorSideBar.setModel(model); } } @@ -540,8 +535,7 @@ public class SmartCSVController extends FXMLController { tableView.getColumns().add(column); } - private void scrollToError() { - ValidationError entry = (ValidationError)errorList.getSelectionModel().getSelectedItem(); + private void scrollToError(ValidationError entry) { if (entry != null) { if (entry.getLineNumber() != null) { tableView.scrollTo(max(0, entry.getLineNumber() - 1)); diff --git a/src/main/java/ninja/javafx/smartcsv/fx/list/ErrorSideBar.java b/src/main/java/ninja/javafx/smartcsv/fx/list/ErrorSideBar.java new file mode 100644 index 0000000..004b14f --- /dev/null +++ b/src/main/java/ninja/javafx/smartcsv/fx/list/ErrorSideBar.java @@ -0,0 +1,201 @@ +/* + The MIT License (MIT) + ----------------------------------------------------------------------------- + + Copyright (c) 2015 javafx.ninja + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + +*/ + +package ninja.javafx.smartcsv.fx.list; + +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.collections.ListChangeListener; +import javafx.collections.WeakListChangeListener; +import javafx.geometry.Insets; +import javafx.scene.Node; +import javafx.scene.canvas.Canvas; +import javafx.scene.canvas.GraphicsContext; +import javafx.scene.layout.Pane; +import javafx.scene.layout.VBox; +import javafx.scene.paint.Color; +import javafx.scene.text.Text; +import ninja.javafx.smartcsv.fx.table.model.CSVModel; +import ninja.javafx.smartcsv.validation.ValidationError; +import org.controlsfx.control.PopOver; + +import java.io.StringWriter; +import java.util.ArrayList; +import java.util.List; +import java.util.ResourceBundle; + +import static ninja.javafx.smartcsv.fx.util.I18nValidationUtil.getI18nValidatioMessage; + +/** + * clickable side bar with error markers + */ +public class ErrorSideBar extends Pane { + + private ListChangeListener errorListListener = c -> requestLayout(); + private WeakListChangeListener weakErrorListListener = new WeakListChangeListener<>(errorListListener); + private ObjectProperty model = new SimpleObjectProperty<>(); + private Canvas canvas = new Canvas(); + private ObjectProperty selectedValidationError = new SimpleObjectProperty<>(); + private PopOver popOver = new PopOver(); + private static final double WIDTH = 20.0; + + public ErrorSideBar(ResourceBundle resourceBundle) { + getChildren().add(canvas); + setFixWidth(); + configurePopOver(); + addModelListener(); + addMouseClickListener(); + addOnMouseOverListenerForPopOver(resourceBundle); + } + + public void setModel(CSVModel model) { + this.model.set(model); + } + + public CSVModel getModel() { + return model.get(); + } + + public ObjectProperty modelProperty() { + return model; + } + + public ValidationError getSelectedValidationError() { + return selectedValidationError.get(); + } + + public ObjectProperty selectedValidationErrorProperty() { + return selectedValidationError; + } + + public void setSelectedValidationError(ValidationError selectedValidationError) { + this.selectedValidationError.set(selectedValidationError); + } + + private void addOnMouseOverListenerForPopOver(ResourceBundle resourceBundle) { + setOnMouseMoved(event -> { + List errors = findValidationErrors(event.getY()); + if (!errors.isEmpty()) { + StringWriter messages = new StringWriter(); + for (ValidationError validationError: errors) { + messages.append(getI18nValidatioMessage(resourceBundle, validationError)).append("\n"); + } + popOver.setContentNode(popupContent(messages.toString())); + popOver.show(ErrorSideBar.this.getParent(), event.getScreenX()-WIDTH, event.getScreenY()); + } else { + popOver.hide(); + } + }); + } + + private void addMouseClickListener() { + setOnMouseClicked(event -> { + List errors = findValidationErrors(event.getY()); + if (!errors.isEmpty()) { + selectedValidationError.setValue(errors.get(0)); + } + }); + } + + private void addModelListener() { + model.addListener((observable, oldValue, newValue) -> { + newValue.getValidationError().addListener(weakErrorListListener); + requestLayout(); + }); + } + + private void configurePopOver() { + popOver.setArrowLocation(PopOver.ArrowLocation.RIGHT_CENTER); + } + + private void setFixWidth() { + setMinWidth(WIDTH); + setPrefWidth(WIDTH); + setMaxWidth(WIDTH); + } + + @Override + protected void layoutChildren() { + int top = (int) snappedTopInset(); + int right = (int) snappedRightInset(); + int bottom = (int) snappedBottomInset(); + int left = (int) snappedLeftInset(); + int w = (int) getWidth() - left - right; + int h = (int) getHeight() - top - bottom; + canvas.setLayoutX(left); + canvas.setLayoutY(top); + if (w != canvas.getWidth() || h != canvas.getHeight()) { + canvas.setWidth(w); + canvas.setHeight(h); + } + drawErrorMarker(w, h); + } + + private List findValidationErrors(double y) { + List errors = new ArrayList<>(); + if (model.get() != null) { + List errorList = model.get().getValidationError(); + if (errorList != null && !errorList.isEmpty()) { + int rows = model.get().getRows().size(); + double space = ((int)canvas.getHeight()) / rows; + for (ValidationError error : errorList) { + double blockStart = space * error.getLineNumber(); + if (blockStart-1 <= y && y <= blockStart+3) { + errors.add(error); + } + } + } + } + return errors; + } + + private void drawErrorMarker(int w, int h) { + + GraphicsContext g = canvas.getGraphicsContext2D(); + g.clearRect(0, 0, w, h); + g.setFill(Color.valueOf("#ff8888")); + + if (model.get() != null) { + List errorList = model.get().getValidationError(); + if (errorList != null && !errorList.isEmpty()) { + int rows = model.get().getRows().size(); + double space = h / rows; + for (ValidationError error : errorList) { + double blockStart = space * error.getLineNumber(); + g.fillRect(0, blockStart, w, 2); + } + } + } + } + + + private Node popupContent(String text) { + VBox vBox = new VBox(); + vBox.setPadding(new Insets(10,10,10,10)); + vBox.getChildren().add(new Text(text)); + return vBox; + } +} diff --git a/src/main/java/ninja/javafx/smartcsv/fx/list/ValidationErrorListCell.java b/src/main/java/ninja/javafx/smartcsv/fx/list/ValidationErrorListCell.java deleted file mode 100644 index 5180c48..0000000 --- a/src/main/java/ninja/javafx/smartcsv/fx/list/ValidationErrorListCell.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - The MIT License (MIT) - ----------------------------------------------------------------------------- - - Copyright (c) 2015 javafx.ninja - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in - all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - THE SOFTWARE. - -*/ - -package ninja.javafx.smartcsv.fx.list; - -import javafx.scene.control.ListCell; -import javafx.scene.text.Text; -import ninja.javafx.smartcsv.validation.ValidationError; - -import java.util.ResourceBundle; - -import static ninja.javafx.smartcsv.fx.util.I18nValidationUtil.getI18nValidatioMessage; - -/** - * Cell to show the error text - */ -public class ValidationErrorListCell extends ListCell { - - private ResourceBundle resourceBundle; - - public ValidationErrorListCell(ResourceBundle resourceBundle) { - this.resourceBundle = resourceBundle; - } - - @Override - public void updateItem(ValidationError validationError, boolean empty) { - super.updateItem(validationError, empty); - if (empty) { - clearContent(); - } else { - addContent(validationError); - } - } - - - private void clearContent() { - setText(null); - setGraphic(null); - } - - private void addContent(ValidationError validationError) { - setText(null); - Text text = new Text(getI18nValidatioMessage(resourceBundle, validationError)); - text.setWrappingWidth(180); - setGraphic(text); - } -} diff --git a/src/main/resources/ninja/javafx/smartcsv/fx/smartcsv.css b/src/main/resources/ninja/javafx/smartcsv/fx/smartcsv.css index 33e1a71..782de96 100644 --- a/src/main/resources/ninja/javafx/smartcsv/fx/smartcsv.css +++ b/src/main/resources/ninja/javafx/smartcsv/fx/smartcsv.css @@ -146,3 +146,10 @@ -fx-padding: 0 5.417em 0 0; } +.tooltip { + -fx-background-radius: 2 2 2 2; + -fx-background-color: linear-gradient(#eeeeee, #aaaaaa); + -fx-text-fill: black; + -fx-font-size: 12px; + -fx-padding: 10 10 10 10; +} \ No newline at end of file diff --git a/src/main/resources/ninja/javafx/smartcsv/fx/smartcsv.fxml b/src/main/resources/ninja/javafx/smartcsv/fx/smartcsv.fxml index af1b856..5e7b92f 100644 --- a/src/main/resources/ninja/javafx/smartcsv/fx/smartcsv.fxml +++ b/src/main/resources/ninja/javafx/smartcsv/fx/smartcsv.fxml @@ -1,17 +1,33 @@ - - - - - - - + + + + + + + + + + + + + + + + + + + + + - + + + - + @@ -103,7 +119,7 @@ - -