summaryrefslogtreecommitdiffstats
path: root/sc/source/ui/cctrl/checklistmenu.cxx
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 09:06:44 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 09:06:44 +0000
commited5640d8b587fbcfed7dd7967f3de04b37a76f26 (patch)
tree7a5f7c6c9d02226d7471cb3cc8fbbf631b415303 /sc/source/ui/cctrl/checklistmenu.cxx
parentInitial commit. (diff)
downloadlibreoffice-ed5640d8b587fbcfed7dd7967f3de04b37a76f26.tar.xz
libreoffice-ed5640d8b587fbcfed7dd7967f3de04b37a76f26.zip
Adding upstream version 4:7.4.7.upstream/4%7.4.7upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'sc/source/ui/cctrl/checklistmenu.cxx')
-rw-r--r--sc/source/ui/cctrl/checklistmenu.cxx1737
1 files changed, 1737 insertions, 0 deletions
diff --git a/sc/source/ui/cctrl/checklistmenu.cxx b/sc/source/ui/cctrl/checklistmenu.cxx
new file mode 100644
index 000000000..82e7488c9
--- /dev/null
+++ b/sc/source/ui/cctrl/checklistmenu.cxx
@@ -0,0 +1,1737 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/*
+ * This file is part of the LibreOffice project.
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ *
+ * This file incorporates work covered by the following license notice:
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed
+ * with this work for additional information regarding copyright
+ * ownership. The ASF licenses this file to you under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file
+ * except in compliance with the License. You may obtain a copy of
+ * the License at http://www.apache.org/licenses/LICENSE-2.0 .
+ */
+
+#include <checklistmenu.hxx>
+#include <o3tl/safeint.hxx>
+#include <o3tl/string_view.hxx>
+#include <globstr.hrc>
+#include <scresid.hxx>
+
+#include <vcl/commandevent.hxx>
+#include <vcl/decoview.hxx>
+#include <vcl/event.hxx>
+#include <vcl/settings.hxx>
+#include <vcl/svapp.hxx>
+#include <vcl/virdev.hxx>
+#include <rtl/math.hxx>
+#include <unotools/charclass.hxx>
+#include <comphelper/lok.hxx>
+#include <LibreOfficeKit/LibreOfficeKitEnums.h>
+#include <tools/json_writer.hxx>
+#include <svl/numformat.hxx>
+
+#include <document.hxx>
+#include <viewdata.hxx>
+
+using namespace com::sun::star;
+using ::com::sun::star::uno::Reference;
+
+ScCheckListMenuControl::MenuItemData::MenuItemData()
+ : mbEnabled(true)
+{
+}
+
+ScCheckListMenuControl::SubMenuItemData::SubMenuItemData(ScCheckListMenuControl* pParent)
+ : maTimer("sc SubMenuItemData maTimer")
+ , mpSubMenu(nullptr)
+ , mnMenuPos(MENU_NOT_SELECTED)
+ , mpParent(pParent)
+{
+ maTimer.SetInvokeHandler(LINK(this, ScCheckListMenuControl::SubMenuItemData, TimeoutHdl));
+ maTimer.SetTimeout(Application::GetSettings().GetMouseSettings().GetMenuDelay());
+}
+
+void ScCheckListMenuControl::SubMenuItemData::reset()
+{
+ mpSubMenu = nullptr;
+ mnMenuPos = MENU_NOT_SELECTED;
+ maTimer.Stop();
+}
+
+IMPL_LINK_NOARG(ScCheckListMenuControl::SubMenuItemData, TimeoutHdl, Timer *, void)
+{
+ mpParent->handleMenuTimeout(this);
+}
+
+IMPL_LINK_NOARG(ScCheckListMenuControl, RowActivatedHdl, weld::TreeView&, bool)
+{
+ executeMenuItem(mxMenu->get_selected_index());
+ return true;
+}
+
+IMPL_LINK(ScCheckListMenuControl, MenuKeyInputHdl, const KeyEvent&, rKEvt, bool)
+{
+ const vcl::KeyCode& rKeyCode = rKEvt.GetKeyCode();
+
+ switch (rKeyCode.GetCode())
+ {
+ case KEY_RIGHT:
+ {
+ if (mnSelectedMenu >= maMenuItems.size() || mnSelectedMenu == MENU_NOT_SELECTED)
+ break;
+
+ const MenuItemData& rMenu = maMenuItems[mnSelectedMenu];
+ if (!rMenu.mxSubMenuWin)
+ break;
+
+ executeMenuItem(mnSelectedMenu);
+ }
+ }
+
+ return false;
+}
+
+IMPL_LINK_NOARG(ScCheckListMenuControl, SelectHdl, weld::TreeView&, void)
+{
+ sal_uInt32 nSelectedMenu = MENU_NOT_SELECTED;
+ if (!mxMenu->get_selected(mxScratchIter.get()))
+ {
+ // reselect current item if its submenu is up and the launching item
+ // became unselected by mouse moving out of the top level menu
+ if (mnSelectedMenu < maMenuItems.size() &&
+ maMenuItems[mnSelectedMenu].mxSubMenuWin &&
+ maMenuItems[mnSelectedMenu].mxSubMenuWin->IsVisible())
+ {
+ mxMenu->select(mnSelectedMenu);
+ return;
+ }
+ }
+ else
+ nSelectedMenu = mxMenu->get_iter_index_in_parent(*mxScratchIter);
+
+ setSelectedMenuItem(nSelectedMenu);
+}
+
+void ScCheckListMenuControl::addMenuItem(const OUString& rText, Action* pAction)
+{
+ MenuItemData aItem;
+ aItem.mbEnabled = true;
+ aItem.mxAction.reset(pAction);
+ maMenuItems.emplace_back(std::move(aItem));
+
+ mxMenu->show();
+ mxMenu->append_text(rText);
+ mxMenu->set_image(mxMenu->n_children() - 1, css::uno::Reference<css::graphic::XGraphic>(), 1);
+}
+
+void ScCheckListMenuControl::addSeparator()
+{
+ MenuItemData aItem;
+ maMenuItems.emplace_back(std::move(aItem));
+
+ mxMenu->append_separator("separator" + OUString::number(maMenuItems.size()));
+}
+
+IMPL_LINK(ScCheckListMenuControl, TreeSizeAllocHdl, const Size&, rSize, void)
+{
+ if (maAllocatedSize == rSize)
+ return;
+ maAllocatedSize = rSize;
+ SetDropdownPos();
+ if (!mnAsyncSetDropdownPosId && Application::GetToolkitName().startsWith("gtk"))
+ {
+ // for gtk retry again later in case it didn't work (wayland)
+ mnAsyncSetDropdownPosId = Application::PostUserEvent(LINK(this, ScCheckListMenuControl, SetDropdownPosHdl));
+ }
+}
+
+void ScCheckListMenuControl::SetDropdownPos()
+{
+ std::vector<int> aWidths
+ {
+ o3tl::narrowing<int>(maAllocatedSize.Width() - (mxMenu->get_text_height() * 3) / 4 - 6)
+ };
+ mxMenu->set_column_fixed_widths(aWidths);
+}
+
+IMPL_LINK_NOARG(ScCheckListMenuControl, SetDropdownPosHdl, void*, void)
+{
+ mnAsyncSetDropdownPosId = nullptr;
+ SetDropdownPos();
+ mxMenu->queue_resize();
+}
+
+void ScCheckListMenuControl::CreateDropDown()
+{
+ int nWidth = (mxMenu->get_text_height() * 3) / 4;
+ mxDropDown->SetOutputSizePixel(Size(nWidth, nWidth));
+ DecorationView aDecoView(mxDropDown.get());
+ aDecoView.DrawSymbol(tools::Rectangle(Point(0, 0), Size(nWidth, nWidth)),
+ SymbolType::SPIN_RIGHT, mxDropDown->GetTextColor(),
+ DrawSymbolFlags::NONE);
+}
+
+ScListSubMenuControl* ScCheckListMenuControl::addSubMenuItem(const OUString& rText, bool bEnabled, bool bColorMenu)
+{
+ MenuItemData aItem;
+ aItem.mbEnabled = bEnabled;
+
+ aItem.mxSubMenuWin.reset(new ScListSubMenuControl(mxMenu.get(), *this, bColorMenu));
+ maMenuItems.emplace_back(std::move(aItem));
+
+ mxMenu->show();
+ mxMenu->append_text(rText);
+ mxMenu->set_image(mxMenu->n_children() - 1, *mxDropDown, 1);
+ return maMenuItems.back().mxSubMenuWin.get();
+}
+
+void ScCheckListMenuControl::executeMenuItem(size_t nPos)
+{
+ if (nPos >= maMenuItems.size())
+ return;
+
+ const MenuItemData& rMenu = maMenuItems[nPos];
+ if (rMenu.mxSubMenuWin)
+ {
+ if (rMenu.mbEnabled)
+ {
+ maOpenTimer.mnMenuPos = nPos;
+ maOpenTimer.mpSubMenu = rMenu.mxSubMenuWin.get();
+ launchSubMenu();
+ }
+ return;
+ }
+
+ if (!maMenuItems[nPos].mxAction)
+ // no action is defined.
+ return;
+
+ const bool bClosePopup = maMenuItems[nPos].mxAction->execute();
+ if (bClosePopup)
+ terminateAllPopupMenus();
+}
+
+void ScCheckListMenuControl::setSelectedMenuItem(size_t nPos)
+{
+ if (mnSelectedMenu == nPos)
+ // nothing to do.
+ return;
+
+ selectMenuItem(nPos, /*bSubMenuTimer*/true);
+}
+
+void ScCheckListMenuControl::handleMenuTimeout(const SubMenuItemData* pTimer)
+{
+ if (pTimer == &maOpenTimer)
+ {
+ // Close any open submenu immediately.
+ if (maCloseTimer.mpSubMenu)
+ {
+ maCloseTimer.mpSubMenu->EndPopupMode();
+ maCloseTimer.mpSubMenu = nullptr;
+ maCloseTimer.maTimer.Stop();
+ }
+
+ launchSubMenu();
+ }
+ else if (pTimer == &maCloseTimer)
+ {
+ // end submenu.
+ if (maCloseTimer.mpSubMenu)
+ {
+ maCloseTimer.mpSubMenu->EndPopupMode();
+ maCloseTimer.mpSubMenu = nullptr;
+
+ // EndPopup sends a user event, and we want this focus to be set after that has done its conflicting focus-setting work
+ if (!mnAsyncPostPopdownId)
+ mnAsyncPostPopdownId = Application::PostUserEvent(LINK(this, ScCheckListMenuControl, PostPopdownHdl));
+ }
+ }
+}
+
+void ScCheckListMenuControl::queueLaunchSubMenu(size_t nPos, ScListSubMenuControl* pMenu)
+{
+ if (!pMenu)
+ return;
+
+ // Set the submenu on launch queue.
+ if (maOpenTimer.mpSubMenu)
+ {
+ if (maOpenTimer.mpSubMenu != pMenu)
+ {
+ // new submenu is being requested.
+ queueCloseSubMenu();
+ }
+ else
+ {
+ if (pMenu == maCloseTimer.mpSubMenu)
+ maCloseTimer.reset();
+ }
+ }
+
+ maOpenTimer.mpSubMenu = pMenu;
+ maOpenTimer.mnMenuPos = nPos;
+ if (comphelper::LibreOfficeKit::isActive())
+ maOpenTimer.maTimer.Invoke();
+ else
+ maOpenTimer.maTimer.Start();
+}
+
+void ScCheckListMenuControl::queueCloseSubMenu()
+{
+ if (!maOpenTimer.mpSubMenu)
+ // There is no submenu to close.
+ return;
+
+ // Stop any submenu on queue for opening.
+ maOpenTimer.maTimer.Stop();
+
+ // Flush any pending close so it doesn't get skipped
+ if (maCloseTimer.mpSubMenu)
+ {
+ maCloseTimer.mpSubMenu->EndPopupMode();
+ }
+
+ maCloseTimer.mpSubMenu = maOpenTimer.mpSubMenu;
+ maCloseTimer.mnMenuPos = maOpenTimer.mnMenuPos;
+ maOpenTimer.mpSubMenu = nullptr;
+ maOpenTimer.mnMenuPos = MENU_NOT_SELECTED;
+
+ if (comphelper::LibreOfficeKit::isActive())
+ maCloseTimer.maTimer.Invoke();
+ else
+ maCloseTimer.maTimer.Start();
+}
+
+tools::Rectangle ScCheckListMenuControl::GetSubMenuParentRect()
+{
+ if (!mxMenu->get_selected(mxScratchIter.get()))
+ return tools::Rectangle();
+ return mxMenu->get_row_area(*mxScratchIter);
+}
+
+void ScCheckListMenuControl::launchSubMenu()
+{
+ ScListSubMenuControl* pSubMenu = maOpenTimer.mpSubMenu;
+ if (!pSubMenu)
+ return;
+
+ if (!mxMenu->get_selected(mxScratchIter.get()))
+ return;
+
+ tools::Rectangle aRect = GetSubMenuParentRect();
+ pSubMenu->StartPopupMode(mxMenu.get(), aRect);
+
+ mxMenu->select(*mxScratchIter);
+ pSubMenu->GrabFocus();
+}
+
+IMPL_LINK_NOARG(ScCheckListMenuControl, PostPopdownHdl, void*, void)
+{
+ mnAsyncPostPopdownId = nullptr;
+ mxMenu->grab_focus();
+}
+
+IMPL_LINK(ScCheckListMenuControl, MouseEnterHdl, const MouseEvent&, rMEvt, bool)
+{
+ if (!rMEvt.IsEnterWindow())
+ return false;
+ selectMenuItem(MENU_NOT_SELECTED, true);
+ return false;
+}
+
+void ScCheckListMenuControl::endSubMenu(ScListSubMenuControl& rSubMenu)
+{
+ rSubMenu.EndPopupMode();
+ maOpenTimer.reset();
+
+ // EndPopup sends a user event, and we want this focus to be set after that has done its conflicting focus-setting work
+ if (!mnAsyncPostPopdownId)
+ mnAsyncPostPopdownId = Application::PostUserEvent(LINK(this, ScCheckListMenuControl, PostPopdownHdl));
+
+ size_t nMenuPos = getSubMenuPos(&rSubMenu);
+ if (nMenuPos != MENU_NOT_SELECTED)
+ {
+ mnSelectedMenu = nMenuPos;
+ mxMenu->select(mnSelectedMenu);
+ }
+}
+
+void ScCheckListMenuControl::selectMenuItem(size_t nPos, bool bSubMenuTimer)
+{
+ mxMenu->select(nPos == MENU_NOT_SELECTED ? -1 : nPos);
+ mnSelectedMenu = nPos;
+
+ if (nPos >= maMenuItems.size() || nPos == MENU_NOT_SELECTED)
+ {
+ queueCloseSubMenu();
+ return;
+ }
+
+ if (!maMenuItems[nPos].mbEnabled)
+ {
+ queueCloseSubMenu();
+ return;
+ }
+
+ if (bSubMenuTimer)
+ {
+ if (maMenuItems[nPos].mxSubMenuWin && mxMenu->changed_by_hover())
+ {
+ ScListSubMenuControl* pSubMenu = maMenuItems[nPos].mxSubMenuWin.get();
+ queueLaunchSubMenu(nPos, pSubMenu);
+ }
+ else
+ queueCloseSubMenu();
+ }
+}
+
+void ScCheckListMenuControl::clearSelectedMenuItem()
+{
+ selectMenuItem(MENU_NOT_SELECTED, false);
+}
+
+size_t ScCheckListMenuControl::getSubMenuPos(const ScListSubMenuControl* pSubMenu)
+{
+ size_t n = maMenuItems.size();
+ for (size_t i = 0; i < n; ++i)
+ {
+ if (maMenuItems[i].mxSubMenuWin.get() == pSubMenu)
+ return i;
+ }
+ return MENU_NOT_SELECTED;
+}
+
+void ScCheckListMenuControl::setSubMenuFocused(const ScListSubMenuControl* pSubMenu)
+{
+ maCloseTimer.reset();
+ size_t nMenuPos = getSubMenuPos(pSubMenu);
+ if (mnSelectedMenu != nMenuPos)
+ {
+ mnSelectedMenu = nMenuPos;
+ mxMenu->select(mnSelectedMenu);
+ }
+}
+
+void ScCheckListMenuControl::EndPopupMode()
+{
+ if (!mbIsPoppedUp)
+ return;
+ mxPopover->connect_closed(Link<weld::Popover&, void>());
+ mxPopover->popdown();
+ PopupModeEndHdl(*mxPopover);
+ assert(mbIsPoppedUp == false);
+}
+
+void ScCheckListMenuControl::StartPopupMode(weld::Widget* pParent, const tools::Rectangle& rRect)
+{
+ mxPopover->connect_closed(LINK(this, ScCheckListMenuControl, PopupModeEndHdl));
+ mbIsPoppedUp = true;
+ mxPopover->popup_at_rect(pParent, rRect);
+ GrabFocus();
+}
+
+void ScCheckListMenuControl::terminateAllPopupMenus()
+{
+ EndPopupMode();
+}
+
+ScCheckListMenuControl::Config::Config() :
+ mbAllowEmptySet(true), mbRTL(false)
+{
+}
+
+ScCheckListMember::ScCheckListMember()
+ : mnValue(0.0)
+ , mbVisible(true)
+ , mbDate(false)
+ , mbLeaf(false)
+ , mbValue(false)
+ , meDatePartType(YEAR)
+{
+}
+
+// the value of border-width of FilterDropDown
+constexpr int nBorderWidth = 4;
+// number of rows visible in checklist
+constexpr int nCheckListVisibleRows = 9;
+// number of rows visible in colorlist
+constexpr int nColorListVisibleRows = 9;
+
+ScCheckListMenuControl::ScCheckListMenuControl(weld::Widget* pParent, ScViewData& rViewData,
+ bool bHasDates, int nWidth)
+ : mxBuilder(Application::CreateBuilder(pParent, "modules/scalc/ui/filterdropdown.ui"))
+ , mxPopover(mxBuilder->weld_popover("FilterDropDown"))
+ , mxContainer(mxBuilder->weld_container("container"))
+ , mxMenu(mxBuilder->weld_tree_view("menu"))
+ , mxScratchIter(mxMenu->make_iterator())
+ , mxNonMenu(mxBuilder->weld_widget("nonmenu"))
+ , mxEdSearch(mxBuilder->weld_entry("search_edit"))
+ , mxBox(mxBuilder->weld_widget("box"))
+ , mxListChecks(mxBuilder->weld_tree_view("check_list_box"))
+ , mxTreeChecks(mxBuilder->weld_tree_view("check_tree_box"))
+ , mxChkToggleAll(mxBuilder->weld_check_button("toggle_all"))
+ , mxBtnSelectSingle(mxBuilder->weld_button("select_current"))
+ , mxBtnUnselectSingle(mxBuilder->weld_button("unselect_current"))
+ , mxButtonBox(mxBuilder->weld_box("buttonbox"))
+ , mxBtnOk(mxBuilder->weld_button("ok"))
+ , mxBtnCancel(mxBuilder->weld_button("cancel"))
+ , mxContextMenu(mxBuilder->weld_menu("contextmenu"))
+ , mxDropDown(mxMenu->create_virtual_device())
+ , mnCheckWidthReq(-1)
+ , mnWndWidth(0)
+ , mnCheckListVisibleRows(nCheckListVisibleRows)
+ , mePrevToggleAllState(TRISTATE_INDET)
+ , mnSelectedMenu(MENU_NOT_SELECTED)
+ , mrViewData(rViewData)
+ , mnAsyncPostPopdownId(nullptr)
+ , mnAsyncSetDropdownPosId(nullptr)
+ , mbHasDates(bHasDates)
+ , mbIsPoppedUp(false)
+ , maOpenTimer(this)
+ , maCloseTimer(this)
+{
+ mxTreeChecks->set_clicks_to_toggle(1);
+ mxListChecks->set_clicks_to_toggle(1);
+
+ mxNonMenu->connect_mouse_move(LINK(this, ScCheckListMenuControl, MouseEnterHdl));
+ mxEdSearch->connect_mouse_move(LINK(this, ScCheckListMenuControl, MouseEnterHdl));
+ mxListChecks->connect_mouse_move(LINK(this, ScCheckListMenuControl, MouseEnterHdl));
+ mxTreeChecks->connect_mouse_move(LINK(this, ScCheckListMenuControl, MouseEnterHdl));
+ mxListChecks->connect_popup_menu(LINK(this, ScCheckListMenuControl, CommandHdl));
+ mxTreeChecks->connect_popup_menu(LINK(this, ScCheckListMenuControl, CommandHdl));
+ mxChkToggleAll->connect_mouse_move(LINK(this, ScCheckListMenuControl, MouseEnterHdl));
+ mxBtnSelectSingle->connect_mouse_move(LINK(this, ScCheckListMenuControl, MouseEnterHdl));
+ mxBtnUnselectSingle->connect_mouse_move(LINK(this, ScCheckListMenuControl, MouseEnterHdl));
+ mxBtnOk->connect_mouse_move(LINK(this, ScCheckListMenuControl, MouseEnterHdl));
+ mxBtnCancel->connect_mouse_move(LINK(this, ScCheckListMenuControl, MouseEnterHdl));
+
+ /*
+ tdf#136559 If we have no dates we don't need a tree
+ structure, just a list. GtkListStore can be then
+ used which is much faster than a GtkTreeStore, so
+ with no dates switch to the treeview which uses the
+ faster GtkListStore
+ */
+ if (mbHasDates)
+ mpChecks = mxTreeChecks.get();
+ else
+ {
+ mxTreeChecks->hide();
+ mxListChecks->show();
+ mpChecks = mxListChecks.get();
+ }
+
+ int nChecksHeight = mxTreeChecks->get_height_rows(mnCheckListVisibleRows);
+ if (nWidth != -1)
+ {
+ mnCheckWidthReq = nWidth - nBorderWidth * 2 - 4;
+ mxTreeChecks->set_size_request(mnCheckWidthReq, nChecksHeight);
+ mxListChecks->set_size_request(mnCheckWidthReq, nChecksHeight);
+ }
+
+ // sort ok/cancel into native order, if this was a dialog they would be auto-sorted, but this
+ // popup isn't a true dialog
+ mxButtonBox->sort_native_button_order();
+
+ mxTreeChecks->enable_toggle_buttons(weld::ColumnToggleType::Check);
+ mxListChecks->enable_toggle_buttons(weld::ColumnToggleType::Check);
+
+ mxBox->show();
+ mxEdSearch->show();
+ mxButtonBox->show();
+
+ mxMenu->connect_row_activated(LINK(this, ScCheckListMenuControl, RowActivatedHdl));
+ mxMenu->connect_changed(LINK(this, ScCheckListMenuControl, SelectHdl));
+ mxMenu->connect_key_press(LINK(this, ScCheckListMenuControl, MenuKeyInputHdl));
+
+ mxBtnOk->connect_clicked(LINK(this, ScCheckListMenuControl, ButtonHdl));
+ mxBtnCancel->connect_clicked(LINK(this, ScCheckListMenuControl, ButtonHdl));
+ mxEdSearch->connect_changed(LINK(this, ScCheckListMenuControl, EdModifyHdl));
+ mxEdSearch->connect_activate(LINK(this, ScCheckListMenuControl, EdActivateHdl));
+ mxTreeChecks->connect_toggled(LINK(this, ScCheckListMenuControl, CheckHdl));
+ mxTreeChecks->connect_key_press(LINK(this, ScCheckListMenuControl, KeyInputHdl));
+ mxListChecks->connect_toggled(LINK(this, ScCheckListMenuControl, CheckHdl));
+ mxListChecks->connect_key_press(LINK(this, ScCheckListMenuControl, KeyInputHdl));
+ mxChkToggleAll->connect_toggled(LINK(this, ScCheckListMenuControl, TriStateHdl));
+ mxBtnSelectSingle->connect_clicked(LINK(this, ScCheckListMenuControl, ButtonHdl));
+ mxBtnUnselectSingle->connect_clicked(LINK(this, ScCheckListMenuControl, ButtonHdl));
+
+ CreateDropDown();
+ mxMenu->connect_size_allocate(LINK(this, ScCheckListMenuControl, TreeSizeAllocHdl));
+
+ // determine what width the checklist will end up with
+ mnCheckWidthReq = mxContainer->get_preferred_size().Width();
+ // make that size fixed now, we can now use mnCheckWidthReq to speed up
+ // bulk_insert_for_each
+ mxTreeChecks->set_size_request(mnCheckWidthReq, nChecksHeight);
+ mxListChecks->set_size_request(mnCheckWidthReq, nChecksHeight);
+}
+
+void ScCheckListMenuControl::GrabFocus()
+{
+ if (mxEdSearch->get_visible())
+ mxEdSearch->grab_focus();
+ else
+ {
+ mxMenu->set_cursor(0);
+ mxMenu->grab_focus();
+ }
+}
+
+void ScCheckListMenuControl::DropPendingEvents()
+{
+ if (mnAsyncPostPopdownId)
+ {
+ Application::RemoveUserEvent(mnAsyncPostPopdownId);
+ mnAsyncPostPopdownId = nullptr;
+ }
+ if (mnAsyncSetDropdownPosId)
+ {
+ Application::RemoveUserEvent(mnAsyncSetDropdownPosId);
+ mnAsyncSetDropdownPosId = nullptr;
+ }
+}
+
+ScCheckListMenuControl::~ScCheckListMenuControl()
+{
+ EndPopupMode();
+ for (auto& rMenuItem : maMenuItems)
+ rMenuItem.mxSubMenuWin.reset();
+ DropPendingEvents();
+}
+
+void ScCheckListMenuControl::prepWindow()
+{
+ mxMenu->set_size_request(-1, mxMenu->get_preferred_size().Height() + 2);
+ mnSelectedMenu = MENU_NOT_SELECTED;
+ if (mxMenu->n_children())
+ {
+ mxMenu->set_cursor(0);
+ mxMenu->unselect_all();
+ }
+
+ mnWndWidth = mxContainer->get_preferred_size().Width() + nBorderWidth * 2 + 4;
+}
+
+void ScCheckListMenuControl::setAllMemberState(bool bSet)
+{
+ mpChecks->all_foreach([this, bSet](weld::TreeIter& rEntry){
+ mpChecks->set_toggle(rEntry, bSet ? TRISTATE_TRUE : TRISTATE_FALSE);
+ return false;
+ });
+
+ if (!maConfig.mbAllowEmptySet)
+ {
+ // We need to have at least one member selected.
+ mxBtnOk->set_sensitive(GetCheckedEntryCount() != 0);
+ }
+}
+
+void ScCheckListMenuControl::selectCurrentMemberOnly(bool bSet)
+{
+ setAllMemberState(!bSet);
+ std::unique_ptr<weld::TreeIter> xEntry = mpChecks->make_iterator();
+ if (!mpChecks->get_cursor(xEntry.get()))
+ return;
+ mpChecks->set_toggle(*xEntry, bSet ? TRISTATE_TRUE : TRISTATE_FALSE);
+}
+
+IMPL_LINK(ScCheckListMenuControl, CommandHdl, const CommandEvent&, rCEvt, bool)
+{
+ if (rCEvt.GetCommand() != CommandEventId::ContextMenu)
+ return false;
+
+ mxContextMenu->set_sensitive("less", mnCheckListVisibleRows > 4);
+ mxContextMenu->set_sensitive("more", mnCheckListVisibleRows < 42);
+
+ OString sCommand = mxContextMenu->popup_at_rect(mpChecks, tools::Rectangle(rCEvt.GetMousePosPixel(), Size(1,1)));
+ if (sCommand.isEmpty())
+ return true;
+
+ if (sCommand == "more")
+ ++mnCheckListVisibleRows;
+ else if (sCommand == "less")
+ --mnCheckListVisibleRows;
+ ResizeToRequest();
+
+ return true;
+}
+
+void ScCheckListMenuControl::ResizeToRequest()
+{
+ int nChecksHeight = mxTreeChecks->get_height_rows(mnCheckListVisibleRows);
+ mxTreeChecks->set_size_request(mnCheckWidthReq, nChecksHeight);
+ mxListChecks->set_size_request(mnCheckWidthReq, nChecksHeight);
+ mxPopover->resize_to_request();
+}
+
+IMPL_LINK(ScCheckListMenuControl, ButtonHdl, weld::Button&, rBtn, void)
+{
+ if (&rBtn == mxBtnOk.get())
+ close(true);
+ else if (&rBtn == mxBtnCancel.get())
+ close(false);
+ else if (&rBtn == mxBtnSelectSingle.get() || &rBtn == mxBtnUnselectSingle.get())
+ {
+ selectCurrentMemberOnly(&rBtn == mxBtnSelectSingle.get());
+ std::unique_ptr<weld::TreeIter> xEntry = mpChecks->make_iterator();
+ if (!mpChecks->get_cursor(xEntry.get()))
+ xEntry.reset();
+ Check(xEntry.get());
+ }
+}
+
+IMPL_LINK_NOARG(ScCheckListMenuControl, TriStateHdl, weld::Toggleable&, void)
+{
+ switch (mePrevToggleAllState)
+ {
+ case TRISTATE_FALSE:
+ mxChkToggleAll->set_state(TRISTATE_TRUE);
+ setAllMemberState(true);
+ break;
+ case TRISTATE_TRUE:
+ mxChkToggleAll->set_state(TRISTATE_FALSE);
+ setAllMemberState(false);
+ break;
+ case TRISTATE_INDET:
+ default:
+ mxChkToggleAll->set_state(TRISTATE_TRUE);
+ setAllMemberState(true);
+ break;
+ }
+
+ mePrevToggleAllState = mxChkToggleAll->get_state();
+}
+
+namespace
+{
+ void insertMember(weld::TreeView& rView, const weld::TreeIter& rIter, const ScCheckListMember& rMember, bool bChecked)
+ {
+ OUString aLabel = rMember.maName;
+ if (aLabel.isEmpty())
+ aLabel = ScResId(STR_EMPTYDATA);
+ rView.set_toggle(rIter, bChecked ? TRISTATE_TRUE : TRISTATE_FALSE);
+ rView.set_text(rIter, aLabel, 0);
+ }
+}
+
+IMPL_LINK_NOARG(ScCheckListMenuControl, EdModifyHdl, weld::Entry&, void)
+{
+ OUString aSearchText = mxEdSearch->get_text();
+ aSearchText = ScGlobal::getCharClass().lowercase( aSearchText );
+ bool bSearchTextEmpty = aSearchText.isEmpty();
+ size_t n = maMembers.size();
+ size_t nSelCount = 0;
+
+ // This branch is the general case, the other is an optimized variant of
+ // this one where we can take advantage of knowing we have no hierarchy
+ if (mbHasDates)
+ {
+ mpChecks->freeze();
+
+ bool bSomeDateDeletes = false;
+
+ for (size_t i = 0; i < n; ++i)
+ {
+ bool bIsDate = maMembers[i].mbDate;
+ bool bPartialMatch = false;
+
+ OUString aLabelDisp = maMembers[i].maName;
+ if ( aLabelDisp.isEmpty() )
+ aLabelDisp = ScResId( STR_EMPTYDATA );
+
+ if ( !bSearchTextEmpty )
+ {
+ if ( !bIsDate )
+ bPartialMatch = ( ScGlobal::getCharClass().lowercase( aLabelDisp ).indexOf( aSearchText ) != -1 );
+ else if ( maMembers[i].meDatePartType == ScCheckListMember::DAY ) // Match with both numerical and text version of month
+ bPartialMatch = (ScGlobal::getCharClass().lowercase( OUString(
+ maMembers[i].maRealName + maMembers[i].maDateParts[1] )).indexOf( aSearchText ) != -1);
+ else
+ continue;
+ }
+ else if ( bIsDate && maMembers[i].meDatePartType != ScCheckListMember::DAY )
+ continue;
+
+ if ( bSearchTextEmpty )
+ {
+ auto xLeaf = ShowCheckEntry(aLabelDisp, maMembers[i], true, maMembers[i].mbVisible);
+ updateMemberParents(xLeaf.get(), i);
+ if ( maMembers[i].mbVisible )
+ ++nSelCount;
+ continue;
+ }
+
+ if ( bPartialMatch )
+ {
+ auto xLeaf = ShowCheckEntry(aLabelDisp, maMembers[i]);
+ updateMemberParents(xLeaf.get(), i);
+ ++nSelCount;
+ }
+ else
+ {
+ ShowCheckEntry(aLabelDisp, maMembers[i], false, false);
+ if( bIsDate )
+ bSomeDateDeletes = true;
+ }
+ }
+
+ if ( bSomeDateDeletes )
+ {
+ for (size_t i = 0; i < n; ++i)
+ {
+ if (!maMembers[i].mbDate)
+ continue;
+ if (maMembers[i].meDatePartType != ScCheckListMember::DAY)
+ continue;
+ updateMemberParents(nullptr, i);
+ }
+ }
+
+ mpChecks->thaw();
+ }
+ else
+ {
+ mpChecks->freeze();
+
+ // when there are a lot of rows, it is cheaper to simply clear the tree and either
+ // re-initialise or just insert the filtered lines
+ mpChecks->clear();
+
+ mpChecks->thaw();
+
+ if (bSearchTextEmpty)
+ nSelCount = initMembers();
+ else
+ {
+ std::vector<size_t> aShownIndexes;
+
+ for (size_t i = 0; i < n; ++i)
+ {
+ assert(!maMembers[i].mbDate);
+
+ OUString aLabelDisp = maMembers[i].maName;
+ if ( aLabelDisp.isEmpty() )
+ aLabelDisp = ScResId( STR_EMPTYDATA );
+
+ bool bPartialMatch = ScGlobal::getCharClass().lowercase( aLabelDisp ).indexOf( aSearchText ) != -1;
+
+ if (!bPartialMatch)
+ continue;
+
+ aShownIndexes.push_back(i);
+ }
+
+ std::vector<int> aFixedWidths { mnCheckWidthReq };
+ // tdf#122419 insert in the fastest order, this might be backwards.
+ mpChecks->bulk_insert_for_each(aShownIndexes.size(), [this, &aShownIndexes, &nSelCount](weld::TreeIter& rIter, int i) {
+ size_t nIndex = aShownIndexes[i];
+ insertMember(*mpChecks, rIter, maMembers[nIndex], true);
+ ++nSelCount;
+ }, nullptr, &aFixedWidths);
+ }
+ }
+
+ if ( nSelCount == n )
+ mxChkToggleAll->set_state( TRISTATE_TRUE );
+ else if ( nSelCount == 0 )
+ mxChkToggleAll->set_state( TRISTATE_FALSE );
+ else
+ mxChkToggleAll->set_state( TRISTATE_INDET );
+
+ if ( !maConfig.mbAllowEmptySet )
+ {
+ const bool bEmptySet( nSelCount == 0 );
+ mpChecks->set_sensitive(!bEmptySet);
+ mxChkToggleAll->set_sensitive(!bEmptySet);
+ mxBtnSelectSingle->set_sensitive(!bEmptySet);
+ mxBtnUnselectSingle->set_sensitive(!bEmptySet);
+ mxBtnOk->set_sensitive(!bEmptySet);
+ }
+}
+
+IMPL_LINK_NOARG(ScCheckListMenuControl, EdActivateHdl, weld::Entry&, bool)
+{
+ if (mxBtnOk->get_sensitive())
+ close(true);
+ return true;
+}
+
+IMPL_LINK( ScCheckListMenuControl, CheckHdl, const weld::TreeView::iter_col&, rRowCol, void )
+{
+ Check(&rRowCol.first);
+}
+
+void ScCheckListMenuControl::Check(const weld::TreeIter* pEntry)
+{
+ if (pEntry)
+ CheckEntry(*pEntry, mpChecks->get_toggle(*pEntry) == TRISTATE_TRUE);
+ size_t nNumChecked = GetCheckedEntryCount();
+ if (nNumChecked == maMembers.size())
+ // all members visible
+ mxChkToggleAll->set_state(TRISTATE_TRUE);
+ else if (nNumChecked == 0)
+ // no members visible
+ mxChkToggleAll->set_state(TRISTATE_FALSE);
+ else
+ mxChkToggleAll->set_state(TRISTATE_INDET);
+
+ if (!maConfig.mbAllowEmptySet)
+ // We need to have at least one member selected.
+ mxBtnOk->set_sensitive(nNumChecked != 0);
+
+ mePrevToggleAllState = mxChkToggleAll->get_state();
+}
+
+void ScCheckListMenuControl::updateMemberParents(const weld::TreeIter* pLeaf, size_t nIdx)
+{
+ if ( !maMembers[nIdx].mbDate || maMembers[nIdx].meDatePartType != ScCheckListMember::DAY )
+ return;
+
+ OUString aYearName = maMembers[nIdx].maDateParts[0];
+ OUString aMonthName = maMembers[nIdx].maDateParts[1];
+ auto aItr = maYearMonthMap.find(aYearName + aMonthName);
+
+ if ( pLeaf )
+ {
+ std::unique_ptr<weld::TreeIter> xYearEntry;
+ std::unique_ptr<weld::TreeIter> xMonthEntry = mpChecks->make_iterator(pLeaf);
+ if (!mpChecks->iter_parent(*xMonthEntry))
+ xMonthEntry.reset();
+ else
+ {
+ xYearEntry = mpChecks->make_iterator(xMonthEntry.get());
+ if (!mpChecks->iter_parent(*xYearEntry))
+ xYearEntry.reset();
+ }
+
+ maMembers[nIdx].mxParent = std::move(xMonthEntry);
+ if ( aItr != maYearMonthMap.end() )
+ {
+ size_t nMonthIdx = aItr->second;
+ maMembers[nMonthIdx].mxParent = std::move(xYearEntry);
+ }
+ }
+ else
+ {
+ std::unique_ptr<weld::TreeIter> xYearEntry = FindEntry(nullptr, aYearName);
+ if (aItr != maYearMonthMap.end() && !xYearEntry)
+ {
+ size_t nMonthIdx = aItr->second;
+ maMembers[nMonthIdx].mxParent.reset();
+ maMembers[nIdx].mxParent.reset();
+ }
+ else if (xYearEntry && !FindEntry(xYearEntry.get(), aMonthName))
+ maMembers[nIdx].mxParent.reset();
+ }
+}
+
+void ScCheckListMenuControl::setMemberSize(size_t n)
+{
+ maMembers.reserve(n);
+}
+
+void ScCheckListMenuControl::addDateMember(const OUString& rsName, double nVal, bool bVisible)
+{
+ SvNumberFormatter* pFormatter = mrViewData.GetDocument().GetFormatTable();
+
+ // Convert the numeric date value to a date object.
+ Date aDate = pFormatter->GetNullDate();
+ aDate.AddDays(rtl::math::approxFloor(nVal));
+
+ sal_Int16 nYear = aDate.GetYear();
+ sal_uInt16 nMonth = aDate.GetMonth();
+ sal_uInt16 nDay = aDate.GetDay();
+
+ // Get the localized month name list.
+ CalendarWrapper& rCalendar = ScGlobal::GetCalendar();
+ uno::Sequence<i18n::CalendarItem2> aMonths = rCalendar.getMonths();
+ if (aMonths.getLength() < nMonth)
+ return;
+
+ OUString aYearName = OUString::number(nYear);
+ OUString aMonthName = aMonths[nMonth-1].FullName;
+ OUString aDayName = OUString::number(nDay);
+
+ if ( aDayName.getLength() == 1 )
+ aDayName = "0" + aDayName;
+
+ mpChecks->freeze();
+
+ std::unique_ptr<weld::TreeIter> xYearEntry = FindEntry(nullptr, aYearName);
+ if (!xYearEntry)
+ {
+ xYearEntry = mpChecks->make_iterator();
+ mpChecks->insert(nullptr, -1, nullptr, nullptr, nullptr, nullptr, false, xYearEntry.get());
+ mpChecks->set_toggle(*xYearEntry, TRISTATE_FALSE);
+ mpChecks->set_text(*xYearEntry, aYearName, 0);
+ ScCheckListMember aMemYear;
+ aMemYear.maName = aYearName;
+ aMemYear.maRealName = rsName;
+ aMemYear.mbDate = true;
+ aMemYear.mbLeaf = false;
+ aMemYear.mbVisible = bVisible;
+ aMemYear.mxParent.reset();
+ aMemYear.meDatePartType = ScCheckListMember::YEAR;
+ maMembers.emplace_back(std::move(aMemYear));
+ }
+
+ std::unique_ptr<weld::TreeIter> xMonthEntry = FindEntry(xYearEntry.get(), aMonthName);
+ if (!xMonthEntry)
+ {
+ xMonthEntry = mpChecks->make_iterator();
+ mpChecks->insert(xYearEntry.get(), -1, nullptr, nullptr, nullptr, nullptr, false, xMonthEntry.get());
+ mpChecks->set_toggle(*xMonthEntry, TRISTATE_FALSE);
+ mpChecks->set_text(*xMonthEntry, aMonthName, 0);
+ ScCheckListMember aMemMonth;
+ aMemMonth.maName = aMonthName;
+ aMemMonth.maRealName = rsName;
+ aMemMonth.mbDate = true;
+ aMemMonth.mbLeaf = false;
+ aMemMonth.mbVisible = bVisible;
+ aMemMonth.mxParent = std::move(xYearEntry);
+ aMemMonth.meDatePartType = ScCheckListMember::MONTH;
+ maMembers.emplace_back(std::move(aMemMonth));
+ maYearMonthMap[aYearName + aMonthName] = maMembers.size() - 1;
+ }
+
+ std::unique_ptr<weld::TreeIter> xDayEntry = FindEntry(xMonthEntry.get(), aDayName);
+ if (!xDayEntry)
+ {
+ xDayEntry = mpChecks->make_iterator();
+ mpChecks->insert(xMonthEntry.get(), -1, nullptr, nullptr, nullptr, nullptr, false, xDayEntry.get());
+ mpChecks->set_toggle(*xDayEntry, TRISTATE_FALSE);
+ mpChecks->set_text(*xDayEntry, aDayName, 0);
+ ScCheckListMember aMemDay;
+ aMemDay.maName = aDayName;
+ aMemDay.maRealName = rsName;
+ aMemDay.maDateParts.resize(2);
+ aMemDay.maDateParts[0] = aYearName;
+ aMemDay.maDateParts[1] = aMonthName;
+ aMemDay.mbDate = true;
+ aMemDay.mbLeaf = true;
+ aMemDay.mbVisible = bVisible;
+ aMemDay.mxParent = std::move(xMonthEntry);
+ aMemDay.meDatePartType = ScCheckListMember::DAY;
+ maMembers.emplace_back(std::move(aMemDay));
+ }
+
+ mpChecks->thaw();
+}
+
+void ScCheckListMenuControl::addMember(const OUString& rName, const double nVal, bool bVisible, bool bValue)
+{
+ ScCheckListMember aMember;
+ // tdf#46062 - indicate hidden whitespaces using quotes
+ aMember.maName = o3tl::trim(rName) != rName ? "\"" + rName + "\"" : rName;
+ aMember.maRealName = rName;
+ aMember.mnValue = nVal;
+ aMember.mbDate = false;
+ aMember.mbLeaf = true;
+ aMember.mbValue = bValue;
+ aMember.mbVisible = bVisible;
+ aMember.mxParent.reset();
+ maMembers.emplace_back(std::move(aMember));
+}
+
+std::unique_ptr<weld::TreeIter> ScCheckListMenuControl::FindEntry(const weld::TreeIter* pParent, std::u16string_view sNode)
+{
+ std::unique_ptr<weld::TreeIter> xEntry = mpChecks->make_iterator(pParent);
+ bool bEntry = pParent ? mpChecks->iter_children(*xEntry) : mpChecks->get_iter_first(*xEntry);
+ while (bEntry)
+ {
+ if (sNode == mpChecks->get_text(*xEntry, 0))
+ return xEntry;
+ bEntry = mpChecks->iter_next_sibling(*xEntry);
+ }
+ return nullptr;
+}
+
+void ScCheckListMenuControl::GetRecursiveChecked(const weld::TreeIter* pEntry, std::unordered_set<OUString>& vOut,
+ OUString& rLabel)
+{
+ if (mpChecks->get_toggle(*pEntry) != TRISTATE_TRUE)
+ return;
+
+ // We have to hash parents and children together.
+ // Per convention for easy access in getResult()
+ // "child;parent;grandparent" while descending.
+ if (rLabel.isEmpty())
+ rLabel = mpChecks->get_text(*pEntry, 0);
+ else
+ rLabel = mpChecks->get_text(*pEntry, 0) + ";" + rLabel;
+
+ // Prerequisite: the selection mechanism guarantees that if a child is
+ // selected then also the parent is selected, so we only have to
+ // inspect the children in case the parent is selected.
+ if (!mpChecks->iter_has_child(*pEntry))
+ return;
+
+ std::unique_ptr<weld::TreeIter> xChild(mpChecks->make_iterator(pEntry));
+ bool bChild = mpChecks->iter_children(*xChild);
+ while (bChild)
+ {
+ OUString aLabel = rLabel;
+ GetRecursiveChecked(xChild.get(), vOut, aLabel);
+ if (!aLabel.isEmpty() && aLabel != rLabel)
+ vOut.insert(aLabel);
+ bChild = mpChecks->iter_next_sibling(*xChild);
+ }
+ // Let the caller not add the parent alone.
+ rLabel.clear();
+}
+
+std::unordered_set<OUString> ScCheckListMenuControl::GetAllChecked()
+{
+ std::unordered_set<OUString> vResults(0);
+
+ std::unique_ptr<weld::TreeIter> xEntry = mpChecks->make_iterator();
+ bool bEntry = mpChecks->get_iter_first(*xEntry);
+ while (bEntry)
+ {
+ OUString aLabel;
+ GetRecursiveChecked(xEntry.get(), vResults, aLabel);
+ if (!aLabel.isEmpty())
+ vResults.insert(aLabel);
+ bEntry = mpChecks->iter_next_sibling(*xEntry);
+ }
+
+ return vResults;
+}
+
+bool ScCheckListMenuControl::IsChecked(std::u16string_view sName, const weld::TreeIter* pParent)
+{
+ std::unique_ptr<weld::TreeIter> xEntry = FindEntry(pParent, sName);
+ return xEntry && mpChecks->get_toggle(*xEntry) == TRISTATE_TRUE;
+}
+
+void ScCheckListMenuControl::CheckEntry(std::u16string_view sName, const weld::TreeIter* pParent, bool bCheck)
+{
+ std::unique_ptr<weld::TreeIter> xEntry = FindEntry(pParent, sName);
+ if (xEntry)
+ CheckEntry(*xEntry, bCheck);
+}
+
+// Recursively check all children of rParent
+void ScCheckListMenuControl::CheckAllChildren(const weld::TreeIter& rParent, bool bCheck)
+{
+ mpChecks->set_toggle(rParent, bCheck ? TRISTATE_TRUE : TRISTATE_FALSE);
+ std::unique_ptr<weld::TreeIter> xEntry = mpChecks->make_iterator(&rParent);
+ bool bEntry = mpChecks->iter_children(*xEntry);
+ while (bEntry)
+ {
+ CheckAllChildren(*xEntry, bCheck);
+ bEntry = mpChecks->iter_next_sibling(*xEntry);
+ }
+}
+
+void ScCheckListMenuControl::CheckEntry(const weld::TreeIter& rParent, bool bCheck)
+{
+ // recursively check all items below rParent
+ CheckAllChildren(rParent, bCheck);
+ // checking rParent can affect ancestors, e.g. if ancestor is unchecked and rParent is
+ // now checked then the ancestor needs to be checked also
+ if (!mpChecks->get_iter_depth(rParent))
+ return;
+
+ std::unique_ptr<weld::TreeIter> xAncestor(mpChecks->make_iterator(&rParent));
+ bool bAncestor = mpChecks->iter_parent(*xAncestor);
+ while (bAncestor)
+ {
+ // if any first level children checked then ancestor
+ // needs to be checked, similarly if no first level children
+ // checked then ancestor needs to be unchecked
+ std::unique_ptr<weld::TreeIter> xChild(mpChecks->make_iterator(xAncestor.get()));
+ bool bChild = mpChecks->iter_children(*xChild);
+ bool bChildChecked = false;
+
+ while (bChild)
+ {
+ if (mpChecks->get_toggle(*xChild) == TRISTATE_TRUE)
+ {
+ bChildChecked = true;
+ break;
+ }
+ bChild = mpChecks->iter_next_sibling(*xChild);
+ }
+ mpChecks->set_toggle(*xAncestor, bChildChecked ? TRISTATE_TRUE : TRISTATE_FALSE);
+ bAncestor = mpChecks->iter_parent(*xAncestor);
+ }
+}
+
+std::unique_ptr<weld::TreeIter> ScCheckListMenuControl::ShowCheckEntry(const OUString& sName, ScCheckListMember& rMember, bool bShow, bool bCheck)
+{
+ std::unique_ptr<weld::TreeIter> xEntry;
+ if (!rMember.mbDate || rMember.mxParent)
+ xEntry = FindEntry(rMember.mxParent.get(), sName);
+
+ if ( bShow )
+ {
+ if (!xEntry)
+ {
+ if (rMember.mbDate)
+ {
+ if (rMember.maDateParts.empty())
+ return nullptr;
+
+ std::unique_ptr<weld::TreeIter> xYearEntry = FindEntry(nullptr, rMember.maDateParts[0]);
+ if (!xYearEntry)
+ {
+ xYearEntry = mpChecks->make_iterator();
+ mpChecks->insert(nullptr, -1, nullptr, nullptr, nullptr, nullptr, false, xYearEntry.get());
+ mpChecks->set_toggle(*xYearEntry, TRISTATE_FALSE);
+ mpChecks->set_text(*xYearEntry, rMember.maDateParts[0], 0);
+ }
+ std::unique_ptr<weld::TreeIter> xMonthEntry = FindEntry(xYearEntry.get(), rMember.maDateParts[1]);
+ if (!xMonthEntry)
+ {
+ xMonthEntry = mpChecks->make_iterator();
+ mpChecks->insert(xYearEntry.get(), -1, nullptr, nullptr, nullptr, nullptr, false, xMonthEntry.get());
+ mpChecks->set_toggle(*xMonthEntry, TRISTATE_FALSE);
+ mpChecks->set_text(*xMonthEntry, rMember.maDateParts[1], 0);
+ }
+ std::unique_ptr<weld::TreeIter> xDayEntry = FindEntry(xMonthEntry.get(), rMember.maName);
+ if (!xDayEntry)
+ {
+ xDayEntry = mpChecks->make_iterator();
+ mpChecks->insert(xMonthEntry.get(), -1, nullptr, nullptr, nullptr, nullptr, false, xDayEntry.get());
+ mpChecks->set_toggle(*xDayEntry, TRISTATE_FALSE);
+ mpChecks->set_text(*xDayEntry, rMember.maName, 0);
+ }
+ return xDayEntry; // Return leaf node
+ }
+
+ xEntry = mpChecks->make_iterator();
+ mpChecks->append(xEntry.get());
+ mpChecks->set_toggle(*xEntry, bCheck ? TRISTATE_TRUE : TRISTATE_FALSE);
+ mpChecks->set_text(*xEntry, sName, 0);
+ }
+ else
+ CheckEntry(*xEntry, bCheck);
+ }
+ else if (xEntry)
+ {
+ mpChecks->remove(*xEntry);
+ if (rMember.mxParent)
+ {
+ std::unique_ptr<weld::TreeIter> xParent(mpChecks->make_iterator(rMember.mxParent.get()));
+ while (xParent && !mpChecks->iter_has_child(*xParent))
+ {
+ std::unique_ptr<weld::TreeIter> xTmp(mpChecks->make_iterator(xParent.get()));
+ if (!mpChecks->iter_parent(*xParent))
+ xParent.reset();
+ mpChecks->remove(*xTmp);
+ }
+ }
+ }
+ return nullptr;
+}
+
+int ScCheckListMenuControl::GetCheckedEntryCount() const
+{
+ int nRet = 0;
+
+ mpChecks->all_foreach([this, &nRet](weld::TreeIter& rEntry){
+ if (mpChecks->get_toggle(rEntry) == TRISTATE_TRUE)
+ ++nRet;
+ return false;
+ });
+
+ return nRet;
+}
+
+IMPL_LINK(ScCheckListMenuControl, KeyInputHdl, const KeyEvent&, rKEvt, bool)
+{
+ const vcl::KeyCode& rKey = rKEvt.GetKeyCode();
+
+ if ( rKey.GetCode() == KEY_RETURN || rKey.GetCode() == KEY_SPACE )
+ {
+ std::unique_ptr<weld::TreeIter> xEntry = mpChecks->make_iterator();
+ bool bEntry = mpChecks->get_cursor(xEntry.get());
+ if (bEntry)
+ {
+ bool bOldCheck = mpChecks->get_toggle(*xEntry) == TRISTATE_TRUE;
+ CheckEntry(*xEntry, !bOldCheck);
+ bool bNewCheck = mpChecks->get_toggle(*xEntry) == TRISTATE_TRUE;
+ if (bOldCheck != bNewCheck)
+ Check(xEntry.get());
+ }
+ return true;
+ }
+
+ return false;
+}
+
+size_t ScCheckListMenuControl::initMembers(int nMaxMemberWidth)
+{
+ size_t n = maMembers.size();
+ size_t nVisMemCount = 0;
+
+ if (nMaxMemberWidth == -1)
+ nMaxMemberWidth = mnCheckWidthReq;
+
+ if (!mpChecks->n_children() && !mbHasDates)
+ {
+ std::vector<int> aFixedWidths { nMaxMemberWidth };
+ // tdf#134038 insert in the fastest order, this might be backwards so only do it for
+ // the !mbHasDates case where no entry depends on another to exist before getting
+ // inserted. We cannot retain pre-existing treeview content, only clear and fill it.
+ mpChecks->bulk_insert_for_each(n, [this, &nVisMemCount](weld::TreeIter& rIter, int i) {
+ assert(!maMembers[i].mbDate);
+ insertMember(*mpChecks, rIter, maMembers[i], maMembers[i].mbVisible);
+ if (maMembers[i].mbVisible)
+ ++nVisMemCount;
+ }, nullptr, &aFixedWidths);
+ }
+ else
+ {
+ mpChecks->freeze();
+
+ std::unique_ptr<weld::TreeIter> xEntry = mpChecks->make_iterator();
+ std::vector<std::unique_ptr<weld::TreeIter>> aExpandRows;
+
+ for (size_t i = 0; i < n; ++i)
+ {
+ if (maMembers[i].mbDate)
+ {
+ CheckEntry(maMembers[i].maName, maMembers[i].mxParent.get(), maMembers[i].mbVisible);
+ // Expand first node of checked dates
+ if (!maMembers[i].mxParent && IsChecked(maMembers[i].maName, maMembers[i].mxParent.get()))
+ {
+ std::unique_ptr<weld::TreeIter> xDateEntry = FindEntry(nullptr, maMembers[i].maName);
+ if (xDateEntry)
+ aExpandRows.emplace_back(std::move(xDateEntry));
+ }
+ }
+ else
+ {
+ mpChecks->append(xEntry.get());
+ insertMember(*mpChecks, *xEntry, maMembers[i], maMembers[i].mbVisible);
+ }
+
+ if (maMembers[i].mbVisible)
+ ++nVisMemCount;
+ }
+
+ mpChecks->thaw();
+
+ for (const auto& rRow : aExpandRows)
+ mpChecks->expand_row(*rRow);
+ }
+
+ if (nVisMemCount == n)
+ {
+ // all members visible
+ mxChkToggleAll->set_state(TRISTATE_TRUE);
+ mePrevToggleAllState = TRISTATE_TRUE;
+ }
+ else if (nVisMemCount == 0)
+ {
+ // no members visible
+ mxChkToggleAll->set_state(TRISTATE_FALSE);
+ mePrevToggleAllState = TRISTATE_FALSE;
+ }
+ else
+ {
+ mxChkToggleAll->set_state(TRISTATE_INDET);
+ mePrevToggleAllState = TRISTATE_INDET;
+ }
+
+ if (nVisMemCount)
+ mpChecks->set_cursor(0);
+
+ return nVisMemCount;
+}
+
+void ScCheckListMenuControl::setConfig(const Config& rConfig)
+{
+ maConfig = rConfig;
+}
+
+bool ScCheckListMenuControl::isAllSelected() const
+{
+ return mxChkToggleAll->get_state() == TRISTATE_TRUE;
+}
+
+void ScCheckListMenuControl::getResult(ResultType& rResult)
+{
+ ResultType aResult;
+ std::unordered_set<OUString> vCheckeds = GetAllChecked();
+ size_t n = maMembers.size();
+ for (size_t i = 0; i < n; ++i)
+ {
+ if ( maMembers[i].mbLeaf )
+ {
+ OUStringBuffer aLabel(maMembers[i].maName);
+ if (aLabel.isEmpty())
+ aLabel = ScResId(STR_EMPTYDATA);
+
+ /* TODO: performance-wise this looks suspicious, concatenating to
+ * do the lookup for each leaf item seems wasteful. */
+ // Checked labels are in the form "child;parent;grandparent".
+ if (maMembers[i].mxParent)
+ {
+ std::unique_ptr<weld::TreeIter> xIter(mpChecks->make_iterator(maMembers[i].mxParent.get()));
+ do
+ {
+ aLabel.append(";" + mpChecks->get_text(*xIter));
+ }
+ while (mpChecks->iter_parent(*xIter));
+ }
+
+ bool bState = vCheckeds.find(aLabel.makeStringAndClear()) != vCheckeds.end();
+
+ ResultEntry aResultEntry;
+ aResultEntry.bValid = bState;
+ aResultEntry.aName = maMembers[i].maRealName;
+ aResultEntry.nValue = maMembers[i].mnValue;
+ aResultEntry.bDate = maMembers[i].mbDate;
+ aResultEntry.bValue = maMembers[i].mbValue;
+ aResult.insert(aResultEntry);
+ }
+ }
+ rResult.swap(aResult);
+}
+
+void ScCheckListMenuControl::launch(weld::Widget* pWidget, const tools::Rectangle& rRect)
+{
+ prepWindow();
+ if (!maConfig.mbAllowEmptySet)
+ // We need to have at least one member selected.
+ mxBtnOk->set_sensitive(GetCheckedEntryCount() != 0);
+
+ tools::Rectangle aRect(rRect);
+ if (maConfig.mbRTL)
+ {
+ // In RTL mode, the logical "left" is visual "right".
+ if (!comphelper::LibreOfficeKit::isActive())
+ {
+ tools::Long nLeft = aRect.Left() - aRect.GetWidth();
+ aRect.SetLeft( nLeft );
+ }
+ else
+ {
+ // in LOK mode, rRect is in document pixel coordinates, so width has to be added
+ // to place the popup next to the (visual) left aligned button.
+ aRect.Move(aRect.GetWidth(), 0);
+ }
+ }
+ else if (mnWndWidth < aRect.GetWidth())
+ {
+ // Target rectangle (i.e. cell width) is wider than the window.
+ // Simulate right-aligned launch by modifying the target rectangle
+ // size.
+ tools::Long nDiff = aRect.GetWidth() - mnWndWidth;
+ aRect.AdjustLeft(nDiff );
+ }
+
+ StartPopupMode(pWidget, aRect);
+}
+
+void ScCheckListMenuControl::close(bool bOK)
+{
+ if (bOK && mxOKAction)
+ mxOKAction->execute();
+ EndPopupMode();
+}
+
+void ScCheckListMenuControl::setExtendedData(std::unique_ptr<ExtendedData> p)
+{
+ mxExtendedData = std::move(p);
+}
+
+ScCheckListMenuControl::ExtendedData* ScCheckListMenuControl::getExtendedData()
+{
+ return mxExtendedData.get();
+}
+
+void ScCheckListMenuControl::setOKAction(Action* p)
+{
+ mxOKAction.reset(p);
+}
+
+void ScCheckListMenuControl::setPopupEndAction(Action* p)
+{
+ mxPopupEndAction.reset(p);
+}
+
+IMPL_LINK_NOARG(ScCheckListMenuControl, PopupModeEndHdl, weld::Popover&, void)
+{
+ mbIsPoppedUp = false;
+ clearSelectedMenuItem();
+ if (mxPopupEndAction)
+ mxPopupEndAction->execute();
+
+ DropPendingEvents();
+}
+
+int ScCheckListMenuControl::GetTextWidth(const OUString& rsName) const
+{
+ return mxDropDown->GetTextWidth(rsName);
+}
+
+int ScCheckListMenuControl::IncreaseWindowWidthToFitText(int nMaxTextWidth)
+{
+ int nBorder = nBorderWidth * 2 + 4;
+ int nNewWidth = nMaxTextWidth - nBorder;
+ if (nNewWidth > mnCheckWidthReq)
+ {
+ mnCheckWidthReq = nNewWidth;
+ int nChecksHeight = mpChecks->get_height_rows(nCheckListVisibleRows);
+ mpChecks->set_size_request(mnCheckWidthReq, nChecksHeight);
+ }
+ return mnCheckWidthReq + nBorder;
+}
+
+ScListSubMenuControl::ScListSubMenuControl(weld::Widget* pParent, ScCheckListMenuControl& rParentControl, bool bColorMenu)
+ : mxBuilder(Application::CreateBuilder(pParent, "modules/scalc/ui/filtersubdropdown.ui"))
+ , mxPopover(mxBuilder->weld_popover("FilterSubDropDown"))
+ , mxContainer(mxBuilder->weld_container("container"))
+ , mxMenu(mxBuilder->weld_tree_view("menu"))
+ , mxBackColorMenu(mxBuilder->weld_tree_view("background"))
+ , mxTextColorMenu(mxBuilder->weld_tree_view("textcolor"))
+ , mxScratchIter(mxMenu->make_iterator())
+ , mrParentControl(rParentControl)
+ , mnBackColorMenuPrefHeight(-1)
+ , mnTextColorMenuPrefHeight(-1)
+ , mbColorMenu(bColorMenu)
+{
+ mxMenu->hide();
+ mxBackColorMenu->hide();
+ mxTextColorMenu->hide();
+
+ if (!bColorMenu)
+ {
+ SetupMenu(*mxMenu);
+ mxMenu->show();
+ }
+ else
+ {
+ mxBackColorMenu->set_clicks_to_toggle(1);
+ mxBackColorMenu->enable_toggle_buttons(weld::ColumnToggleType::Radio);
+ mxBackColorMenu->connect_changed(LINK(this, ScListSubMenuControl, ColorSelChangedHdl));
+ mxTextColorMenu->set_clicks_to_toggle(1);
+ mxTextColorMenu->enable_toggle_buttons(weld::ColumnToggleType::Radio);
+ mxTextColorMenu->connect_changed(LINK(this, ScListSubMenuControl, ColorSelChangedHdl));
+ SetupMenu(*mxBackColorMenu);
+ SetupMenu(*mxTextColorMenu);
+ }
+}
+
+void ScListSubMenuControl::SetupMenu(weld::TreeView& rMenu)
+{
+ rMenu.connect_row_activated(LINK(this, ScListSubMenuControl, RowActivatedHdl));
+ rMenu.connect_key_press(LINK(this, ScListSubMenuControl, MenuKeyInputHdl));
+}
+
+void ScListSubMenuControl::StartPopupMode(weld::Widget* pParent, const tools::Rectangle& rRect)
+{
+ if (mxPopupStartAction)
+ mxPopupStartAction->execute();
+
+ mxPopover->popup_at_rect(pParent, rRect, weld::Placement::End);
+
+ weld::TreeView& rFirstMenu = mbColorMenu ? *mxBackColorMenu : *mxMenu;
+ rFirstMenu.set_cursor(0);
+ rFirstMenu.select(0);
+
+ mrParentControl.setSubMenuFocused(this);
+}
+
+void ScListSubMenuControl::EndPopupMode()
+{
+ mxPopover->popdown();
+}
+
+void ScListSubMenuControl::GrabFocus()
+{
+ weld::TreeView& rFirstMenu = mbColorMenu ? *mxBackColorMenu : *mxMenu;
+ rFirstMenu.grab_focus();
+}
+
+bool ScListSubMenuControl::IsVisible() const
+{
+ return mxPopover->get_visible();
+}
+
+void ScListSubMenuControl::resizeToFitMenuItems()
+{
+ if (!mbColorMenu)
+ mxMenu->set_size_request(-1, mxMenu->get_preferred_size().Height());
+ else
+ {
+ int nBackColorMenuPrefHeight = mnBackColorMenuPrefHeight;
+ if (nBackColorMenuPrefHeight == -1)
+ nBackColorMenuPrefHeight = mxBackColorMenu->get_preferred_size().Height();
+ mxBackColorMenu->set_size_request(-1, nBackColorMenuPrefHeight);
+ int nTextColorMenuPrefHeight = mnTextColorMenuPrefHeight;
+ if (nTextColorMenuPrefHeight == -1)
+ nTextColorMenuPrefHeight = mxTextColorMenu->get_preferred_size().Height();
+ mxTextColorMenu->set_size_request(-1, nTextColorMenuPrefHeight);
+ }
+}
+
+void ScListSubMenuControl::addItem(ScCheckListMenuControl::Action* pAction)
+{
+ ScCheckListMenuControl::MenuItemData aItem;
+ aItem.mbEnabled = true;
+ aItem.mxAction.reset(pAction);
+ maMenuItems.emplace_back(std::move(aItem));
+}
+
+void ScListSubMenuControl::addMenuItem(const OUString& rText, ScCheckListMenuControl::Action* pAction)
+{
+ addItem(pAction);
+ mxMenu->append(weld::toId(pAction), rText);
+}
+
+void ScListSubMenuControl::addMenuColorItem(const OUString& rText, bool bActive, VirtualDevice& rImage,
+ int nMenu, ScCheckListMenuControl::Action* pAction)
+{
+ addItem(pAction);
+
+ weld::TreeView& rColorMenu = nMenu == 0 ? *mxBackColorMenu : *mxTextColorMenu;
+ rColorMenu.show();
+
+ OUString sId = weld::toId(pAction);
+ rColorMenu.insert(nullptr, -1, &rText, &sId, nullptr, nullptr, false, mxScratchIter.get());
+ rColorMenu.set_toggle(*mxScratchIter, bActive ? TRISTATE_TRUE : TRISTATE_FALSE);
+ rColorMenu.set_image(*mxScratchIter, rImage);
+
+ if (mnTextColorMenuPrefHeight == -1 &&
+ &rColorMenu == mxTextColorMenu.get() &&
+ mxTextColorMenu->n_children() == nColorListVisibleRows)
+ {
+ mnTextColorMenuPrefHeight = mxTextColorMenu->get_preferred_size().Height();
+ }
+
+ if (mnBackColorMenuPrefHeight == -1 &&
+ &rColorMenu == mxBackColorMenu.get() &&
+ mxBackColorMenu->n_children() == nColorListVisibleRows)
+ {
+ mnBackColorMenuPrefHeight = mxBackColorMenu->get_preferred_size().Height();
+ }
+}
+
+void ScListSubMenuControl::addSeparator()
+{
+ ScCheckListMenuControl::MenuItemData aItem;
+ maMenuItems.emplace_back(std::move(aItem));
+
+ mxMenu->append_separator("separator" + OUString::number(maMenuItems.size()));
+}
+
+void ScListSubMenuControl::clearMenuItems()
+{
+ maMenuItems.clear();
+ mxMenu->clear();
+ mxBackColorMenu->clear();
+ mnBackColorMenuPrefHeight = -1;
+ mxTextColorMenu->clear();
+ mnTextColorMenuPrefHeight = -1;
+}
+
+IMPL_LINK(ScListSubMenuControl, MenuKeyInputHdl, const KeyEvent&, rKEvt, bool)
+{
+ bool bConsumed = false;
+ const vcl::KeyCode& rKeyCode = rKEvt.GetKeyCode();
+
+ switch (rKeyCode.GetCode())
+ {
+ case KEY_ESCAPE:
+ case KEY_LEFT:
+ {
+ mrParentControl.endSubMenu(*this);
+ bConsumed = true;
+ break;
+ }
+ case KEY_SPACE:
+ case KEY_RETURN:
+ {
+ weld::TreeView& rMenu = !mbColorMenu ? *mxMenu :
+ (mxBackColorMenu->has_focus() ? *mxBackColorMenu : *mxTextColorMenu);
+ // don't toggle checkbutton, go straight to activating entry
+ bConsumed = RowActivatedHdl(rMenu);
+ break;
+ }
+ case KEY_DOWN:
+ {
+ if (mxTextColorMenu->get_visible() &&
+ mxBackColorMenu->has_focus() &&
+ mxBackColorMenu->get_selected_index() == mxBackColorMenu->n_children() - 1)
+ {
+ mxBackColorMenu->unselect_all();
+ mxTextColorMenu->select(0);
+ mxTextColorMenu->set_cursor(0);
+ mxTextColorMenu->grab_focus();
+ bConsumed = true;
+ }
+ break;
+ }
+ case KEY_UP:
+ {
+ if (mxBackColorMenu->get_visible() &&
+ mxTextColorMenu->has_focus() &&
+ mxTextColorMenu->get_selected_index() == 0)
+ {
+ mxTextColorMenu->unselect_all();
+ int nIndex = mxBackColorMenu->n_children() - 1;
+ mxBackColorMenu->select(nIndex);
+ mxBackColorMenu->set_cursor(nIndex);
+ mxBackColorMenu->grab_focus();
+ bConsumed = true;
+ }
+ break;
+ }
+ }
+
+ return bConsumed;
+}
+
+IMPL_LINK(ScListSubMenuControl, ColorSelChangedHdl, weld::TreeView&, rMenu, void)
+{
+ if (rMenu.get_selected_index() == -1)
+ return;
+ if (&rMenu != mxTextColorMenu.get())
+ mxTextColorMenu->unselect_all();
+ else
+ mxBackColorMenu->unselect_all();
+ rMenu.grab_focus();
+}
+
+IMPL_LINK(ScListSubMenuControl, RowActivatedHdl, weld::TreeView&, rMenu, bool)
+{
+ executeMenuItem(weld::fromId<ScCheckListMenuControl::Action*>(rMenu.get_selected_id()));
+ return true;
+}
+
+void ScListSubMenuControl::executeMenuItem(ScCheckListMenuControl::Action* pAction)
+{
+ // if no action is defined.
+ if (!pAction)
+ return;
+
+ const bool bClosePopup = pAction->execute();
+ if (bClosePopup)
+ terminateAllPopupMenus();
+}
+
+void ScListSubMenuControl::setPopupStartAction(ScCheckListMenuControl::Action* p)
+{
+ mxPopupStartAction.reset(p);
+}
+
+void ScListSubMenuControl::terminateAllPopupMenus()
+{
+ EndPopupMode();
+ mrParentControl.terminateAllPopupMenus();
+}
+
+/* vim:set shiftwidth=4 softtabstop=4 expandtab: */