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#define PAL_TRACE(...) // TRACEARGS
9
10#ifdef HAVE_CONFIG_H
11#include "config.h"
12#endif
13
14#include "app/app.h"
15#include "app/color.h"
16#include "app/color_utils.h"
17#include "app/commands/commands.h"
18#include "app/modules/editors.h"
19#include "app/modules/gfx.h"
20#include "app/modules/gui.h"
21#include "app/modules/palettes.h"
22#include "app/site.h"
23#include "app/ui/editor/editor.h"
24#include "app/ui/palette_view.h"
25#include "app/ui/skin/skin_theme.h"
26#include "app/ui/status_bar.h"
27#include "app/ui_context.h"
28#include "app/util/clipboard.h"
29#include "app/util/conversion_to_surface.h"
30#include "app/util/pal_ops.h"
31#include "base/convert_to.h"
32#include "doc/image.h"
33#include "doc/layer_tilemap.h"
34#include "doc/palette.h"
35#include "doc/remap.h"
36#include "doc/tileset.h"
37#include "fmt/format.h"
38#include "gfx/color.h"
39#include "gfx/point.h"
40#include "os/font.h"
41#include "os/surface.h"
42#include "os/surface.h"
43#include "os/system.h"
44#include "ui/graphics.h"
45#include "ui/manager.h"
46#include "ui/message.h"
47#include "ui/paint_event.h"
48#include "ui/size_hint_event.h"
49#include "ui/system.h"
50#include "ui/theme.h"
51#include "ui/view.h"
52#include "ui/widget.h"
53
54#include <algorithm>
55#include <cstdlib>
56#include <cstring>
57#include <set>
58
59namespace app {
60
61using namespace ui;
62using namespace app::skin;
63
64// Interface used to adapt the PaletteView widget to see tilesets too.
65class AbstractPaletteViewAdapter {
66public:
67 virtual ~AbstractPaletteViewAdapter() { }
68 virtual int size() const = 0;
69 virtual void paletteChange(doc::PalettePicks& picks) = 0;
70 virtual void activeSiteChange(const Site& site, doc::PalettePicks& picks) = 0;
71 virtual void clearSelection(PaletteView* paletteView,
72 doc::PalettePicks& picks) = 0;
73 virtual void selectIndex(PaletteView* paletteView,
74 int index, ui::MouseButton button) = 0;
75 virtual void resizePalette(PaletteView* paletteView,
76 int newSize) = 0;
77 virtual void dropColors(PaletteView* paletteView,
78 doc::PalettePicks& picks,
79 int& currentEntry,
80 const int beforeIndex,
81 const bool isCopy) = 0;
82 virtual void showEntryInStatusBar(StatusBar* statusBar, int index) = 0;
83 virtual void showDragInfoInStatusBar(StatusBar* statusBar, bool copy, int destIndex, int newSize) = 0;
84 virtual void showResizeInfoInStatusBar(StatusBar* statusBar, int newSize) = 0;
85 virtual void drawEntry(ui::Graphics* g,
86 SkinTheme* theme,
87 const int palIdx,
88 const int offIdx,
89 const int childSpacing,
90 gfx::Rect& box,
91 gfx::Color& negColor) = 0;
92 virtual doc::Tileset* tileset() const { return nullptr; }
93};
94
95// This default adapter uses the default behavior to use the
96// PaletteView as just a doc::Palette view.
97class PaletteViewAdapter : public AbstractPaletteViewAdapter {
98public:
99 int size() const override { return palette()->size(); }
100 void paletteChange(doc::PalettePicks& picks) override {
101 picks.resize(palette()->size());
102 }
103 void activeSiteChange(const Site& site, doc::PalettePicks& picks) override {
104 // Do nothing
105 }
106 void clearSelection(PaletteView* paletteView,
107 doc::PalettePicks& picks) override {
108 Palette palette(*this->palette());
109 Palette newPalette(palette);
110 newPalette.resize(std::max(1, newPalette.size() - picks.picks()));
111
112 Remap remap = create_remap_to_move_picks(picks, palette.size());
113 for (int i=0; i<palette.size(); ++i) {
114 if (!picks[i])
115 newPalette.setEntry(remap[i], palette.getEntry(i));
116 }
117
118 paletteView->setNewPalette(&palette, &newPalette,
119 PaletteViewModification::CLEAR);
120 }
121 void selectIndex(PaletteView* paletteView,
122 int index, ui::MouseButton button) override {
123 // Emit signal
124 if (paletteView->delegate())
125 paletteView->delegate()->onPaletteViewIndexChange(index, button);
126 }
127 void resizePalette(PaletteView* paletteView,
128 int newSize) override {
129 Palette newPalette(*paletteView->currentPalette());
130 newPalette.resize(newSize);
131 paletteView->setNewPalette(paletteView->currentPalette(),
132 &newPalette,
133 PaletteViewModification::RESIZE);
134 }
135 void dropColors(PaletteView* paletteView,
136 doc::PalettePicks& picks,
137 int& currentEntry,
138 const int beforeIndex,
139 const bool isCopy) override {
140 Palette palette(*paletteView->currentPalette());
141 Palette newPalette(palette);
142 move_or_copy_palette_colors(
143 palette,
144 newPalette,
145 picks,
146 currentEntry,
147 beforeIndex,
148 isCopy);
149 paletteView->setNewPalette(&palette, &newPalette,
150 PaletteViewModification::DRAGANDDROP);
151 }
152 void showEntryInStatusBar(StatusBar* statusBar, int index) override {
153 statusBar->showColor(0, app::Color::fromIndex(index));
154 }
155 void showDragInfoInStatusBar(StatusBar* statusBar, bool copy, int destIndex, int newSize) override {
156 statusBar->setStatusText(
157 0, fmt::format("{} to {} - New Palette Size {}",
158 (copy ? "Copy": "Move"),
159 destIndex, newSize));
160 }
161 void showResizeInfoInStatusBar(StatusBar* statusBar, int newSize) override {
162 statusBar->setStatusText(
163 0, fmt::format("New Palette Size {}", newSize));
164 }
165 void drawEntry(ui::Graphics* g,
166 SkinTheme* theme,
167 const int palIdx,
168 const int offIdx,
169 const int childSpacing,
170 gfx::Rect& box,
171 gfx::Color& negColor) override {
172 doc::color_t palColor =
173 (palIdx < palette()->size() ? palette()->getEntry(palIdx):
174 rgba(0, 0, 0, 255));
175 app::Color appColor = app::Color::fromRgb(
176 rgba_getr(palColor),
177 rgba_getg(palColor),
178 rgba_getb(palColor),
179 rgba_geta(palColor));
180
181 if (childSpacing > 0) {
182 gfx::Color color = theme->colors.paletteEntriesSeparator();
183 g->fillRect(color, gfx::Rect(box).enlarge(childSpacing));
184 }
185 draw_color(g, box, appColor, doc::ColorMode::RGB);
186
187 const gfx::Color gfxColor = gfx::rgba(
188 rgba_getr(palColor),
189 rgba_getg(palColor),
190 rgba_getb(palColor),
191 rgba_geta(palColor));
192 negColor = color_utils::blackandwhite_neg(gfxColor);
193 }
194private:
195 doc::Palette* palette() const {
196 return get_current_palette();
197 }
198};
199
200// This adapter makes it possible to use a PaletteView to edit a
201// doc::Tileset.
202class TilesetViewAdapter : public AbstractPaletteViewAdapter {
203public:
204 int size() const override {
205 if (auto t = tileset())
206 return t->size();
207 else
208 return 0;
209 }
210 void paletteChange(doc::PalettePicks& picks) override {
211 // Do nothing
212 }
213 void activeSiteChange(const Site& site, doc::PalettePicks& picks) override {
214 if (auto tileset = this->tileset())
215 picks.resize(tileset->size());
216 else
217 picks.clear();
218 }
219 void clearSelection(PaletteView* paletteView,
220 doc::PalettePicks& picks) override {
221 // Cannot delete the empty tile (index 0)
222 int i = picks.firstPick();
223 if (i == doc::notile) {
224 picks[i] = false;
225 if (!picks.picks()) {
226 // Cannot remove empty tile
227 StatusBar::instance()->showTip(1000, "Cannot delete the empty tile");
228 return;
229 }
230 }
231
232 paletteView->delegate()->onTilesViewClearTiles(picks);
233 }
234 void selectIndex(PaletteView* paletteView,
235 int index, ui::MouseButton button) override {
236 // Emit signal
237 if (paletteView->delegate())
238 paletteView->delegate()->onTilesViewIndexChange(index, button);
239 }
240 void resizePalette(PaletteView* paletteView,
241 int newSize) override {
242 paletteView->delegate()->onTilesViewResize(newSize);
243 }
244 void dropColors(PaletteView* paletteView,
245 doc::PalettePicks& picks,
246 int& currentEntry,
247 const int _beforeIndex,
248 const bool isCopy) override {
249 PAL_TRACE("dropColors");
250
251 doc::Tileset* tileset = this->tileset();
252 ASSERT(tileset);
253 if (!tileset)
254 return;
255
256 // Important: we create a copy because if we make the tileset
257 // bigger dropping tiles outside the tileset range, the tileset
258 // will be made bigger (see cmd::AddTile() inside
259 // move_tiles_in_tileset() function), a
260 // Doc::notifyTilesetChanged() will be generated, a
261 // ColorBar::onTilesetChanged() called, and finally we'll receive a
262 // PaletteView::deselect() that will clear the whole picks.
263 auto newPicks = picks;
264 int beforeIndex = _beforeIndex;
265
266 // We cannot move the empty tile (index 0) no any place
267 if (beforeIndex == 0)
268 ++beforeIndex;
269 if (!isCopy && newPicks.size() > 0 && newPicks[0])
270 newPicks[0] = false;
271 if (!newPicks.picks()) {
272 // Cannot move empty tile
273 StatusBar::instance()->showTip(1000, "Cannot move the empty tile");
274 return;
275 }
276
277 paletteView->delegate()->onTilesViewDragAndDrop(
278 tileset, newPicks, currentEntry, beforeIndex, isCopy);
279
280 // Copy the new picks
281 picks = newPicks;
282 }
283 void showEntryInStatusBar(StatusBar* statusBar, int index) override {
284 statusBar->showTile(0, doc::tile(index, 0));
285 }
286 void showDragInfoInStatusBar(StatusBar* statusBar, bool copy, int destIndex, int newSize) override {
287 statusBar->setStatusText(
288 0, fmt::format("{} to {} - New Tileset Size {}",
289 (copy ? "Copy": "Move"),
290 destIndex, newSize));
291 }
292 void showResizeInfoInStatusBar(StatusBar* statusBar, int newSize) override {
293 statusBar->setStatusText(
294 0, fmt::format("New Tileset Size {}", newSize));
295 }
296 void drawEntry(ui::Graphics* g,
297 SkinTheme* theme,
298 const int palIdx,
299 const int offIdx,
300 const int childSpacing,
301 gfx::Rect& box,
302 gfx::Color& negColor) override {
303 if (childSpacing > 0) {
304 gfx::Color color = theme->colors.paletteEntriesSeparator();
305 g->fillRect(color, gfx::Rect(box).enlarge(childSpacing));
306 }
307 draw_color(g, box, app::Color::fromMask(), doc::ColorMode::RGB);
308
309 doc::ImageRef tileImage;
310 if (auto t = this->tileset())
311 tileImage = t->get(palIdx);
312 if (tileImage) {
313 int w = tileImage->width();
314 int h = tileImage->height();
315 os::SurfaceRef surface = os::instance()->makeRgbaSurface(w, h);
316 convert_image_to_surface(tileImage.get(), get_current_palette(),
317 surface.get(), 0, 0, 0, 0, w, h);
318
319 ui::Paint paint;
320 paint.blendMode(os::BlendMode::SrcOver);
321
322 os::Sampling sampling;
323 if (w > box.w && h > box.h) {
324 sampling = os::Sampling(os::Sampling::Filter::Linear,
325 os::Sampling::Mipmap::Nearest);
326 }
327
328 g->drawSurface(surface.get(), gfx::Rect(0, 0, w, h), box,
329 sampling, &paint);
330 }
331 negColor = gfx::rgba(255, 255, 255);
332 }
333 doc::Tileset* tileset() const override {
334 Site site = App::instance()->context()->activeSite();
335 if (site.layer() &&
336 site.layer()->isTilemap()) {
337 return static_cast<LayerTilemap*>(site.layer())->tileset();
338 }
339 else
340 return nullptr;
341 }
342private:
343};
344
345PaletteView::PaletteView(bool editable, PaletteViewStyle style, PaletteViewDelegate* delegate, int boxsize)
346 : Widget(kGenericWidget)
347 , m_state(State::WAITING)
348 , m_editable(editable)
349 , m_style(style)
350 , m_delegate(delegate)
351 , m_adapter(isTiles() ? (AbstractPaletteViewAdapter*)new TilesetViewAdapter:
352 (AbstractPaletteViewAdapter*)new PaletteViewAdapter)
353 , m_columns(16)
354 , m_boxsize(boxsize)
355 , m_currentEntry(-1)
356 , m_rangeAnchor(-1)
357 , m_isUpdatingColumns(false)
358 , m_hot(Hit::NONE)
359 , m_copy(false)
360{
361 setFocusStop(true);
362 setDoubleBuffered(true);
363
364 m_palConn = App::instance()->PaletteChange.connect(&PaletteView::onAppPaletteChange, this);
365 m_csConn = App::instance()->ColorSpaceChange.connect(
366 [this]{ invalidate(); });
367
368 {
369 auto& entriesSep = Preferences::instance().colorBar.entriesSeparator;
370 m_withSeparator = entriesSep();
371 m_sepConn = entriesSep.AfterChange.connect(
372 [this, &entriesSep](bool newValue) {
373 m_withSeparator = entriesSep();
374 updateBorderAndChildSpacing();
375 });
376 }
377
378 if (isTiles())
379 UIContext::instance()->add_observer(this);
380
381 InitTheme.connect(
382 [this]{
383 updateBorderAndChildSpacing();
384 });
385 initTheme();
386}
387
388PaletteView::~PaletteView()
389{
390 if (isTiles())
391 UIContext::instance()->remove_observer(this);
392}
393
394void PaletteView::setColumns(int columns)
395{
396 int old_columns = m_columns;
397 m_columns = columns;
398
399 if (m_columns != old_columns) {
400 View* view = View::getView(this);
401 if (view)
402 view->updateView();
403
404 invalidate();
405 }
406}
407
408void PaletteView::deselect()
409{
410 bool invalidate = (m_selectedEntries.picks() > 0);
411
412 m_selectedEntries.resize(m_adapter->size());
413 m_selectedEntries.clear();
414
415 if (invalidate)
416 this->invalidate();
417}
418
419void PaletteView::selectColor(int index)
420{
421 if (index < 0 || index >= m_adapter->size())
422 return;
423
424 if (m_currentEntry != index || !m_selectedEntries[index]) {
425 m_currentEntry = index;
426 m_rangeAnchor = index;
427
428 update_scroll(m_currentEntry);
429 invalidate();
430 }
431}
432
433void PaletteView::selectExactMatchColor(const app::Color& color)
434{
435 int index = findExactIndex(color);
436 if (index >= 0)
437 selectColor(index);
438}
439
440void PaletteView::selectRange(int index1, int index2)
441{
442 m_rangeAnchor = index1;
443 m_currentEntry = index2;
444
445 std::fill(m_selectedEntries.begin()+std::min(index1, index2),
446 m_selectedEntries.begin()+std::max(index1, index2)+1, true);
447
448 update_scroll(index2);
449 invalidate();
450}
451
452int PaletteView::getSelectedEntry() const
453{
454 return m_currentEntry;
455}
456
457bool PaletteView::getSelectedRange(int& index1, int& index2) const
458{
459 int i, i2, j;
460
461 // Find the first selected entry
462 for (i=0; i<(int)m_selectedEntries.size(); ++i)
463 if (m_selectedEntries[i])
464 break;
465
466 // Find the first unselected entry after i
467 for (i2=i+1; i2<(int)m_selectedEntries.size(); ++i2)
468 if (!m_selectedEntries[i2])
469 break;
470
471 // Find the last selected entry
472 for (j=m_selectedEntries.size()-1; j>=0; --j)
473 if (m_selectedEntries[j])
474 break;
475
476 if (j-i+1 == i2-i) {
477 index1 = i;
478 index2 = j;
479 return true;
480 }
481 else
482 return false;
483}
484
485void PaletteView::getSelectedEntries(PalettePicks& entries) const
486{
487 entries = m_selectedEntries;
488}
489
490int PaletteView::getSelectedEntriesCount() const
491{
492 return m_selectedEntries.picks();
493}
494
495void PaletteView::setSelectedEntries(const doc::PalettePicks& entries)
496{
497 m_selectedEntries = entries;
498 m_selectedEntries.resize(m_adapter->size());
499 m_currentEntry = m_selectedEntries.firstPick();
500
501 if (entries.picks() > 0)
502 requestFocus();
503
504 invalidate();
505}
506
507doc::Tileset* PaletteView::tileset() const
508{
509 return m_adapter->tileset();
510}
511
512app::Color PaletteView::getColorByPosition(const gfx::Point& pos)
513{
514 gfx::Point relPos = pos - bounds().origin();
515 for (int i=0; i<m_adapter->size(); ++i) {
516 auto box = getPaletteEntryBounds(i);
517 box.inflate(childSpacing());
518 if (box.contains(relPos))
519 return app::Color::fromIndex(i);
520 }
521 return app::Color::fromMask();
522}
523
524doc::tile_t PaletteView::getTileByPosition(const gfx::Point& pos)
525{
526 gfx::Point relPos = pos - bounds().origin();
527 for (int i=0; i<m_adapter->size(); ++i) {
528 auto box = getPaletteEntryBounds(i);
529 box.inflate(childSpacing());
530 if (box.contains(relPos))
531 return doc::tile(i, 0);
532 }
533 return doc::notile;
534}
535
536void PaletteView::onActiveSiteChange(const Site& site)
537{
538 ASSERT(isTiles());
539 m_adapter->activeSiteChange(site, m_selectedEntries);
540}
541
542int PaletteView::getBoxSize() const
543{
544 return int(m_boxsize);
545}
546
547void PaletteView::setBoxSize(double boxsize)
548{
549 if (isTiles())
550 m_boxsize = std::clamp(boxsize, 4.0, 64.0);
551 else
552 m_boxsize = std::clamp(boxsize, 4.0, 32.0);
553
554 if (m_delegate)
555 m_delegate->onPaletteViewChangeSize(this, int(m_boxsize));
556
557 View* view = View::getView(this);
558 if (view)
559 view->layout();
560}
561
562void PaletteView::clearSelection()
563{
564 if (!m_selectedEntries.picks())
565 return;
566
567 m_adapter->clearSelection(this, m_selectedEntries);
568
569 m_currentEntry = m_selectedEntries.firstPick();
570 m_selectedEntries.clear();
571 stopMarchingAnts();
572}
573
574void PaletteView::cutToClipboard()
575{
576 if (!m_selectedEntries.picks())
577 return;
578
579 Clipboard::instance()->copyPalette(currentPalette(), m_selectedEntries);
580
581 clearSelection();
582}
583
584void PaletteView::copyToClipboard()
585{
586 if (!m_selectedEntries.picks())
587 return;
588
589 Clipboard::instance()->copyPalette(currentPalette(), m_selectedEntries);
590
591 startMarchingAnts();
592 invalidate();
593}
594
595void PaletteView::pasteFromClipboard()
596{
597 auto clipboard = Clipboard::instance();
598 if (clipboard->format() == ClipboardFormat::PaletteEntries) {
599 if (m_delegate)
600 m_delegate->onPaletteViewPasteColors(
601 clipboard->getPalette(),
602 clipboard->getPalettePicks(),
603 m_selectedEntries);
604
605 // We just hide the marching ants, the user can paste multiple
606 // times.
607 stopMarchingAnts();
608 invalidate();
609 }
610}
611
612void PaletteView::discardClipboardSelection()
613{
614 if (isMarchingAntsRunning()) {
615 stopMarchingAnts();
616 invalidate();
617 }
618}
619
620bool PaletteView::onProcessMessage(Message* msg)
621{
622 switch (msg->type()) {
623
624 case kFocusEnterMessage:
625 FocusOrClick(msg);
626 break;
627
628 case kKeyDownMessage:
629 case kKeyUpMessage:
630 case kMouseEnterMessage:
631 if (hasMouse())
632 updateCopyFlag(msg);
633 break;
634
635 case kMouseDownMessage:
636 switch (m_hot.part) {
637
638 case Hit::COLOR:
639 case Hit::POSSIBLE_COLOR:
640 // Clicking outside the palette range will deselect
641 if (m_hot.color >= m_adapter->size()) {
642 deselect();
643 break;
644 }
645
646 m_state = State::SELECTING_COLOR;
647
648 // As we can ctrl+click color bar + timeline, now we have to
649 // re-prioritize the color bar on each click.
650 FocusOrClick(msg);
651 break;
652
653 case Hit::OUTLINE:
654 m_state = State::DRAGGING_OUTLINE;
655 break;
656
657 case Hit::RESIZE_HANDLE:
658 m_state = State::RESIZING_PALETTE;
659 break;
660 }
661
662 captureMouse();
663 [[fallthrough]];
664
665 case kMouseMoveMessage: {
666 MouseMessage* mouseMsg = static_cast<MouseMessage*>(msg);
667
668 if ((m_state == State::SELECTING_COLOR) &&
669 (m_hot.part == Hit::COLOR ||
670 m_hot.part == Hit::POSSIBLE_COLOR)) {
671 int idx = m_hot.color;
672 idx = std::clamp(idx, 0, std::max(0, m_adapter->size()-1));
673
674 const MouseButton button = mouseMsg->button();
675
676 if (hasCapture() &&
677 (idx >= 0 && idx < m_adapter->size()) &&
678 ((idx != m_currentEntry) ||
679 (msg->type() == kMouseDownMessage) ||
680 (button == kButtonMiddle))) {
681 if (button != kButtonMiddle) {
682 if (!msg->ctrlPressed() && !msg->shiftPressed())
683 deselect();
684
685 if (msg->type() == kMouseMoveMessage)
686 selectRange(m_rangeAnchor, idx);
687 else {
688 selectColor(idx);
689 m_selectedEntries[idx] = true;
690 }
691 }
692
693 m_adapter->selectIndex(this, idx, button);
694 }
695 }
696
697 if (m_state == State::DRAGGING_OUTLINE &&
698 m_hot.part == Hit::COLOR) {
699 update_scroll(m_hot.color);
700 }
701
702 if (hasCapture())
703 return true;
704
705 break;
706 }
707
708 case kMouseUpMessage:
709 if (hasCapture()) {
710 releaseMouse();
711
712 switch (m_state) {
713
714 case State::DRAGGING_OUTLINE:
715 if (m_hot.part == Hit::COLOR ||
716 m_hot.part == Hit::POSSIBLE_COLOR) {
717 int i = m_hot.color;
718 if (!m_copy && i > m_selectedEntries.firstPick())
719 i += m_selectedEntries.picks();
720 dropColors(i);
721 }
722 break;
723
724 case State::RESIZING_PALETTE:
725 if (m_hot.part == Hit::COLOR ||
726 m_hot.part == Hit::POSSIBLE_COLOR) {
727 int newSize = std::max(1, m_hot.color);
728 m_adapter->resizePalette(this, newSize);
729 m_selectedEntries.resize(newSize);
730 }
731 break;
732 }
733
734 m_state = State::WAITING;
735 setStatusBar();
736 invalidate();
737 }
738 return true;
739
740 case kMouseWheelMessage: {
741 View* view = View::getView(this);
742 if (!view)
743 break;
744
745 gfx::Point delta = static_cast<MouseMessage*>(msg)->wheelDelta();
746
747 if (msg->onlyCtrlPressed() ||
748 msg->onlyCmdPressed()) {
749 int z = delta.x - delta.y;
750 setBoxSize(m_boxsize + z);
751 }
752 else {
753 gfx::Point scroll = view->viewScroll();
754
755 if (static_cast<MouseMessage*>(msg)->preciseWheel())
756 scroll += delta;
757 else
758 scroll += delta * 3 * boxSizePx();
759
760 view->setViewScroll(scroll);
761 }
762 break;
763 }
764
765 case kMouseLeaveMessage:
766 m_hot = Hit(Hit::NONE);
767 setStatusBar();
768 invalidate();
769 break;
770
771 case kSetCursorMessage: {
772 MouseMessage* mouseMsg = static_cast<MouseMessage*>(msg);
773 Hit hit = hitTest(mouseMsg->position() - bounds().origin());
774 if (hit != m_hot) {
775 // Redraw only when we put the mouse in other part of the
776 // widget (e.g. if we move from color to color, we don't want
777 // to redraw the whole widget if we're on WAITING state).
778 if ((m_state == State::WAITING && hit.part != m_hot.part) ||
779 (m_state != State::WAITING && hit != m_hot)) {
780 invalidate();
781 }
782 m_hot = hit;
783 setStatusBar();
784 }
785 setCursor();
786 return true;
787 }
788
789 case kTouchMagnifyMessage: {
790 setBoxSize(m_boxsize +
791 m_boxsize * static_cast<ui::TouchMessage*>(msg)->magnification());
792 break;
793 }
794
795 }
796
797 return Widget::onProcessMessage(msg);
798}
799
800void PaletteView::onPaint(ui::PaintEvent& ev)
801{
802 auto theme = SkinTheme::get(this);
803 const int outlineWidth = theme->dimensions.paletteOutlineWidth();
804 ui::Graphics* g = ev.graphics();
805 gfx::Rect bounds = clientBounds();
806 Palette* palette = currentPalette();
807 int fgIndex = -1;
808 int bgIndex = -1;
809 int transparentIndex = -1;
810 const bool hotColor = (m_hot.part == Hit::COLOR ||
811 m_hot.part == Hit::POSSIBLE_COLOR);
812 const bool dragging = (m_state == State::DRAGGING_OUTLINE && hotColor);
813 const bool resizing = (m_state == State::RESIZING_PALETTE && hotColor);
814
815 if (m_delegate) {
816 switch (m_style) {
817
818 case FgBgColors:
819 fgIndex = findExactIndex(m_delegate->onPaletteViewGetForegroundIndex());
820 bgIndex = findExactIndex(m_delegate->onPaletteViewGetBackgroundIndex());
821
822 if (current_editor && current_editor->sprite()->pixelFormat() == IMAGE_INDEXED)
823 transparentIndex = current_editor->sprite()->transparentColor();
824 break;
825
826 case FgBgTiles:
827 fgIndex = m_delegate->onPaletteViewGetForegroundTile();
828 bgIndex = m_delegate->onPaletteViewGetBackgroundTile();
829 break;
830 }
831 }
832
833 g->fillRect(theme->colors.editorFace(), bounds);
834
835 // Draw palette/tileset entries
836 int picksCount = m_selectedEntries.picks();
837 int idxOffset = 0;
838 int boxOffset = 0;
839 int palSize = m_adapter->size();
840 if (dragging && !m_copy) palSize -= picksCount;
841 if (resizing) palSize = m_hot.color;
842
843 for (int i=0; i<palSize; ++i) {
844 if (dragging) {
845 if (!m_copy) {
846 while (i+idxOffset < m_selectedEntries.size() &&
847 m_selectedEntries[i+idxOffset])
848 ++idxOffset;
849 }
850 if (!boxOffset && m_hot.color == i) {
851 boxOffset += picksCount;
852 }
853 }
854
855 gfx::Rect box = getPaletteEntryBounds(i + boxOffset);
856 gfx::Color negColor;
857 m_adapter->drawEntry(g, theme, i + idxOffset, i + boxOffset,
858 childSpacing(), box, negColor);
859 const int boxsize = boxSizePx();
860 const int scale = guiscale();
861
862 switch (m_style) {
863
864 case SelectOneColor:
865 if (m_currentEntry == i)
866 g->fillRect(negColor, gfx::Rect(box.center(), gfx::Size(scale, scale)));
867 break;
868
869 case FgBgColors:
870 case FgBgTiles:
871 if (!m_delegate || m_delegate->onIsPaletteViewActive(this)) {
872 if (fgIndex == i) {
873 for (int i=0; i<int(boxsize/2); i += scale) {
874 g->fillRect(negColor,
875 gfx::Rect(box.x, box.y+i, int(boxsize/2)-i, scale));
876 }
877 }
878
879 if (bgIndex == i) {
880 for (int i=0; i<int(boxsize/4); i += scale) {
881 g->fillRect(negColor,
882 gfx::Rect(box.x+box.w-(i+scale),
883 box.y+box.h-int(boxsize/4)+i,
884 i+scale, scale));
885 }
886 }
887
888 if (transparentIndex == i)
889 g->fillRect(negColor, gfx::Rect(box.center(), gfx::Size(scale, scale)));
890 }
891 break;
892 }
893 }
894
895 // Handle to resize palette
896
897 if (m_editable && !dragging) {
898 os::Surface* handle = theme->parts.palResize()->bitmap(0);
899 gfx::Rect box = getPaletteEntryBounds(palSize);
900 g->drawRgbaSurface(handle,
901 box.x+box.w/2-handle->width()/2,
902 box.y+box.h/2-handle->height()/2);
903 }
904
905 // Draw selected entries
906
907 PaintWidgetPartInfo info;
908 if (m_hot.part == Hit::OUTLINE)
909 info.styleFlags |= ui::Style::Layer::kMouse;
910
911 PalettePicks dragPicks;
912 int j = 0;
913 if (dragging) {
914 dragPicks.resize(m_hot.color+picksCount);
915 std::fill(dragPicks.begin()+m_hot.color, dragPicks.end(), true);
916 }
917 PalettePicks& picks = (dragging ? dragPicks: m_selectedEntries);
918
919 const int size = m_adapter->size();
920 for (int i=0; i<size; ++i) {
921 // TODO why does this fail?
922 //ASSERT(i >= 0 && i < m_selectedEntries.size());
923 if (i >= 0 && i < m_selectedEntries.size() &&
924 !m_selectedEntries[i]) {
925 continue;
926 }
927
928 const int k = (dragging ? m_hot.color+j: i);
929
930 gfx::Rect box, clipR;
931 getEntryBoundsAndClip(k, picks, outlineWidth, box, clipR);
932
933 IntersectClip clip(g, clipR);
934 if (clip) {
935 // Draw color being dragged + label
936 if (dragging) {
937 gfx::Rect box2 = getPaletteEntryBounds(k);
938 gfx::Color negColor;
939 m_adapter->drawEntry(g, theme, i, k, childSpacing(),
940 box2, negColor);
941
942 os::Font* minifont = theme->getMiniFont();
943 const std::string text = base::convert_to<std::string>(k);
944 g->setFont(AddRef(minifont));
945 g->drawText(text, negColor, gfx::ColorNone,
946 gfx::Point(box2.x + box2.w/2 - minifont->textLength(text)/2,
947 box2.y + box2.h/2 - minifont->height()/2));
948 }
949
950 // Draw the selection
951 theme->paintWidgetPart(
952 g, theme->styles.colorbarSelection(), box, info);
953 }
954
955 ++j;
956 }
957
958 // Draw marching ants
959 if ((m_state == State::WAITING) &&
960 (isMarchingAntsRunning()) &&
961 (Clipboard::instance()->format() == ClipboardFormat::PaletteEntries)) {
962 auto clipboard = Clipboard::instance();
963 Palette* clipboardPalette = clipboard->getPalette();
964 const PalettePicks& clipboardPicks = clipboard->getPalettePicks();
965
966 if (clipboardPalette &&
967 clipboardPalette->countDiff(palette, nullptr, nullptr) == 0) {
968 for (int i=0; i<clipboardPicks.size(); ++i) {
969 if (!clipboardPicks[i])
970 continue;
971
972 gfx::Rect box, clipR;
973 getEntryBoundsAndClip(i, clipboardPicks, 1*guiscale(), box, clipR);
974
975 IntersectClip clip(g, clipR);
976 if (clip) {
977 ui::Paint paint;
978 paint.style(ui::Paint::Stroke);
979 ui::set_checkered_paint_mode(paint, getMarchingAntsOffset(),
980 gfx::rgba(0, 0, 0, 255),
981 gfx::rgba(255, 255, 255, 255));
982 g->drawRect(box, paint);
983 }
984 }
985 }
986 }
987}
988
989void PaletteView::onResize(ui::ResizeEvent& ev)
990{
991 if (!m_isUpdatingColumns) {
992 m_isUpdatingColumns = true;
993 View* view = View::getView(this);
994 if (view) {
995 int columns =
996 (view->viewportBounds().w
997 -this->border().width()
998 +this->childSpacing())
999 / (boxSizePx()
1000 +this->childSpacing());
1001 setColumns(std::max(1, columns));
1002 }
1003 m_isUpdatingColumns = false;
1004 }
1005
1006 Widget::onResize(ev);
1007}
1008
1009void PaletteView::onSizeHint(ui::SizeHintEvent& ev)
1010{
1011 div_t d = div(m_adapter->size(), m_columns);
1012 int cols = m_columns;
1013 int rows = d.quot + ((d.rem)? 1: 0);
1014
1015 if (m_editable) {
1016 ++rows;
1017 }
1018
1019 const int boxsize = boxSizePx();
1020 ev.setSizeHint(
1021 gfx::Size(
1022 border().width() + cols*boxsize + (cols-1)*childSpacing(),
1023 border().height() + rows*boxsize + (rows-1)*childSpacing()));
1024}
1025
1026void PaletteView::onDrawMarchingAnts()
1027{
1028 invalidate();
1029}
1030
1031void PaletteView::update_scroll(int color)
1032{
1033 View* view = View::getView(this);
1034 if (!view)
1035 return;
1036
1037 gfx::Rect vp = view->viewportBounds();
1038 gfx::Point scroll;
1039 int x, y, cols;
1040 div_t d;
1041 const int boxsize = boxSizePx();
1042
1043 scroll = view->viewScroll();
1044
1045 d = div(m_adapter->size(), m_columns);
1046 cols = m_columns;
1047
1048 y = (boxsize+childSpacing()) * (color / cols);
1049 x = (boxsize+childSpacing()) * (color % cols);
1050
1051 if (scroll.x > x)
1052 scroll.x = x;
1053 else if (scroll.x+vp.w-boxsize-2 < x)
1054 scroll.x = x-vp.w+boxsize+2;
1055
1056 if (scroll.y > y)
1057 scroll.y = y;
1058 else if (scroll.y+vp.h-boxsize-2 < y)
1059 scroll.y = y-vp.h+boxsize+2;
1060
1061 view->setViewScroll(scroll);
1062}
1063
1064void PaletteView::onAppPaletteChange()
1065{
1066 m_adapter->paletteChange(m_selectedEntries);
1067
1068 View* view = View::getView(this);
1069 if (view)
1070 view->layout();
1071}
1072
1073gfx::Rect PaletteView::getPaletteEntryBounds(int index) const
1074{
1075 const gfx::Rect bounds = clientChildrenBounds();
1076 const int col = index % m_columns;
1077 const int row = index / m_columns;
1078 const int boxsize = boxSizePx();
1079 const int spacing = childSpacing();
1080
1081 return gfx::Rect(
1082 bounds.x + col*boxsize + (col-1)*spacing + (m_withSeparator ? border().left(): 0),
1083 bounds.y + row*boxsize + (row-1)*spacing + (m_withSeparator ? border().top(): 0),
1084 boxsize, boxsize);
1085}
1086
1087PaletteView::Hit PaletteView::hitTest(const gfx::Point& pos)
1088{
1089 auto theme = SkinTheme::get(this);
1090 const int outlineWidth = theme->dimensions.paletteOutlineWidth();
1091 const int size = m_adapter->size();
1092
1093 if (m_state == State::WAITING && m_editable) {
1094 // First check if the mouse is inside the selection outline.
1095 for (int i=0; i<size; ++i) {
1096 if (!m_selectedEntries[i])
1097 continue;
1098
1099 bool top = (i >= m_columns && i-m_columns >= 0 ? m_selectedEntries[i-m_columns]: false);
1100 bool bottom = (i < size-m_columns && i+m_columns < size ? m_selectedEntries[i+m_columns]: false);
1101 bool left = ((i%m_columns)>0 && i-1 >= 0 ? m_selectedEntries[i-1]: false);
1102 bool right = ((i%m_columns)<m_columns-1 && i+1 < size ? m_selectedEntries[i+1]: false);
1103
1104 gfx::Rect box = getPaletteEntryBounds(i);
1105 box.enlarge(outlineWidth);
1106
1107 if ((!top && gfx::Rect(box.x, box.y, box.w, outlineWidth).contains(pos)) ||
1108 (!bottom && gfx::Rect(box.x, box.y+box.h-outlineWidth, box.w, outlineWidth).contains(pos)) ||
1109 (!left && gfx::Rect(box.x, box.y, outlineWidth, box.h).contains(pos)) ||
1110 (!right && gfx::Rect(box.x+box.w-outlineWidth, box.y, outlineWidth, box.h).contains(pos)))
1111 return Hit(Hit::OUTLINE, i);
1112 }
1113
1114 // Check if we are in the resize handle
1115 if (getPaletteEntryBounds(size).contains(pos)) {
1116 return Hit(Hit::RESIZE_HANDLE, size);
1117 }
1118 }
1119
1120 // Check if we are inside a color.
1121 View* view = View::getView(this);
1122 ASSERT(view);
1123 gfx::Rect vp = view->viewportBounds();
1124 for (int i=0; ; ++i) {
1125 gfx::Rect box = getPaletteEntryBounds(i);
1126 if (i >= size && box.y2() > vp.h)
1127 break;
1128
1129 box.w += childSpacing();
1130 box.h += childSpacing();
1131 if (box.contains(pos))
1132 return Hit(Hit::COLOR, i);
1133 }
1134
1135 gfx::Rect box = getPaletteEntryBounds(0);
1136
1137 int colsLimit = m_columns;
1138 if (m_state == State::DRAGGING_OUTLINE)
1139 --colsLimit;
1140 int i = std::clamp((pos.x-vp.x)/box.w, 0, colsLimit)
1141 + std::max(0, pos.y/box.h)*m_columns;
1142 return Hit(Hit::POSSIBLE_COLOR, i);
1143}
1144
1145void PaletteView::dropColors(int beforeIndex)
1146{
1147 m_adapter->dropColors(this,
1148 m_selectedEntries,
1149 m_currentEntry,
1150 beforeIndex,
1151 m_copy);
1152}
1153
1154void PaletteView::getEntryBoundsAndClip(int i, const PalettePicks& entries,
1155 const int outlineWidth,
1156 gfx::Rect& box, gfx::Rect& clip) const
1157{
1158 const int childSpacing = this->childSpacing();
1159
1160 box = clip = getPaletteEntryBounds(i);
1161 box.enlarge(outlineWidth);
1162
1163 gfx::Border boxBorder(0, 0, 0, 0);
1164 gfx::Border clipBorder(0, 0, 0, 0);
1165
1166 // Left
1167 if (!pickedXY(entries, i, -1, 0))
1168 clipBorder.left(outlineWidth);
1169 else {
1170 boxBorder.left((childSpacing+1)/2);
1171 clipBorder.left((childSpacing+1)/2);
1172 }
1173
1174 // Top
1175 if (!pickedXY(entries, i, 0, -1))
1176 clipBorder.top(outlineWidth);
1177 else {
1178 boxBorder.top((childSpacing+1)/2);
1179 clipBorder.top((childSpacing+1)/2);
1180 }
1181
1182 // Right
1183 if (!pickedXY(entries, i, +1, 0))
1184 clipBorder.right(outlineWidth);
1185 else {
1186 boxBorder.right(childSpacing/2);
1187 clipBorder.right(childSpacing/2);
1188 }
1189
1190 // Bottom
1191 if (!pickedXY(entries, i, 0, +1))
1192 clipBorder.bottom(outlineWidth);
1193 else {
1194 boxBorder.bottom(childSpacing/2);
1195 clipBorder.bottom(childSpacing/2);
1196 }
1197
1198 box.enlarge(boxBorder);
1199 clip.enlarge(clipBorder);
1200}
1201
1202bool PaletteView::pickedXY(const doc::PalettePicks& entries, int i, int dx, int dy) const
1203{
1204 const int x = (i % m_columns) + dx;
1205 const int y = (i / m_columns) + dy;
1206 const int lastcolor = entries.size()-1;
1207
1208 if (x < 0 || x >= m_columns || y < 0 || y > lastcolor/m_columns)
1209 return false;
1210
1211 i = x + y*m_columns;
1212 if (i >= 0 && i < entries.size())
1213 return entries[i];
1214 else
1215 return false;
1216}
1217
1218void PaletteView::updateCopyFlag(ui::Message* msg)
1219{
1220 bool oldCopy = m_copy;
1221 m_copy = (msg->ctrlPressed() || msg->altPressed());
1222 if (oldCopy != m_copy) {
1223 setCursor();
1224 setStatusBar();
1225 invalidate();
1226 }
1227}
1228
1229void PaletteView::setCursor()
1230{
1231 if (m_state == State::DRAGGING_OUTLINE ||
1232 (m_state == State::WAITING &&
1233 m_hot.part == Hit::OUTLINE)) {
1234 if (m_copy)
1235 ui::set_mouse_cursor(kArrowPlusCursor);
1236 else
1237 ui::set_mouse_cursor(kMoveCursor);
1238 }
1239 else if (m_state == State::RESIZING_PALETTE ||
1240 (m_state == State::WAITING &&
1241 m_hot.part == Hit::RESIZE_HANDLE)) {
1242 ui::set_mouse_cursor(kSizeWECursor);
1243 }
1244 else
1245 ui::set_mouse_cursor(kArrowCursor);
1246}
1247
1248void PaletteView::setStatusBar()
1249{
1250 StatusBar* statusBar = StatusBar::instance();
1251
1252 if (m_hot.part == Hit::NONE) {
1253 statusBar->showDefaultText();
1254 return;
1255 }
1256
1257 switch (m_state) {
1258
1259 case State::WAITING:
1260 case State::SELECTING_COLOR:
1261 if ((m_hot.part == Hit::COLOR ||
1262 m_hot.part == Hit::OUTLINE ||
1263 m_hot.part == Hit::POSSIBLE_COLOR) &&
1264 (m_hot.color < m_adapter->size())) {
1265 const int index = std::max(0, m_hot.color);
1266
1267 m_adapter->showEntryInStatusBar(statusBar, index);
1268 }
1269 else {
1270 statusBar->showDefaultText();
1271 }
1272 break;
1273
1274 case State::DRAGGING_OUTLINE:
1275 if (m_hot.part == Hit::COLOR) {
1276 const int picks = m_selectedEntries.picks();
1277 const int destIndex = std::max(0, m_hot.color);
1278 const int palSize = m_adapter->size();
1279 const int newPalSize =
1280 (m_copy ? std::max(palSize + picks, destIndex + picks):
1281 std::max(palSize, destIndex + picks));
1282
1283 m_adapter->showDragInfoInStatusBar(
1284 statusBar, m_copy, destIndex, newPalSize);
1285 }
1286 else {
1287 statusBar->showDefaultText();
1288 }
1289 break;
1290
1291 case State::RESIZING_PALETTE:
1292 if (m_hot.part == Hit::COLOR ||
1293 m_hot.part == Hit::POSSIBLE_COLOR ||
1294 m_hot.part == Hit::RESIZE_HANDLE) {
1295 const int newSize = std::max(1, m_hot.color);
1296
1297 m_adapter->showResizeInfoInStatusBar(statusBar, newSize);
1298 }
1299 else {
1300 statusBar->showDefaultText();
1301 }
1302 break;
1303 }
1304}
1305
1306doc::Palette* PaletteView::currentPalette() const
1307{
1308 return get_current_palette();
1309}
1310
1311int PaletteView::findExactIndex(const app::Color& color) const
1312{
1313 switch (color.getType()) {
1314
1315 case Color::MaskType: {
1316 if (current_editor && current_editor->sprite()->pixelFormat() == IMAGE_INDEXED)
1317 return current_editor->sprite()->transparentColor();
1318 return currentPalette()->findMaskColor();
1319 }
1320
1321 case Color::RgbType:
1322 case Color::HsvType:
1323 case Color::HslType:
1324 case Color::GrayType:
1325 return currentPalette()->findExactMatch(
1326 color.getRed(), color.getGreen(), color.getBlue(), color.getAlpha(), -1);
1327
1328 case Color::IndexType:
1329 return color.getIndex();
1330 }
1331
1332 ASSERT(false);
1333 return -1;
1334}
1335
1336void PaletteView::setNewPalette(doc::Palette* oldPalette,
1337 doc::Palette* newPalette,
1338 PaletteViewModification mod)
1339{
1340 // No differences
1341 if (!newPalette->countDiff(oldPalette, nullptr, nullptr))
1342 return;
1343
1344 if (m_delegate) {
1345 m_delegate->onPaletteViewModification(newPalette, mod);
1346 if (m_currentEntry >= 0)
1347 m_delegate->onPaletteViewIndexChange(m_currentEntry, ui::kButtonLeft);
1348 }
1349
1350 set_current_palette(newPalette, false);
1351 manager()->invalidate();
1352}
1353
1354int PaletteView::boxSizePx() const
1355{
1356 return m_boxsize*guiscale()
1357 + (m_withSeparator ? 0: childSpacing());
1358}
1359
1360void PaletteView::updateBorderAndChildSpacing()
1361{
1362 auto theme = SkinTheme::get(this);
1363 const int dim = theme->dimensions.paletteEntriesSeparator();
1364 setBorder(gfx::Border(dim));
1365 setChildSpacing(m_withSeparator ? dim: 0);
1366
1367 View* view = View::getView(this);
1368 if (view)
1369 view->updateView();
1370
1371 invalidate();
1372}
1373
1374} // namespace app
1375