| 1 | // Aseprite |
| 2 | // Copyright (C) 2020-2022 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/app.h" |
| 13 | #include "app/cmd/set_layer_blend_mode.h" |
| 14 | #include "app/cmd/set_layer_name.h" |
| 15 | #include "app/cmd/set_layer_opacity.h" |
| 16 | #include "app/cmd/set_tileset_base_index.h" |
| 17 | #include "app/cmd/set_tileset_name.h" |
| 18 | #include "app/cmd/set_user_data.h" |
| 19 | #include "app/commands/command.h" |
| 20 | #include "app/console.h" |
| 21 | #include "app/context_access.h" |
| 22 | #include "app/doc.h" |
| 23 | #include "app/doc_event.h" |
| 24 | #include "app/i18n/strings.h" |
| 25 | #include "app/modules/gui.h" |
| 26 | #include "app/tx.h" |
| 27 | #include "app/ui/main_window.h" |
| 28 | #include "app/ui/separator_in_view.h" |
| 29 | #include "app/ui/tileset_selector.h" |
| 30 | #include "app/ui/timeline/timeline.h" |
| 31 | #include "app/ui/user_data_view.h" |
| 32 | #include "app/ui_context.h" |
| 33 | #include "base/scoped_value.h" |
| 34 | #include "doc/image.h" |
| 35 | #include "doc/layer.h" |
| 36 | #include "doc/layer_tilemap.h" |
| 37 | #include "doc/sprite.h" |
| 38 | #include "doc/tileset.h" |
| 39 | #include "doc/user_data.h" |
| 40 | #include "ui/ui.h" |
| 41 | |
| 42 | #include "layer_properties.xml.h" |
| 43 | #include "tileset_selector_window.xml.h" |
| 44 | |
| 45 | namespace app { |
| 46 | |
| 47 | using namespace ui; |
| 48 | |
| 49 | class LayerPropertiesCommand : public Command { |
| 50 | public: |
| 51 | LayerPropertiesCommand(); |
| 52 | |
| 53 | protected: |
| 54 | bool onEnabled(Context* context) override; |
| 55 | void onExecute(Context* context) override; |
| 56 | }; |
| 57 | |
| 58 | class LayerPropertiesWindow; |
| 59 | static LayerPropertiesWindow* g_window = nullptr; |
| 60 | |
| 61 | class LayerPropertiesWindow : public app::gen::LayerProperties, |
| 62 | public ContextObserver, |
| 63 | public DocObserver { |
| 64 | public: |
| 65 | class BlendModeItem : public ListItem { |
| 66 | public: |
| 67 | BlendModeItem(const std::string& name, |
| 68 | const doc::BlendMode mode) |
| 69 | : ListItem(name) |
| 70 | , m_mode(mode) { |
| 71 | } |
| 72 | doc::BlendMode mode() const { return m_mode; } |
| 73 | private: |
| 74 | doc::BlendMode m_mode; |
| 75 | }; |
| 76 | |
| 77 | LayerPropertiesWindow() |
| 78 | : m_timer(250, this) |
| 79 | , m_userDataView(Preferences::instance().layers.userDataVisibility) { |
| 80 | name()->setMinSize(gfx::Size(128, 0)); |
| 81 | name()->setExpansive(true); |
| 82 | |
| 83 | mode()->addItem(new BlendModeItem(Strings::layer_properties_normal(), |
| 84 | doc::BlendMode::NORMAL)); |
| 85 | mode()->addItem(new SeparatorInView); |
| 86 | mode()->addItem(new BlendModeItem(Strings::layer_properties_darken(), |
| 87 | doc::BlendMode::DARKEN)); |
| 88 | mode()->addItem(new BlendModeItem(Strings::layer_properties_multiply(), |
| 89 | doc::BlendMode::MULTIPLY)); |
| 90 | mode()->addItem(new BlendModeItem(Strings::layer_properties_color_burn(), |
| 91 | doc::BlendMode::COLOR_BURN)); |
| 92 | mode()->addItem(new SeparatorInView); |
| 93 | mode()->addItem(new BlendModeItem(Strings::layer_properties_lighten(), |
| 94 | doc::BlendMode::LIGHTEN)); |
| 95 | mode()->addItem(new BlendModeItem(Strings::layer_properties_screen(), |
| 96 | doc::BlendMode::SCREEN)); |
| 97 | mode()->addItem(new BlendModeItem(Strings::layer_properties_color_dodge(), |
| 98 | doc::BlendMode::COLOR_DODGE)); |
| 99 | mode()->addItem(new BlendModeItem(Strings::layer_properties_addition(), |
| 100 | doc::BlendMode::ADDITION)); |
| 101 | mode()->addItem(new SeparatorInView); |
| 102 | mode()->addItem(new BlendModeItem(Strings::layer_properties_overlay(), |
| 103 | doc::BlendMode::OVERLAY)); |
| 104 | mode()->addItem(new BlendModeItem(Strings::layer_properties_soft_light(), |
| 105 | doc::BlendMode::SOFT_LIGHT)); |
| 106 | mode()->addItem(new BlendModeItem(Strings::layer_properties_hard_light(), |
| 107 | doc::BlendMode::HARD_LIGHT)); |
| 108 | mode()->addItem(new SeparatorInView); |
| 109 | mode()->addItem(new BlendModeItem(Strings::layer_properties_difference(), |
| 110 | doc::BlendMode::DIFFERENCE)); |
| 111 | mode()->addItem(new BlendModeItem(Strings::layer_properties_exclusion(), |
| 112 | doc::BlendMode::EXCLUSION)); |
| 113 | mode()->addItem(new BlendModeItem(Strings::layer_properties_subtract(), |
| 114 | doc::BlendMode::SUBTRACT)); |
| 115 | mode()->addItem(new BlendModeItem(Strings::layer_properties_divide(), |
| 116 | doc::BlendMode::DIVIDE)); |
| 117 | mode()->addItem(new SeparatorInView); |
| 118 | mode()->addItem(new BlendModeItem(Strings::layer_properties_hue(), |
| 119 | doc::BlendMode::HSL_HUE)); |
| 120 | mode()->addItem(new BlendModeItem(Strings::layer_properties_saturation(), |
| 121 | doc::BlendMode::HSL_SATURATION)); |
| 122 | mode()->addItem(new BlendModeItem(Strings::layer_properties_color(), |
| 123 | doc::BlendMode::HSL_COLOR)); |
| 124 | mode()->addItem(new BlendModeItem(Strings::layer_properties_luminosity(), |
| 125 | doc::BlendMode::HSL_LUMINOSITY)); |
| 126 | |
| 127 | name()->Change.connect([this]{ onStartTimer(); }); |
| 128 | mode()->Change.connect([this]{ onStartTimer(); }); |
| 129 | opacity()->Change.connect([this]{ onStartTimer(); }); |
| 130 | m_timer.Tick.connect([this]{ onCommitChange(); }); |
| 131 | userData()->Click.connect([this]{ onToggleUserData(); }); |
| 132 | tileset()->Click.connect([this]{ onTileset(); }); |
| 133 | tileset()->setVisible(false); |
| 134 | |
| 135 | m_userDataView.UserDataChange.connect([this]{ onStartTimer(); }); |
| 136 | |
| 137 | remapWindow(); |
| 138 | centerWindow(); |
| 139 | load_window_pos(this, "LayerProperties" ); |
| 140 | |
| 141 | UIContext::instance()->add_observer(this); |
| 142 | } |
| 143 | |
| 144 | ~LayerPropertiesWindow() { |
| 145 | UIContext::instance()->remove_observer(this); |
| 146 | } |
| 147 | |
| 148 | void setLayer(Doc* doc, Layer* layer) { |
| 149 | if (m_layer) { |
| 150 | m_document->remove_observer(this); |
| 151 | m_layer = nullptr; |
| 152 | } |
| 153 | |
| 154 | m_timer.stop(); |
| 155 | m_document = doc; |
| 156 | m_layer = layer; |
| 157 | m_range = App::instance()->timeline()->range(); |
| 158 | |
| 159 | if (m_document) |
| 160 | m_document->add_observer(this); |
| 161 | |
| 162 | if (countLayers() > 0) { |
| 163 | m_userDataView.configureAndSet(m_layer->userData(), |
| 164 | g_window->propertiesGrid()); |
| 165 | } |
| 166 | |
| 167 | updateFromLayer(); |
| 168 | } |
| 169 | |
| 170 | private: |
| 171 | |
| 172 | std::string nameValue() const { |
| 173 | return name()->text(); |
| 174 | } |
| 175 | |
| 176 | doc::BlendMode blendModeValue() const { |
| 177 | BlendModeItem* item = dynamic_cast<BlendModeItem*>(mode()->getSelectedItem()); |
| 178 | if (item) |
| 179 | return item->mode(); |
| 180 | else |
| 181 | return doc::BlendMode::NORMAL; |
| 182 | } |
| 183 | |
| 184 | int opacityValue() const { |
| 185 | return opacity()->getValue(); |
| 186 | } |
| 187 | |
| 188 | int countLayers() const { |
| 189 | if (!m_document) |
| 190 | return 0; |
| 191 | else if (m_layer && !m_range.enabled()) |
| 192 | return 1; |
| 193 | else if (m_range.enabled()) |
| 194 | return m_range.layers(); |
| 195 | else |
| 196 | return 0; |
| 197 | } |
| 198 | |
| 199 | bool onProcessMessage(ui::Message* msg) override { |
| 200 | switch (msg->type()) { |
| 201 | |
| 202 | case kKeyDownMessage: |
| 203 | if (name()->hasFocus() || |
| 204 | opacity()->hasFocus() || |
| 205 | mode()->hasFocus()) { |
| 206 | KeyScancode scancode = static_cast<KeyMessage*>(msg)->scancode(); |
| 207 | if (scancode == kKeyEnter || |
| 208 | scancode == kKeyEsc) { |
| 209 | onCommitChange(); |
| 210 | closeWindow(this); |
| 211 | return true; |
| 212 | } |
| 213 | } |
| 214 | break; |
| 215 | |
| 216 | case kCloseMessage: |
| 217 | // Save changes before we close the window |
| 218 | setLayer(nullptr, nullptr); |
| 219 | save_window_pos(this, "LayerProperties" ); |
| 220 | |
| 221 | deferDelete(); |
| 222 | g_window = nullptr; |
| 223 | break; |
| 224 | |
| 225 | } |
| 226 | return Window::onProcessMessage(msg); |
| 227 | } |
| 228 | |
| 229 | void onStartTimer() { |
| 230 | if (m_selfUpdate) |
| 231 | return; |
| 232 | |
| 233 | m_timer.start(); |
| 234 | m_pendingChanges = true; |
| 235 | } |
| 236 | |
| 237 | void onCommitChange() { |
| 238 | // Nothing to change |
| 239 | if (!m_pendingChanges) |
| 240 | return; |
| 241 | |
| 242 | // Nothing to do here, as there is no layer selected. |
| 243 | if (!m_layer) |
| 244 | return; |
| 245 | |
| 246 | base::ScopedValue<bool> switchSelf(m_selfUpdate, true, false); |
| 247 | |
| 248 | m_timer.stop(); |
| 249 | |
| 250 | const int count = countLayers(); |
| 251 | |
| 252 | std::string newName = nameValue(); |
| 253 | int newOpacity = opacityValue(); |
| 254 | const doc::UserData newUserData = m_userDataView.userData(); |
| 255 | doc::BlendMode newBlendMode = blendModeValue(); |
| 256 | |
| 257 | if ((count > 1) || |
| 258 | (count == 1 && m_layer && (newName != m_layer->name() || |
| 259 | newUserData != m_layer->userData() || |
| 260 | (m_layer->isImage() && |
| 261 | (newOpacity != static_cast<LayerImage*>(m_layer)->opacity() || |
| 262 | newBlendMode != static_cast<LayerImage*>(m_layer)->blendMode()))))) { |
| 263 | try { |
| 264 | ContextWriter writer(UIContext::instance()); |
| 265 | Tx tx(writer.context(), "Set Layer Properties" ); |
| 266 | |
| 267 | DocRange range; |
| 268 | if (m_range.enabled()) |
| 269 | range = m_range; |
| 270 | else { |
| 271 | range.startRange(m_layer, -1, DocRange::kLayers); |
| 272 | range.endRange(m_layer, -1); |
| 273 | } |
| 274 | |
| 275 | const bool nameChanged = (newName != m_layer->name()); |
| 276 | const bool userDataChanged = (newUserData != m_layer->userData()); |
| 277 | const bool opacityChanged = (m_layer->isImage() && newOpacity != static_cast<LayerImage*>(m_layer)->opacity()); |
| 278 | const bool blendModeChanged = (m_layer->isImage() && newBlendMode != static_cast<LayerImage*>(m_layer)->blendMode()); |
| 279 | |
| 280 | for (Layer* layer : range.selectedLayers()) { |
| 281 | if (nameChanged && newName != layer->name()) |
| 282 | tx(new cmd::SetLayerName(layer, newName)); |
| 283 | |
| 284 | if (userDataChanged && newUserData != layer->userData()) |
| 285 | tx(new cmd::SetUserData(layer, newUserData, m_document)); |
| 286 | |
| 287 | if (layer->isImage()) { |
| 288 | if (opacityChanged && newOpacity != static_cast<LayerImage*>(layer)->opacity()) |
| 289 | tx(new cmd::SetLayerOpacity(static_cast<LayerImage*>(layer), newOpacity)); |
| 290 | |
| 291 | if (blendModeChanged && newBlendMode != static_cast<LayerImage*>(layer)->blendMode()) |
| 292 | tx(new cmd::SetLayerBlendMode(static_cast<LayerImage*>(layer), newBlendMode)); |
| 293 | } |
| 294 | } |
| 295 | |
| 296 | // Redraw timeline because the layer's name/user data/color |
| 297 | // might have changed. |
| 298 | App::instance()->timeline()->invalidate(); |
| 299 | |
| 300 | tx.commit(); |
| 301 | } |
| 302 | catch (const std::exception& e) { |
| 303 | Console::showException(e); |
| 304 | } |
| 305 | |
| 306 | update_screen_for_document(m_document); |
| 307 | } |
| 308 | } |
| 309 | |
| 310 | // ContextObserver impl |
| 311 | void onActiveSiteChange(const Site& site) override { |
| 312 | onCommitChange(); |
| 313 | if (isVisible()) |
| 314 | setLayer(const_cast<Doc*>(site.document()), |
| 315 | const_cast<Layer*>(site.layer())); |
| 316 | else if (m_layer) |
| 317 | setLayer(nullptr, nullptr); |
| 318 | } |
| 319 | |
| 320 | // DocObserver impl |
| 321 | void onLayerNameChange(DocEvent& ev) override { |
| 322 | if (m_layer == ev.layer()) |
| 323 | updateFromLayer(); |
| 324 | } |
| 325 | |
| 326 | void onLayerOpacityChange(DocEvent& ev) override { |
| 327 | if (m_layer == ev.layer()) |
| 328 | updateFromLayer(); |
| 329 | } |
| 330 | |
| 331 | void onLayerBlendModeChange(DocEvent& ev) override { |
| 332 | if (m_layer == ev.layer()) |
| 333 | updateFromLayer(); |
| 334 | } |
| 335 | |
| 336 | void onUserDataChange(DocEvent& ev) override { |
| 337 | if (m_layer == ev.withUserData()) |
| 338 | updateFromLayer(); |
| 339 | } |
| 340 | |
| 341 | void onToggleUserData() { |
| 342 | if (m_layer) { |
| 343 | m_userDataView.toggleVisibility(); |
| 344 | g_window->remapWindow(); |
| 345 | manager()->invalidate(); |
| 346 | } |
| 347 | } |
| 348 | |
| 349 | void onTileset() { |
| 350 | if (!m_layer || !m_layer->isTilemap()) |
| 351 | return; |
| 352 | |
| 353 | auto tilemap = static_cast<LayerTilemap*>(m_layer); |
| 354 | auto tileset = tilemap->tileset(); |
| 355 | |
| 356 | // Information about the tileset to be used for new tilemaps |
| 357 | TilesetSelector::Info tilesetInfo; |
| 358 | tilesetInfo.enabled = false; |
| 359 | tilesetInfo.newTileset = false; |
| 360 | tilesetInfo.grid = tileset->grid(); |
| 361 | tilesetInfo.name = tileset->name(); |
| 362 | tilesetInfo.baseIndex = tileset->baseIndex(); |
| 363 | tilesetInfo.tsi = tilemap->tilesetIndex(); |
| 364 | |
| 365 | try { |
| 366 | gen::TilesetSelectorWindow window; |
| 367 | TilesetSelector tilesetSel(tilemap->sprite(), tilesetInfo); |
| 368 | window.tilesetOptions()->addChild(&tilesetSel); |
| 369 | window.openWindowInForeground(); |
| 370 | if (window.closer() != window.ok()) |
| 371 | return; |
| 372 | |
| 373 | tilesetInfo = tilesetSel.getInfo(); |
| 374 | |
| 375 | if (tileset->name() != tilesetInfo.name || |
| 376 | tileset->baseIndex() != tilesetInfo.baseIndex) { |
| 377 | ContextWriter writer(UIContext::instance()); |
| 378 | Tx tx(writer.context(), "Set Tileset Properties" ); |
| 379 | if (tileset->name() != tilesetInfo.name) |
| 380 | tx(new cmd::SetTilesetName(tileset, tilesetInfo.name)); |
| 381 | if (tileset->baseIndex() != tilesetInfo.baseIndex) |
| 382 | tx(new cmd::SetTilesetBaseIndex(tileset, tilesetInfo.baseIndex)); |
| 383 | // TODO catch the tileset base index modification from the editor |
| 384 | App::instance()->mainWindow()->invalidate(); |
| 385 | tx.commit(); |
| 386 | } |
| 387 | } |
| 388 | catch (const std::exception& e) { |
| 389 | Console::showException(e); |
| 390 | } |
| 391 | } |
| 392 | |
| 393 | void updateFromLayer() { |
| 394 | if (m_selfUpdate) |
| 395 | return; |
| 396 | |
| 397 | m_timer.stop(); // Cancel current editions (just in case) |
| 398 | |
| 399 | base::ScopedValue<bool> switchSelf(m_selfUpdate, true, false); |
| 400 | |
| 401 | const bool tilemapVisibility = (m_layer && m_layer->isTilemap()); |
| 402 | if (m_layer) { |
| 403 | name()->setText(m_layer->name().c_str()); |
| 404 | name()->setEnabled(true); |
| 405 | |
| 406 | if (m_layer->isImage()) { |
| 407 | mode()->setSelectedItem(nullptr); |
| 408 | for (auto item : *mode()) { |
| 409 | if (auto blendModeItem = dynamic_cast<BlendModeItem*>(item)) { |
| 410 | if (blendModeItem->mode() == static_cast<LayerImage*>(m_layer)->blendMode()) { |
| 411 | mode()->setSelectedItem(item); |
| 412 | break; |
| 413 | } |
| 414 | } |
| 415 | } |
| 416 | mode()->setEnabled(!m_layer->isBackground()); |
| 417 | opacity()->setValue(static_cast<LayerImage*>(m_layer)->opacity()); |
| 418 | opacity()->setEnabled(!m_layer->isBackground()); |
| 419 | } |
| 420 | else { |
| 421 | mode()->setEnabled(false); |
| 422 | opacity()->setEnabled(false); |
| 423 | } |
| 424 | |
| 425 | color_t c = m_layer->userData().color(); |
| 426 | m_userDataView.color()->setColor(Color::fromRgb(rgba_getr(c), rgba_getg(c), rgba_getb(c), rgba_geta(c))); |
| 427 | m_userDataView.entry()->setText(m_layer->userData().text()); |
| 428 | } |
| 429 | else { |
| 430 | name()->setText(Strings::layer_properties_no_layer()); |
| 431 | name()->setEnabled(false); |
| 432 | mode()->setEnabled(false); |
| 433 | opacity()->setEnabled(false); |
| 434 | m_userDataView.setVisible(false, false); |
| 435 | } |
| 436 | |
| 437 | if (tileset()->isVisible() != tilemapVisibility) { |
| 438 | tileset()->setVisible(tilemapVisibility); |
| 439 | tileset()->parent()->layout(); |
| 440 | } |
| 441 | } |
| 442 | |
| 443 | Timer m_timer; |
| 444 | bool m_pendingChanges = false; |
| 445 | Doc* m_document = nullptr; |
| 446 | Layer* m_layer = nullptr; |
| 447 | DocRange m_range; |
| 448 | bool m_selfUpdate = false; |
| 449 | UserDataView m_userDataView; |
| 450 | }; |
| 451 | |
| 452 | LayerPropertiesCommand::LayerPropertiesCommand() |
| 453 | : Command(CommandId::LayerProperties(), CmdRecordableFlag) |
| 454 | { |
| 455 | } |
| 456 | |
| 457 | bool LayerPropertiesCommand::onEnabled(Context* context) |
| 458 | { |
| 459 | return context->checkFlags(ContextFlags::ActiveDocumentIsWritable | |
| 460 | ContextFlags::HasActiveLayer); |
| 461 | } |
| 462 | |
| 463 | void LayerPropertiesCommand::onExecute(Context* context) |
| 464 | { |
| 465 | ContextReader reader(context); |
| 466 | Doc* doc = static_cast<Doc*>(reader.document()); |
| 467 | LayerImage* layer = static_cast<LayerImage*>(reader.layer()); |
| 468 | |
| 469 | if (!g_window) |
| 470 | g_window = new LayerPropertiesWindow; |
| 471 | |
| 472 | g_window->setLayer(doc, layer); |
| 473 | g_window->openWindow(); |
| 474 | |
| 475 | // Focus layer name |
| 476 | g_window->name()->requestFocus(); |
| 477 | } |
| 478 | |
| 479 | Command* CommandFactory::createLayerPropertiesCommand() |
| 480 | { |
| 481 | return new LayerPropertiesCommand; |
| 482 | } |
| 483 | |
| 484 | } // namespace app |
| 485 | |