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 | |
31 | namespace app { |
32 | |
33 | using namespace ui; |
34 | |
35 | Console::ConsoleWindow* Console::m_console = nullptr; |
36 | |
37 | class Console::ConsoleWindow final : public Window { |
38 | public: |
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 | |
121 | private: |
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 | |
200 | Console::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 | |
228 | Console::~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 | |
244 | void 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 |
268 | void 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 |
283 | void Console::notifyNewDisplayConfiguration() |
284 | { |
285 | if (m_console) |
286 | m_console->centerConsole(); |
287 | } |
288 | |
289 | } // namespace app |
290 | |