Centralize the warning message for incompatible files/forward compatibility (#3811, #3812)

Several changes were included:
- Fixes in TextBox widget to show it with proper size hint when it's
  outside a viewport
- Added the IncompatFileWindow with a message + link to know how to
  update Aseprite and solve the situation
- Moved CannotModifyWhenReadOnlyException from app/doc.h to
  app/transaction.h
This commit is contained in:
David Capello 2023-05-11 12:18:31 -03:00
parent 77cdbf4739
commit c32b9b07a8
16 changed files with 201 additions and 57 deletions

View File

@ -528,6 +528,10 @@
<background color="textbox_face" />
<text color="textbox_text" align="left" />
</style>
<style id="textbox_label" extends="textbox_text">
<background color="window_face" />
<text color="text" align="left" />
</style>
<style id="label" padding="1">
<text color="text" align="left" />
<text color="disabled" align="left" state="disabled" />

View File

@ -521,6 +521,10 @@
<background color="textbox_face" />
<text color="textbox_text" align="left" />
</style>
<style id="textbox_label" extends="textbox_text">
<background color="window_face" />
<text color="text" align="left" />
</style>
<style id="label" padding="1">
<text color="text" align="left" />
<text color="disabled" align="left" state="disabled" />

View File

@ -41,7 +41,10 @@ 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
cannot_modify_readonly_sprite = <<<END
Cannot modify a read-only sprite.
Use File > Save menu for more information.
END
[alerts]
applying_filter = FX<<Applying effect...||&Cancel
@ -218,17 +221,6 @@ 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
@ -1020,6 +1012,20 @@ layer_name = Sprite Sheet
import = &Import
cancel = &Cancel
[incompat_file]
title = Incompatible File
message = <<<END
This file was originally created with a newer version of Aseprite which contains information we cannot fully read.
It's marked as read-only to avoid losing that information when you try to save/overwrite it.
To solve this situation you can:
- Update Aseprite to the latest version and try to load the file again (recommended), or
- Use the "File > Save As" option to save the file with other name (you will lose information stored in the original file anyway)
END
incompatibilities = Incompatibilities:
update_link = Update Aseprite
[inks]
simple_ink = Simple Ink
alpha_compositing = Alpha Compositing

View File

@ -0,0 +1,23 @@
<!-- Aseprite -->
<!-- Copyright (c) 2023 Igara Studio S.A. -->
<gui>
<window id="incompat_file" text="@.title">
<vbox>
<textbox text="@.message" wordwrap="true" style="textbox_label" />
<vbox id="errors_placeholder" expansive="true">
<label text="@.incompatibilities" />
<view id="errors_view" expansive="true">
<textbox id="errors" text="" />
</view>
</vbox>
<hbox>
<boxfiller />
<vbox>
<link text="@.update_link" url="https://www.aseprite.org/faq/#update" />
<button text="@general.close" closewindow="true" id="ok" magnet="true" minwidth="60" />
</vbox>
<boxfiller />
</hbox>
</vbox>
</window>
</gui>

View File

@ -386,6 +386,7 @@ if(ENABLE_UI)
ui/hex_color_entry.cpp
ui/home_view.cpp
ui/icon_button.cpp
ui/incompat_file_window.cpp
ui/input_chain.cpp
ui/keyboard_shortcuts.cpp
ui/main_menu_bar.cpp

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2019-2022 Igara Studio S.A.
// Copyright (C) 2019-2023 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of
@ -30,6 +30,7 @@
#include "app/recent_files.h"
#include "app/restore_visible_layers.h"
#include "app/ui/export_file_window.h"
#include "app/ui/incompat_file_window.h"
#include "app/ui/layer_frame_comboboxes.h"
#include "app/ui/optional_alert.h"
#include "app/ui/status_bar.h"
@ -112,7 +113,7 @@ void SaveFileBaseCommand::onLoadParams(const Params& params)
// [main thread]
bool SaveFileBaseCommand::onEnabled(Context* context)
{
return context->checkFlags(ContextFlags::ActiveDocumentIsWritable);
return context->checkFlags(ContextFlags::ActiveDocumentIsReadable);
}
std::string SaveFileBaseCommand::saveAsDialog(
@ -200,6 +201,17 @@ void SaveFileBaseCommand::saveDocumentInBackground(
const ResizeOnTheFly resizeOnTheFly,
const gfx::PointF& scale)
{
#ifdef ENABLE_UI
// If the document is read only, we cannot save it directly (we have
// to use File > Save As)
if (document->isReadOnly() &&
context->isUIAvailable()) {
IncompatFileWindow window;
window.show();
return;
}
#endif // ENABLE_UI
if (params().aniDir.isSet()) {
switch (params().aniDir()) {
case AniDir::REVERSE:
@ -323,7 +335,6 @@ public:
protected:
void onExecute(Context* context) override;
bool onEnabled(Context* context) override;
};
SaveFileAsCommand::SaveFileAsCommand()
@ -340,11 +351,6 @@ 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

@ -12,10 +12,8 @@
#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"
@ -149,6 +147,13 @@ namespace app {
void markAsBackedUp();
bool isFullyBackedUp() const;
// TODO This read-only flag might be confusing because it
// indicates that the file was loaded from an incompatible
// version (future unknown feature) and it's preferable to
// mark the sprite as read-only to avoid overwriting unknown
// data. If in the future we want to add the possibility to
// mark a regular file as read-only, this flag'll need a new
// name.
void markAsReadOnly();
bool isReadOnly() const;
void removeReadOnlyMark();
@ -275,14 +280,6 @@ 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

@ -293,7 +293,7 @@ bool AseFormat::onLoad(FileOp* fop)
return false;
Sprite* sprite = delegate.sprite();
fop->createDocument(sprite, fop->hasIncompatibilityError());
fop->createDocument(sprite);
if (sprite->colorSpace() != nullptr &&
sprite->colorSpace()->type() != gfx::ColorSpace::None) {

View File

@ -28,6 +28,7 @@
#include "app/modules/palettes.h"
#include "app/pref/preferences.h"
#include "app/tx.h"
#include "app/ui/incompat_file_window.h"
#include "app/ui/optional_alert.h"
#include "app/ui/status_bar.h"
#include "base/fs.h"
@ -920,12 +921,6 @@ 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 &&
@ -942,10 +937,9 @@ void FileOp::operate(IFileOpProgress* progress)
}
#endif
if (m_document && m_document->isReadOnly()) {
setError(Strings::alerts_cannot_overwrite_readonly().c_str());
return;
}
// TODO Should we check m_document->isReadOnly() here? the flag
// is already checked in SaveFileBaseCommand::saveDocumentInBackground
// and only in UI mode (so the CLI still works)
// Save a sequence
if (isSequence()) {
@ -1092,14 +1086,12 @@ FileOp::~FileOp()
delete m_seq.palette;
}
void FileOp::createDocument(Sprite* spr, bool readOnly)
void FileOp::createDocument(Sprite* spr)
{
// 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()
@ -1228,7 +1220,35 @@ void FileOp::postLoad()
}
}
// Mark this document as associated to a file in the disk (so File >
// Save doesn't ask for a new name)
m_document->markAsSaved();
// In case that the document was loaded without all the information
// from the file, i.e. we loaded an .aseprite file created with a
// newer Aseprite version and cannot interpret all its information,
// saving this file should show a warning that some original data
// will be lost if we save/overwrite it.
if (hasIncompatibilityError()) {
// Mark the active undo state as impossible to reach the original
// disk state.
m_document->impossibleToBackToSavedState();
#ifdef ENABLE_UI
if (m_context && m_context->isUIAvailable()) {
IncompatFileWindow window;
window.show(m_incompatibilityError);
}
else
#endif // ENABLE_UI
{
setError(m_incompatibilityError.c_str());
}
// Mark as read-only so we cannot save the file directly (without
// an incompatibility warning/error).
m_document->markAsReadOnly();
}
}
void FileOp::setLoadedFormatOptions(const FormatOptionsPtr& opts)

View File

@ -168,7 +168,8 @@ namespace app {
const FileOpROI& roi() const { return m_roi; }
void createDocument(Sprite* spr, bool readOnly = false);
// Creates a new document with the given sprite.
void createDocument(Sprite* spr);
void operate(IFileOpProgress* progress = nullptr);
void done();

View File

@ -15,6 +15,7 @@
#include "app/context_access.h"
#include "app/doc.h"
#include "app/doc_undo.h"
#include "app/i18n/strings.h"
#include "app/modules/palettes.h"
#include "doc/sprite.h"
#include "ui/manager.h"
@ -26,6 +27,11 @@ namespace app {
using namespace doc;
CannotModifyWhenReadOnlyException::CannotModifyWhenReadOnlyException() throw()
: base::Exception(Strings::statusbar_tips_cannot_modify_readonly_sprite())
{
}
Transaction::Transaction(
Context* ctx,
Doc* doc,

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2019 Igara Studio S.A.
// Copyright (C) 2019-2023 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of
@ -11,6 +11,7 @@
#include "app/cmd_transaction.h"
#include "app/doc_observer.h"
#include "base/exception.h"
#include <string>
@ -26,6 +27,13 @@ namespace app {
DoesntModifyDocument // This item doesn't modify the document.
};
// 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();
};
// High-level class to group a set of commands to modify the
// document atomically, with enough information to rollback the
// whole operation if something fails (e.g. an exceptions is thrown)

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2019-2022 Igara Studio S.A.
// Copyright (C) 2019-2023 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of
@ -786,7 +786,7 @@ tools::ToolLoop* create_tool_loop(
// If the document is read-only.
if (site.document()->isReadOnly()) {
StatusBar::instance()->showTip(
1000, Strings::statusbar_tips_unmodifiable_sprite());
3000, Strings::statusbar_tips_cannot_modify_readonly_sprite());
return nullptr;
}

View File

@ -0,0 +1,36 @@
// Aseprite
// Copyright (c) 2023 Igara Studio S.A.
//
// This program is distributed under the terms of
// the End-User License Agreement for Aseprite.
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include "app/ui/incompat_file_window.h"
#include "base/trim_string.h"
namespace app {
void IncompatFileWindow::show(std::string incompatibilities)
{
base::trim_string(incompatibilities,
incompatibilities);
if (!incompatibilities.empty()) {
errors()->setText(incompatibilities);
errorsView()->setSizeHint(
errorsView()->border().size()
+ gfx::Size(0, std::min(textHeight()*16, // 16 lines as max height
errors()->sizeHint().h)));
}
else {
errorsPlaceholder()->setVisible(false);
}
// Run modal
openWindowInForeground();
}
} // namespace app

View File

@ -0,0 +1,29 @@
// Aseprite
// Copyright (c) 2023 Igara Studio S.A.
//
// This program is distributed under the terms of
// the End-User License Agreement for Aseprite.
#ifndef APP_UI_INCOMPAT_FILE_WINDOW_H_INCLUDED
#define APP_UI_INCOMPAT_FILE_WINDOW_H_INCLUDED
#pragma once
#include "gfx/size.h"
#include "incompat_file.xml.h"
#include <string>
namespace app {
// Shows the window to offer a solution for forward compatibility
// (don't save/overwrite files that were saved with future Aseprite
// versions/unknown data in the original .aseprite file).
class IncompatFileWindow : public app::gen::IncompatFile {
public:
void show(std::string incompatibilities = std::string());
};
}
#endif

View File

@ -1,5 +1,5 @@
// Aseprite UI Library
// Copyright (C) 2019 Igara Studio S.A.
// Copyright (C) 2019-2023 Igara Studio S.A.
// Copyright (C) 2001-2016 David Capello
//
// This file is released under the terms of the MIT license.
@ -12,6 +12,7 @@
#include "ui/textbox.h"
#include "gfx/size.h"
#include "ui/display.h"
#include "ui/intern.h"
#include "ui/message.h"
#include "ui/size_hint_event.h"
@ -26,6 +27,7 @@ namespace ui {
TextBox::TextBox(const std::string& text, int align)
: Widget(kTextBoxWidget)
{
setBgColor(gfx::ColorNone);
setFocusStop(true);
setAlign(align);
setText(text);
@ -156,8 +158,9 @@ void TextBox::onPaint(PaintEvent& ev)
void TextBox::onSizeHint(SizeHintEvent& ev)
{
int w = 0;
int h = 0;
gfx::Size borderSize = border().size();
int w = borderSize.w;
int h = borderSize.h;
Theme::drawTextBox(nullptr, this, &w, &h, gfx::ColorNone, gfx::ColorNone);
@ -165,17 +168,17 @@ void TextBox::onSizeHint(SizeHintEvent& ev)
View* view = View::getView(this);
int width, min = w;
if (view) {
if (view)
width = view->viewportBounds().w;
}
else {
else if (bounds().w > 0)
width = bounds().w;
}
else if (auto display = this->display())
width = display->size().w / guiscale();
else
width = 0;
w = std::max(min, width);
Theme::drawTextBox(nullptr, this, &w, &h, gfx::ColorNone, gfx::ColorNone);
w = min;
}
ev.setSizeHint(gfx::Size(w, h));