Coverage Report

Created: 2025-11-24 06:30

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/wt/src/Wt/WMenu.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/WApplication.h"
8
#include "Wt/WLogger.h"
9
#include "Wt/WMenu.h"
10
#include "Wt/WMenuItem.h"
11
#include "Wt/WStackedWidget.h"
12
#include "Wt/WTemplate.h"
13
14
#include "WebUtils.h"
15
16
/*
17
 * TODO:
18
 *  - disable everything selection-related for menu items that have a
19
 *    popup menu
20
 */
21
namespace {
22
23
  /*
24
   * Returns the length of the longest matching sub path.
25
   *
26
   * ("a", "b") -> -1
27
   * ("a", "") -> 0
28
   * ("a/ab", "a/ac") -> -1
29
   * ("a", "a") -> 1
30
   */
31
0
  int match(const std::string& path, const std::string& component) {
32
0
    if (component.length() > path.length())
33
0
      return -1;
34
35
0
    int length = std::min(component.length(), path.length());
36
37
0
    int current = -1;
38
39
0
    for (int i = 0; i < length; ++i) {
40
0
      if (component[i] != path[i]) {
41
0
        return current;
42
0
      } else if (component[i] == '/')
43
0
        current = i;
44
0
    }
45
46
0
    return length;
47
0
  }
48
}
49
50
namespace Wt {
51
52
LOGGER("WMenu");
53
54
WMenu::WMenu()
55
0
  : ul_(nullptr),
56
0
    contentsStack_(nullptr),
57
0
    internalPathEnabled_(false),
58
0
    emitPathChange_(false),
59
0
    parentItem_(nullptr),
60
0
    current_(-1),
61
0
    previousStackIndex_(-1),
62
0
    needSelectionEventUpdate_(false)
63
0
{
64
0
  setImplementation(std::unique_ptr<WWidget>(ul_ = new WContainerWidget()));
65
0
  ul_->setList(true);
66
0
}
67
68
WMenu::WMenu(WStackedWidget *contentsStack)
69
0
  : ul_(nullptr),
70
0
    contentsStack_(contentsStack),
71
0
    internalPathEnabled_(false),
72
0
    emitPathChange_(false),
73
0
    parentItem_(nullptr),
74
0
    current_(-1),
75
0
    previousStackIndex_(-1),
76
0
    needSelectionEventUpdate_(false)
77
0
{
78
0
  if (contentsStack_) {
79
0
    contentsStack_->childrenChanged().connect(this,
80
0
                                              &WMenu::updateSelectionEvent);
81
0
  }
82
83
0
  setImplementation(std::unique_ptr<WWidget>(ul_ = new WContainerWidget()));
84
0
  ul_->setList(true);
85
0
}
86
87
void WMenu::load()
88
0
{
89
0
  bool wasLoaded = loaded();
90
91
0
  WCompositeWidget::load();
92
93
0
  if (wasLoaded)
94
0
    return;
95
96
0
  if (currentItem())
97
0
    currentItem()->loadContents();
98
0
}
99
100
WMenu::~WMenu()
101
0
{ }
102
103
void WMenu::setInternalPathEnabled(const std::string& basePath)
104
0
{
105
0
  WApplication *app = WApplication::instance();
106
107
0
  basePath_ = basePath.empty() ? app->internalPath() : basePath;
108
0
  basePath_ = Utils::append(Utils::prepend(basePath_, '/'), '/');
109
110
0
  if (!internalPathEnabled_) {
111
0
    internalPathEnabled_ = true;
112
0
    app->internalPathChanged().connect(this, &WMenu::handleInternalPathChange);
113
0
  }
114
115
0
  previousInternalPath_ = app->internalPath();
116
0
  internalPathChanged(app->internalPath());
117
118
0
  updateItemsInternalPath();
119
0
}
120
121
void WMenu::handleInternalPathChange(const std::string& path)
122
0
{
123
0
  if (!parentItem_ || !parentItem_->internalPathEnabled())
124
0
    internalPathChanged(path);
125
0
}
126
127
void WMenu::setInternalBasePath(const std::string& basePath)
128
0
{
129
0
  setInternalPathEnabled(basePath);
130
0
}
131
132
void WMenu::updateItemsInternalPath()
133
0
{
134
0
  for (int i = 0; i < count(); ++i) {
135
0
    WMenuItem *item = itemAt(i);
136
0
    item->updateInternalPath();
137
0
  }
138
139
0
  updateSelectionEvent();
140
0
}
141
142
WMenuItem *WMenu::addItem(const WString& name,
143
                          std::unique_ptr<WWidget> contents,
144
                          ContentLoading policy)
145
0
{
146
0
  return addItem(std::string(), name, std::move(contents), policy);
147
0
}
148
149
WMenuItem *WMenu::addItem(const std::string& iconPath, const WString& name,
150
                          std::unique_ptr<WWidget> contents,
151
                          ContentLoading policy)
152
0
{
153
0
  return addItem(std::unique_ptr<WMenuItem>
154
0
                 (new WMenuItem(iconPath, name, std::move(contents), policy)));
155
0
}
156
157
WMenuItem *WMenu::addMenu(const WString& text, std::unique_ptr<WMenu> menu)
158
0
{
159
0
  return addMenu(std::string(), text, std::move(menu));
160
0
}
161
162
WMenuItem *WMenu::addMenu(const std::string& iconPath,
163
                          const WString& text, std::unique_ptr<WMenu> menu)
164
0
{
165
0
  WMenuItem *item = addItem(iconPath, text, nullptr,
166
0
                            ContentLoading::Lazy);
167
0
  item->setMenu(std::move(menu));
168
0
  return item;
169
0
}
170
171
WMenuItem *WMenu::addSeparator()
172
0
{
173
0
  return addItem(std::unique_ptr<WMenuItem>
174
0
                 (new WMenuItem(true, WString::Empty)));
175
0
}
176
177
WMenuItem *WMenu::addSectionHeader(const WString& text)
178
0
{
179
0
  return addItem(std::unique_ptr<WMenuItem>(new WMenuItem(false, text)));
180
0
}
181
182
WMenuItem *WMenu::addItem(std::unique_ptr<WMenuItem> item)
183
0
{
184
0
  return insertItem(ul()->count(), std::move(item));
185
0
}
186
187
WMenuItem *WMenu::insertItem(int index, const WString& name,
188
                             std::unique_ptr<WWidget> contents,
189
                             ContentLoading policy)
190
0
{
191
0
  return insertItem(index, std::string(), name, std::move(contents), policy);
192
0
}
193
194
WMenuItem *WMenu::insertItem(int index, const std::string& iconPath,
195
                             const WString& name,
196
                             std::unique_ptr<WWidget> contents,
197
                             ContentLoading policy)
198
0
{
199
0
  return insertItem
200
0
    (index, std::unique_ptr<WMenuItem>
201
0
     (new WMenuItem(iconPath, name, std::move(contents), policy)));
202
0
}
203
204
WMenuItem *WMenu::insertMenu(int index, const WString& text,
205
                             std::unique_ptr<WMenu> menu)
206
0
{
207
0
  return insertMenu(index, std::string(), text, std::move(menu));
208
0
}
209
210
WMenuItem *WMenu::insertMenu(int index, const std::string& iconPath,
211
                             const WString& text, std::unique_ptr<WMenu> menu)
212
0
{
213
0
  WMenuItem *item = insertItem(index, iconPath, text, nullptr);
214
0
  item->setMenu(std::move(menu));
215
0
  return item;
216
0
}
217
218
WMenuItem *WMenu::insertItem(int index, std::unique_ptr<WMenuItem> item)
219
0
{
220
0
  item->setParentMenu(this);
221
222
0
  WMenuItem *result = item.get();
223
0
  ul()->insertWidget(index, std::move(item));
224
225
0
  if (contentsStack_) {
226
0
    std::unique_ptr<WWidget> contentsPtr = result->takeContentsForStack();
227
0
    if (contentsPtr) {
228
0
      WWidget *contents = contentsPtr.get();
229
0
      contentsStack_->addWidget(std::move(contentsPtr));
230
0
      contentsStack_->setLoadPolicy(contentsStack_->count()-1, result->loadPolicy_);
231
232
0
      if (contentsStack_->count() == 1) {
233
0
        setCurrent(0);
234
0
        if (loaded()) {
235
0
          currentItem()->loadContents();
236
0
        }
237
0
        contentsStack_->setCurrentWidget(contents);
238
239
0
        renderSelected(result, true);
240
0
      } else
241
0
        renderSelected(result, false);
242
0
    } else
243
0
      renderSelected(result, false);
244
0
  } else
245
0
    renderSelected(result, false);
246
247
0
  itemPathChanged(result);
248
249
0
  return result;
250
0
}
251
252
void WMenu::itemPathChanged(WMenuItem *item)
253
0
{
254
0
  if (internalPathEnabled_ && item->internalPathEnabled()) {
255
0
    WApplication *app = wApp;
256
257
0
    if (app->internalPathMatches(basePath_ + item->pathComponent()))
258
0
      item->setFromInternalPath(app->internalPath());
259
0
  }
260
0
}
261
262
std::unique_ptr<WMenuItem> WMenu::removeItem(WMenuItem *item)
263
0
{
264
0
  std::unique_ptr<WMenuItem> result;
265
266
0
  WContainerWidget *items = ul();
267
268
0
  if (item->parent() == items) {
269
0
    int itemIndex = items->indexOf(item);
270
271
0
    result = items->removeWidget(item);
272
273
0
    if (contentsStack_ && item->contentsInStack())
274
0
      item->returnContentsInStack
275
0
        (contentsStack_->removeWidget(item->contentsInStack()));
276
277
0
    item->setParentMenu(nullptr);
278
279
0
    if (itemIndex <= current_ && current_ >= 0)
280
0
      --current_;
281
282
0
    select(current_, true);
283
0
  }
284
285
0
  return result;
286
0
}
287
288
void WMenu::moveItem(int fromIndex, int toIndex)
289
0
{
290
0
  moveItem(itemAt(fromIndex), toIndex);
291
0
}
292
293
void WMenu::moveItem(WMenuItem* item, int toIndex)
294
0
{
295
0
  if (item) {
296
0
    bool needReload = item->loadPolicy_ == ContentLoading::Lazy && item->contentsLoaded();
297
0
    std::unique_ptr<WMenuItem> realItem = removeItem(item);
298
0
    if (realItem) {
299
0
      insertItem(toIndex, std::move(realItem));
300
0
      if (needReload) {
301
0
        item->loadContents();
302
0
      }
303
0
    }
304
0
  }
305
0
}
306
307
void WMenu::select(int index)
308
0
{
309
0
  select(index, true);
310
0
}
311
312
void WMenu::setCurrent(int index)
313
0
{
314
0
  current_ = index;
315
0
}
316
317
void WMenu::select(int index, bool changePath)
318
0
{
319
0
  if (parentItem_) {
320
0
    auto parentItemMenu = parentItem_->parentMenu();
321
0
    if (parentItemMenu->currentItem() != parentItem_ && parentItem_->isSelectable())
322
0
      parentItemMenu->select(parentItemMenu->indexOf(parentItem_), false);
323
0
  }
324
325
0
  int last = current_;
326
0
  setCurrent(index);
327
328
0
  selectVisual(current_, changePath, true);
329
330
0
  if (index != -1) {
331
0
    WMenuItem *item = itemAt(index);
332
0
    item->show();
333
0
    if (loaded()) {
334
0
      item->loadContents();
335
0
    }
336
337
0
    observing_ptr<WMenu> self = this;
338
339
0
    if (changePath && emitPathChange_) {
340
0
      WApplication *app = wApp;
341
0
      app->internalPathChanged().emit(app->internalPath());
342
0
      if (!self) {
343
0
        return;
344
0
      }
345
0
      emitPathChange_ = false;
346
0
    }
347
348
0
    if (last != index) {
349
0
      item->triggered().emit(item);
350
0
      if (self) {
351
        // item may have been deleted too
352
0
        if (ul()->indexOf(item) != -1) {
353
0
          itemSelected_.emit(item);
354
0
        } else {
355
0
          select(-1);
356
0
        }
357
0
      }
358
0
    }
359
0
  }
360
0
}
361
362
void WMenu::selectVisual(int index, bool changePath, bool showContents)
363
0
{
364
0
  if (contentsStack_)
365
0
    previousStackIndex_ = contentsStack_->currentIndex();
366
367
0
  WMenuItem *item = index >= 0 ? itemAt(index) : nullptr;
368
369
0
  if (changePath && internalPathEnabled_ &&
370
0
      index != -1 && item->internalPathEnabled()) {
371
0
    WApplication *app = wApp;
372
0
    previousInternalPath_ = app->internalPath();
373
374
0
    std::string newPath = basePath_ + item->pathComponent();
375
0
    if (newPath != app->internalPath())
376
0
      emitPathChange_ = true;
377
378
    // The change is emitted in select()
379
0
    app->setInternalPath(newPath);
380
0
  }
381
382
0
  for (int i = 0; i < count(); ++i)
383
0
    renderSelected(itemAt(i), (int)i == index);
384
385
0
  if (index == -1)
386
0
    return;
387
388
0
  if (showContents && contentsStack_) {
389
0
    WWidget *contents = item->contentsInStack();
390
0
    if (contents)
391
0
      contentsStack_->setCurrentWidget(contents);
392
0
  }
393
394
0
  itemSelectRendered_.emit(item);
395
0
}
396
397
void WMenu::renderSelected(WMenuItem *item, bool selected)
398
0
{
399
0
  item->renderSelected(selected);
400
0
}
401
402
void WMenu::setItemHidden(int index, bool hidden)
403
0
{
404
0
  itemAt(index)->setHidden(hidden);
405
0
}
406
407
void WMenu::onItemHidden(int index, bool hidden)
408
0
{
409
0
  if (hidden) {
410
0
    int nextItem = nextAfterHide(index);
411
0
    if (nextItem != current_)
412
0
      select(nextItem);
413
0
  }
414
0
}
415
416
int WMenu::nextAfterHide(int index)
417
0
{
418
0
  if (current_ == index) {
419
    // Try to find visible item to the right of the current.
420
0
    for (int i = current_ + 1; i < count(); ++i)
421
0
      if (!isItemHidden(i) && itemAt(i)->isEnabled())
422
0
        return i;
423
424
    // Try to find visible item to the left of the current.
425
0
    for (int i = current_ - 1; i >= 0; --i)
426
0
      if (!isItemHidden(i) && itemAt(i)->isEnabled())
427
0
        return i;
428
0
  }
429
430
0
  return current_;
431
0
}
432
433
void WMenu::setItemHidden(WMenuItem *item, bool hidden)
434
0
{
435
0
  item->setHidden(hidden);
436
0
}
437
438
bool WMenu::isItemHidden(int index) const
439
0
{
440
0
  return isItemHidden(itemAt(index));
441
0
}
442
443
bool WMenu::isItemHidden(WMenuItem *item) const
444
0
{
445
0
  return item->isHidden();
446
0
}
447
448
void WMenu::setItemDisabled(int index, bool disabled)
449
0
{
450
0
  setItemDisabled(itemAt(index), disabled);
451
0
}
452
453
void WMenu::setItemDisabled(WMenuItem* item, bool disabled)
454
0
{
455
0
  item->setDisabled(disabled);
456
0
}
457
458
bool WMenu::isItemDisabled(int index) const
459
0
{
460
0
  return isItemDisabled(itemAt(index));
461
0
}
462
463
bool WMenu::isItemDisabled(WMenuItem *item) const
464
0
{
465
0
  return item->isDisabled();
466
0
}
467
468
void WMenu::close(int index)
469
0
{
470
0
  close(itemAt(index));
471
0
}
472
473
void WMenu::close(WMenuItem *item)
474
0
{
475
0
  if (item->isCloseable()) {
476
0
    item->hide();
477
0
    itemClosed_.emit(item);
478
0
  }
479
0
}
480
481
void WMenu::internalPathChanged(const std::string& path)
482
0
{
483
0
  WApplication *app = wApp;
484
485
0
  if (app->internalPathMatches(basePath_)) {
486
0
    std::string subPath = app->internalSubPath(basePath_);
487
488
0
    int bestI = -1, bestMatchLength = -1;
489
490
0
    for (int i = 0; i < count(); ++i) {
491
0
      if (!itemAt(i)->isEnabled() || itemAt(i)->isHidden())
492
0
        continue;
493
494
0
      int matchLength = match(subPath, itemAt(i)->pathComponent());
495
496
0
      if (matchLength > bestMatchLength) {
497
0
        bestMatchLength = matchLength;
498
0
        bestI = i;
499
0
      }
500
0
    }
501
502
0
    if (bestI != -1)
503
0
      itemAt(bestI)->setFromInternalPath(path);
504
0
    else {
505
0
      if (!subPath.empty())
506
0
        LOG_WARN("unknown path: '"<< subPath << "'");
507
0
      else
508
0
        select(-1, false);
509
0
    }
510
0
  }
511
0
}
512
513
void WMenu::select(WMenuItem *item)
514
0
{
515
0
  select(indexOf(item), true);
516
0
}
517
518
void WMenu::selectVisual(WMenuItem *item)
519
0
{
520
0
  selectVisual(indexOf(item), true, true);
521
0
}
522
523
int WMenu::indexOf(WMenuItem *item) const
524
0
{
525
0
  return ul()->indexOf(item);
526
0
}
527
528
void WMenu::undoSelectVisual()
529
0
{
530
0
  std::string prevPath = previousInternalPath_;
531
0
  int prevStackIndex = previousStackIndex_;
532
533
0
  selectVisual(current_, true, true);
534
535
0
  if (internalPathEnabled_) {
536
0
    WApplication *app = wApp;
537
0
    app->setInternalPath(prevPath);
538
0
  }
539
540
0
  if (contentsStack_)
541
0
    contentsStack_->setCurrentIndex(prevStackIndex);
542
0
}
543
544
WMenuItem *WMenu::currentItem() const
545
0
{
546
0
  return current_ >= 0 ? itemAt(current_) : nullptr;
547
0
}
548
549
void WMenu::render(WFlags<RenderFlag> flags)
550
0
{
551
0
  if (needSelectionEventUpdate_) {
552
0
    for (int i = 0; i < count(); ++i)
553
0
      itemAt(i)->resetLearnedSlots();
554
555
0
    needSelectionEventUpdate_ = false;
556
0
  }
557
558
0
  WCompositeWidget::render(flags);
559
0
}
560
561
void WMenu::updateSelectionEvent()
562
0
{
563
0
  needSelectionEventUpdate_ = true;
564
0
  scheduleRender();
565
0
}
566
567
int WMenu::count() const
568
0
{
569
0
  return ul()->count();
570
0
}
571
572
WMenuItem *WMenu::itemAt(int index) const
573
0
{
574
0
  return dynamic_cast<WMenuItem *>(ul()->widget(index));
575
0
}
576
577
std::vector<WMenuItem *> WMenu::items() const
578
0
{
579
0
  std::vector<WMenuItem *> result;
580
581
0
  result.reserve(count());
582
0
  for (int i = 0; i < count(); ++i)
583
0
    result.push_back(itemAt(i));
584
585
0
  return result;
586
0
}
587
588
}