// Copyright (C) 2016 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 #include "qrceditor.h" #include "../resourceeditortr.h" #include "undocommands_p.h" #include #include #include #include #include #include #include #include #include #include #include namespace ResourceEditor::Internal { QrcEditor::QrcEditor(RelativeResourceModel *model, QWidget *parent) : Core::MiniSplitter(Qt::Vertical, parent), m_treeview(new ResourceView(model, &m_history)) { addWidget(m_treeview); auto widget = new QWidget; addWidget(widget); m_treeview->setFrameStyle(QFrame::NoFrame); m_treeview->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Minimum); auto addPrefixButton = new QPushButton(Tr::tr("Add Prefix")); m_addFilesButton = new QPushButton(Tr::tr("Add Files")); m_removeButton = new QPushButton(Tr::tr("Remove")); m_removeNonExistingButton = new QPushButton(Tr::tr("Remove Missing Files")); m_aliasLabel = new QLabel(Tr::tr("Alias:")); m_aliasText = new QLineEdit; m_prefixLabel = new QLabel(Tr::tr("Prefix:")); m_prefixText = new QLineEdit; m_languageLabel = new QLabel(Tr::tr("Language:")); m_languageText = new QLineEdit; using namespace Layouting; Column { Row { addPrefixButton, m_addFilesButton, m_removeButton, m_removeNonExistingButton, st, }, Group { title(Tr::tr("Properties")), Form { m_aliasLabel, m_aliasText, br, m_prefixLabel, m_prefixText, br, m_languageLabel, m_languageText, br, } }, st, }.attachTo(widget); connect(addPrefixButton, &QAbstractButton::clicked, this, &QrcEditor::onAddPrefix); connect(m_addFilesButton, &QAbstractButton::clicked, this, &QrcEditor::onAddFiles); connect(m_removeButton, &QAbstractButton::clicked, this, &QrcEditor::onRemove); connect(m_removeNonExistingButton, &QPushButton::clicked, this, &QrcEditor::onRemoveNonExisting); connect(m_treeview, &ResourceView::removeItem, this, &QrcEditor::onRemove); connect(m_treeview->selectionModel(), &QItemSelectionModel::currentChanged, this, &QrcEditor::updateCurrent); connect(m_treeview, &ResourceView::itemActivated, this, &QrcEditor::itemActivated); connect(m_treeview, &ResourceView::contextMenuShown, this, &QrcEditor::showContextMenu); m_treeview->setFocus(); connect(m_aliasText, &QLineEdit::textEdited, this, &QrcEditor::onAliasChanged); connect(m_prefixText, &QLineEdit::textEdited, this, &QrcEditor::onPrefixChanged); connect(m_languageText, &QLineEdit::textEdited, this, &QrcEditor::onLanguageChanged); // Prevent undo command merging after a switch of focus: // (0) The initial text is "Green". // (1) The user appends " is a color." --> text is "Green is a color." // (2) The user clicks into some other line edit --> loss of focus // (3) The user gives focuse again and substitutes "Green" with "Red" // --> text now is "Red is a color." // (4) The user hits undo --> text now is "Green is a color." // Without calling advanceMergeId() it would have been "Green", instead. connect(m_aliasText, &QLineEdit::editingFinished, m_treeview, &ResourceView::advanceMergeId); connect(m_prefixText, &QLineEdit::editingFinished, m_treeview, &ResourceView::advanceMergeId); connect(m_languageText, &QLineEdit::editingFinished, m_treeview, &ResourceView::advanceMergeId); connect(&m_history, &QUndoStack::canRedoChanged, this, &QrcEditor::updateHistoryControls); connect(&m_history, &QUndoStack::canUndoChanged, this, &QrcEditor::updateHistoryControls); auto agg = new Aggregation::Aggregate; agg->add(m_treeview); agg->add(new Core::ItemViewFind(m_treeview)); updateHistoryControls(); updateCurrent(); } QrcEditor::~QrcEditor() = default; void QrcEditor::loaded(bool success) { if (!success) return; // Set "focus" m_treeview->setCurrentIndex(m_treeview->model()->index(0,0)); // Expand prefix nodes m_treeview->expandAll(); } void QrcEditor::refresh() { m_treeview->refresh(); } // Propagates a change of selection in the tree // to the alias/prefix/language edit controls void QrcEditor::updateCurrent() { const bool isValid = m_treeview->currentIndex().isValid(); const bool isPrefix = m_treeview->isPrefix(m_treeview->currentIndex()) && isValid; const bool isFile = !isPrefix && isValid; m_aliasLabel->setEnabled(isFile); m_aliasText->setEnabled(isFile); m_currentAlias = m_treeview->currentAlias(); m_aliasText->setText(m_currentAlias); m_prefixLabel->setEnabled(isPrefix); m_prefixText->setEnabled(isPrefix); m_currentPrefix = m_treeview->currentPrefix(); m_prefixText->setText(m_currentPrefix); m_languageLabel->setEnabled(isPrefix); m_languageText->setEnabled(isPrefix); m_currentLanguage = m_treeview->currentLanguage(); m_languageText->setText(m_currentLanguage); m_addFilesButton->setEnabled(isValid); m_removeButton->setEnabled(isValid); } void QrcEditor::updateHistoryControls() { emit undoStackChanged(m_history.canUndo(), m_history.canRedo()); } // Helper for resolveLocationIssues(): // For code clarity, a context with convenience functions to execute // the dialogs required for checking the image file paths // (and keep them around for file dialog execution speed). // Basically, resolveLocationIssues() checks the paths of the images // and asks the user to copy them into the resource file location. // When the user does a multiselection of files, this requires popping // up the dialog several times in a row. struct ResolveLocationContext { QAbstractButton *execLocationMessageBox(QWidget *parent, const QString &file, bool wantSkipButton); QString execCopyFileDialog(QWidget *parent, const QDir &dir, const QString &targetPath); QScopedPointer messageBox; QScopedPointer copyFileDialog; QPushButton *copyButton = nullptr; QPushButton *skipButton = nullptr; QPushButton *abortButton = nullptr; }; QAbstractButton *ResolveLocationContext::execLocationMessageBox(QWidget *parent, const QString &file, bool wantSkipButton) { if (messageBox.isNull()) { messageBox.reset(new QMessageBox(QMessageBox::Warning, Tr::tr("Invalid file location"), QString(), QMessageBox::NoButton, parent)); copyButton = messageBox->addButton(Tr::tr("Copy"), QMessageBox::ActionRole); abortButton = messageBox->addButton(Tr::tr("Abort"), QMessageBox::RejectRole); messageBox->setDefaultButton(copyButton); } if (wantSkipButton && !skipButton) { skipButton = messageBox->addButton(Tr::tr("Skip"), QMessageBox::DestructiveRole); messageBox->setEscapeButton(skipButton); } messageBox->setText(Tr::tr("The file %1 is not in a subdirectory of the resource file. " "You now have the option to copy this file to a valid location.") .arg(QDir::toNativeSeparators(file))); messageBox->exec(); return messageBox->clickedButton(); } QString ResolveLocationContext::execCopyFileDialog(QWidget *parent, const QDir &dir, const QString &targetPath) { // Delayed creation of file dialog. if (copyFileDialog.isNull()) { copyFileDialog.reset(new QFileDialog(parent, Tr::tr("Choose Copy Location"))); copyFileDialog->setFileMode(QFileDialog::AnyFile); copyFileDialog->setAcceptMode(QFileDialog::AcceptSave); } copyFileDialog->selectFile(targetPath); // Repeat until the path entered is no longer above 'dir' // (relative is not "../"). while (true) { if (copyFileDialog->exec() != QDialog::Accepted) return QString(); const QStringList files = copyFileDialog->selectedFiles(); if (files.isEmpty()) return QString(); const QString relPath = dir.relativeFilePath(files.front()); if (!relPath.startsWith(QLatin1String("../"))) return files.front(); } return QString(); } // Helper to copy a file with message boxes static inline bool copyFile(const QString &file, const QString ©Name, QWidget *parent) { if (QFileInfo::exists(copyName)) { if (!QFile::remove(copyName)) { QMessageBox::critical(parent, Tr::tr("Overwriting Failed"), Tr::tr("Could not overwrite file %1.") .arg(QDir::toNativeSeparators(copyName))); return false; } } if (!QFile::copy(file, copyName)) { QMessageBox::critical(parent, Tr::tr("Copying Failed"), Tr::tr("Could not copy the file to %1.") .arg(QDir::toNativeSeparators(copyName))); return false; } return true; } void QrcEditor::resolveLocationIssues(QStringList &files) { const QDir dir = m_treeview->filePath().toFileInfo().absoluteDir(); const QString dotdotSlash = QLatin1String("../"); int i = 0; const int count = files.count(); const int initialCount = files.count(); // Find first troublesome file for (; i < count; i++) { QString const &file = files.at(i); const QString relativePath = dir.relativeFilePath(file); if (relativePath.startsWith(dotdotSlash)) break; } // All paths fine -> no interaction needed if (i == count) return; // Interact with user from now on ResolveLocationContext context; bool abort = false; for (QStringList::iterator it = files.begin(); it != files.end(); ) { // Path fine -> skip file QString const &file = *it; QString const relativePath = dir.relativeFilePath(file); if (!relativePath.startsWith(dotdotSlash)) continue; // Path troublesome and aborted -> remove file bool ok = false; if (!abort) { // Path troublesome -> query user "Do you want copy/abort/skip". QAbstractButton *clickedButton = context.execLocationMessageBox(this, file, initialCount > 1); if (clickedButton == context.copyButton) { const QFileInfo fi(file); QFileInfo suggestion; QDir tmpTarget(dir.path() + QLatin1Char('/') + QLatin1String("Resources")); if (tmpTarget.exists()) suggestion.setFile(tmpTarget, fi.fileName()); else suggestion.setFile(dir, fi.fileName()); // Prompt for copy location, copy and replace name. const QString copyName = context.execCopyFileDialog(this, dir, suggestion.absoluteFilePath()); ok = !copyName.isEmpty() && copyFile(file, copyName, this); if (ok) *it = copyName; } else if (clickedButton == context.abortButton) { abort = true; } } // !abort if (ok) { // Remove files where user canceled or failures occurred. ++it; } else { it = files.erase(it); } } // for files } void QrcEditor::setResourceDragEnabled(bool e) { m_treeview->setResourceDragEnabled(e); } bool QrcEditor::resourceDragEnabled() const { return m_treeview->resourceDragEnabled(); } void QrcEditor::editCurrentItem() { if (m_treeview->selectionModel()->currentIndex().isValid()) m_treeview->edit(m_treeview->selectionModel()->currentIndex()); } QString QrcEditor::currentResourcePath() const { return m_treeview->currentResourcePath(); } // Slot for change of line edit content 'alias' void QrcEditor::onAliasChanged(const QString &alias) { const QString &before = m_currentAlias; const QString &after = alias; m_treeview->setCurrentAlias(before, after); m_currentAlias = alias; updateHistoryControls(); } // Slot for change of line edit content 'prefix' void QrcEditor::onPrefixChanged(const QString &prefix) { const QString &before = m_currentPrefix; const QString &after = prefix; m_treeview->setCurrentPrefix(before, after); m_currentPrefix = prefix; updateHistoryControls(); } // Slot for change of line edit content 'language' void QrcEditor::onLanguageChanged(const QString &language) { const QString &before = m_currentLanguage; const QString &after = language; m_treeview->setCurrentLanguage(before, after); m_currentLanguage = language; updateHistoryControls(); } // Slot for 'Remove' button void QrcEditor::onRemove() { // Find current item, push and execute command const QModelIndex current = m_treeview->currentIndex(); int afterDeletionArrayIndex = current.row(); QModelIndex afterDeletionParent = current.parent(); m_treeview->findSamePlacePostDeletionModelIndex(afterDeletionArrayIndex, afterDeletionParent); QUndoCommand * const removeCommand = new RemoveEntryCommand(m_treeview, current); m_history.push(removeCommand); const QModelIndex afterDeletionModelIndex = m_treeview->model()->index(afterDeletionArrayIndex, 0, afterDeletionParent); m_treeview->setCurrentIndex(afterDeletionModelIndex); updateHistoryControls(); } // Slot for 'Remove missing files' button void QrcEditor::onRemoveNonExisting() { QList toRemove = m_treeview->nonExistingFiles(); QUndoCommand * const removeCommand = new RemoveMultipleEntryCommand(m_treeview, toRemove); m_history.push(removeCommand); updateHistoryControls(); } // Slot for 'Add File' button void QrcEditor::onAddFiles() { QModelIndex const current = m_treeview->currentIndex(); int const currentIsPrefixNode = m_treeview->isPrefix(current); int const prefixArrayIndex = currentIsPrefixNode ? current.row() : m_treeview->model()->parent(current).row(); int const cursorFileArrayIndex = currentIsPrefixNode ? 0 : current.row(); QStringList fileNames = m_treeview->fileNamesToAdd(); fileNames = m_treeview->existingFilesSubtracted(prefixArrayIndex, fileNames); resolveLocationIssues(fileNames); if (fileNames.isEmpty()) return; QUndoCommand * const addFilesCommand = new AddFilesCommand( m_treeview, prefixArrayIndex, cursorFileArrayIndex, fileNames); m_history.push(addFilesCommand); updateHistoryControls(); } // Slot for 'Add Prefix' button void QrcEditor::onAddPrefix() { QUndoCommand * const addEmptyPrefixCommand = new AddEmptyPrefixCommand(m_treeview); m_history.push(addEmptyPrefixCommand); updateHistoryControls(); m_prefixText->selectAll(); m_prefixText->setFocus(); } // Slot for 'Undo' button void QrcEditor::onUndo() { m_history.undo(); updateCurrent(); updateHistoryControls(); } // Slot for 'Redo' button void QrcEditor::onRedo() { m_history.redo(); updateCurrent(); updateHistoryControls(); } } // ResourceEditor::Internal