Pokered Save Editor 2
Pokemon Red & Blue save file editor - Qt 6 C++/QML
Loading...
Searching...
No Matches
pokemonstoragemodel.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#include <QQmlEngine>
26
28#include "../bridge/router.h"
30#include <pse-db/pokemon.h>
38
43 : router(router),
46{
47 connect(this->router, &Router::closeNonModal, this, &PokemonStorageModel::pageClosing);
48 connect(this->router, &Router::goHome, this, &PokemonStorageModel::pageClosing);
49 connect(this, &PokemonStorageModel::hasCheckedChanged, this, &PokemonStorageModel::checkStateDirty);
50 connect(this, &PokemonStorageModel::curBoxChanged, this, &PokemonStorageModel::onReset);
51
52 // Do an initial setup
53 switchBox(curBox, true);
54}
55
56void PokemonStorageModel::switchBox(int newBox, bool force)
57{
58 // Do nothing if it's the same box
59 if(!force && curBox == newBox)
60 return;
61
62 // Clear the OUTGOING box's checks: changing boxes should NOT keep the selection
63 // (Twilight's UX rule -- checks only persist across the Pokemon-detail editor
64 // round-trip, handled by Pokemon.qml's Component.onDestruction, not here).
67
68 // Get old current box
69 auto box = getCurBox();
70
71 // Disconnect from it
76 disconnect(box, &PokemonStorageBox::beforePokemonRelocate, this, &PokemonStorageModel::onBeforeRelocate);
77
78 // Change boxes and retrieve new box
79 curBox = newBox;
80 box = getCurBox();
81
82 // Connect to it
87 connect(box, &PokemonStorageBox::beforePokemonRelocate, this, &PokemonStorageModel::onBeforeRelocate);
88
89 // Announce change
90 curBoxChanged();
91}
92
97
99{
100 if(box == PartyBox)
101 return party;
102 else
103 return storage->boxAt(box);
104}
105
106int PokemonStorageModel::rowCount(const QModelIndex& parent) const
107{
108 // Not a tree, just a list, there's no parent
109 Q_UNUSED(parent)
110
111 // Return list count
112 return (getCurBox()->isFull())
113 ? getCurBox()->pokemon.size()
114 : getCurBox()->pokemon.size() + 1;
115}
116
117QVariant PokemonStorageModel::data(const QModelIndex& index, int role) const
118{
119 // If index is invalid in any way, return nothing
120 if (!index.isValid())
121 return QVariant();
122
123 if (index.row() > getCurBox()->pokemon.size())
124 return QVariant();
125
126 if(index.row() == (getCurBox()->pokemon.size()))
127 return getPlaceHolderData(role);
128
129 // Get Item from Item List Cache
130 auto mon = getCurBox()->pokemon.at(index.row());
131 auto monData = mon->toData();
132
133 // Even for glitch Pokemon, there should still be a data entry
134 if(mon == nullptr || monData == nullptr)
135 return QVariant();
136
137 // Now return requested information
138 if (role == IndRole)
139 return monData->ind;
140 else if (role == DexRole)
141 return (monData->pokedex) ? *monData->pokedex : -1;
142 else if (role == NameRole)
143 return monData->readable;
144 else if (role == CheckedRole)
145 return mon->property(isCheckedKey).toBool();
146 else if (role == PlaceholderRole)
147 return false;
148 else if (role == NicknameRole)
149 return mon->nickname;
150 else if (role == LevelRole)
151 return mon->level;
152 else if (role == IsShinyRole)
153 return mon->isShiny();
154 else if (role == IsPartyRole)
155 return !mon->isBoxMon();
156 else if (role == HpRole)
157 return mon->hp;
158 else if (role == HpMaxRole)
159 return mon->hpStat();
160 else if (role == StatusRole)
161 return mon->status;
162
163 // All else fails, return nothing
164 return QVariant();
165}
166
167QHash<int, QByteArray> PokemonStorageModel::roleNames() const
168{
169 QHash<int, QByteArray> roles;
170
171 roles[IndRole] = "itemInd";
172 roles[DexRole] = "itemDex";
173 roles[NameRole] = "itemName";
174 roles[CheckedRole] = "itemChecked";
175 roles[PlaceholderRole] = "itemIsPlaceholder";
176 roles[NicknameRole] = "itemNickname";
177 roles[LevelRole] = "itemLevel";
178 roles[IsShinyRole] = "itemIsShiny";
179 roles[IsPartyRole] = "itemIsParty";
180 roles[HpRole] = "itemHp";
181 roles[HpMaxRole] = "itemHpMax";
182 roles[StatusRole] = "itemStatus";
183
184 return roles;
185}
186
187bool PokemonStorageModel::setData(const QModelIndex& index, const QVariant& value, int role)
188{
189 if(!index.isValid())
190 return false;
191
192 if(index.row() >= getCurBox()->pokemon.size())
193 return false;
194
195 auto mon = getCurBox()->pokemon.at(index.row());
196
197 if(mon == nullptr)
198 return false;
199
200 // Now set requested information
201 if(role == CheckedRole) {
202 mon->setProperty(isCheckedKey, value.toBool());
203 hasCheckedChanged();
204 dataChanged(index, index);
205 return true;
206 }
207
208 return false;
209}
210
212{
213 if (role == IndRole)
214 return -1;
215 else if (role == DexRole)
216 return -1;
217 else if (role == NameRole)
218 return "";
219 else if (role == CheckedRole)
220 return false;
221 else if (role == PlaceholderRole)
222 return true;
223
224 return QVariant();
225}
226
227// QML ListView is the worst I've ever used in any language, so many hours and
228// days have been lost fixing ListView issues, bugs, and gotchas
229//
230// beginMoveRows is the only way to move rows and it's terminology is different
231// than other Qt terminology.
232// "to" is 1 past the item to move to, elsewhere in Qt "to" is the index to move
233// to. This creates all kinds of havoc as the two cannot properly communicate
234// and translation is very error prone leading to all my problems.
235void PokemonStorageModel::onMove(int from, int to)
236{
237 // I'm convinced I'm never going to be able to remove these for the life of
238 // the entire program because I'm convinced that seeking out ListView bugs
239 // specifically and only related to beginMoveRow bugs will never end. Ever!
240 //qDebug() << "[Pre-Move] From" << from << "to" << to;
241
242 if(from == to)
243 return;
244
245 if(to > from)
246 to++;
247
248 //qDebug() << "[To-Move] From" << from << "to" << to;
249
250 beginMoveRows(QModelIndex(), from, from, QModelIndex(), to);
251 endMoveRows();
252}
253
255{
256 beginRemoveRows(QModelIndex(), ind, ind);
257 endRemoveRows();
258 hasCheckedChanged();
259}
260
262{
263 // Doesn't work, no idea why
264 beginInsertRows(QModelIndex(), getCurBox()->pokemon.size()+1, getCurBox()->pokemon.size()+1);
265 endInsertRows();
266}
267
269{
270 beginResetModel();
271 endResetModel();
272 hasCheckedChanged();
273}
274
276{
277 bool ret = false;
278
279 for(auto el : getCurBox()->pokemon) {
280 if(el->property(isCheckedKey).toBool() == true)
281 ret = true;
282 }
283
284 return ret;
285}
286
291
293{
294 QVector<PokemonBox*> ret;
295
296 for(auto el : getCurBox()->pokemon) {
297 if(el->property(isCheckedKey).toBool() == true)
298 ret.append(el);
299 }
300
301 return ret;
302}
303
305{
306 for(auto el : getCurBox()->pokemon) {
307 el->setProperty(isCheckedKey, false);
308 }
309
310 hasCheckedChanged();
311}
312
314{
315 for(int i = 0; i < getCurBox()->pokemon.size(); i++) {
316 auto el = getCurBox()->pokemon.at(i);
317 el->setProperty(isCheckedKey, false);
318 dataChanged(index(i), index(i));
319 }
320
321 hasCheckedChanged();
322}
323
325{
326 for(auto el : getChecked()) {
327 int ind = getCurBox()->pokemon.indexOf(el);
328 getCurBox()->pokemonMove(ind, 0);
329 }
330}
331
333{
334 for(auto el : getChecked()) {
335 int ind = getCurBox()->pokemon.indexOf(el);
336 getCurBox()->pokemonMove(ind, ind - 1);
337 }
338}
339
341{
342 auto checkedItems = getChecked();
343
344 // For stupid ListView, these need to be done in reverse
345 // If moving down
346 for(int i = checkedItems.size() - 1; i >= 0; i--) {
347 auto el = checkedItems.at(i);
348 int ind = getCurBox()->pokemon.indexOf(el);
349 getCurBox()->pokemonMove(ind, ind + 1);
350 }
351}
352
354{
355 auto checkedItems = getChecked();
356
357 // For stupid ListView, these need to be done in reverse
358 // If moving down
359 for(int i = checkedItems.size() - 1; i >= 0; i--) {
360 auto el = checkedItems.at(i);
361 int ind = getCurBox()->pokemon.indexOf(el);
362 getCurBox()->pokemonMove(ind, getCurBox()->pokemon.size() - 1);
363 }
364}
365
367{
368 bool annPlaceholder = getCurBox()->pokemonCount() == getCurBox()->pokemonMax();
369
370 for(auto el : getChecked()) {
371
372 // Stop if the party is down to one Pokemon
373 if(getCurBox() == party && party->pokemonCount() <= 1)
374 break;
375
376 int ind = getCurBox()->pokemon.indexOf(el);
377 getCurBox()->pokemonRemove(ind);
378 }
379
380 // Announce the opening of the placeholder
381 if(annPlaceholder) {
382 beginInsertRows(QModelIndex(), getCurBox()->pokemonCount(), getCurBox()->pokemonCount());
383 endInsertRows();
384 }
385}
386
388{
389 // Stop if both models refer to the same box, this is not suppose to happen
390 if(getCurBox() == otherModel->getCurBox())
391 return;
392
393 for(auto el : getChecked()) {
394
395 // We don't want to convert to and from data structures if we can't move
396 // the pokemon. Perform checks to make sure it's possible.
397
398 // Stop if the party is down to one Pokemon
399 if(getCurBox() == party && party->pokemonCount() <= 1)
400 break;
401
402 // Stop here if the other side is full
403 if(otherModel->getCurBox()->isFull())
404 break;
405
406 // Get index
407 int ind = getCurBox()->pokemon.indexOf(el);
408
409 // If we're converting to a PokemonParty we auto-destroy the box pokemon and
410 // produce the same Pokemon with the PokemonParty extensions. If the other
411 // way around, we can't auto-destroy it so we manually do the conversion and
412 // destroying
413 // We also have to replace the newly converted Pokemon in the array before
414 // relocating it
415 if(otherModel->getCurBox()->isParty) {
417 getCurBox()->pokemon.replace(ind, el);
418 }
419 else if(!otherModel->getCurBox()->isParty && getCurBox()->isParty) {
420 auto partyEl = (PokemonParty*)el;
421 el = partyEl->toBoxData();
422 partyEl->deleteLater();
423 getCurBox()->pokemon.replace(ind, el);
424 }
425
426 // Now do the relocate
427 getCurBox()->relocateOne(otherModel->getCurBox(), ind);
428 }
429
430 hasCheckedChanged();
431}
432
434{
435 if(getCurBox()->pokemon.size() == 0)
436 return;
437
438 bool allValue = !getCurBox()->pokemon.at(0)->property(isCheckedKey).toBool();
439
440 for(int i = 0; i < getCurBox()->pokemon.size(); i++) {
441 auto el = getCurBox()->pokemon.at(i);
442 el->setProperty(isCheckedKey, allValue);
443 }
444
445 // As far as I can tell there is no way to force a ListView row to be
446 // re-created. This means dataChanged is completely and utterly useless. I've
447 // tried all kinds of tricks, I've searched Google for hours, I've even tried
448 // a hack where I remove and re-insert all rows to force the rows to be
449 // re-created. I'm left with no other option but to completely destroy the
450 // model and re-create it.
451 //
452 // The issue stems from another Qt gotcha. Models are expected to change only
453 // from their delegates, they are never expected to externally change and as
454 // such any external changes have no way of being reflected in the model
455 // without a reset.
456 beginResetModel();
457 endResetModel();
458
459 hasCheckedChanged();
460}
461
463{
464 mon->setProperty(isCheckedKey, false);
465 hasCheckedChanged();
466}
467
469{
470 bool _val = hasChecked();
471
472 if(checkedStateDirty == _val)
473 return;
474
475 checkedStateDirty = _val;
476 hasCheckedChangedCached();
477}
478
480{
481 // Intentionally does NOT clear the checked state. This fires on
482 // router.closeNonModal (which includes closing a mon's *detail editor* and
483 // returning to the storage screen) and on goHome -- clearing here wiped every
484 // checkbox the moment the user opened a mon and came back, which read as
485 // "checkboxes lose their selection". Checked state is per-mon and now persists
486 // for the file's lifetime (Twilight's call); transfers/deletes still clear the
487 // specific mons they touch. Kept as a hook in case a future close action needs it.
488}
489
491{
492 // qmlCppOwned: these mons are owned by the storage box in C++ for its
493 // lifetime. Without this the parentless Q_INVOKABLE return defaults to
494 // JavaScriptOwnership, so QML GCs the mon when the details editor closes --
495 // leaving a dangling pointer in getCurBox()->pokemon that hasChecked()
496 // dereferences on the next onReset() -> use-after-free crash. (Opening the
497 // editor and leaving reproduced it.) See qt6-patterns.md / qmlownership.h.
498 return qmlCppOwned(getCurBox()->pokemon.at(index));
499}
500
502{
503 auto mon = getCurBox()->pokemon.at(index);
504 if(mon->isBoxMon())
505 return nullptr;
506
507 // qmlCppOwned: see getBoxMon() above -- same QML-GC use-after-free guard.
508 return qmlCppOwned((PokemonParty*)mon);
509}
510
511void PokemonStorageModel::dragReorder(int fromIndex, int toIndex, bool group)
512{
513 auto& vec = getCurBox()->pokemon;
514
515 // The mons to move, gathered in current box order. A group drag carries the
516 // whole checked set; a plain drag carries just the grabbed mon.
517 QVector<PokemonBox*> set;
518 if(group)
519 set = getChecked();
520 else if(fromIndex >= 0 && fromIndex < vec.size())
521 set.append(vec.at(fromIndex));
522
523 if(set.isEmpty())
524 return;
525
526 // Anchor = the first mon at/after the drop slot that ISN'T being moved; the
527 // set is re-inserted directly before it (or appended when there is none, e.g.
528 // dropping past the last mon / onto the empty trailing slot).
529 PokemonBox* anchor = nullptr;
530 for(int i = qBound(0, toIndex, vec.size()); i < vec.size(); i++) {
531 if(!set.contains(vec.at(i))) {
532 anchor = vec.at(i);
533 break;
534 }
535 }
536
537 // Pull the set out, then splice it back in before the anchor, preserving the
538 // set's internal order.
539 for(auto el : set)
540 vec.removeOne(el);
541
542 int insertAt = (anchor != nullptr) ? vec.indexOf(anchor) : vec.size();
543 for(int i = 0; i < set.size(); i++)
544 vec.insert(insertAt + i, set.at(i));
545
546 // A whole-vector reshuffle: a model reset is the clean, reliable refresh and
547 // sidesteps the beginMoveRows index gymnastics (count is unchanged, so the
548 // box-selector counts -- the only pokemonChanged listener -- need no update).
549 // Mirrors checkedToggleAll's external-change reset.
550 onReset();
551}
552
553void PokemonStorageModel::dragTransfer(int fromIndex, int toIndex, bool group)
554{
555 // Never transfer onto ourselves (both panes showing the same box).
556 if(otherModel == nullptr || getCurBox() == otherModel->getCurBox())
557 return;
558
559 auto src = getCurBox();
560 auto dst = otherModel->getCurBox();
561
562 // The mons to move, in current (source) box order -- checked set or single.
563 QVector<PokemonBox*> set;
564 if(group)
565 set = getChecked();
566 else if(fromIndex >= 0 && fromIndex < src->pokemon.size())
567 set.append(src->pokemon.at(fromIndex));
568
569 if(set.isEmpty())
570 return;
571
572 int inserted = 0;
573
574 for(auto el : set) {
575
576 // Same guards as checkedTransfer: keep the party non-empty, never overflow
577 // the destination.
578 if(src == party && party->pokemonCount() <= 1)
579 break;
580
581 if(dst->isFull())
582 break;
583
584 int ind = src->pokemon.indexOf(el);
585 if(ind < 0)
586 continue;
587
588 // Party and box records aren't interchangeable on disk -- convert to the
589 // destination's format before relocating (mirrors checkedTransfer exactly).
590 if(dst->isParty) {
592 src->pokemon.replace(ind, el);
593 }
594 else if(!dst->isParty && src->isParty) {
595 auto partyEl = (PokemonParty*)el;
596 el = partyEl->toBoxData();
597 partyEl->deleteLater();
598 src->pokemon.replace(ind, el);
599 }
600
601 // relocateOne appends to dst's end and emits the remove/insert signals that
602 // refresh both panes' models.
603 src->relocateOne(dst, ind);
604 inserted++;
605 }
606
607 // The transferred mons are now the last `inserted` slots of dst. Slide that
608 // block to the requested drop slot, clamped to the mons that were already
609 // there so we insert *among* them (never past the freshly-appended block).
610 if(inserted > 0) {
611 int firstAppended = dst->pokemon.size() - inserted;
612 int target = qBound(0, toIndex, firstAppended);
613
614 if(target != firstAppended) {
615 QVector<PokemonBox*> moved = dst->pokemon.mid(firstAppended, inserted);
616 dst->pokemon.remove(firstAppended, inserted);
617 for(int i = 0; i < moved.size(); i++)
618 dst->pokemon.insert(target + i, moved.at(i));
619
620 otherModel->onReset();
621 }
622 }
623
624 hasCheckedChanged();
625}
626
627void PokemonStorageModel::deleteMon(int index, bool group)
628{
629 // A group delete (the mon is checked) removes the whole checked set -- this is
630 // the replacement for the old footer "release" bulk button.
631 if(group) {
633 return;
634 }
635
636 // Single delete (hovering an unchecked mon).
637 auto box = getCurBox();
638
639 if(index < 0 || index >= box->pokemon.size())
640 return;
641
642 // Keep the party non-empty (same guard as checkedDelete).
643 if(box == party && party->pokemonCount() <= 1)
644 return;
645
646 // If the box was full it had no trailing "+" placeholder row; removing one mon
647 // opens it, so announce that new row (mirrors checkedDelete).
648 bool annPlaceholder = box->pokemonCount() == box->pokemonMax();
649
650 box->pokemonRemove(index); // emits pokemonRemoveChange -> onRemove (begin/endRemoveRows)
651
652 if(annPlaceholder) {
653 beginInsertRows(QModelIndex(), box->pokemonCount(), box->pokemonCount());
654 endInsertRows();
655 }
656}
The player's active party – a specialized PokemonStorageBox.
A single Pokemon record – the most property-rich object in the tree.
Definition pokemonbox.h:213
A party Pokemon: a PokemonBox plus the five pre-generated battle stats.
static PokemonParty * convertToParty(PokemonBox *data)
New party mon from a box record (regenerates stats).
Holds contents of a single Pokemon storage box.
int pokemonMax()
Capacity (maxSize).
void pokemonInsertChange()
A mon was inserted.
virtual bool relocateOne(PokemonStorageBox *dst, int ind)
Move one mon into dst.
QVector< PokemonBox * > pokemon
The stored mons.
void pokemonResetChange()
The box was reset.
void pokemonMoveChange(int from, int to)
A mon moved slot.
bool pokemonMove(int from, int to)
Reorder a mon within the box.
int pokemonCount()
Number of mons present.
void pokemonRemove(int ind)
Remove the mon at ind.
void pokemonRemoveChange(int ind)
A mon was removed.
PokemonParty * getPartyMon(int index)
Typed party mon at index (for the details screen).
void checkStateDirty()
Mark the checked-state cache stale.
QVariant getPlaceHolderData(int role) const
The empty-slot placeholder row data.
virtual QVariant data(const QModelIndex &index, int role) const override
Row+role value.
bool setData(const QModelIndex &index, const QVariant &value, int role=Qt::EditRole) override
Edit a row (e.g. checkbox).
Storage * storage
The PC storage.
void dragReorder(int fromIndex, int toIndex, bool group)
Reorder within this box: move fromIndex (or the checked set) to toIndex.
@ HpMaxRole
Computed max HP (hpStat).
@ HpRole
Current HP (for the storage-grid health bar).
@ StatusRole
Raw status byte (0 none; 1-7 sleep; 8 poison; 16 burn; 32 freeze; 64 paralyze).
PokemonStorageBox * getBox(int box) const
The box object for index box.
void checkedDelete()
Delete checked mons.
void onMove(int from, int to)
React to a move.
PokemonStorageModel(Router *router, Storage *storage, PlayerPokemon *party)
Router * router
For page hooks.
static constexpr const char * isCheckedKey
QML attached-property name for the per-row checkbox.
virtual int rowCount(const QModelIndex &parent) const override
Row count of the current box.
void deleteMon(int index, bool group)
Delete the mon at index, or – when group – the whole checked set (the per-cell hover/checked delete b...
void checkedMoveToBottom()
Move checked mons to the bottom.
PokemonStorageModel * otherModel
The paired sibling model (for transfers).
void onInsert()
React to an insert.
virtual QHash< int, QByteArray > roleNames() const override
Role -> QML name.
bool hasCheckedCached()
Cached form (backs the property).
QVector< PokemonBox * > getChecked()
The currently-checked mons.
void onReset()
React to a reset.
void checkedMoveDown()
Move checked mons down one.
void clearCheckedState()
Uncheck everything.
bool checkedStateDirty
Whether the checked-state cache needs refresh.
PokemonBox * getBoxMon(int index)
Typed box mon at index (for the details screen).
void checkedTransfer()
Transfer checked mons to the paired box.
void pageClosing()
Hook for when the page closes.
void onRemove(int ind)
React to a removal.
void clearCheckedStateGone()
Clear checked state for removed rows.
void checkedToggleAll()
Toggle all checkboxes.
void switchBox(int newBox, bool force=false)
Show box newBox.
void checkedMoveUp()
Move checked mons up one.
bool hasChecked()
Are any rows checked (live)?
void onBeforeRelocate(PokemonBox *item)
Cleanup hook before a mon relocates away.
PlayerPokemon * party
The party.
PokemonStorageBox * getCurBox() const
The currently-shown box object.
void checkedMoveToTop()
Move checked mons to the top.
void dragTransfer(int fromIndex, int toIndex, bool group)
Move fromIndex (or the checked set) from this box into the paired pane's box, inserting at toIndex th...
Screen navigation for the UI – the QML StackView's controller.
Definition router.h:74
The PC: the item storage box and all 12 Pokemon boxes.
Definition storage.h:49
qmlCppOwned() – protect Q_INVOKABLE QObject returns from QML's GC.
static T * qmlCppOwned(T *obj)
Hand QML CppOwnership of a C++-owned QObject returned from a Q_INVOKABLE.