Skip to content
Snippets Groups Projects
Commit 2c807471 authored by Hallvard Trætteberg's avatar Hallvard Trætteberg
Browse files

Justeringer på dokument-API og bruk av TOML for konfigurasjon.

parent 982a35eb
No related branches found
No related tags found
No related merge requests found
Showing
with 219 additions and 73 deletions
...@@ -14,8 +14,8 @@ class TodoModelSerializer extends JsonSerializer<TodoModel> { ...@@ -14,8 +14,8 @@ class TodoModelSerializer extends JsonSerializer<TodoModel> {
*/ */
@Override @Override
public void serialize(TodoModel model, JsonGenerator jsonGen, SerializerProvider serializerProvider) public void serialize(TodoModel model, JsonGenerator jsonGen, SerializerProvider
throws IOException { serializerProvider) throws IOException {
jsonGen.writeStartObject(); jsonGen.writeStartObject();
jsonGen.writeArrayFieldStart("lists"); jsonGen.writeArrayFieldStart("lists");
for (TodoList list : model) { for (TodoList list : model) {
......
...@@ -38,6 +38,12 @@ ...@@ -38,6 +38,12 @@
<version>4.7.0-9.1.2</version> <version>4.7.0-9.1.2</version>
</dependency> </dependency>
<dependency>
<groupId>org.tomlj</groupId>
<artifactId>tomlj</artifactId>
<version>1.0.0</version>
</dependency>
<dependency> <dependency>
<groupId>org.junit.jupiter</groupId> <groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId> <artifactId>junit-jupiter-api</artifactId>
......
...@@ -17,7 +17,10 @@ import todolist.json.TodoPersistence; ...@@ -17,7 +17,10 @@ import todolist.json.TodoPersistence;
public class TodoAppController { public class TodoAppController {
private static final String todoListWithTwoItems = private static final String todoListWithTwoItems =
"{\"lists\":[{\"name\":\"todo\",\"items\":[{\"text\":\"item1\",\"checked\":false},{\"text\":\"item2\",\"checked\":true,\"deadline\":\"2020-10-01T14:53:11\"}]}]}"; "{\"lists\":["
+ "{\"name\":\"todo\",\"items\":[{\"text\":\"item1\",\"checked\":false},"
+ "{\"text\":\"item2\",\"checked\":true,\"deadline\":\"2020-10-01T14:53:11\"}]}"
+ "]}";
@FXML @FXML
String userTodoModelPath; String userTodoModelPath;
......
...@@ -8,13 +8,23 @@ import javafx.stage.Stage; ...@@ -8,13 +8,23 @@ import javafx.stage.Stage;
public class TodoDocumentApp extends Application { public class TodoDocumentApp extends Application {
private TodoDocumentAppController controller;
@Override @Override
public void start(Stage stage) throws Exception { public void start(Stage stage) throws Exception {
Parent parent = FXMLLoader.load(getClass().getResource("TodoDocumentApp.fxml")); FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("TodoDocumentApp.fxml"));
Parent parent = fxmlLoader.load();
this.controller = fxmlLoader.getController();
stage.setScene(new Scene(parent)); stage.setScene(new Scene(parent));
stage.show(); stage.show();
} }
@Override
public void stop() throws Exception {
this.controller.writeConfig();
super.stop();
}
public static void main(String[] args) { public static void main(String[] args) {
launch(TodoDocumentApp.class, args); launch(TodoDocumentApp.class, args);
} }
......
package todolist.ui; package todolist.ui;
import fxutil.doc.FileMenuController;
import fxutil.doc.DocumentListener; import fxutil.doc.DocumentListener;
import fxutil.doc.FileMenuController;
import java.io.File; import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.stream.Collectors;
import org.tomlj.Toml;
import org.tomlj.TomlParseResult;
import javafx.fxml.FXML; import javafx.fxml.FXML;
import todolist.core.TodoModel; import todolist.core.TodoModel;
public class TodoDocumentAppController extends FileMenuController implements DocumentListener<TodoModel, File> { public class TodoDocumentAppController implements DocumentListener<TodoModel, File> {
private final TodoModelStorage todoModelStorage; private final TodoModelStorage todoModelStorage;
...@@ -26,19 +34,70 @@ public class TodoDocumentAppController extends FileMenuController implements Doc ...@@ -26,19 +34,70 @@ public class TodoDocumentAppController extends FileMenuController implements Doc
} }
@FXML @FXML
String userTodoModelPath; String userAppConfigPath;
@FXML @FXML
String sampleTodoModelResource; FileMenuController fileMenuController;
@FXML @FXML
TodoModelController todoModelViewController; TodoModelController todoModelViewController;
/**
* Map of config data. Current contents:
*
* fileMenu.recentFiles = [ ... ]
*/
private TomlParseResult config;
@FXML @FXML
private void initialize() { private void initialize() {
setDocumentStorage(todoModelStorage); fileMenuController.setDocumentStorage(todoModelStorage);
todoModelStorage.addDocumentStorageListener(this);
applyConfig();
if (! fileMenuController.openMostRecentFile()) {
todoModelStorage.newDocument(); todoModelStorage.newDocument();
} }
}
private void applyConfig() {
if (userAppConfigPath != null) {
try {
Path configPath = Paths.get(System.getProperty("user.home"), userAppConfigPath);
config = Toml.parse(configPath);
} catch (IOException ioex) {
System.err.println("Fant ingen " + userAppConfigPath + " på hjemmeområdet");
}
}
if (config == null) {
try {
config = Toml.parse(getClass().getResourceAsStream("todo-config.toml"));
} catch (IOException e) {
// ignore
}
}
if (config != null) {
if (config.contains("fileMenu.recentFiles")) {
List<File> recentFiles = config.getArray("fileMenu.recentFiles").toList().stream()
.map(o -> new File(o.toString())).collect(Collectors.toList());
fileMenuController.addRecentFiles(recentFiles.toArray(new File[recentFiles.size()]));
}
}
}
void writeConfig() {
// TODO
Path configPath = Paths.get(System.getProperty("user.home"), userAppConfigPath);
try (FileWriter writer = new FileWriter(configPath.toFile())) {
writer.write("[fileMenu]\n");
writer.write("recentFiles = [ ");
writer.write(fileMenuController.getRecentFiles().stream()
.map(o -> "\"" + o + "\"")
.collect(Collectors.joining(", ")));
writer.write(" ]\n");
} catch(IOException ioe) {
System.out.println("Fikk ikke skrevet konfigurasjon til " + userAppConfigPath + " på hjemmeområdet");
}
}
// DocumentListener // DocumentListener
......
...@@ -34,6 +34,10 @@ public class TodoModelController { ...@@ -34,6 +34,10 @@ public class TodoModelController {
@FXML @FXML
TodoListController todoListViewController; TodoListController todoListViewController;
public TodoModel getTodoModel() {
return todoModel;
}
public void setTodoModel(TodoModel todoModel) { public void setTodoModel(TodoModel todoModel) {
this.todoModel = todoModel; this.todoModel = todoModel;
updateTodoListsView(null); updateTodoListsView(null);
...@@ -80,22 +84,23 @@ public class TodoModelController { ...@@ -80,22 +84,23 @@ public class TodoModelController {
} }
}); });
todoListsView.setEditable(true); todoListsView.setEditable(true);
todoListsView.valueProperty().addListener((prop, oldTodoList, newTodoList) -> { todoListsView.valueProperty().addListener((prop, oldList, newList) -> {
// must identify the case where newTodoList represents an edited name // must identify the case where newTodoList represents an edited name
if (oldTodoList != null && newTodoList != null && (! todoListsView.getItems().contains(newTodoList))) { if (oldList != null && newList != null && (! todoListsView.getItems().contains(newList))) {
// either new name of dummy item or existing item // either new name of dummy item or existing item
if (oldTodoList.getName() == null) { if (oldList.getName() == null) {
// add as new list // add as new list
todoModel.addTodoList(newTodoList); todoModel.addTodoList(newList);
updateTodoListsView(newTodoList); updateTodoListsView(newList);
} else { } else {
// update name // update name
oldTodoList.setName(newTodoList.getName()); oldList.setName(newList.getName());
updateTodoListsView(oldTodoList); updateTodoListsView(oldList);
} }
} }
}); });
todoListsView.getSelectionModel().selectedItemProperty().addListener((prop, oldTodoList, newTodoList) -> { todoListsView.getSelectionModel().selectedItemProperty().addListener((prop, oldList, newList)
-> {
todoListViewController.setTodoList(getSelectedTodoList()); todoListViewController.setTodoList(getSelectedTodoList());
}); });
} }
......
package todolist.ui; package todolist.ui;
import fxutil.doc.AbstractDocumentStorage; import fxutil.doc.AbstractDocumentStorage;
import fxutil.doc.DocumentImporter;
import java.io.File; import java.io.File;
import java.io.FileInputStream;
import java.io.FileWriter; import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.InputStreamReader; import java.io.InputStreamReader;
import java.io.Reader; import java.io.Reader;
import java.io.Writer; import java.io.Writer;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.Collection;
import java.util.Collections;
import todolist.core.TodoModel; import todolist.core.TodoModel;
import todolist.json.TodoPersistence; import todolist.json.TodoPersistence;
public class TodoModelStorage extends AbstractDocumentStorage<TodoModel, File> { public class TodoModelStorage extends AbstractDocumentStorage<TodoModel, File> {
private TodoModel todoModel;
@Override
protected TodoModel getDocument() {
return todoModel;
}
@Override
protected void setDocument(TodoModel todoModel) {
this.todoModel = todoModel;
}
@Override @Override
protected TodoModel createDocument() { protected TodoModel createDocument() {
return new TodoModel(); return new TodoModel();
...@@ -50,14 +33,4 @@ public class TodoModelStorage extends AbstractDocumentStorage<TodoModel, File> { ...@@ -50,14 +33,4 @@ public class TodoModelStorage extends AbstractDocumentStorage<TodoModel, File> {
todoPersistence.writeTodoModel(todoModel, writer); todoPersistence.writeTodoModel(todoModel, writer);
} }
} }
@Override
protected InputStream toInputStream(File file) throws IOException {
return new FileInputStream(file);
}
@Override
public Collection<DocumentImporter> getDocumentImporters() {
return Collections.emptyList();
}
} }
...@@ -7,7 +7,7 @@ ...@@ -7,7 +7,7 @@
<VBox xmlns:fx="http://javafx.com/fxml/1" fx:controller="todolist.ui.TodoAppController"> <VBox xmlns:fx="http://javafx.com/fxml/1" fx:controller="todolist.ui.TodoAppController">
<fx:define> <fx:define>
<String fx:id="userTodoLModelPath" fx:value="todomodel.json"/> <String fx:id="userTodoModelPath" fx:value="todomodel.json"/>
<String fx:id="sampleTodoModelResource" fx:value="sample-todomodel.json"/> <String fx:id="sampleTodoModelResource" fx:value="sample-todomodel.json"/>
</fx:define> </fx:define>
<fx:include fx:id="todoModelView" source="TodoModel.fxml"/> <fx:include fx:id="todoModelView" source="TodoModel.fxml"/>
......
...@@ -8,8 +8,7 @@ ...@@ -8,8 +8,7 @@
<BorderPane xmlns:fx="http://javafx.com/fxml/1" fx:controller="todolist.ui.TodoDocumentAppController"> <BorderPane xmlns:fx="http://javafx.com/fxml/1" fx:controller="todolist.ui.TodoDocumentAppController">
<fx:define> <fx:define>
<String fx:id="userTodoLModelPath" fx:value="todomodel.json"/> <String fx:id="userAppConfigPath" fx:value="todo-config.toml"/>
<String fx:id="sampleTodoModelResource" fx:value="sample-todomodel.json"/>
</fx:define> </fx:define>
<top> <top>
<MenuBar> <MenuBar>
......
...@@ -4,7 +4,12 @@ import javafx.fxml.FXMLLoader; ...@@ -4,7 +4,12 @@ import javafx.fxml.FXMLLoader;
import javafx.scene.Parent; import javafx.scene.Parent;
import javafx.scene.Scene; import javafx.scene.Scene;
import javafx.stage.Stage; import javafx.stage.Stage;
import todolist.json.TodoPersistence;
import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.fail;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.testfx.framework.junit5.ApplicationTest; import org.testfx.framework.junit5.ApplicationTest;
...@@ -22,9 +27,16 @@ public class TodoAppTest extends ApplicationTest { ...@@ -22,9 +27,16 @@ public class TodoAppTest extends ApplicationTest {
stage.show(); stage.show();
} }
private TodoPersistence todoPersistence = new TodoPersistence();
@BeforeEach @BeforeEach
public void setupItems() { public void setupItems() {
// same as in test-todolist.json (should perhaps read it instead) // same as in test-todolist.json (should perhaps read it instead)
try (Reader reader = new InputStreamReader(getClass().getResourceAsStream("test-todomodel.json"))) {
this.controller.setTodoModel(todoPersistence.readTodoModel(reader));
} catch (IOException ioe) {
fail(ioe.getMessage());
}
} }
@Test @Test
......
...@@ -12,12 +12,6 @@ ...@@ -12,12 +12,6 @@
<?import de.jensd.fx.glyphs.fontawesome.FontAwesomeIconView?> <?import de.jensd.fx.glyphs.fontawesome.FontAwesomeIconView?>
<VBox xmlns:fx="http://javafx.com/fxml/1" fx:controller="todolist.ui.TodoModelController"> <VBox xmlns:fx="http://javafx.com/fxml/1" fx:controller="todolist.ui.TodoModelController">
<fx:define>
<!--
<String fx:id="userTodoListPath" fx:value="todolist.json"/>
-->
<String fx:id="sampleTodoListResource" fx:value="test-todolist.json"/>
</fx:define>
<HBox> <HBox>
<ComboBox fx:id="todoListsView"/> <ComboBox fx:id="todoListsView"/>
</HBox> </HBox>
......
...@@ -8,19 +8,20 @@ import java.net.URI; ...@@ -8,19 +8,20 @@ import java.net.URI;
import java.net.URL; import java.net.URL;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.Collections;
/** /**
* Incomplete implementation of **DocumentStorage**, * Incomplete implementation of **DocumentStorage**,
* to simplify implementing ones for specific document and location types. * to simplify implementing ones for specific document and location types.
* The main missing methods are for getting and setting the current document, creating an empty one * The main missing methods creating an empty one and loading and saving them.
* and creating an **InputStream** from a location.
* *
* @author hal * @author hal
* *
* @param <D> the document type * @param <D> the document type
* @param <L> the location type * @param <L> the location type
*/ */
public abstract class AbstractDocumentStorage<D, L> implements DocumentStorage<L>, DocumentPersistence<D, L> { public abstract class AbstractDocumentStorage<D, L> implements DocumentStorage<L>,
DocumentPersistence<D, L> {
private L documentLocation; private L documentLocation;
...@@ -41,23 +42,33 @@ public abstract class AbstractDocumentStorage<D, L> implements DocumentStorage<L ...@@ -41,23 +42,33 @@ public abstract class AbstractDocumentStorage<D, L> implements DocumentStorage<L
setDocumentLocation(documentLocation); setDocumentLocation(documentLocation);
} }
private D document;
/** /**
* Returns the current document. * Returns the current document.
* *
* @return the current document * @return the current document
*/ */
protected abstract D getDocument(); public D getDocument() {
return document;
}
/** /**
* Sets the current document. * Sets the current document.
* Must notify listeners.
* *
* @param document the new document * @param document the new document
*/ */
protected abstract void setDocument(D document); public void setDocument(D document) {
D oldDocument = getDocument();
this.document = document;
fireDocumentChanged(oldDocument);
}
// //
private final Collection<DocumentStorageListener<L>> documentListeners = new ArrayList<DocumentStorageListener<L>>(); private final Collection<DocumentStorageListener<L>> documentListeners =
new ArrayList<DocumentStorageListener<L>>();
@Override @Override
public void addDocumentStorageListener(final DocumentStorageListener<L> documentStorageListener) { public void addDocumentStorageListener(final DocumentStorageListener<L> documentStorageListener) {
...@@ -65,7 +76,8 @@ public abstract class AbstractDocumentStorage<D, L> implements DocumentStorage<L ...@@ -65,7 +76,8 @@ public abstract class AbstractDocumentStorage<D, L> implements DocumentStorage<L
} }
@Override @Override
public void removeDocumentStorageListener(final DocumentStorageListener<L> documentStorageListener) { public void removeDocumentStorageListener(final DocumentStorageListener<L>
documentStorageListener) {
documentListeners.remove(documentStorageListener); documentListeners.remove(documentStorageListener);
} }
...@@ -153,4 +165,11 @@ public abstract class AbstractDocumentStorage<D, L> implements DocumentStorage<L ...@@ -153,4 +165,11 @@ public abstract class AbstractDocumentStorage<D, L> implements DocumentStorage<L
public void saveCopyAs(final L documentLocation) throws Exception { public void saveCopyAs(final L documentLocation) throws Exception {
saveDocument(getDocument(), documentLocation); saveDocument(getDocument(), documentLocation);
} }
//
@Override
public Collection<DocumentImporter> getDocumentImporters() {
return Collections.emptyList();
}
} }
...@@ -5,8 +5,10 @@ import java.util.Collection; ...@@ -5,8 +5,10 @@ import java.util.Collection;
/** /**
* An interface with the methods necessary for supporting the standard File menu actions. * An interface with the methods necessary for supporting the standard File menu actions.
* The class representing the document (domain data container) is implicit in the implementation of this interface. * The class representing the document (domain data container) is implicit in the implementation
* The interface includes methods for getting and setting the location and creating, opening and saving the (current) document. * of this interface.
* The interface includes methods for getting and setting the location and creating, opening and
* saving the (current) document.
* *
* @author hal * @author hal
* *
...@@ -22,13 +24,14 @@ public interface DocumentStorage<L> { ...@@ -22,13 +24,14 @@ public interface DocumentStorage<L> {
/** /**
* Sets the current location (of the current document), can be used by a save-as action. * Sets the current location (of the current document), can be used by a save-as action.
* Must notify listeners.
* *
* @param documentLocation the document location * @param documentLocation the document location
*/ */
public void setDocumentLocation(L documentLocation); public void setDocumentLocation(L documentLocation);
/** /**
* Adds an IDocumentStorageListener that will be notified when the current location changes. * Adds an DocumentStorageListener that will be notified when the current location changes.
* *
* @param documentStorageListener the document storage listener * @param documentStorageListener the document storage listener
*/ */
...@@ -47,7 +50,8 @@ public interface DocumentStorage<L> { ...@@ -47,7 +50,8 @@ public interface DocumentStorage<L> {
public void newDocument(); public void newDocument();
/** /**
* Loads a documents from the provided location and sets it as the current one, can be used by an open action. * Loads a documents from the provided location and sets it as the current one,
* can be used by an open action.
* *
* @param documentLocation the document location * @param documentLocation the document location
*/ */
......
...@@ -7,7 +7,11 @@ import java.io.InputStream; ...@@ -7,7 +7,11 @@ import java.io.InputStream;
import java.net.MalformedURLException; import java.net.MalformedURLException;
import java.net.URL; import java.net.URL;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Optional; import java.util.Optional;
import javafx.event.ActionEvent; import javafx.event.ActionEvent;
import javafx.fxml.FXML; import javafx.fxml.FXML;
...@@ -42,17 +46,57 @@ public class FileMenuController { ...@@ -42,17 +46,57 @@ public class FileMenuController {
private final List<File> recentFiles = new ArrayList<File>(); private final List<File> recentFiles = new ArrayList<File>();
public Collection<File> getRecentFiles() {
return Collections.unmodifiableCollection(recentFiles);
}
public void addRecentFiles(File... files) {
recentFiles.addAll(List.of(files));
updateRecentMenu();
}
public void clearRecentFiles() {
recentFiles.clear();
updateRecentMenu();
}
@FXML @FXML
private Menu recentMenu; private Menu recentMenu;
protected void updateRecentMenu(final File file) { protected void updateRecentFiles(final File file) {
recentFiles.remove(file); recentFiles.remove(file);
recentFiles.add(0, file); recentFiles.add(0, file);
recentMenu.getItems().clear();
updateRecentMenu();
}
private Map<String, String> prefixReplacements = new HashMap<>(Map.of(System.getProperty("user.home"), "~"));
public void setPrefixReplacement(String prefix, String replacement) {
prefixReplacements.put(prefix, replacement);
}
public void removePrefixReplacement(String prefix) {
prefixReplacements.remove(prefix);
}
private String replacePrefix(String s) {
for (Map.Entry<String, String> entry : prefixReplacements.entrySet()) {
if (s.startsWith(entry.getKey())) {
return entry.getValue() + s.substring(entry.getKey().length());
}
}
return s;
}
protected void updateRecentMenu() {
recentMenu.getItems().clear(); recentMenu.getItems().clear();
for (final File recentFile : recentFiles) { for (final File recentFile : recentFiles) {
final MenuItem menuItem = new MenuItem(); final MenuItem menuItem = new MenuItem();
menuItem.setText(recentFile.toString()); String fileString = recentFile.toString();
menuItem.setOnAction(event -> handleOpenAction(event)); String menuItemString = replacePrefix(fileString);
menuItem.setText(menuItemString);
menuItem.setOnAction(this::handleOpenAction);
recentMenu.getItems().add(menuItem); recentMenu.getItems().add(menuItem);
} }
} }
...@@ -92,12 +136,29 @@ public class FileMenuController { ...@@ -92,12 +136,29 @@ public class FileMenuController {
void handleOpenAction(final File selection) { void handleOpenAction(final File selection) {
try { try {
documentStorage.openDocument(selection); documentStorage.openDocument(selection);
updateRecentMenu(selection); updateRecentFiles(selection);
} catch (final IOException e) { } catch (final IOException e) {
showExceptionDialog("Oops, problem when opening " + selection, e); showExceptionDialog("Oops, problem when opening " + selection, e);
} }
} }
/**
* Opens the most recent file.
*
* @return true of the file was opened, false otherwise
*/
public boolean openMostRecentFile() {
if (recentFiles.size() >= 1) {
try {
documentStorage.openDocument(recentFiles.get(0));
return true;
} catch (IOException e) {
return false;
}
}
return false;
}
private void showExceptionDialog(final String message) { private void showExceptionDialog(final String message) {
final Alert alert = new Alert(AlertType.ERROR, message, ButtonType.CLOSE); final Alert alert = new Alert(AlertType.ERROR, message, ButtonType.CLOSE);
alert.showAndWait(); alert.showAndWait();
...@@ -142,6 +203,7 @@ public class FileMenuController { ...@@ -142,6 +203,7 @@ public class FileMenuController {
try { try {
documentStorage.setDocumentLocation(selection); documentStorage.setDocumentLocation(selection);
documentStorage.saveDocument(); documentStorage.saveDocument();
updateRecentFiles(selection);
} catch (final IOException e) { } catch (final IOException e) {
showSaveExceptionDialog(documentStorage.getDocumentLocation(), e); showSaveExceptionDialog(documentStorage.getDocumentLocation(), e);
documentStorage.setDocumentLocation(oldStorage); documentStorage.setDocumentLocation(oldStorage);
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment