diff --git a/src/LogWindow.cpp b/src/LogWindow.cpp new file mode 100644 index 000000000..dba1cd0f5 --- /dev/null +++ b/src/LogWindow.cpp @@ -0,0 +1,362 @@ +/********************************************************************** + + Audacity: A Digital Audio Editor + + AudacityLogger.cpp + +******************************************************************//** + +\class AudacityLogger +\brief AudacityLogger is a thread-safe logger class + +Provides thread-safe logging based on the wxWidgets log facility. + +*//*******************************************************************/ + + + +#include "AudacityLogger.h" + + + +#include "FileNames.h" +#include "Internat.h" +#include "SelectFile.h" +#include "ShuttleGui.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../images/AudacityLogoAlpha.xpm" +#include "widgets/AudacityMessageBox.h" + +// +// AudacityLogger class +// +// Two reasons for this class instead of the wxLogWindow class (or any WX GUI logging class) +// +// 1) If wxLogWindow is used and initialized before the Mac's "root" window, then +// Audacity may crash when terminating. It's not fully understood why this occurs +// but it probably has to do with the order of deletion. However, deferring the +// creation of the log window until it is actually shown circumvents the problem. +// 2) By providing an Audacity specific logging class, it can be made thread-safe and, +// as such, can be used by the ever growing threading within Audacity. +// +enum +{ + LoggerID_Save = wxID_HIGHEST + 1, + LoggerID_Clear, + LoggerID_Close +}; + +namespace { +Destroy_ptr sFrame; +wxWeakRef sText; + +struct LogWindowUpdater : public PrefsListener +{ + // PrefsListener implementation + void UpdatePrefs() override; +}; +// Unique PrefsListener can't be statically constructed before the application +// object initializes, so use Optional +std::optional pUpdater; + +void OnCloseWindow(wxCloseEvent & e); +void OnClose(wxCommandEvent & e); +void OnClear(wxCommandEvent & e); +void OnSave(wxCommandEvent & e); +} + +AudacityLogger *AudacityLogger::Get() +{ + static std::once_flag flag; + std::call_once( flag, []{ + // wxWidgets will clean up the logger for the main thread, so we can say + // safenew. See: + // http://docs.wxwidgets.org/3.0/classwx_log.html#a2525bf54fa3f31dc50e6e3cd8651e71d + std::unique_ptr < wxLog > // DELETE any previous logger + { wxLog::SetActiveTarget(safenew AudacityLogger) }; + } ); + + // Use dynamic_cast so that we get a NULL ptr in case our logger + // is no longer the target. + return dynamic_cast(wxLog::GetActiveTarget()); +} + +AudacityLogger::AudacityLogger() +: wxEvtHandler(), + wxLog() +{ + mUpdated = false; +} + +AudacityLogger::~AudacityLogger() = default; + +void AudacityLogger::Flush() +{ + if (mUpdated && mListener && mListener()) + mUpdated = false; +} + +auto AudacityLogger::SetListener(Listener listener) -> Listener +{ + auto result = std::move(mListener); + mListener = std::move(listener); + return result; +} + +void AudacityLogger::DoLogText(const wxString & str) +{ + if (!wxIsMainThread()) { + wxMutexGuiEnter(); + } + + if (mBuffer.empty()) { + wxString stamp; + + TimeStamp(&stamp); + + mBuffer << stamp << _TS("Audacity ") << AUDACITY_VERSION_STRING << wxT("\n"); + } + + mBuffer << str << wxT("\n"); + + mUpdated = true; + + Flush(); + + if (!wxIsMainThread()) { + wxMutexGuiLeave(); + } +} + +bool AudacityLogger::SaveLog(const wxString &fileName) const +{ + wxFFile file(fileName, wxT("w")); + + if (file.IsOpened()) { + file.Write(mBuffer); + file.Close(); + return true; + } + + return false; +} + +bool AudacityLogger::ClearLog() +{ + mBuffer = wxEmptyString; + DoLogText(wxT("Log Cleared.")); + + return true; +} + +void LogWindow::Show(bool show) +{ + // Hide the frame if created, otherwise do nothing + if (!show) { + if (sFrame) { + sFrame->Show(false); + } + return; + } + + // If the frame already exists, refresh its contents and show it + auto pLogger = AudacityLogger::Get(); + if (sFrame) { + if (!sFrame->IsShown() && sText) { + if (pLogger) + sText->ChangeValue(pLogger->GetBuffer()); + sText->SetInsertionPointEnd(); + sText->ShowPosition(sText->GetLastPosition()); + } + sFrame->Show(); + sFrame->Raise(); + return; + } + + // This is the first use, so create the frame + Destroy_ptr frame + { safenew wxFrame(NULL, wxID_ANY, _("Audacity Log")) }; + frame->SetName(frame->GetTitle()); + frame->SetBackgroundColour(wxSystemSettings::GetColour(wxSYS_COLOUR_3DFACE)); + + // loads either the XPM or the windows resource, depending on the platform + { +#if !defined(__WXMAC__) && !defined(__WXX11__) +#if defined(__WXMSW__) + wxIcon ic{wxICON(AudacityLogo)}; +#elif defined(__WXGTK__) + wxIcon ic{wxICON(AudacityLogoAlpha)}; +#else + wxIcon ic{}; + ic.CopyFromBitmap(theTheme.Bitmap(bmpAudacityLogo48x48)); +#endif + frame->SetIcon(ic); +#endif + } + + // Log text + ShuttleGui S(frame.get(), eIsCreating); + + S.Style(wxNO_BORDER | wxTAB_TRAVERSAL).Prop(true).StartPanel(); + { + S.StartVerticalLay(true); + { + sText = S.Style(wxTE_MULTILINE | wxHSCROLL | wxTE_READONLY | wxTE_RICH) + .AddTextWindow({}); // Populate this text window below + + S.AddSpace(0, 5); + S.StartHorizontalLay(wxALIGN_CENTER, 0); + { + S.AddSpace(10, 0); + S.Id(LoggerID_Save).AddButton(XXO("&Save...")); + S.Id(LoggerID_Clear).AddButton(XXO("Cl&ear")); + S.Id(LoggerID_Close).AddButton(XXO("&Close")); + S.AddSpace(10, 0); + } + S.EndHorizontalLay(); + S.AddSpace(0, 3); + } + S.EndVerticalLay(); + } + S.EndPanel(); + + // Give a place for the menu help text to go + // frame->CreateStatusBar(); + + frame->Layout(); + + // Hook into the frame events + frame->Bind(wxEVT_CLOSE_WINDOW, OnCloseWindow ); + + frame->Bind( wxEVT_COMMAND_MENU_SELECTED, OnSave, LoggerID_Save); + frame->Bind( wxEVT_COMMAND_MENU_SELECTED, OnClear, LoggerID_Clear); + frame->Bind( wxEVT_COMMAND_MENU_SELECTED, OnClose, LoggerID_Close); + frame->Bind( wxEVT_COMMAND_BUTTON_CLICKED, OnSave, LoggerID_Save); + frame->Bind( wxEVT_COMMAND_BUTTON_CLICKED, OnClear, LoggerID_Clear); + frame->Bind( wxEVT_COMMAND_BUTTON_CLICKED, OnClose, LoggerID_Close); + + sFrame = std::move( frame ); + + sFrame->Show(); + + if (pLogger) + pLogger->Flush(); + + // Also create the listeners + if (!pUpdater) + pUpdater.emplace(); + + if (pLogger) { + pLogger->SetListener([]{ + if (auto pLogger = AudacityLogger::Get()) { + if (sFrame && sFrame->IsShown()) { + if (sText) + sText->ChangeValue(pLogger->GetBuffer()); + return true; + } + } + return false; + }); + + // Initial flush populates sText + pLogger->Flush(); + } +} + +wxString AudacityLogger::GetLog(int count) +{ + if (count == 0) + { + return mBuffer; + } + + wxString buffer; + + auto lines = wxStringTokenize(mBuffer, wxT("\r\n"), wxTOKEN_RET_DELIMS); + for (int index = lines.size() - 1; index >= 0 && count > 0; --index, --count) + { + buffer.Prepend(lines[index]); + } + + return buffer; +} + +namespace { +void OnCloseWindow(wxCloseEvent & WXUNUSED(e)) +{ +#if defined(__WXMAC__) + // On the Mac, destroy the window rather than hiding it since the + // log menu will override the root windows menu if there is no + // project window open. + sFrame.reset(); +#else + sFrame->Show(false); +#endif +} + +void OnClose(wxCommandEvent & WXUNUSED(e)) +{ + wxCloseEvent dummy; + OnCloseWindow(dummy); +} + +void OnClear(wxCommandEvent & WXUNUSED(e)) +{ + if (auto pLogger = AudacityLogger::Get()) + pLogger->ClearLog(); +} + +void OnSave(wxCommandEvent & WXUNUSED(e)) +{ + wxString fName = _("log.txt"); + + fName = SelectFile(FileNames::Operation::Export, + XO("Save log to:"), + wxEmptyString, + fName, + wxT("txt"), + { FileNames::TextFiles }, + wxFD_SAVE | wxFD_OVERWRITE_PROMPT | wxRESIZE_BORDER, + sFrame.get()); + + if (fName.empty()) { + return; + } + + if (!(sText && sText->SaveFile(fName))) { + AudacityMessageBox( + XO("Couldn't save log to file: %s").Format( fName ), + XO("Warning"), + wxICON_EXCLAMATION, + sFrame.get()); + return; + } +} + +void LogWindowUpdater::UpdatePrefs() +{ + //! Re-create the non-modal window in case of change of preferred language + if (sFrame) { + bool shown = sFrame->IsShown(); + if (shown) { + LogWindow::Show(false); + } + sFrame.reset(); + if (shown) { + LogWindow::Show(true); + } + } +} +} diff --git a/src/LogWindow.h b/src/LogWindow.h new file mode 100644 index 000000000..1c98e9606 --- /dev/null +++ b/src/LogWindow.h @@ -0,0 +1,72 @@ +/********************************************************************** + + Audacity: A Digital Audio Editor + + AudacityLogger.h + + Dominic Mazzoni + +**********************************************************************/ + +#ifndef __AUDACITY_LOGGER__ +#define __AUDACITY_LOGGER__ + +#include +#include "MemoryX.h" +#include "Prefs.h" +#include // to inherit +#include // to inherit wxEvtHandler + +class wxFrame; +class wxTextCtrl; + +class AUDACITY_DLL_API AudacityLogger final : public wxEvtHandler, + public wxLog +{ + public: + + ~AudacityLogger() override; + + // Get the singleton instance or null + static AudacityLogger *Get(); + + bool SaveLog(const wxString &fileName) const; + bool ClearLog(); + + //! Retrieve all or some of the lines since most recent ClearLog or start of program + /*! If `count == 0` or is more than the number of lines, return all; else return the last `count` lines */ + wxString GetLog(int count = 0); + + //! Get all the accumulated text since program start or last ClearLog() + const wxString &GetBuffer() const { return mBuffer; } + + void Flush() override; + + //! Type of function called by Flush + /*! @return true if flush completed + */ + using Listener = std::function< bool() >; + + //! Set the unique listener, returning any previous one + Listener SetListener(Listener listener); + +protected: + void DoLogText(const wxString & msg) override; + + private: + AudacityLogger(); + + Listener mListener; + wxString mBuffer; + bool mUpdated; +}; + +//! Maintains the unique logging window which displays debug information +class AUDACITY_DLL_API LogWindow +{ +public: + //! Show or hide the unique logging window; create it on demand the first time it is shown + static void Show(bool show = true); +}; + +#endif