From 780766a3b4db7c9f5c42a95ec2dc36b6c056ebb5 Mon Sep 17 00:00:00 2001 From: "Michael Z. Kadaner" Date: Mon, 11 Dec 2023 18:08:44 -0800 Subject: [PATCH] Refactoring `VMenu::ShowMenu` and its vicinity. --- far/dialog.cpp | 10 +- far/vmenu.cpp | 907 ++++++++++++++++++++++++++++--------------------- far/vmenu.hpp | 76 +++-- far/vmenu2.cpp | 2 +- 4 files changed, 572 insertions(+), 423 deletions(-) diff --git a/far/dialog.cpp b/far/dialog.cpp index 8fddb17f8be..7991b02fe55 100644 --- a/far/dialog.cpp +++ b/far/dialog.cpp @@ -770,7 +770,6 @@ void Dialog::InitDialogObjects(size_t ID) static_cast(m_Where.left + Item.X2), static_cast(m_Where.top + Item.Y2) }); - ListPtr->SetBoxType(SHORT_SINGLE_BOX); // поле FarDialogItem.Data для DI_LISTBOX используется как верхний заголовок листа if (!(Item.Flags & DIF_LISTNOBOX) && !DialogMode.Check(DMODE_OBJECTS_CREATED)) @@ -812,7 +811,6 @@ void Dialog::InitDialogObjects(size_t ID) { if (const auto& ListPtr = Item.ListPtr) { - ListPtr->SetBoxType(SHORT_SINGLE_BOX); DialogEdit->SetDropDownBox((Item.Flags& DIF_DROPDOWNLIST) != 0); ListPtr->ChangeFlags(VMENU_WRAPMODE, (Item.Flags& DIF_LISTWRAPMODE) != 0); ListPtr->ChangeFlags(VMENU_DISABLED, (Item.Flags& DIF_DISABLE) != 0); @@ -2034,8 +2032,8 @@ void Dialog::ShowDialog(size_t ID) if (Item.ListPtr) { // Перед отрисовкой спросим об изменении цветовых атрибутов - FarColor RealColors[VMENU_COLOR_COUNT]{}; - FarDialogItemColors ListColors{ sizeof(ListColors), 0, VMENU_COLOR_COUNT, RealColors }; + FarColor RealColors[std::to_underlying(vmenu_colors::COUNT)]{}; + FarDialogItemColors ListColors{ sizeof(ListColors), 0, std::to_underlying(vmenu_colors::COUNT), RealColors }; Item.ListPtr->GetColors(&ListColors); if (DlgProc(DN_CTLCOLORDLGLIST,I,&ListColors)) @@ -3867,8 +3865,8 @@ int Dialog::SelectFromComboBox(DialogItemEx& CurItem, DlgEdit& EditLine) DlgProc(DN_DROPDOWNOPENED, m_FocusPos, ToPtr(1)); SetComboBoxPos(&CurItem); // Перед отрисовкой спросим об изменении цветовых атрибутов - FarColor RealColors[VMENU_COLOR_COUNT]{}; - FarDialogItemColors ListColors{ sizeof(ListColors), 0, VMENU_COLOR_COUNT, RealColors }; + FarColor RealColors[std::to_underlying(vmenu_colors::COUNT)]{}; + FarDialogItemColors ListColors{ sizeof(ListColors), 0, std::to_underlying(vmenu_colors::COUNT), RealColors }; ComboBox->SetColors(nullptr); ComboBox->GetColors(&ListColors); diff --git a/far/vmenu.cpp b/far/vmenu.cpp index e3385f88fb7..da36d787cb1 100644 --- a/far/vmenu.cpp +++ b/far/vmenu.cpp @@ -52,7 +52,6 @@ THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include "ctrlobj.hpp" #include "manager.hpp" #include "constitle.hpp" -#include "interf.hpp" #include "colormix.hpp" #include "config.hpp" #include "processname.hpp" @@ -90,7 +89,6 @@ static MenuItemEx FarList2MenuItem(const FarListItem& FItem) VMenu::VMenu(private_tag, string Title, int MaxHeight, dialog_ptr ParentDialog): strTitle(std::move(Title)), MaxHeight(MaxHeight), - m_BoxType(DOUBLE_BOX), ParentDialog(ParentDialog), MenuId(FarUuid) { @@ -172,6 +170,47 @@ int find_nearest(std::ranges::contiguous_range auto const& Range, const int Pos, : FindPos(drop(Pos), take(Pos) | reverse)); } +template +std::pair Intersect(std::pair A, std::pair B) +{ + assert(A.first < A.second); + assert(B.first < B.second); + + if (B.first < A.first) + std::swap(A, B); + + if (A.second <= B.first) + return { {}, {} }; + + return { B.first, std::min(A.second, B.second) }; +} + +template +void MarkupSliceBoundaries(std::pair Segment, std::ranges::input_range auto const& Slices, std::weakly_incrementable auto Markup) +{ + assert(Segment.first < Segment.second); + + for (const auto Slice : Slices) + { + if (Slice.first >= Slice.second) + continue; + + const auto Intersection{ Intersect(Segment, Slice) }; + + if (Intersection.first == Intersection.second) + continue; + + *Markup = Intersection.first; ++Markup; + *Markup = Intersection.second; ++Markup; + Segment.first = Intersection.second; + + if (Segment.first == Segment.second) + return; + } + + *Markup = Segment.second; +} + //может иметь фокус static bool item_flags_allow_focus(unsigned long long const Flags) { @@ -857,7 +896,7 @@ long long VMenu::VMProcess(int OpCode, void* vParam, long long iParam) { SetSelectPos(I,1); - ShowMenu(true); + DrawMenu(); return GetVisualPos(SelectPos)+1; } @@ -1224,7 +1263,7 @@ bool VMenu::ProcessKey(const Manager::Key& Key) { FarListPos pos{ sizeof(pos), 0, -1 }; SetSelectPos(&pos, 1); - ShowMenu(true); + DrawMenu(); break; } case KEY_END: case KEY_NUMPAD1: @@ -1236,12 +1275,12 @@ bool VMenu::ProcessKey(const Manager::Key& Key) int p = static_cast(Items.size())-1; FarListPos pos{ sizeof(pos), p, std::max(0, p - MaxHeight + 1) }; SetSelectPos(&pos, -1); - ShowMenu(true); + DrawMenu(); break; } case KEY_PGUP: case KEY_NUMPAD9: { - const auto dy = m_Where.height() - (m_BoxType == NO_BOX? 1 : 2); + const auto dy = m_Where.height() - (CalculateBoxType() == NO_BOX? 1 : 2); int p = VisualPosToReal(GetVisualPos(SelectPos)-dy); @@ -1250,12 +1289,12 @@ bool VMenu::ProcessKey(const Manager::Key& Key) FarListPos pos{ sizeof(pos), p, p }; SetSelectPos(&pos, 1); - ShowMenu(true); + DrawMenu(); break; } case KEY_PGDN: case KEY_NUMPAD3: { - const auto dy = m_Where.height() - (m_BoxType == NO_BOX? 1 : 2); + const auto dy = m_Where.height() - (CalculateBoxType() == NO_BOX? 1 : 2); int pSel = VisualPosToReal(GetVisualPos(SelectPos)+dy); int pTop = VisualPosToReal(GetVisualPos(TopPos + 1)); @@ -1265,7 +1304,7 @@ bool VMenu::ProcessKey(const Manager::Key& Key) FarListPos pos{ sizeof(pos), pSel, pTop }; SetSelectPos(&pos, -1); - ShowMenu(true); + DrawMenu(); break; } case KEY_ALTHOME: case KEY_ALT|KEY_NUMPAD7: @@ -1274,15 +1313,15 @@ bool VMenu::ProcessKey(const Manager::Key& Key) case KEY_RALTEND: case KEY_RALT|KEY_NUMPAD1: { if (SetAllItemsShowPos(any_of(LocalKey & ~KEY_CTRLMASK, KEY_HOME, KEY_NUMPAD7)? 0 : -1)) - ShowMenu(true); + DrawMenu(); break; } 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)) - ShowMenu(true); + if (SetItemShowPos(SelectPos, any_of(LocalKey & ~KEY_CTRLMASK, KEY_HOME, KEY_NUMPAD7)? 0 : -1, CalculateMaxLineWidth())) + DrawMenu(); break; } @@ -1292,7 +1331,7 @@ bool VMenu::ProcessKey(const Manager::Key& Key) case KEY_RALTRIGHT: case KEY_RALT|KEY_NUMPAD6: { if (ShiftAllItemsShowPos(any_of(LocalKey & ~KEY_CTRLMASK, KEY_LEFT, KEY_NUMPAD4, KEY_MSWHEEL_LEFT)? -1 : 1)) - ShowMenu(true); + DrawMenu(); break; } @@ -1302,7 +1341,7 @@ bool VMenu::ProcessKey(const Manager::Key& Key) case KEY_CTRLRALTRIGHT: case KEY_CTRLRALTNUMPAD6: { if (ShiftAllItemsShowPos(any_of(LocalKey & ~KEY_CTRLMASK, KEY_LEFT, KEY_NUMPAD4, KEY_MSWHEEL_LEFT)? -20 : 20)) - ShowMenu(true); + DrawMenu(); break; } @@ -1311,29 +1350,29 @@ 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)) - ShowMenu(true); + if (ShiftItemShowPos(SelectPos, any_of(LocalKey & ~KEY_CTRLMASK, KEY_LEFT, KEY_NUMPAD4)? -1 : 1, CalculateMaxLineWidth())) + DrawMenu(); break; } 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)) - ShowMenu(true); + if (ShiftItemShowPos(SelectPos, any_of(LocalKey & ~KEY_CTRLMASK, KEY_LEFT, KEY_NUMPAD4)? -20 : 20, CalculateMaxLineWidth())) + DrawMenu(); break; } case KEY_MSWHEEL_UP: { SetSelectPos(SelectPos - 1, -1, true); - ShowMenu(true); + DrawMenu(); break; } case KEY_MSWHEEL_DOWN: { SetSelectPos(SelectPos + 1, 1, true); - ShowMenu(true); + DrawMenu(); break; } @@ -1341,7 +1380,7 @@ bool VMenu::ProcessKey(const Manager::Key& Key) case KEY_UP: case KEY_NUMPAD8: { SetSelectPos(SelectPos-1,-1,IsRepeatedKey()); - ShowMenu(true); + DrawMenu(); break; } @@ -1349,7 +1388,7 @@ bool VMenu::ProcessKey(const Manager::Key& Key) case KEY_DOWN: case KEY_NUMPAD2: { SetSelectPos(SelectPos+1,1,IsRepeatedKey()); - ShowMenu(true); + DrawMenu(); break; } @@ -1461,7 +1500,7 @@ bool VMenu::ProcessKey(const Manager::Key& Key) { if (Parent->SendMessage(DN_LISTHOTKEY,DialogItemID,ToPtr(NewPos))) { - ShowMenu(true); + DrawMenu(); ClearDone(); break; } @@ -1540,8 +1579,9 @@ bool VMenu::ProcessMouse(const MOUSE_EVENT_RECORD *MouseEvent) return false; } - const int MsX = MouseEvent->dwMousePosition.X; - const int MsY = MouseEvent->dwMousePosition.Y; + const int MsX{ MouseEvent->dwMousePosition.X }; + const int MsY{ MouseEvent->dwMousePosition.Y }; + const auto BoxType{ CalculateBoxType() }; // необходимо знать, что RBtn был нажат ПОСЛЕ появления VMenu, а не до if ((MouseEvent->dwButtonState & RIGHTMOST_BUTTON_PRESSED) && IsMouseButtonEvent(MouseEvent->dwEventFlags)) @@ -1550,7 +1590,7 @@ bool VMenu::ProcessMouse(const MOUSE_EVENT_RECORD *MouseEvent) if ((MouseEvent->dwButtonState & FROM_LEFT_2ND_BUTTON_PRESSED) && IsMouseButtonEvent(MouseEvent->dwEventFlags)) { if ( - m_BoxType == NO_BOX? + BoxType == NO_BOX? MsX >= m_Where.left && MsX <= m_Where.right && MsY >= m_Where.top && MsY <= m_Where.bottom : MsX > m_Where.left && MsX < m_Where.right && MsY > m_Where.top && MsY < m_Where.bottom ) @@ -1574,14 +1614,14 @@ bool VMenu::ProcessMouse(const MOUSE_EVENT_RECORD *MouseEvent) return true; } - const auto SbY1 = m_Where.top + (m_BoxType == NO_BOX? 0 : 1); - const auto SbY2 = m_Where.bottom - (m_BoxType == NO_BOX? 0 : 1); + const auto SbY1 = m_Where.top + (BoxType == NO_BOX? 0 : 1); + const auto SbY2 = m_Where.bottom - (BoxType == NO_BOX? 0 : 1); bool bShowScrollBar = false; if (CheckFlags(VMENU_LISTBOX|VMENU_ALWAYSSCROLLBAR) || Global->Opt->ShowMenuScrollbar) bShowScrollBar = true; - if (bShowScrollBar && MsX == m_Where.right && (m_Where.height() - (m_BoxType == NO_BOX? 0 : 2)) < static_cast(Items.size()) && (MouseEvent->dwButtonState & FROM_LEFT_1ST_BUTTON_PRESSED)) + if (bShowScrollBar && MsX == m_Where.right && (m_Where.height() - (BoxType == NO_BOX? 0 : 2)) < static_cast(Items.size()) && (MouseEvent->dwButtonState & FROM_LEFT_1ST_BUTTON_PRESSED)) { const auto WrapState = CheckFlags(VMENU_WRAPMODE); if (WrapState) @@ -1597,7 +1637,7 @@ bool VMenu::ProcessMouse(const MOUSE_EVENT_RECORD *MouseEvent) return false; ProcessKey(Manager::Key(KEY_UP)); - ShowMenu(true); + DrawMenu(); return true; }); @@ -1613,7 +1653,7 @@ bool VMenu::ProcessMouse(const MOUSE_EVENT_RECORD *MouseEvent) return false; ProcessKey(Manager::Key(KEY_DOWN)); - ShowMenu(true); + DrawMenu(); return true; }); @@ -1645,7 +1685,7 @@ bool VMenu::ProcessMouse(const MOUSE_EVENT_RECORD *MouseEvent) SetSelectPos(VisualPosToReal(MsPos),Delta); - ShowMenu(true); + DrawMenu(); } return true; @@ -1653,7 +1693,7 @@ bool VMenu::ProcessMouse(const MOUSE_EVENT_RECORD *MouseEvent) } // dwButtonState & 3 - Left & Right button - if (m_BoxType != NO_BOX && (MouseEvent->dwButtonState & 3) && MsX > m_Where.left && MsX < m_Where.right) + if (BoxType != NO_BOX && (MouseEvent->dwButtonState & 3) && MsX > m_Where.left && MsX < m_Where.right) { const auto WrapState = CheckFlags(VMENU_WRAPMODE); if (WrapState) @@ -1689,12 +1729,12 @@ bool VMenu::ProcessMouse(const MOUSE_EVENT_RECORD *MouseEvent) } } - if (m_BoxType==NO_BOX? + if (BoxType==NO_BOX? MsX >= m_Where.left && MsX <= m_Where.right && MsY >= m_Where.top && MsY <= m_Where.bottom : MsX > m_Where.left && MsX < m_Where.right && MsY > m_Where.top && MsY < m_Where.bottom ) { - const auto MsPos = VisualPosToReal(GetVisualPos(TopPos) + MsY - m_Where.top - (m_BoxType == NO_BOX? 0 : 1)); + const auto MsPos = VisualPosToReal(GetVisualPos(TopPos) + MsY - m_Where.top - (BoxType == NO_BOX? 0 : 1)); if (MsPos>=0 && MsPos(Items.size()) && item_can_have_focus(Items[MsPos])) { @@ -1719,7 +1759,7 @@ bool VMenu::ProcessMouse(const MOUSE_EVENT_RECORD *MouseEvent) SetSelectPos(MsPos,1); } - ShowMenu(true); + DrawMenu(); } /* $ 13.10.2001 VVM @@ -1769,7 +1809,7 @@ int VMenu::VisualPosToReal(int VPos) const return ItemIterator != Items.cend()? ItemIterator - Items.cbegin() : -1; } -size_t VMenu::GetItemMaxShowPos(int Item) const +size_t VMenu::GetItemMaxShowPos(int Item, const size_t MaxLineWidth) const { const auto Len{ VMFlags.Check(VMENU_SHOWAMPERSAND)? visual_string_length(Items[Item].Name) : HiStrlen(Items[Item].Name) }; if (Len <= MaxLineWidth) @@ -1777,9 +1817,9 @@ size_t VMenu::GetItemMaxShowPos(int Item) const return Len - MaxLineWidth; } -bool VMenu::SetItemShowPos(int Item, int NewShowPos) +bool VMenu::SetItemShowPos(int Item, int NewShowPos, const size_t MaxLineWidth) { - const auto MaxShowPos{ GetItemMaxShowPos(Item) }; + const auto MaxShowPos{ GetItemMaxShowPos(Item, MaxLineWidth) }; auto OldShowPos{ Items[Item].ShowPos }; if (NewShowPos >= 0) @@ -1798,9 +1838,9 @@ bool VMenu::SetItemShowPos(int Item, int NewShowPos) return true; } -bool VMenu::ShiftItemShowPos(int Item, int Shift) +bool VMenu::ShiftItemShowPos(int Item, int Shift, const size_t MaxLineWidth) { - const auto MaxShowPos{ GetItemMaxShowPos(Item) }; + const auto MaxShowPos{ GetItemMaxShowPos(Item, MaxLineWidth) }; auto ItemShowPos{ std::min(Items[Item].ShowPos, MaxShowPos) }; // Just in case if (Shift >= 0) @@ -1824,8 +1864,10 @@ bool VMenu::SetAllItemsShowPos(int NewShowPos) { bool NeedRedraw = false; + const auto MaxLineWidth{ CalculateMaxLineWidth() }; + for (const auto I: std::views::iota(size_t{}, Items.size())) - NeedRedraw |= SetItemShowPos(static_cast(I), NewShowPos); + NeedRedraw |= SetItemShowPos(static_cast(I), NewShowPos, MaxLineWidth); return NeedRedraw; } @@ -1834,15 +1876,69 @@ bool VMenu::ShiftAllItemsShowPos(int Shift) { bool NeedRedraw = false; + const auto MaxLineWidth{ CalculateMaxLineWidth() }; + for (const auto I: std::views::iota(size_t{}, Items.size())) - NeedRedraw |= ShiftItemShowPos(static_cast(I), Shift); + NeedRedraw |= ShiftItemShowPos(static_cast(I), Shift, MaxLineWidth); return NeedRedraw; } +struct item_layout +{ + std::optional LeftBox; + std::optional CheckMark; + std::optional LeftHScroll; + std::optional> Text; // Begin, Width + std::optional RightHScroll; + std::optional SubMenu; + std::optional Scrollbar; + std::optional RightBox; + + item_layout(const VMenu& Menu, int BoxType) + { + auto Left{ Menu.m_Where.left }; + if (NeedBox(BoxType)) LeftBox = Left++; + if (NeedCheckMark()) CheckMark = Left++; + if (NeedLeftHScroll()) LeftHScroll = Left++; + + auto Right{ Menu.m_Where.right }; + if (NeedBox(BoxType)) RightBox = Right; + if (NeedScrollbar(Menu)) Scrollbar = Right; + if (RightBox || Scrollbar) Right--; + if (NeedSubMenu(Menu)) SubMenu = Right--; + if (NeedRightHScroll()) RightHScroll = Right--; + + if (Left <= Right) + Text = { Left, Right + 1 - Left }; + } + + [[nodiscard]] static bool NeedBox(int BoxType) noexcept { return BoxType != NO_BOX; } + [[nodiscard]] static bool NeedCheckMark() noexcept { return true; } + [[nodiscard]] static bool NeedLeftHScroll() noexcept { return true; } + [[nodiscard]] static bool NeedRightHScroll() noexcept { return true; } + [[nodiscard]] static bool NeedSubMenu(const VMenu& Menu) noexcept { return Menu.ItemSubMenusCount > 0; }; + [[nodiscard]] static bool NeedScrollbar(const VMenu& Menu) + { + return (Menu.CheckFlags(VMENU_LISTBOX | VMENU_ALWAYSSCROLLBAR) || Global->Opt->ShowMenuScrollbar) + && ScrollBarRequired(Menu.m_Where.height(), Menu.GetShowItemCount()); + } + + [[nodiscard]] static size_t GetServiceAreaSize(const VMenu& Menu, const int BoxType) + { + return NeedBox(BoxType) + + NeedCheckMark() + + NeedLeftHScroll() + + NeedRightHScroll() + + NeedSubMenu(Menu) + + (NeedBox(BoxType) || NeedScrollbar(Menu)); + } +}; + void VMenu::Show() { - const auto ServiceAreaSize = GetServiceAreaSize(); + const auto BoxType{ CalculateBoxType() }; + const auto ServiceAreaSize = item_layout::GetServiceAreaSize(*this, BoxType); if (!CheckFlags(VMENU_LISTBOX)) { @@ -1867,7 +1963,7 @@ void VMenu::Show() if (m_Where.right <= 0) m_Where.right = static_cast(m_Where.left + MenuWidth); - if (!AutoCenter && m_Where.right > ScrX-4+2*(m_BoxType==SHORT_DOUBLE_BOX || m_BoxType==SHORT_SINGLE_BOX)) + if (!AutoCenter && m_Where.right > ScrX-4+2*(BoxType==SHORT_DOUBLE_BOX || BoxType==SHORT_SINGLE_BOX)) { m_Where.left += ScrX - 4 - m_Where.right; m_Where.right = ScrX - 4; @@ -1957,9 +2053,11 @@ void VMenu::DisplayObject() HideCursor(); + const auto BoxType{ CalculateBoxType() }; + if (!CheckFlags(VMENU_LISTBOX) && !SaveScr) { - if (!CheckFlags(VMENU_DISABLEDRAWBACKGROUND) && !(m_BoxType==SHORT_DOUBLE_BOX || m_BoxType==SHORT_SINGLE_BOX)) + if (!CheckFlags(VMENU_DISABLEDRAWBACKGROUND) && !(BoxType==SHORT_DOUBLE_BOX || BoxType==SHORT_SINGLE_BOX)) SaveScr = std::make_unique(rectangle{ m_Where.left - 2, m_Where.top - 1, m_Where.right + 4, m_Where.bottom + 2 }); else SaveScr = std::make_unique(rectangle{ m_Where.left, m_Where.top, m_Where.right + 2, m_Where.bottom + 1 }); @@ -1967,22 +2065,22 @@ void VMenu::DisplayObject() if (!CheckFlags(VMENU_DISABLEDRAWBACKGROUND) && !CheckFlags(VMENU_LISTBOX)) { - // BUGBUG, dead code + // BUGBUG, dead code -- 2023-07-08 MZK: Is it though? I've got here when pressed hotkey of "Select search area" combobox on "find file" dialog. - if (m_BoxType==SHORT_DOUBLE_BOX || m_BoxType==SHORT_SINGLE_BOX) + if (BoxType==SHORT_DOUBLE_BOX || BoxType==SHORT_SINGLE_BOX) { - SetScreen(m_Where, L' ', Colors[VMenuColorBody]); - Box(m_Where, Colors[VMenuColorBox], m_BoxType); + SetScreen(m_Where, L' ', GetColor(vmenu_colors::Body)); + Box(m_Where, GetColor(vmenu_colors::Box), BoxType); } else { - if (m_BoxType!=NO_BOX) - SetScreen({ m_Where.left - 2, m_Where.top - 1, m_Where.right + 2, m_Where.bottom + 1 }, L' ', Colors[VMenuColorBody]); + if (BoxType!=NO_BOX) + SetScreen({ m_Where.left - 2, m_Where.top - 1, m_Where.right + 2, m_Where.bottom + 1 }, L' ', GetColor(vmenu_colors::Body)); else - SetScreen(m_Where, L' ', Colors[VMenuColorBody]); + SetScreen(m_Where, L' ', GetColor(vmenu_colors::Body)); - if (m_BoxType!=NO_BOX) - Box(m_Where, Colors[VMenuColorBox], m_BoxType); + if (BoxType!=NO_BOX) + Box(m_Where, GetColor(vmenu_colors::Box), BoxType); } //SetMenuFlags(VMENU_DISABLEDRAWBACKGROUND); @@ -1991,7 +2089,158 @@ void VMenu::DisplayObject() if (!CheckFlags(VMENU_LISTBOX)) DrawTitles(); - ShowMenu(true); + DrawMenu(); +} + +namespace +{ + struct item_color_indicies + { + vmenu_colors Normal, Highlighted, HScroller; + + item_color_indicies(const MenuItemEx& CurItem) + { + const auto Selected{ !!(CurItem.Flags & LIF_SELECTED) }; + const auto Grayed{ !!(CurItem.Flags & LIF_GRAYED) }; + const auto Disabled{ !!(CurItem.Flags & LIF_DISABLE) }; + + if (Disabled) + { + Normal = vmenu_colors::Disabled; + Highlighted = vmenu_colors::Disabled; + HScroller = vmenu_colors::ArrowsDisabled; + return; + } + + if (Selected) + { + Normal = Grayed ? vmenu_colors::SelGrayed : vmenu_colors::Selected; + Highlighted = Grayed ? vmenu_colors::SelGrayed : vmenu_colors::HSelect; + HScroller = vmenu_colors::ArrowsSelect; + return; + } + + Normal = Grayed ? vmenu_colors::Grayed : vmenu_colors::Text; + Highlighted = Grayed ? vmenu_colors::Grayed : vmenu_colors::Highlight; + HScroller = vmenu_colors::Arrows; + } + }; + + std::tuple GetItemCheckMark(const MenuItemEx& CurItem, item_color_indicies ColorIndices) noexcept + { + return + { + ColorIndices.Normal, + !(CurItem.Flags & LIF_CHECKED) + ? L' ' + : !(CurItem.Flags & 0x0000FFFF) ? L'√' : static_cast(CurItem.Flags & 0x0000FFFF) + }; + } + + std::tuple GetItemSubMenu(const MenuItemEx& CurItem, item_color_indicies ColorIndices) noexcept + { + return + { + ColorIndices.Normal, + (CurItem.Flags & MIF_SUBMENU) ? L'►' : L' ' + }; + } + + std::tuple GetItemLeftHScroll(const bool NeedLeftHScroll, item_color_indicies ColorIndices) noexcept + { + return + { + NeedLeftHScroll ? ColorIndices.HScroller : ColorIndices.Normal, + NeedLeftHScroll ? L'«' : L' ' + }; + } + + std::tuple GetItemRightHScroll(const bool NeedRightHScroll, item_color_indicies ColorIndices) noexcept + { + return + { + NeedRightHScroll ? ColorIndices.HScroller : ColorIndices.Normal, + NeedRightHScroll ? L'»' : L' ' + }; + } +} + +void VMenu::DrawMenu() +{ + const auto BoxType{ CalculateBoxType() }; + const auto ClientRect{ GetClientRect(BoxType) }; + + if (m_Where.right <= m_Where.left || m_Where.bottom <= m_Where.top) + { + if (!(CheckFlags(VMENU_SHOWNOBOX) && m_Where.bottom == m_Where.top)) + return; + } + + // 2023-12-09 MZK: Do we need this? Why? + if (CheckFlags(VMENU_LISTBOX)) + { + if (!GetShowItemCount()) + SetScreen(m_Where, L' ', GetColor(vmenu_colors::Body)); + + if (BoxType!=NO_BOX) + Box(m_Where, GetColor(vmenu_colors::Box), BoxType); + + DrawTitles(); + } + + if (GetShowItemCount() <= 0) + return; + + if (CheckFlags(VMENU_AUTOHIGHLIGHT)) + AssignHighlights(CheckFlags(VMENU_REVERSEHIGHLIGHT)); + + const auto VisualTopPos{ AdjustTopPos(ClientRect.height()) }; + + if (ClientRect.width() <= 0) + return; + + const item_layout Layout{ *this, BoxType }; + std::vector HighlightMarkup; + const string BlankLine(ClientRect.width(), L' '); + + for (int Y = ClientRect.top, I = TopPos; Y <= ClientRect.bottom; ++Y, ++I) + { + if (I >= static_cast(Items.size())) + { + GotoXY(ClientRect.left, Y); + SetColor(vmenu_colors::Text); + Text(BlankLine); + continue; + } + + if (!item_is_visible(Items[I])) + { + Y--; + continue; + } + + if (Items[I].Flags & LIF_SEPARATOR) + { + DrawSeparator(I, BoxType, Y); + continue; + } + + DrawRegularItem(Items[I], Layout, Y, HighlightMarkup, BlankLine); + } + + if (Layout.Scrollbar) + { + SetColor(vmenu_colors::ScrollBar); + ScrollBar(Layout.Scrollbar.value(), ClientRect.top, ClientRect.height(), VisualTopPos, GetShowItemCount()); + } +} + +rectangle VMenu::GetClientRect(const int BoxType) const noexcept +{ + if (BoxType == NO_BOX) + return m_Where; + + return { m_Where.left + 1, m_Where.top + 1, m_Where.right - 1, m_Where.bottom - 1 }; } void VMenu::DrawTitles() const @@ -2021,7 +2270,7 @@ void VMenu::DrawTitles() const WidthTitle = MaxTitleLength - 1; GotoXY(m_Where.left + (m_Where.width() - 2 - WidthTitle) / 2, m_Where.top); - SetColor(Colors[VMenuColorTitle]); + SetColor(vmenu_colors::Title); Text(concat(L' ', string_view(strDisplayTitle).substr(0, WidthTitle), L' ')); } @@ -2034,348 +2283,182 @@ void VMenu::DrawTitles() const WidthTitle = MaxTitleLength - 1; GotoXY(m_Where.left + (m_Where.width() - 2 - WidthTitle) / 2, m_Where.bottom); - SetColor(Colors[VMenuColorTitle]); + SetColor(vmenu_colors::Title); Text(concat(L' ', string_view(strBottomTitle).substr(0, WidthTitle), L' ')); } } -void VMenu::ShowMenu(bool IsParent) +int VMenu::AdjustTopPos(const int ClientHeight) { - const auto ServiceAreaSize = GetServiceAreaSize(); - const auto CalcMaxLineWidth = ServiceAreaSize > static_cast(m_Where.width())? 0 : m_Where.width() - ServiceAreaSize; - - MaxLineWidth = CalcMaxLineWidth; - - if (m_Where.right <= m_Where.left || m_Where.bottom <= m_Where.top) - { - if (!(CheckFlags(VMENU_SHOWNOBOX) && m_Where.bottom == m_Where.top)) - return; - } - - if (CheckFlags(VMENU_LISTBOX)) - { - if (!IsParent || !GetShowItemCount()) - { - if (GetShowItemCount()) - m_BoxType=CheckFlags(VMENU_SHOWNOBOX)?NO_BOX:SHORT_SINGLE_BOX; - - SetScreen(m_Where, L' ', Colors[VMenuColorBody]); - } - - if (m_BoxType!=NO_BOX) - Box(m_Where, Colors[VMenuColorBox], m_BoxType); - - DrawTitles(); - } - - wchar_t BoxChar[2]{}; - - switch (m_BoxType) - { - case NO_BOX: - *BoxChar=L' '; - break; - - case SINGLE_BOX: - case SHORT_SINGLE_BOX: - *BoxChar=BoxSymbols[BS_V1]; - break; - - case DOUBLE_BOX: - case SHORT_DOUBLE_BOX: - *BoxChar=BoxSymbols[BS_V2]; - break; - } - - if (GetShowItemCount() <= 0) - return; - - if (CheckFlags(VMENU_AUTOHIGHLIGHT)) - AssignHighlights(CheckFlags(VMENU_REVERSEHIGHLIGHT)); - int VisualSelectPos = GetVisualPos(SelectPos); int VisualTopPos = GetVisualPos(TopPos); - // коррекция Top`а - if (VisualTopPos+GetShowItemCount() >= m_Where.height() - 1 && VisualSelectPos == GetShowItemCount()-1) + // 2023-07-09 MZK: What is it? Should it be ClientRect.height() instead of m_Where.height()? + if (VisualTopPos + GetShowItemCount() >= m_Where.height() - 1 && VisualSelectPos == GetShowItemCount() - 1) { VisualTopPos--; - if (VisualTopPos<0) - VisualTopPos=0; + if (VisualTopPos < 0) + VisualTopPos = 0; } - VisualTopPos = std::min(VisualTopPos, GetShowItemCount() - (m_Where.height() - 2 - (m_BoxType == NO_BOX ? 2 : 0))); + VisualTopPos = std::min(VisualTopPos, GetShowItemCount() - (ClientHeight - 4)); - if (VisualSelectPos > VisualTopPos + (m_Where.height() - 1 - (m_BoxType == NO_BOX? 0 : 2))) + if (VisualSelectPos > VisualTopPos + (ClientHeight - 1)) { - VisualTopPos = VisualSelectPos - (m_Where.height() - 1 - (m_BoxType == NO_BOX? 0 : 2)); + VisualTopPos = VisualSelectPos - (ClientHeight - 1); } if (VisualSelectPos < VisualTopPos) { - TopPos=SelectPos; - VisualTopPos=VisualSelectPos; + TopPos = SelectPos; + VisualTopPos = VisualSelectPos; } else { - TopPos=VisualPosToReal(VisualTopPos); + TopPos = VisualPosToReal(VisualTopPos); } - if (VisualTopPos<0) - VisualTopPos=0; - - if (TopPos<0) - TopPos=0; - - for (int Y = m_Where.top + (m_BoxType == NO_BOX? 0 : 1), I = TopPos; Y < m_Where.bottom + (m_BoxType == NO_BOX? 1 : 0); ++Y, ++I) - { - GotoXY(m_Where.left, Y); - - if (I < static_cast(Items.size())) - { - if (!item_is_visible(Items[I])) - { - Y--; - continue; - } - - if (Items[I].Flags&LIF_SEPARATOR) - { - int SepWidth = m_Where.width(); + if (VisualTopPos < 0) + VisualTopPos = 0; - auto strTmpStr = MakeLine(SepWidth, m_BoxType == NO_BOX? line_type::h1_to_none : (m_BoxType == SINGLE_BOX || m_BoxType == SHORT_SINGLE_BOX? line_type::h1_to_v1 : line_type::h1_to_v2)); + if (TopPos < 0) + TopPos = 0; - if (!CheckFlags(VMENU_NOMERGEBORDER) && SepWidth > 3) - { - for (const auto J: std::views::iota(size_t{}, strTmpStr.size() - 3)) - { - const auto AnyPrev = I > 0; - const auto AnyNext = I < static_cast(Items.size() - 1); + return VisualTopPos; +} - const auto PCorrection = AnyPrev && !CheckFlags(VMENU_SHOWAMPERSAND)? HiFindRealPos(Items[I - 1].Name, J) - J : 0; - const auto NCorrection = AnyNext && !CheckFlags(VMENU_SHOWAMPERSAND)? HiFindRealPos(Items[I + 1].Name, J) - J : 0; +void VMenu::DrawSeparator(const size_t CurItemIndex, const int BoxType, const int Y) const +{ + auto separator{ MakeLine( + m_Where.width(), + BoxType == NO_BOX + ? line_type::h1_to_none + : (BoxType == SINGLE_BOX || BoxType == SHORT_SINGLE_BOX? line_type::h1_to_v1 : line_type::h1_to_v2)) }; - wchar_t PrevItem = (AnyPrev && Items[I - 1].Name.size() > J + PCorrection)? Items[I - 1].Name[J + PCorrection] : 0; - wchar_t NextItem = (AnyNext && Items[I + 1].Name.size() > J + NCorrection)? Items[I + 1].Name[J + NCorrection] : 0; + ConnectSeparator(CurItemIndex, separator, BoxType); + ApplySeparatorName(Items[CurItemIndex], separator); + SetColor(vmenu_colors::Separator); + GotoXY(m_Where.left, Y); + Text(separator); +} - if (!PrevItem && !NextItem) - break; +void VMenu::ConnectSeparator(const size_t CurItemIndex, string& separator, const int BoxType) const +{ + if (CheckFlags(VMENU_NOMERGEBORDER) || separator.size() <= 3) + return; - if (PrevItem==BoxSymbols[BS_V1]) - { - if (NextItem==BoxSymbols[BS_V1]) - strTmpStr[J+(m_BoxType==NO_BOX?1:2) + 1] = BoxSymbols[BS_C_H1V1]; - else - strTmpStr[J+(m_BoxType==NO_BOX?1:2) + 1] = BoxSymbols[BS_B_H1V1]; - } - else if (NextItem==BoxSymbols[BS_V1]) - { - strTmpStr[J+(m_BoxType==NO_BOX?1:2) + 1] = BoxSymbols[BS_T_H1V1]; - } - } - } + for (const auto I : std::views::iota(size_t{}, separator.size() - 3)) + { + const auto AnyPrev = CurItemIndex > 0; + const auto AnyNext = CurItemIndex < Items.size() - 1; - SetColor(Colors[VMenuColorSeparator]); - Text(strTmpStr); + const auto PCorrection = AnyPrev && !CheckFlags(VMENU_SHOWAMPERSAND)? HiFindRealPos(Items[CurItemIndex - 1].Name, I) - I : 0; + const auto NCorrection = AnyNext && !CheckFlags(VMENU_SHOWAMPERSAND)? HiFindRealPos(Items[CurItemIndex + 1].Name, I) - I : 0; - if (!Items[I].Name.empty()) - { - auto ItemWidth = static_cast(Items[I].Name.size()); + wchar_t PrevItem = (AnyPrev && Items[CurItemIndex - 1].Name.size() > I + PCorrection)? Items[CurItemIndex - 1].Name[I + PCorrection] : 0; + wchar_t NextItem = (AnyNext && Items[CurItemIndex + 1].Name.size() > I + NCorrection)? Items[CurItemIndex + 1].Name[I + NCorrection] : 0; - if (ItemWidth + 2 > m_Where.width()) - ItemWidth = m_Where.width() - 2; + if (!PrevItem && !NextItem) + break; - GotoXY(m_Where.left + (m_Where.width() - 2 - ItemWidth) / 2, Y); - Text(concat(L' ', fit_to_left(Items[I].Name, ItemWidth), L' ')); - } - } + if (PrevItem == BoxSymbols[BS_V1]) + { + if (NextItem == BoxSymbols[BS_V1]) + separator[I + (BoxType == NO_BOX?1:2) + 1] = BoxSymbols[BS_C_H1V1]; else - { - if (m_BoxType!=NO_BOX) - { - SetColor(Colors[VMenuColorBox]); - Text(BoxChar); - GotoXY(m_Where.right, Y); - Text(BoxChar); - } - - GotoXY(m_Where.left + (m_BoxType == NO_BOX? 0 : 1), Y); - - FarColor CurColor; - if ((Items[I].Flags&LIF_SELECTED)) - CurColor = Colors[Items[I].Flags & LIF_GRAYED? VMenuColorSelGrayed : VMenuColorSelected]; - else - CurColor = Colors[Items[I].Flags & LIF_DISABLE? VMenuColorDisabled : (Items[I].Flags & LIF_GRAYED? VMenuColorGrayed : VMenuColorText)]; - - SetColor(CurColor); - - string strMenuLine; - wchar_t CheckMark = L' '; - - if (Items[I].Flags & LIF_CHECKED) - { - if (!(Items[I].Flags & 0x0000FFFF)) - CheckMark = 0x221A; - else - CheckMark = static_cast(Items[I].Flags & 0x0000FFFF); - } - - strMenuLine.push_back(CheckMark); - strMenuLine.push_back(L' '); // left scroller (<<) placeholder - const auto PrefixSize = strMenuLine.size(); - - size_t HotkeyVisualPos = string::npos; - auto MenuItemForDisplay = CheckFlags(VMENU_SHOWAMPERSAND)? Items[I].Name : HiText2Str(Items[I].Name, &HotkeyVisualPos); + separator[I + (BoxType == NO_BOX?1:2) + 1] = BoxSymbols[BS_B_H1V1]; + } + else if (NextItem == BoxSymbols[BS_V1]) + { + separator[I + (BoxType == NO_BOX?1:2) + 1] = BoxSymbols[BS_T_H1V1]; + } + } +} - MenuItemForDisplay.erase(0, Items[I].ShowPos); +void VMenu::ApplySeparatorName(const MenuItemEx& CurItem, string& separator) const +{ + if (CurItem.Name.empty() || separator.size() <= 3) + return; - if (HotkeyVisualPos != string::npos) - { - if (HotkeyVisualPos < Items[I].ShowPos) - HotkeyVisualPos = string::npos; - else - HotkeyVisualPos -= Items[I].ShowPos; - } + auto NameWidth{ std::min(CurItem.Name.size(), separator.size() - 2) }; + auto NamePos{ (separator.size() - NameWidth) / 2 }; - // fit menu string into available space - bool ShowRightScroller = false; - if (MenuItemForDisplay.size() > MaxLineWidth) - { - MenuItemForDisplay.resize(MaxLineWidth); - ShowRightScroller = true; - } + separator.at(NamePos - 1) = L' '; + separator.replace(NamePos, NameWidth, fit_to_left(CurItem.Name, NameWidth)); + separator.at(NamePos + NameWidth) = L' '; +} - strMenuLine += MenuItemForDisplay; +void VMenu::DrawRegularItem(const MenuItemEx& CurItem, const item_layout& Layout, const int Y, std::vector& HighlightMarkup, const string& BlankLine) const +{ + if (!Layout.Text) return; - // табуляции меняем только при показе!!! - // для сохранение оригинальной строки!!! - std::ranges::replace(strMenuLine, L'\t', L' '); + size_t HotkeyPos = string::npos; + auto ItemTextToDisplay = CheckFlags(VMENU_SHOWAMPERSAND)? CurItem.Name : HiText2Str(CurItem.Name, &HotkeyPos); + std::ranges::replace(ItemTextToDisplay, L'\t', L' '); + const auto ItemTextSize{ static_cast(ItemTextToDisplay.size()) }; - FarColor Col; + const auto [TextBegin, TextWidth] { Layout.Text.value() }; - if (!(Items[I].Flags & LIF_DISABLE)) - { - static const vmenu_colors ItemColors[][2] = - { - { VMenuColorHighlight, VMenuColorHSelect }, - { VMenuColorGrayed, VMenuColorSelGrayed }, - }; + const item_color_indicies ColorIndices{ CurItem }; + auto CurColorIndex{ ColorIndices.Normal }; + auto AltColorIndex{ ColorIndices.Highlighted }; - const auto Index = ItemColors[Items[I].Flags & LIF_GRAYED ? 1 : 0][Items[I].Flags & LIF_SELECTED ? 1 : 0]; - Col = Colors[Index]; - } - else - { - Col = Colors[VMenuColorDisabled]; - } + auto CurPos{ static_cast(CurItem.ShowPos) }; + const auto EndPos{ std::min(CurPos + TextWidth, ItemTextSize) }; - if (!Items[I].Annotations.empty()) - { - size_t Pos = 0; - for (const auto& [AnnPos, AnnSize]: Items[I].Annotations) - { - const int StartOffset = 1; // 1 is '<<' placeholder size - const size_t pre_len = AnnPos - Items[I].ShowPos + StartOffset - Pos + 1; - if (Pos < strMenuLine.size()) - { - Text(string_view(strMenuLine).substr(Pos, pre_len)); - Pos += pre_len; - if (Pos < strMenuLine.size()) - { - SetColor(Col); - Text(string_view(strMenuLine).substr(Pos, AnnSize)); - Pos += AnnSize; - SetColor(CurColor); - } - } - } - if (Pos < strMenuLine.size()) - Text(string_view(strMenuLine).substr(Pos)); - } - else - { - if (HotkeyVisualPos != string::npos || Items[I].AutoHotkey) - { - const auto HotkeyPos = (HotkeyVisualPos != string::npos? HotkeyVisualPos : Items[I].AutoHotkeyPos) + PrefixSize; - - Text(string_view(strMenuLine).substr(0, HotkeyPos)); - - if (HotkeyPos < strMenuLine.size()) - { - const auto SaveColor = CurColor; - SetColor(Col); - Text(strMenuLine[HotkeyPos]); - SetColor(SaveColor); - Text(string_view(strMenuLine).substr(HotkeyPos + 1)); - } - } - else - { - Text(strMenuLine); - } - } - - // сделаем добавочку для NO_BOX - { - int Width = m_Where.right - WhereX() + (m_BoxType == NO_BOX? 1 : 0); - if (Width > 0) - Text(string(Width, L' ')); - } - - if (Items[I].Flags & MIF_SUBMENU) - { - GotoXY(m_Where.right - 1, Y); - Text(L'►'); - } + HighlightMarkup.clear(); + if (!CurItem.Annotations.empty()) + { + MarkupSliceBoundaries( + std::pair{ CurPos, EndPos }, + CurItem.Annotations | std::views::transform([](const auto Ann) { return std::pair{ Ann.first, Ann.first + Ann.second }; }), + std::back_inserter(HighlightMarkup)); + } + else if (HotkeyPos != string::npos || CurItem.AutoHotkey) + { + const auto HighlightPos = static_cast(HotkeyPos != string::npos? HotkeyPos : CurItem.AutoHotkeyPos); + MarkupSliceBoundaries( + std::pair{ CurPos, EndPos }, + std::views::single(std::pair{ HighlightPos, HighlightPos + 1 }), + std::back_inserter(HighlightMarkup)); + } + else + { + HighlightMarkup.emplace_back(EndPos); + } - SetColor(Colors[(Items[I].Flags&LIF_DISABLE)?VMenuColorArrowsDisabled:(Items[I].Flags&LIF_SELECTED?VMenuColorArrowsSelect:VMenuColorArrows)]); + GotoXY(TextBegin, Y); + for (const auto SliceEnd : HighlightMarkup) + { + SetColor(CurColorIndex); + Text(string_view{ ItemTextToDisplay.begin() + CurPos, ItemTextToDisplay.begin() + SliceEnd }); + std::swap(CurColorIndex, AltColorIndex); + CurPos = SliceEnd; + } - if (Items[I].ShowPos) - { - GotoXY(m_Where.left + (m_BoxType == NO_BOX? 0 : 1) + 1, Y); - Text(L'«'); - } + SetColor(ColorIndices.Normal); + Text(string_view{ BlankLine.begin(), BlankLine.begin() + (TextBegin + TextWidth - WhereX()) }); - if (ShowRightScroller) - { - GotoXY(static_cast(m_Where.left + (m_BoxType == NO_BOX? 0 : 1) + 2 + MaxLineWidth), Y); - Text(L'»'); - } - } - } - else + const auto DrawDecorator = [&](const int X, std::tuple ColorAndChar) { - if (m_BoxType!=NO_BOX) - { - SetColor(Colors[VMenuColorBox]); - Text(BoxChar); - GotoXY(m_Where.right, Y); - Text(BoxChar); - GotoXY(m_Where.left + 1, Y); - } - else - { - GotoXY(m_Where.left, Y); - } + GotoXY(X, Y); + SetColor(std::get(ColorAndChar)); + Text(std::get(ColorAndChar)); + }; - SetColor(Colors[VMenuColorText]); - // сделаем добавочку для NO_BOX - const auto Size = m_Where.width() - (m_BoxType == NO_BOX? 0 : 2); - Text(string(Size, L' ')); - } - } + if (Layout.CheckMark) + DrawDecorator(Layout.CheckMark.value(), GetItemCheckMark(CurItem, ColorIndices)); - if (CheckFlags(VMENU_LISTBOX|VMENU_ALWAYSSCROLLBAR) || Global->Opt->ShowMenuScrollbar) - { - SetColor(Colors[VMenuColorScrollBar]); + if (Layout.LeftHScroll) + DrawDecorator(Layout.LeftHScroll.value(), GetItemLeftHScroll(CurItem.ShowPos > 0, ColorIndices)); - if (m_BoxType!=NO_BOX) - ScrollBar(m_Where.right, m_Where.top + 1, m_Where.height() - 2, VisualTopPos, GetShowItemCount()); - else - ScrollBar(m_Where.right, m_Where.top, m_Where.height(), VisualTopPos, GetShowItemCount()); - } + if (Layout.SubMenu) + DrawDecorator(Layout.SubMenu.value(), GetItemSubMenu(CurItem, ColorIndices)); + + if (Layout.RightHScroll) + DrawDecorator(Layout.RightHScroll.value(), GetItemRightHScroll(EndPos < ItemTextSize, ColorIndices)); } int VMenu::CheckHighlights(wchar_t CheckSymbol, int StartPos) const @@ -2489,7 +2572,7 @@ bool VMenu::CheckKeyHiOrAcc(DWORD Key, int Type, bool Translate, bool ChangePos, if (ChangePos) { SetSelectPos(NewPos, 1); - ShowMenu(true); + DrawMenu(); } if ((!GetDialog() || CheckFlags(VMENU_COMBOBOX|VMENU_LISTBOX)) && item_can_be_entered(Items[SelectPos])) @@ -2578,11 +2661,6 @@ void VMenu::OnClose() EnableFilter(false); } -void VMenu::SetBoxType(int BoxType) -{ - m_BoxType=BoxType; -} - void VMenu::SetColors(const FarDialogItemColors *ColorsIn) { if (ColorsIn) @@ -2596,11 +2674,11 @@ void VMenu::SetColors(const FarDialogItemColors *ColorsIn) if (CheckFlags(VMENU_DISABLED)) { - std::fill_n(Colors, size_t{ VMENU_COLOR_COUNT }, colors::PaletteColorToFarColor(StyleMenu? COL_WARNDIALOGDISABLED : COL_DIALOGDISABLED)); + std::fill_n(Colors, std::to_underlying(vmenu_colors::COUNT), colors::PaletteColorToFarColor(StyleMenu? COL_WARNDIALOGDISABLED : COL_DIALOGDISABLED)); } else { - static const PaletteColors StdColor[2][3][VMENU_COLOR_COUNT]= + static const PaletteColors StdColor[2][3][std::to_underlying(vmenu_colors::COUNT)]= { // Not VMENU_WARNDIALOG { @@ -2719,7 +2797,7 @@ void VMenu::SetColors(const FarDialogItemColors *ColorsIn) } }; - std::transform(StdColor[StyleMenu][TypeMenu], StdColor[StyleMenu][TypeMenu] + VMENU_COLOR_COUNT, Colors, colors::PaletteColorToFarColor); + std::ranges::transform(StdColor[StyleMenu][TypeMenu], Colors, colors::PaletteColorToFarColor); } } } @@ -2745,7 +2823,7 @@ bool VMenu::GetVMenuInfo(FarListInfo* Info) const Info->TopPos = TopPos; Info->MaxHeight = MaxHeight; // BUGBUG - const auto ServiceAreaSize = const_cast(this)->GetServiceAreaSize(); + const auto ServiceAreaSize = item_layout::GetServiceAreaSize(*this, CalculateBoxType()); if (static_cast(m_Where.width()) > ServiceAreaSize) Info->MaxLength = m_Where.width() - ServiceAreaSize; else @@ -2926,9 +3004,9 @@ std::vector VMenu::AddHotkeys(std::span const MenuItems) return Result; } -size_t VMenu::MaxItemLength() const +size_t VMenu::GetNaturalMenuWidth() const { - return m_MaxItemLength; + return m_MaxItemLength + item_layout::GetServiceAreaSize(*this, CalculateBoxType()); } void VMenu::EnableFilter(bool const Enable) @@ -2941,36 +3019,29 @@ void VMenu::EnableFilter(bool const Enable) RestoreFilteredItems(); } -size_t VMenu::GetServiceAreaSize() +int VMenu::CalculateBoxType(BitFlags Flags) noexcept { - if (CheckFlags(VMENU_LISTBOX)) + if (Flags.Check(VMENU_LISTBOX)) { - if (CheckFlags(VMENU_LISTSINGLEBOX)) - m_BoxType = SHORT_SINGLE_BOX; - else if (CheckFlags(VMENU_SHOWNOBOX)) - m_BoxType = NO_BOX; - else if (CheckFlags(VMENU_LISTHASFOCUS)) - m_BoxType = SHORT_DOUBLE_BOX; + if (Flags.Check(VMENU_LISTSINGLEBOX)) + return SHORT_SINGLE_BOX; + else if (Flags.Check(VMENU_SHOWNOBOX)) + return NO_BOX; + else if (Flags.Check(VMENU_LISTHASFOCUS)) + return SHORT_DOUBLE_BOX; else - m_BoxType = SHORT_SINGLE_BOX; + return SHORT_SINGLE_BOX; } + else if (Flags.Check(VMENU_COMBOBOX)) + return SHORT_SINGLE_BOX; + else + return DOUBLE_BOX; +} - size_t ServiceAreaSize = 0; - - if (m_BoxType != NO_BOX) - ServiceAreaSize += 2; // frame - - ++ServiceAreaSize; // check mark - ++ServiceAreaSize; // left scroll indicator - ++ServiceAreaSize; // right scroll indicator - - if (ItemSubMenusCount > 0) - ++ServiceAreaSize; // sub menu arrow - - if ((CheckFlags(VMENU_LISTBOX | VMENU_ALWAYSSCROLLBAR) || Global->Opt->ShowMenuScrollbar) && m_BoxType == NO_BOX && ScrollBarRequired(m_Where.height(), GetShowItemCount())) - ++ServiceAreaSize; // scrollbar - - return ServiceAreaSize; +size_t VMenu::CalculateMaxLineWidth() const +{ + const auto Text = item_layout{ *this, CalculateBoxType() }.Text; + return Text ? Text.value().second : 0; } size_t VMenu::Text(string_view const Str) const @@ -3048,4 +3119,72 @@ TEST_CASE("find.nearest.selectable.item") } } +TEST_CASE("intersect.segments") +{ + static constexpr struct test_data + { + std::pair A, B, Intersection; + } TestDataPoints[] = + { + { { 10, 20 }, { -1, 5 }, { 0, 0 } }, + { { 10, 20 }, { -1, 10 }, { 0, 0 } }, + { { 10, 20 }, { -1, 15 }, { 10, 15 } }, + { { 10, 20 }, { -1, 20 }, { 10, 20 } }, + { { 10, 20 }, { -1, 25 }, { 10, 20 } }, + { { 10, 20 }, { 10, 15 }, { 10, 15 } }, + { { 10, 20 }, { 10, 20 }, { 10, 20 } }, + { { 10, 20 }, { 10, 25 }, { 10, 20 } }, + { { 10, 20 }, { 15, 20 }, { 15, 20 } }, + { { 10, 20 }, { 15, 25 }, { 15, 20 } }, + { { 10, 20 }, { 20, 25 }, { 0, 0 } }, + { { 10, 20 }, { 25, 30 }, { 0, 0 } }, + }; + + for (const auto& TestDataPoint : TestDataPoints) + { + REQUIRE(TestDataPoint.Intersection == Intersect(TestDataPoint.A, TestDataPoint.B)); + REQUIRE(TestDataPoint.Intersection == Intersect(TestDataPoint.B, TestDataPoint.A)); + } +} + +TEST_CASE("markup.slice.boundaries") +{ + static struct test_data + { + std::pair Segment; + std::vector> Slices; + std::vector Markup; + } TestDataPoints[] = + { + { { 20, 50 }, { { { 10, 15 } } }, { { 50 } } }, + { { 20, 50 }, { { { 10, 20 } } }, { { 50 } } }, + { { 20, 50 }, { { { 10, 30 } } }, { { 20, 30, 50 } } }, + { { 20, 50 }, { { { 10, 50 } } }, { { 20, 50 } } }, + { { 20, 50 }, { { { 10, 70 } } }, { { 20, 50 } } }, + { { 20, 50 }, { { { 20, 30 } } }, { { 20, 30, 50 } } }, + { { 20, 50 }, { { { 20, 50 } } }, { { 20, 50 } } }, + { { 20, 50 }, { { { 20, 70 } } }, { { 20, 50 } } }, + { { 20, 50 }, { { { 30, 40 } } }, { { 30, 40, 50 } } }, + { { 20, 50 }, { { { 30, 50 } } }, { { 30, 50 } } }, + { { 20, 50 }, { { { 30, 70 } } }, { { 30, 50 } } }, + { { 20, 50 }, { { { 50, 50 } } }, { { 50 } } }, + { { 20, 50 }, { { { 50, 70 } } }, { { 50 } } }, + { { 20, 50 }, { { { 60, 70 } } }, { { 50 } } }, + { { 20, 50 }, { { { 40, 30 } } }, { { 50 } } }, + { { 20, 70 }, { { { 30, 40 }, { 50, 60 } } }, { { 30, 40, 50, 60, 70 } } }, + { { 20, 70 }, { { { 30, 40 }, { 40, 60 } } }, { { 30, 40, 40, 60, 70 } } }, + { { 20, 70 }, { { { 30, 50 }, { 40, 60 } } }, { { 30, 50, 50, 60, 70 } } }, + { { 20, 70 }, { { { 50, 60 }, { 30, 40 } } }, { { 50, 60, 70 } } }, + }; + + std::vector Markup; + + for (const auto& TestDataPoint : TestDataPoints) + { + Markup.clear(); + MarkupSliceBoundaries(TestDataPoint.Segment, TestDataPoint.Slices, std::back_inserter(Markup)); + REQUIRE(std::ranges::equal(TestDataPoint.Markup, Markup)); + } +} + #endif diff --git a/far/vmenu.hpp b/far/vmenu.hpp index 66b164c6b18..4d7de0cb9b0 100644 --- a/far/vmenu.hpp +++ b/far/vmenu.hpp @@ -43,6 +43,7 @@ THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include "modal.hpp" #include "bitflags.hpp" #include "farcolor.hpp" +#include "interf.hpp" // Platform: @@ -53,25 +54,25 @@ THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. //---------------------------------------------------------------------------- // Цветовые атрибуты - индексы в массиве цветов -enum vmenu_colors +enum class vmenu_colors { - VMenuColorBody = 0, // подложка - VMenuColorBox = 1, // рамка - VMenuColorTitle = 2, // заголовок - верхний и нижний - VMenuColorText = 3, // Текст пункта - VMenuColorHighlight = 4, // HotKey - VMenuColorSeparator = 5, // separator - VMenuColorSelected = 6, // Выбранный - VMenuColorHSelect = 7, // Выбранный - HotKey - VMenuColorScrollBar = 8, // ScrollBar - VMenuColorDisabled = 9, // Disabled - VMenuColorArrows =10, // '<' & '>' обычные - VMenuColorArrowsSelect =11, // '<' & '>' выбранные - VMenuColorArrowsDisabled =12, // '<' & '>' Disabled - VMenuColorGrayed =13, // "серый" - VMenuColorSelGrayed =14, // выбранный "серый" - - VMENU_COLOR_COUNT, // всегда последняя - размерность массива + Body = 0, // подложка + Box = 1, // рамка + Title = 2, // заголовок - верхний и нижний + Text = 3, // Текст пункта + Highlight = 4, // HotKey + Separator = 5, // separator + Selected = 6, // Выбранный + HSelect = 7, // Выбранный - HotKey + ScrollBar = 8, // ScrollBar + Disabled = 9, // Disabled + Arrows =10, // '<' & '>' обычные + ArrowsSelect =11, // '<' & '>' выбранные + ArrowsDisabled =12, // '<' & '>' Disabled + Grayed =13, // "серый" + SelGrayed =14, // выбранный "серый" + + COUNT, // всегда последняя - размерность массива }; enum VMENU_FLAGS @@ -100,6 +101,7 @@ enum VMENU_FLAGS VMENU_COMBOBOXEVENTMOUSE = 31_bit, // посылать события мыши в диалоговую проц. для открытого комбобокса }; +class window; class Dialog; class SaveScreen; @@ -166,17 +168,17 @@ struct MenuItemEx: menu_item size_t AutoHotkeyPos{}; short Len[2]{}; // размеры 2-х частей short Idx2{}; // начало 2-й части - std::list> Annotations; + std::vector> Annotations; }; +struct item_layout; + struct SortItemParam { bool Reverse; int Offset; }; -class window; - class VMenu final: public Modal { struct private_tag { explicit private_tag() = default; }; @@ -201,14 +203,12 @@ class VMenu final: public Modal void ShowConsoleTitle() override; void OnClose() override; - void FastShow() { ShowMenu(); } void ResetCursor(); void SetTitle(string_view Title); void SetBottomTitle(string_view BottomTitle); string &GetBottomTitle(string &strDest) const; void SetDialogStyle(bool Style) { ChangeFlags(VMENU_WARNDIALOG, Style); SetColors(nullptr); } void SetUpdateRequired(bool SetUpdate) { ChangeFlags(VMENU_UPDATEREQUIRED, SetUpdate); } - void SetBoxType(int BoxType); void SetMenuFlags(DWORD Flags) { VMFlags.Set(Flags); } void ClearFlags(DWORD Flags) { VMFlags.Clear(Flags); } bool CheckFlags(DWORD Flags) const { return VMFlags.Check(Flags); } @@ -292,23 +292,35 @@ class VMenu final: public Modal static std::vector AddHotkeys(std::span MenuItems); static bool ClickHandler(window* Menu, int MenuClick); - size_t MaxItemLength() const; - size_t GetServiceAreaSize(); + [[nodiscard]] size_t GetNaturalMenuWidth() const; private: + friend struct item_layout; + void init(std::span Data, DWORD Flags); void DisplayObject() override; + void DrawMenu(); - void ShowMenu(bool IsParent = false); + [[nodiscard]] static int CalculateBoxType(BitFlags Flags) noexcept; + [[nodiscard]] int CalculateBoxType() const noexcept { return CalculateBoxType(VMFlags); } + rectangle GetClientRect(int BoxType) const noexcept; void DrawTitles() const; + int AdjustTopPos(int ClientHeight); // Sets TopPos + void DrawSeparator(size_t CurItemIndex, int BoxType, int Y) const; + void ConnectSeparator(size_t CurItemIndex, string& separator, int BoxType) const; + void ApplySeparatorName(const MenuItemEx& CurItem, string& separator) const; + void DrawRegularItem(const MenuItemEx& CurItem, const item_layout& Layout, int Y, std::vector& HighlightMarkup, const string& BlankLine) const; + + [[nodiscard]] size_t CalculateMaxLineWidth() const; + int GetItemPosition(int Position) const; 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) const; - bool SetItemShowPos(int Item, int NewShowPos); // Negative NewShowPos is relative to the right side; -1 aligns the item to the right - bool ShiftItemShowPos(int Item,int Shift); // Shifts item's ShowPos; if Shift is positive, the item visually moves left + size_t GetItemMaxShowPos(int Item, size_t MaxLineWidth) const; + bool SetItemShowPos(int Item, int NewShowPos, size_t MaxLineWidth); // Negative NewShowPos is relative to the right side; -1 aligns the item to the right + bool ShiftItemShowPos(int Item,int Shift, size_t MaxLineWidth); // Shifts item's ShowPos; if Shift is positive, the item visually moves left bool SetAllItemsShowPos(int NewShowPos); bool ShiftAllItemsShowPos(int Shift); void UpdateMaxLengthFromTitles(); @@ -318,6 +330,8 @@ class VMenu final: public Modal void UpdateSelectPos(); void EnableFilter(bool Enable); + [[nodiscard]] FarColor GetColor(vmenu_colors ColorIndex) const noexcept { return Colors[std::to_underlying(ColorIndex)]; } + void SetColor(vmenu_colors ColorIndex) const { ::SetColor(GetColor(ColorIndex)); } size_t Text(string_view Str) const; size_t Text(wchar_t Char) const; @@ -329,7 +343,6 @@ class VMenu final: public Modal int MaxHeight; bool WasAutoHeight{}; size_t m_MaxItemLength{}; - int m_BoxType; window_ptr CurrentWindow; bool PrevCursorVisible{}; size_t PrevCursorSize{}; @@ -344,8 +357,7 @@ class VMenu final: public Modal std::vector Items; intptr_t ItemHiddenCount{}; intptr_t ItemSubMenusCount{}; - FarColor Colors[VMENU_COLOR_COUNT]{}; - size_t MaxLineWidth{}; + FarColor Colors[std::to_underlying(vmenu_colors::COUNT)]{}; bool bRightBtnPressed{}; UUID MenuId; }; diff --git a/far/vmenu2.cpp b/far/vmenu2.cpp index e0159eb5cc4..4f606606500 100644 --- a/far/vmenu2.cpp +++ b/far/vmenu2.cpp @@ -243,7 +243,7 @@ void VMenu2::Resize(bool force) ScrX + 1, m_X2 > 0? m_X2 - X1 + 1 : - static_cast(ListBox().MaxItemLength() + ListBox().GetServiceAreaSize() + (m_BoxType == box_type::full? 4 : 0)) + static_cast(ListBox().GetNaturalMenuWidth() + (m_BoxType == box_type::full? 4 : 0)) ); int height=GetShowItemCount();