capacitor-dex-editor 0.0.38 → 0.0.40

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/android/src/main/AndroidManifest.xml +8 -0
  2. package/android/src/main/assets/availableSyntax.json +54 -0
  3. package/android/src/main/assets/c.json +42 -0
  4. package/android/src/main/assets/colors.json +21 -0
  5. package/android/src/main/assets/cpp.json +79 -0
  6. package/android/src/main/assets/dart.json +108 -0
  7. package/android/src/main/assets/java.json +46 -0
  8. package/android/src/main/assets/js.json +54 -0
  9. package/android/src/main/assets/json.json +33 -0
  10. package/android/src/main/assets/kotlin.json +53 -0
  11. package/android/src/main/assets/lua.json +54 -0
  12. package/android/src/main/assets/php.json +69 -0
  13. package/android/src/main/assets/python.json +87 -0
  14. package/android/src/main/assets/rust.json +139 -0
  15. package/android/src/main/assets/smali.json +131 -0
  16. package/android/src/main/assets/xml.json +96 -0
  17. package/android/src/main/java/com/aetherlink/dexeditor/DexEditorPluginPlugin.java +48 -0
  18. package/android/src/main/java/com/aetherlink/dexeditor/SmaliEditorActivity.java +205 -0
  19. package/android/src/main/java/com/aetherlink/dexeditor/editor/EditView.java +4022 -0
  20. package/android/src/main/java/com/aetherlink/dexeditor/editor/WordWrapLayout.java +275 -0
  21. package/android/src/main/java/com/aetherlink/dexeditor/editor/buffer/BufferCache.java +113 -0
  22. package/android/src/main/java/com/aetherlink/dexeditor/editor/buffer/GapBuffer.java +685 -0
  23. package/android/src/main/java/com/aetherlink/dexeditor/editor/component/ClipboardPanel.java +1380 -0
  24. package/android/src/main/java/com/aetherlink/dexeditor/editor/component/Magnifier.java +363 -0
  25. package/android/src/main/java/com/aetherlink/dexeditor/editor/highlight/Candidate.java +52 -0
  26. package/android/src/main/java/com/aetherlink/dexeditor/editor/highlight/CommentDef.java +47 -0
  27. package/android/src/main/java/com/aetherlink/dexeditor/editor/highlight/LineResult.java +49 -0
  28. package/android/src/main/java/com/aetherlink/dexeditor/editor/highlight/MHSyntaxHighlightEngine.java +841 -0
  29. package/android/src/main/java/com/aetherlink/dexeditor/editor/highlight/Rule.java +53 -0
  30. package/android/src/main/java/com/aetherlink/dexeditor/editor/highlight/Token.java +48 -0
  31. package/android/src/main/java/com/aetherlink/dexeditor/editor/listener/OnTextChangedListener.java +6 -0
  32. package/android/src/main/java/com/aetherlink/dexeditor/editor/treeObserver/OnComputeInternalInsetsListener.java +92 -0
  33. package/android/src/main/java/com/aetherlink/dexeditor/editor/treeObserver/ViewTreeObserverReflection.java +87 -0
  34. package/android/src/main/java/com/aetherlink/dexeditor/editor/utils/LinkChecker.java +28 -0
  35. package/android/src/main/java/com/aetherlink/dexeditor/editor/utils/Pair.java +80 -0
  36. package/android/src/main/java/com/aetherlink/dexeditor/editor/utils/ScreenUtils.java +63 -0
  37. package/android/src/main/java/com/aetherlink/dexeditor/editor/utils/menuUtils/MenuAction.java +49 -0
  38. package/android/src/main/java/com/aetherlink/dexeditor/editor/utils/menuUtils/MenuItemConfig.java +49 -0
  39. package/android/src/main/java/com/aetherlink/dexeditor/editor/utils/menuUtils/MenuItemData.java +49 -0
  40. package/android/src/main/java/com/aetherlink/dexeditor/editor/utils/menuUtils/ViewFader.java +76 -0
  41. package/android/src/main/res/drawable/abc_text_cursor_material.xml +9 -0
  42. package/android/src/main/res/drawable/abc_text_select_handle_left_mtrl.png +0 -0
  43. package/android/src/main/res/drawable/abc_text_select_handle_middle_mtrl.png +0 -0
  44. package/android/src/main/res/drawable/abc_text_select_handle_right_mtrl.png +0 -0
  45. package/android/src/main/res/drawable/ic_arrow_back.xml +12 -0
  46. package/android/src/main/res/drawable/ic_copy.xml +11 -0
  47. package/android/src/main/res/drawable/ic_cut.xml +10 -0
  48. package/android/src/main/res/drawable/ic_delete.xml +12 -0
  49. package/android/src/main/res/drawable/ic_edit_white_24dp.xml +10 -0
  50. package/android/src/main/res/drawable/ic_goto.xml +10 -0
  51. package/android/src/main/res/drawable/ic_launcher_background.xml +186 -0
  52. package/android/src/main/res/drawable/ic_look_white_24dp.xml +11 -0
  53. package/android/src/main/res/drawable/ic_more.xml +10 -0
  54. package/android/src/main/res/drawable/ic_open_link.xml +11 -0
  55. package/android/src/main/res/drawable/ic_paste.xml +11 -0
  56. package/android/src/main/res/drawable/ic_redo_white_24dp.xml +10 -0
  57. package/android/src/main/res/drawable/ic_select.xml +11 -0
  58. package/android/src/main/res/drawable/ic_select_all.xml +10 -0
  59. package/android/src/main/res/drawable/ic_share.xml +11 -0
  60. package/android/src/main/res/drawable/ic_toggle_comment.xml +12 -0
  61. package/android/src/main/res/drawable/ic_translate.xml +10 -0
  62. package/android/src/main/res/drawable/ic_undo_white_24dp.xml +11 -0
  63. package/android/src/main/res/drawable/magnifier_bg.xml +5 -0
  64. package/android/src/main/res/drawable/popup_background.xml +8 -0
  65. package/android/src/main/res/drawable/ripple_effect.xml +9 -0
  66. package/android/src/main/res/drawable/selection_menu_background.xml +5 -0
  67. package/android/src/main/res/layout/custom_selection_menu.xml +44 -0
  68. package/android/src/main/res/layout/expand_button.xml +23 -0
  69. package/android/src/main/res/layout/item_autocomplete.xml +25 -0
  70. package/android/src/main/res/layout/magnifier_popup.xml +17 -0
  71. package/android/src/main/res/layout/menu_item.xml +30 -0
  72. package/android/src/main/res/layout/text_selection_menu.xml +36 -0
  73. package/package.json +1 -1
@@ -0,0 +1,1380 @@
1
+ /*
2
+ * MH-TextEditor - An Advanced and optimized TextEditor for android
3
+ * Copyright 2025, developer-krushna
4
+ *
5
+ * Redistribution and use in source and binary forms, with or without
6
+ * modification, are permitted provided that the following conditions are
7
+ * met:
8
+ *
9
+ * * Redistributions of source code must retain the above copyright
10
+ * notice, this list of conditions and the following disclaimer.
11
+ * * Redistributions in binary form must reproduce the above
12
+ * copyright notice, this list of conditions and the following disclaimer
13
+ * in the documentation and/or other materials provided with the
14
+ * distribution.
15
+ * * Neither the name of developer-krushna nor the names of its
16
+ * contributors may be used to endorse or promote products derived from
17
+ * this software without specific prior written permission.
18
+ *
19
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
20
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
21
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
22
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
23
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
24
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
25
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
26
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
27
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
28
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30
+
31
+
32
+ * Please contact Krushna by email modder-hub@zohomail.in if you need
33
+ * additional information or have any questions
34
+ */
35
+
36
+ package com.aetherlink.dexeditor.editor.component;
37
+
38
+ import android.content.ClipboardManager;
39
+ import android.content.Context;
40
+ import android.content.Intent;
41
+ import android.content.SharedPreferences;
42
+ import android.content.res.ColorStateList;
43
+ import android.content.res.TypedArray;
44
+ import android.graphics.Color;
45
+ import android.graphics.Outline;
46
+ import android.graphics.Rect;
47
+ import android.graphics.Region;
48
+ import android.graphics.drawable.ColorDrawable;
49
+ import android.graphics.drawable.GradientDrawable;
50
+ import android.graphics.drawable.RippleDrawable;
51
+ import android.graphics.drawable.StateListDrawable;
52
+ import android.os.Build;
53
+ import android.os.Handler;
54
+ import android.util.Log;
55
+ import android.util.TypedValue;
56
+ import android.view.Gravity;
57
+ import android.view.LayoutInflater;
58
+ import android.view.View;
59
+ import android.view.ViewGroup;
60
+ import android.view.ViewOutlineProvider;
61
+ import android.view.animation.Animation;
62
+ import android.view.animation.Animation.AnimationListener;
63
+ import android.view.animation.AnimationSet;
64
+ import android.view.animation.Transformation;
65
+ import android.widget.FrameLayout;
66
+ import android.widget.ImageView;
67
+ import android.widget.LinearLayout;
68
+ import android.widget.PopupWindow;
69
+ import android.widget.TextView;
70
+ import java.util.ArrayList;
71
+ import java.util.HashMap;
72
+ import java.util.Map;
73
+ import com.aetherlink.dexeditor.editor.EditView;
74
+ import com.aetherlink.dexeditor.R;
75
+ import com.aetherlink.dexeditor.editor.treeObserver.OnComputeInternalInsetsListener;
76
+ import com.aetherlink.dexeditor.editor.treeObserver.ViewTreeObserverReflection;
77
+ import com.aetherlink.dexeditor.editor.utils.LinkChecker;
78
+ import com.aetherlink.dexeditor.editor.utils.menuUtils.MenuAction;
79
+ import com.aetherlink.dexeditor.editor.utils.menuUtils.MenuItemConfig;
80
+ import com.aetherlink.dexeditor.editor.utils.menuUtils.MenuItemData;
81
+ import com.aetherlink.dexeditor.editor.utils.menuUtils.ViewFader;
82
+ import org.json.JSONArray;
83
+ import org.json.JSONObject;
84
+
85
+ /*
86
+ * Author : @developer-krushna (Krushna Chandra)
87
+ * Ideas copied from Android System FloatingToolBar.java class
88
+ * Fixed bug, comments, and optimization done by ChatGPT
89
+ */
90
+
91
+ public class ClipboardPanel {
92
+ // ===============================================
93
+ // CLASS VARIABLES AND CONSTANTS
94
+ // ===============================================
95
+
96
+ protected EditView _editView;
97
+ private Context _context;
98
+
99
+ // Custom popup menu variables
100
+ private PopupWindow _customPopupWindow;
101
+ private FrameLayout _popupContentHolder;
102
+ private FrameLayout _contentContainer;
103
+ private LinearLayout _primaryMenu;
104
+ private LinearLayout _secondaryMenu;
105
+ private View _upButton;
106
+ private View _divider;
107
+ private boolean _isExpanded = false;
108
+ private boolean _isAnimating = false;
109
+
110
+ private Rect _caretRect;
111
+ private boolean _isSelectionMode;
112
+
113
+ // Menu display mode
114
+ private MenuDisplayMode _menuDisplayMode = MenuDisplayMode.ICON_ONLY;
115
+
116
+ // Menu item dimensions
117
+ private int _menuItemHeight;
118
+ private int _menuItemMinWidth;
119
+ private int _menuIconSize;
120
+
121
+ // Sizes
122
+ private int _primaryWidth;
123
+ private int _primaryHeight;
124
+ private int _secondaryWidth;
125
+ private int _secondaryHeight;
126
+
127
+ // Overflow management
128
+ private boolean _hasOverflow = false;
129
+ private ArrayList<MenuItemData> _overflowItems = new ArrayList<>();
130
+
131
+ // Menu items list in JSON order
132
+ private ArrayList<String> _menuItems = new ArrayList<>();
133
+ private Map<String, MenuItemConfig> _allMenuItems = new HashMap<>();
134
+
135
+ // Animation variables
136
+ private AnimationSet _openOverflowAnimation = new AnimationSet(true);
137
+ private AnimationSet _closeOverflowAnimation = new AnimationSet(true);
138
+
139
+ private Handler _autoHideHandler = new Handler();
140
+ private static final long AUTO_HIDE_DELAY = 5000;
141
+ private Runnable _autoHideRunnable = new Runnable() {
142
+ @Override
143
+ public void run() {
144
+ hide();
145
+ }
146
+ };
147
+
148
+ // Faders
149
+ private ViewFader _primaryFader;
150
+ private ViewFader _secondaryFader;
151
+
152
+ // class fields
153
+ private OnComputeInternalInsetsListener mInvocationHandler;
154
+ private Region mTouchableRegion = new Region();
155
+
156
+ // Edge margin
157
+ private int _edgeMargin;
158
+
159
+ // Menu display mode enum
160
+ public enum MenuDisplayMode {
161
+ TEXT_ONLY,
162
+ ICON_ONLY,
163
+ ICON_AND_TEXT
164
+ }
165
+
166
+ // ===============================================
167
+ // CONSTRUCTOR AND INITIALIZATION
168
+ // ===============================================
169
+
170
+ public ClipboardPanel(EditView editView) {
171
+ _editView = editView;
172
+ _context = editView.getContext();
173
+
174
+ // Initialize dimensions
175
+ _menuItemHeight = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 48, _context.getResources().getDisplayMetrics());
176
+ _menuItemMinWidth = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 64, _context.getResources().getDisplayMetrics());
177
+ _menuIconSize = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 24, _context.getResources().getDisplayMetrics());
178
+ _edgeMargin = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 16, _context.getResources().getDisplayMetrics());
179
+
180
+ // Initialize all menu items configuration
181
+ initializeAllMenuItems();
182
+
183
+ initCustomPopup();
184
+ }
185
+
186
+ // ===============================================
187
+ // MENU CONFIGURATION METHODS
188
+ // ===============================================
189
+
190
+ /** Initialize all possible menu items with their configurations */
191
+ private void initializeAllMenuItems() {
192
+ _allMenuItems.clear();
193
+ _allMenuItems.put("panel_btn_select", new MenuItemConfig("Select", R.drawable.ic_select, MenuAction.SELECT));
194
+ _allMenuItems.put("panel_btn_copy", new MenuItemConfig("Copy", R.drawable.ic_copy, MenuAction.COPY));
195
+ _allMenuItems.put("panel_btn_cut", new MenuItemConfig("Cut", R.drawable.ic_cut, MenuAction.CUT));
196
+ _allMenuItems.put("panel_btn_paste", new MenuItemConfig("Paste", R.drawable.ic_paste, MenuAction.PASTE));
197
+ _allMenuItems.put("panel_btn_select_all", new MenuItemConfig("Select All", R.drawable.ic_select_all, MenuAction.SELECT_ALL));
198
+ _allMenuItems.put("share_btn", new MenuItemConfig("Share", R.drawable.ic_share, MenuAction.SHARE));
199
+ _allMenuItems.put("goto_btn", new MenuItemConfig("Go to", R.drawable.ic_goto, MenuAction.GOTO));
200
+ _allMenuItems.put("comment_btn", new MenuItemConfig("Toggle comment", R.drawable.ic_toggle_comment, MenuAction.TOGGLE_COMMENT));
201
+ _allMenuItems.put("openLink_btn", new MenuItemConfig("Open link", R.drawable.ic_open_link, MenuAction.OPEN_LINK));
202
+ _allMenuItems.put("panel_btn_translate", new MenuItemConfig("Translate", R.drawable.ic_translate, MenuAction.TRANSLATE));
203
+ _allMenuItems.put("delete_btn", new MenuItemConfig("Delete", R.drawable.ic_delete, MenuAction.DELETE));
204
+ }
205
+
206
+ /** Load menu configuration from JSON with sequence */
207
+ public void loadMenuConfiguration() {
208
+ SharedPreferences prefs = _context.getSharedPreferences("editor_prefs", Context.MODE_PRIVATE);
209
+ String jsonConfig = prefs.getString("menu_order", null);
210
+
211
+ // If not exists, save default config first
212
+ if (jsonConfig == null) {
213
+ jsonConfig = "[" +
214
+ "{\"id\":\"panel_btn_select\",\"title\":\"Select\",\"disabled\":false}," +
215
+ "{\"id\":\"panel_btn_select_all\",\"title\":\"Select All\",\"disabled\":false}," +
216
+ "{\"id\":\"panel_btn_copy\",\"title\":\"Copy\",\"disabled\":false}," +
217
+ "{\"id\":\"panel_btn_paste\",\"title\":\"Paste\",\"disabled\":false}," +
218
+ "{\"id\":\"goto_btn\",\"title\":\"Go To\",\"disabled\":true}," +
219
+ "{\"id\":\"panel_btn_cut\",\"title\":\"Cut\",\"disabled\":false}," +
220
+ "{\"id\":\"share_btn\",\"title\":\"Share\",\"disabled\":false}," +
221
+ "{\"id\":\"comment_btn\",\"title\":\"Toggle comment\",\"disabled\":false}," +
222
+ "{\"id\":\"openLink_btn\",\"title\":\"Open link\",\"disabled\":false}," +
223
+ "{\"id\":\"panel_btn_translate\",\"title\":\"Translate\",\"disabled\":false}," +
224
+ "{\"id\":\"delete_btn\",\"title\":\"Delete\",\"disabled\":false}" +
225
+ "]";
226
+ prefs.edit().putString("menu_order", jsonConfig).apply();
227
+ }
228
+
229
+ _menuItems.clear();
230
+
231
+ try {
232
+ JSONArray jsonArray = new JSONArray(jsonConfig);
233
+ for (int i = 0; i < jsonArray.length(); i++) {
234
+ JSONObject item = jsonArray.getJSONObject(i);
235
+ String id = item.getString("id");
236
+ boolean disabled = item.optBoolean("disabled", false);
237
+
238
+ // Only add if not disabled and exists in our configuration
239
+ if (!disabled && _allMenuItems.containsKey(id)) {
240
+ _menuItems.add(id);
241
+ }
242
+ }
243
+ } catch (Exception e) {
244
+ Log.e("ClipboardPanel", "Error loading menu config: " + e.getMessage());
245
+ // Fallback to default order
246
+ setDefaultMenuOrder();
247
+ }
248
+
249
+ // If no items loaded, use default order
250
+ if (_menuItems.isEmpty()) {
251
+ setDefaultMenuOrder();
252
+ }
253
+
254
+ Log.d("ClipboardPanel", "Loaded " + _menuItems.size() + " menu items in order: " + _menuItems);
255
+ }
256
+
257
+ /** Set default menu order when no configuration is available */
258
+ private void setDefaultMenuOrder() {
259
+ _menuItems.clear();
260
+ _menuItems.add("panel_btn_select");
261
+ _menuItems.add("panel_btn_select_all");
262
+ _menuItems.add("panel_btn_copy");
263
+ _menuItems.add("panel_btn_paste");
264
+ _menuItems.add("goto_btn");
265
+ _menuItems.add("panel_btn_cut");
266
+ _menuItems.add("share_btn");
267
+ _menuItems.add("comment_btn");
268
+ _menuItems.add("openLink_btn");
269
+ _menuItems.add("panel_btn_translate");
270
+ _menuItems.add("delete_btn");
271
+ }
272
+
273
+ // ===============================================
274
+ // POPUP WINDOW INITIALIZATION
275
+ // ===============================================
276
+
277
+ /** Initialize the custom popup window */
278
+ private void initCustomPopup() {
279
+ _popupContentHolder = new FrameLayout(_context);
280
+
281
+ _customPopupWindow = new PopupWindow(
282
+ _popupContentHolder,
283
+ ViewGroup.LayoutParams.WRAP_CONTENT,
284
+ ViewGroup.LayoutParams.WRAP_CONTENT
285
+ );
286
+ _customPopupWindow.setOutsideTouchable(true);
287
+ _customPopupWindow.setFocusable(false);
288
+ _customPopupWindow.setElevation(24f);
289
+ _customPopupWindow.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
290
+
291
+ _contentContainer = new FrameLayout(_context);
292
+
293
+ // Create background with stroke
294
+ GradientDrawable background = new GradientDrawable();
295
+ background.setColor(0xE6FFFFFF); // Semi-transparent white
296
+ background.setCornerRadius(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 12, _context.getResources().getDisplayMetrics()));
297
+ background.setStroke(
298
+ (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 1, _context.getResources().getDisplayMetrics()),
299
+ 0x33000000 // Semi-transparent black stroke
300
+ );
301
+
302
+ if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) {
303
+ // Enable proper clipping for rounded corners
304
+ _contentContainer.setClipToOutline(true);
305
+ _contentContainer.setOutlineProvider(new ViewOutlineProvider() {
306
+ @Override
307
+ public void getOutline(View view, Outline outline) {
308
+ float radius = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 12, _context.getResources().getDisplayMetrics());
309
+ outline.setRoundRect(0, 0, view.getWidth(), view.getHeight(), radius);
310
+ }
311
+ });
312
+ }
313
+
314
+ _contentContainer.setBackground(background);
315
+ _popupContentHolder.addView(_contentContainer, new FrameLayout.LayoutParams(
316
+ FrameLayout.LayoutParams.MATCH_PARENT,
317
+ FrameLayout.LayoutParams.MATCH_PARENT
318
+ ));
319
+
320
+ _divider = new View(_context);
321
+ _divider.setBackgroundColor(0xFFDDDDDD);
322
+ _customPopupWindow.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED);
323
+ }
324
+
325
+ // ===============================================
326
+ // TOUCH AND INSETS HANDLING
327
+ // ===============================================
328
+
329
+ /** Configure touch handling for the popup */
330
+ private void configTouch() {
331
+ mInvocationHandler = new OnComputeInternalInsetsListener();
332
+ ViewTreeObserverReflection.removeOnComputeInternalInsetsListener(_popupContentHolder.getViewTreeObserver());
333
+ ViewTreeObserverReflection.addOnComputeInternalInsetsListener(_popupContentHolder.getViewTreeObserver(), mInvocationHandler.getListener());
334
+ mInvocationHandler.setTouchRegion(mTouchableRegion);
335
+ }
336
+
337
+ /** Detach insets listener when hiding popup */
338
+ private void detachInsetsListener() {
339
+ if (mInvocationHandler != null && _popupContentHolder != null) {
340
+ ViewTreeObserverReflection.removeOnComputeInternalInsetsListener(_popupContentHolder.getViewTreeObserver());
341
+ mInvocationHandler = null;
342
+ }
343
+ }
344
+
345
+ // ===============================================
346
+ // MENU ITEM CREATION AND SETUP
347
+ // ===============================================
348
+
349
+ /** Set up menu items based on configuration and display mode */
350
+ private void setupMenuItems() {
351
+ Log.d("ClipboardPanel", "setupMenuItems called with mode: " + _menuDisplayMode);
352
+
353
+ // Load menu configuration first
354
+ loadMenuConfiguration();
355
+
356
+ // Clear existing
357
+ if (_primaryMenu != null) {
358
+ _primaryFader = null;
359
+ _primaryMenu.removeAllViews();
360
+ }
361
+ if (_secondaryMenu != null) {
362
+ _secondaryFader = null;
363
+ _secondaryMenu.removeAllViews();
364
+ if (_upButton != null) {
365
+ _secondaryMenu.removeView(_upButton);
366
+ _upButton = null;
367
+ }
368
+ }
369
+ _overflowItems.clear();
370
+ _hasOverflow = false;
371
+
372
+ // Create primary
373
+ _primaryMenu = new LinearLayout(_context);
374
+ _primaryMenu.setOrientation(LinearLayout.HORIZONTAL);
375
+ _primaryFader = new ViewFader(_primaryMenu);
376
+
377
+ // Create menu items in JSON-defined order
378
+ ArrayList<MenuItemData> allItems = new ArrayList<>();
379
+ for (String menuId : _menuItems) {
380
+ MenuItemConfig config = _allMenuItems.get(menuId);
381
+ if (config != null) {
382
+ if (config.action == MenuAction.SELECT) {
383
+ if (isTextSelected()) {
384
+ continue;
385
+ }
386
+ if (_editView.getBuffer().length() == 0) {
387
+ continue;
388
+ }
389
+ }
390
+ if (config.action == MenuAction.SELECT_ALL) {
391
+ if (_editView.isAllTextSelected()) {
392
+ continue;
393
+ }
394
+ if (_editView.getBuffer().length() == 0) {
395
+ continue;
396
+ }
397
+ }
398
+
399
+ if (config.action == MenuAction.PASTE) {
400
+ if (!canPaste()) {
401
+ continue;
402
+ }
403
+ if (!_editView.getEditedMode()) {
404
+ continue;
405
+ }
406
+ }
407
+
408
+ if (config.action == MenuAction.CUT) {
409
+ if (!isTextSelected()) {
410
+ continue;
411
+ }
412
+ if (!_editView.getEditedMode()) {
413
+ continue;
414
+ }
415
+ }
416
+
417
+ if (config.action == MenuAction.TOGGLE_COMMENT) {
418
+ if (!_editView.getEditedMode()) {
419
+ continue;
420
+ }
421
+ }
422
+
423
+ if (config.action == MenuAction.OPEN_LINK) {
424
+ if (!isTextSelected()) {
425
+ continue;
426
+ }
427
+ int selLen = _editView.getSelectionLength();
428
+ if (selLen < 500 && selLen > 0) {
429
+ String text = _editView.getSelectedText(); // small → safe
430
+ if (!LinkChecker.isLink(text)) continue;
431
+ } else {
432
+ continue;
433
+ }
434
+ }
435
+
436
+ if (config.action == MenuAction.COPY && !isTextSelected()) {
437
+ continue;
438
+ }
439
+
440
+ if (config.action == MenuAction.DELETE) {
441
+ if (!isTextSelected()) {
442
+ continue;
443
+ }
444
+ if (!_editView.getEditedMode()) {
445
+ continue;
446
+ }
447
+ }
448
+ if (config.action == MenuAction.SHARE && !isTextSelected()) {
449
+ continue;
450
+ }
451
+
452
+ if (config.action == MenuAction.TRANSLATE && !isTextSelected()) {
453
+ continue;
454
+ }
455
+
456
+ if (config.action == MenuAction.GOTO && !isTextSelected()) {
457
+ continue;
458
+ }
459
+
460
+ allItems.add(createMenuItem(config.title, config.iconRes, config.action));
461
+ }
462
+ }
463
+
464
+ // Get screen density and calculate available space more accurately
465
+ float density = _context.getResources().getDisplayMetrics().density;
466
+ int screenWidth = _editView.getWidth();
467
+
468
+ Log.d("ClipboardPanel", "Screen width: " + screenWidth + "px, Density: " + density + ", Total items: " + allItems.size());
469
+
470
+ ArrayList<MenuItemData> primaryItems = new ArrayList<>();
471
+ ArrayList<MenuItemData> secondaryItems = new ArrayList<>();
472
+
473
+ // If no overflow needed (all fit in primary), no expand button
474
+ int maxPrimaryItems = calculateMaxPrimaryItems(screenWidth, density);
475
+ boolean needsOverflow = allItems.size() > maxPrimaryItems;
476
+
477
+ Log.d("ClipboardPanel", "Max primary items possible: " + maxPrimaryItems + ", Needs overflow: " + needsOverflow);
478
+
479
+ // Simple logic: always show first N items in primary, rest in secondary
480
+ for (int i = 0; i < allItems.size(); i++) {
481
+ if (i < maxPrimaryItems) {
482
+ primaryItems.add(allItems.get(i));
483
+ } else {
484
+ secondaryItems.add(allItems.get(i));
485
+ _hasOverflow = needsOverflow;
486
+ }
487
+ }
488
+
489
+ // Add primary menu items
490
+ for (int i = 0; i < primaryItems.size(); i++) {
491
+ boolean isLastPrimary = (i == primaryItems.size() - 1) && !_hasOverflow;
492
+ addMenuItemToLayout(primaryItems.get(i), _primaryMenu, false, i, isLastPrimary);
493
+ }
494
+
495
+ // Add expand button only if we have overflow items
496
+ if (_hasOverflow && !secondaryItems.isEmpty()) {
497
+ addExpandButtonToPrimaryMenu();
498
+ _overflowItems.addAll(secondaryItems);
499
+ Log.d("ClipboardPanel", "Added expand button with " + secondaryItems.size() + " overflow items");
500
+
501
+ // Create secondary menu
502
+ _secondaryMenu = new LinearLayout(_context);
503
+ _secondaryMenu.setOrientation(LinearLayout.VERTICAL);
504
+ _secondaryFader = new ViewFader(_secondaryMenu);
505
+
506
+ createUpButton();
507
+ LinearLayout.LayoutParams dividerParams = new LinearLayout.LayoutParams(
508
+ LinearLayout.LayoutParams.MATCH_PARENT, 1);
509
+ _secondaryMenu.addView(_divider, dividerParams);
510
+
511
+ for (int i = 0; i < _overflowItems.size(); i++) {
512
+ addMenuItemToLayout(_overflowItems.get(i), _secondaryMenu, true, i, i == _overflowItems.size() - 1);
513
+ }
514
+ }
515
+
516
+ // Measure
517
+ _primaryMenu.measure(
518
+ View.MeasureSpec.UNSPECIFIED,
519
+ View.MeasureSpec.UNSPECIFIED
520
+ );
521
+ _primaryWidth = _primaryMenu.getMeasuredWidth();
522
+ _primaryHeight = _primaryMenu.getMeasuredHeight();
523
+
524
+ if (_hasOverflow) {
525
+ _secondaryMenu.measure(
526
+ View.MeasureSpec.UNSPECIFIED,
527
+ View.MeasureSpec.UNSPECIFIED
528
+ );
529
+ _secondaryWidth = _secondaryMenu.getMeasuredWidth();
530
+ _secondaryHeight = _secondaryMenu.getMeasuredHeight();
531
+ }
532
+
533
+ allItems.clear();
534
+
535
+ Log.d("ClipboardPanel", "Final distribution: " + primaryItems.size() + " primary, " +
536
+ secondaryItems.size() + " secondary, overflow: " + _hasOverflow);
537
+ }
538
+
539
+ /** Calculate maximum number of primary menu items that can fit on screen */
540
+ private int calculateMaxPrimaryItems(int screenWidth, float density) {
541
+ // Convert screen width to dp for consistent calculations
542
+ int screenWidthDp = (int) (screenWidth / density);
543
+
544
+ Log.d("ClipboardPanel", "Screen width in dp: " + screenWidthDp);
545
+
546
+ // Calculate item width based on display mode
547
+ int itemWidthDp;
548
+ switch (_menuDisplayMode) {
549
+ case TEXT_ONLY:
550
+ itemWidthDp = 60; // Approximate width for text items
551
+ break;
552
+ case ICON_ONLY:
553
+ itemWidthDp = 56; // Compact width for icon-only
554
+ break;
555
+ case ICON_AND_TEXT:
556
+ itemWidthDp = 90; // Wider for icon + text
557
+ break;
558
+ default:
559
+ itemWidthDp = 80;
560
+ }
561
+
562
+ // Expand button width
563
+ int expandButtonWidthDp = 48;
564
+
565
+ // Available width (use 85% of screen width)
566
+ int availableWidthDp = (int) (screenWidthDp * 0.85f);
567
+
568
+ // Calculate how many items we can fit
569
+ int maxItemsWithoutOverflow = availableWidthDp / itemWidthDp;
570
+
571
+ // With overflow button, we need space for it
572
+ int maxItemsWithOverflow = (availableWidthDp - expandButtonWidthDp) / itemWidthDp;
573
+
574
+ // We always want at least 2 items in primary
575
+ int minItems = 2;
576
+ int maxItems = Math.max(minItems, maxItemsWithOverflow);
577
+
578
+ // For icon-only mode, we can be more aggressive
579
+ if (_menuDisplayMode == MenuDisplayMode.ICON_ONLY) {
580
+ maxItems = Math.min(5, maxItemsWithOverflow + 1); // Allow one more since icons are
581
+ // compact
582
+ }
583
+
584
+ // For text-only mode, follow Android system behavior (usually 4 items)
585
+ if (_menuDisplayMode == MenuDisplayMode.TEXT_ONLY) {
586
+ maxItems = Math.min(4, maxItemsWithOverflow);
587
+ }
588
+
589
+ Log.d("ClipboardPanel", "Item width: " + itemWidthDp + "dp, Available: " + availableWidthDp +
590
+ "dp, Max items: " + maxItems);
591
+
592
+ return maxItems;
593
+ }
594
+
595
+ /** Add expand button to primary menu for accessing overflow items */
596
+ private void addExpandButtonToPrimaryMenu() {
597
+ LayoutInflater inflater = LayoutInflater.from(_primaryMenu.getContext());
598
+ View expandButtonView = inflater.inflate(R.layout.expand_button, _primaryMenu, false);
599
+
600
+ ImageView expandIcon = expandButtonView.findViewById(R.id.expandIcon);
601
+ expandIcon.setImageResource(R.drawable.ic_more);
602
+ expandIcon.getDrawable().setTint(Color.BLACK);
603
+
604
+ // Set proper icon size - this is the key fix
605
+ ViewGroup.LayoutParams iconParams = expandIcon.getLayoutParams();
606
+ if (iconParams != null) {
607
+ iconParams.width = _menuIconSize;
608
+ iconParams.height = _menuIconSize;
609
+ expandIcon.setLayoutParams(iconParams);
610
+ } else {
611
+ // If no layout params, create new ones
612
+ expandIcon.setLayoutParams(new LinearLayout.LayoutParams(_menuIconSize, _menuIconSize));
613
+ }
614
+
615
+ // Center the icon
616
+ if (expandButtonView instanceof LinearLayout) {
617
+ ((LinearLayout) expandButtonView).setGravity(Gravity.CENTER);
618
+ }
619
+
620
+ // Set fixed height for expand button
621
+ ViewGroup.LayoutParams params = expandButtonView.getLayoutParams();
622
+ if (params != null) {
623
+ params.height = _menuItemHeight;
624
+ if (params instanceof LinearLayout.LayoutParams) {
625
+ ((LinearLayout.LayoutParams) params).width = _menuItemHeight; // Make it square
626
+ }
627
+ }
628
+
629
+ expandButtonView.setOnClickListener(new View.OnClickListener() {
630
+ @Override
631
+ public void onClick(View view) {
632
+ if (!_isAnimating) {
633
+ toggleExpand();
634
+ }
635
+ }
636
+ });
637
+
638
+ applyPerfectRoundedBackground(expandButtonView, false, true, false, true);
639
+ _primaryMenu.addView(expandButtonView);
640
+ }
641
+
642
+ /** Create up button for secondary menu navigation */
643
+ private void createUpButton() {
644
+ if (_upButton != null || _secondaryMenu == null) return;
645
+
646
+ LayoutInflater inflater = LayoutInflater.from(_context);
647
+ _upButton = inflater.inflate(R.layout.expand_button, _secondaryMenu, false);
648
+
649
+ ImageView icon = _upButton.findViewById(R.id.expandIcon);
650
+ icon.setImageResource(R.drawable.ic_arrow_back);
651
+
652
+ // Set proper icon size - same as more icon
653
+ ViewGroup.LayoutParams iconParams = icon.getLayoutParams();
654
+ if (iconParams != null) {
655
+ iconParams.width = _menuIconSize;
656
+ iconParams.height = _menuIconSize;
657
+ icon.setLayoutParams(iconParams);
658
+ } else {
659
+ icon.setLayoutParams(new LinearLayout.LayoutParams(_menuIconSize, _menuIconSize));
660
+ }
661
+
662
+ // Set fixed height for up button
663
+ ViewGroup.LayoutParams params = _upButton.getLayoutParams();
664
+ if (params != null) {
665
+ params.height = _menuItemHeight;
666
+ if (params instanceof LinearLayout.LayoutParams) {
667
+ ((LinearLayout.LayoutParams) params).width = LinearLayout.LayoutParams.MATCH_PARENT;
668
+ }
669
+ }
670
+ _upButton.setLayoutParams(params);
671
+
672
+ // KEY FIX: Align icon to left instead of center
673
+ if (_upButton instanceof LinearLayout) {
674
+ ((LinearLayout) _upButton).setGravity(Gravity.CENTER_VERTICAL | Gravity.START); // Left
675
+ // alignment
676
+ }
677
+
678
+ // Add left padding to position the icon properly
679
+ int leftPadding = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 16, _context.getResources().getDisplayMetrics());
680
+ int verticalPadding = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 12, _context.getResources().getDisplayMetrics());
681
+ _upButton.setPadding(leftPadding, verticalPadding, leftPadding, verticalPadding);
682
+
683
+ _upButton.setOnClickListener(new View.OnClickListener() {
684
+ @Override
685
+ public void onClick(View v) {
686
+ if (!_isAnimating) {
687
+ collapseMenu();
688
+ }
689
+ }
690
+ });
691
+
692
+ applyPerfectRoundedBackground(_upButton, true, true, false, false);
693
+ _secondaryMenu.addView(_upButton, 0);
694
+ }
695
+
696
+ /** Create a menu item data object */
697
+ private MenuItemData createMenuItem(String title, int iconRes, MenuAction action) {
698
+ return new MenuItemData(title, iconRes, action);
699
+ }
700
+
701
+ /** Add menu item to layout with proper styling */
702
+ private void addMenuItemToLayout(final MenuItemData menuItem, LinearLayout layout, final boolean isVertical, final int position, final boolean isEdgeItem) {
703
+ LayoutInflater inflater = LayoutInflater.from(layout.getContext());
704
+ View menuItemView = inflater.inflate(R.layout.menu_item, layout, false);
705
+
706
+ // Set fixed height for menu items
707
+ ViewGroup.LayoutParams params = menuItemView.getLayoutParams();
708
+ if (params != null) {
709
+ params.height = _menuItemHeight;
710
+ }
711
+
712
+ ImageView icon = menuItemView.findViewById(R.id.menuIcon);
713
+ TextView titleView = menuItemView.findViewById(R.id.menuTitle);
714
+
715
+ // Set icon size
716
+ ViewGroup.LayoutParams iconParams = icon.getLayoutParams();
717
+ if (iconParams != null) {
718
+ iconParams.width = _menuIconSize;
719
+ iconParams.height = _menuIconSize;
720
+ icon.setLayoutParams(iconParams);
721
+ }
722
+
723
+ // Apply display mode with special handling for secondary menu
724
+ applyDisplayMode(menuItemView, menuItem, isVertical);
725
+
726
+ // Add tooltip only for icon-only mode
727
+ if (_menuDisplayMode == MenuDisplayMode.ICON_ONLY) {
728
+ if (menuItemView != null && Build.VERSION.SDK_INT >= 26) {
729
+ menuItemView.setTooltipText(menuItem.title);
730
+ }
731
+ }
732
+
733
+ if (isVertical) {
734
+ // Vertical layout - full width
735
+ LinearLayout.LayoutParams layoutParams = (LinearLayout.LayoutParams) menuItemView.getLayoutParams();
736
+ layoutParams.width = LinearLayout.LayoutParams.MATCH_PARENT;
737
+ layoutParams.height = _menuItemHeight;
738
+ menuItemView.setLayoutParams(layoutParams);
739
+
740
+ // Center content vertically for vertical layout
741
+ if (menuItemView instanceof LinearLayout) {
742
+ ((LinearLayout) menuItemView).setGravity(Gravity.CENTER_VERTICAL);
743
+ }
744
+ } else {
745
+ // Horizontal layout - fixed width based on content
746
+ LinearLayout.LayoutParams layoutParams = (LinearLayout.LayoutParams) menuItemView.getLayoutParams();
747
+
748
+ // Calculate width based on content
749
+ int itemWidth;
750
+ titleView.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED);
751
+ switch (_menuDisplayMode) {
752
+ case TEXT_ONLY:
753
+ itemWidth = titleView.getMeasuredWidth() + (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 32, _context.getResources().getDisplayMetrics());
754
+ break;
755
+ case ICON_ONLY:
756
+ itemWidth = _menuItemHeight; // Square for icon-only
757
+ break;
758
+ case ICON_AND_TEXT:
759
+ itemWidth = titleView.getMeasuredWidth() + _menuIconSize + (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 40, _context.getResources().getDisplayMetrics());
760
+ break;
761
+ default:
762
+ itemWidth = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 80, _context.getResources().getDisplayMetrics());
763
+ }
764
+
765
+ // Ensure minimum width
766
+ int minWidth = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 56, _context.getResources().getDisplayMetrics());
767
+ layoutParams.width = Math.max(itemWidth, minWidth);
768
+ layoutParams.height = _menuItemHeight;
769
+ menuItemView.setLayoutParams(layoutParams);
770
+
771
+ // Center content both vertically and horizontally for horizontal layout
772
+ if (menuItemView instanceof LinearLayout) {
773
+ ((LinearLayout) menuItemView).setGravity(Gravity.CENTER);
774
+ }
775
+ }
776
+
777
+ // Apply perfect rounded background based on position and menu type
778
+ if (isVertical) {
779
+ boolean isFirst = position == 0;
780
+ boolean isLast = isEdgeItem;
781
+ applyPerfectRoundedBackground(menuItemView, isFirst, isFirst, isLast, isLast);
782
+ } else {
783
+ boolean isFirst = position == 0;
784
+ boolean isLast = isEdgeItem;
785
+ applyPerfectRoundedBackground(menuItemView, isFirst, false, isLast, false);
786
+ }
787
+
788
+ menuItemView.setOnClickListener(new View.OnClickListener() {
789
+ @Override
790
+ public void onClick(View view) {
791
+ handleCustomMenuAction(menuItem.action);
792
+ if (!isVertical) {
793
+ hide();
794
+ }
795
+ }
796
+ });
797
+
798
+ layout.addView(menuItemView);
799
+ }
800
+
801
+ // ===============================================
802
+ // DISPLAY MODE AND STYLING
803
+ // ===============================================
804
+
805
+ /** Apply display mode to menu item (icon only, text only, or both) */
806
+ private void applyDisplayMode(View menuItemView, MenuItemData menuItem, boolean isSecondaryMenu) {
807
+ ImageView icon = menuItemView.findViewById(R.id.menuIcon);
808
+ TextView titleView = menuItemView.findViewById(R.id.menuTitle);
809
+
810
+ Log.d("ClipboardPanel", "applyDisplayMode: " + _menuDisplayMode + " for " + menuItem.title + ", secondary: " + isSecondaryMenu);
811
+
812
+ // Primary menu follows the selected display mode
813
+ switch (_menuDisplayMode) {
814
+ case TEXT_ONLY:
815
+ icon.setVisibility(View.GONE);
816
+ titleView.setVisibility(View.VISIBLE);
817
+ Log.d("ClipboardPanel", "TEXT_ONLY - hiding icon for: " + menuItem.title);
818
+ break;
819
+ case ICON_ONLY:
820
+ icon.setVisibility(View.VISIBLE);
821
+ titleView.setVisibility(View.GONE);
822
+ Log.d("ClipboardPanel", "ICON_ONLY - hiding text for: " + menuItem.title);
823
+ break;
824
+ case ICON_AND_TEXT:
825
+ icon.setVisibility(View.VISIBLE);
826
+ titleView.setVisibility(View.VISIBLE);
827
+ Log.d("ClipboardPanel", "ICON_AND_TEXT - showing both for: " + menuItem.title);
828
+ break;
829
+ }
830
+
831
+ // For secondary menu, override to show both unless TEXT_ONLY (hide icon)
832
+ if (isSecondaryMenu && _menuDisplayMode != MenuDisplayMode.TEXT_ONLY) {
833
+ icon.setVisibility(View.VISIBLE);
834
+ titleView.setVisibility(View.VISIBLE);
835
+ Log.d("ClipboardPanel", "Secondary menu - showing icon+text for: " + menuItem.title);
836
+ }
837
+
838
+ // Set the content
839
+ icon.setImageResource(menuItem.iconRes);
840
+ titleView.setText(menuItem.title);
841
+ }
842
+
843
+ /** Apply rounded background with specific corner rounding */
844
+ private void applyPerfectRoundedBackground(View view, boolean topLeft, boolean topRight, boolean bottomLeft, boolean bottomRight) {
845
+ if (view == null) return;
846
+
847
+ float cornerRadius = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 4, _context.getResources().getDisplayMetrics());
848
+ float[] radii = new float[]{
849
+ topLeft ? cornerRadius : 0, topLeft ? cornerRadius : 0,
850
+ topRight ? cornerRadius : 0, topRight ? cornerRadius : 0,
851
+ bottomRight ? cornerRadius : 0, bottomRight ? cornerRadius : 0,
852
+ bottomLeft ? cornerRadius : 0, bottomLeft ? cornerRadius : 0
853
+ };
854
+
855
+ if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) {
856
+ // Create a transparent background with proper corners for ripple mask
857
+ GradientDrawable transparentBackground = new GradientDrawable();
858
+ transparentBackground.setColor(Color.TRANSPARENT);
859
+ transparentBackground.setCornerRadii(radii);
860
+
861
+ // Create mask with same corners for proper ripple bounds
862
+ GradientDrawable mask = new GradientDrawable();
863
+ mask.setColor(Color.WHITE); // Color doesn't matter for mask, just needs to be opaque
864
+ mask.setCornerRadii(radii);
865
+
866
+ RippleDrawable rippleDrawable = new RippleDrawable(
867
+ getRippleColor(),
868
+ transparentBackground, // Transparent background but maintains corner shape
869
+ mask
870
+ );
871
+ view.setBackground(rippleDrawable);
872
+ } else {
873
+ // For older versions - transparent background with pressed state
874
+ GradientDrawable pressedBackground = new GradientDrawable();
875
+ pressedBackground.setColor(getPressedColor());
876
+ pressedBackground.setCornerRadii(radii);
877
+
878
+ GradientDrawable normalBackground = new GradientDrawable();
879
+ normalBackground.setColor(Color.TRANSPARENT);
880
+ normalBackground.setCornerRadii(radii);
881
+
882
+ StateListDrawable stateListDrawable = new StateListDrawable();
883
+ stateListDrawable.addState(new int[]{android.R.attr.state_pressed}, pressedBackground);
884
+ stateListDrawable.addState(new int[]{}, normalBackground);
885
+ view.setBackground(stateListDrawable);
886
+ }
887
+
888
+ // Set padding to ensure content stays within bounds
889
+ int padding = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 16, _context.getResources().getDisplayMetrics());
890
+ view.setPadding(padding, padding, padding, padding);
891
+ }
892
+
893
+ /** Get pressed state color */
894
+ private int getPressedColor() {
895
+ return 0x20000000;
896
+ }
897
+
898
+ /** Get ripple color from theme or default */
899
+ private ColorStateList getRippleColor() {
900
+ try {
901
+ TypedArray a = _context.obtainStyledAttributes(new int
902
+ []{android.R.attr.colorControlHighlight});
903
+ int color = a.getColor(0, 0x20000000);
904
+ a.recycle();
905
+ return ColorStateList.valueOf(color);
906
+ } catch (Exception e) {
907
+ return ColorStateList.valueOf(0x20000000);
908
+ }
909
+ }
910
+
911
+ // ===============================================
912
+ // PUBLIC API METHODS
913
+ // ===============================================
914
+
915
+ /** Set menu display mode and force recreation if showing */
916
+ public void setMenuDisplayMode(MenuDisplayMode mode) {
917
+ Log.d("ClipboardPanel", "SETTING MENU MODE: " + mode);
918
+ _menuDisplayMode = mode;
919
+
920
+ // If popup is showing, hide it so it recreates with new mode
921
+ if (_customPopupWindow != null && _customPopupWindow.isShowing()) {
922
+ Log.d("ClipboardPanel", "Popup is showing, hiding to force recreation");
923
+ hideCustomPopup();
924
+ }
925
+ }
926
+
927
+ public MenuDisplayMode getMenuDisplayMode() {
928
+ return _menuDisplayMode;
929
+ }
930
+
931
+ public void setTextOnlyMenu() {
932
+ setMenuDisplayMode(MenuDisplayMode.TEXT_ONLY);
933
+ }
934
+
935
+ public void setIconOnlyMenu() {
936
+ setMenuDisplayMode(MenuDisplayMode.ICON_ONLY);
937
+ }
938
+
939
+ public void setIconAndTextMenu() {
940
+ setMenuDisplayMode(MenuDisplayMode.ICON_AND_TEXT);
941
+ }
942
+
943
+ /** Force recreation of the popup */
944
+ public void forceRecreate() {
945
+ if (_customPopupWindow != null && _customPopupWindow.isShowing()) {
946
+ hideCustomPopup();
947
+ }
948
+ }
949
+
950
+ public Context getContext() {
951
+ return _context;
952
+ }
953
+
954
+ /** Show the clipboard panel */
955
+ public void show() {
956
+ showAtLocation(null);
957
+ }
958
+
959
+ /** Show the clipboard panel at specified location */
960
+ public void showAtLocation(Rect preferredRect) {
961
+ showCustomPopup(preferredRect);
962
+ startAutoHideTimer();
963
+ }
964
+
965
+ /** Hide the clipboard panel */
966
+ public void hide() {
967
+ cancelAutoHideTimer();
968
+ hideCustomPopup();
969
+ }
970
+
971
+ /** Update panel position (e.g., when caret moves) */
972
+ public void updatePosition() {
973
+ if (_customPopupWindow.isShowing()) {
974
+ int popupWidth = _customPopupWindow.getWidth();
975
+ int popupHeight = _customPopupWindow.getHeight();
976
+ Rect positionRect = new Rect();
977
+ calculateOptimalPosition(positionRect, popupWidth, popupHeight);
978
+ _customPopupWindow.update(
979
+ positionRect.left,
980
+ positionRect.top,
981
+ popupWidth,
982
+ popupHeight
983
+ );
984
+ }
985
+ }
986
+
987
+ // ===============================================
988
+ // EXPANSION AND ANIMATION METHODS
989
+ // ===============================================
990
+
991
+ /** Toggle between expanded and collapsed state */
992
+ private void toggleExpand() {
993
+ if (_isExpanded) {
994
+ collapseMenu();
995
+ } else {
996
+ expandMenu();
997
+ }
998
+ }
999
+
1000
+ /** Expand menu to show overflow items */
1001
+ private void expandMenu() {
1002
+ if (_isAnimating || !_hasOverflow) return;
1003
+
1004
+ _isAnimating = true;
1005
+ _isExpanded = true;
1006
+
1007
+ _primaryFader.fadeOut(true);
1008
+
1009
+ final int targetWidth = _secondaryWidth;
1010
+ final int targetHeight = _secondaryHeight;
1011
+ final int startWidth = _contentContainer.getWidth();
1012
+ final int startHeight = _contentContainer.getHeight();
1013
+ final int popupWidth = _customPopupWindow.getWidth();
1014
+ final float startY = _contentContainer.getY();
1015
+ final boolean morphUpwards = false; // Assume down
1016
+
1017
+ Animation widthAnimation = new Animation() {
1018
+ @Override
1019
+ protected void applyTransformation(float interpolatedTime, Transformation t) {
1020
+ ViewGroup.LayoutParams params = _contentContainer.getLayoutParams();
1021
+ params.width = (int) (startWidth + interpolatedTime * (targetWidth - startWidth));
1022
+ _contentContainer.setLayoutParams(params);
1023
+ // Anchor from right
1024
+ _contentContainer.setX(popupWidth - params.width);
1025
+ }
1026
+ };
1027
+ widthAnimation.setDuration(240);
1028
+
1029
+ Animation heightAnimation = new Animation() {
1030
+ @Override
1031
+ protected void applyTransformation(float interpolatedTime, Transformation t) {
1032
+ ViewGroup.LayoutParams params = _contentContainer.getLayoutParams();
1033
+ params.height = (int) (startHeight + interpolatedTime * (targetHeight - startHeight));
1034
+ _contentContainer.setLayoutParams(params);
1035
+ if (morphUpwards) {
1036
+ _contentContainer.setY(startY - (params.height - startHeight));
1037
+ }
1038
+ }
1039
+ };
1040
+ heightAnimation.setDuration(180);
1041
+ heightAnimation.setStartOffset(60);
1042
+
1043
+ _openOverflowAnimation = new AnimationSet(true);
1044
+ _openOverflowAnimation.addAnimation(widthAnimation);
1045
+ _openOverflowAnimation.addAnimation(heightAnimation);
1046
+ _openOverflowAnimation.setAnimationListener(new AnimationListener() {
1047
+ @Override
1048
+ public void onAnimationStart(Animation animation) {}
1049
+
1050
+ @Override
1051
+ public void onAnimationEnd(Animation animation) {
1052
+ setSecondaryAsContent();
1053
+ _secondaryFader.fadeIn(true);
1054
+ _isAnimating = false;
1055
+ // Update touch region after animation completes
1056
+ updateTouchRegion();
1057
+ }
1058
+
1059
+ @Override
1060
+ public void onAnimationRepeat(Animation animation) {}
1061
+ });
1062
+
1063
+ _contentContainer.startAnimation(_openOverflowAnimation);
1064
+ }
1065
+
1066
+ /** Collapse menu back to primary items */
1067
+ private void collapseMenu() {
1068
+ if (_isAnimating || !_hasOverflow) return;
1069
+
1070
+ _isAnimating = true;
1071
+ _isExpanded = false;
1072
+
1073
+ _secondaryFader.fadeOut(true);
1074
+
1075
+ final int targetWidth = _primaryWidth;
1076
+ final int targetHeight = _primaryHeight;
1077
+ final int startWidth = _contentContainer.getWidth();
1078
+ final int startHeight = _contentContainer.getHeight();
1079
+ final int popupWidth = _customPopupWindow.getWidth();
1080
+ final float startY = _contentContainer.getY();
1081
+ final boolean morphUpwards = false;
1082
+
1083
+ Animation widthAnimation = new Animation() {
1084
+ @Override
1085
+ protected void applyTransformation(float interpolatedTime, Transformation t) {
1086
+ ViewGroup.LayoutParams params = _contentContainer.getLayoutParams();
1087
+ params.width = (int) (startWidth + interpolatedTime * (targetWidth - startWidth));
1088
+ _contentContainer.setLayoutParams(params);
1089
+ // Anchor from right during collapse
1090
+ _contentContainer.setX(popupWidth - params.width);
1091
+ }
1092
+ };
1093
+ widthAnimation.setDuration(150);
1094
+ widthAnimation.setStartOffset(150);
1095
+
1096
+ Animation heightAnimation = new Animation() {
1097
+ @Override
1098
+ protected void applyTransformation(float interpolatedTime, Transformation t) {
1099
+ ViewGroup.LayoutParams params = _contentContainer.getLayoutParams();
1100
+ params.height = (int) (startHeight + interpolatedTime * (targetHeight - startHeight));
1101
+ _contentContainer.setLayoutParams(params);
1102
+ if (morphUpwards) {
1103
+ _contentContainer.setY(startY + (startHeight - params.height));
1104
+ }
1105
+ }
1106
+ };
1107
+ heightAnimation.setDuration(210);
1108
+
1109
+ _closeOverflowAnimation = new AnimationSet(true);
1110
+ _closeOverflowAnimation.addAnimation(widthAnimation);
1111
+ _closeOverflowAnimation.addAnimation(heightAnimation);
1112
+ _closeOverflowAnimation.setAnimationListener(new AnimationListener() {
1113
+ @Override
1114
+ public void onAnimationStart(Animation animation) {}
1115
+
1116
+ @Override
1117
+ public void onAnimationEnd(Animation animation) {
1118
+ setPrimaryAsContent();
1119
+ _primaryFader.fadeIn(true);
1120
+ _isAnimating = false;
1121
+ // Update touch region after animation completes
1122
+ updateTouchRegion();
1123
+ }
1124
+
1125
+ @Override
1126
+ public void onAnimationRepeat(Animation animation) {}
1127
+ });
1128
+
1129
+ _contentContainer.startAnimation(_closeOverflowAnimation);
1130
+ }
1131
+
1132
+ /** Switch to secondary menu content */
1133
+ private void setSecondaryAsContent() {
1134
+ _contentContainer.removeAllViews();
1135
+ _contentContainer.addView(_secondaryMenu);
1136
+ ViewGroup.LayoutParams params = _contentContainer.getLayoutParams();
1137
+ params.width = _secondaryWidth;
1138
+ params.height = _secondaryHeight;
1139
+ _contentContainer.setLayoutParams(params);
1140
+ _contentContainer.setX(_customPopupWindow.getWidth() - _secondaryWidth);
1141
+ _contentContainer.setY(0);
1142
+ _contentContainer.requestLayout(); // Trigger layout update
1143
+ _secondaryFader.fadeIn(true);
1144
+
1145
+ // CRITICAL: Update touch region after switching to secondary menu
1146
+ updateTouchRegion();
1147
+ }
1148
+
1149
+ /** Switch to primary menu content */
1150
+ private void setPrimaryAsContent() {
1151
+ _contentContainer.removeAllViews();
1152
+ _contentContainer.addView(_primaryMenu);
1153
+ ViewGroup.LayoutParams params = _contentContainer.getLayoutParams();
1154
+ params.width = _primaryWidth;
1155
+ params.height = _primaryHeight;
1156
+ _contentContainer.setLayoutParams(params);
1157
+ _contentContainer.setX(0);
1158
+ _contentContainer.setY(0);
1159
+ _contentContainer.requestLayout(); // Trigger layout update
1160
+ _primaryFader.fadeIn(false);
1161
+
1162
+ // CRITICAL: Update touch region after switching to primary menu
1163
+ updateTouchRegion();
1164
+ }
1165
+
1166
+ // ===============================================
1167
+ // TOUCH REGION AND POSITIONING
1168
+ // ===============================================
1169
+
1170
+ /** Update the touchable region for proper touch handling */
1171
+ private void updateTouchRegion() {
1172
+ if (_contentContainer != null && mInvocationHandler != null) {
1173
+ // Use post to ensure layout is complete
1174
+ _contentContainer.post(new Runnable() {
1175
+ @Override
1176
+ public void run() {
1177
+ if (mInvocationHandler == null) {
1178
+ return; // Handler detached, ignore
1179
+ }
1180
+ mTouchableRegion.setEmpty();
1181
+ Rect bounds = getContentBounds();
1182
+ mTouchableRegion.op(bounds.left, bounds.top, bounds.right, bounds.bottom, Region.Op.UNION);
1183
+ mInvocationHandler.setTouchRegion(mTouchableRegion);
1184
+ Log.d("ClipboardPanel", "Touch region updated: " + bounds.toString());
1185
+ }
1186
+ });
1187
+ }
1188
+ }
1189
+
1190
+ /** Get content bounds for touch region calculation */
1191
+ private Rect getContentBounds() {
1192
+ Rect bounds = new Rect();
1193
+ _contentContainer.getGlobalVisibleRect(bounds);
1194
+ return bounds;
1195
+ }
1196
+
1197
+ /** Show custom popup at specified location */
1198
+ private void showCustomPopup(Rect preferredRect) {
1199
+ if (_customPopupWindow.isShowing()) {
1200
+ return;
1201
+ }
1202
+
1203
+ Log.d("ClipboardPanel", "showCustomPopup with mode: " + _menuDisplayMode);
1204
+
1205
+ setupMenuItems();
1206
+
1207
+ int popupWidth = _primaryWidth;
1208
+ int popupHeight = _primaryHeight;
1209
+ if (_hasOverflow) {
1210
+ popupWidth = Math.max(popupWidth, _secondaryWidth);
1211
+ popupHeight = Math.max(popupHeight, _secondaryHeight);
1212
+ }
1213
+
1214
+ _customPopupWindow.setWidth(popupWidth);
1215
+ _customPopupWindow.setHeight(popupHeight);
1216
+
1217
+ resetMenuToCollapsed();
1218
+
1219
+ Rect positionRect = new Rect();
1220
+ calculateOptimalPosition(positionRect, popupWidth, popupHeight);
1221
+
1222
+ _customPopupWindow.showAtLocation(
1223
+ _editView,
1224
+ Gravity.NO_GRAVITY,
1225
+ positionRect.left,
1226
+ positionRect.top
1227
+ );
1228
+
1229
+ // Wait a frame for the view to be attached
1230
+ _popupContentHolder.post(new Runnable() {
1231
+ @Override
1232
+ public void run() {
1233
+ configTouch();
1234
+ updateTouchRegion();
1235
+ }
1236
+ });
1237
+ }
1238
+
1239
+ /** Reset menu to collapsed state */
1240
+ private void resetMenuToCollapsed() {
1241
+ _isExpanded = false;
1242
+ _isAnimating = false;
1243
+ setPrimaryAsContent();
1244
+ }
1245
+
1246
+ /** Hide custom popup and clean up */
1247
+ private void hideCustomPopup() {
1248
+ if (_customPopupWindow.isShowing()) {
1249
+ _customPopupWindow.dismiss();
1250
+ }
1251
+ detachInsetsListener();
1252
+ }
1253
+
1254
+ /** Calculate optimal position for the popup */
1255
+ private void calculateOptimalPosition(Rect outRect, int width, int height) {
1256
+ _caretRect = _editView.getBoundingBox(_editView.getCaretPosition());
1257
+ _isSelectionMode = _editView.isSelectMode();
1258
+
1259
+ if (_caretRect == null) {
1260
+ outRect.set(_edgeMargin, _edgeMargin, _edgeMargin + width, _edgeMargin + height);
1261
+ return;
1262
+ }
1263
+
1264
+ int caretX = _caretRect.left;
1265
+ int caretY = _caretRect.top;
1266
+ int lineHeight = _editView.getLineHeight();
1267
+ int screenWidth = _editView.getWidth();
1268
+ int screenHeight = _editView.getHeight();
1269
+
1270
+ int actionModeWidth = width;
1271
+ int actionModeHeight = height;
1272
+
1273
+ // Center X on caret, clamped
1274
+ int optimalX = caretX + (_caretRect.width() / 2) - (actionModeWidth / 2);
1275
+ optimalX = Math.max(_edgeMargin, Math.min(optimalX, screenWidth - actionModeWidth - _edgeMargin));
1276
+
1277
+ // Prefer below cursor (downward), fallback above or bottom
1278
+ int optimalY = caretY + lineHeight * 2; // Start below
1279
+ if (optimalY + actionModeHeight > screenHeight - _edgeMargin) {
1280
+ // Fallback to above if no space below
1281
+ optimalY = caretY - actionModeHeight - lineHeight;
1282
+ if (optimalY < _edgeMargin) {
1283
+ // Final fallback to bottom
1284
+ optimalY = screenHeight - actionModeHeight - _edgeMargin;
1285
+ }
1286
+ }
1287
+ optimalY = Math.max(_edgeMargin, Math.min(optimalY, screenHeight - actionModeHeight - _edgeMargin));
1288
+
1289
+ outRect.set(optimalX, optimalY, optimalX + actionModeWidth, optimalY + actionModeHeight);
1290
+ }
1291
+
1292
+ // ===============================================
1293
+ // AUTO-HIDE TIMER METHODS
1294
+ // ===============================================
1295
+
1296
+ /** Start auto-hide timer */
1297
+ private void startAutoHideTimer() {
1298
+ cancelAutoHideTimer();
1299
+ _autoHideHandler.postDelayed(_autoHideRunnable, AUTO_HIDE_DELAY);
1300
+ }
1301
+
1302
+ /** Restart auto-hide timer */
1303
+ private void restartAutoHideTimer() {
1304
+ cancelAutoHideTimer();
1305
+ _autoHideHandler.postDelayed(_autoHideRunnable, AUTO_HIDE_DELAY);
1306
+ }
1307
+
1308
+ /** Cancel auto-hide timer */
1309
+ private void cancelAutoHideTimer() {
1310
+ _autoHideHandler.removeCallbacks(_autoHideRunnable);
1311
+ }
1312
+
1313
+ // ===============================================
1314
+ // MENU ACTION HANDLING
1315
+ // ===============================================
1316
+
1317
+ /** Handle menu item actions */
1318
+ private void handleCustomMenuAction(MenuAction action) {
1319
+ switch (action) {
1320
+ case SELECT:
1321
+ _editView.selectNearestWord();
1322
+ break;
1323
+ case COPY:
1324
+ _editView.copy();
1325
+ break;
1326
+ case CUT:
1327
+ _editView.cut();
1328
+ break;
1329
+ case PASTE:
1330
+ _editView.paste();
1331
+ break;
1332
+ case SELECT_ALL:
1333
+ _editView.selectAll();
1334
+ break;
1335
+ case OPEN_LINK:
1336
+ String selectedText = _editView.getSelectedText();
1337
+ if (selectedText != null) {
1338
+ LinkChecker.openLinkInBrowser(_editView.getContext(), selectedText);
1339
+ }
1340
+ break;
1341
+ case SHARE:
1342
+ String selectedText2 = _editView.getSelectedText();
1343
+ if (selectedText2 != null) {
1344
+ try {
1345
+ Intent shareIntent = new Intent(Intent.ACTION_SEND);
1346
+ shareIntent.setType("text/plain");
1347
+ shareIntent.putExtra(Intent.EXTRA_TEXT, selectedText2);
1348
+ _editView.getContext().startActivity(Intent.createChooser(shareIntent, "Share Text"));
1349
+ } catch (Exception e) {
1350
+ }
1351
+ }
1352
+ break;
1353
+ case GOTO:
1354
+ break;
1355
+ case DELETE:
1356
+ _editView.delete();
1357
+ break;
1358
+ case TOGGLE_COMMENT:
1359
+ _editView.toggleComment();
1360
+ break;
1361
+ }
1362
+ hide();
1363
+ restartAutoHideTimer();
1364
+ }
1365
+
1366
+ // ===============================================
1367
+ // UTILITY METHODS
1368
+ // ===============================================
1369
+
1370
+ /** Check if paste is available */
1371
+ private boolean canPaste() {
1372
+ ClipboardManager clipboard = (ClipboardManager) _context.getSystemService(Context.CLIPBOARD_SERVICE);
1373
+ return clipboard != null && clipboard.hasPrimaryClip();
1374
+ }
1375
+
1376
+ /** Check if text is currently selected */
1377
+ private boolean isTextSelected() {
1378
+ return _editView.isSelectMode();
1379
+ }
1380
+ }