mirror of
https://github.com/cookiengineer/audacity
synced 2025-07-13 15:17:42 +02:00
Journal playback and recording utilities, not yet used
This commit is contained in:
parent
b8f22981ee
commit
af0aab83c3
@ -142,6 +142,8 @@ list( APPEND SOURCES
|
||||
HitTestResult.h
|
||||
ImageManipulation.cpp
|
||||
ImageManipulation.h
|
||||
Journal.cpp
|
||||
Journal.h
|
||||
KeyboardCapture.cpp
|
||||
KeyboardCapture.h
|
||||
LabelDialog.cpp
|
||||
|
331
src/Journal.cpp
Normal file
331
src/Journal.cpp
Normal file
@ -0,0 +1,331 @@
|
||||
/**********************************************************************
|
||||
|
||||
Audacity: A Digital Audio Editor
|
||||
|
||||
Journal.cpp
|
||||
|
||||
Paul Licameli
|
||||
|
||||
*******************************************************************//*!
|
||||
|
||||
\namespace Journal
|
||||
\brief Facilities for recording and playback of sequences of user interaction
|
||||
|
||||
*//*******************************************************************/
|
||||
|
||||
#include "Journal.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <unordered_map>
|
||||
#include <wx/app.h>
|
||||
#include <wx/filename.h>
|
||||
#include <wx/textfile.h>
|
||||
|
||||
#include "MemoryX.h"
|
||||
#include "Prefs.h"
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr auto SeparatorCharacter = ',';
|
||||
constexpr auto CommentCharacter = '#';
|
||||
|
||||
wxString sFileNameIn;
|
||||
wxTextFile sFileIn;
|
||||
|
||||
wxString sLine;
|
||||
// Invariant: the input file has not been opened, or else sLineNumber counts
|
||||
// the number of lines consumed by the tokenizer
|
||||
int sLineNumber = -1;
|
||||
|
||||
struct FlushingTextFile : wxTextFile {
|
||||
// Flush output when the program quits, even if that makes an incomplete
|
||||
// journal file without an exit
|
||||
~FlushingTextFile() { if ( IsOpened() ) { Write(); Close(); } }
|
||||
} sFileOut;
|
||||
|
||||
BoolSetting JournalEnabled{ L"/Journal/Enabled", false };
|
||||
|
||||
bool sError = false;
|
||||
|
||||
using Dictionary = std::unordered_map< wxString, Journal::Dispatcher >;
|
||||
Dictionary &sDictionary()
|
||||
{
|
||||
static Dictionary theDictionary;
|
||||
return theDictionary;
|
||||
}
|
||||
|
||||
inline void NextIn()
|
||||
{
|
||||
if ( !sFileIn.Eof() ) {
|
||||
sLine = sFileIn.GetNextLine();
|
||||
++sLineNumber;
|
||||
}
|
||||
}
|
||||
|
||||
wxArrayStringEx PeekTokens()
|
||||
{
|
||||
wxArrayStringEx tokens;
|
||||
if ( Journal::IsReplaying() )
|
||||
for ( ; !sFileIn.Eof(); NextIn() ) {
|
||||
if ( sLine.StartsWith( CommentCharacter ) )
|
||||
continue;
|
||||
|
||||
tokens = wxSplit( sLine, SeparatorCharacter );
|
||||
if ( tokens.empty() )
|
||||
// Ignore blank lines
|
||||
continue;
|
||||
|
||||
break;
|
||||
}
|
||||
return tokens;
|
||||
}
|
||||
|
||||
constexpr auto VersionToken = wxT("Version");
|
||||
|
||||
// Numbers identifying the journal format version
|
||||
int journalVersionNumbers[] = {
|
||||
1
|
||||
};
|
||||
|
||||
wxString VersionString()
|
||||
{
|
||||
wxString result;
|
||||
for ( auto number : journalVersionNumbers ) {
|
||||
auto str = wxString::Format( "%d", number );
|
||||
result += ( result.empty() ? str : ( '.' + str ) );
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
//! True if value is an acceptable journal version number to be rerun
|
||||
bool VersionCheck( const wxString &value )
|
||||
{
|
||||
auto strings = wxSplit( value, '.' );
|
||||
std::vector<int> numbers;
|
||||
for ( auto &string : strings ) {
|
||||
long value;
|
||||
if ( !string.ToCLong( &value ) )
|
||||
return false;
|
||||
numbers.push_back( value );
|
||||
}
|
||||
// OK if the static version number is not less than the given value
|
||||
// Maybe in the future there will be a compatibility break
|
||||
return !std::lexicographical_compare(
|
||||
std::begin( journalVersionNumbers ), std::end( journalVersionNumbers ),
|
||||
numbers.begin(), numbers.end() );
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
namespace Journal {
|
||||
|
||||
SyncException::SyncException()
|
||||
{
|
||||
// If the exception is ever constructed, cause nonzero program exit code
|
||||
sError = true;
|
||||
}
|
||||
|
||||
SyncException::~SyncException() {}
|
||||
|
||||
void SyncException::DelayedHandlerAction()
|
||||
{
|
||||
// Simulate the application Exit menu item
|
||||
wxCommandEvent evt{ wxEVT_MENU, wxID_EXIT };
|
||||
wxTheApp->AddPendingEvent( evt );
|
||||
}
|
||||
|
||||
bool RecordEnabled()
|
||||
{
|
||||
return JournalEnabled.Read();
|
||||
}
|
||||
|
||||
bool SetRecordEnabled(bool value)
|
||||
{
|
||||
auto result = JournalEnabled.Write(value);
|
||||
gPrefs->Flush();
|
||||
return result;
|
||||
}
|
||||
|
||||
bool IsRecording()
|
||||
{
|
||||
return sFileOut.IsOpened();
|
||||
}
|
||||
|
||||
bool IsReplaying()
|
||||
{
|
||||
return sFileIn.IsOpened();
|
||||
}
|
||||
|
||||
void SetInputFileName(const wxString &path)
|
||||
{
|
||||
sFileNameIn = path;
|
||||
}
|
||||
|
||||
bool Begin( const FilePath &dataDir )
|
||||
{
|
||||
if ( !sError && !sFileNameIn.empty() ) {
|
||||
wxFileName fName{ sFileNameIn };
|
||||
fName.MakeAbsolute( dataDir );
|
||||
const auto path = fName.GetFullPath();
|
||||
sFileIn.Open( path );
|
||||
if ( !sFileIn.IsOpened() )
|
||||
sError = true;
|
||||
else {
|
||||
sLine = sFileIn.GetFirstLine();
|
||||
sLineNumber = 0;
|
||||
|
||||
auto tokens = PeekTokens();
|
||||
NextIn();
|
||||
sError = !(
|
||||
tokens.size() == 2 &&
|
||||
tokens[0] == VersionToken &&
|
||||
VersionCheck( tokens[1] )
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if ( !sError && RecordEnabled() ) {
|
||||
wxFileName fName{ dataDir, "journal", "txt" };
|
||||
const auto path = fName.GetFullPath();
|
||||
sFileOut.Open( path );
|
||||
if ( sFileOut.IsOpened() )
|
||||
sFileOut.Clear();
|
||||
else {
|
||||
sFileOut.Create();
|
||||
sFileOut.Open( path );
|
||||
}
|
||||
if ( !sFileOut.IsOpened() )
|
||||
sError = true;
|
||||
else {
|
||||
// Generate a header
|
||||
Comment( wxString::Format(
|
||||
wxT("Journal recorded by %s on %s")
|
||||
, wxGetUserName()
|
||||
, wxDateTime::Now().Format()
|
||||
) );
|
||||
Output({ VersionToken, VersionString() });
|
||||
}
|
||||
}
|
||||
|
||||
return !sError;
|
||||
}
|
||||
|
||||
wxArrayStringEx GetTokens()
|
||||
{
|
||||
auto result = PeekTokens();
|
||||
if ( !result.empty() ) {
|
||||
NextIn();
|
||||
return result;
|
||||
}
|
||||
throw SyncException{};
|
||||
}
|
||||
|
||||
RegisteredCommand::RegisteredCommand(
|
||||
const wxString &name, Dispatcher dispatcher )
|
||||
{
|
||||
if ( !sDictionary().insert( { name, dispatcher } ).second ) {
|
||||
wxLogDebug( wxString::Format (
|
||||
wxT("Duplicated registration of Journal command name %s"),
|
||||
name
|
||||
) );
|
||||
// Cause failure of startup of journalling and graceful exit
|
||||
sError = true;
|
||||
}
|
||||
}
|
||||
|
||||
bool Dispatch()
|
||||
{
|
||||
if ( sError )
|
||||
// Don't repeatedly indicate error
|
||||
// Do nothing
|
||||
return false;
|
||||
|
||||
if ( !IsReplaying() )
|
||||
return false;
|
||||
|
||||
// This will throw if no lines remain. A proper journal should exit the
|
||||
// program before that happens.
|
||||
auto words = GetTokens();
|
||||
|
||||
// Lookup dispatch function by the first field of the line
|
||||
auto &table = sDictionary();
|
||||
auto &name = words[0];
|
||||
auto iter = table.find( name );
|
||||
if ( iter == table.end() )
|
||||
throw SyncException{};
|
||||
|
||||
// Pass all the fields including the command name to the function
|
||||
if ( !iter->second( words ) )
|
||||
throw SyncException{};
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void Output( const wxString &string )
|
||||
{
|
||||
if ( IsRecording() )
|
||||
sFileOut.AddLine( string );
|
||||
}
|
||||
|
||||
void Output( const wxArrayString &strings )
|
||||
{
|
||||
if ( IsRecording() )
|
||||
Output( ::wxJoin( strings, SeparatorCharacter ) );
|
||||
}
|
||||
|
||||
void Output( std::initializer_list< const wxString > strings )
|
||||
{
|
||||
return Output( wxArrayStringEx( strings ) );
|
||||
}
|
||||
|
||||
void Comment( const wxString &string )
|
||||
{
|
||||
if ( IsRecording() )
|
||||
sFileOut.AddLine( CommentCharacter + string );
|
||||
}
|
||||
|
||||
void Sync( const wxString &string )
|
||||
{
|
||||
if ( IsRecording() || IsReplaying() ) {
|
||||
if ( IsRecording() )
|
||||
sFileOut.AddLine( string );
|
||||
if ( IsReplaying() ) {
|
||||
if ( sFileIn.Eof() || sLine != string )
|
||||
throw SyncException{};
|
||||
NextIn();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Sync( const wxArrayString &strings )
|
||||
{
|
||||
if ( IsRecording() || IsReplaying() ) {
|
||||
auto string = ::wxJoin( strings, SeparatorCharacter );
|
||||
Sync( string );
|
||||
}
|
||||
}
|
||||
|
||||
void Sync( std::initializer_list< const wxString > strings )
|
||||
{
|
||||
return Sync( wxArrayStringEx( strings ) );
|
||||
}
|
||||
|
||||
int GetExitCode()
|
||||
{
|
||||
// Unconsumed commands remaining in the input file is also an error condition.
|
||||
if( !sError && !PeekTokens().empty() ) {
|
||||
NextIn();
|
||||
sError = true;
|
||||
}
|
||||
if ( sError ) {
|
||||
// Return nonzero
|
||||
// Returning the (1-based) line number at which the script failed is a
|
||||
// simple way to communicate that information to the test driver script.
|
||||
return sLineNumber ? sLineNumber : -1;
|
||||
}
|
||||
|
||||
// Return zero to mean all is well, the convention for command-line tools
|
||||
return 0;
|
||||
}
|
||||
|
||||
}
|
111
src/Journal.h
Normal file
111
src/Journal.h
Normal file
@ -0,0 +1,111 @@
|
||||
/**********************************************************************
|
||||
|
||||
Audacity: A Digital Audio Editor
|
||||
|
||||
Journal.h
|
||||
|
||||
Paul Licameli
|
||||
|
||||
**********************************************************************/
|
||||
|
||||
#ifndef __AUDACITY_JOURNAL__
|
||||
#define __AUDACITY_JOURNAL__
|
||||
|
||||
#include <functional>
|
||||
#include <initializer_list>
|
||||
#include "Identifier.h"
|
||||
class wxArrayString;
|
||||
class wxArrayStringEx;
|
||||
class wxString;
|
||||
|
||||
#include "AudacityException.h"
|
||||
|
||||
// Whether the journalling feature is shown to the end user
|
||||
#undef END_USER_JOURNALLING
|
||||
|
||||
namespace Journal
|
||||
{
|
||||
//\brief Whether recording is enabled; but recording will happen only if this
|
||||
// was true at application start up
|
||||
bool RecordEnabled();
|
||||
|
||||
//\brief Change the enablement of recording and store in preferences
|
||||
//\return whether successful
|
||||
bool SetRecordEnabled(bool value);
|
||||
|
||||
//\brief Whether actually recording.
|
||||
// IsRecording() && IsReplaying() is possible
|
||||
bool IsRecording();
|
||||
|
||||
//\brief Whether actually replaying.
|
||||
// IsRecording() && IsReplaying() is possible
|
||||
bool IsReplaying();
|
||||
|
||||
//\brief Set the played back journal file at start up
|
||||
void SetInputFileName( const wxString &path );
|
||||
|
||||
//\brief Initialize playback if a file name has been set, and initialize
|
||||
// output if recording is enabled.
|
||||
// @param dataDir the output journal.txt will be in this directory, and the
|
||||
// input file, if it was relative, is made absolute with respect to it
|
||||
// @return true if successful
|
||||
bool Begin( const FilePath &dataDir );
|
||||
|
||||
//\brief Consume next line from the input journal (skipping blank lines and
|
||||
// comments) and tokenize it.
|
||||
// Throws SyncException if no next line or not replaying
|
||||
wxArrayStringEx GetTokens();
|
||||
|
||||
//\brief Type of a function that interprets a line of the input journal.
|
||||
// It may indicate failure either by throwing SyncException or returning
|
||||
// false (which will cause Journal::Dispatch to throw a SyncException)
|
||||
using Dispatcher = std::function< bool(const wxArrayString &fields) >;
|
||||
|
||||
//\brief Associates a dispatcher with a keyword in the default dictionary.
|
||||
// The keyword will also be the first field passed to the dispatcher. This
|
||||
// struct is meant for static construction
|
||||
struct RegisteredCommand{
|
||||
explicit RegisteredCommand(
|
||||
const wxString &name, Dispatcher dispatcher );
|
||||
};
|
||||
|
||||
//\brief if playing back and commands remain, may execute one.
|
||||
// May throw SyncException if playing back but none remain, or if other error
|
||||
// conditions are encountered.
|
||||
// Returns true if any command was dispatched
|
||||
bool Dispatch();
|
||||
|
||||
//\brief write the strings to the output journal, if recording
|
||||
// None of them may contain newlines
|
||||
void Output( const wxString &string );
|
||||
void Output( const wxArrayString &strings );
|
||||
void Output( std::initializer_list< const wxString > strings );
|
||||
|
||||
//\brief if recording, emit a comment in the output journal that will have
|
||||
// no effect on playback
|
||||
void Comment( const wxString &string );
|
||||
|
||||
//\brief If recording, output the strings; if playing back, require
|
||||
// identical strings. None of them may contain newlines
|
||||
void Sync( const wxString &string );
|
||||
void Sync( const wxArrayString &strings );
|
||||
void Sync( std::initializer_list< const wxString > strings );
|
||||
|
||||
//\brief Get the value that the application will return to the command line
|
||||
int GetExitCode();
|
||||
|
||||
//\brief thrown when playback of a journal doesn't match the recording
|
||||
class SyncException : public AudacityException {
|
||||
public:
|
||||
SyncException();
|
||||
~SyncException() override;
|
||||
|
||||
// The delayed handler action forces the program to quit gracefully,
|
||||
// so that the test playback failure is prompty reported. This is
|
||||
// unlike other AudacityExceptions that roll back the project and
|
||||
// continue.
|
||||
void DelayedHandlerAction() override;
|
||||
};
|
||||
}
|
||||
|
||||
#endif
|
Loading…
x
Reference in New Issue
Block a user