capacitor-dex-editor 0.0.37 → 0.0.39
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.
- package/android/src/main/AndroidManifest.xml +8 -0
- package/android/src/main/assets/availableSyntax.json +54 -0
- package/android/src/main/assets/c.json +42 -0
- package/android/src/main/assets/colors.json +21 -0
- package/android/src/main/assets/cpp.json +79 -0
- package/android/src/main/assets/dart.json +108 -0
- package/android/src/main/assets/java.json +46 -0
- package/android/src/main/assets/js.json +54 -0
- package/android/src/main/assets/json.json +33 -0
- package/android/src/main/assets/kotlin.json +53 -0
- package/android/src/main/assets/lua.json +54 -0
- package/android/src/main/assets/php.json +69 -0
- package/android/src/main/assets/python.json +87 -0
- package/android/src/main/assets/rust.json +139 -0
- package/android/src/main/assets/smali.json +131 -0
- package/android/src/main/assets/xml.json +96 -0
- package/android/src/main/java/com/aetherlink/dexeditor/DexEditorPluginPlugin.java +67 -0
- package/android/src/main/java/com/aetherlink/dexeditor/DexManager.java +165 -0
- package/android/src/main/java/com/aetherlink/dexeditor/SmaliEditorActivity.java +205 -0
- package/android/src/main/java/com/aetherlink/dexeditor/editor/EditView.java +4022 -0
- package/android/src/main/java/com/aetherlink/dexeditor/editor/WordWrapLayout.java +275 -0
- package/android/src/main/java/com/aetherlink/dexeditor/editor/buffer/BufferCache.java +113 -0
- package/android/src/main/java/com/aetherlink/dexeditor/editor/buffer/GapBuffer.java +685 -0
- package/android/src/main/java/com/aetherlink/dexeditor/editor/component/ClipboardPanel.java +1380 -0
- package/android/src/main/java/com/aetherlink/dexeditor/editor/component/Magnifier.java +363 -0
- package/android/src/main/java/com/aetherlink/dexeditor/editor/highlight/Candidate.java +52 -0
- package/android/src/main/java/com/aetherlink/dexeditor/editor/highlight/CommentDef.java +47 -0
- package/android/src/main/java/com/aetherlink/dexeditor/editor/highlight/LineResult.java +49 -0
- package/android/src/main/java/com/aetherlink/dexeditor/editor/highlight/MHSyntaxHighlightEngine.java +841 -0
- package/android/src/main/java/com/aetherlink/dexeditor/editor/highlight/Rule.java +53 -0
- package/android/src/main/java/com/aetherlink/dexeditor/editor/highlight/Token.java +48 -0
- package/android/src/main/java/com/aetherlink/dexeditor/editor/listener/OnTextChangedListener.java +6 -0
- package/android/src/main/java/com/aetherlink/dexeditor/editor/utils/Pair.java +80 -0
- package/android/src/main/java/com/aetherlink/dexeditor/editor/utils/ScreenUtils.java +63 -0
- package/android/src/main/res/drawable/abc_text_cursor_material.xml +9 -0
- package/android/src/main/res/drawable/abc_text_select_handle_left_mtrl.png +0 -0
- package/android/src/main/res/drawable/abc_text_select_handle_middle_mtrl.png +0 -0
- package/android/src/main/res/drawable/abc_text_select_handle_right_mtrl.png +0 -0
- package/android/src/main/res/drawable/ic_arrow_back.xml +12 -0
- package/android/src/main/res/drawable/ic_copy.xml +11 -0
- package/android/src/main/res/drawable/ic_cut.xml +10 -0
- package/android/src/main/res/drawable/ic_delete.xml +12 -0
- package/android/src/main/res/drawable/ic_edit_white_24dp.xml +10 -0
- package/android/src/main/res/drawable/ic_goto.xml +10 -0
- package/android/src/main/res/drawable/ic_launcher_background.xml +186 -0
- package/android/src/main/res/drawable/ic_look_white_24dp.xml +11 -0
- package/android/src/main/res/drawable/ic_more.xml +10 -0
- package/android/src/main/res/drawable/ic_open_link.xml +11 -0
- package/android/src/main/res/drawable/ic_paste.xml +11 -0
- package/android/src/main/res/drawable/ic_redo_white_24dp.xml +10 -0
- package/android/src/main/res/drawable/ic_select.xml +11 -0
- package/android/src/main/res/drawable/ic_select_all.xml +10 -0
- package/android/src/main/res/drawable/ic_share.xml +11 -0
- package/android/src/main/res/drawable/ic_toggle_comment.xml +12 -0
- package/android/src/main/res/drawable/ic_translate.xml +10 -0
- package/android/src/main/res/drawable/ic_undo_white_24dp.xml +11 -0
- package/android/src/main/res/drawable/magnifier_bg.xml +5 -0
- package/android/src/main/res/drawable/popup_background.xml +8 -0
- package/android/src/main/res/drawable/ripple_effect.xml +9 -0
- package/android/src/main/res/drawable/selection_menu_background.xml +5 -0
- package/android/src/main/res/layout/custom_selection_menu.xml +44 -0
- package/android/src/main/res/layout/expand_button.xml +23 -0
- package/android/src/main/res/layout/item_autocomplete.xml +25 -0
- package/android/src/main/res/layout/magnifier_popup.xml +17 -0
- package/android/src/main/res/layout/menu_item.xml +30 -0
- package/android/src/main/res/layout/text_selection_menu.xml +36 -0
- 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
|
+
}
|