|
| 1 | +/* QtNetworkCrumbs - Some networking toys for Qt |
| 2 | + * Copyright (C) 2019-2024 Mathias Hasselmann |
| 3 | + */ |
| 4 | +#include "detailmodel.h" |
| 5 | + |
| 6 | +// Qt headers |
| 7 | +#include <QLoggingCategory> |
| 8 | +#include <QUrl> |
| 9 | + |
| 10 | +namespace qnc::core { |
| 11 | +namespace { |
| 12 | + |
| 13 | +Q_LOGGING_CATEGORY(lcDetailModel, "qnc.core.detailmodel") |
| 14 | + |
| 15 | +struct Path |
| 16 | +{ |
| 17 | + static constexpr int BitsPerLength = 2; |
| 18 | + static constexpr int BitsPerRow = 10; |
| 19 | + static constexpr int MaximumLength = (1 << BitsPerLength) - 1; |
| 20 | + static constexpr int MaximumRow = (1 << BitsPerRow) - 1; |
| 21 | + static constexpr quintptr LengthMask = static_cast<quintptr>(MaximumLength); |
| 22 | + static constexpr quintptr RowMask = static_cast<quintptr>(MaximumRow); |
| 23 | + static constexpr quintptr IndexMask = ~LengthMask; |
| 24 | + |
| 25 | + quintptr value; |
| 26 | + |
| 27 | + Q_IMPLICIT constexpr Path(quintptr path = 0) noexcept : value{path} {} |
| 28 | + Q_IMPLICIT constexpr operator quintptr() const noexcept { return value; } |
| 29 | + |
| 30 | + explicit constexpr Path(const QModelIndex &index) noexcept : Path{index.internalId()} {} |
| 31 | + explicit constexpr Path(Path parent, int row) noexcept : value{make(parent, row).value} {} |
| 32 | + explicit constexpr Path(const QModelIndex &parent, int row) noexcept : Path{parent.internalId(), row} {} |
| 33 | + |
| 34 | + [[nodiscard]] constexpr static Path make(Path parent, int row) noexcept |
| 35 | + { |
| 36 | + if (row < 0 || row > MaximumRow) |
| 37 | + return 0; |
| 38 | + if (parent.length() >= MaximumLength) |
| 39 | + return 0; |
| 40 | + |
| 41 | + const auto shift = parent.length() * BitsPerRow + BitsPerLength; |
| 42 | + |
| 43 | + return (parent.value & IndexMask) |
| 44 | + | static_cast<quintptr>(row << shift) |
| 45 | + | static_cast<quintptr>(parent.length() + 1); |
| 46 | + } |
| 47 | + |
| 48 | + [[nodiscard]] constexpr int length() const noexcept |
| 49 | + { |
| 50 | + return static_cast<int>(value & LengthMask); |
| 51 | + } |
| 52 | + |
| 53 | + [[nodiscard]] constexpr int at(int index) const noexcept |
| 54 | + { |
| 55 | + if (Q_UNLIKELY(index < 0 || index >= length())) |
| 56 | + return -1; |
| 57 | + |
| 58 | + const auto shift = index * BitsPerRow + BitsPerLength; |
| 59 | + return static_cast<int>((value >> shift) & RowMask); |
| 60 | + } |
| 61 | + |
| 62 | + [[nodiscard]] constexpr int last() const noexcept |
| 63 | + { |
| 64 | + return at(length() - 1); |
| 65 | + } |
| 66 | + |
| 67 | + [[nodiscard]] constexpr Path parent() const noexcept |
| 68 | + { |
| 69 | + if (Q_UNLIKELY(length() < 1)) |
| 70 | + return {}; |
| 71 | + |
| 72 | + const auto shift = (length() - 1) * BitsPerRow + BitsPerLength; |
| 73 | + const auto prefix = (value & ~(RowMask << shift)) & IndexMask; |
| 74 | + return prefix | static_cast<quintptr>(length() - 1); |
| 75 | + } |
| 76 | + |
| 77 | + [[nodiscard]] friend constexpr bool operator==(const Path &l, const Path &r) noexcept { return l.value == r.value; } |
| 78 | +}; |
| 79 | + |
| 80 | +static_assert(Path{} .length() == 0); |
| 81 | +static_assert(Path{0x00}.length() == 0); |
| 82 | +static_assert(Path{0x01}.length() == 1); |
| 83 | +static_assert(Path{0x11}.length() == 1); |
| 84 | +static_assert(Path{0x13}.length() == 3); |
| 85 | + |
| 86 | +static_assert(Path{} .at(0) == -1); |
| 87 | +static_assert(Path{0x0011}.at(0) == 4); |
| 88 | +static_assert(Path{0x0012}.at(1) == 0); |
| 89 | + |
| 90 | +static_assert(Path{0x841602f}.length() == 3); |
| 91 | +static_assert(Path{0x841602f}.at(0) == 11); |
| 92 | +static_assert(Path{0x841602f}.at(1) == 22); |
| 93 | +static_assert(Path{0x841602f}.at(2) == 33); |
| 94 | +static_assert(Path{0x841602f}.at(3) == -1); |
| 95 | + |
| 96 | +static_assert(Path{0x841602f}.parent().length() == 2); |
| 97 | +static_assert(Path{0x841602f}.parent().at(0) == 11); |
| 98 | +static_assert(Path{0x841602f}.parent().at(1) == 22); |
| 99 | +static_assert(Path{0x841602f}.parent().at(2) == -1); |
| 100 | + |
| 101 | +static_assert(Path{0x1602e}.length() == 2); |
| 102 | +static_assert(Path{0x1602e}.at(0) == 11); |
| 103 | +static_assert(Path{0x1602e}.at(1) == 22); |
| 104 | +static_assert(Path{0x1602e}.at(2) == -1); |
| 105 | + |
| 106 | +static_assert(Path{0x1602e, 33}.length() == 3); |
| 107 | +static_assert(Path{0x1602e, 33}.at(0) == 11); |
| 108 | +static_assert(Path{0x1602e, 33}.at(1) == 22); |
| 109 | +static_assert(Path{0x1602e, 33}.at(2) == 33); |
| 110 | +static_assert(Path{0x1602e, 33}.at(3) == -1); |
| 111 | + |
| 112 | +static_assert(Path{0x841602f}.parent() == Path{0x1602e}); |
| 113 | +static_assert(Path{0x841602f} == Path{0x1602e, 33}); |
| 114 | + |
| 115 | +template <DetailModel::Column column, DetailModel::Role role> |
| 116 | +[[nodiscard]] QVariant modelData(const QModelIndex &index) |
| 117 | +{ |
| 118 | + const auto &sibling = index.siblingAtColumn(qToUnderlying(column)); |
| 119 | + return sibling.data(qToUnderlying(role)); |
| 120 | +} |
| 121 | + |
| 122 | +QByteArray toByteArray(Path path) |
| 123 | +{ |
| 124 | + auto string = QByteArray{}; |
| 125 | + |
| 126 | + if (path.length() > 0) { |
| 127 | + string += QByteArray::number(path.at(0)); |
| 128 | + |
| 129 | + for (auto i = 1; i < path.length(); ++i) { |
| 130 | + string += '/'; |
| 131 | + string += QByteArray::number(path.at(i)); |
| 132 | + } |
| 133 | + } |
| 134 | + |
| 135 | + return string; |
| 136 | +} |
| 137 | + |
| 138 | +bool validate(const DetailModel::RowList &rows, Path path = {}) |
| 139 | +{ |
| 140 | + auto valid = true; |
| 141 | + |
| 142 | + if (rows.count() > Path::MaximumRow) { |
| 143 | + qCWarning(lcDetailModel, "Too many items at (%s); ignoring rows from %d to %d", |
| 144 | + toByteArray(path).constData(), Path::MaximumRow + 1, |
| 145 | + static_cast<int>(rows.count())); |
| 146 | + |
| 147 | + valid = false; |
| 148 | + } |
| 149 | + |
| 150 | + for (auto i = 0; i < rows.count(); ++i) { |
| 151 | + if (rows[i].hasChildren()) { |
| 152 | + if (path.length() == Path::MaximumLength) { |
| 153 | + qCWarning(lcDetailModel, |
| 154 | + "Maximum tree depth reached; ignoring children of (%s/%d)", |
| 155 | + toByteArray(path).constData(), i); |
| 156 | + |
| 157 | + valid = false; |
| 158 | + } else { |
| 159 | + valid &= validate(rows[i].children(), Path{path, i}); |
| 160 | + } |
| 161 | + } |
| 162 | + } |
| 163 | + |
| 164 | + return valid; |
| 165 | +} |
| 166 | + |
| 167 | +} // namespace |
| 168 | + |
| 169 | +void DetailModel::reset(const RowList &rows) |
| 170 | +{ |
| 171 | + core::validate(rows); |
| 172 | + |
| 173 | + beginResetModel(); |
| 174 | + m_rows = rows; |
| 175 | + endResetModel(); |
| 176 | +} |
| 177 | + |
| 178 | +QModelIndex DetailModel::index(int row, int column, const QModelIndex &parent) const |
| 179 | +{ |
| 180 | + if (!parent.isValid()) |
| 181 | + return createIndex(row, column, Path{}); |
| 182 | + else if (Path{parent}.length() < Path::MaximumLength) |
| 183 | + return createIndex(row, column, Path{parent, parent.row()}); |
| 184 | + else |
| 185 | + return {}; |
| 186 | +} |
| 187 | + |
| 188 | +QModelIndex DetailModel::parent(const QModelIndex &child) const |
| 189 | +{ |
| 190 | + if (const auto path = Path{child.internalId()}; path.length() > 0) |
| 191 | + return createIndex(path.last(), 0, path.parent()); |
| 192 | + |
| 193 | + return {}; |
| 194 | +} |
| 195 | + |
| 196 | +int DetailModel::rowCount(const QModelIndex &parent) const |
| 197 | +{ |
| 198 | + if (!parent.isValid()) |
| 199 | + return std::min(static_cast<int>(m_rows.size()), Path::MaximumRow); |
| 200 | + else if (parent.column() != 0) |
| 201 | + return 0; |
| 202 | + else if (Path{parent}.length() == Path::MaximumLength) |
| 203 | + return 0; |
| 204 | + else if (const auto &data = value(parent); !hasChildren(data)) |
| 205 | + return 0; |
| 206 | + else |
| 207 | + return std::min(static_cast<int>(children(data).size()), Path::MaximumRow); |
| 208 | +} |
| 209 | + |
| 210 | +int DetailModel::columnCount(const QModelIndex &) const |
| 211 | +{ |
| 212 | + return 2; |
| 213 | +} |
| 214 | + |
| 215 | +QVariant DetailModel::Row::data(Column column, Role role) const |
| 216 | +{ |
| 217 | + switch (static_cast<Role>(role)) { |
| 218 | + case Role::Display: |
| 219 | + return displayData(column); |
| 220 | + case Role::Value: |
| 221 | + return valueData(column); |
| 222 | + } |
| 223 | + |
| 224 | + return {}; |
| 225 | +} |
| 226 | + |
| 227 | +QVariant DetailModel::Row::displayData(Column column) const |
| 228 | +{ |
| 229 | + switch (column) { |
| 230 | + case Column::Name: |
| 231 | + return name; |
| 232 | + case Column::Value: |
| 233 | + return value.toString(); |
| 234 | + } |
| 235 | + |
| 236 | + return {}; |
| 237 | +} |
| 238 | + |
| 239 | +QVariant DetailModel::Row::valueData(Column column) const |
| 240 | +{ |
| 241 | + switch (column) { |
| 242 | + case Column::Name: |
| 243 | + return name; |
| 244 | + case Column::Value: |
| 245 | + return value; |
| 246 | + } |
| 247 | + |
| 248 | + return {}; |
| 249 | +} |
| 250 | + |
| 251 | +QVariant DetailModel::data(const QModelIndex &index, int role) const |
| 252 | +{ |
| 253 | + if (index.isValid() && checkIndex(index)) { |
| 254 | + const auto path = Path{index.internalId()}; |
| 255 | + auto rowList = m_rows; |
| 256 | + |
| 257 | + for (auto i = 0; i < path.length(); ++i) { |
| 258 | + const auto &row = rowList.at(path.at(i)); |
| 259 | + Q_ASSERT(row.hasChildren()); |
| 260 | + rowList = row.children(); |
| 261 | + } |
| 262 | + |
| 263 | + const auto &row = rowList.at(index.row()); |
| 264 | + const auto column = static_cast<Column>(index.column()); |
| 265 | + return row.data(column, static_cast<Role>(role)); |
| 266 | + } |
| 267 | + |
| 268 | + return {}; |
| 269 | +} |
| 270 | + |
| 271 | +QVariant DetailModel::headerData(int section, Qt::Orientation orientation, int role) const |
| 272 | +{ |
| 273 | + if (orientation == Qt::Horizontal && role == Qt::DisplayRole) { |
| 274 | + switch (static_cast<Column>(section)) { |
| 275 | + case Column::Name: |
| 276 | + return tr("Property"); |
| 277 | + case Column::Value: |
| 278 | + return tr("Value"); |
| 279 | + } |
| 280 | + } |
| 281 | + |
| 282 | + return {}; |
| 283 | +} |
| 284 | + |
| 285 | +QVariant DetailModel::value(const QModelIndex &index) |
| 286 | +{ |
| 287 | + return modelData<Column::Value, Role::Value>(index); |
| 288 | +} |
| 289 | + |
| 290 | +QUrl DetailModel::url(const QModelIndex &index) |
| 291 | +{ |
| 292 | + if (const auto &data = value(index); data.userType() == QMetaType::QUrl) |
| 293 | + return data.toUrl(); |
| 294 | + |
| 295 | + return {}; |
| 296 | +} |
| 297 | + |
| 298 | +bool DetailModel::validate(const RowList &rows) |
| 299 | +{ |
| 300 | + return core::validate(rows); |
| 301 | +} |
| 302 | + |
| 303 | +} // namespace qnc::core |
0 commit comments