diff --git a/app/src/main/java/com/itsaky/androidide/EditorActivity.java b/app/src/main/java/com/itsaky/androidide/EditorActivity.java
index c45479b54..7ddc3eb99 100755
--- a/app/src/main/java/com/itsaky/androidide/EditorActivity.java
+++ b/app/src/main/java/com/itsaky/androidide/EditorActivity.java
@@ -354,6 +354,10 @@ public class EditorActivity extends StudioActivity
super.onPause();
dispatchOnPauseToEditors();
EditorActivityActions.clear();
+
+ if (mFileTreeFragment != null) {
+ mFileTreeFragment.saveTreeState();
+ }
}
@Override
@@ -365,7 +369,10 @@ public class EditorActivity extends StudioActivity
try {
checkForCompilerModule();
dispatchOnResumeToEditors();
- mFileTreeFragment.listProjectFiles();
+
+ if (mFileTreeFragment != null) {
+ mFileTreeFragment.listProjectFiles();
+ }
} catch (Throwable th) {
LOG.error("Failed to update files list", th);
getApp().toast(R.string.msg_failed_list_files, Toaster.Type.ERROR);
diff --git a/app/src/main/java/com/itsaky/androidide/fragments/FileTreeFragment.java b/app/src/main/java/com/itsaky/androidide/fragments/FileTreeFragment.java
index 019f8d430..865ffb5c9 100755
--- a/app/src/main/java/com/itsaky/androidide/fragments/FileTreeFragment.java
+++ b/app/src/main/java/com/itsaky/androidide/fragments/FileTreeFragment.java
@@ -1,8 +1,6 @@
-/************************************************************************************
+/*
* This file is part of AndroidIDE.
*
- *
- *
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
@@ -16,11 +14,14 @@
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see .
*
- **************************************************************************************/
+ */
package com.itsaky.androidide.fragments;
+import static com.unnamed.b.atv.view.AndroidTreeView.NODES_PATH_SEPARATOR;
+
import android.content.Context;
import android.os.Bundle;
+import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@@ -43,6 +44,7 @@ import com.itsaky.androidide.project.AndroidProject;
import com.itsaky.androidide.tasks.TaskExecutor;
import com.itsaky.androidide.tasks.callables.FileTreeCallable;
import com.itsaky.androidide.utils.Environment;
+import com.itsaky.androidide.utils.Logger;
import com.itsaky.androidide.views.editor.CodeEditorView;
import com.itsaky.toaster.Toaster;
import com.unnamed.b.atv.model.TreeNode;
@@ -50,15 +52,20 @@ import com.unnamed.b.atv.view.AndroidTreeView;
import java.io.File;
import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
public class FileTreeFragment extends BottomSheetDialogFragment
implements TreeNode.TreeNodeClickListener, TreeNode.TreeNodeLongClickListener {
+ private static final String KEY_STORED_TREE_STATE = "fileTree_state";
+ private static final Logger LOG = Logger.newInstance("FileTreeFragment");
private LayoutEditorFileTreeBinding binding;
private AndroidTreeView mFileTreeView;
private FileActionListener mFileActionListener;
private AndroidProject mProject;
private TreeNode mRoot;
+ private String mTreeState;
public FileTreeFragment() {}
@@ -71,6 +78,52 @@ public class FileTreeFragment extends BottomSheetDialogFragment
return frag;
}
+ public void saveTreeState() {
+ if (mFileTreeView != null) {
+ mTreeState = mFileTreeView.getSaveState();
+ } else {
+ LOG.error("Unable to save tree state. TreeView is null.");
+ mTreeState = null;
+ }
+ }
+
+ private void tryRestoreState() {
+ tryRestoreState(mTreeState);
+ }
+
+ private void tryRestoreState(String state) {
+ if (!TextUtils.isEmpty(state) && mFileTreeView != null) {
+
+ LOG.debug("Restoring tree view state:", "'" + state + "'");
+
+ mFileTreeView.collapseAll();
+ final var openNodesArray = state.split(NODES_PATH_SEPARATOR);
+ final var openNodes = new HashSet<>(Arrays.asList(openNodesArray));
+ restoreNodeState(mRoot, openNodes);
+ } else {
+ LOG.error(
+ "Unable to restore tree state",
+ "treeState=" + state,
+ "treeView=" + mFileTreeView);
+ }
+ }
+
+ private void restoreNodeState(@NonNull TreeNode root, Set openNodes) {
+ final var children = root.getChildren();
+ for (int i = 0, childrenSize = children.size(); i < childrenSize; i++) {
+ final var node = children.get(i);
+ if (openNodes.contains(node.getPath())) {
+ listNode(
+ node,
+ () -> {
+ updateChevron(node);
+ expandNode(node);
+ restoreNodeState(node, openNodes);
+ });
+ }
+ }
+ }
+
@Override
public void onAttach(@NonNull Context context) {
super.onAttach(context);
@@ -89,6 +142,10 @@ public class FileTreeFragment extends BottomSheetDialogFragment
super.onViewCreated(view, savedInstanceState);
mProject = requireArguments().getParcelable("project");
+ if (savedInstanceState != null && savedInstanceState.containsKey(KEY_STORED_TREE_STATE)) {
+ mTreeState = savedInstanceState.getString(KEY_STORED_TREE_STATE, null);
+ }
+
listProjectFiles();
}
@@ -121,41 +178,48 @@ public class FileTreeFragment extends BottomSheetDialogFragment
expandNode(node);
} else {
setLoading(node);
- listNode(node);
+ listNode(
+ node,
+ () -> {
+ updateChevron(node);
+ expandNode(node);
+ });
}
}
}
- private void listNode(@NonNull TreeNode node) {
+ @Override
+ public void onSaveInstanceState(@NonNull Bundle outState) {
+ super.onSaveInstanceState(outState);
+ saveTreeState();
+ outState.putString(KEY_STORED_TREE_STATE, mTreeState);
+ }
+
+ private void listNode(@NonNull TreeNode node, Runnable whenDone) {
+
+ if (whenDone == null) {
+ whenDone = () -> {};
+ }
+
node.getChildren().clear();
node.setExpanded(false);
- new TaskExecutor()
- .executeAsync(
- () -> {
- getNodeFromFiles(
- node.getValue()
- .listFiles(
- /* new FileTreeCallable.HiddenFilesFilter() */ ),
- node);
- TreeNode temp = node;
- while (temp.size() == 1) {
- temp = temp.childAt(0);
- if (!temp.getValue().isDirectory()) {
- break;
- }
- getNodeFromFiles(
- temp.getValue()
- .listFiles(
- /* new FileTreeCallable.HiddenFilesFilter() */ ),
- temp);
- temp.setExpanded(true);
- }
- return null;
- },
- __ -> {
- updateChevron(node);
- expandNode(node);
- });
+
+ final var finalWhenDone = whenDone;
+ TaskExecutor.execAsync(
+ () -> {
+ getNodeFromFiles(node.getValue().listFiles(), node);
+ TreeNode temp = node;
+ while (temp.size() == 1) {
+ temp = temp.childAt(0);
+ if (!temp.getValue().isDirectory()) {
+ break;
+ }
+ getNodeFromFiles(temp.getValue().listFiles(), temp);
+ temp.setExpanded(true);
+ }
+ return null;
+ },
+ __ -> finalWhenDone.run());
}
private void getNodeFromFiles(File[] files, TreeNode parent) {
@@ -246,7 +310,11 @@ public class FileTreeFragment extends BottomSheetDialogFragment
tree.setDefaultNodeClickListener(FileTreeFragment.this);
tree.setDefaultNodeLongClickListener(FileTreeFragment.this);
getScrollView().removeAllViews();
- getScrollView().addView(tree.getView());
+
+ final var view = tree.getView();
+ getScrollView().addView(view);
+
+ view.post(this::tryRestoreState);
}
});
}
diff --git a/app/src/main/java/com/itsaky/androidide/tasks/TaskExecutor.java b/app/src/main/java/com/itsaky/androidide/tasks/TaskExecutor.java
index 4443a4311..52d038f24 100755
--- a/app/src/main/java/com/itsaky/androidide/tasks/TaskExecutor.java
+++ b/app/src/main/java/com/itsaky/androidide/tasks/TaskExecutor.java
@@ -29,6 +29,10 @@ public class TaskExecutor {
private final Logger LOG = Logger.newInstance("TaskExecutor");
+ public static void execAsync(Callable callable, Callback callback) {
+ new TaskExecutor().executeAsync(callable, callback);
+ }
+
public void executeAsync(Callable callable, Callback callback) {
CompletableFuture.supplyAsync(
() -> {
diff --git a/treeview/src/main/java/com/unnamed/b/atv/view/AndroidTreeView.java b/treeview/src/main/java/com/unnamed/b/atv/view/AndroidTreeView.java
index 632a3ec71..fa9a0b5e1 100755
--- a/treeview/src/main/java/com/unnamed/b/atv/view/AndroidTreeView.java
+++ b/treeview/src/main/java/com/unnamed/b/atv/view/AndroidTreeView.java
@@ -26,10 +26,9 @@ import java.util.Set;
@SuppressWarnings("rawtypes")
public class AndroidTreeView {
- private static final String NODES_PATH_SEPARATOR = ";";
-
- protected TreeNode mRoot;
+ public static final String NODES_PATH_SEPARATOR = ";";
private final Context mContext;
+ protected TreeNode mRoot;
private boolean applyForRoot;
private int containerStyle = 0;
private int nodeViewBackground = 0;
@@ -40,10 +39,6 @@ public class AndroidTreeView {
private boolean use2dScroll = false;
private boolean enableAutoToggle = true;
- public void setRoot(TreeNode mRoot) {
- this.mRoot = mRoot;
- }
-
public AndroidTreeView(Context context, TreeNode root, @DrawableRes int nodeBackground) {
this.mRoot = root;
this.mContext = context;
@@ -51,6 +46,71 @@ public class AndroidTreeView {
this.defaultViewHolder = new SimpleViewHolder(context);
}
+ private static void expand(final View v) {
+ v.measure(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT);
+ final int targetHeight = v.getMeasuredHeight();
+
+ v.getLayoutParams().height = 0;
+ v.setVisibility(View.VISIBLE);
+ Animation a =
+ new Animation() {
+ @Override
+ protected void applyTransformation(float interpolatedTime, Transformation t) {
+ v.getLayoutParams().height =
+ interpolatedTime == 1
+ ? LinearLayout.LayoutParams.WRAP_CONTENT
+ : (int) (targetHeight * interpolatedTime);
+ v.requestLayout();
+ }
+
+ @Override
+ public boolean willChangeBounds() {
+ return true;
+ }
+ };
+
+ // 1dp/ms
+ a.setDuration(
+ (int)
+ ((targetHeight / v.getContext().getResources().getDisplayMetrics().density)
+ / 2));
+ v.startAnimation(a);
+ }
+
+ private static void collapse(final View v) {
+ final int initialHeight = v.getMeasuredHeight();
+
+ Animation a =
+ new Animation() {
+ @Override
+ protected void applyTransformation(float interpolatedTime, Transformation t) {
+ if (interpolatedTime == 1) {
+ v.setVisibility(View.GONE);
+ } else {
+ v.getLayoutParams().height =
+ initialHeight - (int) (initialHeight * interpolatedTime);
+ v.requestLayout();
+ }
+ }
+
+ @Override
+ public boolean willChangeBounds() {
+ return true;
+ }
+ };
+
+ // 1dp/ms
+ a.setDuration(
+ (int)
+ ((initialHeight / v.getContext().getResources().getDisplayMetrics().density)
+ / 2));
+ v.startAnimation(a);
+ }
+
+ public void setRoot(TreeNode mRoot) {
+ this.mRoot = mRoot;
+ }
+
public void setDefaultContainerStyle(int style) {
setDefaultContainerStyle(style, false);
}
@@ -229,6 +289,9 @@ public class AndroidTreeView {
}
}
+ // ------------------------------------------------------------
+ // Selection methods
+
public void expandNode(final TreeNode node, boolean includeSubnodes) {
node.setExpanded(true);
final TreeNode.BaseNodeViewHolder parentViewHolder = getViewHolderForNode(node);
@@ -261,54 +324,31 @@ public class AndroidTreeView {
}
nodeView.setOnClickListener(
- new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- if (n.getClickListener() != null) {
- n.getClickListener().onClick(n, n.getValue());
- } else if (nodeClickListener != null) {
- nodeClickListener.onClick(n, n.getValue());
- }
- if (enableAutoToggle) {
- toggleNode(n);
- }
+ v -> {
+ if (n.getClickListener() != null) {
+ n.getClickListener().onClick(n, n.getValue());
+ } else if (nodeClickListener != null) {
+ nodeClickListener.onClick(n, n.getValue());
+ }
+ if (enableAutoToggle) {
+ toggleNode(n);
}
});
nodeView.setOnLongClickListener(
- new View.OnLongClickListener() {
- @Override
- public boolean onLongClick(View view) {
- if (n.getLongClickListener() != null) {
- return n.getLongClickListener().onLongClick(n, n.getValue());
- } else if (nodeLongClickListener != null) {
- return nodeLongClickListener.onLongClick(n, n.getValue());
- }
- if (enableAutoToggle) {
- toggleNode(n);
- }
- return false;
+ view -> {
+ if (n.getLongClickListener() != null) {
+ return n.getLongClickListener().onLongClick(n, n.getValue());
+ } else if (nodeLongClickListener != null) {
+ return nodeLongClickListener.onLongClick(n, n.getValue());
}
+ if (enableAutoToggle) {
+ toggleNode(n);
+ }
+ return false;
});
}
- // ------------------------------------------------------------
- // Selection methods
-
- public void setSelectionModeEnabled(boolean selectionModeEnabled) {
- if (!selectionModeEnabled) {
- // TODO fix double iteration over tree
- deselectAll();
- }
- mSelectionModeEnabled = selectionModeEnabled;
-
- List children = mRoot.getChildren();
- for (int i = 0; i < children.size(); i++) {
- TreeNode node = children.get(i);
- toggleSelectionMode(node, selectionModeEnabled);
- }
- }
-
public List getSelectedValues(Class clazz) {
List result = new ArrayList<>();
List selected = getSelected();
@@ -325,6 +365,20 @@ public class AndroidTreeView {
return mSelectionModeEnabled;
}
+ public void setSelectionModeEnabled(boolean selectionModeEnabled) {
+ if (!selectionModeEnabled) {
+ // TODO fix double iteration over tree
+ deselectAll();
+ }
+ mSelectionModeEnabled = selectionModeEnabled;
+
+ List children = mRoot.getChildren();
+ for (int i = 0; i < children.size(); i++) {
+ TreeNode node = children.get(i);
+ toggleSelectionMode(node, selectionModeEnabled);
+ }
+ }
+
private void toggleSelectionMode(TreeNode parent, boolean mSelectionModeEnabled) {
toogleSelectionForNode(parent, mSelectionModeEnabled);
if (parent.isExpanded()) {
@@ -387,7 +441,7 @@ public class AndroidTreeView {
private void selectNode(TreeNode parent, boolean selected, boolean skipCollapsed) {
parent.setSelected(selected);
toogleSelectionForNode(parent, true);
- boolean toContinue = skipCollapsed ? parent.isExpanded() : true;
+ boolean toContinue = !skipCollapsed || parent.isExpanded();
if (toContinue) {
List children = parent.getChildren();
for (int i = 0; i < children.size(); i++) {
@@ -418,67 +472,6 @@ public class AndroidTreeView {
return viewHolder;
}
- private static void expand(final View v) {
- v.measure(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT);
- final int targetHeight = v.getMeasuredHeight();
-
- v.getLayoutParams().height = 0;
- v.setVisibility(View.VISIBLE);
- Animation a =
- new Animation() {
- @Override
- protected void applyTransformation(float interpolatedTime, Transformation t) {
- v.getLayoutParams().height =
- interpolatedTime == 1
- ? LinearLayout.LayoutParams.WRAP_CONTENT
- : (int) (targetHeight * interpolatedTime);
- v.requestLayout();
- }
-
- @Override
- public boolean willChangeBounds() {
- return true;
- }
- };
-
- // 1dp/ms
- a.setDuration(
- (int)
- ((targetHeight / v.getContext().getResources().getDisplayMetrics().density)
- / 2));
- v.startAnimation(a);
- }
-
- private static void collapse(final View v) {
- final int initialHeight = v.getMeasuredHeight();
-
- Animation a =
- new Animation() {
- @Override
- protected void applyTransformation(float interpolatedTime, Transformation t) {
- if (interpolatedTime == 1) {
- v.setVisibility(View.GONE);
- } else {
- v.getLayoutParams().height =
- initialHeight - (int) (initialHeight * interpolatedTime);
- v.requestLayout();
- }
- }
-
- @Override
- public boolean willChangeBounds() {
- return true;
- }
- };
-
- // 1dp/ms
- a.setDuration(
- (int)
- ((initialHeight / v.getContext().getResources().getDisplayMetrics().density)
- / 2));
- v.startAnimation(a);
- }
-
// -----------------------------------------------------------------
// Add / Remove