2 Commits
0.4 ... 0.5

Author SHA1 Message Date
b6731f7641 - added unique validation
- show line number
- goto line action
2016-08-05 23:09:47 +02:00
d2f81d7d3e added introduction talk 2016-08-03 10:58:01 +02:00
12 changed files with 210 additions and 20 deletions

View File

@@ -14,7 +14,10 @@ even in a "normal" CSV editor. So I decided to write this simple JavaFX applicat
[Wiki & Documentation](https://github.com/frosch95/SmartCSV.fx/wiki)
binary distribution of the [latest release (0.4)](https://drive.google.com/open?id=0BwY9gBUvn5qmREdCc0FvNDNEQTA)
binary distribution of the [latest release (0.5)](https://drive.google.com/file/d/0BwY9gBUvn5qmejllOTRwbEJYdDA/view?usp=sharing)
##Talks
[Introduction](http://javafx.ninja/talks/introduction/)
##License
###The MIT License (MIT)

View File

@@ -1,5 +1,5 @@
group 'ninja.javafx'
version '0.4-SNAPSHOT'
version '0.5-SNAPSHOT'
apply plugin: 'java'
apply plugin: 'groovy'

View File

@@ -1,2 +1,2 @@
rootProject.name = 'SmartCSV.ninja.javafx.smartcsv.fx'
rootProject.name = 'SmartCSV.fx'

View File

@@ -26,6 +26,10 @@
package ninja.javafx.smartcsv.fx;
import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.collections.ListChangeListener;
import javafx.collections.WeakListChangeListener;
import javafx.concurrent.WorkerStateEvent;
@@ -37,11 +41,13 @@ import javafx.scene.control.*;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.BorderPane;
import javafx.stage.FileChooser;
import javafx.util.converter.NumberStringConverter;
import ninja.javafx.smartcsv.csv.CSVFileReader;
import ninja.javafx.smartcsv.csv.CSVFileWriter;
import ninja.javafx.smartcsv.files.FileStorage;
import ninja.javafx.smartcsv.fx.about.AboutController;
import ninja.javafx.smartcsv.fx.list.ErrorSideBar;
import ninja.javafx.smartcsv.fx.list.GotoLineDialog;
import ninja.javafx.smartcsv.fx.preferences.PreferencesController;
import ninja.javafx.smartcsv.fx.table.ObservableMapValueFactory;
import ninja.javafx.smartcsv.fx.table.ValidationCellFactory;
@@ -65,9 +71,11 @@ import org.supercsv.prefs.CsvPreference;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.text.MessageFormat;
import java.util.Optional;
import java.util.ResourceBundle;
import static java.lang.Integer.parseInt;
import static java.lang.Math.max;
import static java.text.MessageFormat.format;
import static javafx.application.Platform.exit;
@@ -153,6 +161,9 @@ public class SmartCSVController extends FXMLController {
@FXML
private MenuItem addRowMenuItem;
@FXML
private MenuItem gotoLineMenuItem;
@FXML
private Button saveButton;
@@ -177,6 +188,9 @@ public class SmartCSVController extends FXMLController {
@FXML
private Button addRowButton;
@FXML
private Label currentLineNumber;
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// members
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
@@ -205,7 +219,7 @@ public class SmartCSVController extends FXMLController {
setupTableCellFactory();
setupErrorSideBar(resourceBundle);
bindMenuItemsToContentExistence(currentCsvFile, saveMenuItem, saveAsMenuItem, addRowMenuItem, createConfigMenuItem, loadConfigMenuItem);
bindMenuItemsToContentExistence(currentCsvFile, saveMenuItem, saveAsMenuItem, addRowMenuItem, gotoLineMenuItem, createConfigMenuItem, loadConfigMenuItem);
bindButtonsToContentExistence(currentCsvFile, saveButton, saveAsButton, addRowButton, createConfigButton, loadConfigButton);
bindMenuItemsToContentExistence(currentConfigFile, saveConfigMenuItem, saveAsConfigMenuItem);
@@ -346,6 +360,23 @@ public class SmartCSVController extends FXMLController {
selectNewRow();
}
@FXML
public void gotoLine(ActionEvent actionEvent) {
int maxLineNumber = currentCsvFile.getContent().getRows().size();
GotoLineDialog dialog = new GotoLineDialog(maxLineNumber);
dialog.setTitle(resourceBundle.getString("dialog.goto.line.title"));
dialog.setHeaderText(format(resourceBundle.getString("dialog.goto.line.header.text"), maxLineNumber));
dialog.setContentText(resourceBundle.getString("dialog.goto.line.label"));
Optional<Integer> result = dialog.showAndWait();
if (result.isPresent()){
Integer lineNumber = result.get();
if (lineNumber != null) {
tableView.scrollTo(max(0, lineNumber - 2));
tableView.getSelectionModel().select(lineNumber - 1);
}
}
}
public boolean canExit() {
boolean canExit = true;
if (currentCsvFile.getContent() != null && currentCsvFile.isFileChanged()) {
@@ -425,6 +456,10 @@ public class SmartCSVController extends FXMLController {
configurationName.textProperty().bind(selectString(currentConfigFile.fileProperty(), "name"));
}
private void bindLineNumber() {
currentLineNumber.textProperty().bind(tableView.getSelectionModel().selectedIndexProperty().add(1).asString());
}
private void loadCsvPreferencesFromFile() {
if (csvPreferenceFile.getFile().exists()) {
useLoadFileService(csvPreferenceFile, event -> setCsvPreference(csvPreferenceFile.getContent()));
@@ -529,6 +564,7 @@ public class SmartCSVController extends FXMLController {
currentCsvFile.getContent().setValidationConfiguration(currentConfigFile.getContent());
validationEditorController.setValidationConfiguration(currentConfigFile.getContent());
tableView = new TableView<>();
bindLineNumber();
bindMenuItemsToTableSelection(deleteRowMenuItem);
bindButtonsToTableSelection(deleteRowButton);

View File

@@ -0,0 +1,91 @@
package ninja.javafx.smartcsv.fx.list;
import com.sun.javafx.scene.control.skin.resources.ControlResources;
import javafx.application.Platform;
import javafx.geometry.Pos;
import javafx.scene.control.*;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.Priority;
import javafx.scene.layout.Region;
import java.util.function.UnaryOperator;
/**
* Created by abi on 05.08.2016.
*/
public class GotoLineDialog extends Dialog<Integer> {
private final GridPane grid;
private final Label label;
private final TextField textField;
public GotoLineDialog(int maxLineNumber) {
final DialogPane dialogPane = getDialogPane();
this.textField = new TextField("");
this.textField.setMaxWidth(Double.MAX_VALUE);
UnaryOperator<TextFormatter.Change> filter = change -> {
String text = change.getText();
if (text.matches("[0-9]*")) {
try {
int lineNumber = Integer.parseInt(textField.getText() + text);
if (lineNumber <= maxLineNumber) {
return change;
}
} catch (NumberFormatException e) {
// this happens when focusing textfield or press special keys like DEL
return change;
}
}
return null;
};
TextFormatter<String> textFormatter = new TextFormatter<>(filter);
textField.setTextFormatter(textFormatter);
GridPane.setHgrow(textField, Priority.ALWAYS);
GridPane.setFillWidth(textField, true);
label = createContentLabel(dialogPane.getContentText());
label.setPrefWidth(Region.USE_COMPUTED_SIZE);
label.textProperty().bind(dialogPane.contentTextProperty());
this.grid = new GridPane();
this.grid.setHgap(10);
this.grid.setMaxWidth(Double.MAX_VALUE);
this.grid.setAlignment(Pos.CENTER_LEFT);
dialogPane.contentTextProperty().addListener(o -> updateGrid());
dialogPane.getButtonTypes().addAll(ButtonType.OK, ButtonType.CANCEL);
updateGrid();
setResultConverter((dialogButton) -> {
ButtonBar.ButtonData data = dialogButton == null ? null : dialogButton.getButtonData();
return data == ButtonBar.ButtonData.OK_DONE ? Integer.parseInt(textField.getText()) : null;
});
}
private Label createContentLabel(String text) {
Label label = new Label(text);
label.setMaxWidth(Double.MAX_VALUE);
label.setMaxHeight(Double.MAX_VALUE);
label.getStyleClass().add("content");
label.setWrapText(true);
label.setPrefWidth(360);
return label;
}
private void updateGrid() {
grid.getChildren().clear();
grid.add(label, 0, 0);
grid.add(textField, 1, 0);
getDialogPane().setContent(grid);
Platform.runLater(() -> textField.requestFocus());
}
}

View File

@@ -52,6 +52,10 @@ public class ValidationConfiguration {
headerConfiguration.setNames(headerNames);
}
public Boolean getUniquenessRuleFor(String column) {
return (Boolean)getValue(column, "unique");
}
public Boolean getIntegerRuleFor(String column) {
return (Boolean)getValue(column, "integer");
}

View File

@@ -32,6 +32,7 @@ import groovy.lang.Script;
import org.codehaus.groovy.control.CompilationFailedException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
@@ -51,7 +52,7 @@ public class Validator {
private ValidationConfiguration validationConfig;
private GroovyShell shell = new GroovyShell();
private Map<String, Script> scriptCache = new HashMap<>();
private Map<String, HashMap<String, Integer>> uniquenessLookupTable = new HashMap<>();
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// constructors
@@ -92,6 +93,7 @@ public class Validator {
checkGroovy(column, value, error);
checkValueOf(column, value, error);
checkDouble(column, value, error);
checkUniqueness(column, value, lineNumber, error);
}
if (!error.isEmpty()) {
@@ -109,6 +111,29 @@ public class Validator {
// private methods
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
private void checkUniqueness(String column, String value, Integer lineNumber, ValidationError error) {
if (validationConfig.getUniquenessRuleFor(column) != null && validationConfig.getUniquenessRuleFor(column)) {
HashMap<String, Integer> columnValueMap = uniquenessLookupTable.get(column);
columnValueMap = getColumnValueMap(column, columnValueMap);
Integer valueInLineNumber = columnValueMap.get(value);
if (valueInLineNumber != null) {
if (!valueInLineNumber.equals(lineNumber)) {
error.add("validation.message.uniqueness", value, valueInLineNumber.toString());
}
} else {
columnValueMap.put(value, lineNumber);
}
}
}
private HashMap<String, Integer> getColumnValueMap(String column, HashMap<String, Integer> valueLineNumber) {
if (valueLineNumber == null) {
valueLineNumber = new HashMap<>();
uniquenessLookupTable.put(column, valueLineNumber);
}
return valueLineNumber;
}
private void checkGroovy(String column, String value, ValidationError error) {
String groovyScript = validationConfig.getGroovyRuleFor(column);
if (groovyScript != null) {

View File

@@ -1,5 +1,5 @@
application.name = SmartCSV.fx
application.version = 0.4
application.version = 0.5
# fxml views
fxml.smartcvs.view = /ninja/javafx/smartcsv/fx/smartcsv.fxml

View File

@@ -79,6 +79,12 @@
-glyph-size: 16px;
}
.goto-icon {
-glyph-name: "SUBDIRECTORY_ARROW_RIGHT";
-glyph-size: 16px;
}
/* toolbar customization based on http://fxexperience.com/2012/02/customized-segmented-toolbar-buttons/ */
#background {

View File

@@ -1,6 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import de.jensd.fx.glyphs.GlyphsStack?>
<?import de.jensd.fx.glyphs.materialdesignicons.MaterialDesignIconView?>
<?import java.net.URL?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Button?>
@@ -21,8 +22,7 @@
<?import javafx.scene.layout.RowConstraints?>
<?import javafx.scene.layout.VBox?>
<?import de.jensd.fx.glyphs.materialdesignicons.MaterialDesignIconView?>
<BorderPane fx:id="applicationPane" maxHeight="-Infinity" maxWidth="1000.0" minHeight="700.0" minWidth="-Infinity" prefHeight="700.0" prefWidth="1000.0" xmlns="http://javafx.com/javafx/8.0.65" xmlns:fx="http://javafx.com/fxml/1">
<BorderPane fx:id="applicationPane" maxHeight="-Infinity" maxWidth="1000.0" minHeight="700.0" minWidth="-Infinity" prefHeight="700.0" prefWidth="1000.0" xmlns="http://javafx.com/javafx/8.0.101" xmlns:fx="http://javafx.com/fxml/1">
<top>
<VBox id="background" prefWidth="100.0" BorderPane.alignment="CENTER">
<children>
@@ -52,7 +52,7 @@
<GlyphsStack>
<children>
<MaterialDesignIconView styleClass="config-icon" />
<MaterialDesignIconView style="-fx-opacity: 0.7;" styleClass="create-config-icon" />
<MaterialDesignIconView style="-fx-opacity: 0.7;" styleClass="create-config-icon" />
</children>
</GlyphsStack>
</graphic>
@@ -62,7 +62,7 @@
<GlyphsStack>
<children>
<MaterialDesignIconView styleClass="config-icon" />
<MaterialDesignIconView style="-fx-opacity: 0.7;" styleClass="load-config-icon" />
<MaterialDesignIconView style="-fx-opacity: 0.7;" styleClass="load-config-icon" />
</children>
</GlyphsStack>
</graphic>
@@ -72,7 +72,7 @@
<GlyphsStack>
<children>
<MaterialDesignIconView styleClass="config-icon" />
<MaterialDesignIconView style="-fx-opacity: 0.7;" styleClass="save-config-icon" />
<MaterialDesignIconView style="-fx-opacity: 0.7;" styleClass="save-config-icon" />
</children>
</GlyphsStack>
</graphic>
@@ -82,7 +82,7 @@
<GlyphsStack>
<children>
<MaterialDesignIconView styleClass="config-icon" />
<MaterialDesignIconView style="-fx-opacity: 0.7;" styleClass="save-config-icon" />
<MaterialDesignIconView style="-fx-opacity: 0.7;" styleClass="save-config-icon" />
</children>
</GlyphsStack>
</graphic>
@@ -114,6 +114,12 @@
<MaterialDesignIconView styleClass="add-icon" />
</graphic>
</MenuItem>
<SeparatorMenuItem mnemonicParsing="false" />
<MenuItem fx:id="gotoLineMenuItem" disable="true" mnemonicParsing="false" onAction="#gotoLine" text="%menu.goto.line">
<graphic>
<MaterialDesignIconView styleClass="goto-icon" />
</graphic>
</MenuItem>
</items>
</Menu>
<Menu mnemonicParsing="false" text="%menu.help">
@@ -164,7 +170,7 @@
<GlyphsStack>
<children>
<MaterialDesignIconView styleClass="config-icon" />
<MaterialDesignIconView style="-fx-opacity: 0.7;" styleClass="create-config-icon" />
<MaterialDesignIconView style="-fx-opacity: 0.7;" styleClass="create-config-icon" />
</children>
</GlyphsStack>
</graphic>
@@ -177,7 +183,7 @@
<GlyphsStack>
<children>
<MaterialDesignIconView styleClass="config-icon" />
<MaterialDesignIconView style="-fx-opacity: 0.7;" styleClass="load-config-icon" />
<MaterialDesignIconView style="-fx-opacity: 0.7;" styleClass="load-config-icon" />
</children>
</GlyphsStack>
</graphic>
@@ -190,7 +196,7 @@
<GlyphsStack>
<children>
<MaterialDesignIconView styleClass="config-icon" />
<MaterialDesignIconView style="-fx-opacity: 0.7;" styleClass="save-config-icon" />
<MaterialDesignIconView style="-fx-opacity: 0.7;" styleClass="save-config-icon" />
</children>
</GlyphsStack>
</graphic>
@@ -203,7 +209,7 @@
<GlyphsStack>
<children>
<MaterialDesignIconView styleClass="config-icon" />
<MaterialDesignIconView style="-fx-opacity: 0.7;" styleClass="save-config-icon" />
<MaterialDesignIconView style="-fx-opacity: 0.7;" styleClass="save-config-icon" />
</children>
</GlyphsStack>
@@ -275,7 +281,8 @@
<ColumnConstraints hgrow="NEVER" minWidth="10.0" />
<ColumnConstraints hgrow="ALWAYS" minWidth="10.0" />
<ColumnConstraints hgrow="NEVER" minWidth="10.0" />
<ColumnConstraints hgrow="ALWAYS" minWidth="10.0" prefWidth="100.0" />
<ColumnConstraints fillWidth="false" halignment="RIGHT" hgrow="NEVER" minWidth="10.0" prefWidth="100.0" />
<ColumnConstraints hgrow="ALWAYS" minWidth="10.0" prefWidth="100.0" />
</columnConstraints>
<rowConstraints>
<RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
@@ -287,6 +294,8 @@
<MaterialDesignIconView styleClass="config-check-icon" GridPane.columnIndex="3" GridPane.hgrow="NEVER" />
<Label text="%stateline.configuration" GridPane.columnIndex="4" GridPane.hgrow="NEVER" />
<Label fx:id="configurationName" GridPane.columnIndex="5" GridPane.hgrow="ALWAYS" />
<Label text="%lineNumber" GridPane.columnIndex="7" />
<Label fx:id="currentLineNumber" text="" GridPane.columnIndex="8" />
</children>
<BorderPane.margin>
<Insets bottom="4.0" left="8.0" right="8.0" top="4.0" />

View File

@@ -13,6 +13,7 @@ menu.help = Help
menu.preferences = Preferences
menu.delete.row = Delete row
menu.add.row = Add row
menu.goto.line = Goto line
title.validation.errors = Validation Errors:
@@ -30,6 +31,10 @@ dialog.exit.text = There are changes made to the csv file. If you close now, the
dialog.preferences.title = Preferences
dialog.preferences.header.text = Preferences
dialog.goto.line.title = Go to line
dialog.goto.line.header.text = Go to given line (max. {0}) and select line!
dialog.goto.line.label = Line:
preferences.quoteChar = Quote character:
preferences.delimiterChar = Delimiter character:
preferences.ignoreEmptyLines = Ignore empty lines:
@@ -47,9 +52,10 @@ validation.message.min.length = has not min length of {0}
validation.message.max.length = has not max length of {0}
validation.message.date.format = is not a date of format {0}
validation.message.regexp = does not match {0}
validation.message.uniqueness = value {0} is not unique (found in line {1})
validation.message.header.length = number of headers is not correct! there are {0} but there should be {1}
validation.message.header.match = header number {0} does not match "{1}" should be "{3}"
validation.message.header.match = header number {0} does not match "{1}" should be "{2}"
validation.message.value.of = Value {0} is not part of this list {1}
validation.rule.label.not_empty = not empty:
@@ -69,3 +75,5 @@ validation.rules.value = value
dialog.validation.rules.title = Validation rules
dialog.validation.rules.header = Validation rules of column "{0}"
context.menu.edit.column.rules = Edit rules
lineNumber = Selected line:

View File

@@ -22,6 +22,7 @@ menu.help = Hilfe
menu.preferences = Einstellungen
menu.delete.row = Zeile l\u00f6schen
menu.add.row = Zeile hinzuf\u00fcgen
menu.goto.line = Springe zur Zeile
title.validation.errors = Fehler in der Datei:
@@ -35,6 +36,10 @@ state.unchanged = Unver\u00e4ndert
dialog.preferences.title = Eigenschaften
dialog.preferences.header.text = Eigenschaften
dialog.goto.line.title = Springe zur Zeile
dialog.goto.line.header.text = Zur angegebenen Zeile (max. {0}) springen und ausw\u00e4hlen!
dialog.goto.line.label = Zeile:
dialog.exit.title = Anwendung beenden
dialog.exit.header.text = M\u00f6chten Sie wirklich die Anwendung beenden?
dialog.exit.text = Es gibt noch ungespeicherte \u00c4nderungen, die verloren gehen, wenn Sie die Anwendung jetzt beenden.
@@ -56,6 +61,7 @@ validation.message.min.length = Hat nicht die minimale L\u00e4nge von {0}
validation.message.max.length = Hat nicht die maximale L\u00e4nge von {0}
validation.message.date.format = Das Datumsformat entspricht nicht {0}
validation.message.regexp = entspricht nicht dem regul\u00e4ren Ausdruck {0}
validation.message.uniqueness = Wert {0} ist nicht einmalig (gefunden in Zeile {1})
validation.message.header.length = Anzahl der \u00dcberschriften ist nicht korrekt! Es sind {0} aber es sollten {1} sein
validation.message.header.match = \u00dcberschrift in Spalte {0} stimmt nicht. "{1}" sollte "{3}" sein
@@ -79,3 +85,5 @@ dialog.validation.rules.title = Pr\u00fcfregeln
dialog.validation.rules.header = Pr\u00fcfregeln f\u00fcr die Spalte "{0}"
context.menu.edit.column.rules = Pr\u00fcfregeln bearbeiten
lineNumber = Ausgew\u00e4hlte Zeile: