mirror of
https://github.com/frosch95/SmartCSV.fx.git
synced 2026-04-11 21:48:22 +02:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b6731f7641 | |||
| d2f81d7d3e |
@@ -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)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
group 'ninja.javafx'
|
||||
version '0.4-SNAPSHOT'
|
||||
version '0.5-SNAPSHOT'
|
||||
|
||||
apply plugin: 'java'
|
||||
apply plugin: 'groovy'
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
rootProject.name = 'SmartCSV.ninja.javafx.smartcsv.fx'
|
||||
rootProject.name = 'SmartCSV.fx'
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
@@ -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">
|
||||
@@ -275,6 +281,7 @@
|
||||
<ColumnConstraints hgrow="NEVER" minWidth="10.0" />
|
||||
<ColumnConstraints hgrow="ALWAYS" minWidth="10.0" />
|
||||
<ColumnConstraints hgrow="NEVER" minWidth="10.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>
|
||||
@@ -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" />
|
||||
|
||||
@@ -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:
|
||||
@@ -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:
|
||||
Reference in New Issue
Block a user