1// Aseprite
2// Copyright (C) 2019-2021 Igara Studio S.A.
3// Copyright (C) 2001-2018 David Capello
4//
5// This program is distributed under the terms of
6// the End-User License Agreement for Aseprite.
7
8#ifdef HAVE_CONFIG_H
9#include "config.h"
10#endif
11
12#include "app/commands/cmd_open_file.h"
13
14#include "app/app.h"
15#include "app/commands/command.h"
16#include "app/commands/params.h"
17#include "app/console.h"
18#include "app/doc.h"
19#include "app/file/file.h"
20#include "app/file_selector.h"
21#include "app/i18n/strings.h"
22#include "app/job.h"
23#include "app/modules/editors.h"
24#include "app/modules/gui.h"
25#include "app/pref/preferences.h"
26#include "app/recent_files.h"
27#include "app/ui/status_bar.h"
28#include "app/ui_context.h"
29#include "base/fs.h"
30#include "base/thread.h"
31#include "doc/sprite.h"
32#include "ui/ui.h"
33
34#include <cstdio>
35
36namespace app {
37
38class OpenFileJob : public Job, public IFileOpProgress {
39public:
40 OpenFileJob(FileOp* fop)
41 : Job(Strings::open_file_loading().c_str())
42 , m_fop(fop)
43 {
44 }
45
46 void showProgressWindow() {
47 startJob();
48
49 if (isCanceled())
50 m_fop->stop();
51
52 waitJob();
53 }
54
55private:
56 // Thread to do the hard work: load the file from the disk.
57 virtual void onJob() override {
58 try {
59 m_fop->operate(this);
60 }
61 catch (const std::exception& e) {
62 m_fop->setError("Error loading file:\n%s", e.what());
63 }
64
65 if (m_fop->isStop() && m_fop->document())
66 delete m_fop->releaseDocument();
67
68 m_fop->done();
69 }
70
71 virtual void ackFileOpProgress(double progress) override {
72 jobProgress(progress);
73 }
74
75 FileOp* m_fop;
76};
77
78OpenFileCommand::OpenFileCommand()
79 : Command(CommandId::OpenFile(), CmdRecordableFlag)
80 , m_repeatCheckbox(false)
81 , m_oneFrame(false)
82 , m_seqDecision(gen::SequenceDecision::ASK)
83{
84}
85
86void OpenFileCommand::onLoadParams(const Params& params)
87{
88 m_filename = params.get("filename");
89 m_folder = params.get("folder"); // Initial folder
90 m_repeatCheckbox = params.get_as<bool>("repeat_checkbox");
91 m_oneFrame = params.get_as<bool>("oneframe");
92
93 std::string sequence = params.get("sequence");
94 if (m_oneFrame ||
95 sequence == "skip" ||
96 sequence == "no") {
97 m_seqDecision = gen::SequenceDecision::NO;
98 }
99 else if (sequence == "agree" ||
100 sequence == "yes") {
101 m_seqDecision = gen::SequenceDecision::YES;
102 }
103 else {
104 m_seqDecision = gen::SequenceDecision::ASK;
105 }
106}
107
108void OpenFileCommand::onExecute(Context* context)
109{
110 Console console;
111
112 m_usedFiles.clear();
113
114 base::paths filenames;
115
116 // interactive
117#ifdef ENABLE_UI
118 if (context->isUIAvailable() && m_filename.empty()) {
119 base::paths exts = get_readable_extensions();
120
121 // Add backslash as show_file_selector() expected a filename as
122 // initial path (and the file part is removed from the path).
123 if (!m_folder.empty() && !base::is_path_separator(m_folder[m_folder.size()-1]))
124 m_folder.push_back(base::path_separator);
125
126 if (!app::show_file_selector(Strings::open_file_title(), m_folder, exts,
127 FileSelectorType::OpenMultiple,
128 filenames)) {
129 // The user cancelled the operation through UI
130 return;
131 }
132
133 // If the user selected several files, show the checkbox to repeat
134 // the action for future filenames in the batch of selected files
135 // to open.
136 if (filenames.size() > 1)
137 m_repeatCheckbox = true;
138 }
139 else
140#endif // ENABLE_UI
141 if (!m_filename.empty()) {
142 filenames.push_back(m_filename);
143 }
144
145 if (filenames.empty())
146 return;
147
148 int flags =
149 FILE_LOAD_DATA_FILE |
150 FILE_LOAD_CREATE_PALETTE |
151 (m_repeatCheckbox ? FILE_LOAD_SEQUENCE_ASK_CHECKBOX: 0);
152
153 if (context->isUIAvailable() &&
154 m_seqDecision == gen::SequenceDecision::ASK) {
155 if (Preferences::instance().openFile.openSequence() == gen::SequenceDecision::ASK) {
156 // Do nothing (ask by default, or whatever the command params
157 // specified)
158 }
159 else {
160 m_seqDecision = Preferences::instance().openFile.openSequence();
161 }
162 }
163
164 switch (m_seqDecision) {
165 case gen::SequenceDecision::ASK:
166 flags |= FILE_LOAD_SEQUENCE_ASK;
167 break;
168 case gen::SequenceDecision::YES:
169 flags |= FILE_LOAD_SEQUENCE_YES;
170 break;
171 case gen::SequenceDecision::NO:
172 flags |= FILE_LOAD_SEQUENCE_NONE;
173 break;
174 }
175
176 if (m_oneFrame)
177 flags |= FILE_LOAD_ONE_FRAME;
178
179 std::string filename;
180 while (!filenames.empty()) {
181 filename = filenames[0];
182 filenames.erase(filenames.begin());
183
184 std::unique_ptr<FileOp> fop(
185 FileOp::createLoadDocumentOperation(
186 context, filename, flags));
187 bool unrecent = false;
188
189 // Do nothing (the user cancelled or something like that)
190 if (!fop)
191 return;
192
193 if (fop->hasError()) {
194 console.printf(fop->error().c_str());
195 unrecent = true;
196 }
197 else {
198 if (fop->isSequence()) {
199 if (fop->sequenceFlags() & FILE_LOAD_SEQUENCE_YES) {
200 m_seqDecision = gen::SequenceDecision::YES;
201 flags &= ~FILE_LOAD_SEQUENCE_ASK;
202 flags |= FILE_LOAD_SEQUENCE_YES;
203 }
204 else if (fop->sequenceFlags() & FILE_LOAD_SEQUENCE_NONE) {
205 m_seqDecision = gen::SequenceDecision::NO;
206 flags &= ~FILE_LOAD_SEQUENCE_ASK;
207 flags |= FILE_LOAD_SEQUENCE_NONE;
208 }
209
210 for (std::string fn : fop->filenames()) {
211 fn = base::normalize_path(fn);
212 m_usedFiles.push_back(fn);
213
214 auto it = std::find(filenames.begin(), filenames.end(), fn);
215 if (it != filenames.end())
216 filenames.erase(it);
217 }
218 }
219 else {
220 auto fn = base::normalize_path(fop->filename());
221 m_usedFiles.push_back(fn);
222 }
223
224 OpenFileJob task(fop.get());
225 task.showProgressWindow();
226
227 // Post-load processing, it is called from the GUI because may require user intervention.
228 fop->postLoad();
229
230 // Show any error
231 if (fop->hasError() && !fop->isStop())
232 console.printf(fop->error().c_str());
233
234 Doc* doc = fop->document();
235 if (doc) {
236 if (context->isUIAvailable()) {
237 App::instance()->recentFiles()->addRecentFile(fop->filename().c_str());
238 auto& docPref = Preferences::instance().document(doc);
239
240 if (fop->hasEmbeddedGridBounds() &&
241 !doc->sprite()->gridBounds().isEmpty()) {
242 // If the sprite contains the grid bounds inside, we put
243 // those grid bounds into the settings (e.g. useful to
244 // interact with old versions of Aseprite saving the grid
245 // bounds in the aseprite.ini file)
246 docPref.grid.bounds(doc->sprite()->gridBounds());
247 }
248 else {
249 // Get grid bounds from preferences
250 doc->sprite()->setGridBounds(docPref.grid.bounds());
251 }
252 }
253
254 doc->setContext(context);
255 }
256 else if (!fop->isStop())
257 unrecent = true;
258 }
259
260 // The file was not found or was loaded loaded with errors,
261 // so we can remove it from the recent-file list
262 if (unrecent) {
263 if (context->isUIAvailable())
264 App::instance()->recentFiles()->removeRecentFile(m_filename);
265 }
266 }
267}
268
269Command* CommandFactory::createOpenFileCommand()
270{
271 return new OpenFileCommand;
272}
273
274} // namespace app
275