Restore File tree state when application is resumed (closes #150)

This commit is contained in:
Akash Yadav
2022-04-16 10:45:43 +05:30
parent 25272b3a93
commit 34e084107e
4 changed files with 216 additions and 144 deletions
@@ -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);
@@ -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 <https://www.gnu.org/licenses/>.
*
**************************************************************************************/
*/
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<String> 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);
}
});
}
@@ -29,6 +29,10 @@ public class TaskExecutor {
private final Logger LOG = Logger.newInstance("TaskExecutor");
public static <R> void execAsync(Callable<R> callable, Callback<R> callback) {
new TaskExecutor().executeAsync(callable, callback);
}
public <R> void executeAsync(Callable<R> callable, Callback<R> callback) {
CompletableFuture.supplyAsync(
() -> {
@@ -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<TreeNode> children = mRoot.getChildren();
for (int i = 0; i < children.size(); i++) {
TreeNode node = children.get(i);
toggleSelectionMode(node, selectionModeEnabled);
}
}
public <E> List<E> getSelectedValues(Class<E> clazz) {
List<E> result = new ArrayList<>();
List<TreeNode> 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<TreeNode> 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<TreeNode> 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