• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

ossia / score / 31975605394

16 Aug 2026 10:10PM UTC coverage: 20.35%. First build
31975605394

Pull #2211

github

web-flow
Merge 1defa584c into bb5e533af
Pull Request #2211: project: collect, relink, trim and archive a project's files

1182 of 1996 new or added lines in 43 files covered. (59.22%)

43464 of 213584 relevant lines covered (20.35%)

5552.46 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

42.17
/src/plugins/score-lib-process/Process/MissingFilesDialog.cpp
1
#include <Process/MissingFilesDialog.hpp>
2

3
#include <score/document/DocumentContext.hpp>
4

5
#include <core/document/Document.hpp>
6

7
#include <QApplication>
8
#include <QDialogButtonBox>
9
#include <QFileDialog>
10
#include <QFileInfo>
11
#include <QGuiApplication>
12
#include <QHBoxLayout>
13
#include <QHeaderView>
14
#include <QLabel>
15
#include <QMessageBox>
16
#include <QPushButton>
17
#include <QSettings>
18
#include <QTreeWidget>
19
#include <QVBoxLayout>
20

21
namespace Process
22
{
23
namespace
24
{
25
//! See the note in FileReportView.cpp: anonymous namespaces are merged across
26
//! this library under a unity build.
27
enum class MissingColumn : int
28
{
29
  Owner = 0,
30
  File,
31
  Found
32
};
33

34
constexpr auto last_folder_setting = "Project/LastRelinkFolder";
35
}
36

37
MissingFilesDialog::MissingFilesDialog(score::Document& doc, QWidget* parent)
1✔
38
    : QDialog{parent}
1✔
39
    , m_doc{&doc}
1✔
40
{
1✔
41
  setWindowTitle(tr("Missing files"));
1✔
42
  setAttribute(Qt::WA_DeleteOnClose);
1✔
43
  resize(860, 460);
1✔
44

45
  auto lay = new QVBoxLayout{this};
1✔
46

47
  m_summary = new QLabel{this};
1✔
48
  m_summary->setWordWrap(true);
1✔
49
  lay->addWidget(m_summary);
1✔
50

51
  m_files = new QTreeWidget{this};
1✔
52
  m_files->setRootIsDecorated(false);
1✔
53
  m_files->setAlternatingRowColors(true);
1✔
54
  m_files->setColumnCount(3);
1✔
55
  m_files->setHeaderLabels({tr("Used by"), tr("Missing file"), tr("Found at")});
1✔
56
  m_files->header()->setSectionResizeMode((int)MissingColumn::File, QHeaderView::Stretch);
1✔
57
  m_files->header()->setSectionResizeMode((int)MissingColumn::Found, QHeaderView::Stretch);
1✔
58
  lay->addWidget(m_files, 1);
1✔
59

60
  auto tools = new QHBoxLayout;
1✔
61
  lay->addLayout(tools);
1✔
62

63
  m_search = new QPushButton{tr("Search a folder..."), this};
1✔
64
  m_search->setToolTip(
1✔
65
      tr("Looks through that folder and everything under it for files with the "
1✔
66
         "same names. Nothing is changed until you apply."));
67
  tools->addWidget(m_search);
1✔
68

69
  m_locate = new QPushButton{tr("Locate..."), this};
1✔
70
  m_locate->setToolTip(tr("Pick the file for the selected row by hand."));
1✔
71
  tools->addWidget(m_locate);
1✔
72
  tools->addStretch(1);
1✔
73

74
  auto buttons
1✔
75
      = new QDialogButtonBox{QDialogButtonBox::Apply | QDialogButtonBox::Close, this};
1✔
76
  m_apply = buttons->button(QDialogButtonBox::Apply);
1✔
77
  m_apply->setText(tr("Relink"));
1✔
78
  lay->addWidget(buttons);
1✔
79

80
  connect(m_search, &QPushButton::clicked, this, &MissingFilesDialog::searchFolder);
1✔
81
  connect(m_locate, &QPushButton::clicked, this, &MissingFilesDialog::locateSelected);
1✔
82
  connect(m_apply, &QPushButton::clicked, this, &MissingFilesDialog::applyRelink);
1✔
83
  connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::close);
1✔
84
  connect(
1✔
85
      m_files, &QTreeWidget::itemSelectionChanged, this,
1✔
86
      [this] { m_locate->setEnabled(m_files->currentItem() != nullptr); });
1✔
87

88
  m_lastSearchFolder = QSettings{}.value(last_folder_setting).toString();
1✔
89

90
  rescan();
1✔
NEW
91
}
×
92

NEW
93
MissingFilesDialog::~MissingFilesDialog() = default;
×
94

95
bool MissingFilesDialog::nothingMissing(const score::DocumentContext& ctx)
2✔
96
{
97
  return scanMissingFiles(ctx).count(FileAction::Missing) == 0;
2✔
98
}
99

100
void MissingFilesDialog::rescan()
1✔
101
{
102
  // The document can be closed while this window is up: it is not modal on
103
  // purpose. Go away rather than read a context that no longer exists.
104
  if(!m_doc)
1✔
105
  {
NEW
106
    close();
×
NEW
107
    return;
×
108
  }
109

110
  m_report = scanMissingFiles(m_doc->context());
1✔
111
  m_files->clear();
1✔
112

113
  QList<QTreeWidgetItem*> items;
1✔
114
  for(const auto* e : m_report.with(FileAction::Missing))
2✔
115
  {
116
    auto* item = new QTreeWidgetItem{{e->owner, e->storedPath, tr("not found")}};
1✔
117
    item->setData((int)MissingColumn::File, Qt::UserRole, e->storedPath);
1✔
118
    items.push_back(item);
1✔
119
  }
120
  m_files->addTopLevelItems(items);
1✔
121

122
  // A reference the user already resolved keeps its answer across a rescan.
123
  for(auto it = m_resolutions.constBegin(); it != m_resolutions.constEnd(); ++it)
1✔
124
  {
NEW
125
    if(it->chosen < 0)
×
NEW
126
      continue;
×
NEW
127
    if(auto* item = itemFor(it.key()))
×
NEW
128
      item->setText((int)MissingColumn::Found, it->candidates[it->chosen]);
×
NEW
129
  }
×
130

131
  updateSummary();
1✔
132
}
1✔
133

NEW
134
QTreeWidgetItem* MissingFilesDialog::itemFor(const QString& storedPath) const
×
135
{
NEW
136
  for(int i = 0; i < m_files->topLevelItemCount(); i++)
×
137
  {
NEW
138
    auto* item = m_files->topLevelItem(i);
×
NEW
139
    if(item->data((int)MissingColumn::File, Qt::UserRole).toString() == storedPath)
×
NEW
140
      return item;
×
NEW
141
  }
×
NEW
142
  return nullptr;
×
NEW
143
}
×
144

145
void MissingFilesDialog::updateSummary()
1✔
146
{
147
  const int missing = m_files->topLevelItemCount();
1✔
148
  int resolved = 0;
1✔
149
  for(auto it = m_resolutions.constBegin(); it != m_resolutions.constEnd(); ++it)
1✔
NEW
150
    if(it->chosen >= 0 && itemFor(it.key()))
×
NEW
151
      ++resolved;
×
152

153
  if(missing == 0)
1✔
154
  {
NEW
155
    m_summary->setText(tr("Every file this project uses was found."));
×
NEW
156
  }
×
157
  else
158
  {
159
    m_summary->setText(
2✔
160
        tr("<b>%1 file(s) cannot be found.</b> The project still opens and plays "
1✔
161
           "everything else; the processes using them stay silent until they are "
162
           "relinked.<br/>%2 of them have a candidate ready to apply.")
163
            .arg(missing)
1✔
164
            .arg(resolved));
1✔
165
  }
166

167
  m_apply->setEnabled(resolved > 0);
1✔
168
  m_locate->setEnabled(m_files->currentItem() != nullptr);
1✔
169
}
1✔
170

NEW
171
void MissingFilesDialog::searchFolder()
×
172
{
NEW
173
  const QString folder = QFileDialog::getExistingDirectory(
×
NEW
174
      this, tr("Look for the missing files in"), m_lastSearchFolder);
×
NEW
175
  if(folder.isEmpty())
×
NEW
176
    return;
×
177

NEW
178
  m_lastSearchFolder = folder;
×
NEW
179
  QSettings{}.setValue(last_folder_setting, folder);
×
180

NEW
181
  FileIndex index;
×
182
  {
183
    // Indexing a large drive takes a while and gives no feedback otherwise.
NEW
184
    QApplication::setOverrideCursor(Qt::WaitCursor);
×
NEW
185
    index.scan(folder);
×
NEW
186
    QApplication::restoreOverrideCursor();
×
187
  }
188

NEW
189
  int found = 0;
×
NEW
190
  for(int i = 0; i < m_files->topLevelItemCount(); i++)
×
191
  {
NEW
192
    auto* item = m_files->topLevelItem(i);
×
NEW
193
    const QString stored = item->data((int)MissingColumn::File, Qt::UserRole).toString();
×
194

NEW
195
    auto candidates = index.candidates(stored);
×
NEW
196
    if(candidates.empty())
×
NEW
197
      continue;
×
198

NEW
199
    Resolution res{std::move(candidates), 0};
×
NEW
200
    item->setText(
×
201
        (int)MissingColumn::Found,
NEW
202
        res.candidates.size() == 1
×
NEW
203
            ? res.candidates[0]
×
NEW
204
            : tr("%1  (+%2 other candidate(s))")
×
NEW
205
                  .arg(res.candidates[0])
×
NEW
206
                  .arg(res.candidates.size() - 1));
×
NEW
207
    item->setToolTip((int)MissingColumn::Found, res.candidates[0]);
×
NEW
208
    m_resolutions.insert(stored, std::move(res));
×
NEW
209
    ++found;
×
NEW
210
  }
×
211

NEW
212
  if(found == 0)
×
213
  {
NEW
214
    QMessageBox::information(
×
NEW
215
        this, tr("Nothing found"),
×
NEW
216
        index.truncated()
×
NEW
217
            ? tr("No matching file name under %1 -- and the search stopped early "
×
218
                 "because that folder holds too many files. Try pointing it at a "
219
                 "narrower folder.")
NEW
220
                  .arg(folder)
×
NEW
221
            : tr("No file with a matching name was found under %1.").arg(folder));
×
NEW
222
  }
×
223

NEW
224
  updateSummary();
×
NEW
225
}
×
226

NEW
227
void MissingFilesDialog::locateSelected()
×
228
{
NEW
229
  auto* item = m_files->currentItem();
×
NEW
230
  if(!item)
×
NEW
231
    return;
×
232

NEW
233
  const QString stored = item->data((int)MissingColumn::File, Qt::UserRole).toString();
×
NEW
234
  const QString name = QFileInfo{stored}.fileName();
×
235

NEW
236
  const QString picked = QFileDialog::getOpenFileName(
×
NEW
237
      this, tr("Locate %1").arg(name), m_lastSearchFolder);
×
NEW
238
  if(picked.isEmpty())
×
NEW
239
    return;
×
240

NEW
241
  m_lastSearchFolder = QFileInfo{picked}.absolutePath();
×
NEW
242
  m_resolutions.insert(stored, Resolution{{picked}, 0});
×
NEW
243
  item->setText((int)MissingColumn::Found, picked);
×
NEW
244
  item->setToolTip((int)MissingColumn::Found, picked);
×
245

NEW
246
  updateSummary();
×
NEW
247
}
×
248

NEW
249
void MissingFilesDialog::applyRelink()
×
250
{
NEW
251
  if(!m_doc)
×
252
  {
NEW
253
    close();
×
NEW
254
    return;
×
255
  }
256

NEW
257
  QHash<QString, QString> chosen;
×
NEW
258
  for(auto it = m_resolutions.constBegin(); it != m_resolutions.constEnd(); ++it)
×
NEW
259
    if(it->chosen >= 0 && itemFor(it.key()))
×
NEW
260
      chosen.insert(it.key(), it->candidates[it->chosen]);
×
261

NEW
262
  if(chosen.isEmpty())
×
NEW
263
    return;
×
264

NEW
265
  const auto report = relinkFiles(m_doc->context(), chosen);
×
266

NEW
267
  if(const int failed = report.count(FileAction::Failed); failed > 0)
×
268
  {
NEW
269
    QString detail;
×
NEW
270
    for(const auto* e : report.with(FileAction::Failed))
×
NEW
271
      detail += e->note + '\n';
×
NEW
272
    QMessageBox::warning(this, tr("Some files could not be relinked"), detail);
×
NEW
273
  }
×
274

NEW
275
  m_resolutions.clear();
×
NEW
276
  rescan();
×
NEW
277
}
×
278
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc