1 // Copyright 2016 Dolphin Emulator Project
2 // Licensed under GPLv2+
3 // Refer to the license.txt file included.
4 
5 #include "DolphinQt/Config/FilesystemWidget.h"
6 
7 #include <QApplication>
8 #include <QCoreApplication>
9 #include <QFileDialog>
10 #include <QFileInfo>
11 #include <QHeaderView>
12 #include <QMenu>
13 #include <QStandardItemModel>
14 #include <QStyleFactory>
15 #include <QTreeView>
16 #include <QVBoxLayout>
17 
18 #include <future>
19 
20 #include "DiscIO/DiscExtractor.h"
21 #include "DiscIO/Filesystem.h"
22 #include "DiscIO/Volume.h"
23 
24 #include "DolphinQt/QtUtils/ModalMessageBox.h"
25 #include "DolphinQt/QtUtils/ParallelProgressDialog.h"
26 #include "DolphinQt/Resources.h"
27 
28 #include "UICommon/UICommon.h"
29 
30 constexpr int ENTRY_PARTITION = Qt::UserRole;
31 constexpr int ENTRY_NAME = Qt::UserRole + 1;
32 constexpr int ENTRY_TYPE = Qt::UserRole + 2;
33 
34 enum class EntryType
35 {
36   Disc = -2,
37   Partition = -1,
38   File = 0,
39   Dir = 1
40 };
41 Q_DECLARE_METATYPE(EntryType);
42 
FilesystemWidget(std::shared_ptr<DiscIO::Volume> volume)43 FilesystemWidget::FilesystemWidget(std::shared_ptr<DiscIO::Volume> volume)
44     : m_volume(std::move(volume))
45 {
46   CreateWidgets();
47   ConnectWidgets();
48   PopulateView();
49 }
50 
51 FilesystemWidget::~FilesystemWidget() = default;
52 
CreateWidgets()53 void FilesystemWidget::CreateWidgets()
54 {
55   auto* layout = new QVBoxLayout;
56 
57   m_tree_model = new QStandardItemModel(0, 2);
58   m_tree_model->setHorizontalHeaderLabels({tr("Name"), tr("Size")});
59 
60   m_tree_view = new QTreeView(this);
61   m_tree_view->setModel(m_tree_model);
62   m_tree_view->setContextMenuPolicy(Qt::CustomContextMenu);
63 
64   auto* header = m_tree_view->header();
65 
66   header->setSectionResizeMode(0, QHeaderView::Stretch);
67   header->setSectionResizeMode(1, QHeaderView::ResizeToContents);
68   header->setStretchLastSection(false);
69 
70 // Windows: Set style to (old) windows, which draws branch lines
71 #ifdef _WIN32
72   if (QApplication::style()->objectName() == QStringLiteral("windowsvista"))
73     m_tree_view->setStyle(QStyleFactory::create(QStringLiteral("windows")));
74 #endif
75 
76   layout->addWidget(m_tree_view);
77 
78   setLayout(layout);
79 }
80 
ConnectWidgets()81 void FilesystemWidget::ConnectWidgets()
82 {
83   connect(m_tree_view, &QTreeView::customContextMenuRequested, this,
84           &FilesystemWidget::ShowContextMenu);
85 }
86 
PopulateView()87 void FilesystemWidget::PopulateView()
88 {
89   // Cache these two icons, the tree will use them a lot.
90   m_folder_icon = Resources::GetScaledIcon("isoproperties_folder");
91   m_file_icon = Resources::GetScaledIcon("isoproperties_file");
92 
93   auto* disc = new QStandardItem(tr("Disc"));
94   disc->setEditable(false);
95   disc->setIcon(Resources::GetScaledIcon("isoproperties_disc"));
96   disc->setData(QVariant::fromValue(EntryType::Disc), ENTRY_TYPE);
97   m_tree_model->appendRow(disc);
98   m_tree_view->expand(disc->index());
99 
100   const auto& partitions = m_volume->GetPartitions();
101 
102   for (size_t i = 0; i < partitions.size(); i++)
103   {
104     auto* item = new QStandardItem(tr("Partition %1").arg(i));
105     item->setEditable(false);
106 
107     item->setIcon(Resources::GetScaledIcon("isoproperties_disc"));
108     item->setData(static_cast<qlonglong>(i), ENTRY_PARTITION);
109     item->setData(QVariant::fromValue(EntryType::Partition), ENTRY_TYPE);
110 
111     PopulateDirectory(static_cast<int>(i), item, partitions[i]);
112 
113     disc->appendRow(item);
114 
115     if (m_volume->GetGamePartition() == partitions[i])
116       m_tree_view->expand(item->index());
117   }
118 
119   if (partitions.empty())
120     PopulateDirectory(-1, disc, DiscIO::PARTITION_NONE);
121 }
122 
PopulateDirectory(int partition_id,QStandardItem * root,const DiscIO::Partition & partition)123 void FilesystemWidget::PopulateDirectory(int partition_id, QStandardItem* root,
124                                          const DiscIO::Partition& partition)
125 {
126   const DiscIO::FileSystem* const file_system = m_volume->GetFileSystem(partition);
127   if (file_system)
128     PopulateDirectory(partition_id, root, file_system->GetRoot());
129 }
130 
PopulateDirectory(int partition_id,QStandardItem * root,const DiscIO::FileInfo & directory)131 void FilesystemWidget::PopulateDirectory(int partition_id, QStandardItem* root,
132                                          const DiscIO::FileInfo& directory)
133 {
134   for (const auto& info : directory)
135   {
136     auto* item = new QStandardItem(QString::fromStdString(info.GetName()));
137     item->setEditable(false);
138     item->setIcon(info.IsDirectory() ? m_folder_icon : m_file_icon);
139 
140     if (info.IsDirectory())
141       PopulateDirectory(partition_id, item, info);
142 
143     item->setData(partition_id, ENTRY_PARTITION);
144     item->setData(QString::fromStdString(info.GetPath()), ENTRY_NAME);
145     item->setData(QVariant::fromValue(info.IsDirectory() ? EntryType::Dir : EntryType::File),
146                   ENTRY_TYPE);
147 
148     const auto size = info.GetTotalSize();
149 
150     auto* size_item = new QStandardItem(QString::fromStdString(UICommon::FormatSize(size)));
151     size_item->setTextAlignment(Qt::AlignRight);
152     size_item->setEditable(false);
153 
154     root->appendRow({item, size_item});
155   }
156 }
157 
SelectFolder()158 QString FilesystemWidget::SelectFolder()
159 {
160   return QFileDialog::getExistingDirectory(this, QObject::tr("Choose the folder to extract to"));
161 }
162 
ShowContextMenu(const QPoint &)163 void FilesystemWidget::ShowContextMenu(const QPoint&)
164 {
165   auto* selection = m_tree_view->selectionModel();
166   if (!selection->hasSelection())
167     return;
168 
169   auto* item = m_tree_model->itemFromIndex(selection->selectedIndexes()[0]);
170 
171   QMenu* menu = new QMenu(this);
172 
173   EntryType type = item->data(ENTRY_TYPE).value<EntryType>();
174 
175   DiscIO::Partition partition = type == EntryType::Disc ?
176                                     DiscIO::PARTITION_NONE :
177                                     GetPartitionFromID(item->data(ENTRY_PARTITION).toInt());
178   QString path = item->data(ENTRY_NAME).toString();
179 
180   const bool is_filesystem_root = (type == EntryType::Disc && m_volume->GetPartitions().empty()) ||
181                                   type == EntryType::Partition;
182 
183   if (type == EntryType::Dir || is_filesystem_root)
184   {
185     menu->addAction(tr("Extract Files..."), this, [this, partition, path] {
186       auto folder = SelectFolder();
187 
188       if (!folder.isEmpty())
189         ExtractDirectory(partition, path, folder);
190     });
191   }
192 
193   if (is_filesystem_root)
194   {
195     menu->addAction(tr("Extract System Data..."), this, [this, partition] {
196       auto folder = SelectFolder();
197 
198       if (folder.isEmpty())
199         return;
200 
201       if (ExtractSystemData(partition, folder))
202         ModalMessageBox::information(this, tr("Success"),
203                                      tr("Successfully extracted system data."));
204       else
205         ModalMessageBox::critical(this, tr("Error"), tr("Failed to extract system data."));
206     });
207   }
208 
209   switch (type)
210   {
211   case EntryType::Disc:
212     menu->addAction(tr("Extract Entire Disc..."), this, [this, path] {
213       auto folder = SelectFolder();
214 
215       if (folder.isEmpty())
216         return;
217 
218       if (m_volume->GetPartitions().empty())
219       {
220         ExtractPartition(DiscIO::PARTITION_NONE, folder);
221       }
222       else
223       {
224         for (DiscIO::Partition& p : m_volume->GetPartitions())
225         {
226           if (const std::optional<u32> partition_type = m_volume->GetPartitionType(p))
227           {
228             const std::string partition_name = DiscIO::NameForPartitionType(*partition_type, true);
229             ExtractPartition(p, folder + QChar(u'/') + QString::fromStdString(partition_name));
230           }
231         }
232       }
233     });
234     break;
235   case EntryType::Partition:
236     menu->addAction(tr("Extract Entire Partition..."), this, [this, partition] {
237       auto folder = SelectFolder();
238       if (!folder.isEmpty())
239         ExtractPartition(partition, folder);
240     });
241     break;
242   case EntryType::File:
243     menu->addAction(tr("Extract File..."), this, [this, partition, path] {
244       auto dest =
245           QFileDialog::getSaveFileName(this, tr("Save File to"), QFileInfo(path).fileName());
246 
247       if (!dest.isEmpty())
248         ExtractFile(partition, path, dest);
249     });
250     break;
251   case EntryType::Dir:
252     // Handled above the switch statement
253     break;
254   }
255 
256   menu->exec(QCursor::pos());
257 }
258 
GetPartitionFromID(int id)259 DiscIO::Partition FilesystemWidget::GetPartitionFromID(int id)
260 {
261   return id == -1 ? DiscIO::PARTITION_NONE : m_volume->GetPartitions()[id];
262 }
263 
ExtractPartition(const DiscIO::Partition & partition,const QString & out)264 void FilesystemWidget::ExtractPartition(const DiscIO::Partition& partition, const QString& out)
265 {
266   ExtractDirectory(partition, QString{}, out + QStringLiteral("/files"));
267   ExtractSystemData(partition, out);
268 }
269 
ExtractSystemData(const DiscIO::Partition & partition,const QString & out)270 bool FilesystemWidget::ExtractSystemData(const DiscIO::Partition& partition, const QString& out)
271 {
272   return DiscIO::ExportSystemData(*m_volume, partition, out.toStdString());
273 }
274 
ExtractDirectory(const DiscIO::Partition & partition,const QString & path,const QString & out)275 void FilesystemWidget::ExtractDirectory(const DiscIO::Partition& partition, const QString& path,
276                                         const QString& out)
277 {
278   const DiscIO::FileSystem* filesystem = m_volume->GetFileSystem(partition);
279   if (!filesystem)
280     return;
281 
282   std::unique_ptr<DiscIO::FileInfo> info = filesystem->FindFileInfo(path.toStdString());
283   u32 size = info->GetTotalChildren();
284 
285   ParallelProgressDialog dialog(this);
286   dialog.GetRaw()->setMinimum(0);
287   dialog.GetRaw()->setMaximum(size);
288   dialog.GetRaw()->setWindowTitle(tr("Progress"));
289 
290   const bool all = path.isEmpty();
291 
292   std::future<void> future = std::async(std::launch::async, [&] {
293     int progress = 0;
294 
295     DiscIO::ExportDirectory(
296         *m_volume, partition, *info, true, path.toStdString(), out.toStdString(),
297         [all, &dialog, &progress](const std::string& current) {
298           dialog.SetLabelText(
299               (all ? QObject::tr("Extracting All Files...") :
300                      QObject::tr("Extracting Directory..."))
301                   .append(QStringLiteral(" %1").arg(QString::fromStdString(current))));
302           dialog.SetValue(++progress);
303 
304           QCoreApplication::processEvents();
305           return dialog.WasCanceled();
306         });
307 
308     dialog.Reset();
309   });
310 
311   dialog.GetRaw()->exec();
312   future.get();
313 }
314 
ExtractFile(const DiscIO::Partition & partition,const QString & path,const QString & out)315 void FilesystemWidget::ExtractFile(const DiscIO::Partition& partition, const QString& path,
316                                    const QString& out)
317 {
318   const DiscIO::FileSystem* filesystem = m_volume->GetFileSystem(partition);
319   if (!filesystem)
320     return;
321 
322   bool success = DiscIO::ExportFile(
323       *m_volume, partition, filesystem->FindFileInfo(path.toStdString()).get(), out.toStdString());
324 
325   if (success)
326     ModalMessageBox::information(this, tr("Success"), tr("Successfully extracted file."));
327   else
328     ModalMessageBox::critical(this, tr("Error"), tr("Failed to extract file."));
329 }
330