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

ossia / score / 35742451820

22 Sep 2026 02:44PM UTC coverage: 40.337% (+0.3%) from 40.008%
35742451820

push

github

jcelerier
gfx: name DirectShow camera formats by codec, and widen the subtypes

A compressed pin carries no pixel format, so CameraSettings holds
AV_PIX_FMT_NONE for it and av_get_pix_fmt_name answers nullptr. The device
list's description built its name from that with only MJPEG special-cased,
which left every other compressed codec assigning a null pointer to a
std::string. Gfx::cameraFormatName names a format from its codec when it has
no layout and from its layout otherwise, and neither lookup is used
unchecked.

The compressed half of the subtype decision now goes to the fourcc -> codec
table FFmpeg maintains for RIFF/AVI, which is what ffmpeg's own dshow device
resolves a pin through, rather than the six-entry list it replaces. H.264's
eight spellings, DV's five, VP8/VP9/AV1, MPEG-4/DIVX/Xvid, H.263, JPEG 2000,
v210 and the lossless card codecs all arrive from there. The layout table is
consulted first, so a fourcc it names stays raw even where that table calls
it a codec -- Y41P, which we unpack.

What that table lacks and DirectShow emits is listed alongside it: H.265 in
both spellings in the wild (UVC cameras label the pin 'H265', OBS and the LAV
filters 'HEVC'), the CFCC/IJPG/TVMJ/WAKE/Plum Motion-JPEG fourccs from
dshow.h, and MDVF. A camera whose only pin is H.265 had no format to offer
at all.

The raw table gains the aliases FFmpeg's own raw table carries, so a pin
spelt any of them resolves to the layout we offer it under: HDYC (the UYVY
capture cards present), the other packed 4:2:2 spellings, the planar AVI
ones, YUV9, Y411 and 'Y16 '.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QnTb3Zd2ctRWoTs5wMtoiC

51 of 52 new or added lines in 3 files covered. (98.08%)

8408 existing lines in 137 files now uncovered.

119310 of 295781 relevant lines covered (40.34%)

112391.58 hits per line

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

42.41
/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 <QDialogButtonBox>
8
#include <QFileDialog>
9
#include <QFileInfo>
10
#include <QFontMetrics>
11
#include <QHBoxLayout>
12
#include <QHeaderView>
13
#include <QLabel>
14
#include <QMessageBox>
15
#include <QPushButton>
16
#include <QSettings>
17
#include <QTimer>
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
constexpr auto missing_files_header_setting = "Project/MissingFilesHeader";
36

37
//! Size of the missing file, when the document knows it, for ranking.
38
constexpr int SizeRole = Qt::UserRole + 1;
39
}
40

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

49
  auto lay = new QVBoxLayout{this};
1✔
50

51
  m_summary = new QLabel{this};
1✔
52
  m_summary->setWordWrap(true);
1✔
53
  lay->addWidget(m_summary);
1✔
54

55
  m_files = new QTreeWidget{this};
1✔
56
  m_files->setRootIsDecorated(false);
1✔
57
  m_files->setAlternatingRowColors(true);
1✔
58
  m_files->setColumnCount(3);
1✔
59
  m_files->setHeaderLabels({tr("Used by"), tr("Missing file"), tr("Found at")});
1✔
60
  lay->addWidget(m_files, 1);
1✔
61

62
  // Stretched sections cannot be dragged.
63
  auto* header = m_files->header();
1✔
64
  header->setSectionResizeMode(QHeaderView::Interactive);
1✔
65
  header->setSectionsMovable(true);
1✔
66
  header->setStretchLastSection(false);
1✔
67
  header->resizeSection((int)MissingColumn::Owner, 180);
1✔
68
  header->resizeSection((int)MissingColumn::File, 320);
1✔
69
  header->resizeSection((int)MissingColumn::Found, 320);
1✔
70
  header->restoreState(QSettings{}.value(missing_files_header_setting).toByteArray());
1✔
71

72
  auto tools = new QHBoxLayout;
1✔
73
  lay->addLayout(tools);
1✔
74

75
  m_search = new QPushButton{tr("Search a folder..."), this};
1✔
76
  m_search->setToolTip(
1✔
77
      tr("Looks through that folder and everything under it for files with the "
1✔
78
         "same names. Nothing is changed until you apply."));
79
  tools->addWidget(m_search);
1✔
80

81
  m_locate = new QPushButton{tr("Locate..."), this};
1✔
82
  m_locate->setToolTip(tr("Pick the file for the selected row by hand."));
1✔
83
  tools->addWidget(m_locate);
1✔
84
  tools->addStretch(1);
1✔
85

86
  m_progressRow = new QWidget{this};
1✔
87
  {
88
    auto progressLayout = new QHBoxLayout{m_progressRow};
1✔
89
    progressLayout->setContentsMargins(0, 0, 0, 0);
1✔
90

91
    m_progress = new QLabel{m_progressRow};
1✔
92
    m_progress->setTextFormat(Qt::PlainText);
1✔
93
    m_progress->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Preferred);
1✔
94
    m_progress->setFixedHeight(2 * m_progress->fontMetrics().lineSpacing());
1✔
95
    m_progress->setAlignment(Qt::AlignLeft | Qt::AlignVCenter);
1✔
96
    progressLayout->addWidget(m_progress, 1);
1✔
97

98
    m_cancel = new QPushButton{tr("Cancel"), m_progressRow};
1✔
99
    progressLayout->addWidget(m_cancel);
1✔
100
  }
101
  m_progressRow->hide();
1✔
102
  lay->addWidget(m_progressRow);
1✔
103

104
  m_progressTimer = new QTimer{this};
1✔
105
  m_progressTimer->setInterval(100);
1✔
106

107
  auto buttons
1✔
108
      = new QDialogButtonBox{QDialogButtonBox::Apply | QDialogButtonBox::Close, this};
1✔
109
  m_apply = buttons->button(QDialogButtonBox::Apply);
1✔
110
  m_apply->setText(tr("Relink"));
1✔
111
  lay->addWidget(buttons);
1✔
112

113
  connect(m_search, &QPushButton::clicked, this, &MissingFilesDialog::searchFolder);
1✔
114
  connect(m_cancel, &QPushButton::clicked, this, &MissingFilesDialog::cancelSearch);
1✔
115
  connect(
1✔
116
      m_progressTimer, &QTimer::timeout, this,
1✔
117
      &MissingFilesDialog::updateSearchProgress);
118
  connect(m_locate, &QPushButton::clicked, this, &MissingFilesDialog::locateSelected);
1✔
119
  connect(m_apply, &QPushButton::clicked, this, &MissingFilesDialog::applyRelink);
1✔
120
  connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::close);
1✔
121
  connect(
1✔
122
      m_files, &QTreeWidget::itemSelectionChanged, this,
1✔
123
      [this] { m_locate->setEnabled(m_files->currentItem() != nullptr); });
1✔
124

125
  m_lastSearchFolder = QSettings{}.value(last_folder_setting).toString();
1✔
126

127
  rescan();
1✔
128
}
×
129

UNCOV
130
MissingFilesDialog::~MissingFilesDialog()
×
UNCOV
131
{
×
UNCOV
132
  if(m_scan)
×
UNCOV
133
    m_scan->cancel();
×
134

UNCOV
135
  QSettings{}.setValue(missing_files_header_setting, m_files->header()->saveState());
×
136
}
×
137

138
bool MissingFilesDialog::nothingMissing(const score::DocumentContext& ctx)
19✔
139
{
140
  return scanMissingFiles(ctx).count(FileAction::Missing) == 0;
19✔
141
}
142

143
void MissingFilesDialog::rescan()
1✔
144
{
145
  // The document can be closed while this window is up: it is not modal on
146
  // purpose. Go away rather than read a context that no longer exists.
147
  if(!m_doc)
1✔
148
  {
UNCOV
149
    close();
×
150
    return;
×
151
  }
152

153
  m_report = scanMissingFiles(m_doc->context());
1✔
154
  m_files->clear();
1✔
155

156
  QList<QTreeWidgetItem*> items;
1✔
157
  for(const auto* e : m_report.with(FileAction::Missing))
2✔
158
  {
159
    auto* item = new QTreeWidgetItem{{e->owner, e->storedPath, tr("not found")}};
1✔
160
    item->setData((int)MissingColumn::File, Qt::UserRole, e->storedPath);
1✔
161
    item->setData((int)MissingColumn::File, SizeRole, e->size);
1✔
162
    items.push_back(item);
1✔
163
  }
164
  m_files->addTopLevelItems(items);
1✔
165

166
  // A reference the user already resolved keeps its answer across a rescan.
167
  for(auto it = m_resolutions.constBegin(); it != m_resolutions.constEnd(); ++it)
1✔
168
  {
UNCOV
169
    if(it->chosen < 0)
×
UNCOV
170
      continue;
×
171
    if(auto* item = itemFor(it.key()))
×
UNCOV
172
      item->setText((int)MissingColumn::Found, it->candidates[it->chosen]);
×
173
  }
×
174

175
  updateSummary();
1✔
176
}
1✔
177

178
QTreeWidgetItem* MissingFilesDialog::itemFor(const QString& storedPath) const
×
179
{
UNCOV
180
  for(int i = 0; i < m_files->topLevelItemCount(); i++)
×
181
  {
UNCOV
182
    auto* item = m_files->topLevelItem(i);
×
UNCOV
183
    if(item->data((int)MissingColumn::File, Qt::UserRole).toString() == storedPath)
×
184
      return item;
×
185
  }
×
186
  return nullptr;
×
UNCOV
187
}
×
188

189
void MissingFilesDialog::updateSummary()
1✔
190
{
191
  const int missing = m_files->topLevelItemCount();
1✔
192
  int resolved = 0;
1✔
193
  for(auto it = m_resolutions.constBegin(); it != m_resolutions.constEnd(); ++it)
1✔
UNCOV
194
    if(it->chosen >= 0 && itemFor(it.key()))
×
195
      ++resolved;
×
196

197
  if(missing == 0)
1✔
198
  {
199
    m_summary->setText(tr("Every file this project uses was found."));
×
200
  }
×
201
  else
202
  {
203
    m_summary->setText(
2✔
204
        tr("<b>%1 file(s) cannot be found.</b> The project still opens and plays "
1✔
205
           "everything else; the processes using them stay silent until they are "
206
           "relinked.<br/>%2 of them have a candidate ready to apply.")
207
            .arg(missing)
1✔
208
            .arg(resolved));
1✔
209
  }
210

211
  m_apply->setEnabled(resolved > 0);
1✔
212
  m_locate->setEnabled(m_files->currentItem() != nullptr);
1✔
213
}
1✔
214

215
void MissingFilesDialog::searchFolder()
×
216
{
217
  if(m_scan)
×
UNCOV
218
    return;
×
219

220
  const QString folder = QFileDialog::getExistingDirectory(
×
221
      this, tr("Look for the missing files in"), m_lastSearchFolder);
×
222
  if(folder.isEmpty())
×
UNCOV
223
    return;
×
224

225
  m_lastSearchFolder = folder;
×
UNCOV
226
  m_searchRoot = folder;
×
227
  QSettings{}.setValue(last_folder_setting, folder);
×
228

229
  m_search->setEnabled(false);
×
230
  m_cancel->setEnabled(true);
×
231
  m_progress->setText(tr("Searching %1...").arg(folder));
×
UNCOV
232
  m_progressRow->show();
×
233

234
  m_scan = FileScan::start(folder, this, [this](FileIndex index, bool cancelled) {
×
UNCOV
235
    finishSearch(std::move(index), cancelled);
×
236
  });
×
237
  m_progressTimer->start();
×
238
}
×
239

UNCOV
240
void MissingFilesDialog::cancelSearch()
×
241
{
242
  if(!m_scan)
×
243
    return;
×
244

UNCOV
245
  m_scan->cancel();
×
246
  m_cancel->setEnabled(false);
×
247
  m_progressTimer->stop();
×
UNCOV
248
  m_progress->setText(tr("Stopping..."));
×
249
}
×
250

251
void MissingFilesDialog::updateSearchProgress()
×
252
{
253
  if(!m_scan)
×
254
    return;
×
255

256
  // Elide the folder alone: elidedText works on a single line, and the count
257
  // must stay readable however deep the folder is.
258
  QString folder = m_scan->currentFolder();
×
259
  if(const int room = m_progress->width(); room > 0)
×
260
    folder = m_progress->fontMetrics().elidedText(folder, Qt::ElideMiddle, room);
×
261

262
  m_progress->setText(
×
263
      tr("%1 file(s) seen").arg(m_scan->filesSeen()) + QLatin1Char('\n') + folder);
×
UNCOV
264
}
×
265

UNCOV
266
void MissingFilesDialog::finishSearch(FileIndex index, bool cancelled)
×
267
{
UNCOV
268
  m_progressTimer->stop();
×
269
  m_progressRow->hide();
×
270
  m_search->setEnabled(true);
×
271
  m_scan.reset();
×
272

273
  int found = 0;
×
UNCOV
274
  for(int i = 0; i < m_files->topLevelItemCount(); i++)
×
275
  {
276
    auto* item = m_files->topLevelItem(i);
×
277
    const QString stored = item->data((int)MissingColumn::File, Qt::UserRole).toString();
×
UNCOV
278
    const qint64 size = item->data((int)MissingColumn::File, SizeRole).toLongLong();
×
279

UNCOV
280
    auto candidates = index.candidates(stored, size);
×
UNCOV
281
    if(candidates.empty())
×
UNCOV
282
      continue;
×
283

UNCOV
284
    Resolution res{std::move(candidates), 0};
×
UNCOV
285
    item->setText(
×
286
        (int)MissingColumn::Found,
UNCOV
287
        res.candidates.size() == 1
×
UNCOV
288
            ? res.candidates[0]
×
UNCOV
289
            : tr("%1  (+%2 other candidate(s))")
×
UNCOV
290
                  .arg(res.candidates[0])
×
UNCOV
291
                  .arg(res.candidates.size() - 1));
×
UNCOV
292
    item->setToolTip((int)MissingColumn::Found, res.candidates[0]);
×
UNCOV
293
    m_resolutions.insert(stored, std::move(res));
×
UNCOV
294
    ++found;
×
UNCOV
295
  }
×
296

UNCOV
297
  if(found == 0 && !cancelled)
×
298
  {
UNCOV
299
    QMessageBox::information(
×
UNCOV
300
        this, tr("Nothing found"),
×
UNCOV
301
        tr("No file with a matching name was found under %1.").arg(m_searchRoot));
×
UNCOV
302
  }
×
303

UNCOV
304
  updateSummary();
×
UNCOV
305
}
×
306

UNCOV
307
void MissingFilesDialog::locateSelected()
×
308
{
UNCOV
309
  auto* item = m_files->currentItem();
×
UNCOV
310
  if(!item)
×
UNCOV
311
    return;
×
312

UNCOV
313
  const QString stored = item->data((int)MissingColumn::File, Qt::UserRole).toString();
×
UNCOV
314
  const QString name = QFileInfo{stored}.fileName();
×
315

UNCOV
316
  const QString picked = QFileDialog::getOpenFileName(
×
UNCOV
317
      this, tr("Locate %1").arg(name), m_lastSearchFolder);
×
UNCOV
318
  if(picked.isEmpty())
×
UNCOV
319
    return;
×
320

UNCOV
321
  m_lastSearchFolder = QFileInfo{picked}.absolutePath();
×
UNCOV
322
  m_resolutions.insert(stored, Resolution{{picked}, 0});
×
UNCOV
323
  item->setText((int)MissingColumn::Found, picked);
×
UNCOV
324
  item->setToolTip((int)MissingColumn::Found, picked);
×
325

UNCOV
326
  updateSummary();
×
UNCOV
327
}
×
328

UNCOV
329
void MissingFilesDialog::applyRelink()
×
330
{
UNCOV
331
  if(!m_doc)
×
332
  {
UNCOV
333
    close();
×
UNCOV
334
    return;
×
335
  }
336

UNCOV
337
  QHash<QString, QString> chosen;
×
UNCOV
338
  for(auto it = m_resolutions.constBegin(); it != m_resolutions.constEnd(); ++it)
×
UNCOV
339
    if(it->chosen >= 0 && itemFor(it.key()))
×
UNCOV
340
      chosen.insert(it.key(), it->candidates[it->chosen]);
×
341

UNCOV
342
  if(chosen.isEmpty())
×
UNCOV
343
    return;
×
344

UNCOV
345
  const auto report = relinkFiles(m_doc->context(), chosen);
×
346

UNCOV
347
  if(const int failed = report.count(FileAction::Failed); failed > 0)
×
348
  {
UNCOV
349
    QString detail;
×
UNCOV
350
    for(const auto* e : report.with(FileAction::Failed))
×
UNCOV
351
      detail += e->note + '\n';
×
UNCOV
352
    QMessageBox::warning(this, tr("Some files could not be relinked"), detail);
×
UNCOV
353
  }
×
354

UNCOV
355
  m_resolutions.clear();
×
UNCOV
356
  rescan();
×
UNCOV
357
}
×
358
}
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