Coverage Report

Created: 2025-11-24 06:30

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/wt/src/Wt/WMenuItem.C
Line
Count
Source
1
/*
2
 * Copyright (C) 2008 Emweb bv, Herent, Belgium.
3
 *
4
 * See the LICENSE file for terms of use.
5
 */
6
7
#include "Wt/WAnchor.h"
8
#include "Wt/WApplication.h"
9
#include "Wt/WBootstrap5Theme.h"
10
#include "Wt/WCheckBox.h"
11
#include "Wt/WContainerWidget.h"
12
#include "Wt/WEnvironment.h"
13
#include "Wt/WException.h"
14
#include "Wt/WLabel.h"
15
#include "Wt/WMenuItem.h"
16
#include "Wt/WMenu.h"
17
#include "Wt/WPopupMenu.h"
18
#include "Wt/WSignal.h"
19
#include "Wt/WStackedWidget.h"
20
#include "Wt/WText.h"
21
#include "Wt/WTheme.h"
22
23
#include "StdWidgetItemImpl.h"
24
25
#include <algorithm>
26
#include <cctype>
27
#include <memory>
28
29
#include <Wt/WLogger.h>
30
31
namespace Wt {
32
33
LOGGER("WMenuItem");
34
35
WMenuItem::WMenuItem(const WString& text, std::unique_ptr<WWidget> contents,
36
                     ContentLoading policy)
37
0
  : separator_(false)
38
0
{
39
0
  create(std::string(), text, std::move(contents), policy);
40
0
}
41
42
WMenuItem::WMenuItem(const std::string& iconPath, const WString& text,
43
                     std::unique_ptr<WWidget> contents, ContentLoading policy)
44
0
  : separator_(false)
45
0
{
46
0
  create(iconPath, text, std::move(contents), policy);
47
0
}
48
49
WMenuItem::WMenuItem(bool separator, const WString& text)
50
0
  : separator_(true)
51
0
{
52
0
  create(std::string(), WString::Empty, nullptr, ContentLoading::Lazy);
53
54
0
  separator_ = separator;
55
0
  selectable_ = false;
56
0
  internalPathEnabled_ = false;
57
58
0
  if (!text.empty()) {
59
0
    text_ = addWidget(std::make_unique<WLabel>());
60
0
    text_->setTextFormat(TextFormat::Plain);
61
0
    text_->setText(text);
62
0
  }
63
0
}
64
65
void WMenuItem::create(const std::string& iconPath, const WString& text,
66
                       std::unique_ptr<WWidget> contents, ContentLoading policy)
67
0
{
68
0
  customLink_ = false;
69
70
0
  menu_ = nullptr;
71
0
  customPathComponent_ = false;
72
0
  internalPathEnabled_ = true;
73
0
  closeable_ = false;
74
0
  selectable_ = true;
75
0
  selectConnected_ = false;
76
0
  menuItemCheckedConnected_ = false;
77
0
  selected_ = false;
78
0
  setThemeStyle_ = false;
79
80
0
  text_ = nullptr;
81
0
  icon_ = nullptr;
82
0
  checkBox_ = nullptr;
83
0
  subMenu_ = nullptr;
84
0
  data_ = nullptr;
85
86
0
  setContents(std::move(contents), policy);
87
88
0
  if (!separator_) {
89
0
    addWidget(std::make_unique<WAnchor>());
90
0
    updateInternalPath();
91
0
  }
92
93
0
  signalsConnected_ = false;
94
95
0
  if (!iconPath.empty())
96
0
    setIcon(iconPath);
97
98
0
  if (!separator_)
99
0
    setText(text);
100
0
}
101
102
WMenuItem::~WMenuItem()
103
0
{ }
104
105
void WMenuItem::setContents(std::unique_ptr<WWidget> contents,
106
                            ContentLoading policy)
107
0
{
108
0
  int menuIdx = -1;
109
0
  WMenu *menu = menu_;
110
0
  std::unique_ptr<WMenuItem> self;
111
0
  if (menu) {
112
0
    menuIdx = menu->indexOf(this);
113
0
    self = menu->removeItem(this);
114
0
  }
115
116
0
  uContents_ = std::move(contents);
117
0
  oContents_ = uContents_.get();
118
0
  loadPolicy_ = policy;
119
120
0
  if (uContents_ && loadPolicy_ == ContentLoading::Lazy) {
121
0
     if (!oContentsContainer_) {
122
0
       uContentsContainer_.reset(new WContainerWidget());
123
0
       oContentsContainer_ = uContentsContainer_.get();
124
0
       oContentsContainer_
125
0
         ->setJavaScriptMember("wtResize",
126
0
                               StdWidgetItemImpl::childrenResizeJS());
127
128
0
       oContentsContainer_->resize(WLength::Auto,
129
0
                                  WLength(100, LengthUnit::Percentage));
130
0
     }
131
0
   }
132
133
0
  if (menu) {
134
0
    menu->insertItem(menuIdx, std::move(self));
135
0
  }
136
0
}
137
138
bool WMenuItem::isSectionHeader() const
139
0
{
140
0
  WAnchor *a = anchor();
141
0
  return !separator_ && !a && !subMenu_ && text_;
142
0
}
143
144
WAnchor *WMenuItem::anchor() const
145
0
{
146
0
  for (int i = 0; i < count(); ++i) {
147
0
    WAnchor *result = dynamic_cast<WAnchor *>(widget(i));
148
0
    if (result)
149
0
      return result;
150
0
  }
151
152
0
  return nullptr;
153
0
}
154
155
void WMenuItem::setIcon(const std::string& path)
156
0
{
157
0
  if (!icon_) {
158
0
    WAnchor *a = anchor();
159
0
    if (!a)
160
0
      return;
161
162
0
    icon_ = a->insertWidget(0, std::make_unique<WText>(" "));
163
164
0
    WApplication *app = WApplication::instance();
165
0
    app->theme()->apply(this, icon_, MenuItemIcon);
166
0
  }
167
168
0
  icon_->decorationStyle().setBackgroundImage(WLink(path));
169
0
}
170
171
std::string WMenuItem::icon() const
172
0
{
173
0
  if (icon_)
174
0
    return icon_->decorationStyle().backgroundImage();
175
0
  else
176
0
    return std::string();
177
0
}
178
179
void WMenuItem::setText(const WString& text)
180
0
{
181
0
  if (!text_) {
182
0
    text_ = anchor()->addWidget(std::make_unique<WLabel>());
183
0
    text_->setTextFormat(TextFormat::Plain);
184
0
  }
185
186
0
  text_->setText(text);
187
188
0
  if (!customPathComponent_) {
189
0
    std::string result;
190
#ifdef WT_TARGET_JAVA
191
    WString t = text;
192
#else
193
0
    const WString& t = text;
194
0
#endif
195
196
0
    if (t.literal())
197
0
      result = t.narrow();
198
0
    else
199
0
      result = t.key();
200
201
0
    for (unsigned i = 0; i < result.length(); ++i) {
202
0
      if (std::isspace((unsigned char)result[i]))
203
0
        result[i] = '-';
204
0
      else if (std::isalnum((unsigned char)result[i]))
205
0
        result[i] = std::tolower((unsigned char)result[i]);
206
0
      else
207
0
        result[i] = '_';
208
0
    }
209
210
0
    setPathComponent(result);
211
0
    customPathComponent_ = false;
212
0
  }
213
0
}
214
215
WString WMenuItem::text() const
216
0
{
217
0
  if (text_)
218
0
    return text_->text();
219
0
  else
220
0
    return WString::Empty;
221
0
}
222
223
std::string WMenuItem::pathComponent() const
224
0
{
225
0
  return pathComponent_;
226
0
}
227
228
void WMenuItem::setInternalPathEnabled(bool enabled)
229
0
{
230
0
  internalPathEnabled_ = enabled;
231
0
  updateInternalPath();
232
0
}
233
234
bool WMenuItem::internalPathEnabled() const
235
0
{
236
0
  return internalPathEnabled_;
237
0
}
238
239
void WMenuItem::setLink(const WLink& link)
240
0
{
241
0
  WAnchor *a = anchor();
242
0
  if (a)
243
0
    a->setLink(link);
244
245
0
  customLink_ = true;
246
0
}
247
248
WLink WMenuItem::link() const
249
0
{
250
0
  WAnchor *a = anchor();
251
0
  if (a)
252
0
    return a->link();
253
0
  else
254
0
    return WLink();
255
0
}
256
257
void WMenuItem::updateInternalPath()
258
0
{
259
0
  if (menu_ && menu_->internalPathEnabled() && internalPathEnabled()) {
260
0
    std::string internalPath = menu_->internalBasePath() + pathComponent();
261
0
    WLink link(LinkType::InternalPath, internalPath);
262
0
    WAnchor *a = anchor();
263
0
    if (a)
264
0
      a->setLink(link);
265
0
  } else {
266
0
    WAnchor *a = anchor();
267
0
    if (a && !customLink_) {
268
0
      if (WApplication::instance()->environment().agent() ==
269
0
          UserAgent::IE6)
270
0
        a->setLink(WLink("#"));
271
0
      else
272
0
        a->setLink(WLink());
273
0
    }
274
0
  }
275
0
}
276
277
void WMenuItem::setPathComponent(const std::string& path)
278
0
{
279
0
  customPathComponent_ = true;
280
0
  pathComponent_ = path;
281
282
0
  updateInternalPath();
283
0
  if (menu_)
284
0
    menu_->itemPathChanged(this);
285
0
}
286
287
void WMenuItem::setSelectable(bool selectable)
288
0
{
289
0
  selectable_ = selectable;
290
0
}
291
292
void WMenuItem::setCloseable(bool closeable)
293
0
{
294
0
  if (closeable_ != closeable) {
295
0
    closeable_ = closeable;
296
297
0
    if (closeable_) {
298
0
      WText *closeIcon = insertWidget(0, std::make_unique<WText>(""));
299
0
      WApplication *app = WApplication::instance();
300
0
      app->theme()->apply(this, closeIcon, MenuItemClose);
301
302
0
      closeIcon->clicked().connect(this, &WMenuItem::close);
303
0
    } else
304
0
      removeWidget(widget(0));
305
0
  }
306
0
}
307
308
void WMenuItem::setCheckable(bool checkable)
309
0
{
310
0
  if (isCheckable() != checkable) {
311
0
    if (checkable) {
312
0
      checkBox_ = anchor()->insertWidget(0, std::make_unique<WCheckBox>());
313
0
      setText(text());
314
315
0
      text_->setBuddy(checkBox_);
316
317
0
      WApplication *app = WApplication::instance();
318
0
      app->theme()->apply(this, checkBox_, MenuItemCheckBox);
319
320
0
      signalsConnected_ = false;
321
0
      repaint();
322
0
    } else {
323
0
      anchor()->removeWidget(checkBox_);
324
0
      checkBox_ = nullptr;
325
0
    }
326
0
  }
327
0
}
328
329
void WMenuItem::setChecked(bool checked)
330
0
{
331
0
  if (isCheckable()) {
332
0
    WCheckBox *cb = dynamic_cast<WCheckBox *>(anchor()->widget(0));
333
0
    cb->setChecked(checked);
334
0
  }
335
0
}
336
337
bool WMenuItem::isChecked() const
338
0
{
339
0
  if (isCheckable()) {
340
0
    WCheckBox *cb = dynamic_cast<WCheckBox *>(anchor()->widget(0));
341
0
    return cb->isChecked();
342
0
  } else
343
0
    return false;
344
0
}
345
346
void WMenuItem::close()
347
0
{
348
0
  if (menu_)
349
0
    menu_->close(this);
350
0
}
351
352
void WMenuItem::enableAjax()
353
0
{
354
0
  if (menu_->internalPathEnabled())
355
0
    resetLearnedSlots();
356
357
0
  if (uContents_)
358
0
    uContents_->enableAjax();
359
360
0
  WContainerWidget::enableAjax();
361
0
}
362
363
void WMenuItem::setDisabled(bool disabled)
364
0
{
365
0
  WContainerWidget::setDisabled(disabled);
366
367
0
  if (disabled)
368
0
    if (menu_ && !menu_->isDisabled())
369
0
      menu_->onItemHidden(menu_->indexOf(this), true);
370
0
}
371
372
void WMenuItem::setHidden(bool hidden,
373
                          const WAnimation& animation)
374
0
{
375
0
  WContainerWidget::setHidden(hidden, animation);
376
377
0
  if (hidden)
378
0
    if (menu_ && !menu_->isHidden())
379
0
      menu_->onItemHidden(menu_->indexOf(this), true);
380
0
}
381
382
void WMenuItem::render(WFlags<RenderFlag> flags)
383
0
{
384
0
  if (isThemeStyleEnabled()) {
385
0
    setThemeStyle_ = true;
386
0
    renderSelected(selected_);
387
0
  }
388
0
  connectSignals();
389
390
0
  WContainerWidget::render(flags);
391
0
}
392
393
void WMenuItem::renderSelected(bool selected)
394
0
{
395
0
  selected_ = selected;
396
397
0
  if (setThemeStyle_) {
398
0
    WApplication *app = WApplication::instance();
399
400
0
    std::string active = app->theme()->activeClass();
401
402
0
    auto bs5Theme = std::dynamic_pointer_cast<WBootstrap5Theme>(app->theme());
403
404
0
    if (active == "Wt-selected"){ // for CSS theme, our styles are messed up
405
0
      removeStyleClass(!selected ? "itemselected" : "item", true);
406
0
      addStyleClass(selected ? "itemselected" : "item", true);
407
0
    } else {
408
0
      if (bs5Theme) {
409
0
        auto a = anchor();
410
0
        if (a) {
411
0
          a->toggleStyleClass(active, selected, true);
412
0
        }
413
0
      }
414
0
      toggleStyleClass(active, selected, true);
415
0
    }
416
0
  }
417
0
}
418
419
void WMenuItem::selectNotLoaded()
420
0
{
421
0
  if (!contentsLoaded())
422
0
    select();
423
0
}
424
425
bool WMenuItem::contentsLoaded() const
426
0
{
427
0
  return oContents_ && !uContents_;
428
0
}
429
430
void WMenuItem::loadContents()
431
0
{
432
0
  if (!uContents_)
433
0
    return;
434
0
  else if (!contentsLoaded()) {
435
0
    oContentsContainer_->addWidget(std::move(uContents_));
436
0
    uContents_.reset();
437
0
    signalsConnected_ = false;
438
0
    connectSignals();
439
0
  }
440
0
}
441
442
void WMenuItem::connectSignals()
443
0
{
444
0
  if (!signalsConnected_) {
445
0
    signalsConnected_ = true;
446
447
0
    if (!oContents_ || contentsLoaded())
448
0
      implementStateless(&WMenuItem::selectVisual,
449
0
                         &WMenuItem::undoSelectVisual);
450
451
0
    WAnchor *a = anchor();
452
453
0
    if (a) {
454
0
      SignalBase *as;
455
0
      bool selectFromCheckbox = false;
456
457
0
      if (checkBox_ && !checkBox_->clicked().propagationPrevented()) {
458
0
        as = &checkBox_->changed();
459
        /*
460
         * Because the checkbox is not a properly exposed form object,
461
         * we need to relay its value ourselves
462
         */
463
0
        checkBox_->checked().connect(this, &WMenuItem::setCheckBox);
464
0
        checkBox_->unChecked().connect(this, &WMenuItem::setUnCheckBox);
465
0
        selectFromCheckbox = true;
466
0
      } else {
467
0
        as = &a->clicked();
468
0
      }
469
470
0
      if (checkBox_) {
471
0
        a->setLink(WLink());
472
0
      }
473
474
0
      if (uContents_) {
475
0
        as->connect(this, &WMenuItem::selectNotLoaded);
476
0
      } else {
477
0
        if (!menuItemCheckedConnected_ && !selectConnected_) {
478
0
          as->connect(this, &WMenuItem::selectVisual);
479
0
        }
480
0
        if (!selectFromCheckbox && !selectConnected_) {
481
0
          as->connect(this, &WMenuItem::select);
482
0
          selectConnected_ = true;
483
0
        } else if (selectFromCheckbox && !menuItemCheckedConnected_) {
484
          /*  #12367: Fixes a bug where the checkbox of a menu item is not
485
          *  clicked if the menu item itself is clicked, but the checkbox
486
          *  or its label were not clicked.
487
          */
488
0
          (&a->clicked())->connect(this, &WMenuItem::menuItemCheckedPropagate);
489
0
          menuItemCheckedConnected_ = true;
490
0
        }
491
0
      }
492
0
    }
493
0
  }
494
0
}
495
496
void WMenuItem::setCheckBox()
497
0
{
498
0
  setChecked(true);
499
0
  select();
500
0
}
501
502
void WMenuItem::setUnCheckBox()
503
0
{
504
0
  setChecked(false);
505
0
  select();
506
0
}
507
508
0
void WMenuItem::menuItemCheckedPropagate(){
509
0
  if (isCheckable()) {
510
0
    if (isChecked()){
511
0
      checkBox_->unChecked().emit();
512
0
    }
513
0
    else{
514
0
      checkBox_->checked().emit();
515
0
    }
516
0
  }
517
0
}
518
519
void WMenuItem::setParentMenu(WMenu *menu)
520
0
{
521
0
  menu_ = menu;
522
523
0
  updateInternalPath();
524
525
0
  if (menu && menu->isPopup() &&
526
0
      subMenu_ && subMenu_->isPopup()) {
527
0
    subMenu_->webWidget()->setZIndex(std::max(menu->zIndex() + 1000, subMenu_->zIndex()));
528
0
  }
529
0
}
530
531
WWidget *WMenuItem::contents() const
532
0
{
533
0
  return oContents_.get();
534
0
}
535
536
WWidget *WMenuItem::contentsInStack() const
537
0
{
538
0
  if (oContentsContainer_ && !uContentsContainer_)
539
0
    return oContentsContainer_.get();
540
0
  else if (oContents_ && !uContents_)
541
0
    return oContents_.get();
542
0
  else
543
0
    return nullptr;
544
0
}
545
546
std::unique_ptr<WWidget> WMenuItem::removeContents()
547
0
{
548
0
  auto contents = oContents_.get();
549
0
  oContents_.reset();
550
551
0
  WWidget *c = contentsInStack();
552
553
0
  if (c) {
554
    /* Remove from stack */
555
0
    std::unique_ptr<WWidget> w = c->parent()->removeWidget(c);
556
557
0
    if (oContentsContainer_)
558
0
      return oContentsContainer_->removeWidget(contents);
559
0
    else
560
0
      return w;
561
0
  } else {
562
0
    auto result = std::move(uContents_);
563
0
    uContents_.reset();
564
0
    return result;
565
0
  }
566
0
}
567
568
std::unique_ptr<WWidget> WMenuItem::takeContentsForStack()
569
0
{
570
0
  if (!uContents_)
571
0
    return nullptr;
572
0
  else {
573
0
    if (loadPolicy_ == ContentLoading::Lazy) {
574
0
      std::unique_ptr<WWidget> result = std::move(uContentsContainer_);
575
0
      uContentsContainer_.reset();
576
0
      return result;
577
0
    } else {
578
0
      auto result = std::move(uContents_);
579
0
      uContents_.reset();
580
0
      return result;
581
0
    }
582
0
  }
583
0
}
584
585
void WMenuItem::returnContentsInStack(std::unique_ptr<WWidget> widget)
586
0
{
587
0
  if (oContentsContainer_) {
588
0
    if (!uContents_)
589
0
      uContents_ = oContentsContainer_->removeWidget(oContents_.get());
590
591
0
    WWidget* realWidget = widget.release();
592
0
    WContainerWidget* uContentsContainer = dynamic_cast<WContainerWidget*>(realWidget);
593
0
    if (uContentsContainer) {
594
0
      uContentsContainer_ = std::unique_ptr<WContainerWidget>(uContentsContainer);
595
0
      oContentsContainer_ = uContentsContainer_.get();
596
0
    } else {
597
0
      delete realWidget;
598
0
      oContentsContainer_ = nullptr;
599
0
      LOG_ERROR("returnContentsInStack: widget is not oContentsContainer_. This should not happen.");
600
0
    }
601
0
  } else
602
0
    uContents_ = std::move(widget);
603
0
}
604
605
void WMenuItem::setFromInternalPath(const std::string& path)
606
0
{
607
0
  if (internalPathEnabled() &&
608
0
      menu_->contentsStack_ &&
609
0
      menu_->contentsStack_->currentWidget() != contents())
610
0
    menu_->select(menu_->indexOf(this), false);
611
612
0
  if (subMenu_ && subMenu_->internalPathEnabled())
613
0
    subMenu_->internalPathChanged(path);
614
0
}
615
616
void WMenuItem::select()
617
0
{
618
0
  if (menu_ && selectable_ && !isDisabled())
619
0
    menu_->select(this);
620
0
}
621
622
void WMenuItem::selectVisual()
623
0
{
624
0
  if (menu_ && selectable_)
625
0
    menu_->selectVisual(this);
626
0
}
627
628
void WMenuItem::undoSelectVisual()
629
0
{
630
0
  if (menu_ && selectable_)
631
0
    menu_->undoSelectVisual();
632
0
}
633
634
void WMenuItem::setMenu(std::unique_ptr<WMenu> menu)
635
0
{
636
0
  subMenu_ = menu.get();
637
0
  subMenu_->parentItem_ = this;
638
639
0
  WPopupMenu *popup = dynamic_cast<WPopupMenu *>(subMenu_);
640
0
  if (popup) {
641
0
    Wt::WApplication::instance()->removeGlobalWidget(menu.get());
642
0
  }
643
0
  addWidget(std::move(menu));
644
645
0
  if (subMenu_->isPopup() &&
646
0
      parentMenu() && parentMenu()->isPopup()) {
647
0
    subMenu_->webWidget()->setZIndex(std::max(parentMenu()->zIndex() + 1000, subMenu_->zIndex()));
648
0
  }
649
650
0
  if (popup) {
651
0
    setSelectable(false);
652
0
    popup->setButton(anchor());
653
0
    updateInternalPath();
654
    // WPopupMenus are hidden by default, 'show' this WPopupMenu
655
    // but not really, since the parent is still hidden. This fixes
656
    // an issue where child widgets would remain unexposed, even
657
    // though this submenu was open (e.g. in a submenu where items
658
    // are checkable)
659
0
    if (dynamic_cast<WPopupMenu*>(menu_))
660
0
      popup->show();
661
0
  }
662
0
}
663
664
void WMenuItem::setItemPadding(bool padding)
665
0
{
666
0
  if (!checkBox_ && !icon_) {
667
0
    WAnchor *a = anchor();
668
0
    if (a)
669
0
      a->toggleStyleClass("Wt-padded", padding);
670
0
  }
671
0
}
672
673
}