1
0
Fork 0
libreoffice/sc/source/ui/cctrl/checklistmenu.cxx
Daniel Baumann 8e63e14cf6
Adding upstream version 4:25.2.3.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
2025-06-22 16:20:04 +02:00

1970 lines
64 KiB
C++

/* -*- 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)
{
// Assume that once the keyboard is used that focus should restore to this menu
// on dismissing a submenu
SetRestoreFocus(ScCheckListMenuControl::RestoreFocus::Menu);
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()
{
const StyleSettings& rStyleSettings = Application::GetSettings().GetStyleSettings();
Color aSpinColor = rStyleSettings.GetDialogTextColor();
int nWidth = (mxMenu->get_text_height() * 3) / 4;
mxDropDown->SetOutputSizePixel(Size(nWidth, nWidth), /*bErase*/true, /*bAlphaMaskTransparent*/true);
DecorationView aDecoView(mxDropDown.get());
aDecoView.DrawSymbol(tools::Rectangle(Point(0, 0), Size(nWidth, nWidth)),
SymbolType::SPIN_RIGHT, aSpinColor,
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;
meRestoreFocus = DetermineRestoreFocus();
tools::Rectangle aRect = GetSubMenuParentRect();
pSubMenu->StartPopupMode(mxMenu.get(), aRect);
mxMenu->select(*mxScratchIter);
pSubMenu->GrabFocus();
}
ScCheckListMenuControl::RestoreFocus ScCheckListMenuControl::DetermineRestoreFocus() const
{
if (mxEdSearch->has_focus())
return RestoreFocus::EdSearch;
if (mpChecks->has_focus())
return RestoreFocus::Checks;
if (mxChkToggleAll->has_focus())
return RestoreFocus::ChkToggleAll;
if (mxChkLockChecked->has_focus())
return RestoreFocus::ChkLockChecked;
if (mxBtnSelectSingle->has_focus())
return RestoreFocus::BtnSelectSingle;
if (mxBtnUnselectSingle->has_focus())
return RestoreFocus::BtnUnselectSingle;
return RestoreFocus::Menu;
}
void ScCheckListMenuControl::RestorePreviousFocus()
{
switch (meRestoreFocus)
{
case RestoreFocus::EdSearch:
mxEdSearch->grab_focus();
break;
case RestoreFocus::Checks:
mpChecks->grab_focus();
break;
case RestoreFocus::ChkToggleAll:
mxChkToggleAll->grab_focus();
break;
case RestoreFocus::ChkLockChecked:
mxChkLockChecked->grab_focus();
break;
case RestoreFocus::BtnSelectSingle:
mxBtnSelectSingle->grab_focus();
break;
case RestoreFocus::BtnUnselectSingle:
mxBtnUnselectSingle->grab_focus();
break;
default:
mxMenu->grab_focus();
break;
}
}
IMPL_LINK_NOARG(ScCheckListMenuControl, PostPopdownHdl, void*, void)
{
mnAsyncPostPopdownId = nullptr;
RestorePreviousFocus();
}
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::addFields(const std::vector<OUString>& aFields)
{
if (!mbIsMultiField)
return;
mxFieldsCombo->clear();
for (auto& aField: aFields)
mxFieldsCombo->append_text(aField);
mxFieldsCombo->set_active(0);
}
tools::Long ScCheckListMenuControl::getField()
{
if (!mbIsMultiField)
return -1;
return mxFieldsCombo->get_active();
}
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)
, mbMarked(false)
, mbCheck(true)
, mbHiddenByOtherFilter(false)
, 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, bool bIsMultiField)
: mxBuilder(Application::CreateBuilder(pParent, u"modules/scalc/ui/filterdropdown.ui"_ustr))
, mxPopover(mxBuilder->weld_popover(u"FilterDropDown"_ustr))
, mxContainer(mxBuilder->weld_container(u"container"_ustr))
, mxMenu(mxBuilder->weld_tree_view(u"menu"_ustr))
, mxScratchIter(mxMenu->make_iterator())
, mxNonMenu(mxBuilder->weld_widget(u"nonmenu"_ustr))
, mxFieldsComboLabel(mxBuilder->weld_label(u"select_field_label"_ustr))
, mxFieldsCombo(mxBuilder->weld_combo_box(u"multi_field_combo"_ustr))
, mxEdSearch(mxBuilder->weld_entry(u"search_edit"_ustr))
, mxBox(mxBuilder->weld_widget(u"box"_ustr))
, mxListChecks(mxBuilder->weld_tree_view(u"check_list_box"_ustr))
, mxTreeChecks(mxBuilder->weld_tree_view(u"check_tree_box"_ustr))
, mxChkToggleAll(mxBuilder->weld_check_button(u"toggle_all"_ustr))
, mxChkLockChecked(mxBuilder->weld_check_button(u"lock_checked"_ustr))
, mxBtnSelectSingle(mxBuilder->weld_button(u"select_current"_ustr))
, mxBtnUnselectSingle(mxBuilder->weld_button(u"unselect_current"_ustr))
, mxButtonBox(mxBuilder->weld_box(u"buttonbox"_ustr))
, mxBtnOk(mxBuilder->weld_button(u"ok"_ustr))
, mxBtnCancel(mxBuilder->weld_button(u"cancel"_ustr))
, mxContextMenu(mxBuilder->weld_menu(u"contextmenu"_ustr))
, mxDropDown(mxMenu->create_virtual_device())
, mnCheckWidthReq(-1)
, mnWndWidth(0)
, mnCheckListVisibleRows(nCheckListVisibleRows)
, mePrevToggleAllState(TRISTATE_INDET)
, mnSelectedMenu(MENU_NOT_SELECTED)
, mrViewData(rViewData)
, mnAsyncPostPopdownId(nullptr)
, mnAsyncSetDropdownPosId(nullptr)
, meRestoreFocus(RestoreFocus::Menu)
, mbHasDates(bHasDates)
, mbIsPoppedUp(false)
, maOpenTimer(this)
, maCloseTimer(this)
, maSearchEditTimer("ScCheckListMenuControl maSearchEditTimer")
, mbIsMultiField(bIsMultiField)
{
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));
mxChkLockChecked->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();
if (mbIsMultiField)
{
mxFieldsComboLabel->show();
mxFieldsCombo->show();
}
else
{
mxFieldsComboLabel->hide();
mxFieldsCombo->hide();
}
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));
if (mbIsMultiField)
mxFieldsCombo->connect_changed(LINK(this, ScCheckListMenuControl, ComboChangedHdl));
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));
mxChkLockChecked->connect_toggled(LINK(this, ScCheckListMenuControl, LockCheckedHdl));
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);
maSearchEditTimer.SetTimeout(EDIT_UPDATEDATA_TIMEOUT);
maSearchEditTimer.SetInvokeHandler(LINK(this, ScCheckListMenuControl, SearchEditTimeoutHdl));
if (comphelper::LibreOfficeKit::isActive())
{
mxBtnSelectSingle->hide();
mxBtnUnselectSingle->hide();
}
}
void ScCheckListMenuControl::GrabFocus()
{
if (mxEdSearch->get_visible())
{
mxEdSearch->grab_focus();
meRestoreFocus = RestoreFocus::EdSearch;
}
else
{
mxMenu->set_cursor(0);
mxMenu->grab_focus();
meRestoreFocus = RestoreFocus::Menu;
}
}
void ScCheckListMenuControl::DropPendingEvents()
{
if (mnAsyncPostPopdownId)
{
Application::RemoveUserEvent(mnAsyncPostPopdownId);
mnAsyncPostPopdownId = nullptr;
}
if (mnAsyncSetDropdownPosId)
{
Application::RemoveUserEvent(mnAsyncSetDropdownPosId);
mnAsyncSetDropdownPosId = nullptr;
}
}
ScCheckListMenuControl::~ScCheckListMenuControl()
{
maSearchEditTimer.Stop();
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){
if (mpChecks->get_sensitive(rEntry, 0))
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(u"less"_ustr, mnCheckListVisibleRows > 4);
mxContextMenu->set_sensitive(u"more"_ustr, mnCheckListVisibleRows < 42);
OUString 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())
{
std::unique_ptr<weld::TreeIter> xEntry = mpChecks->make_iterator();
bool bEntry = mpChecks->get_cursor(xEntry.get());
if (!bEntry)
xEntry.reset();
if (bEntry && mpChecks->get_sensitive(*xEntry, 0))
{
selectCurrentMemberOnly(&rBtn == mxBtnSelectSingle.get());
Check(xEntry.get());
}
}
}
namespace
{
void insertMember(weld::TreeView& rView, const weld::TreeIter& rIter, const ScCheckListMember& rMember, bool bChecked, bool bLock=false)
{
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);
if (bLock)
rView.set_sensitive(rIter, !rMember.mbHiddenByOtherFilter && !rMember.mbMarked);
else
rView.set_sensitive(rIter, !rMember.mbHiddenByOtherFilter);
}
void loadSearchedMembers(std::vector<int>& rSearchedMembers, std::vector<ScCheckListMember>& rMembers,
const OUString& rSearchText, bool bLock=false)
{
const OUString aSearchText = ScGlobal::getCharClass().lowercase( rSearchText );
for (size_t i = 0; i < rMembers.size(); ++i)
{
assert(!rMembers[i].mbDate);
OUString aLabelDisp = rMembers[i].maName;
if ( aLabelDisp.isEmpty() )
aLabelDisp = ScResId( STR_EMPTYDATA );
bool bPartialMatch = ScGlobal::getCharClass().lowercase( aLabelDisp ).indexOf( aSearchText ) != -1;
if (!bPartialMatch)
continue;
if (!bLock || (!rMembers[i].mbMarked && !rMembers[i].mbHiddenByOtherFilter))
rSearchedMembers.push_back(i);
}
if (bLock)
for (size_t i = 0; i < rMembers.size(); ++i)
if (rMembers[i].mbMarked && !rMembers[i].mbHiddenByOtherFilter)
rSearchedMembers.push_back(i);
}
}
IMPL_LINK_NOARG(ScCheckListMenuControl, LockCheckedHdl, weld::Toggleable&, void)
{
// assume all members are checked
for (auto& aMember : maMembers)
aMember.mbCheck = true;
// go over the members visible in the popup, and remember which one is
// checked, and which one is not
mpChecks->all_foreach([this](weld::TreeIter& rEntry){
if (mpChecks->get_toggle(rEntry) == TRISTATE_TRUE)
{
for (auto& aMember : maMembers)
if (aMember.maName == mpChecks->get_text(rEntry))
aMember.mbMarked = true;
}
else
{
for (auto& aMember : maMembers)
if (aMember.maName == mpChecks->get_text(rEntry))
aMember.mbCheck = false;
}
return false;
});
mpChecks->freeze();
mpChecks->clear();
mpChecks->thaw();
OUString aSearchText = mxEdSearch->get_text();
if (aSearchText.isEmpty())
{
initMembers(-1, !mxChkLockChecked->get_active());
}
else
{
std::vector<int> aShownIndexes;
loadSearchedMembers(aShownIndexes, maMembers, aSearchText, true);
std::vector<int> aFixedWidths { mnCheckWidthReq };
// insert the members, remember whether checked or unchecked.
mpChecks->bulk_insert_for_each(aShownIndexes.size(), [this, &aShownIndexes](weld::TreeIter& rIter, int i) {
size_t nIndex = aShownIndexes[i];
insertMember(*mpChecks, rIter, maMembers[nIndex], maMembers[nIndex].mbCheck, mxChkLockChecked->get_active());
}, nullptr, &aFixedWidths);
}
// unmarking should happen after the members are inserted
if (!mxChkLockChecked->get_active())
for (auto& aMember : maMembers)
aMember.mbMarked = false;
}
IMPL_LINK_NOARG(ScCheckListMenuControl, TriStateHdl, weld::Toggleable&, void)
{
switch (mePrevToggleAllState)
{
case TRISTATE_TRUE:
mxChkToggleAll->set_state(TRISTATE_FALSE);
setAllMemberState(false);
break;
case TRISTATE_FALSE:
case TRISTATE_INDET:
default:
mxChkToggleAll->set_state(TRISTATE_TRUE);
setAllMemberState(true);
break;
}
mePrevToggleAllState = mxChkToggleAll->get_state();
}
IMPL_LINK_NOARG(ScCheckListMenuControl, ComboChangedHdl, weld::ComboBox&, void)
{
if (mbIsMultiField && mxFieldChangedAction)
mxFieldChangedAction->execute();
}
IMPL_LINK_NOARG(ScCheckListMenuControl, SearchEditTimeoutHdl, Timer*, void)
{
OUString aSearchText = mxEdSearch->get_text();
aSearchText = ScGlobal::getCharClass().lowercase( aSearchText );
bool bSearchTextEmpty = aSearchText.isEmpty();
size_t nEnableMember = std::count_if(maMembers.begin(), maMembers.end(),
[](const ScCheckListMember& rLMem) { return !rLMem.mbHiddenByOtherFilter; });
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 < nEnableMember; ++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 < nEnableMember; ++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<int> aShownIndexes;
loadSearchedMembers(aShownIndexes, maMembers, aSearchText, mxChkLockChecked->get_active());
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, mxChkLockChecked->get_active());
++nSelCount;
}, nullptr, &aFixedWidths);
}
}
if ( nSelCount == nEnableMember )
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, EdModifyHdl, weld::Entry&, void)
{
maSearchEditTimer.Start();
}
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();
size_t nEnableMember = std::count_if(maMembers.begin(), maMembers.end(),
[](const ScCheckListMember& rLMem) { return !rLMem.mbHiddenByOtherFilter; });
if (nNumChecked == nEnableMember)
// 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, bool bHiddenByOtherFilter)
{
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);
mpChecks->set_sensitive(*xYearEntry, !bHiddenByOtherFilter);
ScCheckListMember aMemYear;
aMemYear.maName = aYearName;
aMemYear.maRealName = rsName;
aMemYear.mbDate = true;
aMemYear.mbLeaf = false;
aMemYear.mbVisible = bVisible;
aMemYear.mbHiddenByOtherFilter = bHiddenByOtherFilter;
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);
mpChecks->set_sensitive(*xMonthEntry, !bHiddenByOtherFilter);
ScCheckListMember aMemMonth;
aMemMonth.maName = aMonthName;
aMemMonth.maRealName = rsName;
aMemMonth.mbDate = true;
aMemMonth.mbLeaf = false;
aMemMonth.mbVisible = bVisible;
aMemMonth.mbHiddenByOtherFilter = bHiddenByOtherFilter;
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);
mpChecks->set_sensitive(*xDayEntry, !bHiddenByOtherFilter);
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.mbHiddenByOtherFilter = bHiddenByOtherFilter;
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 bHiddenByOtherFilter, 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.mbMarked = false;
aMember.mbCheck = true;
aMember.mbHiddenByOtherFilter = bHiddenByOtherFilter;
aMember.mxParent.reset();
maMembers.emplace_back(std::move(aMember));
}
void ScCheckListMenuControl::clearMembers()
{
maMembers.clear();
mpChecks->freeze();
mpChecks->clear();
mpChecks->thaw();
}
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 && mpChecks->get_sensitive(*xEntry, 0))
{
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, bool bUnlock)
{
size_t n = maMembers.size();
size_t nEnableMember = std::count_if(maMembers.begin(), maMembers.end(),
[](const ScCheckListMember& rLMem) { return !rLMem.mbHiddenByOtherFilter; });
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, &bUnlock](weld::TreeIter& rIter, int i) {
assert(!maMembers[i].mbDate);
bool bCheck = ((mxChkLockChecked->get_active() || bUnlock) ? maMembers[i].mbMarked : maMembers[i].mbVisible);
insertMember(*mpChecks, rIter, maMembers[i], bCheck, mxChkLockChecked->get_active());
if (bCheck)
++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 == nEnableMember)
{
// 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 && !maMembers[i].mbHiddenByOtherFilter;
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);
}
void ScCheckListMenuControl::setFieldChangedAction(Action* p)
{
mxFieldChangedAction.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, u"modules/scalc/ui/filtersubdropdown.ui"_ustr))
, mxPopover(mxBuilder->weld_popover(u"FilterSubDropDown"_ustr))
, mxContainer(mxBuilder->weld_container(u"container"_ustr))
, mxMenu(mxBuilder->weld_tree_view(u"menu"_ustr))
, mxBackColorMenu(mxBuilder->weld_tree_view(u"background"_ustr))
, mxTextColorMenu(mxBuilder->weld_tree_view(u"textcolor"_ustr))
, 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();
const sal_uInt16 eKeyCode = rKeyCode.GetCode();
// Assume that once the keyboard is used that focus should restore to the
// parent menu
if (eKeyCode != KEY_ESCAPE)
mrParentControl.SetRestoreFocus(ScCheckListMenuControl::RestoreFocus::Menu);
switch (eKeyCode)
{
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: */