From 5230fddaf8cf7b1d7fa726588ac0007647356373 Mon Sep 17 00:00:00 2001 From: "Michael Z. Kadaner" Date: Fri, 29 Dec 2023 22:05:27 -0800 Subject: [PATCH] `Ctrl+Numpad5` vertically aligns found text in "All matching entries" menu. 1. `Ctrl+Numpad5` in Editor menu "All matching entries" vertically aligns found text in all items. To preserve vertical alignment while scrolling items horizontally, the items can be moved beyond the left or right window edges. 2. New `far:config` `VMenu.SwapHScrollDirection` changes the meaning of left and right keys in all menus and lists. See `far:config` help for details. --- far/FarCze.hlf.m4 | 3 + far/FarEng.hlf.m4 | 3 + far/FarGer.hlf.m4 | 3 + far/FarHun.hlf.m4 | 3 + far/FarPol.hlf.m4 | 3 + far/FarRus.hlf.m4 | 3 + far/FarSky.hlf.m4 | 3 + far/FarUkr.hlf.m4 | 3 + far/changelog | 7 + far/dialog.cpp | 1 + far/editor.cpp | 2 +- far/vbuild.m4 | 2 +- far/vmenu.cpp | 520 ++++++++++++++++++++++++++++++++++++---------- far/vmenu.hpp | 72 ++++--- far/vmenu2.cpp | 2 +- 15 files changed, 481 insertions(+), 149 deletions(-) diff --git a/far/FarCze.hlf.m4 b/far/FarCze.hlf.m4 index ff4993bacda..85da636221f 100644 --- a/far/FarCze.hlf.m4 +++ b/far/FarCze.hlf.m4 @@ -3488,6 +3488,9 @@ $ #Editor: All matching entries menu# #Ctrl+Enter#, #Ctrl+Left mouse click# Go to the position of the found text. + #Ctrl+Numpad5# + Vertically align all found entries. + #Gray +# Add session bookmark with the current position. diff --git a/far/FarEng.hlf.m4 b/far/FarEng.hlf.m4 index b80b2c6c1f5..311a6b30712 100644 --- a/far/FarEng.hlf.m4 +++ b/far/FarEng.hlf.m4 @@ -3454,6 +3454,9 @@ $ #Editor: All matching entries menu# #Ctrl+Enter#, #Ctrl+Left mouse click# Go to the position of the found text. + #Ctrl+Numpad5# + Vertically align all found entries. + #Gray +# Add session bookmark with the current position. diff --git a/far/FarGer.hlf.m4 b/far/FarGer.hlf.m4 index 44a91faf90f..cd3c9e30b21 100644 --- a/far/FarGer.hlf.m4 +++ b/far/FarGer.hlf.m4 @@ -3528,6 +3528,9 @@ $ #Editor: All matching entries menu# #Ctrl+Enter#, #Ctrl+Left mouse click# Go to the position of the found text. + #Ctrl+Numpad5# + Vertically align all found entries. + #Gray +# Add session bookmark with the current position. diff --git a/far/FarHun.hlf.m4 b/far/FarHun.hlf.m4 index 566a561dec3..f6557dcb9d1 100644 --- a/far/FarHun.hlf.m4 +++ b/far/FarHun.hlf.m4 @@ -3537,6 +3537,9 @@ $ #Editor: All matching entries menu# #Ctrl+Enter#, #Ctrl+Left mouse click# Go to the position of the found text. + #Ctrl+Numpad5# + Vertically align all found entries. + #Gray +# Add session bookmark with the current position. diff --git a/far/FarPol.hlf.m4 b/far/FarPol.hlf.m4 index 7fac149ac53..d876dcdf9df 100644 --- a/far/FarPol.hlf.m4 +++ b/far/FarPol.hlf.m4 @@ -3452,6 +3452,9 @@ $ #Edytor: menu wszystkich znalezionych wystąpień# #Ctrl+Enter#, #Ctrl+Kliknięcie lewym przyciskiem myszy# Przechodzi do pozycji znalezionego tekstu. + #Ctrl+Numpad5# + Vertically align all found entries. + #Szary +# Dodaje zakładkę sesji w bieżącej pozycji. diff --git a/far/FarRus.hlf.m4 b/far/FarRus.hlf.m4 index 9c2de67612b..050c306dc93 100644 --- a/far/FarRus.hlf.m4 +++ b/far/FarRus.hlf.m4 @@ -3506,6 +3506,9 @@ $ #Редактор: Меню результатов поиска всех вх #Ctrl+Enter#, #Ctrl+Щелчок левой кнопки мыши# Перейти к позиции найденного текста. + #Ctrl+Numpad5# + Выровнять все вхождения по вертикали. + #Gray +# Добавить сеансовую закладку с текущей позицией. diff --git a/far/FarSky.hlf.m4 b/far/FarSky.hlf.m4 index cbde26530d0..715826f5a14 100644 --- a/far/FarSky.hlf.m4 +++ b/far/FarSky.hlf.m4 @@ -3449,6 +3449,9 @@ $ #Editor: All matching entries menu# #Ctrl+Enter#, #Ctrl+Left mouse click# Go to the position of the found text. + #Ctrl+Numpad5# + Vertically align all found entries. + #Gray +# Add session bookmark with the current position. diff --git a/far/FarUkr.hlf.m4 b/far/FarUkr.hlf.m4 index cb7df1f8130..a769ce60cb1 100644 --- a/far/FarUkr.hlf.m4 +++ b/far/FarUkr.hlf.m4 @@ -3512,6 +3512,9 @@ $ #Editor: All matching entries menu# #Ctrl+Enter#, #Ctrl+Left mouse click# Go to the position of the found text. + #Ctrl+Numpad5# + Vertically align all found entries. + #Gray +# Add session bookmark with the current position. diff --git a/far/changelog b/far/changelog index bed1834c276..f117c313491 100644 --- a/far/changelog +++ b/far/changelog @@ -1,3 +1,10 @@ +-------------------------------------------------------------------------------- +MZK 2024-02-03 13:11:20-08:00 - build 6268 + +1. `Ctrl+Numpad5` in Editor menu "All matching entries" vertically aligns + found text in all items. To preserve vertical alignment while scrolling items + horizontally, the items can be moved beyond the left or right window edges. + -------------------------------------------------------------------------------- drkns 2024-02-03 16:21:32+00:00 - build 6267 diff --git a/far/dialog.cpp b/far/dialog.cpp index f35d51da8e0..2ab60e88a29 100644 --- a/far/dialog.cpp +++ b/far/dialog.cpp @@ -2548,6 +2548,7 @@ bool Dialog::ProcessKey(const Manager::Key& Key) case KEY_MSWHEEL_RIGHT: case KEY_NUMENTER: case KEY_ENTER: + case KEY_NUMPAD5: auto& List = Items[m_FocusPos].ListPtr; int CurListPos=List->GetSelectPos(); List->ProcessKey(Key); diff --git a/far/editor.cpp b/far/editor.cpp index 6744cd2cf80..70d1ca1cf2d 100644 --- a/far/editor.cpp +++ b/far/editor.cpp @@ -3751,7 +3751,7 @@ void Editor::DoSearchReplace(const SearchReplaceDisposition Disposition) { const auto MenuY1 = std::max(0, ScrY - 20); const auto MenuY2 = std::min(ScrY, MenuY1 + std::min(static_cast(FindAllList->size()), 10) + 2); - FindAllList->SetMenuFlags(VMENU_WRAPMODE | VMENU_SHOWAMPERSAND); + FindAllList->SetMenuFlags(VMENU_WRAPMODE | VMENU_SHOWAMPERSAND | VMENU_ENABLEALIGNANNOTATIONS); FindAllList->SetPosition({ -1, MenuY1, 0, MenuY2 }); FindAllList->SetTitle(far::vformat(msg(lng::MEditSearchStatistics), FindAllList->size(), AllRefLines)); FindAllList->SetBottomTitle(KeysToLocalizedText(KEY_CTRLENTER, KEY_F5, KEY_ADD, KEY_CTRLUP, KEY_CTRLDOWN)); diff --git a/far/vbuild.m4 b/far/vbuild.m4 index 6ab38747965..311191cad2e 100644 --- a/far/vbuild.m4 +++ b/far/vbuild.m4 @@ -1 +1 @@ -6267 +6268 diff --git a/far/vmenu.cpp b/far/vmenu.cpp index b3b611afe94..2ca505ae0e6 100644 --- a/far/vmenu.cpp +++ b/far/vmenu.cpp @@ -136,12 +136,12 @@ struct menu_layout return DOUBLE_BOX; } - [[nodiscard]] static size_t get_service_area_size(const VMenu& Menu) + [[nodiscard]] static int get_service_area_size(const VMenu& Menu) { return get_service_area_size(Menu, need_box(Menu)); } - [[nodiscard]] static size_t get_service_area_size(const VMenu& Menu, const short BoxType) + [[nodiscard]] static int get_service_area_size(const VMenu& Menu, const short BoxType) { return get_service_area_size(Menu, need_box(BoxType)); } @@ -155,7 +155,7 @@ struct menu_layout return { Menu.m_Where.left + 1, Menu.m_Where.top + 1, Menu.m_Where.right - 1, Menu.m_Where.bottom - 1 }; } - [[nodiscard]] static size_t get_service_area_size(const VMenu& Menu, const bool NeedBox) + [[nodiscard]] static int get_service_area_size(const VMenu& Menu, const bool NeedBox) { return NeedBox + need_check_mark() @@ -282,6 +282,74 @@ namespace return !(Item.Flags & (LIF_HIDDEN | LIF_FILTERED)); } + int get_item_visual_length(const bool ShowAmpersand, const string_view ItemName) + { + return static_cast(ShowAmpersand ? visual_string_length(ItemName) : HiStrlen(ItemName)); + } + + enum class item_hscroll_policy + { + unbound, // The item can move freely beyond window edges. + cling_to_edge, // The item can move beyond window edges, but at least one character is always visible. + bound, // The item can move only within the window boundaries. + bound_stick_to_left // Like bound, but if the item shorter than TextAreaWidth, it is always attached to the left window edge. + }; + + std::pair item_hpos_limits(const int ItemLength, const int TextAreaWidth, const item_hscroll_policy Policy) noexcept + { + using enum item_hscroll_policy; + + assert(ItemLength > 0); + assert(TextAreaWidth > 0); + + switch (Policy) + { + case unbound: + return{ std::numeric_limits::min(), std::numeric_limits::max()}; + + case cling_to_edge: + return{ 1 - ItemLength, TextAreaWidth - 1 }; + + case bound: + return{ std::min(0, TextAreaWidth - ItemLength), std::max(0, TextAreaWidth - ItemLength) }; + + case bound_stick_to_left: + return{ std::min(0, TextAreaWidth - ItemLength), 0 }; + + default: + std::unreachable(); + } + } + + int get_item_absolute_hpos(const int NewHPos, const int ItemLength, const int TextAreaWidth, const item_hscroll_policy Policy) + { + const auto [HPosMin, HPosMax]{ item_hpos_limits(ItemLength, TextAreaWidth, Policy) }; + return std::clamp(NewHPos, HPosMin, HPosMax); + } + + int get_item_smart_hpos(const int NewHPos, const int ItemLength, const int TextAreaWidth, const item_hscroll_policy Policy) + { + return get_item_absolute_hpos(NewHPos >= 0 ? NewHPos : TextAreaWidth - ItemLength + NewHPos + 1, ItemLength, TextAreaWidth, Policy); + } + + int adjust_hpos_shift(const int Shift, const int Left, const int Right, const int TextAreaWidth) + { + assert(Left < Right); + + if (Shift == 0) return 0; + + // Shift left. + if (Shift > 0) + { + const auto ShiftLimit{ std::max(TextAreaWidth - Left - 1, 0) }; + const auto GapLeftOfTextArea{ std::max(-Right, 0) }; + return std::min(Shift + GapLeftOfTextArea, ShiftLimit); + } + + // Shift right. It's just shift left seen from behind the screen. + return -adjust_hpos_shift(-Shift, TextAreaWidth - Right, TextAreaWidth - Left, TextAreaWidth); + } + // Indices in the color array enum class color_indices { @@ -679,15 +747,14 @@ int VMenu::AddItem(MenuItemEx&& NewItem,int PosAdd) NewMenuItem.AutoHotkey = {}; NewMenuItem.AutoHotkeyPos = 0; - NewMenuItem.ShowPos = 0; + NewMenuItem.HPos = 0; if (PosAdd <= SelectPos) SelectPos++; - if (CheckFlags(VMENU_SHOWAMPERSAND)) - UpdateMaxLength(visual_string_length(NewMenuItem.Name)); - else - UpdateMaxLength(HiStrlen(NewMenuItem.Name)); + const auto ItemLength{ get_item_visual_length(CheckFlags(VMENU_SHOWAMPERSAND), NewMenuItem.Name) }; + UpdateMaxLength(ItemLength); + UpdateAllItemsBoundaries(NewMenuItem.HPos, ItemLength); const auto NewFlags = NewMenuItem.Flags; NewMenuItem.Flags = 0; @@ -715,10 +782,9 @@ bool VMenu::UpdateItem(const FarListUpdate *NewItem) UpdateItemFlags(NewItem->Index, NewItem->Item.Flags); Item.SimpleUserData = NewItem->Item.UserData; - if (CheckFlags(VMENU_SHOWAMPERSAND)) - UpdateMaxLength(visual_string_length(Item.Name)); - else - UpdateMaxLength(HiStrlen(Item.Name)); + const auto ItemLength{ get_item_visual_length(CheckFlags(VMENU_SHOWAMPERSAND), Item.Name) }; + UpdateMaxLength(ItemLength); + UpdateAllItemsBoundaries(Item.HPos, ItemLength); SetMenuFlags(VMENU_UPDATEREQUIRED | (bFilterEnabled ? VMENU_REFILTERREQUIRED : VMENU_NONE)); @@ -789,6 +855,7 @@ void VMenu::clear() TopPos=0; m_MaxItemLength = 0; UpdateMaxLengthFromTitles(); + ResetAllItemsBoundaries(); SetMenuFlags(VMENU_UPDATEREQUIRED); } @@ -1434,6 +1501,17 @@ bool VMenu::ProcessKey(const Manager::Key& Key) } } }; + + const auto HScrollSmartPos = [=]() + { + return any_of(LocalKey & ~KEY_CTRLMASK, KEY_HOME, KEY_NUMPAD7) ? 0 : -1; + }; + + const auto HScrollShiftSign = [=]() + { + return any_of(LocalKey & ~KEY_CTRLMASK, KEY_LEFT, KEY_NUMPAD4, KEY_MSWHEEL_LEFT) ? 1 : -1; + }; + switch (LocalKey) { case KEY_ALTF9: @@ -1517,7 +1595,7 @@ bool VMenu::ProcessKey(const Manager::Key& Key) case KEY_ALTEND: case KEY_ALT|KEY_NUMPAD1: case KEY_RALTEND: case KEY_RALT|KEY_NUMPAD1: { - if (SetAllItemsShowPos(any_of(LocalKey & ~KEY_CTRLMASK, KEY_HOME, KEY_NUMPAD7)? 0 : -1)) + if (SetAllItemsSmartHPos(HScrollSmartPos())) DrawMenu(); break; @@ -1525,7 +1603,7 @@ bool VMenu::ProcessKey(const Manager::Key& Key) case KEY_ALTSHIFTHOME: case KEY_ALTSHIFT|KEY_NUMPAD7: case KEY_ALTSHIFTEND: case KEY_ALTSHIFT|KEY_NUMPAD1: { - if (SetItemShowPos(SelectPos, any_of(LocalKey & ~KEY_CTRLMASK, KEY_HOME, KEY_NUMPAD7)? 0 : -1, CalculateTextAreaWidth())) + if (SetCurItemSmartHPos(HScrollSmartPos())) DrawMenu(); break; @@ -1535,7 +1613,7 @@ bool VMenu::ProcessKey(const Manager::Key& Key) case KEY_ALTRIGHT: case KEY_ALT|KEY_NUMPAD6: case KEY_MSWHEEL_RIGHT: case KEY_RALTRIGHT: case KEY_RALT|KEY_NUMPAD6: { - if (ShiftAllItemsShowPos(any_of(LocalKey & ~KEY_CTRLMASK, KEY_LEFT, KEY_NUMPAD4, KEY_MSWHEEL_LEFT)? -1 : 1)) + if (ShiftAllItemsHPos(HScrollShiftSign())) DrawMenu(); break; @@ -1545,7 +1623,7 @@ bool VMenu::ProcessKey(const Manager::Key& Key) case KEY_CTRLALTRIGHT: case KEY_CTRLALTNUMPAD6: case KEY_CTRL|KEY_MSWHEEL_RIGHT: case KEY_CTRLRALTRIGHT: case KEY_CTRLRALTNUMPAD6: { - if (ShiftAllItemsShowPos(any_of(LocalKey & ~KEY_CTRLMASK, KEY_LEFT, KEY_NUMPAD4, KEY_MSWHEEL_LEFT)? -20 : 20)) + if (ShiftAllItemsHPos(20 * HScrollShiftSign())) DrawMenu(); break; @@ -1555,7 +1633,7 @@ bool VMenu::ProcessKey(const Manager::Key& Key) case KEY_ALTSHIFTRIGHT: case KEY_ALTSHIFTNUMPAD6: case KEY_RALTSHIFTRIGHT: case KEY_RALTSHIFTNUMPAD6: { - if (ShiftItemShowPos(SelectPos, any_of(LocalKey & ~KEY_CTRLMASK, KEY_LEFT, KEY_NUMPAD4)? -1 : 1, CalculateTextAreaWidth())) + if (ShiftCurItemHPos(HScrollShiftSign())) DrawMenu(); break; @@ -1563,7 +1641,15 @@ bool VMenu::ProcessKey(const Manager::Key& Key) case KEY_CTRLSHIFTLEFT: case KEY_CTRLSHIFTNUMPAD4: case KEY_CTRLSHIFTRIGHT: case KEY_CTRLSHIFTNUMPAD6: { - if (ShiftItemShowPos(SelectPos, any_of(LocalKey & ~KEY_CTRLMASK, KEY_LEFT, KEY_NUMPAD4)? -20 : 20, CalculateTextAreaWidth())) + if (ShiftCurItemHPos(20 * HScrollShiftSign())) + DrawMenu(); + + break; + } + case KEY_CTRLNUMPAD5: + case KEY_RCTRLNUMPAD5: + { + if (AlignAnnotations()) DrawMenu(); break; @@ -2011,79 +2097,121 @@ int VMenu::VisualPosToReal(int VPos) const return ItemIterator != Items.cend()? ItemIterator - Items.cbegin() : -1; } -size_t VMenu::GetItemMaxShowPos(int Item, const size_t MaxLineWidth) const +template +bool VMenu::SetItemHPos(MenuItemEx& Item, const GetNewHPos_T GetNewHPos, int const TextAreaWidth) { - const auto Len{ VMFlags.Check(VMENU_SHOWAMPERSAND)? visual_string_length(Items[Item].Name) : HiStrlen(Items[Item].Name) }; - if (Len <= MaxLineWidth) - return 0; - return Len - MaxLineWidth; -} + if (Item.Flags & LIF_SEPARATOR) return false; -bool VMenu::SetItemShowPos(int Item, int NewShowPos, const size_t MaxLineWidth) -{ - const auto MaxShowPos{ GetItemMaxShowPos(Item, MaxLineWidth) }; - auto OldShowPos{ Items[Item].ShowPos }; + const auto ItemLength{ get_item_visual_length(CheckFlags(VMENU_SHOWAMPERSAND), Item.Name) }; + if (ItemLength <= 0) return false; - if (NewShowPos >= 0) + const auto NewHPos = [&]() -> int { - Items[Item].ShowPos = std::min(static_cast(NewShowPos), MaxShowPos); - } - else - { - Items[Item].ShowPos = MaxShowPos - std::min(static_cast(-(NewShowPos + 1)), MaxShowPos); - } + if constexpr (std::is_invocable_r_v) + return GetNewHPos(Item); + if constexpr (std::is_invocable_r_v) + return GetNewHPos(ItemLength); + if constexpr (std::is_invocable_r_v) + return GetNewHPos(Item.HPos, ItemLength); + }(); - if (Items[Item].ShowPos == OldShowPos) - return false; + UpdateAllItemsBoundaries(NewHPos, ItemLength); + + if (Item.HPos == NewHPos) return false; - VMFlags.Set(VMENU_UPDATEREQUIRED); + Item.HPos = NewHPos; return true; } -bool VMenu::ShiftItemShowPos(int Item, int Shift, const size_t MaxLineWidth) +bool VMenu::SetCurItemSmartHPos(const int NewHPos) { - const auto MaxShowPos{ GetItemMaxShowPos(Item, MaxLineWidth) }; - auto ItemShowPos{ std::min(Items[Item].ShowPos, MaxShowPos) }; // Just in case + const auto TextAreaWidth{ CalculateTextAreaWidth() }; + if (TextAreaWidth <= 0) return false; + + const auto Policy{ CheckFlags(VMENU_ENABLEALIGNANNOTATIONS) ? item_hscroll_policy::cling_to_edge : item_hscroll_policy::bound_stick_to_left }; - if (Shift >= 0) + if (SetItemHPos( + Items[SelectPos], + [=](const int ItemLength) { return get_item_smart_hpos(NewHPos, ItemLength, TextAreaWidth, Policy); }, + TextAreaWidth)) { - ItemShowPos += std::min(static_cast(Shift), MaxShowPos - ItemShowPos); + SetMenuFlags(VMENU_UPDATEREQUIRED); + return true; } - else + + return false; +} + +bool VMenu::ShiftCurItemHPos(const int Shift) +{ + const auto TextAreaWidth{ CalculateTextAreaWidth() }; + if (TextAreaWidth <= 0) return false; + + const auto Policy{ CheckFlags(VMENU_ENABLEALIGNANNOTATIONS) ? item_hscroll_policy::cling_to_edge : item_hscroll_policy::bound_stick_to_left }; + + if (SetItemHPos( + Items[SelectPos], + [=](const int ItemHPos, const int ItemLength) { return get_item_absolute_hpos(ItemHPos + Shift, ItemLength, TextAreaWidth, Policy); }, + TextAreaWidth)) { - ItemShowPos -= std::min(static_cast(-Shift), ItemShowPos); + SetMenuFlags(VMENU_UPDATEREQUIRED); + return true; } - if (ItemShowPos == Items[Item].ShowPos) - return false; - - Items[Item].ShowPos = ItemShowPos; - VMFlags.Set(VMENU_UPDATEREQUIRED); - return true; + return false; } -bool VMenu::SetAllItemsShowPos(int NewShowPos) +template +bool VMenu::SetAllItemsHPos(GetNewHPos_T&& GetNewHPos, int const TextAreaWidth) { - bool NeedRedraw = false; - - const auto MaxLineWidth{ CalculateTextAreaWidth() }; + ResetAllItemsBoundaries(); + bool NeedRedraw{}; - for (const auto I: std::views::iota(0uz, Items.size())) - NeedRedraw |= SetItemShowPos(static_cast(I), NewShowPos, MaxLineWidth); + for (auto& Item : Items) + NeedRedraw |= SetItemHPos(Item, std::forward(GetNewHPos), TextAreaWidth); + if (NeedRedraw) SetMenuFlags(VMENU_UPDATEREQUIRED); return NeedRedraw; } -bool VMenu::ShiftAllItemsShowPos(int Shift) +bool VMenu::SetAllItemsSmartHPos(const int NewHPos) { - bool NeedRedraw = false; + const auto TextAreaWidth{ CalculateTextAreaWidth() }; + if (TextAreaWidth <= 0) return false; - const auto MaxLineWidth{ CalculateTextAreaWidth() }; + const auto Policy{ CheckFlags(VMENU_ENABLEALIGNANNOTATIONS) ? item_hscroll_policy::unbound : item_hscroll_policy::bound_stick_to_left }; - for (const auto I: std::views::iota(0uz, Items.size())) - NeedRedraw |= ShiftItemShowPos(static_cast(I), Shift, MaxLineWidth); + return SetAllItemsHPos( + [=](const int ItemLength) { return get_item_smart_hpos(NewHPos, ItemLength, TextAreaWidth, Policy); }, + TextAreaWidth); +} - return NeedRedraw; +bool VMenu::ShiftAllItemsHPos(const int Shift) +{ + const auto TextAreaWidth{ CalculateTextAreaWidth() }; + if (TextAreaWidth <= 0) return false; + + const auto AdjustedShift{ adjust_hpos_shift(Shift, m_AllItemsBoundaries.first, m_AllItemsBoundaries.second, TextAreaWidth) }; + if (!AdjustedShift) return false; + + const auto Policy{ CheckFlags(VMENU_ENABLEALIGNANNOTATIONS) ? item_hscroll_policy::unbound : item_hscroll_policy::bound_stick_to_left }; + + return SetAllItemsHPos( + [=](const int ItemHPos, const int ItemLength) { return get_item_absolute_hpos(ItemHPos + AdjustedShift, ItemLength, TextAreaWidth, Policy); }, + TextAreaWidth); +} + +bool VMenu::AlignAnnotations() +{ + if (!CheckFlags(VMENU_ENABLEALIGNANNOTATIONS)) return false; + + const auto TextAreaWidth{ CalculateTextAreaWidth() }; + if (TextAreaWidth <= 0 || TextAreaWidth + 2 <= 0) return false; + const auto AlignPos{ (TextAreaWidth + 2) / 4 }; + + return SetAllItemsHPos( + [=](const MenuItemEx& Item) { return Item.Annotations.empty() ? 0 : AlignPos - Item.Annotations.front().first; }, + TextAreaWidth); } void VMenu::Show() @@ -2097,7 +2225,7 @@ void VMenu::Show() if (!CheckFlags(VMENU_COMBOBOX)) { - const auto VisibleMaxItemLength = std::min(static_cast(ScrX) > ServiceAreaSize? ScrX - ServiceAreaSize : 0, m_MaxItemLength); + const auto VisibleMaxItemLength = std::min(ScrX > ServiceAreaSize? ScrX - ServiceAreaSize : 0, m_MaxItemLength); const auto MenuWidth = ServiceAreaSize + VisibleMaxItemLength; bool AutoCenter = false; @@ -2318,7 +2446,6 @@ void VMenu::DrawTitles() const if (CheckFlags(VMENU_SHOWNOBOX)) return; const auto MaxTitleLength = m_Where.width() - 3; - int WidthTitle; if (!strTitle.empty() || bFilterEnabled) { @@ -2334,7 +2461,7 @@ void VMenu::DrawTitles() const append(strDisplayTitle, bFilterLocked? L'<' : L'[', strFilter, bFilterLocked? L'>' : L']'); } - WidthTitle = static_cast(strDisplayTitle.size()); + auto WidthTitle = static_cast(strDisplayTitle.size()); if (WidthTitle > MaxTitleLength) WidthTitle = MaxTitleLength - 1; @@ -2347,7 +2474,7 @@ void VMenu::DrawTitles() const if (!strBottomTitle.empty()) { - WidthTitle = static_cast(strBottomTitle.size()); + auto WidthTitle = static_cast(strBottomTitle.size()); if (WidthTitle > MaxTitleLength) WidthTitle = MaxTitleLength - 1; @@ -2357,6 +2484,18 @@ void VMenu::DrawTitles() const Text(concat(L' ', string_view(strBottomTitle).substr(0, WidthTitle), L' ')); } + +#ifdef _DEBUG + set_color(Colors, color_indices::Title); + + const auto AllItemsBoundariesLabel{ std::format(L" [{}, {}] ", m_AllItemsBoundaries.first, m_AllItemsBoundaries.second) }; + GotoXY(m_Where.left + 2, m_Where.bottom); + Text(AllItemsBoundariesLabel); + + const auto TextAreaWidthLabel{ std::format(L" [{}] ", CalculateTextAreaWidth()) }; + GotoXY(m_Where.right - 1 - static_cast(TextAreaWidthLabel.size()), m_Where.bottom); + Text(TextAreaWidthLabel); +#endif // _DEBUG } int VMenu::AdjustTopPos(const int BoxType) @@ -2468,48 +2607,56 @@ void VMenu::DrawRegularItem(const MenuItemEx& Item, const menu_layout& Layout, c { if (!Layout.TextArea) return; - size_t HotkeyPos = string::npos; - auto ItemTextToDisplay = CheckFlags(VMENU_SHOWAMPERSAND)? Item.Name : HiText2Str(Item.Name, &HotkeyPos); - std::ranges::replace(ItemTextToDisplay, L'\t', L' '); - const auto ItemTextSize{ static_cast(ItemTextToDisplay.size()) }; - const auto [TextAreaBegin, TextAreaWidth] { Layout.TextArea.value() }; + GotoXY(TextAreaBegin, Y); + const item_color_indicies ColorIndices{ Item }; auto CurColorIndex{ ColorIndices.Normal }; auto AltColorIndex{ ColorIndices.Highlighted }; - auto CurPos{ static_cast(Item.ShowPos) }; - const auto EndPos{ std::min(CurPos + TextAreaWidth, ItemTextSize) }; + size_t HotkeyPos = string::npos; + auto ItemTextToDisplay = CheckFlags(VMENU_SHOWAMPERSAND)? Item.Name : HiText2Str(Item.Name, &HotkeyPos); + std::ranges::replace(ItemTextToDisplay, L'\t', L' '); + const auto ItemTextSize{ static_cast(ItemTextToDisplay.size()) }; - HighlightMarkup.clear(); - if (!Item.Annotations.empty()) - { - markup_slice_boundaries( - std::pair{ CurPos, EndPos }, - Item.Annotations | std::views::transform([](const auto Ann) { return std::pair{ Ann.first, Ann.first + Ann.second }; }), - HighlightMarkup); - } - else if (HotkeyPos != string::npos || Item.AutoHotkey) - { - const auto HighlightPos = static_cast(HotkeyPos != string::npos? HotkeyPos : Item.AutoHotkeyPos); - markup_slice_boundaries( - std::pair{ CurPos, EndPos }, - std::views::single(std::pair{ HighlightPos, HighlightPos + 1 }), - HighlightMarkup); - } - else - { - HighlightMarkup.emplace_back(EndPos); - } + const auto [ItemTextBegin, ItemTextEnd] { intersect({ 0, ItemTextSize }, { -Item.HPos, TextAreaWidth - Item.HPos }) }; - GotoXY(TextAreaBegin, Y); - for (const auto SliceEnd : HighlightMarkup) + if (ItemTextBegin < ItemTextEnd) { - set_color(Colors, CurColorIndex); - Text(string_view{ ItemTextToDisplay }.substr(CurPos, SliceEnd - CurPos)); - std::ranges::swap(CurColorIndex, AltColorIndex); - CurPos = SliceEnd; + HighlightMarkup.clear(); + if (!Item.Annotations.empty()) + { + markup_slice_boundaries( + std::pair{ ItemTextBegin, ItemTextEnd }, + Item.Annotations | std::views::transform([](const auto Ann) { return std::pair{ Ann.first, Ann.first + Ann.second }; }), + HighlightMarkup); + } + else if (HotkeyPos != string::npos || Item.AutoHotkey) + { + const auto HighlightPos = static_cast(HotkeyPos != string::npos? HotkeyPos : Item.AutoHotkeyPos); + markup_slice_boundaries( + std::pair{ ItemTextBegin, ItemTextEnd }, + std::views::single(std::pair{ HighlightPos, HighlightPos + 1 }), + HighlightMarkup); + } + else + { + HighlightMarkup.emplace_back(ItemTextEnd); + } + + set_color(Colors, ColorIndices.Normal); + Text(BlankLine.substr(0, std::max(Item.HPos, 0))); + + auto ItemTextPos{ ItemTextBegin }; + + for (const auto SliceEnd : HighlightMarkup) + { + set_color(Colors, CurColorIndex); + Text(string_view{ ItemTextToDisplay }.substr(ItemTextPos, SliceEnd - ItemTextPos)); + std::ranges::swap(CurColorIndex, AltColorIndex); + ItemTextPos = SliceEnd; + } } set_color(Colors, ColorIndices.Normal); @@ -2526,13 +2673,13 @@ void VMenu::DrawRegularItem(const MenuItemEx& Item, const menu_layout& Layout, c DrawDecorator(Layout.CheckMark.value(), get_item_check_mark(Item, ColorIndices)); if (Layout.LeftHScroll) - DrawDecorator(Layout.LeftHScroll.value(), get_item_left_hscroll(Item.ShowPos > 0, ColorIndices)); + DrawDecorator(Layout.LeftHScroll.value(), get_item_left_hscroll(Item.HPos < 0, ColorIndices)); if (Layout.SubMenu) DrawDecorator(Layout.SubMenu.value(), get_item_submenu(Item, ColorIndices)); if (Layout.RightHScroll) - DrawDecorator(Layout.RightHScroll.value(), get_item_right_hscroll(EndPos < ItemTextSize, ColorIndices)); + DrawDecorator(Layout.RightHScroll.value(), get_item_right_hscroll(Item.HPos + ItemTextSize > TextAreaWidth, ColorIndices)); } int VMenu::CheckHighlights(wchar_t CheckSymbol, int StartPos) const @@ -2613,7 +2760,7 @@ void VMenu::AssignHighlights(bool Reverse) if (!ShowAmpersand && HotkeyVisualPos != string::npos) continue; - MenuItemForDisplay.erase(0, Items[I].ShowPos); + MenuItemForDisplay.erase(0, std::max(-Items[I].HPos, 0)); // TODO: проверка на LIF_HIDDEN for (const auto& Ch: MenuItemForDisplay) @@ -2664,12 +2811,26 @@ bool VMenu::CheckKeyHiOrAcc(DWORD Key, int Type, bool Translate, bool ChangePos, void VMenu::UpdateMaxLengthFromTitles() { //тайтл + 2 пробела вокруг - UpdateMaxLength(std::max(strTitle.size(), strBottomTitle.size()) + 2); + UpdateMaxLength(static_cast(std::max(strTitle.size(), strBottomTitle.size()) + 2)); } -void VMenu::UpdateMaxLength(size_t const Length) +void VMenu::UpdateMaxLength(int const ItemLength) { - m_MaxItemLength = std::max(m_MaxItemLength, Length); + m_MaxItemLength = std::max(m_MaxItemLength, ItemLength); +} + +void VMenu::ResetAllItemsBoundaries() +{ + m_AllItemsBoundaries = { std::numeric_limits::max(), std::numeric_limits::min() }; +} + +void VMenu::UpdateAllItemsBoundaries(int const ItemHPos, int const ItemLength) +{ + m_AllItemsBoundaries = + { + std::min(m_AllItemsBoundaries.first, ItemHPos), + std::max(m_AllItemsBoundaries.second, ItemHPos + ItemLength) + }; } void VMenu::SetMaxHeight(int NewMaxHeight) @@ -2697,7 +2858,7 @@ void VMenu::SetBottomTitle(string_view const BottomTitle) strBottomTitle = BottomTitle; - UpdateMaxLength(strBottomTitle.size() + 2); + UpdateMaxLength(static_cast(strBottomTitle.size() + 2)); } void VMenu::SetTitle(string_view const Title) @@ -2706,7 +2867,7 @@ void VMenu::SetTitle(string_view const Title) strTitle = Title; - UpdateMaxLength(strTitle.size() + 2); + UpdateMaxLength(static_cast(strTitle.size() + 2)); } void VMenu::ResizeConsole() @@ -2898,7 +3059,7 @@ bool VMenu::GetVMenuInfo(FarListInfo* Info) const Info->MaxHeight = MaxHeight; // BUGBUG const auto ServiceAreaSize = menu_layout::get_service_area_size(*this); - if (static_cast(m_Where.width()) > ServiceAreaSize) + if (m_Where.width() > ServiceAreaSize) Info->MaxLength = m_Where.width() - ServiceAreaSize; else Info->MaxLength = 0; @@ -3080,7 +3241,7 @@ std::vector VMenu::AddHotkeys(std::span const MenuItems) size_t VMenu::GetNaturalMenuWidth() const { - return m_MaxItemLength + menu_layout::get_service_area_size(*this); + return static_cast(m_MaxItemLength) + menu_layout::get_service_area_size(*this); } void VMenu::EnableFilter(bool const Enable) @@ -3204,12 +3365,14 @@ TEST_CASE("intersect.segments") TEST_CASE("markup.slice.boundaries") { - static const struct test_data + struct test_data { std::pair Segment; std::initializer_list> Slices; std::initializer_list Markup; - } TestDataPoints[] = + }; + + static const test_data TestDataPoints[] = { { { 20, 50 }, { { 10, 15 } }, { 50 } }, { { 20, 50 }, { { 10, 20 } }, { 50 } }, @@ -3246,4 +3409,137 @@ TEST_CASE("markup.slice.boundaries") } } +TEST_CASE("item.hpos.limits") +{ + using enum item_hscroll_policy; + + struct test_data + { + int ItemLength; + int TextAreaWidth; + // cling_to_edge, bound, bound_stick_to_left + std::initializer_list> Expected; + }; + + static const test_data TestDataPoints[] = + { + { 1, 5, { { 0, 4 }, { 0, 4 }, { 0, 0 } } }, + { 3, 5, { { -2, 4 }, { 0, 2 }, { 0, 0 } } }, + { 5, 5, { { -4, 4 }, { 0, 0 }, { 0, 0 } } }, + { 7, 5, { { -6, 4 }, { -2, 0 }, { -2, 0 } } }, + { 10, 5, { { -9, 4 }, { -5, 0 }, { -5, 0 } } }, + }; + + for (const auto& TestDataPoint : TestDataPoints) + { + REQUIRE(std::pair{ std::numeric_limits::min(), std::numeric_limits::max() } + == item_hpos_limits(TestDataPoint.ItemLength, TestDataPoint.TextAreaWidth, unbound)); + + for (const auto& Policy : { cling_to_edge, bound, bound_stick_to_left }) + { + REQUIRE(std::ranges::data(TestDataPoint.Expected)[std::to_underlying(Policy) - 1] + == item_hpos_limits(TestDataPoint.ItemLength, TestDataPoint.TextAreaWidth, Policy)); + } + } +} + +TEST_CASE("adjust.hpos.shift") +{ + static constexpr int TextAreaWidth{ 10 }; + static constexpr std::array Shifts{ -20, -19, -18, -17, -15, -14, -13, -11, -10, -9, -7, -5, -3, -1, 0, 1, 3, 5, 7, 9, 10, 11, 13, 14, 15, 17, 18, 19, 20 }; + + struct test_data + { + int Left; + int Right; + std::array Expected; + }; + + static constexpr test_data TestDataPoints[] = + { + // Shifts{ -20, -19, -18, -17, -15, -14, -13, -11, -10, -9, -7, -5, -3, -1, 0, 1, 3, 5, 7, 9, 10, 11, 13, 14, 15, 17, 18, 19, 20 } + { -10, -5, { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 8, 10, 12, 14, 15, 16, 18, 19, 19, 19, 19, 19, 19 } }, + { -10, -1, { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 4, 6, 8, 10, 11, 12, 14, 15, 16, 18, 19, 19, 19 } }, + { -10, 0, { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 3, 5, 7, 9, 10, 11, 13, 14, 15, 17, 18, 19, 19 } }, + { -10, 1, { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 3, 5, 7, 9, 10, 11, 13, 14, 15, 17, 18, 19, 19 } }, + { -10, 5, { -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -3, -1, 0, 1, 3, 5, 7, 9, 10, 11, 13, 14, 15, 17, 18, 19, 19 } }, + { -10, 9, { -8, -8, -8, -8, -8, -8, -8, -8, -8, -8, -7, -5, -3, -1, 0, 1, 3, 5, 7, 9, 10, 11, 13, 14, 15, 17, 18, 19, 19 } }, + { -10, 10, { -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -7, -5, -3, -1, 0, 1, 3, 5, 7, 9, 10, 11, 13, 14, 15, 17, 18, 19, 19 } }, + { -10, 11, { -10, -10, -10, -10, -10, -10, -10, -10, -10, -9, -7, -5, -3, -1, 0, 1, 3, 5, 7, 9, 10, 11, 13, 14, 15, 17, 18, 19, 19 } }, + { -10, 15, { -14, -14, -14, -14, -14, -14, -13, -11, -10, -9, -7, -5, -3, -1, 0, 1, 3, 5, 7, 9, 10, 11, 13, 14, 15, 17, 18, 19, 19 } }, + { -10, 20, { -19, -19, -18, -17, -15, -14, -13, -11, -10, -9, -7, -5, -3, -1, 0, 1, 3, 5, 7, 9, 10, 11, 13, 14, 15, 17, 18, 19, 19 } }, + + // Shifts{ -20, -19, -18, -17, -15, -14, -13, -11, -10, -9, -7, -5, -3, -1, 0, 1, 3, 5, 7, 9, 10, 11, 13, 14, 15, 17, 18, 19, 20 } + { -5, -1, { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 4, 6, 8, 10, 11, 12, 14, 14, 14, 14, 14, 14, 14 } }, + { -5, 0, { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 3, 5, 7, 9, 10, 11, 13, 14, 14, 14, 14, 14, 14 } }, + { -5, 1, { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 3, 5, 7, 9, 10, 11, 13, 14, 14, 14, 14, 14, 14 } }, + { -5, 5, { -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -3, -1, 0, 1, 3, 5, 7, 9, 10, 11, 13, 14, 14, 14, 14, 14, 14 } }, + { -5, 9, { -8, -8, -8, -8, -8, -8, -8, -8, -8, -8, -7, -5, -3, -1, 0, 1, 3, 5, 7, 9, 10, 11, 13, 14, 14, 14, 14, 14, 14 } }, + { -5, 10, { -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -7, -5, -3, -1, 0, 1, 3, 5, 7, 9, 10, 11, 13, 14, 14, 14, 14, 14, 14 } }, + { -5, 11, { -10, -10, -10, -10, -10, -10, -10, -10, -10, -9, -7, -5, -3, -1, 0, 1, 3, 5, 7, 9, 10, 11, 13, 14, 14, 14, 14, 14, 14 } }, + { -5, 15, { -14, -14, -14, -14, -14, -14, -13, -11, -10, -9, -7, -5, -3, -1, 0, 1, 3, 5, 7, 9, 10, 11, 13, 14, 14, 14, 14, 14, 14 } }, + { -5, 20, { -19, -19, -18, -17, -15, -14, -13, -11, -10, -9, -7, -5, -3, -1, 0, 1, 3, 5, 7, 9, 10, 11, 13, 14, 14, 14, 14, 14, 14 } }, + + // Shifts{ -20, -19, -18, -17, -15, -14, -13, -11, -10, -9, -7, -5, -3, -1, 0, 1, 3, 5, 7, 9, 10, 11, 13, 14, 15, 17, 18, 19, 20 } + { -1, 0, { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 3, 5, 7, 9, 10, 10, 10, 10, 10, 10, 10, 10, 10 } }, + { -1, 1, { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 3, 5, 7, 9, 10, 10, 10, 10, 10, 10, 10, 10, 10 } }, + { -1, 5, { -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -3, -1, 0, 1, 3, 5, 7, 9, 10, 10, 10, 10, 10, 10, 10, 10, 10 } }, + { -1, 9, { -8, -8, -8, -8, -8, -8, -8, -8, -8, -8, -7, -5, -3, -1, 0, 1, 3, 5, 7, 9, 10, 10, 10, 10, 10, 10, 10, 10, 10 } }, + { -1, 10, { -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -7, -5, -3, -1, 0, 1, 3, 5, 7, 9, 10, 10, 10, 10, 10, 10, 10, 10, 10 } }, + { -1, 11, { -10, -10, -10, -10, -10, -10, -10, -10, -10, -9, -7, -5, -3, -1, 0, 1, 3, 5, 7, 9, 10, 10, 10, 10, 10, 10, 10, 10, 10 } }, + { -1, 15, { -14, -14, -14, -14, -14, -14, -13, -11, -10, -9, -7, -5, -3, -1, 0, 1, 3, 5, 7, 9, 10, 10, 10, 10, 10, 10, 10, 10, 10 } }, + { -1, 20, { -19, -19, -18, -17, -15, -14, -13, -11, -10, -9, -7, -5, -3, -1, 0, 1, 3, 5, 7, 9, 10, 10, 10, 10, 10, 10, 10, 10, 10 } }, + + // Shifts{ -20, -19, -18, -17, -15, -14, -13, -11, -10, -9, -7, -5, -3, -1, 0, 1, 3, 5, 7, 9, 10, 11, 13, 14, 15, 17, 18, 19, 20 } + { 0, 1, { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 3, 5, 7, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9 } }, + { 0, 5, { -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -3, -1, 0, 1, 3, 5, 7, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9 } }, + { 0, 9, { -8, -8, -8, -8, -8, -8, -8, -8, -8, -8, -7, -5, -3, -1, 0, 1, 3, 5, 7, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9 } }, + { 0, 10, { -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -7, -5, -3, -1, 0, 1, 3, 5, 7, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9 } }, + { 0, 11, { -10, -10, -10, -10, -10, -10, -10, -10, -10, -9, -7, -5, -3, -1, 0, 1, 3, 5, 7, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9 } }, + { 0, 15, { -14, -14, -14, -14, -14, -14, -13, -11, -10, -9, -7, -5, -3, -1, 0, 1, 3, 5, 7, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9 } }, + { 0, 20, { -19, -19, -18, -17, -15, -14, -13, -11, -10, -9, -7, -5, -3, -1, 0, 1, 3, 5, 7, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9 } }, + + // Shifts{ -20, -19, -18, -17, -15, -14, -13, -11, -10, -9, -7, -5, -3, -1, 0, 1, 3, 5, 7, 9, 10, 11, 13, 14, 15, 17, 18, 19, 20 } + { 1, 5, { -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -3, -1, 0, 1, 3, 5, 7, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8 } }, + { 1, 9, { -8, -8, -8, -8, -8, -8, -8, -8, -8, -8, -7, -5, -3, -1, 0, 1, 3, 5, 7, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8 } }, + { 1, 10, { -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -7, -5, -3, -1, 0, 1, 3, 5, 7, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8 } }, + { 1, 11, { -10, -10, -10, -10, -10, -10, -10, -10, -10, -9, -7, -5, -3, -1, 0, 1, 3, 5, 7, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8 } }, + { 1, 15, { -14, -14, -14, -14, -14, -14, -13, -11, -10, -9, -7, -5, -3, -1, 0, 1, 3, 5, 7, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8 } }, + { 1, 20, { -19, -19, -18, -17, -15, -14, -13, -11, -10, -9, -7, -5, -3, -1, 0, 1, 3, 5, 7, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8 } }, + + // Shifts{ -20, -19, -18, -17, -15, -14, -13, -11, -10, -9, -7, -5, -3, -1, 0, 1, 3, 5, 7, 9, 10, 11, 13, 14, 15, 17, 18, 19, 20 } + { 5, 9, { -8, -8, -8, -8, -8, -8, -8, -8, -8, -8, -7, -5, -3, -1, 0, 1, 3, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4 } }, + { 5, 10, { -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -7, -5, -3, -1, 0, 1, 3, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4 } }, + { 5, 11, { -10, -10, -10, -10, -10, -10, -10, -10, -10, -9, -7, -5, -3, -1, 0, 1, 3, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4 } }, + { 5, 15, { -14, -14, -14, -14, -14, -14, -13, -11, -10, -9, -7, -5, -3, -1, 0, 1, 3, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4 } }, + { 5, 20, { -19, -19, -18, -17, -15, -14, -13, -11, -10, -9, -7, -5, -3, -1, 0, 1, 3, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4 } }, + + // Shifts{ -20, -19, -18, -17, -15, -14, -13, -11, -10, -9, -7, -5, -3, -1, 0, 1, 3, 5, 7, 9, 10, 11, 13, 14, 15, 17, 18, 19, 20 } + { 9, 10, { -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -7, -5, -3, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } }, + { 9, 11, { -10, -10, -10, -10, -10, -10, -10, -10, -10, -9, -7, -5, -3, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } }, + { 9, 15, { -14, -14, -14, -14, -14, -14, -13, -11, -10, -9, -7, -5, -3, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } }, + { 9, 20, { -19, -19, -18, -17, -15, -14, -13, -11, -10, -9, -7, -5, -3, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } }, + + // Shifts{ -20, -19, -18, -17, -15, -14, -13, -11, -10, -9, -7, -5, -3, -1, 0, 1, 3, 5, 7, 9, 10, 11, 13, 14, 15, 17, 18, 19, 20 } + { 10, 11, { -10, -10, -10, -10, -10, -10, -10, -10, -10, -9, -7, -5, -3, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } }, + { 10, 15, { -14, -14, -14, -14, -14, -14, -13, -11, -10, -9, -7, -5, -3, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } }, + { 10, 20, { -19, -19, -18, -17, -15, -14, -13, -11, -10, -9, -7, -5, -3, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } }, + + // Shifts{ -20, -19, -18, -17, -15, -14, -13, -11, -10, -9, -7, -5, -3, -1, 0, 1, 3, 5, 7, 9, 10, 11, 13, 14, 15, 17, 18, 19, 20 } + { 11, 15, { -14, -14, -14, -14, -14, -14, -14, -12, -11, -10, -8, -6, -4, -2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } }, + { 11, 20, { -19, -19, -19, -18, -16, -15, -14, -12, -11, -10, -8, -6, -4, -2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } }, + + // Shifts{ -20, -19, -18, -17, -15, -14, -13, -11, -10, -9, -7, -5, -3, -1, 0, 1, 3, 5, 7, 9, 10, 11, 13, 14, 15, 17, 18, 19, 20 } + { 15, 20, { -19, -19, -19, -19, -19, -19, -18, -16, -15, -14, -12, -10, -8, -6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } }, + }; + + for (const auto& TestDataPoint : TestDataPoints) + { + for (const auto& I : std::views::iota(0uz, Shifts.size())) + { + REQUIRE(TestDataPoint.Expected[I] == adjust_hpos_shift(Shifts[I], TestDataPoint.Left, TestDataPoint.Right, TextAreaWidth)); + } + } +} + #endif diff --git a/far/vmenu.hpp b/far/vmenu.hpp index afa8b70f20b..cc54d282111 100644 --- a/far/vmenu.hpp +++ b/far/vmenu.hpp @@ -60,28 +60,29 @@ using vmenu_colors_t = std::array; enum VMENU_FLAGS { - VMENU_NONE = 0, - VMENU_ALWAYSSCROLLBAR = 8_bit, // всегда показывать скроллбар - VMENU_LISTBOX = 9_bit, // Это список в диалоге - VMENU_SHOWNOBOX = 10_bit, // показать без рамки - VMENU_AUTOHIGHLIGHT = 11_bit, // автоматически выбирать симолы подсветки - VMENU_REVERSEHIGHLIGHT = 12_bit, // ... только с конца - VMENU_UPDATEREQUIRED = 13_bit, // лист необходимо обновить (перерисовать) - VMENU_DISABLEDRAWBACKGROUND= 14_bit, // подложку не рисовать - VMENU_WRAPMODE = 15_bit, // зацикленный список (при перемещении) - VMENU_SHOWAMPERSAND = 16_bit, // символ '&' показывать AS IS - VMENU_WARNDIALOG = 17_bit, // - VMENU_LISTHASFOCUS = 21_bit, // меню является списком в диалоге и имеет фокус - VMENU_COMBOBOX = 22_bit, // меню является комбобоксом и обрабатывается менеджером по-особому. - VMENU_MOUSEDOWN = 23_bit, // - VMENU_CHANGECONSOLETITLE = 24_bit, // - VMENU_MOUSEREACTION = 25_bit, // реагировать на движение мыши? (перемещать позицию при перемещении курсора мыши?) - VMENU_DISABLED = 26_bit, // - VMENU_NOMERGEBORDER = 27_bit, // - VMENU_REFILTERREQUIRED = 28_bit, // перед отрисовкой необходимо обновить фильтр - VMENU_LISTSINGLEBOX = 29_bit, // список, всегда с одинарной рамкой - VMENU_COMBOBOXEVENTKEY = 30_bit, // посылать события клавиатуры в диалоговую проц. для открытого комбобокса - VMENU_COMBOBOXEVENTMOUSE = 31_bit, // посылать события мыши в диалоговую проц. для открытого комбобокса + VMENU_NONE = 0, + VMENU_ALWAYSSCROLLBAR = 8_bit, // всегда показывать скроллбар + VMENU_LISTBOX = 9_bit, // Это список в диалоге + VMENU_SHOWNOBOX = 10_bit, // показать без рамки + VMENU_AUTOHIGHLIGHT = 11_bit, // автоматически выбирать симолы подсветки + VMENU_REVERSEHIGHLIGHT = 12_bit, // ... только с конца + VMENU_UPDATEREQUIRED = 13_bit, // лист необходимо обновить (перерисовать) + VMENU_DISABLEDRAWBACKGROUND = 14_bit, // подложку не рисовать + VMENU_WRAPMODE = 15_bit, // зацикленный список (при перемещении) + VMENU_SHOWAMPERSAND = 16_bit, // символ '&' показывать AS IS + VMENU_WARNDIALOG = 17_bit, // + VMENU_ENABLEALIGNANNOTATIONS = 18_bit, // Enable vertical alignment of item annotations and HscrollEnBlocMode + VMENU_LISTHASFOCUS = 21_bit, // меню является списком в диалоге и имеет фокус + VMENU_COMBOBOX = 22_bit, // меню является комбобоксом и обрабатывается менеджером по-особому. + VMENU_MOUSEDOWN = 23_bit, // + VMENU_CHANGECONSOLETITLE = 24_bit, // + VMENU_MOUSEREACTION = 25_bit, // реагировать на движение мыши? (перемещать позицию при перемещении курсора мыши?) + VMENU_DISABLED = 26_bit, // + VMENU_NOMERGEBORDER = 27_bit, // + VMENU_REFILTERREQUIRED = 28_bit, // перед отрисовкой необходимо обновить фильтр + VMENU_LISTSINGLEBOX = 29_bit, // список, всегда с одинарной рамкой + VMENU_COMBOBOXEVENTKEY = 30_bit, // посылать события клавиатуры в диалоговую проц. для открытого комбобокса + VMENU_COMBOBOXEVENTMOUSE = 31_bit, // посылать события мыши в диалоговую проц. для открытого комбобокса }; struct menu_item @@ -142,11 +143,9 @@ struct MenuItemEx: menu_item std::any ComplexUserData; intptr_t SimpleUserData{}; - size_t ShowPos{}; + int HPos{}; // Positive: Indent; Negative: Hanging wchar_t AutoHotkey{}; size_t AutoHotkeyPos{}; - short Len[2]{}; // размеры 2-х частей - short Idx2{}; // начало 2-й части std::list> Annotations; }; @@ -294,15 +293,19 @@ class VMenu final: public Modal bool CheckKeyHiOrAcc(DWORD Key, int Type, bool Translate, bool ChangePos, int& NewPos); int CheckHighlights(wchar_t CheckSymbol,int StartPos=0) const; wchar_t GetHighlights(const MenuItemEx *Item) const; - size_t GetItemMaxShowPos(int Item, size_t MaxLineWidth) const; - // Negative NewShowPos is relative to the right side; -1 aligns the item to the right - bool SetItemShowPos(int Item, int NewShowPos, size_t MaxLineWidth); - // Shifts item's ShowPos; if Shift is positive, the item visually moves left - bool ShiftItemShowPos(int Item,int Shift, size_t MaxLineWidth); - bool SetAllItemsShowPos(int NewShowPos); - bool ShiftAllItemsShowPos(int Shift); + + [[nodiscard]] bool SetItemHPos(MenuItemEx& Item, auto GetNewHPos, int TextAreaWidth); + [[nodiscard]] bool SetCurItemSmartHPos(int NewHPos); + [[nodiscard]] bool ShiftCurItemHPos(int Shift); + [[nodiscard]] bool SetAllItemsHPos(auto&& GetNewHPos, int TextAreaWidth); + [[nodiscard]] bool SetAllItemsSmartHPos(int NewHPos); + [[nodiscard]] bool ShiftAllItemsHPos(int Shift); + [[nodiscard]] bool AlignAnnotations(); + void UpdateMaxLengthFromTitles(); - void UpdateMaxLength(size_t Length); + void UpdateMaxLength(int ItemLength); + void ResetAllItemsBoundaries(); + void UpdateAllItemsBoundaries(int ItemHPos, int ItemLength); bool ShouldSendKeyToFilter(unsigned Key) const; //корректировка текущей позиции и флагов SELECTED void UpdateSelectPos(); @@ -318,7 +321,8 @@ class VMenu final: public Modal int TopPos{}; int MaxHeight; bool WasAutoHeight{}; - size_t m_MaxItemLength{}; + int m_MaxItemLength{}; + std::pair m_AllItemsBoundaries{}; window_ptr CurrentWindow; bool PrevCursorVisible{}; size_t PrevCursorSize{}; diff --git a/far/vmenu2.cpp b/far/vmenu2.cpp index a238bee48f4..01177b7b38c 100644 --- a/far/vmenu2.cpp +++ b/far/vmenu2.cpp @@ -416,7 +416,7 @@ void VMenu2::SetMenuFlags(DWORD Flags) if (Flags&VMENU_NOMERGEBORDER) fdi.Flags|=DIF_LISTNOMERGEBORDER; - ListBox().SetMenuFlags(Flags & (VMENU_REVERSEHIGHLIGHT | VMENU_LISTSINGLEBOX)); + ListBox().SetMenuFlags(Flags & (VMENU_REVERSEHIGHLIGHT | VMENU_LISTSINGLEBOX | VMENU_ENABLEALIGNANNOTATIONS)); SendMessage(DM_SETDLGITEMSHORT, 0, &fdi); }