| 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 | |