14 setWindowTitle(
"Shader Playlist Settings");
15 setMinimumSize(600, 600);
21 QVBoxLayout *mainLayout =
new QVBoxLayout(
this);
26 QLabel *infoLabel =
new QLabel(
27 "Build a playlist tree of shaders. Create named nodes, then add shaders to each node.\n"
28 "When enabled, the playlist file is passed to acmx2. Press P to toggle; Up/Down to navigate.",
30 infoLabel->setWordWrap(
true);
31 mainLayout->addWidget(infoLabel);
33 QGroupBox *shaderGroup =
new QGroupBox(
"Playlist Shader Selection",
this);
34 QVBoxLayout *shaderMainLayout =
new QVBoxLayout(shaderGroup);
36 QHBoxLayout *searchLayout =
new QHBoxLayout();
37 QLabel *searchLabel =
new QLabel(
"Search:",
this);
41 searchLayout->addWidget(searchLabel);
43 shaderMainLayout->addLayout(searchLayout);
45 QHBoxLayout *comboLayout =
new QHBoxLayout();
46 QLabel *availableLabel =
new QLabel(
"Available Shaders:",
this);
48 shaderComboBox->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
54 proxyModel->setFilterCaseSensitivity(Qt::CaseInsensitive);
55 proxyModel->setSortCaseSensitivity(Qt::CaseInsensitive);
58 comboLayout->addWidget(availableLabel);
60 shaderMainLayout->addLayout(comboLayout);
62 QHBoxLayout *nodeButtonLayout =
new QHBoxLayout();
69 nodeButtonLayout->addStretch();
70 shaderMainLayout->addLayout(nodeButtonLayout);
72 QHBoxLayout *buttonLayout =
new QHBoxLayout();
73 addButton =
new QPushButton(
"Add Shader",
this);
75 upButton =
new QPushButton(
"↑ Up",
this);
85 shaderMainLayout->addLayout(buttonLayout);
87 QLabel *selectedLabel =
new QLabel(
"Playlist Tree (press P in acmx2 to toggle, Up/Down to navigate):",
this);
88 shaderMainLayout->addWidget(selectedLabel);
92 playlistTree->setDragDropMode(QAbstractItemView::InternalMove);
93 playlistTree->setSelectionMode(QAbstractItemView::SingleSelection);
96 QHBoxLayout *fileButtonLayout =
new QHBoxLayout();
97 saveButton =
new QPushButton(
"Save Playlist...",
this);
98 loadButton =
new QPushButton(
"Load Playlist...",
this);
99 concatButton =
new QPushButton(
"Concat Playlist...",
this);
103 fileButtonLayout->addStretch();
104 shaderMainLayout->addLayout(fileButtonLayout);
106 QHBoxLayout *autopilotLayout =
new QHBoxLayout();
107 QLabel *autopilotLabel =
new QLabel(
108 "Autopilot frames (minimum 4; switch to a random shader after this many frames, toggle with J):",
110 autopilotLabel->setWordWrap(
true);
118 autopilotLayout->addWidget(autopilotLabel, 1);
121 shaderMainLayout->addLayout(autopilotLayout);
123 mainLayout->addWidget(shaderGroup);
125 QHBoxLayout *dialogButtonLayout =
new QHBoxLayout();
126 okButton =
new QPushButton(
"OK",
this);
128 dialogButtonLayout->addStretch();
129 dialogButtonLayout->addWidget(
okButton);
131 mainLayout->addLayout(dialogButtonLayout);
145 connect(
okButton, &QPushButton::clicked,
this, &QDialog::accept);
146 connect(
cancelButton, &QPushButton::clicked,
this, &QDialog::reject);
149 connect(
enableCheckBox, &QCheckBox::toggled,
this, [
this](
bool checked) {
200 for (
int i = 0; i < shaderNames.size(); ++i) {
201 QString name = shaderNames[i];
204 QStandardItem *item =
new QStandardItem(name);
205 item->setData(i, Qt::UserRole);
209 if (!selectedNames.isEmpty()) {
225 if (!current->parent())
227 return current->parent();
232 QString name = QInputDialog::getText(
this,
"New Playlist Node",
"Node name:", QLineEdit::Normal, QString(), &ok);
233 if (!ok || name.trimmed().isEmpty())
237 nodeItem->setText(0, name.trimmed());
238 nodeItem->setFlags(nodeItem->flags() | Qt::ItemIsEditable);
239 nodeItem->setExpanded(
true);
246 QMessageBox::information(
this,
"No Node Selected",
"Select a playlist node to rename.");
251 QString name = QInputDialog::getText(
this,
"Rename Node",
"New name:", QLineEdit::Normal, node->text(0), &ok);
252 if (ok && !name.trimmed().isEmpty()) {
253 node->setText(0, name.trimmed());
260 QMessageBox::information(
this,
"No Node Selected",
"Select a playlist node to remove.");
264 if (node->childCount() > 0) {
265 auto reply = QMessageBox::question(
this,
"Remove Node",
266 "Node \"" + node->text(0) +
"\" has " + QString::number(node->childCount()) +
267 " shader(s). Remove it and all its shaders?",
268 QMessageBox::Yes | QMessageBox::No);
269 if (reply != QMessageBox::Yes)
284 nodeItem->setText(0,
"Default");
285 nodeItem->setFlags(nodeItem->flags() | Qt::ItemIsEditable);
286 nodeItem->setExpanded(
true);
289 QMessageBox::information(
this,
"No Node Selected",
"Select a playlist node to add the shader to.");
295 auto *item =
new QTreeWidgetItem(node);
296 item->setText(0, shaderName);
300 node->setExpanded(
true);
307 if (!current->parent()) {
319 QTreeWidgetItem *parent = current->parent();
321 int idx = parent->indexOfChild(current);
323 parent->takeChild(idx);
324 parent->insertChild(idx - 1, current);
342 QTreeWidgetItem *parent = current->parent();
344 int idx = parent->indexOfChild(current);
345 if (idx < parent->childCount() - 1) {
346 parent->takeChild(idx);
347 parent->insertChild(idx + 1, current);
352 if (idx < playlistTree->topLevelItemCount() - 1) {
365 int totalShaders = 0;
366 for (
int i = 0; i <
playlistTree->topLevelItemCount(); ++i) {
367 totalShaders +=
playlistTree->topLevelItem(i)->childCount();
369 if (totalShaders == 0) {
370 QMessageBox::information(
this,
"Empty Playlist",
"Add shaders to the playlist before shuffling.");
374 static thread_local std::mt19937 rng{std::random_device{}()};
376 for (
int i = 0; i <
playlistTree->topLevelItemCount(); ++i) {
378 const int n = node->childCount();
382 QList<QTreeWidgetItem *> children;
384 while (node->childCount() > 0) {
385 children.append(node->takeChild(0));
387 std::shuffle(children.begin(), children.end(), rng);
388 for (QTreeWidgetItem *child : children) {
389 node->addChild(child);
391 node->setExpanded(
true);
396 QSettings appSettings(
"LostSideDead");
397 QString lastDir = appSettings.value(
"lastPlaylistDir",
"").toString();
398 QString filePath = QFileDialog::getOpenFileName(
this,
"Concat Playlist", lastDir,
399 "Text Files (*.txt);;All Files (*)");
400 if (filePath.isEmpty())
403 appSettings.setValue(
"lastPlaylistDir", QFileInfo(filePath).absolutePath());
405 QFile file(filePath);
406 if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
407 QMessageBox::critical(
this,
"Error",
"Could not open playlist file: " + filePath);
411 QTextStream in(&file);
413 int skippedCount = 0;
415 QTreeWidgetItem *currentNode =
nullptr;
417 while (!in.atEnd()) {
418 QString line = in.readLine().trimmed();
422 if (line.startsWith(
'[') && line.endsWith(
']')) {
423 QString nodeName = line.mid(1, line.length() - 2);
425 currentNode->setText(0, nodeName);
426 currentNode->setFlags(currentNode->flags() | Qt::ItemIsEditable);
427 currentNode->setExpanded(
true);
431 QString nodeName = QFileInfo(filePath).baseName();
432 if (nodeName.isEmpty())
435 currentNode->setText(0, nodeName);
436 currentNode->setFlags(currentNode->flags() | Qt::ItemIsEditable);
437 currentNode->setExpanded(
true);
441 auto *item =
new QTreeWidgetItem(currentNode);
442 item->setText(0, line);
452 QString msg =
"Concatenated " + QString::number(loadedCount) +
" shader(s) into " +
453 QString::number(nodesAdded) +
" node(s).";
454 if (skippedCount > 0)
455 msg +=
"\n" + QString::number(skippedCount) +
" shader(s) not found and skipped.";
456 QMessageBox::information(
this,
"Playlist Concatenated", msg);
461 for (
int i = 0; i <
playlistTree->topLevelItemCount(); ++i) {
462 shaderCount +=
playlistTree->topLevelItem(i)->childCount();
464 if (shaderCount == 0) {
465 QMessageBox::information(
this,
"Empty Playlist",
"Add shaders to the playlist before saving.");
469 QSettings appSettings(
"LostSideDead");
470 QString lastDir = appSettings.value(
"lastPlaylistDir",
"").toString();
471 QString filePath = QFileDialog::getSaveFileName(
this,
"Save Playlist", lastDir,
"Text Files (*.txt);;All Files (*)");
472 if (filePath.isEmpty())
475 appSettings.setValue(
"lastPlaylistDir", QFileInfo(filePath).absolutePath());
477 QFile file(filePath);
478 if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
479 QMessageBox::critical(
this,
"Error",
"Could not save playlist file: " + filePath);
483 QTextStream out(&file);
484 for (
int i = 0; i <
playlistTree->topLevelItemCount(); ++i) {
486 out <<
"[" << node->text(0) <<
"]\n";
487 for (
int j = 0; j < node->childCount(); ++j) {
488 out << node->child(j)->text(0) <<
"\n";
493 QMessageBox::information(
this,
"Saved",
"Playlist saved to: " + filePath);
497 QSettings appSettings(
"LostSideDead");
498 QString lastDir = appSettings.value(
"lastPlaylistDir",
"").toString();
499 QString filePath = QFileDialog::getOpenFileName(
this,
"Load Playlist", lastDir,
"Text Files (*.txt);;All Files (*)");
500 if (filePath.isEmpty())
503 appSettings.setValue(
"lastPlaylistDir", QFileInfo(filePath).absolutePath());
505 QFile file(filePath);
506 if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
507 QMessageBox::critical(
this,
"Error",
"Could not open playlist file: " + filePath);
512 QTextStream in(&file);
514 int skippedCount = 0;
515 QTreeWidgetItem *currentNode =
nullptr;
517 while (!in.atEnd()) {
518 QString line = in.readLine().trimmed();
522 if (line.startsWith(
'[') && line.endsWith(
']')) {
523 QString nodeName = line.mid(1, line.length() - 2);
525 currentNode->setText(0, nodeName);
526 currentNode->setFlags(currentNode->flags() | Qt::ItemIsEditable);
527 currentNode->setExpanded(
true);
531 currentNode->setText(0,
"Default");
532 currentNode->setFlags(currentNode->flags() | Qt::ItemIsEditable);
533 currentNode->setExpanded(
true);
536 auto *item =
new QTreeWidgetItem(currentNode);
537 item->setText(0, line);
548 QString msg =
"Loaded " + QString::number(loadedCount) +
" shader(s).";
549 if (skippedCount > 0)
550 msg +=
"\n" + QString::number(skippedCount) +
" shader(s) not found and skipped.";
551 QMessageBox::information(
this,
"Playlist Loaded", msg);
557 for (
int i = 0; i <
playlistTree->topLevelItemCount(); ++i) {
566 for (
int i = 0; i <
playlistTree->topLevelItemCount(); ++i) {
568 for (
int j = 0; j < node->childCount(); ++j) {
569 names.append(node->child(j)->text(0));
576 QList<QPair<QString, QStringList>> tree;
577 for (
int i = 0; i <
playlistTree->topLevelItemCount(); ++i) {
580 for (
int j = 0; j < node->childCount(); ++j) {
581 shaders.append(node->child(j)->text(0));
583 tree.append({node->text(0), shaders});
624 node->setText(0,
"Default");
625 node->setFlags(node->flags() | Qt::ItemIsEditable);
626 node->setExpanded(
true);
628 for (
const QString &name : names) {
630 auto *item =
new QTreeWidgetItem(node);
631 item->setText(0, name);
639 for (
const auto &[nodeName, shaders] : tree) {
641 node->setText(0, nodeName);
642 node->setFlags(node->flags() | Qt::ItemIsEditable);
643 node->setExpanded(
true);
644 for (
const QString &name : shaders) {
646 auto *item =
new QTreeWidgetItem(node);
647 item->setText(0, name);
656 if (!path.isEmpty()) {
658 if (file.open(QIODevice::ReadOnly | QIODevice::Text)) {
659 QTextStream in(&file);
660 bool hasNodes =
false;
661 while (!in.atEnd()) {
662 QString line = in.readLine().trimmed();
663 if (line.startsWith(
'[') && line.endsWith(
']')) {
670 if (hasNodes && file.open(QIODevice::ReadOnly | QIODevice::Text)) {
671 QTextStream in2(&file);
673 QTreeWidgetItem *currentNode =
nullptr;
674 while (!in2.atEnd()) {
675 QString line = in2.readLine().trimmed();
678 if (line.startsWith(
'[') && line.endsWith(
']')) {
679 QString nodeName = line.mid(1, line.length() - 2);
681 currentNode->setText(0, nodeName);
682 currentNode->setFlags(currentNode->flags() | Qt::ItemIsEditable);
683 currentNode->setExpanded(
true);
685 auto *item =
new QTreeWidgetItem(currentNode);
686 item->setText(0, line);
Dialog for building nested shader playlists.
QPushButton * shuffleButton
void loadShaders(const QStringList &shaderNames)
int getAutopilotFrames() const
Return frames-per-shader threshold for autopilot mode (minimum 4).
void setPlaylistTree(const QList< QPair< QString, QStringList > > &tree)
QPushButton * concatButton
PlaylistDialog(const QStringList &shaderNames, QWidget *parent=nullptr)
QPushButton * renameNodeButton
QStringList getSelectedShaderNames() const
Return flattened selection of shaders in current playlist.
QLineEdit * searchLineEdit
QStringList shaderNamesList
void setEnabled(bool enabled)
bool isPlaylistEnabled() const
Return whether playlist mode is enabled.
void setPlaylistFile(const QString &path)
QSortFilterProxyModel * proxyModel
QCheckBox * enableCheckBox
QPushButton * removeButton
QPushButton * cancelButton
void setSelectedShaderNames(const QStringList &names)
QTreeWidget * playlistTree
QString getPlaylistFile() const
Return current playlist file path.
void setAutopilotRandom(bool enabled)
Enable or disable random autopilot timeout mode.
QTreeWidgetItem * currentNodeItem() const
QPushButton * clearButton
void updateShaderList(const QStringList &shaderNames)
bool isAutopilotRandom() const
Return true when random autopilot timeout mode is enabled.
QList< QPair< QString, QStringList > > getPlaylistTree() const
Return tree representation as node-name and shader-list pairs.
void filterSearchChanged(const QString &text)
QPushButton * removeNodeButton
void setAutopilotFrames(int frames)
Set frames-per-shader threshold for autopilot mode.
QMap< QString, int > shaderNameToIndex
QPushButton * addNodeButton
QComboBox * shaderComboBox
QStandardItemModel * shaderModel
QSpinBox * autopilotFramesSpinBox
QCheckBox * autopilotRandomCheckBox
void applyCustomStyleIfEnabled(QWidget *widget)