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