From 5fa666ba02f9cf3b055c0c70152395486b407aca Mon Sep 17 00:00:00 2001 From: Lenin Date: Tue, 17 Mar 2026 23:58:04 -0500 Subject: [PATCH 01/18] Refactor: Remove unused MD3Test7State and MD3TestState classes - Deleted MD3Test7State.hx and MD3TestState.hx as they were no longer needed. - Updated navigation in existing states to reflect the removal of these classes. Enhancement: Implement native message box for Android - Added JNI integration to show native message boxes using `showMessageBox` method in PlusEngineExtension. - Updated CoolUtil to utilize the new native message box functionality on Android. Feature: Introduce native dropdown component for Android - Created AndroidNativeDropDown class to handle dropdowns via JNI. - Implemented show, pollSelection, and isDialogVisible methods for dropdown functionality. - Added DropDown component to manage dropdown UI and interactions. New: Add NativeCrashHandler and NativeCrashActivity for crash reporting - Implemented NativeCrashHandler to capture uncaught exceptions and display crash reports. - Created NativeCrashActivity to show detailed crash information with options to copy stack trace. Documentation: Add licensing information to Kotlin files - Added copyright and licensing information to all Kotlin source files. --- .../backend/native/AndroidNativeDropDown.hx | 148 +++++++++++ .../ui/components/PsychUIDropDownMenu.hx | 58 ++++ source/funkin/ui/debug/MD3Test1State.hx | 120 --------- source/funkin/ui/debug/MD3Test2State.hx | 143 ---------- source/funkin/ui/debug/MD3Test3State.hx | 118 --------- source/funkin/ui/debug/MD3Test4State.hx | 131 --------- source/funkin/ui/debug/MD3Test5State.hx | 120 --------- source/funkin/ui/debug/MD3Test6State.hx | 250 ------------------ source/funkin/ui/debug/MD3Test7State.hx | 234 ---------------- source/funkin/ui/debug/MD3TestState.hx | 100 ------- source/funkin/util/CoolUtil.hx | 32 ++- source/kotlin/FileManagerActivity.kt | 24 ++ source/kotlin/FileUtil.kt | 24 ++ source/kotlin/NativeCrashActivity.kt | 111 ++++++++ source/kotlin/NativeCrashHandler.kt | 83 ++++++ source/kotlin/NativeUI.kt | 24 ++ source/kotlin/PlusEngineExtension.kt | 40 +++ source/kotlin/components/DropDown.kt | 195 ++++++++++++++ source/kotlin/include_android.xml | 3 + 19 files changed, 738 insertions(+), 1220 deletions(-) create mode 100644 source/funkin/mobile/backend/native/AndroidNativeDropDown.hx delete mode 100644 source/funkin/ui/debug/MD3Test1State.hx delete mode 100644 source/funkin/ui/debug/MD3Test2State.hx delete mode 100644 source/funkin/ui/debug/MD3Test3State.hx delete mode 100644 source/funkin/ui/debug/MD3Test4State.hx delete mode 100644 source/funkin/ui/debug/MD3Test5State.hx delete mode 100644 source/funkin/ui/debug/MD3Test6State.hx delete mode 100644 source/funkin/ui/debug/MD3Test7State.hx delete mode 100644 source/funkin/ui/debug/MD3TestState.hx create mode 100644 source/kotlin/NativeCrashActivity.kt create mode 100644 source/kotlin/NativeCrashHandler.kt create mode 100644 source/kotlin/components/DropDown.kt diff --git a/source/funkin/mobile/backend/native/AndroidNativeDropDown.hx b/source/funkin/mobile/backend/native/AndroidNativeDropDown.hx new file mode 100644 index 00000000000..b9714c97f4b --- /dev/null +++ b/source/funkin/mobile/backend/native/AndroidNativeDropDown.hx @@ -0,0 +1,148 @@ +/* + * Copyright (C) 2026 Lenin + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software. + * + * This Software may not be claimed as the original work of any other + * individual or entity. + * + * Attribution to the original author is appreciated, but is not required. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. + * + * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +package funkin.mobile.backend.native; + +#if android +import haxe.Json; +import lime.system.JNI; + +/** + * JNI bridge for Android native MD3 dropdown component. + */ +class AndroidNativeDropDown +{ + public static inline var NO_SELECTION:Int = -1; + public static inline var CANCELED:Int = -2; + + @:noCompletion private static var _initialized:Bool = false; + @:noCompletion private static var _showDropDown_jni:Dynamic = null; + @:noCompletion private static var _pollSelection_jni:Dynamic = null; + @:noCompletion private static var _isDialogVisible_jni:Dynamic = null; + + @:noCompletion + private static function ensureInit():Bool + { + if (_initialized) + return _showDropDown_jni != null && _pollSelection_jni != null && _isDialogVisible_jni != null; + + _initialized = true; + try + { + _showDropDown_jni = JNI.createStaticMethod( + 'com/leninasto/plusengine/components/DropDown', + 'showDropDown', + '(Ljava/lang/String;Ljava/lang/String;I)Z' + ); + + _pollSelection_jni = JNI.createStaticMethod( + 'com/leninasto/plusengine/components/DropDown', + 'pollSelection', + '()I' + ); + + _isDialogVisible_jni = JNI.createStaticMethod( + 'com/leninasto/plusengine/components/DropDown', + 'isDialogVisible', + '()Z' + ); + } + catch (e:Dynamic) + { + trace('[AndroidNativeDropDown] JNI init failed: ' + e); + } + + return _showDropDown_jni != null && _pollSelection_jni != null && _isDialogVisible_jni != null; + } + + public static function show(title:String, items:Array, selectedIndex:Int):Bool + { + if (items == null || items.length == 0) + return false; + + if (!ensureInit()) + return false; + + try + { + return _showDropDown_jni(title, Json.stringify(items), selectedIndex); + } + catch (e:Dynamic) + { + trace('[AndroidNativeDropDown] show failed: ' + e); + } + + return false; + } + + public static function pollSelection():Int + { + if (!ensureInit()) + return NO_SELECTION; + + try + { + return _pollSelection_jni(); + } + catch (e:Dynamic) + { + trace('[AndroidNativeDropDown] pollSelection failed: ' + e); + } + + return NO_SELECTION; + } + + public static function isDialogVisible():Bool + { + if (!ensureInit()) + return false; + + try + { + return _isDialogVisible_jni(); + } + catch (e:Dynamic) + { + trace('[AndroidNativeDropDown] isDialogVisible failed: ' + e); + } + + return false; + } +} +#else +class AndroidNativeDropDown +{ + public static inline var NO_SELECTION:Int = -1; + public static inline var CANCELED:Int = -2; + + public static function show(title:String, items:Array, selectedIndex:Int):Bool + return false; + + public static function pollSelection():Int + return NO_SELECTION; + + public static function isDialogVisible():Bool + return false; +} +#end \ No newline at end of file diff --git a/source/funkin/ui/components/PsychUIDropDownMenu.hx b/source/funkin/ui/components/PsychUIDropDownMenu.hx index 59a399dbf7b..dbbdb487407 100644 --- a/source/funkin/ui/components/PsychUIDropDownMenu.hx +++ b/source/funkin/ui/components/PsychUIDropDownMenu.hx @@ -1,6 +1,9 @@ package funkin.ui.components; import funkin.ui.components.PsychUIBox.UIStyleData; +#if android +import funkin.mobile.backend.native.AndroidNativeDropDown; +#end class PsychUIDropDownMenu extends PsychUIInputText { @@ -93,10 +96,65 @@ class PsychUIDropDownMenu extends PsychUIInputText var _prevMouseY:Float = 0; var _touchDragDist:Float = 0; #end + #if android + var waitingNativeSelection:Bool = false; + inline function tryOpenNativeDropDown():Bool + { + if(list == null || list.length <= 0) + return false; + + var nativeSelectedIndex:Int = selectedIndex >= 0 ? selectedIndex : 0; + if(AndroidNativeDropDown.show('Select option', list, nativeSelectedIndex)) + { + waitingNativeSelection = true; + PsychUIInputText.focusOn = null; + FlxG.stage.window.textInputEnabled = false; + showDropDown(false); + return true; + } + + return false; + } + #end override function update(elapsed:Float) { + #if android + if(FlxG.mouse.justPressed) + { + var pressedButton:Bool = FlxG.mouse.overlaps(button, camera); + var pressedField:Bool = FlxG.mouse.overlaps(behindText, camera); + + if(pressedButton || pressedField) + { + button.animation.play('pressed', true); + if(tryOpenNativeDropDown()) + { + return; + } + } + } + #end + var lastFocus = PsychUIInputText.focusOn; super.update(elapsed); + + #if android + if(waitingNativeSelection) + { + var nativeSelection:Int = AndroidNativeDropDown.pollSelection(); + if(nativeSelection >= 0) + { + waitingNativeSelection = false; + if(nativeSelection < list.length) + clickedOn(nativeSelection, list[nativeSelection]); + } + else if(nativeSelection == AndroidNativeDropDown.CANCELED || !AndroidNativeDropDown.isDialogVisible()) + { + waitingNativeSelection = false; + } + } + #end + if(FlxG.mouse.justPressed) { if(FlxG.mouse.overlaps(button, camera)) diff --git a/source/funkin/ui/debug/MD3Test1State.hx b/source/funkin/ui/debug/MD3Test1State.hx deleted file mode 100644 index dfe1dc77984..00000000000 --- a/source/funkin/ui/debug/MD3Test1State.hx +++ /dev/null @@ -1,120 +0,0 @@ -package funkin.ui.debug; - -import flixel.FlxG; -import flixel.FlxSprite; -import flixel.text.FlxText; -import flixel.util.FlxColor; -import funkin.ui.MusicBeatState; -import funkin.ui.debug.MD3TestState; -import funkin.ui.debug.MD3Test2State; -import funkin.ui.components.md3.*; - -/** - * MD3 component debug viewer — Page 2/4: Chips, Tabs, Badges. - * Navigate with LEFT/RIGHT arrow keys. Press BACK/ESC to exit. - */ -class MD3Test1State extends MusicBeatState -{ - static inline var PAGE_INDEX:Int = 1; - static inline var TOTAL_PAGES:Int = 7; - - var snackbar:MaterialSnackbar; - var filterChips:Array = []; - - override function create():Void - { - super.create(); - Cursor.show(); - - var bg = new FlxSprite(); - bg.makeGraphic(FlxG.width, FlxG.height, 0xFFFEF7FF); - add(bg); - - buildContent(); - - var pageLabel = new FlxText(0, FlxG.height - 28, FlxG.width, - '← → to navigate Page ${PAGE_INDEX + 1} / $TOTAL_PAGES ESC to exit', 14); - pageLabel.setFormat(Paths.font("phantom.ttf"), 14, 0xFF49454F, CENTER); - add(pageLabel); - - snackbar = new MaterialSnackbar(340); - add(snackbar); - - addTouchPad('LEFT_RIGHT', 'B'); - } - - function sectionLabel(text:String, lx:Float, ly:Float):Void - { - var lbl = new FlxText(lx, ly, 0, text, 12); - lbl.setFormat(Paths.font("phantom.ttf"), 12, 0xFF79747E, LEFT); - add(lbl); - } - - function buildContent():Void - { - sectionLabel("Chips", 20, 12); - - add(new MaterialChip(20, 36, "Assist", ASSIST, false, function() { snackbar.show("Assist chip tapped", 2); })); - add(new MaterialChip(120, 36, "Suggestion", SUGGESTION, false, function() { snackbar.show("Suggestion chip tapped", 2); })); - add(new MaterialChip(260, 36, "Input chip", INPUT, false, null, function() { snackbar.show("Input chip deleted!", 2); })); - - sectionLabel("Filter chips (toggle)", 20, 78); - - var filterLabels = ["Hard", "Expert", "Normal", "Easy"]; - filterChips = []; - for (i in 0...filterLabels.length) - { - var chip = new MaterialChip(20 + i * 106, 100, filterLabels[i], FILTER, i == 0, function() - { - var selected = filterChips.filter(c -> c.selected).map(c -> c.label); - snackbar.show("Selected: " + (selected.length > 0 ? selected.join(", ") : "none"), 2); - }); - filterChips.push(chip); - add(chip); - } - - sectionLabel("Tabs (Primary)", 20, 152); - - var tabsPrimary = new MaterialTabs(20, 174, ["Songs", "Characters", "Stages", "Options"], PRIMARY, 460, function(i, name) - { - snackbar.show('Tab: "$name"', 2); - }); - add(tabsPrimary); - - sectionLabel("Tabs (Secondary)", 20, 232); - - var tabsSec = new MaterialTabs(20, 254, ["Info", "Settings", "Logs"], SECONDARY, 300, function(i, name) - { - snackbar.show('Secondary tab: "$name"', 2); - }); - add(tabsSec); - - sectionLabel("Badge", 20, 316); - - // Badge centers are all aligned to y_center = 347. - // Dot (6 px tall): y = 347 - 3 = 344. Numeric (16 px tall): y = 347 - 8 = 339. - var dotBadge = new MaterialBadge(20, 344, -1); - add(dotBadge); - sectionLabel("dot", 30, 340); - - var numBadge = new MaterialBadge(88, 339, 5); - add(numBadge); - sectionLabel("5", 108, 340); - - var bigBadge = new MaterialBadge(158, 339, 1337); // displays "999+" - add(bigBadge); - sectionLabel("1337 →999+", 210, 340); - } - - override function update(elapsed:Float):Void - { - super.update(elapsed); - - if (controls.UI_LEFT_P || FlxG.keys.justPressed.LEFT) - FlxG.switchState(new MD3TestState()); - if (controls.UI_RIGHT_P || FlxG.keys.justPressed.RIGHT) - FlxG.switchState(new MD3Test2State()); - if (controls.BACK) - FlxG.switchState(new funkin.ui.title.TitleState()); - } -} diff --git a/source/funkin/ui/debug/MD3Test2State.hx b/source/funkin/ui/debug/MD3Test2State.hx deleted file mode 100644 index 5147d38d56b..00000000000 --- a/source/funkin/ui/debug/MD3Test2State.hx +++ /dev/null @@ -1,143 +0,0 @@ -package funkin.ui.debug; - -import flixel.FlxG; -import flixel.FlxSprite; -import flixel.text.FlxText; -import flixel.util.FlxColor; -import funkin.ui.MusicBeatState; -import funkin.ui.debug.MD3Test1State; -import funkin.ui.debug.MD3Test3State; -import funkin.ui.components.md3.*; - -/** - * MD3 component debug viewer — Page 3/4: Cards, Progress Indicators. - * Navigate with LEFT/RIGHT arrow keys. Press BACK/ESC to exit. - */ -class MD3Test2State extends MusicBeatState -{ - static inline var PAGE_INDEX:Int = 2; - static inline var TOTAL_PAGES:Int = 7; - - var snackbar:MaterialSnackbar; - - // Determinate progress indicators animated in update() - var linearDet:MaterialProgressIndicator; - var circularDet:MaterialProgressIndicator; - var progressTimer:Float = 0; - - override function create():Void - { - super.create(); - Cursor.show(); - - var bg = new FlxSprite(); - bg.makeGraphic(FlxG.width, FlxG.height, 0xFFFEF7FF); - add(bg); - - buildContent(); - - var pageLabel = new FlxText(0, FlxG.height - 28, FlxG.width, - '← → to navigate Page ${PAGE_INDEX + 1} / $TOTAL_PAGES ESC to exit', 14); - pageLabel.setFormat(Paths.font("phantom.ttf"), 14, 0xFF49454F, CENTER); - add(pageLabel); - - snackbar = new MaterialSnackbar(340); - add(snackbar); - - addTouchPad('LEFT_RIGHT', 'B'); - } - - function sectionLabel(text:String, lx:Float, ly:Float):Void - { - var lbl = new FlxText(lx, ly, 0, text, 12); - lbl.setFormat(Paths.font("phantom.ttf"), 12, 0xFF79747E, LEFT); - add(lbl); - } - - function buildContent():Void - { - sectionLabel("Cards", 20, 12); - - // Elevated card - var cardEl = new MaterialCard(20, 34, ELEVATED, 180, 100, function() { snackbar.show("Elevated card clicked!", 2); }); - var elLbl = new FlxText(12, 12, 156, "Elevated Card\nClick me!", 13); - elLbl.setFormat(Paths.font("phantom.ttf"), 13, 0xFF1C1B1F, LEFT); - cardEl.addContent(elLbl); - add(cardEl); - - // Filled card - var cardFi = new MaterialCard(218, 34, FILLED, 180, 100, function() { snackbar.show("Filled card clicked!", 2); }); - var fiLbl = new FlxText(12, 12, 156, "Filled Card\nClick me!", 13); - fiLbl.setFormat(Paths.font("phantom.ttf"), 13, 0xFF1C1B1F, LEFT); - cardFi.addContent(fiLbl); - add(cardFi); - - // Outlined card - var cardOu = new MaterialCard(416, 34, OUTLINED, 180, 100, function() { snackbar.show("Outlined card clicked!", 2); }); - var ouLbl = new FlxText(12, 12, 156, "Outlined Card\nClick me!", 13); - ouLbl.setFormat(Paths.font("phantom.ttf"), 13, 0xFF1C1B1F, LEFT); - cardOu.addContent(ouLbl); - add(cardOu); - - sectionLabel("Progress Linear", 20, 152); - - // Linear determinate (auto-animated in update) - linearDet = new MaterialProgressIndicator(20, 172, LINEAR, 300); - add(linearDet); - sectionLabel("determinate (auto-fills)", 332, 168); - - // Linear indeterminate - var linearInd = new MaterialProgressIndicator(20, 204, LINEAR, 300); - linearInd.indeterminate = true; - add(linearInd); - sectionLabel("indeterminate", 332, 200); - - sectionLabel("Progress Circular", 20, 234); - - // Circular determinate (auto-animated in update) - circularDet = new MaterialProgressIndicator(20, 254, CIRCULAR); - add(circularDet); - sectionLabel("determinate", 82, 274); - - // Circular indeterminate - var circularInd = new MaterialProgressIndicator(175, 254, CIRCULAR); - circularInd.indeterminate = true; - add(circularInd); - sectionLabel("indeterminate", 237, 274); - - sectionLabel("Loading Indicator (M3)", 20, 318); - - // Default loading indicator (48 px, no container) - var li1 = new MaterialLoadingIndicator(20, 340); - add(li1); - sectionLabel("default", 84, 366); - - // With container background - var li2 = new MaterialLoadingIndicator(175, 340, 48, true); - add(li2); - sectionLabel("with container", 239, 366); - - // Larger size (64 px) - var li3 = new MaterialLoadingIndicator(370, 332, 64); - add(li3); - sectionLabel("64 dp", 448, 366); - } - - override function update(elapsed:Float):Void - { - super.update(elapsed); - - // Animate determinate bars - progressTimer += elapsed * 0.3; - if (progressTimer > 1) progressTimer = 0; - if (linearDet != null) linearDet.value = progressTimer; - if (circularDet != null) circularDet.value = progressTimer; - - if (controls.UI_LEFT_P || FlxG.keys.justPressed.LEFT) - FlxG.switchState(new MD3Test1State()); - if (controls.UI_RIGHT_P || FlxG.keys.justPressed.RIGHT) - FlxG.switchState(new MD3Test3State()); - if (controls.BACK) - FlxG.switchState(new funkin.ui.title.TitleState()); - } -} diff --git a/source/funkin/ui/debug/MD3Test3State.hx b/source/funkin/ui/debug/MD3Test3State.hx deleted file mode 100644 index 9c54e9d2338..00000000000 --- a/source/funkin/ui/debug/MD3Test3State.hx +++ /dev/null @@ -1,118 +0,0 @@ -package funkin.ui.debug; - -import flixel.FlxG; -import flixel.FlxSprite; -import flixel.text.FlxText; -import flixel.util.FlxColor; -import funkin.ui.MusicBeatState; -import funkin.ui.debug.MD3TestState; -import funkin.ui.debug.MD3Test2State; -import funkin.ui.debug.MD3Test4State; -import funkin.ui.components.md3.*; - -/** - * MD3 component debug viewer — Page 4/4: Dialog, Menu, Snackbar, Tooltip. - * Navigate with LEFT/RIGHT arrow keys. Press BACK/ESC to exit. - */ -class MD3Test3State extends MusicBeatState -{ - static inline var PAGE_INDEX:Int = 3; - static inline var TOTAL_PAGES:Int = 7; - - var snackbar:MaterialSnackbar; - var dialog:MaterialDialog; - - override function create():Void - { - super.create(); - Cursor.show(); - - var bg = new FlxSprite(); - bg.makeGraphic(FlxG.width, FlxG.height, 0xFFFEF7FF); - add(bg); - - buildContent(); - - var pageLabel = new FlxText(0, FlxG.height - 28, FlxG.width, - '← → to navigate Page ${PAGE_INDEX + 1} / $TOTAL_PAGES ESC to exit', 14); - pageLabel.setFormat(Paths.font("phantom.ttf"), 14, 0xFF49454F, CENTER); - add(pageLabel); - - // Dialog renders above content but below snackbar - if (dialog != null) - add(dialog); - - // Snackbar always on top - snackbar = new MaterialSnackbar(340); - add(snackbar); - - addTouchPad('LEFT_RIGHT', 'B'); - } - - function sectionLabel(text:String, lx:Float, ly:Float):Void - { - var lbl = new FlxText(lx, ly, 0, text, 12); - lbl.setFormat(Paths.font("phantom.ttf"), 12, 0xFF79747E, LEFT); - add(lbl); - } - - function buildContent():Void - { - // ---- Dialog ---- - sectionLabel("Dialog", 20, 12); - - dialog = new MaterialDialog( - "Delete song?", - "This will permanently remove the song and all its assets. This action cannot be undone.", - "Delete", "Cancel", - function() { snackbar.show("Song deleted!", 3); }, - function() { snackbar.show("Cancelled.", 2); } - ); - - var dlgBtn = new MaterialButton(20, 34, "Open Dialog", FILLED, 160, function() { dialog.open(); }); - add(dlgBtn); - - // ---- Menu ---- - sectionLabel("Menu (Dropdown)", 20, 96); - - var menuBtn = new MaterialButton(20, 116, "Open Menu ▼", OUTLINED, 160, null); - var menu = new MaterialMenu(20, 160, ["New song", "Import chart", "Export", "Delete", "Properties"], 200, - function(i, item) { snackbar.show('Menu: "$item"', 2); }); - menuBtn.onClick = function() { menu.toggle(); }; - add(menuBtn); - add(menu); - - // ---- Snackbar ---- - sectionLabel("Snackbar", 260, 12); - - add(new MaterialButton(260, 34, "Show snackbar", FILLED, 170, function() - { - snackbar.show("Hello from snackbar!", 4, "UNDO", function() { snackbar.show("Undo pressed!", 2); }); - })); - add(new MaterialButton(260, 84, "Persistent snack", OUTLINED, 170, function() - { - snackbar.show("Persistent — no auto-hide.", 0, "OK", function() {}); - })); - - // ---- Tooltip ---- - sectionLabel("Tooltip (hover button)", 20, 252); - - var tipBtn = new MaterialButton(20, 272, "Hover me", OUTLINED, 140); - var tooltip = new MaterialTooltip("This is a tooltip!\nHover to trigger."); - tooltip.attachTo(20, 272, 140, 40); - add(tipBtn); - add(tooltip); - } - - override function update(elapsed:Float):Void - { - super.update(elapsed); - - if (controls.UI_LEFT_P || FlxG.keys.justPressed.LEFT) - FlxG.switchState(new MD3Test2State()); - if (controls.UI_RIGHT_P || FlxG.keys.justPressed.RIGHT) - FlxG.switchState(new MD3Test4State()); - if (controls.BACK) - FlxG.switchState(new funkin.ui.title.TitleState()); - } -} diff --git a/source/funkin/ui/debug/MD3Test4State.hx b/source/funkin/ui/debug/MD3Test4State.hx deleted file mode 100644 index 7baa71b5bd8..00000000000 --- a/source/funkin/ui/debug/MD3Test4State.hx +++ /dev/null @@ -1,131 +0,0 @@ -package funkin.ui.debug; - -import flixel.FlxG; -import flixel.FlxSprite; -import flixel.text.FlxText; -import funkin.ui.MusicBeatState; -import funkin.ui.debug.MD3Test3State; -import funkin.ui.debug.MD3Test5State; -import funkin.ui.components.md3.*; - -/** - * MD3 component debug viewer — Page 5/6: Checkbox, Switch, Radio Button. - * Navigate with LEFT/RIGHT arrow keys. Press BACK/ESC to exit. - */ -class MD3Test4State extends MusicBeatState -{ - static inline var PAGE_INDEX:Int = 4; - static inline var TOTAL_PAGES:Int = 7; - - var snackbar:MaterialSnackbar; - - override function create():Void - { - super.create(); - Cursor.show(); - - var bg = new FlxSprite(); - bg.makeGraphic(FlxG.width, FlxG.height, 0xFFFEF7FF); - add(bg); - - // Snackbar must exist before buildContent() so callbacks don't null-crash - snackbar = new MaterialSnackbar(340); - - buildContent(); - - var pageLabel = new FlxText(0, FlxG.height - 28, FlxG.width, - '← → to navigate Page ${PAGE_INDEX + 1} / $TOTAL_PAGES ESC to exit', 14); - pageLabel.setFormat(Paths.font("phantom.ttf"), 14, 0xFF49454F, CENTER); - add(pageLabel); - - add(snackbar); - - addTouchPad('LEFT_RIGHT', 'B'); - } - - function sectionLabel(text:String, lx:Float, ly:Float):Void - { - var lbl = new FlxText(lx, ly, 0, text, 12); - lbl.setFormat(Paths.font("phantom.ttf"), 12, 0xFF79747E, LEFT); - add(lbl); - } - - function buildContent():Void - { - // ---- CHECKBOX ---- - sectionLabel("Checkbox", 20, 12); - - var cb1 = new MaterialCheckbox(20, 34, "Unchecked", false, function(v) { snackbar.show('Checkbox 1: $v', 2); }); - add(cb1); - - var cb2 = new MaterialCheckbox(20, 80, "Checked", true, function(v) { snackbar.show('Checkbox 2: $v', 2); }); - add(cb2); - - var cb3 = new MaterialCheckbox(20, 126, "Disabled Off"); - cb3.enabled = false; - add(cb3); - - var cb4 = new MaterialCheckbox(20, 172, "Disabled On", true); - cb4.enabled = false; - add(cb4); - - // ---- SWITCH ---- - sectionLabel("Switch", 260, 12); - - var sw1 = new MaterialSwitch(260, 34, false); - sw1.onChange = function(v) { snackbar.show('Switch A: ${v ? "On" : "Off"}', 2); }; - add(sw1); - sectionLabel("Off", 322, 42); - - var sw2 = new MaterialSwitch(260, 80, true); - sw2.onChange = function(v) { snackbar.show('Switch B: ${v ? "On" : "Off"}', 2); }; - add(sw2); - sectionLabel("On", 322, 88); - - var sw3 = new MaterialSwitch(260, 126, false); - sw3.enabled = false; - add(sw3); - sectionLabel("Disabled", 322, 134); - - // ---- RADIO BUTTON ---- - sectionLabel("Radio Button (Group 1)", 20, 224); - - var rb1 = new MaterialRadioButton(20, 248, "Option A", "A", "p5group1", true, - function(v) { snackbar.show('Radio selected: $v', 2); }); - add(rb1); - - var rb2 = new MaterialRadioButton(20, 294, "Option B", "B", "p5group1", false, - function(v) { snackbar.show('Radio selected: $v', 2); }); - add(rb2); - - var rb3 = new MaterialRadioButton(20, 340, "Option C", "C", "p5group1", false, - function(v) { snackbar.show('Radio selected: $v', 2); }); - add(rb3); - - sectionLabel("Radio Button (Group 2)", 260, 224); - - var rb4 = new MaterialRadioButton(260, 248, "Yes", "yes", "p5group2", true, - function(v) { snackbar.show('Answer: $v', 2); }); - add(rb4); - - var rb5 = new MaterialRadioButton(260, 294, "No", "no", "p5group2", false, - function(v) { snackbar.show('Answer: $v', 2); }); - add(rb5); - - var rb6 = new MaterialRadioButton(260, 340, "Maybe", "maybe", "p5group2", false, - function(v) { snackbar.show('Answer: $v', 2); }); - add(rb6); - } - - override function update(elapsed:Float):Void - { - super.update(elapsed); - - if (controls.UI_LEFT_P || FlxG.keys.justPressed.LEFT) - FlxG.switchState(new MD3Test3State()); - if (controls.UI_RIGHT_P || FlxG.keys.justPressed.RIGHT) - FlxG.switchState(new MD3Test5State()); - if (controls.BACK) - FlxG.switchState(new funkin.ui.title.TitleState()); - } -} diff --git a/source/funkin/ui/debug/MD3Test5State.hx b/source/funkin/ui/debug/MD3Test5State.hx deleted file mode 100644 index fc744c98c52..00000000000 --- a/source/funkin/ui/debug/MD3Test5State.hx +++ /dev/null @@ -1,120 +0,0 @@ -package funkin.ui.debug; - -import flixel.FlxG; -import flixel.FlxSprite; -import flixel.text.FlxText; -import funkin.ui.MusicBeatState; -import funkin.ui.debug.MD3Test4State; -import funkin.ui.debug.MD3Test6State; -import funkin.ui.debug.MD3TestState; -import funkin.ui.components.md3.*; - -/** - * MD3 component debug viewer — Page 6/6: Slider, Outlined TextField, Filled TextField. - * Navigate with LEFT/RIGHT arrow keys. Press BACK/ESC to exit. - */ -class MD3Test5State extends MusicBeatState -{ - static inline var PAGE_INDEX:Int = 5; - static inline var TOTAL_PAGES:Int = 7; - - var snackbar:MaterialSnackbar; - - // Text fields kept as class vars so update() can check focused state - var tf1:MaterialTextField; - var tf2:MaterialTextField; - var ff1:FilledTextField; - var ff2:FilledTextField; - - override function create():Void - { - super.create(); - Cursor.show(); - - var bg = new FlxSprite(); - bg.makeGraphic(FlxG.width, FlxG.height, 0xFFFEF7FF); - add(bg); - - // Snackbar must exist before buildContent() so callbacks don't null-crash - snackbar = new MaterialSnackbar(340); - - buildContent(); - - var pageLabel = new FlxText(0, FlxG.height - 28, FlxG.width, - '← → to navigate Page ${PAGE_INDEX + 1} / $TOTAL_PAGES ESC to exit', 14); - pageLabel.setFormat(Paths.font("phantom.ttf"), 14, 0xFF49454F, CENTER); - add(pageLabel); - - add(snackbar); - - addTouchPad('LEFT_RIGHT', 'B'); - } - - function sectionLabel(text:String, lx:Float, ly:Float):Void - { - var lbl = new FlxText(lx, ly, 0, text, 12); - lbl.setFormat(Paths.font("phantom.ttf"), 12, 0xFF79747E, LEFT); - add(lbl); - } - - function buildContent():Void - { - // ---- SLIDER ---- - sectionLabel("Slider (continuous 0 – 1)", 20, 12); - - var slider1 = new MaterialSlider(20, 40, 280, 0.5, 0, 1); - slider1.onChange = function(v) { snackbar.show('Slider: ${Math.round(v * 100)}%', 1); }; - add(slider1); - - sectionLabel("Slider (integer 0 – 100)", 20, 96); - - var slider2 = new MaterialSlider(20, 124, 280, 50, 0, 100); - slider2.onChange = function(v) { snackbar.show('Volume: ${Std.int(v)}', 1); }; - add(slider2); - - sectionLabel("Slider (disabled)", 20, 180); - - var slider3 = new MaterialSlider(20, 208, 280, 0.7, 0, 1); - slider3.enabled = false; - add(slider3); - - // ---- OUTLINED TEXT FIELD ---- - sectionLabel("Outlined Text Field", 360, 12); - - tf1 = new MaterialTextField(360, 36, 220, "Username"); - add(tf1); - - tf2 = new MaterialTextField(360, 116, 220, "Email"); - add(tf2); - - // ---- FILLED TEXT FIELD ---- - sectionLabel("Filled Text Field", 360, 196); - - ff1 = new FilledTextField(360, 220, 220, "Search"); - add(ff1); - - ff2 = new FilledTextField(360, 300, 220, "Notes"); - add(ff2); - } - - override function update(elapsed:Float):Void - { - super.update(elapsed); - - // Block navigation while any text field is focused - var anyFocused:Bool = (tf1 != null && tf1.focused) - || (tf2 != null && tf2.focused) - || (ff1 != null && ff1.focused) - || (ff2 != null && ff2.focused); - - if (!anyFocused) - { - if (controls.UI_LEFT_P || FlxG.keys.justPressed.LEFT) - FlxG.switchState(new MD3Test4State()); - if (controls.UI_RIGHT_P || FlxG.keys.justPressed.RIGHT) - FlxG.switchState(new MD3Test6State()); - if (controls.BACK) - FlxG.switchState(new funkin.ui.title.TitleState()); - } - } -} diff --git a/source/funkin/ui/debug/MD3Test6State.hx b/source/funkin/ui/debug/MD3Test6State.hx deleted file mode 100644 index 5436d53e5a7..00000000000 --- a/source/funkin/ui/debug/MD3Test6State.hx +++ /dev/null @@ -1,250 +0,0 @@ -package funkin.ui.debug; - -import flixel.FlxG; -import flixel.FlxSprite; -import flixel.text.FlxText; -import flixel.util.FlxColor; -import funkin.ui.MusicBeatState; -import funkin.ui.debug.MD3Test5State; -import funkin.ui.debug.MD3Test7State; -import funkin.ui.components.md3.*; -import funkin.ui.components.md3.MaterialButton.ButtonType; - -/** - * MD3 component debug viewer — Page 7/7: Live accent color theme switcher. - * Tap any accent swatch to regenerate the full M3 palette; all components update instantly. - * Navigate with LEFT/RIGHT arrow keys. Press BACK/ESC to exit. - */ -class MD3Test6State extends MusicBeatState -{ - static inline var PAGE_INDEX:Int = 6; - static inline var TOTAL_PAGES:Int = 8; - - // Predefined accent colors from MD3Theme - static var ACCENT_NAMES:Array = ["Purple", "Teal", "Red", "Green", "Amber", "Indigo", "Pink"]; - static var ACCENT_COLORS:Array = [ - 0xFF6750A4, // Purple - 0xFF006B5F, // Teal - 0xFFB3261E, // Red - 0xFF146C2E, // Green - 0xFFAA8000, // Amber - 0xFF3F51B5, // Indigo - 0xFF7D3255 // Pink - ]; - - // Palette row labels and role accessors - static var ROLE_LABELS:Array = [ - "Primary", "On Primary", "Primary Cont.", "On P.Cont.", - "Secondary", "On Secondary", "Sec. Cont.", "On Sec.Cont.", - "Tertiary", "On Tertiary", "Tert. Cont.", "On T.Cont.", - "Surface", "On Surface", "Surf. Var.", "On S.Var.", - "Outline", "Outline Var.", "Inv. Surface", "Inv. Primary" - ]; - - // Live demo components that respond to theme changes - var demoButton:MaterialButton; - var demoSwitch:MaterialSwitch; - var demoChip:MaterialChip; - var demoFAB:MaterialFAB; - var demoProgress:MaterialProgressIndicator; - - // Palette swatches (updated on theme change) - var swatches:Array = []; - var swatchLabels:Array = []; - - // Active accent button highlight - var accentHighlight:FlxSprite; - var accentButtons:Array = []; - var activeAccentIndex:Int = 0; - - override function create():Void - { - super.create(); - Cursor.show(); - - var bg = new FlxSprite(); - bg.makeGraphic(FlxG.width, FlxG.height, 0xFFFEF7FF); - add(bg); - - buildAccentPicker(); - buildPaletteGrid(); - buildLiveDemos(); - - var pageLabel = new FlxText(0, FlxG.height - 28, FlxG.width, - '← → to navigate Page ${PAGE_INDEX + 1} / $TOTAL_PAGES ESC to exit', 14); - pageLabel.setFormat(Paths.font("phantom.ttf"), 14, 0xFF49454F, CENTER); - add(pageLabel); - - addTouchPad('LEFT_RIGHT', 'B'); - } - - function sectionLabel(text:String, lx:Float, ly:Float):Void - { - var lbl = new FlxText(lx, ly, 0, text, 12); - lbl.setFormat(Paths.font("phantom.ttf"), 12, 0xFF79747E, LEFT); - add(lbl); - } - - function buildAccentPicker():Void - { - sectionLabel("ACCENT COLORS — tap to change theme", 16, 10); - - // Highlight box behind active swatch - accentHighlight = new FlxSprite(); - accentHighlight.makeGraphic(46, 46, 0xFF000000); - add(accentHighlight); - - var startX:Float = 16; - var startY:Float = 30; - var gap:Float = 54; - - for (i in 0...ACCENT_COLORS.length) - { - var swatch = new FlxSprite(startX + i * gap, startY); - swatch.makeGraphic(42, 42, ACCENT_COLORS[i]); - add(swatch); - - var lbl = new FlxText(startX + i * gap, startY + 44, 48, ACCENT_NAMES[i], 9); - lbl.setFormat(Paths.font("phantom.ttf"), 9, 0xFF49454F, CENTER); - add(lbl); - - accentButtons.push(swatch); - } - - // Position highlight on default (index 0) - updateHighlight(0); - - // A hint label - var hintLbl = new FlxText(startX + ACCENT_COLORS.length * gap + 8, startY + 8, 180, - "Changing accent regenerates\nthe full M3 palette.", 11); - hintLbl.setFormat(Paths.font("phantom.ttf"), 11, 0xFF79747E, LEFT); - add(hintLbl); - } - - function buildPaletteGrid():Void - { - sectionLabel("COLOR PALETTE", 16, 102); - - var cols:Int = 10; - var sW:Int = 62; - var sH:Int = 32; - var gapX:Int = 2; - var gapY:Int = 22; - var startX:Float = 16; - var startY:Float = 120; - - for (i in 0...ROLE_LABELS.length) - { - var col = i % cols; - var row = Std.int(i / cols); - var sx = startX + col * (sW + gapX); - var sy = startY + row * (sH + gapY); - - var swatch = new FlxSprite(sx, sy); - swatch.makeGraphic(sW, sH, FlxColor.WHITE); - add(swatch); - swatches.push(swatch); - - var lbl = new FlxText(sx, sy + sH + 2, sW, ROLE_LABELS[i], 9); - lbl.setFormat(Paths.font("phantom.ttf"), 9, 0xFF49454F, CENTER); - add(lbl); - swatchLabels.push(lbl); - } - - refreshPaletteSwatches(); - MD3Theme.addListener(refreshPaletteSwatches); - } - - function buildLiveDemos():Void - { - sectionLabel("LIVE DEMO (updates with theme)", 16, 228); - - demoButton = new MaterialButton(16, 250, "Filled Button", FILLED, 140); - add(demoButton); - - demoSwitch = new MaterialSwitch(220, 258, true); - add(demoSwitch); - - demoChip = new MaterialChip(280, 252, "Filter Chip", FILTER, true); - add(demoChip); - - demoFAB = new MaterialFAB(450, 244, REGULAR, "", null); - add(demoFAB); - - demoProgress = new MaterialProgressIndicator(16, 320, LINEAR, 380); - demoProgress.value = 0.65; - add(demoProgress); - - var circProg = new MaterialProgressIndicator(420, 296, CIRCULAR); - circProg.value = 0.72; - add(circProg); - } - - function refreshPaletteSwatches():Void - { - var roles:Array = [ - MD3Theme.primary, MD3Theme.onPrimary, MD3Theme.primaryContainer, MD3Theme.onPrimaryContainer, - MD3Theme.secondary, MD3Theme.onSecondary, MD3Theme.secondaryContainer, MD3Theme.onSecondaryContainer, - MD3Theme.tertiary, MD3Theme.onTertiary, MD3Theme.tertiaryContainer, MD3Theme.onTertiaryContainer, - MD3Theme.surface, MD3Theme.onSurface, MD3Theme.surfaceVariant, MD3Theme.onSurfaceVariant, - MD3Theme.outline, MD3Theme.outlineVariant, MD3Theme.inverseSurface, MD3Theme.inversePrimary - ]; - - for (i in 0...swatches.length) - { - if (i < roles.length) - swatches[i].color = roles[i]; - } - } - - function applyAccent(index:Int):Void - { - activeAccentIndex = index; - MD3Theme.setAccent(ACCENT_COLORS[index]); - updateHighlight(index); - refreshPaletteSwatches(); - } - - function updateHighlight(index:Int):Void - { - if (accentButtons.length == 0) return; - var target = accentButtons[index]; - accentHighlight.x = target.x - 2; - accentHighlight.y = target.y - 2; - } - - override function update(elapsed:Float):Void - { - super.update(elapsed); - - #if FLX_MOUSE - if (FlxG.mouse.justPressed) - { - var mx = FlxG.mouse.x; - var my = FlxG.mouse.y; - for (i in 0...accentButtons.length) - { - var btn = accentButtons[i]; - if (mx >= btn.x && mx <= btn.x + btn.width && my >= btn.y && my <= btn.y + btn.height) - { - applyAccent(i); - break; - } - } - } - #end - - if (controls.UI_LEFT_P || FlxG.keys.justPressed.LEFT) - FlxG.switchState(new MD3Test5State()); - if (controls.UI_RIGHT_P || FlxG.keys.justPressed.RIGHT) - FlxG.switchState(new MD3Test7State()); - if (controls.BACK) - FlxG.switchState(new funkin.ui.title.TitleState()); - } - - override function destroy():Void - { - MD3Theme.removeListener(refreshPaletteSwatches); - super.destroy(); - } -} diff --git a/source/funkin/ui/debug/MD3Test7State.hx b/source/funkin/ui/debug/MD3Test7State.hx deleted file mode 100644 index 04a863cb769..00000000000 --- a/source/funkin/ui/debug/MD3Test7State.hx +++ /dev/null @@ -1,234 +0,0 @@ -package funkin.ui.debug; - -import flixel.FlxG; -import flixel.FlxSprite; -import flixel.text.FlxText; -import flixel.util.FlxColor; -import funkin.ui.MusicBeatState; -import funkin.ui.debug.MD3Test6State; -import funkin.ui.debug.MD3TestState; -import funkin.ui.components.md3.*; -import funkin.ui.components.md3.MaterialButton.ButtonType; -import funkin.ui.components.md3.MaterialChip.ChipType; - -/** - * MD3 component debug viewer — Page 8/8: MaterialBox demo. - * Shows multiple MaterialBox panels with different configurations: - * Left box — settings panel with slider/switch/checkbox inside - * Right box — info panel (read-only), demonstrating resize() + close - * Bottom — tiny box demonstrating canMinimize (double-click title) - * Navigate with LEFT/RIGHT arrow keys. Press BACK/ESC to exit. - */ -class MD3Test7State extends MusicBeatState -{ - static inline var PAGE_INDEX:Int = 7; - static inline var TOTAL_PAGES:Int = 8; - - var settingsBox:MaterialBox; - var infoBox:MaterialBox; - var miniBox:MaterialBox; - - // Controls inside settingsBox - var slider:MaterialSlider; - var toggle:MaterialSwitch; - var checkbox:MaterialCheckbox; - var statusText:FlxText; - - // Log inside infoBox - var logText:FlxText; - var logLines:Array = []; - - override function create():Void - { - super.create(); - Cursor.show(); - - // Background - var bg = new FlxSprite(); - bg.makeGraphic(FlxG.width, FlxG.height, MD3Theme.background); - add(bg); - - // ---------------------------------------------------------------- - // Settings box (left) — has interactive controls inside - // ---------------------------------------------------------------- - settingsBox = new MaterialBox(24, 40, 300, 260, "Settings"); - settingsBox.canMinimize = true; - - // Volume slider - var volLabel = new FlxText(10, 12, 220, "Volume", 12); - volLabel.setFormat(Paths.font("phantom.ttf"), 12, MD3Theme.onSurfaceVariant, LEFT); - settingsBox.content.add(volLabel); - - slider = new MaterialSlider(10, 30, 260, 70, 0, 100); - settingsBox.content.add(slider); - - // Fullscreen toggle - var fsLabel = new FlxText(10, 72, 180, "Fullscreen", 12); - fsLabel.setFormat(Paths.font("phantom.ttf"), 12, MD3Theme.onSurface, LEFT); - settingsBox.content.add(fsLabel); - - toggle = new MaterialSwitch(236, 66, false); - settingsBox.content.add(toggle); - - // Anti-aliasing checkbox - checkbox = new MaterialCheckbox(10, 106, "Anti-aliasing", true, function(v) { log('AA: $v'); }); - settingsBox.content.add(checkbox); - - // Apply button - var applyBtn = new MaterialButton(10, 150, "Apply", FILLED, 120, function() - { - log('Volume set to ${Std.int(slider.value)}'); - }); - settingsBox.content.add(applyBtn); - - // Reset button (tonal) - var resetBtn = new MaterialButton(140, 150, "Reset", OUTLINED, 110, function() - { - slider.value = 70; - log("Settings reset"); - }); - settingsBox.content.add(resetBtn); - - // Live status text at bottom of content - statusText = new FlxText(10, 192, 280, "Double-click title bar to minimize", 11); - statusText.setFormat(Paths.font("phantom.ttf"), 11, MD3Theme.onSurfaceVariant, LEFT); - settingsBox.content.add(statusText); - - add(settingsBox); - - // ---------------------------------------------------------------- - // Info / log box (right) — shows a scrolling log, has close button - // ---------------------------------------------------------------- - infoBox = new MaterialBox(340, 40, 260, 200, "Event Log"); - infoBox.canMinimize = true; - infoBox.onClose = function() - { - infoBox.visible = false; - log("[info box closed]"); - }; - - logText = new FlxText(8, 8, 240, "", 11); - logText.setFormat(Paths.font("phantom.ttf"), 11, MD3Theme.onSurface, LEFT); - infoBox.content.add(logText); - - add(infoBox); - - // ---------------------------------------------------------------- - // Mini box (bottom center) — very small, minimizable only - // ---------------------------------------------------------------- - miniBox = new MaterialBox(220, 330, 200, 150, "Mini Panel"); - miniBox.canMinimize = true; - - var miniChip = new MaterialChip("Chip A", ASSIST, false, null); - miniChip.setPosition(8, 10); - miniBox.content.add(miniChip); - - var miniChip2 = new MaterialChip("Chip B", FILTER, false, null); - miniChip2.setPosition(90, 10); - miniBox.content.add(miniChip2); - - var miniBtn = new MaterialButton(8, 55, "Action", TEXT, 160, function() - { - log("Mini panel action"); - }); - miniBox.content.add(miniBtn); - - var miniHint = new FlxText(8, 88, 180, "Double-click title to collapse", 10); - miniHint.setFormat(Paths.font("phantom.ttf"), 10, MD3Theme.onSurfaceVariant, LEFT); - miniBox.content.add(miniHint); - - add(miniBox); - - // ---------------------------------------------------------------- - // Accent color buttons (top-right strip) - // ---------------------------------------------------------------- - buildAccentStrip(); - - // ---------------------------------------------------------------- - // Page footer - // ---------------------------------------------------------------- - var footer = new FlxText(0, FlxG.height - 28, FlxG.width, - '← → to navigate Page ${PAGE_INDEX + 1} / $TOTAL_PAGES ESC to exit', 13); - footer.setFormat(Paths.font("phantom.ttf"), 13, MD3Theme.onSurfaceVariant, CENTER); - add(footer); - - addTouchPad('LEFT_RIGHT', 'B'); - - log("MaterialBox demo loaded"); - log("Try dragging the title bars"); - } - - // ---------------------------------------------------------------- - // Compact accent strip (top-right) - // ---------------------------------------------------------------- - - static var ACCENT_STRIP:Array = [0xFF6750A4, 0xFF006B5F, 0xFFB3261E, 0xFF146C2E, 0xFFAA8000]; - var accentSwatches:Array = []; - - function buildAccentStrip():Void - { - var lbl = new FlxText(0, 6, FlxG.width - 10, "ACCENT →", 11); - lbl.setFormat(Paths.font("phantom.ttf"), 11, MD3Theme.onSurfaceVariant, RIGHT); - add(lbl); - - var sx = FlxG.width - 10 - ACCENT_STRIP.length * 30; - var sy:Float = 20; - for (i in 0...ACCENT_STRIP.length) - { - var swatch = new FlxSprite(sx + i * 30, sy); - swatch.makeGraphic(24, 24, ACCENT_STRIP[i]); - accentSwatches.push(swatch); - add(swatch); - } - } - - // ---------------------------------------------------------------- - // Log helper - // ---------------------------------------------------------------- - - function log(msg:String):Void - { - logLines.push(msg); - if (logLines.length > 10) logLines.shift(); - if (logText != null) logText.text = logLines.join("\n"); - } - - // ---------------------------------------------------------------- - // Update - // ---------------------------------------------------------------- - - override function update(elapsed:Float):Void - { - super.update(elapsed); - - // Sync status text with slider value - statusText.text = 'Volume: ${Std.int(slider.value)} AA: ${checkbox.checked} Fullscreen: ${toggle.checked}'; - - #if FLX_MOUSE - if (FlxG.mouse.justPressed) - { - var mx = FlxG.mouse.x; - var my = FlxG.mouse.y; - - for (i in 0...accentSwatches.length) - { - var s = accentSwatches[i]; - if (mx >= s.x && mx <= s.x + s.width && my >= s.y && my <= s.y + s.height) - { - MD3Theme.setAccent(ACCENT_STRIP[i]); - // Re-tint background to match new surface color - cast(members[0], FlxSprite).color = MD3Theme.background; - break; - } - } - } - #end - - if (controls.UI_LEFT_P || FlxG.keys.justPressed.LEFT) - FlxG.switchState(new MD3Test6State()); - if (controls.UI_RIGHT_P || FlxG.keys.justPressed.RIGHT) - FlxG.switchState(new MD3TestState()); - if (controls.BACK) - FlxG.switchState(new funkin.ui.title.TitleState()); - } -} diff --git a/source/funkin/ui/debug/MD3TestState.hx b/source/funkin/ui/debug/MD3TestState.hx deleted file mode 100644 index a21a1e3633b..00000000000 --- a/source/funkin/ui/debug/MD3TestState.hx +++ /dev/null @@ -1,100 +0,0 @@ -package funkin.ui.debug; - -import flixel.FlxG; -import flixel.FlxSprite; -import flixel.text.FlxText; -import flixel.util.FlxColor; -import funkin.ui.MusicBeatState; -import funkin.ui.debug.MD3Test1State; -import funkin.ui.debug.MD3Test7State; -import funkin.ui.components.md3.*; - -/** - * MD3 component debug viewer — Page 1/4: Buttons, Icon Buttons, FABs, Dividers. - * Navigate with LEFT/RIGHT arrow keys. Press BACK/ESC to exit. - */ -class MD3TestState extends MusicBeatState -{ - static inline var PAGE_INDEX:Int = 0; - static inline var TOTAL_PAGES:Int = 8; - - var snackbar:MaterialSnackbar; - - override function create():Void - { - super.create(); - Cursor.show(); - - var bg = new FlxSprite(); - bg.makeGraphic(FlxG.width, FlxG.height, 0xFFFEF7FF); - add(bg); - - buildContent(); - - var pageLabel = new FlxText(0, FlxG.height - 28, FlxG.width, - '← → to navigate Page ${PAGE_INDEX + 1} / $TOTAL_PAGES ESC to exit', 14); - pageLabel.setFormat(Paths.font("phantom.ttf"), 14, 0xFF49454F, CENTER); - add(pageLabel); - - snackbar = new MaterialSnackbar(340); - add(snackbar); - - addTouchPad('LEFT_RIGHT', 'B'); - } - - function sectionLabel(text:String, lx:Float, ly:Float):Void - { - var lbl = new FlxText(lx, ly, 0, text, 12); - lbl.setFormat(Paths.font("phantom.ttf"), 12, 0xFF79747E, LEFT); - add(lbl); - } - - function buildContent():Void - { - sectionLabel("Buttons", 20, 12); - - var btnFilled = new MaterialButton(20, 36, "Filled", FILLED, 120, function() { snackbar.show("Filled button clicked!", 3); }); - add(btnFilled); - var btnOutlined = new MaterialButton(155, 36, "Outlined", OUTLINED, 120, function() { snackbar.show("Outlined button clicked!", 3); }); - add(btnOutlined); - var btnText = new MaterialButton(290, 36, "Text", TEXT, 100, function() { snackbar.show("Text button clicked!", 3); }); - add(btnText); - var btnDisabled = new MaterialButton(405, 36, "Disabled", FILLED, 120); - btnDisabled.enabled = false; - add(btnDisabled); - - sectionLabel("Icon Buttons", 20, 96); - - add(new MaterialIconButton(20, 118, STANDARD, function() { snackbar.show("Icon: Standard", 2); })); - add(new MaterialIconButton(72, 118, FILLED, function() { snackbar.show("Icon: Filled", 2); })); - add(new MaterialIconButton(124, 118, FILLED_TONAL, function() { snackbar.show("Icon: Tonal", 2); })); - add(new MaterialIconButton(176, 118, OUTLINED, function() { snackbar.show("Icon: Outlined", 2); })); - - sectionLabel("FAB", 20, 178); - - add(new MaterialFAB(20, 200, SMALL, "", function() { snackbar.show("FAB Small", 2); })); - add(new MaterialFAB(72, 196, REGULAR, "", function() { snackbar.show("FAB Regular", 2); })); - add(new MaterialFAB(136, 186, LARGE, "", function() { snackbar.show("FAB Large", 2); })); - add(new MaterialFAB(256, 206, REGULAR, "New song", function() { snackbar.show("Extended FAB clicked", 2); })); - - sectionLabel("Dividers", 20, 300); - - add(new MaterialDivider(20, 320, 400, false)); - add(new MaterialDivider(20, 340, 400, false, 60, 60)); - sectionLabel("← inset", 430, 334); - add(new MaterialDivider(20, 355, 100, true)); - sectionLabel("↕ vertical", 32, 358); - } - - override function update(elapsed:Float):Void - { - super.update(elapsed); - - if (controls.UI_LEFT_P || FlxG.keys.justPressed.LEFT) - FlxG.switchState(new MD3Test7State()); - if (controls.UI_RIGHT_P || FlxG.keys.justPressed.RIGHT) - FlxG.switchState(new MD3Test1State()); - if (controls.BACK) - FlxG.switchState(new funkin.ui.title.TitleState()); - } -} diff --git a/source/funkin/util/CoolUtil.hx b/source/funkin/util/CoolUtil.hx index e4eaa733210..b0440db05b7 100644 --- a/source/funkin/util/CoolUtil.hx +++ b/source/funkin/util/CoolUtil.hx @@ -2,12 +2,19 @@ package funkin.util; import openfl.utils.Assets; import lime.utils.Assets as LimeAssets; +#if android +import lime.system.JNI; +#end #if cpp @:cppFileCode('#include ') #end class CoolUtil { + #if android + private static var showMessageBox_jni:Dynamic = null; + #end + // Legacy update checker variables (forwarded to UpdateManager for compatibility) public static var hasUpdate(get, never):Bool; public static var latestVersion(get, never):String; @@ -197,11 +204,28 @@ class CoolUtil public static function showPopUp(message:String, title:String):Void { - /*#if android - AndroidTools.showAlertDialog(title, message, {name: "OK", func: null}, null); - #else*/ + #if android + try + { + if (showMessageBox_jni == null) + { + showMessageBox_jni = JNI.createStaticMethod( + 'com/leninasto/plusengine/PlusEngineExtension', + 'showMessageBox', + '(Ljava/lang/String;Ljava/lang/String;)V' + ); + } + + showMessageBox_jni(title, message); + } + catch (e:Dynamic) + { + trace('[CoolUtil] Native showPopUp failed: ' + e); + FlxG.stage.window.alert(message, title); + } + #else FlxG.stage.window.alert(message, title); - //#end + #end } #if cpp diff --git a/source/kotlin/FileManagerActivity.kt b/source/kotlin/FileManagerActivity.kt index 2454c942a9a..6094b54d9d4 100644 --- a/source/kotlin/FileManagerActivity.kt +++ b/source/kotlin/FileManagerActivity.kt @@ -1,3 +1,27 @@ +/* + * Copyright (C) 2026 Lenin + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software. + * + * This Software may not be claimed as the original work of any other + * individual or entity. + * + * Attribution to the original author is appreciated, but is not required. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. + * + * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + package com.leninasto.plusengine import android.Manifest diff --git a/source/kotlin/FileUtil.kt b/source/kotlin/FileUtil.kt index f68dc8e6ec2..dd9b09c9645 100644 --- a/source/kotlin/FileUtil.kt +++ b/source/kotlin/FileUtil.kt @@ -1,3 +1,27 @@ +/* + * Copyright (C) 2026 Lenin + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software. + * + * This Software may not be claimed as the original work of any other + * individual or entity. + * + * Attribution to the original author is appreciated, but is not required. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. + * + * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + package com.leninasto.plusengine import android.content.Context diff --git a/source/kotlin/NativeCrashActivity.kt b/source/kotlin/NativeCrashActivity.kt new file mode 100644 index 00000000000..122f395aca3 --- /dev/null +++ b/source/kotlin/NativeCrashActivity.kt @@ -0,0 +1,111 @@ +/* + * Copyright (C) 2026 Lenin + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software. + * + * This Software may not be claimed as the original work of any other + * individual or entity. + * + * Attribution to the original author is appreciated, but is not required. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. + * + * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +package com.leninasto.plusengine + +import android.os.Bundle +import android.view.ViewGroup +import android.widget.Button +import android.widget.LinearLayout +import android.widget.ScrollView +import android.widget.TextView +import androidx.appcompat.app.AppCompatActivity +import android.content.ClipboardManager +import android.content.ClipData + +/** + * Simple activity to display native Java/Kotlin crash information. + */ +class NativeCrashActivity : AppCompatActivity() { + + companion object { + const val EXTRA_CRASH_TITLE = "crash_title" + const val EXTRA_CRASH_MESSAGE = "crash_message" + const val EXTRA_CRASH_TRACE = "crash_trace" + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + val crashTitle = intent.getStringExtra(EXTRA_CRASH_TITLE) ?: "Native Crash" + val crashMessage = intent.getStringExtra(EXTRA_CRASH_MESSAGE) ?: "No message" + val crashTrace = intent.getStringExtra(EXTRA_CRASH_TRACE) ?: "No stack trace" + + title = "Crash Report" + + val root = LinearLayout(this).apply { + orientation = LinearLayout.VERTICAL + setPadding(dp(16), dp(16), dp(16), dp(16)) + layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT) + } + + val header = TextView(this).apply { + text = "${crashTitle}: ${crashMessage}" + textSize = 16f + setPadding(0, 0, 0, dp(12)) + } + + val scroll = ScrollView(this).apply { + layoutParams = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 0, 1f) + } + + val traceText = TextView(this).apply { + text = crashTrace + textSize = 12f + setTextIsSelectable(true) + } + scroll.addView(traceText) + + val actions = LinearLayout(this).apply { + orientation = LinearLayout.HORIZONTAL + layoutParams = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) + } + + val copyButton = Button(this).apply { + text = "Copy" + setOnClickListener { + val clipboard = getSystemService(CLIPBOARD_SERVICE) as ClipboardManager + clipboard.setPrimaryClip(ClipData.newPlainText("Native Crash Trace", crashTrace)) + } + } + + val closeButton = Button(this).apply { + text = "Close" + setOnClickListener { finishAffinity() } + } + + actions.addView(copyButton, LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f)) + actions.addView(closeButton, LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f)) + + root.addView(header) + root.addView(scroll) + root.addView(actions) + + setContentView(root) + } + + private fun dp(value: Int): Int { + return (value * resources.displayMetrics.density).toInt() + } +} diff --git a/source/kotlin/NativeCrashHandler.kt b/source/kotlin/NativeCrashHandler.kt new file mode 100644 index 00000000000..58bd26e141d --- /dev/null +++ b/source/kotlin/NativeCrashHandler.kt @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2026 Lenin + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software. + * + * This Software may not be claimed as the original work of any other + * individual or entity. + * + * Attribution to the original author is appreciated, but is not required. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. + * + * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +package com.leninasto.plusengine + +import android.content.Intent +import android.util.Log +import org.haxe.extension.Extension +import java.io.PrintWriter +import java.io.StringWriter +import java.util.concurrent.atomic.AtomicBoolean + +/** + * Captures Java/Kotlin crashes and opens a native crash activity. + */ +object NativeCrashHandler { + + private const val TAG = "NativeCrashHandler" + private val installed = AtomicBoolean(false) + + @JvmStatic + fun install() { + if (installed.getAndSet(true)) return + + val previousHandler = Thread.getDefaultUncaughtExceptionHandler() + Thread.setDefaultUncaughtExceptionHandler { thread, throwable -> + try { + showCrashActivity(throwable) + } catch (handlerError: Throwable) { + Log.e(TAG, "Failed to open crash activity", handlerError) + } + + if (previousHandler != null) { + previousHandler.uncaughtException(thread, throwable) + } + } + } + + @JvmStatic + fun showCrashActivity(throwable: Throwable) { + val activity = Extension.mainActivity ?: return + val stackTrace = throwable.toDetailedStackTrace() + + activity.runOnUiThread { + val intent = Intent(activity, NativeCrashActivity::class.java).apply { + putExtra(NativeCrashActivity.EXTRA_CRASH_TITLE, throwable.javaClass.simpleName) + putExtra(NativeCrashActivity.EXTRA_CRASH_MESSAGE, throwable.message ?: "No message") + putExtra(NativeCrashActivity.EXTRA_CRASH_TRACE, stackTrace) + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP) + } + activity.startActivity(intent) + } + } + + private fun Throwable.toDetailedStackTrace(): String { + val writer = StringWriter() + val printer = PrintWriter(writer) + this.printStackTrace(printer) + printer.flush() + return writer.toString() + } +} diff --git a/source/kotlin/NativeUI.kt b/source/kotlin/NativeUI.kt index 65beb8114da..a8c00208726 100644 --- a/source/kotlin/NativeUI.kt +++ b/source/kotlin/NativeUI.kt @@ -1,3 +1,27 @@ +/* + * Copyright (C) 2026 Lenin + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software. + * + * This Software may not be claimed as the original work of any other + * individual or entity. + * + * Attribution to the original author is appreciated, but is not required. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. + * + * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + package com.leninasto.plusengine import android.content.Context diff --git a/source/kotlin/PlusEngineExtension.kt b/source/kotlin/PlusEngineExtension.kt index c21065715d0..0cb585e0bc9 100644 --- a/source/kotlin/PlusEngineExtension.kt +++ b/source/kotlin/PlusEngineExtension.kt @@ -1,3 +1,27 @@ +/* + * Copyright (C) 2026 Lenin + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software. + * + * This Software may not be claimed as the original work of any other + * individual or entity. + * + * Attribution to the original author is appreciated, but is not required. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. + * + * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + package com.leninasto.plusengine import android.content.Intent @@ -106,5 +130,21 @@ class PlusEngineExtension : Extension() { val stat = android.os.StatFs(android.os.Environment.getExternalStorageDirectory().path) return stat.availableBlocksLong * stat.blockSizeLong } + + /** + * Show a native MD3 message box from Haxe. + */ + @JvmStatic + fun showMessageBox(title: String, message: String) { + val activity = Extension.mainActivity ?: return + activity.runOnUiThread { + NativeUI.showDialog( + context = activity, + title = title, + message = message, + positiveText = "OK" + ) + } + } } } diff --git a/source/kotlin/components/DropDown.kt b/source/kotlin/components/DropDown.kt new file mode 100644 index 00000000000..f186930453c --- /dev/null +++ b/source/kotlin/components/DropDown.kt @@ -0,0 +1,195 @@ +/* + * Copyright (C) 2026 Lenin + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software. + * + * This Software may not be claimed as the original work of any other + * individual or entity. + * + * Attribution to the original author is appreciated, but is not required. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. + * + * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +package com.leninasto.plusengine.components + +import android.text.Editable +import android.text.TextWatcher +import android.view.ViewGroup +import android.widget.ArrayAdapter +import android.widget.LinearLayout +import android.widget.ListView +import androidx.appcompat.app.AlertDialog +import com.leninasto.plusengine.NativeCrashHandler +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.google.android.material.textfield.TextInputEditText +import com.google.android.material.textfield.TextInputLayout +import org.haxe.extension.Extension +import org.json.JSONArray +import java.util.concurrent.atomic.AtomicBoolean +import java.util.concurrent.atomic.AtomicInteger + +/** + * Native Android MD3 dropdown helper. + * + * This class exposes static methods for JNI so Haxe can open + * a native single-choice popup and poll the selected item index. + */ +object DropDown { + + const val NO_SELECTION: Int = -1 + const val CANCELED: Int = -2 + + private val pendingSelection: AtomicInteger = AtomicInteger(NO_SELECTION) + private val dialogVisible: AtomicBoolean = AtomicBoolean(false) + + @JvmStatic + fun showDropDown(title: String?, itemsJson: String, selectedIndex: Int): Boolean { + NativeCrashHandler.install() + val activity = Extension.mainActivity ?: return false + val items = parseItems(itemsJson) + if (items.isEmpty()) return false + + activity.runOnUiThread { + try { + if (dialogVisible.get()) return@runOnUiThread + + dialogVisible.set(true) + val safeSelectedIndex = selectedIndex.coerceIn(0, items.lastIndex) + val displayTitle = if (title.isNullOrBlank()) "Select option" else title + val filteredItems = items.toMutableList() + val filteredIndices = items.indices.toMutableList() + + val listView = ListView(activity).apply { + choiceMode = ListView.CHOICE_MODE_SINGLE + } + val adapter = ArrayAdapter(activity, android.R.layout.simple_list_item_single_choice, filteredItems) + listView.adapter = adapter + + val searchInput = TextInputEditText(activity) + val searchLayout = TextInputLayout(activity).apply { + hint = "Search" + addView( + searchInput, + LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) + ) + } + + val container = LinearLayout(activity).apply { + orientation = LinearLayout.VERTICAL + setPadding(dp(20), dp(8), dp(20), dp(0)) + addView(searchLayout, ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)) + addView( + listView, + LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, dp(280)) + ) + } + + var dialogRef: AlertDialog? = null + + fun refreshList(query: String?) { + val trimmed = query?.trim()?.lowercase() ?: "" + filteredItems.clear() + filteredIndices.clear() + + for (index in items.indices) { + val item = items[index] + if (trimmed.isEmpty() || item.lowercase().contains(trimmed)) { + filteredItems.add(item) + filteredIndices.add(index) + } + } + + adapter.notifyDataSetChanged() + val selectedFilteredIndex = filteredIndices.indexOf(safeSelectedIndex) + if (selectedFilteredIndex >= 0) { + listView.setItemChecked(selectedFilteredIndex, true) + listView.setSelection(selectedFilteredIndex) + } else { + listView.clearChoices() + } + } + + searchInput.addTextChangedListener(object : TextWatcher { + override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} + override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { + refreshList(s?.toString()) + } + override fun afterTextChanged(s: Editable?) {} + }) + + listView.setOnItemClickListener { _, _, position, _ -> + if (position >= 0 && position < filteredIndices.size) { + pendingSelection.set(filteredIndices[position]) + dialogRef?.dismiss() + } + } + + dialogRef = MaterialAlertDialogBuilder(activity) + .setTitle(displayTitle) + .setView(container) + .setNegativeButton(android.R.string.cancel) { _, _ -> + pendingSelection.set(CANCELED) + } + .setOnCancelListener { + pendingSelection.set(CANCELED) + } + .setOnDismissListener { + dialogVisible.set(false) + } + .show() + + refreshList(null) + } catch (throwable: Throwable) { + dialogVisible.set(false) + pendingSelection.set(CANCELED) + NativeCrashHandler.showCrashActivity(throwable) + } + } + + return true + } + + @JvmStatic + fun pollSelection(): Int { + val currentValue = pendingSelection.get() + if (currentValue == NO_SELECTION) return NO_SELECTION + + pendingSelection.set(NO_SELECTION) + return currentValue + } + + @JvmStatic + fun isDialogVisible(): Boolean { + return dialogVisible.get() + } + + private fun parseItems(itemsJson: String): List { + return try { + val json = JSONArray(itemsJson) + buildList(json.length()) { + for (index in 0 until json.length()) { + add(json.optString(index, "")) + } + } + } catch (_: Throwable) { + emptyList() + } + } + + private fun dp(value: Int): Int { + val activity = Extension.mainActivity ?: return value + return (value * activity.resources.displayMetrics.density).toInt() + } +} diff --git a/source/kotlin/include_android.xml b/source/kotlin/include_android.xml index 6f0e86ff6d4..adc7131c8c9 100644 --- a/source/kotlin/include_android.xml +++ b/source/kotlin/include_android.xml @@ -15,6 +15,9 @@