| 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 | |