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/console.h"
13
14#include "app/app.h"
15#include "app/context.h"
16#include "app/i18n/strings.h"
17#include "app/modules/gui.h"
18#include "app/ui/main_window.h"
19#include "app/ui/status_bar.h"
20#include "base/memory.h"
21#include "base/string.h"
22#include "ui/system.h"
23#include "ui/ui.h"
24
25#include <cstdarg>
26#include <cstdio>
27#include <memory>
28
29#define TRACE_CON(...) // TRACEARGS(__VA_ARGS__)
30
31namespace app {
32
33using namespace ui;
34
35Console::ConsoleWindow* Console::m_console = nullptr;
36
37class Console::ConsoleWindow final : public Window {
38public:
39 ConsoleWindow() : Window(Window::WithTitleBar, Strings::debugger_console()),
40 m_textbox("", WORDWRAP),
41 m_button(Strings::debugger_cancel()) {
42 TRACE_CON("CON: ConsoleWindow this=", this);
43
44 m_button.Click.connect([this]{ closeWindow(&m_button); });
45
46 // When the main window is closed, we should close the console (in
47 // other case the main message loop will continue running for the
48 // console too).
49 m_mainWindowClosedConn =
50 App::instance()->mainWindow()->Close.connect(
51 [this]{ closeWindow(nullptr); });
52
53 // When the window is closed, we clear the text
54 Close.connect(
55 [this]{
56 m_mainWindowClosedConn.disconnect();
57 m_textbox.setText(std::string());
58
59 Console::m_console->deferDelete();
60 Console::m_console = nullptr;
61 TRACE_CON("CON: Close signal");
62 });
63
64 m_view.attachToView(&m_textbox);
65
66 ui::Grid* grid = new ui::Grid(1, false);
67 grid->addChildInCell(&m_view, 1, 1, HORIZONTAL | VERTICAL);
68 grid->addChildInCell(&m_button, 1, 1, CENTER);
69 addChild(grid);
70
71 m_textbox.setFocusMagnet(true);
72 m_button.setFocusMagnet(true);
73 m_view.setExpansive(true);
74
75 initTheme();
76 }
77
78 ~ConsoleWindow() {
79 TRACE_CON("CON: ~ConsoleWindow this=", this);
80 }
81
82 void addMessage(const std::string& msg) {
83 if (!m_hasText) {
84 m_hasText = true;
85 centerConsole();
86 }
87
88 gfx::Size maxSize = m_view.getScrollableSize();
89 gfx::Size visible = m_view.visibleSize();
90 gfx::Point pt = m_view.viewScroll();
91 const bool autoScroll = (pt.y >= maxSize.h - visible.h);
92
93 m_textbox.setText(m_textbox.text() + msg);
94
95 if (autoScroll) {
96 maxSize = m_view.getScrollableSize();
97 visible = m_view.visibleSize();
98 pt.y = maxSize.h - visible.h;
99 m_view.setViewScroll(pt);
100 }
101 }
102
103 bool hasConsoleText() const {
104 return m_hasText;
105 }
106
107 void centerConsole() {
108 initTheme();
109
110 Display* display = ui::Manager::getDefault()->display();
111 const gfx::Rect displayRc = display->bounds();
112 gfx::Rect rc;
113 rc.w = displayRc.w*9/10;
114 rc.h = displayRc.h*6/10;
115 rc.x = displayRc.x + displayRc.w/2 - rc.w/2;
116 rc.y = displayRc.y + displayRc.h/2 - rc.h/2;
117
118 ui::fit_bounds(display, this, rc);
119 }
120
121private:
122 // As Esc key activates the close button only on foreground windows,
123 // we have to override this method to allow pressing the window
124 // close button using Esc key even in this window (which runs in the
125 // background).
126 bool shouldProcessEscKeyToCloseWindow() const override {
127 return true;
128 }
129
130 bool onProcessMessage(ui::Message* msg) override {
131 switch (msg->type()) {
132
133 case ui::kKeyDownMessage: {
134 auto scancode = static_cast<KeyMessage*>(msg)->scancode();
135
136#if defined __APPLE__
137 if (msg->onlyCmdPressed())
138#else
139 if (msg->onlyCtrlPressed())
140#endif
141 {
142 if (scancode == kKeyC)
143 set_clipboard_text(m_textbox.text());
144 }
145
146 // Esc to close the window.
147 if (auto closeButton = this->closeButton()) {
148 bool p = msg->propagateToParent();
149 msg->setPropagateToParent(false);
150
151 if (closeButton->sendMessage(msg))
152 return true;
153
154 msg->setPropagateToParent(p);
155 }
156
157 // Send Enter key to the Close button, Tab to change focus
158 if ((scancode == kKeyEnter) ||
159 (scancode == kKeyEnterPad))
160 return m_button.sendMessage(msg);
161
162 if (scancode == kKeyTab) {
163 if (auto mgr = manager())
164 return mgr->processFocusMovementMessage(msg);
165 }
166
167 // All keys are used if we have this window focused (so they
168 // don't trigger commands)
169 return true;
170 }
171
172 case ui::kKeyUpMessage:
173 if (auto closeButton = this->closeButton()) {
174 bool p = msg->propagateToParent();
175 msg->setPropagateToParent(false);
176
177 if (closeButton->sendMessage(msg))
178 return true;
179
180 msg->setPropagateToParent(p);
181 }
182 break;
183 }
184 return Window::onProcessMessage(msg);
185 }
186
187 void onInitTheme(InitThemeEvent& ev) override {
188 Window::onInitTheme(ev);
189
190 m_button.setMinSize(gfx::Size(60*ui::guiscale(), 0));
191 }
192
193 obs::scoped_connection m_mainWindowClosedConn;
194 View m_view;
195 TextBox m_textbox;
196 Button m_button;
197 bool m_hasText = false;
198};
199
200Console::Console(Context* ctx)
201 : m_withUI(false)
202{
203 TRACE_CON("CON: Console this=", this, "ctx=", ctx, "is_ui_thread=", ui::is_ui_thread(), "{");
204
205 if (!ui::is_ui_thread())
206 return;
207
208 if (ctx) {
209 m_withUI = (ctx->isUIAvailable());
210 }
211 else {
212 m_withUI =
213 (App::instance() &&
214 App::instance()->isGui() &&
215 Manager::getDefault() &&
216 Manager::getDefault()->display()->nativeWindow());
217 }
218
219 if (!m_withUI)
220 return;
221
222 TRACE_CON("CON: -> withUI=", m_withUI);
223
224 if (!m_console)
225 m_console = new ConsoleWindow;
226}
227
228Console::~Console()
229{
230 TRACE_CON("CON: } ~Console this=", this, "withUI=", m_withUI);
231
232 if (!m_withUI)
233 return;
234
235 if (m_console &&
236 m_console->hasConsoleText() &&
237 !m_console->isVisible()) {
238 m_console->manager()->attractFocus(m_console);
239 m_console->openWindow();
240 TRACE_CON("CON: openWindow");
241 }
242}
243
244void Console::printf(const char* format, ...)
245{
246 std::va_list ap;
247 va_start(ap, format);
248 std::string msg = base::string_vprintf(format, ap);
249 va_end(ap);
250
251 if (!m_withUI || !m_console) {
252 fputs(msg.c_str(), stdout);
253 fflush(stdout);
254 return;
255 }
256
257 // Open the window
258 if (!m_console->isVisible()) {
259 m_console->openWindow();
260 ui::Manager::getDefault()->invalidate();
261 }
262
263 // Update the textbox
264 m_console->addMessage(msg);
265}
266
267// static
268void Console::showException(const std::exception& e)
269{
270 if (!ui::is_ui_thread()) {
271 LOG(ERROR, "A problem has occurred.\n\nDetails:\n%s\n", e.what());
272 return;
273 }
274
275 Console console;
276 if (typeid(e) == typeid(std::bad_alloc))
277 console.printf("There is not enough memory to complete the action.");
278 else
279 console.printf("A problem has occurred.\n\nDetails:\n%s\n", e.what());
280}
281
282// static
283void Console::notifyNewDisplayConfiguration()
284{
285 if (m_console)
286 m_console->centerConsole();
287}
288
289} // namespace app
290