diff options
Diffstat (limited to 'src/yuzu')
| -rw-r--r-- | src/yuzu/configuration/config.cpp | 44 | ||||
| -rw-r--r-- | src/yuzu/configuration/configure_general.cpp | 5 | ||||
| -rw-r--r-- | src/yuzu/configuration/configure_general.ui | 7 | ||||
| -rw-r--r-- | src/yuzu/game_list.cpp | 436 | ||||
| -rw-r--r-- | src/yuzu/game_list.h | 43 | ||||
| -rw-r--r-- | src/yuzu/game_list_p.h | 127 | ||||
| -rw-r--r-- | src/yuzu/game_list_worker.cpp | 86 | ||||
| -rw-r--r-- | src/yuzu/game_list_worker.h | 26 | ||||
| -rw-r--r-- | src/yuzu/main.cpp | 87 | ||||
| -rw-r--r-- | src/yuzu/main.h | 8 | ||||
| -rw-r--r-- | src/yuzu/main.ui | 1 | ||||
| -rw-r--r-- | src/yuzu/uisettings.h | 21 | 
12 files changed, 695 insertions, 196 deletions
| diff --git a/src/yuzu/configuration/config.cpp b/src/yuzu/configuration/config.cpp index 0456248ac..f594106bf 100644 --- a/src/yuzu/configuration/config.cpp +++ b/src/yuzu/configuration/config.cpp @@ -517,10 +517,37 @@ void Config::ReadPathValues() {      UISettings::values.roms_path = ReadSetting(QStringLiteral("romsPath")).toString();      UISettings::values.symbols_path = ReadSetting(QStringLiteral("symbolsPath")).toString();      UISettings::values.screenshot_path = ReadSetting(QStringLiteral("screenshotPath")).toString(); -    UISettings::values.game_directory_path = +    UISettings::values.game_dir_deprecated =          ReadSetting(QStringLiteral("gameListRootDir"), QStringLiteral(".")).toString(); -    UISettings::values.game_directory_deepscan = +    UISettings::values.game_dir_deprecated_deepscan =          ReadSetting(QStringLiteral("gameListDeepScan"), false).toBool(); +    const int gamedirs_size = qt_config->beginReadArray(QStringLiteral("gamedirs")); +    for (int i = 0; i < gamedirs_size; ++i) { +        qt_config->setArrayIndex(i); +        UISettings::GameDir game_dir; +        game_dir.path = ReadSetting(QStringLiteral("path")).toString(); +        game_dir.deep_scan = ReadSetting(QStringLiteral("deep_scan"), false).toBool(); +        game_dir.expanded = ReadSetting(QStringLiteral("expanded"), true).toBool(); +        UISettings::values.game_dirs.append(game_dir); +    } +    qt_config->endArray(); +    // create NAND and SD card directories if empty, these are not removable through the UI, +    // also carries over old game list settings if present +    if (UISettings::values.game_dirs.isEmpty()) { +        UISettings::GameDir game_dir; +        game_dir.path = QStringLiteral("SDMC"); +        game_dir.expanded = true; +        UISettings::values.game_dirs.append(game_dir); +        game_dir.path = QStringLiteral("UserNAND"); +        UISettings::values.game_dirs.append(game_dir); +        game_dir.path = QStringLiteral("SysNAND"); +        UISettings::values.game_dirs.append(game_dir); +        if (UISettings::values.game_dir_deprecated != QStringLiteral(".")) { +            game_dir.path = UISettings::values.game_dir_deprecated; +            game_dir.deep_scan = UISettings::values.game_dir_deprecated_deepscan; +            UISettings::values.game_dirs.append(game_dir); +        } +    }      UISettings::values.recent_files = ReadSetting(QStringLiteral("recentFiles")).toStringList();      qt_config->endGroup(); @@ -899,10 +926,15 @@ void Config::SavePathValues() {      WriteSetting(QStringLiteral("romsPath"), UISettings::values.roms_path);      WriteSetting(QStringLiteral("symbolsPath"), UISettings::values.symbols_path);      WriteSetting(QStringLiteral("screenshotPath"), UISettings::values.screenshot_path); -    WriteSetting(QStringLiteral("gameListRootDir"), UISettings::values.game_directory_path, -                 QStringLiteral(".")); -    WriteSetting(QStringLiteral("gameListDeepScan"), UISettings::values.game_directory_deepscan, -                 false); +    qt_config->beginWriteArray(QStringLiteral("gamedirs")); +    for (int i = 0; i < UISettings::values.game_dirs.size(); ++i) { +        qt_config->setArrayIndex(i); +        const auto& game_dir = UISettings::values.game_dirs[i]; +        WriteSetting(QStringLiteral("path"), game_dir.path); +        WriteSetting(QStringLiteral("deep_scan"), game_dir.deep_scan, false); +        WriteSetting(QStringLiteral("expanded"), game_dir.expanded, true); +    } +    qt_config->endArray();      WriteSetting(QStringLiteral("recentFiles"), UISettings::values.recent_files);      qt_config->endGroup(); diff --git a/src/yuzu/configuration/configure_general.cpp b/src/yuzu/configuration/configure_general.cpp index 75fcbfea3..727836b17 100644 --- a/src/yuzu/configuration/configure_general.cpp +++ b/src/yuzu/configuration/configure_general.cpp @@ -19,22 +19,17 @@ ConfigureGeneral::ConfigureGeneral(QWidget* parent)      }      SetConfiguration(); - -    connect(ui->toggle_deepscan, &QCheckBox::stateChanged, this, -            [] { UISettings::values.is_game_list_reload_pending.exchange(true); });  }  ConfigureGeneral::~ConfigureGeneral() = default;  void ConfigureGeneral::SetConfiguration() { -    ui->toggle_deepscan->setChecked(UISettings::values.game_directory_deepscan);      ui->toggle_check_exit->setChecked(UISettings::values.confirm_before_closing);      ui->toggle_user_on_boot->setChecked(UISettings::values.select_user_on_boot);      ui->theme_combobox->setCurrentIndex(ui->theme_combobox->findData(UISettings::values.theme));  }  void ConfigureGeneral::ApplyConfiguration() { -    UISettings::values.game_directory_deepscan = ui->toggle_deepscan->isChecked();      UISettings::values.confirm_before_closing = ui->toggle_check_exit->isChecked();      UISettings::values.select_user_on_boot = ui->toggle_user_on_boot->isChecked();      UISettings::values.theme = diff --git a/src/yuzu/configuration/configure_general.ui b/src/yuzu/configuration/configure_general.ui index 184fdd329..e747a4ce2 100644 --- a/src/yuzu/configuration/configure_general.ui +++ b/src/yuzu/configuration/configure_general.ui @@ -25,13 +25,6 @@          <item>           <layout class="QVBoxLayout" name="GeneralVerticalLayout">            <item> -           <widget class="QCheckBox" name="toggle_deepscan"> -            <property name="text"> -             <string>Search sub-directories for games</string> -            </property> -           </widget> -          </item> -          <item>             <widget class="QCheckBox" name="toggle_check_exit">              <property name="text">               <string>Confirm exit while emulation is running</string> diff --git a/src/yuzu/game_list.cpp b/src/yuzu/game_list.cpp index d18b96519..d5fab2f1f 100644 --- a/src/yuzu/game_list.cpp +++ b/src/yuzu/game_list.cpp @@ -34,7 +34,6 @@ bool GameListSearchField::KeyReleaseEater::eventFilter(QObject* obj, QEvent* eve          return QObject::eventFilter(obj, event);      QKeyEvent* keyEvent = static_cast<QKeyEvent*>(event); -    int rowCount = gamelist->tree_view->model()->rowCount();      QString edit_filter_text = gamelist->search_field->edit_filter->text().toLower();      // If the searchfield's text hasn't changed special function keys get checked @@ -56,19 +55,9 @@ bool GameListSearchField::KeyReleaseEater::eventFilter(QObject* obj, QEvent* eve          // If there is only one result launch this game          case Qt::Key_Return:          case Qt::Key_Enter: { -            QStandardItemModel* item_model = new QStandardItemModel(gamelist->tree_view); -            QModelIndex root_index = item_model->invisibleRootItem()->index(); -            QStandardItem* child_file; -            QString file_path; -            int resultCount = 0; -            for (int i = 0; i < rowCount; ++i) { -                if (!gamelist->tree_view->isRowHidden(i, root_index)) { -                    ++resultCount; -                    child_file = gamelist->item_model->item(i, 0); -                    file_path = child_file->data(GameListItemPath::FullPathRole).toString(); -                } -            } -            if (resultCount == 1) { +            if (gamelist->search_field->visible == 1) { +                QString file_path = gamelist->getLastFilterResultItem(); +                  // To avoid loading error dialog loops while confirming them using enter                  // Also users usually want to run a different game after closing one                  gamelist->search_field->edit_filter->clear(); @@ -88,9 +77,31 @@ bool GameListSearchField::KeyReleaseEater::eventFilter(QObject* obj, QEvent* eve  }  void GameListSearchField::setFilterResult(int visible, int total) { +    this->visible = visible; +    this->total = total; +      label_filter_result->setText(tr("%1 of %n result(s)", "", total).arg(visible));  } +QString GameList::getLastFilterResultItem() const { +    QStandardItem* folder; +    QStandardItem* child; +    QString file_path; +    const int folder_count = item_model->rowCount(); +    for (int i = 0; i < folder_count; ++i) { +        folder = item_model->item(i, 0); +        const QModelIndex folder_index = folder->index(); +        const int children_count = folder->rowCount(); +        for (int j = 0; j < children_count; ++j) { +            if (!tree_view->isRowHidden(j, folder_index)) { +                child = folder->child(j, 0); +                file_path = child->data(GameListItemPath::FullPathRole).toString(); +            } +        } +    } +    return file_path; +} +  void GameListSearchField::clear() {      edit_filter->clear();  } @@ -147,45 +158,120 @@ static bool ContainsAllWords(const QString& haystack, const QString& userinput)                         [&haystack](const QString& s) { return haystack.contains(s); });  } +// Syncs the expanded state of Game Directories with settings to persist across sessions +void GameList::onItemExpanded(const QModelIndex& item) { +    const auto type = item.data(GameListItem::TypeRole).value<GameListItemType>(); +    if (type == GameListItemType::CustomDir || type == GameListItemType::SdmcDir || +        type == GameListItemType::UserNandDir || type == GameListItemType::SysNandDir) +        item.data(GameListDir::GameDirRole).value<UISettings::GameDir*>()->expanded = +            tree_view->isExpanded(item); +} +  // Event in order to filter the gamelist after editing the searchfield  void GameList::onTextChanged(const QString& new_text) { -    const int row_count = tree_view->model()->rowCount(); -    const QString edit_filter_text = new_text.toLower(); -    const QModelIndex root_index = item_model->invisibleRootItem()->index(); +    const int folder_count = tree_view->model()->rowCount(); +    QString edit_filter_text = new_text.toLower(); +    QStandardItem* folder; +    QStandardItem* child; +    int children_total = 0; +    QModelIndex root_index = item_model->invisibleRootItem()->index();      // If the searchfield is empty every item is visible      // Otherwise the filter gets applied      if (edit_filter_text.isEmpty()) { -        for (int i = 0; i < row_count; ++i) { -            tree_view->setRowHidden(i, root_index, false); +        for (int i = 0; i < folder_count; ++i) { +            folder = item_model->item(i, 0); +            const QModelIndex folder_index = folder->index(); +            const int children_count = folder->rowCount(); +            for (int j = 0; j < children_count; ++j) { +                ++children_total; +                tree_view->setRowHidden(j, folder_index, false); +            }          } -        search_field->setFilterResult(row_count, row_count); +        search_field->setFilterResult(children_total, children_total);      } else {          int result_count = 0; -        for (int i = 0; i < row_count; ++i) { -            const QStandardItem* child_file = item_model->item(i, 0); -            const QString file_path = -                child_file->data(GameListItemPath::FullPathRole).toString().toLower(); -            const QString file_title = -                child_file->data(GameListItemPath::TitleRole).toString().toLower(); -            const QString file_program_id = -                child_file->data(GameListItemPath::ProgramIdRole).toString().toLower(); - -            // Only items which filename in combination with its title contains all words -            // that are in the searchfield will be visible in the gamelist -            // The search is case insensitive because of toLower() -            // I decided not to use Qt::CaseInsensitive in containsAllWords to prevent -            // multiple conversions of edit_filter_text for each game in the gamelist -            const QString file_name = file_path.mid(file_path.lastIndexOf(QLatin1Char{'/'}) + 1) + -                                      QLatin1Char{' '} + file_title; -            if (ContainsAllWords(file_name, edit_filter_text) || -                (file_program_id.count() == 16 && edit_filter_text.contains(file_program_id))) { -                tree_view->setRowHidden(i, root_index, false); -                ++result_count; -            } else { -                tree_view->setRowHidden(i, root_index, true); +        for (int i = 0; i < folder_count; ++i) { +            folder = item_model->item(i, 0); +            const QModelIndex folder_index = folder->index(); +            const int children_count = folder->rowCount(); +            for (int j = 0; j < children_count; ++j) { +                ++children_total; +                const QStandardItem* child = folder->child(j, 0); +                const QString file_path = +                    child->data(GameListItemPath::FullPathRole).toString().toLower(); +                const QString file_title = +                    child->data(GameListItemPath::TitleRole).toString().toLower(); +                const QString file_program_id = +                    child->data(GameListItemPath::ProgramIdRole).toString().toLower(); + +                // Only items which filename in combination with its title contains all words +                // that are in the searchfield will be visible in the gamelist +                // The search is case insensitive because of toLower() +                // I decided not to use Qt::CaseInsensitive in containsAllWords to prevent +                // multiple conversions of edit_filter_text for each game in the gamelist +                const QString file_name = +                    file_path.mid(file_path.lastIndexOf(QLatin1Char{'/'}) + 1) + QLatin1Char{' '} + +                    file_title; +                if (ContainsAllWords(file_name, edit_filter_text) || +                    (file_program_id.count() == 16 && edit_filter_text.contains(file_program_id))) { +                    tree_view->setRowHidden(j, folder_index, false); +                    ++result_count; +                } else { +                    tree_view->setRowHidden(j, folder_index, true); +                } +                search_field->setFilterResult(result_count, children_total);              } -            search_field->setFilterResult(result_count, row_count); +        } +    } +} + +void GameList::onUpdateThemedIcons() { +    for (int i = 0; i < item_model->invisibleRootItem()->rowCount(); i++) { +        QStandardItem* child = item_model->invisibleRootItem()->child(i); + +        const int icon_size = std::min(static_cast<int>(UISettings::values.icon_size), 64); +        switch (child->data(GameListItem::TypeRole).value<GameListItemType>()) { +        case GameListItemType::SdmcDir: +            child->setData( +                QIcon::fromTheme(QStringLiteral("sd_card")) +                    .pixmap(icon_size) +                    .scaled(icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation), +                Qt::DecorationRole); +            break; +        case GameListItemType::UserNandDir: +            child->setData( +                QIcon::fromTheme(QStringLiteral("chip")) +                    .pixmap(icon_size) +                    .scaled(icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation), +                Qt::DecorationRole); +            break; +        case GameListItemType::SysNandDir: +            child->setData( +                QIcon::fromTheme(QStringLiteral("chip")) +                    .pixmap(icon_size) +                    .scaled(icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation), +                Qt::DecorationRole); +            break; +        case GameListItemType::CustomDir: { +            const UISettings::GameDir* game_dir = +                child->data(GameListDir::GameDirRole).value<UISettings::GameDir*>(); +            const QString icon_name = QFileInfo::exists(game_dir->path) +                                          ? QStringLiteral("folder") +                                          : QStringLiteral("bad_folder"); +            child->setData( +                QIcon::fromTheme(icon_name).pixmap(icon_size).scaled( +                    icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation), +                Qt::DecorationRole); +            break; +        } +        case GameListItemType::AddDir: +            child->setData( +                QIcon::fromTheme(QStringLiteral("plus")) +                    .pixmap(icon_size) +                    .scaled(icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation), +                Qt::DecorationRole); +            break;          }      }  } @@ -214,7 +300,6 @@ GameList::GameList(FileSys::VirtualFilesystem vfs, FileSys::ManualContentProvide      tree_view->setHorizontalScrollMode(QHeaderView::ScrollPerPixel);      tree_view->setSortingEnabled(true);      tree_view->setEditTriggers(QHeaderView::NoEditTriggers); -    tree_view->setUniformRowHeights(true);      tree_view->setContextMenuPolicy(Qt::CustomContextMenu);      tree_view->setStyleSheet(QStringLiteral("QTreeView{ border: none; }")); @@ -230,12 +315,16 @@ GameList::GameList(FileSys::VirtualFilesystem vfs, FileSys::ManualContentProvide          item_model->setHeaderData(COLUMN_FILE_TYPE - 1, Qt::Horizontal, tr("File type"));          item_model->setHeaderData(COLUMN_SIZE - 1, Qt::Horizontal, tr("Size"));      } +    item_model->setSortRole(GameListItemPath::TitleRole); +    connect(main_window, &GMainWindow::UpdateThemedIcons, this, &GameList::onUpdateThemedIcons);      connect(tree_view, &QTreeView::activated, this, &GameList::ValidateEntry);      connect(tree_view, &QTreeView::customContextMenuRequested, this, &GameList::PopupContextMenu); +    connect(tree_view, &QTreeView::expanded, this, &GameList::onItemExpanded); +    connect(tree_view, &QTreeView::collapsed, this, &GameList::onItemExpanded); -    // We must register all custom types with the Qt Automoc system so that we are able to use it -    // with signals/slots. In this case, QList falls under the umbrells of custom types. +    // We must register all custom types with the Qt Automoc system so that we are able to use +    // it with signals/slots. In this case, QList falls under the umbrells of custom types.      qRegisterMetaType<QList<QStandardItem*>>("QList<QStandardItem*>");      layout->setContentsMargins(0, 0, 0, 0); @@ -263,38 +352,68 @@ void GameList::clearFilter() {      search_field->clear();  } -void GameList::AddEntry(const QList<QStandardItem*>& entry_items) { +void GameList::AddDirEntry(GameListDir* entry_items) {      item_model->invisibleRootItem()->appendRow(entry_items); +    tree_view->setExpanded( +        entry_items->index(), +        entry_items->data(GameListDir::GameDirRole).value<UISettings::GameDir*>()->expanded);  } -void GameList::ValidateEntry(const QModelIndex& item) { -    // We don't care about the individual QStandardItem that was selected, but its row. -    const int row = item_model->itemFromIndex(item)->row(); -    const QStandardItem* child_file = item_model->invisibleRootItem()->child(row, COLUMN_NAME); -    const QString file_path = child_file->data(GameListItemPath::FullPathRole).toString(); - -    if (file_path.isEmpty()) -        return; - -    if (!QFileInfo::exists(file_path)) -        return; +void GameList::AddEntry(const QList<QStandardItem*>& entry_items, GameListDir* parent) { +    parent->appendRow(entry_items); +} -    const QFileInfo file_info{file_path}; -    if (file_info.isDir()) { -        const QDir dir{file_path}; -        const QStringList matching_main = dir.entryList({QStringLiteral("main")}, QDir::Files); -        if (matching_main.size() == 1) { -            emit GameChosen(dir.path() + QDir::separator() + matching_main[0]); +void GameList::ValidateEntry(const QModelIndex& item) { +    const auto selected = item.sibling(item.row(), 0); + +    switch (selected.data(GameListItem::TypeRole).value<GameListItemType>()) { +    case GameListItemType::Game: { +        const QString file_path = selected.data(GameListItemPath::FullPathRole).toString(); +        if (file_path.isEmpty()) +            return; +        const QFileInfo file_info(file_path); +        if (!file_info.exists()) +            return; + +        if (file_info.isDir()) { +            const QDir dir{file_path}; +            const QStringList matching_main = dir.entryList({QStringLiteral("main")}, QDir::Files); +            if (matching_main.size() == 1) { +                emit GameChosen(dir.path() + QDir::separator() + matching_main[0]); +            } +            return;          } -        return; + +        // Users usually want to run a different game after closing one +        search_field->clear(); +        emit GameChosen(file_path); +        break;      } +    case GameListItemType::AddDir: +        emit AddDirectory(); +        break; +    } +} -    // Users usually want to run a diffrent game after closing one -    search_field->clear(); -    emit GameChosen(file_path); +bool GameList::isEmpty() const { +    for (int i = 0; i < item_model->rowCount(); i++) { +        const QStandardItem* child = item_model->invisibleRootItem()->child(i); +        const auto type = static_cast<GameListItemType>(child->type()); +        if (!child->hasChildren() && +            (type == GameListItemType::SdmcDir || type == GameListItemType::UserNandDir || +             type == GameListItemType::SysNandDir)) { +            item_model->invisibleRootItem()->removeRow(child->row()); +            i--; +        }; +    } +    return !item_model->invisibleRootItem()->hasChildren();  }  void GameList::DonePopulating(QStringList watch_list) { +    emit ShowList(!isEmpty()); + +    item_model->invisibleRootItem()->appendRow(new GameListAddDir()); +      // Clear out the old directories to watch for changes and add the new ones      auto watch_dirs = watcher->directories();      if (!watch_dirs.isEmpty()) { @@ -311,9 +430,13 @@ void GameList::DonePopulating(QStringList watch_list) {          QCoreApplication::processEvents();      }      tree_view->setEnabled(true); -    int rowCount = tree_view->model()->rowCount(); -    search_field->setFilterResult(rowCount, rowCount); -    if (rowCount > 0) { +    const int folder_count = tree_view->model()->rowCount(); +    int children_total = 0; +    for (int i = 0; i < folder_count; ++i) { +        children_total += item_model->item(i, 0)->rowCount(); +    } +    search_field->setFilterResult(children_total, children_total); +    if (children_total > 0) {          search_field->setFocus();      }  } @@ -323,12 +446,27 @@ void GameList::PopupContextMenu(const QPoint& menu_location) {      if (!item.isValid())          return; -    int row = item_model->itemFromIndex(item)->row(); -    QStandardItem* child_file = item_model->invisibleRootItem()->child(row, COLUMN_NAME); -    u64 program_id = child_file->data(GameListItemPath::ProgramIdRole).toULongLong(); -    std::string path = child_file->data(GameListItemPath::FullPathRole).toString().toStdString(); - +    const auto selected = item.sibling(item.row(), 0);      QMenu context_menu; +    switch (selected.data(GameListItem::TypeRole).value<GameListItemType>()) { +    case GameListItemType::Game: +        AddGamePopup(context_menu, selected.data(GameListItemPath::ProgramIdRole).toULongLong(), +                     selected.data(GameListItemPath::FullPathRole).toString().toStdString()); +        break; +    case GameListItemType::CustomDir: +        AddPermDirPopup(context_menu, selected); +        AddCustomDirPopup(context_menu, selected); +        break; +    case GameListItemType::SdmcDir: +    case GameListItemType::UserNandDir: +    case GameListItemType::SysNandDir: +        AddPermDirPopup(context_menu, selected); +        break; +    } +    context_menu.exec(tree_view->viewport()->mapToGlobal(menu_location)); +} + +void GameList::AddGamePopup(QMenu& context_menu, u64 program_id, std::string path) {      QAction* open_save_location = context_menu.addAction(tr("Open Save Data Location"));      QAction* open_lfs_location = context_menu.addAction(tr("Open Mod Data Location"));      QAction* open_transferable_shader_cache = @@ -344,19 +482,86 @@ void GameList::PopupContextMenu(const QPoint& menu_location) {      auto it = FindMatchingCompatibilityEntry(compatibility_list, program_id);      navigate_to_gamedb_entry->setVisible(it != compatibility_list.end() && program_id != 0); -    connect(open_save_location, &QAction::triggered, -            [&]() { emit OpenFolderRequested(program_id, GameListOpenTarget::SaveData); }); -    connect(open_lfs_location, &QAction::triggered, -            [&]() { emit OpenFolderRequested(program_id, GameListOpenTarget::ModData); }); +    connect(open_save_location, &QAction::triggered, [this, program_id]() { +        emit OpenFolderRequested(program_id, GameListOpenTarget::SaveData); +    }); +    connect(open_lfs_location, &QAction::triggered, [this, program_id]() { +        emit OpenFolderRequested(program_id, GameListOpenTarget::ModData); +    });      connect(open_transferable_shader_cache, &QAction::triggered, -            [&]() { emit OpenTransferableShaderCacheRequested(program_id); }); -    connect(dump_romfs, &QAction::triggered, [&]() { emit DumpRomFSRequested(program_id, path); }); -    connect(copy_tid, &QAction::triggered, [&]() { emit CopyTIDRequested(program_id); }); -    connect(navigate_to_gamedb_entry, &QAction::triggered, -            [&]() { emit NavigateToGamedbEntryRequested(program_id, compatibility_list); }); -    connect(properties, &QAction::triggered, [&]() { emit OpenPerGameGeneralRequested(path); }); +            [this, program_id]() { emit OpenTransferableShaderCacheRequested(program_id); }); +    connect(dump_romfs, &QAction::triggered, +            [this, program_id, path]() { emit DumpRomFSRequested(program_id, path); }); +    connect(copy_tid, &QAction::triggered, +            [this, program_id]() { emit CopyTIDRequested(program_id); }); +    connect(navigate_to_gamedb_entry, &QAction::triggered, [this, program_id]() { +        emit NavigateToGamedbEntryRequested(program_id, compatibility_list); +    }); +    connect(properties, &QAction::triggered, +            [this, path]() { emit OpenPerGameGeneralRequested(path); }); +}; + +void GameList::AddCustomDirPopup(QMenu& context_menu, QModelIndex selected) { +    UISettings::GameDir& game_dir = +        *selected.data(GameListDir::GameDirRole).value<UISettings::GameDir*>(); + +    QAction* deep_scan = context_menu.addAction(tr("Scan Subfolders")); +    QAction* delete_dir = context_menu.addAction(tr("Remove Game Directory")); + +    deep_scan->setCheckable(true); +    deep_scan->setChecked(game_dir.deep_scan); + +    connect(deep_scan, &QAction::triggered, [this, &game_dir] { +        game_dir.deep_scan = !game_dir.deep_scan; +        PopulateAsync(UISettings::values.game_dirs); +    }); +    connect(delete_dir, &QAction::triggered, [this, &game_dir, selected] { +        UISettings::values.game_dirs.removeOne(game_dir); +        item_model->invisibleRootItem()->removeRow(selected.row()); +    }); +} -    context_menu.exec(tree_view->viewport()->mapToGlobal(menu_location)); +void GameList::AddPermDirPopup(QMenu& context_menu, QModelIndex selected) { +    UISettings::GameDir& game_dir = +        *selected.data(GameListDir::GameDirRole).value<UISettings::GameDir*>(); + +    QAction* move_up = context_menu.addAction(tr(u8"\U000025b2 Move Up")); +    QAction* move_down = context_menu.addAction(tr(u8"\U000025bc Move Down ")); +    QAction* open_directory_location = context_menu.addAction(tr("Open Directory Location")); + +    const int row = selected.row(); + +    move_up->setEnabled(row > 0); +    move_down->setEnabled(row < item_model->rowCount() - 2); + +    connect(move_up, &QAction::triggered, [this, selected, row, &game_dir] { +        // find the indices of the items in settings and swap them +        std::swap(UISettings::values.game_dirs[UISettings::values.game_dirs.indexOf(game_dir)], +                  UISettings::values.game_dirs[UISettings::values.game_dirs.indexOf( +                      *selected.sibling(row - 1, 0) +                           .data(GameListDir::GameDirRole) +                           .value<UISettings::GameDir*>())]); +        // move the treeview items +        QList<QStandardItem*> item = item_model->takeRow(row); +        item_model->invisibleRootItem()->insertRow(row - 1, item); +        tree_view->setExpanded(selected, game_dir.expanded); +    }); + +    connect(move_down, &QAction::triggered, [this, selected, row, &game_dir] { +        // find the indices of the items in settings and swap them +        std::swap(UISettings::values.game_dirs[UISettings::values.game_dirs.indexOf(game_dir)], +                  UISettings::values.game_dirs[UISettings::values.game_dirs.indexOf( +                      *selected.sibling(row + 1, 0) +                           .data(GameListDir::GameDirRole) +                           .value<UISettings::GameDir*>())]); +        // move the treeview items +        const QList<QStandardItem*> item = item_model->takeRow(row); +        item_model->invisibleRootItem()->insertRow(row + 1, item); +        tree_view->setExpanded(selected, game_dir.expanded); +    }); + +    connect(open_directory_location, &QAction::triggered, +            [this, game_dir] { emit OpenDirectory(game_dir.path); });  }  void GameList::LoadCompatibilityList() { @@ -403,14 +608,7 @@ void GameList::LoadCompatibilityList() {      }  } -void GameList::PopulateAsync(const QString& dir_path, bool deep_scan) { -    const QFileInfo dir_info{dir_path}; -    if (!dir_info.exists() || !dir_info.isDir()) { -        LOG_ERROR(Frontend, "Could not find game list folder at {}", dir_path.toStdString()); -        search_field->setFilterResult(0, 0); -        return; -    } - +void GameList::PopulateAsync(QVector<UISettings::GameDir>& game_dirs) {      tree_view->setEnabled(false);      // Update the columns in case UISettings has changed @@ -433,17 +631,19 @@ void GameList::PopulateAsync(const QString& dir_path, bool deep_scan) {      // Delete any rows that might already exist if we're repopulating      item_model->removeRows(0, item_model->rowCount()); +    search_field->clear();      emit ShouldCancelWorker(); -    GameListWorker* worker = -        new GameListWorker(vfs, provider, dir_path, deep_scan, compatibility_list); +    GameListWorker* worker = new GameListWorker(vfs, provider, game_dirs, compatibility_list);      connect(worker, &GameListWorker::EntryReady, this, &GameList::AddEntry, Qt::QueuedConnection); +    connect(worker, &GameListWorker::DirEntryReady, this, &GameList::AddDirEntry, +            Qt::QueuedConnection);      connect(worker, &GameListWorker::Finished, this, &GameList::DonePopulating,              Qt::QueuedConnection); -    // Use DirectConnection here because worker->Cancel() is thread-safe and we want it to cancel -    // without delay. +    // Use DirectConnection here because worker->Cancel() is thread-safe and we want it to +    // cancel without delay.      connect(this, &GameList::ShouldCancelWorker, worker, &GameListWorker::Cancel,              Qt::DirectConnection); @@ -471,10 +671,40 @@ const QStringList GameList::supported_file_extensions = {      QStringLiteral("xci"), QStringLiteral("nsp"), QStringLiteral("kip")};  void GameList::RefreshGameDirectory() { -    if (!UISettings::values.game_directory_path.isEmpty() && current_worker != nullptr) { +    if (!UISettings::values.game_dirs.isEmpty() && current_worker != nullptr) {          LOG_INFO(Frontend, "Change detected in the games directory. Reloading game list."); -        search_field->clear(); -        PopulateAsync(UISettings::values.game_directory_path, -                      UISettings::values.game_directory_deepscan); +        PopulateAsync(UISettings::values.game_dirs);      }  } + +GameListPlaceholder::GameListPlaceholder(GMainWindow* parent) : QWidget{parent} { +    connect(parent, &GMainWindow::UpdateThemedIcons, this, +            &GameListPlaceholder::onUpdateThemedIcons); + +    layout = new QVBoxLayout; +    image = new QLabel; +    text = new QLabel; +    layout->setAlignment(Qt::AlignCenter); +    image->setPixmap(QIcon::fromTheme(QStringLiteral("plus_folder")).pixmap(200)); + +    text->setText(tr("Double-click to add a new folder to the game list")); +    QFont font = text->font(); +    font.setPointSize(20); +    text->setFont(font); +    text->setAlignment(Qt::AlignHCenter); +    image->setAlignment(Qt::AlignHCenter); + +    layout->addWidget(image); +    layout->addWidget(text); +    setLayout(layout); +} + +GameListPlaceholder::~GameListPlaceholder() = default; + +void GameListPlaceholder::onUpdateThemedIcons() { +    image->setPixmap(QIcon::fromTheme(QStringLiteral("plus_folder")).pixmap(200)); +} + +void GameListPlaceholder::mouseDoubleClickEvent(QMouseEvent* event) { +    emit GameListPlaceholder::AddDirectory(); +} diff --git a/src/yuzu/game_list.h b/src/yuzu/game_list.h index f8f8bd6c5..878d94413 100644 --- a/src/yuzu/game_list.h +++ b/src/yuzu/game_list.h @@ -8,6 +8,7 @@  #include <QHBoxLayout>  #include <QLabel>  #include <QLineEdit> +#include <QList>  #include <QModelIndex>  #include <QSettings>  #include <QStandardItem> @@ -16,13 +17,16 @@  #include <QToolButton>  #include <QTreeView>  #include <QVBoxLayout> +#include <QVector>  #include <QWidget>  #include "common/common_types.h" +#include "uisettings.h"  #include "yuzu/compatibility_list.h"  class GameListWorker;  class GameListSearchField; +class GameListDir;  class GMainWindow;  namespace FileSys { @@ -52,12 +56,14 @@ public:                        FileSys::ManualContentProvider* provider, GMainWindow* parent = nullptr);      ~GameList() override; +    QString getLastFilterResultItem() const;      void clearFilter();      void setFilterFocus();      void setFilterVisible(bool visibility); +    bool isEmpty() const;      void LoadCompatibilityList(); -    void PopulateAsync(const QString& dir_path, bool deep_scan); +    void PopulateAsync(QVector<UISettings::GameDir>& game_dirs);      void SaveInterfaceLayout();      void LoadInterfaceLayout(); @@ -74,19 +80,29 @@ signals:      void NavigateToGamedbEntryRequested(u64 program_id,                                          const CompatibilityList& compatibility_list);      void OpenPerGameGeneralRequested(const std::string& file); +    void OpenDirectory(const QString& directory); +    void AddDirectory(); +    void ShowList(bool show);  private slots: +    void onItemExpanded(const QModelIndex& item);      void onTextChanged(const QString& new_text);      void onFilterCloseClicked(); +    void onUpdateThemedIcons();  private: -    void AddEntry(const QList<QStandardItem*>& entry_items); +    void AddDirEntry(GameListDir* entry_items); +    void AddEntry(const QList<QStandardItem*>& entry_items, GameListDir* parent);      void ValidateEntry(const QModelIndex& item);      void DonePopulating(QStringList watch_list); -    void PopupContextMenu(const QPoint& menu_location);      void RefreshGameDirectory(); +    void PopupContextMenu(const QPoint& menu_location); +    void AddGamePopup(QMenu& context_menu, u64 program_id, std::string path); +    void AddCustomDirPopup(QMenu& context_menu, QModelIndex selected); +    void AddPermDirPopup(QMenu& context_menu, QModelIndex selected); +      std::shared_ptr<FileSys::VfsFilesystem> vfs;      FileSys::ManualContentProvider* provider;      GameListSearchField* search_field; @@ -102,3 +118,24 @@ private:  };  Q_DECLARE_METATYPE(GameListOpenTarget); + +class GameListPlaceholder : public QWidget { +    Q_OBJECT +public: +    explicit GameListPlaceholder(GMainWindow* parent = nullptr); +    ~GameListPlaceholder(); + +signals: +    void AddDirectory(); + +private slots: +    void onUpdateThemedIcons(); + +protected: +    void mouseDoubleClickEvent(QMouseEvent* event) override; + +private: +    QVBoxLayout* layout = nullptr; +    QLabel* image = nullptr; +    QLabel* text = nullptr; +}; diff --git a/src/yuzu/game_list_p.h b/src/yuzu/game_list_p.h index ece534dd6..a8d888fee 100644 --- a/src/yuzu/game_list_p.h +++ b/src/yuzu/game_list_p.h @@ -10,6 +10,7 @@  #include <utility>  #include <QCoreApplication> +#include <QFileInfo>  #include <QImage>  #include <QObject>  #include <QStandardItem> @@ -22,6 +23,17 @@  #include "yuzu/uisettings.h"  #include "yuzu/util/util.h" +enum class GameListItemType { +    Game = QStandardItem::UserType + 1, +    CustomDir = QStandardItem::UserType + 2, +    SdmcDir = QStandardItem::UserType + 3, +    UserNandDir = QStandardItem::UserType + 4, +    SysNandDir = QStandardItem::UserType + 5, +    AddDir = QStandardItem::UserType + 6 +}; + +Q_DECLARE_METATYPE(GameListItemType); +  /**   * Gets the default icon (for games without valid title metadata)   * @param size The desired width and height of the default icon. @@ -36,8 +48,13 @@ static QPixmap GetDefaultIcon(u32 size) {  class GameListItem : public QStandardItem {  public: +    // used to access type from item index +    static const int TypeRole = Qt::UserRole + 1; +    static const int SortRole = Qt::UserRole + 2;      GameListItem() = default; -    explicit GameListItem(const QString& string) : QStandardItem(string) {} +    GameListItem(const QString& string) : QStandardItem(string) { +        setData(string, SortRole); +    }  };  /** @@ -48,14 +65,15 @@ public:   */  class GameListItemPath : public GameListItem {  public: -    static const int FullPathRole = Qt::UserRole + 1; -    static const int TitleRole = Qt::UserRole + 2; -    static const int ProgramIdRole = Qt::UserRole + 3; -    static const int FileTypeRole = Qt::UserRole + 4; +    static const int TitleRole = SortRole; +    static const int FullPathRole = SortRole + 1; +    static const int ProgramIdRole = SortRole + 2; +    static const int FileTypeRole = SortRole + 3;      GameListItemPath() = default;      GameListItemPath(const QString& game_path, const std::vector<u8>& picture_data,                       const QString& game_name, const QString& game_type, u64 program_id) { +        setData(type(), TypeRole);          setData(game_path, FullPathRole);          setData(game_name, TitleRole);          setData(qulonglong(program_id), ProgramIdRole); @@ -72,6 +90,10 @@ public:          setData(picture, Qt::DecorationRole);      } +    int type() const override { +        return static_cast<int>(GameListItemType::Game); +    } +      QVariant data(int role) const override {          if (role == Qt::DisplayRole) {              std::string filename; @@ -103,9 +125,11 @@ public:  class GameListItemCompat : public GameListItem {      Q_DECLARE_TR_FUNCTIONS(GameListItemCompat)  public: -    static const int CompatNumberRole = Qt::UserRole + 1; +    static const int CompatNumberRole = SortRole;      GameListItemCompat() = default;      explicit GameListItemCompat(const QString& compatibility) { +        setData(type(), TypeRole); +          struct CompatStatus {              QString color;              const char* text; @@ -135,6 +159,10 @@ public:          setData(CreateCirclePixmapFromColor(status.color), Qt::DecorationRole);      } +    int type() const override { +        return static_cast<int>(GameListItemType::Game); +    } +      bool operator<(const QStandardItem& other) const override {          return data(CompatNumberRole) < other.data(CompatNumberRole);      } @@ -146,12 +174,12 @@ public:   * human-readable string representation will be displayed to the user.   */  class GameListItemSize : public GameListItem { -  public: -    static const int SizeRole = Qt::UserRole + 1; +    static const int SizeRole = SortRole;      GameListItemSize() = default;      explicit GameListItemSize(const qulonglong size_bytes) { +        setData(type(), TypeRole);          setData(size_bytes, SizeRole);      } @@ -167,6 +195,10 @@ public:          }      } +    int type() const override { +        return static_cast<int>(GameListItemType::Game); +    } +      /**       * This operator is, in practice, only used by the TreeView sorting systems.       * Override it so that it will correctly sort by numerical value instead of by string @@ -177,6 +209,82 @@ public:      }  }; +class GameListDir : public GameListItem { +public: +    static const int GameDirRole = Qt::UserRole + 2; + +    explicit GameListDir(UISettings::GameDir& directory, +                         GameListItemType dir_type = GameListItemType::CustomDir) +        : dir_type{dir_type} { +        setData(type(), TypeRole); + +        UISettings::GameDir* game_dir = &directory; +        setData(QVariant::fromValue(game_dir), GameDirRole); + +        const int icon_size = std::min(static_cast<int>(UISettings::values.icon_size), 64); +        switch (dir_type) { +        case GameListItemType::SdmcDir: +            setData( +                QIcon::fromTheme(QStringLiteral("sd_card")) +                    .pixmap(icon_size) +                    .scaled(icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation), +                Qt::DecorationRole); +            setData(QObject::tr("Installed SD Titles"), Qt::DisplayRole); +            break; +        case GameListItemType::UserNandDir: +            setData( +                QIcon::fromTheme(QStringLiteral("chip")) +                    .pixmap(icon_size) +                    .scaled(icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation), +                Qt::DecorationRole); +            setData(QObject::tr("Installed NAND Titles"), Qt::DisplayRole); +            break; +        case GameListItemType::SysNandDir: +            setData( +                QIcon::fromTheme(QStringLiteral("chip")) +                    .pixmap(icon_size) +                    .scaled(icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation), +                Qt::DecorationRole); +            setData(QObject::tr("System Titles"), Qt::DisplayRole); +            break; +        case GameListItemType::CustomDir: +            const QString icon_name = QFileInfo::exists(game_dir->path) +                                          ? QStringLiteral("folder") +                                          : QStringLiteral("bad_folder"); +            setData(QIcon::fromTheme(icon_name).pixmap(icon_size).scaled( +                        icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation), +                    Qt::DecorationRole); +            setData(game_dir->path, Qt::DisplayRole); +            break; +        }; +    }; + +    int type() const override { +        return static_cast<int>(dir_type); +    } + +private: +    GameListItemType dir_type; +}; + +class GameListAddDir : public GameListItem { +public: +    explicit GameListAddDir() { +        setData(type(), TypeRole); + +        const int icon_size = std::min(static_cast<int>(UISettings::values.icon_size), 64); +        setData(QIcon::fromTheme(QStringLiteral("plus")) +                    .pixmap(icon_size) +                    .scaled(icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation), +                Qt::DecorationRole); +        setData(QObject::tr("Add New Game Directory"), Qt::DisplayRole); +    } + +    int type() const override { +        return static_cast<int>(GameListItemType::AddDir); +    } +}; +  class GameList;  class QHBoxLayout;  class QTreeView; @@ -208,6 +316,9 @@ private:          // EventFilter in order to process systemkeys while editing the searchfield          bool eventFilter(QObject* obj, QEvent* event) override;      }; +    int visible; +    int total; +      QHBoxLayout* layout_filter = nullptr;      QTreeView* tree_view = nullptr;      QLabel* label_filter = nullptr; diff --git a/src/yuzu/game_list_worker.cpp b/src/yuzu/game_list_worker.cpp index 77f358630..fd21a9761 100644 --- a/src/yuzu/game_list_worker.cpp +++ b/src/yuzu/game_list_worker.cpp @@ -223,21 +223,37 @@ QList<QStandardItem*> MakeGameListEntry(const std::string& path, const std::stri  } // Anonymous namespace  GameListWorker::GameListWorker(FileSys::VirtualFilesystem vfs, -                               FileSys::ManualContentProvider* provider, QString dir_path, -                               bool deep_scan, const CompatibilityList& compatibility_list) -    : vfs(std::move(vfs)), provider(provider), dir_path(std::move(dir_path)), deep_scan(deep_scan), +                               FileSys::ManualContentProvider* provider, +                               QVector<UISettings::GameDir>& game_dirs, +                               const CompatibilityList& compatibility_list) +    : vfs(std::move(vfs)), provider(provider), game_dirs(game_dirs),        compatibility_list(compatibility_list) {}  GameListWorker::~GameListWorker() = default; -void GameListWorker::AddTitlesToGameList() { -    const auto& cache = dynamic_cast<FileSys::ContentProviderUnion&>( -        Core::System::GetInstance().GetContentProvider()); -    const auto installed_games = cache.ListEntriesFilterOrigin( -        std::nullopt, FileSys::TitleType::Application, FileSys::ContentRecordType::Program); +void GameListWorker::AddTitlesToGameList(GameListDir* parent_dir) { +    using namespace FileSys; + +    const auto& cache = +        dynamic_cast<ContentProviderUnion&>(Core::System::GetInstance().GetContentProvider()); + +    std::vector<std::pair<ContentProviderUnionSlot, ContentProviderEntry>> installed_games; +    installed_games = cache.ListEntriesFilterOrigin(std::nullopt, TitleType::Application, +                                                    ContentRecordType::Program); + +    if (parent_dir->type() == static_cast<int>(GameListItemType::SdmcDir)) { +        installed_games = cache.ListEntriesFilterOrigin( +            ContentProviderUnionSlot::SDMC, TitleType::Application, ContentRecordType::Program); +    } else if (parent_dir->type() == static_cast<int>(GameListItemType::UserNandDir)) { +        installed_games = cache.ListEntriesFilterOrigin( +            ContentProviderUnionSlot::UserNAND, TitleType::Application, ContentRecordType::Program); +    } else if (parent_dir->type() == static_cast<int>(GameListItemType::SysNandDir)) { +        installed_games = cache.ListEntriesFilterOrigin( +            ContentProviderUnionSlot::SysNAND, TitleType::Application, ContentRecordType::Program); +    }      for (const auto& [slot, game] : installed_games) { -        if (slot == FileSys::ContentProviderUnionSlot::FrontendManual) +        if (slot == ContentProviderUnionSlot::FrontendManual)              continue;          const auto file = cache.GetEntryUnparsed(game.title_id, game.type); @@ -250,21 +266,22 @@ void GameListWorker::AddTitlesToGameList() {          u64 program_id = 0;          loader->ReadProgramId(program_id); -        const FileSys::PatchManager patch{program_id}; -        const auto control = cache.GetEntry(game.title_id, FileSys::ContentRecordType::Control); +        const PatchManager patch{program_id}; +        const auto control = cache.GetEntry(game.title_id, ContentRecordType::Control);          if (control != nullptr)              GetMetadataFromControlNCA(patch, *control, icon, name);          emit EntryReady(MakeGameListEntry(file->GetFullPath(), name, icon, *loader, program_id, -                                          compatibility_list, patch)); +                                          compatibility_list, patch), +                        parent_dir);      }  }  void GameListWorker::ScanFileSystem(ScanTarget target, const std::string& dir_path, -                                    unsigned int recursion) { -    const auto callback = [this, target, recursion](u64* num_entries_out, -                                                    const std::string& directory, -                                                    const std::string& virtual_name) -> bool { +                                    unsigned int recursion, GameListDir* parent_dir) { +    const auto callback = [this, target, recursion, +                           parent_dir](u64* num_entries_out, const std::string& directory, +                                       const std::string& virtual_name) -> bool {          if (stop_processing) {              // Breaks the callback loop.              return false; @@ -317,11 +334,12 @@ void GameListWorker::ScanFileSystem(ScanTarget target, const std::string& dir_pa                  const FileSys::PatchManager patch{program_id};                  emit EntryReady(MakeGameListEntry(physical_name, name, icon, *loader, program_id, -                                                  compatibility_list, patch)); +                                                  compatibility_list, patch), +                                parent_dir);              }          } else if (is_dir && recursion > 0) {              watch_list.append(QString::fromStdString(physical_name)); -            ScanFileSystem(target, physical_name, recursion - 1); +            ScanFileSystem(target, physical_name, recursion - 1, parent_dir);          }          return true; @@ -332,12 +350,32 @@ void GameListWorker::ScanFileSystem(ScanTarget target, const std::string& dir_pa  void GameListWorker::run() {      stop_processing = false; -    watch_list.append(dir_path); -    provider->ClearAllEntries(); -    ScanFileSystem(ScanTarget::FillManualContentProvider, dir_path.toStdString(), -                   deep_scan ? 256 : 0); -    AddTitlesToGameList(); -    ScanFileSystem(ScanTarget::PopulateGameList, dir_path.toStdString(), deep_scan ? 256 : 0); + +    for (UISettings::GameDir& game_dir : game_dirs) { +        if (game_dir.path == QStringLiteral("SDMC")) { +            auto* const game_list_dir = new GameListDir(game_dir, GameListItemType::SdmcDir); +            emit DirEntryReady({game_list_dir}); +            AddTitlesToGameList(game_list_dir); +        } else if (game_dir.path == QStringLiteral("UserNAND")) { +            auto* const game_list_dir = new GameListDir(game_dir, GameListItemType::UserNandDir); +            emit DirEntryReady({game_list_dir}); +            AddTitlesToGameList(game_list_dir); +        } else if (game_dir.path == QStringLiteral("SysNAND")) { +            auto* const game_list_dir = new GameListDir(game_dir, GameListItemType::SysNandDir); +            emit DirEntryReady({game_list_dir}); +            AddTitlesToGameList(game_list_dir); +        } else { +            watch_list.append(game_dir.path); +            auto* const game_list_dir = new GameListDir(game_dir); +            emit DirEntryReady({game_list_dir}); +            provider->ClearAllEntries(); +            ScanFileSystem(ScanTarget::FillManualContentProvider, game_dir.path.toStdString(), 2, +                           game_list_dir); +            ScanFileSystem(ScanTarget::PopulateGameList, game_dir.path.toStdString(), +                           game_dir.deep_scan ? 256 : 0, game_list_dir); +        } +    }; +      emit Finished(watch_list);  } diff --git a/src/yuzu/game_list_worker.h b/src/yuzu/game_list_worker.h index 7c3074af9..6e52fca89 100644 --- a/src/yuzu/game_list_worker.h +++ b/src/yuzu/game_list_worker.h @@ -14,6 +14,7 @@  #include <QObject>  #include <QRunnable>  #include <QString> +#include <QVector>  #include "common/common_types.h"  #include "yuzu/compatibility_list.h" @@ -33,9 +34,10 @@ class GameListWorker : public QObject, public QRunnable {      Q_OBJECT  public: -    GameListWorker(std::shared_ptr<FileSys::VfsFilesystem> vfs, -                   FileSys::ManualContentProvider* provider, QString dir_path, bool deep_scan, -                   const CompatibilityList& compatibility_list); +    explicit GameListWorker(std::shared_ptr<FileSys::VfsFilesystem> vfs, +                            FileSys::ManualContentProvider* provider, +                            QVector<UISettings::GameDir>& game_dirs, +                            const CompatibilityList& compatibility_list);      ~GameListWorker() override;      /// Starts the processing of directory tree information. @@ -48,31 +50,33 @@ signals:      /**       * The `EntryReady` signal is emitted once an entry has been prepared and is ready       * to be added to the game list. -     * @param entry_items a list with `QStandardItem`s that make up the columns of the new entry. +     * @param entry_items a list with `QStandardItem`s that make up the columns of the new +     * entry.       */ -    void EntryReady(QList<QStandardItem*> entry_items); +    void DirEntryReady(GameListDir* entry_items); +    void EntryReady(QList<QStandardItem*> entry_items, GameListDir* parent_dir);      /** -     * After the worker has traversed the game directory looking for entries, this signal is emitted -     * with a list of folders that should be watched for changes as well. +     * After the worker has traversed the game directory looking for entries, this signal is +     * emitted with a list of folders that should be watched for changes as well.       */      void Finished(QStringList watch_list);  private: -    void AddTitlesToGameList(); +    void AddTitlesToGameList(GameListDir* parent_dir);      enum class ScanTarget {          FillManualContentProvider,          PopulateGameList,      }; -    void ScanFileSystem(ScanTarget target, const std::string& dir_path, unsigned int recursion = 0); +    void ScanFileSystem(ScanTarget target, const std::string& dir_path, unsigned int recursion, +                        GameListDir* parent_dir);      std::shared_ptr<FileSys::VfsFilesystem> vfs;      FileSys::ManualContentProvider* provider;      QStringList watch_list; -    QString dir_path; -    bool deep_scan;      const CompatibilityList& compatibility_list; +    QVector<UISettings::GameDir>& game_dirs;      std::atomic_bool stop_processing;  }; diff --git a/src/yuzu/main.cpp b/src/yuzu/main.cpp index ac57229d5..6d249cb3e 100644 --- a/src/yuzu/main.cpp +++ b/src/yuzu/main.cpp @@ -216,8 +216,7 @@ GMainWindow::GMainWindow()      OnReinitializeKeys(ReinitializeKeyBehavior::NoWarning);      game_list->LoadCompatibilityList(); -    game_list->PopulateAsync(UISettings::values.game_directory_path, -                             UISettings::values.game_directory_deepscan); +    game_list->PopulateAsync(UISettings::values.game_dirs);      // Show one-time "callout" messages to the user      ShowTelemetryCallout(); @@ -427,6 +426,10 @@ void GMainWindow::InitializeWidgets() {      game_list = new GameList(vfs, provider.get(), this);      ui.horizontalLayout->addWidget(game_list); +    game_list_placeholder = new GameListPlaceholder(this); +    ui.horizontalLayout->addWidget(game_list_placeholder); +    game_list_placeholder->setVisible(false); +      loading_screen = new LoadingScreen(this);      loading_screen->hide();      ui.horizontalLayout->addWidget(loading_screen); @@ -660,6 +663,7 @@ void GMainWindow::RestoreUIState() {  void GMainWindow::ConnectWidgetEvents() {      connect(game_list, &GameList::GameChosen, this, &GMainWindow::OnGameListLoadFile); +    connect(game_list, &GameList::OpenDirectory, this, &GMainWindow::OnGameListOpenDirectory);      connect(game_list, &GameList::OpenFolderRequested, this, &GMainWindow::OnGameListOpenFolder);      connect(game_list, &GameList::OpenTransferableShaderCacheRequested, this,              &GMainWindow::OnTransferableShaderCacheOpenFile); @@ -667,6 +671,11 @@ void GMainWindow::ConnectWidgetEvents() {      connect(game_list, &GameList::CopyTIDRequested, this, &GMainWindow::OnGameListCopyTID);      connect(game_list, &GameList::NavigateToGamedbEntryRequested, this,              &GMainWindow::OnGameListNavigateToGamedbEntry); +    connect(game_list, &GameList::AddDirectory, this, &GMainWindow::OnGameListAddDirectory); +    connect(game_list_placeholder, &GameListPlaceholder::AddDirectory, this, +            &GMainWindow::OnGameListAddDirectory); +    connect(game_list, &GameList::ShowList, this, &GMainWindow::OnGameListShowList); +      connect(game_list, &GameList::OpenPerGameGeneralRequested, this,              &GMainWindow::OnGameListOpenPerGameProperties); @@ -684,8 +693,6 @@ void GMainWindow::ConnectMenuEvents() {      connect(ui.action_Load_Folder, &QAction::triggered, this, &GMainWindow::OnMenuLoadFolder);      connect(ui.action_Install_File_NAND, &QAction::triggered, this,              &GMainWindow::OnMenuInstallToNAND); -    connect(ui.action_Select_Game_List_Root, &QAction::triggered, this, -            &GMainWindow::OnMenuSelectGameListRoot);      connect(ui.action_Select_NAND_Directory, &QAction::triggered, this,              [this] { OnMenuSelectEmulatedDirectory(EmulatedDirectoryTarget::NAND); });      connect(ui.action_Select_SDMC_Directory, &QAction::triggered, this, @@ -950,6 +957,7 @@ void GMainWindow::BootGame(const QString& filename) {      // Update the GUI      if (ui.action_Single_Window_Mode->isChecked()) {          game_list->hide(); +        game_list_placeholder->hide();      }      status_bar_update_timer.start(2000); @@ -1007,7 +1015,10 @@ void GMainWindow::ShutdownGame() {      render_window->hide();      loading_screen->hide();      loading_screen->Clear(); -    game_list->show(); +    if (game_list->isEmpty()) +        game_list_placeholder->show(); +    else +        game_list->show();      game_list->setFilterFocus();      UpdateWindowTitle(); @@ -1298,6 +1309,47 @@ void GMainWindow::OnGameListNavigateToGamedbEntry(u64 program_id,      QDesktopServices::openUrl(QUrl(QStringLiteral("https://yuzu-emu.org/game/") + directory));  } +void GMainWindow::OnGameListOpenDirectory(const QString& directory) { +    QString path; +    if (directory == QStringLiteral("SDMC")) { +        path = QString::fromStdString(FileUtil::GetUserPath(FileUtil::UserPath::SDMCDir) + +                                      "Nintendo/Contents/registered"); +    } else if (directory == QStringLiteral("UserNAND")) { +        path = QString::fromStdString(FileUtil::GetUserPath(FileUtil::UserPath::NANDDir) + +                                      "user/Contents/registered"); +    } else if (directory == QStringLiteral("SysNAND")) { +        path = QString::fromStdString(FileUtil::GetUserPath(FileUtil::UserPath::NANDDir) + +                                      "system/Contents/registered"); +    } else { +        path = directory; +    } +    if (!QFileInfo::exists(path)) { +        QMessageBox::critical(this, tr("Error Opening %1").arg(path), tr("Folder does not exist!")); +        return; +    } +    QDesktopServices::openUrl(QUrl::fromLocalFile(path)); +} + +void GMainWindow::OnGameListAddDirectory() { +    const QString dir_path = QFileDialog::getExistingDirectory(this, tr("Select Directory")); +    if (dir_path.isEmpty()) +        return; +    UISettings::GameDir game_dir{dir_path, false, true}; +    if (!UISettings::values.game_dirs.contains(game_dir)) { +        UISettings::values.game_dirs.append(game_dir); +        game_list->PopulateAsync(UISettings::values.game_dirs); +    } else { +        LOG_WARNING(Frontend, "Selected directory is already in the game list"); +    } +} + +void GMainWindow::OnGameListShowList(bool show) { +    if (emulation_running && ui.action_Single_Window_Mode->isChecked()) +        return; +    game_list->setVisible(show); +    game_list_placeholder->setVisible(!show); +}; +  void GMainWindow::OnGameListOpenPerGameProperties(const std::string& file) {      u64 title_id{};      const auto v_file = Core::GetGameFileFromPath(vfs, file); @@ -1316,8 +1368,7 @@ void GMainWindow::OnGameListOpenPerGameProperties(const std::string& file) {          const auto reload = UISettings::values.is_game_list_reload_pending.exchange(false);          if (reload) { -            game_list->PopulateAsync(UISettings::values.game_directory_path, -                                     UISettings::values.game_directory_deepscan); +            game_list->PopulateAsync(UISettings::values.game_dirs);          }          config->Save(); @@ -1407,8 +1458,7 @@ void GMainWindow::OnMenuInstallToNAND() {      const auto success = [this]() {          QMessageBox::information(this, tr("Successfully Installed"),                                   tr("The file was successfully installed.")); -        game_list->PopulateAsync(UISettings::values.game_directory_path, -                                 UISettings::values.game_directory_deepscan); +        game_list->PopulateAsync(UISettings::values.game_dirs);          FileUtil::DeleteDirRecursively(FileUtil::GetUserPath(FileUtil::UserPath::CacheDir) +                                         DIR_SEP + "game_list");      }; @@ -1533,14 +1583,6 @@ void GMainWindow::OnMenuInstallToNAND() {      }  } -void GMainWindow::OnMenuSelectGameListRoot() { -    QString dir_path = QFileDialog::getExistingDirectory(this, tr("Select Directory")); -    if (!dir_path.isEmpty()) { -        UISettings::values.game_directory_path = dir_path; -        game_list->PopulateAsync(dir_path, UISettings::values.game_directory_deepscan); -    } -} -  void GMainWindow::OnMenuSelectEmulatedDirectory(EmulatedDirectoryTarget target) {      const auto res = QMessageBox::information(          this, tr("Changing Emulated Directory"), @@ -1559,8 +1601,7 @@ void GMainWindow::OnMenuSelectEmulatedDirectory(EmulatedDirectoryTarget target)                                                                        : FileUtil::UserPath::NANDDir,                                dir_path.toStdString());          Service::FileSystem::CreateFactories(*vfs); -        game_list->PopulateAsync(UISettings::values.game_directory_path, -                                 UISettings::values.game_directory_deepscan); +        game_list->PopulateAsync(UISettings::values.game_dirs);      }  } @@ -1724,11 +1765,11 @@ void GMainWindow::OnConfigure() {      if (UISettings::values.enable_discord_presence != old_discord_presence) {          SetDiscordEnabled(UISettings::values.enable_discord_presence);      } +    emit UpdateThemedIcons();      const auto reload = UISettings::values.is_game_list_reload_pending.exchange(false);      if (reload) { -        game_list->PopulateAsync(UISettings::values.game_directory_path, -                                 UISettings::values.game_directory_deepscan); +        game_list->PopulateAsync(UISettings::values.game_dirs);      }      config->Save(); @@ -1992,8 +2033,7 @@ void GMainWindow::OnReinitializeKeys(ReinitializeKeyBehavior behavior) {      Service::FileSystem::CreateFactories(*vfs);      if (behavior == ReinitializeKeyBehavior::Warning) { -        game_list->PopulateAsync(UISettings::values.game_directory_path, -                                 UISettings::values.game_directory_deepscan); +        game_list->PopulateAsync(UISettings::values.game_dirs);      }  } @@ -2158,7 +2198,6 @@ void GMainWindow::UpdateUITheme() {      }      QIcon::setThemeSearchPaths(theme_paths); -    emit UpdateThemedIcons();  }  void GMainWindow::SetDiscordEnabled([[maybe_unused]] bool state) { diff --git a/src/yuzu/main.h b/src/yuzu/main.h index 501608ddc..7d16188cb 100644 --- a/src/yuzu/main.h +++ b/src/yuzu/main.h @@ -30,6 +30,7 @@ class ProfilerWidget;  class QLabel;  class WaitTreeWidget;  enum class GameListOpenTarget; +class GameListPlaceholder;  namespace Core::Frontend {  struct SoftwareKeyboardParameters; @@ -186,12 +187,13 @@ private slots:      void OnGameListCopyTID(u64 program_id);      void OnGameListNavigateToGamedbEntry(u64 program_id,                                           const CompatibilityList& compatibility_list); +    void OnGameListOpenDirectory(const QString& directory); +    void OnGameListAddDirectory(); +    void OnGameListShowList(bool show);      void OnGameListOpenPerGameProperties(const std::string& file);      void OnMenuLoadFile();      void OnMenuLoadFolder();      void OnMenuInstallToNAND(); -    /// Called whenever a user selects the "File->Select Game List Root" menu item -    void OnMenuSelectGameListRoot();      /// Called whenever a user select the "File->Select -- Directory" where -- is NAND or SD Card      void OnMenuSelectEmulatedDirectory(EmulatedDirectoryTarget target);      void OnMenuRecentFile(); @@ -223,6 +225,8 @@ private:      GameList* game_list;      LoadingScreen* loading_screen; +    GameListPlaceholder* game_list_placeholder; +      // Status bar elements      QLabel* message_label = nullptr;      QLabel* emu_speed_label = nullptr; diff --git a/src/yuzu/main.ui b/src/yuzu/main.ui index ffcabb495..a1ce3c0c3 100644 --- a/src/yuzu/main.ui +++ b/src/yuzu/main.ui @@ -62,7 +62,6 @@      <addaction name="action_Load_File"/>      <addaction name="action_Load_Folder"/>      <addaction name="separator"/> -    <addaction name="action_Select_Game_List_Root"/>      <addaction name="menu_recent_files"/>      <addaction name="separator"/>      <addaction name="action_Select_NAND_Directory"/> diff --git a/src/yuzu/uisettings.h b/src/yuzu/uisettings.h index a62cd6911..c57290006 100644 --- a/src/yuzu/uisettings.h +++ b/src/yuzu/uisettings.h @@ -8,8 +8,10 @@  #include <atomic>  #include <vector>  #include <QByteArray> +#include <QMetaType>  #include <QString>  #include <QStringList> +#include <QVector>  #include "common/common_types.h"  namespace UISettings { @@ -25,6 +27,18 @@ struct Shortcut {  using Themes = std::array<std::pair<const char*, const char*>, 2>;  extern const Themes themes; +struct GameDir { +    QString path; +    bool deep_scan; +    bool expanded; +    bool operator==(const GameDir& rhs) const { +        return path == rhs.path; +    }; +    bool operator!=(const GameDir& rhs) const { +        return !operator==(rhs); +    }; +}; +  struct Values {      QByteArray geometry;      QByteArray state; @@ -55,8 +69,9 @@ struct Values {      QString roms_path;      QString symbols_path;      QString screenshot_path; -    QString game_directory_path; -    bool game_directory_deepscan; +    QString game_dir_deprecated; +    bool game_dir_deprecated_deepscan; +    QVector<UISettings::GameDir> game_dirs;      QStringList recent_files;      QString theme; @@ -84,3 +99,5 @@ struct Values {  extern Values values;  } // namespace UISettings + +Q_DECLARE_METATYPE(UISettings::GameDir*); | 
