Commit 6e299a57 authored by Robert Zenz's avatar Robert Zenz

Work on the help system.

parent 5096d194
...@@ -19,6 +19,9 @@ ...@@ -19,6 +19,9 @@
package org.bonsaimind.jmathpaper.core.resources; package org.bonsaimind.jmathpaper.core.resources;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.regex.Pattern; import java.util.regex.Pattern;
...@@ -66,6 +69,28 @@ public final class ResourceLoader { ...@@ -66,6 +69,28 @@ public final class ResourceLoader {
processResource(relativePath, lineProcessor, null); processResource(relativePath, lineProcessor, null);
} }
public static final String readResource(String relativePath) {
StringBuilder content = new StringBuilder();
try (BufferedReader reader = new BufferedReader(new InputStreamReader(
ResourceLoader.class.getResourceAsStream(relativePath)))) {
String line = reader.readLine();
while (line != null) {
content.append(line);
content.append("\n");
line = reader.readLine();
}
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return content.toString();
}
/** /**
* Iterates over each line of the given resource. * Iterates over each line of the given resource.
* <p> * <p>
......
Basics
======
When you start jMathPaper you are greeted with a (currently empty) list of previous expressions and the input box to enter a new one. You can enter any expression you want to evaluate. Additionally, there is a larger input are to the right which allows you to take notes.
You can save and load papers from the menu, they are stored in a very simple cleartext format, so it is easy to create and edit them without jMathPaper. Examples can be found in the examples directory.
Let us start with something simple and enter a few simple expressions:
ID Expression Result
--------------------------------------------------
#1 1+1 = 2
#2 5*5 = 25
#3 239-38 = 201
One of the big features of jMathPaper is that you can easily reference
a previous result by using its ID, like this:
#1 1+1 = 2
#2 5*5 = 25
#3 239-38 = 201
#4 #2*25 = 625
We can also define variables which we can reference:
#1 1+1 = 2
#2 5*5 = 25
#3 239-38 = 201
#4 #2*25 = 625
length length=10 = 10
width width=5 = 5
area area=length*width = 50
Now if we made a mistake, for example the width really is 7, we can simply press
up and down to scroll through the history and execute the corrected expression
again. The previously executed expressions do not change, so we need to evaluate
them again:
#1 1+1 = 2
#2 5*5 = 25
#3 239-38 = 201
#4 #2*25 = 625
length length=10 = 10
width width=5 = 5
area area=length*width = 50
width width=7 = 7
area area=length*width = 70
We can also use mathematical functions, like `sin`:
#1 sin(70) = 0.9396926
See the documentation of [EvalEx](https://github.com/uklimaschewski/EvalEx/blob/master/README.md) to see what functions are supported.
jMathPaper
==========
This is a simple calculator application which aims to provide a similar experience like solving equations by taking notes on a sheet of paper. By coincidence it also works similar to spreadsheet applications, but with being limited to a mere list.
You can access further help topics with the `help` command, for example by typing
help basics
into the input field.
main.markdown
basics/main.markdown
\ No newline at end of file
package org.bonsaimind.jmathpaper.core.support;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.bonsaimind.jmathpaper.core.resources.ResourceLoader;
public class Topic {
protected String content = null;
protected String name = null;
protected Topic parent = null;
protected String title = null;
protected List<Topic> topics = new ArrayList<>();
protected Map<String, Topic> topicsByName = new HashMap<>();
private List<Topic> topicsReadonly = null;
public Topic(String name, String content) {
super();
this.name = name;
this.content = content;
updateTitle();
}
public static final Topic buildFrom(String relativeResourcePackage) {
Topic rootNode = new Topic("", "");
ResourceLoader.processResource(relativeResourcePackage + "/topics.index", (line) -> {
String path = line;
if (path.endsWith(".markdown")) {
path = path.substring(0, path.length() - 9);
}
if (path.endsWith("/main")) {
path = path.substring(0, path.length() - 5);
} else {
path = "";
}
rootNode.addTopic(path, ResourceLoader.readResource(relativeResourcePackage + "/" + line));
});
return rootNode;
}
public void addTopic(String path, String content) {
Topic topic = getTopic(path);
if (topic == this) {
this.content = content;
updateTitle();
} else if (topic != null) {
topic.content = content;
topic.updateTitle();
} else {
String targetPath = "";
String name = path;
if (name.endsWith("/")) {
name = name.substring(0, name.length() - 1);
}
int lastSlashIndex = name.lastIndexOf("/");
if (lastSlashIndex >= 0) {
targetPath = name.substring(0, lastSlashIndex);
name = name.substring(lastSlashIndex + 1);
}
addTopic(targetPath, new Topic(name, content));
}
}
public void addTopic(String path, Topic topic) {
Topic parent = getTopic(path);
if (parent == null) {
parent = this;
for (String name : path.split("/")) {
if (!name.isEmpty()) {
Topic subTopic = parent.getTopic(name);
if (subTopic == null) {
subTopic = new Topic(name, "");
parent.addTopic(subTopic);
}
parent = subTopic;
}
}
}
Topic alreadyExistingTopic = parent.getTopic(topic.getName());
if (alreadyExistingTopic != null) {
topic.addTopics(alreadyExistingTopic.getTopics());
}
parent.addTopic(topic);
}
public void addTopic(Topic topic) {
if (topic == null) {
return;
}
topics.add(topic);
Topic previousTopic = topicsByName.put(topic.getName(), topic);
topic.setParent(this);
if (previousTopic != null) {
previousTopic.setParent(null);
topics.remove(previousTopic);
}
}
public void addTopics(Iterable<Topic> topics) {
if (topics == null) {
return;
}
for (Topic topic : topics) {
addTopic(topic);
}
}
public String getContent() {
return content;
}
public String getName() {
return name;
}
public Topic getParent() {
return parent;
}
public Topic getRoot() {
Topic ancestor = this;
while (ancestor.getParent() != null) {
ancestor = ancestor.getParent();
}
return ancestor;
}
public String getTitle() {
return title;
}
public Topic getTopic(String pathOrName) {
if (pathOrName == null || pathOrName.isEmpty()) {
return this;
}
Topic topic = topicsByName.get(pathOrName);
if (topic == null && pathOrName.contains("/")) {
topic = this;
for (String name : pathOrName.split("/")) {
if (!name.isEmpty()) {
topic = topic.getTopic(name);
if (topic == null) {
return null;
}
}
}
}
return topic;
}
public List<Topic> getTopics() {
if (topicsReadonly == null) {
topicsReadonly = Collections.unmodifiableList(topics);
}
return topicsReadonly;
}
@Override
public String toString() {
return name;
}
protected void setParent(Topic topic) {
parent = topic;
}
protected void updateTitle() {
title = name;
if (content != null) {
int newlineIndex = content.indexOf("\n");
if (newlineIndex > 0) {
title = content.substring(0, newlineIndex);
} else if (newlineIndex < 0) {
title = content;
}
}
}
}
...@@ -34,6 +34,7 @@ import org.bonsaimind.jmathpaper.core.EvaluatedExpression; ...@@ -34,6 +34,7 @@ import org.bonsaimind.jmathpaper.core.EvaluatedExpression;
import org.bonsaimind.jmathpaper.core.InvalidExpressionException; import org.bonsaimind.jmathpaper.core.InvalidExpressionException;
import org.bonsaimind.jmathpaper.core.Paper; import org.bonsaimind.jmathpaper.core.Paper;
import org.bonsaimind.jmathpaper.core.configuration.Definitions; import org.bonsaimind.jmathpaper.core.configuration.Definitions;
import org.bonsaimind.jmathpaper.core.support.Topic;
/** /**
* {@link AbstractPapersUi} is an {@link Ui} implementation which implements the * {@link AbstractPapersUi} is an {@link Ui} implementation which implements the
...@@ -44,6 +45,8 @@ public abstract class AbstractPapersUi implements Ui { ...@@ -44,6 +45,8 @@ public abstract class AbstractPapersUi implements Ui {
/** The {@link Definitions} to use by default. */ /** The {@link Definitions} to use by default. */
protected Definitions defaultDefinitions = null; protected Definitions defaultDefinitions = null;
protected Topic help = null;
/** The currently selected {@link Paper}. */ /** The currently selected {@link Paper}. */
protected Paper paper = null; protected Paper paper = null;
...@@ -202,6 +205,22 @@ public abstract class AbstractPapersUi implements Ui { ...@@ -202,6 +205,22 @@ public abstract class AbstractPapersUi implements Ui {
} }
break; break;
case HELP:
String topicName = null;
if (parameters != null && parameters.length > 0) {
topicName = String.join(" ", parameters).trim();
}
Topic topic = help.getTopic(topicName);
if (topic != null) {
showHelp(topic);
} else {
throw new CommandExecutionException("No such help topic: " + topicName);
}
break;
case NEXT: case NEXT:
next(); next();
break; break;
...@@ -301,6 +320,8 @@ public abstract class AbstractPapersUi implements Ui { ...@@ -301,6 +320,8 @@ public abstract class AbstractPapersUi implements Ui {
@Override @Override
public void init(UiParameters uiParameters) throws Exception { public void init(UiParameters uiParameters) throws Exception {
this.uiParameters = uiParameters; this.uiParameters = uiParameters;
help = Topic.buildFrom("help");
} }
/** /**
...@@ -911,6 +932,10 @@ public abstract class AbstractPapersUi implements Ui { ...@@ -911,6 +932,10 @@ public abstract class AbstractPapersUi implements Ui {
} }
} }
protected void showHelp(Topic topic) {
System.out.println(topic.getContent());
}
/** /**
* Splits the given {@String input} into single parameters. * Splits the given {@String input} into single parameters.
* *
......
...@@ -41,6 +41,8 @@ public enum Command { ...@@ -41,6 +41,8 @@ public enum Command {
/** The specified part will be copied to the clipboard. */ /** The specified part will be copied to the clipboard. */
COPY("copy", "cp", "y"), COPY("copy", "cp", "y"),
HELP("help", "man", "manual"),
/** Start a new paper. */ /** Start a new paper. */
NEW("new", ":new"), NEW("new", ":new"),
......
package org.bonsaimind.jmathpaper.uis.swt;
import org.bonsaimind.jmathpaper.core.support.Topic;
import org.bonsaimind.jmathpaper.uis.swt.events.ForwardingSelectionListener;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.SashForm;
import org.eclipse.swt.custom.StyledText;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Tree;
import org.eclipse.swt.widgets.TreeItem;
public class HelpComponent extends SashForm {
private StyledText contentText = null;
private Tree topicsTree = null;
public HelpComponent(Composite parent, int style) {
super(parent, style);
topicsTree = new Tree(this, SWT.BORDER);
topicsTree.addSelectionListener(new ForwardingSelectionListener(this::onTopicsTreeSelectionChanged));
contentText = new StyledText(this, SWT.BORDER | SWT.H_SCROLL | SWT.MULTI | SWT.V_SCROLL | SWT.WRAP);
contentText.setAlwaysShowScrollBars(false);
contentText.setEditable(false);
contentText.setWordWrap(true);
setWeights(new int[] { 1, 2 });
}
public void showHelp(Topic topic) {
rebuildTree(topic.getRoot());
if (topicsTree.getItemCount() > 0) {
TreeItem treeItem = topicsTree.getItem(0);
if (treeItem.getData() != topic) {
treeItem = findTreeItem(topic, topicsTree.getItem(0));
}
if (treeItem != null) {
topicsTree.select(treeItem);
}
}
onTopicsTreeSelectionChanged();
}
private void attachTopics(Topic topic, TreeItem parentTreeItem) {
if (topic == null) {
return;
}
TreeItem treeItem = setupTreeItem(new TreeItem(parentTreeItem, SWT.NONE), topic);
for (Topic subStopic : topic.getTopics()) {
attachTopics(subStopic, treeItem);
}
treeItem.setExpanded(true);
}
private TreeItem findTreeItem(Topic topic, TreeItem currentTreeItem) {
for (TreeItem treeItem : currentTreeItem.getItems()) {
if (treeItem.getData() == topic) {
return treeItem;
} else {
TreeItem foundTreeItem = findTreeItem(topic, treeItem);
if (foundTreeItem != null) {
return foundTreeItem;
}
}
}
return null;
}
private void onTopicsTreeSelectionChanged() {
if (topicsTree.getSelectionCount() > 0) {
TreeItem selectedTreeItem = topicsTree.getSelection()[0];
Topic selectedTopic = (Topic)selectedTreeItem.getData();
contentText.setText(selectedTopic.getContent());
}
}
private void rebuildTree(Topic topic) {
topicsTree.removeAll();
TreeItem treeItem = setupTreeItem(new TreeItem(topicsTree, SWT.NONE), topic);
for (Topic subStopic : topic.getTopics()) {
attachTopics(subStopic, treeItem);
}
treeItem.setExpanded(true);
}
private TreeItem setupTreeItem(TreeItem treeItem, Topic topic) {
treeItem.setData(topic);
treeItem.setText(topic.getTitle());
return treeItem;
}
}
...@@ -27,6 +27,7 @@ import java.nio.file.Paths; ...@@ -27,6 +27,7 @@ import java.nio.file.Paths;
import org.bonsaimind.jmathpaper.Version; import org.bonsaimind.jmathpaper.Version;
import org.bonsaimind.jmathpaper.core.InvalidExpressionException; import org.bonsaimind.jmathpaper.core.InvalidExpressionException;
import org.bonsaimind.jmathpaper.core.Paper; import org.bonsaimind.jmathpaper.core.Paper;
import org.bonsaimind.jmathpaper.core.support.Topic;
import org.bonsaimind.jmathpaper.core.ui.AbstractPapersUi; import org.bonsaimind.jmathpaper.core.ui.AbstractPapersUi;
import org.bonsaimind.jmathpaper.core.ui.UiParameters; import org.bonsaimind.jmathpaper.core.ui.UiParameters;
import org.bonsaimind.jmathpaper.uis.swt.events.EventForwarder; import org.bonsaimind.jmathpaper.uis.swt.events.EventForwarder;
...@@ -47,6 +48,7 @@ import org.eclipse.swt.widgets.Shell; ...@@ -47,6 +48,7 @@ import org.eclipse.swt.widgets.Shell;
public class Swt extends AbstractPapersUi { public class Swt extends AbstractPapersUi {
protected Display display = null; protected Display display = null;
protected HelpComponent helpComponent = null;
protected Shell shell = null; protected Shell shell = null;
private MenuItem clearMenuItem = null; private MenuItem clearMenuItem = null;
private MenuItem closeAllMenuItem = null; private MenuItem closeAllMenuItem = null;
...@@ -55,6 +57,7 @@ public class Swt extends AbstractPapersUi { ...@@ -55,6 +57,7 @@ public class Swt extends AbstractPapersUi {
private Label errorLabel = null; private Label errorLabel = null;
private FileDialog fileOpenDialog = null; private FileDialog fileOpenDialog = null;
private FileDialog fileSaveDialog = null; private FileDialog fileSaveDialog = null;
private Shell helpShell = null;
private MenuItem nextPaperMenuItem = null; private MenuItem nextPaperMenuItem = null;
private MenuItem openMenuItem = null; private MenuItem openMenuItem = null;
private int paperCounter = 0; private int paperCounter = 0;
...@@ -311,6 +314,22 @@ public class Swt extends AbstractPapersUi { ...@@ -311,6 +314,22 @@ public class Swt extends AbstractPapersUi {
return null; return null;
} }
@Override
protected void showHelp(Topic topic) {
if (helpShell == null || helpShell.isDisposed()) {
helpShell = new Shell();
helpShell.setLayout(new GridLayout(1, true));
helpShell.setSize(720, 480);
helpShell.setText("jMathPaper " + Version.CURRENT + " - Help");
helpComponent = new HelpComponent(helpShell, SWT.NONE);
helpComponent.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true, 1, 1));
}
helpComponent.showHelp(topic);
helpShell.setVisible(true);
}
protected void updateCurrentPaper() { protected void updateCurrentPaper() {
if (cTabFolder.getSelection() != null) { if (cTabFolder.getSelection() != null) {
setPaper(getCurrentPaperComponent().getPaper()); setPaper(getCurrentPaperComponent().getPaper());
......
package org.bonsaimind.jmathpaper.core.help;
import org.bonsaimind.jmathpaper.core.support.Topic;
import org.junit.Assert;
import org.junit.Test;
public class TestHelp {
@Test
public void testInsertAndRetrieve() {
Topic topic = new Topic("", "");
topic.addTopic("/a/b/c", new Topic("d1", "d1"));
topic.addTopic("/a/b/c/d2", "d2");
assertTopic("a", "", topic.getTopic("a"));
assertTopic("a", "", topic.getTopic("/a"));
assertTopic("a", "", topic.getTopic("a/"));
assertTopic("a", "", topic.getTopic("/a/"));
assertTopic("b", "", topic.getTopic("/a/b"));
assertTopic("c", "", topic.getTopic("/a/b/c"));
assertTopic("d1", "d1", topic.getTopic("/a/b/c/d1"));
assertTopic("d2", "d2", topic.getTopic("/a/b/c/d2"));
}
@Test
public void testInsertReplacement() {
Topic topic = new Topic("", "");
topic.addTopic("/a/b/c", new Topic("d", "d"));
topic.addTopic("/a/", new Topic("b", "b"));
assertTopic("b", "b", topic.getTopic("/a/b"));
assertTopic("c", "", topic.getTopic("/a/b/c"));
assertTopic("d", "d", topic.getTopic("/a/b/c/d"));
}
@Test
public void testLoadDefault() {
Topic topic = Topic.buildFrom("help");
Assert.assertNotNull(topic);
Assert.assertFalse(topic.getTopics().isEmpty());
}
private final void assertTopic(String expectedName, String expectedContent, Topic topic) {
Assert.assertNotNull(topic);
Assert.assertEquals(expectedName, topic.getName());
Assert.assertEquals(expectedContent, topic.getContent());
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment