cheatmanagerwindow.cpp (16913B)
1 // SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin <stenzek@gmail.com> and contributors. 2 // SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0) 3 4 #include "cheatmanagerwindow.h" 5 #include "cheatcodeeditordialog.h" 6 #include "mainwindow.h" 7 #include "qthost.h" 8 #include "qtutils.h" 9 10 #include "core/bus.h" 11 #include "core/cpu_core.h" 12 #include "core/host.h" 13 #include "core/system.h" 14 15 #include "common/assert.h" 16 #include "common/string_util.h" 17 18 #include <QtCore/QFileInfo> 19 #include <QtGui/QColor> 20 #include <QtWidgets/QFileDialog> 21 #include <QtWidgets/QInputDialog> 22 #include <QtWidgets/QMenu> 23 #include <QtWidgets/QMessageBox> 24 #include <QtWidgets/QTreeWidgetItemIterator> 25 #include <array> 26 #include <utility> 27 28 CheatManagerWindow::CheatManagerWindow() : QWidget() 29 { 30 m_ui.setupUi(this); 31 32 connectUi(); 33 34 updateCheatList(); 35 } 36 37 CheatManagerWindow::~CheatManagerWindow() = default; 38 39 void CheatManagerWindow::connectUi() 40 { 41 connect(m_ui.cheatList, &QTreeWidget::currentItemChanged, this, &CheatManagerWindow::cheatListCurrentItemChanged); 42 connect(m_ui.cheatList, &QTreeWidget::itemActivated, this, &CheatManagerWindow::cheatListItemActivated); 43 connect(m_ui.cheatList, &QTreeWidget::itemChanged, this, &CheatManagerWindow::cheatListItemChanged); 44 connect(m_ui.cheatListNewCategory, &QPushButton::clicked, this, &CheatManagerWindow::newCategoryClicked); 45 connect(m_ui.cheatListAdd, &QPushButton::clicked, this, &CheatManagerWindow::addCodeClicked); 46 connect(m_ui.cheatListEdit, &QPushButton::clicked, this, &CheatManagerWindow::editCodeClicked); 47 connect(m_ui.cheatListRemove, &QPushButton::clicked, this, &CheatManagerWindow::deleteCodeClicked); 48 connect(m_ui.cheatListActivate, &QPushButton::clicked, this, &CheatManagerWindow::activateCodeClicked); 49 connect(m_ui.cheatListImport, &QPushButton::clicked, this, &CheatManagerWindow::importClicked); 50 connect(m_ui.cheatListExport, &QPushButton::clicked, this, &CheatManagerWindow::exportClicked); 51 connect(m_ui.cheatListClear, &QPushButton::clicked, this, &CheatManagerWindow::clearClicked); 52 connect(m_ui.cheatListReset, &QPushButton::clicked, this, &CheatManagerWindow::resetClicked); 53 54 connect(g_emu_thread, &EmuThread::cheatEnabled, this, &CheatManagerWindow::setCheatCheckState); 55 connect(g_emu_thread, &EmuThread::runningGameChanged, this, &CheatManagerWindow::updateCheatList); 56 } 57 58 void CheatManagerWindow::showEvent(QShowEvent* event) 59 { 60 QWidget::showEvent(event); 61 resizeColumns(); 62 } 63 64 void CheatManagerWindow::closeEvent(QCloseEvent* event) 65 { 66 QWidget::closeEvent(event); 67 emit closed(); 68 } 69 70 void CheatManagerWindow::resizeEvent(QResizeEvent* event) 71 { 72 QWidget::resizeEvent(event); 73 resizeColumns(); 74 } 75 76 void CheatManagerWindow::resizeColumns() 77 { 78 QtUtils::ResizeColumnsForTreeView(m_ui.cheatList, {-1, 100, 150, 100}); 79 } 80 81 QTreeWidgetItem* CheatManagerWindow::getItemForCheatIndex(u32 index) const 82 { 83 QTreeWidgetItemIterator iter(m_ui.cheatList); 84 while (*iter) 85 { 86 QTreeWidgetItem* item = *iter; 87 const QVariant item_data(item->data(0, Qt::UserRole)); 88 if (item_data.isValid() && item_data.toUInt() == index) 89 return item; 90 91 ++iter; 92 } 93 94 return nullptr; 95 } 96 97 QTreeWidgetItem* CheatManagerWindow::getItemForCheatGroup(const QString& group_name) const 98 { 99 const int count = m_ui.cheatList->topLevelItemCount(); 100 for (int i = 0; i < count; i++) 101 { 102 QTreeWidgetItem* item = m_ui.cheatList->topLevelItem(i); 103 if (item->text(0) == group_name) 104 return item; 105 } 106 107 return nullptr; 108 } 109 110 QTreeWidgetItem* CheatManagerWindow::createItemForCheatGroup(const QString& group_name) const 111 { 112 QTreeWidgetItem* group = new QTreeWidgetItem(); 113 group->setFlags(group->flags() | Qt::ItemIsUserCheckable); 114 group->setText(0, group_name); 115 m_ui.cheatList->addTopLevelItem(group); 116 return group; 117 } 118 119 QStringList CheatManagerWindow::getCheatGroupNames() const 120 { 121 QStringList group_names; 122 123 const int count = m_ui.cheatList->topLevelItemCount(); 124 for (int i = 0; i < count; i++) 125 { 126 QTreeWidgetItem* item = m_ui.cheatList->topLevelItem(i); 127 group_names.push_back(item->text(0)); 128 } 129 130 return group_names; 131 } 132 133 static int getCheatIndexFromItem(QTreeWidgetItem* item) 134 { 135 QVariant item_data(item->data(0, Qt::UserRole)); 136 if (!item_data.isValid()) 137 return -1; 138 139 return static_cast<int>(item_data.toUInt()); 140 } 141 142 int CheatManagerWindow::getSelectedCheatIndex() const 143 { 144 QList<QTreeWidgetItem*> sel = m_ui.cheatList->selectedItems(); 145 if (sel.isEmpty()) 146 return -1; 147 148 return static_cast<int>(getCheatIndexFromItem(sel.first())); 149 } 150 151 CheatList* CheatManagerWindow::getCheatList() const 152 { 153 return System::IsValid() ? System::GetCheatList() : nullptr; 154 } 155 156 void CheatManagerWindow::updateCheatList() 157 { 158 QSignalBlocker sb(m_ui.cheatList); 159 while (m_ui.cheatList->topLevelItemCount() > 0) 160 delete m_ui.cheatList->takeTopLevelItem(0); 161 162 m_ui.cheatList->setEnabled(false); 163 m_ui.cheatListAdd->setEnabled(false); 164 m_ui.cheatListNewCategory->setEnabled(false); 165 m_ui.cheatListEdit->setEnabled(false); 166 m_ui.cheatListRemove->setEnabled(false); 167 m_ui.cheatListActivate->setText(tr("Activate")); 168 m_ui.cheatListActivate->setEnabled(false); 169 m_ui.cheatListImport->setEnabled(false); 170 m_ui.cheatListExport->setEnabled(false); 171 m_ui.cheatListClear->setEnabled(false); 172 m_ui.cheatListReset->setEnabled(false); 173 174 Host::RunOnCPUThread([]() { 175 if (!System::IsValid()) 176 return; 177 178 CheatList* list = System::GetCheatList(); 179 if (!list) 180 { 181 System::LoadCheatList(); 182 list = System::GetCheatList(); 183 } 184 if (!list) 185 { 186 System::LoadCheatListFromDatabase(); 187 list = System::GetCheatList(); 188 } 189 if (!list) 190 { 191 System::SetCheatList(std::make_unique<CheatList>()); 192 list = System::GetCheatList(); 193 } 194 195 // still racey... 196 QtHost::RunOnUIThread([list]() { 197 if (!QtHost::IsSystemValid()) 198 return; 199 200 CheatManagerWindow* cm = g_main_window->getCheatManagerWindow(); 201 if (!cm) 202 return; 203 204 QSignalBlocker sb(cm->m_ui.cheatList); 205 206 const std::vector<std::string> groups = list->GetCodeGroups(); 207 for (const std::string& group_name : groups) 208 { 209 QTreeWidgetItem* group = cm->createItemForCheatGroup(QString::fromStdString(group_name)); 210 211 const u32 count = list->GetCodeCount(); 212 bool all_enabled = true; 213 for (u32 i = 0; i < count; i++) 214 { 215 const CheatCode& code = list->GetCode(i); 216 if (code.group != group_name) 217 continue; 218 219 QTreeWidgetItem* item = new QTreeWidgetItem(group); 220 cm->fillItemForCheatCode(item, i, code); 221 222 all_enabled &= code.enabled; 223 } 224 225 group->setCheckState(0, all_enabled ? Qt::Checked : Qt::Unchecked); 226 group->setExpanded(true); 227 } 228 229 cm->m_ui.cheatList->setEnabled(true); 230 cm->m_ui.cheatListAdd->setEnabled(true); 231 cm->m_ui.cheatListNewCategory->setEnabled(true); 232 cm->m_ui.cheatListImport->setEnabled(true); 233 cm->m_ui.cheatListClear->setEnabled(true); 234 cm->m_ui.cheatListReset->setEnabled(true); 235 cm->m_ui.cheatListExport->setEnabled(cm->m_ui.cheatList->topLevelItemCount() > 0); 236 }); 237 }); 238 } 239 240 void CheatManagerWindow::fillItemForCheatCode(QTreeWidgetItem* item, u32 index, const CheatCode& code) 241 { 242 item->setData(0, Qt::UserRole, QVariant(static_cast<uint>(index))); 243 if (code.IsManuallyActivated()) 244 { 245 item->setFlags(item->flags() & ~(Qt::ItemIsUserCheckable)); 246 } 247 else 248 { 249 item->setFlags(item->flags() | Qt::ItemIsUserCheckable); 250 item->setCheckState(0, code.enabled ? Qt::Checked : Qt::Unchecked); 251 } 252 item->setText(0, QString::fromStdString(code.description)); 253 item->setText(1, qApp->translate("Cheats", CheatCode::GetTypeDisplayName(code.type))); 254 item->setText(2, qApp->translate("Cheats", CheatCode::GetActivationDisplayName(code.activation))); 255 item->setText(3, QString::number(static_cast<uint>(code.instructions.size()))); 256 } 257 258 void CheatManagerWindow::saveCheatList() 259 { 260 Host::RunOnCPUThread([]() { System::SaveCheatList(); }); 261 } 262 263 void CheatManagerWindow::cheatListCurrentItemChanged(QTreeWidgetItem* current, QTreeWidgetItem* previous) 264 { 265 const int cheat_index = current ? getCheatIndexFromItem(current) : -1; 266 const bool has_current = (cheat_index >= 0); 267 m_ui.cheatListEdit->setEnabled(has_current); 268 m_ui.cheatListRemove->setEnabled(has_current); 269 m_ui.cheatListActivate->setEnabled(has_current); 270 271 if (!has_current) 272 { 273 m_ui.cheatListActivate->setText(tr("Activate")); 274 } 275 else 276 { 277 const bool manual_activation = getCheatList()->GetCode(static_cast<u32>(cheat_index)).IsManuallyActivated(); 278 m_ui.cheatListActivate->setText(manual_activation ? tr("Activate") : tr("Toggle")); 279 } 280 } 281 282 void CheatManagerWindow::cheatListItemActivated(QTreeWidgetItem* item) 283 { 284 if (!item) 285 return; 286 287 const int index = getCheatIndexFromItem(item); 288 if (index >= 0) 289 activateCheat(static_cast<u32>(index)); 290 } 291 292 void CheatManagerWindow::cheatListItemChanged(QTreeWidgetItem* item, int column) 293 { 294 if (!item || column != 0) 295 return; 296 297 CheatList* list = getCheatList(); 298 299 const int index = getCheatIndexFromItem(item); 300 if (index < 0) 301 { 302 // we're probably a parent/group node 303 const int child_count = item->childCount(); 304 const Qt::CheckState cs = item->checkState(0); 305 for (int i = 0; i < child_count; i++) 306 item->child(i)->setCheckState(0, cs); 307 308 return; 309 } 310 311 if (static_cast<u32>(index) >= list->GetCodeCount()) 312 return; 313 314 CheatCode& cc = list->GetCode(static_cast<u32>(index)); 315 if (cc.IsManuallyActivated()) 316 return; 317 318 const bool new_enabled = (item->checkState(0) == Qt::Checked); 319 if (cc.enabled == new_enabled) 320 return; 321 322 Host::RunOnCPUThread([index, new_enabled]() { 323 System::GetCheatList()->SetCodeEnabled(static_cast<u32>(index), new_enabled); 324 System::SaveCheatList(); 325 }); 326 } 327 328 void CheatManagerWindow::activateCheat(u32 index) 329 { 330 CheatList* list = getCheatList(); 331 if (index >= list->GetCodeCount()) 332 return; 333 334 CheatCode& cc = list->GetCode(index); 335 if (cc.IsManuallyActivated()) 336 { 337 g_emu_thread->applyCheat(index); 338 return; 339 } 340 341 const bool new_enabled = !cc.enabled; 342 setCheatCheckState(index, new_enabled); 343 344 Host::RunOnCPUThread([index, new_enabled]() { 345 System::GetCheatList()->SetCodeEnabled(index, new_enabled); 346 System::SaveCheatList(); 347 }); 348 } 349 350 void CheatManagerWindow::setCheatCheckState(u32 index, bool checked) 351 { 352 QTreeWidgetItem* item = getItemForCheatIndex(index); 353 if (item) 354 { 355 QSignalBlocker sb(m_ui.cheatList); 356 item->setCheckState(0, checked ? Qt::Checked : Qt::Unchecked); 357 } 358 } 359 360 void CheatManagerWindow::newCategoryClicked() 361 { 362 QString group_name = QInputDialog::getText(this, tr("Add Group"), tr("Group Name:")); 363 if (group_name.isEmpty()) 364 return; 365 366 if (getItemForCheatGroup(group_name) != nullptr) 367 { 368 QMessageBox::critical(this, tr("Error"), tr("This group name already exists.")); 369 return; 370 } 371 372 createItemForCheatGroup(group_name); 373 } 374 375 void CheatManagerWindow::addCodeClicked() 376 { 377 CheatList* list = getCheatList(); 378 379 CheatCode new_code; 380 new_code.group = "Ungrouped"; 381 382 CheatCodeEditorDialog editor(getCheatGroupNames(), &new_code, this); 383 if (editor.exec() > 0) 384 { 385 const QString group_name_qstr(QString::fromStdString(new_code.group)); 386 QTreeWidgetItem* group_item = getItemForCheatGroup(group_name_qstr); 387 if (!group_item) 388 group_item = createItemForCheatGroup(group_name_qstr); 389 390 QTreeWidgetItem* item = new QTreeWidgetItem(group_item); 391 fillItemForCheatCode(item, list->GetCodeCount(), new_code); 392 group_item->setExpanded(true); 393 394 Host::RunOnCPUThread( 395 [&new_code]() { 396 System::GetCheatList()->AddCode(std::move(new_code)); 397 System::SaveCheatList(); 398 }, 399 true); 400 } 401 } 402 403 void CheatManagerWindow::editCodeClicked() 404 { 405 int index = getSelectedCheatIndex(); 406 if (index < 0) 407 return; 408 409 CheatList* list = getCheatList(); 410 if (static_cast<u32>(index) >= list->GetCodeCount()) 411 return; 412 413 CheatCode new_code = list->GetCode(static_cast<u32>(index)); 414 CheatCodeEditorDialog editor(getCheatGroupNames(), &new_code, this); 415 if (editor.exec() > 0) 416 { 417 QTreeWidgetItem* item = getItemForCheatIndex(static_cast<u32>(index)); 418 if (item) 419 { 420 if (new_code.group != list->GetCode(static_cast<u32>(index)).group) 421 { 422 item = item->parent()->takeChild(item->parent()->indexOfChild(item)); 423 424 const QString group_name_qstr(QString::fromStdString(new_code.group)); 425 QTreeWidgetItem* group_item = getItemForCheatGroup(group_name_qstr); 426 if (!group_item) 427 group_item = createItemForCheatGroup(group_name_qstr); 428 group_item->addChild(item); 429 group_item->setExpanded(true); 430 } 431 432 fillItemForCheatCode(item, static_cast<u32>(index), new_code); 433 } 434 else 435 { 436 // shouldn't happen... 437 updateCheatList(); 438 } 439 440 Host::RunOnCPUThread( 441 [index, &new_code]() { 442 System::GetCheatList()->SetCode(static_cast<u32>(index), std::move(new_code)); 443 System::SaveCheatList(); 444 }, 445 true); 446 } 447 } 448 449 void CheatManagerWindow::deleteCodeClicked() 450 { 451 int index = getSelectedCheatIndex(); 452 if (index < 0) 453 return; 454 455 CheatList* list = getCheatList(); 456 if (static_cast<u32>(index) >= list->GetCodeCount()) 457 return; 458 459 if (QMessageBox::question(this, tr("Delete Code"), 460 tr("Are you sure you wish to delete the selected code? This action is not reversible."), 461 QMessageBox::Yes, QMessageBox::No) != QMessageBox::Yes) 462 { 463 return; 464 } 465 466 Host::RunOnCPUThread( 467 [index]() { 468 System::GetCheatList()->RemoveCode(static_cast<u32>(index)); 469 System::SaveCheatList(); 470 }, 471 true); 472 updateCheatList(); 473 } 474 475 void CheatManagerWindow::activateCodeClicked() 476 { 477 int index = getSelectedCheatIndex(); 478 if (index < 0) 479 return; 480 481 activateCheat(static_cast<u32>(index)); 482 } 483 484 void CheatManagerWindow::importClicked() 485 { 486 QMenu menu(this); 487 connect(menu.addAction(tr("From File...")), &QAction::triggered, this, &CheatManagerWindow::importFromFileTriggered); 488 connect(menu.addAction(tr("From Text...")), &QAction::triggered, this, &CheatManagerWindow::importFromTextTriggered); 489 menu.exec(QCursor::pos()); 490 } 491 492 void CheatManagerWindow::importFromFileTriggered() 493 { 494 const QString filter(tr("PCSXR/Libretro Cheat Files (*.cht *.txt);;All Files (*.*)")); 495 const QString filename = 496 QDir::toNativeSeparators(QFileDialog::getOpenFileName(this, tr("Import Cheats"), QString(), filter)); 497 if (filename.isEmpty()) 498 return; 499 500 CheatList new_cheats; 501 if (!new_cheats.LoadFromFile(filename.toUtf8().constData(), CheatList::Format::Autodetect)) 502 { 503 QMessageBox::critical(this, tr("Error"), tr("Failed to parse cheat file. The log may contain more information.")); 504 return; 505 } 506 507 Host::RunOnCPUThread( 508 [&new_cheats]() { 509 DebugAssert(System::HasCheatList()); 510 System::GetCheatList()->MergeList(new_cheats); 511 System::SaveCheatList(); 512 }, 513 true); 514 updateCheatList(); 515 } 516 517 void CheatManagerWindow::importFromTextTriggered() 518 { 519 const QString text = QInputDialog::getMultiLineText(this, tr("Import Cheats"), tr("Cheat File Text:")); 520 if (text.isEmpty()) 521 return; 522 523 CheatList new_cheats; 524 if (!new_cheats.LoadFromString(text.toStdString(), CheatList::Format::Autodetect)) 525 { 526 QMessageBox::critical(this, tr("Error"), tr("Failed to parse cheat file. The log may contain more information.")); 527 return; 528 } 529 530 Host::RunOnCPUThread( 531 [&new_cheats]() { 532 DebugAssert(System::HasCheatList()); 533 System::GetCheatList()->MergeList(new_cheats); 534 System::SaveCheatList(); 535 }, 536 true); 537 updateCheatList(); 538 } 539 540 void CheatManagerWindow::exportClicked() 541 { 542 const QString filter(tr("PCSXR Cheat Files (*.cht);;All Files (*.*)")); 543 const QString filename = 544 QDir::toNativeSeparators(QFileDialog::getSaveFileName(this, tr("Export Cheats"), QString(), filter)); 545 if (filename.isEmpty()) 546 return; 547 548 if (!getCheatList()->SaveToPCSXRFile(filename.toUtf8().constData())) 549 QMessageBox::critical(this, tr("Error"), tr("Failed to save cheat file. The log may contain more information.")); 550 } 551 552 void CheatManagerWindow::clearClicked() 553 { 554 if (QMessageBox::question(this, tr("Confirm Clear"), 555 tr("Are you sure you want to remove all cheats? This is not reversible.")) != 556 QMessageBox::Yes) 557 { 558 return; 559 } 560 561 Host::RunOnCPUThread([] { System::ClearCheatList(true); }, true); 562 updateCheatList(); 563 } 564 565 void CheatManagerWindow::resetClicked() 566 { 567 if (QMessageBox::question( 568 this, tr("Confirm Reset"), 569 tr( 570 "Are you sure you want to reset the cheat list? Any cheats not in the DuckStation database WILL BE LOST.")) != 571 QMessageBox::Yes) 572 { 573 return; 574 } 575 576 Host::RunOnCPUThread([] { System::DeleteCheatList(); }, true); 577 updateCheatList(); 578 }