memorycardeditorwindow.cpp (20385B)
1 // SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin <stenzek@gmail.com> 2 // SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0) 3 4 #include "memorycardeditorwindow.h" 5 #include "qtutils.h" 6 7 #include "core/host.h" 8 #include "core/settings.h" 9 10 #include "common/assert.h" 11 #include "common/error.h" 12 #include "common/file_system.h" 13 #include "common/path.h" 14 #include "common/string_util.h" 15 16 #include <QtCore/QFileInfo> 17 #include <QtWidgets/QFileDialog> 18 #include <QtWidgets/QMessageBox> 19 20 static constexpr char MEMORY_CARD_IMAGE_FILTER[] = QT_TRANSLATE_NOOP( 21 "MemoryCardEditorWindow", "All Memory Card Types (*.mcd *.mcr *.mc *.srm *.psm *.ps *.ddf *.mem *.vgs *.psx)"); 22 static constexpr char MEMORY_CARD_IMPORT_FILTER[] = 23 QT_TRANSLATE_NOOP("MemoryCardEditorWindow", "All Importable Memory Card Types (*.mcd *.mcr *.mc *.gme)"); 24 static constexpr char SINGLE_SAVEFILE_FILTER[] = 25 TRANSLATE_NOOP("MemoryCardEditorWindow", "Single Save Files (*.mcs);;All Files (*.*)"); 26 27 MemoryCardEditorWindow::MemoryCardEditorWindow() : QWidget() 28 { 29 m_ui.setupUi(this); 30 setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); 31 32 m_deleteFile = m_ui.centerButtonBox->addButton(tr("Delete File"), QDialogButtonBox::ActionRole); 33 m_undeleteFile = m_ui.centerButtonBox->addButton(tr("Undelete File"), QDialogButtonBox::ActionRole); 34 m_exportFile = m_ui.centerButtonBox->addButton(tr("Export File"), QDialogButtonBox::ActionRole); 35 m_moveLeft = m_ui.centerButtonBox->addButton(tr("<<"), QDialogButtonBox::ActionRole); 36 m_moveRight = m_ui.centerButtonBox->addButton(tr(">>"), QDialogButtonBox::ActionRole); 37 38 m_card_a.path_cb = m_ui.cardAPath; 39 m_card_a.table = m_ui.cardA; 40 m_card_a.blocks_free_label = m_ui.cardAUsage; 41 m_card_b.path_cb = m_ui.cardBPath; 42 m_card_b.table = m_ui.cardB; 43 m_card_b.blocks_free_label = m_ui.cardBUsage; 44 45 createCardButtons(&m_card_a, m_ui.buttonBoxA); 46 createCardButtons(&m_card_b, m_ui.buttonBoxB); 47 connectUi(); 48 connectCardUi(&m_card_a, m_ui.buttonBoxA); 49 connectCardUi(&m_card_b, m_ui.buttonBoxB); 50 populateComboBox(m_ui.cardAPath); 51 populateComboBox(m_ui.cardBPath); 52 53 const QString new_card_hover_text(tr("New Card...")); 54 const QString open_card_hover_text(tr("Open Card...")); 55 m_ui.newCardA->setToolTip(new_card_hover_text); 56 m_ui.newCardB->setToolTip(new_card_hover_text); 57 m_ui.openCardA->setToolTip(open_card_hover_text); 58 m_ui.openCardB->setToolTip(open_card_hover_text); 59 } 60 61 MemoryCardEditorWindow::~MemoryCardEditorWindow() = default; 62 63 bool MemoryCardEditorWindow::setCardA(const QString& path) 64 { 65 int index = m_ui.cardAPath->findData(QVariant(QDir::toNativeSeparators(path))); 66 if (index < 0) 67 { 68 QFileInfo file(path); 69 if (!file.exists()) 70 return false; 71 72 QSignalBlocker sb(m_card_a.path_cb); 73 m_card_a.path_cb->addItem(file.baseName(), QVariant(path)); 74 index = m_card_a.path_cb->count() - 1; 75 } 76 77 m_ui.cardAPath->setCurrentIndex(index); 78 return true; 79 } 80 81 bool MemoryCardEditorWindow::setCardB(const QString& path) 82 { 83 int index = m_ui.cardBPath->findData(QVariant(QDir::toNativeSeparators(path))); 84 if (index < 0) 85 { 86 QFileInfo file(path); 87 if (!file.exists()) 88 return false; 89 90 QSignalBlocker sb(m_card_b.path_cb); 91 m_card_b.path_cb->addItem(file.baseName(), QVariant(path)); 92 index = m_card_b.path_cb->count() - 1; 93 } 94 95 m_ui.cardBPath->setCurrentIndex(index); 96 return true; 97 } 98 99 bool MemoryCardEditorWindow::createMemoryCard(const QString& path, Error* error) 100 { 101 MemoryCardImage::DataArray data; 102 MemoryCardImage::Format(&data); 103 104 return MemoryCardImage::SaveToFile(data, path.toUtf8().constData(), error); 105 } 106 107 void MemoryCardEditorWindow::resizeEvent(QResizeEvent* ev) 108 { 109 QtUtils::ResizeColumnsForTableView(m_card_a.table, {32, -1, 155, 45}); 110 QtUtils::ResizeColumnsForTableView(m_card_b.table, {32, -1, 155, 45}); 111 } 112 113 void MemoryCardEditorWindow::closeEvent(QCloseEvent* ev) 114 { 115 m_card_a.path_cb->setCurrentIndex(0); 116 m_card_b.path_cb->setCurrentIndex(0); 117 } 118 119 void MemoryCardEditorWindow::createCardButtons(Card* card, QDialogButtonBox* buttonBox) 120 { 121 card->format_button = buttonBox->addButton(tr("Format Card"), QDialogButtonBox::ActionRole); 122 card->import_file_button = buttonBox->addButton(tr("Import File..."), QDialogButtonBox::ActionRole); 123 card->import_button = buttonBox->addButton(tr("Import Card..."), QDialogButtonBox::ActionRole); 124 card->save_button = buttonBox->addButton(tr("Save"), QDialogButtonBox::ActionRole); 125 } 126 127 void MemoryCardEditorWindow::connectCardUi(Card* card, QDialogButtonBox* buttonBox) 128 { 129 connect(card->save_button, &QPushButton::clicked, [this, card] { saveCard(card); }); 130 connect(card->format_button, &QPushButton::clicked, [this, card] { formatCard(card); }); 131 connect(card->import_file_button, &QPushButton::clicked, [this, card] { importSaveFile(card); }); 132 connect(card->import_button, &QPushButton::clicked, [this, card] { importCard(card); }); 133 } 134 135 void MemoryCardEditorWindow::connectUi() 136 { 137 connect(m_ui.cardA, &QTableWidget::itemSelectionChanged, this, &MemoryCardEditorWindow::onCardASelectionChanged); 138 connect(m_ui.cardB, &QTableWidget::itemSelectionChanged, this, &MemoryCardEditorWindow::onCardBSelectionChanged); 139 connect(m_moveLeft, &QPushButton::clicked, this, &MemoryCardEditorWindow::doCopyFile); 140 connect(m_moveRight, &QPushButton::clicked, this, &MemoryCardEditorWindow::doCopyFile); 141 connect(m_deleteFile, &QPushButton::clicked, this, &MemoryCardEditorWindow::doDeleteFile); 142 connect(m_undeleteFile, &QPushButton::clicked, this, &MemoryCardEditorWindow::doUndeleteFile); 143 144 connect(m_ui.cardAPath, QOverload<int>::of(&QComboBox::currentIndexChanged), 145 [this](int index) { loadCardFromComboBox(&m_card_a, index); }); 146 connect(m_ui.cardBPath, QOverload<int>::of(&QComboBox::currentIndexChanged), 147 [this](int index) { loadCardFromComboBox(&m_card_b, index); }); 148 connect(m_ui.newCardA, &QPushButton::clicked, [this]() { newCard(&m_card_a); }); 149 connect(m_ui.newCardB, &QPushButton::clicked, [this]() { newCard(&m_card_b); }); 150 connect(m_ui.openCardA, &QPushButton::clicked, [this]() { openCard(&m_card_a); }); 151 connect(m_ui.openCardB, &QPushButton::clicked, [this]() { openCard(&m_card_b); }); 152 connect(m_exportFile, &QPushButton::clicked, this, &MemoryCardEditorWindow::doExportSaveFile); 153 } 154 155 void MemoryCardEditorWindow::populateComboBox(QComboBox* cb) 156 { 157 QSignalBlocker sb(cb); 158 159 cb->clear(); 160 161 cb->addItem(QString()); 162 163 FileSystem::FindResultsArray results; 164 FileSystem::FindFiles(EmuFolders::MemoryCards.c_str(), "*.mcd", 165 FILESYSTEM_FIND_FILES | FILESYSTEM_FIND_RELATIVE_PATHS | FILESYSTEM_FIND_SORT_BY_NAME, 166 &results); 167 for (FILESYSTEM_FIND_DATA& fd : results) 168 { 169 std::string real_filename(Path::Combine(EmuFolders::MemoryCards, fd.FileName)); 170 std::string::size_type pos = fd.FileName.rfind('.'); 171 if (pos != std::string::npos) 172 fd.FileName.erase(pos); 173 174 cb->addItem(QString::fromStdString(fd.FileName), QVariant(QString::fromStdString(real_filename))); 175 } 176 } 177 178 void MemoryCardEditorWindow::loadCardFromComboBox(Card* card, int index) 179 { 180 loadCard(card->path_cb->itemData(index).toString(), card); 181 } 182 183 void MemoryCardEditorWindow::onCardASelectionChanged() 184 { 185 { 186 QSignalBlocker cb(m_card_b.table); 187 m_card_b.table->clearSelection(); 188 } 189 190 updateButtonState(); 191 } 192 193 void MemoryCardEditorWindow::onCardBSelectionChanged() 194 { 195 { 196 QSignalBlocker cb(m_card_a.table); 197 m_card_a.table->clearSelection(); 198 } 199 200 updateButtonState(); 201 } 202 203 void MemoryCardEditorWindow::clearSelection() 204 { 205 { 206 QSignalBlocker cb(m_card_a.table); 207 m_card_a.table->clearSelection(); 208 } 209 210 { 211 QSignalBlocker cb(m_card_b.table); 212 m_card_b.table->clearSelection(); 213 } 214 215 updateButtonState(); 216 } 217 218 bool MemoryCardEditorWindow::loadCard(const QString& filename, Card* card) 219 { 220 promptForSave(card); 221 222 card->table->setRowCount(0); 223 card->dirty = false; 224 card->blocks_free_label->clear(); 225 card->save_button->setEnabled(false); 226 227 card->filename.clear(); 228 229 if (filename.isEmpty()) 230 { 231 updateButtonState(); 232 return false; 233 } 234 235 Error error; 236 std::string filename_str = filename.toStdString(); 237 if (!MemoryCardImage::LoadFromFile(&card->data, filename_str.c_str(), &error)) 238 { 239 QMessageBox::critical(this, tr("Error"), 240 tr("Failed to load memory card: %1").arg(QString::fromStdString(error.GetDescription()))); 241 return false; 242 } 243 244 card->filename = std::move(filename_str); 245 updateCardTable(card); 246 updateCardBlocksFree(card); 247 updateButtonState(); 248 return true; 249 } 250 251 static void setCardTableItemProperties(QTableWidgetItem* item, const MemoryCardImage::FileInfo& fi) 252 { 253 item->setFlags(item->flags() & ~(Qt::ItemIsEditable)); 254 if (fi.deleted) 255 { 256 item->setBackground(Qt::darkRed); 257 item->setForeground(Qt::white); 258 } 259 } 260 261 void MemoryCardEditorWindow::updateCardTable(Card* card) 262 { 263 card->table->setRowCount(0); 264 265 card->files = MemoryCardImage::EnumerateFiles(card->data, true); 266 for (const MemoryCardImage::FileInfo& fi : card->files) 267 { 268 const int row = card->table->rowCount(); 269 card->table->insertRow(row); 270 271 if (!fi.icon_frames.empty()) 272 { 273 const QImage image(reinterpret_cast<const u8*>(fi.icon_frames[0].pixels), MemoryCardImage::ICON_WIDTH, 274 MemoryCardImage::ICON_HEIGHT, QImage::Format_RGBA8888); 275 276 QTableWidgetItem* icon = new QTableWidgetItem(); 277 setCardTableItemProperties(icon, fi); 278 icon->setIcon(QIcon(QPixmap::fromImage(image))); 279 card->table->setItem(row, 0, icon); 280 } 281 282 QString title_str(QString::fromStdString(fi.title)); 283 if (fi.deleted) 284 title_str += tr(" (Deleted)"); 285 286 QTableWidgetItem* item = new QTableWidgetItem(title_str); 287 setCardTableItemProperties(item, fi); 288 card->table->setItem(row, 1, item); 289 290 item = new QTableWidgetItem(QString::fromStdString(fi.filename)); 291 setCardTableItemProperties(item, fi); 292 card->table->setItem(row, 2, item); 293 294 item = new QTableWidgetItem(QString::number(fi.num_blocks)); 295 setCardTableItemProperties(item, fi); 296 card->table->setItem(row, 3, item); 297 } 298 } 299 300 void MemoryCardEditorWindow::updateCardBlocksFree(Card* card) 301 { 302 card->blocks_free = MemoryCardImage::GetFreeBlockCount(card->data); 303 card->blocks_free_label->setText( 304 tr("%n block(s) free%1", "", card->blocks_free).arg(card->dirty ? QStringLiteral(" (*)") : QString())); 305 } 306 307 void MemoryCardEditorWindow::setCardDirty(Card* card) 308 { 309 card->dirty = true; 310 card->save_button->setEnabled(true); 311 } 312 313 void MemoryCardEditorWindow::newCard(Card* card) 314 { 315 promptForSave(card); 316 317 QString filename = QDir::toNativeSeparators( 318 QFileDialog::getSaveFileName(this, tr("Select Memory Card"), QString(), tr(MEMORY_CARD_IMAGE_FILTER))); 319 if (filename.isEmpty()) 320 return; 321 322 { 323 // add to combo box 324 QFileInfo file(filename); 325 QSignalBlocker sb(card->path_cb); 326 card->path_cb->addItem(file.baseName(), QVariant(filename)); 327 card->path_cb->setCurrentIndex(card->path_cb->count() - 1); 328 } 329 330 card->filename = filename.toStdString(); 331 332 MemoryCardImage::Format(&card->data); 333 updateCardTable(card); 334 updateCardBlocksFree(card); 335 updateButtonState(); 336 saveCard(card); 337 } 338 339 void MemoryCardEditorWindow::openCard(Card* card) 340 { 341 promptForSave(card); 342 343 QString filename = QDir::toNativeSeparators( 344 QFileDialog::getOpenFileName(this, tr("Select Memory Card"), QString(), tr(MEMORY_CARD_IMAGE_FILTER))); 345 if (filename.isEmpty()) 346 return; 347 348 Error error; 349 if (!MemoryCardImage::LoadFromFile(&card->data, filename.toUtf8().constData(), &error)) 350 { 351 QMessageBox::critical(this, tr("Error"), 352 tr("Failed to load memory card: %1").arg(QString::fromStdString(error.GetDescription()))); 353 return; 354 } 355 356 { 357 // add to combo box 358 QFileInfo file(filename); 359 QSignalBlocker sb(card->path_cb); 360 card->path_cb->addItem(file.baseName(), QVariant(filename)); 361 card->path_cb->setCurrentIndex(card->path_cb->count() - 1); 362 } 363 364 card->filename = filename.toStdString(); 365 updateCardTable(card); 366 updateCardBlocksFree(card); 367 updateButtonState(); 368 } 369 370 void MemoryCardEditorWindow::saveCard(Card* card) 371 { 372 if (card->filename.empty()) 373 return; 374 375 Error error; 376 if (!MemoryCardImage::SaveToFile(card->data, card->filename.c_str(), &error)) 377 { 378 QMessageBox::critical(this, tr("Error"), 379 tr("Failed to save memory card: %1").arg(QString::fromStdString(error.GetDescription()))); 380 return; 381 } 382 383 card->dirty = false; 384 card->save_button->setEnabled(false); 385 updateCardBlocksFree(card); 386 } 387 388 void MemoryCardEditorWindow::promptForSave(Card* card) 389 { 390 if (card->filename.empty() || !card->dirty) 391 return; 392 393 if (QMessageBox::question(this, tr("Save memory card?"), 394 tr("Memory card '%1' is not saved, do you want to save before closing?") 395 .arg(QString::fromStdString(card->filename)), 396 QMessageBox::Yes, QMessageBox::No) == QMessageBox::No) 397 { 398 return; 399 } 400 401 saveCard(card); 402 } 403 404 void MemoryCardEditorWindow::doCopyFile() 405 { 406 const auto [src, fi] = getSelectedFile(); 407 if (!fi) 408 return; 409 410 Card* dst = (src == &m_card_a) ? &m_card_b : &m_card_a; 411 412 for (const MemoryCardImage::FileInfo& dst_fi : dst->files) 413 { 414 if (dst_fi.filename == fi->filename) 415 { 416 QMessageBox::critical( 417 this, tr("Error"), 418 tr("Destination memory card already contains a save file with the same name (%1) as the one you are attempting " 419 "to copy. Please delete this file from the destination memory card before copying.") 420 .arg(QString(fi->filename.c_str()))); 421 return; 422 } 423 } 424 425 if (dst->blocks_free < fi->num_blocks) 426 { 427 QMessageBox::critical(this, tr("Error"), 428 tr("Insufficient blocks, this file needs %1 but only %2 are available.") 429 .arg(fi->num_blocks) 430 .arg(dst->blocks_free)); 431 return; 432 } 433 434 Error error; 435 std::vector<u8> buffer; 436 if (!MemoryCardImage::ReadFile(src->data, *fi, &buffer, &error)) 437 { 438 QMessageBox::critical(this, tr("Error"), 439 tr("Failed to read file %1:\n%2") 440 .arg(QString::fromStdString(fi->filename)) 441 .arg(QString::fromStdString(error.GetDescription()))); 442 return; 443 } 444 445 if (!MemoryCardImage::WriteFile(&dst->data, fi->filename, buffer, &error)) 446 { 447 QMessageBox::critical(this, tr("Error"), 448 tr("Failed to write file %1:\n%2") 449 .arg(QString::fromStdString(fi->filename)) 450 .arg(QString::fromStdString(error.GetDescription()))); 451 return; 452 } 453 454 clearSelection(); 455 setCardDirty(dst); 456 updateCardTable(dst); 457 updateCardBlocksFree(dst); 458 updateButtonState(); 459 } 460 461 void MemoryCardEditorWindow::doDeleteFile() 462 { 463 const auto [card, fi] = getSelectedFile(); 464 if (!fi) 465 return; 466 467 if (!MemoryCardImage::DeleteFile(&card->data, *fi, fi->deleted)) 468 { 469 QMessageBox::critical(this, tr("Error"), tr("Failed to delete file %1").arg(QString::fromStdString(fi->filename))); 470 return; 471 } 472 473 clearSelection(); 474 setCardDirty(card); 475 updateCardTable(card); 476 updateCardBlocksFree(card); 477 updateButtonState(); 478 } 479 480 void MemoryCardEditorWindow::doUndeleteFile() 481 { 482 const auto [card, fi] = getSelectedFile(); 483 if (!fi) 484 return; 485 486 if (!MemoryCardImage::UndeleteFile(&card->data, *fi)) 487 { 488 QMessageBox::critical( 489 this, tr("Error"), 490 tr("Failed to undelete file %1. The file may have been partially overwritten by another save.") 491 .arg(QString::fromStdString(fi->filename))); 492 return; 493 } 494 495 clearSelection(); 496 setCardDirty(card); 497 updateCardTable(card); 498 updateCardBlocksFree(card); 499 updateButtonState(); 500 } 501 502 void MemoryCardEditorWindow::doExportSaveFile() 503 { 504 QString filename = QDir::toNativeSeparators( 505 QFileDialog::getSaveFileName(this, tr("Select Single Savefile"), QString(), tr(SINGLE_SAVEFILE_FILTER))); 506 507 if (filename.isEmpty()) 508 return; 509 510 const auto [card, fi] = getSelectedFile(); 511 if (!fi) 512 return; 513 514 Error error; 515 if (!MemoryCardImage::ExportSave(&card->data, *fi, filename.toStdString().c_str(), &error)) 516 { 517 QMessageBox::critical(this, tr("Error"), 518 tr("Failed to export save file %1:\n%2") 519 .arg(QString::fromStdString(fi->filename)) 520 .arg(QString::fromStdString(error.GetDescription()))); 521 return; 522 } 523 } 524 525 void MemoryCardEditorWindow::importCard(Card* card) 526 { 527 promptForSave(card); 528 529 QString filename = QDir::toNativeSeparators( 530 QFileDialog::getOpenFileName(this, tr("Select Import File"), QString(), tr(MEMORY_CARD_IMPORT_FILTER))); 531 if (filename.isEmpty()) 532 return; 533 534 Error error; 535 std::unique_ptr<MemoryCardImage::DataArray> temp = std::make_unique<MemoryCardImage::DataArray>(); 536 if (!MemoryCardImage::ImportCard(temp.get(), filename.toStdString().c_str(), &error)) 537 { 538 QMessageBox::critical(this, tr("Error"), 539 tr("Failed to import memory card from %1:\n%2") 540 .arg(QFileInfo(filename).fileName()) 541 .arg(QString::fromStdString(error.GetDescription()))); 542 return; 543 } 544 545 clearSelection(); 546 547 card->data = *temp; 548 setCardDirty(card); 549 updateCardTable(card); 550 updateCardBlocksFree(card); 551 updateButtonState(); 552 } 553 554 void MemoryCardEditorWindow::formatCard(Card* card) 555 { 556 promptForSave(card); 557 558 if (QMessageBox::question(this, tr("Format memory card?"), 559 tr("Formatting the memory card will destroy all saves, and they will not be recoverable. " 560 "The memory card which will be formatted is located at '%1'.") 561 .arg(QString::fromStdString(card->filename)), 562 QMessageBox::Yes, QMessageBox::No) == QMessageBox::No) 563 { 564 return; 565 } 566 567 clearSelection(); 568 569 MemoryCardImage::Format(&card->data); 570 571 setCardDirty(card); 572 updateCardTable(card); 573 updateCardBlocksFree(card); 574 updateButtonState(); 575 } 576 577 void MemoryCardEditorWindow::importSaveFile(Card* card) 578 { 579 QString filename = QDir::toNativeSeparators( 580 QFileDialog::getOpenFileName(this, tr("Select Save File"), QString(), tr(SINGLE_SAVEFILE_FILTER))); 581 582 if (filename.isEmpty()) 583 return; 584 585 Error error; 586 if (!MemoryCardImage::ImportSave(&card->data, filename.toStdString().c_str(), &error)) 587 { 588 QMessageBox::critical(this, tr("Error"), 589 tr("Failed to import save from %1:\n%2") 590 .arg(QFileInfo(filename).fileName()) 591 .arg(QString::fromStdString(error.GetDescription()))); 592 return; 593 } 594 595 setCardDirty(card); 596 updateCardTable(card); 597 updateCardBlocksFree(card); 598 } 599 600 std::tuple<MemoryCardEditorWindow::Card*, const MemoryCardImage::FileInfo*> MemoryCardEditorWindow::getSelectedFile() 601 { 602 QList<QTableWidgetSelectionRange> sel = m_card_a.table->selectedRanges(); 603 Card* card = &m_card_a; 604 605 if (sel.isEmpty()) 606 { 607 sel = m_card_b.table->selectedRanges(); 608 card = &m_card_b; 609 } 610 611 if (sel.isEmpty()) 612 return std::tuple<Card*, const MemoryCardImage::FileInfo*>(nullptr, nullptr); 613 614 const int index = sel.front().topRow(); 615 Assert(index >= 0 && static_cast<u32>(index) < card->files.size()); 616 617 return std::tuple<Card*, const MemoryCardImage::FileInfo*>(card, &card->files[index]); 618 } 619 620 void MemoryCardEditorWindow::updateButtonState() 621 { 622 const auto [selected_card, selected_file] = getSelectedFile(); 623 const bool is_card_b = (selected_card == &m_card_b); 624 const bool has_selection = (selected_file != nullptr); 625 const bool is_deleted = (selected_file != nullptr && selected_file->deleted); 626 const bool card_a_present = !m_card_a.filename.empty(); 627 const bool card_b_present = !m_card_b.filename.empty(); 628 const bool both_cards_present = card_a_present && card_b_present; 629 m_deleteFile->setEnabled(has_selection); 630 m_undeleteFile->setEnabled(is_deleted); 631 m_exportFile->setEnabled(has_selection); 632 m_moveLeft->setEnabled(both_cards_present && has_selection && is_card_b); 633 m_moveRight->setEnabled(both_cards_present && has_selection && !is_card_b); 634 m_ui.buttonBoxA->setEnabled(card_a_present); 635 m_ui.buttonBoxB->setEnabled(card_b_present); 636 }