Skip to main content

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