Coverage Report

Created: 2025-12-27 06:47

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/wt/src/Wt/WComboBox.C
Line
Count
Source
1
2
/*
3
 * Copyright (C) 2008 Emweb bv, Herent, Belgium.
4
 *
5
 * See the LICENSE file for terms of use.
6
 */
7
#include "Wt/WComboBox.h"
8
#include "Wt/WLogger.h"
9
#include "Wt/WStringListModel.h"
10
11
#include "DomElement.h"
12
#include "WebUtils.h"
13
14
namespace Wt {
15
16
LOGGER("WComboBox");
17
18
WComboBox::WComboBox()
19
0
  : modelColumn_(0),
20
0
    currentIndex_(-1),
21
0
    currentIndexRaw_(nullptr),
22
0
    itemsChanged_(false),
23
0
    selectionChanged_(true),
24
0
    currentlyConnected_(false),
25
0
    noSelectionEnabled_(false)
26
0
{
27
0
  setInline(true);
28
0
  setFormObject(true);
29
0
  setModel(std::make_shared<WStringListModel>());
30
0
}
31
32
void WComboBox::setModel(const std::shared_ptr<WAbstractItemModel> model)
33
0
{
34
0
  if (model_) {
35
    /* disconnect slots from previous model */
36
0
    for (unsigned i = 0; i < modelConnections_.size(); ++i)
37
0
      modelConnections_[i].disconnect();
38
0
    modelConnections_.clear();
39
0
  }
40
41
0
  model_ = model;
42
43
0
  modelConnections_.push_back
44
0
    (model_->columnsInserted().connect(this, &WComboBox::itemsChanged));
45
0
  modelConnections_.push_back
46
0
    (model_->columnsRemoved().connect(this, &WComboBox::itemsChanged));
47
0
  modelConnections_.push_back
48
0
    (model_->rowsInserted().connect(this, &WComboBox::rowsInserted));
49
0
  modelConnections_.push_back
50
0
    (model_->rowsRemoved().connect(this, &WComboBox::rowsRemoved));
51
0
  modelConnections_.push_back
52
0
    (model_->dataChanged().connect(this, &WComboBox::itemsChanged));
53
0
  modelConnections_.push_back
54
0
    (model_->modelReset().connect(this, &WComboBox::itemsChanged));
55
0
  modelConnections_.push_back
56
0
    (model_->layoutAboutToBeChanged().connect(this,
57
0
                                              &WComboBox::saveSelection));
58
0
  modelConnections_.push_back
59
0
    (model_->layoutChanged().connect(this, &WComboBox::layoutChanged));
60
61
  /* Redraw contents of the combo box to match the contents of the new model.
62
   */
63
0
  refresh();
64
0
}
65
66
void WComboBox::rowsRemoved(WT_MAYBE_UNUSED const WModelIndex&, int from, int to)
67
0
{
68
0
  itemsChanged_ = true;
69
0
  repaint(RepaintFlag::SizeAffected);
70
71
0
  if (currentIndex_ < from) // selection is not affected
72
0
    return;
73
74
0
  int count = to - from + 1;
75
76
0
  if (currentIndex_ > to) // shift up the selection by amount of removed rows
77
0
    currentIndex_ -= count;
78
0
  else if (currentIndex_ >= from) {
79
0
    currentIndex_ = -1;
80
0
    makeCurrentIndexValid();
81
0
  }
82
0
}
83
84
void WComboBox::rowsInserted(WT_MAYBE_UNUSED const WModelIndex&, int from, int to)
85
0
{
86
0
  itemsChanged_ = true;
87
0
  repaint(RepaintFlag::SizeAffected);
88
89
0
  int count = to - from + 1;
90
91
0
  if (currentIndex_ == -1)
92
0
    makeCurrentIndexValid();
93
0
  else if (currentIndex_ >= from)
94
0
    currentIndex_ += count;
95
0
}
96
97
void WComboBox::setModelColumn(int index)
98
0
{
99
0
  modelColumn_ = index;
100
0
}
101
102
void WComboBox::addItem(const WString& text)
103
0
{
104
0
  insertItem(count(), text);
105
0
}
106
107
int WComboBox::count() const
108
0
{
109
0
  return model_->rowCount();
110
0
}
111
112
int WComboBox::currentIndex() const
113
0
{
114
0
  return currentIndex_;
115
0
}
116
117
const WString WComboBox::currentText() const
118
0
{
119
0
  if (currentIndex_ != -1)
120
0
    return asString(model_->data(currentIndex_, modelColumn_));
121
0
  else
122
0
    return WString();
123
0
}
124
125
void WComboBox::insertItem(int index, const WString& text)
126
0
{
127
0
  if (model_->insertRow(index)) {
128
0
    setItemText(index, text);
129
0
    makeCurrentIndexValid();
130
0
  }
131
0
}
132
133
const WString WComboBox::itemText(int index) const
134
0
{
135
0
  return asString(model_->data(index, modelColumn_));
136
0
}
137
138
void WComboBox::removeItem(int index)
139
0
{
140
0
  model_->removeRow(index);
141
142
0
  makeCurrentIndexValid();
143
0
}
144
145
void WComboBox::setCurrentIndex(int index)
146
0
{
147
0
  int newIndex = std::min(index, count() - 1);
148
149
0
  if (currentIndex_ != newIndex) {
150
0
    currentIndex_ = newIndex;
151
0
    makeCurrentIndexValid();
152
153
0
    validate();
154
155
0
    selectionChanged_ = true;
156
0
    repaint();
157
0
  }
158
0
}
159
160
void WComboBox::setItemText(int index, const WString& text)
161
0
{
162
0
  model_->setData(index, modelColumn_, cpp17::any(text));
163
0
}
164
165
void WComboBox::clear()
166
0
{
167
0
  model_->removeRows(0, count());
168
169
0
  makeCurrentIndexValid();
170
0
}
171
172
void WComboBox::propagateChange()
173
0
{
174
  /*
175
   * copy values for when widget would be deleted from activated_.emit()
176
   */
177
0
  int myCurrentIndex = currentIndex_;
178
0
  WString myCurrentValue;
179
180
0
  if (currentIndex_ != -1)
181
0
    myCurrentValue = currentText();
182
183
0
  observing_ptr<WComboBox> guard(this);
184
185
0
  activated_.emit(currentIndex_);
186
187
0
  if (guard) {
188
0
    if (myCurrentIndex != - 1)
189
0
      sactivated_.emit(myCurrentValue);
190
0
  }
191
0
}
192
193
bool WComboBox::isSelected(int index) const
194
0
{
195
0
  return index == currentIndex_;
196
0
}
197
198
void WComboBox::setNoSelectionEnabled(bool enabled)
199
0
{
200
0
  if (noSelectionEnabled_ != enabled) {
201
0
    noSelectionEnabled_ = enabled;
202
203
0
    makeCurrentIndexValid();
204
0
  }
205
0
}
206
207
void WComboBox::makeCurrentIndexValid()
208
0
{
209
0
  int c = count();
210
211
0
  if (currentIndex_ > c - 1)
212
0
    setCurrentIndex(c - 1);
213
0
  else if (c > 0 && currentIndex_ == -1 && !supportsNoSelection())
214
0
    setCurrentIndex(0);
215
0
}
216
217
bool WComboBox::supportsNoSelection() const
218
0
{
219
0
  return noSelectionEnabled_;
220
0
}
221
222
void WComboBox::updateDom(DomElement& element, bool all)
223
0
{
224
0
  if (itemsChanged_ || all) {
225
0
    if (!all) {
226
0
      element.removeAllChildren();
227
228
      // For 'no selection', the index must be explicitly set after rerender
229
0
      if (currentIndex_ == -1)
230
0
        selectionChanged_ = true;
231
0
    }
232
233
0
    DomElement *currentGroup = nullptr;
234
0
    bool groupDisabled = true;
235
236
0
    int size = count();
237
0
    for (int i = 0; i < size; ++i) {
238
      // Make new option item
239
0
      DomElement *item = DomElement::createNew(DomElementType::OPTION);
240
0
      item->setProperty(Property::Value, std::to_string(i));
241
0
      item->setProperty(Property::InnerHTML,
242
0
                        escapeText(asString(model_->data(i, modelColumn_)))
243
0
                        .toUTF8());
244
245
0
      if (!(model_->flags(model_->index(i, modelColumn_)) &
246
0
            ItemFlag::Selectable))
247
0
        item->setProperty(Property::Disabled, "true");
248
249
0
      if (isSelected(i))
250
0
        item->setProperty(Property::Selected, "true");
251
252
0
      WString sc = asString(model_->data(i, modelColumn_,
253
0
                                         ItemDataRole::StyleClass));
254
0
      if (!sc.empty())
255
0
        item->setProperty(Property::Class, sc.toUTF8());
256
257
258
      // Read out opt-group
259
0
      WString groupname = Wt::asString(model_->data(i, modelColumn_,
260
0
                                                    ItemDataRole::Level));
261
262
0
      bool isSoloItem = false;
263
0
      if (groupname.empty()) { // no group
264
0
        isSoloItem = true;
265
266
0
        if (currentGroup) { // possibly close off an active group
267
0
          if (groupDisabled)
268
0
            currentGroup->setProperty(Property::Disabled, "true");
269
0
          element.addChild(currentGroup);
270
0
          currentGroup = nullptr;
271
0
        }
272
0
      } else {
273
0
        isSoloItem = false;
274
275
        // not same as current group
276
0
        if (!currentGroup ||
277
0
            currentGroup->getProperty(Property::Label) != groupname.toUTF8()) {
278
0
          if (currentGroup) { // possibly close off an active group
279
0
            if (groupDisabled)
280
0
              currentGroup->setProperty(Property::Disabled, "true");
281
0
            element.addChild(currentGroup);
282
0
            currentGroup = nullptr;
283
0
          }
284
285
          // make group
286
0
          currentGroup = DomElement::createNew(DomElementType::OPTGROUP);
287
0
          currentGroup->setProperty(Property::Label, groupname.toUTF8());
288
0
          groupDisabled = !(model_->flags(model_->index(i, modelColumn_)) &
289
0
                            ItemFlag::Selectable);
290
0
        } else {
291
0
          if (model_->flags(model_->index(i, modelColumn_)).test(
292
0
              ItemFlag::Selectable))
293
0
            groupDisabled = false;
294
0
        }
295
0
      }
296
297
0
      if (isSoloItem)
298
0
        element.addChild(item);
299
0
      else
300
0
        currentGroup->addChild(item);
301
302
      // last loop and there's still an open group
303
0
      if (i == size - 1 && currentGroup) {
304
0
        if (groupDisabled)
305
0
          currentGroup->setProperty(Property::Disabled, "true");
306
0
        element.addChild(currentGroup);
307
0
        currentGroup = nullptr;
308
0
      }
309
0
    }
310
311
0
    itemsChanged_ = false;
312
0
  }
313
314
0
  if (selectionChanged_ ||
315
0
      (all && (selectionMode() == SelectionMode::Single))) {
316
0
    element.setProperty(Property::SelectedIndex, std::to_string(currentIndex_));
317
0
    selectionChanged_ = false;
318
0
  }
319
320
0
  if (!currentlyConnected_
321
0
      && (activated_.isConnected() || sactivated_.isConnected())) {
322
0
    currentlyConnected_ = true;
323
0
    changed().connect(this, &WComboBox::propagateChange);
324
0
  }
325
326
0
  WFormWidget::updateDom(element, all);
327
0
}
328
329
void WComboBox::propagateRenderOk(bool deep)
330
0
{
331
0
  itemsChanged_ = false;
332
0
  selectionChanged_ = false;
333
334
0
  WFormWidget::propagateRenderOk(deep);
335
0
}
336
337
DomElementType WComboBox::domElementType() const
338
0
{
339
0
  return DomElementType::SELECT;
340
0
}
341
342
void WComboBox::setFormData(const FormData& formData)
343
0
{
344
0
  if (selectionChanged_ || isReadOnly())
345
0
    return;
346
347
0
  if (!Utils::isEmpty(formData.values)) {
348
0
    const std::string& value = formData.values[0];
349
350
0
    if (!value.empty()) {
351
0
      try {
352
0
        currentIndex_ = Utils::stoi(value);
353
0
      } catch (std::exception& e) {
354
0
        LOG_ERROR("received illegal form value: '" << value << "'");
355
0
      }
356
0
    } else
357
0
      currentIndex_ = -1;
358
359
0
    makeCurrentIndexValid();
360
0
  }
361
0
}
362
363
void WComboBox::refresh()
364
0
{
365
0
  itemsChanged();
366
367
0
  WFormWidget::refresh();
368
0
}
369
370
WT_USTRING WComboBox::valueText() const
371
0
{
372
0
  return currentText();
373
0
}
374
375
void WComboBox::setValueText(const WT_USTRING& value)
376
0
{
377
0
#ifndef WT_TARGET_JAVA
378
0
  int i = findText(value, MatchFlag::Exactly);
379
0
  setCurrentIndex(i);
380
#else
381
  int size = count();
382
  for (int i = 0; i < size; ++i) {
383
    if (Wt::asString(model_->index(i, modelColumn_).data(ItemDataRole::Display))
384
        == value) {
385
      setCurrentIndex(i);
386
      return;
387
    }
388
  }
389
390
  setCurrentIndex(-1);
391
#endif
392
0
}
393
394
void WComboBox::itemsChanged()
395
0
{
396
0
  itemsChanged_ = true;
397
0
  repaint(RepaintFlag::SizeAffected);
398
399
0
  makeCurrentIndexValid();
400
0
}
401
402
void WComboBox::saveSelection()
403
0
{
404
0
  if (currentIndex_ >= 0)
405
0
    currentIndexRaw_ =
406
0
      model_->toRawIndex(model_->index(currentIndex_, modelColumn_));
407
0
  else
408
0
    currentIndexRaw_ = nullptr;
409
0
}
410
411
void WComboBox::restoreSelection()
412
0
{
413
0
  if (currentIndexRaw_) {
414
0
    WModelIndex m = model_->fromRawIndex(currentIndexRaw_);
415
0
    if (m.isValid())
416
0
      currentIndex_ = m.row();
417
0
    else
418
0
      currentIndex_ = -1;
419
0
  } else
420
0
    currentIndex_ = -1;
421
422
0
  makeCurrentIndexValid();
423
424
0
  currentIndexRaw_ = nullptr;
425
0
}
426
427
void WComboBox::layoutChanged()
428
0
{
429
0
  itemsChanged_ = true;
430
0
  repaint(RepaintFlag::SizeAffected);
431
432
0
  restoreSelection();
433
0
}
434
435
int WComboBox::findText(const WString& text, WFlags<MatchFlag> flags) const
436
0
{
437
0
  WModelIndexList list = model_->match(model_->index(0, modelColumn_),
438
0
                                       ItemDataRole::Display, cpp17::any(text),
439
0
                                       1, flags);
440
441
0
  if (list.empty())
442
0
    return -1;
443
0
  else
444
0
    return list[0].row();
445
0
}
446
447
}