aboutsummaryrefslogtreecommitdiff
path: root/libs/installer_bsplugins/src/GUI
diff options
context:
space:
mode:
Diffstat (limited to 'libs/installer_bsplugins/src/GUI')
-rw-r--r--libs/installer_bsplugins/src/GUI/CopyEventFilter.cpp53
-rw-r--r--libs/installer_bsplugins/src/GUI/CopyEventFilter.h47
-rw-r--r--libs/installer_bsplugins/src/GUI/GenericIconDelegate.cpp29
-rw-r--r--libs/installer_bsplugins/src/GUI/GenericIconDelegate.h45
-rw-r--r--libs/installer_bsplugins/src/GUI/IGeometrySettings.h44
-rw-r--r--libs/installer_bsplugins/src/GUI/IStateSettings.h44
-rw-r--r--libs/installer_bsplugins/src/GUI/IconDelegate.cpp89
-rw-r--r--libs/installer_bsplugins/src/GUI/IconDelegate.h44
-rw-r--r--libs/installer_bsplugins/src/GUI/ListDialog.cpp66
-rw-r--r--libs/installer_bsplugins/src/GUI/ListDialog.h42
-rw-r--r--libs/installer_bsplugins/src/GUI/MessageDialog.cpp98
-rw-r--r--libs/installer_bsplugins/src/GUI/MessageDialog.h59
-rw-r--r--libs/installer_bsplugins/src/GUI/SelectionDialog.cpp102
-rw-r--r--libs/installer_bsplugins/src/GUI/SelectionDialog.h69
-rw-r--r--libs/installer_bsplugins/src/GUI/ViewMarkingScrollBar.cpp80
-rw-r--r--libs/installer_bsplugins/src/GUI/ViewMarkingScrollBar.h34
-rw-r--r--libs/installer_bsplugins/src/GUI/listdialog.ui103
-rw-r--r--libs/installer_bsplugins/src/GUI/messagedialog.ui94
-rw-r--r--libs/installer_bsplugins/src/GUI/selectiondialog.ui85
19 files changed, 1227 insertions, 0 deletions
diff --git a/libs/installer_bsplugins/src/GUI/CopyEventFilter.cpp b/libs/installer_bsplugins/src/GUI/CopyEventFilter.cpp
new file mode 100644
index 0000000..abd23fd
--- /dev/null
+++ b/libs/installer_bsplugins/src/GUI/CopyEventFilter.cpp
@@ -0,0 +1,53 @@
+#include "CopyEventFilter.h"
+
+#include <QClipboard>
+#include <QGuiApplication>
+#include <QKeyEvent>
+
+namespace GUI
+{
+
+CopyEventFilter::CopyEventFilter(QAbstractItemView* view, int column, int role)
+ : CopyEventFilter(view, [=](const auto& index) {
+ return index.sibling(index.row(), column).data(role).toString();
+ })
+{}
+
+CopyEventFilter::CopyEventFilter(QAbstractItemView* view,
+ std::function<QString(const QModelIndex&)> format)
+ : QObject(view), m_View{view}, m_Format{format}
+{}
+
+void CopyEventFilter::copySelection() const
+{
+ if (!m_View->selectionModel()->hasSelection()) {
+ return;
+ }
+
+ // sort to reflect the visual order
+ QModelIndexList selectedRows = m_View->selectionModel()->selectedRows();
+ std::ranges::sort(selectedRows, [this](const auto& lidx, const auto& ridx) {
+ return m_View->visualRect(lidx).top() < m_View->visualRect(ridx).top();
+ });
+
+ QStringList rows;
+ for (const auto& idx : selectedRows) {
+ rows.append(m_Format(idx));
+ }
+
+ QGuiApplication::clipboard()->setText(rows.join("\n"));
+}
+
+bool CopyEventFilter::eventFilter(QObject* sender, QEvent* event)
+{
+ if (sender == m_View && event->type() == QEvent::KeyPress) {
+ QKeyEvent* const keyEvent = static_cast<QKeyEvent*>(event);
+ if (keyEvent->modifiers() == Qt::ControlModifier && keyEvent->key() == Qt::Key_C) {
+ copySelection();
+ return true;
+ }
+ }
+ return QObject::eventFilter(sender, event);
+}
+
+} // namespace GUI
diff --git a/libs/installer_bsplugins/src/GUI/CopyEventFilter.h b/libs/installer_bsplugins/src/GUI/CopyEventFilter.h
new file mode 100644
index 0000000..fca7b61
--- /dev/null
+++ b/libs/installer_bsplugins/src/GUI/CopyEventFilter.h
@@ -0,0 +1,47 @@
+#ifndef GUI_COPY_EVENT_FILTER_H
+#define GUI_COPY_EVENT_FILTER_H
+
+#include <QAbstractItemView>
+#include <QModelIndex>
+#include <QObject>
+
+#include <functional>
+
+namespace GUI
+{
+
+// this small class provides copy on Ctrl+C and also
+// exposes a method to actual copy the selection
+//
+// the way the selection is copied can be customized by
+// passing a functor to format each index, by default
+// it only extracts the display role
+//
+// only works for view that selects whole row since it only
+// considers the first cell in each row
+//
+class CopyEventFilter : public QObject
+{
+ Q_OBJECT
+
+public:
+ explicit CopyEventFilter(QAbstractItemView* view, int column = 0,
+ int role = Qt::DisplayRole);
+ CopyEventFilter(QAbstractItemView* view,
+ std::function<QString(const QModelIndex&)> format);
+
+ // copy the selection of the view associated with this
+ // event filter into the clipboard
+ //
+ void copySelection() const;
+
+ bool eventFilter(QObject* sender, QEvent* event) override;
+
+private:
+ QAbstractItemView* m_View;
+ std::function<QString(const QModelIndex&)> m_Format;
+};
+
+} // namespace GUI
+
+#endif // GUI_COPY_EVENT_FILTER_H
diff --git a/libs/installer_bsplugins/src/GUI/GenericIconDelegate.cpp b/libs/installer_bsplugins/src/GUI/GenericIconDelegate.cpp
new file mode 100644
index 0000000..9e5f31c
--- /dev/null
+++ b/libs/installer_bsplugins/src/GUI/GenericIconDelegate.cpp
@@ -0,0 +1,29 @@
+#include "GenericIconDelegate.h"
+
+namespace GUI
+{
+
+GenericIconDelegate::GenericIconDelegate(QTreeView* parent, int role, int logicalIndex,
+ int compactSize)
+ : IconDelegate(parent, logicalIndex, compactSize), m_Role{role}
+{}
+
+QList<QString> GenericIconDelegate::getIcons(const QModelIndex& index) const
+{
+ QList<QString> result;
+ if (index.isValid()) {
+ for (const QVariant& var : index.data(m_Role).toList()) {
+ if (!compact() || !var.toString().isEmpty()) {
+ result.append(var.toString());
+ }
+ }
+ }
+ return result;
+}
+
+int GenericIconDelegate::getNumIcons(const QModelIndex& index) const
+{
+ return index.data(m_Role).toList().count();
+}
+
+} // namespace GUI
diff --git a/libs/installer_bsplugins/src/GUI/GenericIconDelegate.h b/libs/installer_bsplugins/src/GUI/GenericIconDelegate.h
new file mode 100644
index 0000000..c0d517e
--- /dev/null
+++ b/libs/installer_bsplugins/src/GUI/GenericIconDelegate.h
@@ -0,0 +1,45 @@
+#ifndef GUI_GENERICICONDELEGATE_H
+#define GUI_GENERICICONDELEGATE_H
+
+#include "IconDelegate.h"
+
+namespace GUI
+{
+
+/**
+ * @brief an icon delegate that takes the list of icons from a user-defines data role
+ */
+class GenericIconDelegate : public IconDelegate
+{
+ Q_OBJECT
+public:
+ /**
+ * @brief constructor
+ * @param parent parent object
+ * @param role role of the itemmodel from which the icon list can be queried (as a
+ * QVariantList)
+ * @param logicalIndex logical index within the model. This is part of a "hack".
+ * Normally "empty" icons will be allocated the same space as a regular icon. This way
+ * the model can use empty icons as spacers and thus align same icons horizontally.
+ * Now, if you set the logical Index to a valid column and connect
+ * the columnResized slot to the sectionResized signal of the view, the delegate will
+ * turn off this behaviour if the column is smaller than "compactSize"
+ * @param compactSize see explanation of logicalIndex
+ */
+ GenericIconDelegate(QTreeView* parent, int role, int logicalIndex = -1,
+ int compactSize = 150);
+
+private:
+ [[nodiscard]] QList<QString> getIcons(const QModelIndex& index) const override;
+ [[nodiscard]] int getNumIcons(const QModelIndex& index) const override;
+
+private:
+ int m_Role;
+ int m_LogicalIndex;
+ int m_CompactSize;
+ bool m_Compact;
+};
+
+} // namespace GUI
+
+#endif // GUI_GENERICICONDELEGATE_H
diff --git a/libs/installer_bsplugins/src/GUI/IGeometrySettings.h b/libs/installer_bsplugins/src/GUI/IGeometrySettings.h
new file mode 100644
index 0000000..681fc11
--- /dev/null
+++ b/libs/installer_bsplugins/src/GUI/IGeometrySettings.h
@@ -0,0 +1,44 @@
+#ifndef GUI_IGEOMETRYSETTINGS_H
+#define GUI_IGEOMETRYSETTINGS_H
+
+#include <concepts>
+
+namespace GUI
+{
+
+template <class W>
+class IGeometrySettings
+{
+public:
+ virtual void saveGeometry(const W* widget) = 0;
+ virtual void restoreGeometry(W* widget) const = 0;
+};
+
+template <class W>
+class [[nodiscard]] GeometrySaver final
+{
+public:
+ GeometrySaver(IGeometrySettings<W>& s, W* w) : m_Settings{s}, m_Widget{w}
+ {
+ m_Settings.restoreGeometry(m_Widget);
+ }
+
+ GeometrySaver(const GeometrySaver<W>&) = delete;
+ GeometrySaver(GeometrySaver<W>&&) = delete;
+
+ ~GeometrySaver() { m_Settings.saveGeometry(m_Widget); }
+
+ GeometrySaver<W>& operator=(const GeometrySaver<W>&) = delete;
+ GeometrySaver<W>& operator=(GeometrySaver<W>&&) = delete;
+
+private:
+ IGeometrySettings<W>& m_Settings;
+ W* m_Widget;
+};
+
+template <class W, std::derived_from<W> Derived>
+GeometrySaver(IGeometrySettings<W>&, Derived*) -> GeometrySaver<W>;
+
+} // namespace GUI
+
+#endif // GUI_IGEOMETRYSETTINGS_H
diff --git a/libs/installer_bsplugins/src/GUI/IStateSettings.h b/libs/installer_bsplugins/src/GUI/IStateSettings.h
new file mode 100644
index 0000000..15425a0
--- /dev/null
+++ b/libs/installer_bsplugins/src/GUI/IStateSettings.h
@@ -0,0 +1,44 @@
+#ifndef GUI_ISTATESETTINGS_H
+#define GUI_ISTATESETTINGS_H
+
+#include <concepts>
+
+namespace GUI
+{
+
+template <class W>
+class IStateSettings
+{
+public:
+ virtual void saveState(const W* widget) = 0;
+ virtual void restoreState(W* widget) const = 0;
+};
+
+template <class W>
+class [[nodiscard]] StateSaver final
+{
+public:
+ StateSaver(IStateSettings<W>& s, W* w) : m_Settings{s}, m_Widget{w}
+ {
+ m_Settings.restoreState(m_Widget);
+ }
+
+ StateSaver(const StateSaver<W>&) = delete;
+ StateSaver(StateSaver<W>&&) = delete;
+
+ ~StateSaver() { m_Settings.saveState(m_Widget); }
+
+ StateSaver<W>& operator=(const StateSaver<W>&) = delete;
+ StateSaver<W>& operator=(StateSaver<W>&&) = delete;
+
+private:
+ IStateSettings<W>& m_Settings;
+ W* m_Widget;
+};
+
+template <class W, class I = IStateSettings<W>>
+StateSaver(I&, W*) -> StateSaver<W>;
+
+} // namespace GUI
+
+#endif // GUI_ISTATESETTINGS_H
diff --git a/libs/installer_bsplugins/src/GUI/IconDelegate.cpp b/libs/installer_bsplugins/src/GUI/IconDelegate.cpp
new file mode 100644
index 0000000..ff99405
--- /dev/null
+++ b/libs/installer_bsplugins/src/GUI/IconDelegate.cpp
@@ -0,0 +1,89 @@
+#include "IconDelegate.h"
+
+#include <QDebug>
+#include <QHBoxLayout>
+#include <QHeaderView>
+#include <QLabel>
+#include <QPainter>
+#include <QPixmapCache>
+#include <algorithm>
+#include <log.h>
+
+namespace GUI
+{
+
+using namespace MOBase;
+
+constexpr int MAX_WIDTH = 16;
+constexpr int MARGIN_X = 4;
+
+IconDelegate::IconDelegate(QTreeView* view, int column, int compactSize)
+ : QStyledItemDelegate(view), m_Column{column}, m_CompactSize{compactSize},
+ m_Compact{false}
+{
+ if (view) {
+ connect(view->header(), &QHeaderView::sectionResized,
+ [=](int logicalIndex, [[maybe_unused]] int oldSize, int newSize) {
+ if (logicalIndex == m_Column) {
+ m_Compact = newSize < m_CompactSize;
+ }
+ });
+ }
+}
+
+void IconDelegate::paintIcons(QPainter* painter, const QStyleOptionViewItem& option,
+ [[maybe_unused]] const QModelIndex& index,
+ const QList<QString>& icons)
+{
+ int x = MARGIN_X;
+ painter->save();
+
+ int iconWidth =
+ icons.size() > 0 ? ((option.rect.width() / icons.size()) - MARGIN_X) : MAX_WIDTH;
+ // Clamp to at least 8px so narrow columns don't make QIcon::pixmap()
+ // return null and spuriously log "failed to load icon".
+ iconWidth = std::clamp(iconWidth, 8, MAX_WIDTH);
+
+ const int margin = (option.rect.height() - iconWidth) / 2;
+
+ painter->translate(option.rect.topLeft());
+ for (const QString& iconId : icons) {
+ if (iconId.isEmpty()) {
+ x += iconWidth + MARGIN_X;
+ continue;
+ }
+ QPixmap icon;
+ const QString fullIconId = QString("%1_%2").arg(iconId).arg(iconWidth);
+ if (!QPixmapCache::find(fullIconId, &icon)) {
+ icon = QIcon(iconId).pixmap(iconWidth, iconWidth);
+ if (icon.isNull()) {
+ log::warn("failed to load icon {}", iconId);
+ }
+ QPixmapCache::insert(fullIconId, icon);
+ }
+ painter->drawPixmap(x, margin, iconWidth, iconWidth, icon);
+ x += iconWidth + MARGIN_X;
+ }
+
+ painter->restore();
+}
+
+void IconDelegate::paint(QPainter* painter, const QStyleOptionViewItem& option,
+ const QModelIndex& index) const
+{
+ if (auto* const w = qobject_cast<QAbstractItemView*>(parent())) {
+ w->itemDelegate()->paint(painter, option, index);
+ } else {
+ QStyledItemDelegate::paint(painter, option, index);
+ }
+
+ paintIcons(painter, option, index, getIcons(index));
+}
+
+QSize IconDelegate::sizeHint(const QStyleOptionViewItem& option,
+ const QModelIndex& index) const
+{
+ return QSize(getNumIcons(index) * (MAX_WIDTH + MARGIN_X), option.rect.height());
+}
+
+} // namespace GUI
diff --git a/libs/installer_bsplugins/src/GUI/IconDelegate.h b/libs/installer_bsplugins/src/GUI/IconDelegate.h
new file mode 100644
index 0000000..057cd4e
--- /dev/null
+++ b/libs/installer_bsplugins/src/GUI/IconDelegate.h
@@ -0,0 +1,44 @@
+#ifndef GUI_ICONDELEGATE_H
+#define GUI_ICONDELEGATE_H
+
+#include <QAbstractItemView>
+#include <QAbstractProxyModel>
+#include <QStyledItemDelegate>
+#include <QTreeView>
+
+namespace GUI
+{
+
+class IconDelegate : public QStyledItemDelegate
+{
+ Q_OBJECT
+
+public:
+ explicit IconDelegate(QTreeView* view, int column = -1, int compactSize = 100);
+
+ void paint(QPainter* painter, const QStyleOptionViewItem& option,
+ const QModelIndex& index) const override;
+
+ QSize sizeHint(const QStyleOptionViewItem& option,
+ const QModelIndex& index) const override;
+
+protected:
+ // check if icons should be compacted or not
+ //
+ [[nodiscard]] bool compact() const { return m_Compact; }
+
+ static void paintIcons(QPainter* painter, const QStyleOptionViewItem& option,
+ const QModelIndex& index, const QList<QString>& icons);
+
+ [[nodiscard]] virtual QList<QString> getIcons(const QModelIndex& index) const = 0;
+ [[nodiscard]] virtual int getNumIcons(const QModelIndex& index) const = 0;
+
+private:
+ int m_Column;
+ int m_CompactSize;
+ bool m_Compact;
+};
+
+} // namespace GUI
+
+#endif // GUI_ICONDELEGATE_H
diff --git a/libs/installer_bsplugins/src/GUI/ListDialog.cpp b/libs/installer_bsplugins/src/GUI/ListDialog.cpp
new file mode 100644
index 0000000..784280a
--- /dev/null
+++ b/libs/installer_bsplugins/src/GUI/ListDialog.cpp
@@ -0,0 +1,66 @@
+#include "ListDialog.h"
+#include "ui_listdialog.h"
+
+namespace GUI
+{
+
+ListDialog::ListDialog(IGeometrySettings<QDialog>& settings, QWidget* parent)
+ : QDialog(parent), ui{new Ui::ListDialog}, m_Settings{settings}
+{
+ ui->setupUi(this);
+ ui->filterEdit->setFocus();
+ connect(ui->choiceList, &QListWidget::itemDoubleClicked, this, &QDialog::accept);
+}
+
+ListDialog::~ListDialog() noexcept
+{
+ delete ui;
+}
+
+int ListDialog::exec()
+{
+ GeometrySaver gs{m_Settings, this};
+ return QDialog::exec();
+}
+
+void ListDialog::setChoices(QStringList choices)
+{
+ m_Choices = choices;
+ ui->choiceList->clear();
+ ui->choiceList->addItems(m_Choices);
+}
+
+QString ListDialog::getChoice() const
+{
+ if (ui->choiceList->selectedItems().length()) {
+ return ui->choiceList->currentItem()->text();
+ } else {
+ return "";
+ }
+}
+
+void ListDialog::on_filterEdit_textChanged(QString filter)
+{
+ QStringList newChoices;
+ for (auto choice : m_Choices) {
+ if (choice.contains(filter, Qt::CaseInsensitive)) {
+ newChoices << choice;
+ }
+ }
+ ui->choiceList->clear();
+ ui->choiceList->addItems(newChoices);
+
+ if (newChoices.length() == 1) {
+ QListWidgetItem* item = ui->choiceList->item(0);
+ item->setSelected(true);
+ ui->choiceList->setCurrentItem(item);
+ }
+
+ if (!filter.isEmpty()) {
+ ui->choiceList->setStyleSheet("QListWidget { border: 2px ridge #f00; }");
+ } else {
+ ui->choiceList->setStyleSheet("");
+ }
+}
+
+} // namespace GUI
diff --git a/libs/installer_bsplugins/src/GUI/ListDialog.h b/libs/installer_bsplugins/src/GUI/ListDialog.h
new file mode 100644
index 0000000..8cd2373
--- /dev/null
+++ b/libs/installer_bsplugins/src/GUI/ListDialog.h
@@ -0,0 +1,42 @@
+#ifndef GUI_LISTDIALOG_H
+#define GUI_LISTDIALOG_H
+
+#include "IGeometrySettings.h"
+
+#include <QDialog>
+
+namespace Ui
+{
+class ListDialog;
+}
+
+namespace GUI
+{
+
+class ListDialog : public QDialog
+{
+ Q_OBJECT
+
+public:
+ ListDialog(IGeometrySettings<QDialog>& settings, QWidget* parent = nullptr);
+ ~ListDialog() noexcept;
+
+ // also saves and restores geometry
+ //
+ int exec() override;
+
+ void setChoices(QStringList choices);
+ [[nodiscard]] QString getChoice() const;
+
+public slots:
+ void on_filterEdit_textChanged(QString filter);
+
+private:
+ Ui::ListDialog* ui;
+ IGeometrySettings<QDialog>& m_Settings;
+ QStringList m_Choices;
+};
+
+} // namespace GUI
+
+#endif // GUI_LISTDIALOG_H
diff --git a/libs/installer_bsplugins/src/GUI/MessageDialog.cpp b/libs/installer_bsplugins/src/GUI/MessageDialog.cpp
new file mode 100644
index 0000000..92197c5
--- /dev/null
+++ b/libs/installer_bsplugins/src/GUI/MessageDialog.cpp
@@ -0,0 +1,98 @@
+#include "MessageDialog.h"
+#include "ui_messagedialog.h"
+
+#include <log.h>
+
+#include <QMainWindow>
+#include <QResizeEvent>
+#include <QTimer>
+
+using namespace MOBase;
+
+namespace GUI
+{
+
+MessageDialog::MessageDialog(const QString& text, QWidget* reference)
+ : QDialog(reference), ui{new Ui::MessageDialog}
+{
+ ui->setupUi(this);
+
+ // very crude way to ensure no single word in the test is wider than the message
+ // window. ellide in the center if necessary
+ QFontMetrics metrics(ui->message->font());
+ QString restrictedText;
+ QStringList lines = text.split("\n");
+ foreach (const QString& line, lines) {
+ QString newLine;
+ QStringList words = line.split(" ");
+ foreach (const QString& word, words) {
+ if (word.length() > 10) {
+ newLine +=
+ "<span style=\"nobreak\">" +
+ metrics.elidedText(word, Qt::ElideMiddle, ui->message->maximumWidth()) +
+ "</span>";
+ } else {
+ newLine += word;
+ }
+ newLine += " ";
+ }
+ restrictedText += newLine + "\n";
+ }
+
+ ui->message->setText(restrictedText);
+ this->setWindowFlags(Qt::Tool | Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint);
+ this->setFocusPolicy(Qt::NoFocus);
+ this->setAttribute(Qt::WA_ShowWithoutActivating);
+ QTimer::singleShot(1000 + (text.length() * 40), this, SLOT(hide()));
+ if (reference != nullptr) {
+ QPoint position =
+ reference->mapToGlobal(QPoint(reference->width() / 2, reference->height()));
+ position.rx() -= this->width() / 2;
+ position.ry() -= this->height() + 5;
+ move(position);
+ }
+}
+
+MessageDialog::~MessageDialog()
+{
+ delete ui;
+}
+
+void MessageDialog::resizeEvent(QResizeEvent* event)
+{
+ QWidget* par = parentWidget();
+ if (par != nullptr) {
+ QPoint position = par->mapToGlobal(QPoint(par->width() / 2, par->height()));
+ position.rx() -= event->size().width() / 2;
+ position.ry() -= event->size().height() + 5;
+ move(position);
+ }
+}
+
+void MessageDialog::showMessage(const QString& text, QWidget* reference,
+ bool bringToFront)
+{
+ log::debug("{}", text);
+
+ if (!reference) {
+ for (QWidget* w : qApp->topLevelWidgets()) {
+ if (dynamic_cast<QMainWindow*>(w)) {
+ reference = w;
+ break;
+ }
+ }
+ }
+
+ if (!reference) {
+ return;
+ }
+
+ MessageDialog* dialog = new MessageDialog(text, reference);
+ dialog->show();
+
+ if (bringToFront) {
+ reference->activateWindow();
+ }
+}
+
+} // namespace GUI
diff --git a/libs/installer_bsplugins/src/GUI/MessageDialog.h b/libs/installer_bsplugins/src/GUI/MessageDialog.h
new file mode 100644
index 0000000..4ea63cb
--- /dev/null
+++ b/libs/installer_bsplugins/src/GUI/MessageDialog.h
@@ -0,0 +1,59 @@
+#ifndef GUI_MESSAGEDIALOG_H
+#define GUI_MESSAGEDIALOG_H
+
+#include <QDialog>
+
+namespace Ui
+{
+class MessageDialog;
+}
+
+namespace GUI
+{
+
+/**
+ * borderless dialog used to display short messages that will automatically
+ * vanish after a moment
+ **/
+class MessageDialog : public QDialog
+{
+ Q_OBJECT
+
+public:
+ /**
+ * @brief constructor
+ *
+ * @param text the message to display
+ * @param reference parent widget. This will also be used to position the message at
+ * the bottom center of the dialog
+ **/
+
+ explicit MessageDialog(const QString& text, QWidget* reference);
+
+ ~MessageDialog();
+
+ /**
+ * factory function for message dialogs. This can be used as a fire-and-forget. The
+ * message will automatically positioned to the reference dialog and get a reasonable
+ * view time
+ *
+ * @param text the text to display. The length of this text is used to determine how
+ * long the dialog is to be shown
+ * @param reference the reference widget on top of which the message should be
+ * displayed
+ * @param true if the message should bring MO to front to ensure this message is
+ * visible
+ **/
+ static void showMessage(const QString& text, QWidget* reference,
+ bool bringToFront = true);
+
+protected:
+ virtual void resizeEvent(QResizeEvent* event);
+
+private:
+ Ui::MessageDialog* ui;
+};
+
+} // namespace GUI
+
+#endif // GUI_MESSAGEDIALOG_H
diff --git a/libs/installer_bsplugins/src/GUI/SelectionDialog.cpp b/libs/installer_bsplugins/src/GUI/SelectionDialog.cpp
new file mode 100644
index 0000000..3572abe
--- /dev/null
+++ b/libs/installer_bsplugins/src/GUI/SelectionDialog.cpp
@@ -0,0 +1,102 @@
+#include "SelectionDialog.h"
+#include "ui_selectiondialog.h"
+
+#include <QCommandLinkButton>
+
+namespace GUI
+{
+
+SelectionDialog::SelectionDialog(const QString& description, QWidget* parent,
+ const QSize& iconSize)
+ : QDialog(parent), ui{new Ui::SelectionDialog}, m_Choice{nullptr},
+ m_ValidateByData{false}, m_IconSize{iconSize}
+{
+ ui->setupUi(this);
+
+ ui->descriptionLabel->setText(description);
+}
+
+SelectionDialog::~SelectionDialog() noexcept
+{
+ delete ui;
+}
+
+void SelectionDialog::addChoice(const QString& buttonText, const QString& description,
+ const QVariant& data)
+{
+ QAbstractButton* button =
+ new QCommandLinkButton(buttonText, description, ui->buttonBox);
+ if (m_IconSize.isValid()) {
+ button->setIconSize(m_IconSize);
+ }
+ button->setProperty("data", data);
+ ui->buttonBox->addButton(button, QDialogButtonBox::AcceptRole);
+ if (data.isValid())
+ m_ValidateByData = true;
+}
+
+void SelectionDialog::addChoice(const QIcon& icon, const QString& buttonText,
+ const QString& description, const QVariant& data)
+{
+ QAbstractButton* button =
+ new QCommandLinkButton(buttonText, description, ui->buttonBox);
+ if (m_IconSize.isValid()) {
+ button->setIconSize(m_IconSize);
+ }
+ button->setIcon(icon);
+ button->setProperty("data", data);
+ ui->buttonBox->addButton(button, QDialogButtonBox::AcceptRole);
+ if (data.isValid())
+ m_ValidateByData = true;
+}
+
+int SelectionDialog::numChoices() const
+{
+ return ui->buttonBox->findChildren<QCommandLinkButton*>(QString()).count();
+}
+
+QVariant SelectionDialog::getChoiceData()
+{
+ return m_Choice->property("data");
+}
+
+QString SelectionDialog::getChoiceString()
+{
+ if ((m_Choice == nullptr) ||
+ (m_ValidateByData && !m_Choice->property("data").isValid())) {
+ return QString();
+ } else {
+ return m_Choice->text();
+ }
+}
+
+QString SelectionDialog::getChoiceDescription()
+{
+ if (m_Choice == nullptr)
+ return QString();
+ else
+ return m_Choice->accessibleDescription();
+}
+
+void SelectionDialog::disableCancel()
+{
+ ui->cancelButton->setEnabled(false);
+ ui->cancelButton->setHidden(true);
+}
+
+void SelectionDialog::on_buttonBox_clicked(QAbstractButton* button)
+{
+ m_Choice = button;
+ if (!m_ValidateByData || m_Choice->property("data").isValid()) {
+ this->accept();
+ } else {
+ this->reject();
+ }
+}
+
+void SelectionDialog::on_cancelButton_clicked()
+{
+ this->reject();
+}
+
+} // namespace GUI
diff --git a/libs/installer_bsplugins/src/GUI/SelectionDialog.h b/libs/installer_bsplugins/src/GUI/SelectionDialog.h
new file mode 100644
index 0000000..52fdcf9
--- /dev/null
+++ b/libs/installer_bsplugins/src/GUI/SelectionDialog.h
@@ -0,0 +1,69 @@
+#ifndef GUI_SELECTIONDIALOG_H
+#define GUI_SELECTIONDIALOG_H
+
+#include <QAbstractButton>
+#include <QDialog>
+
+namespace Ui
+{
+class SelectionDialog;
+}
+
+namespace GUI
+{
+
+class SelectionDialog : public QDialog
+{
+ Q_OBJECT
+
+public:
+ explicit SelectionDialog(const QString& description, QWidget* parent = 0,
+ const QSize& iconSize = QSize());
+
+ SelectionDialog(const SelectionDialog&) = delete;
+ SelectionDialog(SelectionDialog&&) = delete;
+
+ ~SelectionDialog() noexcept;
+
+ SelectionDialog& operator=(const SelectionDialog&) = delete;
+ SelectionDialog& operator=(SelectionDialog&&) = delete;
+
+ /**
+ * @brief add a choice to the dialog
+ * @param buttonText the text to be displayed on the button
+ * @param description the description that shows up under in small letters inside the
+ * button
+ * @param data data to be stored with the button. Please note that as soon as one
+ * choice has data associated with it (non-invalid QVariant) all buttons that contain
+ * no data will be treated as "cancel" buttons
+ */
+ void addChoice(const QString& buttonText, const QString& description,
+ const QVariant& data);
+
+ void addChoice(const QIcon& icon, const QString& buttonText,
+ const QString& description, const QVariant& data);
+
+ int numChoices() const;
+
+ QVariant getChoiceData();
+ QString getChoiceString();
+ QString getChoiceDescription();
+
+ void disableCancel();
+
+private slots:
+
+ void on_buttonBox_clicked(QAbstractButton* button);
+
+ void on_cancelButton_clicked();
+
+private:
+ Ui::SelectionDialog* ui;
+ QAbstractButton* m_Choice;
+ bool m_ValidateByData;
+ QSize m_IconSize;
+};
+
+} // namespace GUI
+
+#endif // GUI_SELECTIONDIALOG_H
diff --git a/libs/installer_bsplugins/src/GUI/ViewMarkingScrollBar.cpp b/libs/installer_bsplugins/src/GUI/ViewMarkingScrollBar.cpp
new file mode 100644
index 0000000..94da308
--- /dev/null
+++ b/libs/installer_bsplugins/src/GUI/ViewMarkingScrollBar.cpp
@@ -0,0 +1,80 @@
+#include "ViewMarkingScrollBar.h"
+
+#include <QPainter>
+#include <QStyle>
+#include <QStyleOptionSlider>
+
+namespace GUI
+{
+
+static QModelIndexList visibleIndexImpl(QTreeView* view, int column,
+ const QModelIndex& parent)
+{
+ if (parent.isValid() && !view->isExpanded(parent)) {
+ return {};
+ }
+
+ const auto* const model = view->model();
+ QModelIndexList index;
+ for (int i = 0; i < model->rowCount(parent); ++i) {
+ index.append(model->index(i, column, parent));
+ index.append(visibleIndexImpl(view, column, index.back()));
+ }
+ return index;
+}
+
+static QModelIndexList visibleIndex(QTreeView* view, int column)
+{
+ return visibleIndexImpl(view, column, QModelIndex());
+}
+
+ViewMarkingScrollBar::ViewMarkingScrollBar(QTreeView* view, int role)
+ : QScrollBar(view), m_View{view}, m_Role{role}
+{
+ // not implemented for horizontal sliders
+ Q_ASSERT(this->orientation() == Qt::Vertical);
+}
+
+QColor ViewMarkingScrollBar::color(const QModelIndex& index) const
+{
+ const auto data = index.data(m_Role);
+ if (data.canConvert<QColor>()) {
+ return data.value<QColor>();
+ }
+ return QColor();
+}
+
+void ViewMarkingScrollBar::paintEvent(QPaintEvent* event)
+{
+ if (m_View->model() == nullptr) {
+ return;
+ }
+ QScrollBar::paintEvent(event);
+
+ QStyleOptionSlider styleOption;
+ initStyleOption(&styleOption);
+
+ QPainter painter(this);
+
+ QRect handleRect = style()->subControlRect(QStyle::CC_ScrollBar, &styleOption,
+ QStyle::SC_ScrollBarSlider, this);
+ QRect innerRect = style()->subControlRect(QStyle::CC_ScrollBar, &styleOption,
+ QStyle::SC_ScrollBarGroove, this);
+
+ auto indices = visibleIndex(m_View, 0);
+
+ painter.translate(innerRect.topLeft() + QPoint(0, 3));
+ const qreal scale =
+ static_cast<qreal>(innerRect.height() - 3) / static_cast<qreal>(indices.size());
+
+ for (int i = 0; i < indices.size(); ++i) {
+ const QColor color = this->color(indices[i]);
+ if (color.isValid()) {
+ painter.setPen(color);
+ painter.setBrush(color);
+ painter.drawRect(QRect(2, i * scale - 2, handleRect.width() - 5, 3));
+ }
+ }
+}
+
+} // namespace GUI
diff --git a/libs/installer_bsplugins/src/GUI/ViewMarkingScrollBar.h b/libs/installer_bsplugins/src/GUI/ViewMarkingScrollBar.h
new file mode 100644
index 0000000..750c743
--- /dev/null
+++ b/libs/installer_bsplugins/src/GUI/ViewMarkingScrollBar.h
@@ -0,0 +1,34 @@
+#ifndef GUI_VIEWMARKINGSCROLLBAR_H
+#define GUI_VIEWMARKINGSCROLLBAR_H
+
+#include <QColor>
+#include <QModelIndex>
+#include <QPaintEvent>
+#include <QScrollBar>
+#include <QTreeView>
+
+namespace GUI
+{
+
+class ViewMarkingScrollBar : public QScrollBar
+{
+public:
+ ViewMarkingScrollBar(QTreeView* view, int role);
+
+protected:
+ void paintEvent(QPaintEvent* event) override;
+
+ // retrieve the color of the marker for the given index
+ //
+ [[nodiscard]] virtual QColor color(const QModelIndex& index) const;
+
+protected:
+ QTreeView* m_View;
+
+private:
+ int m_Role;
+};
+
+} // namespace GUI
+
+#endif // GUI_VIEWMARKINGSCROLLBAR_H
diff --git a/libs/installer_bsplugins/src/GUI/listdialog.ui b/libs/installer_bsplugins/src/GUI/listdialog.ui
new file mode 100644
index 0000000..7aa3eba
--- /dev/null
+++ b/libs/installer_bsplugins/src/GUI/listdialog.ui
@@ -0,0 +1,103 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>ListDialog</class>
+ <widget class="QDialog" name="ListDialog">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>260</width>
+ <height>380</height>
+ </rect>
+ </property>
+ <property name="windowTitle">
+ <string>Select an item...</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <item>
+ <widget class="QListWidget" name="choiceList">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Preferred" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="minimumSize">
+ <size>
+ <width>240</width>
+ <height>280</height>
+ </size>
+ </property>
+ <property name="verticalScrollMode">
+ <enum>QAbstractItemView::ScrollPerPixel</enum>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="MOBase::LineEditClear" name="filterEdit">
+ <property name="text">
+ <string/>
+ </property>
+ <property name="placeholderText">
+ <string>Filter</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QDialogButtonBox" name="buttonBox">
+ <property name="layoutDirection">
+ <enum>Qt::LeftToRight</enum>
+ </property>
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="standardButtons">
+ <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <customwidgets>
+ <customwidget>
+ <class>MOBase::LineEditClear</class>
+ <extends>QLineEdit</extends>
+ <header>lineeditclear.h</header>
+ </customwidget>
+ </customwidgets>
+ <resources/>
+ <connections>
+ <connection>
+ <sender>buttonBox</sender>
+ <signal>accepted()</signal>
+ <receiver>ListDialog</receiver>
+ <slot>accept()</slot>
+ <hints>
+ <hint type="sourcelabel">
+ <x>248</x>
+ <y>254</y>
+ </hint>
+ <hint type="destinationlabel">
+ <x>157</x>
+ <y>274</y>
+ </hint>
+ </hints>
+ </connection>
+ <connection>
+ <sender>buttonBox</sender>
+ <signal>rejected()</signal>
+ <receiver>ListDialog</receiver>
+ <slot>reject()</slot>
+ <hints>
+ <hint type="sourcelabel">
+ <x>316</x>
+ <y>260</y>
+ </hint>
+ <hint type="destinationlabel">
+ <x>286</x>
+ <y>274</y>
+ </hint>
+ </hints>
+ </connection>
+ </connections>
+</ui>
diff --git a/libs/installer_bsplugins/src/GUI/messagedialog.ui b/libs/installer_bsplugins/src/GUI/messagedialog.ui
new file mode 100644
index 0000000..ed785ff
--- /dev/null
+++ b/libs/installer_bsplugins/src/GUI/messagedialog.ui
@@ -0,0 +1,94 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>MessageDialog</class>
+ <widget class="QDialog" name="MessageDialog">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>106</width>
+ <height>35</height>
+ </rect>
+ </property>
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Preferred" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="maximumSize">
+ <size>
+ <width>16777215</width>
+ <height>16777215</height>
+ </size>
+ </property>
+ <property name="sizeIncrement">
+ <size>
+ <width>0</width>
+ <height>0</height>
+ </size>
+ </property>
+ <property name="windowTitle">
+ <string>Placeholder</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="QLabel" name="message">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="MinimumExpanding" vsizetype="MinimumExpanding">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="maximumSize">
+ <size>
+ <width>300</width>
+ <height>16777215</height>
+ </size>
+ </property>
+ <property name="sizeIncrement">
+ <size>
+ <width>0</width>
+ <height>0</height>
+ </size>
+ </property>
+ <property name="frameShape">
+ <enum>QFrame::Box</enum>
+ </property>
+ <property name="text">
+ <string>Placeholder</string>
+ </property>
+ <property name="textFormat">
+ <enum>Qt::RichText</enum>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignCenter</set>
+ </property>
+ <property name="wordWrap">
+ <bool>true</bool>
+ </property>
+ <property name="margin">
+ <number>2</number>
+ </property>
+ <property name="textInteractionFlags">
+ <set>Qt::NoTextInteraction</set>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/libs/installer_bsplugins/src/GUI/selectiondialog.ui b/libs/installer_bsplugins/src/GUI/selectiondialog.ui
new file mode 100644
index 0000000..9b7f7e3
--- /dev/null
+++ b/libs/installer_bsplugins/src/GUI/selectiondialog.ui
@@ -0,0 +1,85 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>SelectionDialog</class>
+ <widget class="QDialog" name="SelectionDialog">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>527</width>
+ <height>327</height>
+ </rect>
+ </property>
+ <property name="windowTitle">
+ <string>Select</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <property name="spacing">
+ <number>20</number>
+ </property>
+ <item>
+ <widget class="QLabel" name="descriptionLabel">
+ <property name="text">
+ <string>Placeholder</string>
+ </property>
+ <property name="wordWrap">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QScrollArea" name="scrollArea">
+ <property name="horizontalScrollBarPolicy">
+ <enum>Qt::ScrollBarAlwaysOff</enum>
+ </property>
+ <property name="widgetResizable">
+ <bool>true</bool>
+ </property>
+ <widget class="QWidget" name="scrollAreaWidgetContents">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>503</width>
+ <height>219</height>
+ </rect>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_2">
+ <property name="margin">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="QDialogButtonBox" name="buttonBox">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Expanding" vsizetype="Expanding">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="standardButtons">
+ <set>QDialogButtonBox::NoButton</set>
+ </property>
+ <property name="centerButtons">
+ <bool>false</bool>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="cancelButton">
+ <property name="text">
+ <string>Cancel</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>