| 1 | // Aseprite |
| 2 | // Copyright (C) 2019-2022 Igara Studio S.A. |
| 3 | // |
| 4 | // This program is distributed under the terms of |
| 5 | // the End-User License Agreement for Aseprite. |
| 6 | |
| 7 | #ifdef HAVE_CONFIG_H |
| 8 | #include "config.h" |
| 9 | #endif |
| 10 | |
| 11 | #include "app/color.h" |
| 12 | #include "app/color_utils.h" |
| 13 | #include "app/commands/command.h" |
| 14 | #include "app/commands/filters/filter_manager_impl.h" |
| 15 | #include "app/commands/filters/filter_window.h" |
| 16 | #include "app/commands/filters/filter_worker.h" |
| 17 | #include "app/commands/new_params.h" |
| 18 | #include "app/context.h" |
| 19 | #include "app/ini_file.h" |
| 20 | #include "app/modules/gui.h" |
| 21 | #include "app/pref/preferences.h" |
| 22 | #include "app/ui/color_bar.h" |
| 23 | #include "app/ui/color_button.h" |
| 24 | #include "app/ui/skin/skin_theme.h" |
| 25 | #include "doc/image.h" |
| 26 | #include "doc/mask.h" |
| 27 | #include "doc/sprite.h" |
| 28 | #include "filters/outline_filter.h" |
| 29 | #include "ui/button.h" |
| 30 | #include "ui/label.h" |
| 31 | #include "ui/slider.h" |
| 32 | #include "ui/widget.h" |
| 33 | #include "ui/window.h" |
| 34 | |
| 35 | #include "outline.xml.h" |
| 36 | |
| 37 | namespace app { |
| 38 | |
| 39 | using namespace app::skin; |
| 40 | |
| 41 | enum { CIRCLE, SQUARE, HORZ, VERT }; |
| 42 | |
| 43 | struct OutlineParams : public NewParams { |
| 44 | Param<bool> ui { this, true, "ui" }; |
| 45 | Param<filters::Target> channels { this, 0, "channels" }; |
| 46 | Param<filters::OutlineFilter::Place> place { this, OutlineFilter::Place::Outside, "place" }; |
| 47 | Param<filters::OutlineFilter::Matrix> matrix { this, OutlineFilter::Matrix::Circle, "matrix" }; |
| 48 | Param<app::Color> color { this, app::Color(), "color" }; |
| 49 | Param<app::Color> bgColor { this, app::Color(), "bgColor" }; |
| 50 | Param<filters::TiledMode> tiledMode { this, filters::TiledMode::NONE, "tiledMode" }; |
| 51 | }; |
| 52 | |
| 53 | // Wrapper for ReplaceColorFilter to handle colors in an easy way |
| 54 | class OutlineFilterWrapper : public OutlineFilter { |
| 55 | public: |
| 56 | OutlineFilterWrapper(Layer* layer) : m_layer(layer) { } |
| 57 | |
| 58 | void color(const app::Color& color) { |
| 59 | m_color = color; |
| 60 | if (m_layer) |
| 61 | OutlineFilter::color(color_utils::color_for_layer(color, m_layer)); |
| 62 | } |
| 63 | |
| 64 | void bgColor(const app::Color& color) { |
| 65 | m_bgColor = color; |
| 66 | if (m_layer) |
| 67 | OutlineFilter::bgColor(color_utils::color_for_layer(color, m_layer)); |
| 68 | } |
| 69 | |
| 70 | app::Color color() const { return m_color; } |
| 71 | app::Color bgColor() const { return m_bgColor; } |
| 72 | |
| 73 | private: |
| 74 | Layer* m_layer; |
| 75 | app::Color m_color; |
| 76 | app::Color m_bgColor; |
| 77 | }; |
| 78 | |
| 79 | #ifdef ENABLE_UI |
| 80 | |
| 81 | static const char* ConfigSection = "Outline" ; |
| 82 | |
| 83 | class OutlineWindow : public FilterWindow { |
| 84 | public: |
| 85 | OutlineWindow(OutlineFilterWrapper& filter, |
| 86 | FilterManagerImpl& filterMgr) |
| 87 | : FilterWindow("Outline" , ConfigSection, &filterMgr, |
| 88 | WithChannelsSelector, |
| 89 | WithTiledCheckBox, |
| 90 | filter.tiledMode()) |
| 91 | , m_filter(filter) { |
| 92 | getContainer()->addChild(&m_panel); |
| 93 | |
| 94 | m_panel.color()->setColor(m_filter.color()); |
| 95 | m_panel.bgColor()->setColor(m_filter.bgColor()); |
| 96 | m_panel.place()->setSelectedItem((int)m_filter.place()); |
| 97 | updateButtonsFromMatrix(); |
| 98 | |
| 99 | m_panel.color()->Change.connect(&OutlineWindow::onColorChange, this); |
| 100 | m_panel.bgColor()->Change.connect(&OutlineWindow::onBgColorChange, this); |
| 101 | m_panel.outlineType()->ItemChange.connect( |
| 102 | [this](ButtonSet::Item*){ |
| 103 | onMatrixTypeChange(); |
| 104 | }); |
| 105 | m_panel.outlineMatrix()->ItemChange.connect( |
| 106 | [this](ButtonSet::Item* item){ |
| 107 | onMatrixPixelChange(m_panel.outlineMatrix()->getItemIndex(item)); |
| 108 | }); |
| 109 | m_panel.place()->ItemChange.connect( |
| 110 | [this](ButtonSet::Item*){ |
| 111 | onPlaceChange((OutlineFilter::Place)m_panel.place()->selectedItem()); |
| 112 | }); |
| 113 | } |
| 114 | |
| 115 | private: |
| 116 | void updateButtonsFromMatrix() { |
| 117 | const OutlineFilter::Matrix matrix = m_filter.matrix(); |
| 118 | |
| 119 | int commonMatrix = -1; |
| 120 | switch (matrix) { |
| 121 | case OutlineFilter::Matrix::Circle: commonMatrix = CIRCLE; break; |
| 122 | case OutlineFilter::Matrix::Square: commonMatrix = SQUARE; break; |
| 123 | case OutlineFilter::Matrix::Horizontal: commonMatrix = HORZ; break; |
| 124 | case OutlineFilter::Matrix::Vertical: commonMatrix = VERT; break; |
| 125 | } |
| 126 | m_panel.outlineType()->setSelectedItem(commonMatrix, false); |
| 127 | |
| 128 | auto theme = SkinTheme::get(this); |
| 129 | auto emptyIcon = theme->parts.outlineEmptyPixel(); |
| 130 | auto pixelIcon = theme->parts.outlineFullPixel(); |
| 131 | |
| 132 | for (int i=0; i<9; ++i) { |
| 133 | m_panel.outlineMatrix() |
| 134 | ->getItem(i)->setIcon( |
| 135 | (((int)matrix) & (1 << (8-i))) ? pixelIcon: emptyIcon); |
| 136 | } |
| 137 | } |
| 138 | |
| 139 | void onColorChange(const app::Color& color) { |
| 140 | stopPreview(); |
| 141 | m_filter.color(color); |
| 142 | restartPreview(); |
| 143 | } |
| 144 | |
| 145 | void onBgColorChange(const app::Color& color) { |
| 146 | stopPreview(); |
| 147 | m_filter.bgColor(color); |
| 148 | restartPreview(); |
| 149 | } |
| 150 | |
| 151 | void onPlaceChange(OutlineFilter::Place place) { |
| 152 | stopPreview(); |
| 153 | m_filter.place(place); |
| 154 | restartPreview(); |
| 155 | } |
| 156 | |
| 157 | void onMatrixTypeChange() { |
| 158 | stopPreview(); |
| 159 | |
| 160 | OutlineFilter::Matrix matrix = OutlineFilter::Matrix::None; |
| 161 | switch (m_panel.outlineType()->selectedItem()) { |
| 162 | case CIRCLE: matrix = OutlineFilter::Matrix::Circle; break; |
| 163 | case SQUARE: matrix = OutlineFilter::Matrix::Square; break; |
| 164 | case HORZ: matrix = OutlineFilter::Matrix::Horizontal; break; |
| 165 | case VERT: matrix = OutlineFilter::Matrix::Vertical; break; |
| 166 | } |
| 167 | m_filter.matrix(matrix); |
| 168 | updateButtonsFromMatrix(); |
| 169 | restartPreview(); |
| 170 | } |
| 171 | |
| 172 | void onMatrixPixelChange(const int index) { |
| 173 | stopPreview(); |
| 174 | |
| 175 | int matrix = (int)m_filter.matrix(); |
| 176 | matrix ^= (1 << (8-index)); |
| 177 | m_filter.matrix((OutlineFilter::Matrix)matrix); |
| 178 | updateButtonsFromMatrix(); |
| 179 | restartPreview(); |
| 180 | } |
| 181 | |
| 182 | void setupTiledMode(TiledMode tiledMode) override { |
| 183 | m_filter.tiledMode(tiledMode); |
| 184 | } |
| 185 | |
| 186 | OutlineFilterWrapper& m_filter; |
| 187 | gen::Outline m_panel; |
| 188 | }; |
| 189 | |
| 190 | #endif // ENABLE_UI |
| 191 | |
| 192 | class OutlineCommand : public CommandWithNewParams<OutlineParams> { |
| 193 | public: |
| 194 | OutlineCommand(); |
| 195 | |
| 196 | protected: |
| 197 | bool onEnabled(Context* context) override; |
| 198 | void onExecute(Context* context) override; |
| 199 | }; |
| 200 | |
| 201 | OutlineCommand::OutlineCommand() |
| 202 | : CommandWithNewParams<OutlineParams>(CommandId::Outline(), CmdRecordableFlag) |
| 203 | { |
| 204 | } |
| 205 | |
| 206 | bool OutlineCommand::onEnabled(Context* context) |
| 207 | { |
| 208 | return context->checkFlags(ContextFlags::ActiveDocumentIsWritable | |
| 209 | ContextFlags::HasActiveSprite); |
| 210 | } |
| 211 | |
| 212 | void OutlineCommand::onExecute(Context* context) |
| 213 | { |
| 214 | #ifdef ENABLE_UI |
| 215 | const bool ui = (params().ui() && context->isUIAvailable()); |
| 216 | #endif |
| 217 | |
| 218 | Site site = context->activeSite(); |
| 219 | |
| 220 | OutlineFilterWrapper filter(site.layer()); |
| 221 | if (site.layer() && site.layer()->isBackground() && site.image()) { |
| 222 | // TODO configure default pixel (same as Autocrop/Trim refpixel) |
| 223 | filter.bgColor(app::Color::fromImage(site.image()->pixelFormat(), |
| 224 | site.image()->getPixel(0, 0))); |
| 225 | } |
| 226 | else { |
| 227 | filter.bgColor(app::Color::fromMask()); |
| 228 | } |
| 229 | |
| 230 | #ifdef ENABLE_UI |
| 231 | if (ui) { |
| 232 | filter.place((OutlineFilter::Place)get_config_int(ConfigSection, "Place" , int(OutlineFilter::Place::Outside))); |
| 233 | filter.matrix((OutlineFilter::Matrix)get_config_int(ConfigSection, "Matrix" , int(OutlineFilter::Matrix::Circle))); |
| 234 | filter.color(ColorBar::instance()->getFgColor()); |
| 235 | |
| 236 | DocumentPreferences& docPref = Preferences::instance() |
| 237 | .document(site.document()); |
| 238 | filter.tiledMode(docPref.tiled.mode()); |
| 239 | } |
| 240 | #endif // ENABLE_UI |
| 241 | |
| 242 | if (params().place.isSet()) filter.place(params().place()); |
| 243 | if (params().matrix.isSet()) filter.matrix(params().matrix()); |
| 244 | if (params().color.isSet()) filter.color(params().color()); |
| 245 | if (params().bgColor.isSet()) filter.bgColor(params().bgColor()); |
| 246 | if (params().tiledMode.isSet()) filter.tiledMode(params().tiledMode()); |
| 247 | |
| 248 | FilterManagerImpl filterMgr(context, &filter); |
| 249 | filterMgr.setTarget( |
| 250 | site.sprite()->pixelFormat() == IMAGE_INDEXED ? |
| 251 | TARGET_INDEX_CHANNEL: |
| 252 | TARGET_RED_CHANNEL | |
| 253 | TARGET_GREEN_CHANNEL | |
| 254 | TARGET_BLUE_CHANNEL | |
| 255 | TARGET_GRAY_CHANNEL | |
| 256 | TARGET_ALPHA_CHANNEL); |
| 257 | |
| 258 | if (params().channels.isSet()) filterMgr.setTarget(params().channels()); |
| 259 | |
| 260 | #ifdef ENABLE_UI |
| 261 | if (ui) { |
| 262 | OutlineWindow window(filter, filterMgr); |
| 263 | if (window.doModal()) { |
| 264 | set_config_int(ConfigSection, "Place" , int(filter.place())); |
| 265 | set_config_int(ConfigSection, "Matrix" , int(filter.matrix())); |
| 266 | } |
| 267 | } |
| 268 | else |
| 269 | #endif // ENABLE_UI |
| 270 | { |
| 271 | start_filter_worker(&filterMgr); |
| 272 | } |
| 273 | } |
| 274 | |
| 275 | Command* CommandFactory::createOutlineCommand() |
| 276 | { |
| 277 | return new OutlineCommand; |
| 278 | } |
| 279 | |
| 280 | } // namespace app |
| 281 | |