/* -*- 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_selection_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_selection_changed(
LINK(this , ScListSubMenuControl, ColorSelChangedHdl));
mxTextColorMenu->set_clicks_to_toggle(1 );
mxTextColorMenu->enable_toggle_buttons(weld::ColumnToggleType::Radio);
mxTextColorMenu->connect_selection_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: */
Messung V0.5 in Prozent C=87 H=95 G=90
¤ Dauer der Verarbeitung: 0.22 Sekunden
¤
*© Formatika GbR, Deutschland