Skip to content

Commit

Permalink
FarGroupgh-750: Fixed VMenu set selection behavior around list edges.
Browse files Browse the repository at this point in the history
Warning! Bugs are expected.
  • Loading branch information
MKadaner committed Nov 27, 2023
1 parent e019233 commit 0518ebb
Show file tree
Hide file tree
Showing 4 changed files with 110 additions and 68 deletions.
6 changes: 6 additions & 0 deletions far/changelog
Original file line number Diff line number Diff line change
@@ -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

Expand Down
1 change: 1 addition & 0 deletions far/headers.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#include <mutex>
#include <numeric>
#include <optional>
#include <ranges>
#include <random>
#include <regex>
#include <set>
Expand Down
2 changes: 1 addition & 1 deletion far/vbuild.m4
Original file line number Diff line number Diff line change
@@ -1 +1 @@
6218
6219
169 changes: 102 additions & 67 deletions far/vmenu.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -140,15 +140,45 @@ void VMenu::ResetCursor()
GetCursorType(PrevCursorVisible,PrevCursorSize);
}

template <typename coll, typename pred>
int FindNearestSelectableItem(const coll& Coll, const int Pos, pred&& Pred, const bool GoBackward, const bool DoWrap)
{
assert(0 <= Pos && Pos < static_cast<int>(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<int>(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);
}

//может быть выбран
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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<int>(Items.size()-1);
TopPos = Pos;
}
else
{
Pos = 0;
TopPos = 0;
Pass++;
}
}
else if (Pos>=static_cast<int>(Items.size()))
{
if (CheckFlags(VMENU_WRAPMODE))
{
Pos = 0;
TopPos = 0;
}
else
{
Pos = static_cast<int>(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<int>(Items.size()) };

if (I>=static_cast<int>(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))
{
Expand All @@ -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;
}

Expand Down Expand Up @@ -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)
{
Expand Down Expand Up @@ -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<int>(Items.size()-1))
{
FarListPos Pos{ sizeof(Pos), SelectPos + 1, TopPos };
const auto ItemsSize = static_cast<int>(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;
}

Expand Down Expand Up @@ -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<int, 10> 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<int>(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<int>(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

0 comments on commit 0518ebb

Please sign in to comment.