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 | |
86 | namespace app { |
87 | |
88 | using namespace app::skin; |
89 | using namespace gfx; |
90 | using namespace ui; |
91 | using namespace tools; |
92 | |
93 | static bool g_updatingFromCode = false; |
94 | |
95 | class ContextBar::ZoomButtons : public ButtonSet { |
96 | public: |
97 | ZoomButtons() |
98 | : ButtonSet(3) { |
99 | addItem("100%" ); |
100 | addItem(Strings::context_bar_center()); |
101 | addItem(Strings::context_bar_fit_screen()); |
102 | } |
103 | |
104 | private: |
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 | |
141 | class ContextBar::BrushBackField : public ButtonSet { |
142 | public: |
143 | BrushBackField() |
144 | : ButtonSet(1) { |
145 | addItem(Strings::context_bar_back()); |
146 | } |
147 | |
148 | protected: |
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 | |
158 | class ContextBar::BrushTypeField : public ButtonSet { |
159 | public: |
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 () { |
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 | |
201 | protected: |
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 | |
218 | private: |
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 () { |
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 () { |
236 | m_popupWindow.closeWindow(NULL); |
237 | deselectItems(); |
238 | } |
239 | |
240 | ContextBar* m_owner; |
241 | AppBrushes& m_brushes; |
242 | BrushPopup ; |
243 | }; |
244 | |
245 | class ContextBar::BrushSizeField : public IntEntry { |
246 | public: |
247 | BrushSizeField() : IntEntry(Brush::kMinBrushSize, Brush::kMaxBrushSize) { |
248 | setSuffix("px" ); |
249 | } |
250 | |
251 | private: |
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 | |
264 | class ContextBar::BrushAngleField : public IntEntry { |
265 | public: |
266 | BrushAngleField(BrushTypeField* brushType) |
267 | : IntEntry(-180, 180) |
268 | , m_brushType(brushType) { |
269 | setSuffix("\xc2\xb0" ); |
270 | } |
271 | |
272 | protected: |
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 | |
286 | private: |
287 | BrushTypeField* m_brushType; |
288 | }; |
289 | |
290 | class ContextBar::BrushPatternField : public ComboBox { |
291 | public: |
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 | |
319 | protected: |
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 | |
340 | class ContextBar::ToleranceField : public IntEntry { |
341 | public: |
342 | ToleranceField() : IntEntry(0, 255) { |
343 | } |
344 | |
345 | protected: |
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 | |
357 | class ContextBar::ContiguousField : public CheckBox { |
358 | public: |
359 | ContiguousField() |
360 | : CheckBox(Strings::context_bar_contiguous()) { |
361 | initTheme(); |
362 | } |
363 | |
364 | protected: |
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 | |
380 | class ContextBar::PaintBucketSettingsField : public ButtonSet { |
381 | public: |
382 | PaintBucketSettingsField() : ButtonSet(1) { |
383 | auto theme = SkinTheme::get(this); |
384 | addItem(theme->parts.timelineGear()); |
385 | } |
386 | |
387 | protected: |
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 ; |
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 | |
465 | class ContextBar::InkTypeField : public ButtonSet { |
466 | public: |
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 | |
503 | protected: |
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 | |
520 | class ContextBar::InkShadesField : public HBox { |
521 | public: |
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 | |
571 | private: |
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 () { |
581 | loadShades(); |
582 | gfx::Rect bounds = m_button.bounds(); |
583 | |
584 | 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 | |
706 | class ContextBar::InkOpacityField : public IntEntry { |
707 | public: |
708 | InkOpacityField() : IntEntry(0, 255) { |
709 | } |
710 | |
711 | protected: |
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 | |
732 | class ContextBar::SprayWidthField : public IntEntry { |
733 | public: |
734 | SprayWidthField() : IntEntry(1, 32) { |
735 | } |
736 | |
737 | protected: |
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 | |
748 | class ContextBar::SpraySpeedField : public IntEntry { |
749 | public: |
750 | SpraySpeedField() : IntEntry(1, 100) { |
751 | } |
752 | |
753 | protected: |
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 | |
765 | class ContextBar::TransparentColorField : public HBox { |
766 | public: |
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 | |
796 | private: |
797 | |
798 | void () { |
799 | gfx::Rect bounds = this->bounds(); |
800 | |
801 | 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 | |
863 | class ContextBar::PivotField : public ButtonSet { |
864 | public: |
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 | |
875 | private: |
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 ; |
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 | |
944 | class ContextBar::RotAlgorithmField : public ComboBox { |
945 | public: |
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 | |
960 | protected: |
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 | |
973 | private: |
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 | |
990 | class ContextBar::TransformationFields : public HBox { |
991 | public: |
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 | |
1077 | private: |
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 | |
1142 | class ContextBar::DynamicsField : public ButtonSet |
1143 | , public DynamicsPopup::Delegate { |
1144 | public: |
1145 | DynamicsField(ContextBar* ctxBar) |
1146 | : ButtonSet(1) |
1147 | , m_ctxBar(ctxBar) { |
1148 | addItem(SkinTheme::get(this)->parts.dynamics()); |
1149 | } |
1150 | |
1151 | void () { |
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 | |
1190 | private: |
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> ; |
1220 | ContextBar* m_ctxBar; |
1221 | mutable tools::DynamicsOptions m_dynamics; |
1222 | bool m_optionsGridVisibility = true; |
1223 | }; |
1224 | |
1225 | class ContextBar::FreehandAlgorithmField : public CheckBox { |
1226 | public: |
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 | |
1247 | protected: |
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 | |
1266 | class ContextBar::SelectionModeField : public app::SelectionModeField { |
1267 | public: |
1268 | SelectionModeField() { } |
1269 | protected: |
1270 | void onSelectionModeChange(gen::SelectionMode mode) override { |
1271 | Preferences::instance().selection.mode(mode); |
1272 | } |
1273 | }; |
1274 | |
1275 | class ContextBar::GradientTypeField : public ButtonSet { |
1276 | public: |
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 | |
1298 | class ContextBar::DropPixelsField : public ButtonSet { |
1299 | public: |
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 | |
1316 | protected: |
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 | |
1327 | class ContextBar::EyedropperField : public HBox { |
1328 | public: |
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 | |
1361 | private: |
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 | |
1376 | class ContextBar::AutoSelectLayerField : public CheckBox { |
1377 | public: |
1378 | AutoSelectLayerField() |
1379 | : CheckBox(Strings::context_bar_auto_select_layer()) |
1380 | { |
1381 | initTheme(); |
1382 | } |
1383 | |
1384 | protected: |
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 | |
1406 | class ContextBar::SymmetryField : public ButtonSet { |
1407 | public: |
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 | |
1433 | private: |
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 ; |
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 | |
1477 | class 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 | |
1525 | public: |
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 | |
1611 | private: |
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 | |
1727 | ContextBar::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 | |
1803 | ContextBar::~ContextBar() |
1804 | { |
1805 | UIContext::instance()->remove_observer(this); |
1806 | App::instance()->activeToolManager()->remove_observer(this); |
1807 | } |
1808 | |
1809 | void 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 | |
1823 | void ContextBar::onSizeHint(SizeHintEvent& ev) |
1824 | { |
1825 | auto theme = SkinTheme::get(this); |
1826 | ev.setSizeHint(gfx::Size(0, theme->dimensions.contextBarHeight())); |
1827 | } |
1828 | |
1829 | void ContextBar::onToolSetOpacity(const int& newOpacity) |
1830 | { |
1831 | if (g_updatingFromCode) |
1832 | return; |
1833 | |
1834 | m_inkOpacity->setTextf("%d" , newOpacity); |
1835 | } |
1836 | |
1837 | void 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 | |
1846 | void 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 | |
1855 | void ContextBar::onActiveSiteChange(const Site& site) |
1856 | { |
1857 | DocObserverWidget<ui::HBox>::onActiveSiteChange(site); |
1858 | if (m_sliceFields->isVisible()) |
1859 | updateSliceFields(site); |
1860 | } |
1861 | |
1862 | void ContextBar::onDocChange(Doc* doc) |
1863 | { |
1864 | DocObserverWidget<ui::HBox>::onDocChange(doc); |
1865 | m_sliceFields->setDoc(doc); |
1866 | } |
1867 | |
1868 | void ContextBar::onAddSlice(DocEvent& ev) |
1869 | { |
1870 | if (ev.slice()) |
1871 | m_sliceFields->addSlice(ev.slice()); |
1872 | } |
1873 | |
1874 | void ContextBar::onRemoveSlice(DocEvent& ev) |
1875 | { |
1876 | if (ev.slice()) |
1877 | m_sliceFields->removeSlice(ev.slice()); |
1878 | } |
1879 | |
1880 | void ContextBar::onSliceNameChange(DocEvent& ev) |
1881 | { |
1882 | if (ev.slice()) |
1883 | m_sliceFields->updateSlice(ev.slice()); |
1884 | } |
1885 | |
1886 | void ContextBar::onBrushSizeChange() |
1887 | { |
1888 | if (m_activeBrush->type() != kImageBrushType) |
1889 | discardActiveBrush(); |
1890 | |
1891 | updateForActiveTool(); |
1892 | } |
1893 | |
1894 | void ContextBar::onBrushAngleChange() |
1895 | { |
1896 | if (m_activeBrush->type() != kImageBrushType) |
1897 | discardActiveBrush(); |
1898 | } |
1899 | |
1900 | void ContextBar::onActiveToolChange(tools::Tool* tool) |
1901 | { |
1902 | if (m_activeBrush->type() != kImageBrushType) |
1903 | setActiveBrush(ContextBar::createBrushFromPreferences()); |
1904 | else { |
1905 | updateForTool(tool); |
1906 | } |
1907 | } |
1908 | |
1909 | void ContextBar::onSymmetryModeChange() |
1910 | { |
1911 | updateForActiveTool(); |
1912 | } |
1913 | |
1914 | void 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 | |
1935 | void ContextBar::onDropPixels(ContextBarObserver::DropAction action) |
1936 | { |
1937 | notify_observers(&ContextBarObserver::onDropPixels, action); |
1938 | } |
1939 | |
1940 | void 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 | |
1949 | void ContextBar::updateForActiveTool() |
1950 | { |
1951 | updateForTool(App::instance()->activeTool()); |
1952 | } |
1953 | |
1954 | void 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 | |
2152 | void 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 | |
2167 | void 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 | |
2178 | void 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 | |
2191 | bool 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 | |
2211 | void ContextBar::updateAutoSelectLayer(bool state) |
2212 | { |
2213 | m_autoSelectLayer->setSelected(state); |
2214 | } |
2215 | |
2216 | bool ContextBar::isAutoSelectLayer() const |
2217 | { |
2218 | return m_autoSelectLayer->isSelected(); |
2219 | } |
2220 | |
2221 | void 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 | |
2311 | void 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 | |
2330 | doc::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 | |
2355 | void ContextBar::discardActiveBrush() |
2356 | { |
2357 | setActiveBrush(ContextBar::createBrushFromPreferences()); |
2358 | } |
2359 | |
2360 | // static |
2361 | doc::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 | |
2377 | BrushSlot 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 | |
2407 | Shade ContextBar::getShade() const |
2408 | { |
2409 | return m_inkShades->getShade(); |
2410 | } |
2411 | |
2412 | doc::Remap* ContextBar::createShadeRemap(bool left) |
2413 | { |
2414 | return m_inkShades->createShadeRemap(left); |
2415 | } |
2416 | |
2417 | void ContextBar::reverseShadeColors() |
2418 | { |
2419 | m_inkShades->reverseShadeColors(); |
2420 | } |
2421 | |
2422 | void ContextBar::setInkType(tools::InkType type) |
2423 | { |
2424 | m_inkType->setInkType(type); |
2425 | } |
2426 | |
2427 | render::DitheringMatrix ContextBar::ditheringMatrix() |
2428 | { |
2429 | return m_ditheringSelector->ditheringMatrix(); |
2430 | } |
2431 | |
2432 | render::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 | |
2454 | render::GradientType ContextBar::gradientType() |
2455 | { |
2456 | return m_gradientType->gradientType(); |
2457 | } |
2458 | |
2459 | const tools::DynamicsOptions& ContextBar::getDynamics() const |
2460 | { |
2461 | return m_dynamics->getDynamics(); |
2462 | } |
2463 | |
2464 | void 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 | |
2506 | void 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 | |
2521 | void ContextBar::showBrushes() |
2522 | { |
2523 | if (m_brushType->isVisible()) |
2524 | m_brushType->switchPopup(); |
2525 | } |
2526 | |
2527 | void ContextBar::showDynamics() |
2528 | { |
2529 | if (m_dynamics->isVisible()) |
2530 | m_dynamics->switchPopup(); |
2531 | } |
2532 | |
2533 | bool 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 | |