From ddec86c8bdc76e33cece9891b55150a007a63a8b 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 | 18 ++ far/FarEng.hlf.m4 | 18 ++ far/FarGer.hlf.m4 | 18 ++ far/FarHun.hlf.m4 | 18 ++ far/FarPol.hlf.m4 | 18 ++ far/FarRus.hlf.m4 | 18 ++ far/FarSky.hlf.m4 | 18 ++ far/FarUkr.hlf.m4 | 18 ++ far/changelog | 10 + far/config.cpp | 1 + far/config.hpp | 1 + far/dialog.cpp | 1 + far/editor.cpp | 2 +- far/vbuild.m4 | 2 +- far/vmenu.cpp | 522 ++++++++++++++++++++++++++++++++++++---------- far/vmenu.hpp | 72 ++++--- far/vmenu2.cpp | 2 +- 17 files changed, 608 insertions(+), 149 deletions(-) diff --git a/far/FarCze.hlf.m4 b/far/FarCze.hlf.m4 index ff4993bacda..4936868b30b 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. @@ -6662,6 +6665,21 @@ or the search starting point. This parameter can be changed via ~far:config~@FarConfig@ only. +@VMenu.SwapHScrollDirection +$ #far:config VMenu.SwapHScrollDirection# + This Boolean parameter swaps the direction of ~horizontal scrolling~@MenuCmd@ +of menu and list box items. + + False - ^#Left# arrow scrolls items to the right, #Right# arrow scrolls items to the left. +In other words, these keys control the menu window. + True - #Left# arrow scrolls items to the left, #Right# arrow scrolls items to the right. +I.e., these keys control the menu items. + + Default value: False (arrow keys control the menu window). + + This parameter can be changed via ~far:config~@FarConfig@ only. + + @XLat.Layouts $ #far:config XLat.Layouts# This string parameter defines the input locales (keyboard layouts) diff --git a/far/FarEng.hlf.m4 b/far/FarEng.hlf.m4 index b80b2c6c1f5..54926a2f3f0 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. @@ -6629,6 +6632,21 @@ or the search starting point. This parameter can be changed via ~far:config~@FarConfig@ only. +@VMenu.SwapHScrollDirection +$ #far:config VMenu.SwapHScrollDirection# + This Boolean parameter swaps the direction of ~horizontal scrolling~@MenuCmd@ +of menu and list box items. + + False - ^#Left# arrow scrolls items to the right, #Right# arrow scrolls items to the left. +In other words, these keys control the menu window. + True - #Left# arrow scrolls items to the left, #Right# arrow scrolls items to the right. +I.e., these keys control the menu items. + + Default value: False (arrow keys control the menu window). + + This parameter can be changed via ~far:config~@FarConfig@ only. + + @XLat.Layouts $ #far:config XLat.Layouts# This string parameter defines the input locales (keyboard layouts) diff --git a/far/FarGer.hlf.m4 b/far/FarGer.hlf.m4 index 44a91faf90f..8a7f2b697a7 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. @@ -6720,6 +6723,21 @@ or the search starting point. This parameter can be changed via ~far:config~@FarConfig@ only. +@VMenu.SwapHScrollDirection +$ #far:config VMenu.SwapHScrollDirection# + This Boolean parameter swaps the direction of ~horizontal scrolling~@MenuCmd@ +of menu and list box items. + + False - ^#Left# arrow scrolls items to the right, #Right# arrow scrolls items to the left. +In other words, these keys control the menu window. + True - #Left# arrow scrolls items to the left, #Right# arrow scrolls items to the right. +I.e., these keys control the menu items. + + Default value: False (arrow keys control the menu window). + + This parameter can be changed via ~far:config~@FarConfig@ only. + + @XLat.Layouts $ #far:config XLat.Layouts# This string parameter defines the input locales (keyboard layouts) diff --git a/far/FarHun.hlf.m4 b/far/FarHun.hlf.m4 index 566a561dec3..2b853ce2727 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. @@ -6740,6 +6743,21 @@ or the search starting point. This parameter can be changed via ~far:config~@FarConfig@ only. +@VMenu.SwapHScrollDirection +$ #far:config VMenu.SwapHScrollDirection# + This Boolean parameter swaps the direction of ~horizontal scrolling~@MenuCmd@ +of menu and list box items. + + False - ^#Left# arrow scrolls items to the right, #Right# arrow scrolls items to the left. +In other words, these keys control the menu window. + True - #Left# arrow scrolls items to the left, #Right# arrow scrolls items to the right. +I.e., these keys control the menu items. + + Default value: False (arrow keys control the menu window). + + This parameter can be changed via ~far:config~@FarConfig@ only. + + @XLat.Layouts $ #far:config XLat.Layouts# This string parameter defines the input locales (keyboard layouts) diff --git a/far/FarPol.hlf.m4 b/far/FarPol.hlf.m4 index 7fac149ac53..1fad142eef6 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. @@ -6639,6 +6642,21 @@ lub osiągnie początkowy punkt wyszukiwania. Ten parametr można zmienić tylko w ~far:config~@FarConfig@. +@VMenu.SwapHScrollDirection +$ #far:config VMenu.SwapHScrollDirection# + This Boolean parameter swaps the direction of ~horizontal scrolling~@MenuCmd@ +of menu and list box items. + + False - ^#Left# arrow scrolls items to the right, #Right# arrow scrolls items to the left. +In other words, these keys control the menu window. + True - #Left# arrow scrolls items to the left, #Right# arrow scrolls items to the right. +I.e., these keys control the menu items. + + Default value: False (arrow keys control the menu window). + + This parameter can be changed via ~far:config~@FarConfig@ only. + + @XLat.Layouts $ #far:config XLat.Layouts# Ten parametr tekstowy definiuje języki wejściowe (układy klawiatury), diff --git a/far/FarRus.hlf.m4 b/far/FarRus.hlf.m4 index 9c2de67612b..1b3249ee523 100644 --- a/far/FarRus.hlf.m4 +++ b/far/FarRus.hlf.m4 @@ -3506,6 +3506,9 @@ $ #Редактор: Меню результатов поиска всех вх #Ctrl+Enter#, #Ctrl+Щелчок левой кнопки мыши# Перейти к позиции найденного текста. + #Ctrl+Numpad5# + Выровнять все вхождения по вертикали. + #Gray +# Добавить сеансовую закладку с текущей позицией. @@ -6737,6 +6740,21 @@ $ #far:config Viewer.SearchWrapStop# Изменить этот параметр можно только через ~far:config~@FarConfig@. +@VMenu.SwapHScrollDirection +$ #far:config VMenu.SwapHScrollDirection# + Этот логический (Boolean) параметр меняет направление ~горизонтального перемещения~@MenuCmd@ +элементов меню и списков. + + False - ^Стрелка влево перемещает элементы вправо. Стрелка вправо перемещает элементы влево. +Другими словами, эти клавиши управляют окном меню. + True - Стрелка влево перемещает элементы влево. Стрелка вправо перемещает элементы вправо. +То есть, эти клавиши управляют элементами меню. + + Значение по умолчанию: False (Стрелки управляют окном меню). + + Изменить этот параметр можно только через ~far:config~@FarConfig@. + + @XLat.Layouts $ #far:config XLat.Layouts# Этот строковый параметр определят языки ввода (раскладки клавиатуры), diff --git a/far/FarSky.hlf.m4 b/far/FarSky.hlf.m4 index cbde26530d0..67fe86ebcd0 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. @@ -6623,6 +6626,21 @@ or the search starting point. This parameter can be changed via ~far:config~@FarConfig@ only. +@VMenu.SwapHScrollDirection +$ #far:config VMenu.SwapHScrollDirection# + This Boolean parameter swaps the direction of ~horizontal scrolling~@MenuCmd@ +of menu and list box items. + + False - ^#Left# arrow scrolls items to the right, #Right# arrow scrolls items to the left. +In other words, these keys control the menu window. + True - #Left# arrow scrolls items to the left, #Right# arrow scrolls items to the right. +I.e., these keys control the menu items. + + Default value: False (arrow keys control the menu window). + + This parameter can be changed via ~far:config~@FarConfig@ only. + + @XLat.Layouts $ #far:config XLat.Layouts# This string parameter defines the input locales (keyboard layouts) diff --git a/far/FarUkr.hlf.m4 b/far/FarUkr.hlf.m4 index cb7df1f8130..d491e3bf3cc 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. @@ -6711,6 +6714,21 @@ or the search starting point. This parameter can be changed via ~far:config~@FarConfig@ only. +@VMenu.SwapHScrollDirection +$ #far:config VMenu.SwapHScrollDirection# + This Boolean parameter swaps the direction of ~horizontal scrolling~@MenuCmd@ +of menu and list box items. + + False - ^#Left# arrow scrolls items to the right, #Right# arrow scrolls items to the left. +In other words, these keys control the menu window. + True - #Left# arrow scrolls items to the left, #Right# arrow scrolls items to the right. +I.e., these keys control the menu items. + + Default value: False (arrow keys control the menu window). + + This parameter can be changed via ~far:config~@FarConfig@ only. + + @XLat.Layouts $ #far:config XLat.Layouts# This string parameter defines the input locales (keyboard layouts) diff --git a/far/changelog b/far/changelog index bed1834c276..7cb58f2cde8 100644 --- a/far/changelog +++ b/far/changelog @@ -1,3 +1,13 @@ +-------------------------------------------------------------------------------- +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. + +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. + -------------------------------------------------------------------------------- drkns 2024-02-03 16:21:32+00:00 - build 6267 diff --git a/far/config.cpp b/far/config.cpp index 8372cdf7a26..a9a0c769b48 100644 --- a/far/config.cpp +++ b/far/config.cpp @@ -2111,6 +2111,7 @@ void Options::InitConfigsData() {FSSF_PRIVATE, NKeyVMenu, L"LBtnClick"sv, VMenu.LBtnClick, VMENUCLICK_CANCEL}, {FSSF_PRIVATE, NKeyVMenu, L"MBtnClick"sv, VMenu.MBtnClick, VMENUCLICK_APPLY}, {FSSF_PRIVATE, NKeyVMenu, L"RBtnClick"sv, VMenu.RBtnClick, VMENUCLICK_CANCEL}, + {FSSF_PRIVATE, NKeyVMenu, L"SwapHScrollDirection"sv, VMenu.SwapHScrollDirection, false}, {FSSF_PRIVATE, NKeyXLat, L"Flags"sv, XLat.Flags, XLAT_SWITCHKEYBLAYOUT|XLAT_CONVERTALLCMDLINE}, {FSSF_PRIVATE, NKeyXLat, L"Layouts"sv, XLat.strLayouts, L""sv}, {FSSF_PRIVATE, NKeyXLat, L"Rules1"sv, XLat.Rules[0], L""sv}, diff --git a/far/config.hpp b/far/config.hpp index 4a6ee96be24..342722e584e 100644 --- a/far/config.hpp +++ b/far/config.hpp @@ -634,6 +634,7 @@ class Options: noncopyable IntOption LBtnClick; IntOption RBtnClick; IntOption MBtnClick; + BoolOption SwapHScrollDirection; }; struct CommandLineOptions 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..7692eab2afe 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,19 @@ 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) == Global->Opt->VMenu.SwapHScrollDirection + ? -1 + : 1; + }; + switch (LocalKey) { case KEY_ALTF9: @@ -1517,7 +1597,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 +1605,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 +1615,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 +1625,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 +1635,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 +1643,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 +2099,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 +2227,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 +2448,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 +2463,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 +2476,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 +2486,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 +2609,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 +2675,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 +2762,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 +2813,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 +2860,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 +2869,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 +3061,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 +3243,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 +3367,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 +3411,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); }