node's attributes, as we would
+ // only be interested in it's children
+ if (xpp.isEmptyElementTag()) {
+ Log.d(MainApplication.TAG, "Received empty richcontent node - skipping");
+
+ } else {
+ String richTextContent = loadRichContentNodes(xpp);
+
+ // if we have no parent node, something went seriously wrong - we can't have a richcontent that is not part of a mindmap node
+ if (nodeStack.empty()) {
+ throw new IllegalStateException("Received richtext without a parent node");
+ }
+
+ MindmapNode parentNode = nodeStack.peek();
+ parentNode.addRichTextContent(richTextContent);
+
+ // let view know that node content has changed
+ if (parentNode.hasNodeRichContentChangedSubscribers()) {
+ MindmapNode finalParentNode = parentNode;
+ mainActivity.runOnUiThread(() -> {
+ finalParentNode.notifySubscribersNodeRichContentChanged();
+ });
+ }
+ }
+ }
+
+ else if (xpp.getName().equals("font")) {
+ String boldAttribute = xpp.getAttributeValue(null, "BOLD");
+
+ // if we have no parent node, something went seriously wrong - we can't have a font node that is not part of a mindmap node
+ if (nodeStack.empty()) {
+ throw new IllegalStateException("Received richtext without a parent node");
+ }
+ MindmapNode parentNode = nodeStack.peek();
+
+ if (boldAttribute != null && boldAttribute.equals("true")) {
+ parentNode.setBold(true);
+ }
+
+ String italicsAttribute = xpp.getAttributeValue(null, "ITALIC");
+ if (italicsAttribute != null && italicsAttribute.equals("true")) {
+ parentNode.setItalic(true);
+ }
+
+ // let view know that node content has changed
+ if (parentNode.hasNodeStyleChangedSubscribers()) {
+ MindmapNode finalParentNode = parentNode;
+ mainActivity.runOnUiThread(() -> {
+ finalParentNode.notifySubscribersNodeStyleChanged();
+ });
+ }
+
+ }
+
+ else if (xpp.getName().equals("icon") && xpp.getAttributeValue(null, "BUILTIN") != null) {
+ String iconName = xpp.getAttributeValue(null, "BUILTIN");
+
+ // if we have no parent node, something went seriously wrong - we can't have icons that is not part of a mindmap node
+ if (nodeStack.empty()) {
+ throw new IllegalStateException("Received icon without a parent node");
+ }
+
+ MindmapNode parentNode = nodeStack.peek();
+ parentNode.addIconName(iconName);
+
+ // let view know that node content has changed
+ if (parentNode.hasNodeStyleChangedSubscribers()) {
+ MindmapNode finalParentNode = parentNode;
+ mainActivity.runOnUiThread(() -> {
+ finalParentNode.notifySubscribersNodeStyleChanged();
+ });
+ }
+
+ }
+
+ else if (xpp.getName().equals("arrowlink")) {
+ String destinationId = xpp.getAttributeValue(null, "DESTINATION");
+
+ // if we have no parent node, something went seriously wrong - we can't have icons that is not part of a mindmap node
+ if (nodeStack.empty()) {
+ throw new IllegalStateException("Received arrowlink without a parent node");
+ }
+
+ MindmapNode parentNode = nodeStack.peek();
+ parentNode.addArrowLinkDestinationId(destinationId);
+
+ }
+
+ else {
+ // Log.d(MainApplication.TAG, "Received unknown node " + xpp.getName());
+ }
+
+
+ } else if (eventType == XmlPullParser.END_TAG) {
+ if (xpp.getName().equals("node")) {
+ MindmapNode completedMindmapNode = nodeStack.pop();
+ completedMindmapNode.setLoaded(true);
+ }
+
+ } else if (eventType == XmlPullParser.TEXT) {
+ // TODO: do we have TEXT nodes in the mindmap at all?
+
+ } else {
+ throw new IllegalStateException("Received unknown event " + eventType);
+ }
+ eventType = xpp.next();
+ }
+
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+
+ // stack should now be empty
+ if (!nodeStack.empty()) {
+ throw new RuntimeException("Stack should be empty");
+ // TODO: we could try to be lenient here to allow opening partial documents (which sometimes happens when dropbox doesn't fully sync). Probably doesn't work anyways, as we already throw a runtime exception above if we receive garbage
+ }
+
+
+ // TODO: can we do this as we stream through the XML above?
+
+ // load all nodes of root node into simplified MindmapNode, and index them by ID for faster lookup
+ MindmapIndexes mindmapIndexes = loadAndIndexNodesByIds(rootNode);
+ mindmap.setMindmapIndexes(mindmapIndexes);
+
+ // Nodes can refer to other nodes with arrowlinks. We want to have the link on both ends of the link, so we can
+ // now set the corresponding links
+ fillArrowLinks();
+
+
+ long loadDocumentEndTime = System.currentTimeMillis();
+ Tracker tracker = MainApplication.getTracker();
+ tracker.send(new HitBuilders.TimingBuilder()
+ .setCategory("document")
+ .setValue(loadDocumentEndTime - loadDocumentStartTime)
+ .setVariable("loadDocument")
+ .setLabel("loadTime")
+ .build());
+ Log.d(MainApplication.TAG, "Document loaded");
+
+ //long numNodes = document.getElementsByTagName("node").getLength();
+ tracker.send(new HitBuilders.EventBuilder()
+ .setCategory("document")
+ .setAction("loadDocument")
+ .setLabel("numNodes")
+ .setValue(numNodes)
+ .build()
+ );
+
+ // now the full mindmap is loaded
+ mindmap.setLoaded(true);
+ mainActivity.setMindmapIsLoading(false);
+
+ }
+
+ private String loadRichContentNodes(XmlPullParser xpp) throws IOException, XmlPullParserException {
+ // as we are stream processing the XML, we need to consume the full XML until the
+ // richcontent tag is closed (i.e. until we're back at the current parsing depth)
+ // eagerly parse until richcontent node is closed
+ int startingDepth = xpp.getDepth();
+ String richTextContent = "";
+
+ int richContentSubParserEventType = xpp.next();
+
+ do {
+
+ // EVENT TYPES as reported by next()
+ switch (richContentSubParserEventType) {
+ /**
+ * Signalize that parser is at the very beginning of the document
+ * and nothing was read yet.
+ * This event type can only be observed by calling getEvent()
+ * before the first call to next(), nextToken, or nextTag()).
+ */
+ case XmlPullParser.START_DOCUMENT:
+ throw new IllegalStateException("Received START_DOCUMENT but were already within the document");
+
+ /**
+ * Logical end of the xml document. Returned from getEventType, next()
+ * and nextToken()
+ * when the end of the input document has been reached.
+ * NOTE: subsequent calls to
+ * next() or nextToken()
+ * may result in exception being thrown.
+ */
+ case XmlPullParser.END_DOCUMENT:
+ throw new IllegalStateException("Received END_DOCUMENT but expected to just parse a sub-document");
+
+ /**
+ * Returned from getEventType(),
+ * next(), nextToken() when
+ * a start tag was read.
+ * The name of start tag is available from getName(), its namespace and prefix are
+ * available from getNamespace() and getPrefix()
+ * if namespaces are enabled.
+ * See getAttribute* methods to retrieve element attributes.
+ * See getNamespace* methods to retrieve newly declared namespaces.
+ */
+ case XmlPullParser.START_TAG: {
+ String tagString = "";
+
+ String tagName = xpp.getName();
+ tagString += "<" + tagName;
+
+ for (int i = 0; i < xpp.getAttributeCount(); i++) {
+ String attributeName = xpp.getAttributeName(i);
+ String attributeValue = xpp.getAttributeValue(i);
+
+ String attributeString = " " + attributeName + "=" + '"' + attributeValue + '"';
+ tagString += attributeString;
+ }
+
+ tagString += ">";
+
+ richTextContent += tagString;
+
+ break;
+ }
+
+ /**
+ * Returned from getEventType(), next(), or
+ * nextToken() when an end tag was read.
+ * The name of start tag is available from getName(), its
+ * namespace and prefix are
+ * available from getNamespace() and getPrefix().
+ */
+ case XmlPullParser.END_TAG: {
+ String tagName = xpp.getName();
+ String tagString = "" + tagName + ">";
+ richTextContent += tagString;
+ break;
+ }
+
+ /**
+ * Character data was read and will is available by calling getText().
+ *
Please note: next() will
+ * accumulate multiple
+ * events into one TEXT event, skipping IGNORABLE_WHITESPACE,
+ * PROCESSING_INSTRUCTION and COMMENT events,
+ * In contrast, nextToken() will stop reading
+ * text when any other event is observed.
+ * Also, when the state was reached by calling next(), the text value will
+ * be normalized, whereas getText() will
+ * return unnormalized content in the case of nextToken(). This allows
+ * an exact roundtrip without changing line ends when examining low
+ * level events, whereas for high level applications the text is
+ * normalized appropriately.
+ */
+ case XmlPullParser.TEXT: {
+ String text = xpp.getText();
+ richTextContent += text;
+ break;
+ }
+
+ default:
+ throw new IllegalStateException("Received unexpected event type " + richContentSubParserEventType);
+
+ }
+
+ richContentSubParserEventType = xpp.next();
+
+ // stop parsing once we have come out far enough from the XML to be at the starting depth again
+ } while (xpp.getDepth() != startingDepth);
+ return richTextContent;
+ }
+
+ private MindmapNode parseNodeTag(XmlPullParser xpp, MindmapNode parentNode) {
+ String id = xpp.getAttributeValue(null, "ID");
+ int numericId;
+ try {
+ numericId = Integer.parseInt(id.replaceAll("\\D+", ""));
+ } catch (NumberFormatException e) {
+ numericId = id.hashCode();
+ }
+
+ String text = xpp.getAttributeValue(null, "TEXT");
+
+ // get link
+ String linkAttribute = xpp.getAttributeValue(null, "LINK");
+ Uri link;
+ if (linkAttribute != null && !linkAttribute.equals("")) {
+ link = Uri.parse(linkAttribute);
+ } else {
+ link = null;
+ }
+
+ // get tree ID (of cloned node)
+ String treeIdAttribute = xpp.getAttributeValue(null, "TREE_ID");
+
+ MindmapNode newMindmapNode = new MindmapNode(mindmap, parentNode, id, numericId, text, link, treeIdAttribute);
+ return newMindmapNode;
+ }
+
+
+ /**
+ * Index all nodes (and child nodes) by their ID, for fast lookup
+ *
+ * @param root
+ */
+ private MindmapIndexes loadAndIndexNodesByIds(MindmapNode root) {
+
+ // TODO: check if this optimization was necessary - otherwise go back to old implementation
+
+ // TODO: this causes us to load all mindmap nodes, defeating the lazy loading in ch.benediktkoeppel.code.droidplane.model.MindmapNode.getChildNodes
+
+ Stack stack = new Stack<>();
+ stack.push(root);
+
+ // try first to just extract all IDs and the respective node, and
+ // only insert into the hashmap once we know the size of the hashmap
+ List> idAndNode = new ArrayList<>();
+ List> numericIdAndNode = new ArrayList<>();
+
+ while (!stack.isEmpty()) {
+ MindmapNode node = stack.pop();
+
+ idAndNode.add(new Pair<>(node.getId(), node));
+ numericIdAndNode.add(new Pair<>(node.getNumericId(), node));
+
+ for (MindmapNode mindmapNode : node.getChildMindmapNodes()) {
+ stack.push(mindmapNode);
+ }
+
+ }
+
+ Map newNodesById = new HashMap<>(idAndNode.size());
+ Map newNodesByNumericId = new HashMap<>(numericIdAndNode.size());
+
+ for (Pair i : idAndNode) {
+ newNodesById.put(i.first, i.second);
+ }
+ for (Pair i : numericIdAndNode) {
+ newNodesByNumericId.put(i.first, i.second);
+ }
+
+ return new MindmapIndexes(newNodesById, newNodesByNumericId);
+
+ }
+
+ private void fillArrowLinks() {
+
+ Map nodesById = mindmap.getMindmapIndexes().getNodesByIdIndex();
+
+ for (String nodeId : nodesById.keySet()) {
+ MindmapNode mindmapNode = nodesById.get(nodeId);
+ for (String linkDestinationId : mindmapNode.getArrowLinkDestinationIds()) {
+ MindmapNode destinationNode = nodesById.get(linkDestinationId);
+ if (destinationNode != null) {
+ mindmapNode.getArrowLinkDestinationNodes().add(destinationNode);
+ destinationNode.getArrowLinkIncomingNodes().add(mindmapNode);
+ }
+ }
+ }
+ }
+
+}
diff --git a/app/src/main/java/ch/benediktkoeppel/code/droidplane/controller/OnRootNodeLoadedListener.java b/app/src/main/java/ch/benediktkoeppel/code/droidplane/controller/OnRootNodeLoadedListener.java
new file mode 100644
index 0000000..2d28d14
--- /dev/null
+++ b/app/src/main/java/ch/benediktkoeppel/code/droidplane/controller/OnRootNodeLoadedListener.java
@@ -0,0 +1,9 @@
+package ch.benediktkoeppel.code.droidplane.controller;
+
+import ch.benediktkoeppel.code.droidplane.model.Mindmap;
+import ch.benediktkoeppel.code.droidplane.model.MindmapNode;
+
+public interface OnRootNodeLoadedListener {
+
+ void rootNodeLoaded(Mindmap mindmap, MindmapNode rootNode);
+}
diff --git a/app/src/main/java/ch/benediktkoeppel/code/droidplane/AndroidHelper.java b/app/src/main/java/ch/benediktkoeppel/code/droidplane/helper/AndroidHelper.java
similarity index 90%
rename from app/src/main/java/ch/benediktkoeppel/code/droidplane/AndroidHelper.java
rename to app/src/main/java/ch/benediktkoeppel/code/droidplane/helper/AndroidHelper.java
index f55e212..0c3757e 100644
--- a/app/src/main/java/ch/benediktkoeppel/code/droidplane/AndroidHelper.java
+++ b/app/src/main/java/ch/benediktkoeppel/code/droidplane/helper/AndroidHelper.java
@@ -1,4 +1,4 @@
-package ch.benediktkoeppel.code.droidplane;
+package ch.benediktkoeppel.code.droidplane.helper;
import android.app.Activity;
import android.content.Context;
diff --git a/app/src/main/java/ch/benediktkoeppel/code/droidplane/model/Mindmap.java b/app/src/main/java/ch/benediktkoeppel/code/droidplane/model/Mindmap.java
new file mode 100644
index 0000000..b6ac19e
--- /dev/null
+++ b/app/src/main/java/ch/benediktkoeppel/code/droidplane/model/Mindmap.java
@@ -0,0 +1,93 @@
+package ch.benediktkoeppel.code.droidplane.model;
+
+import android.net.Uri;
+
+import androidx.lifecycle.ViewModel;
+
+/**
+ * Mindmap handles the loading and storing of a mind map document.
+ */
+public class Mindmap extends ViewModel {
+
+ /**
+ * The currently loaded Uri
+ */
+ private Uri uri;
+
+ /**
+ * The root node of the document.
+ */
+ private MindmapNode rootNode;
+
+ /**
+ * A map that resolves node IDs to Node objects
+ */
+ MindmapIndexes mindmapIndexes;
+
+ // whether the mindmap has finished loading
+ private boolean isLoaded = false;
+
+ /**
+ * Returns the Uri which is currently loaded in document.
+ *
+ * @return Uri
+ */
+ public Uri getUri() {
+
+ return this.uri;
+ }
+
+ /**
+ * Set the Uri after loading a new document.
+ *
+ * @param uri
+ */
+ public void setUri(Uri uri) {
+
+ this.uri = uri;
+ }
+
+ /**
+ * Returns the root node of the currently loaded mind map
+ *
+ * @return the root node
+ */
+ public MindmapNode getRootNode() {
+
+ return rootNode;
+ }
+
+ /**
+ * Returns the node for a given Node ID
+ *
+ * @param id
+ * @return
+ */
+ public MindmapNode getNodeByID(String id) {
+ return mindmapIndexes.getNodesByIdIndex().get(id);
+ }
+
+ public MindmapNode getNodeByNumericID(Integer numericId) {
+ return mindmapIndexes.getNodesByNumericIndex().get(numericId);
+ }
+
+ public boolean isLoaded() {
+ return isLoaded;
+ }
+
+ public void setLoaded(boolean loaded) {
+ isLoaded = loaded;
+ }
+
+ public void setRootNode(MindmapNode rootNode) {
+ this.rootNode = rootNode;
+ }
+
+ public void setMindmapIndexes(MindmapIndexes mindmapIndexes) {
+ this.mindmapIndexes = mindmapIndexes;
+ }
+
+ public MindmapIndexes getMindmapIndexes() {
+ return mindmapIndexes;
+ }
+}
diff --git a/app/src/main/java/ch/benediktkoeppel/code/droidplane/model/MindmapIndexes.java b/app/src/main/java/ch/benediktkoeppel/code/droidplane/model/MindmapIndexes.java
new file mode 100644
index 0000000..306a8ef
--- /dev/null
+++ b/app/src/main/java/ch/benediktkoeppel/code/droidplane/model/MindmapIndexes.java
@@ -0,0 +1,24 @@
+package ch.benediktkoeppel.code.droidplane.model;
+
+import java.util.Map;
+
+import ch.benediktkoeppel.code.droidplane.model.MindmapNode;
+
+public class MindmapIndexes {
+
+ private final Map nodesById;
+ private final Map nodesByNumericId;
+
+ public MindmapIndexes(Map nodesById, Map nodesByNumericId) {
+ this.nodesById = nodesById;
+ this.nodesByNumericId = nodesByNumericId;
+ }
+
+ public Map getNodesByIdIndex() {
+ return this.nodesById;
+ }
+
+ public Map getNodesByNumericIndex() {
+ return this.nodesByNumericId;
+ }
+}
diff --git a/app/src/main/java/ch/benediktkoeppel/code/droidplane/model/MindmapNode.java b/app/src/main/java/ch/benediktkoeppel/code/droidplane/model/MindmapNode.java
new file mode 100644
index 0000000..06a2a06
--- /dev/null
+++ b/app/src/main/java/ch/benediktkoeppel/code/droidplane/model/MindmapNode.java
@@ -0,0 +1,465 @@
+package ch.benediktkoeppel.code.droidplane.model;
+
+import android.net.Uri;
+import android.text.Html;
+
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.List;
+
+import ch.benediktkoeppel.code.droidplane.MainActivity;
+import ch.benediktkoeppel.code.droidplane.view.MindmapNodeLayout;
+import ch.benediktkoeppel.code.droidplane.view.NodeColumn;
+
+
+/**
+ * A MindMapNode is a special type of DOM Node. A DOM Node can be converted to a MindMapNode if it has type ELEMENT,
+ * and tag "node".
+ */
+//@Builder
+public class MindmapNode {
+
+ /**
+ * The ID of the node (ID attribute)
+ */
+ private final String id;
+
+ /**
+ * The numeric representation of this ID
+ */
+ private Integer numericId;
+
+ /**
+ * The mindmap, in which this node is
+ */
+ private final Mindmap mindmap;
+
+ /**
+ * The Parent MindmapNode
+ */
+ private final MindmapNode parentNode;
+
+ /**
+ * The Text of the node (TEXT attribute).
+ */
+ private final String text;
+
+ /**
+ * The Rich Text content of the node (if any)
+ */
+ private final List richTextContents;
+
+ /**
+ * Bold style
+ */
+ private boolean isBold;
+
+ /**
+ * Italic style
+ */
+ private boolean isItalic;
+
+ /**
+ * The names of the icon
+ */
+ private final List iconNames;
+
+ /**
+ * If the node has a LINK attribute, it will be stored in Uri link
+ */
+ private final Uri link;
+
+ /**
+ * The XML DOM node from which this MindMapNode is derived
+ */
+ // TODO: MindmapNode should not need this node
+ //private final Node node;
+
+ /**
+ * Whether the node is selected or not, will be set after it was clicked by the user
+ */
+ // TODO: this has nothing to do with the model
+ private boolean selected;
+
+ /**
+ * The list of child MindmapNodes. We support lazy loading.
+ */
+ private List childMindmapNodes;
+
+ /**
+ * If the node clones another node, it doesn't have text or richtext, but a TREE_ID
+ */
+ private final String treeIdAttribute;
+
+ /**
+ * List of outgoing arrow links
+ */
+ private final List arrowLinkDestinationIds;
+
+ /**
+ * List of outgoing arrow MindmapNodes
+ */
+ private List arrowLinkDestinationNodes = new ArrayList<>();
+
+ /**
+ * List of incoming arrow MindmapNodes
+ */
+ private List arrowLinkIncomingNodes = new ArrayList<>();
+ private WeakReference subscribedNodeColumn = null;
+ private WeakReference subscribedMainActivity = null;
+ private WeakReference subscribedNodeLayout = null;
+ private boolean loaded;
+
+ public MindmapNode(Mindmap mindmap, MindmapNode parentNode, String id, int numericId, String text, Uri link, String treeIdAttribute) {
+ this.mindmap = mindmap;
+ this.parentNode = parentNode;
+ this.id = id;
+ this.numericId = numericId;
+ this.text = text;
+ this.childMindmapNodes = new ArrayList<>();
+ this.richTextContents = new ArrayList<>();
+ isBold = false;
+ isItalic = false;
+ iconNames = new ArrayList<>();
+ this.link = link;
+ this.treeIdAttribute = treeIdAttribute;
+ arrowLinkDestinationIds = new ArrayList<>();
+ //node = null;
+ }
+
+ /**
+ * Creates a new MindMapNode from Node. The node needs to be of type ELEMENT and have tag "node". Throws a
+ * {@link ClassCastException} if the Node can not be converted to a MindmapNode.
+ *
+ * @param node
+ */
+// public MindmapNode(Node node, MindmapNode parentNode, Mindmap mindmap) {
+//
+// this.mindmap = mindmap;
+//
+// // store the parentNode
+// this.parentNode = parentNode;
+//
+// // convert the XML Node to a XML Element
+// Element tmpElement;
+// if (isMindmapNode(node)) {
+// tmpElement = (Element)node;
+// } else {
+// throw new ClassCastException("Can not convert Node to MindmapNode");
+// }
+//
+// // store the Node
+// this.node = node;
+//
+// // extract the ID of the node
+// id = tmpElement.getAttribute("ID");
+//
+// try {
+// numericId = Integer.parseInt(id.replaceAll("\\D+", ""));
+// } catch (NumberFormatException e) {
+// numericId = id.hashCode();
+// }
+//
+//
+// // extract the string (TEXT attribute) of the nodes
+// String text = tmpElement.getAttribute("TEXT");
+//
+// // extract the richcontent (HTML) of the node. This works both for nodes with a rich text content
+// // (TYPE="NODE"), for "Notes" (TYPE="NOTE"), for "Details" (TYPE="DETAILS").
+// String richTextContent = null;
+// // find 'richcontent TYPE="NODE"' subnode, which will contain the rich text content
+// NodeList richtextNodeList = tmpElement.getChildNodes();
+// for (int i = 0; i < richtextNodeList.getLength(); i++) {
+// Node n = richtextNodeList.item(i);
+// if (n.getNodeType() == Node.ELEMENT_NODE && n.getNodeName().equals("richcontent")) {
+// Element richcontentElement = (Element)n;
+// String typeAttribute = richcontentElement.getAttribute("TYPE");
+// if (typeAttribute.equals("NODE") || typeAttribute.equals("NOTE") || typeAttribute.equals("DETAILS")) {
+//
+// // extract the whole rich text (XML), to show in a WebView activity
+// try {
+// Transformer transformer = TransformerFactory.newInstance().newTransformer();
+// ByteArrayOutputStream boas = new ByteArrayOutputStream();
+// transformer.transform(new DOMSource(richtextNodeList.item(0)), new StreamResult(boas));
+// richTextContent = boas.toString();
+// } catch (TransformerException e) {
+// e.printStackTrace();
+// }
+//
+// // if the node has no text itself, then convert the rich text content to a text
+// if (text == null || text.equals("")) {
+// // convert the content (text only) into a string, to show in the normal list view
+// text = Html.fromHtml(richcontentElement.getTextContent()).toString();
+// }
+// }
+// }
+// }
+// this.richTextContent = richTextContent;
+// this.text = text;
+//
+//
+// // extract styles
+// NodeList styleNodeList = tmpElement.getChildNodes();
+// boolean isBold = false;
+// boolean isItalic = false;
+// for (int i = 0; i < styleNodeList.getLength(); i++) {
+// Node n = styleNodeList.item(i);
+// if (n.getNodeType() == Node.ELEMENT_NODE && n.getNodeName().equals("font")) {
+// Element fontElement = (Element)n;
+// if (fontElement.hasAttribute("BOLD") && fontElement.getAttribute("BOLD").equals("true")) {
+// Log.d(MainApplication.TAG, "Found bold node");
+// isBold = true;
+// }
+// if (fontElement.hasAttribute("ITALIC") && fontElement.getAttribute("ITALIC").equals("true")) {
+// isItalic = true;
+// }
+// }
+// }
+// this.isBold = isBold;
+// this.isItalic = isItalic;
+//
+// // extract icons
+// iconNames = getIcons();
+//
+// // find out if it has sub nodes
+// // TODO: this should just go into a getter
+// isExpandable = (getNumChildMindmapNodes() > 0);
+//
+// // extract link
+// String linkAttribute = tmpElement.getAttribute("LINK");
+// if (!linkAttribute.equals("")) {
+// link = Uri.parse(linkAttribute);
+// } else {
+// link = null;
+// }
+//
+// // get cloned node's info
+// treeIdAttribute = tmpElement.getAttribute("TREE_ID");
+//
+// // get arrow link destinations
+// arrowLinkDestinationIds = new ArrayList<>();
+// arrowLinkDestinationNodes = new ArrayList<>();
+// arrowLinkIncomingNodes = new ArrayList<>();
+// NodeList arrowlinkList = tmpElement.getChildNodes();
+// for (int i = 0; i< arrowlinkList.getLength(); i++) {
+// Node n = arrowlinkList.item(i);
+// if (n.getNodeType() == Node.ELEMENT_NODE && n.getNodeName().equals("arrowlink")) {
+// Element arrowlinkElement = (Element)n;
+// String destinationId = arrowlinkElement.getAttribute("DESTINATION");
+// arrowLinkDestinationIds.add(destinationId);
+// }
+// }
+//
+// }
+
+
+ /**
+ * Selects or deselects this node
+ *
+ * @param selected
+ */
+ public void setSelected(boolean selected) {
+
+ this.selected = selected;
+ }
+
+ /**
+ * Returns whether this node is selected
+ */
+ public boolean getIsSelected() {
+
+ return this.selected;
+ }
+
+
+
+ public List getIconNames() {
+
+ return iconNames;
+ }
+
+ // TODO: this should probably live in a view controller, not here
+ public String getText() {
+
+ // if this is a cloned node, get the text from the original node
+ if (treeIdAttribute != null && !treeIdAttribute.equals("")) {
+ // TODO this now fails when loading, because the background indexing is not done yet - so we maybe should mark this as "pending", and put it into a queue, to be updated once the linked node is there
+ MindmapNode linkedNode = mindmap.getNodeByID(treeIdAttribute);
+ if (linkedNode != null) {
+ return linkedNode.getText();
+ }
+ }
+
+ // if this is a rich text node, get the HTML content instead
+ if (this.text == null && this.getRichTextContents() != null && !this.getRichTextContents().isEmpty()) {
+
+ String richTextContent = this.getRichTextContents().get(0);
+ return Html.fromHtml(richTextContent).toString();
+
+ }
+
+ return text;
+ }
+
+ public boolean isBold() {
+
+ return isBold;
+ }
+
+ public boolean isItalic() {
+
+ return isItalic;
+ }
+
+ public boolean isExpandable() {
+
+ return !childMindmapNodes.isEmpty();
+ }
+
+ public Uri getLink() {
+
+ return link;
+ }
+
+ public Mindmap getMindmap() {
+
+ return mindmap;
+ }
+
+ public String getId() {
+
+ return id;
+ }
+
+ public MindmapNode getParentNode() {
+
+ return parentNode;
+ }
+
+ public List getRichTextContents() {
+
+ return richTextContents;
+ }
+
+ public void addRichTextContent(String richTextContent) {
+ this.richTextContents.add(richTextContent);
+ }
+
+ public List getArrowLinkDestinationIds() {
+
+ return arrowLinkDestinationIds;
+ }
+
+ public List getArrowLinkDestinationNodes() {
+
+ return arrowLinkDestinationNodes;
+ }
+
+ public List getArrowLinkIncomingNodes() {
+
+ return arrowLinkIncomingNodes;
+ }
+
+ public List getArrowLinks() {
+ ArrayList combinedArrowLists = new ArrayList<>();
+ combinedArrowLists.addAll(arrowLinkDestinationNodes);
+ combinedArrowLists.addAll(arrowLinkIncomingNodes);
+ return combinedArrowLists;
+ }
+
+ public Integer getNumericId() {
+
+ return numericId;
+ }
+
+ public List getChildMindmapNodes() {
+ return this.childMindmapNodes;
+ }
+
+ public void setChildMindmapNodes(List childMindmapNodes) {
+ this.childMindmapNodes = childMindmapNodes;
+ }
+
+ public int getNumChildMindmapNodes() {
+ return childMindmapNodes.size();
+ }
+
+ public void subscribe(NodeColumn nodeColumn) {
+ this.subscribedNodeColumn = new WeakReference<>(nodeColumn);
+ }
+
+ public void addChildMindmapNode(MindmapNode newMindmapNode) {
+ this.childMindmapNodes.add(newMindmapNode);
+ }
+
+ public boolean hasAddedChildMindmapNodeSubscribers() {
+ return this.subscribedNodeColumn != null;
+ }
+ public void notifySubscribersAddedChildMindmapNode(MindmapNode mindmapNode) {
+ if (this.subscribedNodeColumn != null) {
+ subscribedNodeColumn.get().notifyNewMindmapNode(mindmapNode);
+ }
+ }
+
+ public boolean hasNodeRichContentChangedSubscribers() {
+ return this.subscribedMainActivity != null;
+ }
+
+ public void notifySubscribersNodeRichContentChanged() {
+ if (this.subscribedMainActivity != null) {
+ subscribedMainActivity.get().notifyNodeRichContentChanged();
+ }
+ }
+
+ // TODO: ugly that MainActivity is needed here. Would be better to introduce an listener interface (same for node column above)
+ public void subscribeNodeRichContentChanged(MainActivity mainActivity) {
+ this.subscribedMainActivity = new WeakReference<>(mainActivity);
+ }
+
+ public void setLoaded(boolean loaded) {
+ this.loaded = loaded;
+ }
+
+ public void setBold(boolean bold) {
+ isBold = bold;
+ }
+
+ public void setItalic(boolean italic) {
+ isItalic = italic;
+ }
+
+ public boolean hasNodeStyleChangedSubscribers() {
+ return this.subscribedNodeLayout != null;
+ }
+
+ public void subscribeNodeStyleChanged(MindmapNodeLayout nodeLayout) {
+ this.subscribedNodeLayout = new WeakReference<>(nodeLayout);
+ }
+
+ public void notifySubscribersNodeStyleChanged() {
+ if (this.subscribedNodeLayout != null) {
+ this.subscribedNodeLayout.get().notifyNodeStyleChanged();
+ }
+ }
+
+ public void addIconName(String iconName) {
+ this.iconNames.add(iconName);
+ }
+
+ public void addArrowLinkDestinationId(String destinationId) {
+ this.arrowLinkDestinationIds.add(destinationId);
+ }
+
+ /** Depth-first search in the core text of the nodes in this sub-tree. */
+ // TODO: this doesn't work while mindmap is still loading
+ public List search(String searchString) {
+ var res = new ArrayList();
+ if (this.getText().toUpperCase().contains(searchString.toUpperCase())) { // TODO: npe here when text is null, because text is a rich text
+ res.add(this);
+ }
+ for (MindmapNode child : childMindmapNodes) {
+ res.addAll(child.search(searchString));
+ }
+ return res;
+ }
+}
diff --git a/app/src/main/java/ch/benediktkoeppel/code/droidplane/HorizontalMindmapView.java b/app/src/main/java/ch/benediktkoeppel/code/droidplane/view/HorizontalMindmapView.java
similarity index 90%
rename from app/src/main/java/ch/benediktkoeppel/code/droidplane/HorizontalMindmapView.java
rename to app/src/main/java/ch/benediktkoeppel/code/droidplane/view/HorizontalMindmapView.java
index 10450e6..98bcdd4 100644
--- a/app/src/main/java/ch/benediktkoeppel/code/droidplane/HorizontalMindmapView.java
+++ b/app/src/main/java/ch/benediktkoeppel/code/droidplane/view/HorizontalMindmapView.java
@@ -1,9 +1,10 @@
-package ch.benediktkoeppel.code.droidplane;
+package ch.benediktkoeppel.code.droidplane.view;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.Context;
import android.os.Handler;
+import android.text.Html;
import android.util.Log;
import android.view.GestureDetector;
import android.view.GestureDetector.SimpleOnGestureListener;
@@ -26,6 +27,13 @@
import java.util.Map;
import java.util.Objects;
+import ch.benediktkoeppel.code.droidplane.MainActivity;
+import ch.benediktkoeppel.code.droidplane.MainApplication;
+import ch.benediktkoeppel.code.droidplane.R;
+import ch.benediktkoeppel.code.droidplane.helper.AndroidHelper;
+import ch.benediktkoeppel.code.droidplane.model.Mindmap;
+import ch.benediktkoeppel.code.droidplane.model.MindmapNode;
+
public class HorizontalMindmapView extends HorizontalScrollView implements OnTouchListener, OnItemClickListener {
/**
@@ -56,26 +64,32 @@ public class HorizontalMindmapView extends HorizontalScrollView implements OnTou
*/
private final Map listViewToNodeColumn = new HashMap<>();
- private final Mindmap mindmap;
+ private Mindmap mindmap;
private final MainActivity mainActivity;
-
+
+ /**
+ * The deepest selected mindmap node
+ */
+ private MindmapNode deepestSelectedMindmapNode;
+
+
// Search state
private String lastSearchString;
private List searchResultNodes = List.of();
private int currentSearchResultIndex;
-
+
/**
* Setting up a HorizontalMindmapView. We initialize the nodeColumns, define the layout parameters for the
* HorizontalScrollView and create the LinearLayout view inside the HorizontalScrollView.
*
* @param mainActivity the Application Context
*/
- public HorizontalMindmapView(Mindmap mindmap, MainActivity mainActivity) {
+ public HorizontalMindmapView(MainActivity mainActivity) {
super(mainActivity);
- this.mindmap = mindmap;
+ // TODO: why does the view need access to the mainActivity?
this.mainActivity = mainActivity;
// list where all columns are stored
@@ -105,13 +119,26 @@ public HorizontalMindmapView(Mindmap mindmap, MainActivity mainActivity) {
// fix the widths of all columns
resizeAllColumns(getContext());
+ }
+
+ public void setMindmap(Mindmap mindmap) {
+ this.mindmap = mindmap;
+ }
+
+ // TODO: comment missing
+ public void onRootNodeLoaded() {
+
// expand the selected node chain
- downTo(getContext(), mindmap.getDeepestSelectedMindmapNode(), true);
+ downTo(getContext(), this.getDeepestSelectedMindmapNode(), true);
// and then scroll to the right
scrollToRight();
}
+ private MindmapNode getDeepestSelectedMindmapNode() {
+ return this.deepestSelectedMindmapNode;
+ }
+
/**
* Add a new NodeColumn to the HorizontalMindmapView
*
@@ -251,7 +278,15 @@ private String getTitleOfRightmostParent() {
if (!nodeColumns.isEmpty()) {
MindmapNode parent = nodeColumns.get(nodeColumns.size() - 1).getParentNode();
- return parent.getText();
+ String text = parent.getText();
+ if (text != null && !text.isEmpty()) {
+ return text;
+ } else if (parent.getRichTextContents() != null && !parent.getRichTextContents().isEmpty()) {
+ String richTextContent = parent.getRichTextContents().get(0);
+ return Html.fromHtml(richTextContent).toString();
+ } else {
+ return "";
+ }
}
@@ -351,8 +386,18 @@ private void up(boolean force) {
private void down(Context context, MindmapNode node) {
// add a new column for this node and add it to the HorizontalMindmapView
- NodeColumn nodeColumn = new NodeColumn(getContext(), node);
- addColumn(nodeColumn);
+ NodeColumn nodeColumn;
+ synchronized (node) {
+ if (node.getParentNode() != null) {
+ synchronized (node.getParentNode()) {
+ nodeColumn = new NodeColumn(getContext(), node);
+ addColumn(nodeColumn);
+ }
+ } else {
+ nodeColumn = new NodeColumn(getContext(), node);
+ addColumn(nodeColumn);
+ }
+ }
// keep track of which list view belongs to which node column. This is necessary because onItemClick will get a
// ListView (the one that was clicked), and we need to know which NodeColumn this is.
@@ -372,7 +417,7 @@ private void down(Context context, MindmapNode node) {
node.setSelected(true);
// keep track in the mind map which node is currently selected
- mindmap.setDeepestSelectedMindmapNode(node);
+ this.setDeepestSelectedMindmapNode(node);
}
@@ -389,7 +434,7 @@ public void downTo(Context context, MindmapNode node, boolean openLast) {
// go upwards from the target node, and keep track of each node leading down to the target node
List nodeHierarchy = new ArrayList<>();
MindmapNode tmpNode = node;
- while (tmpNode.getParentNode() != null) {
+ while (tmpNode.getParentNode() != null) { // TODO: this gives a NPE when rotating the device
nodeHierarchy.add(tmpNode);
tmpNode = tmpNode.getParentNode();
}
@@ -423,26 +468,31 @@ private void scrollTo(MindmapNode node) {
*/
public void setApplicationTitle(Context context) {
+ // TODO: this needs to update when richtext content is loaded
+
// get the title of the parent of the rightmost column (i.e. the
// selected node in the 2nd-rightmost column)
// set the application title to this nodeTitle. If the nodeTitle is
// empty, we set the default Application title
String nodeTitle = getTitleOfRightmostParent();
Log.d(MainApplication.TAG, "nodeTitle = " + nodeTitle);
- if (nodeTitle.equals("")) {
+ if (nodeTitle == null || nodeTitle.equals("")) {
Log.d(MainApplication.TAG, "Setting application title to default string: " +
getResources().getString(R.string.app_name));
AndroidHelper.getActivity(context, Activity.class).setTitle(R.string.app_name);
+
} else {
Log.d(MainApplication.TAG, "Setting application title to node name: " + nodeTitle);
AndroidHelper.getActivity(context, Activity.class).setTitle(nodeTitle);
+ // TODO: java.lang.NullPointerException: Attempt to invoke virtual method 'void android.app.Activity.setTitle(java.lang.CharSequence)' on a null object reference
}
}
/**
* Enables the Home button in the application if we have enough columns, i.e. if "Up" will remove a column.
*/
- void enableHomeButtonIfEnoughColumns(Context context) {
+ // TODO: the view should not do this
+ public void enableHomeButtonIfEnoughColumns(Context context) {
// if we only have one column (i.e. this is the root node), then we
// disable the home button
int numberOfColumns = getNumberOfColumns();
@@ -492,7 +542,7 @@ else if (clickedNode.getMindmapNode().getLink() != null) {
}
// if the clicked node has a rich text content (and is a leaf), open the rich text
- else if (clickedNode.getMindmapNode().getRichTextContent() != null) {
+ else if (clickedNode.getMindmapNode().getRichTextContents() != null && !clickedNode.getMindmapNode().getRichTextContents().isEmpty()) {
clickedNode.openRichText(mainActivity);
}
@@ -628,7 +678,7 @@ private int getVisiblePixelOfLeftmostColumn() {
}
/** Shows a dialog to input the search string and fires the search. */
- void startSearch() {
+ public void startSearch() {
AlertDialog.Builder alert = new AlertDialog.Builder(getContext());
alert.setTitle("Search");
@@ -640,11 +690,11 @@ void startSearch() {
alert.setPositiveButton("Search", (dialog, which) -> search(input.getText().toString()));
alert.create().show();
}
-
+
/** Performs the search, stores the result, and selects the first matching node. */
private void search(String searchString) {
lastSearchString = searchString;
- var searchRoot = nodeColumns.get(nodeColumns.size() - 1).getParentNode();
+ MindmapNode searchRoot = nodeColumns.get(nodeColumns.size() - 1).getParentNode();
searchResultNodes = searchRoot.search(searchString);
currentSearchResultIndex = 0;
showCurrentSearchResult();
@@ -663,7 +713,7 @@ private void showCurrentSearchResult() {
}
/** Selects the next search result node. */
- void searchNext() {
+ public void searchNext() {
if (currentSearchResultIndex < searchResultNodes.size() - 1) {
currentSearchResultIndex++;
showCurrentSearchResult();
@@ -671,14 +721,21 @@ void searchNext() {
}
/** Selects the previous search result node. */
- void searchPrevious() {
+ public void searchPrevious() {
if (currentSearchResultIndex > 0) {
currentSearchResultIndex--;
showCurrentSearchResult();
}
}
-
+ public void setDeepestSelectedMindmapNode(MindmapNode deepestSelectedMindmapNode) {
+ this.deepestSelectedMindmapNode = deepestSelectedMindmapNode;
+ }
+
+ public void notifyNodeContentChanged(Context context) {
+ setApplicationTitle(context);
+ }
+
/**
* The HorizontalMindmapViewGestureDetector should detect the onFling event. However, it never receives the
* onDown event, so when it gets the onFling the event1 is empty, and we can't detect the fling properly.
diff --git a/app/src/main/java/ch/benediktkoeppel/code/droidplane/view/MindmapNodeAdapter.java b/app/src/main/java/ch/benediktkoeppel/code/droidplane/view/MindmapNodeAdapter.java
new file mode 100644
index 0000000..42d4b09
--- /dev/null
+++ b/app/src/main/java/ch/benediktkoeppel/code/droidplane/view/MindmapNodeAdapter.java
@@ -0,0 +1,51 @@
+package ch.benediktkoeppel.code.droidplane.view;
+
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+
+import androidx.annotation.NonNull;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * The MindmapNodeAdapter is the data provider for the NodeColumn (respectively its ListView).
+ */
+class MindmapNodeAdapter extends ArrayAdapter {
+
+ private final List mindmapNodeLayouts;
+
+ public MindmapNodeAdapter(Context context, int textViewResourceId, ArrayList mindmapNodeLayouts) {
+
+ super(context, textViewResourceId, mindmapNodeLayouts);
+ this.mindmapNodeLayouts = mindmapNodeLayouts;
+ }
+
+ /* (non-Javadoc)
+ * getView is responsible to return a view for each individual element in the ListView
+ * @param int position: the position in the mindmapNodes array, for which we need to generate a view
+ * @param View convertView: the view we should recycle
+ * @param ViewGroup parent: not sure, is this the NodeColumn for which the Adapter is generating views?
+ * @see android.widget.ArrayAdapter#getView(int, android.view.View, android.view.ViewGroup)
+ */
+ @NonNull
+ @SuppressLint("InlinedApi")
+ @Override
+ public View getView(int position, View convertView, @NonNull ViewGroup parent) {
+
+ // when convertView != null, we should take the convertView and update it appropriately. Android is
+ // optimizing the performance and thus recycling GUI elements. However, we don't want to recycle anything,
+ // because these are genuine Mindmap nodes. Recycling the view here would show one node twice in the tree,
+ // while leaving out the actual node we should display.
+
+ MindmapNodeLayout view = mindmapNodeLayouts.get(position);
+
+ // tell the node to refresh it's view
+ view.refreshView();
+
+ return view;
+ }
+}
diff --git a/app/src/main/java/ch/benediktkoeppel/code/droidplane/MindmapNodeLayout.java b/app/src/main/java/ch/benediktkoeppel/code/droidplane/view/MindmapNodeLayout.java
similarity index 94%
rename from app/src/main/java/ch/benediktkoeppel/code/droidplane/MindmapNodeLayout.java
rename to app/src/main/java/ch/benediktkoeppel/code/droidplane/view/MindmapNodeLayout.java
index db16858..a8fa6ab 100644
--- a/app/src/main/java/ch/benediktkoeppel/code/droidplane/MindmapNodeLayout.java
+++ b/app/src/main/java/ch/benediktkoeppel/code/droidplane/view/MindmapNodeLayout.java
@@ -1,4 +1,4 @@
-package ch.benediktkoeppel.code.droidplane;
+package ch.benediktkoeppel.code.droidplane.view;
import java.io.File;
import java.util.ArrayList;
@@ -26,6 +26,16 @@
import android.widget.TextView;
import android.widget.Toast;
+import java.io.File;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+
+import ch.benediktkoeppel.code.droidplane.MainActivity;
+import ch.benediktkoeppel.code.droidplane.MainApplication;
+import ch.benediktkoeppel.code.droidplane.R;
+import ch.benediktkoeppel.code.droidplane.model.MindmapNode;
+
/**
* A MindmapNodeLayout is the UI (layout) part of a MindmapNode.
@@ -69,6 +79,7 @@ public MindmapNodeLayout(Context context, MindmapNode mindmapNode) {
super(context);
this.mindmapNode = mindmapNode;
+ mindmapNode.subscribeNodeStyleChanged(this);
// extract icons
Resources resources = context.getResources();
@@ -87,7 +98,7 @@ public MindmapNodeLayout(Context context, MindmapNode mindmapNode) {
}
// set the rich text icon if it has
- if (mindmapNode.getRichTextContent() != null) {
+ if (mindmapNode.getRichTextContents() != null && !mindmapNode.getRichTextContents().isEmpty()) {
iconResourceIds.add(0, resources.getIdentifier("@drawable/richtext", "id", packageName));
}
@@ -212,7 +223,7 @@ public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuIn
}
// add menu to show rich text, if the node has
- if (mindmapNode.getRichTextContent() != null) {
+ if (mindmapNode.getRichTextContents() != null && !mindmapNode.getRichTextContents().isEmpty()) {
menu.add(CONTEXT_MENU_NORMAL_GROUP_ID, R.id.openrichtext, 0, R.string.openrichtext);
}
@@ -342,10 +353,14 @@ public MindmapNode getMindmapNode() {
public void openRichText(MainActivity mainActivity) {
- String richTextContent = getMindmapNode().getRichTextContent();
+ String richTextContent = getMindmapNode().getRichTextContents().get(0);
Intent intent = new Intent(mainActivity, RichTextViewActivity.class);
intent.putExtra("richTextContent", richTextContent);
mainActivity.startActivity(intent);
}
+
+ public void notifyNodeStyleChanged() {
+ this.refreshView();
+ }
}
diff --git a/app/src/main/java/ch/benediktkoeppel/code/droidplane/NodeColumn.java b/app/src/main/java/ch/benediktkoeppel/code/droidplane/view/NodeColumn.java
similarity index 82%
rename from app/src/main/java/ch/benediktkoeppel/code/droidplane/NodeColumn.java
rename to app/src/main/java/ch/benediktkoeppel/code/droidplane/view/NodeColumn.java
index e1a6a3c..ee1ba44 100644
--- a/app/src/main/java/ch/benediktkoeppel/code/droidplane/NodeColumn.java
+++ b/app/src/main/java/ch/benediktkoeppel/code/droidplane/view/NodeColumn.java
@@ -1,4 +1,4 @@
-package ch.benediktkoeppel.code.droidplane;
+package ch.benediktkoeppel.code.droidplane.view;
import java.util.ArrayList;
import java.util.List;
@@ -17,11 +17,14 @@
import android.view.WindowManager;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
-import android.widget.ArrayAdapter;
import android.widget.LinearLayout;
import android.widget.ListView;
import androidx.annotation.NonNull;
+import ch.benediktkoeppel.code.droidplane.MainApplication;
+import ch.benediktkoeppel.code.droidplane.R;
+import ch.benediktkoeppel.code.droidplane.model.MindmapNode;
+
/**
* A column of MindmapNodes, i.e. one level in the mind map. It extends LinearLayout, and then embeds a ListView.
* This is because we want to have a fine border around the ListView and we can only achieve this by having it
@@ -33,6 +36,7 @@ public class NodeColumn extends LinearLayout implements OnCreateContextMenuListe
* The parent node (i.e. the node that is parent to everything we display in this column)
*/
private final MindmapNode parent;
+ private final Context context;
/**
* The list of all MindmapNodeLayouts which we display in this column
@@ -60,6 +64,7 @@ public NodeColumn(Context context) {
super(context);
parent = null;
+ this.context = context;
if (!isInEditMode()) {
throw new IllegalArgumentException(
"The constructor public NodeColumn(Context context) may only be called by graphical layout tools," +
@@ -79,17 +84,19 @@ public NodeColumn(Context context, MindmapNode parent) {
super(context);
- // extract all elements from the parent node, create layouts, and add them to the mindmapNodes list
+ this.context = context;
+
+ this.parent = parent;
+
+ parent.subscribe(this);
+
+ // create list items for each child node
mindmapNodeLayouts = new ArrayList<>();
- List mindmapNodes = parent.getChildNodes();
+ List mindmapNodes = parent.getChildMindmapNodes();
for (MindmapNode mindmapNode : mindmapNodes) {
mindmapNodeLayouts.add(new MindmapNodeLayout(context, mindmapNode));
}
- // store the parent node
- this.parent = parent;
-
-
// define the layout of this LinearView
int linearViewHeight = LayoutParams.MATCH_PARENT;
int linearViewWidth = getOptimalColumnWidth(context);
@@ -124,6 +131,15 @@ public NodeColumn(Context context, MindmapNode parent) {
this.addView(listView);
}
+ public void notifyNewMindmapNode(MindmapNode mindmapNode) {
+
+ mindmapNodeLayouts.add(new MindmapNodeLayout(context, mindmapNode));
+ adapter.notifyDataSetChanged();
+
+ }
+
+ // TODO we need a new notifier, if the node itself has updated (if text was updated, or icon was updated)
+
/**
* Sets the width of this column to columnWidth
*
@@ -286,41 +302,3 @@ public ListView getListView() {
}
-/**
- * The MindmapNodeAdapter is the data provider for the NodeColumn (respectively its ListView).
- */
-class MindmapNodeAdapter extends ArrayAdapter {
-
- private final List mindmapNodeLayouts;
-
- public MindmapNodeAdapter(Context context, int textViewResourceId, ArrayList mindmapNodeLayouts) {
-
- super(context, textViewResourceId, mindmapNodeLayouts);
- this.mindmapNodeLayouts = mindmapNodeLayouts;
- }
-
- /* (non-Javadoc)
- * getView is responsible to return a view for each individual element in the ListView
- * @param int position: the position in the mindmapNodes array, for which we need to generate a view
- * @param View convertView: the view we should recycle
- * @param ViewGroup parent: not sure, is this the NodeColumn for which the Adapter is generating views?
- * @see android.widget.ArrayAdapter#getView(int, android.view.View, android.view.ViewGroup)
- */
- @NonNull
- @SuppressLint("InlinedApi")
- @Override
- public View getView(int position, View convertView, @NonNull ViewGroup parent) {
-
- // when convertView != null, we should take the convertView and update it appropriately. Android is
- // optimizing the performance and thus recycling GUI elements. However, we don't want to recycle anything,
- // because these are genuine Mindmap nodes. Recycling the view here would show one node twice in the tree,
- // while leaving out the actual node we should display.
-
- MindmapNodeLayout view = mindmapNodeLayouts.get(position);
-
- // tell the node to refresh it's view
- view.refreshView();
-
- return view;
- }
-}
diff --git a/app/src/main/java/ch/benediktkoeppel/code/droidplane/RichTextViewActivity.java b/app/src/main/java/ch/benediktkoeppel/code/droidplane/view/RichTextViewActivity.java
similarity index 94%
rename from app/src/main/java/ch/benediktkoeppel/code/droidplane/RichTextViewActivity.java
rename to app/src/main/java/ch/benediktkoeppel/code/droidplane/view/RichTextViewActivity.java
index ce2d6bd..c55ae02 100644
--- a/app/src/main/java/ch/benediktkoeppel/code/droidplane/RichTextViewActivity.java
+++ b/app/src/main/java/ch/benediktkoeppel/code/droidplane/view/RichTextViewActivity.java
@@ -1,10 +1,12 @@
-package ch.benediktkoeppel.code.droidplane;
+package ch.benediktkoeppel.code.droidplane.view;
import android.app.Activity;
import android.os.Bundle;
import android.util.Base64;
import android.webkit.WebView;
+import ch.benediktkoeppel.code.droidplane.R;
+
public class RichTextViewActivity extends Activity {
@Override
diff --git a/app/src/main/res/layout/action_bar_progress.xml b/app/src/main/res/layout/action_bar_progress.xml
new file mode 100644
index 0000000..60ec1d3
--- /dev/null
+++ b/app/src/main/res/layout/action_bar_progress.xml
@@ -0,0 +1,13 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_rich_text_view.xml b/app/src/main/res/layout/activity_rich_text_view.xml
index d82ff84..d11024a 100644
--- a/app/src/main/res/layout/activity_rich_text_view.xml
+++ b/app/src/main/res/layout/activity_rich_text_view.xml
@@ -4,7 +4,7 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
- tools:context=".RichTextViewActivity">
+ tools:context=".view.RichTextViewActivity">
-