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