fresh/view/settings/
entry_dialog.rs

1//! Entry detail dialog for editing complex map entries
2//!
3//! Provides a modal dialog for editing complex map entries using the same
4//! SettingItem/SettingControl infrastructure as the main settings UI.
5
6use super::items::{build_item_from_value, control_to_value, SettingControl, SettingItem};
7use super::schema::{SettingSchema, SettingType};
8use crate::view::controls::{FocusState, TextInputState};
9use serde_json::Value;
10
11/// State for the entry detail dialog
12#[derive(Debug, Clone)]
13pub struct EntryDialogState {
14    /// The entry key (e.g., "rust" for language)
15    pub entry_key: String,
16    /// The map path this entry belongs to (e.g., "/languages", "/lsp")
17    pub map_path: String,
18    /// Human-readable title for the dialog
19    pub title: String,
20    /// Whether this is a new entry (vs editing existing)
21    pub is_new: bool,
22    /// Items in the dialog (using same SettingItem structure as main settings)
23    pub items: Vec<SettingItem>,
24    /// Currently selected item index
25    pub selected_item: usize,
26    /// Sub-focus index within the selected item (for TextList/Map navigation)
27    pub sub_focus: Option<usize>,
28    /// Whether we're in text editing mode
29    pub editing_text: bool,
30    /// Currently focused button (0=Save, 1=Delete, 2=Cancel for existing; 0=Save, 1=Cancel for new)
31    pub focused_button: usize,
32    /// Whether focus is on buttons (true) or items (false)
33    pub focus_on_buttons: bool,
34    /// Whether deletion was requested
35    pub delete_requested: bool,
36    /// Scroll offset for the items area
37    pub scroll_offset: usize,
38    /// Last known viewport height (updated during render)
39    pub viewport_height: usize,
40    /// Hovered item index (for mouse hover feedback)
41    pub hover_item: Option<usize>,
42    /// Hovered button index (for mouse hover feedback)
43    pub hover_button: Option<usize>,
44    /// Original value when dialog was opened (for Cancel to restore)
45    pub original_value: Value,
46    /// Index of first editable item (items before this are read-only)
47    /// Used for rendering separator and focus navigation
48    pub first_editable_index: usize,
49    /// Whether deletion is disabled (for auto-managed entries like plugins)
50    pub no_delete: bool,
51}
52
53impl EntryDialogState {
54    /// Create a dialog from a schema definition
55    ///
56    /// This is the primary, schema-driven constructor. It builds items
57    /// dynamically from the SettingSchema's properties using the same
58    /// build logic as the main settings UI.
59    pub fn from_schema(
60        key: String,
61        value: &Value,
62        schema: &SettingSchema,
63        map_path: &str,
64        is_new: bool,
65        no_delete: bool,
66    ) -> Self {
67        let mut items = Vec::new();
68
69        // Add key field as first item (read-only for existing entries)
70        let key_item = SettingItem {
71            path: "__key__".to_string(),
72            name: "Key".to_string(),
73            description: Some("unique identifier for this entry".to_string()),
74            control: SettingControl::Text(TextInputState::new("Key").with_value(&key)),
75            default: None,
76            modified: false,
77            layer_source: crate::config_io::ConfigLayer::System,
78            read_only: !is_new, // Key is editable only for new entries
79            is_auto_managed: false,
80        };
81        items.push(key_item);
82
83        // Add schema-driven items from object properties
84        if let SettingType::Object { properties } = &schema.setting_type {
85            for prop in properties {
86                let field_name = prop.path.trim_start_matches('/');
87                let field_value = value.get(field_name);
88                let item = build_item_from_value(prop, field_value);
89                items.push(item);
90            }
91        }
92
93        // Sort items: read-only first, then editable
94        items.sort_by_key(|item| !item.read_only);
95
96        // Find the first editable item index
97        let first_editable_index = items
98            .iter()
99            .position(|item| !item.read_only)
100            .unwrap_or(items.len());
101
102        // If all items are read-only, start with focus on buttons
103        let focus_on_buttons = first_editable_index >= items.len();
104        let selected_item = if focus_on_buttons {
105            0
106        } else {
107            first_editable_index
108        };
109
110        let title = if is_new {
111            format!("Add {}", schema.name)
112        } else {
113            format!("Edit {}", schema.name)
114        };
115
116        Self {
117            entry_key: key,
118            map_path: map_path.to_string(),
119            title,
120            is_new,
121            items,
122            selected_item,
123            sub_focus: None,
124            editing_text: false,
125            focused_button: 0,
126            focus_on_buttons,
127            delete_requested: false,
128            scroll_offset: 0,
129            viewport_height: 20, // Default, updated during render
130            hover_item: None,
131            hover_button: None,
132            original_value: value.clone(),
133            first_editable_index,
134            no_delete,
135        }
136    }
137
138    /// Create a dialog for an array item (no key field)
139    ///
140    /// Used for ObjectArray controls where items are identified by index, not key.
141    pub fn for_array_item(
142        index: Option<usize>,
143        value: &Value,
144        schema: &SettingSchema,
145        array_path: &str,
146        is_new: bool,
147    ) -> Self {
148        let mut items = Vec::new();
149
150        // Add schema-driven items from object properties (no key field for arrays)
151        if let SettingType::Object { properties } = &schema.setting_type {
152            for prop in properties {
153                let field_name = prop.path.trim_start_matches('/');
154                let field_value = value.get(field_name);
155                let item = build_item_from_value(prop, field_value);
156                items.push(item);
157            }
158        }
159
160        // Sort items: read-only first, then editable
161        items.sort_by_key(|item| !item.read_only);
162
163        // Find the first editable item index
164        let first_editable_index = items
165            .iter()
166            .position(|item| !item.read_only)
167            .unwrap_or(items.len());
168
169        // If all items are read-only, start with focus on buttons
170        let focus_on_buttons = first_editable_index >= items.len();
171        let selected_item = if focus_on_buttons {
172            0
173        } else {
174            first_editable_index
175        };
176
177        let title = if is_new {
178            format!("Add {}", schema.name)
179        } else {
180            format!("Edit {}", schema.name)
181        };
182
183        Self {
184            entry_key: index.map_or(String::new(), |i| i.to_string()),
185            map_path: array_path.to_string(),
186            title,
187            is_new,
188            items,
189            selected_item,
190            sub_focus: None,
191            editing_text: false,
192            focused_button: 0,
193            focus_on_buttons,
194            delete_requested: false,
195            scroll_offset: 0,
196            viewport_height: 20,
197            hover_item: None,
198            hover_button: None,
199            original_value: value.clone(),
200            first_editable_index,
201            no_delete: false, // Arrays typically allow deletion
202        }
203    }
204
205    /// Get the current key value from the key item
206    pub fn get_key(&self) -> String {
207        // Find the key item by path (may not be first after sorting)
208        for item in &self.items {
209            if item.path == "__key__" {
210                if let SettingControl::Text(state) = &item.control {
211                    return state.value.clone();
212                }
213            }
214        }
215        self.entry_key.clone()
216    }
217
218    /// Get button count (3 for existing entries with Delete, 2 for new/no_delete entries)
219    pub fn button_count(&self) -> usize {
220        if self.is_new || self.no_delete {
221            2 // Save, Cancel (no Delete for new entries or when no_delete is set)
222        } else {
223            3
224        }
225    }
226
227    /// Convert dialog state back to JSON value (excludes the __key__ item)
228    pub fn to_value(&self) -> Value {
229        let mut obj = serde_json::Map::new();
230
231        for item in &self.items {
232            // Skip the special key item - it's stored separately
233            if item.path == "__key__" {
234                continue;
235            }
236
237            let field_name = item.path.trim_start_matches('/');
238            let value = control_to_value(&item.control);
239            obj.insert(field_name.to_string(), value);
240        }
241
242        Value::Object(obj)
243    }
244
245    /// Get currently selected item
246    pub fn current_item(&self) -> Option<&SettingItem> {
247        if self.focus_on_buttons {
248            None
249        } else {
250            self.items.get(self.selected_item)
251        }
252    }
253
254    /// Get currently selected item mutably
255    pub fn current_item_mut(&mut self) -> Option<&mut SettingItem> {
256        if self.focus_on_buttons {
257            None
258        } else {
259            self.items.get_mut(self.selected_item)
260        }
261    }
262
263    /// Move focus to next item or button
264    pub fn focus_next(&mut self) {
265        if self.editing_text {
266            return; // Don't change focus while editing
267        }
268
269        if self.focus_on_buttons {
270            if self.focused_button + 1 < self.button_count() {
271                // Move to next button
272                self.focused_button += 1;
273            } else {
274                // Wrap to first editable item (skip read-only items)
275                if self.first_editable_index < self.items.len() {
276                    self.focus_on_buttons = false;
277                    self.selected_item = self.first_editable_index;
278                }
279                // If all items are read-only, stay on buttons (don't wrap)
280            }
281        } else {
282            // Check if current item is an ObjectArray that can navigate internally
283            let array_nav_result = self.items.get(self.selected_item).and_then(|item| {
284                if let SettingControl::ObjectArray(state) = &item.control {
285                    // Navigation order: entries -> add-new -> exit
286                    match state.focused_index {
287                        Some(idx) if idx + 1 < state.bindings.len() => {
288                            // On entry, can go to next entry
289                            Some(true)
290                        }
291                        Some(_) => {
292                            // On last entry, can go to add-new
293                            Some(true)
294                        }
295                        None => {
296                            // On add-new, exit to next dialog item
297                            Some(false)
298                        }
299                    }
300                } else {
301                    None
302                }
303            });
304
305            match array_nav_result {
306                Some(true) => {
307                    // Navigate within the ObjectArray
308                    if let Some(item) = self.items.get_mut(self.selected_item) {
309                        if let SettingControl::ObjectArray(state) = &mut item.control {
310                            state.focus_next();
311                        }
312                    }
313                }
314                Some(false) => {
315                    // Exit ObjectArray, go to next item
316                    if self.selected_item + 1 < self.items.len() {
317                        self.selected_item += 1;
318                        self.sub_focus = None;
319                        // Initialize next item's ObjectArray if it has entries
320                        self.init_object_array_focus();
321                    } else {
322                        self.focus_on_buttons = true;
323                        self.focused_button = 0;
324                    }
325                }
326                None => {
327                    // Not an ObjectArray, normal navigation
328                    // All items after first_editable_index are editable (sorted)
329                    if self.selected_item + 1 < self.items.len() {
330                        self.selected_item += 1;
331                        self.sub_focus = None;
332                        // Initialize next item's ObjectArray if it has entries
333                        self.init_object_array_focus();
334                    } else {
335                        self.focus_on_buttons = true;
336                        self.focused_button = 0;
337                    }
338                }
339            }
340        }
341
342        self.update_focus_states();
343        self.ensure_selected_visible(self.viewport_height);
344    }
345
346    /// Move focus to previous item or button
347    pub fn focus_prev(&mut self) {
348        if self.editing_text {
349            return; // Don't change focus while editing
350        }
351
352        if self.focus_on_buttons {
353            if self.focused_button > 0 {
354                self.focused_button -= 1;
355            } else {
356                // Move back to last editable item
357                if self.first_editable_index < self.items.len() {
358                    self.focus_on_buttons = false;
359                    self.selected_item = self.items.len().saturating_sub(1);
360                }
361                // If all items are read-only, stay on buttons (don't wrap)
362            }
363        } else {
364            // Check if current item is an ObjectArray that can navigate internally
365            let array_nav_result = self.items.get(self.selected_item).and_then(|item| {
366                if let SettingControl::ObjectArray(state) = &item.control {
367                    // Navigation order (reverse): exit <- entries <- add-new
368                    match state.focused_index {
369                        None => {
370                            // On add-new, can go back to last entry (if any)
371                            if !state.bindings.is_empty() {
372                                Some(true)
373                            } else {
374                                Some(false) // No entries, exit
375                            }
376                        }
377                        Some(0) => {
378                            // On first entry, exit to previous dialog item
379                            Some(false)
380                        }
381                        Some(_) => {
382                            // On entry, can go to previous entry
383                            Some(true)
384                        }
385                    }
386                } else {
387                    None
388                }
389            });
390
391            match array_nav_result {
392                Some(true) => {
393                    // Navigate within the ObjectArray
394                    if let Some(item) = self.items.get_mut(self.selected_item) {
395                        if let SettingControl::ObjectArray(state) = &mut item.control {
396                            state.focus_prev();
397                        }
398                    }
399                }
400                Some(false) => {
401                    // Exit ObjectArray, go to previous editable item (not into read-only)
402                    if self.selected_item > self.first_editable_index {
403                        self.selected_item -= 1;
404                        self.sub_focus = None;
405                        // Initialize previous item's ObjectArray to add-new (end)
406                        self.init_object_array_focus_end();
407                    } else {
408                        // At first editable item, go to buttons
409                        self.focus_on_buttons = true;
410                        self.focused_button = self.button_count().saturating_sub(1);
411                    }
412                }
413                None => {
414                    // Not an ObjectArray, normal navigation
415                    // Don't go below first_editable_index (read-only items)
416                    if self.selected_item > self.first_editable_index {
417                        self.selected_item -= 1;
418                        self.sub_focus = None;
419                        // Initialize previous item's ObjectArray to add-new (end)
420                        self.init_object_array_focus_end();
421                    } else {
422                        // At first editable item, go to buttons
423                        self.focus_on_buttons = true;
424                        self.focused_button = self.button_count().saturating_sub(1);
425                    }
426                }
427            }
428        }
429
430        self.update_focus_states();
431        self.ensure_selected_visible(self.viewport_height);
432    }
433
434    /// Initialize ObjectArray focus to first entry (when arriving from above)
435    fn init_object_array_focus(&mut self) {
436        if let Some(item) = self.items.get_mut(self.selected_item) {
437            if let SettingControl::ObjectArray(state) = &mut item.control {
438                // Start at first entry if there are any, otherwise stay on add-new
439                if !state.bindings.is_empty() {
440                    state.focused_index = Some(0);
441                }
442            }
443        }
444    }
445
446    /// Initialize ObjectArray focus to add-new (when arriving from below)
447    fn init_object_array_focus_end(&mut self) {
448        if let Some(item) = self.items.get_mut(self.selected_item) {
449            if let SettingControl::ObjectArray(state) = &mut item.control {
450                // Start at add-new row (None)
451                state.focused_index = None;
452            }
453        }
454    }
455
456    /// Move to next sub-item within current control (for TextList, Map)
457    pub fn sub_focus_next(&mut self) {
458        if let Some(item) = self.items.get(self.selected_item) {
459            let max_sub = match &item.control {
460                SettingControl::TextList(state) => state.items.len(), // +1 for add-new
461                SettingControl::Map(state) => state.entries.len(),    // +1 for add-new
462                _ => 0,
463            };
464
465            if max_sub > 0 {
466                let current = self.sub_focus.unwrap_or(0);
467                if current < max_sub {
468                    self.sub_focus = Some(current + 1);
469                } else {
470                    // Move to next item
471                    self.sub_focus = None;
472                    self.focus_next();
473                }
474            } else {
475                self.focus_next();
476            }
477        } else {
478            self.focus_next();
479        }
480    }
481
482    /// Move to previous sub-item within current control
483    pub fn sub_focus_prev(&mut self) {
484        if let Some(sub) = self.sub_focus {
485            if sub > 0 {
486                self.sub_focus = Some(sub - 1);
487            } else {
488                self.sub_focus = None;
489            }
490        } else {
491            self.focus_prev();
492        }
493    }
494
495    /// Update focus states for all items
496    pub fn update_focus_states(&mut self) {
497        for (idx, item) in self.items.iter_mut().enumerate() {
498            let state = if !self.focus_on_buttons && idx == self.selected_item {
499                FocusState::Focused
500            } else {
501                FocusState::Normal
502            };
503
504            match &mut item.control {
505                SettingControl::Toggle(s) => s.focus = state,
506                SettingControl::Number(s) => s.focus = state,
507                SettingControl::Dropdown(s) => s.focus = state,
508                SettingControl::Text(s) => s.focus = state,
509                SettingControl::TextList(s) => s.focus = state,
510                SettingControl::Map(s) => s.focus = state,
511                SettingControl::ObjectArray(s) => s.focus = state,
512                SettingControl::Json(s) => s.focus = state,
513                SettingControl::Complex { .. } => {}
514            }
515        }
516    }
517
518    /// Calculate total content height for all items (including separator)
519    pub fn total_content_height(&self) -> usize {
520        let items_height: usize = self
521            .items
522            .iter()
523            .map(|item| item.control.control_height() as usize)
524            .sum();
525        // Add 1 for separator if we have both read-only and editable items
526        let separator_height =
527            if self.first_editable_index > 0 && self.first_editable_index < self.items.len() {
528                1
529            } else {
530                0
531            };
532        items_height + separator_height
533    }
534
535    /// Calculate the Y offset of the selected item (including separator)
536    pub fn selected_item_offset(&self) -> usize {
537        let items_offset: usize = self
538            .items
539            .iter()
540            .take(self.selected_item)
541            .map(|item| item.control.control_height() as usize)
542            .sum();
543        // Add 1 for separator if selected item is after it
544        let separator_offset = if self.first_editable_index > 0
545            && self.first_editable_index < self.items.len()
546            && self.selected_item >= self.first_editable_index
547        {
548            1
549        } else {
550            0
551        };
552        items_offset + separator_offset
553    }
554
555    /// Calculate the height of the selected item
556    pub fn selected_item_height(&self) -> usize {
557        self.items
558            .get(self.selected_item)
559            .map(|item| item.control.control_height() as usize)
560            .unwrap_or(1)
561    }
562
563    /// Ensure the selected item is visible within the viewport
564    pub fn ensure_selected_visible(&mut self, viewport_height: usize) {
565        if self.focus_on_buttons {
566            // Scroll to bottom when buttons are focused
567            let total = self.total_content_height();
568            if total > viewport_height {
569                self.scroll_offset = total.saturating_sub(viewport_height);
570            }
571            return;
572        }
573
574        let item_start = self.selected_item_offset();
575        let item_end = item_start + self.selected_item_height();
576
577        // If item starts before viewport, scroll up
578        if item_start < self.scroll_offset {
579            self.scroll_offset = item_start;
580        }
581        // If item ends after viewport, scroll down
582        else if item_end > self.scroll_offset + viewport_height {
583            self.scroll_offset = item_end.saturating_sub(viewport_height);
584        }
585    }
586
587    /// Ensure the cursor within a JSON editor is visible
588    ///
589    /// When editing a multiline JSON control, this adjusts scroll_offset
590    /// to keep the cursor row visible within the viewport.
591    pub fn ensure_cursor_visible(&mut self) {
592        if !self.editing_text || self.focus_on_buttons {
593            return;
594        }
595
596        // Get cursor row from current item (if it's a JSON editor)
597        let cursor_row = if let Some(item) = self.items.get(self.selected_item) {
598            if let SettingControl::Json(state) = &item.control {
599                state.cursor_pos().0
600            } else {
601                return; // Not a JSON editor
602            }
603        } else {
604            return;
605        };
606
607        // Calculate absolute position of cursor row in content:
608        // item_offset + 1 (for label row) + cursor_row
609        let item_offset = self.selected_item_offset();
610        let cursor_content_row = item_offset + 1 + cursor_row;
611
612        let viewport_height = self.viewport_height;
613
614        // If cursor is above viewport, scroll up
615        if cursor_content_row < self.scroll_offset {
616            self.scroll_offset = cursor_content_row;
617        }
618        // If cursor is below viewport, scroll down
619        else if cursor_content_row >= self.scroll_offset + viewport_height {
620            self.scroll_offset = cursor_content_row.saturating_sub(viewport_height) + 1;
621        }
622    }
623
624    /// Scroll up by one line
625    pub fn scroll_up(&mut self) {
626        self.scroll_offset = self.scroll_offset.saturating_sub(1);
627    }
628
629    /// Scroll down by one line
630    pub fn scroll_down(&mut self, viewport_height: usize) {
631        let max_scroll = self.total_content_height().saturating_sub(viewport_height);
632        if self.scroll_offset < max_scroll {
633            self.scroll_offset += 1;
634        }
635    }
636
637    /// Scroll to a position based on ratio (0.0 = top, 1.0 = bottom)
638    ///
639    /// Used for scrollbar drag operations.
640    pub fn scroll_to_ratio(&mut self, ratio: f32) {
641        let max_scroll = self
642            .total_content_height()
643            .saturating_sub(self.viewport_height);
644        let new_offset = (ratio * max_scroll as f32).round() as usize;
645        self.scroll_offset = new_offset.min(max_scroll);
646    }
647
648    /// Start text editing mode for the current control
649    pub fn start_editing(&mut self) {
650        if let Some(item) = self.current_item_mut() {
651            // Don't allow editing read-only fields
652            if item.read_only {
653                return;
654            }
655            match &mut item.control {
656                SettingControl::Text(state) => {
657                    // TextInputState uses focus state, cursor is already at end from with_value
658                    state.cursor = state.value.len();
659                    self.editing_text = true;
660                }
661                SettingControl::TextList(state) => {
662                    // Focus on the new item input by default
663                    state.focus_new_item();
664                    self.editing_text = true;
665                }
666                SettingControl::Number(state) => {
667                    state.start_editing();
668                    self.editing_text = true;
669                }
670                SettingControl::Json(_) => {
671                    // JSON editor is always ready to edit, just set the flag
672                    self.editing_text = true;
673                }
674                _ => {}
675            }
676        }
677    }
678
679    /// Stop text editing mode
680    pub fn stop_editing(&mut self) {
681        if let Some(item) = self.current_item_mut() {
682            if let SettingControl::Number(state) = &mut item.control {
683                state.cancel_editing();
684            }
685        }
686        self.editing_text = false;
687    }
688
689    /// Handle character input
690    pub fn insert_char(&mut self, c: char) {
691        if !self.editing_text {
692            return;
693        }
694        if let Some(item) = self.current_item_mut() {
695            match &mut item.control {
696                SettingControl::Text(state) => {
697                    state.insert(c);
698                }
699                SettingControl::TextList(state) => {
700                    state.insert(c);
701                }
702                SettingControl::Number(state) => {
703                    state.insert_char(c);
704                }
705                SettingControl::Json(state) => {
706                    state.insert(c);
707                }
708                _ => {}
709            }
710        }
711    }
712
713    pub fn insert_str(&mut self, s: &str) {
714        if !self.editing_text {
715            return;
716        }
717        if let Some(item) = self.current_item_mut() {
718            match &mut item.control {
719                SettingControl::Text(state) => {
720                    state.insert_str(s);
721                }
722                SettingControl::TextList(state) => {
723                    state.insert_str(s);
724                }
725                SettingControl::Number(state) => {
726                    for c in s.chars() {
727                        state.insert_char(c);
728                    }
729                }
730                SettingControl::Json(state) => {
731                    state.insert_str(s);
732                }
733                _ => {}
734            }
735        }
736    }
737
738    /// Handle backspace
739    pub fn backspace(&mut self) {
740        if !self.editing_text {
741            return;
742        }
743        if let Some(item) = self.current_item_mut() {
744            match &mut item.control {
745                SettingControl::Text(state) => {
746                    state.backspace();
747                }
748                SettingControl::TextList(state) => {
749                    state.backspace();
750                }
751                SettingControl::Number(state) => {
752                    state.backspace();
753                }
754                SettingControl::Json(state) => {
755                    state.backspace();
756                }
757                _ => {}
758            }
759        }
760    }
761
762    /// Handle cursor left
763    pub fn cursor_left(&mut self) {
764        if !self.editing_text {
765            return;
766        }
767        if let Some(item) = self.current_item_mut() {
768            match &mut item.control {
769                SettingControl::Text(state) => {
770                    state.move_left();
771                }
772                SettingControl::TextList(state) => {
773                    state.move_left();
774                }
775                SettingControl::Json(state) => {
776                    state.move_left();
777                }
778                _ => {}
779            }
780        }
781    }
782
783    /// Handle cursor left with selection (Shift+Left)
784    pub fn cursor_left_selecting(&mut self) {
785        if !self.editing_text {
786            return;
787        }
788        if let Some(item) = self.current_item_mut() {
789            if let SettingControl::Json(state) = &mut item.control {
790                state.editor.move_left_selecting();
791            }
792        }
793    }
794
795    /// Handle cursor right
796    pub fn cursor_right(&mut self) {
797        if !self.editing_text {
798            return;
799        }
800        if let Some(item) = self.current_item_mut() {
801            match &mut item.control {
802                SettingControl::Text(state) => {
803                    state.move_right();
804                }
805                SettingControl::TextList(state) => {
806                    state.move_right();
807                }
808                SettingControl::Json(state) => {
809                    state.move_right();
810                }
811                _ => {}
812            }
813        }
814    }
815
816    /// Handle cursor right with selection (Shift+Right)
817    pub fn cursor_right_selecting(&mut self) {
818        if !self.editing_text {
819            return;
820        }
821        if let Some(item) = self.current_item_mut() {
822            if let SettingControl::Json(state) = &mut item.control {
823                state.editor.move_right_selecting();
824            }
825        }
826    }
827
828    /// Handle cursor up (for multiline controls)
829    pub fn cursor_up(&mut self) {
830        if !self.editing_text {
831            return;
832        }
833        if let Some(item) = self.current_item_mut() {
834            if let SettingControl::Json(state) = &mut item.control {
835                state.move_up();
836            }
837        }
838        self.ensure_cursor_visible();
839    }
840
841    /// Handle cursor up with selection (Shift+Up)
842    pub fn cursor_up_selecting(&mut self) {
843        if !self.editing_text {
844            return;
845        }
846        if let Some(item) = self.current_item_mut() {
847            if let SettingControl::Json(state) = &mut item.control {
848                state.editor.move_up_selecting();
849            }
850        }
851        self.ensure_cursor_visible();
852    }
853
854    /// Handle cursor down (for multiline controls)
855    pub fn cursor_down(&mut self) {
856        if !self.editing_text {
857            return;
858        }
859        if let Some(item) = self.current_item_mut() {
860            if let SettingControl::Json(state) = &mut item.control {
861                state.move_down();
862            }
863        }
864        self.ensure_cursor_visible();
865    }
866
867    /// Handle cursor down with selection (Shift+Down)
868    pub fn cursor_down_selecting(&mut self) {
869        if !self.editing_text {
870            return;
871        }
872        if let Some(item) = self.current_item_mut() {
873            if let SettingControl::Json(state) = &mut item.control {
874                state.editor.move_down_selecting();
875            }
876        }
877        self.ensure_cursor_visible();
878    }
879
880    /// Insert newline in JSON editor
881    pub fn insert_newline(&mut self) {
882        if !self.editing_text {
883            return;
884        }
885        if let Some(item) = self.current_item_mut() {
886            if let SettingControl::Json(state) = &mut item.control {
887                state.insert('\n');
888            }
889        }
890    }
891
892    /// Revert JSON changes to original and stop editing
893    pub fn revert_json_and_stop(&mut self) {
894        if let Some(item) = self.current_item_mut() {
895            if let SettingControl::Json(state) = &mut item.control {
896                state.revert();
897            }
898        }
899        self.editing_text = false;
900    }
901
902    /// Check if current control is a JSON editor
903    pub fn is_editing_json(&self) -> bool {
904        if !self.editing_text {
905            return false;
906        }
907        self.current_item()
908            .map(|item| matches!(&item.control, SettingControl::Json(_)))
909            .unwrap_or(false)
910    }
911
912    /// Toggle boolean value
913    pub fn toggle_bool(&mut self) {
914        if let Some(item) = self.current_item_mut() {
915            // Don't allow toggling read-only fields
916            if item.read_only {
917                return;
918            }
919            if let SettingControl::Toggle(state) = &mut item.control {
920                state.checked = !state.checked;
921            }
922        }
923    }
924
925    /// Toggle dropdown open state
926    pub fn toggle_dropdown(&mut self) {
927        if let Some(item) = self.current_item_mut() {
928            // Don't allow editing read-only fields
929            if item.read_only {
930                return;
931            }
932            if let SettingControl::Dropdown(state) = &mut item.control {
933                state.open = !state.open;
934            }
935        }
936    }
937
938    /// Move dropdown selection up
939    pub fn dropdown_prev(&mut self) {
940        if let Some(item) = self.current_item_mut() {
941            if let SettingControl::Dropdown(state) = &mut item.control {
942                if state.open {
943                    state.select_prev();
944                }
945            }
946        }
947    }
948
949    /// Move dropdown selection down
950    pub fn dropdown_next(&mut self) {
951        if let Some(item) = self.current_item_mut() {
952            if let SettingControl::Dropdown(state) = &mut item.control {
953                if state.open {
954                    state.select_next();
955                }
956            }
957        }
958    }
959
960    /// Confirm dropdown selection
961    pub fn dropdown_confirm(&mut self) {
962        if let Some(item) = self.current_item_mut() {
963            if let SettingControl::Dropdown(state) = &mut item.control {
964                state.open = false;
965            }
966        }
967    }
968
969    /// Increment number value
970    pub fn increment_number(&mut self) {
971        if let Some(item) = self.current_item_mut() {
972            // Don't allow editing read-only fields
973            if item.read_only {
974                return;
975            }
976            if let SettingControl::Number(state) = &mut item.control {
977                state.increment();
978            }
979        }
980    }
981
982    /// Decrement number value
983    pub fn decrement_number(&mut self) {
984        if let Some(item) = self.current_item_mut() {
985            // Don't allow editing read-only fields
986            if item.read_only {
987                return;
988            }
989            if let SettingControl::Number(state) = &mut item.control {
990                state.decrement();
991            }
992        }
993    }
994
995    /// Delete the currently focused item from a TextList control
996    pub fn delete_list_item(&mut self) {
997        if let Some(item) = self.current_item_mut() {
998            if let SettingControl::TextList(state) = &mut item.control {
999                // Remove the currently focused item if any
1000                if let Some(idx) = state.focused_item {
1001                    state.remove_item(idx);
1002                }
1003            }
1004        }
1005    }
1006
1007    /// Delete character at cursor (forward delete)
1008    pub fn delete(&mut self) {
1009        if !self.editing_text {
1010            return;
1011        }
1012        if let Some(item) = self.current_item_mut() {
1013            match &mut item.control {
1014                SettingControl::Text(state) => {
1015                    state.delete();
1016                }
1017                SettingControl::TextList(state) => {
1018                    state.delete();
1019                }
1020                SettingControl::Json(state) => {
1021                    state.delete();
1022                }
1023                _ => {}
1024            }
1025        }
1026    }
1027
1028    /// Move cursor to beginning of line
1029    pub fn cursor_home(&mut self) {
1030        if !self.editing_text {
1031            return;
1032        }
1033        if let Some(item) = self.current_item_mut() {
1034            match &mut item.control {
1035                SettingControl::Text(state) => {
1036                    state.move_home();
1037                }
1038                SettingControl::TextList(state) => {
1039                    state.move_home();
1040                }
1041                SettingControl::Json(state) => {
1042                    state.move_home();
1043                }
1044                _ => {}
1045            }
1046        }
1047    }
1048
1049    /// Move cursor to end of line
1050    pub fn cursor_end(&mut self) {
1051        if !self.editing_text {
1052            return;
1053        }
1054        if let Some(item) = self.current_item_mut() {
1055            match &mut item.control {
1056                SettingControl::Text(state) => {
1057                    state.move_end();
1058                }
1059                SettingControl::TextList(state) => {
1060                    state.move_end();
1061                }
1062                SettingControl::Json(state) => {
1063                    state.move_end();
1064                }
1065                _ => {}
1066            }
1067        }
1068    }
1069
1070    /// Select all text in current control
1071    pub fn select_all(&mut self) {
1072        if !self.editing_text {
1073            return;
1074        }
1075        if let Some(item) = self.current_item_mut() {
1076            if let SettingControl::Json(state) = &mut item.control {
1077                state.select_all();
1078            }
1079            // Note: Text and TextList don't have select_all implemented
1080        }
1081    }
1082
1083    /// Check if any field is currently in edit mode
1084    pub fn is_editing(&self) -> bool {
1085        self.editing_text
1086            || self
1087                .current_item()
1088                .map(|item| {
1089                    matches!(
1090                        &item.control,
1091                        SettingControl::Dropdown(s) if s.open
1092                    )
1093                })
1094                .unwrap_or(false)
1095    }
1096}
1097
1098#[cfg(test)]
1099mod tests {
1100    use super::*;
1101
1102    fn create_test_schema() -> SettingSchema {
1103        SettingSchema {
1104            path: "/test".to_string(),
1105            name: "Test".to_string(),
1106            description: Some("Test schema".to_string()),
1107            setting_type: SettingType::Object {
1108                properties: vec![
1109                    SettingSchema {
1110                        path: "/enabled".to_string(),
1111                        name: "Enabled".to_string(),
1112                        description: Some("Enable this".to_string()),
1113                        setting_type: SettingType::Boolean,
1114                        default: Some(serde_json::json!(true)),
1115                        read_only: false,
1116                    },
1117                    SettingSchema {
1118                        path: "/command".to_string(),
1119                        name: "Command".to_string(),
1120                        description: Some("Command to run".to_string()),
1121                        setting_type: SettingType::String,
1122                        default: Some(serde_json::json!("")),
1123                        read_only: false,
1124                    },
1125                ],
1126            },
1127            default: None,
1128            read_only: false,
1129        }
1130    }
1131
1132    #[test]
1133    fn from_schema_creates_key_item_first() {
1134        let schema = create_test_schema();
1135        let dialog = EntryDialogState::from_schema(
1136            "test".to_string(),
1137            &serde_json::json!({}),
1138            &schema,
1139            "/test",
1140            false,
1141            false,
1142        );
1143
1144        assert!(!dialog.items.is_empty());
1145        assert_eq!(dialog.items[0].path, "__key__");
1146        assert_eq!(dialog.items[0].name, "Key");
1147    }
1148
1149    #[test]
1150    fn from_schema_creates_items_from_properties() {
1151        let schema = create_test_schema();
1152        let dialog = EntryDialogState::from_schema(
1153            "test".to_string(),
1154            &serde_json::json!({"enabled": true, "command": "test-cmd"}),
1155            &schema,
1156            "/test",
1157            false,
1158            false,
1159        );
1160
1161        // Key + 2 properties = 3 items
1162        assert_eq!(dialog.items.len(), 3);
1163        assert_eq!(dialog.items[1].name, "Enabled");
1164        assert_eq!(dialog.items[2].name, "Command");
1165    }
1166
1167    #[test]
1168    fn get_key_returns_key_value() {
1169        let schema = create_test_schema();
1170        let dialog = EntryDialogState::from_schema(
1171            "mykey".to_string(),
1172            &serde_json::json!({}),
1173            &schema,
1174            "/test",
1175            false,
1176            false,
1177        );
1178
1179        assert_eq!(dialog.get_key(), "mykey");
1180    }
1181
1182    #[test]
1183    fn to_value_excludes_key() {
1184        let schema = create_test_schema();
1185        let dialog = EntryDialogState::from_schema(
1186            "test".to_string(),
1187            &serde_json::json!({"enabled": true, "command": "cmd"}),
1188            &schema,
1189            "/test",
1190            false,
1191            false,
1192        );
1193
1194        let value = dialog.to_value();
1195        assert!(value.get("__key__").is_none());
1196        assert!(value.get("enabled").is_some());
1197    }
1198
1199    #[test]
1200    fn focus_navigation_works() {
1201        let schema = create_test_schema();
1202        let mut dialog = EntryDialogState::from_schema(
1203            "test".to_string(),
1204            &serde_json::json!({}),
1205            &schema,
1206            "/test",
1207            false, // existing entry - Key is read-only
1208            false, // allow delete
1209        );
1210
1211        // With is_new=false, Key is read-only and sorted first
1212        // Items: [Key (read-only), Enabled, Command]
1213        // Focus starts at first editable item (index 1)
1214        assert_eq!(dialog.first_editable_index, 1);
1215        assert_eq!(dialog.selected_item, 1); // First editable (Enabled)
1216        assert!(!dialog.focus_on_buttons);
1217
1218        dialog.focus_next();
1219        assert_eq!(dialog.selected_item, 2); // Command
1220
1221        dialog.focus_next();
1222        assert!(dialog.focus_on_buttons); // No more editable items
1223        assert_eq!(dialog.focused_button, 0);
1224
1225        // Going back should skip read-only Key
1226        dialog.focus_prev();
1227        assert!(!dialog.focus_on_buttons);
1228        assert_eq!(dialog.selected_item, 2); // Last editable (Command)
1229
1230        dialog.focus_prev();
1231        assert_eq!(dialog.selected_item, 1); // First editable (Enabled)
1232
1233        dialog.focus_prev();
1234        assert!(dialog.focus_on_buttons); // Wraps to buttons, not to read-only Key
1235    }
1236
1237    #[test]
1238    fn button_count_differs_for_new_vs_existing() {
1239        let schema = create_test_schema();
1240
1241        let new_dialog = EntryDialogState::from_schema(
1242            "test".to_string(),
1243            &serde_json::json!({}),
1244            &schema,
1245            "/test",
1246            true,
1247            false,
1248        );
1249        assert_eq!(new_dialog.button_count(), 2); // Save, Cancel
1250
1251        let existing_dialog = EntryDialogState::from_schema(
1252            "test".to_string(),
1253            &serde_json::json!({}),
1254            &schema,
1255            "/test",
1256            false,
1257            false, // allow delete
1258        );
1259        assert_eq!(existing_dialog.button_count(), 3); // Save, Delete, Cancel
1260
1261        // no_delete hides the Delete button even for existing entries
1262        let no_delete_dialog = EntryDialogState::from_schema(
1263            "test".to_string(),
1264            &serde_json::json!({}),
1265            &schema,
1266            "/test",
1267            false,
1268            true, // no delete (auto-managed entries like plugins)
1269        );
1270        assert_eq!(no_delete_dialog.button_count(), 2); // Save, Cancel (no Delete)
1271    }
1272}