[office/tellico] /: Add gui class for multi-value Choice fields

Robby Stephenson null at kde.org
Mon Mar 18 01:31:51 GMT 2024


Git commit 9948ebd5c3790617be86fa5679c17f9985ca5418 by Robby Stephenson.
Committed on 18/03/2024 at 01:31.
Pushed by rstephenson into branch 'master'.

Add gui class for multi-value Choice fields

Taken from the QGIS project at
https://api.qgis.org/api/qgscheckablecombobox_8h_source.html

BUG:483831
FIXED-IN:4.0

M  +4    -0    ChangeLog
M  +2    -2    doc/details.docbook
M  +11   -6    src/collection.cpp
M  +13   -9    src/collectionfieldsdialog.cpp
M  +3    -4    src/entry.cpp
M  +1    -0    src/gui/CMakeLists.txt
A  +241  -0    src/gui/checkablecombobox.cpp     [License: GPL (v2/3)]
A  +104  -0    src/gui/checkablecombobox.h     [License: GPL (v2/3)]
M  +79   -28   src/gui/choicefieldwidget.cpp
M  +9    -0    src/gui/choicefieldwidget.h

https://invent.kde.org/office/tellico/-/commit/9948ebd5c3790617be86fa5679c17f9985ca5418

diff --git a/ChangeLog b/ChangeLog
index 3605776a0..6c78614f3 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,3 +1,7 @@
+2024-03-17  Robby Stephenson  <robby at periapsis.org>
+
+	* Updated Choice fields to allow multiple values (Bug 483831).
+
 2024-03-16  Robby Stephenson  <robby at periapsis.org>
 
 	* Updated FilmAffinity data source.
diff --git a/doc/details.docbook b/doc/details.docbook
index 772174715..5774185dc 100644
--- a/doc/details.docbook
+++ b/doc/details.docbook
@@ -82,8 +82,8 @@ summaries, or reviews should be entered using this field type.</para>
 <para>
 When a field should be limited to a few preset values, a
 <emphasis>Choice</emphasis> type is used. The acceptable values are
-presented in a drop down box for selection. Obviously, multiple values
-are not applicable. Fields such as bibliography type or personal rating
+presented in a drop down box for selection. Multiple values can be allowed.
+Fields such as bibliography type or personal rating
 are <emphasis>Choice</emphasis>-type fields.</para>
 
 <para>Semi-colons should be used to separated the allowed values.</para>
diff --git a/src/collection.cpp b/src/collection.cpp
index 5095d2b32..7e7fb1f5a 100644
--- a/src/collection.cpp
+++ b/src/collection.cpp
@@ -637,19 +637,24 @@ bool Collection::isAllowed(const QString& field_, const QString& value_) const {
     return true;
   }
 
-  // find the field with a name of 'key_'
   FieldPtr field = fieldByName(field_);
-
-  // if the type is not multiple choice or if value_ is allowed, return true
-  if(field && (field->type() != Field::Choice || field->allowed().contains(value_))) {
+  if(!field) return false;
+  // non-choice or single-value choice fields with alowed values
+  if(field->type() != Field::Choice ||
+     (!field->hasFlag(Field::AllowMultiple) && field->allowed().contains(value_))) {
     return true;
   }
 
-  return false;
+  // now have to split the text into separate values and check each one
+  const auto values = FieldFormat::splitValue(value_, FieldFormat::StringSplit);
+  for(const auto& value : values) {
+    if(!field->allowed().contains(value)) return false;
+  }
+
+  return true;
 }
 
 Tellico::Data::EntryGroupDict* Collection::entryGroupDictByName(const QString& name_) {
-//  myDebug() << name_;
   m_lastGroupField = name_; // keep track, even if it's invalid
   if(name_.isEmpty() || !m_entryGroupDicts.contains(name_) || m_entries.isEmpty()) {
     return nullptr;
diff --git a/src/collectionfieldsdialog.cpp b/src/collectionfieldsdialog.cpp
index 6b5f479e3..d375c29ed 100644
--- a/src/collectionfieldsdialog.cpp
+++ b/src/collectionfieldsdialog.cpp
@@ -575,25 +575,29 @@ void CollectionFieldsDialog::slotTypeChanged(const QString& type_) {
   m_allowEdit->setEnabled(type == Data::Field::Choice);
 
   // paragraphs, tables, and images are their own category
-  bool isCategory = (type == Data::Field::Para || type == Data::Field::Table ||
+  bool isCategory = (type == Data::Field::Para ||
+                     type == Data::Field::Table ||
                      type == Data::Field::Image);
   m_catCombo->setEnabled(!isCategory);
 
   // formatting is only applicable when the type is simple text or a table
-  bool isText = (type == Data::Field::Line || type == Data::Field::Table);
   // formatNone is the default
-  m_formatCombo->setEnabled(isText);
+  m_formatCombo->setEnabled(type == Data::Field::Line ||
+                            type == Data::Field::Table);
 
-  // multiple is only applicable for simple text and number
-  isText = (type == Data::Field::Line || type == Data::Field::Number);
-  m_multiple->setEnabled(isText);
+  // multiple is only applicable for simple text, number, and choice
+  m_multiple->setEnabled(type == Data::Field::Line ||
+                         type == Data::Field::Number ||
+                         type == Data::Field::Choice);
 
   // completion is only applicable for simple text, number, and URL
-  isText = (isText || type == Data::Field::URL);
-  m_complete->setEnabled(isText);
+  m_complete->setEnabled(type == Data::Field::Line ||
+                         type == Data::Field::Number ||
+                         type == Data::Field::URL);
 
   // grouping is not possible with paragraphs or images
-  m_grouped->setEnabled(type != Data::Field::Para && type != Data::Field::Image);
+  m_grouped->setEnabled(type != Data::Field::Para &&
+                        type != Data::Field::Image);
 }
 
 void CollectionFieldsDialog::slotHighlightedChanged(int index_) {
diff --git a/src/entry.cpp b/src/entry.cpp
index f7af90e09..583949f08 100644
--- a/src/entry.cpp
+++ b/src/entry.cpp
@@ -241,15 +241,14 @@ bool Entry::setFieldImpl(const QString& name_, const QString& value_) {
     return false;
   }
 
-  // the string store is probable only useful for fields with auto-completion or choice/number/bool
+  // the string store is probably only useful for fields with auto-completion or choice/number/bool
   bool shareType = f->type() == Field::Choice ||
                    f->type() == Field::Bool ||
                    f->type() == Field::Image ||
                    f->type() == Field::Rating ||
                    f->type() == Field::Number;
-  if(!(f->hasFlag(Field::AllowMultiple)) &&
-     (shareType ||
-      (f->type() == Field::Line && (f->flags() & Field::AllowCompletion)))) {
+  if(!f->hasFlag(Field::AllowMultiple) &&
+     (shareType || (f->type() == Field::Line && f->hasFlag(Field::AllowCompletion)))) {
     m_fieldValues.insert(Tellico::shareString(name_), Tellico::shareString(value_));
   } else {
     m_fieldValues.insert(Tellico::shareString(name_), value_);
diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt
index b19cbbaa5..0157293a5 100644
--- a/src/gui/CMakeLists.txt
+++ b/src/gui/CMakeLists.txt
@@ -2,6 +2,7 @@
 
 SET(gui_STAT_SRCS
    boolfieldwidget.cpp
+   checkablecombobox.cpp
    choicefieldwidget.cpp
    collectiontemplatedialog.cpp
    collectiontypecombo.cpp
diff --git a/src/gui/checkablecombobox.cpp b/src/gui/checkablecombobox.cpp
new file mode 100644
index 000000000..cf04f3d03
--- /dev/null
+++ b/src/gui/checkablecombobox.cpp
@@ -0,0 +1,241 @@
+/***************************************************************************
+    Copyright (C) 2024 Robby Stephenson <robby at periapsis.org>
+    Adapted from code (C) 2017 by Alexander Bruy
+ ***************************************************************************/
+
+/***************************************************************************
+ *                                                                         *
+ *   This program is free software; you can redistribute it and/or         *
+ *   modify it under the terms of the GNU General Public License as        *
+ *   published by the Free Software Foundation; either version 2 of        *
+ *   the License or (at your option) version 3 or any later version        *
+ *   accepted by the membership of KDE e.V. (or its successor approved     *
+ *   by the membership of KDE e.V.), which shall act as a proxy            *
+ *   defined in Section 14 of version 3 of the license.                    *
+ *                                                                         *
+ *   This program is distributed in the hope that it will be useful,       *
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
+ *   GNU General Public License for more details.                          *
+ *                                                                         *
+ *   You should have received a copy of the GNU General Public License     *
+ *   along with this program.  If not, see <http://www.gnu.org/licenses/>. *
+ *                                                                         *
+ ***************************************************************************/
+
+#include "checkablecombobox.h"
+
+#include <QEvent>
+#include <QMouseEvent>
+#include <QLineEdit>
+#include <QPoint>
+#include <QAbstractItemView>
+
+using Tellico::GUI::CheckableItemModel;
+using Tellico::GUI::CheckBoxDelegate;
+using Tellico::GUI::CheckableComboBox;
+
+CheckableItemModel::CheckableItemModel(QObject* parent_)
+    : QStandardItemModel(0, 1, parent_) {
+}
+
+Qt::ItemFlags CheckableItemModel::flags(const QModelIndex& index_) const {
+  return index_.row() > 0 ? QStandardItemModel::flags(index_) | Qt::ItemIsUserCheckable :
+                            QStandardItemModel::flags(index_);
+}
+
+QVariant CheckableItemModel::data(const QModelIndex& index_, int role_) const {
+  QVariant value = QStandardItemModel::data(index_, role_);
+  if(index_.isValid() && role_ == Qt::CheckStateRole && !value.isValid()) {
+    value = Qt::Unchecked;
+  }
+  return value;
+}
+
+bool CheckableItemModel::setData(const QModelIndex& index_, const QVariant& value_, int role_) {
+  const bool ok = QStandardItemModel::setData(index_, value_, role_);
+  if(ok && role_ == Qt::CheckStateRole) {
+    emit itemCheckStateChanged(index_);
+  }
+  emit dataChanged(index_, index_);
+  return ok;
+}
+
+/**************************************************************/
+
+CheckBoxDelegate::CheckBoxDelegate(QObject* parent_)
+  : QStyledItemDelegate(parent_) {
+}
+
+void CheckBoxDelegate::paint(QPainter* painter_, const QStyleOptionViewItem& option_, const QModelIndex& index_) const {
+  QStyleOptionViewItem& nonConstOpt = const_cast<QStyleOptionViewItem &>(option_);
+  nonConstOpt.showDecorationSelected = false;
+  QStyledItemDelegate::paint(painter_, nonConstOpt, index_);
+}
+
+/**************************************************************/
+
+CheckableComboBox::CheckableComboBox(QWidget* parent_)
+    : QComboBox(parent_)
+    , m_separator(QStringLiteral(", ")) {
+  setModel(new CheckableItemModel(this));
+  setItemDelegate(new CheckBoxDelegate(this));
+
+  auto lineEdit = new QLineEdit(this);
+  lineEdit->setReadOnly(true);
+  setLineEdit(lineEdit);
+  lineEdit->installEventFilter(this);
+  view()->viewport()->installEventFilter(this);
+
+  connect(model(), &QStandardItemModel::rowsInserted, this, [=](const QModelIndex&, int, int) { updateDisplayText(); });
+  connect(model(), &QStandardItemModel::rowsRemoved, this,  [=](const QModelIndex&, int, int) { updateDisplayText(); });
+  connect(model(), &QStandardItemModel::dataChanged, this,  [=](const QModelIndex&, const QModelIndex&, const QVector<int> &) { updateDisplayText(); });
+
+  connect(this, &QComboBox::editTextChanged,
+          this, &CheckableComboBox::setCheckedDataText);
+}
+
+QString CheckableComboBox::separator() const {
+  return m_separator;
+}
+
+void CheckableComboBox::setSeparator(const QString& separator_) {
+  if(m_separator != separator_) {
+    m_separator = separator_;
+    updateDisplayText();
+  }
+}
+
+QStringList CheckableComboBox::checkedItems() const {
+  QStringList items;
+  if(auto* lModel = model()) {
+    const QModelIndex index = lModel->index(0, modelColumn(), rootModelIndex());
+    const QModelIndexList indexes = lModel->match(index, Qt::CheckStateRole, Qt::Checked, -1, Qt::MatchExactly);
+    for(const QModelIndex& index : indexes) {
+      items += index.data().toString();
+    }
+  }
+  return items;
+}
+
+QVariantList CheckableComboBox::checkedItemsData() const {
+  QVariantList data;
+  if(auto* lModel = model()) {
+    const QModelIndex index = lModel->index(0, modelColumn(), rootModelIndex());
+    const QModelIndexList indexes = lModel->match(index, Qt::CheckStateRole, Qt::Checked, -1, Qt::MatchExactly);
+    for(const QModelIndex& index : indexes) {
+      data += index.data(Qt::UserRole).toString();
+    }
+  }
+  return data;
+}
+
+Qt::CheckState CheckableComboBox::itemCheckState(int index_) const {
+  return static_cast<Qt::CheckState>(itemData(index_, Qt::CheckStateRole).toInt());
+}
+
+void CheckableComboBox::setAllCheckState(Qt::CheckState checkState_) {
+  for(int i = 0;  i < count(); i++) {
+    setItemData(i, checkState_, Qt::CheckStateRole);
+  }
+  updateCheckedItems();
+}
+
+void CheckableComboBox::hidePopup() {
+  if(!m_skipHide) {
+    QComboBox::hidePopup();
+  }
+  m_skipHide = false;
+}
+
+bool CheckableComboBox::eventFilter(QObject* obj_, QEvent* ev_) {
+  if(obj_ == lineEdit()) {
+    if(ev_->type() == QEvent::MouseButtonPress && static_cast<QMouseEvent*>(ev_)->button() == Qt::LeftButton) {
+      m_skipHide = true;
+      showPopup();
+    }
+  } else if((ev_->type() == QEvent::MouseButtonPress || ev_->type() == QEvent::MouseButtonRelease)
+              && obj_ == view()->viewport()) {
+    m_skipHide = true;
+    if(ev_->type() == QEvent::MouseButtonRelease && static_cast<QMouseEvent*>(ev_)->button() == Qt::RightButton) {
+      return true;
+    }
+
+    if(ev_->type() == QEvent::MouseButtonRelease) {
+      const QModelIndex index = view()->indexAt(static_cast<QMouseEvent*>(ev_)->pos());
+      if(index.isValid()) {
+        auto item = static_cast<CheckableItemModel*>(model())->itemFromIndex(index);
+        // uncheck the first item, if a different item was checked
+        // uncheckk all other items if the first item was checked
+        // remember the chech state hasn't been changed yet. Do this check first so that the state changed signal
+        // gets fired after all items are updated
+        blockSignals(true);
+        if(item->checkState() == Qt::Unchecked) {
+          if(index.row() == 0) {
+            for(int i = 1; i < count(); i++) {
+              setItemData(i, Qt::Unchecked, Qt::CheckStateRole);
+            }
+          } else {
+             setItemData(0, Qt::Unchecked, Qt::CheckStateRole);
+          }
+        }
+        item->setCheckState(item->checkState() == Qt::Checked ? Qt::Unchecked : Qt::Checked);
+        blockSignals(false);
+        updateCheckedItems();
+      }
+      return true;
+    }
+  }
+
+  return QComboBox::eventFilter(obj_, ev_);
+}
+
+void CheckableComboBox::setCheckedData(const QStringList& values_) {
+  blockSignals(true);
+  setItemData(0, values_.isEmpty() ? Qt::Checked : Qt::Unchecked, Qt::CheckStateRole);
+  for(int i = 1; i < count(); i++) {
+    setItemData(i, values_.contains(itemData(i).toString()) ? Qt::Checked : Qt::Unchecked, Qt::CheckStateRole);
+  }
+  blockSignals(false);
+  updateCheckedItems();
+}
+
+// have to update the checked item state after setting the custom text
+void CheckableComboBox::setCheckedDataText(const QString& text_) {
+  blockSignals(true);
+  const auto values = text_.split(m_separator);
+  setItemData(0, text_.isEmpty() ? Qt::Checked : Qt::Unchecked, Qt::CheckStateRole);
+  for(int i = 1; i < count(); i++) {
+    const bool hasValue = values.contains(itemData(i).toString());
+    setItemData(i, hasValue ? Qt::Checked : Qt::Unchecked, Qt::CheckStateRole);
+  }
+  blockSignals(false);
+  updateCheckedItems();
+}
+
+void CheckableComboBox::resizeEvent(QResizeEvent* ev_) {
+  QComboBox::resizeEvent(ev_);
+  updateDisplayText();
+}
+
+void CheckableComboBox::updateCheckedItems() {
+  const QStringList items = checkedItems();
+  updateDisplayText();
+  emit checkedItemsChanged(items);
+}
+
+void CheckableComboBox::updateDisplayText() {
+  // There is only a line edit if the combobox is in editable state
+  if(!lineEdit()) return;
+
+  const QString oldText = lineEdit()->text();
+  QString text;
+  QStringList items = checkedItems();
+  items.removeOne(QString()); // skip empty string
+  if(!items.isEmpty()) {
+    text = items.join(m_separator);
+  }
+  if(oldText != text) {
+    setEditText(text);
+  }
+}
diff --git a/src/gui/checkablecombobox.h b/src/gui/checkablecombobox.h
new file mode 100644
index 000000000..908c9246b
--- /dev/null
+++ b/src/gui/checkablecombobox.h
@@ -0,0 +1,104 @@
+/***************************************************************************
+    Copyright (C) 2024 Robby Stephenson <robby at periapsis.org>
+    Adapted from code (C) 2017 by Alexander Bruy
+ ***************************************************************************/
+
+/***************************************************************************
+ *                                                                         *
+ *   This program is free software; you can redistribute it and/or         *
+ *   modify it under the terms of the GNU General Public License as        *
+ *   published by the Free Software Foundation; either version 2 of        *
+ *   the License or (at your option) version 3 or any later version        *
+ *   accepted by the membership of KDE e.V. (or its successor approved     *
+ *   by the membership of KDE e.V.), which shall act as a proxy            *
+ *   defined in Section 14 of version 3 of the license.                    *
+ *                                                                         *
+ *   This program is distributed in the hope that it will be useful,       *
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
+ *   GNU General Public License for more details.                          *
+ *                                                                         *
+ *   You should have received a copy of the GNU General Public License     *
+ *   along with this program.  If not, see <http://www.gnu.org/licenses/>. *
+ *                                                                         *
+ ***************************************************************************/
+
+#ifndef TELLICO_GUI_CHECKABLECOMBOBOX_H
+#define TELLICO_GUI_CHECKABLECOMBOBOX_H
+
+#include <QComboBox>
+#include <QMenu>
+#include <QStandardItemModel>
+#include <QStyledItemDelegate>
+
+class QEvent;
+
+namespace Tellico {
+  namespace GUI {
+
+class CheckableItemModel : public QStandardItemModel {
+Q_OBJECT
+
+public:
+  CheckableItemModel(QObject* parent = nullptr);
+
+  Qt::ItemFlags flags(const QModelIndex& index) const Q_DECL_OVERRIDE;
+
+  QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const Q_DECL_OVERRIDE;
+  bool setData(const QModelIndex& index, const QVariant &value, int role = Qt::EditRole) Q_DECL_OVERRIDE;
+
+Q_SIGNALS:
+  void itemCheckStateChanged(const QModelIndex& index);
+};
+
+class CheckBoxDelegate : public QStyledItemDelegate {
+Q_OBJECT
+
+public:
+  CheckBoxDelegate(QObject* parent = nullptr);
+
+  void paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const Q_DECL_OVERRIDE;
+};
+
+class CheckableComboBox : public QComboBox {
+Q_OBJECT
+
+public:
+  CheckableComboBox(QWidget* parent = nullptr);
+
+  QString separator() const;
+  void setSeparator(const QString& separator);
+
+  QStringList checkedItems() const;
+  QVariantList checkedItemsData() const;
+
+  Qt::CheckState itemCheckState(int index) const;
+  void setAllCheckState(Qt::CheckState checkState);
+
+  void hidePopup() override;
+  bool eventFilter(QObject* object, QEvent* event) Q_DECL_OVERRIDE;
+
+Q_SIGNALS:
+  void checkedItemsChanged(const QStringList& items);
+
+public Q_SLOTS:
+  void setCheckedDataText(const QString& text);
+  void setCheckedData(const QStringList& values);
+
+protected:
+  void resizeEvent(QResizeEvent* event) override;
+
+protected:
+
+private:
+  void updateCheckedItems();
+  void updateDisplayText();
+
+  QString m_separator;
+  bool m_skipHide = false;
+};
+
+  } // end namespace
+} //end namespace
+
+#endif
diff --git a/src/gui/choicefieldwidget.cpp b/src/gui/choicefieldwidget.cpp
index 2efecd153..7cfcb434c 100644
--- a/src/gui/choicefieldwidget.cpp
+++ b/src/gui/choicefieldwidget.cpp
@@ -23,11 +23,12 @@
  ***************************************************************************/
 
 #include "choicefieldwidget.h"
+#include "checkablecombobox.h"
 #include "../field.h"
 
-#include <QComboBox>
 #include <QGuiApplication>
 #include <QScreen>
+#include <QBoxLayout>
 
 namespace {
   const double MAX_FRACTION_SCREEN_WIDTH = 0.4;
@@ -36,11 +37,31 @@ namespace {
 using Tellico::GUI::ChoiceFieldWidget;
 
 ChoiceFieldWidget::ChoiceFieldWidget(Tellico::Data::FieldPtr field_, QWidget* parent_)
-    : FieldWidget(field_, parent_), m_comboBox(nullptr) {
+    : FieldWidget(field_, parent_), m_comboBox(nullptr), m_checkableComboBox(nullptr) {
 
+  if(field_->hasFlag(Data::Field::AllowMultiple)) {
+    initCheckableComboBox();
+  } else {
+    initComboBox();
+  }
+  initCommon(field_);
+
+  registerWidget();
+}
+
+void ChoiceFieldWidget::initComboBox() {
   m_comboBox = new QComboBox(this);
   void (QComboBox::* indexChanged)(int) = &QComboBox::currentIndexChanged;
   connect(m_comboBox, indexChanged, this, &ChoiceFieldWidget::checkModified);
+}
+
+void ChoiceFieldWidget::initCheckableComboBox() {
+  m_checkableComboBox = new CheckableComboBox(this);
+  m_checkableComboBox->setSeparator(FieldFormat::delimiterString());
+  connect(m_checkableComboBox, &CheckableComboBox::checkedItemsChanged, this, &ChoiceFieldWidget::checkModified);
+}
+
+void ChoiceFieldWidget::initCommon(Tellico::Data::FieldPtr field_) {
   m_maxTextWidth = MAX_FRACTION_SCREEN_WIDTH * QGuiApplication::primaryScreen()->size().width();
 
   QStringList values = field_->allowed();
@@ -48,54 +69,84 @@ ChoiceFieldWidget::ChoiceFieldWidget(Tellico::Data::FieldPtr field_, QWidget* pa
   values.removeAll(QString());
 
   const QFontMetrics& fm = fontMetrics();
-  m_comboBox->addItem(QString(), QString());
+  auto cb = comboBox(); // everything here applies to either type of combobox;
+  cb->addItem(QString(), QString());
   foreach(const QString& value, values) {
-    m_comboBox->addItem(fm.elidedText(value, Qt::ElideMiddle, m_maxTextWidth), value);
+    cb->addItem(fm.elidedText(value, Qt::ElideMiddle, m_maxTextWidth), value);
   }
-
-  m_comboBox->setMinimumWidth(5*fm.maxWidth());
-  registerWidget();
+  cb->setMinimumWidth(5*fm.maxWidth());
 }
 
 QString ChoiceFieldWidget::text() const {
-  return m_comboBox->currentData().toString();
+  return isMultiSelect() ? comboBox()->currentText()
+                         : comboBox()->currentData().toString();
 }
 
 void ChoiceFieldWidget::setTextImpl(const QString& text_) {
-  int idx = m_comboBox->findData(text_);
-  if(idx < 0) {
-    m_comboBox->addItem(fontMetrics().elidedText(text_, Qt::ElideMiddle, m_maxTextWidth), text_);
-    m_comboBox->setCurrentIndex(m_comboBox->count()-1);
+  auto cb = comboBox();
+  if(isMultiSelect()) {
+    // first uncheck all the boxes
+    m_checkableComboBox->setAllCheckState(Qt::Unchecked);
+    const auto values = FieldFormat::splitValue(text_, FieldFormat::StringSplit);
+    for(const auto& value : qAsConst(values)) {
+      int idx = cb->findData(value);
+      if(idx < 0) {
+        m_checkableComboBox->addItem(fontMetrics().elidedText(text_, Qt::ElideMiddle, m_maxTextWidth), text_);
+        idx = m_checkableComboBox->count()-1;
+      }
+      m_checkableComboBox->setItemData(idx, Qt::Checked, Qt::CheckStateRole);
+    }
   } else {
-    m_comboBox->setCurrentIndex(idx);
+    int idx = cb->findData(text_);
+    if(idx < 0) {
+      m_comboBox->addItem(fontMetrics().elidedText(text_, Qt::ElideMiddle, m_maxTextWidth), text_);
+      m_comboBox->setCurrentIndex(m_comboBox->count()-1);
+    } else {
+      m_comboBox->setCurrentIndex(idx);
+    }
   }
 }
 
 void ChoiceFieldWidget::clearImpl() {
-  m_comboBox->setCurrentIndex(0); // first item is empty
+  comboBox()->setCurrentIndex(0); // first item is empty
   editMultiple(false);
 }
 
 void ChoiceFieldWidget::updateFieldHook(Tellico::Data::FieldPtr, Tellico::Data::FieldPtr newField_) {
-  const QString oldValue = text();
-  int idx = m_comboBox->currentIndex();
-  m_comboBox->blockSignals(true);
-  m_comboBox->clear();
+  bool wasMultiSelect = isMultiSelect();
+  bool nowMultiSelect = newField_->hasFlag(Data::Field::AllowMultiple);
 
-  QStringList values = newField_->allowed();
-  values.removeAll(QString());
+  const QString oldValue = text();
 
-  const QFontMetrics& fm = fontMetrics();
-  // always have empty choice
-  m_comboBox->addItem(QString(), QString());
-  foreach(const QString& value, values) {
-    m_comboBox->addItem(fm.elidedText(value, Qt::ElideMiddle, m_maxTextWidth), value);
+  const int widgetIndex = layout()->indexOf(widget());
+  Q_ASSERT(widgetIndex > -1);
+
+  if(wasMultiSelect && !nowMultiSelect) {
+    layout()->removeWidget(m_checkableComboBox);
+    delete m_checkableComboBox;
+    m_checkableComboBox = nullptr;
+    initComboBox();
+  } else if(!wasMultiSelect && nowMultiSelect) {
+    layout()->removeWidget(m_comboBox);
+    delete m_comboBox;
+    m_comboBox = nullptr;
+    initCheckableComboBox();
+  } else {
+    // remove existing values
+    comboBox()->clear();
   }
-  m_comboBox->setCurrentIndex(idx);
-  m_comboBox->blockSignals(false);
+  initCommon(newField_);
+
+  static_cast<QBoxLayout*>(layout())->insertWidget(widgetIndex, widget(), 1 /*stretch*/);
+  widget()->show();
+
   setTextImpl(oldValue); // set back to previous value
 }
 
+QComboBox* ChoiceFieldWidget::comboBox() const {
+  return isMultiSelect() ? m_checkableComboBox : m_comboBox;
+}
+
 QWidget* ChoiceFieldWidget::widget() {
-  return m_comboBox;
+  return comboBox();
 }
diff --git a/src/gui/choicefieldwidget.h b/src/gui/choicefieldwidget.h
index ac97973a5..e06071e7c 100644
--- a/src/gui/choicefieldwidget.h
+++ b/src/gui/choicefieldwidget.h
@@ -34,6 +34,8 @@ class FieldWidgetTest;
 namespace Tellico {
   namespace GUI {
 
+class CheckableComboBox;
+
 /**
  * @author Robby Stephenson
  */
@@ -57,7 +59,14 @@ protected:
   virtual void updateFieldHook(Data::FieldPtr oldField, Data::FieldPtr newField) Q_DECL_OVERRIDE;
 
 private:
+  QComboBox* comboBox() const;
+  bool isMultiSelect() const { return (m_checkableComboBox != nullptr); }
+  void initComboBox();
+  void initCheckableComboBox();
+  void initCommon(Data::FieldPtr field);
+
   QComboBox* m_comboBox;
+  CheckableComboBox* m_checkableComboBox;
   int m_maxTextWidth;
 };
 


More information about the kde-doc-english mailing list