|
| 1 | +# FlutterX Settings UI DSL 2.0 Redesign Draft |
| 2 | + |
| 3 | +## Current Issues |
| 4 | + |
| 5 | +- The basic page mixes global language, Flutter update checking, Dio socket, request list display, assets scan, and copy-key mapping in one long vertical flow. |
| 6 | +- The language row uses six radio buttons in one line, which becomes crowded in narrow settings windows and creates a very wide first row. |
| 7 | +- The Flutter version section contains enable state, mirror selection, current version, changelog, ignore list, and documentation, but the hierarchy is not clear. |
| 8 | +- The Dio section combines server control and display options. The port field and restart button do not visually read as a single server-control row. |
| 9 | +- Several dependent options stay visually active even when their parent feature is disabled. |
| 10 | +- Advanced options such as copy-key mapping are always visible, increasing scan cost for regular users. |
| 11 | + |
| 12 | +## Proposed Information Architecture |
| 13 | + |
| 14 | +```text |
| 15 | +Basic |
| 16 | +├─ Language |
| 17 | +│ └─ Language: [System v] Restart IDEA to take effect |
| 18 | +│ |
| 19 | +├─ Flutter SDK Updates |
| 20 | +│ ├─ [x] Check for new Flutter versions |
| 21 | +│ ├─ Flutter release source: [Use default v] Visit |
| 22 | +│ ├─ Pub server: [Use default v] Visit |
| 23 | +│ ├─ Current version: 3.44.0 [Changelog] |
| 24 | +│ ├─ Ignored versions: [list] |
| 25 | +│ └─ Documentation |
| 26 | +│ |
| 27 | +├─ Dio Socket |
| 28 | +│ ├─ [x] Enable FlutterX socket (?) |
| 29 | +│ ├─ Listening port: [9999] [Restart] |
| 30 | +│ └─ [x] New API notification |
| 31 | +│ |
| 32 | +├─ Dio Request List |
| 33 | +│ ├─ [x] Display domain name [ ] Display query parameters |
| 34 | +│ ├─ [x] Show request method [x] Display status code |
| 35 | +│ ├─ [x] Display request time [x] Display date |
| 36 | +│ ├─ [x] Bold URL [x] Display data size |
| 37 | +│ └─ Documentation |
| 38 | +│ |
| 39 | +├─ Assets Scan |
| 40 | +│ ├─ [x] Enable assets scan |
| 41 | +│ ├─ Trigger text: [assets] |
| 42 | +│ ├─ Trigger length:[3] |
| 43 | +│ ├─ Scan folder: [assets] |
| 44 | +│ └─ Documentation |
| 45 | +│ |
| 46 | +└─ Advanced |
| 47 | + └─ Copy All Keys |
| 48 | + ├─ URL: [url] |
| 49 | + ├─ Method: [method] |
| 50 | + ├─ Headers: [headers] |
| 51 | + ├─ Query params: [query] |
| 52 | + ├─ Body: [body] |
| 53 | + ├─ Response status code:[responseStatusCode] |
| 54 | + ├─ Response: [response] |
| 55 | + ├─ Request time: [requestTime] |
| 56 | + └─ Timestamp: [timestamp] |
| 57 | +``` |
| 58 | + |
| 59 | +## Layout Rules |
| 60 | + |
| 61 | +- Use `comboBox` for language selection instead of six radio buttons. This keeps the first group compact and localizes better. |
| 62 | +- Keep server control in a dedicated `Dio Socket` group and request display switches in a separate `Dio Request List` group. |
| 63 | +- Wrap dependent rows with `indent { ... }.enabledIf(master.selected)` so disabled features look disabled. |
| 64 | +- Move copy-key mapping into `collapsibleGroup("Advanced")`, collapsed by default if supported by the current platform version. |
| 65 | +- Prefer `row("Label:")` for editable fields so the DSL creates correct label relationships and accessibility metadata. |
| 66 | +- Use `columns(COLUMNS_SHORT)` for the port and numeric fields, and `resizableColumn()` only on fields that should expand. |
| 67 | + |
| 68 | +## Kotlin UI DSL 2.0 Draft |
| 69 | + |
| 70 | +```kotlin |
| 71 | +fun redesignedBasicSettingsPanel( |
| 72 | + project: Project, |
| 73 | + model: AppStateModel, |
| 74 | + dioSetting: DoxListeningSetting, |
| 75 | + parentDisposable: Disposable, |
| 76 | + onChange: (state: AppStateModel) -> Unit |
| 77 | +): DialogPanel { |
| 78 | + val languageList = listOf("System", "中文", "繁體", "English", "한국어", "日本語") |
| 79 | + |
| 80 | + return panel { |
| 81 | + group(PluginBundle.get("setting.language")) { |
| 82 | + row("${PluginBundle.get("setting.language")}:") { |
| 83 | + comboBox(languageList) |
| 84 | + .bindItem( |
| 85 | + { model.lang }, |
| 86 | + { model.lang = it ?: "System" } |
| 87 | + ) |
| 88 | + .columns(COLUMNS_MEDIUM) |
| 89 | + }.rowComment(PluginBundle.get("setting.reset.tip")) |
| 90 | + } |
| 91 | + |
| 92 | + group(PluginBundle.get("check_flutter_version_title")) { |
| 93 | + lateinit var checkFlutterVersion: Cell<JBCheckBox> |
| 94 | + |
| 95 | + row { |
| 96 | + checkFlutterVersion = checkBox(PluginBundle.get("open")) |
| 97 | + .bindSelected(dioSetting::checkFlutterVersion) |
| 98 | + }.rowComment(PluginBundle.get("check_flutter_version_comment")) |
| 99 | + |
| 100 | + indent { |
| 101 | + row("Flutter release source:") { |
| 102 | + PubDevMirrorImageSetting.flutterCheckComboBox(this, dioSetting) |
| 103 | + browserLink("Visit", dioSetting.checkFlutterVersionUrl) |
| 104 | + } |
| 105 | + |
| 106 | + row("Pub server:") { |
| 107 | + PubDevMirrorImageSetting.pubServerComboBox(this, dioSetting) |
| 108 | + browserLink("Visit", dioSetting.pubServerUrl) |
| 109 | + } |
| 110 | + |
| 111 | + row("Current Flutter version:") { |
| 112 | + cell(FlutterVersionCheckPanel(project = project)) |
| 113 | + } |
| 114 | + |
| 115 | + row(PluginBundle.get("ignore_this_version_to_check")) { |
| 116 | + cell(FlutterVersionIgnoreList()) |
| 117 | + .align(AlignX.FILL) |
| 118 | + .resizableColumn() |
| 119 | + }.resizableRow() |
| 120 | + }.enabledIf(checkFlutterVersion.selected) |
| 121 | + |
| 122 | + documentCommentRow(Links.CHECK_FLUTTER_VERSION_DOC_LINK) |
| 123 | + } |
| 124 | + |
| 125 | + group("Dio Socket") { |
| 126 | + lateinit var enableSocket: Cell<JBCheckBox> |
| 127 | + |
| 128 | + row { |
| 129 | + enableSocket = checkBox("Enable FlutterX socket") |
| 130 | + .bindSelected(dioSetting::enableFlutterXDioSocket) |
| 131 | + .gap(RightGap.SMALL) |
| 132 | + contextHelp( |
| 133 | + PluginBundle.get("enable_flutterx_socket_setting_contexthelp") + " (Dio, SP, Hive, Log)", |
| 134 | + "Tips" |
| 135 | + ) |
| 136 | + } |
| 137 | + |
| 138 | + indent { |
| 139 | + row("Listening port:") { |
| 140 | + intTextField() |
| 141 | + .bindIntText( |
| 142 | + { model.serverPort.toInt() }, |
| 143 | + { model.serverPort = it.toString() } |
| 144 | + ) |
| 145 | + .columns(COLUMNS_SHORT) |
| 146 | + .gap(RightGap.SMALL) |
| 147 | + .onChanged { component -> |
| 148 | + PluginStateService.changeState { it.copy(serverPort = component.text) } |
| 149 | + } |
| 150 | + |
| 151 | + button(PluginBundle.get("reset")) { |
| 152 | + DioApiService.getInstance().reset(project) |
| 153 | + } |
| 154 | + } |
| 155 | + |
| 156 | + row { |
| 157 | + checkBox(PluginBundle.get("setting.new.tips")) |
| 158 | + .bindSelected(model::apiInToolwindowTop) |
| 159 | + } |
| 160 | + }.enabledIf(enableSocket.selected) |
| 161 | + } |
| 162 | + |
| 163 | + group("Dio Request List") { |
| 164 | + twoColumnsRow({ |
| 165 | + checkBox(PluginBundle.get("display_domain_name")) |
| 166 | + .bindSelected(dioSetting::showHost) |
| 167 | + }, { |
| 168 | + checkBox(PluginBundle.get("display.query.parameters")) |
| 169 | + .bindSelected(dioSetting::showQueryParams) |
| 170 | + }) |
| 171 | + |
| 172 | + twoColumnsRow({ |
| 173 | + checkBox(PluginBundle.get("show.request.method")) |
| 174 | + .bindSelected(dioSetting::showMethod) |
| 175 | + }, { |
| 176 | + checkBox(PluginBundle.get("display.status.code")) |
| 177 | + .bindSelected(dioSetting::showStatusCode) |
| 178 | + }) |
| 179 | + |
| 180 | + twoColumnsRow({ |
| 181 | + checkBox(PluginBundle.get("display.time")) |
| 182 | + .bindSelected(dioSetting::showTimestamp) |
| 183 | + }, { |
| 184 | + checkBox(PluginBundle.get("time")) |
| 185 | + .bindSelected(dioSetting::showDate) |
| 186 | + }) |
| 187 | + |
| 188 | + twoColumnsRow({ |
| 189 | + checkBox(PluginBundle.get("bold.link")) |
| 190 | + .bindSelected(dioSetting::urlBold) |
| 191 | + }, { |
| 192 | + checkBox(PluginBundle.get("dio.setting.show.data.size")) |
| 193 | + .bindSelected(dioSetting::showDataSize) |
| 194 | + }) |
| 195 | + |
| 196 | + row { |
| 197 | + comment(Links.generateDocCommit(Links.DIO)) |
| 198 | + } |
| 199 | + } |
| 200 | + |
| 201 | + group(PluginBundle.get("ass.setting.title")) { |
| 202 | + lateinit var enableAssetsScan: Cell<JBCheckBox> |
| 203 | + |
| 204 | + row { |
| 205 | + enableAssetsScan = checkBox("Enable assets scan") |
| 206 | + .bindSelected(model::assetsScanEnable) |
| 207 | + } |
| 208 | + |
| 209 | + indent { |
| 210 | + row(PluginBundle.get("ass.1")) { |
| 211 | + textField() |
| 212 | + .bindText(model::assetCompilationTriggerString) |
| 213 | + .columns(COLUMNS_MEDIUM) |
| 214 | + } |
| 215 | + |
| 216 | + row(PluginBundle.get("ass.3")) { |
| 217 | + intTextField() |
| 218 | + .bindIntText(model::assetCompilationTriggerLen) |
| 219 | + .columns(COLUMNS_SHORT) |
| 220 | + } |
| 221 | + |
| 222 | + row(PluginBundle.get("ass.5")) { |
| 223 | + textField() |
| 224 | + .bindText(model::assetScanFolderName) |
| 225 | + .columns(COLUMNS_MEDIUM) |
| 226 | + } |
| 227 | + }.enabledIf(enableAssetsScan.selected) |
| 228 | + |
| 229 | + row { |
| 230 | + comment(Links.generateDocCommit(Links.ACCESS_ICON)) |
| 231 | + } |
| 232 | + } |
| 233 | + |
| 234 | + collapsibleGroup("Advanced") { |
| 235 | + group("Copy All Keys") { |
| 236 | + row("URL:") { textField().bindText(dioSetting.copyKeys::url).columns(COLUMNS_MEDIUM) } |
| 237 | + row("Method:") { textField().bindText(dioSetting.copyKeys::method).columns(COLUMNS_MEDIUM) } |
| 238 | + row("Headers:") { textField().bindText(dioSetting.copyKeys::headers).columns(COLUMNS_MEDIUM) } |
| 239 | + row("Query params:") { textField().bindText(dioSetting.copyKeys::queryParams).columns(COLUMNS_MEDIUM) } |
| 240 | + row("Body:") { textField().bindText(dioSetting.copyKeys::body).columns(COLUMNS_MEDIUM) } |
| 241 | + row("Response status code:") { textField().bindText(dioSetting.copyKeys::responseStatusCode).columns(COLUMNS_MEDIUM) } |
| 242 | + row("Response:") { textField().bindText(dioSetting.copyKeys::response).columns(COLUMNS_MEDIUM) } |
| 243 | + row("Request time:") { textField().bindText(dioSetting.copyKeys::requestTime).columns(COLUMNS_MEDIUM) } |
| 244 | + row("Timestamp:") { textField().bindText(dioSetting.copyKeys::timestamp).columns(COLUMNS_MEDIUM) } |
| 245 | + row { comment(Links.generateDocCommit(Links.DIO_IMAGE)) } |
| 246 | + } |
| 247 | + } |
| 248 | + } |
| 249 | +} |
| 250 | +``` |
| 251 | + |
| 252 | +## Optional Extraction |
| 253 | + |
| 254 | +The current `PubDevMirrorImageSetting.createPanel(...)` and `createFlutterCheckUrlPanel(...)` create complete rows. For the redesigned layout, split them into smaller cell-level helpers: |
| 255 | + |
| 256 | +```kotlin |
| 257 | +fun pubServerComboBox(row: Row, setting: DoxListeningSetting): Cell<ComboBox<DartPubMirrorImage>> |
| 258 | +fun flutterCheckComboBox(row: Row, setting: DoxListeningSetting): Cell<ComboBox<FlutterCheckUrlMirrorImage>> |
| 259 | +``` |
| 260 | + |
| 261 | +This lets the settings page own labels and links while the mirror helper only owns the combo-box model and binding. |
0 commit comments