From dbc21af3b11940018c25a44dc3ccd33e4ebc0a06 Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Tue, 11 Jun 2024 17:23:42 +0200 Subject: Extract function wrapMessageToWidth This steals even more code from `gocui.lineWrap`. We'll make use of this in the next commit. --- pkg/gui/controllers/helpers/confirmation_helper.go | 27 +++++++++++++++++----- 1 file changed, 21 insertions(+), 6 deletions(-) (limited to 'pkg') diff --git a/pkg/gui/controllers/helpers/confirmation_helper.go b/pkg/gui/controllers/helpers/confirmation_helper.go index 1e60b5f08..7801081fb 100644 --- a/pkg/gui/controllers/helpers/confirmation_helper.go +++ b/pkg/gui/controllers/helpers/confirmation_helper.go @@ -63,15 +63,20 @@ func (self *ConfirmationHelper) DeactivateConfirmationPrompt() { // Temporary hack: we're just duplicating the logic in `gocui.lineWrap` func getMessageHeight(wrap bool, message string, width int) int { + return len(wrapMessageToWidth(wrap, message, width)) +} + +func wrapMessageToWidth(wrap bool, message string, width int) []string { + lines := strings.Split(message, "\n") if !wrap { - return len(strings.Split(message, "\n")) + return lines } - lineCount := 0 - lines := strings.Split(message, "\n") + wrappedLines := make([]string, 0, len(lines)) for _, line := range lines { n := 0 + offset := 0 lastWhitespaceIndex := -1 for i, currChr := range line { rw := runewidth.RuneWidth(currChr) @@ -79,28 +84,38 @@ func getMessageHeight(wrap bool, message string, width int) int { if n > width { if currChr == ' ' { + wrappedLines = append(wrappedLines, line[offset:i]) + offset = i + 1 n = 0 } else if currChr == '-' { + wrappedLines = append(wrappedLines, line[offset:i]) + offset = i n = rw } else if lastWhitespaceIndex != -1 && lastWhitespaceIndex+1 != i { if line[lastWhitespaceIndex] == '-' { + wrappedLines = append(wrappedLines, line[offset:lastWhitespaceIndex+1]) + offset = lastWhitespaceIndex + 1 n = i - lastWhitespaceIndex } else { + wrappedLines = append(wrappedLines, line[offset:lastWhitespaceIndex]) + offset = lastWhitespaceIndex + 1 n = i - lastWhitespaceIndex + 1 } } else { + wrappedLines = append(wrappedLines, line[offset:i]) + offset = i n = rw } - lineCount++ lastWhitespaceIndex = -1 } else if currChr == ' ' || currChr == '-' { lastWhitespaceIndex = i } } - lineCount++ + + wrappedLines = append(wrappedLines, line[offset:]) } - return lineCount + return wrappedLines } func (self *ConfirmationHelper) getPopupPanelDimensions(wrap bool, prompt string) (int, int, int, int) { -- cgit v1.2.3 From 7e92dbfd3d49c6bb859c5bd1bf5a43f09ef324b3 Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Tue, 11 Jun 2024 15:49:19 +0200 Subject: Add menu prompt This makes it possible to add a prompt to a menu. It will be shown above the menu items, separated from them by a blank line. --- pkg/gui/context/menu_context.go | 31 ++++++++++++++++++-- pkg/gui/controllers/helpers/confirmation_helper.go | 34 ++++++++++++++++++++-- pkg/gui/menu_panel.go | 1 + pkg/gui/types/common.go | 1 + 4 files changed, 63 insertions(+), 4 deletions(-) (limited to 'pkg') diff --git a/pkg/gui/context/menu_context.go b/pkg/gui/context/menu_context.go index a6b0e77cb..e4b26f884 100644 --- a/pkg/gui/context/menu_context.go +++ b/pkg/gui/context/menu_context.go @@ -50,6 +50,8 @@ func NewMenuContext( type MenuViewModel struct { c *ContextCommon menuItems []*types.MenuItem + prompt string + promptLines []string columnAlignment []utils.Alignment *FilteredListViewModel[*types.MenuItem] } @@ -73,6 +75,23 @@ func (self *MenuViewModel) SetMenuItems(items []*types.MenuItem, columnAlignment self.columnAlignment = columnAlignment } +func (self *MenuViewModel) GetPrompt() string { + return self.prompt +} + +func (self *MenuViewModel) SetPrompt(prompt string) { + self.prompt = prompt + self.promptLines = nil +} + +func (self *MenuViewModel) GetPromptLines() []string { + return self.promptLines +} + +func (self *MenuViewModel) SetPromptLines(promptLines []string) { + self.promptLines = promptLines +} + // TODO: move into presentation package func (self *MenuViewModel) GetDisplayStrings(_ int, _ int) [][]string { menuItems := self.FilteredListViewModel.GetItems() @@ -94,14 +113,22 @@ func (self *MenuViewModel) GetDisplayStrings(_ int, _ int) [][]string { } func (self *MenuViewModel) GetNonModelItems() []*NonModelItem { + result := []*NonModelItem{} + result = append(result, lo.Map(self.promptLines, func(line string, _ int) *NonModelItem { + return &NonModelItem{ + Index: 0, + Column: 0, + Content: line, + } + })...) + // Don't display section headers when we are filtering, and the filter mode // is fuzzy. The reason is that filtering changes the order of the items // (they are sorted by best match), so all the sections would be messed up. if self.FilteredListViewModel.IsFiltering() && self.c.UserConfig.Gui.UseFuzzySearch() { - return []*NonModelItem{} + return result } - result := []*NonModelItem{} menuItems := self.FilteredListViewModel.GetItems() var prevSection *types.MenuSection = nil for i, menuItem := range menuItems { diff --git a/pkg/gui/controllers/helpers/confirmation_helper.go b/pkg/gui/controllers/helpers/confirmation_helper.go index 7801081fb..8f7c60b5a 100644 --- a/pkg/gui/controllers/helpers/confirmation_helper.go +++ b/pkg/gui/controllers/helpers/confirmation_helper.go @@ -373,7 +373,9 @@ func (self *ConfirmationHelper) resizeMenu() { itemCount := self.c.Contexts().Menu.UnfilteredLen() offset := 3 panelWidth := self.getPopupPanelWidth() - x0, y0, x1, y1 := self.getPopupPanelDimensionsForContentHeight(panelWidth, itemCount+offset) + contentWidth := panelWidth - 2 // minus 2 for the frame + promptLinesCount := self.layoutMenuPrompt(contentWidth) + x0, y0, x1, y1 := self.getPopupPanelDimensionsForContentHeight(panelWidth, itemCount+offset+promptLinesCount) menuBottom := y1 - offset _, _ = self.c.GocuiGui().SetView(self.c.Views().Menu.Name(), x0, y0, x1, menuBottom, 0) @@ -383,11 +385,39 @@ func (self *ConfirmationHelper) resizeMenu() { if selectedItem != nil { tooltip = self.TooltipForMenuItem(selectedItem) } - contentWidth := panelWidth - 2 // minus 2 for the frame tooltipHeight := getMessageHeight(true, tooltip, contentWidth) + 2 // plus 2 for the frame _, _ = self.c.GocuiGui().SetView(self.c.Views().Tooltip.Name(), x0, tooltipTop, x1, tooltipTop+tooltipHeight-1, 0) } +// Wraps the lines of the menu prompt to the available width and rerenders the +// menu if neeeded. Returns the number of lines the prompt takes up. +func (self *ConfirmationHelper) layoutMenuPrompt(contentWidth int) int { + oldPromptLines := self.c.Contexts().Menu.GetPromptLines() + var promptLines []string + prompt := self.c.Contexts().Menu.GetPrompt() + if len(prompt) > 0 { + promptLines = wrapMessageToWidth(true, prompt, contentWidth) + promptLines = append(promptLines, "") + } + self.c.Contexts().Menu.SetPromptLines(promptLines) + if len(oldPromptLines) != len(promptLines) { + // The number of lines in the prompt has changed; this happens either + // because we're now showing a menu that has a prompt, and the previous + // menu didn't (or vice versa), or because the user is resizing the + // terminal window while a menu with a prompt is open. + + // We need to rerender to give the menu context a chance to update its + // non-model items, and reinitialize the data it uses for converting + // between view index and model index. + _ = self.c.Contexts().Menu.HandleRender() + + // Then we need to refocus to ensure the cursor is in the right place in + // the view. + _ = self.c.Contexts().Menu.HandleFocus(types.OnFocusOpts{}) + } + return len(promptLines) +} + func (self *ConfirmationHelper) resizeConfirmationPanel() { suggestionsViewHeight := 0 if self.c.Views().Suggestions.Visible { diff --git a/pkg/gui/menu_panel.go b/pkg/gui/menu_panel.go index b777536ee..ca03ea69e 100644 --- a/pkg/gui/menu_panel.go +++ b/pkg/gui/menu_panel.go @@ -42,6 +42,7 @@ func (gui *Gui) createMenu(opts types.CreateMenuOptions) error { } gui.State.Contexts.Menu.SetMenuItems(opts.Items, opts.ColumnAlignment) + gui.State.Contexts.Menu.SetPrompt(opts.Prompt) gui.State.Contexts.Menu.SetSelection(0) gui.Views.Menu.Title = opts.Title diff --git a/pkg/gui/types/common.go b/pkg/gui/types/common.go index 77f2f56eb..fc9168406 100644 --- a/pkg/gui/types/common.go +++ b/pkg/gui/types/common.go @@ -159,6 +159,7 @@ const ( type CreateMenuOptions struct { Title string + Prompt string // a message that will be displayed above the menu options Items []*MenuItem HideCancel bool ColumnAlignment []utils.Alignment -- cgit v1.2.3 From ddd6323aa5429908f712f7b7fe67760b15ed398c Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Mon, 18 Mar 2024 14:12:35 +0100 Subject: Add prompt to the remote branch checkout menu --- pkg/gui/controllers/helpers/refs_helper.go | 1 + pkg/i18n/english.go | 2 ++ 2 files changed, 3 insertions(+) (limited to 'pkg') diff --git a/pkg/gui/controllers/helpers/refs_helper.go b/pkg/gui/controllers/helpers/refs_helper.go index d837d8266..ccfe71799 100644 --- a/pkg/gui/controllers/helpers/refs_helper.go +++ b/pkg/gui/controllers/helpers/refs_helper.go @@ -130,6 +130,7 @@ func (self *RefsHelper) CheckoutRemoteBranch(fullBranchName string, localBranchN Title: utils.ResolvePlaceholderString(self.c.Tr.RemoteBranchCheckoutTitle, map[string]string{ "branchName": fullBranchName, }), + Prompt: self.c.Tr.RemoteBranchCheckoutPrompt, Items: []*types.MenuItem{ { Label: self.c.Tr.CheckoutTypeNewBranch, diff --git a/pkg/i18n/english.go b/pkg/i18n/english.go index 7aabbbc2b..78b8b84cf 100644 --- a/pkg/i18n/english.go +++ b/pkg/i18n/english.go @@ -113,6 +113,7 @@ type TranslationSet struct { CheckoutByName string CheckoutByNameTooltip string RemoteBranchCheckoutTitle string + RemoteBranchCheckoutPrompt string CheckoutTypeNewBranch string CheckoutTypeNewBranchTooltip string CheckoutTypeDetachedHead string @@ -1079,6 +1080,7 @@ func EnglishTranslationSet() TranslationSet { CheckoutByName: "Checkout by name", CheckoutByNameTooltip: "Checkout by name. In the input box you can enter '-' to switch to the last branch.", RemoteBranchCheckoutTitle: "Checkout {{.branchName}}", + RemoteBranchCheckoutPrompt: "How would you like to check out this branch?", CheckoutTypeNewBranch: "New local branch", CheckoutTypeNewBranchTooltip: "Checkout the remote branch as a local branch, tracking the remote branch.", CheckoutTypeDetachedHead: "Detached head", -- cgit v1.2.3