diff --git a/.idea/misc.xml b/.idea/misc.xml index 06b3731ae..ceb55db53 100755 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -49,6 +49,7 @@ + diff --git a/app/src/main/assets/data/common/gradle-plugin.zip b/app/src/main/assets/data/common/gradle-plugin.zip index 2d3e7a159..d266fa744 100644 Binary files a/app/src/main/assets/data/common/gradle-plugin.zip and b/app/src/main/assets/data/common/gradle-plugin.zip differ diff --git a/app/src/main/java/com/itsaky/androidide/adapters/TextActionItemAdapter.java b/app/src/main/java/com/itsaky/androidide/adapters/TextActionItemAdapter.java new file mode 100644 index 000000000..2a204705d --- /dev/null +++ b/app/src/main/java/com/itsaky/androidide/adapters/TextActionItemAdapter.java @@ -0,0 +1,84 @@ +/* + * 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 + * (at your option) any later version. + * + * AndroidIDE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with AndroidIDE. If not, see . + */ + +package com.itsaky.androidide.adapters; + +import android.view.LayoutInflater; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import com.itsaky.androidide.databinding.LayoutTextActionItemBinding; +import com.itsaky.androidide.views.editor.IDEEditor; + +import java.util.List; +import java.util.Objects; +import java.util.function.Consumer; + +/** + * Adapter for text actions in editor. + * @author Akash Yadav + */ +public class TextActionItemAdapter extends RecyclerView.Adapter { + + private final List actions; + private final Consumer onClick; + + public TextActionItemAdapter (List actions, Consumer onClick) { + Objects.requireNonNull (actions); + this.actions = actions; + this.onClick = onClick; + } + + @NonNull + @Override + public VH onCreateViewHolder (@NonNull ViewGroup parent, int viewType) { + return new VH (LayoutTextActionItemBinding.inflate (LayoutInflater.from (parent.getContext ()), parent, false)); + } + + @Override + public void onBindViewHolder (@NonNull VH holder, int position) { + final var binding = holder.binding; + final var action = actions.get (position); + final var button = binding.getRoot (); + + button.setText (action.titleId); + button.setCompoundDrawablesRelative (action.icon, null, null, null); + + button.setOnClickListener (v -> { + if (onClick != null) { + onClick.accept (action); + } + }); + } + + @Override + public int getItemCount () { + return actions.size (); + } + + static class VH extends RecyclerView.ViewHolder { + + private final LayoutTextActionItemBinding binding; + + public VH (@NonNull LayoutTextActionItemBinding binding) { + super (binding.getRoot ()); + this.binding = binding; + } + } +} diff --git a/app/src/main/java/com/itsaky/androidide/syntax/colorschemes/SchemeAndroidIDE.java b/app/src/main/java/com/itsaky/androidide/syntax/colorschemes/SchemeAndroidIDE.java index 84365da4b..eff61452c 100755 --- a/app/src/main/java/com/itsaky/androidide/syntax/colorschemes/SchemeAndroidIDE.java +++ b/app/src/main/java/com/itsaky/androidide/syntax/colorschemes/SchemeAndroidIDE.java @@ -73,7 +73,7 @@ public class SchemeAndroidIDE extends EditorColorScheme { setColor (WHOLE_BACKGROUND, 0xff212121); setColor (LINE_NUMBER_BACKGROUND, 0xff212121); setColor (LINE_DIVIDER, Color.TRANSPARENT); - setColor (LINE_NUMBER, 0xffaaaaaa); + setColor (LINE_NUMBER, 0xffaaaaaa ); setColor (LINE_NUMBER_PANEL, 0xff000000); setColor (LINE_NUMBER_PANEL_TEXT, 0xffffffff); setColor (TEXT_NORMAL, 0xfff5f5f5); diff --git a/app/src/main/java/com/itsaky/androidide/views/editor/EditorTextActionWindow.java b/app/src/main/java/com/itsaky/androidide/views/editor/EditorTextActionWindow.java new file mode 100644 index 000000000..a2d297a1a --- /dev/null +++ b/app/src/main/java/com/itsaky/androidide/views/editor/EditorTextActionWindow.java @@ -0,0 +1,230 @@ +/* + * 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 + * (at your option) any later version. + * + * AndroidIDE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with AndroidIDE. If not, see . + */ +package com.itsaky.androidide.views.editor; + +import android.graphics.RectF; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.GradientDrawable; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.core.content.ContextCompat; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import com.itsaky.androidide.R; +import com.itsaky.androidide.adapters.TextActionItemAdapter; + +import java.util.Objects; +import java.util.Set; +import java.util.TreeSet; +import java.util.stream.Collectors; + +import io.github.rosemoe.sora.event.HandleStateChangeEvent; +import io.github.rosemoe.sora.event.ScrollEvent; +import io.github.rosemoe.sora.event.SelectionChangeEvent; +import io.github.rosemoe.sora.widget.CodeEditor; +import io.github.rosemoe.sora.widget.EditorTouchEventHandler; +import io.github.rosemoe.sora.widget.base.EditorPopupWindow; + +/** + * Presents text actions in a popup widow. + * + * @author Akash Yadav + * @see io.github.rosemoe.sora.widget.component.EditorTextActionWindow + */ +public class EditorTextActionWindow extends EditorPopupWindow implements IDEEditor.ITextActionPresenter { + + private IDEEditor editor; + private RecyclerView actionsList; + private EditorTouchEventHandler touchHandler; + + private final static long DELAY = 200; + private long mLastScroll; + private int mLastPosition; + + private final Set registeredActions = new TreeSet<> (); + + /** + * Create a popup window for editor + * + * @param editor The editor + * @see #FEATURE_SCROLL_AS_CONTENT + * @see #FEATURE_SHOW_OUTSIDE_VIEW_ALLOWED + * @see #FEATURE_HIDE_WHEN_FAST_SCROLL + */ + public EditorTextActionWindow (@NonNull CodeEditor editor) { + super (editor, FEATURE_SHOW_OUTSIDE_VIEW_ALLOWED); + } + + @Override + public void bindEditor (@NonNull IDEEditor editor) { + Objects.requireNonNull (editor, "Cannot bind with null editor"); + + this.editor = editor; + this.touchHandler = editor.getEventHandler (); + this.actionsList = new RecyclerView (editor.getContext ()); + this.actionsList.setLayoutParams (new ViewGroup.LayoutParams (ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)); + this.actionsList.setLayoutManager (new LinearLayoutManager (editor.getContext ())); + this.actionsList.setBackground (createBackground ()); + this.actionsList.setVerticalFadingEdgeEnabled (true); + this.actionsList.setFadingEdgeLength ((int) (10 * editor.getDpUnit ())); + this.actionsList.setClipToOutline (true); // prevent items from being drawn outside window + this.editor.getComponent (io.github.rosemoe.sora.widget.component.EditorTextActionWindow.class).setEnabled (false); + + setContentView (this.actionsList); + subscribeToEvents (); + + this.registeredActions.clear (); + } + + @NonNull + private Drawable createBackground () { + final var drawable = new GradientDrawable (); + drawable.setShape (GradientDrawable.RECTANGLE); + drawable.setColor (ContextCompat.getColor (editor.getContext (), R.color.primaryLightColor)); + drawable.setCornerRadius (8 * editor.getDpUnit ()); + return drawable; + } + + @Override + public void registerAction (@NonNull IDEEditor.TextAction action) { + Objects.requireNonNull (editor, "No editor attached!"); + + this.registeredActions.add (action); + } + + @Override + public void show () { + Objects.requireNonNull (editor, "No editor attached!"); + + final var actions = this.registeredActions.stream () + .filter (action -> this.editor.shouldShowTextAction (action.id)) + .collect (Collectors.toList ()); + this.actionsList.setAdapter (new TextActionItemAdapter (actions, this::performTextAction)); + this.actionsList.measure ( + View.MeasureSpec.makeMeasureSpec ( + editor.getWidth (), + View.MeasureSpec.AT_MOST + ), + View.MeasureSpec.makeMeasureSpec ( + (int) (260 * editor.getDpUnit ()), // 260dp at most + View.MeasureSpec.AT_MOST + ) + ); + setSize (this.actionsList.getMeasuredWidth (), this.actionsList.getMeasuredHeight ()); + + super.show (); + } + + private void subscribeToEvents () { + this.editor.subscribeEvent (SelectionChangeEvent.class, (event, unsubscribe) -> { + if (touchHandler.hasAnyHeldHandle ()) { + return; + } + if (event.isSelected ()) { + if (!isShowing ()) { + this.editor.post (this::displayWindow); + } + mLastPosition = -1; + } else { + var show = false; + if (event.getCause () == SelectionChangeEvent.CAUSE_TAP + && event.getLeft ().index == mLastPosition + && !isShowing () && !this.editor.getText ().isInBatchEdit ()) { + this.editor.post (this::displayWindow); + show = true; + } else { + dismiss (); + } + if (event.getCause () == SelectionChangeEvent.CAUSE_TAP && !show) { + mLastPosition = event.getLeft ().index; + } else { + mLastPosition = -1; + } + } + }); + this.editor.subscribeEvent (ScrollEvent.class, ((event, unsubscribe) -> { + var last = mLastScroll; + mLastScroll = System.currentTimeMillis (); + if (mLastScroll - last < DELAY) { + postDisplay (); + } + })); + this.editor.subscribeEvent (HandleStateChangeEvent.class, ((event, unsubscribe) -> { + if (event.isHeld ()) { + postDisplay (); + } + })); + } + + private void performTextAction (@NonNull IDEEditor.TextAction action) { + this.editor.performTextAction (action); + dismiss (); + } + + private int selectTop (@NonNull RectF rect) { + var rowHeight = editor.getRowHeight (); + if (rect.top - rowHeight * 3 / 2F > getHeight ()) { + return (int) (rect.top - rowHeight * 3 / 2 - getHeight ()); + } else { + return (int) (rect.bottom + rowHeight / 2); + } + } + + public void displayWindow () { + int top; + var cursor = this.editor.getCursor (); + if (cursor.isSelected ()) { + var leftRect = this.editor.getLeftHandleDescriptor ().position; + var rightRect = this.editor.getRightHandleDescriptor ().position; + var top1 = selectTop (leftRect); + var top2 = selectTop (rightRect); + top = Math.min (top1, top2); + } else { + top = selectTop (this.editor.getInsertHandleDescriptor ().position); + } + top = Math.max (0, Math.min (top, this.editor.getHeight () - getHeight () - 5)); + float handleLeftX = this.editor.getOffset (this.editor.getCursor ().getLeftLine (), this.editor.getCursor ().getLeftColumn ()); + float handleRightX = this.editor.getOffset (this.editor.getCursor ().getRightLine (), this.editor.getCursor ().getRightColumn ()); + int panelX = (int) ((handleLeftX + handleRightX) / 2f); + setLocationAbsolutely (panelX, top); + show (); + } + + private void postDisplay () { + if (!isShowing ()) { + return; + } + dismiss (); + if (!this.editor.getCursor ().isSelected ()) { + return; + } + this.editor.postDelayed (new Runnable () { + @Override + public void run () { + if (!touchHandler.hasAnyHeldHandle () && System.currentTimeMillis () - mLastScroll > DELAY + && touchHandler.getScroller ().isFinished ()) { + displayWindow (); + } else { + editor.postDelayed (this, DELAY); + } + } + }, DELAY); + } +} diff --git a/app/src/main/java/com/itsaky/androidide/views/editor/IDEEditor.java b/app/src/main/java/com/itsaky/androidide/views/editor/IDEEditor.java index e40d666d2..40aeb0612 100644 --- a/app/src/main/java/com/itsaky/androidide/views/editor/IDEEditor.java +++ b/app/src/main/java/com/itsaky/androidide/views/editor/IDEEditor.java @@ -77,7 +77,6 @@ public class IDEEditor extends CodeEditor { private SignatureHelpWindow mSignatureHelpWindow; private DiagnosticWindow mDiagnosticWindow; - @SuppressWarnings("FieldCanBeLocal,unused") private ITextActionPresenter mTextActionPresenter; public static final String KEY_FILE = "editor_file"; @@ -99,7 +98,7 @@ public class IDEEditor extends CodeEditor { super (context, attrs, defStyleAttr, defStyleRes); setColorScheme (new SchemeAndroidIDE ()); - setTextActionPresenter (new EditorTextActionMode ()); + setTextActionPresenter (new EditorTextActionWindow (this)); subscribeEvent (SelectionChangeEvent.class, (event, unsubscribe) -> handleSelectionChange (event)); subscribeEvent (ContentChangeEvent.class, (event, unsubscribe) -> handleContentChange (event)); diff --git a/app/src/main/res/layout/layout_text_action_item.xml b/app/src/main/res/layout/layout_text_action_item.xml new file mode 100644 index 000000000..f84454d18 --- /dev/null +++ b/app/src/main/res/layout/layout_text_action_item.xml @@ -0,0 +1,35 @@ + + \ No newline at end of file