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