/*** Copyright (C) 2021 J Reece Wilson (a/k/a "Reece"). All rights reserved. File: ConsoleWxWidgets.cpp Date: 2021-6-8 Author: Reece ***/ #include #include "ConsoleWxWidgets.hpp" #include #if defined(_AUHAS_WXWIDGETS) #if defined(AURORA_PLATFORM_WIN32) #include #include #endif #include #include #include #include #include class ConsoleFrame; static AuList gPendingLines; static AuThreadPrimitives::Mutex gMutex; static bool gWxConsoleReady; static bool gConsoleStarted = false; static ConsoleFrame *gWxFrame; class WxSplitterLine : public wxWindow { public: WxSplitterLine() {} WxSplitterLine(wxSize splitter, AuOptional color, wxWindow *parent, wxWindowID winid) : wxWindow(parent, winid), _color(color) { SetMinSize(splitter); } void OnPaint(wxPaintEvent &paint) { wxPaintDC re(this); re.SetBrush(_color.value_or(this->GetParent()->GetBackgroundColour())); re.SetBackground(_color.value_or(this->GetParent()->GetBackgroundColour())); re.Clear(); } private: DECLARE_EVENT_TABLE() AuOptional _color; }; wxBEGIN_EVENT_TABLE(WxSplitterLine, wxPanel) EVT_PAINT(WxSplitterLine::OnPaint) wxEND_EVENT_TABLE() class ConsoleApp : public wxApp { public: ~ConsoleApp(); virtual bool OnInit(); }; IMPLEMENT_APP_NO_MAIN(ConsoleApp); ConsoleApp::~ConsoleApp() { } class ConsoleFrame : public wxFrame { public: ConsoleFrame(const wxString &title, const wxPoint &pos, const wxSize &size); ~ConsoleFrame() { AuThreadPrimitives::MutexUnique_t re(gMutex.get()); gWxConsoleReady = false; } void WriteLine(const Aurora::Console::ConsoleMessage &msg); private: void OnHello(wxCommandEvent &event); void OnExit(wxCommandEvent &event); void OnAbout(wxCommandEvent &event); void OnBugWrite(wxCommandEvent &event); void OnIssueWrite(wxCommandEvent &event); void OpenLogDir(wxCommandEvent &event); void OnInit(wxWindowCreateEvent &event); void OnShow(wxWindowCreateEvent &event); void OnCmd(wxCommandEvent &event); void OnPump(wxTimerEvent& event); WxSplitterLine *NewSplitter(wxSize splitter, wxColor color); WxSplitterLine *NewSplitter(wxSize splitter); wxDECLARE_EVENT_TABLE(); wxRichTextCtrl *richtextbox_; wxTextCtrl *commandbox_; wxTimer timer_; }; enum { ID_Hello = 1, ID_BLACKBOX, ID_WRITE_CARD, ID_WRITE_BUG, ID_OPEN_LOG_DIR, ID_OPENBIN }; wxBEGIN_EVENT_TABLE(ConsoleFrame, wxFrame) EVT_MENU(ID_Hello, ConsoleFrame::OnHello) EVT_MENU(wxID_EXIT, ConsoleFrame::OnExit) EVT_MENU(wxID_ABOUT, ConsoleFrame::OnAbout) EVT_MENU(ID_WRITE_CARD, ConsoleFrame::OnIssueWrite) EVT_MENU(ID_WRITE_BUG, ConsoleFrame::OnBugWrite) EVT_MENU(ID_OPEN_LOG_DIR, ConsoleFrame::OpenLogDir) EVT_WINDOW_CREATE(ConsoleFrame::OnInit) wxEND_EVENT_TABLE() bool ConsoleApp::OnInit() { //gRuntimeConfig.console. ConsoleFrame *frame = _new ConsoleFrame("Aurora SDK Sample", wxPoint(50, 50), wxSize(1080, 550)); frame->Show(true); gWxFrame = frame; return true; } void ConsoleFrame::OnCmd(wxCommandEvent &event) { // im lazy, i know, bite me auto textbox = reinterpret_cast(this); AuString line = textbox->GetLineText(0); if (line.empty()) { return; } textbox->Clear(); Aurora::Console::DispatchRawLine(line); } WxSplitterLine *ConsoleFrame::NewSplitter(wxSize splitter, wxColor color) { return new WxSplitterLine(splitter, color, this, -1); } WxSplitterLine *ConsoleFrame::NewSplitter(wxSize splitter) { return new WxSplitterLine(splitter, {}, this, -1); } void ConsoleFrame::WriteLine(const Aurora::Console::ConsoleMessage &string) { static AuArray(Aurora::Console::EAnsiColor::eEnumCount)> ColorMap { *wxRED, // kRed, *wxRED, // kBoldRed, *wxGREEN, // kGreen, *wxGREEN, // kBoldGreen, *wxYELLOW, // kYellow, *wxYELLOW, // kBoldYellow, *wxBLUE, // kBlue, *wxBLUE, // kBoldBlue, *wxRED, // kMagenta, *wxRED, // kBoldMagenta, *wxBLUE, // kCyan, *wxBLUE, // kBoldCyan, *wxWHITE, // kReset }; static AuArray(Aurora::Console::EAnsiColor::eEnumCount)> HahaBald { false, // kRed, true, // kBoldRed, false, // kGreen, true, // kBoldGreen, false, // kYellow, true, // kBoldYellow, false, // kBlue, true, // kBoldBlue, false, // kMagenta, true, // kBoldMagenta, false, // kCyan, true, // kBoldCyan, false, // kReset }; auto writeLine = string.ToSimplified(); bool useColor = true;// DarkModeEnabled(); auto color = useColor ? ColorMap[static_cast(string.color)] : wxColor(0, 0, 0); auto bold = useColor ? HahaBald[static_cast(string.color)] : false; if (richtextbox_->GetNumberOfLines() > 1000) richtextbox_->Clear(); //richtextbox_->BeginParagraphSpacing(0, 20); auto pos = richtextbox_->GetCaretPosition(); auto selPos = richtextbox_->GetLastPosition() - 1; richtextbox_->SetCaretPosition(selPos); richtextbox_->BeginTextColour(color); if (bold) richtextbox_->BeginBold(); richtextbox_->WriteText(wxString::FromUTF8(writeLine)); richtextbox_->Newline(); if (bold) richtextbox_->EndBold(); richtextbox_->EndTextColour(); if ((pos == -1) || // true once (pos == selPos) || // most likely (abs(pos - selPos) < 5)) // ux { auto newPos = richtextbox_->GetLastPosition() - 1; richtextbox_->ScrollIntoView(newPos, WXK_DOWN); richtextbox_->SetCaretPosition(newPos); } else { richtextbox_->SetCaretPosition(pos); } //richtextbox_->Disable //richtextbox_->EndParagraphSpacing(); } #include ConsoleFrame::ConsoleFrame(const wxString &title, const wxPoint &pos, const wxSize &size) : wxFrame(NULL, wxID_ANY, title, pos, size) { #if defined(AURORA_PLATFORM_WIN32) bool useWin32DarkHack = Aurora::Extensions::Win32::g_darkModeSupported; #else bool useWin32DarkHack = false; #endif // _ _ _ _ _ _ _ // (_) | | | | | | | | | | | | // __ ____ _ _ __ ___ _ __ _ _ __ ___ ___ ___ __ _| |__ ___ __ _ __| | | | | | | | | | // \ \ /\ / / _` | '__| / __| '__| | '_ ` _ \ / _ \/ __| / _` | '_ \ / _ \/ _` |/ _` | | | | | | | | | // \ V V / (_| | | | (__| | | | | | | | | __/\__ \ | (_| | | | | __/ (_| | (_| | |_| |_| |_| |_| // \_/\_/ \__,_|_| \___|_| |_|_| |_| |_|\___||___/ \__,_|_| |_|\___|\__,_|\__,_| (_) (_) (_) (_) // // // I dont even care if this function leaks on failure. sucks to be you, i dont care. // valgrind can fail when your system is running low on resources at start up static const auto kEnableDark = (useWin32DarkHack) || (Aurora::Build::kCurrentPlatform != Aurora::Build::EPlatform::ePlatformWin32); static const auto kIsBgColorDefault = !kEnableDark; static const auto kBackgroundColor = kIsBgColorDefault ? this->GetBackgroundColour() : wxColour(38, 38, 38); static const auto kTextBoxColor = kIsBgColorDefault ? this->GetForegroundColour() : wxColour(255, 255, 255); static const auto kTextBoxOutter = wxColour(0x15, 0x15, 0x15); static const auto kTextBoxInner = wxColour(0x38, 0x38, 0x38); static const auto kHeaderPadUpDown = 14; static const auto kHeaderPadLeftRight = 15; static const auto kTextboxWin32PadInner = 2; static const auto kTextboxWin32PadOutter = 1; static const auto kTitleContentSplitterDepth = 2; static const auto kTextboxHeight = 20; static const auto kTextBoxHandlePad = kEnableDark; static const auto kTextBoxHandleColor = kEnableDark; static const auto kDrawOwnCheckboxes = kEnableDark; // 1080, 550 this->SetClientSize(this->FromDIP(wxSize(903, 434))); auto menuBar_ = _new wxMenuBar(); if (!menuBar_) return; { auto menuFile = _new wxMenu(); if (!menuFile) return; menuFile->Append(wxID_SAVE, "Export text log"); menuFile->AppendSeparator(); menuFile->Append(ID_OPEN_LOG_DIR, "Open log directory"); menuBar_->Append(menuFile, "&Logs"); } { auto menuHelp = _new wxMenu(); if (!menuHelp) return; menuHelp->Append(ID_BLACKBOX, "&Report application state to the black box"); #if defined(STAGING) || defined(DEBUG) menuHelp->Append(ID_WRITE_CARD, "&[Internal] Write a bug report"); #endif menuHelp->Append(ID_WRITE_BUG, "&[Public] Write a bug report"); menuBar_->Append(menuHelp, "&Bugs"); } if (useWin32DarkHack) { this->Connect(wxEVT_SHOW, wxWindowCreateEventHandler(ConsoleFrame::OnShow)); } auto sizer = _new wxBoxSizer(wxVERTICAL); if (!sizer) return; auto font = this->GetFont(); font.SetPointSize(9); font.MakeBold(); auto font2 = this->GetFont(); font2.SetPointSize(9); auto addTextbox = [&](wxBoxSizer *parent, int minx, int miny, bool alt = false) -> wxTextCtrl * { auto filterBarContainer = _new wxBoxSizer(wxVERTICAL); if (!filterBarContainer) return {}; auto filterBar = _new wxBoxSizer(wxHORIZONTAL); if (!filterBar) return{}; // TODO: a leak idc about auto filterBox = _new wxTextCtrl(this, -1, wxEmptyString, wxDefaultPosition, alt ? wxDefaultSize : wxSize(400, 20), (kTextBoxHandlePad ? wxBORDER_NONE : 0) | wxTE_PROCESS_ENTER | wxEXPAND); if (!filterBox) return{}; // TODO: a leak idc about filterBox->SetMinSize({minx, miny}); if (kTextBoxHandlePad) { filterBarContainer->Add(NewSplitter(wxSize(kTextboxWin32PadOutter, kTextboxWin32PadOutter), kTextBoxOutter), 0, wxEXPAND); filterBarContainer->Add(NewSplitter(wxSize(kTextboxWin32PadInner, kTextboxWin32PadInner), kTextBoxInner), 0, wxEXPAND); } filterBarContainer->Add(filterBox, wxSizerFlags().Proportion(1).Expand()); if (kTextBoxHandleColor) { filterBox->SetBackgroundColour(kBackgroundColor); filterBox->SetForegroundColour(kTextBoxColor); } if (kTextBoxHandlePad) { filterBarContainer->Add(NewSplitter(wxSize(kTextboxWin32PadInner, kTextboxWin32PadInner), kTextBoxInner), 0, wxEXPAND); filterBarContainer->Add(NewSplitter(wxSize(kTextboxWin32PadOutter, kTextboxWin32PadOutter), kTextBoxOutter), 0, wxEXPAND); } if (kTextBoxHandlePad) { filterBar->Add(NewSplitter(wxSize(kTextboxWin32PadOutter, kTextboxWin32PadOutter), kTextBoxOutter) , 0, wxEXPAND | wxTOP | wxBOTTOM, 1); filterBar->Add(NewSplitter(wxSize(kTextboxWin32PadInner, kTextboxWin32PadInner), kTextBoxInner), 0, wxEXPAND | wxTOP | wxBOTTOM, 1); } filterBar->Add(filterBarContainer, wxSizerFlags().Proportion(1).Expand()); if (kTextBoxHandlePad) { filterBar->Add(NewSplitter(wxSize(kTextboxWin32PadInner, kTextboxWin32PadInner), kTextBoxInner), 0, wxEXPAND | wxTOP | wxBOTTOM, 1); filterBar->Add(NewSplitter(wxSize(kTextboxWin32PadOutter, kTextboxWin32PadOutter), kTextBoxOutter), 0, wxEXPAND | wxTOP | wxBOTTOM, 1); } parent->Add(filterBar, wxSizerFlags().Expand()); return filterBox; }; auto header = _new wxBoxSizer(wxHORIZONTAL); auto addHeaderPanel = [&](wxBoxSizer *parent, wxBoxSizer *inner, bool up, bool down, bool left, bool right, int top, int sides) { auto filterBar = _new wxBoxSizer(wxVERTICAL); if (!filterBar) return; auto innerBar = _new wxBoxSizer(wxHORIZONTAL); if (!innerBar) return; // PAD FILTER REGION UP if (up) { filterBar->Add(NewSplitter(wxSize(top, top)), wxSizerFlags().Expand()); } { // PAD FILTER REGION LEFT if (left) { innerBar->Add(NewSplitter(wxSize(sides, sides)), wxSizerFlags().Expand()); } innerBar->Add(inner, wxSizerFlags().Expand()); // PAD FILTER REGION RIGHT if (right) { innerBar->Add(NewSplitter(wxSize(sides, sides)), wxSizerFlags().Expand()); } } filterBar->Add(innerBar, wxSizerFlags().Expand()); // PAD FILTER REGION DOWN if (down) { filterBar->Add(NewSplitter(wxSize(top, top)), wxSizerFlags().Expand()); } parent->Add(filterBar, wxSizerFlags().Expand()); }; this->SetBackgroundColour(kBackgroundColor); // HEADER PANEL 1# filter { auto filterBarContainer = _new wxBoxSizer(wxVERTICAL); if (!filterBarContainer) return; auto filterLabel = _new wxStaticText(this, -1, "FILTER"); if (!filterLabel) return; filterLabel->SetFont(font); filterLabel->SetBackgroundColour(kBackgroundColor); filterLabel->SetForegroundColour(kTextBoxColor); filterBarContainer->Add(filterLabel); filterBarContainer->Add(NewSplitter(wxSize(kTitleContentSplitterDepth, kTitleContentSplitterDepth)), wxSizerFlags().Expand()); addTextbox(filterBarContainer, 400, kTextboxHeight); addHeaderPanel(header, filterBarContainer, true, true, true, false, kHeaderPadUpDown, kHeaderPadLeftRight); } // Align right header->AddStretchSpacer(); // Draw a little bar //header->Add(NewSplitter(wxSize(1, 1), wxColour(0x38, 0x38, 0x38)), 0, wxEXPAND); // HEADER PANEL 2# levels { AuList chkName = {"Debug", "Warn", "Info", "Game"}; auto checkboxes = _new wxBoxSizer(wxHORIZONTAL); if (!checkboxes) return; auto checkboxesContainer = _new wxBoxSizer(wxVERTICAL); if (!checkboxes) return; auto checkboxLabel = _new wxStaticText(this, -1, "LEVEL"); if (!checkboxLabel) return; checkboxLabel->SetFont(font); checkboxLabel->SetBackgroundColour(kBackgroundColor); checkboxLabel->SetForegroundColour(kTextBoxColor); for (int i = 0; i < chkName.size(); i++) { const auto &name = chkName[i]; if (kDrawOwnCheckboxes) { if (i != 0) { checkboxes->Add(NewSplitter(wxSize(6, 6)), wxSizerFlags().Expand()); } auto label = _new wxStaticText(this, -1, name.c_str()); label->SetBackgroundColour(kBackgroundColor); label->SetForegroundColour(kTextBoxColor); label->SetFont(font2); checkboxes->Add(label); checkboxes->Add(NewSplitter(wxSize(2, 2)), wxSizerFlags().Expand()); } auto checkbox1 = new wxCheckBox(this, -1, kDrawOwnCheckboxes ? "" : name.c_str(), wxDefaultPosition, wxDefaultSize); checkbox1->SetFont(font2); checkbox1->SetValue(true); checkboxes->Add(NewSplitter(wxSize(2, 2)), wxSizerFlags().Expand()); checkboxes->Add(checkbox1, wxSizerFlags(0).CenterVertical().GetFlags()); checkboxes->Add(NewSplitter(wxSize(2, 2)), wxSizerFlags().Expand()); } checkboxesContainer->Add(checkboxLabel); checkboxesContainer->Add(NewSplitter(wxSize(kTitleContentSplitterDepth, kTitleContentSplitterDepth)), wxSizerFlags().Expand()); checkboxesContainer->Add(checkboxes); addHeaderPanel(header, checkboxesContainer, true, true, false, true, kHeaderPadUpDown, kHeaderPadLeftRight); } sizer->Add(header, 0, wxEXPAND); // Shadow down if (/*useWin32DarkHack*/ true) { sizer->Add(NewSplitter(wxSize(kTextboxWin32PadInner, kTextboxWin32PadInner), kTextBoxInner), 0, wxEXPAND); sizer->Add(NewSplitter(wxSize(kTextboxWin32PadOutter, kTextboxWin32PadOutter), kTextBoxOutter), 0, wxEXPAND); } // Rich text box { richtextbox_ = _new wxRichTextCtrl(this, -1, wxEmptyString, wxDefaultPosition, wxDefaultSize, (kTextBoxHandlePad ? wxBORDER_NONE : 0) | wxTE_MULTILINE | wxRE_READONLY | wxRE_CENTER_CARET); if (!richtextbox_) return; if (useWin32DarkHack) { richtextbox_->SetBackgroundColour(kBackgroundColor); richtextbox_->SetForegroundColour(kTextBoxColor); this->Connect(wxEVT_SHOW, wxWindowCreateEventHandler(ConsoleFrame::OnShow)); } sizer->Add(richtextbox_, 1, wxEXPAND); } // Shadow down if (/*useWin32DarkHack*/ true) { sizer->Add(NewSplitter(wxSize(kTextboxWin32PadInner, kTextboxWin32PadInner), kTextBoxInner), 0, wxEXPAND); sizer->Add(NewSplitter(wxSize(kTextboxWin32PadOutter, kTextboxWin32PadOutter), kTextBoxOutter), 0, wxEXPAND); } // Command Box { commandbox_ = addTextbox(sizer, 0, kTextboxHeight, true); commandbox_->SetHint("Type a command here"); commandbox_->Connect(wxEVT_COMMAND_TEXT_ENTER, wxCommandEventHandler(ConsoleFrame::OnCmd)); } SetSizer(sizer); SetMenuBar(menuBar_); // SetAutoLayout(true); timer_.Bind(wxEVT_TIMER, &ConsoleFrame::OnPump, this); timer_.Start(100); gWxConsoleReady = true; } void ConsoleFrame::OnInit(wxWindowCreateEvent &event) { OnShow(event); } #if defined(AURORA_PLATFORM_WIN32) void ConsoleFrame::OnShow(wxWindowCreateEvent &event) { if (!Aurora::Extensions::Win32::g_darkModeSupported) return; auto handle = event.GetWindow()->GetHWND(); Aurora::Extensions::Win32::AllowDarkModeForWindow(handle, true); if (Aurora::Extensions::Win32::_FlushMenuThemes) Aurora::Extensions::Win32::_FlushMenuThemes(); if (pSetWindowTheme) { pSetWindowTheme(handle, L"DarkMode_Explorer", NULL); } SendMessageW(handle, WM_THEMECHANGED, 0, 0); Aurora::Extensions::Win32::RefreshTitleBarThemeColor(handle); UpdateWindow(handle); } #else void ConsoleFrame::OnShow(wxWindowCreateEvent &event) {} static bool DarkModeEnabled() { return Aurora::Build::EPlatform == Aurora::Build::EPlatform::kPlatformLinux; } #endif void ConsoleFrame::OnExit(wxCommandEvent &event) { Close(true); } void ConsoleFrame::OnAbout(wxCommandEvent &event) { } void ConsoleFrame::OnHello(wxCommandEvent &event) { AuLogGame("nani?!"); } void ConsoleFrame::OnBugWrite(wxCommandEvent &event) { Aurora::Processes::OpenUri(gRuntimeConfig.console.supportPublic); } void ConsoleFrame::OnIssueWrite(wxCommandEvent &event) { Aurora::Processes::OpenUri(gRuntimeConfig.console.supportInternal); } void ConsoleFrame::OpenLogDir(wxCommandEvent &event) { Aurora::Processes::OpenFile(Aurora::Console::ConsoleFIO::GetLogDirectory()); } namespace Aurora::Console::ConsoleWxWidgets { static void WxWidgetsPump(); } void ConsoleFrame::OnPump(wxTimerEvent &event) { Aurora::Console::ConsoleWxWidgets::WxWidgetsPump(); } namespace Aurora::Console::ConsoleWxWidgets { static wxInitializer *gInitializer; static bool UseWxConsole() { if (!gRuntimeConfig.console.enableConsole) { return false; } if (!gRuntimeConfig.console.enableWxWidgets) { return false; } #if defined(AURORA_PLATFORM_WIN32) if (GetConsoleWindow()) { if (!gRuntimeConfig.console.forceToolKitWindow) { return false; } } #endif return true; } static bool WxWidgetsInit() { wxApp::SetInstance(_new ConsoleApp()); #if defined(AURORA_PLATFORM_WIN32) || true std::wstring auroraw(L"Aurora"); static wxChar *s = auroraw.data(); static int count = 1; #endif gInitializer = new wxInitializer(count, &s); if (!gInitializer->IsOk()) { #if wxUSE_LOG delete wxLog::SetActiveTarget(NULL); #endif } try { if (!wxTheApp->CallOnInit()) { return false; } } catch (...) { wxTheApp->OnUnhandledException(); return false; } return true; } void WxWidgetsRequestExit() { if (!gWxConsoleReady) { gMutex.reset(); return; } gMutex->Lock(); gWxConsoleReady = false; gMutex->Unlock(); auto window = wxTheApp->GetTopWindow(); if (!window) { gMutex.reset(); return; } window->GetEventHandler()->CallAfter([]() { wxTheApp->Exit(); gMutex.reset(); }); } static AuThreads::ThreadUnique_t gWxWidgetsThread; static void WxWidgetsThreadMain() { if (!WxWidgetsInit()) { return; } try { wxTheApp->OnRun(); } catch (...) { wxTheApp->OnUnhandledException(); } } void Init() { if (!UseWxConsole()) { return; } Start(); } class ConsoleMessageSubscriber : public Aurora::Console::Hooks::IConsoleSubscriber { public: void OnMessage(const ConsoleMessage &string) override; }; void ConsoleMessageSubscriber::OnMessage(const ConsoleMessage &string) { AU_LOCK_GUARD(gMutex); gPendingLines.push_back(string); } static ConsoleMessageSubscriber gConsoleMessageSubscriber; void Start() { if (AuExchange(gConsoleStarted, true)) return; Aurora::Console::Hooks::AddSubscription(AuUnsafeRaiiToShared(&gConsoleMessageSubscriber)); gWxWidgetsThread = AuThreads::ThreadUnique(AuThreads::ThreadInfo( AuMakeShared(AuThreads::IThreadVectorsFunctional::OnEntry_t(std::bind(WxWidgetsThreadMain)), AuThreads::IThreadVectorsFunctional::OnExit_t{}), "WxWidgets" )); if (!gWxWidgetsThread) return; gWxWidgetsThread->Run(); } static void WxWidgetsPump() { // called from within WxWidget main loop, do not worry about locking is ready // we _know_ the wxwidget app is referenceable if (!wxTheApp) return; if (!gWxConsoleReady) return; AuList lines; { AU_LOCK_GUARD(gMutex); lines = AuExchange(gPendingLines, {}); } for (const auto& line : lines) { gWxFrame->WriteLine(line); } } void Pump() { } void Exit() { WxWidgetsRequestExit(); Aurora::Console::Hooks::RemoveSubscription(AuUnsafeRaiiToShared(&gConsoleMessageSubscriber)); gWxWidgetsThread.reset(); auto wxHandle = wxTheApp; if (wxHandle) { delete wxHandle; } gPendingLines.clear(); gConsoleStarted = false; } } #else namespace Aurora::Console::ConsoleWxWidgets { void Init() { } void Pump() { } void Exit() { } void Start() { } } #endif