diff --git a/crates/edit/src/bin/edit/draw_editor.rs b/crates/edit/src/bin/edit/draw_editor.rs index 479c053ac4d..a79dd1f7477 100644 --- a/crates/edit/src/bin/edit/draw_editor.rs +++ b/crates/edit/src/bin/edit/draw_editor.rs @@ -25,15 +25,279 @@ pub fn draw_editor(ctx: &mut Context, state: &mut State) { _ => 2, }; - if let Some(doc) = state.documents.active() { - ctx.textarea("textarea", doc.buffer.clone()); - ctx.inherit_focus(); + let editor_height = size.height - height_reduction; + + // Synchronize split layout with current document state + sync_split_layout(state); + + // Handle split view requests + handle_split_requests(state); + + // Draw the editor area (single or split) + draw_editor_panes(ctx, state, editor_height); +} + +/// Synchronize the split layout with the current document state. +/// Ensures panes reference valid documents. +fn sync_split_layout(state: &mut State) { + // If no panes exist, create one for the active document + if state.split_layout.panes.is_empty() { + if let Some(doc) = state.documents.active() { + state.split_layout.panes.push(Pane { + document_index: 0, + buffer: doc.buffer.clone(), + filename: doc.filename.clone(), + }); + state.split_layout.active_pane = 0; + } + } else if state.split_layout.split_direction == SplitDirection::None { + // In single-pane mode, always sync with the active document + if let Some(doc) = state.documents.active() { + if let Some(pane) = state.split_layout.panes.get_mut(0) { + pane.buffer = doc.buffer.clone(); + pane.filename = doc.filename.clone(); + pane.document_index = 0; + } + } + } +} + +/// Handle split view requests (split, close pane, focus navigation). +fn handle_split_requests(state: &mut State) { + if std::mem::take(&mut state.wants_split_horizontal) { + do_split(state, SplitDirection::Horizontal); + } + if std::mem::take(&mut state.wants_split_vertical) { + do_split(state, SplitDirection::Vertical); + } + if std::mem::take(&mut state.wants_close_pane) { + close_active_pane(state); + } + + let count = state.split_layout.pane_count(); + if count > 1 { + if std::mem::take(&mut state.wants_focus_next_pane) { + state.split_layout.active_pane = (state.split_layout.active_pane + 1) % count; + } + if std::mem::take(&mut state.wants_focus_prev_pane) { + state.split_layout.active_pane = (state.split_layout.active_pane + count - 1) % count; + } + } +} + +/// Perform a split operation. +fn do_split(state: &mut State, direction: SplitDirection) { + // Need at least one document to split + let Some(doc) = state.documents.active() else { + return; + }; + + if state.split_layout.split_direction == SplitDirection::None { + // First split: change direction and add a second pane + state.split_layout.split_direction = direction; + + // Clone the current document's buffer into the new pane + // (same document shown in both panes) + state.split_layout.panes.push(Pane { + document_index: 0, + buffer: doc.buffer.clone(), + filename: doc.filename.clone(), + }); + + // Focus the new pane + state.split_layout.active_pane = state.split_layout.panes.len() - 1; } else { + // Already split - just change direction (we only support 2 panes for now) + state.split_layout.split_direction = direction; + } +} + +/// Close the active pane. +fn close_active_pane(state: &mut State) { + if state.split_layout.panes.len() <= 1 { + // Can't close the last pane, close the document instead + return; + } + + state.split_layout.panes.remove(state.split_layout.active_pane); + + // Adjust active pane index + if state.split_layout.active_pane >= state.split_layout.panes.len() { + state.split_layout.active_pane = state.split_layout.panes.len() - 1; + } + + // If only one pane remains, go back to single-pane mode + if state.split_layout.panes.len() == 1 { + state.split_layout.split_direction = SplitDirection::None; + } +} + +/// Draw the editor panes based on split layout. +fn draw_editor_panes(ctx: &mut Context, state: &mut State, editor_height: CoordType) { + let pane_count = state.split_layout.pane_count(); + + if pane_count == 0 { + // No documents open ctx.block_begin("empty"); ctx.block_end(); + ctx.attr_intrinsic_size(Size { width: 0, height: editor_height }); + return; + } + + if pane_count == 1 || state.split_layout.split_direction == SplitDirection::None { + // Single pane mode + if let Some(pane) = state.split_layout.panes.first() { + ctx.textarea("textarea", pane.buffer.clone()); + ctx.inherit_focus(); + } else { + ctx.block_begin("empty"); + ctx.block_end(); + } + ctx.attr_intrinsic_size(Size { width: 0, height: editor_height }); + } else { + // Split mode - draw panes in a table layout + draw_split_panes(ctx, state, editor_height); + } +} + +/// Draw split panes using table layout. +fn draw_split_panes(ctx: &mut Context, state: &mut State, editor_height: CoordType) { + let direction = state.split_layout.split_direction; + let active_pane = state.split_layout.active_pane; + let pane_count = state.split_layout.panes.len(); + + // For now, only support 2 panes to keep it simple + if pane_count != 2 { + // Fall back to single pane + if let Some(pane) = state.split_layout.panes.first() { + ctx.textarea("textarea", pane.buffer.clone()); + ctx.inherit_focus(); + } + ctx.attr_intrinsic_size(Size { width: 0, height: editor_height }); + return; + } + + let screen_width = ctx.size().width; + let half_width = (screen_width - 1) / 2; // -1 for the gap/separator + + if direction == SplitDirection::Horizontal { + draw_horizontal_split(ctx, state, editor_height, half_width, active_pane); + } else { + draw_vertical_split(ctx, state, editor_height, active_pane); + } +} + +/// Draw horizontal split layout (side by side panes). +fn draw_horizontal_split( + ctx: &mut Context, + state: &State, + editor_height: CoordType, + half_width: CoordType, + active_pane: usize, +) { + ctx.table_begin("split-h"); + ctx.table_set_columns(&[half_width, half_width]); + ctx.table_set_cell_gap(Size { width: 1, height: 0 }); + ctx.table_next_row(); + + for (idx, (pane, name)) in state.split_layout.panes.iter() + .zip(["left-pane", "right-pane"]) + .enumerate() + { + let is_active = active_pane == idx; + ctx.block_begin(name); + { + draw_pane_header(ctx, pane, is_active); + let textarea_name = if idx == 0 { "textarea-left" } else { "textarea-right" }; + ctx.textarea(textarea_name, pane.buffer.clone()); + if is_active { + ctx.inherit_focus(); + } + ctx.attr_intrinsic_size(Size { width: 0, height: editor_height - 1 }); + } + ctx.block_end(); + ctx.attr_intrinsic_size(Size { width: 0, height: editor_height }); + } + + ctx.table_end(); + ctx.attr_intrinsic_size(Size { width: 0, height: editor_height }); +} + +/// Draw vertical split layout (stacked panes). +fn draw_vertical_split( + ctx: &mut Context, + state: &State, + editor_height: CoordType, + active_pane: usize, +) { + let pane_height = (editor_height - 1) / 2; // -1 for separator + + ctx.block_begin("split-v"); + { + // Top pane + draw_pane_block(ctx, &state.split_layout.panes[0], "top", "textarea-top", active_pane == 0, pane_height); + + // Separator + ctx.block_begin("separator"); + ctx.attr_background_rgba(ctx.indexed(IndexedColor::BrightBlack)); + ctx.attr_intrinsic_size(Size { width: COORD_TYPE_SAFE_MAX, height: 1 }); + ctx.block_end(); + + // Bottom pane + draw_pane_block(ctx, &state.split_layout.panes[1], "bottom", "textarea-bottom", active_pane == 1, pane_height); } + ctx.block_end(); + ctx.attr_intrinsic_size(Size { width: 0, height: editor_height }); +} - ctx.attr_intrinsic_size(Size { width: 0, height: size.height - height_reduction }); +/// Draw a single pane block with header and textarea. +fn draw_pane_block( + ctx: &mut Context, + pane: &Pane, + block_name: &'static str, + textarea_name: &'static str, + is_active: bool, + height: CoordType, +) { + ctx.block_begin(block_name); + { + draw_pane_header(ctx, pane, is_active); + ctx.textarea(textarea_name, pane.buffer.clone()); + if is_active { + ctx.inherit_focus(); + } + ctx.attr_intrinsic_size(Size { width: 0, height: height - 1 }); + } + ctx.block_end(); + ctx.attr_intrinsic_size(Size { width: 0, height: height }); +} + +/// Draw a pane header showing the filename. +fn draw_pane_header(ctx: &mut Context, pane: &Pane, is_active: bool) { + use std::borrow::Cow; + + let (bg, fg) = if is_active { + (ctx.indexed(IndexedColor::Blue), ctx.indexed(IndexedColor::BrightWhite)) + } else { + (ctx.indexed(IndexedColor::BrightBlack), ctx.indexed(IndexedColor::White)) + }; + + ctx.block_begin("pane-header"); + { + let label: Cow<'_, str> = if pane.buffer.borrow().is_dirty() { + format!("● {}", pane.filename).into() + } else { + Cow::Borrowed(&pane.filename) + }; + + ctx.label("filename", &label); + ctx.attr_overflow(Overflow::TruncateMiddle); + ctx.attr_background_rgba(bg); + ctx.attr_foreground_rgba(fg); + } + ctx.block_end(); + ctx.attr_intrinsic_size(Size { width: COORD_TYPE_SAFE_MAX, height: 1 }); + ctx.attr_background_rgba(bg); } fn draw_search(ctx: &mut Context, state: &mut State) { @@ -219,6 +483,8 @@ pub fn draw_handle_wants_close(ctx: &mut Context, state: &mut State) { if !doc.buffer.borrow().is_dirty() { state.documents.remove_active(); + // Reset split layout when document is removed (will be re-synced) + state.split_layout.reset(); state.wants_close = false; ctx.needs_rerender(); return; @@ -291,6 +557,8 @@ pub fn draw_handle_wants_close(ctx: &mut Context, state: &mut State) { } Action::Discard => { state.documents.remove_active(); + // Reset split layout when document is removed (will be re-synced) + state.split_layout.reset(); state.wants_close = false; } Action::Cancel => { diff --git a/crates/edit/src/bin/edit/draw_menubar.rs b/crates/edit/src/bin/edit/draw_menubar.rs index 8f14f70c1ed..e1d999566a9 100644 --- a/crates/edit/src/bin/edit/draw_menubar.rs +++ b/crates/edit/src/bin/edit/draw_menubar.rs @@ -121,6 +121,30 @@ fn draw_menu_view(ctx: &mut Context, state: &mut State) { tb.set_word_wrap(!word_wrap); ctx.needs_rerender(); } + + // We need to drop the borrow before accessing split_layout + drop(tb); + + // Split view menu items + if ctx.menubar_menu_button(loc(LocId::ViewSplitHorizontal), 'H', kbmod::CTRL | vk::BACKSLASH) + { + state.wants_split_horizontal = true; + ctx.needs_rerender(); + } + if ctx.menubar_menu_button(loc(LocId::ViewSplitVertical), 'V', kbmod::CTRL_SHIFT | vk::BACKSLASH) { + state.wants_split_vertical = true; + ctx.needs_rerender(); + } + if state.split_layout.pane_count() > 1 { + if ctx.menubar_menu_button(loc(LocId::ViewClosePane), 'C', kbmod::CTRL_SHIFT | vk::W) { + state.wants_close_pane = true; + ctx.needs_rerender(); + } + if ctx.menubar_menu_button(loc(LocId::ViewFocusNextPane), 'N', vk::F6) { + state.wants_focus_next_pane = true; + ctx.needs_rerender(); + } + } } ctx.menubar_menu_end(); diff --git a/crates/edit/src/bin/edit/main.rs b/crates/edit/src/bin/edit/main.rs index a05756a009c..b46ca471814 100644 --- a/crates/edit/src/bin/edit/main.rs +++ b/crates/edit/src/bin/edit/main.rs @@ -96,11 +96,15 @@ fn run() -> apperr::Result<()> { .indexed_alpha(IndexedColor::Background, 2, 3) .oklab_blend(tui.indexed_alpha(IndexedColor::Foreground, 1, 3)); let floater_fg = tui.contrasted(floater_bg); - tui.setup_modifier_translations(ModifierTranslations { - ctrl: loc(LocId::Ctrl), - alt: loc(LocId::Alt), - shift: loc(LocId::Shift), - }); + + // Use Mac-style modifier symbols on macOS, otherwise use localized text + #[cfg(any(target_os = "macos", target_os = "ios"))] + let modifier_translations = ModifierTranslations { ctrl: "⌘", alt: "⌥", shift: "⇧" }; + #[cfg(not(any(target_os = "macos", target_os = "ios")))] + let modifier_translations = + ModifierTranslations { ctrl: loc(LocId::Ctrl), alt: loc(LocId::Alt), shift: loc(LocId::Shift) }; + + tui.setup_modifier_translations(modifier_translations); tui.set_floater_default_bg(floater_bg); tui.set_floater_default_fg(floater_fg); tui.set_modal_default_bg(floater_bg); @@ -370,6 +374,21 @@ fn draw(ctx: &mut Context, state: &mut State) { state.wants_search.focus = true; } else if key == vk::F3 { search_execute(ctx, state, SearchAction::Search); + } else if key == kbmod::CTRL | vk::BACKSLASH { + // Split editor horizontally (side by side) + state.wants_split_horizontal = true; + } else if key == kbmod::CTRL_SHIFT | vk::BACKSLASH { + // Split editor vertically (stacked) + state.wants_split_vertical = true; + } else if key == kbmod::CTRL_SHIFT | vk::W && state.split_layout.pane_count() > 1 { + // Close current pane (if split) + state.wants_close_pane = true; + } else if key == vk::F6 { + // Focus next pane + state.wants_focus_next_pane = true; + } else if key == kbmod::SHIFT | vk::F6 { + // Focus previous pane + state.wants_focus_prev_pane = true; } else { return; } diff --git a/crates/edit/src/bin/edit/state.rs b/crates/edit/src/bin/edit/state.rs index c8d45bd8ca6..d7c4410bd5e 100644 --- a/crates/edit/src/bin/edit/state.rs +++ b/crates/edit/src/bin/edit/state.rs @@ -127,6 +127,58 @@ pub struct OscTitleFileStatus { pub dirty: bool, } +/// Direction for split views. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)] +pub enum SplitDirection { + #[default] + None, // Single pane (no split) + Horizontal, // Panes arranged left | right + Vertical, // Panes arranged top / bottom +} + +/// A pane in the split view layout. +/// Each pane displays a document (via its buffer reference). +#[derive(Clone)] +pub struct Pane { + /// Index of the document in DocumentManager (for reference). + /// Note: This may become stale when documents are closed. + pub document_index: usize, + /// Reference to the document's text buffer. + /// Multiple panes can share the same buffer. + pub buffer: edit::buffer::RcTextBuffer, + /// The filename to display for this pane. + pub filename: String, +} + +/// Manages the split view layout. +#[derive(Default)] +pub struct SplitLayout { + /// List of panes. When split_direction is None, only panes[0] is used. + pub panes: Vec, + /// Index of the currently active/focused pane. + pub active_pane: usize, + /// How panes are arranged. + pub split_direction: SplitDirection, +} + +impl SplitLayout { + /// Returns the number of visible panes. + #[inline] + pub fn pane_count(&self) -> usize { + if self.split_direction == SplitDirection::None { + 1.min(self.panes.len()) + } else { + self.panes.len() + } + } + + /// Resets the layout to default state. + #[inline] + pub fn reset(&mut self) { + *self = Self::default(); + } +} + pub struct State { pub menubar_color_bg: StraightRgba, pub menubar_color_fg: StraightRgba, @@ -168,6 +220,14 @@ pub struct State { pub goto_target: String, pub goto_invalid: bool, + // Split view state + pub split_layout: SplitLayout, + pub wants_split_horizontal: bool, + pub wants_split_vertical: bool, + pub wants_close_pane: bool, + pub wants_focus_next_pane: bool, + pub wants_focus_prev_pane: bool, + pub osc_title_file_status: OscTitleFileStatus, pub osc_clipboard_sync: bool, pub osc_clipboard_always_send: bool, @@ -216,6 +276,14 @@ impl State { goto_target: Default::default(), goto_invalid: false, + // Split view state + split_layout: Default::default(), + wants_split_horizontal: false, + wants_split_vertical: false, + wants_close_pane: false, + wants_focus_next_pane: false, + wants_focus_prev_pane: false, + osc_title_file_status: Default::default(), osc_clipboard_sync: false, osc_clipboard_always_send: false, @@ -273,3 +341,129 @@ pub fn draw_error_log(ctx: &mut Context, state: &mut State) { state.error_log_count = 0; } } + +#[cfg(test)] +mod tests { + use super::*; + use edit::buffer::TextBuffer; + + /// Helper to create a test pane with a dummy buffer. + fn create_test_pane(filename: &str) -> Pane { + Pane { + document_index: 0, + buffer: TextBuffer::new_rc(true).unwrap(), + filename: filename.to_string(), + } + } + + #[test] + fn split_layout_default_is_empty() { + let layout = SplitLayout::default(); + assert!(layout.panes.is_empty()); + assert_eq!(layout.active_pane, 0); + assert_eq!(layout.split_direction, SplitDirection::None); + } + + #[test] + fn pane_count_returns_zero_when_empty() { + let layout = SplitLayout::default(); + // pane_count returns min(1, 0) = 0 when no panes + assert_eq!(layout.pane_count(), 0); + } + + #[test] + fn pane_count_returns_one_in_single_mode() { + let mut layout = SplitLayout::default(); + layout.panes.push(create_test_pane("test.txt")); + layout.split_direction = SplitDirection::None; + + assert_eq!(layout.pane_count(), 1); + } + + #[test] + fn pane_count_returns_one_even_with_multiple_panes_in_none_mode() { + let mut layout = SplitLayout::default(); + layout.panes.push(create_test_pane("file1.txt")); + layout.panes.push(create_test_pane("file2.txt")); + layout.split_direction = SplitDirection::None; + + // In None mode, only 1 pane is visible regardless of how many exist + assert_eq!(layout.pane_count(), 1); + } + + #[test] + fn pane_count_returns_actual_count_in_horizontal_split() { + let mut layout = SplitLayout::default(); + layout.panes.push(create_test_pane("file1.txt")); + layout.panes.push(create_test_pane("file2.txt")); + layout.split_direction = SplitDirection::Horizontal; + + assert_eq!(layout.pane_count(), 2); + } + + #[test] + fn pane_count_returns_actual_count_in_vertical_split() { + let mut layout = SplitLayout::default(); + layout.panes.push(create_test_pane("file1.txt")); + layout.panes.push(create_test_pane("file2.txt")); + layout.split_direction = SplitDirection::Vertical; + + assert_eq!(layout.pane_count(), 2); + } + + #[test] + fn reset_clears_layout() { + let mut layout = SplitLayout::default(); + layout.panes.push(create_test_pane("file1.txt")); + layout.panes.push(create_test_pane("file2.txt")); + layout.active_pane = 1; + layout.split_direction = SplitDirection::Horizontal; + + layout.reset(); + + assert!(layout.panes.is_empty()); + assert_eq!(layout.active_pane, 0); + assert_eq!(layout.split_direction, SplitDirection::None); + } + + #[test] + fn split_direction_default_is_none() { + assert_eq!(SplitDirection::default(), SplitDirection::None); + } + + #[test] + fn focus_next_pane_wraps_around() { + let mut layout = SplitLayout::default(); + layout.panes.push(create_test_pane("file1.txt")); + layout.panes.push(create_test_pane("file2.txt")); + layout.split_direction = SplitDirection::Horizontal; + layout.active_pane = 0; + + // Simulate focus next + let count = layout.pane_count(); + layout.active_pane = (layout.active_pane + 1) % count; + assert_eq!(layout.active_pane, 1); + + // Wrap around + layout.active_pane = (layout.active_pane + 1) % count; + assert_eq!(layout.active_pane, 0); + } + + #[test] + fn focus_prev_pane_wraps_around() { + let mut layout = SplitLayout::default(); + layout.panes.push(create_test_pane("file1.txt")); + layout.panes.push(create_test_pane("file2.txt")); + layout.split_direction = SplitDirection::Horizontal; + layout.active_pane = 0; + + // Simulate focus prev (wraps to last) + let count = layout.pane_count(); + layout.active_pane = (layout.active_pane + count - 1) % count; + assert_eq!(layout.active_pane, 1); + + // Go back to first + layout.active_pane = (layout.active_pane + count - 1) % count; + assert_eq!(layout.active_pane, 0); + } +} diff --git a/crates/edit/src/input.rs b/crates/edit/src/input.rs index bff72fc9b8a..f40d095fb31 100644 --- a/crates/edit/src/input.rs +++ b/crates/edit/src/input.rs @@ -123,6 +123,8 @@ pub mod vk { pub const INSERT: InputKey = InputKey::new(0x2D); pub const DELETE: InputKey = InputKey::new(0x2E); + pub const BACKSLASH: InputKey = InputKey::new('\\' as u32); + pub const N0: InputKey = InputKey::new('0' as u32); pub const N1: InputKey = InputKey::new('1' as u32); pub const N2: InputKey = InputKey::new('2' as u32); diff --git a/crates/edit/src/tui.rs b/crates/edit/src/tui.rs index 247505a6041..b166f15f454 100644 --- a/crates/edit/src/tui.rs +++ b/crates/edit/src/tui.rs @@ -3314,8 +3314,46 @@ impl<'a> Context<'a, '_> { } fn menubar_shortcut(&mut self, shortcut: InputKey) { - let shortcut_letter = shortcut.value() as u8 as char; - if shortcut_letter.is_ascii_uppercase() { + let shortcut_key = shortcut.key().value(); + let shortcut_letter = shortcut_key as u8 as char; + + // Check if it's a displayable shortcut (letter, function key, or special key) + let key_name: Option<&str> = if shortcut_letter.is_ascii_uppercase() { + None // Will use the letter directly + } else { + match shortcut_key { + // Arrow keys + 0x25 => Some("←"), + 0x26 => Some("↑"), + 0x27 => Some("→"), + 0x28 => Some("↓"), + // Function keys + 0x70 => Some("F1"), + 0x71 => Some("F2"), + 0x72 => Some("F3"), + 0x73 => Some("F4"), + 0x74 => Some("F5"), + 0x75 => Some("F6"), + 0x76 => Some("F7"), + 0x77 => Some("F8"), + 0x78 => Some("F9"), + 0x79 => Some("F10"), + 0x7A => Some("F11"), + 0x7B => Some("F12"), + // Other special keys + 0x21 => Some("PgUp"), + 0x22 => Some("PgDn"), + 0x23 => Some("End"), + 0x24 => Some("Home"), + 0x2D => Some("Ins"), + 0x2E => Some("Del"), + 0x1B => Some("Esc"), + 0x5C => Some("\\"), // Backslash + _ => None, + } + }; + + if shortcut_letter.is_ascii_uppercase() || key_name.is_some() { let mut shortcut_text = ArenaString::new_in(self.arena()); if shortcut.modifiers_contains(kbmod::CTRL) { shortcut_text.push_str(self.tui.modifier_translations.ctrl); @@ -3329,7 +3367,11 @@ impl<'a> Context<'a, '_> { shortcut_text.push_str(self.tui.modifier_translations.shift); shortcut_text.push('+'); } - shortcut_text.push(shortcut_letter); + if let Some(name) = key_name { + shortcut_text.push_str(name); + } else { + shortcut_text.push(shortcut_letter); + } self.label("shortcut", &shortcut_text); } else { diff --git a/i18n/edit.toml b/i18n/edit.toml index 01248b1577a..d86265aeb71 100644 --- a/i18n/edit.toml +++ b/i18n/edit.toml @@ -902,6 +902,62 @@ vi = "Đi tới tệp…" zh_hans = "转到文件…" zh_hant = "跳至檔案…" +# Split editor horizontally (side by side) +[ViewSplitHorizontal] +en = "Split Editor Right" +de = "Editor rechts teilen" +es = "Dividir editor a la derecha" +fr = "Diviser l'éditeur à droite" +it = "Dividi editor a destra" +ja = "エディターを右に分割" +ko = "편집기를 오른쪽으로 분할" +pt_br = "Dividir editor à direita" +ru = "Разделить редактор вправо" +zh_hans = "向右拆分编辑器" +zh_hant = "向右分割編輯器" + +# Split editor vertically (stacked) +[ViewSplitVertical] +en = "Split Editor Down" +de = "Editor nach unten teilen" +es = "Dividir editor hacia abajo" +fr = "Diviser l'éditeur vers le bas" +it = "Dividi editor in basso" +ja = "エディターを下に分割" +ko = "편집기를 아래로 분할" +pt_br = "Dividir editor para baixo" +ru = "Разделить редактор вниз" +zh_hans = "向下拆分编辑器" +zh_hant = "向下分割編輯器" + +# Close the current pane +[ViewClosePane] +en = "Close Pane" +de = "Bereich schließen" +es = "Cerrar panel" +fr = "Fermer le volet" +it = "Chiudi riquadro" +ja = "ペインを閉じる" +ko = "창 닫기" +pt_br = "Fechar painel" +ru = "Закрыть панель" +zh_hans = "关闭窗格" +zh_hant = "關閉窗格" + +# Focus the next pane +[ViewFocusNextPane] +en = "Focus Next Pane" +de = "Nächsten Bereich fokussieren" +es = "Enfocar siguiente panel" +fr = "Sélectionner le volet suivant" +it = "Seleziona riquadro successivo" +ja = "次のペインにフォーカス" +ko = "다음 창에 포커스" +pt_br = "Focar no próximo painel" +ru = "Фокус на следующую панель" +zh_hans = "聚焦下一个窗格" +zh_hant = "聚焦下一個窗格" + # A menu bar item [Help] en = "Help"