1 | // Aseprite |
2 | // Copyright (C) 2019-2022 Igara Studio S.A. |
3 | // Copyright (C) 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/ui/export_file_window.h" |
13 | |
14 | #include "app/doc.h" |
15 | #include "app/file/file.h" |
16 | #include "app/i18n/strings.h" |
17 | #include "app/site.h" |
18 | #include "app/ui/layer_frame_comboboxes.h" |
19 | #include "app/ui_context.h" |
20 | #include "base/convert_to.h" |
21 | #include "base/fs.h" |
22 | #include "base/string.h" |
23 | #include "doc/selected_frames.h" |
24 | #include "doc/tag.h" |
25 | #include "fmt/format.h" |
26 | #include "ui/alert.h" |
27 | |
28 | #include <algorithm> |
29 | |
30 | namespace app { |
31 | |
32 | ExportFileWindow::ExportFileWindow(const Doc* doc) |
33 | : m_doc(doc) |
34 | , m_docPref(Preferences::instance().document(doc)) |
35 | , m_preferredResize(1) |
36 | { |
37 | // Is a default output filename in the preferences? |
38 | if (!m_docPref.saveCopy.filename().empty()) { |
39 | setOutputFilename(m_docPref.saveCopy.filename()); |
40 | } |
41 | else { |
42 | std::string newFn = base::replace_extension( |
43 | doc->filename(), |
44 | defaultExtension()); |
45 | if (newFn == doc->filename()) { |
46 | newFn = base::join_path( |
47 | base::get_file_path(newFn), |
48 | base::get_file_title(newFn) + "-export." + base::get_file_extension(newFn)); |
49 | } |
50 | setOutputFilename(newFn); |
51 | } |
52 | |
53 | // Default export configuration |
54 | setResizeScale(m_docPref.saveCopy.resizeScale()); |
55 | fill_area_combobox(m_doc->sprite(), area(), m_docPref.saveCopy.area()); |
56 | fill_layers_combobox(m_doc->sprite(), layers(), m_docPref.saveCopy.layer(), m_docPref.saveCopy.layerIndex()); |
57 | fill_frames_combobox(m_doc->sprite(), frames(), m_docPref.saveCopy.frameTag()); |
58 | fill_anidir_combobox(anidir(), m_docPref.saveCopy.aniDir()); |
59 | pixelRatio()->setSelected(m_docPref.saveCopy.applyPixelRatio()); |
60 | forTwitter()->setSelected(m_docPref.saveCopy.forTwitter()); |
61 | adjustResize()->setVisible(false); |
62 | |
63 | // Here we don't call updateAniDir() because it's already filled and |
64 | // set by the function fill_anidir_combobox(). So if the user |
65 | // exported a tag with a specific AniDir, we want to keep the option |
66 | // in the preference (instead of the tag's AniDir). |
67 | //updateAniDir(); |
68 | |
69 | updateAdjustResizeButton(); |
70 | |
71 | outputFilename()->Change.connect( |
72 | [this]{ |
73 | m_outputFilename = outputFilename()->text(); |
74 | onOutputFilenameEntryChange(); |
75 | }); |
76 | outputFilenameBrowse()->Click.connect( |
77 | [this]{ |
78 | std::string fn = SelectOutputFile(); |
79 | if (!fn.empty()) { |
80 | setOutputFilename(fn); |
81 | } |
82 | }); |
83 | |
84 | resize()->Change.connect([this]{ updateAdjustResizeButton(); }); |
85 | frames()->Change.connect([this]{ updateAniDir(); }); |
86 | forTwitter()->Click.connect([this]{ updateAdjustResizeButton(); }); |
87 | adjustResize()->Click.connect([this]{ onAdjustResize(); }); |
88 | ok()->Click.connect([this]{ onOK(); }); |
89 | } |
90 | |
91 | bool ExportFileWindow::show() |
92 | { |
93 | openWindowInForeground(); |
94 | return (closer() == ok()); |
95 | } |
96 | |
97 | void ExportFileWindow::savePref() |
98 | { |
99 | m_docPref.saveCopy.filename(outputFilenameValue()); |
100 | m_docPref.saveCopy.resizeScale(resizeValue()); |
101 | m_docPref.saveCopy.area(areaValue()); |
102 | m_docPref.saveCopy.layer(layersValue()); |
103 | m_docPref.saveCopy.layerIndex(layersIndex()); |
104 | m_docPref.saveCopy.aniDir(aniDirValue()); |
105 | m_docPref.saveCopy.frameTag(framesValue()); |
106 | m_docPref.saveCopy.applyPixelRatio(applyPixelRatio()); |
107 | m_docPref.saveCopy.forTwitter(isForTwitter()); |
108 | } |
109 | |
110 | std::string ExportFileWindow::outputFilenameValue() const |
111 | { |
112 | return base::join_path(m_outputPath, |
113 | m_outputFilename); |
114 | } |
115 | |
116 | double ExportFileWindow::resizeValue() const |
117 | { |
118 | double value = resize()->getEntryWidget()->textDouble() / 100.0; |
119 | return std::clamp(value, 0.001, 100000000.0); |
120 | } |
121 | |
122 | std::string ExportFileWindow::areaValue() const |
123 | { |
124 | return area()->getValue(); |
125 | } |
126 | |
127 | std::string ExportFileWindow::layersValue() const |
128 | { |
129 | return layers()->getValue(); |
130 | } |
131 | |
132 | int ExportFileWindow::layersIndex() const |
133 | { |
134 | int i = layers()->getSelectedItemIndex() - kLayersComboboxExtraInitialItems; |
135 | return i < 0 ? -1 : i; |
136 | } |
137 | |
138 | std::string ExportFileWindow::framesValue() const |
139 | { |
140 | return frames()->getValue(); |
141 | } |
142 | |
143 | doc::AniDir ExportFileWindow::aniDirValue() const |
144 | { |
145 | return (doc::AniDir)anidir()->getSelectedItemIndex(); |
146 | } |
147 | |
148 | bool ExportFileWindow::applyPixelRatio() const |
149 | { |
150 | return pixelRatio()->isSelected(); |
151 | } |
152 | |
153 | bool ExportFileWindow::() const |
154 | { |
155 | return forTwitter()->isSelected(); |
156 | } |
157 | |
158 | void ExportFileWindow::setResizeScale(double scale) |
159 | { |
160 | resize()->setValue(fmt::format("{:.2f}" , 100.0 * scale)); |
161 | } |
162 | |
163 | void ExportFileWindow::setArea(const std::string& areaValue) |
164 | { |
165 | area()->setValue(areaValue); |
166 | } |
167 | |
168 | void ExportFileWindow::setAniDir(const doc::AniDir aniDir) |
169 | { |
170 | anidir()->setSelectedItemIndex(int(aniDir)); |
171 | } |
172 | |
173 | void ExportFileWindow::setOutputFilename(const std::string& pathAndFilename) |
174 | { |
175 | m_outputPath = base::get_file_path(pathAndFilename); |
176 | m_outputFilename = base::get_file_name(pathAndFilename); |
177 | |
178 | updateOutputFilenameEntry(); |
179 | } |
180 | |
181 | void ExportFileWindow::updateOutputFilenameEntry() |
182 | { |
183 | outputFilename()->setText(m_outputFilename); |
184 | onOutputFilenameEntryChange(); |
185 | } |
186 | |
187 | void ExportFileWindow::onOutputFilenameEntryChange() |
188 | { |
189 | ok()->setEnabled(!m_outputFilename.empty()); |
190 | } |
191 | |
192 | void ExportFileWindow::updateAniDir() |
193 | { |
194 | std::string framesValue = this->framesValue(); |
195 | if (!framesValue.empty() && |
196 | framesValue != kAllFrames && |
197 | framesValue != kSelectedFrames) { |
198 | SelectedFrames selFrames; |
199 | Tag* tag = calculate_selected_frames( |
200 | UIContext::instance()->activeSite(), framesValue, selFrames); |
201 | if (tag) |
202 | anidir()->setSelectedItemIndex(int(tag->aniDir())); |
203 | } |
204 | else |
205 | anidir()->setSelectedItemIndex(int(doc::AniDir::FORWARD)); |
206 | } |
207 | |
208 | void ExportFileWindow::updateAdjustResizeButton() |
209 | { |
210 | // Calculate a better size for Twitter |
211 | m_preferredResize = 1; |
212 | while (m_preferredResize < 10 && |
213 | (m_doc->width()*m_preferredResize < 240 || |
214 | m_doc->height()*m_preferredResize < 240)) { |
215 | ++m_preferredResize; |
216 | } |
217 | |
218 | const bool newState = |
219 | forTwitter()->isSelected() && |
220 | ((int)resizeValue() < m_preferredResize); |
221 | |
222 | if (adjustResize()->isVisible() != newState) { |
223 | adjustResize()->setVisible(newState); |
224 | if (newState) |
225 | adjustResize()->setText(fmt::format(Strings::export_file_adjust_resize(), |
226 | 100 * m_preferredResize)); |
227 | adjustResize()->parent()->layout(); |
228 | } |
229 | } |
230 | |
231 | void ExportFileWindow::onAdjustResize() |
232 | { |
233 | resize()->setValue(fmt::format("{:.2f}" , 100.0 * m_preferredResize)); |
234 | |
235 | adjustResize()->setVisible(false); |
236 | adjustResize()->parent()->layout(); |
237 | } |
238 | |
239 | void ExportFileWindow::onOK() |
240 | { |
241 | base::paths exts = get_writable_extensions(); |
242 | std::string ext = base::string_to_lower( |
243 | base::get_file_extension(m_outputFilename)); |
244 | |
245 | // Add default extension to output filename |
246 | if (std::find(exts.begin(), exts.end(), ext) == exts.end()) { |
247 | if (ext.empty()) { |
248 | m_outputFilename = |
249 | base::replace_extension(m_outputFilename, |
250 | defaultExtension()); |
251 | } |
252 | else { |
253 | ui::Alert::show( |
254 | fmt::format(Strings::alerts_unknown_output_file_format_error(), ext)); |
255 | return; |
256 | } |
257 | } |
258 | |
259 | closeWindow(ok()); |
260 | } |
261 | |
262 | std::string ExportFileWindow::defaultExtension() const |
263 | { |
264 | auto& pref = Preferences::instance(); |
265 | if (m_doc->sprite()->totalFrames() > 1) |
266 | return pref.exportFile.animationDefaultExtension(); |
267 | else |
268 | return pref.exportFile.imageDefaultExtension(); |
269 | } |
270 | |
271 | } // namespace app |
272 | |