1 /* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
2 /*
3  * This file is part of the LibreOffice project.
4  *
5  * This Source Code Form is subject to the terms of the Mozilla Public
6  * License, v. 2.0. If a copy of the MPL was not distributed with this
7  * file, You can obtain one at http://mozilla.org/MPL/2.0/.
8  *
9  * This file incorporates work covered by the following license notice:
10  *
11  *   Licensed to the Apache Software Foundation (ASF) under one or more
12  *   contributor license agreements. See the NOTICE file distributed
13  *   with this work for additional information regarding copyright
14  *   ownership. The ASF licenses this file to you under the Apache
15  *   License, Version 2.0 (the "License"); you may not use this file
16  *   except in compliance with the License. You may obtain a copy of
17  *   the License at http://www.apache.org/licenses/LICENSE-2.0 .
18  */
19 
20 #include <checklistmenu.hxx>
21 #include <globstr.hrc>
22 #include <scresid.hxx>
23 
24 #include <vcl/decoview.hxx>
25 #include <vcl/event.hxx>
26 #include <vcl/dockwin.hxx>
27 #include <vcl/settings.hxx>
28 #include <vcl/svapp.hxx>
29 #include <vcl/virdev.hxx>
30 #include <rtl/math.hxx>
31 #include <unotools/charclass.hxx>
32 #include <comphelper/lok.hxx>
33 #include <LibreOfficeKit/LibreOfficeKitEnums.h>
34 #include <tools/json_writer.hxx>
35 #include <sfx2/viewsh.hxx>
36 #include <vcl/jsdialog/executor.hxx>
37 
38 #include <document.hxx>
39 
40 using namespace com::sun::star;
41 using ::com::sun::star::uno::Reference;
42 
MenuItemData()43 ScCheckListMenuControl::MenuItemData::MenuItemData()
44     : mbEnabled(true)
45 {
46 }
47 
SubMenuItemData(ScCheckListMenuControl * pParent)48 ScCheckListMenuControl::SubMenuItemData::SubMenuItemData(ScCheckListMenuControl* pParent)
49     : mpSubMenu(nullptr)
50     , mnMenuPos(MENU_NOT_SELECTED)
51     , mpParent(pParent)
52 {
53     maTimer.SetInvokeHandler(LINK(this, ScCheckListMenuControl::SubMenuItemData, TimeoutHdl));
54     maTimer.SetTimeout(Application::GetSettings().GetMouseSettings().GetMenuDelay());
55 }
56 
reset()57 void ScCheckListMenuControl::SubMenuItemData::reset()
58 {
59     mpSubMenu = nullptr;
60     mnMenuPos = MENU_NOT_SELECTED;
61     maTimer.Stop();
62 }
63 
IMPL_LINK_NOARG(ScCheckListMenuControl::SubMenuItemData,TimeoutHdl,Timer *,void)64 IMPL_LINK_NOARG(ScCheckListMenuControl::SubMenuItemData, TimeoutHdl, Timer *, void)
65 {
66     mpParent->handleMenuTimeout(this);
67 }
68 
IMPL_LINK_NOARG(ScCheckListMenuControl,RowActivatedHdl,weld::TreeView &,bool)69 IMPL_LINK_NOARG(ScCheckListMenuControl, RowActivatedHdl, weld::TreeView&, bool)
70 {
71     executeMenuItem(mxMenu->get_selected_index());
72     return true;
73 }
74 
IMPL_LINK(ScCheckListMenuControl,MenuKeyInputHdl,const KeyEvent &,rKEvt,bool)75 IMPL_LINK(ScCheckListMenuControl, MenuKeyInputHdl, const KeyEvent&, rKEvt, bool)
76 {
77     const vcl::KeyCode& rKeyCode = rKEvt.GetKeyCode();
78 
79     switch (rKeyCode.GetCode())
80     {
81         case KEY_LEFT:
82         {
83             ScCheckListMenuWindow* pParentMenu = mxFrame->GetParentMenu();
84             if (pParentMenu)
85                 pParentMenu->get_widget().endSubMenu(*this);
86             break;
87         }
88         case KEY_RIGHT:
89         {
90             if (mnSelectedMenu >= maMenuItems.size() || mnSelectedMenu == MENU_NOT_SELECTED)
91                 break;
92 
93             const MenuItemData& rMenu = maMenuItems[mnSelectedMenu];
94             if (!rMenu.mbEnabled || !rMenu.mxSubMenuWin)
95                 break;
96 
97             maOpenTimer.mnMenuPos = mnSelectedMenu;
98             maOpenTimer.mpSubMenu = rMenu.mxSubMenuWin.get();
99             launchSubMenu(true);
100         }
101     }
102 
103     return false;
104 }
105 
IMPL_LINK_NOARG(ScCheckListMenuControl,SelectHdl,weld::TreeView &,void)106 IMPL_LINK_NOARG(ScCheckListMenuControl, SelectHdl, weld::TreeView&, void)
107 {
108     sal_uInt32 nSelectedMenu = MENU_NOT_SELECTED;
109     if (!mxMenu->get_selected(mxScratchIter.get()))
110     {
111         // reselect current item if its submenu is up and the launching item
112         // became unselected
113         if (mnSelectedMenu < maMenuItems.size() &&
114             maMenuItems[mnSelectedMenu].mxSubMenuWin &&
115             maMenuItems[mnSelectedMenu].mxSubMenuWin->IsVisible())
116         {
117             mxMenu->select(mnSelectedMenu);
118             return;
119         }
120     }
121     else
122         nSelectedMenu = mxMenu->get_iter_index_in_parent(*mxScratchIter);
123 
124     setSelectedMenuItem(nSelectedMenu, true);
125 }
126 
addMenuItem(const OUString & rText,Action * pAction,bool bIndicateSubMenu)127 void ScCheckListMenuControl::addMenuItem(const OUString& rText, Action* pAction, bool bIndicateSubMenu)
128 {
129     MenuItemData aItem;
130     aItem.mbEnabled = true;
131     aItem.mxAction.reset(pAction);
132     maMenuItems.emplace_back(std::move(aItem));
133 
134     mxMenu->show();
135     mxMenu->append_text(rText);
136     if (mbCanHaveSubMenu)
137     {
138         if (bIndicateSubMenu)
139             mxMenu->set_image(mxMenu->n_children() - 1, *mxDropDown, 1);
140         else
141             mxMenu->set_image(mxMenu->n_children() - 1, css::uno::Reference<css::graphic::XGraphic>(), 1);
142     }
143 }
144 
addSeparator()145 void ScCheckListMenuControl::addSeparator()
146 {
147     MenuItemData aItem;
148     maMenuItems.emplace_back(std::move(aItem));
149 
150     mxMenu->append_separator("separator" + OUString::number(maMenuItems.size()));
151 }
152 
IMPL_LINK(ScCheckListMenuControl,TreeSizeAllocHdl,const Size &,rSize,void)153 IMPL_LINK(ScCheckListMenuControl, TreeSizeAllocHdl, const Size&, rSize, void)
154 {
155     assert(mbCanHaveSubMenu);
156     std::vector<int> aWidths;
157     aWidths.push_back(rSize.Width() - (mxMenu->get_text_height() * 3) / 4 - 6);
158     mxMenu->set_column_fixed_widths(aWidths);
159 }
160 
CreateDropDown()161 void ScCheckListMenuControl::CreateDropDown()
162 {
163     int nWidth = (mxMenu->get_text_height() * 3) / 4;
164     mxDropDown->SetOutputSizePixel(Size(nWidth, nWidth));
165     DecorationView aDecoView(mxDropDown.get());
166     aDecoView.DrawSymbol(tools::Rectangle(Point(0, 0), Size(nWidth, nWidth)),
167                          SymbolType::SPIN_RIGHT, mxDropDown->GetTextColor(),
168                          DrawSymbolFlags::NONE);
169 }
170 
addSubMenuItem(const OUString & rText,bool bEnabled)171 ScCheckListMenuWindow* ScCheckListMenuControl::addSubMenuItem(const OUString& rText, bool bEnabled)
172 {
173     assert(mbCanHaveSubMenu);
174 
175     MenuItemData aItem;
176     aItem.mbEnabled = bEnabled;
177     vcl::Window *pContainer = mxFrame->GetWindow(GetWindowType::FirstChild);
178 
179     vcl::ILibreOfficeKitNotifier* pNotifier = nullptr;
180     if (comphelper::LibreOfficeKit::isActive())
181         pNotifier = SfxViewShell::Current();
182 
183     aItem.mxSubMenuWin.reset(VclPtr<ScCheckListMenuWindow>::Create(pContainer, mpDoc, false,
184                                                                    false, -1, mxFrame.get(),
185                                                                    pNotifier));
186     maMenuItems.emplace_back(std::move(aItem));
187 
188     mxMenu->show();
189     mxMenu->append_text(rText);
190     if (mbCanHaveSubMenu)
191         mxMenu->set_image(mxMenu->n_children() - 1, *mxDropDown, 1);
192 
193     return maMenuItems.back().mxSubMenuWin.get();
194 }
195 
executeMenuItem(size_t nPos)196 void ScCheckListMenuControl::executeMenuItem(size_t nPos)
197 {
198     if (nPos >= maMenuItems.size())
199         return;
200 
201     if (!maMenuItems[nPos].mxAction)
202         // no action is defined.
203         return;
204 
205     const bool bClosePopup = maMenuItems[nPos].mxAction->execute();
206     if (bClosePopup)
207         terminateAllPopupMenus();
208 }
209 
setSelectedMenuItem(size_t nPos,bool bSubMenuTimer)210 void ScCheckListMenuControl::setSelectedMenuItem(size_t nPos, bool bSubMenuTimer)
211 {
212     if (mnSelectedMenu == nPos)
213         // nothing to do.
214         return;
215 
216     selectMenuItem(nPos, bSubMenuTimer);
217 }
218 
handleMenuTimeout(const SubMenuItemData * pTimer)219 void ScCheckListMenuControl::handleMenuTimeout(const SubMenuItemData* pTimer)
220 {
221     if (pTimer == &maOpenTimer)
222     {
223         // Close any open submenu immediately.
224         if (maCloseTimer.mpSubMenu)
225         {
226             vcl::Window::GetDockingManager()->EndPopupMode(maCloseTimer.mpSubMenu);
227             maCloseTimer.mpSubMenu = nullptr;
228             maCloseTimer.maTimer.Stop();
229         }
230 
231         launchSubMenu(false);
232     }
233     else if (pTimer == &maCloseTimer)
234     {
235         // end submenu.
236         if (maCloseTimer.mpSubMenu)
237         {
238             maOpenTimer.mpSubMenu = nullptr;
239 
240             vcl::Window::GetDockingManager()->EndPopupMode(maCloseTimer.mpSubMenu);
241             maCloseTimer.mpSubMenu = nullptr;
242 
243             maOpenTimer.mnMenuPos = MENU_NOT_SELECTED;
244         }
245     }
246 }
247 
queueLaunchSubMenu(size_t nPos,ScCheckListMenuWindow * pMenu)248 void ScCheckListMenuControl::queueLaunchSubMenu(size_t nPos, ScCheckListMenuWindow* pMenu)
249 {
250     if (!pMenu)
251         return;
252 
253     // Set the submenu on launch queue.
254     if (maOpenTimer.mpSubMenu)
255     {
256         if (maOpenTimer.mpSubMenu == pMenu)
257         {
258             if (pMenu == maCloseTimer.mpSubMenu)
259                 maCloseTimer.reset();
260             return;
261         }
262 
263         // new submenu is being requested.
264         queueCloseSubMenu();
265     }
266 
267     maOpenTimer.mpSubMenu = pMenu;
268     maOpenTimer.mnMenuPos = nPos;
269     maOpenTimer.maTimer.Start();
270 }
271 
queueCloseSubMenu()272 void ScCheckListMenuControl::queueCloseSubMenu()
273 {
274     if (!maOpenTimer.mpSubMenu)
275         // There is no submenu to close.
276         return;
277 
278     // Stop any submenu on queue for opening.
279     maOpenTimer.maTimer.Stop();
280 
281     maCloseTimer.mpSubMenu = maOpenTimer.mpSubMenu;
282     maCloseTimer.mnMenuPos = maOpenTimer.mnMenuPos;
283     maCloseTimer.maTimer.Start();
284 }
285 
GetSubMenuParentRect()286 tools::Rectangle ScCheckListMenuControl::GetSubMenuParentRect()
287 {
288     if (!mxMenu->get_selected(mxScratchIter.get()))
289         return tools::Rectangle();
290     return mxMenu->get_row_area(*mxScratchIter);
291 }
292 
launchSubMenu(bool bSetMenuPos)293 void ScCheckListMenuControl::launchSubMenu(bool bSetMenuPos)
294 {
295     ScCheckListMenuWindow* pSubMenu = maOpenTimer.mpSubMenu;
296     if (!pSubMenu)
297         return;
298 
299     if (!mxMenu->get_selected(mxScratchIter.get()))
300         return;
301 
302     tools::Rectangle aRect = GetSubMenuParentRect();
303     ScCheckListMenuControl& rSubMenuControl = pSubMenu->get_widget();
304     rSubMenuControl.StartPopupMode(aRect, FloatWinPopupFlags::Right);
305     if (bSetMenuPos)
306         rSubMenuControl.setSelectedMenuItem(0, false); // select menu item after the popup becomes fully visible.
307 
308     mxMenu->select(*mxScratchIter);
309     rSubMenuControl.GrabFocus();
310 
311     if (comphelper::LibreOfficeKit::isActive())
312         jsdialog::SendFullUpdate(pSubMenu->GetLOKWindowId(), "toggle_all");
313 }
314 
IMPL_LINK_NOARG(ScCheckListMenuControl,PostPopdownHdl,void *,void)315 IMPL_LINK_NOARG(ScCheckListMenuControl, PostPopdownHdl, void*, void)
316 {
317     mnAsyncPostPopdownId = nullptr;
318     mxMenu->grab_focus();
319 }
320 
endSubMenu(ScCheckListMenuControl & rSubMenu)321 void ScCheckListMenuControl::endSubMenu(ScCheckListMenuControl& rSubMenu)
322 {
323     rSubMenu.EndPopupMode();
324     maOpenTimer.reset();
325 
326     // EndPopup sends a user event, and we want this focus to be set after that has done its conflicting focus-setting work
327     if (!mnAsyncPostPopdownId)
328         mnAsyncPostPopdownId = Application::PostUserEvent(LINK(this, ScCheckListMenuControl, PostPopdownHdl));
329 
330     size_t nMenuPos = getSubMenuPos(&rSubMenu);
331     if (nMenuPos != MENU_NOT_SELECTED)
332     {
333         mnSelectedMenu = nMenuPos;
334         mxMenu->select(mnSelectedMenu);
335     }
336 }
337 
resizeToFitMenuItems()338 void ScCheckListMenuControl::resizeToFitMenuItems()
339 {
340     mxMenu->set_size_request(-1, mxMenu->get_preferred_size().Height() + 2);
341 }
342 
selectMenuItem(size_t nPos,bool bSubMenuTimer)343 void ScCheckListMenuControl::selectMenuItem(size_t nPos, bool bSubMenuTimer)
344 {
345     mxMenu->select(nPos == MENU_NOT_SELECTED ? -1 : nPos);
346     mnSelectedMenu = nPos;
347 
348     if (nPos >= maMenuItems.size() || nPos == MENU_NOT_SELECTED)
349     {
350         queueCloseSubMenu();
351         return;
352     }
353 
354     if (!maMenuItems[nPos].mbEnabled)
355     {
356         queueCloseSubMenu();
357         return;
358     }
359 
360     ScCheckListMenuWindow* pParentMenu = mxFrame->GetParentMenu();
361     if (pParentMenu)
362         pParentMenu->get_widget().setSubMenuFocused(this);
363 
364     if (bSubMenuTimer)
365     {
366         if (maMenuItems[nPos].mxSubMenuWin)
367         {
368             ScCheckListMenuWindow* pSubMenu = maMenuItems[nPos].mxSubMenuWin.get();
369             queueLaunchSubMenu(nPos, pSubMenu);
370         }
371         else
372             queueCloseSubMenu();
373     }
374 }
375 
clearSelectedMenuItem()376 void ScCheckListMenuControl::clearSelectedMenuItem()
377 {
378     selectMenuItem(MENU_NOT_SELECTED, false);
379 }
380 
getSubMenuPos(const ScCheckListMenuControl * pSubMenu)381 size_t ScCheckListMenuControl::getSubMenuPos(const ScCheckListMenuControl* pSubMenu)
382 {
383     size_t n = maMenuItems.size();
384     for (size_t i = 0; i < n; ++i)
385     {
386         if (!maMenuItems[i].mxSubMenuWin)
387             continue;
388         if (&maMenuItems[i].mxSubMenuWin->get_widget() == pSubMenu)
389             return i;
390     }
391     return MENU_NOT_SELECTED;
392 }
393 
setSubMenuFocused(const ScCheckListMenuControl * pSubMenu)394 void ScCheckListMenuControl::setSubMenuFocused(const ScCheckListMenuControl* pSubMenu)
395 {
396     maCloseTimer.reset();
397     size_t nMenuPos = getSubMenuPos(pSubMenu);
398     if (mnSelectedMenu != nMenuPos)
399     {
400         mnSelectedMenu = nMenuPos;
401         mxMenu->select(mnSelectedMenu);
402     }
403 }
404 
EndPopupMode()405 void ScCheckListMenuControl::EndPopupMode()
406 {
407     vcl::Window::GetDockingManager()->EndPopupMode(mxFrame);
408     mxFrame->EnableDocking(false);
409 }
410 
StartPopupMode(const tools::Rectangle & rRect,FloatWinPopupFlags eFlags)411 void ScCheckListMenuControl::StartPopupMode(const tools::Rectangle& rRect, FloatWinPopupFlags eFlags)
412 {
413     mxFrame->EnableDocking(true);
414     DockingManager* pDockingManager = vcl::Window::GetDockingManager();
415     pDockingManager->SetPopupModeEndHdl(mxFrame, LINK(this, ScCheckListMenuControl, PopupModeEndHdl));
416     pDockingManager->StartPopupMode(mxFrame, rRect, (eFlags | FloatWinPopupFlags::GrabFocus));
417 }
418 
terminateAllPopupMenus()419 void ScCheckListMenuControl::terminateAllPopupMenus()
420 {
421     if (comphelper::LibreOfficeKit::isActive())
422         NotifyCloseLOK();
423 
424     EndPopupMode();
425     ScCheckListMenuWindow* pParentMenu = mxFrame->GetParentMenu();
426     if (pParentMenu)
427         pParentMenu->get_widget().terminateAllPopupMenus();
428 }
429 
Config()430 ScCheckListMenuControl::Config::Config() :
431     mbAllowEmptySet(true), mbRTL(false)
432 {
433 }
434 
ScCheckListMember()435 ScCheckListMember::ScCheckListMember()
436     : mnValue(0.0)
437     , mbVisible(true)
438     , mbDate(false)
439     , mbLeaf(false)
440     , mbValue(false)
441     , mbDuplicated(false)
442     , meDatePartType(YEAR)
443 {
444 }
445 
ScCheckListMenuControl(ScCheckListMenuWindow * pParent,vcl::Window * pContainer,ScDocument * pDoc,bool bCanHaveSubMenu,bool bHasDates,int nWidth)446 ScCheckListMenuControl::ScCheckListMenuControl(ScCheckListMenuWindow* pParent, vcl::Window* pContainer,
447                                                ScDocument* pDoc, bool bCanHaveSubMenu,
448                                                bool bHasDates, int nWidth)
449     : mxFrame(pParent)
450     , mxBuilder(Application::CreateInterimBuilder(pContainer, "modules/scalc/ui/filterdropdown.ui", false))
451     , mxContainer(mxBuilder->weld_container("FilterDropDown"))
452     , mxMenu(mxBuilder->weld_tree_view("menu"))
453     , mxScratchIter(mxMenu->make_iterator())
454     , mxEdSearch(mxBuilder->weld_entry("search_edit"))
455     , mxBox(mxBuilder->weld_widget("box"))
456     , mxListChecks(mxBuilder->weld_tree_view("check_list_box"))
457     , mxTreeChecks(mxBuilder->weld_tree_view("check_tree_box"))
458     , mxChkToggleAll(mxBuilder->weld_check_button("toggle_all"))
459     , mxBtnSelectSingle(mxBuilder->weld_button("select_current"))
460     , mxBtnUnselectSingle(mxBuilder->weld_button("unselect_current"))
461     , mxButtonBox(mxBuilder->weld_box("buttonbox"))
462     , mxBtnOk(mxBuilder->weld_button("ok"))
463     , mxBtnCancel(mxBuilder->weld_button("cancel"))
464     , mxDropDown(mxMenu->create_virtual_device())
465     , mnCheckWidthReq(-1)
466     , mnWndWidth(0)
467     , mePrevToggleAllState(TRISTATE_INDET)
468     , mnSelectedMenu(MENU_NOT_SELECTED)
469     , mpDoc(pDoc)
470     , mnAsyncPostPopdownId(nullptr)
471     , mbHasDates(bHasDates)
472     , mbCanHaveSubMenu(bCanHaveSubMenu)
473     , maOpenTimer(this)
474     , maCloseTimer(this)
475 {
476     mxTreeChecks->set_clicks_to_toggle(1);
477     mxListChecks->set_clicks_to_toggle(1);
478     mxMenu->hide(); // show only when has items
479 
480     /*
481        tdf#136559 If we have no dates we don't need a tree
482        structure, just a list. GtkListStore can be then
483        used which is much faster than a GtkTreeStore, so
484        with no dates switch to the treeview which uses the
485        faster GtkListStore
486     */
487     if (mbHasDates)
488         mpChecks = mxTreeChecks.get();
489     else
490     {
491         mxTreeChecks->hide();
492         mxListChecks->show();
493         mpChecks = mxListChecks.get();
494     }
495 
496     bool bIsSubMenu = pParent->GetParentMenu();
497 
498     int nChecksHeight = mxTreeChecks->get_height_rows(9);
499     if (!bIsSubMenu && nWidth != -1)
500     {
501         mnCheckWidthReq = nWidth - mxFrame->get_border_width() * 2 - 4;
502         mxTreeChecks->set_size_request(mnCheckWidthReq, nChecksHeight);
503         mxListChecks->set_size_request(mnCheckWidthReq, nChecksHeight);
504     }
505 
506     // sort ok/cancel into native order, if this was a dialog they would be auto-sorted, but this
507     // popup isn't a true dialog
508     mxButtonBox->sort_native_button_order();
509 
510     if (!bIsSubMenu)
511     {
512         mxTreeChecks->enable_toggle_buttons(weld::ColumnToggleType::Check);
513         mxListChecks->enable_toggle_buttons(weld::ColumnToggleType::Check);
514 
515         mxBox->show();
516         mxEdSearch->show();
517         mxButtonBox->show();
518     }
519 
520     mxContainer->connect_focus_in(LINK(this, ScCheckListMenuControl, FocusHdl));
521     mxMenu->connect_row_activated(LINK(this, ScCheckListMenuControl, RowActivatedHdl));
522     mxMenu->connect_changed(LINK(this, ScCheckListMenuControl, SelectHdl));
523     mxMenu->connect_key_press(LINK(this, ScCheckListMenuControl, MenuKeyInputHdl));
524 
525     if (!bIsSubMenu)
526     {
527         mxBtnOk->connect_clicked(LINK(this, ScCheckListMenuControl, ButtonHdl));
528         mxBtnCancel->connect_clicked(LINK(this, ScCheckListMenuControl, ButtonHdl));
529         mxEdSearch->connect_changed(LINK(this, ScCheckListMenuControl, EdModifyHdl));
530         mxEdSearch->connect_activate(LINK(this, ScCheckListMenuControl, EdActivateHdl));
531         mxTreeChecks->connect_toggled(LINK(this, ScCheckListMenuControl, CheckHdl));
532         mxTreeChecks->connect_key_press(LINK(this, ScCheckListMenuControl, KeyInputHdl));
533         mxListChecks->connect_toggled(LINK(this, ScCheckListMenuControl, CheckHdl));
534         mxListChecks->connect_key_press(LINK(this, ScCheckListMenuControl, KeyInputHdl));
535         mxChkToggleAll->connect_toggled(LINK(this, ScCheckListMenuControl, TriStateHdl));
536         mxBtnSelectSingle->connect_clicked(LINK(this, ScCheckListMenuControl, ButtonHdl));
537         mxBtnUnselectSingle->connect_clicked(LINK(this, ScCheckListMenuControl, ButtonHdl));
538     }
539 
540     if (mbCanHaveSubMenu)
541     {
542         CreateDropDown();
543         mxMenu->connect_size_allocate(LINK(this, ScCheckListMenuControl, TreeSizeAllocHdl));
544     }
545 
546     if (!bIsSubMenu)
547     {
548         // determine what width the checklist will end up with
549         mnCheckWidthReq = mxContainer->get_preferred_size().Width();
550         // make that size fixed now, we can now use mnCheckWidthReq to speed up
551         // bulk_insert_for_each
552         mxTreeChecks->set_size_request(mnCheckWidthReq, nChecksHeight);
553         mxListChecks->set_size_request(mnCheckWidthReq, nChecksHeight);
554     }
555 }
556 
IMPL_LINK_NOARG(ScCheckListMenuControl,FocusHdl,weld::Widget &,void)557 IMPL_LINK_NOARG(ScCheckListMenuControl, FocusHdl, weld::Widget&, void)
558 {
559     GrabFocus();
560 }
561 
GrabFocus()562 void ScCheckListMenuControl::GrabFocus()
563 {
564     if (mxEdSearch->get_visible())
565         mxEdSearch->grab_focus();
566     else
567     {
568         mxMenu->set_cursor(0);
569         mxMenu->grab_focus();
570     }
571 }
572 
~ScCheckListMenuControl()573 ScCheckListMenuControl::~ScCheckListMenuControl()
574 {
575     EndPopupMode();
576     for (auto& rMenuItem : maMenuItems)
577         rMenuItem.mxSubMenuWin.disposeAndClear();
578     if (mnAsyncPostPopdownId)
579     {
580         Application::RemoveUserEvent(mnAsyncPostPopdownId);
581         mnAsyncPostPopdownId = nullptr;
582     }
583 }
584 
ScCheckListMenuWindow(vcl::Window * pParent,ScDocument * pDoc,bool bCanHaveSubMenu,bool bTreeMode,int nWidth,ScCheckListMenuWindow * pParentMenu,vcl::ILibreOfficeKitNotifier * pNotifier)585 ScCheckListMenuWindow::ScCheckListMenuWindow(vcl::Window* pParent, ScDocument* pDoc, bool bCanHaveSubMenu,
586                                              bool bTreeMode, int nWidth, ScCheckListMenuWindow* pParentMenu,
587                                              vcl::ILibreOfficeKitNotifier* pNotifier)
588     : DropdownDockingWindow(pParent)
589     , mxParentMenu(pParentMenu)
590 {
591     if (pNotifier)
592         SetLOKNotifier(pNotifier);
593     setDeferredProperties();
594     mxControl.reset(new ScCheckListMenuControl(this, m_xBox.get(), pDoc, bCanHaveSubMenu, bTreeMode, nWidth));
595     SetBackground(Application::GetSettings().GetStyleSettings().GetMenuColor());
596     set_id("check_list_menu");
597 }
598 
EventNotify(NotifyEvent & rNEvt)599 bool ScCheckListMenuWindow::EventNotify(NotifyEvent& rNEvt)
600 {
601     if (rNEvt.GetType() == MouseNotifyEvent::MOUSEMOVE)
602     {
603         ScCheckListMenuControl& rMenuControl = get_widget();
604         rMenuControl.queueCloseSubMenu();
605         rMenuControl.clearSelectedMenuItem();
606     }
607     return DropdownDockingWindow::EventNotify(rNEvt);
608 }
609 
~ScCheckListMenuWindow()610 ScCheckListMenuWindow::~ScCheckListMenuWindow()
611 {
612     disposeOnce();
613 }
614 
dispose()615 void ScCheckListMenuWindow::dispose()
616 {
617     mxControl.reset();
618     mxParentMenu.clear();
619     DropdownDockingWindow::dispose();
620 }
621 
GetFocus()622 void ScCheckListMenuWindow::GetFocus()
623 {
624     DropdownDockingWindow::GetFocus();
625     if (!mxControl)
626         return;
627     mxControl->GrabFocus();
628 }
629 
prepWindow()630 void ScCheckListMenuControl::prepWindow()
631 {
632     mxMenu->set_size_request(-1, mxMenu->get_preferred_size().Height() + 2);
633     mnSelectedMenu = 0;
634     mxMenu->set_cursor(mnSelectedMenu);
635     mxMenu->unselect_all();
636 
637     mnWndWidth = mxContainer->get_preferred_size().Width() + mxFrame->get_border_width() * 2 + 4;
638 }
639 
setAllMemberState(bool bSet)640 void ScCheckListMenuControl::setAllMemberState(bool bSet)
641 {
642     mpChecks->all_foreach([this, bSet](weld::TreeIter& rEntry){
643         mpChecks->set_toggle(rEntry, bSet ? TRISTATE_TRUE : TRISTATE_FALSE);
644         return false;
645     });
646 
647     if (!maConfig.mbAllowEmptySet)
648     {
649         // We need to have at least one member selected.
650         mxBtnOk->set_sensitive(GetCheckedEntryCount() != 0);
651     }
652 }
653 
selectCurrentMemberOnly(bool bSet)654 void ScCheckListMenuControl::selectCurrentMemberOnly(bool bSet)
655 {
656     setAllMemberState(!bSet);
657     std::unique_ptr<weld::TreeIter> xEntry = mpChecks->make_iterator();
658     if (!mpChecks->get_cursor(xEntry.get()))
659         return;
660     mpChecks->set_toggle(*xEntry, bSet ? TRISTATE_TRUE : TRISTATE_FALSE);
661 }
662 
IMPL_LINK(ScCheckListMenuControl,ButtonHdl,weld::Button &,rBtn,void)663 IMPL_LINK(ScCheckListMenuControl, ButtonHdl, weld::Button&, rBtn, void)
664 {
665     if (&rBtn == mxBtnOk.get())
666         close(true);
667     else if (&rBtn == mxBtnCancel.get())
668         close(false);
669     else if (&rBtn == mxBtnSelectSingle.get() || &rBtn == mxBtnUnselectSingle.get())
670     {
671         selectCurrentMemberOnly(&rBtn == mxBtnSelectSingle.get());
672         std::unique_ptr<weld::TreeIter> xEntry = mpChecks->make_iterator();
673         if (!mpChecks->get_cursor(xEntry.get()))
674             xEntry.reset();
675         Check(xEntry.get());
676     }
677 }
678 
IMPL_LINK_NOARG(ScCheckListMenuControl,TriStateHdl,weld::Toggleable &,void)679 IMPL_LINK_NOARG(ScCheckListMenuControl, TriStateHdl, weld::Toggleable&, void)
680 {
681     switch (mePrevToggleAllState)
682     {
683         case TRISTATE_FALSE:
684             mxChkToggleAll->set_state(TRISTATE_TRUE);
685             setAllMemberState(true);
686         break;
687         case TRISTATE_TRUE:
688             mxChkToggleAll->set_state(TRISTATE_FALSE);
689             setAllMemberState(false);
690         break;
691         case TRISTATE_INDET:
692         default:
693             mxChkToggleAll->set_state(TRISTATE_TRUE);
694             setAllMemberState(true);
695         break;
696     }
697 
698     mePrevToggleAllState = mxChkToggleAll->get_state();
699 }
700 
701 namespace
702 {
insertMember(weld::TreeView & rView,const weld::TreeIter & rIter,const ScCheckListMember & rMember,bool bChecked)703     void insertMember(weld::TreeView& rView, const weld::TreeIter& rIter, const ScCheckListMember& rMember, bool bChecked)
704     {
705         OUString aLabel = rMember.maName;
706         if (aLabel.isEmpty())
707             aLabel = ScResId(STR_EMPTYDATA);
708         rView.set_toggle(rIter, bChecked ? TRISTATE_TRUE : TRISTATE_FALSE);
709         rView.set_text(rIter, aLabel, 0);
710     }
711 }
712 
IMPL_LINK_NOARG(ScCheckListMenuControl,EdModifyHdl,weld::Entry &,void)713 IMPL_LINK_NOARG(ScCheckListMenuControl, EdModifyHdl, weld::Entry&, void)
714 {
715     OUString aSearchText = mxEdSearch->get_text();
716     aSearchText = ScGlobal::getCharClassPtr()->lowercase( aSearchText );
717     bool bSearchTextEmpty = aSearchText.isEmpty();
718     size_t n = maMembers.size();
719     size_t nSelCount = 0;
720 
721     // This branch is the general case, the other is an optimized variant of
722     // this one where we can take advantage of knowing we have no hierarchy
723     if (mbHasDates)
724     {
725         mpChecks->freeze();
726 
727         bool bSomeDateDeletes = false;
728 
729         for (size_t i = 0; i < n; ++i)
730         {
731             bool bIsDate = maMembers[i].mbDate;
732             bool bPartialMatch = false;
733 
734             OUString aLabelDisp = maMembers[i].maName;
735             if ( aLabelDisp.isEmpty() )
736                 aLabelDisp = ScResId( STR_EMPTYDATA );
737 
738             if ( !bSearchTextEmpty )
739             {
740                 if ( !bIsDate )
741                     bPartialMatch = ( ScGlobal::getCharClassPtr()->lowercase( aLabelDisp ).indexOf( aSearchText ) != -1 );
742                 else if ( maMembers[i].meDatePartType == ScCheckListMember::DAY ) // Match with both numerical and text version of month
743                     bPartialMatch = (ScGlobal::getCharClassPtr()->lowercase( OUString(
744                                     maMembers[i].maRealName + maMembers[i].maDateParts[1] )).indexOf( aSearchText ) != -1);
745                 else
746                     continue;
747             }
748             else if ( bIsDate && maMembers[i].meDatePartType != ScCheckListMember::DAY )
749                 continue;
750 
751             if ( bSearchTextEmpty )
752             {
753                 auto xLeaf = ShowCheckEntry(aLabelDisp, maMembers[i], true, maMembers[i].mbVisible);
754                 updateMemberParents(xLeaf.get(), i);
755                 if ( maMembers[i].mbVisible )
756                     ++nSelCount;
757                 continue;
758             }
759 
760             if ( bPartialMatch )
761             {
762                 auto xLeaf = ShowCheckEntry(aLabelDisp, maMembers[i]);
763                 updateMemberParents(xLeaf.get(), i);
764                 ++nSelCount;
765             }
766             else
767             {
768                 ShowCheckEntry(aLabelDisp, maMembers[i], false, false);
769                 if( bIsDate )
770                     bSomeDateDeletes = true;
771             }
772         }
773 
774         if ( bSomeDateDeletes )
775         {
776             for (size_t i = 0; i < n; ++i)
777             {
778                 if (!maMembers[i].mbDate)
779                     continue;
780                 if (maMembers[i].meDatePartType != ScCheckListMember::DAY)
781                     continue;
782                 updateMemberParents(nullptr, i);
783             }
784         }
785 
786         mpChecks->thaw();
787     }
788     else
789     {
790         mpChecks->freeze();
791 
792         // when there are a lot of rows, it is cheaper to simply clear the tree and either
793         // re-initialise or just insert the filtered lines
794         mpChecks->clear();
795 
796         mpChecks->thaw();
797 
798         if (bSearchTextEmpty)
799             nSelCount = initMembers();
800         else
801         {
802             std::vector<size_t> aShownIndexes;
803 
804             for (size_t i = 0; i < n; ++i)
805             {
806                 assert(!maMembers[i].mbDate);
807 
808                 OUString aLabelDisp = maMembers[i].maName;
809                 if ( aLabelDisp.isEmpty() )
810                     aLabelDisp = ScResId( STR_EMPTYDATA );
811 
812                 bool bPartialMatch = ScGlobal::getCharClassPtr()->lowercase( aLabelDisp ).indexOf( aSearchText ) != -1;
813 
814                 if (!bPartialMatch)
815                     continue;
816 
817                 aShownIndexes.push_back(i);
818             }
819 
820             std::vector<int> aFixedWidths { mnCheckWidthReq };
821             // tdf#122419 insert in the fastest order, this might be backwards.
822             mpChecks->bulk_insert_for_each(aShownIndexes.size(), [this, &aShownIndexes, &nSelCount](weld::TreeIter& rIter, int i) {
823                 size_t nIndex = aShownIndexes[i];
824                 insertMember(*mpChecks, rIter, maMembers[nIndex], true);
825                 ++nSelCount;
826             }, nullptr, &aFixedWidths);
827         }
828     }
829 
830     if ( nSelCount == n )
831         mxChkToggleAll->set_state( TRISTATE_TRUE );
832     else if ( nSelCount == 0 )
833         mxChkToggleAll->set_state( TRISTATE_FALSE );
834     else
835         mxChkToggleAll->set_state( TRISTATE_INDET );
836 
837     if ( !maConfig.mbAllowEmptySet )
838     {
839         const bool bEmptySet( nSelCount == 0 );
840         mpChecks->set_sensitive(!bEmptySet);
841         mxChkToggleAll->set_sensitive(!bEmptySet);
842         mxBtnSelectSingle->set_sensitive(!bEmptySet);
843         mxBtnUnselectSingle->set_sensitive(!bEmptySet);
844         mxBtnOk->set_sensitive(!bEmptySet);
845     }
846 }
847 
IMPL_LINK_NOARG(ScCheckListMenuControl,EdActivateHdl,weld::Entry &,bool)848 IMPL_LINK_NOARG(ScCheckListMenuControl, EdActivateHdl, weld::Entry&, bool)
849 {
850     if (mxBtnOk->get_sensitive())
851         close(true);
852     return true;
853 }
854 
IMPL_LINK(ScCheckListMenuControl,CheckHdl,const weld::TreeView::iter_col &,rRowCol,void)855 IMPL_LINK( ScCheckListMenuControl, CheckHdl, const weld::TreeView::iter_col&, rRowCol, void )
856 {
857     Check(&rRowCol.first);
858 }
859 
Check(const weld::TreeIter * pEntry)860 void ScCheckListMenuControl::Check(const weld::TreeIter* pEntry)
861 {
862     if (pEntry)
863         CheckEntry(*pEntry, mpChecks->get_toggle(*pEntry) == TRISTATE_TRUE);
864     size_t nNumChecked = GetCheckedEntryCount();
865     if (nNumChecked == maMembers.size())
866         // all members visible
867         mxChkToggleAll->set_state(TRISTATE_TRUE);
868     else if (nNumChecked == 0)
869         // no members visible
870         mxChkToggleAll->set_state(TRISTATE_FALSE);
871     else
872         mxChkToggleAll->set_state(TRISTATE_INDET);
873 
874     if (!maConfig.mbAllowEmptySet)
875         // We need to have at least one member selected.
876         mxBtnOk->set_sensitive(nNumChecked != 0);
877 
878     mePrevToggleAllState = mxChkToggleAll->get_state();
879 }
880 
updateMemberParents(const weld::TreeIter * pLeaf,size_t nIdx)881 void ScCheckListMenuControl::updateMemberParents(const weld::TreeIter* pLeaf, size_t nIdx)
882 {
883     if ( !maMembers[nIdx].mbDate || maMembers[nIdx].meDatePartType != ScCheckListMember::DAY )
884         return;
885 
886     OUString aYearName  = maMembers[nIdx].maDateParts[0];
887     OUString aMonthName = maMembers[nIdx].maDateParts[1];
888     auto aItr = maYearMonthMap.find(aYearName + aMonthName);
889 
890     if ( pLeaf )
891     {
892         std::unique_ptr<weld::TreeIter> xYearEntry;
893         std::unique_ptr<weld::TreeIter> xMonthEntry = mpChecks->make_iterator(pLeaf);
894         if (!mpChecks->iter_parent(*xMonthEntry))
895             xMonthEntry.reset();
896         else
897         {
898             xYearEntry = mpChecks->make_iterator(xMonthEntry.get());
899             if (!mpChecks->iter_parent(*xYearEntry))
900                 xYearEntry.reset();
901         }
902 
903         maMembers[nIdx].mxParent = std::move(xMonthEntry);
904         if ( aItr != maYearMonthMap.end() )
905         {
906             size_t nMonthIdx = aItr->second;
907             maMembers[nMonthIdx].mxParent = std::move(xYearEntry);
908         }
909     }
910     else
911     {
912         std::unique_ptr<weld::TreeIter> xYearEntry = FindEntry(nullptr, aYearName);
913         if (aItr != maYearMonthMap.end() && !xYearEntry)
914         {
915             size_t nMonthIdx = aItr->second;
916             maMembers[nMonthIdx].mxParent.reset();
917             maMembers[nIdx].mxParent.reset();
918         }
919         else if (xYearEntry && !FindEntry(xYearEntry.get(), aMonthName))
920             maMembers[nIdx].mxParent.reset();
921     }
922 }
923 
setMemberSize(size_t n)924 void ScCheckListMenuControl::setMemberSize(size_t n)
925 {
926     maMembers.reserve(n);
927 }
928 
addDateMember(const OUString & rsName,double nVal,bool bVisible)929 void ScCheckListMenuControl::addDateMember(const OUString& rsName, double nVal, bool bVisible)
930 {
931     SvNumberFormatter* pFormatter = mpDoc->GetFormatTable();
932 
933     // Convert the numeric date value to a date object.
934     Date aDate = pFormatter->GetNullDate();
935     aDate.AddDays(rtl::math::approxFloor(nVal));
936 
937     sal_Int16 nYear = aDate.GetYear();
938     sal_uInt16 nMonth = aDate.GetMonth();
939     sal_uInt16 nDay = aDate.GetDay();
940 
941     // Get the localized month name list.
942     CalendarWrapper* pCalendar = ScGlobal::GetCalendar();
943     uno::Sequence<i18n::CalendarItem2> aMonths = pCalendar->getMonths();
944     if (aMonths.getLength() < nMonth)
945         return;
946 
947     OUString aYearName = OUString::number(nYear);
948     OUString aMonthName = aMonths[nMonth-1].FullName;
949     OUString aDayName = OUString::number(nDay);
950 
951     if ( aDayName.getLength() == 1 )
952         aDayName = "0" + aDayName;
953 
954     mpChecks->freeze();
955 
956     std::unique_ptr<weld::TreeIter> xYearEntry = FindEntry(nullptr, aYearName);
957     if (!xYearEntry)
958     {
959         xYearEntry = mpChecks->make_iterator();
960         mpChecks->insert(nullptr, -1, nullptr, nullptr, nullptr, nullptr, false, xYearEntry.get());
961         mpChecks->set_toggle(*xYearEntry, TRISTATE_FALSE);
962         mpChecks->set_text(*xYearEntry, aYearName, 0);
963         ScCheckListMember aMemYear;
964         aMemYear.maName = aYearName;
965         aMemYear.maRealName = rsName;
966         aMemYear.mbDate = true;
967         aMemYear.mbLeaf = false;
968         aMemYear.mbVisible = bVisible;
969         aMemYear.mxParent.reset();
970         aMemYear.meDatePartType = ScCheckListMember::YEAR;
971         maMembers.emplace_back(std::move(aMemYear));
972     }
973 
974     std::unique_ptr<weld::TreeIter> xMonthEntry = FindEntry(xYearEntry.get(), aMonthName);
975     if (!xMonthEntry)
976     {
977         xMonthEntry = mpChecks->make_iterator();
978         mpChecks->insert(xYearEntry.get(), -1, nullptr, nullptr, nullptr, nullptr, false, xMonthEntry.get());
979         mpChecks->set_toggle(*xMonthEntry, TRISTATE_FALSE);
980         mpChecks->set_text(*xMonthEntry, aMonthName, 0);
981         ScCheckListMember aMemMonth;
982         aMemMonth.maName = aMonthName;
983         aMemMonth.maRealName = rsName;
984         aMemMonth.mbDate = true;
985         aMemMonth.mbLeaf = false;
986         aMemMonth.mbVisible = bVisible;
987         aMemMonth.mxParent = std::move(xYearEntry);
988         aMemMonth.meDatePartType = ScCheckListMember::MONTH;
989         maMembers.emplace_back(std::move(aMemMonth));
990         maYearMonthMap[aYearName + aMonthName] = maMembers.size() - 1;
991     }
992 
993     std::unique_ptr<weld::TreeIter> xDayEntry = FindEntry(xMonthEntry.get(), aDayName);
994     if (!xDayEntry)
995     {
996         xDayEntry = mpChecks->make_iterator();
997         mpChecks->insert(xMonthEntry.get(), -1, nullptr, nullptr, nullptr, nullptr, false, xDayEntry.get());
998         mpChecks->set_toggle(*xDayEntry, TRISTATE_FALSE);
999         mpChecks->set_text(*xDayEntry, aDayName, 0);
1000         ScCheckListMember aMemDay;
1001         aMemDay.maName = aDayName;
1002         aMemDay.maRealName = rsName;
1003         aMemDay.maDateParts.resize(2);
1004         aMemDay.maDateParts[0] = aYearName;
1005         aMemDay.maDateParts[1] = aMonthName;
1006         aMemDay.mbDate = true;
1007         aMemDay.mbLeaf = true;
1008         aMemDay.mbVisible = bVisible;
1009         aMemDay.mxParent = std::move(xMonthEntry);
1010         aMemDay.meDatePartType = ScCheckListMember::DAY;
1011         maMembers.emplace_back(std::move(aMemDay));
1012     }
1013 
1014     mpChecks->thaw();
1015 }
1016 
addMember(const OUString & rName,const double nVal,bool bVisible,bool bValue)1017 void ScCheckListMenuControl::addMember(const OUString& rName, const double nVal, bool bVisible, bool bValue)
1018 {
1019     ScCheckListMember aMember;
1020     // tdf#46062 - indicate hidden whitespaces using quotes
1021     aMember.maName = rName.trim() != rName ? "\"" + rName + "\"" : rName;
1022     aMember.maRealName = rName;
1023     aMember.mnValue = nVal;
1024     aMember.mbDate = false;
1025     aMember.mbLeaf = true;
1026     aMember.mbValue = bValue;
1027     aMember.mbVisible = bVisible;
1028     aMember.mxParent.reset();
1029     maMembers.emplace_back(std::move(aMember));
1030 }
1031 
FindEntry(const weld::TreeIter * pParent,std::u16string_view sNode)1032 std::unique_ptr<weld::TreeIter> ScCheckListMenuControl::FindEntry(const weld::TreeIter* pParent, std::u16string_view sNode)
1033 {
1034     std::unique_ptr<weld::TreeIter> xEntry = mpChecks->make_iterator(pParent);
1035     bool bEntry = pParent ? mpChecks->iter_children(*xEntry) : mpChecks->get_iter_first(*xEntry);
1036     while (bEntry)
1037     {
1038         if (sNode == mpChecks->get_text(*xEntry, 0))
1039             return xEntry;
1040         bEntry = mpChecks->iter_next_sibling(*xEntry);
1041     }
1042     return nullptr;
1043 }
1044 
GetRecursiveChecked(const weld::TreeIter * pEntry,std::unordered_set<OUString> & vOut,OUString & rLabel)1045 void ScCheckListMenuControl::GetRecursiveChecked(const weld::TreeIter* pEntry, std::unordered_set<OUString>& vOut,
1046                                                  OUString& rLabel)
1047 {
1048     if (mpChecks->get_toggle(*pEntry) != TRISTATE_TRUE)
1049         return;
1050 
1051     // We have to hash parents and children together.
1052     // Per convention for easy access in getResult()
1053     // "child;parent;grandparent" while descending.
1054     if (rLabel.isEmpty())
1055         rLabel = mpChecks->get_text(*pEntry, 0);
1056     else
1057         rLabel = mpChecks->get_text(*pEntry, 0) + ";" + rLabel;
1058 
1059     // Prerequisite: the selection mechanism guarantees that if a child is
1060     // selected then also the parent is selected, so we only have to
1061     // inspect the children in case the parent is selected.
1062     if (!mpChecks->iter_has_child(*pEntry))
1063         return;
1064 
1065     std::unique_ptr<weld::TreeIter> xChild(mpChecks->make_iterator(pEntry));
1066     bool bChild = mpChecks->iter_children(*xChild);
1067     while (bChild)
1068     {
1069         OUString aLabel = rLabel;
1070         GetRecursiveChecked(xChild.get(), vOut, aLabel);
1071         if (!aLabel.isEmpty() && aLabel != rLabel)
1072             vOut.insert(aLabel);
1073         bChild = mpChecks->iter_next_sibling(*xChild);
1074     }
1075     // Let the caller not add the parent alone.
1076     rLabel.clear();
1077 }
1078 
GetAllChecked()1079 std::unordered_set<OUString> ScCheckListMenuControl::GetAllChecked()
1080 {
1081     std::unordered_set<OUString> vResults(0);
1082 
1083     std::unique_ptr<weld::TreeIter> xEntry = mpChecks->make_iterator();
1084     bool bEntry = mpChecks->get_iter_first(*xEntry);
1085     while (bEntry)
1086     {
1087         OUString aLabel;
1088         GetRecursiveChecked(xEntry.get(), vResults, aLabel);
1089         if (!aLabel.isEmpty())
1090             vResults.insert(aLabel);
1091         bEntry = mpChecks->iter_next_sibling(*xEntry);
1092     }
1093 
1094     return vResults;
1095 }
1096 
IsChecked(std::u16string_view sName,const weld::TreeIter * pParent)1097 bool ScCheckListMenuControl::IsChecked(std::u16string_view sName, const weld::TreeIter* pParent)
1098 {
1099     std::unique_ptr<weld::TreeIter> xEntry = FindEntry(pParent, sName);
1100     return xEntry && mpChecks->get_toggle(*xEntry) == TRISTATE_TRUE;
1101 }
1102 
CheckEntry(std::u16string_view sName,const weld::TreeIter * pParent,bool bCheck)1103 void ScCheckListMenuControl::CheckEntry(std::u16string_view sName, const weld::TreeIter* pParent, bool bCheck)
1104 {
1105     std::unique_ptr<weld::TreeIter> xEntry = FindEntry(pParent, sName);
1106     if (xEntry)
1107         CheckEntry(*xEntry, bCheck);
1108 }
1109 
1110 // Recursively check all children of rParent
CheckAllChildren(const weld::TreeIter & rParent,bool bCheck)1111 void ScCheckListMenuControl::CheckAllChildren(const weld::TreeIter& rParent, bool bCheck)
1112 {
1113     mpChecks->set_toggle(rParent, bCheck ? TRISTATE_TRUE : TRISTATE_FALSE);
1114     std::unique_ptr<weld::TreeIter> xEntry = mpChecks->make_iterator(&rParent);
1115     bool bEntry = mpChecks->iter_children(*xEntry);
1116     while (bEntry)
1117     {
1118         CheckAllChildren(*xEntry, bCheck);
1119         bEntry = mpChecks->iter_next_sibling(*xEntry);
1120     }
1121 }
1122 
CheckEntry(const weld::TreeIter & rParent,bool bCheck)1123 void ScCheckListMenuControl::CheckEntry(const weld::TreeIter& rParent, bool bCheck)
1124 {
1125     // recursively check all items below rParent
1126     CheckAllChildren(rParent, bCheck);
1127     // checking rParent can affect ancestors, e.g. if ancestor is unchecked and rParent is
1128     // now checked then the ancestor needs to be checked also
1129     if (!mpChecks->get_iter_depth(rParent))
1130         return;
1131 
1132     std::unique_ptr<weld::TreeIter> xAncestor(mpChecks->make_iterator(&rParent));
1133     bool bAncestor = mpChecks->iter_parent(*xAncestor);
1134     while (bAncestor)
1135     {
1136         // if any first level children checked then ancestor
1137         // needs to be checked, similarly if no first level children
1138         // checked then ancestor needs to be unchecked
1139         std::unique_ptr<weld::TreeIter> xChild(mpChecks->make_iterator(xAncestor.get()));
1140         bool bChild = mpChecks->iter_children(*xChild);
1141         bool bChildChecked = false;
1142 
1143         while (bChild)
1144         {
1145             if (mpChecks->get_toggle(*xChild) == TRISTATE_TRUE)
1146             {
1147                 bChildChecked = true;
1148                 break;
1149             }
1150             bChild = mpChecks->iter_next_sibling(*xChild);
1151         }
1152         mpChecks->set_toggle(*xAncestor, bChildChecked ? TRISTATE_TRUE : TRISTATE_FALSE);
1153         bAncestor = mpChecks->iter_parent(*xAncestor);
1154     }
1155 }
1156 
ShowCheckEntry(const OUString & sName,ScCheckListMember & rMember,bool bShow,bool bCheck)1157 std::unique_ptr<weld::TreeIter> ScCheckListMenuControl::ShowCheckEntry(const OUString& sName, ScCheckListMember& rMember, bool bShow, bool bCheck)
1158 {
1159     std::unique_ptr<weld::TreeIter> xEntry;
1160     if (!rMember.mbDate || rMember.mxParent)
1161         xEntry = FindEntry(rMember.mxParent.get(), sName);
1162 
1163     if ( bShow )
1164     {
1165         if (!xEntry)
1166         {
1167             if (rMember.mbDate)
1168             {
1169                 if (rMember.maDateParts.empty())
1170                     return nullptr;
1171 
1172                 std::unique_ptr<weld::TreeIter> xYearEntry = FindEntry(nullptr, rMember.maDateParts[0]);
1173                 if (!xYearEntry)
1174                 {
1175                     xYearEntry = mpChecks->make_iterator();
1176                     mpChecks->insert(nullptr, -1, nullptr, nullptr, nullptr, nullptr, false, xYearEntry.get());
1177                     mpChecks->set_toggle(*xYearEntry, TRISTATE_FALSE);
1178                     mpChecks->set_text(*xYearEntry, rMember.maDateParts[0], 0);
1179                 }
1180                 std::unique_ptr<weld::TreeIter> xMonthEntry = FindEntry(xYearEntry.get(), rMember.maDateParts[1]);
1181                 if (!xMonthEntry)
1182                 {
1183                     xMonthEntry = mpChecks->make_iterator();
1184                     mpChecks->insert(xYearEntry.get(), -1, nullptr, nullptr, nullptr, nullptr, false, xMonthEntry.get());
1185                     mpChecks->set_toggle(*xMonthEntry, TRISTATE_FALSE);
1186                     mpChecks->set_text(*xMonthEntry, rMember.maDateParts[1], 0);
1187                 }
1188                 std::unique_ptr<weld::TreeIter> xDayEntry = FindEntry(xMonthEntry.get(), rMember.maName);
1189                 if (!xDayEntry)
1190                 {
1191                     xDayEntry = mpChecks->make_iterator();
1192                     mpChecks->insert(xMonthEntry.get(), -1, nullptr, nullptr, nullptr, nullptr, false, xDayEntry.get());
1193                     mpChecks->set_toggle(*xDayEntry, TRISTATE_FALSE);
1194                     mpChecks->set_text(*xDayEntry, rMember.maName, 0);
1195                 }
1196                 return xDayEntry; // Return leaf node
1197             }
1198 
1199             xEntry = mpChecks->make_iterator();
1200             mpChecks->append(xEntry.get());
1201             mpChecks->set_toggle(*xEntry, bCheck ? TRISTATE_TRUE : TRISTATE_FALSE);
1202             mpChecks->set_text(*xEntry, sName, 0);
1203         }
1204         else
1205             CheckEntry(*xEntry, bCheck);
1206     }
1207     else if (xEntry)
1208     {
1209         mpChecks->remove(*xEntry);
1210         if (rMember.mxParent)
1211         {
1212             std::unique_ptr<weld::TreeIter> xParent(mpChecks->make_iterator(rMember.mxParent.get()));
1213             while (xParent && !mpChecks->iter_has_child(*xParent))
1214             {
1215                 std::unique_ptr<weld::TreeIter> xTmp(mpChecks->make_iterator(xParent.get()));
1216                 if (!mpChecks->iter_parent(*xParent))
1217                     xParent.reset();
1218                 mpChecks->remove(*xTmp);
1219             }
1220         }
1221     }
1222     return nullptr;
1223 }
1224 
GetCheckedEntryCount() const1225 int ScCheckListMenuControl::GetCheckedEntryCount() const
1226 {
1227     int nRet = 0;
1228 
1229     mpChecks->all_foreach([this, &nRet](weld::TreeIter& rEntry){
1230         if (mpChecks->get_toggle(rEntry) == TRISTATE_TRUE)
1231             ++nRet;
1232         return false;
1233     });
1234 
1235     return nRet;
1236 }
1237 
IMPL_LINK(ScCheckListMenuControl,KeyInputHdl,const KeyEvent &,rKEvt,bool)1238 IMPL_LINK(ScCheckListMenuControl, KeyInputHdl, const KeyEvent&, rKEvt, bool)
1239 {
1240     const vcl::KeyCode& rKey = rKEvt.GetKeyCode();
1241 
1242     if ( rKey.GetCode() == KEY_RETURN || rKey.GetCode() == KEY_SPACE )
1243     {
1244         std::unique_ptr<weld::TreeIter> xEntry = mpChecks->make_iterator();
1245         bool bEntry = mpChecks->get_cursor(xEntry.get());
1246         if (bEntry)
1247         {
1248             bool bOldCheck = mpChecks->get_toggle(*xEntry) == TRISTATE_TRUE;
1249             CheckEntry(*xEntry, !bOldCheck);
1250             bool bNewCheck = mpChecks->get_toggle(*xEntry) == TRISTATE_TRUE;
1251             if (bOldCheck != bNewCheck)
1252                 Check(xEntry.get());
1253         }
1254         return true;
1255     }
1256 
1257     return false;
1258 }
1259 
initMembers(int nMaxMemberWidth)1260 size_t ScCheckListMenuControl::initMembers(int nMaxMemberWidth)
1261 {
1262     size_t n = maMembers.size();
1263     size_t nVisMemCount = 0;
1264 
1265     if (nMaxMemberWidth == -1)
1266         nMaxMemberWidth = mnCheckWidthReq;
1267 
1268     if (!mpChecks->n_children() && !mbHasDates)
1269     {
1270         std::vector<int> aFixedWidths { nMaxMemberWidth };
1271         // tdf#134038 insert in the fastest order, this might be backwards so only do it for
1272         // the !mbHasDates case where no entry depends on another to exist before getting
1273         // inserted. We cannot retain pre-existing treeview content, only clear and fill it.
1274         mpChecks->bulk_insert_for_each(n, [this, &nVisMemCount](weld::TreeIter& rIter, int i) {
1275             assert(!maMembers[i].mbDate);
1276             insertMember(*mpChecks, rIter, maMembers[i], maMembers[i].mbVisible);
1277             if (maMembers[i].mbVisible)
1278                 ++nVisMemCount;
1279         }, nullptr, &aFixedWidths);
1280     }
1281     else
1282     {
1283         mpChecks->freeze();
1284 
1285         std::unique_ptr<weld::TreeIter> xEntry = mpChecks->make_iterator();
1286         std::vector<std::unique_ptr<weld::TreeIter>> aExpandRows;
1287 
1288         for (size_t i = 0; i < n; ++i)
1289         {
1290             if (maMembers[i].mbDate)
1291             {
1292                 CheckEntry(maMembers[i].maName, maMembers[i].mxParent.get(), maMembers[i].mbVisible);
1293                 // Expand first node of checked dates
1294                 if (!maMembers[i].mxParent && IsChecked(maMembers[i].maName, maMembers[i].mxParent.get()))
1295                 {
1296                     std::unique_ptr<weld::TreeIter> xDateEntry = FindEntry(nullptr, maMembers[i].maName);
1297                     if (xDateEntry)
1298                         aExpandRows.emplace_back(std::move(xDateEntry));
1299                 }
1300             }
1301             else
1302             {
1303                 mpChecks->append(xEntry.get());
1304                 insertMember(*mpChecks, *xEntry, maMembers[i], maMembers[i].mbVisible);
1305             }
1306 
1307             if (maMembers[i].mbVisible)
1308                 ++nVisMemCount;
1309         }
1310 
1311         mpChecks->thaw();
1312 
1313         for (auto& rRow : aExpandRows)
1314             mpChecks->expand_row(*rRow);
1315     }
1316 
1317     if (nVisMemCount == n)
1318     {
1319         // all members visible
1320         mxChkToggleAll->set_state(TRISTATE_TRUE);
1321         mePrevToggleAllState = TRISTATE_TRUE;
1322     }
1323     else if (nVisMemCount == 0)
1324     {
1325         // no members visible
1326         mxChkToggleAll->set_state(TRISTATE_FALSE);
1327         mePrevToggleAllState = TRISTATE_FALSE;
1328     }
1329     else
1330     {
1331         mxChkToggleAll->set_state(TRISTATE_INDET);
1332         mePrevToggleAllState = TRISTATE_INDET;
1333     }
1334 
1335     if (nVisMemCount)
1336         mpChecks->select(0);
1337 
1338     return nVisMemCount;
1339 }
1340 
setConfig(const Config & rConfig)1341 void ScCheckListMenuControl::setConfig(const Config& rConfig)
1342 {
1343     maConfig = rConfig;
1344 }
1345 
isAllSelected() const1346 bool ScCheckListMenuControl::isAllSelected() const
1347 {
1348     return mxChkToggleAll->get_state() == TRISTATE_TRUE;
1349 }
1350 
getResult(ResultType & rResult)1351 void ScCheckListMenuControl::getResult(ResultType& rResult)
1352 {
1353     ResultType aResult;
1354     std::unordered_set<OUString> vCheckeds = GetAllChecked();
1355     size_t n = maMembers.size();
1356     for (size_t i = 0; i < n; ++i)
1357     {
1358         if ( maMembers[i].mbLeaf )
1359         {
1360             OUStringBuffer aLabel = maMembers[i].maName;
1361             if (aLabel.isEmpty())
1362                 aLabel = ScResId(STR_EMPTYDATA);
1363 
1364             /* TODO: performance-wise this looks suspicious, concatenating to
1365              * do the lookup for each leaf item seems wasteful. */
1366             // Checked labels are in the form "child;parent;grandparent".
1367             if (maMembers[i].mxParent)
1368             {
1369                 std::unique_ptr<weld::TreeIter> xIter(mpChecks->make_iterator(maMembers[i].mxParent.get()));
1370                 do
1371                 {
1372                     aLabel.append(";" + mpChecks->get_text(*xIter));
1373                 }
1374                 while (mpChecks->iter_parent(*xIter));
1375             }
1376 
1377             bool bState = vCheckeds.find(aLabel.makeStringAndClear()) != vCheckeds.end();
1378 
1379             ResultEntry aResultEntry;
1380             aResultEntry.bValid = bState;
1381             aResultEntry.aName = maMembers[i].maRealName;
1382             aResultEntry.nValue = maMembers[i].mnValue;
1383             aResultEntry.bDate = maMembers[i].mbDate;
1384             aResultEntry.bValue = maMembers[i].mbValue;
1385             aResult.insert(aResultEntry);
1386         }
1387     }
1388     rResult.swap(aResult);
1389 }
1390 
launch(const tools::Rectangle & rRect)1391 void ScCheckListMenuControl::launch(const tools::Rectangle& rRect)
1392 {
1393     prepWindow();
1394     if (!maConfig.mbAllowEmptySet)
1395         // We need to have at least one member selected.
1396         mxBtnOk->set_sensitive(GetCheckedEntryCount() != 0);
1397 
1398     tools::Rectangle aRect(rRect);
1399     if (maConfig.mbRTL)
1400     {
1401         // In RTL mode, the logical "left" is visual "right".
1402         tools::Long nLeft = aRect.Left() - aRect.GetWidth();
1403         aRect.SetLeft( nLeft );
1404     }
1405     else if (mnWndWidth < aRect.GetWidth())
1406     {
1407         // Target rectangle (i.e. cell width) is wider than the window.
1408         // Simulate right-aligned launch by modifying the target rectangle
1409         // size.
1410         tools::Long nDiff = aRect.GetWidth() - mnWndWidth;
1411         aRect.AdjustLeft(nDiff );
1412     }
1413 
1414     StartPopupMode(aRect, FloatWinPopupFlags::Down);
1415 }
1416 
NotifyCloseLOK()1417 void ScCheckListMenuControl::NotifyCloseLOK()
1418 {
1419     VclPtr<vcl::Window> aNotifierWindow = mxFrame->GetParentWithLOKNotifier();
1420     if (!aNotifierWindow)
1421         return;
1422 
1423     const vcl::ILibreOfficeKitNotifier* pNotifier = aNotifierWindow->GetLOKNotifier();
1424     if (pNotifier)
1425     {
1426         tools::JsonWriter aJsonWriter;
1427         aJsonWriter.put("jsontype", "autofilter");
1428         aJsonWriter.put("action", "close");
1429 
1430         const std::string message = aJsonWriter.extractAsStdString();
1431         pNotifier->libreOfficeKitViewCallback(LOK_CALLBACK_JSDIALOG, message.c_str());
1432     }
1433 }
1434 
close(bool bOK)1435 void ScCheckListMenuControl::close(bool bOK)
1436 {
1437     if (bOK && mxOKAction)
1438         mxOKAction->execute();
1439     EndPopupMode();
1440 
1441     if (comphelper::LibreOfficeKit::isActive())
1442         NotifyCloseLOK();
1443 }
1444 
setExtendedData(std::unique_ptr<ExtendedData> p)1445 void ScCheckListMenuControl::setExtendedData(std::unique_ptr<ExtendedData> p)
1446 {
1447     mxExtendedData = std::move(p);
1448 }
1449 
getExtendedData()1450 ScCheckListMenuControl::ExtendedData* ScCheckListMenuControl::getExtendedData()
1451 {
1452     return mxExtendedData.get();
1453 }
1454 
setOKAction(Action * p)1455 void ScCheckListMenuControl::setOKAction(Action* p)
1456 {
1457     mxOKAction.reset(p);
1458 }
1459 
setPopupEndAction(Action * p)1460 void ScCheckListMenuControl::setPopupEndAction(Action* p)
1461 {
1462     mxPopupEndAction.reset(p);
1463 }
1464 
IMPL_LINK_NOARG(ScCheckListMenuControl,PopupModeEndHdl,FloatingWindow *,void)1465 IMPL_LINK_NOARG(ScCheckListMenuControl, PopupModeEndHdl, FloatingWindow*, void)
1466 {
1467     clearSelectedMenuItem();
1468     if (mxPopupEndAction)
1469         mxPopupEndAction->execute();
1470 
1471     if (comphelper::LibreOfficeKit::isActive())
1472         NotifyCloseLOK();
1473 }
1474 
GetTextWidth(const OUString & rsName) const1475 int ScCheckListMenuControl::GetTextWidth(const OUString& rsName) const
1476 {
1477     return mxDropDown->GetTextWidth(rsName);
1478 }
1479 
IncreaseWindowWidthToFitText(int nMaxTextWidth)1480 int ScCheckListMenuControl::IncreaseWindowWidthToFitText(int nMaxTextWidth)
1481 {
1482     int nBorder = mxFrame->get_border_width() * 2 + 4;
1483     int nNewWidth = nMaxTextWidth - nBorder;
1484     if (nNewWidth > mnCheckWidthReq)
1485     {
1486         mnCheckWidthReq = nNewWidth;
1487         int nChecksHeight = mpChecks->get_height_rows(9);
1488         mpChecks->set_size_request(mnCheckWidthReq, nChecksHeight);
1489     }
1490     return mnCheckWidthReq + nBorder;
1491 }
1492 
1493 /* vim:set shiftwidth=4 softtabstop=4 expandtab: */
1494