Commit fef9d7ff authored by Eike Ziller's avatar Eike Ziller

Editor manager: Abort with a single message if file is not readable.

We show a dialog that offers opening a file in a different editor type
if opening a file fails, but we should not do that if opening the file
fails because it is not readable.
With this change, documents now specify if they failed to open a file
because reading failed, or because they could not handle the file
contents.

Task-number: QTCREATORBUG-14495
Change-Id: I5d4b7cfa74b87ef21b9b55bc30b3ebe2f8238dfa
Reviewed-by: default avatarDaniel Teske <daniel.teske@theqtcompany.com>
Reviewed-by: default avatarLeena Miettinen <riitta-leena.miettinen@theqtcompany.com>
parent be0aa405
......@@ -265,13 +265,13 @@ public:
}
}
bool open(QString *errorString, const QString &fileName, const QString &realFileName)
OpenResult open(QString *errorString, const QString &fileName, const QString &realFileName)
{
QTC_CHECK(fileName == realFileName); // The bineditor can do no autosaving
return openImpl(errorString, fileName);
}
bool openImpl(QString *errorString, const QString &fileName, quint64 offset = 0)
OpenResult openImpl(QString *errorString, const QString &fileName, quint64 offset = 0)
{
QFile file(fileName);
if (file.open(QIODevice::ReadOnly)) {
......@@ -283,7 +283,7 @@ public:
*errorString = msg;
else
QMessageBox::critical(ICore::mainWindow(), tr("File Error"), msg);
return false;
return OpenResult::CannotHandle;
}
if (size > INT_MAX) {
QString msg = tr("The file is too big for the Binary Editor (max. 2GB).");
......@@ -291,13 +291,13 @@ public:
*errorString = msg;
else
QMessageBox::critical(ICore::mainWindow(), tr("File Error"), msg);
return false;
return OpenResult::CannotHandle;
}
if (offset >= size)
return false;
return OpenResult::CannotHandle;
setFilePath(FileName::fromString(fileName));
m_widget->setSizes(offset, file.size());
return true;
return OpenResult::Success;
}
QString errStr = tr("Cannot open %1: %2").arg(
QDir::toNativeSeparators(fileName), file.errorString());
......@@ -305,7 +305,7 @@ public:
*errorString = errStr;
else
QMessageBox::critical(ICore::mainWindow(), tr("File Error"), errStr);
return false;
return OpenResult::ReadError;
}
private slots:
......@@ -363,7 +363,7 @@ public:
emit aboutToReload();
int cPos = m_widget->cursorPosition();
m_widget->clear();
const bool success = openImpl(errorString, filePath().toString());
const bool success = (openImpl(errorString, filePath().toString()) == OpenResult::Success);
m_widget->setCursorPosition(cPos);
emit reloadFinished(success);
return success;
......
......@@ -612,12 +612,25 @@ IEditor *EditorManagerPrivate::openEditor(EditorView *view, const QString &fileN
}
QString errorString;
if (editor->document()->open(&errorString, fn, realFn))
IDocument::OpenResult openResult = editor->document()->open(&errorString, fn, realFn);
if (openResult == IDocument::OpenResult::Success)
break;
overrideCursor.reset();
delete editor;
if (openResult == IDocument::OpenResult::ReadError) {
QMessageBox msgbox(QMessageBox::Critical, EditorManager::tr("File Error"),
tr("Could not open \"%1\" for reading. "
"Either the file does not exist or you do not have "
"the permissions to open it.")
.arg(FileName::fromString(realFn).toUserOutput()),
QMessageBox::Ok, ICore::dialogParent());
msgbox.exec();
return 0;
}
QTC_CHECK(openResult == IDocument::OpenResult::CannotHandle);
if (errorString.isEmpty()) {
errorString = tr("Could not open \"%1\": Unknown error.")
.arg(FileName::fromString(realFn).toUserOutput());
......
......@@ -117,6 +117,19 @@ Id IDocument::id() const
return d->id;
}
/*!
\enum IDocument::OpenResult
The OpenResult enum describes whether a file was successfully opened.
\value Success
The file was read successfully and can be handled by this document type.
\value ReadError
The file could not be opened for reading, either because it does not exist or
because of missing permissions.
\value CannotHandle
This document type could not handle the file content.
*/
/*!
* Used to load a file if this document is part of an IEditor implementation, when the editor
* is opened.
......@@ -127,14 +140,14 @@ Id IDocument::id() const
* If the editor is opened from a regular file, \a fileName and \a realFileName are the same.
* Use \a errorString to return an error message, if this document can not handle the
* file contents.
* Returns true on success, false if an error occurred.
* Returns whether the file was opened and read successfully.
*/
bool IDocument::open(QString *errorString, const QString &fileName, const QString &realFileName)
IDocument::OpenResult IDocument::open(QString *errorString, const QString &fileName, const QString &realFileName)
{
Q_UNUSED(errorString)
Q_UNUSED(fileName)
Q_UNUSED(realFileName)
return false;
return OpenResult::CannotHandle;
}
/*!
......
......@@ -50,6 +50,12 @@ class CORE_EXPORT IDocument : public QObject
Q_OBJECT
public:
enum class OpenResult {
Success,
ReadError,
CannotHandle
};
// This enum must match the indexes of the reloadBehavior widget
// in generalsettings.ui
enum ReloadSetting {
......@@ -86,7 +92,7 @@ public:
Id id() const;
// required to be re-implemented for documents of IEditors
virtual bool open(QString *errorString, const QString &fileName, const QString &realFileName);
virtual OpenResult open(QString *errorString, const QString &fileName, const QString &realFileName);
virtual bool save(QString *errorString, const QString &fileName = QString(), bool autoSave = false) = 0;
virtual bool setContents(const QByteArray &contents);
......
......@@ -70,23 +70,27 @@ FormWindowFile::FormWindowFile(QDesignerFormWindowInterface *form, QObject *pare
m_resourceHandler, &ResourceHandler::updateResources);
}
bool FormWindowFile::open(QString *errorString, const QString &fileName, const QString &realFileName)
Core::IDocument::OpenResult FormWindowFile::open(QString *errorString, const QString &fileName,
const QString &realFileName)
{
if (Designer::Constants::Internal::debug)
qDebug() << "FormWindowFile::open" << fileName;
QDesignerFormWindowInterface *form = formWindow();
QTC_ASSERT(form, return false);
QTC_ASSERT(form, return OpenResult::CannotHandle);
if (fileName.isEmpty())
return true;
return OpenResult::ReadError;
const QFileInfo fi(fileName);
const QString absfileName = fi.absoluteFilePath();
QString contents;
if (read(absfileName, &contents, errorString) != Utils::TextFileFormat::ReadSuccess)
return false;
Utils::TextFileFormat::ReadResult readResult = read(absfileName, &contents, errorString);
if (readResult == Utils::TextFileFormat::ReadEncodingError)
return OpenResult::CannotHandle;
else if (readResult != Utils::TextFileFormat::ReadSuccess)
return OpenResult::ReadError;
form->setFileName(absfileName);
const QByteArray contentsBA = contents.toUtf8();
......@@ -94,7 +98,7 @@ bool FormWindowFile::open(QString *errorString, const QString &fileName, const Q
str.setData(contentsBA);
str.open(QIODevice::ReadOnly);
if (!form->setContents(&str, errorString))
return false;
return OpenResult::CannotHandle;
form->setDirty(fileName != realFileName);
syncXmlFromFormWindow();
......@@ -102,7 +106,7 @@ bool FormWindowFile::open(QString *errorString, const QString &fileName, const Q
setShouldAutoSave(false);
resourceHandler()->updateProjectResources();
return true;
return OpenResult::Success;
}
bool FormWindowFile::save(QString *errorString, const QString &name, bool autoSave)
......@@ -209,7 +213,8 @@ bool FormWindowFile::reload(QString *errorString, ReloadFlag flag, ChangeType ty
emit changed();
} else {
emit aboutToReload();
const bool success = open(errorString, filePath().toString(), filePath().toString());
const bool success
= (open(errorString, filePath().toString(), filePath().toString()) == OpenResult::Success);
emit reloadFinished(success);
return success;
}
......
......@@ -53,8 +53,8 @@ public:
~FormWindowFile() override { }
// IDocument
bool open(QString *errorString, const QString &fileName,
const QString &realFileName) override;
OpenResult open(QString *errorString, const QString &fileName,
const QString &realFileName) override;
bool save(QString *errorString, const QString &fileName, bool autoSave) override;
bool setContents(const QByteArray &contents) override;
bool shouldAutoSave() const override;
......
......@@ -235,18 +235,20 @@ bool DiffEditorDocument::reload(QString *errorString, ReloadFlag flag, ChangeTyp
Q_UNUSED(type)
if (flag == FlagIgnore)
return true;
return open(errorString, filePath().toString(), filePath().toString());
return open(errorString, filePath().toString(), filePath().toString()) == OpenResult::Success;
}
bool DiffEditorDocument::open(QString *errorString, const QString &fileName,
Core::IDocument::OpenResult DiffEditorDocument::open(QString *errorString, const QString &fileName,
const QString &realFileName)
{
QTC_ASSERT(errorString, return false);
QTC_ASSERT(fileName == realFileName, return false); // does not support autosave
QTC_CHECK(fileName == realFileName); // does not support autosave
beginReload();
QString patch;
if (read(fileName, &patch, errorString) != TextFileFormat::ReadSuccess)
return false;
ReadResult readResult = read(fileName, &patch, errorString);
if (readResult == TextFileFormat::ReadEncodingError)
return OpenResult::CannotHandle;
else if (readResult != TextFileFormat::ReadSuccess)
return OpenResult::ReadError;
bool ok = false;
QList<FileData> fileDataList = DiffUtils::readPatch(patch, &ok);
......@@ -262,7 +264,7 @@ bool DiffEditorDocument::open(QString *errorString, const QString &fileName,
setDiffFiles(fileDataList, fi.absolutePath());
}
endReload(ok);
return ok;
return ok ? OpenResult::Success : OpenResult::CannotHandle;
}
QString DiffEditorDocument::suggestedFileName() const
......
......@@ -77,7 +77,7 @@ public:
bool save(QString *errorString, const QString &fileName, bool autoSave);
void reload();
bool reload(QString *errorString, ReloadFlag flag, ChangeType type);
bool open(QString *errorString, const QString &fileName, const QString &realFileName);
OpenResult open(QString *errorString, const QString &fileName, const QString &realFileName);
QString plainText() const;
......
......@@ -85,25 +85,29 @@ ImageViewerFile::~ImageViewerFile()
cleanUp();
}
bool ImageViewerFile::open(QString *errorString, const QString &fileName, const QString &realFileName)
Core::IDocument::OpenResult ImageViewerFile::open(QString *errorString, const QString &fileName,
const QString &realFileName)
{
QTC_CHECK(fileName == realFileName); // does not support auto save
bool success = openImpl(errorString, fileName);
emit openFinished(success);
OpenResult success = openImpl(errorString, fileName);
emit openFinished(success == OpenResult::Success);
return success;
}
bool ImageViewerFile::openImpl(QString *errorString, const QString &fileName)
Core::IDocument::OpenResult ImageViewerFile::openImpl(QString *errorString, const QString &fileName)
{
cleanUp();
m_type = TypeInvalid;
if (!QFileInfo(fileName).isReadable())
return OpenResult::ReadError;
QByteArray format = QImageReader::imageFormat(fileName);
// if it is impossible to recognize a file format - file will not be open correctly
if (format.isEmpty()) {
if (errorString)
*errorString = tr("Image format not supported.");
return false;
return OpenResult::CannotHandle;
}
#ifndef QT_NO_SVG
......@@ -114,7 +118,7 @@ bool ImageViewerFile::openImpl(QString *errorString, const QString &fileName)
if (bound.width() == 0 && bound.height() == 0) {
if (errorString)
*errorString = tr("Failed to read SVG image.");
return false;
return OpenResult::CannotHandle;
}
emit imageSizeChanged(QSize());
} else
......@@ -135,7 +139,7 @@ bool ImageViewerFile::openImpl(QString *errorString, const QString &fileName)
if (errorString)
*errorString = tr("Failed to read image.");
delete m_pixmap;
return false;
return OpenResult::CannotHandle;
}
emit imageSizeChanged(m_pixmap->size());
}
......@@ -143,7 +147,7 @@ bool ImageViewerFile::openImpl(QString *errorString, const QString &fileName)
setFilePath(Utils::FileName::fromString(fileName));
Utils::MimeDatabase mdb;
setMimeType(mdb.mimeTypeForFile(fileName).name());
return true;
return OpenResult::Success;
}
Core::IDocument::ReloadBehavior ImageViewerFile::reloadBehavior(ChangeTrigger state, ChangeType type) const
......@@ -166,7 +170,7 @@ bool ImageViewerFile::reload(QString *errorString,
return true;
}
emit aboutToReload();
bool success = openImpl(errorString, filePath().toString());
bool success = (openImpl(errorString, filePath().toString()) == OpenResult::Success);
emit reloadFinished(success);
return success;
}
......
......@@ -65,7 +65,7 @@ public:
ImageViewerFile();
~ImageViewerFile();
bool open(QString *errorString, const QString &fileName, const QString &realFileName);
OpenResult open(QString *errorString, const QString &fileName, const QString &realFileName);
bool save(QString *errorString, const QString &fileName, bool autoSave);
bool setContents(const QByteArray &contents);
......@@ -93,7 +93,7 @@ signals:
private:
void cleanUp();
bool openImpl(QString *errorString, const QString &fileName);
OpenResult openImpl(QString *errorString, const QString &fileName);
ImageType m_type = TypeInvalid;
#ifndef QT_NO_SVG
......
......@@ -113,19 +113,19 @@ ResourceFile::~ResourceFile()
clearPrefixList();
}
bool ResourceFile::load()
Core::IDocument::OpenResult ResourceFile::load()
{
m_error_message.clear();
if (m_file_name.isEmpty()) {
m_error_message = tr("The file name is empty.");
return false;
return Core::IDocument::OpenResult::ReadError;
}
QFile file(m_file_name);
if (!file.open(QIODevice::ReadOnly)) {
m_error_message = file.errorString();
return false;
return Core::IDocument::OpenResult::ReadError;
}
QByteArray data = file.readAll();
// Detect line ending style
......@@ -143,13 +143,13 @@ bool ResourceFile::load()
if (!doc.setContent(data, &error_msg, &error_line, &error_col)) {
m_error_message = tr("XML error on line %1, col %2: %3")
.arg(error_line).arg(error_col).arg(error_msg);
return false;
return Core::IDocument::OpenResult::CannotHandle;
}
QDomElement root = doc.firstChildElement(QLatin1String("RCC"));
if (root.isNull()) {
m_error_message = tr("The <RCC> root element is missing.");
return false;
return Core::IDocument::OpenResult::CannotHandle;
}
QDomElement relt = root.firstChildElement(QLatin1String("qresource"));
......@@ -181,7 +181,7 @@ bool ResourceFile::load()
}
}
return true;
return Core::IDocument::OpenResult::Success;
}
QString ResourceFile::contents() const
......@@ -1068,11 +1068,11 @@ QModelIndex ResourceModel::deleteItem(const QModelIndex &idx)
return index(file_idx, 0, prefix_model_idx);
}
bool ResourceModel::reload()
Core::IDocument::OpenResult ResourceModel::reload()
{
beginResetModel();
const bool result = m_resource_file.load();
if (result)
Core::IDocument::OpenResult result = m_resource_file.load();
if (result == Core::IDocument::OpenResult::Success)
setDirty(false);
endResetModel();
return result;
......
......@@ -38,6 +38,7 @@
#include <QStringList>
#include <QIcon>
#include <coreplugin/idocument.h>
#include <utils/textfileformat.h>
namespace ResourceEditor {
......@@ -137,7 +138,7 @@ public:
void setFileName(const QString &file_name) { m_file_name = file_name; }
QString fileName() const { return m_file_name; }
bool load();
Core::IDocument::OpenResult load();
bool save();
QString contents() const;
QString errorMessage() const { return m_error_message; }
......@@ -255,7 +256,7 @@ private:
bool renameFile(const QString &fileName, const QString &newFileName);
public:
virtual bool reload();
virtual Core::IDocument::OpenResult reload();
virtual bool save();
QString contents() const { return m_resource_file.contents(); }
......
......@@ -120,8 +120,9 @@ ResourceEditorW::~ResourceEditorW()
delete m_toolBar;
}
bool ResourceEditorDocument::open(QString *errorString, const QString &fileName,
const QString &realFileName)
Core::IDocument::OpenResult ResourceEditorDocument::open(QString *errorString,
const QString &fileName,
const QString &realFileName)
{
if (debugResourceEditorW)
qDebug() << "ResourceEditorW::open: " << fileName;
......@@ -130,11 +131,12 @@ bool ResourceEditorDocument::open(QString *errorString, const QString &fileName,
m_model->setFileName(realFileName);
if (!m_model->reload()) {
OpenResult openResult = m_model->reload();
if (openResult != OpenResult::Success) {
*errorString = m_model->errorMessage();
setBlockDirtyChanged(false);
emit loaded(false);
return false;
return openResult;
}
setFilePath(FileName::fromString(fileName));
......@@ -143,7 +145,7 @@ bool ResourceEditorDocument::open(QString *errorString, const QString &fileName,
m_shouldAutoSave = false;
emit loaded(true);
return true;
return OpenResult::Success;
}
bool ResourceEditorDocument::save(QString *errorString, const QString &name, bool autoSave)
......@@ -194,7 +196,7 @@ bool ResourceEditorDocument::setContents(const QByteArray &contents)
const QString originalFileName = m_model->fileName();
m_model->setFileName(saver.fileName());
const bool success = m_model->reload();
const bool success = (m_model->reload() == OpenResult::Success);
m_model->setFileName(originalFileName);
m_shouldAutoSave = false;
if (debugResourceEditorW)
......@@ -253,7 +255,7 @@ bool ResourceEditorDocument::reload(QString *errorString, ReloadFlag flag, Chang
} else {
emit aboutToReload();
QString fn = filePath().toString();
const bool success = open(errorString, fn, fn);
const bool success = (open(errorString, fn, fn) == OpenResult::Success);
emit reloadFinished(success);
return success;
}
......
......@@ -57,7 +57,7 @@ public:
ResourceEditorDocument(QObject *parent = 0);
//IDocument
bool open(QString *errorString, const QString &fileName, const QString &realFileName);
OpenResult open(QString *errorString, const QString &fileName, const QString &realFileName);
bool save(QString *errorString, const QString &fileName, bool autoSave);
QString plainText() const;
bool setContents(const QByteArray &contents);
......
......@@ -139,7 +139,7 @@ void ResourceTopLevelNode::update()
QMap<QPair<QString, QString>, QList<ProjectExplorer::FileNode *> > filesToAdd;
ResourceFile file(path().toString());
if (file.load()) {
if (file.load() == Core::IDocument::OpenResult::Success) {
QSet<QPair<QString, QString > > prefixes;
int prfxcount = file.prefixCount();
......
......@@ -94,12 +94,11 @@ bool TaskFile::reload(QString *errorString, ReloadFlag flag, ChangeType type)
deleteLater();
return true;
}
return open(errorString, filePath().toString(), filePath().toString());
return load(errorString, filePath().toString());
}
bool TaskFile::open(QString *errorString, const QString &fileName, const QString &realFileName)
bool TaskFile::load(QString *errorString, const QString &fileName)
{
Q_UNUSED(realFileName)
setFilePath(Utils::FileName::fromString(fileName));
return TaskListPlugin::loadFile(errorString, m_baseDir, fileName);
}
......
......@@ -55,7 +55,7 @@ public:
ReloadBehavior reloadBehavior(ChangeTrigger state, ChangeType type) const;
bool reload(QString *errorString, ReloadFlag flag, ChangeType type);
bool open(QString *errorString, const QString &fileName, const QString &realFileName);
bool load(QString *errorString, const QString &fileName);
QString baseDir() const;
void setBaseDir(const QString &base);
......
......@@ -177,7 +177,7 @@ IDocument *TaskListPlugin::openTasks(const QString &base, const QString &fileNam
file->setBaseDir(base);
QString errorString;
if (!file->open(&errorString, fileName, fileName)) {
if (!file->load(&errorString, fileName)) {
QMessageBox::critical(ICore::mainWindow(), tr("File Error"), errorString);
delete file;
return 0;
......
......@@ -558,20 +558,21 @@ void TextDocument::checkPermissions()
emit changed();
}
bool TextDocument::open(QString *errorString, const QString &fileName, const QString &realFileName)
Core::IDocument::OpenResult TextDocument::open(QString *errorString, const QString &fileName,
const QString &realFileName)
{
emit aboutToOpen(fileName, realFileName);
bool success = openImpl(errorString, fileName, realFileName);
if (success) {
OpenResult success = openImpl(errorString, fileName, realFileName);
if (success == OpenResult::Success) {
Utils::MimeDatabase mdb;
setMimeType(mdb.mimeTypeForFile(fileName).name());
emit openFinishedSuccessfully();
return true;
}
return false;
return success;
}
bool TextDocument::openImpl(QString *errorString, const QString &fileName, const QString &realFileName)
Core::IDocument::OpenResult TextDocument::openImpl(QString *errorString, const QString &fileName,
const QString &realFileName)
{
QStringList content;
......@@ -608,14 +609,15 @@ bool TextDocument::openImpl(QString *errorString, const QString &fileName, const
}
TextDocumentLayout *documentLayout =
qobject_cast<TextDocumentLayout*>(d->m_document.documentLayout());
QTC_ASSERT(documentLayout, return true);
QTC_ASSERT(documentLayout, return OpenResult::CannotHandle);
documentLayout->lastSaveRevision = d->m_autoSaveRevision = d->m_document.revision();
d->updateRevisions();
d->m_document.setModified(fileName != realFileName);
setFilePath(Utils::FileName::fromUserInput(fi.absoluteFilePath()));
}
return readResult == Utils::TextFileFormat::ReadSuccess
|| readResult == Utils::TextFileFormat::ReadEncodingError;
if (readResult == Utils::TextFileFormat::ReadIOError)
return OpenResult::ReadError;
return OpenResult::Success;
}
bool TextDocument::reload(QString *errorString, QTextCodec *codec)
......@@ -634,7 +636,7 @@ bool TextDocument::reload(QString *errorString)
if (documentLayout)
marks = documentLayout->documentClosing(); // removes text marks non-permanently
bool success = openImpl(errorString, filePath().toString(), filePath().toString());
bool success = (openImpl(errorString, filePath().toString(), filePath().toString()) == OpenResult::Success);
if (documentLayout)
documentLayout->documentReloaded(marks, this); // re-adds text marks
......
......@@ -117,7 +117,7 @@ public:
void setDefaultPath(const QString &defaultPath);
void setSuggestedFileName(const QString &suggestedFileName);
bool open(QString *errorString, const QString &fileName, const QString &realFileName);
OpenResult open(QString *errorString, const QString &fileName, const QString &realFileName);
virtual bool reload(QString *errorString);
bool setPlainText(const QString &text);
......@@ -148,7 +148,7 @@ protected slots:
virtual void applyFontSettings();
private:
bool openImpl(QString *errorString, const QString &fileName, const QString &realFileName);
OpenResult openImpl(QString *errorString, const QString &fileName, const QString &realFileName);
void cleanWhitespace(QTextCursor &cursor, bool cleanIndentation, bool inEntireDocument);
void ensureFinalNewLine(QTextCursor &cursor);
......
......@@ -57,22 +57,23 @@ SubmitEditorFile::SubmitEditorFile(const VcsBaseSubmitEditorParameters *paramete
setTemporary(true);
}
bool SubmitEditorFile::open(QString *errorString, const QString &fileName, const QString &realFileName)
Core::IDocument::OpenResult SubmitEditorFile::open(QString *errorString, const QString &fileName,
const QString &realFileName)
{
if (fileName.isEmpty())
return false;
return OpenResult::ReadError;
FileReader reader;
if (!reader.fetch(realFileName, QIODevice::Text, errorString))
return false;
return OpenResult::ReadError;
const QString text = QString::fromLocal8Bit(reader.data());
if (!m_editor->setFileContents(text.toUtf8()))
return false;
return OpenResult::CannotHandle;
setFilePath(FileName::fromString(fileName));
setModified(fileName != realFileName);
return true;
return OpenResult::Success;
}
bool SubmitEditorFile::setContents(const QByteArray &contents)
......
......@@ -47,7 +47,7 @@ public:
explicit SubmitEditorFile(const VcsBaseSubmitEditorParameters *parameters,
VcsBaseSubmitEditor *parent = 0);
bool open(QString *errorString, const QString &fileName, const QString &realFileName);
OpenResult open(QString *errorString, const QString &fileName, const QString &realFileName);
bool setContents(const QByteArray &contents);
QString defaultPath() const { return QString(); }
QString suggestedFileName() const { return QString(); }
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment