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