diff --git a/far/changelog b/far/changelog index 3f538d8628a..e79b8780296 100644 --- a/far/changelog +++ b/far/changelog @@ -1,3 +1,9 @@ +-------------------------------------------------------------------------------- +MZK 2023-11-26 18:50:33-08:00 - build 6219 + +1. gh-750: Fixed VMenu set selection behavior around list edges. + Warning! Bugs are expected. + -------------------------------------------------------------------------------- drkns 2023-11-26 15:22:17+00:00 - build 6218 diff --git a/far/headers.hpp b/far/headers.hpp index 81813217bc6..63ab5bb9cb5 100644 --- a/far/headers.hpp +++ b/far/headers.hpp @@ -82,6 +82,7 @@ THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include #include #include +#include #include #include #include diff --git a/far/vbuild.m4 b/far/vbuild.m4 index db4950bc294..0c6cff2b0fd 100644 --- a/far/vbuild.m4 +++ b/far/vbuild.m4 @@ -1 +1 @@ -6218 +6219 diff --git a/far/vmenu.cpp b/far/vmenu.cpp index 1ae315f49b9..cee2ddbb495 100644 --- a/far/vmenu.cpp +++ b/far/vmenu.cpp @@ -140,15 +140,45 @@ void VMenu::ResetCursor() GetCursorType(PrevCursorVisible,PrevCursorSize); } +template +int FindNearestSelectableItem(const coll& Coll, const int Pos, pred&& Pred, const bool GoBackward, const bool DoWrap) +{ + assert(0 <= Pos && Pos < static_cast(Coll.size())); + + const auto FindPos{ + [&](const auto First, const auto Second) + { + const auto FindPos{ + [&](const auto Part) + { + const auto Found{ std::ranges::find_if(Part, Pred) }; + if (Found == std::ranges::end(Part)) return -1; + return static_cast(std::ranges::distance(&*std::ranges::begin(Coll), &*Found)); + } }; + + if (const auto Found{ FindPos(Coll | First) }; Found != -1) return Found; + if (const auto Found{ FindPos(Coll | Second) }; Found != -1) return Found; + return -1; + } }; + + return GoBackward + ? (DoWrap + ? FindPos(std::views::take(Pos + 1) | std::views::reverse, std::views::drop(Pos + 1) | std::views::reverse) + : FindPos(std::views::take(Pos + 1) | std::views::reverse, std::views::drop(Pos + 1))) + : (DoWrap + ? FindPos(std::views::drop(Pos), std::views::take(Pos)) + : FindPos(std::views::drop(Pos), std::views::take(Pos) | std::views::reverse)); +} + //может иметь фокус -static bool ItemCanHaveFocus(unsigned long long const Flags) +static bool ItemCanHaveFocusFlags(unsigned long long const Flags) { return !(Flags & (LIF_DISABLE | LIF_HIDDEN | LIF_FILTERED | LIF_SEPARATOR)); } static bool ItemCanHaveFocus(MenuItemEx const& Item) { - return ItemCanHaveFocus(Item.Flags); + return ItemCanHaveFocusFlags(Item.Flags); } //может быть выбран @@ -177,7 +207,7 @@ void VMenu::UpdateItemFlags(int Pos, unsigned long long NewFlags) --ItemHiddenCount; - if (!ItemCanHaveFocus(NewFlags)) + if (!ItemCanHaveFocusFlags(NewFlags)) NewFlags &= ~LIF_SELECTED; //remove selection @@ -228,54 +258,20 @@ int VMenu::SetSelectPos(int Pos, int Direct, bool stop_on_edge) i.Flags &= ~LIF_SELECTED; } - for (int Pass=0, I=0;;I++) - { - if (Pos<0) - { - if (CheckFlags(VMENU_WRAPMODE)) - { - Pos = static_cast(Items.size()-1); - TopPos = Pos; - } - else - { - Pos = 0; - TopPos = 0; - Pass++; - } - } - else if (Pos>=static_cast(Items.size())) - { - if (CheckFlags(VMENU_WRAPMODE)) - { - Pos = 0; - TopPos = 0; - } - else - { - Pos = static_cast(Items.size()-1); - Pass++; - } - } - - if (ItemCanHaveFocus(Items[Pos])) - break; - - if (Pass) - { - Pos = SelectPos; - break; - } - - Pos += Direct; + const auto DoWrap{ CheckFlags(VMENU_WRAPMODE) && Direct != 0 && !stop_on_edge }; + const auto GoBackward{ Direct < 0 }; + const auto ItemsSize{ static_cast(Items.size()) }; - if (I>=static_cast(Items.size())) // круг пройден - ничего не найдено :-( - Pass++; + if (Pos < 0) + { + Pos = DoWrap ? ItemsSize - 1 : 0; + } + else if (Pos >= ItemsSize) + { + Pos = DoWrap ? 0 : ItemsSize - 1; } - if (stop_on_edge && CheckFlags(VMENU_WRAPMODE) && ((Direct > 0 && Pos < SelectPos) || (Direct<0 && Pos>SelectPos))) - Pos = SelectPos; - + Pos = FindNearestSelectableItem(Items, Pos, ItemCanHaveFocus, GoBackward, DoWrap); if (Pos != SelectPos && CheckFlags(VMENU_COMBOBOX | VMENU_LISTBOX)) { @@ -287,11 +283,11 @@ int VMenu::SetSelectPos(int Pos, int Direct, bool stop_on_edge) } if (Pos >= 0) - UpdateItemFlags(Pos, Items[Pos].Flags|LIF_SELECTED); + UpdateItemFlags(Pos, Items[Pos].Flags | LIF_SELECTED); SetMenuFlags(VMENU_UPDATEREQUIRED); - SelectPosResult=Pos; + SelectPosResult = Pos; return Pos; } @@ -523,7 +519,7 @@ int VMenu::DeleteItem(int ID, int Count) ID--; } SelectPos = -1; - SetSelectPos(ID,1); + SetSelectPos(ID, 0, true); } else if (SelectPos >= ID+Count) { @@ -1328,26 +1324,14 @@ bool VMenu::ProcessKey(const Manager::Key& Key) } case KEY_MSWHEEL_UP: { - if(SelectPos) - { - FarListPos Pos{ sizeof(Pos), SelectPos - 1, TopPos - 1 }; - SetSelectPos(&Pos); - ShowMenu(true); - } + SetSelectPos(SelectPos - 1, -1, true); + ShowMenu(true); break; } case KEY_MSWHEEL_DOWN: { - if(SelectPos < static_cast(Items.size()-1)) - { - FarListPos Pos{ sizeof(Pos), SelectPos + 1, TopPos }; - const auto ItemsSize = static_cast(Items.size()); - const auto HeightSize = std::max(0, m_Where.height() - (m_BoxType == NO_BOX? 0 : 2)); - if (!(ItemsSize - TopPos <= HeightSize || ItemsSize <= HeightSize)) - Pos.TopPos++; - SetSelectPos(&Pos); - ShowMenu(true); - } + SetSelectPos(SelectPos + 1, 1, true); + ShowMenu(true); break; } @@ -2996,3 +2980,54 @@ size_t VMenu::Text(wchar_t const Char) const { return ::Text(Char, m_Where.width() - (WhereX() - m_Where.left)); } + +#ifdef ENABLE_TESTS + +#include "testing.hpp" + +TEST_CASE("find.nearest.selectable.item") +{ + std::array arr{}; + + const auto Pred{ [](const int b) { return b != 0; } }; + + const auto TestAllPositions{ + [&](const int Found) + { + for (const auto Pos : std::views::iota(0, static_cast(arr.size()))) + { + REQUIRE(FindNearestSelectableItem(arr, Pos, Pred, false, false) == Found); + REQUIRE(FindNearestSelectableItem(arr, Pos, Pred, false, true) == Found); + REQUIRE(FindNearestSelectableItem(arr, Pos, Pred, true, false) == Found); + REQUIRE(FindNearestSelectableItem(arr, Pos, Pred, true, true) == Found); + } + } }; + + TestAllPositions(-1); + + for (const auto Found : std::views::iota(0, static_cast(arr.size()))) + { + std::ranges::fill(arr, int{}); + arr[Found] = true; + TestAllPositions(Found); + } + + std::ranges::fill(arr, int{}); + arr[3] = arr[7] = true; + + REQUIRE(FindNearestSelectableItem(arr, 1, Pred, false, false) == 3); + REQUIRE(FindNearestSelectableItem(arr, 1, Pred, false, true) == 3); + REQUIRE(FindNearestSelectableItem(arr, 5, Pred, false, false) == 7); + REQUIRE(FindNearestSelectableItem(arr, 5, Pred, false, true) == 7); + REQUIRE(FindNearestSelectableItem(arr, 9, Pred, false, false) == 7); + REQUIRE(FindNearestSelectableItem(arr, 9, Pred, false, true) == 3); + + REQUIRE(FindNearestSelectableItem(arr, 1, Pred, true, false) == 3); + REQUIRE(FindNearestSelectableItem(arr, 1, Pred, true, true) == 7); + REQUIRE(FindNearestSelectableItem(arr, 5, Pred, true, false) == 3); + REQUIRE(FindNearestSelectableItem(arr, 5, Pred, true, true) == 3); + REQUIRE(FindNearestSelectableItem(arr, 9, Pred, true, false) == 7); + REQUIRE(FindNearestSelectableItem(arr, 9, Pred, true, true) == 7); +} + +#endif