diff --git a/catroid/src/main/java/org/catrobat/catroid/content/Script.java b/catroid/src/main/java/org/catrobat/catroid/content/Script.java index 1b8173e12d0..1626a37029f 100644 --- a/catroid/src/main/java/org/catrobat/catroid/content/Script.java +++ b/catroid/src/main/java/org/catrobat/catroid/content/Script.java @@ -45,6 +45,7 @@ public abstract class Script implements Serializable, Cloneable { protected List brickList = new ArrayList<>(); protected boolean commentedOut = false; + private transient boolean collapsed = false; @XStreamAsAttribute protected float posX; @@ -71,6 +72,7 @@ public Script clone() throws CloneNotSupportedException { } clone.commentedOut = commentedOut; + clone.collapsed = false; clone.scriptBrick = null; clone.posX = posX; clone.posY = posY; @@ -109,6 +111,14 @@ public void setCommentedOut(boolean commentedOut) { } } + public boolean isCollapsed() { + return collapsed; + } + + public void setCollapsed(boolean collapsed) { + this.collapsed = collapsed; + } + public void setParents() { ScriptBrick scriptBrick = getScriptBrick(); scriptBrick.setParent(null); @@ -145,8 +155,14 @@ public void addBrick(int position, Brick brick) { public void addToFlatList(List bricks) { bricks.add(getScriptBrick()); - for (Brick brick : brickList) { - brick.addToFlatList(bricks); + if (!collapsed) { + for (Brick brick : brickList) { + brick.addToFlatList(bricks); + } + } else { + for (int i = 0; i < Math.min(3, brickList.size()); i++) { + bricks.add(brickList.get(i)); + } } } diff --git a/catroid/src/main/java/org/catrobat/catroid/content/bricks/BrickBaseType.java b/catroid/src/main/java/org/catrobat/catroid/content/bricks/BrickBaseType.java index 0a1e8cf4b36..06e211c540f 100644 --- a/catroid/src/main/java/org/catrobat/catroid/content/bricks/BrickBaseType.java +++ b/catroid/src/main/java/org/catrobat/catroid/content/bricks/BrickBaseType.java @@ -54,6 +54,8 @@ public abstract class BrickBaseType implements Brick { protected boolean commentedOut; + protected transient boolean collapsed; + protected UUID brickId = UUID.randomUUID(); @Override @@ -66,6 +68,14 @@ public void setCommentedOut(boolean commentedOut) { this.commentedOut = commentedOut; } + public boolean isCollapsed() { + return collapsed; + } + + public void setCollapsed(boolean collapsed) { + this.collapsed = collapsed; + } + @Nullable @Override public CheckBox getCheckBox() { @@ -79,6 +89,7 @@ public Brick clone() throws CloneNotSupportedException { clone.checkbox = null; clone.parent = null; clone.commentedOut = commentedOut; + clone.collapsed = false; clone.brickId = UUID.randomUUID(); return clone; } diff --git a/catroid/src/main/java/org/catrobat/catroid/ui/SpriteActivity.java b/catroid/src/main/java/org/catrobat/catroid/ui/SpriteActivity.java index 865c2ab9e37..4a6a69e70ee 100644 --- a/catroid/src/main/java/org/catrobat/catroid/ui/SpriteActivity.java +++ b/catroid/src/main/java/org/catrobat/catroid/ui/SpriteActivity.java @@ -238,6 +238,7 @@ public void setUndoMenuItemVisibility(boolean isVisible) { public boolean onPrepareOptionsMenu(Menu menu) { if (getCurrentFragment() instanceof ScriptFragment) { menu.findItem(R.id.comment_in_out).setVisible(true); + menu.findItem(R.id.collapse_expand).setVisible(true); showUndo(isUndoMenuItemVisible); } else if (getCurrentFragment() instanceof LookListFragment) { showUndo(isUndoMenuItemVisible); diff --git a/catroid/src/main/java/org/catrobat/catroid/ui/UiUtils.java b/catroid/src/main/java/org/catrobat/catroid/ui/UiUtils.java index 4c03a257912..fb47ad8f7df 100644 --- a/catroid/src/main/java/org/catrobat/catroid/ui/UiUtils.java +++ b/catroid/src/main/java/org/catrobat/catroid/ui/UiUtils.java @@ -101,6 +101,12 @@ public static AppCompatActivity getActivityFromView(View view) { return R.drawable.ic_library_add_small; case R.string.menu_rate_us: return R.drawable.ic_star_rate; + case R.string.brick_context_dialog_collapse_script: + case R.string.brick_context_dialog_collapse_brick: + return R.drawable.ic_collapse; + case R.string.brick_context_dialog_expand_script: + case R.string.brick_context_dialog_expand_brick: + return R.drawable.ic_expand; default: return R.drawable.ic_placeholder; } diff --git a/catroid/src/main/java/org/catrobat/catroid/ui/recyclerview/adapter/BrickAdapter.kt b/catroid/src/main/java/org/catrobat/catroid/ui/recyclerview/adapter/BrickAdapter.kt index 52c737810a6..b1eff9b12e2 100644 --- a/catroid/src/main/java/org/catrobat/catroid/ui/recyclerview/adapter/BrickAdapter.kt +++ b/catroid/src/main/java/org/catrobat/catroid/ui/recyclerview/adapter/BrickAdapter.kt @@ -22,18 +22,27 @@ */ package org.catrobat.catroid.ui.recyclerview.adapter +import android.graphics.Color import android.graphics.ColorMatrix import android.graphics.ColorMatrixColorFilter import android.graphics.drawable.Drawable +import android.view.Gravity import android.view.View import android.view.ViewGroup +import android.widget.AbsListView import android.widget.AdapterView import android.widget.AdapterView.OnItemLongClickListener import android.widget.BaseAdapter +import android.widget.FrameLayout +import android.widget.ImageView +import android.widget.LinearLayout import androidx.annotation.IntDef +import androidx.core.content.ContextCompat +import org.catrobat.catroid.R import org.catrobat.catroid.content.Script import org.catrobat.catroid.content.Sprite import org.catrobat.catroid.content.bricks.Brick +import org.catrobat.catroid.content.bricks.BrickBaseType import org.catrobat.catroid.content.bricks.CompositeBrick import org.catrobat.catroid.content.bricks.EmptyEventBrick import org.catrobat.catroid.content.bricks.EndBrick @@ -53,7 +62,7 @@ class BrickAdapter(private val sprite: Sprite) : AdapterView.OnItemClickListener, OnItemLongClickListener { @kotlin.annotation.Retention(AnnotationRetention.SOURCE) - @IntDef(NONE, ALL, SCRIPTS_ONLY, CONNECTED_ONLY) + @IntDef(NONE, ALL, SCRIPTS_ONLY, CONNECTED_ONLY, COLLAPSE_EXPAND) internal annotation class CheckBoxMode @CheckBoxMode @@ -70,6 +79,7 @@ class BrickAdapter(private val sprite: Sprite) : private var selectionListener: SelectionListener? = null val items: MutableList = ArrayList() + private val squashedBricks = mutableSetOf() init { updateItems(sprite) @@ -81,6 +91,9 @@ class BrickAdapter(private val sprite: Sprite) : const val ALL = 1 const val SCRIPTS_ONLY = 2 const val CONNECTED_ONLY = 3 + const val COLLAPSE_EXPAND = 4 + const val COLLAPSE_INDICATOR_TAG = "collapse_indicator" + const val SQUASHED_HEIGHT_DP = 16 @JvmStatic fun colorAsCommentedOut(background: Drawable) { @@ -117,9 +130,70 @@ class BrickAdapter(private val sprite: Sprite) : script.setParents() script.addToFlatList(items) } + filterCollapsedCompositeBricks() notifyDataSetChanged() } + private fun filterCollapsedCompositeBricks() { + val filteredItems = ArrayList() + squashedBricks.clear() + + var skipDepth = 0 + var skipEndBricks = 0 + var squashedRemaining = 0 + + for (item in items) { + if (item is ScriptBrick) { + filteredItems.add(item) + squashedRemaining = if (item.script.isCollapsed) 3 else 0 + skipDepth = 0 + continue + } + + if (squashedRemaining > 0 && skipDepth == 0) { + filteredItems.add(item) + squashedBricks.add(item) + squashedRemaining-- + continue + } + + if (skipDepth > 0) { + if (item is CompositeBrick) { + skipDepth++ + skipEndBricks++ + } + if (item is EndBrick) { + if (skipEndBricks > 0) { + skipEndBricks-- + } else { + skipDepth-- + if (skipDepth == 0) { + filteredItems.add(item) + continue + } + } + } + + if (squashedRemaining > 0) { + filteredItems.add(item) + squashedBricks.add(item) + squashedRemaining-- + } + continue + } + + filteredItems.add(item) + if (item is CompositeBrick && item is BrickBaseType && (item as BrickBaseType).isCollapsed) { + skipDepth = 1 + skipEndBricks = 0 + squashedRemaining = 3 + } + } + + items.clear() + items.addAll(filteredItems) + } + override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { val item = items[position] val itemView = item.getView(parent.context) @@ -150,9 +224,124 @@ class BrickAdapter(private val sprite: Sprite) : } item.checkBox.isChecked = selectionManager.isPositionSelected(position) item.checkBox.isEnabled = viewStateManager.isEnabled(position) + + addCollapseIndicator(itemView, item) + return itemView } + private fun addCollapseIndicator(itemView: View, item: Brick) { + val brickViewContainer = itemView.findViewWithTag("brick_view_container") + ?: (itemView as? ViewGroup)?.let { if (it.childCount > 1) it.getChildAt(1) else it.getChildAt(0) } + ?: return + + val isCollapsed = (item is ScriptBrick && item.script.isCollapsed) || + (item is CompositeBrick && item is BrickBaseType && (item as BrickBaseType).isCollapsed) + val isSquashed = squashedBricks.contains(item) + + val existingIndicator = itemView.findViewWithTag(COLLAPSE_INDICATOR_TAG) + if (existingIndicator != null) { + (existingIndicator.parent as? ViewGroup)?.removeView(existingIndicator) + } + + if (isSquashed) { + val density = itemView.resources.displayMetrics.density + val squashedHeight = (SQUASHED_HEIGHT_DP * density).toInt() + itemView.layoutParams = AbsListView.LayoutParams( + AbsListView.LayoutParams.MATCH_PARENT, + squashedHeight + ) + if (itemView is ViewGroup) { + itemView.clipChildren = true + // Hide checkbox + if (itemView.childCount > 0) { + itemView.getChildAt(0).visibility = View.GONE + } + // Show colored brick layout but hide its internal text/icons + brickViewContainer.visibility = View.VISIBLE + if (brickViewContainer is ViewGroup) { + for (i in 0 until brickViewContainer.childCount) { + brickViewContainer.getChildAt(i).visibility = View.GONE + } + } + } + return + } else { + // Reset to normal state + itemView.layoutParams = AbsListView.LayoutParams( + AbsListView.LayoutParams.MATCH_PARENT, + AbsListView.LayoutParams.WRAP_CONTENT + ) + if (itemView is ViewGroup) { + itemView.clipChildren = false + if (itemView.childCount > 0) { + itemView.getChildAt(0).visibility = View.VISIBLE + } + brickViewContainer.visibility = View.VISIBLE + if (brickViewContainer is ViewGroup) { + for (i in 0 until brickViewContainer.childCount) { + val child = brickViewContainer.getChildAt(i) + if (child.tag != COLLAPSE_INDICATOR_TAG) { + child.visibility = View.VISIBLE + } + } + } + } + } + + val canCollapse = item is ScriptBrick || item is CompositeBrick + if (!canCollapse) return + + val containerParent = brickViewContainer.parent as? ViewGroup ?: return + val containerIndex = containerParent.indexOfChild(brickViewContainer) + + val indicatorIcon = ImageView(itemView.context).apply { + tag = COLLAPSE_INDICATOR_TAG + setImageResource(if (isCollapsed) R.drawable.ic_collapse else R.drawable.ic_expand) + scaleType = ImageView.ScaleType.CENTER_INSIDE + val density = resources.displayMetrics.density + layoutParams = FrameLayout.LayoutParams( + (32 * density).toInt(), + (32 * density).toInt() + ).apply { + gravity = Gravity.END or Gravity.CENTER_VERTICAL + marginEnd = (8 * density).toInt() + } + setColorFilter(Color.WHITE) + alpha = if (isCollapsed) 0.8f else 0.5f + + setOnClickListener { + if (item is ScriptBrick) { + item.script.isCollapsed = !item.script.isCollapsed + } else if (item is CompositeBrick && item is BrickBaseType) { + (item as BrickBaseType).isCollapsed = !(item as BrickBaseType).isCollapsed + } + updateItems(sprite) + } + } + + val wrapper: FrameLayout + if (containerParent is FrameLayout && containerParent.tag == "collapse_wrapper") { + wrapper = containerParent + } else { + wrapper = FrameLayout(itemView.context).apply { + tag = "collapse_wrapper" + layoutParams = LinearLayout.LayoutParams( + 0, + LinearLayout.LayoutParams.WRAP_CONTENT, + 1.0f + ) + } + containerParent.removeView(brickViewContainer) + wrapper.addView(brickViewContainer) + containerParent.addView(wrapper, containerIndex) + } + + if (wrapper.findViewWithTag(COLLAPSE_INDICATOR_TAG) == null) { + wrapper.addView(indicatorIcon) + } + } + private fun checkBoxClickListener(item: Brick, itemView: ViewGroup, position: Int) { item.checkBox.setOnClickListener { onCheckBoxClick(position) } if (viewStateManager.isEnabled(position)) { @@ -165,6 +354,7 @@ class BrickAdapter(private val sprite: Sprite) : CONNECTED_ONLY -> handleCheckBoxModeConnectedOnly(item, itemView, position) ALL -> handleCheckBoxModeAll(item) SCRIPTS_ONLY -> handleCheckBoxModeScriptsOnly(item) + COLLAPSE_EXPAND -> handleCheckBoxModeAll(item) } } @@ -348,6 +538,42 @@ class BrickAdapter(private val sprite: Sprite) : notifyDataSetChanged() } + fun selectAllCollapsedScripts() { + for (i in items.indices) { + val item = items[i] + if (item is ScriptBrick) { + val script = item.script + if (script != null && script.isCollapsed) { + selectionManager.setSelectionTo(true, i) + } + } else if (item is CompositeBrick && item is BrickBaseType) { + if ((item as BrickBaseType).isCollapsed) { + selectionManager.setSelectionTo(true, i) + } + } + } + notifyDataSetChanged() + } + + fun toggleCollapseExpand(selectedItems: List) { + for (brick in selectedItems) { + if (brick is ScriptBrick) { + val script = brick.script + if (script != null) { + script.isCollapsed = !script.isCollapsed + } + } else if (brick is CompositeBrick && brick is BrickBaseType) { + (brick as BrickBaseType).isCollapsed = !(brick as BrickBaseType).isCollapsed + } + } + updateItemsFromCurrentScripts() + } + + private fun handleCheckBoxModeCollapseExpand(item: Brick) { + item.checkBox.visibility = View.VISIBLE + item.disableSpinners() + } + fun addItem(position: Int, item: Brick?) { item?.let { items.add(position, it) } notifyDataSetChanged() diff --git a/catroid/src/main/java/org/catrobat/catroid/ui/recyclerview/fragment/ScriptFragment.java b/catroid/src/main/java/org/catrobat/catroid/ui/recyclerview/fragment/ScriptFragment.java index 0cdce4084e3..f12241f65d7 100644 --- a/catroid/src/main/java/org/catrobat/catroid/ui/recyclerview/fragment/ScriptFragment.java +++ b/catroid/src/main/java/org/catrobat/catroid/ui/recyclerview/fragment/ScriptFragment.java @@ -49,6 +49,8 @@ import org.catrobat.catroid.content.Sprite; import org.catrobat.catroid.content.StartScript; import org.catrobat.catroid.content.bricks.Brick; +import org.catrobat.catroid.content.bricks.BrickBaseType; +import org.catrobat.catroid.content.bricks.CompositeBrick; import org.catrobat.catroid.content.bricks.EmptyEventBrick; import org.catrobat.catroid.content.bricks.FormulaBrick; import org.catrobat.catroid.content.bricks.ScriptBrick; @@ -114,7 +116,7 @@ public class ScriptFragment extends ListFragment implements ActionMode.Callback, private static final String SCRIPT_TAG = "scriptToFocus"; @Retention(RetentionPolicy.SOURCE) - @IntDef({NONE, BACKPACK, COPY, DELETE, COMMENT}) + @IntDef({NONE, BACKPACK, COPY, DELETE, COMMENT, COLLAPSE_EXPAND}) @interface ActionModeType { } @@ -123,6 +125,7 @@ public class ScriptFragment extends ListFragment implements ActionMode.Callback, private static final int COPY = 2; private static final int DELETE = 3; private static final int COMMENT = 4; + private static final int COLLAPSE_EXPAND = 5; @ActionModeType private int actionModeType = NONE; @@ -203,6 +206,11 @@ public boolean onCreateActionMode(ActionMode mode, Menu menu) { adapter.setCheckBoxMode(BrickAdapter.ALL); mode.setTitle(getString(R.string.comment_in_out)); break; + case COLLAPSE_EXPAND: + adapter.selectAllCollapsedScripts(); + adapter.setCheckBoxMode(BrickAdapter.COLLAPSE_EXPAND); + mode.setTitle(getString(R.string.collapse_expand)); + break; case NONE: adapter.setCheckBoxMode(NONE); actionMode.finish(); @@ -253,6 +261,9 @@ private void handleContextualAction() { case COMMENT: toggleComments(adapter.getSelectedItems()); break; + case COLLAPSE_EXPAND: + toggleCollapseExpand(adapter.getSelectedItems()); + break; case NONE: throw new IllegalStateException("ActionModeType not set correctly"); } @@ -449,6 +460,9 @@ public boolean onOptionsItemSelected(MenuItem item) { case R.id.comment_in_out: startActionMode(COMMENT); break; + case R.id.collapse_expand: + startActionMode(COLLAPSE_EXPAND); + break; case R.id.find: scriptFinder.open(); break; @@ -567,6 +581,9 @@ public void onSelectionChanged(int selectedItemCnt) { case COMMENT: actionMode.setTitle(getString(R.string.comment_in_out) + " " + selectedItemCnt); break; + case COLLAPSE_EXPAND: + actionMode.setTitle(getString(R.string.collapse_expand) + " " + selectedItemCnt); + break; case NONE: throw new IllegalStateException("ActionModeType not set Correctly"); } @@ -685,6 +702,10 @@ public static List getContextMenuItems(Brick brick) { } items.add(R.string.brick_context_dialog_move_script); + items.add(brick.getScript().isCollapsed() + ? R.string.brick_context_dialog_expand_script + : R.string.brick_context_dialog_collapse_script); + items.add(R.string.brick_context_dialog_help); } else { items.add(R.string.brick_context_dialog_copy_brick); @@ -704,6 +725,12 @@ public static List getContextMenuItems(Brick brick) { items.add(R.string.brick_context_dialog_move_brick); } + if (brick instanceof CompositeBrick) { + items.add(((BrickBaseType) brick).isCollapsed() + ? R.string.brick_context_dialog_expand_brick + : R.string.brick_context_dialog_collapse_brick); + } + if (brick.hasHelpPage()) { items.add(R.string.brick_context_dialog_help); } @@ -774,6 +801,22 @@ private void handleContextMenuItemClick(int itemId, Brick brick, int position) { } listView.highlightControlStructureBricks(positions); break; + case R.string.brick_context_dialog_collapse_script: + brick.getScript().setCollapsed(true); + adapter.updateItems(ProjectManager.getInstance().getCurrentSprite()); + break; + case R.string.brick_context_dialog_expand_script: + brick.getScript().setCollapsed(false); + adapter.updateItems(ProjectManager.getInstance().getCurrentSprite()); + break; + case R.string.brick_context_dialog_collapse_brick: + ((BrickBaseType) brick).setCollapsed(true); + adapter.updateItems(ProjectManager.getInstance().getCurrentSprite()); + break; + case R.string.brick_context_dialog_expand_brick: + ((BrickBaseType) brick).setCollapsed(false); + adapter.updateItems(ProjectManager.getInstance().getCurrentSprite()); + break; } } @@ -873,6 +916,11 @@ private void toggleComments(List selectedBricks) { finishActionMode(); } + private void toggleCollapseExpand(List selectedBricks) { + adapter.toggleCollapseExpand(selectedBricks); + finishActionMode(); + } + public void setUndoBrickPosition(Brick brick) { undoBrickPosition = adapter.getPosition(brick); } diff --git a/catroid/src/main/res/drawable/ic_collapse.xml b/catroid/src/main/res/drawable/ic_collapse.xml new file mode 100644 index 00000000000..aa73f3d8efd --- /dev/null +++ b/catroid/src/main/res/drawable/ic_collapse.xml @@ -0,0 +1,34 @@ + + + + + + diff --git a/catroid/src/main/res/drawable/ic_expand.xml b/catroid/src/main/res/drawable/ic_expand.xml new file mode 100644 index 00000000000..7987784aeb4 --- /dev/null +++ b/catroid/src/main/res/drawable/ic_expand.xml @@ -0,0 +1,34 @@ + + + + + + diff --git a/catroid/src/main/res/menu/menu_script_activity.xml b/catroid/src/main/res/menu/menu_script_activity.xml index e9f6b2d551f..5099f993033 100644 --- a/catroid/src/main/res/menu/menu_script_activity.xml +++ b/catroid/src/main/res/menu/menu_script_activity.xml @@ -64,6 +64,12 @@ android:visible="false" android:icon="@drawable/ic_placeholder" app:showAsAction="never" /> + Disable script Help Disable/enable + Collapse/expand + Collapse script + Expand script + Collapse + Expand