| 1 | // Aseprite |
| 2 | // Copyright (C) 2018-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/ui/brush_popup.h" |
| 13 | |
| 14 | #include "app/app.h" |
| 15 | #include "app/brush_slot.h" |
| 16 | #include "app/commands/command.h" |
| 17 | #include "app/commands/commands.h" |
| 18 | #include "app/i18n/strings.h" |
| 19 | #include "app/modules/gui.h" |
| 20 | #include "app/modules/palettes.h" |
| 21 | #include "app/pref/preferences.h" |
| 22 | #include "app/tools/tool.h" |
| 23 | #include "app/ui/app_menuitem.h" |
| 24 | #include "app/ui/button_set.h" |
| 25 | #include "app/ui/context_bar.h" |
| 26 | #include "app/ui/keyboard_shortcuts.h" |
| 27 | #include "app/ui/main_window.h" |
| 28 | #include "app/ui/skin/skin_theme.h" |
| 29 | #include "app/ui_context.h" |
| 30 | #include "app/util/conversion_to_surface.h" |
| 31 | #include "base/convert_to.h" |
| 32 | #include "doc/brush.h" |
| 33 | #include "doc/image.h" |
| 34 | #include "doc/palette.h" |
| 35 | #include "gfx/border.h" |
| 36 | #include "gfx/region.h" |
| 37 | #include "os/surface.h" |
| 38 | #include "os/system.h" |
| 39 | #include "ui/button.h" |
| 40 | #include "ui/fit_bounds.h" |
| 41 | #include "ui/link_label.h" |
| 42 | #include "ui/listitem.h" |
| 43 | #include "ui/menu.h" |
| 44 | #include "ui/message.h" |
| 45 | #include "ui/separator.h" |
| 46 | |
| 47 | #include "brush_slot_params.xml.h" |
| 48 | |
| 49 | namespace app { |
| 50 | |
| 51 | using namespace app::skin; |
| 52 | using namespace doc; |
| 53 | using namespace ui; |
| 54 | |
| 55 | namespace { |
| 56 | |
| 57 | void (PopupWindow* , |
| 58 | Menu* , |
| 59 | const gfx::Point& pt, |
| 60 | Display* display) |
| 61 | { |
| 62 | // Add the menu window region when it popups to the the BrushPopup |
| 63 | // hot region, so when we click inside the popup menu it doesn't |
| 64 | // close the BrushPopup. |
| 65 | obs::scoped_connection c = popupMenu->OpenPopup.connect([popupWindow, popupMenu]{ |
| 66 | gfx::Region rgn(popupWindow->boundsOnScreen()); |
| 67 | rgn |= gfx::Region(popupMenu->boundsOnScreen()); |
| 68 | popupWindow->setHotRegion(rgn); |
| 69 | }); |
| 70 | |
| 71 | popupMenu->showPopup(pt, display); |
| 72 | |
| 73 | // Restore hot region of the BrushPopup window |
| 74 | popupWindow->setHotRegion(gfx::Region(popupWindow->boundsOnScreen())); |
| 75 | } |
| 76 | |
| 77 | class SelectBrushItem : public ButtonSet::Item { |
| 78 | public: |
| 79 | SelectBrushItem(const BrushSlot& brush, int slot = -1) |
| 80 | : m_brushes(App::instance()->brushes()) |
| 81 | , m_brush(brush) |
| 82 | , m_slot(slot) { |
| 83 | if (m_brush.hasBrush()) { |
| 84 | SkinPartPtr icon(new SkinPart); |
| 85 | icon->setBitmap(0, BrushPopup::createSurfaceForBrush( |
| 86 | m_brush.brush(), |
| 87 | m_brush.hasFlag(BrushSlot::Flags::ImageColor))); |
| 88 | setIcon(icon); |
| 89 | } |
| 90 | } |
| 91 | |
| 92 | const BrushSlot& brush() const { |
| 93 | return m_brush; |
| 94 | } |
| 95 | |
| 96 | private: |
| 97 | void onClick() override { |
| 98 | ContextBar* contextBar = App::instance()->contextBar(); |
| 99 | tools::Tool* tool = App::instance()->activeTool(); |
| 100 | |
| 101 | if (m_slot >= 0) |
| 102 | contextBar->setActiveBrushBySlot(tool, m_slot); |
| 103 | else if (m_brush.hasBrush()) { |
| 104 | auto& brushPref = Preferences::instance().tool(tool).brush; |
| 105 | BrushRef brush; |
| 106 | |
| 107 | brush.reset( |
| 108 | new Brush( |
| 109 | static_cast<doc::BrushType>(m_brush.brush()->type()), |
| 110 | brushPref.size(), |
| 111 | brushPref.angle())); |
| 112 | |
| 113 | contextBar->setActiveBrush(brush); |
| 114 | } |
| 115 | } |
| 116 | |
| 117 | AppBrushes& m_brushes; |
| 118 | BrushSlot m_brush; |
| 119 | int m_slot; |
| 120 | }; |
| 121 | |
| 122 | class BrushShortcutItem : public ButtonSet::Item { |
| 123 | public: |
| 124 | BrushShortcutItem(const std::string& text, int slot) |
| 125 | : m_slot(slot) { |
| 126 | setText(text); |
| 127 | } |
| 128 | |
| 129 | private: |
| 130 | void onClick() override { |
| 131 | Params params; |
| 132 | params.set("change" , "custom" ); |
| 133 | params.set("slot" , base::convert_to<std::string>(m_slot).c_str()); |
| 134 | Command* cmd = Commands::instance()->byId(CommandId::ChangeBrush()); |
| 135 | cmd->loadParams(params); |
| 136 | std::string search = cmd->friendlyName(); |
| 137 | if (!search.empty()) { |
| 138 | params.clear(); |
| 139 | params.set("search" , search.c_str()); |
| 140 | cmd = Commands::instance()->byId(CommandId::KeyboardShortcuts()); |
| 141 | ASSERT(cmd); |
| 142 | if (cmd) |
| 143 | UIContext::instance()->executeCommand(cmd, params); |
| 144 | } |
| 145 | } |
| 146 | |
| 147 | int m_slot; |
| 148 | }; |
| 149 | |
| 150 | class BrushOptionsItem : public ButtonSet::Item { |
| 151 | public: |
| 152 | (BrushPopup* , int slot) |
| 153 | : m_popup(popup) |
| 154 | , m_brushes(App::instance()->brushes()) |
| 155 | , m_slot(slot) { |
| 156 | auto theme = skin::SkinTheme::get(this); |
| 157 | setIcon(theme->parts.iconArrowDown(), true); |
| 158 | } |
| 159 | |
| 160 | private: |
| 161 | |
| 162 | void onClick() override { |
| 163 | Menu ; |
| 164 | AppMenuItem save(Strings::brush_slot_params_save_brush()); |
| 165 | AppMenuItem lockItem(Strings::brush_slot_params_locked()); |
| 166 | AppMenuItem deleteItem(Strings::brush_slot_params_delete()); |
| 167 | AppMenuItem deleteAllItem(Strings::brush_slot_params_delete_all()); |
| 168 | |
| 169 | lockItem.setSelected(m_brushes.isBrushSlotLocked(m_slot)); |
| 170 | |
| 171 | save.Click.connect(&BrushOptionsItem::onSaveBrush, this); |
| 172 | lockItem.Click.connect(&BrushOptionsItem::onLockBrush, this); |
| 173 | deleteItem.Click.connect(&BrushOptionsItem::onDeleteBrush, this); |
| 174 | deleteAllItem.Click.connect(&BrushOptionsItem::onDeleteAllBrushes, this); |
| 175 | |
| 176 | menu.addChild(&save); |
| 177 | menu.addChild(new MenuSeparator); |
| 178 | menu.addChild(&lockItem); |
| 179 | menu.addChild(&deleteItem); |
| 180 | menu.addChild(new MenuSeparator); |
| 181 | menu.addChild(&deleteAllItem); |
| 182 | menu.addChild(new Label("" )); |
| 183 | menu.addChild( |
| 184 | new Separator(Strings::brush_slot_params_saved_parameters(), HORIZONTAL)); |
| 185 | |
| 186 | app::gen::BrushSlotParams params; |
| 187 | menu.addChild(¶ms); |
| 188 | |
| 189 | // Load preferences |
| 190 | BrushSlot brush = m_brushes.getBrushSlot(m_slot); |
| 191 | params.brushType()->setSelected(brush.hasFlag(BrushSlot::Flags::BrushType)); |
| 192 | params.brushSize()->setSelected(brush.hasFlag(BrushSlot::Flags::BrushSize)); |
| 193 | params.brushAngle()->setSelected(brush.hasFlag(BrushSlot::Flags::BrushAngle)); |
| 194 | params.fgColor()->setSelected(brush.hasFlag(BrushSlot::Flags::FgColor)); |
| 195 | params.bgColor()->setSelected(brush.hasFlag(BrushSlot::Flags::BgColor)); |
| 196 | params.imageColor()->setSelected(brush.hasFlag(BrushSlot::Flags::ImageColor)); |
| 197 | params.inkType()->setSelected(brush.hasFlag(BrushSlot::Flags::InkType)); |
| 198 | params.inkOpacity()->setSelected(brush.hasFlag(BrushSlot::Flags::InkOpacity)); |
| 199 | params.shade()->setSelected(brush.hasFlag(BrushSlot::Flags::Shade)); |
| 200 | params.pixelPerfect()->setSelected(brush.hasFlag(BrushSlot::Flags::PixelPerfect)); |
| 201 | |
| 202 | m_changeFlags = true; |
| 203 | show_popup_menu(m_popup, &menu, |
| 204 | gfx::Point(origin().x, origin().y+bounds().h), |
| 205 | display()); |
| 206 | |
| 207 | if (m_changeFlags) { |
| 208 | brush = m_brushes.getBrushSlot(m_slot); |
| 209 | |
| 210 | int flags = (int(brush.flags()) & int(BrushSlot::Flags::Locked)); |
| 211 | if (params.brushType()->isSelected()) flags |= int(BrushSlot::Flags::BrushType); |
| 212 | if (params.brushSize()->isSelected()) flags |= int(BrushSlot::Flags::BrushSize); |
| 213 | if (params.brushAngle()->isSelected()) flags |= int(BrushSlot::Flags::BrushAngle); |
| 214 | if (params.fgColor()->isSelected()) flags |= int(BrushSlot::Flags::FgColor); |
| 215 | if (params.bgColor()->isSelected()) flags |= int(BrushSlot::Flags::BgColor); |
| 216 | if (params.imageColor()->isSelected()) flags |= int(BrushSlot::Flags::ImageColor); |
| 217 | if (params.inkType()->isSelected()) flags |= int(BrushSlot::Flags::InkType); |
| 218 | if (params.inkOpacity()->isSelected()) flags |= int(BrushSlot::Flags::InkOpacity); |
| 219 | if (params.shade()->isSelected()) flags |= int(BrushSlot::Flags::Shade); |
| 220 | if (params.pixelPerfect()->isSelected()) flags |= int(BrushSlot::Flags::PixelPerfect); |
| 221 | |
| 222 | if (brush.flags() != BrushSlot::Flags(flags)) { |
| 223 | brush.setFlags(BrushSlot::Flags(flags)); |
| 224 | m_brushes.setBrushSlot(m_slot, brush); |
| 225 | } |
| 226 | } |
| 227 | } |
| 228 | |
| 229 | private: |
| 230 | |
| 231 | void onSaveBrush() { |
| 232 | ContextBar* contextBar = App::instance()->contextBar(); |
| 233 | |
| 234 | m_brushes.setBrushSlot( |
| 235 | m_slot, contextBar->createBrushSlotFromPreferences()); |
| 236 | m_brushes.lockBrushSlot(m_slot); |
| 237 | |
| 238 | m_changeFlags = false; |
| 239 | } |
| 240 | |
| 241 | void onLockBrush() { |
| 242 | if (m_brushes.isBrushSlotLocked(m_slot)) |
| 243 | m_brushes.unlockBrushSlot(m_slot); |
| 244 | else |
| 245 | m_brushes.lockBrushSlot(m_slot); |
| 246 | } |
| 247 | |
| 248 | void onDeleteBrush() { |
| 249 | m_brushes.removeBrushSlot(m_slot); |
| 250 | m_changeFlags = false; |
| 251 | } |
| 252 | |
| 253 | void onDeleteAllBrushes() { |
| 254 | m_brushes.removeAllBrushSlots(); |
| 255 | m_changeFlags = false; |
| 256 | } |
| 257 | |
| 258 | BrushPopup* ; |
| 259 | AppBrushes& m_brushes; |
| 260 | BrushRef m_brush; |
| 261 | int m_slot; |
| 262 | bool m_changeFlags; |
| 263 | }; |
| 264 | |
| 265 | class NewCustomBrushItem : public ButtonSet::Item { |
| 266 | public: |
| 267 | NewCustomBrushItem() { |
| 268 | setText(Strings::brush_slot_params_save_brush()); |
| 269 | } |
| 270 | |
| 271 | private: |
| 272 | void onClick() override { |
| 273 | ContextBar* contextBar = App::instance()->contextBar(); |
| 274 | |
| 275 | auto& brushes = App::instance()->brushes(); |
| 276 | int slot = brushes.addBrushSlot( |
| 277 | contextBar->createBrushSlotFromPreferences()); |
| 278 | brushes.lockBrushSlot(slot); |
| 279 | } |
| 280 | }; |
| 281 | |
| 282 | class NewBrushOptionsItem : public ButtonSet::Item { |
| 283 | public: |
| 284 | NewBrushOptionsItem() { |
| 285 | auto theme = skin::SkinTheme::get(this); |
| 286 | setIcon(theme->parts.iconArrowDown(), true); |
| 287 | } |
| 288 | |
| 289 | private: |
| 290 | void onClick() override { |
| 291 | Menu ; |
| 292 | |
| 293 | menu.addChild(new Separator("Parameters to Save" , HORIZONTAL)); |
| 294 | |
| 295 | app::gen::BrushSlotParams params; |
| 296 | menu.addChild(¶ms); |
| 297 | |
| 298 | // Load preferences |
| 299 | auto& saveBrush = Preferences::instance().saveBrush; |
| 300 | params.brushType()->setSelected(saveBrush.brushType()); |
| 301 | params.brushSize()->setSelected(saveBrush.brushSize()); |
| 302 | params.brushAngle()->setSelected(saveBrush.brushAngle()); |
| 303 | params.fgColor()->setSelected(saveBrush.fgColor()); |
| 304 | params.bgColor()->setSelected(saveBrush.bgColor()); |
| 305 | params.imageColor()->setSelected(saveBrush.imageColor()); |
| 306 | params.inkType()->setSelected(saveBrush.inkType()); |
| 307 | params.inkOpacity()->setSelected(saveBrush.inkOpacity()); |
| 308 | params.shade()->setSelected(saveBrush.shade()); |
| 309 | params.pixelPerfect()->setSelected(saveBrush.pixelPerfect()); |
| 310 | |
| 311 | show_popup_menu(static_cast<PopupWindow*>(window()), |
| 312 | &menu, |
| 313 | gfx::Point(origin().x, origin().y+bounds().h), |
| 314 | display()); |
| 315 | |
| 316 | // Save preferences |
| 317 | if (saveBrush.brushType() != params.brushType()->isSelected()) |
| 318 | saveBrush.brushType(params.brushType()->isSelected()); |
| 319 | if (saveBrush.brushSize() != params.brushSize()->isSelected()) |
| 320 | saveBrush.brushSize(params.brushSize()->isSelected()); |
| 321 | if (saveBrush.brushAngle() != params.brushAngle()->isSelected()) |
| 322 | saveBrush.brushAngle(params.brushAngle()->isSelected()); |
| 323 | if (saveBrush.fgColor() != params.fgColor()->isSelected()) |
| 324 | saveBrush.fgColor(params.fgColor()->isSelected()); |
| 325 | if (saveBrush.bgColor() != params.bgColor()->isSelected()) |
| 326 | saveBrush.bgColor(params.bgColor()->isSelected()); |
| 327 | if (saveBrush.imageColor() != params.imageColor()->isSelected()) |
| 328 | saveBrush.imageColor(params.imageColor()->isSelected()); |
| 329 | if (saveBrush.inkType() != params.inkType()->isSelected()) |
| 330 | saveBrush.inkType(params.inkType()->isSelected()); |
| 331 | if (saveBrush.inkOpacity() != params.inkOpacity()->isSelected()) |
| 332 | saveBrush.inkOpacity(params.inkOpacity()->isSelected()); |
| 333 | if (saveBrush.shade() != params.shade()->isSelected()) |
| 334 | saveBrush.shade(params.shade()->isSelected()); |
| 335 | if (saveBrush.pixelPerfect() != params.pixelPerfect()->isSelected()) |
| 336 | saveBrush.pixelPerfect(params.pixelPerfect()->isSelected()); |
| 337 | } |
| 338 | }; |
| 339 | |
| 340 | } // anonymous namespace |
| 341 | |
| 342 | BrushPopup::() |
| 343 | : PopupWindow("" , ClickBehavior::CloseOnClickOutsideHotRegion) |
| 344 | , m_standardBrushes(3) |
| 345 | , m_customBrushes(nullptr) |
| 346 | { |
| 347 | auto& brushes = App::instance()->brushes(); |
| 348 | |
| 349 | setAutoRemap(false); |
| 350 | |
| 351 | m_standardBrushes.setTriggerOnMouseUp(true); |
| 352 | |
| 353 | addChild(&m_box); |
| 354 | |
| 355 | HBox* top = new HBox; |
| 356 | top->addChild(&m_standardBrushes); |
| 357 | top->addChild(new BoxFiller); |
| 358 | |
| 359 | m_box.addChild(top); |
| 360 | m_box.addChild(new Separator("" , HORIZONTAL)); |
| 361 | |
| 362 | for (const auto& brush : brushes.getStandardBrushes()) { |
| 363 | m_standardBrushes.addItem( |
| 364 | new SelectBrushItem( |
| 365 | BrushSlot(BrushSlot::Flags::BrushType, brush))) |
| 366 | ->setMono(true); |
| 367 | } |
| 368 | m_standardBrushes.setTransparent(true); |
| 369 | |
| 370 | brushes.ItemsChange.connect(&BrushPopup::onBrushChanges, this); |
| 371 | |
| 372 | InitTheme.connect( |
| 373 | [this]{ |
| 374 | setBorder(gfx::Border(2)*guiscale()); |
| 375 | setChildSpacing(0); |
| 376 | m_box.noBorderNoChildSpacing(); |
| 377 | m_standardBrushes.setBgColor(gfx::ColorNone); |
| 378 | }); |
| 379 | initTheme(); |
| 380 | } |
| 381 | |
| 382 | void BrushPopup::(Brush* brush) |
| 383 | { |
| 384 | for (auto child : m_standardBrushes.children()) { |
| 385 | SelectBrushItem* item = static_cast<SelectBrushItem*>(child); |
| 386 | |
| 387 | // Same type and same image |
| 388 | if (item->brush().hasBrush() && |
| 389 | item->brush().brush()->type() == brush->type() && |
| 390 | (brush->type() != kImageBrushType || |
| 391 | item->brush().brush()->image() == brush->image())) { |
| 392 | m_standardBrushes.setSelectedItem(item); |
| 393 | return; |
| 394 | } |
| 395 | } |
| 396 | } |
| 397 | |
| 398 | void BrushPopup::(ui::Display* display, |
| 399 | const gfx::Point& pos) |
| 400 | { |
| 401 | auto& brushSlots = App::instance()->brushes().getBrushSlots(); |
| 402 | |
| 403 | if (m_customBrushes) { |
| 404 | // As BrushPopup::regenerate() can be called when a |
| 405 | // "m_customBrushes" button is clicked we cannot delete |
| 406 | // "m_customBrushes" right now. |
| 407 | m_customBrushes->parent()->removeChild(m_customBrushes); |
| 408 | m_customBrushes->deferDelete(); |
| 409 | } |
| 410 | |
| 411 | m_customBrushes = new ButtonSet(3); |
| 412 | m_customBrushes->setTriggerOnMouseUp(true); |
| 413 | |
| 414 | int slot = 0; |
| 415 | for (const auto& brush : brushSlots) { |
| 416 | ++slot; |
| 417 | |
| 418 | // Get shortcut |
| 419 | std::string shortcut; |
| 420 | { |
| 421 | Params params; |
| 422 | params.set("change" , "custom" ); |
| 423 | params.set("slot" , base::convert_to<std::string>(slot).c_str()); |
| 424 | KeyPtr key = KeyboardShortcuts::instance()->command( |
| 425 | CommandId::ChangeBrush(), params); |
| 426 | if (key && !key->accels().empty()) |
| 427 | shortcut = key->accels().front().toString(); |
| 428 | } |
| 429 | m_customBrushes->addItem(new SelectBrushItem(brush, slot)); |
| 430 | m_customBrushes->addItem(new BrushShortcutItem(shortcut, slot)); |
| 431 | m_customBrushes->addItem(new BrushOptionsItem(this, slot)); |
| 432 | } |
| 433 | |
| 434 | m_customBrushes->addItem(new NewCustomBrushItem, 2, 1); |
| 435 | m_customBrushes->addItem(new NewBrushOptionsItem); |
| 436 | m_customBrushes->setExpansive(true); |
| 437 | m_box.addChild(m_customBrushes); |
| 438 | |
| 439 | // Resize the window and change the hot region. |
| 440 | fit_bounds(display, this, gfx::Rect(pos, sizeHint())); |
| 441 | setHotRegion(gfx::Region(boundsOnScreen())); |
| 442 | } |
| 443 | |
| 444 | void BrushPopup::() |
| 445 | { |
| 446 | if (isVisible()) { |
| 447 | gfx::Region rgn; |
| 448 | getDrawableRegion(rgn, kCutTopWindowsAndUseChildArea); |
| 449 | |
| 450 | Display* mainDisplay = manager()->display(); |
| 451 | regenerate(mainDisplay, |
| 452 | mainDisplay->nativeWindow()->pointFromScreen(boundsOnScreen().origin())); |
| 453 | invalidate(); |
| 454 | |
| 455 | parent()->invalidateRegion(rgn); |
| 456 | } |
| 457 | } |
| 458 | |
| 459 | // static |
| 460 | os::SurfaceRef BrushPopup::(const BrushRef& origBrush, |
| 461 | const bool useOriginalImage) |
| 462 | { |
| 463 | Image* image = nullptr; |
| 464 | BrushRef brush = origBrush; |
| 465 | if (brush) { |
| 466 | if (brush->type() != kImageBrushType && brush->size() > 10) { |
| 467 | brush.reset(new Brush(*brush)); |
| 468 | brush->setSize(10); |
| 469 | } |
| 470 | // Show the original image in the popup (without the image colors |
| 471 | // modified if there were some modification). |
| 472 | if (useOriginalImage) |
| 473 | image = brush->originalImage(); |
| 474 | else |
| 475 | image = brush->image(); |
| 476 | } |
| 477 | |
| 478 | os::SurfaceRef surface = os::instance()->makeRgbaSurface( |
| 479 | std::min(10, image ? image->width(): 4), |
| 480 | std::min(10, image ? image->height(): 4)); |
| 481 | |
| 482 | if (image) { |
| 483 | Palette* palette = get_current_palette(); |
| 484 | if (image->pixelFormat() == IMAGE_BITMAP) { |
| 485 | palette = new Palette(frame_t(0), 2); |
| 486 | palette->setEntry(0, rgba(0, 0, 0, 0)); |
| 487 | palette->setEntry(1, rgba(0, 0, 0, 255)); |
| 488 | } |
| 489 | |
| 490 | convert_image_to_surface( |
| 491 | image, palette, surface.get(), |
| 492 | 0, 0, 0, 0, image->width(), image->height()); |
| 493 | |
| 494 | if (image->pixelFormat() == IMAGE_BITMAP) |
| 495 | delete palette; |
| 496 | } |
| 497 | else { |
| 498 | surface->clear(); |
| 499 | } |
| 500 | |
| 501 | return surface; |
| 502 | } |
| 503 | |
| 504 | } // namespace app |
| 505 | |