Skip to content

Commit 7910fa9

Browse files
authored
feat: Implement tooltips for template parameter input fields (#288)
* feat: Implement tooltips for template parameter input fields This change introduces tooltips for various template parameter input fields, such as project name, package name, language, minimum SDK, and "use KTS" options. Specifically, it includes the following modifications: - Added `applyCallTooltip` to `ITemplateWidgetViewProvider` interface to enable tooltip functionality. - Implemented `TooltipTextInputLayout`, a custom `TextInputLayout` that detects long-press gestures to display tooltips without interfering with child views. - Updated `layout_textfield.xml` to use `TooltipTextInputLayout`. - Modified `parameters.kt` to set default `tooltipTag` values for `projectNameParameter`, `packageNameParameter`, `projectLanguageParameter`, `minSdkParameter`, and `useKtsParameter`. - Added `tooltipTag` to `projectLocationParameter` in `base.kt`. - Updated `TemplateWidgetsListAdapter.kt` to fetch and display tooltips when a long-press is detected on a widget. - Implemented `applyCallTooltip` in `TemplateWidgetViewProviderImpl.kt` and updated `createView` to set up long-press listeners for tooltips on created views. * Refactor: Move Tooltip classes to resources module - Move `TooltipTextInputLayout` from `templates-impl` to `resources` module and convert to Java. - Introduce `ITooltipView` interface in `resources` module for common tooltip long-press handling. - Create `TooltipMaterialCheckBox` in `resources` module, implementing `ITooltipView`. - Update layout files (`layout_spinner.xml`, `layout_textfield.xml`, `layout_checkbox.xml`) to use the new `TooltipTextInputLayout` and `TooltipMaterialCheckBox` from the `resources` module. - Adapt `TemplateWidgetViewProviderImpl` to use `ITooltipView` for setting long-press listeners. - Remove redundant lifecycle owner check in `TemplateWidgetsListAdapter`.
1 parent 5689e60 commit 7910fa9

11 files changed

Lines changed: 615 additions & 367 deletions

File tree

‎app/src/main/java/com/itsaky/androidide/adapters/TemplateWidgetsListAdapter.kt‎

Lines changed: 69 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -17,47 +17,89 @@
1717

1818
package com.itsaky.androidide.adapters
1919

20+
import android.content.Context
21+
import android.util.Log
2022
import android.view.LayoutInflater
2123
import android.view.ViewGroup
2224
import android.widget.LinearLayout
25+
import androidx.lifecycle.LifecycleOwner
26+
import androidx.lifecycle.lifecycleScope
2327
import androidx.recyclerview.widget.RecyclerView
2428
import com.itsaky.androidide.adapters.TemplateWidgetsListAdapter.WidgetViewHolder
2529
import com.itsaky.androidide.databinding.LayoutTemplateWidgetlistItemBinding
30+
import com.itsaky.androidide.idetooltips.IDETooltipItem
31+
import com.itsaky.androidide.idetooltips.TooltipManager
2632
import com.itsaky.androidide.templates.ITemplateWidgetViewProvider
2733
import com.itsaky.androidide.templates.Widget
34+
import com.itsaky.androidide.utils.TooltipUtils
35+
import kotlinx.coroutines.Dispatchers
36+
import kotlinx.coroutines.launch
37+
import kotlinx.coroutines.withContext
2838

2939
/**
3040
* A [RecyclerView.Adapter] that is used to show the widgets from templates.
3141
*
3242
* @author Akash Yadav
3343
*/
3444
class TemplateWidgetsListAdapter(private val widgets: List<Widget<*>>) :
35-
RecyclerView.Adapter<WidgetViewHolder>() {
36-
37-
class WidgetViewHolder(
38-
internal val binding: LayoutTemplateWidgetlistItemBinding
39-
) : RecyclerView.ViewHolder(binding.root)
40-
41-
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int
42-
): WidgetViewHolder {
43-
return WidgetViewHolder(LayoutTemplateWidgetlistItemBinding.inflate(
44-
LayoutInflater.from(parent.context), parent, false))
45-
}
46-
47-
override fun getItemCount(): Int {
48-
return widgets.size
49-
}
50-
51-
override fun onBindViewHolder(holder: WidgetViewHolder, position: Int) {
52-
holder.binding.apply {
53-
val viewProvider = ITemplateWidgetViewProvider.getInstance()
54-
val widget = widgets[position]
55-
val view = viewProvider.createView(root.context, widget)
56-
57-
root.removeAllViews()
58-
root.addView(view,
59-
LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
60-
ViewGroup.LayoutParams.WRAP_CONTENT))
45+
RecyclerView.Adapter<WidgetViewHolder>() {
46+
47+
class WidgetViewHolder(
48+
internal val binding: LayoutTemplateWidgetlistItemBinding
49+
) : RecyclerView.ViewHolder(binding.root)
50+
51+
override fun onCreateViewHolder(
52+
parent: ViewGroup, viewType: Int
53+
): WidgetViewHolder {
54+
return WidgetViewHolder(
55+
LayoutTemplateWidgetlistItemBinding.inflate(
56+
LayoutInflater.from(parent.context), parent, false
57+
)
58+
)
59+
}
60+
61+
override fun getItemCount(): Int {
62+
return widgets.size
63+
}
64+
65+
override fun onBindViewHolder(holder: WidgetViewHolder, position: Int) {
66+
holder.binding.apply {
67+
val viewProvider = ITemplateWidgetViewProvider.getInstance()
68+
val widget = widgets[position]
69+
val view = viewProvider.createView(root.context, widget)
70+
viewProvider.applyCallTooltip { tooltipTag ->
71+
val lifecycleOwner = root.context as? LifecycleOwner ?: return@applyCallTooltip
72+
73+
lifecycleOwner.lifecycleScope.launch {
74+
try {
75+
// Call the method on the interface.
76+
val tooltipData = getTooltipData(root.context, "ide", tooltipTag)
77+
tooltipData?.let {
78+
TooltipUtils.showIDETooltip(
79+
context = root.context,
80+
level = 0,
81+
tooltipItem = tooltipData,
82+
anchorView = root
83+
)
84+
}
85+
} catch (e: Exception) {
86+
Log.e("Tooltip", "Error showing tooltip for $tooltipTag", e)
87+
}
88+
}
89+
90+
}
91+
root.removeAllViews()
92+
root.addView(
93+
view, LinearLayout.LayoutParams(
94+
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT
95+
)
96+
)
97+
}
98+
}
99+
100+
suspend fun getTooltipData(context: Context, category: String, tag: String): IDETooltipItem? {
101+
return withContext(Dispatchers.IO) {
102+
TooltipManager.getTooltip(context, category, tag)
103+
}
61104
}
62-
}
63105
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package com.itsaky.androidide.resources;
2+
3+
/**
4+
* An interface for views that support a custom long-press action for showing tooltips.
5+
*/
6+
public interface ITooltipView {
7+
8+
/**
9+
* A simple listener for long-press events.
10+
*/
11+
interface OnTooltipLongPressListener {
12+
void onLongPress();
13+
}
14+
15+
/**
16+
* Sets the listener to be called when a long-press gesture is detected.
17+
*
18+
* @param listener The listener to be invoked.
19+
*/
20+
void setTooltipLongPressListener(OnTooltipLongPressListener listener);
21+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package com.itsaky.androidide.resources;
2+
3+
import android.content.Context;
4+
import android.util.AttributeSet;
5+
6+
import androidx.annotation.NonNull;
7+
import androidx.annotation.Nullable;
8+
9+
import com.google.android.material.checkbox.MaterialCheckBox;
10+
11+
/**
12+
* A MaterialCheckBox that implements ITooltipView to provide a unified
13+
* long-press listener for tooltips.
14+
*/
15+
public class TooltipMaterialCheckBox extends MaterialCheckBox implements ITooltipView {
16+
17+
public TooltipMaterialCheckBox(@NonNull Context context) {
18+
super(context);
19+
}
20+
21+
public TooltipMaterialCheckBox(@NonNull Context context, @Nullable AttributeSet attrs) {
22+
super(context, attrs);
23+
}
24+
25+
public TooltipMaterialCheckBox(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
26+
super(context, attrs, defStyleAttr);
27+
}
28+
29+
@Override
30+
public void setTooltipLongPressListener(OnTooltipLongPressListener listener) {
31+
if (listener == null) {
32+
setOnLongClickListener(null);
33+
return;
34+
}
35+
// Bridge our interface listener to the standard Android OnLongClickListener
36+
setOnLongClickListener(v -> {
37+
listener.onLongPress();
38+
// Return true to consume the event, preventing other actions
39+
return true;
40+
});
41+
}
42+
}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
package com.itsaky.androidide.resources;
2+
3+
import android.content.Context;
4+
import android.util.AttributeSet;
5+
import android.view.GestureDetector;
6+
import android.view.MotionEvent;
7+
import android.view.View;
8+
9+
import androidx.annotation.NonNull;
10+
import androidx.annotation.Nullable;
11+
12+
import com.google.android.material.textfield.TextInputLayout;
13+
14+
/**
15+
* A custom TextInputLayout that uses a GestureDetector to reliably detect
16+
* a long-press gesture anywhere within its bounds, without interfering
17+
* with child views like the TextInputEditText.
18+
*/
19+
public class TooltipTextInputLayout extends TextInputLayout implements ITooltipView {
20+
21+
public interface OnLongPressListener {
22+
void onLongPress(View v);
23+
}
24+
25+
private OnLongPressListener onLongPressListener;
26+
private final GestureDetector gestureDetector;
27+
28+
private boolean isLongPressHandled;
29+
30+
public TooltipTextInputLayout(@NonNull Context context) {
31+
this(context, null);
32+
}
33+
34+
public TooltipTextInputLayout(@NonNull Context context, @Nullable AttributeSet attrs) {
35+
this(context, attrs, 0);
36+
}
37+
38+
public TooltipTextInputLayout(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
39+
super(context, attrs, defStyleAttr);
40+
41+
gestureDetector = new GestureDetector(context, new GestureDetector.SimpleOnGestureListener() {
42+
@Override
43+
public void onLongPress(@NonNull MotionEvent e) {
44+
if (onLongPressListener != null) {
45+
onLongPressListener.onLongPress(TooltipTextInputLayout.this);
46+
}
47+
isLongPressHandled = true;
48+
}
49+
});
50+
}
51+
52+
@Override
53+
public void setTooltipLongPressListener(OnTooltipLongPressListener listener) {
54+
if (listener == null) {
55+
// Allow unsetting the listener
56+
setOnLongPressListener(null);
57+
return;
58+
}
59+
// Bridge our interface listener to the view's specific listener
60+
setOnLongPressListener(v -> listener.onLongPress());
61+
}
62+
63+
public void setOnLongPressListener(@Nullable OnLongPressListener listener) {
64+
this.onLongPressListener = listener;
65+
}
66+
67+
@Override
68+
public boolean onInterceptTouchEvent(MotionEvent ev) {
69+
// On the first touch event (ACTION_DOWN), reset the flag.
70+
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
71+
isLongPressHandled = false;
72+
}
73+
74+
// Let the GestureDetector inspect the event. If it detects a long press,
75+
// it will set our 'isLongPressHandled' flag to true.
76+
gestureDetector.onTouchEvent(ev);
77+
78+
// We intercept the touch event stream ONLY if our flag is true.
79+
// This "steals" the rest of the gesture (like the final ACTION_UP)
80+
// from child views. For a normal tap, this remains false.
81+
return isLongPressHandled;
82+
}
83+
84+
@Override
85+
public boolean onTouchEvent(MotionEvent event) {
86+
// This method is only called if onInterceptTouchEvent returns true.
87+
// We must return true here to signal that we have consumed the event.
88+
return true;
89+
}
90+
}

‎resources/src/main/res/layout/layout_spinner.xml‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
~ along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
1616
-->
1717

18-
<com.google.android.material.textfield.TextInputLayout xmlns:android="http://schemas.android.com/apk/res/android"
18+
<com.itsaky.androidide.resources.TooltipTextInputLayout xmlns:android="http://schemas.android.com/apk/res/android"
1919
xmlns:tools="http://schemas.android.com/tools"
2020
android:id="@+id/spinner_layout"
2121
style="@style/Widget.Material3.TextInputLayout.FilledBox.ExposedDropdownMenu"
@@ -30,4 +30,4 @@
3030
android:background="@android:color/transparent"
3131
android:inputType="none" />
3232

33-
</com.google.android.material.textfield.TextInputLayout>
33+
</com.itsaky.androidide.resources.TooltipTextInputLayout>

‎templates-api/src/main/java/com/itsaky/androidide/templates/ITemplateWidgetViewProvider.kt‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,4 +53,5 @@ interface ITemplateWidgetViewProvider {
5353
* @return The view.
5454
*/
5555
fun <T> createView(context: Context, widget: Widget<T>): View
56+
fun applyCallTooltip(callTooltip: (String) -> Unit)
5657
}

‎templates-api/src/main/java/com/itsaky/androidide/templates/base/base.kt‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,9 +86,11 @@ inline fun baseProject(
8686
default = Environment.PROJECTS_DIR.absolutePath
8787
endIcon = { R.drawable.ic_folder }
8888
constraints = listOf(NONEMPTY, DIRECTORY, EXISTS)
89-
inputType = android.text.InputType.TYPE_CLASS_TEXT or android.text.InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS
89+
inputType =
90+
android.text.InputType.TYPE_CLASS_TEXT or android.text.InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS
9091
imeOptions = android.view.inputmethod.EditorInfo.IME_ACTION_DONE
9192
maxLines = 1
93+
tooltipTag = "setup.save.location"
9294
}
9395

9496
projectName.doBeforeCreateView {
@@ -182,7 +184,8 @@ inline fun baseAndroidModule(
182184
name = R.string.wizard_module_name
183185
default = ":app"
184186
constraints = listOf(NONEMPTY, MODULE_NAME)
185-
inputType = android.text.InputType.TYPE_CLASS_TEXT or android.text.InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS
187+
inputType =
188+
android.text.InputType.TYPE_CLASS_TEXT or android.text.InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS
186189
imeOptions = android.view.inputmethod.EditorInfo.IME_ACTION_DONE
187190
maxLines = 1
188191
}

0 commit comments

Comments
 (0)