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/context_bar.h"
13
14#include "app/app.h"
15#include "app/app_brushes.h"
16#include "app/app_menus.h"
17#include "app/color_utils.h"
18#include "app/commands/commands.h"
19#include "app/commands/quick_command.h"
20#include "app/doc.h"
21#include "app/doc_event.h"
22#include "app/i18n/strings.h"
23#include "app/ini_file.h"
24#include "app/match_words.h"
25#include "app/modules/editors.h"
26#include "app/pref/preferences.h"
27#include "app/shade.h"
28#include "app/site.h"
29#include "app/tools/active_tool.h"
30#include "app/tools/controller.h"
31#include "app/tools/ink.h"
32#include "app/tools/ink_type.h"
33#include "app/tools/point_shape.h"
34#include "app/tools/tool.h"
35#include "app/tools/tool_box.h"
36#include "app/tools/tool_loop_modifiers.h"
37#include "app/ui/brush_popup.h"
38#include "app/ui/button_set.h"
39#include "app/ui/color_button.h"
40#include "app/ui/color_shades.h"
41#include "app/ui/dithering_selector.h"
42#include "app/ui/dynamics_popup.h"
43#include "app/ui/editor/editor.h"
44#include "app/ui/expr_entry.h"
45#include "app/ui/icon_button.h"
46#include "app/ui/keyboard_shortcuts.h"
47#include "app/ui/sampling_selector.h"
48#include "app/ui/selection_mode_field.h"
49#include "app/ui/skin/skin_theme.h"
50#include "app/ui_context.h"
51#include "base/fs.h"
52#include "base/pi.h"
53#include "base/scoped_value.h"
54#include "doc/brush.h"
55#include "doc/image.h"
56#include "doc/palette.h"
57#include "doc/remap.h"
58#include "doc/selected_objects.h"
59#include "doc/slice.h"
60#include "fmt/format.h"
61#include "obs/connection.h"
62#include "os/sampling.h"
63#include "os/surface.h"
64#include "os/system.h"
65#include "render/dithering.h"
66#include "render/error_diffusion.h"
67#include "render/ordered_dither.h"
68#include "ui/button.h"
69#include "ui/combobox.h"
70#include "ui/fit_bounds.h"
71#include "ui/int_entry.h"
72#include "ui/label.h"
73#include "ui/listbox.h"
74#include "ui/listitem.h"
75#include "ui/menu.h"
76#include "ui/message.h"
77#include "ui/paint_event.h"
78#include "ui/popup_window.h"
79#include "ui/size_hint_event.h"
80#include "ui/system.h"
81#include "ui/theme.h"
82#include "ui/tooltips.h"
83
84#include <algorithm>
85
86namespace app {
87
88using namespace app::skin;
89using namespace gfx;
90using namespace ui;
91using namespace tools;
92
93static bool g_updatingFromCode = false;
94
95class ContextBar::ZoomButtons : public ButtonSet {
96public:
97 ZoomButtons()
98 : ButtonSet(3) {
99 addItem("100%");
100 addItem(Strings::context_bar_center());
101 addItem(Strings::context_bar_fit_screen());
102 }
103
104private:
105 void onItemChange(Item* item) override {
106 ButtonSet::onItemChange(item);
107
108 Command* cmd = nullptr;
109 Params params;
110
111 switch (selectedItem()) {
112
113 case 0: {
114 cmd = Commands::instance()->byId(CommandId::Zoom());
115 params.set("action", "set");
116 params.set("percentage", "100");
117 params.set("focus", "center");
118 UIContext::instance()->executeCommand(cmd, params);
119 break;
120 }
121
122 case 1: {
123 cmd = Commands::instance()->byId(CommandId::ScrollCenter());
124 break;
125 }
126
127 case 2: {
128 cmd = Commands::instance()->byId(CommandId::FitScreen());
129 break;
130 }
131 }
132
133 if (cmd)
134 UIContext::instance()->executeCommand(cmd, params);
135
136 deselectItems();
137 manager()->freeFocus();
138 }
139};
140
141class ContextBar::BrushBackField : public ButtonSet {
142public:
143 BrushBackField()
144 : ButtonSet(1) {
145 addItem(Strings::context_bar_back());
146 }
147
148protected:
149 void onItemChange(Item* item) override {
150 ButtonSet::onItemChange(item);
151
152 Command* discardBrush = Commands::instance()
153 ->byId(CommandId::DiscardBrush());
154 UIContext::instance()->executeCommand(discardBrush);
155 }
156};
157
158class ContextBar::BrushTypeField : public ButtonSet {
159public:
160 BrushTypeField(ContextBar* owner)
161 : ButtonSet(1)
162 , m_owner(owner)
163 , m_brushes(App::instance()->brushes()) {
164 SkinPartPtr part(new SkinPart);
165 part->setBitmap(0, BrushPopup::createSurfaceForBrush(BrushRef(nullptr)));
166 addItem(part);
167
168 m_popupWindow.Open.connect(
169 [this]{
170 gfx::Region rgn(m_popupWindow.boundsOnScreen());
171 rgn |= gfx::Region(this->boundsOnScreen());
172 m_popupWindow.setHotRegion(rgn);
173 });
174 }
175
176 ~BrushTypeField() {
177 closePopup();
178 }
179
180 void updateBrush(tools::Tool* tool = nullptr) {
181 BrushRef brush = m_owner->activeBrush(tool);
182 SkinPartPtr part(new SkinPart);
183 part->setBitmap(0, BrushPopup::createSurfaceForBrush(brush));
184
185 const bool mono = (brush->type() != kImageBrushType);
186 getItem(0)->setIcon(part, mono);
187 }
188
189 void switchPopup() {
190 if (!m_popupWindow.isVisible())
191 openPopup();
192 else
193 closePopup();
194 }
195
196 void showPopupAndHighlightSlot(int slot) {
197 // TODO use slot?
198 openPopup();
199 }
200
201protected:
202 void onItemChange(Item* item) override {
203 ButtonSet::onItemChange(item);
204 switchPopup();
205 }
206
207 void onSizeHint(SizeHintEvent& ev) override {
208 auto theme = SkinTheme::get(this);
209 ev.setSizeHint(Size(theme->dimensions.brushTypeWidth(),
210 theme->dimensions.contextBarHeight()));
211 }
212
213 void onInitTheme(InitThemeEvent& ev) override {
214 ButtonSet::onInitTheme(ev);
215 m_popupWindow.initTheme();
216 }
217
218private:
219 // Returns a little rectangle that can be used by the popup as the
220 // first brush position.
221 gfx::Point popupPosCandidate() const {
222 Rect rc = bounds();
223 rc.y += rc.h - 2*guiscale();
224 return rc.origin();
225 }
226
227 void openPopup() {
228 doc::BrushRef brush = m_owner->activeBrush();
229
230 m_popupWindow.regenerate(display(), popupPosCandidate());
231 m_popupWindow.setBrush(brush.get());
232 m_popupWindow.openWindow();
233 }
234
235 void closePopup() {
236 m_popupWindow.closeWindow(NULL);
237 deselectItems();
238 }
239
240 ContextBar* m_owner;
241 AppBrushes& m_brushes;
242 BrushPopup m_popupWindow;
243};
244
245class ContextBar::BrushSizeField : public IntEntry {
246public:
247 BrushSizeField() : IntEntry(Brush::kMinBrushSize, Brush::kMaxBrushSize) {
248 setSuffix("px");
249 }
250
251private:
252 void onValueChange() override {
253 if (g_updatingFromCode)
254 return;
255
256 IntEntry::onValueChange();
257 base::ScopedValue<bool> lockFlag(g_updatingFromCode, true, g_updatingFromCode);
258
259 Tool* tool = App::instance()->activeTool();
260 Preferences::instance().tool(tool).brush.size(getValue());
261 }
262};
263
264class ContextBar::BrushAngleField : public IntEntry {
265public:
266 BrushAngleField(BrushTypeField* brushType)
267 : IntEntry(-180, 180)
268 , m_brushType(brushType) {
269 setSuffix("\xc2\xb0");
270 }
271
272protected:
273 void onValueChange() override {
274 if (g_updatingFromCode)
275 return;
276
277 IntEntry::onValueChange();
278 base::ScopedValue<bool> lockFlag(g_updatingFromCode, true, g_updatingFromCode);
279
280 Tool* tool = App::instance()->activeTool();
281 Preferences::instance().tool(tool).brush.angle(getValue());
282
283 m_brushType->updateBrush();
284 }
285
286private:
287 BrushTypeField* m_brushType;
288};
289
290class ContextBar::BrushPatternField : public ComboBox {
291public:
292 BrushPatternField() {
293 // We use "m_lock" variable to avoid setting the pattern
294 // brush when we call ComboBox::addItem() (because the first
295 // addItem() generates an onChange() event).
296 m_lock = true;
297 addItem(Strings::context_bar_pattern_aligned_to_src());
298 addItem(Strings::context_bar_pattern_aligned_to_dest());
299 addItem(Strings::context_bar_paint_brush());
300 m_lock = false;
301
302 setSelectedItemIndex((int)Preferences::instance().brush.pattern());
303 }
304
305 void setBrushPattern(BrushPattern type) {
306 int index = 0;
307
308 switch (type) {
309 case BrushPattern::ALIGNED_TO_SRC: index = 0; break;
310 case BrushPattern::ALIGNED_TO_DST: index = 1; break;
311 case BrushPattern::PAINT_BRUSH: index = 2; break;
312 }
313
314 m_lock = true;
315 setSelectedItemIndex(index);
316 m_lock = false;
317 }
318
319protected:
320 void onChange() override {
321 ComboBox::onChange();
322
323 if (m_lock)
324 return;
325
326 BrushPattern type = BrushPattern::ALIGNED_TO_SRC;
327
328 switch (getSelectedItemIndex()) {
329 case 0: type = BrushPattern::ALIGNED_TO_SRC; break;
330 case 1: type = BrushPattern::ALIGNED_TO_DST; break;
331 case 2: type = BrushPattern::PAINT_BRUSH; break;
332 }
333
334 Preferences::instance().brush.pattern(type);
335 }
336
337 bool m_lock;
338};
339
340class ContextBar::ToleranceField : public IntEntry {
341public:
342 ToleranceField() : IntEntry(0, 255) {
343 }
344
345protected:
346 void onValueChange() override {
347 if (g_updatingFromCode)
348 return;
349
350 IntEntry::onValueChange();
351
352 Tool* tool = App::instance()->activeTool();
353 Preferences::instance().tool(tool).tolerance(getValue());
354 }
355};
356
357class ContextBar::ContiguousField : public CheckBox {
358public:
359 ContiguousField()
360 : CheckBox(Strings::context_bar_contiguous()) {
361 initTheme();
362 }
363
364protected:
365 void onInitTheme(InitThemeEvent& ev) override {
366 CheckBox::onInitTheme(ev);
367 setStyle(SkinTheme::get(this)->styles.miniCheckBox());
368 }
369
370 void onClick(Event& ev) override {
371 CheckBox::onClick(ev);
372
373 Tool* tool = App::instance()->activeTool();
374 Preferences::instance().tool(tool).contiguous(isSelected());
375
376 releaseFocus();
377 }
378};
379
380class ContextBar::PaintBucketSettingsField : public ButtonSet {
381public:
382 PaintBucketSettingsField() : ButtonSet(1) {
383 auto theme = SkinTheme::get(this);
384 addItem(theme->parts.timelineGear());
385 }
386
387protected:
388 void onItemChange(Item* item) override {
389 ButtonSet::onItemChange(item);
390 const gfx::Rect bounds = this->bounds();
391
392 Tool* tool = App::instance()->activeTool();
393 auto& toolPref = Preferences::instance().tool(tool);
394
395 Menu menu;
396 MenuItem
397 stopAtGrid(Strings::context_bar_stop_at_grid()),
398 activeLayer(Strings::context_bar_refer_active_layer()),
399 allLayers(Strings::context_bar_refer_visible_layer());
400 menu.addChild(&stopAtGrid);
401 menu.addChild(new MenuSeparator());
402 menu.addChild(&activeLayer);
403 menu.addChild(&allLayers);
404
405 menu.addChild(new MenuSeparator);
406 menu.addChild(new Label(Strings::context_bar_pixel_connectivity()));
407
408 HBox box;
409 ButtonSet buttonset(2);
410 buttonset.addItem("4-Connected");
411 buttonset.addItem("8-connected");
412 box.addChild(&buttonset);
413 menu.addChild(&box);
414
415 stopAtGrid.setSelected(
416 toolPref.floodfill.stopAtGrid() == app::gen::StopAtGrid::IF_VISIBLE);
417 activeLayer.setSelected(
418 toolPref.floodfill.referTo() == app::gen::FillReferTo::ACTIVE_LAYER);
419 allLayers.setSelected(
420 toolPref.floodfill.referTo() == app::gen::FillReferTo::ALL_LAYERS);
421
422 int index = 0;
423
424 switch (toolPref.floodfill.pixelConnectivity()) {
425 case app::gen::PixelConnectivity::FOUR_CONNECTED: index = 0; break;
426 case app::gen::PixelConnectivity::EIGHT_CONNECTED: index = 1; break;
427 }
428
429 buttonset.setSelectedItem(index);
430
431 stopAtGrid.Click.connect(
432 [&]{
433 toolPref.floodfill.stopAtGrid(
434 toolPref.floodfill.stopAtGrid() == app::gen::StopAtGrid::IF_VISIBLE ?
435 app::gen::StopAtGrid::NEVER: app::gen::StopAtGrid::IF_VISIBLE);
436 });
437 activeLayer.Click.connect(
438 [&]{
439 toolPref.floodfill.referTo(app::gen::FillReferTo::ACTIVE_LAYER);
440 });
441 allLayers.Click.connect(
442 [&]{
443 toolPref.floodfill.referTo(app::gen::FillReferTo::ALL_LAYERS);
444 });
445
446 buttonset.ItemChange.connect(
447 [&buttonset, &toolPref](ButtonSet::Item* item){
448 switch (buttonset.selectedItem()) {
449 case 0:
450 toolPref.floodfill.pixelConnectivity(app::gen::PixelConnectivity::FOUR_CONNECTED);
451 break;
452 case 1:
453 toolPref.floodfill.pixelConnectivity(app::gen::PixelConnectivity::EIGHT_CONNECTED);
454 break;
455 }
456 });
457
458 menu.showPopup(gfx::Point(bounds.x, bounds.y2()),
459 display());
460 deselectItems();
461 }
462
463};
464
465class ContextBar::InkTypeField : public ButtonSet {
466public:
467 InkTypeField(ContextBar* owner) : ButtonSet(1)
468 , m_owner(owner) {
469 auto theme = SkinTheme::get(this);
470 addItem(theme->parts.inkSimple());
471 }
472
473 void setInkType(InkType inkType) {
474 Preferences& pref = Preferences::instance();
475
476 if (pref.shared.shareInk()) {
477 for (Tool* tool : *App::instance()->toolBox())
478 pref.tool(tool).ink(inkType);
479 }
480 else {
481 Tool* tool = App::instance()->activeTool();
482 pref.tool(tool).ink(inkType);
483 }
484
485 m_owner->updateForActiveTool();
486 }
487
488 void setInkTypeIcon(InkType inkType) {
489 auto theme = SkinTheme::get(this);
490 SkinPartPtr part = theme->parts.inkSimple();
491
492 switch (inkType) {
493 case InkType::SIMPLE: part = theme->parts.inkSimple(); break;
494 case InkType::ALPHA_COMPOSITING: part = theme->parts.inkAlphaCompositing(); break;
495 case InkType::COPY_COLOR: part = theme->parts.inkCopyColor(); break;
496 case InkType::LOCK_ALPHA: part = theme->parts.inkLockAlpha(); break;
497 case InkType::SHADING: part = theme->parts.inkShading(); break;
498 }
499
500 getItem(0)->setIcon(part);
501 }
502
503protected:
504 void onItemChange(Item* item) override {
505 ButtonSet::onItemChange(item);
506
507 gfx::Rect bounds = this->bounds();
508
509 AppMenus::instance()
510 ->getInkPopupMenu()
511 ->showPopup(gfx::Point(bounds.x, bounds.y2()),
512 display());
513
514 deselectItems();
515 }
516
517 ContextBar* m_owner;
518};
519
520class ContextBar::InkShadesField : public HBox {
521public:
522 InkShadesField(ColorBar* colorBar)
523 : m_button(SkinTheme::get(this)->parts.iconArrowDown())
524 , m_shade(Shade(), ColorShades::DragAndDropEntries)
525 , m_loaded(false) {
526 addChild(&m_button);
527 addChild(&m_shade);
528
529 m_shade.setText(Strings::context_bar_select_palette_color());
530 m_shade.setMinColors(2);
531 m_conn = colorBar->ChangeSelection.connect(
532 [this]{ onChangeColorBarSelection(); });
533
534 m_button.setFocusStop(false);
535 m_button.Click.connect([this]{ onShowMenu(); });
536
537 initTheme();
538 }
539
540 ~InkShadesField() {
541 saveShades();
542 }
543
544 void reverseShadeColors() {
545 m_shade.reverseShadeColors();
546 }
547
548 doc::Remap* createShadeRemap(bool left) {
549 return m_shade.createShadeRemap(left);
550 }
551
552 Shade getShade() const {
553 return m_shade.getShade();
554 }
555
556 void setShade(const Shade& shade) {
557 m_shade.setShade(shade);
558 }
559
560 void updateShadeFromColorBarPicks() {
561 auto colorBar = ColorBar::instance();
562 if (!colorBar)
563 return;
564
565 doc::PalettePicks picks;
566 colorBar->getPaletteView()->getSelectedEntries(picks);
567 if (picks.picks() >= 2)
568 onChangeColorBarSelection();
569 }
570
571private:
572 void onInitTheme(InitThemeEvent& ev) override {
573 HBox::onInitTheme(ev);
574 auto theme = SkinTheme::get(this);
575 noBorderNoChildSpacing();
576 m_shade.setStyle(theme->styles.topShadeView());
577 m_button.setBgColor(theme->colors.workspace());
578 }
579
580 void onShowMenu() {
581 loadShades();
582 gfx::Rect bounds = m_button.bounds();
583
584 Menu menu;
585 MenuItem
586 reverse(Strings::context_bar_reverse_shade()),
587 save(Strings::context_bar_save_shade());
588 menu.addChild(&reverse);
589 menu.addChild(&save);
590
591 bool hasShade = (m_shade.size() >= 2);
592 reverse.setEnabled(hasShade);
593 save.setEnabled(hasShade);
594 reverse.Click.connect([this]{ reverseShadeColors(); });
595 save.Click.connect([this]{ onSaveShade(); });
596
597 if (!m_shades.empty()) {
598 auto theme = SkinTheme::get(this);
599
600 menu.addChild(new MenuSeparator);
601
602 int i = 0;
603 for (const Shade& shade : m_shades) {
604 auto shadeWidget = new ColorShades(shade, ColorShades::ClickWholeShade);
605 shadeWidget->setExpansive(true);
606 shadeWidget->Click.connect(
607 [&](ColorShades::ClickEvent&){
608 m_shade.setShade(shade);
609 });
610
611 auto close = new IconButton(theme->parts.iconClose());
612 close->InitTheme.connect(
613 [close]{
614 close->setBgColor(
615 SkinTheme::get(close)->colors.menuitemNormalFace());
616 });
617 close->initTheme();
618 close->Click.connect(
619 [this, i, close]{
620 m_shades.erase(m_shades.begin()+i);
621 close->closeWindow();
622 });
623
624 auto item = new HBox();
625 item->InitTheme.connect(
626 [item]{
627 item->noBorderNoChildSpacing();
628 });
629 item->initTheme();
630 item->addChild(shadeWidget);
631 item->addChild(close);
632 menu.addChild(item);
633 ++i;
634 }
635 }
636
637 menu.showPopup(gfx::Point(bounds.x, bounds.y2()),
638 display());
639 m_button.invalidate();
640 }
641
642 void onSaveShade() {
643 loadShades();
644 m_shades.push_back(m_shade.getShade());
645 }
646
647 void loadShades() {
648 if (m_loaded)
649 return;
650
651 m_loaded = true;
652
653 char buf[32];
654 int n = get_config_int("shades", "count", 0);
655 n = std::clamp(n, 0, 256);
656 for (int i=0; i<n; ++i) {
657 sprintf(buf, "shade%d", i);
658 Shade shade = shade_from_string(get_config_string("shades", buf, ""));
659 if (shade.size() >= 2)
660 m_shades.push_back(shade);
661 }
662 }
663
664 void saveShades() {
665 if (!m_loaded)
666 return;
667
668 char buf[32];
669 int n = int(m_shades.size());
670 set_config_int("shades", "count", n);
671 for (int i=0; i<n; ++i) {
672 sprintf(buf, "shade%d", i);
673 set_config_string("shades", buf, shade_to_string(m_shades[i]).c_str());
674 }
675 }
676
677 void onChangeColorBarSelection() {
678 if (!m_shade.isVisible())
679 return;
680
681 doc::PalettePicks picks;
682 ColorBar::instance()->getPaletteView()->getSelectedEntries(picks);
683
684 Shade newShade = m_shade.getShade();
685 newShade.resize(picks.picks());
686
687 int i = 0, j = 0;
688 for (bool pick : picks) {
689 if (pick)
690 newShade[j++] = app::Color::fromIndex(i);
691 ++i;
692 }
693
694 m_shade.setShade(newShade);
695
696 layout();
697 }
698
699 IconButton m_button;
700 ColorShades m_shade;
701 std::vector<Shade> m_shades;
702 bool m_loaded;
703 obs::scoped_connection m_conn;
704};
705
706class ContextBar::InkOpacityField : public IntEntry {
707public:
708 InkOpacityField() : IntEntry(0, 255) {
709 }
710
711protected:
712 void onValueChange() override {
713 if (g_updatingFromCode)
714 return;
715
716 IntEntry::onValueChange();
717 base::ScopedValue<bool> lockFlag(g_updatingFromCode, true, g_updatingFromCode);
718
719 int newValue = getValue();
720 Preferences& pref = Preferences::instance();
721 if (pref.shared.shareInk()) {
722 for (Tool* tool : *App::instance()->toolBox())
723 pref.tool(tool).opacity(newValue);
724 }
725 else {
726 Tool* tool = App::instance()->activeTool();
727 pref.tool(tool).opacity(newValue);
728 }
729 }
730};
731
732class ContextBar::SprayWidthField : public IntEntry {
733public:
734 SprayWidthField() : IntEntry(1, 32) {
735 }
736
737protected:
738 void onValueChange() override {
739 IntEntry::onValueChange();
740 if (g_updatingFromCode)
741 return;
742
743 Tool* tool = App::instance()->activeTool();
744 Preferences::instance().tool(tool).spray.width(getValue());
745 }
746};
747
748class ContextBar::SpraySpeedField : public IntEntry {
749public:
750 SpraySpeedField() : IntEntry(1, 100) {
751 }
752
753protected:
754 void onValueChange() override {
755 if (g_updatingFromCode)
756 return;
757
758 IntEntry::onValueChange();
759
760 Tool* tool = App::instance()->activeTool();
761 Preferences::instance().tool(tool).spray.speed(getValue());
762 }
763};
764
765class ContextBar::TransparentColorField : public HBox {
766public:
767 TransparentColorField(ContextBar* owner,
768 TooltipManager* tooltipManager)
769 : m_icon(1)
770 , m_maskColor(app::Color::fromMask(), IMAGE_RGB, ColorButtonOptions())
771 , m_owner(owner) {
772 auto theme = SkinTheme::get(this);
773
774 addChild(&m_icon);
775 addChild(&m_maskColor);
776
777 m_icon.addItem(theme->parts.selectionOpaque());
778 gfx::Size sz = m_icon.getItem(0)->sizeHint();
779 sz.w += 2*guiscale();
780 m_icon.getItem(0)->setMinSize(sz);
781
782 m_icon.ItemChange.connect([this]{ onPopup(); });
783 m_maskColor.Change.connect([this]{ onChangeColor(); });
784
785 Preferences::instance().selection.opaque.AfterChange.connect(
786 [this]{ onOpaqueChange(); });
787
788 onOpaqueChange();
789
790 tooltipManager->addTooltipFor(
791 m_icon.at(0), Strings::context_bar_transparent_color_options(), BOTTOM);
792 tooltipManager->addTooltipFor(
793 &m_maskColor, Strings::context_bar_transparent_color(), BOTTOM);
794 }
795
796private:
797
798 void onPopup() {
799 gfx::Rect bounds = this->bounds();
800
801 Menu menu;
802 MenuItem
803 opaque(Strings::context_bar_opaque()),
804 masked(Strings::context_bar_transparent()),
805 automatic(Strings::context_bar_auto_adjust_layer());
806 menu.addChild(&opaque);
807 menu.addChild(&masked);
808 menu.addChild(new MenuSeparator);
809 menu.addChild(&automatic);
810
811 if (Preferences::instance().selection.opaque())
812 opaque.setSelected(true);
813 else
814 masked.setSelected(true);
815 automatic.setSelected(Preferences::instance().selection.autoOpaque());
816
817 opaque.Click.connect([this]{ setOpaque(true); });
818 masked.Click.connect([this]{ setOpaque(false); });
819 automatic.Click.connect([this]{ onAutomatic(); });
820
821 menu.showPopup(gfx::Point(bounds.x, bounds.y2()),
822 display());
823 }
824
825 void onChangeColor() {
826 Preferences::instance().selection.transparentColor(
827 m_maskColor.getColor());
828 }
829
830 void setOpaque(bool opaque) {
831 Preferences::instance().selection.opaque(opaque);
832 }
833
834 // When the preference is changed from outside the context bar
835 void onOpaqueChange() {
836 bool opaque = Preferences::instance().selection.opaque();
837
838 auto theme = SkinTheme::get(this);
839 SkinPartPtr part = (opaque ? theme->parts.selectionOpaque():
840 theme->parts.selectionMasked());
841 m_icon.getItem(0)->setIcon(part);
842
843 m_maskColor.setVisible(!opaque);
844 if (!opaque) {
845 Preferences::instance().selection.transparentColor(
846 m_maskColor.getColor());
847 }
848
849 if (m_owner)
850 m_owner->layout();
851 }
852
853 void onAutomatic() {
854 Preferences::instance().selection.autoOpaque(
855 !Preferences::instance().selection.autoOpaque());
856 }
857
858 ButtonSet m_icon;
859 ColorButton m_maskColor;
860 ContextBar* m_owner;
861};
862
863class ContextBar::PivotField : public ButtonSet {
864public:
865 PivotField()
866 : ButtonSet(1) {
867 addItem(SkinTheme::get(this)->parts.pivotCenter());
868
869 m_pivotConn = Preferences::instance().selection.pivotPosition.AfterChange.connect(
870 [this]{ onPivotChange(); });
871
872 onPivotChange();
873 }
874
875private:
876
877 void onItemChange(Item* item) override {
878 ButtonSet::onItemChange(item);
879
880 auto theme = SkinTheme::get(this);
881 gfx::Rect bounds = this->bounds();
882
883 Menu menu;
884 CheckBox visible(Strings::context_bar_default_display_pivot());
885 HBox box;
886 ButtonSet buttonset(3);
887 buttonset.addItem(theme->parts.pivotNorthwest());
888 buttonset.addItem(theme->parts.pivotNorth());
889 buttonset.addItem(theme->parts.pivotNortheast());
890 buttonset.addItem(theme->parts.pivotWest());
891 buttonset.addItem(theme->parts.pivotCenter());
892 buttonset.addItem(theme->parts.pivotEast());
893 buttonset.addItem(theme->parts.pivotSouthwest());
894 buttonset.addItem(theme->parts.pivotSouth());
895 buttonset.addItem(theme->parts.pivotSoutheast());
896 box.addChild(&buttonset);
897
898 menu.addChild(&visible);
899 menu.addChild(new MenuSeparator);
900 menu.addChild(&box);
901
902 bool isVisible = Preferences::instance().selection.pivotVisibility();
903 app::gen::PivotPosition pos = Preferences::instance().selection.pivotPosition();
904 visible.setSelected(isVisible);
905 buttonset.setSelectedItem(int(pos));
906
907 visible.Click.connect(
908 [&visible](Event&){
909 Preferences::instance().selection.pivotVisibility(
910 visible.isSelected());
911 });
912
913 buttonset.ItemChange.connect(
914 [&buttonset](ButtonSet::Item* item){
915 Preferences::instance().selection.pivotPosition(
916 app::gen::PivotPosition(buttonset.selectedItem()));
917 });
918
919 menu.showPopup(gfx::Point(bounds.x, bounds.y2()),
920 display());
921 }
922
923 void onPivotChange() {
924 auto theme = SkinTheme::get(this);
925 SkinPartPtr part;
926 switch (Preferences::instance().selection.pivotPosition()) {
927 case app::gen::PivotPosition::NORTHWEST: part = theme->parts.pivotNorthwest(); break;
928 case app::gen::PivotPosition::NORTH: part = theme->parts.pivotNorth(); break;
929 case app::gen::PivotPosition::NORTHEAST: part = theme->parts.pivotNortheast(); break;
930 case app::gen::PivotPosition::WEST: part = theme->parts.pivotWest(); break;
931 case app::gen::PivotPosition::CENTER: part = theme->parts.pivotCenter(); break;
932 case app::gen::PivotPosition::EAST: part = theme->parts.pivotEast(); break;
933 case app::gen::PivotPosition::SOUTHWEST: part = theme->parts.pivotSouthwest(); break;
934 case app::gen::PivotPosition::SOUTH: part = theme->parts.pivotSouth(); break;
935 case app::gen::PivotPosition::SOUTHEAST: part = theme->parts.pivotSoutheast(); break;
936 }
937 if (part)
938 getItem(0)->setIcon(part);
939 }
940
941 obs::scoped_connection m_pivotConn;
942};
943
944class ContextBar::RotAlgorithmField : public ComboBox {
945public:
946 RotAlgorithmField() {
947 // We use "m_lockChange" variable to avoid setting the rotation
948 // algorithm when we call ComboBox::addItem() (because the first
949 // addItem() generates an onChange() event).
950 m_lockChange = true;
951 addItem(
952 new Item(Strings::context_bar_fast_rotation(), tools::RotationAlgorithm::FAST));
953 addItem(
954 new Item(Strings::context_bar_rotsprite(), tools::RotationAlgorithm::ROTSPRITE));
955 m_lockChange = false;
956
957 setSelectedItemIndex((int)Preferences::instance().selection.rotationAlgorithm());
958 }
959
960protected:
961 void onChange() override {
962 if (m_lockChange)
963 return;
964
965 Preferences::instance().selection.rotationAlgorithm(
966 static_cast<Item*>(getSelectedItem())->algo());
967 }
968
969 void onCloseListBox() override {
970 releaseFocus();
971 }
972
973private:
974 class Item : public ListItem {
975 public:
976 Item(const std::string& text, tools::RotationAlgorithm algo) :
977 ListItem(text),
978 m_algo(algo) {
979 }
980
981 tools::RotationAlgorithm algo() const { return m_algo; }
982
983 private:
984 tools::RotationAlgorithm m_algo;
985 };
986
987 bool m_lockChange;
988};
989
990class ContextBar::TransformationFields : public HBox {
991public:
992 class CustomEntry : public ExprEntry {
993 public:
994 CustomEntry() {
995 setDecimals(1);
996 }
997 private:
998 bool onProcessMessage(Message* msg) override {
999 switch (msg->type()) {
1000
1001 case kKeyDownMessage:
1002 if (ExprEntry::onProcessMessage(msg))
1003 return true;
1004 else if (hasFocus() && manager()->processFocusMovementMessage(msg))
1005 return true;
1006 return false;
1007
1008 case kFocusLeaveMessage: {
1009 bool res = ExprEntry::onProcessMessage(msg);
1010 deselectText();
1011 setCaretPos(0);
1012 return res;
1013 }
1014 }
1015 return ExprEntry::onProcessMessage(msg);
1016 }
1017 void onVisible(bool visible) override {
1018 if (!visible && hasFocus()) {
1019 releaseFocus();
1020 }
1021 ExprEntry::onVisible(visible);
1022 }
1023 void onFormatExprFocusLeave(std::string& buf) override {
1024 buf = formatDec(onGetTextDouble());
1025 }
1026 };
1027
1028 TransformationFields() {
1029 m_angle.setSuffix("°");
1030 m_skew.setSuffix("°");
1031
1032 addChild(new Label("P:"));
1033 addChild(&m_x);
1034 addChild(&m_y);
1035 addChild(&m_w);
1036 addChild(&m_h);
1037 addChild(new Label("R:"));
1038 addChild(&m_angle);
1039 addChild(&m_skew);
1040
1041 InitTheme.connect(
1042 [this]{
1043 gfx::Size sz(
1044 font()->textLength("8")*4 + m_x.border().width(),
1045 std::numeric_limits<int>::max());
1046 setChildSpacing(0);
1047 m_x.setMaxSize(sz);
1048 m_y.setMaxSize(sz);
1049 m_w.setMaxSize(sz);
1050 m_h.setMaxSize(sz);
1051 m_angle.setMaxSize(sz);
1052 m_skew.setMaxSize(sz);
1053 });
1054 initTheme();
1055
1056 m_x.Change.connect([this]{ auto rc = bounds(); rc.x = m_x.textDouble(); onChangePos(rc); });
1057 m_y.Change.connect([this]{ auto rc = bounds(); rc.y = m_y.textDouble(); onChangePos(rc); });
1058 m_w.Change.connect([this]{ auto rc = bounds(); rc.w = m_w.textDouble(); onChangeSize(rc); });
1059 m_h.Change.connect([this]{ auto rc = bounds(); rc.h = m_h.textDouble(); onChangeSize(rc); });
1060 m_angle.Change.connect([this]{ onChangeAngle(); });
1061 m_skew.Change.connect([this]{ onChangeSkew(); });
1062 }
1063
1064 void update(const Transformation& t) {
1065 auto rc = t.bounds();
1066
1067 m_x.setText(formatDec(rc.x));
1068 m_y.setText(formatDec(rc.y));
1069 m_w.setText(formatDec(rc.w));
1070 m_h.setText(formatDec(rc.h));
1071 m_angle.setText(formatDec(180.0 * t.angle() / PI));
1072 m_skew.setText(formatDec(180.0 * t.skew() / PI));
1073
1074 m_t = t;
1075 }
1076
1077private:
1078 static std::string formatDec(const double x) {
1079 std::string s = fmt::format("{:0.1f}", x);
1080 if (s.size() > 2 &&
1081 s[s.size()-1] == '0' &&
1082 s[s.size()-2] == '.') {
1083 s.erase(s.size()-2, 2);
1084 }
1085 return s;
1086 }
1087
1088 gfx::RectF bounds() const {
1089 return m_t.bounds();
1090 }
1091 void onChangePos(gfx::RectF newBounds) {
1092 // Adjust new pivot position depending on the new bounds origin
1093 gfx::RectF bounds = m_t.bounds();
1094 gfx::PointF pivot = m_t.pivot();
1095 if (!bounds.isEmpty()) {
1096 pivot.x = (pivot.x - bounds.x) / bounds.w;
1097 pivot.y = (pivot.y - bounds.y) / bounds.h;
1098 pivot.x = newBounds.x + pivot.x*newBounds.w;
1099 pivot.y = newBounds.y + pivot.y*newBounds.h;
1100 m_t.pivot(pivot);
1101 }
1102 m_t.bounds(newBounds);
1103 updateEditor();
1104 }
1105 void onChangeSize(gfx::RectF newBounds) {
1106 // Adjust bounds origin depending on the new size and the current pivot
1107 gfx::RectF bounds = m_t.bounds();
1108 gfx::PointF pivot = m_t.pivot();
1109 if (!bounds.isEmpty()) {
1110 pivot.x = (pivot.x - bounds.x) / bounds.w;
1111 pivot.y = (pivot.y - bounds.y) / bounds.h;
1112 newBounds.x -= (newBounds.w-bounds.w)*pivot.x;
1113 newBounds.y -= (newBounds.h-bounds.h)*pivot.y;
1114
1115 m_x.setText(formatDec(newBounds.x));
1116 m_y.setText(formatDec(newBounds.y));
1117 }
1118 m_t.bounds(newBounds);
1119 updateEditor();
1120 }
1121 void onChangeAngle() {
1122 m_t.angle(PI * m_angle.textDouble() / 180.0);
1123 updateEditor();
1124 }
1125 void onChangeSkew() {
1126 double newSkew = PI * m_skew.textDouble() / 180.0;
1127 newSkew = std::clamp(newSkew, -PI*85.0/180.0, PI*85.0/180.0);
1128 m_t.skew(newSkew);
1129 updateEditor();
1130 }
1131 void updateEditor() {
1132 if (current_editor)
1133 current_editor->updateTransformation(m_t);
1134 }
1135
1136 CustomEntry m_x, m_y, m_w, m_h;
1137 CustomEntry m_angle;
1138 CustomEntry m_skew;
1139 Transformation m_t;
1140};
1141
1142class ContextBar::DynamicsField : public ButtonSet
1143 , public DynamicsPopup::Delegate {
1144public:
1145 DynamicsField(ContextBar* ctxBar)
1146 : ButtonSet(1)
1147 , m_ctxBar(ctxBar) {
1148 addItem(SkinTheme::get(this)->parts.dynamics());
1149 }
1150
1151 void switchPopup() {
1152 if (m_popup &&
1153 m_popup->isVisible()) {
1154 m_popup->closeWindow(nullptr);
1155 return;
1156 }
1157
1158 if (!m_popup) {
1159 m_popup.reset(new DynamicsPopup(this));
1160 m_popup->setOptionsGridVisibility(m_optionsGridVisibility);
1161 m_popup->Close.connect(
1162 [this](CloseEvent&){
1163 deselectItems();
1164 m_dynamics = m_popup->getDynamics();
1165 });
1166 }
1167
1168 const gfx::Rect bounds = this->bounds();
1169 m_popup->remapWindow();
1170 fit_bounds(display(), m_popup.get(),
1171 gfx::Rect(gfx::Point(bounds.x, bounds.y2()),
1172 m_popup->sizeHint()));
1173
1174 m_popup->openWindow();
1175 m_popup->setHotRegion(gfx::Region(m_popup->boundsOnScreen()));
1176 }
1177
1178 const tools::DynamicsOptions& getDynamics() const {
1179 if (m_popup && m_popup->isVisible())
1180 m_dynamics = m_popup->getDynamics();
1181 return m_dynamics;
1182 }
1183
1184 void setOptionsGridVisibility(bool state) {
1185 m_optionsGridVisibility = state;
1186 if (m_popup)
1187 m_popup->setOptionsGridVisibility(state);
1188 }
1189
1190private:
1191 // DynamicsPopup::Delegate impl
1192 doc::BrushRef getActiveBrush() override {
1193 return m_ctxBar->activeBrush();
1194 }
1195
1196 void setMaxSize(int size) override {
1197 Tool* tool = App::instance()->activeTool();
1198 Preferences::instance().tool(tool).brush.size(size);
1199 }
1200
1201 void setMaxAngle(int angle) override {
1202 Tool* tool = App::instance()->activeTool();
1203 Preferences::instance().tool(tool).brush.angle(angle);
1204 }
1205
1206 // ButtonSet overrides
1207 void onItemChange(Item* item) override {
1208 ButtonSet::onItemChange(item);
1209 switchPopup();
1210 }
1211
1212 // Widget overrides
1213 void onInitTheme(InitThemeEvent& ev) override {
1214 ButtonSet::onInitTheme(ev);
1215 if (m_popup)
1216 m_popup->initTheme();
1217 }
1218
1219 std::unique_ptr<DynamicsPopup> m_popup;
1220 ContextBar* m_ctxBar;
1221 mutable tools::DynamicsOptions m_dynamics;
1222 bool m_optionsGridVisibility = true;
1223};
1224
1225class ContextBar::FreehandAlgorithmField : public CheckBox {
1226public:
1227 FreehandAlgorithmField()
1228 : CheckBox(Strings::context_bar_pixel_perfect())
1229 {
1230 initTheme();
1231 }
1232
1233 void setFreehandAlgorithm(tools::FreehandAlgorithm algo) {
1234 switch (algo) {
1235 case tools::FreehandAlgorithm::DEFAULT:
1236 setSelected(false);
1237 break;
1238 case tools::FreehandAlgorithm::PIXEL_PERFECT:
1239 setSelected(true);
1240 break;
1241 case tools::FreehandAlgorithm::DOTS:
1242 // Not available
1243 break;
1244 }
1245 }
1246
1247protected:
1248 void onInitTheme(InitThemeEvent& ev) override {
1249 CheckBox::onInitTheme(ev);
1250 setStyle(SkinTheme::get(this)->styles.miniCheckBox());
1251 }
1252
1253 void onClick(Event& ev) override {
1254 CheckBox::onClick(ev);
1255
1256 Tool* tool = App::instance()->activeTool();
1257 Preferences::instance().tool(tool).freehandAlgorithm(
1258 isSelected() ?
1259 tools::FreehandAlgorithm::PIXEL_PERFECT:
1260 tools::FreehandAlgorithm::DEFAULT);
1261
1262 releaseFocus();
1263 }
1264};
1265
1266class ContextBar::SelectionModeField : public app::SelectionModeField {
1267public:
1268 SelectionModeField() { }
1269protected:
1270 void onSelectionModeChange(gen::SelectionMode mode) override {
1271 Preferences::instance().selection.mode(mode);
1272 }
1273};
1274
1275class ContextBar::GradientTypeField : public ButtonSet {
1276public:
1277 GradientTypeField() : ButtonSet(2) {
1278 auto theme = SkinTheme::get(this);
1279
1280 addItem(theme->parts.linearGradient());
1281 addItem(theme->parts.radialGradient());
1282
1283 setSelectedItem(0);
1284 }
1285
1286 void setupTooltips(TooltipManager* tooltipManager) {
1287 tooltipManager->addTooltipFor(
1288 at(0), Strings::context_bar_linear_gradient(), BOTTOM);
1289 tooltipManager->addTooltipFor(
1290 at(1), Strings::context_bar_radial_gradient(), BOTTOM);
1291 }
1292
1293 render::GradientType gradientType() const {
1294 return (render::GradientType)selectedItem();
1295 }
1296};
1297
1298class ContextBar::DropPixelsField : public ButtonSet {
1299public:
1300 DropPixelsField() : ButtonSet(2) {
1301 auto theme = SkinTheme::get(this);
1302
1303 addItem(theme->parts.dropPixelsOk());
1304 addItem(theme->parts.dropPixelsCancel());
1305 setOfferCapture(false);
1306 }
1307
1308 void setupTooltips(TooltipManager* tooltipManager) {
1309 // TODO Enter and Esc should be configurable keys
1310 tooltipManager->addTooltipFor(at(0), Strings::context_bar_drop_pixel(), BOTTOM);
1311 tooltipManager->addTooltipFor(at(1), Strings::context_bar_cancel_drag(), BOTTOM);
1312 }
1313
1314 obs::signal<void(ContextBarObserver::DropAction)> DropPixels;
1315
1316protected:
1317 void onItemChange(Item* item) override {
1318 ButtonSet::onItemChange(item);
1319
1320 switch (selectedItem()) {
1321 case 0: DropPixels(ContextBarObserver::DropPixels); break;
1322 case 1: DropPixels(ContextBarObserver::CancelDrag); break;
1323 }
1324 }
1325};
1326
1327class ContextBar::EyedropperField : public HBox {
1328public:
1329 EyedropperField() {
1330 m_channel.addItem("Color+Alpha");
1331 m_channel.addItem("Color");
1332 m_channel.addItem("Alpha");
1333 m_channel.addItem("RGB+Alpha");
1334 m_channel.addItem("RGB");
1335 m_channel.addItem("HSV+Alpha");
1336 m_channel.addItem("HSV");
1337 m_channel.addItem("HSL+Alpha");
1338 m_channel.addItem("HSL");
1339 m_channel.addItem("Gray+Alpha");
1340 m_channel.addItem("Gray");
1341 m_channel.addItem(Strings::context_bar_best_fit_index());
1342
1343 m_sample.addItem(Strings::context_bar_all_layers());
1344 m_sample.addItem(Strings::context_bar_current_layer());
1345 m_sample.addItem(Strings::context_bar_first_ref_layer());
1346
1347 addChild(new Label(Strings::context_bar_pick()));
1348 addChild(&m_channel);
1349 addChild(new Label(Strings::context_bar_sample()));
1350 addChild(&m_sample);
1351
1352 m_channel.Change.connect([this]{ onChannelChange(); });
1353 m_sample.Change.connect([this]{ onSampleChange(); });
1354 }
1355
1356 void updateFromPreferences(app::Preferences::Eyedropper& prefEyedropper) {
1357 m_channel.setSelectedItemIndex((int)prefEyedropper.channel());
1358 m_sample.setSelectedItemIndex((int)prefEyedropper.sample());
1359 }
1360
1361private:
1362 void onChannelChange() {
1363 Preferences::instance().eyedropper.channel(
1364 (app::gen::EyedropperChannel)m_channel.getSelectedItemIndex());
1365 }
1366
1367 void onSampleChange() {
1368 Preferences::instance().eyedropper.sample(
1369 (app::gen::EyedropperSample)m_sample.getSelectedItemIndex());
1370 }
1371
1372 ComboBox m_channel;
1373 ComboBox m_sample;
1374};
1375
1376class ContextBar::AutoSelectLayerField : public CheckBox {
1377public:
1378 AutoSelectLayerField()
1379 : CheckBox(Strings::context_bar_auto_select_layer())
1380 {
1381 initTheme();
1382 }
1383
1384protected:
1385 void onInitTheme(InitThemeEvent& ev) override {
1386 CheckBox::onInitTheme(ev);
1387 setStyle(SkinTheme::get(this)->styles.miniCheckBox());
1388 }
1389
1390 void onClick(Event& ev) override {
1391 CheckBox::onClick(ev);
1392
1393 auto atm = App::instance()->activeToolManager();
1394 if (atm->quickTool() &&
1395 atm->quickTool()->getInk(0)->isCelMovement()) {
1396 Preferences::instance().editor.autoSelectLayerQuick(isSelected());
1397 }
1398 else {
1399 Preferences::instance().editor.autoSelectLayer(isSelected());
1400 }
1401
1402 releaseFocus();
1403 }
1404};
1405
1406class ContextBar::SymmetryField : public ButtonSet {
1407public:
1408 SymmetryField() : ButtonSet(3) {
1409 setMultiMode(MultiMode::Set);
1410 auto theme = SkinTheme::get(this);
1411 addItem(theme->parts.horizontalSymmetry());
1412 addItem(theme->parts.verticalSymmetry());
1413 addItem("...");
1414 }
1415
1416 void setupTooltips(TooltipManager* tooltipManager) {
1417 tooltipManager->addTooltipFor(at(0), Strings::symmetry_toggle_horizontal(), BOTTOM);
1418 tooltipManager->addTooltipFor(at(1), Strings::symmetry_toggle_vertical(), BOTTOM);
1419 tooltipManager->addTooltipFor(at(2), Strings::symmetry_show_options(), BOTTOM);
1420 }
1421
1422 void updateWithCurrentDocument() {
1423 Doc* doc = UIContext::instance()->activeDocument();
1424 if (!doc)
1425 return;
1426
1427 DocumentPreferences& docPref = Preferences::instance().document(doc);
1428
1429 at(0)->setSelected(int(docPref.symmetry.mode()) & int(app::gen::SymmetryMode::HORIZONTAL) ? true: false);
1430 at(1)->setSelected(int(docPref.symmetry.mode()) & int(app::gen::SymmetryMode::VERTICAL) ? true: false);
1431 }
1432
1433private:
1434 void onItemChange(Item* item) override {
1435 ButtonSet::onItemChange(item);
1436
1437 Doc* doc = UIContext::instance()->activeDocument();
1438 if (!doc)
1439 return;
1440
1441 DocumentPreferences& docPref =
1442 Preferences::instance().document(doc);
1443
1444 int mode = 0;
1445 if (at(0)->isSelected()) mode |= int(app::gen::SymmetryMode::HORIZONTAL);
1446 if (at(1)->isSelected()) mode |= int(app::gen::SymmetryMode::VERTICAL);
1447 if (app::gen::SymmetryMode(mode) != docPref.symmetry.mode()) {
1448 docPref.symmetry.mode(app::gen::SymmetryMode(mode));
1449 // Redraw symmetry rules
1450 doc->notifyGeneralUpdate();
1451 }
1452 else if (at(2)->isSelected()) {
1453 auto item = at(2);
1454
1455 gfx::Rect bounds = item->bounds();
1456 item->setSelected(false);
1457
1458 Menu menu;
1459 MenuItem
1460 reset(Strings::symmetry_reset_position());
1461 menu.addChild(&reset);
1462
1463 reset.Click.connect(
1464 [doc, &docPref]{
1465 docPref.symmetry.xAxis(doc->sprite()->width()/2.0);
1466 docPref.symmetry.yAxis(doc->sprite()->height()/2.0);
1467 // Redraw symmetry rules
1468 doc->notifyGeneralUpdate();
1469 });
1470
1471 menu.showPopup(gfx::Point(bounds.x, bounds.y2()),
1472 display());
1473 }
1474 }
1475};
1476
1477class ContextBar::SliceFields : public HBox {
1478 class Item : public ListItem {
1479 public:
1480 Item(const doc::Slice* slice)
1481 : ListItem(slice->name())
1482 , m_slice(slice) { }
1483 const doc::Slice* slice() const { return m_slice; }
1484 private:
1485 const doc::Slice* m_slice;
1486 };
1487
1488 class Combo : public ComboBox {
1489 SliceFields* m_sliceFields;
1490 public:
1491 Combo(SliceFields* sliceFields)
1492 : m_sliceFields(sliceFields) {
1493 }
1494 protected:
1495 void onChange() override {
1496 ComboBox::onChange();
1497 m_sliceFields->onSelectSliceFromComboBox();
1498 }
1499 void onEntryChange() override {
1500 ComboBox::onEntryChange();
1501 m_sliceFields->onComboBoxEntryChange();
1502 }
1503 void onBeforeOpenListBox() override {
1504 ComboBox::onBeforeOpenListBox();
1505 m_sliceFields->fillSlices();
1506 }
1507 void onEnterOnEditableEntry() override {
1508 ComboBox::onEnterOnEditableEntry();
1509
1510 const Slice* slice = nullptr;
1511 if (auto item = dynamic_cast<Item*>(getSelectedItem())) {
1512 if (item->slice()->name() == getValue()) {
1513 slice = item->slice();
1514 }
1515 }
1516 if (!slice && current_editor)
1517 slice = current_editor->sprite()->slices().getByName(getValue());
1518 if (slice)
1519 m_sliceFields->scrollToSlice(slice);
1520
1521 closeListBox();
1522 }
1523 };
1524
1525public:
1526
1527 SliceFields()
1528 : m_doc(nullptr)
1529 , m_sel(2)
1530 , m_combobox(this)
1531 , m_action(2)
1532 {
1533 auto theme = SkinTheme::get(this);
1534
1535 m_sel.addItem(Strings::context_bar_all());
1536 m_sel.addItem(Strings::context_bar_none());
1537 m_sel.ItemChange.connect(
1538 [this](ButtonSet::Item* item){
1539 onSelAction(m_sel.selectedItem());
1540 });
1541
1542 m_combobox.setEditable(true);
1543 m_combobox.setExpansive(true);
1544 m_combobox.setMinSize(gfx::Size(256*guiscale(), 0));
1545
1546 m_action.addItem(theme->parts.iconUserData())->setMono(true);
1547 m_action.addItem(theme->parts.iconClose())->setMono(true);
1548 m_action.ItemChange.connect(
1549 [this](ButtonSet::Item* item){
1550 onAction(m_action.selectedItem());
1551 });
1552
1553 addChild(&m_sel);
1554 addChild(&m_combobox);
1555 addChild(&m_action);
1556
1557 m_combobox.setVisible(false);
1558 m_action.setVisible(false);
1559 }
1560
1561 void setupTooltips(TooltipManager* tooltipManager) {
1562 tooltipManager->addTooltipFor(
1563 m_sel.at(0), Strings::context_bar_select_slices(), BOTTOM);
1564 tooltipManager->addTooltipFor(
1565 m_sel.at(1), Strings::context_bar_deselect_slices(), BOTTOM);
1566 tooltipManager->addTooltipFor(
1567 m_action.at(0), Strings::context_bar_slice_props(), BOTTOM);
1568 tooltipManager->addTooltipFor(
1569 m_action.at(1), Strings::context_bar_delete_slice(), BOTTOM);
1570 }
1571
1572 void setDoc(Doc* doc) {
1573 m_doc = doc;
1574 }
1575
1576 void addSlice(const doc::Slice* slice) {
1577 m_changeFromEntry = true;
1578 m_combobox.setValue(slice->name());
1579 updateLayout();
1580 m_changeFromEntry = false;
1581 }
1582
1583 void removeSlice(const doc::Slice* slice) {
1584 m_combobox.setValue(std::string());
1585 updateLayout();
1586 }
1587
1588 void updateSlice(const doc::Slice* slice) {
1589 m_combobox.setValue(slice->name());
1590 updateLayout();
1591 }
1592
1593 void selectSlices(const doc::Sprite* sprite,
1594 const doc::SelectedObjects& slices) {
1595 if (!slices.empty()) {
1596 auto selected = slices.frontAs<doc::Slice>();
1597 if (m_combobox.getValue() != selected->name())
1598 m_combobox.setValue(selected->name());
1599 }
1600 else {
1601 if (!m_combobox.getValue().empty())
1602 m_combobox.setValue(std::string());
1603 }
1604 updateLayout();
1605 }
1606
1607 void closeComboBox() {
1608 m_combobox.closeListBox();
1609 }
1610
1611private:
1612 void onVisible(bool visible) override {
1613 HBox::onVisible(visible);
1614 m_combobox.closeListBox();
1615 }
1616
1617 void fillSlices() {
1618 m_combobox.deleteAllItems();
1619 if (m_doc && m_doc->sprite()) {
1620 MatchWords match(m_filter);
1621
1622 std::vector<doc::Slice*> slices;
1623 for (auto slice : m_doc->sprite()->slices()) {
1624 if (match(slice->name()))
1625 slices.push_back(slice);
1626 }
1627 std::sort(slices.begin(), slices.end(),
1628 [](const doc::Slice* a, const doc::Slice* b){
1629 return (base::compare_filenames(a->name(), b->name()) < 0);
1630 });
1631
1632 for (auto slice : slices) {
1633 Item* item = new Item(slice);
1634 m_combobox.addItem(item);
1635 }
1636 }
1637 }
1638
1639 void scrollToSlice(const Slice* slice) {
1640 if (current_editor && slice) {
1641 if (const SliceKey* key = slice->getByFrame(current_editor->frame())) {
1642 current_editor->centerInSpritePoint(key->bounds().center());
1643 }
1644 }
1645 }
1646
1647 void updateLayout() {
1648 const bool visible = (m_doc && !m_doc->sprite()->slices().empty());
1649 const bool relayout = (visible != m_combobox.isVisible() ||
1650 visible != m_action.isVisible());
1651
1652 m_combobox.setVisible(visible);
1653 m_action.setVisible(visible);
1654
1655 if (relayout)
1656 parent()->layout();
1657 }
1658
1659 void onSelAction(const int item) {
1660 m_sel.deselectItems();
1661 switch (item) {
1662 case 0:
1663 if (current_editor)
1664 current_editor->selectAllSlices();
1665 break;
1666 case 1:
1667 if (current_editor)
1668 current_editor->clearSlicesSelection();
1669 break;
1670 }
1671 }
1672
1673 void onSelectSliceFromComboBox() {
1674 if (m_changeFromEntry)
1675 return;
1676
1677 m_filter.clear();
1678
1679 if (auto item = dynamic_cast<Item*>(m_combobox.getSelectedItem())) {
1680 if (current_editor) {
1681 const doc::Slice* slice = item->slice();
1682 current_editor->clearSlicesSelection();
1683 current_editor->selectSlice(slice);
1684 }
1685 }
1686 }
1687
1688 void onComboBoxEntryChange() {
1689 m_changeFromEntry = true;
1690 m_combobox.closeListBox();
1691
1692 m_filter = m_combobox.getValue();
1693
1694 m_combobox.openListBox();
1695 m_changeFromEntry = false;
1696 }
1697
1698 void onAction(const int item) {
1699 m_action.deselectItems();
1700
1701 Command* cmd = nullptr;
1702 Params params;
1703
1704 switch (item) {
1705 case 0:
1706 cmd = Commands::instance()->byId(CommandId::SliceProperties());
1707 break;
1708 case 1:
1709 cmd = Commands::instance()->byId(CommandId::RemoveSlice());
1710 break;
1711 }
1712
1713 if (cmd)
1714 UIContext::instance()->executeCommand(cmd, params);
1715
1716 updateLayout();
1717 }
1718
1719 Doc* m_doc;
1720 ButtonSet m_sel;
1721 Combo m_combobox;
1722 ButtonSet m_action;
1723 bool m_changeFromEntry;
1724 std::string m_filter;
1725};
1726
1727ContextBar::ContextBar(TooltipManager* tooltipManager,
1728 ColorBar* colorBar)
1729{
1730 auto& pref = Preferences::instance();
1731
1732 addChild(m_selectionOptionsBox = new HBox());
1733 m_selectionOptionsBox->addChild(m_dropPixels = new DropPixelsField());
1734 m_selectionOptionsBox->addChild(m_selectionMode = new SelectionModeField);
1735 m_selectionOptionsBox->addChild(m_transparentColor = new TransparentColorField(this, tooltipManager));
1736 m_selectionOptionsBox->addChild(m_transformation = new TransformationFields);
1737 m_selectionOptionsBox->addChild(m_pivot = new PivotField);
1738 m_selectionOptionsBox->addChild(m_rotAlgo = new RotAlgorithmField());
1739
1740 addChild(m_zoomButtons = new ZoomButtons);
1741 addChild(m_samplingSelector = new SamplingSelector);
1742
1743 addChild(m_brushBack = new BrushBackField);
1744 addChild(m_brushType = new BrushTypeField(this));
1745 addChild(m_brushSize = new BrushSizeField());
1746 addChild(m_brushAngle = new BrushAngleField(m_brushType));
1747 addChild(m_brushPatternField = new BrushPatternField());
1748
1749 addChild(m_toleranceLabel = new Label("Tolerance:"));
1750 addChild(m_tolerance = new ToleranceField());
1751 addChild(m_contiguous = new ContiguousField());
1752 addChild(m_paintBucketSettings = new PaintBucketSettingsField());
1753 addChild(m_gradientType = new GradientTypeField());
1754 addChild(m_ditheringSelector = new DitheringSelector(DitheringSelector::SelectMatrix));
1755 m_ditheringSelector->setUseCustomWidget(false); // Disable custom widget because the context bar is too small
1756
1757 addChild(m_inkType = new InkTypeField(this));
1758 addChild(m_inkOpacityLabel = new Label("Opacity:"));
1759 addChild(m_inkOpacity = new InkOpacityField());
1760 addChild(m_inkShades = new InkShadesField(colorBar));
1761
1762 addChild(m_eyedropperField = new EyedropperField());
1763
1764 addChild(m_autoSelectLayer = new AutoSelectLayerField());
1765
1766 addChild(m_sprayBox = new HBox());
1767 m_sprayBox->addChild(m_sprayLabel = new Label("Spray:"));
1768 m_sprayBox->addChild(m_sprayWidth = new SprayWidthField());
1769 m_sprayBox->addChild(m_spraySpeed = new SpraySpeedField());
1770
1771 addChild(m_selectBoxHelp = new Label(""));
1772 addChild(m_freehandBox = new HBox());
1773
1774 m_freehandBox->addChild(m_dynamics = new DynamicsField(this));
1775 m_freehandBox->addChild(m_freehandAlgo = new FreehandAlgorithmField());
1776
1777 addChild(m_symmetry = new SymmetryField());
1778 m_symmetry->setVisible(pref.symmetryMode.enabled());
1779
1780 addChild(m_sliceFields = new SliceFields);
1781
1782 setupTooltips(tooltipManager);
1783
1784 App::instance()->activeToolManager()->add_observer(this);
1785 UIContext::instance()->add_observer(this);
1786
1787 m_symmModeConn = pref.symmetryMode.enabled.AfterChange.connect(
1788 [this]{ onSymmetryModeChange(); });
1789 m_fgColorConn = pref.colorBar.fgColor.AfterChange.connect(
1790 [this]{ onFgOrBgColorChange(doc::Brush::ImageColor::MainColor); });
1791 m_bgColorConn = pref.colorBar.bgColor.AfterChange.connect(
1792 [this]{ onFgOrBgColorChange(doc::Brush::ImageColor::BackgroundColor); });
1793 m_keysConn = KeyboardShortcuts::instance()->UserChange.connect(
1794 [this, tooltipManager]{ setupTooltips(tooltipManager); });
1795 m_dropPixelsConn = m_dropPixels->DropPixels.connect(&ContextBar::onDropPixels, this);
1796
1797 setActiveBrush(createBrushFromPreferences());
1798
1799 initTheme();
1800 registerCommands();
1801}
1802
1803ContextBar::~ContextBar()
1804{
1805 UIContext::instance()->remove_observer(this);
1806 App::instance()->activeToolManager()->remove_observer(this);
1807}
1808
1809void ContextBar::onInitTheme(ui::InitThemeEvent& ev)
1810{
1811 Box::onInitTheme(ev);
1812
1813 auto theme = SkinTheme::get(this);
1814 gfx::Border border = this->border();
1815 border.bottom(2*guiscale());
1816 setBorder(border);
1817 setBgColor(theme->colors.workspace());
1818 m_sprayLabel->setStyle(theme->styles.miniLabel());
1819 m_toleranceLabel->setStyle(theme->styles.miniLabel());
1820 m_inkOpacityLabel->setStyle(theme->styles.miniLabel());
1821}
1822
1823void ContextBar::onSizeHint(SizeHintEvent& ev)
1824{
1825 auto theme = SkinTheme::get(this);
1826 ev.setSizeHint(gfx::Size(0, theme->dimensions.contextBarHeight()));
1827}
1828
1829void ContextBar::onToolSetOpacity(const int& newOpacity)
1830{
1831 if (g_updatingFromCode)
1832 return;
1833
1834 m_inkOpacity->setTextf("%d", newOpacity);
1835}
1836
1837void ContextBar::onToolSetFreehandAlgorithm()
1838{
1839 Tool* tool = App::instance()->activeTool();
1840 if (tool) {
1841 m_freehandAlgo->setFreehandAlgorithm(
1842 Preferences::instance().tool(tool).freehandAlgorithm());
1843 }
1844}
1845
1846void ContextBar::onToolSetContiguous()
1847{
1848 Tool* tool = App::instance()->activeTool();
1849 if (tool) {
1850 m_contiguous->setSelected(
1851 Preferences::instance().tool(tool).contiguous());
1852 }
1853}
1854
1855void ContextBar::onActiveSiteChange(const Site& site)
1856{
1857 DocObserverWidget<ui::HBox>::onActiveSiteChange(site);
1858 if (m_sliceFields->isVisible())
1859 updateSliceFields(site);
1860}
1861
1862void ContextBar::onDocChange(Doc* doc)
1863{
1864 DocObserverWidget<ui::HBox>::onDocChange(doc);
1865 m_sliceFields->setDoc(doc);
1866}
1867
1868void ContextBar::onAddSlice(DocEvent& ev)
1869{
1870 if (ev.slice())
1871 m_sliceFields->addSlice(ev.slice());
1872}
1873
1874void ContextBar::onRemoveSlice(DocEvent& ev)
1875{
1876 if (ev.slice())
1877 m_sliceFields->removeSlice(ev.slice());
1878}
1879
1880void ContextBar::onSliceNameChange(DocEvent& ev)
1881{
1882 if (ev.slice())
1883 m_sliceFields->updateSlice(ev.slice());
1884}
1885
1886void ContextBar::onBrushSizeChange()
1887{
1888 if (m_activeBrush->type() != kImageBrushType)
1889 discardActiveBrush();
1890
1891 updateForActiveTool();
1892}
1893
1894void ContextBar::onBrushAngleChange()
1895{
1896 if (m_activeBrush->type() != kImageBrushType)
1897 discardActiveBrush();
1898}
1899
1900void ContextBar::onActiveToolChange(tools::Tool* tool)
1901{
1902 if (m_activeBrush->type() != kImageBrushType)
1903 setActiveBrush(ContextBar::createBrushFromPreferences());
1904 else {
1905 updateForTool(tool);
1906 }
1907}
1908
1909void ContextBar::onSymmetryModeChange()
1910{
1911 updateForActiveTool();
1912}
1913
1914void ContextBar::onFgOrBgColorChange(doc::Brush::ImageColor imageColor)
1915{
1916 if (!m_activeBrush)
1917 return;
1918
1919 if (m_activeBrush->type() == kImageBrushType) {
1920 ASSERT(m_activeBrush->image());
1921
1922 auto& pref = Preferences::instance();
1923 m_activeBrush->setImageColor(
1924 imageColor,
1925 color_utils::color_for_target_mask(
1926 (imageColor == doc::Brush::ImageColor::MainColor ?
1927 pref.colorBar.fgColor():
1928 pref.colorBar.bgColor()),
1929 ColorTarget(ColorTarget::TransparentLayer,
1930 m_activeBrush->image()->pixelFormat(),
1931 -1)));
1932 }
1933}
1934
1935void ContextBar::onDropPixels(ContextBarObserver::DropAction action)
1936{
1937 notify_observers(&ContextBarObserver::onDropPixels, action);
1938}
1939
1940void ContextBar::updateSliceFields(const Site& site)
1941{
1942 if (site.sprite())
1943 m_sliceFields->selectSlices(site.sprite(),
1944 site.selectedSlices());
1945 else
1946 m_sliceFields->closeComboBox();
1947}
1948
1949void ContextBar::updateForActiveTool()
1950{
1951 updateForTool(App::instance()->activeTool());
1952}
1953
1954void ContextBar::updateForTool(tools::Tool* tool)
1955{
1956 // TODO Improve the design of the visibility of ContextBar
1957 // items. Actually this manual show/hide logic is a mess. There
1958 // should be a IContextBarUser interface, with a method to ask who
1959 // needs which items to be visible. E.g. different tools elements
1960 // (inks, controllers, etc.) and sprite editor states are the main
1961 // target to implement this new IContextBarUser and ask for
1962 // ContextBar elements.
1963
1964 const bool oldUpdatingFromCode = g_updatingFromCode;
1965 base::ScopedValue<bool> lockFlag(g_updatingFromCode, true, oldUpdatingFromCode);
1966
1967 ToolPreferences* toolPref = nullptr;
1968 ToolPreferences::Brush* brushPref = nullptr;
1969 Preferences& preferences = Preferences::instance();
1970
1971 if (tool) {
1972 toolPref = &preferences.tool(tool);
1973 brushPref = &toolPref->brush;
1974 }
1975
1976 if (toolPref) {
1977 m_sizeConn = brushPref->size.AfterChange.connect([this]{ onBrushSizeChange(); });
1978 m_angleConn = brushPref->angle.AfterChange.connect([this]{ onBrushAngleChange(); });
1979 m_opacityConn = toolPref->opacity.AfterChange.connect(&ContextBar::onToolSetOpacity, this);
1980 m_freehandAlgoConn = toolPref->freehandAlgorithm.AfterChange.connect([this]{ onToolSetFreehandAlgorithm(); });
1981 m_contiguousConn = toolPref->contiguous.AfterChange.connect([this]{ onToolSetContiguous(); });
1982 }
1983
1984 if (tool)
1985 m_brushType->updateBrush(tool);
1986
1987 if (brushPref) {
1988 if (!oldUpdatingFromCode) {
1989 m_brushSize->setTextf("%d", brushPref->size());
1990 m_brushAngle->setTextf("%d", brushPref->angle());
1991 }
1992 }
1993
1994 m_brushPatternField->setBrushPattern(
1995 preferences.brush.pattern());
1996
1997 // Tool ink
1998 bool isPaint = tool &&
1999 (tool->getInk(0)->isPaint() ||
2000 tool->getInk(1)->isPaint());
2001 bool isEffect = tool &&
2002 (tool->getInk(0)->isEffect() ||
2003 tool->getInk(1)->isEffect());
2004
2005 // True if the current tool support opacity slider
2006 bool supportOpacity = (isPaint || isEffect);
2007
2008 // True if it makes sense to change the ink property for the current
2009 // tool.
2010 bool hasInk = tool &&
2011 ((tool->getInk(0)->isPaint() && !tool->getInk(0)->isEffect()) ||
2012 (tool->getInk(1)->isPaint() && !tool->getInk(1)->isEffect()));
2013
2014 bool hasInkWithOpacity = false;
2015 bool hasInkShades = false;
2016
2017 if (toolPref) {
2018 m_tolerance->setTextf("%d", toolPref->tolerance());
2019 m_contiguous->setSelected(toolPref->contiguous());
2020
2021 m_inkType->setInkTypeIcon(toolPref->ink());
2022 m_inkOpacity->setTextf("%d", toolPref->opacity());
2023
2024 hasInkWithOpacity =
2025 ((isPaint && tools::inkHasOpacity(toolPref->ink())) ||
2026 (isEffect));
2027
2028 hasInkShades =
2029 (isPaint && !isEffect && toolPref->ink() == InkType::SHADING);
2030
2031 m_freehandAlgo->setFreehandAlgorithm(toolPref->freehandAlgorithm());
2032
2033 m_sprayWidth->setValue(toolPref->spray.width());
2034 m_spraySpeed->setValue(toolPref->spray.speed());
2035 }
2036
2037 const bool updateShade = (!m_inkShades->isVisible() && hasInkShades);
2038
2039 m_eyedropperField->updateFromPreferences(preferences.eyedropper);
2040 m_autoSelectLayer->setSelected(preferences.editor.autoSelectLayer());
2041
2042 // True if we have an image as brush
2043 const bool hasImageBrush = (activeBrush()->type() == kImageBrushType);
2044
2045 // True if the brush type supports angle.
2046 const bool hasBrushWithAngle =
2047 (activeBrush()->size() > 1) &&
2048 (activeBrush()->type() == kSquareBrushType ||
2049 activeBrush()->type() == kLineBrushType);
2050
2051 // True if the current tool is eyedropper.
2052 const bool isEyedropper = tool &&
2053 (tool->getInk(0)->isEyedropper() ||
2054 tool->getInk(1)->isEyedropper());
2055
2056 // True if the current tool is move tool.
2057 const bool isMove = tool &&
2058 (tool->getInk(0)->isCelMovement() ||
2059 tool->getInk(1)->isCelMovement());
2060
2061 // True if the current tool is slice tool.
2062 const bool isSlice = tool &&
2063 (tool->getInk(0)->isSlice() ||
2064 tool->getInk(1)->isSlice());
2065
2066 // True if the current tool is floodfill
2067 const bool isFloodfill = tool &&
2068 (tool->getPointShape(0)->isFloodFill() ||
2069 tool->getPointShape(1)->isFloodFill());
2070
2071 // True if the current tool needs tolerance options
2072 const bool hasTolerance = tool &&
2073 (tool->getPointShape(0)->isFloodFill() ||
2074 tool->getPointShape(1)->isFloodFill());
2075
2076 // True if the current tool needs spray options
2077 const bool hasSprayOptions = tool &&
2078 (tool->getPointShape(0)->isSpray() ||
2079 tool->getPointShape(1)->isSpray());
2080
2081 const bool hasSelectOptions = tool &&
2082 (tool->getInk(0)->isSelection() ||
2083 tool->getInk(1)->isSelection());
2084
2085 const bool isFreehand = tool &&
2086 (tool->getController(0)->isFreehand() ||
2087 tool->getController(1)->isFreehand());
2088
2089 const bool showOpacity =
2090 (supportOpacity) &&
2091 ((isPaint && (hasInkWithOpacity || hasImageBrush)) ||
2092 (isEffect));
2093
2094 const bool withDithering = tool &&
2095 (tool->getInk(0)->withDitheringOptions() ||
2096 tool->getInk(1)->withDitheringOptions());
2097
2098 // True if the brush supports dynamics
2099 // TODO add support for dynamics in custom brushes in the future
2100 const bool supportDynamics = (!hasImageBrush);
2101
2102 // Show/Hide fields
2103 m_zoomButtons->setVisible(needZoomButtons(tool));
2104 m_brushBack->setVisible(supportOpacity && hasImageBrush && !withDithering);
2105 m_brushType->setVisible(supportOpacity && (!isFloodfill || (isFloodfill && hasImageBrush && !withDithering)));
2106 m_brushSize->setVisible(supportOpacity && !isFloodfill && !hasImageBrush);
2107 m_brushAngle->setVisible(supportOpacity && !isFloodfill && !hasImageBrush && hasBrushWithAngle);
2108 m_brushPatternField->setVisible(supportOpacity && hasImageBrush && !withDithering);
2109 m_inkType->setVisible(hasInk);
2110 m_inkOpacityLabel->setVisible(showOpacity);
2111 m_inkOpacity->setVisible(showOpacity);
2112 m_inkShades->setVisible(hasInkShades);
2113 m_eyedropperField->setVisible(isEyedropper);
2114 m_autoSelectLayer->setVisible(isMove);
2115 m_dynamics->setVisible(isFreehand && supportDynamics);
2116 m_dynamics->setOptionsGridVisibility(isFreehand && !hasSelectOptions);
2117 m_freehandBox->setVisible(isFreehand && (supportOpacity || hasSelectOptions));
2118 m_toleranceLabel->setVisible(hasTolerance);
2119 m_tolerance->setVisible(hasTolerance);
2120 m_contiguous->setVisible(hasTolerance);
2121 m_paintBucketSettings->setVisible(hasTolerance);
2122 m_sprayBox->setVisible(hasSprayOptions);
2123 m_selectionOptionsBox->setVisible(hasSelectOptions);
2124 m_gradientType->setVisible(withDithering);
2125 m_ditheringSelector->setVisible(withDithering);
2126 m_selectionMode->setVisible(true);
2127 m_pivot->setVisible(true);
2128 m_dropPixels->setVisible(false);
2129 m_transformation->setVisible(false);
2130 m_selectBoxHelp->setVisible(false);
2131
2132 m_symmetry->setVisible(
2133 Preferences::instance().symmetryMode.enabled() &&
2134 (isPaint || isEffect || hasSelectOptions));
2135 m_symmetry->updateWithCurrentDocument();
2136
2137 m_sliceFields->setVisible(isSlice);
2138 if (isSlice)
2139 updateSliceFields(UIContext::instance()->activeSite());
2140
2141 // Update ink shades with the current selected palette entries
2142 if (updateShade)
2143 m_inkShades->updateShadeFromColorBarPicks();
2144
2145 if (!updateSamplingVisibility(tool)) {
2146 // updateSamplingVisibility() returns false if it doesn't layout()
2147 // the ContextBar.
2148 layout();
2149 }
2150}
2151
2152void ContextBar::updateForMovingPixels(const Transformation& t)
2153{
2154 tools::Tool* tool = App::instance()->toolBox()->getToolById(
2155 tools::WellKnownTools::RectangularMarquee);
2156 if (tool)
2157 updateForTool(tool);
2158
2159 m_dropPixels->deselectItems();
2160 m_dropPixels->setVisible(true);
2161 m_selectionMode->setVisible(false);
2162 m_transformation->setVisible(true);
2163 m_transformation->update(t);
2164 layout();
2165}
2166
2167void ContextBar::updateForSelectingBox(const std::string& text)
2168{
2169 if (m_selectBoxHelp->isVisible() && m_selectBoxHelp->text() == text)
2170 return;
2171
2172 updateForTool(nullptr);
2173 m_selectBoxHelp->setText(text);
2174 m_selectBoxHelp->setVisible(true);
2175 layout();
2176}
2177
2178void ContextBar::updateToolLoopModifiersIndicators(tools::ToolLoopModifiers modifiers)
2179{
2180 gen::SelectionMode mode = gen::SelectionMode::DEFAULT;
2181 if (int(modifiers) & int(tools::ToolLoopModifiers::kAddSelection))
2182 mode = gen::SelectionMode::ADD;
2183 else if (int(modifiers) & int(tools::ToolLoopModifiers::kSubtractSelection))
2184 mode = gen::SelectionMode::SUBTRACT;
2185 else if (int(modifiers) & int(tools::ToolLoopModifiers::kIntersectSelection))
2186 mode = gen::SelectionMode::INTERSECT;
2187
2188 m_selectionMode->setSelectionMode(mode);
2189}
2190
2191bool ContextBar::updateSamplingVisibility(tools::Tool* tool)
2192{
2193 if (!tool)
2194 tool = App::instance()->activeTool();
2195
2196 const bool newVisibility =
2197 needZoomButtons(tool) &&
2198 current_editor &&
2199 (current_editor->projection().scaleX() < 1.0 ||
2200 current_editor->projection().scaleY() < 1.0) &&
2201 current_editor->isUsingNewRenderEngine();
2202
2203 if (newVisibility == m_samplingSelector->hasFlags(HIDDEN)) {
2204 m_samplingSelector->setVisible(newVisibility);
2205 layout();
2206 return true;
2207 }
2208 return false;
2209}
2210
2211void ContextBar::updateAutoSelectLayer(bool state)
2212{
2213 m_autoSelectLayer->setSelected(state);
2214}
2215
2216bool ContextBar::isAutoSelectLayer() const
2217{
2218 return m_autoSelectLayer->isSelected();
2219}
2220
2221void ContextBar::setActiveBrushBySlot(tools::Tool* tool, int slot)
2222{
2223 ASSERT(tool);
2224 if (!tool)
2225 return;
2226
2227 AppBrushes& brushes = App::instance()->brushes();
2228 BrushSlot brush = brushes.getBrushSlot(slot);
2229 if (!brush.isEmpty()) {
2230 brushes.lockBrushSlot(slot);
2231
2232 Preferences& pref = Preferences::instance();
2233 ToolPreferences& toolPref = pref.tool(tool);
2234 ToolPreferences::Brush& brushPref = toolPref.brush;
2235
2236 if (brush.brush()) {
2237 if (brush.brush()->type() == doc::kImageBrushType) {
2238 // Reset the colors of the image when we select the brush from
2239 // the slot.
2240 if (brush.hasFlag(BrushSlot::Flags::ImageColor))
2241 brush.brush()->resetImageColors();
2242
2243 setActiveBrush(brush.brush());
2244 }
2245 else {
2246 if (brush.hasFlag(BrushSlot::Flags::BrushType))
2247 brushPref.type(static_cast<app::gen::BrushType>(brush.brush()->type()));
2248
2249 if (brush.hasFlag(BrushSlot::Flags::BrushSize))
2250 brushPref.size(brush.brush()->size());
2251
2252 if (brush.hasFlag(BrushSlot::Flags::BrushAngle))
2253 brushPref.angle(brush.brush()->angle());
2254
2255 setActiveBrush(ContextBar::createBrushFromPreferences());
2256 }
2257 }
2258
2259 if (brush.hasFlag(BrushSlot::Flags::FgColor))
2260 pref.colorBar.fgColor(brush.fgColor());
2261
2262 if (brush.hasFlag(BrushSlot::Flags::BgColor))
2263 pref.colorBar.bgColor(brush.bgColor());
2264
2265 // If the image/stamp brush doesn't have the "ImageColor" flag, it
2266 // means that we have to change the image color to the current
2267 // "foreground color".
2268 if (brush.brush() &&
2269 brush.brush()->type() == doc::kImageBrushType &&
2270 !brush.hasFlag(BrushSlot::Flags::ImageColor)) {
2271 auto pixelFormat = brush.brush()->image()->pixelFormat();
2272
2273 brush.brush()->setImageColor(
2274 Brush::ImageColor::MainColor,
2275 color_utils::color_for_target_mask(
2276 pref.colorBar.fgColor(),
2277 ColorTarget(ColorTarget::TransparentLayer,
2278 pixelFormat,
2279 -1)));
2280
2281 brush.brush()->setImageColor(
2282 Brush::ImageColor::BackgroundColor,
2283 color_utils::color_for_target_mask(
2284 pref.colorBar.bgColor(),
2285 ColorTarget(ColorTarget::TransparentLayer,
2286 pixelFormat,
2287 -1)));
2288 }
2289
2290 if (brush.hasFlag(BrushSlot::Flags::InkType))
2291 setInkType(brush.inkType());
2292
2293 if (brush.hasFlag(BrushSlot::Flags::InkOpacity))
2294 toolPref.opacity(brush.inkOpacity());
2295
2296 if (brush.hasFlag(BrushSlot::Flags::Shade))
2297 m_inkShades->setShade(brush.shade());
2298
2299 if (brush.hasFlag(BrushSlot::Flags::PixelPerfect))
2300 toolPref.freehandAlgorithm(
2301 (brush.pixelPerfect() ?
2302 tools::FreehandAlgorithm::PIXEL_PERFECT:
2303 tools::FreehandAlgorithm::REGULAR));
2304 }
2305 else {
2306 updateForTool(tool);
2307 m_brushType->showPopupAndHighlightSlot(slot);
2308 }
2309}
2310
2311void ContextBar::setActiveBrush(const doc::BrushRef& brush)
2312{
2313 if (brush->type() == kImageBrushType)
2314 m_activeBrush = brush;
2315 else {
2316 Tool* tool = App::instance()->activeTool();
2317 auto& brushPref = Preferences::instance().tool(tool).brush;
2318 auto newBrushType = static_cast<app::gen::BrushType>(brush->type());
2319 if (brushPref.type() != newBrushType)
2320 brushPref.type(newBrushType);
2321
2322 m_activeBrush = brush;
2323 }
2324
2325 BrushChange();
2326
2327 updateForActiveTool();
2328}
2329
2330doc::BrushRef ContextBar::activeBrush(tools::Tool* tool,
2331 tools::Ink* ink) const
2332{
2333 if (ink == nullptr)
2334 ink = (tool ? tool->getInk(0): nullptr);
2335
2336 // Selection tools use a brush with size = 1 (always)
2337 if (ink && ink->isSelection()) {
2338 doc::BrushRef brush;
2339 brush.reset(new Brush(kCircleBrushType, 1, 0));
2340 return brush;
2341 }
2342
2343 if ((tool == nullptr) ||
2344 (tool == App::instance()->activeTool()) ||
2345 (ink && ink->isPaint() &&
2346 m_activeBrush->type() == kImageBrushType)) {
2347 m_activeBrush->setPattern(Preferences::instance().brush.pattern());
2348 return m_activeBrush;
2349 }
2350
2351 return ContextBar::createBrushFromPreferences(
2352 &Preferences::instance().tool(tool).brush);
2353}
2354
2355void ContextBar::discardActiveBrush()
2356{
2357 setActiveBrush(ContextBar::createBrushFromPreferences());
2358}
2359
2360// static
2361doc::BrushRef ContextBar::createBrushFromPreferences(ToolPreferences::Brush* brushPref)
2362{
2363 if (brushPref == nullptr) {
2364 tools::Tool* tool = App::instance()->activeTool();
2365 brushPref = &Preferences::instance().tool(tool).brush;
2366 }
2367
2368 doc::BrushRef brush;
2369 brush.reset(
2370 new Brush(
2371 static_cast<doc::BrushType>(brushPref->type()),
2372 brushPref->size(),
2373 brushPref->angle()));
2374 return brush;
2375}
2376
2377BrushSlot ContextBar::createBrushSlotFromPreferences()
2378{
2379 tools::Tool* activeTool = App::instance()->activeTool();
2380 auto& pref = Preferences::instance();
2381 auto& saveBrush = pref.saveBrush;
2382 auto& toolPref = pref.tool(activeTool);
2383
2384 int flags = 0;
2385 if (saveBrush.brushType()) flags |= int(BrushSlot::Flags::BrushType);
2386 if (saveBrush.brushSize()) flags |= int(BrushSlot::Flags::BrushSize);
2387 if (saveBrush.brushAngle()) flags |= int(BrushSlot::Flags::BrushAngle);
2388 if (saveBrush.fgColor()) flags |= int(BrushSlot::Flags::FgColor);
2389 if (saveBrush.bgColor()) flags |= int(BrushSlot::Flags::BgColor);
2390 if (saveBrush.imageColor()) flags |= int(BrushSlot::Flags::ImageColor);
2391 if (saveBrush.inkType()) flags |= int(BrushSlot::Flags::InkType);
2392 if (saveBrush.inkOpacity()) flags |= int(BrushSlot::Flags::InkOpacity);
2393 if (saveBrush.shade()) flags |= int(BrushSlot::Flags::Shade);
2394 if (saveBrush.pixelPerfect()) flags |= int(BrushSlot::Flags::PixelPerfect);
2395
2396 return BrushSlot(
2397 BrushSlot::Flags(flags),
2398 activeBrush(activeTool),
2399 pref.colorBar.fgColor(),
2400 pref.colorBar.bgColor(),
2401 toolPref.ink(),
2402 toolPref.opacity(),
2403 getShade(),
2404 toolPref.freehandAlgorithm() == tools::FreehandAlgorithm::PIXEL_PERFECT);
2405}
2406
2407Shade ContextBar::getShade() const
2408{
2409 return m_inkShades->getShade();
2410}
2411
2412doc::Remap* ContextBar::createShadeRemap(bool left)
2413{
2414 return m_inkShades->createShadeRemap(left);
2415}
2416
2417void ContextBar::reverseShadeColors()
2418{
2419 m_inkShades->reverseShadeColors();
2420}
2421
2422void ContextBar::setInkType(tools::InkType type)
2423{
2424 m_inkType->setInkType(type);
2425}
2426
2427render::DitheringMatrix ContextBar::ditheringMatrix()
2428{
2429 return m_ditheringSelector->ditheringMatrix();
2430}
2431
2432render::DitheringAlgorithmBase* ContextBar::ditheringAlgorithm()
2433{
2434 static std::unique_ptr<render::DitheringAlgorithmBase> s_dither;
2435
2436 switch (m_ditheringSelector->ditheringAlgorithm()) {
2437 case render::DitheringAlgorithm::None:
2438 s_dither.reset(nullptr);
2439 break;
2440 case render::DitheringAlgorithm::Ordered:
2441 s_dither.reset(new render::OrderedDither2(-1));
2442 break;
2443 case render::DitheringAlgorithm::Old:
2444 s_dither.reset(new render::OrderedDither(-1));
2445 break;
2446 case render::DitheringAlgorithm::ErrorDiffusion:
2447 s_dither.reset(new render::ErrorDiffusionDither(-1));
2448 break;
2449 }
2450
2451 return s_dither.get();
2452}
2453
2454render::GradientType ContextBar::gradientType()
2455{
2456 return m_gradientType->gradientType();
2457}
2458
2459const tools::DynamicsOptions& ContextBar::getDynamics() const
2460{
2461 return m_dynamics->getDynamics();
2462}
2463
2464void ContextBar::setupTooltips(TooltipManager* tooltipManager)
2465{
2466 tooltipManager->addTooltipFor(
2467 m_brushBack->at(0), Strings::context_bar_discard_brush(), BOTTOM);
2468 tooltipManager->addTooltipFor(
2469 m_brushType->at(0), Strings::context_bar_brush_type(), BOTTOM);
2470 tooltipManager->addTooltipFor(
2471 m_brushSize, Strings::context_bar_brush_size(), BOTTOM);
2472 tooltipManager->addTooltipFor(
2473 m_brushAngle, Strings::context_bar_brush_angle(), BOTTOM);
2474 tooltipManager->addTooltipFor(
2475 m_inkType->at(0), Strings::context_bar_ink(), BOTTOM);
2476 tooltipManager->addTooltipFor(
2477 m_inkOpacity, Strings::context_bar_opacity(), BOTTOM);
2478 tooltipManager->addTooltipFor(
2479 m_inkShades->at(0), Strings::context_bar_shades(), BOTTOM);
2480 tooltipManager->addTooltipFor(
2481 m_sprayWidth, Strings::context_bar_spray_width(), BOTTOM);
2482 tooltipManager->addTooltipFor(
2483 m_spraySpeed, Strings::context_bar_spray_speed(), BOTTOM);
2484 tooltipManager->addTooltipFor(
2485 m_pivot->at(0), Strings::context_bar_rotation_pivot(), BOTTOM);
2486 tooltipManager->addTooltipFor(
2487 m_rotAlgo, Strings::context_bar_rotation_algorithm(), BOTTOM);
2488 tooltipManager->addTooltipFor(
2489 m_dynamics->at(0), Strings::context_bar_dynamics(), BOTTOM);
2490 tooltipManager->addTooltipFor(m_freehandAlgo,
2491 key_tooltip("Freehand trace algorithm",
2492 CommandId::PixelPerfectMode()), BOTTOM);
2493 tooltipManager->addTooltipFor(m_contiguous,
2494 key_tooltip("Fill contiguous areas color",
2495 CommandId::ContiguousFill()), BOTTOM);
2496 tooltipManager->addTooltipFor(
2497 m_paintBucketSettings->at(0), Strings::context_bar_paint_bucket_option(), BOTTOM);
2498
2499 m_selectionMode->setupTooltips(tooltipManager);
2500 m_gradientType->setupTooltips(tooltipManager);
2501 m_dropPixels->setupTooltips(tooltipManager);
2502 m_symmetry->setupTooltips(tooltipManager);
2503 m_sliceFields->setupTooltips(tooltipManager);
2504}
2505
2506void ContextBar::registerCommands()
2507{
2508 Commands::instance()
2509 ->add(
2510 new QuickCommand(
2511 CommandId::ShowBrushes(),
2512 [this]{ this->showBrushes(); }));
2513
2514 Commands::instance()
2515 ->add(
2516 new QuickCommand(
2517 CommandId::ShowDynamics(),
2518 [this]{ this->showDynamics(); }));
2519}
2520
2521void ContextBar::showBrushes()
2522{
2523 if (m_brushType->isVisible())
2524 m_brushType->switchPopup();
2525}
2526
2527void ContextBar::showDynamics()
2528{
2529 if (m_dynamics->isVisible())
2530 m_dynamics->switchPopup();
2531}
2532
2533bool ContextBar::needZoomButtons(tools::Tool* tool) const
2534{
2535 return tool &&
2536 (tool->getInk(0)->isZoom() ||
2537 tool->getInk(1)->isZoom() ||
2538 tool->getInk(0)->isScrollMovement() ||
2539 tool->getInk(1)->isScrollMovement());
2540}
2541
2542} // namespace app
2543