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/app.h"
13#include "app/commands/commands.h"
14#include "app/commands/params.h"
15#include "app/context_access.h"
16#include "app/doc_access.h"
17#include "app/doc_event.h"
18#include "app/doc_range.h"
19#include "app/i18n/strings.h"
20#include "app/modules/editors.h"
21#include "app/modules/gfx.h"
22#include "app/modules/gui.h"
23#include "app/modules/palettes.h"
24#include "app/pref/preferences.h"
25#include "app/tools/active_tool.h"
26#include "app/tools/tool.h"
27#include "app/ui/button_set.h"
28#include "app/ui/color_button.h"
29#include "app/ui/editor/editor.h"
30#include "app/ui/keyboard_shortcuts.h"
31#include "app/ui/main_window.h"
32#include "app/ui/skin/skin_theme.h"
33#include "app/ui/status_bar.h"
34#include "app/ui/timeline/timeline.h"
35#include "app/ui/toolbar.h"
36#include "app/ui/zoom_entry.h"
37#include "app/ui_context.h"
38#include "app/util/range_utils.h"
39#include "base/fs.h"
40#include "base/string.h"
41#include "doc/image.h"
42#include "doc/layer.h"
43#include "doc/sprite.h"
44#include "fmt/format.h"
45#include "gfx/size.h"
46#include "os/font.h"
47#include "os/surface.h"
48#include "ui/ui.h"
49#include "ver/info.h"
50
51#include <algorithm>
52#include <cstdarg>
53#include <cstdio>
54#include <cstring>
55#include <limits>
56#include <vector>
57
58namespace app {
59
60using namespace app::skin;
61using namespace gfx;
62using namespace ui;
63using namespace doc;
64
65class StatusBar::AboutStatusBar : public HBox {
66public:
67 AboutStatusBar()
68 : m_label(fmt::format("{} {} by ", get_app_name(), get_app_version()))
69 , m_link("", "Igara Studio")
70 {
71 m_link.Click.connect(
72 []{
73 Command* cmd = Commands::instance()->byId(CommandId::About());
74 UIContext::instance()->executeCommandFromMenuOrShortcut(cmd);
75 });
76
77 addChild(new BoxFiller);
78 addChild(&m_label);
79 addChild(&m_link);
80 addChild(new BoxFiller);
81
82 InitTheme.connect(
83 [this]{
84 auto theme = SkinTheme::get(this);
85 ui::Style* style = theme->styles.workspaceLink();
86 noBorderNoChildSpacing();
87 m_label.setStyle(style);
88 m_link.setStyle(style);
89 m_label.noBorderNoChildSpacing();
90 m_link.noBorderNoChildSpacing();
91 });
92 initTheme();
93 }
94
95 ui::Label m_label;
96 ui::LinkLabel m_link;
97};
98
99class StatusBar::Indicators : public HBox {
100
101 class Indicator : public Widget {
102 public:
103 enum IndicatorType {
104 kText,
105 kIcon,
106 kColor,
107 kTile
108 };
109 Indicator(IndicatorType type) : m_type(type) { }
110 IndicatorType indicatorType() const { return m_type; }
111 private:
112 IndicatorType m_type;
113 };
114
115 class TextIndicator : public Indicator {
116 public:
117 TextIndicator(const char* text) : Indicator(kText) {
118 updateIndicator(text);
119 }
120
121 bool updateIndicator(const char* text) {
122 if (this->text() == text)
123 return false;
124
125 setText(text);
126
127 if (minSize().w > textSize().w*2)
128 setMinSize(textSize());
129 else
130 setMinSize(minSize().createUnion(textSize()));
131 return true;
132 }
133
134 private:
135 void onPaint(ui::PaintEvent& ev) override {
136 auto theme = SkinTheme::get(this);
137 gfx::Color textColor = theme->colors.statusBarText();
138 Rect rc = clientBounds();
139 Graphics* g = ev.graphics();
140
141 g->fillRect(bgColor(), rc);
142 if (!text().empty()) {
143 g->drawText(text(), textColor, ColorNone,
144 Point(rc.x, rc.y + rc.h/2 - font()->height()/2));
145 }
146 }
147 };
148
149 class IconIndicator : public Indicator {
150 public:
151 IconIndicator(const skin::SkinPartPtr& part, bool colored)
152 : Indicator(kIcon)
153 , m_part(part)
154 , m_colored(colored) {
155 InitTheme.connect(
156 [this]{
157 updateIndicator();
158 });
159 initTheme();
160 }
161
162 bool updateIndicator(const skin::SkinPartPtr& part, bool colored) {
163 if (m_part.get() == part.get() &&
164 m_colored == colored)
165 return false;
166
167 ASSERT(part);
168 m_part = part;
169 m_colored = colored;
170 updateIndicator();
171 return true;
172 }
173
174 private:
175 void updateIndicator() {
176 setMinSize(
177 minSize().createUnion(Size(m_part->bitmap(0)->width(),
178 m_part->bitmap(0)->height())));
179 }
180
181 void onPaint(ui::PaintEvent& ev) override {
182 auto theme = SkinTheme::get(this);
183 gfx::Color textColor = theme->colors.statusBarText();
184 Rect rc = clientBounds();
185 Graphics* g = ev.graphics();
186 os::Surface* icon = m_part->bitmap(0);
187
188 g->fillRect(bgColor(), rc);
189 if (m_colored)
190 g->drawColoredRgbaSurface(
191 icon, textColor,
192 rc.x, rc.y + rc.h/2 - icon->height()/2);
193 else
194 g->drawRgbaSurface(
195 icon, rc.x, rc.y + rc.h/2 - icon->height()/2);
196 }
197
198 skin::SkinPartPtr m_part;
199 bool m_colored;
200 };
201
202 class ColorIndicator : public Indicator {
203 public:
204 ColorIndicator(const app::Color& color)
205 : Indicator(kColor)
206 , m_color(Color::fromMask()) {
207 updateIndicator(color, true);
208 }
209
210 bool updateIndicator(const app::Color& color, bool first = false) {
211 if (m_color == color && !first)
212 return false;
213
214 m_color = color;
215 setMinSize(minSize().createUnion(Size(32*guiscale(), 1)));
216 return true;
217 }
218
219 private:
220 void onPaint(ui::PaintEvent& ev) override {
221 Rect rc = clientBounds();
222 Graphics* g = ev.graphics();
223
224 g->fillRect(bgColor(), rc);
225 draw_color_button(
226 g, Rect(rc.x, rc.y, 32*guiscale(), rc.h),
227 m_color,
228 (doc::ColorMode)app_get_current_pixel_format(), false, false);
229 }
230
231 app::Color m_color;
232 };
233
234 class TileIndicator : public Indicator {
235 public:
236 TileIndicator(doc::tile_t tile)
237 : Indicator(kTile)
238 , m_tile(doc::notile) {
239 updateIndicator(tile, true);
240 }
241
242 bool updateIndicator(doc::tile_t tile, bool first = false) {
243 if (m_tile == tile && !first)
244 return false;
245
246 m_tile = tile;
247 setMinSize(minSize().createUnion(Size(32*guiscale(), 1)));
248 return true;
249 }
250
251 private:
252 void onPaint(ui::PaintEvent& ev) override {
253 Rect rc = clientBounds();
254 Graphics* g = ev.graphics();
255
256 // TODO could the site came from the Indicators or StatusBar itself
257 Site site = UIContext::instance()->activeSite();
258
259 g->fillRect(bgColor(), rc);
260 draw_tile_button(
261 g, Rect(rc.x, rc.y, 32*guiscale(), rc.h),
262 site, m_tile,
263 false, false);
264 }
265
266 doc::tile_t m_tile;
267 };
268
269public:
270
271 Indicators()
272 : m_backupIcon(BackupIcon::None)
273 , m_redraw(true) {
274 m_leftArea.setBorder(gfx::Border(0));
275 m_leftArea.setVisible(true);
276 m_leftArea.setExpansive(true);
277
278 m_rightArea.setBorder(gfx::Border(0));
279 m_rightArea.setVisible(false);
280
281 addChild(&m_leftArea);
282 addChild(&m_rightArea);
283 }
284
285 void startIndicators() {
286 m_iterator = m_indicators.begin();
287 }
288
289 void endIndicators() {
290 removeAllNextIndicators();
291 if (m_redraw) {
292 m_redraw = false;
293 layout();
294 }
295 }
296
297 void addTextIndicator(const char* text) {
298 // Re-use indicator
299 if (m_iterator != m_indicators.end()) {
300 if ((*m_iterator)->indicatorType() == Indicator::kText) {
301 m_redraw |= static_cast<TextIndicator*>(*m_iterator)
302 ->updateIndicator(text);
303 ++m_iterator;
304 return;
305 }
306 else {
307 removeAllNextIndicators();
308 }
309 }
310
311 auto indicator = new TextIndicator(text);
312 m_indicators.push_back(indicator);
313 m_iterator = m_indicators.end();
314 m_leftArea.addChild(indicator);
315 m_redraw = true;
316 }
317
318 void addIconIndicator(const skin::SkinPartPtr& part, bool colored) {
319 if (m_iterator != m_indicators.end()) {
320 if ((*m_iterator)->indicatorType() == Indicator::kIcon) {
321 m_redraw |= static_cast<IconIndicator*>(*m_iterator)
322 ->updateIndicator(part, colored);
323 ++m_iterator;
324 return;
325 }
326 else
327 removeAllNextIndicators();
328 }
329
330 auto indicator = new IconIndicator(part, colored);
331 m_indicators.push_back(indicator);
332 m_iterator = m_indicators.end();
333 m_leftArea.addChild(indicator);
334 m_redraw = true;
335 }
336
337 void addColorIndicator(const app::Color& color) {
338 if (m_iterator != m_indicators.end()) {
339 if ((*m_iterator)->indicatorType() == Indicator::kColor) {
340 m_redraw |= static_cast<ColorIndicator*>(*m_iterator)
341 ->updateIndicator(color);
342 ++m_iterator;
343 return;
344 }
345 else
346 removeAllNextIndicators();
347 }
348
349 auto indicator = new ColorIndicator(color);
350 m_indicators.push_back(indicator);
351 m_iterator = m_indicators.end();
352 m_leftArea.addChild(indicator);
353 m_redraw = true;
354 }
355
356 void addTileIndicator(doc::tile_t tile) {
357 if (m_iterator != m_indicators.end()) {
358 if ((*m_iterator)->indicatorType() == Indicator::kTile) {
359 m_redraw |= static_cast<TileIndicator*>(*m_iterator)
360 ->updateIndicator(tile);
361 ++m_iterator;
362 return;
363 }
364 else
365 removeAllNextIndicators();
366 }
367
368 auto indicator = new TileIndicator(tile);
369 m_indicators.push_back(indicator);
370 m_iterator = m_indicators.end();
371 m_leftArea.addChild(indicator);
372 m_redraw = true;
373 }
374
375 void showBackupIcon(BackupIcon icon) {
376 m_backupIcon = icon;
377 if (m_backupIcon != BackupIcon::None) {
378 auto theme = SkinTheme::get(this);
379 SkinPartPtr part =
380 (m_backupIcon == BackupIcon::Normal ?
381 theme->parts.iconSave():
382 theme->parts.iconSaveSmall());
383
384 m_rightArea.setVisible(true);
385 if (m_rightArea.children().empty()) {
386 m_rightArea.addChild(new IconIndicator(part, true));
387 }
388 else {
389 ((IconIndicator*)m_rightArea.lastChild())->updateIndicator(part, true);
390 }
391 }
392 else {
393 m_rightArea.setVisible(false);
394 }
395 layout();
396 }
397
398private:
399 void removeAllNextIndicators() {
400 auto it = m_iterator;
401 auto end = m_indicators.end();
402 if (m_iterator != end) {
403 for (; it != end; ++it) {
404 auto indicator = *it;
405 m_leftArea.removeChild(indicator);
406 delete indicator;
407 }
408 m_indicators.erase(m_iterator, end);
409 m_redraw = true;
410 }
411 }
412
413 std::vector<Indicator*> m_indicators;
414 std::vector<Indicator*>::iterator m_iterator;
415 BackupIcon m_backupIcon;
416 HBox m_leftArea;
417 HBox m_rightArea;
418 bool m_redraw;
419};
420
421class StatusBar::IndicatorsGeneration {
422public:
423 IndicatorsGeneration(StatusBar::Indicators* indicators)
424 : m_indicators(indicators) {
425 m_indicators->startIndicators();
426 }
427
428 ~IndicatorsGeneration() {
429 m_indicators->endIndicators();
430 }
431
432 IndicatorsGeneration& add(const char* text) {
433 auto theme = SkinTheme::get(m_indicators);
434
435 for (auto i = text; *i; ) {
436 // Icon
437 if (*i == ':' && (i == text || *(i-1) == ' ')) {
438 const char* j = i+1;
439 for (; *j; ++j) {
440 if (*j == ':')
441 break;
442 }
443
444 if (*j == ':' && (*(j+1) == 0 || *(j+1) == ' ')) {
445 if (i != text) {
446 // Here i is ':' and i-1 is a whitespace ' '
447 m_indicators->addTextIndicator(std::string(text, i-1).c_str());
448 }
449
450 auto part = theme->getPartById("icon_" + std::string(i+1, j));
451 if (part)
452 add(part, true);
453
454 text = i = (*(j+1) == ' ' ? j+2: j+1);
455 }
456 else
457 i = j;
458 }
459 else
460 ++i;
461 }
462
463 if (*text != 0)
464 m_indicators->addTextIndicator(text);
465
466 return *this;
467 }
468
469 IndicatorsGeneration& add(const skin::SkinPartPtr& part, bool colored) {
470 if (part.get())
471 m_indicators->addIconIndicator(part, colored);
472 return *this;
473 }
474
475 IndicatorsGeneration& add(const app::Color& color) {
476 auto theme = SkinTheme::get(m_indicators);
477
478 // Eyedropper icon
479 add(theme->getToolPart("eyedropper"), false);
480
481 // Color
482 m_indicators->addColorIndicator(color);
483
484 // Color description
485 std::string str = color.toHumanReadableString(
486 app_get_current_pixel_format(),
487 app::Color::LongHumanReadableString);
488 if (color.getAlpha() < 255)
489 str += fmt::format(" A{}", color.getAlpha());
490 m_indicators->addTextIndicator(str.c_str());
491
492 return *this;
493 }
494
495 IndicatorsGeneration& add(doc::tile_t tile) {
496 auto theme = SkinTheme::get(m_indicators);
497
498 // Eyedropper icon
499 add(theme->getToolPart("eyedropper"), false);
500
501 // Color
502 m_indicators->addTileIndicator(tile);
503
504 // Color description
505 std::string str;
506 if (tile == doc::notile) {
507 str += "Empty";
508 }
509 else {
510 // TODO could the site came from the Indicators or StatusBar itself
511 int baseIndex = 1;
512 Site site = UIContext::instance()->activeSite();
513 if (site.tileset())
514 baseIndex = site.tileset()->baseIndex();
515
516 doc::tile_index ti = doc::tile_geti(tile);
517 doc::tile_flags tf = doc::tile_getf(tile);
518 if (baseIndex < 0)
519 str += fmt::format("{}", ((int)ti) + baseIndex - 1);
520 else
521 str += fmt::format("{}", ti + baseIndex - 1);
522 if (tf) {
523 if (tf & doc::tile_f_flipx) str += " FlipX";
524 if (tf & doc::tile_f_flipy) str += " FlipY";
525 if (tf & doc::tile_f_90cw) str += " Rot90CW";
526 }
527 }
528 m_indicators->addTextIndicator(str.c_str());
529
530 return *this;
531 }
532
533 IndicatorsGeneration& add(tools::Tool* tool) {
534 auto theme = SkinTheme::get(m_indicators);
535
536 // Tool icon + text
537 add(theme->getToolPart(tool->getId().c_str()), false);
538 m_indicators->addTextIndicator(tool->getText().c_str());
539
540 // Tool shortcut
541 KeyPtr key = KeyboardShortcuts::instance()->tool(tool);
542 if (key && !key->accels().empty()) {
543 add(theme->parts.iconKey(), true);
544 m_indicators->addTextIndicator(key->accels().front().toString().c_str());
545 }
546 return *this;
547 }
548
549private:
550 StatusBar::Indicators* m_indicators;
551};
552
553class StatusBar::CustomizedTipWindow : public ui::TipWindow {
554public:
555 CustomizedTipWindow(const std::string& text)
556 : ui::TipWindow(text) {
557 }
558
559 void setInterval(int msecs) {
560 if (!m_timer)
561 m_timer.reset(new ui::Timer(msecs, this));
562 else
563 m_timer->setInterval(msecs);
564 }
565
566 void startTimer() {
567 m_timer->start();
568 }
569
570protected:
571 bool onProcessMessage(Message* msg) override {
572 switch (msg->type()) {
573 case kTimerMessage:
574 closeWindow(nullptr);
575 m_timer->stop();
576 break;
577 }
578 return ui::TipWindow::onProcessMessage(msg);
579 }
580
581private:
582 std::unique_ptr<ui::Timer> m_timer;
583};
584
585// TODO Use a ui::TipWindow with rounded borders, when we add support
586// to invalidate transparent windows.
587class StatusBar::SnapToGridWindow : public ui::PopupWindow {
588public:
589 SnapToGridWindow()
590 : ui::PopupWindow("", ClickBehavior::DoNothingOnClick)
591 , m_button(Strings::statusbar_tips_disable_snap_grid()) {
592 InitTheme.connect(
593 [this]{
594 setBorder(gfx::Border(2 * guiscale()));
595 setBgColor(gfx::rgba(255, 255, 200));
596 });
597 initTheme();
598 makeFloating();
599
600 addChild(&m_button);
601 m_button.Click.connect([this]{ onDisableSnapToGrid(); });
602 }
603
604 void setDocument(Doc* doc) {
605 m_doc = doc;
606 }
607
608private:
609 void onDisableSnapToGrid() {
610 Preferences::instance().document(m_doc).grid.snap(false);
611 closeWindow(nullptr);
612 }
613
614 Doc* m_doc;
615 ui::Button m_button;
616};
617
618// This widget is used to show the current frame.
619class GotoFrameEntry : public Entry {
620public:
621 GotoFrameEntry() : Entry(4, "") {
622 }
623
624 bool onProcessMessage(Message* msg) override {
625 switch (msg->type()) {
626
627 // When the mouse enter in this entry, it got the focus and the
628 // text is automatically selected.
629 case kMouseEnterMessage:
630 if (Preferences::instance().statusBar.focusFrameFieldOnMouseover()) {
631 requestFocus();
632 selectAllText();
633 }
634 break;
635
636 case kKeyDownMessage: {
637 KeyMessage* keymsg = static_cast<KeyMessage*>(msg);
638 KeyScancode scancode = keymsg->scancode();
639
640 if (hasFocus() &&
641 (scancode == kKeyEnter || // TODO customizable keys
642 scancode == kKeyEnterPad)) {
643 Command* cmd = Commands::instance()->byId(CommandId::GotoFrame());
644 Params params;
645 params.set("frame", text().c_str());
646 UIContext::instance()->executeCommandFromMenuOrShortcut(cmd, params);
647
648 // Select the text again
649 selectAllText();
650 releaseFocus();
651 return true; // Key used.
652 }
653 break;
654 }
655 }
656
657 bool result = Entry::onProcessMessage(msg);
658
659 if (msg->type() == kMouseDownMessage)
660 selectText(0, -1);
661
662 return result;
663 }
664
665};
666
667StatusBar* StatusBar::m_instance = NULL;
668
669StatusBar::StatusBar(TooltipManager* tooltipManager)
670 : m_timeout(0)
671 , m_about(new AboutStatusBar)
672 , m_indicators(new Indicators)
673 , m_docControls(new HBox)
674 , m_tipwindow(nullptr)
675 , m_snapToGridWindow(nullptr)
676{
677 m_instance = this;
678
679 setDoubleBuffered(true);
680 setFocusStop(true);
681
682 m_about->setExpansive(true);
683 m_about->setVisible(true);
684 m_indicators->setExpansive(true);
685 m_indicators->setVisible(false);
686 m_docControls->setVisible(false);
687 addChild(m_about);
688 addChild(m_indicators);
689 addChild(m_docControls);
690
691 // Construct the commands box
692 {
693 Box* box1 = new Box(HORIZONTAL);
694 Box* box4 = new Box(HORIZONTAL);
695
696 m_frameLabel = new Label(Strings::statusbar_tips_frame());
697 m_currentFrame = new GotoFrameEntry();
698 m_newFrame = new Button("+");
699 m_newFrame->Click.connect([this]{ newFrame(); });
700 m_zoomEntry = new ZoomEntry;
701 m_zoomEntry->ZoomChange.connect(&StatusBar::onChangeZoom, this);
702
703 setup_mini_look(m_currentFrame);
704
705 box4->addChild(m_currentFrame);
706 box4->addChild(m_newFrame);
707
708 box1->addChild(m_frameLabel);
709 box1->addChild(box4);
710 box1->addChild(m_zoomEntry);
711
712 m_docControls->addChild(box1);
713 m_commandsBox = box1;
714 }
715
716 // Tooltips
717 tooltipManager->addTooltipFor(
718 m_currentFrame, Strings::statusbar_tips_current_frame(), BOTTOM);
719 tooltipManager->addTooltipFor(
720 m_zoomEntry, Strings::statusbar_tips_zoom_level(), BOTTOM);
721 tooltipManager->addTooltipFor(
722 m_newFrame, Strings::statusbar_tips_new_frame(), BOTTOM);
723
724 App::instance()->activeToolManager()->add_observer(this);
725
726 initTheme();
727}
728
729StatusBar::~StatusBar()
730{
731 App::instance()->activeToolManager()->remove_observer(this);
732
733 delete m_tipwindow; // widget
734 delete m_snapToGridWindow;
735}
736
737void StatusBar::onSelectedToolChange(tools::Tool* tool)
738{
739 if (isVisible() && tool)
740 showTool(500, tool);
741}
742
743void StatusBar::clearText()
744{
745 showIndicators();
746 setStatusText(1, std::string());
747}
748
749// TODO Workspace views should have a method to set the default status
750// bar text, because here the StatusBar is depending on too many
751// details of the main window/docs/etc.
752void StatusBar::showDefaultText()
753{
754 auto mainWindow = (App::instance() ? App::instance()->mainWindow(): nullptr);
755 if (mainWindow)
756 mainWindow->showDefaultStatusBar();
757 else
758 clearText();
759}
760
761void StatusBar::showDefaultText(Doc* doc)
762{
763 clearText();
764 if (doc) {
765 std::string buf =
766 fmt::format("{} :size: {} {}",
767 doc->name(), doc->width(), doc->height());
768 if (doc->getTransformation().bounds().w != 0) {
769 buf += fmt::format(" :selsize: {} {}",
770 int(doc->getTransformation().bounds().w),
771 int(doc->getTransformation().bounds().h));
772 }
773 if (Preferences::instance().general.showFullPath()) {
774 std::string path = base::get_file_path(doc->filename());
775 if (!path.empty())
776 buf += fmt::format(" ({})", path);
777 }
778
779 setStatusText(1, buf);
780 }
781}
782
783void StatusBar::updateFromEditor(Editor* editor)
784{
785 if (editor) {
786 showIndicators();
787 m_zoomEntry->setZoom(editor->zoom());
788 }
789}
790
791void StatusBar::showBackupIcon(BackupIcon icon)
792{
793 showIndicators();
794 m_indicators->showBackupIcon(icon);
795}
796
797bool StatusBar::setStatusText(int msecs, const std::string& msg)
798{
799 if ((base::current_tick() > m_timeout) || (msecs > 0)) {
800 showIndicators();
801 IndicatorsGeneration(m_indicators).add(msg.c_str());
802 m_timeout = base::current_tick() + msecs;
803 return true;
804 }
805 else
806 return false;
807}
808
809void StatusBar::showTip(int msecs, const std::string& msg)
810{
811 ASSERT(msecs > 0);
812
813 if (m_tipwindow == NULL) {
814 m_tipwindow = new CustomizedTipWindow(msg);
815 }
816 else {
817 m_tipwindow->setText(msg);
818 }
819
820 m_tipwindow->setInterval(msecs);
821
822 if (m_tipwindow->isVisible())
823 m_tipwindow->closeWindow(nullptr);
824
825 m_tipwindow->remapWindow();
826
827 gfx::Rect rc = m_tipwindow->bounds();
828 rc.x = bounds().x2() - rc.w;
829 rc.y = bounds().y - rc.h;
830 ui::fit_bounds(display(), m_tipwindow, rc);
831
832 m_tipwindow->openWindow();
833 m_tipwindow->startTimer();
834
835 // Set the text in status-bar (with inmediate timeout)
836 IndicatorsGeneration(m_indicators).add(msg.c_str());
837 m_timeout = base::current_tick();
838}
839
840void StatusBar::showColor(int msecs, const app::Color& color,
841 const std::string& text)
842{
843 if ((base::current_tick() > m_timeout) || (msecs > 0)) {
844 showIndicators();
845 IndicatorsGeneration gen(m_indicators);
846 gen.add(color);
847 if (!text.empty())
848 gen.add(text.c_str());
849
850 m_timeout = base::current_tick() + msecs;
851 }
852}
853
854void StatusBar::showTile(int msecs, doc::tile_t tile,
855 const std::string& text)
856{
857 if ((base::current_tick() > m_timeout) || (msecs > 0)) {
858 IndicatorsGeneration gen(m_indicators);
859 gen.add(tile);
860 if (!text.empty())
861 gen.add(text.c_str());
862
863 m_timeout = base::current_tick() + msecs;
864 }
865}
866
867void StatusBar::showTool(int msecs, tools::Tool* tool)
868{
869 showIndicators();
870
871 ASSERT(tool != nullptr);
872 IndicatorsGeneration(m_indicators).add(tool);
873
874 m_timeout = base::current_tick() + msecs;
875}
876
877void StatusBar::showSnapToGridWarning(bool state)
878{
879 if (state) {
880 // this->doc() can be nullptr if "snap to grid" command is pressed
881 // without an opened document. (E.g. to change the default
882 // setting)
883 if (!doc())
884 return;
885
886 if (!m_snapToGridWindow)
887 m_snapToGridWindow = new SnapToGridWindow;
888
889 m_snapToGridWindow->setDisplay(display(), false);
890
891 if (!m_snapToGridWindow->isVisible()) {
892 m_snapToGridWindow->openWindow();
893 m_snapToGridWindow->remapWindow();
894 updateSnapToGridWindowPosition();
895 }
896
897 m_snapToGridWindow->setDocument(doc());
898 }
899 else {
900 if (m_snapToGridWindow)
901 m_snapToGridWindow->closeWindow(nullptr);
902 }
903}
904
905//////////////////////////////////////////////////////////////////////
906// StatusBar message handler
907
908void StatusBar::onInitTheme(ui::InitThemeEvent& ev)
909{
910 HBox::onInitTheme(ev);
911
912 auto theme = SkinTheme::get(this);
913 setBgColor(theme->colors.statusBarFace());
914 setBorder(gfx::Border(6*guiscale(), 0, 6*guiscale(), 0));
915 setMinSize(Size(0, textHeight()+8*guiscale()));
916 setMaxSize(Size(std::numeric_limits<int>::max(),
917 textHeight()+8*guiscale()));
918
919 m_newFrame->setStyle(theme->styles.newFrameButton());
920 m_commandsBox->setBorder(gfx::Border(2, 1, 2, 2)*guiscale());
921
922 if (m_snapToGridWindow) {
923 m_snapToGridWindow->initTheme();
924 if (m_snapToGridWindow->isVisible())
925 updateSnapToGridWindowPosition();
926 }
927}
928
929void StatusBar::onResize(ResizeEvent& ev)
930{
931 Rect rc = ev.bounds();
932 m_docControls->setVisible(doc() && rc.w > 300*ui::guiscale());
933
934 HBox::onResize(ev);
935
936 if (m_snapToGridWindow &&
937 m_snapToGridWindow->isVisible())
938 updateSnapToGridWindowPosition();
939}
940
941void StatusBar::onActiveSiteChange(const Site& site)
942{
943 DocObserverWidget<ui::HBox>::onActiveSiteChange(site);
944
945 const bool controlsWereVisible = m_docControls->isVisible();
946
947 if (doc()) {
948 auto& docPref = Preferences::instance().document(doc());
949
950 m_docControls->setVisible(true);
951 showSnapToGridWarning(docPref.grid.snap());
952
953 // Current frame
954 {
955 std::string newText =
956 fmt::format("{}", site.frame()+docPref.timeline.firstFrame());
957 if (m_currentFrame->text() != newText)
958 m_currentFrame->setText(newText);
959 }
960
961 // Zoom level
962 if (current_editor)
963 updateFromEditor(current_editor);
964 }
965 else {
966 m_docControls->setVisible(false);
967 showSnapToGridWarning(false);
968 }
969
970 // Relayout the StatusBar so we can put the m_docControls widget in
971 // the right place now that it's visibility changed.
972 if (m_docControls->isVisible() != controlsWereVisible)
973 layout();
974}
975
976void StatusBar::onPixelFormatChanged(DocEvent& ev)
977{
978 // If this is called from the non-UI thread it means that the pixel
979 // format change was made in the background,
980 // i.e. ChangePixelFormatCommand uses a background thread to change
981 // the sprite format.
982 if (!ui::is_ui_thread())
983 return;
984
985 onActiveSiteChange(UIContext::instance()->activeSite());
986}
987
988void StatusBar::newFrame()
989{
990 Command* cmd = Commands::instance()->byId(CommandId::NewFrame());
991 UIContext::instance()->executeCommandFromMenuOrShortcut(cmd);
992}
993
994void StatusBar::onChangeZoom(const render::Zoom& zoom)
995{
996 if (current_editor)
997 current_editor->setEditorZoom(zoom);
998}
999
1000void StatusBar::updateSnapToGridWindowPosition()
1001{
1002 ASSERT(m_snapToGridWindow);
1003
1004 Rect rc = bounds();
1005 int toolBarWidth = ToolBar::instance()->sizeHint().w;
1006
1007 gfx::Rect snapRc = m_snapToGridWindow->bounds();
1008 snapRc.x = rc.x+rc.w-toolBarWidth-snapRc.w;
1009 snapRc.y = rc.y-snapRc.h;
1010 m_snapToGridWindow->setBounds(snapRc);
1011}
1012
1013void StatusBar::showAbout()
1014{
1015 if (!m_about->isVisible()) {
1016 m_indicators->setVisible(false);
1017 m_about->setVisible(true);
1018 m_about->layout();
1019 }
1020}
1021
1022void StatusBar::showIndicators()
1023{
1024 if (!m_indicators->isVisible()) {
1025 m_about->setVisible(false);
1026 m_indicators->setVisible(true);
1027 m_indicators->layout();
1028 }
1029}
1030
1031} // namespace app
1032