Pokered Save Editor 2
Pokemon Red & Blue save file editor - Qt 6 C++/QML
Loading...
Searching...
No Matches
itemstoragemodel.cpp
Go to the documentation of this file.
1/*
2 * Copyright 2020 Twilight
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15*/
16
21
22#include <algorithm>
23#include <QCollator>
24#include <QDebug>
25
26#include <pse-db/itemsdb.h>
27#include "./itemstoragemodel.h"
30#include "../bridge/router.h"
31
33 : items(items),
35{
36 // Connect signals from the box
37 connect(this->items, &ItemStorageBox::itemMoveChange, this, &ItemStorageModel::onMove);
39
40 // Doesn't work, no idea why
41 //connect(this->items, &ItemStorageBox::itemInsertChange, this, &ItemStorageModel::onInsert);
42
43 // A hack because insert doesn't work as advertised, yet another millionth QML
44 // and Qt Gotcha I could spend hours, days, weeks, or months trying ti fix
46
48
49 // Clear checked on non-modal screen change. The checked state is completely
50 // temporary, it should not persist between non-modal screens
51 connect(this->router, &Router::closeNonModal, this, &ItemStorageModel::pageClosing);
52 connect(this->router, &Router::goHome, this, &ItemStorageModel::pageClosing);
53
54 connect(this->items, &ItemStorageBox::beforeItemRelocate, this, &ItemStorageModel::onBeforeRelocate);
55
56 connect(this, &ItemStorageModel::hasCheckedChanged, this, &ItemStorageModel::checkStateDirty);
57}
58
59int ItemStorageModel::rowCount(const QModelIndex& parent) const
60{
61 // Not a tree, just a list, there's no parent
62 Q_UNUSED(parent)
63
64 // Return list count
65 return items->items.size() + 1;
66}
67
68QVariant ItemStorageModel::data(const QModelIndex& index, int role) const
69{
70 // If index is invalid in any way, return nothing
71 if (!index.isValid())
72 return QVariant();
73
74 if (index.row() > items->items.size())
75 return QVariant();
76
77 if(index.row() == (items->items.size()))
78 return getPlaceHolderData(role);
79
80 // Get Item from Item List Cache
81 auto item = items->items.at(index.row());
82
83 if(item == nullptr)
84 return QVariant();
85
86 // Now return requested information
87 if (role == IdRole)
88 return item->ind;
89 else if (role == CountRole)
90 return item->amount;
91 else if (role == CheckedRole)
92 return item->property(isCheckedKey).toBool();
93 else if (role == PlaceholderRole)
94 return false;
95
96 // All else fails, return nothing
97 return QVariant();
98}
99
100QHash<int, QByteArray> ItemStorageModel::roleNames() const
101{
102 QHash<int, QByteArray> roles;
103
104 roles[IdRole] = "itemId";
105 roles[CountRole] = "itemCount";
106 roles[CheckedRole] = "itemChecked";
107 roles[PlaceholderRole] = "itemIsPlaceholder";
108
109 return roles;
110}
111
112bool ItemStorageModel::setData(const QModelIndex& index, const QVariant& value, int role)
113{
114 if(!index.isValid())
115 return false;
116
117 if(index.row() >= items->items.size())
118 return false;
119
120 auto item = items->items.at(index.row());
121
122 if(item == nullptr)
123 return false;
124
125 // Now set requested information
126 if (role == IdRole) {
127 item->ind = value.toInt();
128 dataChanged(index, index);
129 return true;
130 }
131 else if (role == CountRole) {
132 item->amount = value.toInt();
133 dataChanged(index, index);
134 return true;
135 }
136 else if (role == CheckedRole) {
137 item->setProperty(isCheckedKey, value.toBool());
138 hasCheckedChanged();
139 dataChanged(index, index);
140 return true;
141 }
142
143 return false;
144}
145
147{
148 if (role == IdRole)
149 return -1;
150 else if (role == CountRole)
151 return 0;
152 else if (role == CheckedRole)
153 return 0;
154 else if (role == PlaceholderRole)
155 return true;
156
157 return QVariant();
158}
159
160// QML ListView is the worst I've ever used in any language, so many hours and
161// days have been lost fixing ListView issues, bugs, and gotchas
162//
163// beginMoveRows is the only way to move rows and it's terminology is different
164// than other Qt terminology.
165// "to" is 1 past the item to move to, elsewhere in Qt "to" is the index to move
166// to. This creates all kinds of havoc as the two cannot properly communicate
167// and translation is very error prone leading to all my problems.
168void ItemStorageModel::onMove(int from, int to)
169{
170 // I'm convinced I'm never going to be able to remove these for the life of
171 // the entire program because I'm convinced that seeking out ListView bugs
172 // specifically and only related to beginMoveRow bugs will never end. Ever!
173 //qDebug() << "[Pre-Move] From" << from << "to" << to;
174
175 if(from == to)
176 return;
177
178 if(to > from)
179 to++;
180
181 //qDebug() << "[To-Move] From" << from << "to" << to;
182
183 beginMoveRows(QModelIndex(), from, from, QModelIndex(), to);
184 endMoveRows();
185}
186
188{
189 beginRemoveRows(QModelIndex(), ind, ind);
190 endRemoveRows();
191 hasCheckedChanged();
192}
193
195{
196 // Doesn't work, no idea why
197 beginInsertRows(QModelIndex(), items->items.size()+1, items->items.size()+1);
198 endInsertRows();
199}
200
202{
203 beginResetModel();
204 endResetModel();
205 hasCheckedChanged();
206}
207
209{
210 bool ret = false;
211
212 for(auto el : items->items) {
213 if(el->property(isCheckedKey).toBool() == true)
214 ret = true;
215 }
216
217 return ret;
218}
219
224
226{
227 QVector<Item*> ret;
228
229 for(auto el : items->items) {
230 if(el->property(isCheckedKey).toBool() == true)
231 ret.append(el);
232 }
233
234 return ret;
235}
236
238{
239 for(auto el : items->items) {
240 el->setProperty(isCheckedKey, false);
241 }
242
243 hasCheckedChanged();
244}
245
247{
248 for(int i = 0; i < items->items.size(); i++) {
249 auto el = items->items.at(i);
250 el->setProperty(isCheckedKey, false);
251 dataChanged(index(i), index(i));
252 }
253
254 hasCheckedChanged();
255}
256
258{
259 for(auto el : getChecked()) {
260 int ind = items->items.indexOf(el);
261 items->itemMove(ind, 0);
262 }
263}
264
266{
267 for(auto el : getChecked()) {
268 int ind = items->items.indexOf(el);
269 items->itemMove(ind, ind - 1);
270 }
271}
272
274{
275 auto checkedItems = getChecked();
276
277 // For stupid ListView, these need to be done in reverse
278 // If moving down
279 for(int i = checkedItems.size() - 1; i >= 0; i--) {
280 auto el = checkedItems.at(i);
281 int ind = items->items.indexOf(el);
282 items->itemMove(ind, ind + 1);
283 }
284}
285
287{
288 auto checkedItems = getChecked();
289
290 // For stupid ListView, these need to be done in reverse
291 // If moving down
292 for(int i = checkedItems.size() - 1; i >= 0; i--) {
293 auto el = checkedItems.at(i);
294 int ind = items->items.indexOf(el);
295 items->itemMove(ind, items->items.size() - 1);
296 }
297}
298
300{
301 for(auto el : getChecked()) {
302 int ind = items->items.indexOf(el);
303 items->itemRemove(ind);
304 }
305}
306
308{
309 for(auto el : getChecked()) {
310 int ind = items->items.indexOf(el);
311 items->relocateOne(ind);
312 }
313
314 hasCheckedChanged();
315}
316
318{
319 if(items->items.size() == 0)
320 return;
321
322 bool allValue = !items->items.at(0)->property(isCheckedKey).toBool();
323
324 for(int i = 0; i < items->items.size(); i++) {
325 auto el = items->items.at(i);
326 el->setProperty(isCheckedKey, allValue);
327 }
328
329 // As far as I can tell there is no way to force a ListView row to be
330 // re-created. This means dataChanged is completely and utterly useless. I've
331 // tried all kinds of tricks, I've searched Google for hours, I've even tried
332 // a hack where I remove and re-insert all rows to force the rows to be
333 // re-created. I'm left with no other option but to completely destroy the
334 // model and re-create it.
335 //
336 // The issue stems from another Qt gotcha. Models are expected to change only
337 // from their delegates, they are never expected to externally change and as
338 // such any external changes have no way of being reflected in the model
339 // without a reset.
340 beginResetModel();
341 endResetModel();
342
343 hasCheckedChanged();
344}
345
347{
348 item->setProperty(isCheckedKey, false);
349 hasCheckedChanged();
350}
351
353{
354 bool _val = hasChecked();
355
356 if(checkedStateDirty == _val)
357 return;
358
360 hasCheckedChangedCached();
361}
362
368
369void ItemStorageModel::dragReorder(int fromIndex, int toIndex, bool group)
370{
371 auto& vec = items->items;
372
373 // The items to move, gathered in current box order. A group drag carries the
374 // whole checked set; a plain drag carries just the grabbed item.
375 QVector<Item*> set;
376 if(group)
377 set = getChecked();
378 else if(fromIndex >= 0 && fromIndex < vec.size())
379 set.append(vec.at(fromIndex));
380
381 if(set.isEmpty())
382 return;
383
384 // Anchor = the first item at/after the drop slot that ISN'T being moved; the
385 // set is re-inserted directly before it (or appended when there is none, e.g.
386 // dropping past the last item / onto the empty trailing "+" slot).
387 Item* anchor = nullptr;
388 for(int i = qBound(0, toIndex, vec.size()); i < vec.size(); i++) {
389 if(!set.contains(vec.at(i))) {
390 anchor = vec.at(i);
391 break;
392 }
393 }
394
395 // Pull the set out, then splice it back in before the anchor, preserving the
396 // set's internal order.
397 for(auto el : set)
398 vec.removeOne(el);
399
400 int insertAt = (anchor != nullptr) ? vec.indexOf(anchor) : vec.size();
401 for(int i = 0; i < set.size(); i++)
402 vec.insert(insertAt + i, set.at(i));
403
404 // A whole-vector reshuffle: a model reset is the clean, reliable refresh and
405 // sidesteps the beginMoveRows index gymnastics (mirrors PokemonStorageModel's
406 // dragReorder and checkedToggleAll). Count is unchanged.
407 onReset();
408}
409
410void ItemStorageModel::dragTransfer(int fromIndex, int toIndex, bool group)
411{
412 // Never transfer onto ourselves (both panes are the bag + PC, never the same
413 // box, but guard anyway).
414 if(otherModel == nullptr || items == otherModel->items)
415 return;
416
417 auto src = items;
418 auto dst = items->destBox();
419
420 // The items to move, in current (source) box order -- checked set or single.
421 QVector<Item*> set;
422 if(group)
423 set = getChecked();
424 else if(fromIndex >= 0 && fromIndex < src->items.size())
425 set.append(src->items.at(fromIndex));
426
427 if(set.isEmpty())
428 return;
429
430 int inserted = 0; // how many NEW rows were appended to dst (for the slide)
431 bool dstStacked = false; // did we merge into an existing dst stack?
432
433 for(auto el : set) {
434
435 int ind = src->items.indexOf(el);
436 if(ind < 0)
437 continue;
438
439 // Auto-stack: if the destination already holds this item, merge the moved
440 // amount onto the existing stack instead of creating a duplicate row. We
441 // stack onto the LAST matching row (Twilight's rule -- if there are e.g. four
442 // Antidotes, the bottom one is the stack target). Pre-existing duplicates are
443 // left untouched; only the moved item folds in.
444 Item* stackTarget = nullptr;
445 for(int i = dst->items.size() - 1; i >= 0; i--) {
446 if(dst->items.at(i)->ind == el->ind) {
447 stackTarget = dst->items.at(i);
448 break;
449 }
450 }
451
452 // Merge ONLY when the whole amount fits under the Gen 1 max (99). We never
453 // clamp a stack and drop the excess -- losing legitimate items is bad UX.
454 // If it would overflow, the item falls through to the new-row path below
455 // (a 2nd row), and if even that has no room the transfer is refused (the
456 // item stays put), never silently truncated.
457 if(stackTarget != nullptr && stackTarget->amount + el->amount <= 99) {
458 stackTarget->setAmount(stackTarget->amount + el->amount);
459 src->itemRemove(ind); // emits itemRemoveChange -> this model's onRemove; deletes el
460 dstStacked = true;
461 continue;
462 }
463
464 // No (fitting) stack target -> move as a new row (capacity-guarded). This
465 // covers both "item not present in dst" and "present but the stack would
466 // overflow 99" (the overflow becomes its own 2nd row, lossless). Use
467 // `continue`, not `break`: when dst is row-count full we REFUSE this item but
468 // a later item in the set might still fully-merge into an existing dst stack.
469 if(dst->items.size() >= dst->itemsMax())
470 continue;
471
472 // relocateOne appends to dst's end and emits the remove/insert signals that
473 // refresh both panes' models (items need no party/box-style conversion). The
474 // moved item keeps its exact amount -- no clamp, no loss.
475 src->relocateOne(ind);
476 inserted++;
477 }
478
479 // The newly-added (non-stacked) items are the last `inserted` slots of dst.
480 // Slide that block to the requested drop slot, clamped to the items that were
481 // already there so we insert *among* them (never past the freshly-appended
482 // block). Stacked items don't move a row, so they don't participate here.
483 if(inserted > 0) {
484 int firstAppended = dst->items.size() - inserted;
485 int target = qBound(0, toIndex, firstAppended);
486
487 if(target != firstAppended) {
488 QVector<Item*> moved = dst->items.mid(firstAppended, inserted);
489 dst->items.remove(firstAppended, inserted);
490 for(int i = 0; i < moved.size(); i++)
491 dst->items.insert(target + i, moved.at(i));
492 }
493 }
494
495 // One dst refresh covers both the slide reorder and any stacked-count display
496 // change (a stack updates an Item amount but emits no row insert, so the dst
497 // model needs a reset to re-read it). relocateOne already reset the dst model
498 // per new row; an extra reset is idempotent and cheap.
499 if(inserted > 0 || dstStacked)
500 otherModel->onReset();
501
502 hasCheckedChanged();
503}
504
505void ItemStorageModel::deleteItem(int index, bool group)
506{
507 // A group delete (the item is checked) removes the whole checked set -- this
508 // is the replacement for the old footer delete bulk button.
509 if(group) {
511 return;
512 }
513
514 // Single delete (hovering an unchecked item). itemRemove emits
515 // itemRemoveChange -> onRemove (begin/endRemoveRows). An item box has no
516 // minimum, so no "never empties" guard is needed.
517 if(index < 0 || index >= items->items.size())
518 return;
519
520 items->itemRemove(index);
521}
A container of Items – either the trainer's bag or a PC item box.
void itemsResetChange()
The box was reset.
void itemInsertChange()
An item was inserted.
void itemMoveChange(int from, int to)
An item moved slot.
void itemRemoveChange(int ind)
An item was removed.
static constexpr const char * isCheckedKey
QML attached-property name for the per-row checkbox.
void onReset()
React to a box reset.
bool setData(const QModelIndex &index, const QVariant &value, int role=Qt::EditRole) override
Edit a row (e.g. checkbox).
void clearCheckedStateGone()
Clear checked state for removed rows.
bool checkedStateDirty
Whether the checked-state cache needs refresh.
void onMove(int from, int to)
React to a box move.
QVector< Item * > getChecked()
The currently-checked items.
virtual int rowCount(const QModelIndex &parent) const override
Row count.
void checkedMoveToBottom()
Move checked rows to the bottom.
virtual QVariant data(const QModelIndex &index, int role) const override
Row+role value.
void clearCheckedState()
Uncheck everything.
void checkedTransfer()
Transfer checked rows to the paired box.
ItemStorageBox * items
The wrapped item box.
void onBeforeRelocate(Item *item)
Cleanup hook before an item relocates away.
ItemStorageModel(ItemStorageBox *items, Router *router)
bool hasChecked()
Are any rows checked (live)?
void checkedMoveToTop()
Move checked rows to the top.
void pageClosing()
Hook for when the page closes.
QVariant getPlaceHolderData(int role) const
The "empty slot" placeholder row's data.
void deleteItem(int index, bool group)
Delete the item at index, or – when group – the whole checked set (the per-row hover/checked delete b...
void onRemove(int ind)
React to a box removal.
void dragReorder(int fromIndex, int toIndex, bool group)
Reorder within this box: move fromIndex (or the checked set) to toIndex.
void checkedToggleAll()
Toggle all checkboxes.
bool hasCheckedCached()
Cached form (backs the property).
void checkedMoveUp()
Move checked rows up one.
virtual QHash< int, QByteArray > roleNames() const override
Role -> QML name.
void onInsert()
React to a box insert.
void checkedDelete()
Delete checked rows.
void dragTransfer(int fromIndex, int toIndex, bool group)
Move fromIndex (or the checked set) from this box into the paired box, inserting at toIndex there.
Router * router
For page hooks.
ItemStorageModel * otherModel
The paired sibling model (for cross-pane transfers).
void checkedMoveDown()
Move checked rows down one.
void checkStateDirty()
Mark the checked-state cache stale.
One inventory slot: an item index and an amount, with live pricing.
Definition item.h:36
int amount
Item amount (max 99 in Gen 1; backs property).
Definition item.h:109
void setAmount(int val)
Set amount (backs property WRITE; clamped to the Gen 1 max).
Definition item.cpp:179
Screen navigation for the UI – the QML StackView's controller.
Definition router.h:74