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); }