Add warning when loading unsupported property type and mark the file as read-only (fix #3812, fix #3811)

This commit is contained in:
Martín Capello 2023-04-25 13:26:35 -03:00 committed by David Capello
parent 66efb35a8c
commit 3f581a5dfa
15 changed files with 142 additions and 20 deletions

View File

@ -41,6 +41,7 @@ non_transformable_reference_layer = Layer '{}' is reference, cannot be transform
sprite_locked_somewhere = The sprite is locked in other editor
not_enough_transform_memory = Not enough memory to transform the selection
not_enough_rotsprite_memory = Not enough memory for RotSprite
unmodifiable_sprite = Read-only sprite cannot be modified
[alerts]
applying_filter = FX<<Applying effect...||&Cancel
@ -217,6 +218,17 @@ Information
<<Activating Aseprite will give you access to automatic updates.
||&OK
END
load_file_with_incompatibilities = <<<END
Incompatibility error found:
{0}.
This file will be opened as read-only. Please upgrade Aseprite to the latest version to be able to save changes without losing data.
END
cannot_overwrite_readonly = Cannot save/overwrite a read-only sprite. Use File > Save As option.
cannot_modify_readonly = Cannot modify a read-only sprite.
[brightness_contrast]
title = Brightness/Contrast

View File

@ -766,6 +766,9 @@ void App::updateDisplayTitleBar()
if (docView) {
// Prepend the document's filename.
title += docView->document()->name();
if (docView->document()->isReadOnly()) {
title += " [Read-Only]";
}
title += " - ";
}

View File

@ -41,7 +41,7 @@ PlayAnimationCommand::PlayAnimationCommand()
bool PlayAnimationCommand::onEnabled(Context* ctx)
{
return ctx->checkFlags(ContextFlags::ActiveDocumentIsWritable |
return ctx->checkFlags(ContextFlags::ActiveDocumentIsReadable |
ContextFlags::HasActiveSprite);
}

View File

@ -162,6 +162,12 @@ std::string SaveFileBaseCommand::saveAsDialog(
if (filename.empty())
return std::string();
// Document is being saved under a different path/name, remove
// read-only mark then.
if (filename != initialFilename) {
document->removeReadOnlyMark();
}
if (saveInBackground == SaveInBackground::On) {
saveDocumentInBackground(
context, document,
@ -238,8 +244,10 @@ void SaveFileBaseCommand::saveDocumentInBackground(
console.printf(fop->error().c_str());
// We don't know if the file was saved correctly or not. So mark
// it as it should be saved again.
document->impossibleToBackToSavedState();
// it as it should be saved again. Except for read-only documents,
// since we know they must not be saved.
if (!document->isReadOnly())
document->impossibleToBackToSavedState();
}
// If the job was cancelled, mark the document as modified.
else if (fop->isStop()) {
@ -315,6 +323,7 @@ public:
protected:
void onExecute(Context* context) override;
bool onEnabled(Context* context) override;
};
SaveFileAsCommand::SaveFileAsCommand()
@ -331,6 +340,11 @@ void SaveFileAsCommand::onExecute(Context* context)
MarkAsSaved::On);
}
bool SaveFileAsCommand::onEnabled(Context* context)
{
return context->checkFlags(ContextFlags::ActiveDocumentIsReadable);
}
class SaveFileCopyAsCommand : public SaveFileBaseCommand {
public:
SaveFileCopyAsCommand();

View File

@ -44,7 +44,7 @@ void ContextFlags::update(Context* context)
updateFlagsFromSite(site);
if (document->canWriteLockFromRead())
if (document->canWriteLockFromRead() && !document->isReadOnly())
m_flags |= ActiveDocumentIsWritable;
document->unlock();

View File

@ -332,6 +332,25 @@ bool Doc::isFullyBackedUp() const
return (m_flags & kFullyBackedUp ? true: false);
}
void Doc::markAsReadOnly()
{
DOC_TRACE("DOC: Mark as read-only", this);
m_flags |= kReadOnly;
}
bool Doc::isReadOnly() const
{
return (m_flags & kReadOnly ? true: false);
}
void Doc::removeReadOnlyMark()
{
DOC_TRACE("DOC: Read-only mark removed", this);
m_flags &= ~kReadOnly;
}
//////////////////////////////////////////////////////////////////////
// Loaded options from file

View File

@ -12,8 +12,10 @@
#include "app/doc_observer.h"
#include "app/extra_cel.h"
#include "app/file/format_options.h"
#include "app/i18n/strings.h"
#include "app/transformation.h"
#include "base/disable_copying.h"
#include "base/exception.h"
#include "base/rw_lock.h"
#include "doc/blend_mode.h"
#include "doc/color.h"
@ -64,6 +66,7 @@ namespace app {
kMaskVisible = 2, // The mask wasn't hidden by the user
kInhibitBackup = 4, // Inhibit the backup process
kFullyBackedUp = 8, // Full backup was done
kReadOnly = 16,// This document is read-only
};
public:
Doc(Sprite* sprite);
@ -146,6 +149,10 @@ namespace app {
void markAsBackedUp();
bool isFullyBackedUp() const;
void markAsReadOnly();
bool isReadOnly() const;
void removeReadOnlyMark();
//////////////////////////////////////////////////////////////////////
// Loaded options from file
@ -268,6 +275,14 @@ namespace app {
DISABLE_COPYING(Doc);
};
// Exception thrown when we want to modify a sprite (add new
// app::Cmd objects) marked as read-only.
class CannotModifyWhenReadOnlyException : public base::Exception {
public:
CannotModifyWhenReadOnlyException() throw()
: base::Exception(Strings::alerts_cannot_modify_readonly()) { }
};
} // namespace app
#endif

View File

@ -54,6 +54,10 @@ public:
m_fop->setError(msg.c_str());
}
void incompatibilityError(const std::string& msg) override {
m_fop->setIncompatibilityError(msg.c_str());
}
void progress(double fromZeroToOne) override {
m_fop->setProgress(fromZeroToOne);
}
@ -289,7 +293,7 @@ bool AseFormat::onLoad(FileOp* fop)
return false;
Sprite* sprite = delegate.sprite();
fop->createDocument(sprite);
fop->createDocument(sprite, fop->hasIncompatibilityError());
if (sprite->colorSpace() != nullptr &&
sprite->colorSpace()->type() != gfx::ColorSpace::None) {

View File

@ -920,6 +920,11 @@ void FileOp::operate(IFileOpProgress* progress)
setError("Error loading data file: %s\n", ex.what());
}
}
if (hasIncompatibilityError()) {
setError(fmt::format(Strings::alerts_load_file_with_incompatibilities(),
m_incompatibilityError).c_str());
}
}
// Save //////////////////////////////////////////////////////////////////////
else if (m_type == FileOpSave &&
@ -936,6 +941,11 @@ void FileOp::operate(IFileOpProgress* progress)
}
#endif
if (m_document && m_document->isReadOnly()) {
setError(fmt::format(Strings::alerts_cannot_overwrite_readonly()).c_str());
return;
}
// Save a sequence
if (isSequence()) {
ASSERT(m_format->support(FILE_SUPPORT_SEQUENCES));
@ -1081,12 +1091,14 @@ FileOp::~FileOp()
delete m_seq.palette;
}
void FileOp::createDocument(Sprite* spr)
void FileOp::createDocument(Sprite* spr, bool readOnly)
{
// spr can be NULL if the sprite is set in onPostLoad() then
ASSERT(m_document == NULL);
m_document = new Doc(spr);
if (readOnly)
m_document->markAsReadOnly();
}
void FileOp::postLoad()
@ -1362,6 +1374,18 @@ void FileOp::setError(const char *format, ...)
}
}
void FileOp::setIncompatibilityError(const std::string& msg)
{
// Concatenate the new error
{
std::lock_guard lock(m_mutex);
// Add a newline char automatically if it's needed
if (!m_incompatibilityError.empty() && m_incompatibilityError.back() != '\n')
m_incompatibilityError.push_back('\n');
m_incompatibilityError += msg;
}
}
void FileOp::setProgress(double progress)
{
std::lock_guard lock(m_mutex);

View File

@ -168,7 +168,7 @@ namespace app {
const FileOpROI& roi() const { return m_roi; }
void createDocument(Sprite* spr);
void createDocument(Sprite* spr, bool readOnly = false);
void operate(IFileOpProgress* progress = nullptr);
void done();
@ -247,6 +247,8 @@ namespace app {
const std::string& error() const { return m_error; }
void setError(const char *error, ...);
bool hasError() const { return !m_error.empty(); }
void setIncompatibilityError(const std::string& msg);
bool hasIncompatibilityError() const { return !m_incompatibilityError.empty(); }
double progress() const;
void setProgress(double progress);
@ -283,6 +285,7 @@ namespace app {
double m_progress; // Progress (1.0 is ready).
IFileOpProgress* m_progressInterface;
std::string m_error; // Error string.
std::string m_incompatibilityError; // Incompatibility error string.
bool m_done; // True if the operation finished.
bool m_stop; // Force the break of the operation.
bool m_oneframe; // Load just one frame (in formats

View File

@ -140,6 +140,12 @@ void Transaction::rollback(CmdTransaction* newCmds)
void Transaction::execute(Cmd* cmd)
{
// Read-only sprites cannot be modified.
if (m_doc->isReadOnly()) {
delete cmd;
throw CannotModifyWhenReadOnlyException();
}
// If we are undoing/redoing, just throw an exception, we cannot
// modify the sprite while we are moving throw the undo history.
// To undo/redo we have just to call the onUndo/onRedo of each

View File

@ -2418,7 +2418,8 @@ bool Editor::canDraw()
m_layer->isImage() &&
m_layer->isVisibleHierarchy() &&
m_layer->isEditableHierarchy() &&
!m_layer->isReference());
!m_layer->isReference() &&
!m_document->isReadOnly());
}
bool Editor::isInsideSelection()

View File

@ -783,6 +783,14 @@ tools::ToolLoop* create_tool_loop(
Site site = editor->getSite();
doc::Grid grid = site.grid();
// If the document is read-only.
if (site.document()->isReadOnly()) {
StatusBar::instance()->showTip(
1000,
fmt::format(Strings::statusbar_tips_unmodifiable_sprite()));
return nullptr;
}
ToolLoopParams params;
params.tool = editor->getCurrentEditorTool();
params.ink = editor->getCurrentEditorInk();

View File

@ -1295,20 +1295,26 @@ void AsepriteDecoder::readPropertiesMaps(doc::UserData::PropertiesMaps& properti
auto startPos = f()->tell();
auto size = read32();
auto numMaps = read32();
for (int i=0; i<numMaps; ++i) {
auto id = read32();
std::string extensionId; // extensionId = empty by default (when id == 0)
if (id &&
!extFiles.getFilenameByID(id, extensionId)) {
// This shouldn't happen, but if it does, we put the properties
// in an artificial extensionId.
extensionId = fmt::format("__missed__{}", id);
delegate()->error(
fmt::format("Error: Invalid extension ID (id={0} not found)", id));
try {
for (int i=0; i<numMaps; ++i) {
auto id = read32();
std::string extensionId; // extensionId = empty by default (when id == 0)
if (id &&
!extFiles.getFilenameByID(id, extensionId)) {
// This shouldn't happen, but if it does, we put the properties
// in an artificial extensionId.
extensionId = fmt::format("__missed__{}", id);
delegate()->error(
fmt::format("Error: Invalid extension ID (id={0} not found)", id));
}
auto properties = readPropertyValue(USER_DATA_PROPERTY_TYPE_PROPERTIES);
propertiesMaps[extensionId] = doc::get_value<doc::UserData::Properties>(properties);
}
auto properties = readPropertyValue(USER_DATA_PROPERTY_TYPE_PROPERTIES);
propertiesMaps[extensionId] = doc::get_value<doc::UserData::Properties>(properties);
}
catch(const base::Exception& e) {
delegate()->incompatibilityError(fmt::format("Error reading custom properties: {0}", e.what()));
}
f()->seek(startPos+size);
}
@ -1420,6 +1426,9 @@ const doc::UserData::Variant AsepriteDecoder::readPropertyValue(uint16_t type)
}
return value;
}
default: {
throw base::Exception(fmt::format("Unexpected property type '{0}' at file position {1}", type, f()->tell()));
}
}
return doc::UserData::Variant{};

View File

@ -24,6 +24,10 @@ public:
// Used to log errors
virtual void error(const std::string& msg) { }
// Sets an error when an incompatibility issue is
// detected.
virtual void incompatibilityError(const std::string &msg) { }
// Used to report progress of the whole operation
virtual void progress(double fromZeroToOne) { }