| /****************************************************************************  | 
| **  | 
| ** Copyright (C) 2016 The Qt Company Ltd.  | 
| ** Contact: https://www.qt.io/licensing/  | 
| **  | 
| ** This file is part of the Qt VS Tools.  | 
| **  | 
| ** $QT_BEGIN_LICENSE:GPL-EXCEPT$  | 
| ** Commercial License Usage  | 
| ** Licensees holding valid commercial Qt licenses may use this file in  | 
| ** accordance with the commercial license agreement provided with the  | 
| ** Software or, alternatively, in accordance with the terms contained in  | 
| ** a written agreement between you and The Qt Company. For licensing terms  | 
| ** and conditions see https://www.qt.io/terms-conditions. For further  | 
| ** information use the contact form at https://www.qt.io/contact-us.  | 
| **  | 
| ** GNU General Public License Usage  | 
| ** Alternatively, this file may be used under the terms of the GNU  | 
| ** General Public License version 3 as published by the Free Software  | 
| ** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT  | 
| ** included in the packaging of this file. Please review the following  | 
| ** information to ensure the GNU General Public License requirements will  | 
| ** be met: https://www.gnu.org/licenses/gpl-3.0.html.  | 
| **  | 
| ** $QT_END_LICENSE$  | 
| **  | 
| ****************************************************************************/  | 
|   | 
| #include "qrceditor.h"  | 
| #include "undocommands_p.h"  | 
|   | 
| #include <QtCore/QDebug>  | 
| #include <QtWidgets/QMenu>  | 
| #include <QtWidgets/QFileDialog>  | 
| #include <QtWidgets/QMessageBox>  | 
|   | 
| using namespace SharedTools;  | 
|   | 
| QrcEditor::QrcEditor(QWidget *parent)  | 
|   : QWidget(parent),  | 
|     m_treeview(new ResourceView(&m_history)),  | 
|     m_addFileAction(0)  | 
| {  | 
|     m_ui.setupUi(this);  | 
|     QHBoxLayout *layout = new QHBoxLayout;  | 
|     layout->setSpacing(0);  | 
|     layout->setMargin(0);  | 
|     m_ui.centralWidget->setLayout(layout);  | 
|   | 
|     m_treeview->enableContextMenu(false);  | 
|     layout->addWidget(m_treeview);  | 
|     connect(m_ui.removeButton, SIGNAL(clicked()), this, SLOT(onRemove()));  | 
|   | 
|     // 'Add' button with menu  | 
|     QMenu *addMenu = new QMenu(this);  | 
|     m_addFileAction = addMenu->addAction(tr("Add Files"), this, SLOT(onAddFiles()));  | 
|     addMenu->addAction(tr("Add Prefix"), this, SLOT(onAddPrefix()));  | 
|     m_ui.addButton->setMenu(addMenu);  | 
|   | 
|     connect(m_treeview, SIGNAL(addPrefixTriggered()), this, SLOT(onAddPrefix()));  | 
|     connect(m_treeview, SIGNAL(addFilesTriggered(QString)), this, SLOT(onAddFiles()));  | 
|     connect(m_treeview, SIGNAL(removeItem()), this, SLOT(onRemove()));  | 
|     connect(m_treeview, SIGNAL(currentIndexChanged()), this, SLOT(updateCurrent()));  | 
|     connect(m_treeview, SIGNAL(dirtyChanged(bool)), this, SIGNAL(dirtyChanged(bool)));  | 
|     m_treeview->setFocus();  | 
|   | 
|     connect(m_ui.aliasText, SIGNAL(textEdited(QString)),  | 
|             this, SLOT(onAliasChanged(QString)));  | 
|     connect(m_ui.prefixText, SIGNAL(textEdited(QString)),  | 
|             this, SLOT(onPrefixChanged(QString)));  | 
|     connect(m_ui.languageText, SIGNAL(textEdited(QString)),  | 
|             this, SLOT(onLanguageChanged(QString)));  | 
|   | 
|     // 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_ui.aliasText, SIGNAL(editingFinished()),  | 
|             m_treeview, SLOT(advanceMergeId()));  | 
|     connect(m_ui.prefixText, SIGNAL(editingFinished()),  | 
|             m_treeview, SLOT(advanceMergeId()));  | 
|     connect(m_ui.languageText, SIGNAL(editingFinished()),  | 
|             m_treeview, SLOT(advanceMergeId()));  | 
|   | 
|     connect(m_treeview, SIGNAL(addFilesTriggered(const QString&)),  | 
|         this, SIGNAL(addFilesTriggered(const QString&)));  | 
|   | 
|     connect(&m_history, SIGNAL(canRedoChanged(bool)), this, SLOT(updateHistoryControls()));  | 
|     connect(&m_history, SIGNAL(canUndoChanged(bool)), this, SLOT(updateHistoryControls()));  | 
|     updateHistoryControls();  | 
|     updateCurrent();  | 
| }  | 
|   | 
| QrcEditor::~QrcEditor()  | 
| {  | 
| }  | 
|   | 
| QString QrcEditor::fileName() const  | 
| {  | 
|     return m_treeview->fileName();  | 
| }  | 
|   | 
| void QrcEditor::setFileName(const QString &fileName)  | 
| {  | 
|     m_treeview->setFileName(fileName);  | 
| }  | 
|   | 
| bool QrcEditor::load(const QString &fileName)  | 
| {  | 
|     const bool success = m_treeview->load(fileName);  | 
|     if (success) {  | 
|         // Set "focus"  | 
|         m_treeview->setCurrentIndex(m_treeview->model()->index(0,0));  | 
|   | 
|         // Expand prefix nodes  | 
|         m_treeview->expandAll();  | 
|     }  | 
|     return success;  | 
| }  | 
|   | 
| bool QrcEditor::save()  | 
| {  | 
|     return m_treeview->save();  | 
| }  | 
|   | 
| bool QrcEditor::isDirty()  | 
| {  | 
|     return m_treeview->isDirty();  | 
| }  | 
|   | 
| void QrcEditor::setDirty(bool dirty)  | 
| {  | 
|     m_treeview->setDirty(dirty);  | 
| }  | 
|   | 
| // 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_ui.aliasLabel->setEnabled(isFile);  | 
|     m_ui.aliasText->setEnabled(isFile);  | 
|     m_currentAlias = m_treeview->currentAlias();  | 
|     m_ui.aliasText->setText(m_currentAlias);  | 
|   | 
|     m_ui.prefixLabel->setEnabled(isPrefix);  | 
|     m_ui.prefixText->setEnabled(isPrefix);  | 
|     m_currentPrefix = m_treeview->currentPrefix();  | 
|     m_ui.prefixText->setText(m_currentPrefix);  | 
|   | 
|     m_ui.languageLabel->setEnabled(isPrefix);  | 
|     m_ui.languageText->setEnabled(isPrefix);  | 
|     m_currentLanguage = m_treeview->currentLanguage();  | 
|     m_ui.languageText->setText(m_currentLanguage);  | 
|   | 
|     m_ui.urlLabel->setEnabled(isFile);  | 
|     m_ui.urlText->setEnabled(isFile);  | 
|     if (isFile) {  | 
|         QString url = QLatin1String(":");  | 
|         url += m_currentPrefix;  | 
|         if (!url.endsWith(QLatin1Char('/')))  | 
|             url += QLatin1Char('/');  | 
|         if (m_currentAlias.isEmpty())  | 
|             url += m_treeview->currentIndex().data().toString();  | 
|         else  | 
|             url += m_currentAlias;  | 
|         m_ui.urlText->setText(url);  | 
|     } else {  | 
|         m_ui.urlText->clear();  | 
|     }  | 
|   | 
|     m_ui.addButton->setEnabled(true);  | 
|     m_addFileAction->setEnabled(isValid);  | 
|     m_ui.removeButton->setEnabled(isValid);  | 
| }  | 
|   | 
| void QrcEditor::updateHistoryControls()  | 
| {  | 
|     emit undoStackChanged(m_history.canUndo(), m_history.canRedo());  | 
| }  | 
|   | 
| void QrcEditor::resolveLocationIssues(QStringList &files)  | 
| {  | 
|     const QDir dir = QFileInfo(m_treeview->fileName()).absoluteDir();  | 
|     const QString dotdotSlash = QLatin1String("../");  | 
|     int i = 0;  | 
|     int count = files.count();  | 
|     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  | 
|     bool abort = false;  | 
|     for (; i < count; i++) {  | 
|         // Path fine -> skip file  | 
|         QString const &file = files.at(i);  | 
|         QString const relativePath = dir.relativeFilePath(file);  | 
|         if (!relativePath.startsWith(dotdotSlash)) {  | 
|             continue;  | 
|         }  | 
|   | 
|         // Path troublesome and aborted -> remove file  | 
|         if (abort) {  | 
|             files.removeAt(i);  | 
|             count--;  | 
|             i--;  | 
|             continue;  | 
|         } else {  | 
|             // Path troublesome -> query user  | 
|             QMessageBox message(this);  | 
|             message.setWindowTitle(tr("Invalid File Location"));  | 
|             message.setIcon(QMessageBox::Warning);  | 
|             QPushButton * const copyButton = message.addButton(tr("Copy"), QMessageBox::ActionRole);  | 
|             QPushButton * skipButton = NULL;  | 
|             if (initialCount > 1)  | 
|             {  | 
|                 skipButton = message.addButton(tr("Skip"), QMessageBox::DestructiveRole);  | 
|                 message.setEscapeButton(skipButton);  | 
|             }  | 
|             QPushButton * const abortButton = message.addButton(tr("Abort"), QMessageBox::RejectRole);  | 
|             message.setDefaultButton(copyButton);  | 
|             message.setText(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)));  | 
|             message.exec();  | 
|             if (message.clickedButton() == skipButton) {  | 
|                 files.removeAt(i);  | 
|                 count--;  | 
|                 i--; // Compensate i++  | 
|             } else if (message.clickedButton() == copyButton) {  | 
|                 const QFileInfo fi(file);  | 
|                 QFileInfo suggestion;  | 
|                 QDir tmpTarget(dir.path() + QString(QDir::separator()) + QString("Resources"));;  | 
|                 if (tmpTarget.exists())  | 
|                     suggestion.setFile(tmpTarget, fi.fileName());  | 
|                 else  | 
|                     suggestion.setFile(dir, fi.fileName());  | 
|                 const QString copyName = QFileDialog::getSaveFileName(this, tr("Choose copy location"),  | 
|                                                                 suggestion.absoluteFilePath());  | 
|                 if (!copyName.isEmpty()) {  | 
|                     QString relPath = dir.relativeFilePath(copyName);  | 
|                     if (relPath.startsWith(dotdotSlash)) {   // directory is still invalid  | 
|                         i--; // Compensate i++ and try again  | 
|                         continue;  | 
|                     }  | 
|                     if (QFile::exists(copyName)) {  | 
|                         if (!QFile::remove(copyName)) {  | 
|                             QMessageBox::critical(this, tr("Overwrite Failed"),  | 
|                                                   tr("Could not overwrite file %1.")  | 
|                                                   .arg(QDir::toNativeSeparators(copyName)));  | 
|                             // Remove file  | 
|                             files.removeAt(i);  | 
|                             count--;  | 
|                             i--; // Compensate i++  | 
|                             continue;  | 
|                         }  | 
|                     }  | 
|                     if (!QFile::copy(file, copyName)) {  | 
|                         QMessageBox::critical(this, tr("Copying Failed"),  | 
|                                               tr("Could not copy the file to %1.")  | 
|                                               .arg(QDir::toNativeSeparators(copyName)));  | 
|                         // Remove file  | 
|                         files.removeAt(i);  | 
|                         count--;  | 
|                         i--; // Compensate i++  | 
|                         continue;  | 
|                     }  | 
|                     files[i] = copyName;  | 
|                 } else {  | 
|                     // Remove file  | 
|                     files.removeAt(i);  | 
|                     count--;  | 
|                     i--; // Compensate i++  | 
|                 }  | 
|             } else if (message.clickedButton() == abortButton) {  | 
|                 abort = true;  | 
|   | 
|                 files.removeAt(i);  | 
|                 count--;  | 
|                 i--; // Compensate i++  | 
|             }  | 
|         }  | 
|     }  | 
| }  | 
|   | 
| void QrcEditor::setResourceDragEnabled(bool e)  | 
| {  | 
|     m_treeview->setResourceDragEnabled(e);  | 
| }  | 
|   | 
| bool QrcEditor::resourceDragEnabled() const  | 
| {  | 
|     return m_treeview->resourceDragEnabled();  | 
| }  | 
|   | 
| void QrcEditor::setDefaultAddFileEnabled(bool enable)  | 
| {  | 
|     m_treeview->setDefaultAddFileEnabled(enable);  | 
| }  | 
|   | 
| bool QrcEditor::defaultAddFileEnabled() const  | 
| {  | 
|     return m_treeview->defaultAddFileEnabled();  | 
| }  | 
|   | 
| void QrcEditor::addFile(const QString &prefix, const QString &file)  | 
| {  | 
|     // TODO: make this function UNDO / REDO aware  | 
|     m_treeview->addFile(prefix, file);  | 
| }  | 
|   | 
| /*  | 
| void QrcEditor::removeFile(const QString &prefix, const QString &file)  | 
| {  | 
|     m_treeview->removeFile(prefix, file);  | 
| }  | 
| */  | 
| // 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;  | 
|     updateCurrent();  | 
|     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;  | 
|     updateCurrent();  | 
|     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 '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();  | 
|     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();  | 
| }  | 
|   | 
| // Slot for 'Undo' button  | 
| void QrcEditor::onUndo()  | 
| {  | 
|     m_history.undo();  | 
|     updateCurrent();  | 
|     updateHistoryControls();  | 
| }  | 
|   | 
| // Slot for 'Redo' button  | 
| void QrcEditor::onRedo()  | 
| {  | 
|     m_history.redo();  | 
|     updateCurrent();  | 
|     updateHistoryControls();  | 
| }  |