mirror of
https://github.com/cookiengineer/audacity
synced 2025-04-30 15:49:41 +02:00
What I tried to do here, as well as the alternative fixes I considered, is way too large to explain. Long story short, I wanted to initially check whether Tenacity.Project OR Audacity.Project was defined, but a typo as I was formatting the change led to a logic error. However, even if I did what I wanted to do, that would mean that we'd "lose" our opportunity to ask the user whether they want to use Tenacity instead of other programs. The Audacity developers basically already fixed the problem that I was trying to fix, and the solution was right in front of us. Signed-off-by: Panagiotis Vasilopoulos <hello@alwayslivid.com>
2337 lines
79 KiB
C++
2337 lines
79 KiB
C++
/**********************************************************************
|
|
|
|
Tenacity
|
|
|
|
AudacityApp.cpp
|
|
|
|
Dominic Mazzoni
|
|
|
|
******************************************************************//**
|
|
|
|
\class AudacityApp
|
|
\brief AudacityApp is the 'main' class for Audacity
|
|
|
|
It handles initialization and termination by subclassing wxApp.
|
|
|
|
*//*******************************************************************/
|
|
|
|
|
|
#include "AudacityApp.h"
|
|
|
|
|
|
|
|
#if 0
|
|
// This may be used to debug memory leaks.
|
|
// See: Visual Leak Detector @ http://vld.codeplex.com/
|
|
#include <vld.h>
|
|
#endif
|
|
|
|
#include <wx/setup.h> // for wxUSE_* macros
|
|
#include <wx/wxcrtvararg.h>
|
|
#include <wx/defs.h>
|
|
#include <wx/evtloop.h>
|
|
#include <wx/app.h>
|
|
#include <wx/bitmap.h>
|
|
#include <wx/docview.h>
|
|
#include <wx/event.h>
|
|
#include <wx/ipc.h>
|
|
#include <wx/window.h>
|
|
#include <wx/intl.h>
|
|
#include <wx/menu.h>
|
|
#include <wx/snglinst.h>
|
|
#include <wx/splash.h>
|
|
#include <wx/stdpaths.h>
|
|
#include <wx/sysopt.h>
|
|
#include <wx/fontmap.h>
|
|
|
|
#include <wx/fs_zip.h>
|
|
#include <wx/image.h>
|
|
|
|
#include <wx/dir.h>
|
|
#include <wx/file.h>
|
|
#include <wx/filename.h>
|
|
|
|
#ifdef __WXGTK__
|
|
#include <unistd.h>
|
|
#ifdef HAVE_GTK
|
|
#include <gtk/gtk.h>
|
|
#endif
|
|
#endif
|
|
|
|
// chmod, lstat, geteuid
|
|
#ifdef __UNIX__
|
|
#include <sys/types.h>
|
|
#include <sys/file.h>
|
|
#include <sys/stat.h>
|
|
#include <stdio.h>
|
|
#endif
|
|
|
|
#if defined(__WXMSW__)
|
|
#include <wx/msw/registry.h> // for wxRegKey
|
|
#endif
|
|
|
|
#include "AudacityLogger.h"
|
|
#include "AboutDialog.h"
|
|
#include "AColor.h"
|
|
#include "AudacityFileConfig.h"
|
|
#include "AudioIO.h"
|
|
#include "Benchmark.h"
|
|
#include "Clipboard.h"
|
|
#include "commands/CommandHandler.h"
|
|
#include "commands/AppCommandEvent.h"
|
|
#include "widgets/ASlider.h"
|
|
#include "FFmpeg.h"
|
|
//#include "LangChoice.h"
|
|
#include "Languages.h"
|
|
#include "Menus.h"
|
|
#include "PluginManager.h"
|
|
#include "Project.h"
|
|
#include "ProjectAudioIO.h"
|
|
#include "ProjectAudioManager.h"
|
|
#include "ProjectFileIO.h"
|
|
#include "ProjectFileManager.h"
|
|
#include "ProjectHistory.h"
|
|
#include "ProjectManager.h"
|
|
#include "ProjectSettings.h"
|
|
#include "ProjectWindow.h"
|
|
#include "Screenshot.h"
|
|
#include "Sequence.h"
|
|
#include "TempDirectory.h"
|
|
#include "Track.h"
|
|
#include "prefs/PrefsDialog.h"
|
|
#include "Theme.h"
|
|
#include "PlatformCompatibility.h"
|
|
#include "AutoRecoveryDialog.h"
|
|
#include "SplashDialog.h"
|
|
#include "FFT.h"
|
|
#include "widgets/AudacityMessageBox.h"
|
|
#include "prefs/DirectoriesPrefs.h"
|
|
#include "prefs/GUIPrefs.h"
|
|
#include "tracks/ui/Scrubbing.h"
|
|
#include "widgets/FileConfig.h"
|
|
#include "widgets/FileHistory.h"
|
|
|
|
#ifdef EXPERIMENTAL_EASY_CHANGE_KEY_BINDINGS
|
|
#include "prefs/KeyConfigPrefs.h"
|
|
#endif
|
|
|
|
//temporarily commented out till it is added to all projects
|
|
//#include "Profiler.h"
|
|
|
|
#include "ModuleManager.h"
|
|
|
|
#include "import/Import.h"
|
|
|
|
#ifdef EXPERIMENTAL_SCOREALIGN
|
|
#include "effects/ScoreAlignDialog.h"
|
|
#endif
|
|
|
|
#if 0
|
|
#ifdef _DEBUG
|
|
#ifdef _MSC_VER
|
|
#undef THIS_FILE
|
|
static char* THIS_FILE = __FILE__;
|
|
#define new new(_NORMAL_BLOCK, THIS_FILE, __LINE__)
|
|
#endif
|
|
#endif
|
|
#endif
|
|
|
|
// DA: Logo for Splash Screen
|
|
#ifdef EXPERIMENTAL_DA
|
|
#include "../images/DarkAudacityLogoWithName.xpm"
|
|
#else
|
|
#include "../images/AudacityLogoWithName.xpm"
|
|
#endif
|
|
|
|
#include <thread>
|
|
|
|
|
|
////////////////////////////////////////////////////////////
|
|
/// Custom events
|
|
////////////////////////////////////////////////////////////
|
|
|
|
#if 0
|
|
#ifdef __WXGTK__
|
|
static void wxOnAssert(const wxChar* fileName, int lineNumber, const wxChar* msg) {
|
|
if (msg)
|
|
wxPrintf("ASSERTION FAILED: %s\n%s: %d\n", (const char*)wxString(msg).mb_str(), (const char*)wxString(fileName).mb_str(), lineNumber);
|
|
else
|
|
wxPrintf("ASSERTION FAILED!\n%s: %d\n", (const char*)wxString(fileName).mb_str(), lineNumber);
|
|
|
|
// Force core dump
|
|
int* i = 0;
|
|
if (*i)
|
|
exit(1);
|
|
|
|
exit(0);
|
|
}
|
|
#endif
|
|
#endif
|
|
|
|
namespace
|
|
{
|
|
|
|
void PopulatePreferences() {
|
|
bool resetPrefs = false;
|
|
wxString langCode = gPrefs->Read(wxT("/Locale/Language"), wxEmptyString);
|
|
bool writeLang = false;
|
|
|
|
const wxFileName fn(
|
|
FileNames::ResourcesDir(),
|
|
wxT("FirstTime.ini"));
|
|
if (fn.FileExists()) // it will exist if the (win) installer put it there
|
|
{
|
|
const wxString fullPath{fn.GetFullPath()};
|
|
|
|
auto pIni =
|
|
AudacityFileConfig::Create({}, {}, fullPath, {},
|
|
wxCONFIG_USE_LOCAL_FILE);
|
|
auto& ini = *pIni;
|
|
|
|
wxString lang;
|
|
if (ini.Read(wxT("/FromInno/Language"), &lang) && !lang.empty()) {
|
|
// Only change "langCode" if the language was actually specified in the ini file.
|
|
langCode = lang;
|
|
writeLang = true;
|
|
|
|
// Inno Setup doesn't allow special characters in the Name values, so "0" is used
|
|
// to represent the "@" character.
|
|
langCode.Replace(wxT("0"), wxT("@"));
|
|
}
|
|
|
|
ini.Read(wxT("/FromInno/ResetPrefs"), &resetPrefs, false);
|
|
|
|
bool gone = wxRemoveFile(fullPath); // remove FirstTime.ini
|
|
if (!gone) {
|
|
AudacityMessageBox(
|
|
XO("Failed to remove %s").Format(fullPath),
|
|
XO("Failed!"));
|
|
}
|
|
}
|
|
|
|
// Use the system default language if one wasn't specified or if the user selected System.
|
|
if (langCode.empty())
|
|
langCode =
|
|
Languages::GetSystemLanguageCode(FileNames::AudacityPathList());
|
|
|
|
langCode = GUIPrefs::SetLang(langCode);
|
|
|
|
// User requested that the preferences be completely reset
|
|
if (resetPrefs) {
|
|
// pop up a dialogue
|
|
auto prompt = XO(
|
|
"Reset Preferences?\n\nThis is a one-time question, after an 'install' where you asked to have the Preferences reset.");
|
|
int action = AudacityMessageBox(
|
|
prompt,
|
|
XO("Reset Audacity Preferences"),
|
|
wxYES_NO, NULL);
|
|
if (action == wxYES) // reset
|
|
{
|
|
gPrefs->DeleteAll();
|
|
writeLang = true;
|
|
}
|
|
}
|
|
|
|
// Save the specified language
|
|
if (writeLang) {
|
|
gPrefs->Write(wxT("/Locale/Language"), langCode);
|
|
}
|
|
|
|
// In AUdacity 2.1.0 support for the legacy 1.2.x preferences (depreciated since Audacity
|
|
// 1.3.1) is dropped. As a result we can drop the import flag
|
|
// first time this version of Audacity is run we try to migrate
|
|
// old preferences.
|
|
bool newPrefsInitialized = false;
|
|
gPrefs->Read(wxT("/NewPrefsInitialized"), &newPrefsInitialized, false);
|
|
if (newPrefsInitialized) {
|
|
gPrefs->DeleteEntry(wxT("/NewPrefsInitialized"), true); // take group as well if empty
|
|
}
|
|
|
|
// record the Prefs version for future checking (this has not been used for a very
|
|
// long time).
|
|
gPrefs->Write(wxT("/PrefsVersion"), wxString(wxT(AUDACITY_PREFS_VERSION_STRING)));
|
|
|
|
// Check if some prefs updates need to happen based on audacity version.
|
|
// Unfortunately we can't use the PrefsVersion prefs key because that resets things.
|
|
// In the future we may want to integrate that better.
|
|
// these are done on a case-by-case basis for now so they must be backwards compatible
|
|
// (meaning the changes won't mess audacity up if the user goes back to an earlier version)
|
|
int vMajor = gPrefs->Read(wxT("/Version/Major"), (long)0);
|
|
int vMinor = gPrefs->Read(wxT("/Version/Minor"), (long)0);
|
|
int vMicro = gPrefs->Read(wxT("/Version/Micro"), (long)0);
|
|
|
|
gPrefs->SetVersionKeysInit(vMajor, vMinor, vMicro); // make a note of these initial values
|
|
// for use by ToolManager::ReadConfig()
|
|
|
|
// These integer version keys were introduced april 4 2011 for 1.3.13
|
|
// The device toolbar needs to be enabled due to removal of source selection features in
|
|
// the mixer toolbar.
|
|
if ((vMajor < 1) ||
|
|
(vMajor == 1 && vMinor < 3) ||
|
|
(vMajor == 1 && vMinor == 3 && vMicro < 13)) {
|
|
|
|
|
|
// Do a full reset of the Device Toolbar to get it on the screen.
|
|
if (gPrefs->Exists(wxT("/GUI/ToolBars/Device")))
|
|
gPrefs->DeleteGroup(wxT("/GUI/ToolBars/Device"));
|
|
|
|
// We keep the mixer toolbar prefs (shown/not shown)
|
|
// the width of the mixer toolbar may have shrunk, the prefs will keep the larger value
|
|
// if the user had a device that had more than one source.
|
|
if (gPrefs->Exists(wxT("/GUI/ToolBars/Mixer"))) {
|
|
// Use the default width
|
|
gPrefs->Write(wxT("/GUI/ToolBars/Mixer/W"), -1);
|
|
}
|
|
}
|
|
|
|
// In 2.1.0, the Meter toolbar was split and lengthened, but strange arrangements happen
|
|
// if upgrading due to the extra length. So, if a user is upgrading, use the pre-2.1.0
|
|
// lengths, but still use the NEW split versions.
|
|
if (gPrefs->Exists(wxT("/GUI/ToolBars/Meter")) &&
|
|
!gPrefs->Exists(wxT("/GUI/ToolBars/CombinedMeter"))) {
|
|
|
|
// Read in all of the existing values
|
|
long dock, order, show, x, y, w, h;
|
|
gPrefs->Read(wxT("/GUI/ToolBars/Meter/Dock"), &dock, -1);
|
|
gPrefs->Read(wxT("/GUI/ToolBars/Meter/Order"), &order, -1);
|
|
gPrefs->Read(wxT("/GUI/ToolBars/Meter/Show"), &show, -1);
|
|
gPrefs->Read(wxT("/GUI/ToolBars/Meter/X"), &x, -1);
|
|
gPrefs->Read(wxT("/GUI/ToolBars/Meter/Y"), &y, -1);
|
|
gPrefs->Read(wxT("/GUI/ToolBars/Meter/W"), &w, -1);
|
|
gPrefs->Read(wxT("/GUI/ToolBars/Meter/H"), &h, -1);
|
|
|
|
// "Order" must be adjusted since we're inserting two NEW toolbars
|
|
if (dock > 0) {
|
|
wxString oldPath = gPrefs->GetPath();
|
|
gPrefs->SetPath(wxT("/GUI/ToolBars"));
|
|
|
|
wxString bar;
|
|
long ndx = 0;
|
|
bool cont = gPrefs->GetFirstGroup(bar, ndx);
|
|
while (cont) {
|
|
long o;
|
|
if (gPrefs->Read(bar + wxT("/Order"), &o) && o >= order) {
|
|
gPrefs->Write(bar + wxT("/Order"), o + 2);
|
|
}
|
|
cont = gPrefs->GetNextGroup(bar, ndx);
|
|
}
|
|
gPrefs->SetPath(oldPath);
|
|
|
|
// And override the height
|
|
h = 27;
|
|
}
|
|
|
|
// Write the split meter bar values
|
|
gPrefs->Write(wxT("/GUI/ToolBars/RecordMeter/Dock"), dock);
|
|
gPrefs->Write(wxT("/GUI/ToolBars/RecordMeter/Order"), order);
|
|
gPrefs->Write(wxT("/GUI/ToolBars/RecordMeter/Show"), show);
|
|
gPrefs->Write(wxT("/GUI/ToolBars/RecordMeter/X"), -1);
|
|
gPrefs->Write(wxT("/GUI/ToolBars/RecordMeter/Y"), -1);
|
|
gPrefs->Write(wxT("/GUI/ToolBars/RecordMeter/W"), w);
|
|
gPrefs->Write(wxT("/GUI/ToolBars/RecordMeter/H"), h);
|
|
gPrefs->Write(wxT("/GUI/ToolBars/PlayMeter/Dock"), dock);
|
|
gPrefs->Write(wxT("/GUI/ToolBars/PlayMeter/Order"), order + 1);
|
|
gPrefs->Write(wxT("/GUI/ToolBars/PlayMeter/Show"), show);
|
|
gPrefs->Write(wxT("/GUI/ToolBars/PlayMeter/X"), -1);
|
|
gPrefs->Write(wxT("/GUI/ToolBars/PlayMeter/Y"), -1);
|
|
gPrefs->Write(wxT("/GUI/ToolBars/PlayMeter/W"), w);
|
|
gPrefs->Write(wxT("/GUI/ToolBars/PlayMeter/H"), h);
|
|
|
|
// And hide the old combined meter bar
|
|
gPrefs->Write(wxT("/GUI/ToolBars/Meter/Dock"), -1);
|
|
}
|
|
|
|
// Upgrading pre 2.2.0 configs we assume extended set of defaults.
|
|
if ((0 < vMajor && vMajor < 2) ||
|
|
(vMajor == 2 && vMinor < 2)) {
|
|
gPrefs->Write(wxT("/GUI/Shortcuts/FullDefaults"), 1);
|
|
}
|
|
|
|
// Upgrading pre 2.4.0 configs, the selection toolbar is now split.
|
|
if ((0 < vMajor && vMajor < 2) ||
|
|
(vMajor == 2 && vMinor < 4)) {
|
|
gPrefs->Write(wxT("/GUI/Toolbars/Selection/W"), "");
|
|
gPrefs->Write(wxT("/GUI/Toolbars/SpectralSelection/W"), "");
|
|
gPrefs->Write(wxT("/GUI/Toolbars/Time/X"), -1);
|
|
gPrefs->Write(wxT("/GUI/Toolbars/Time/Y"), -1);
|
|
gPrefs->Write(wxT("/GUI/Toolbars/Time/H"), 55);
|
|
gPrefs->Write(wxT("/GUI/Toolbars/Time/W"), 251);
|
|
gPrefs->Write(wxT("/GUI/Toolbars/Time/DockV2"), 2);
|
|
gPrefs->Write(wxT("/GUI/Toolbars/Time/Dock"), 2);
|
|
gPrefs->Write(wxT("/GUI/Toolbars/Time/Path"), "0,1");
|
|
gPrefs->Write(wxT("/GUI/Toolbars/Time/Show"), 1);
|
|
}
|
|
|
|
// write out the version numbers to the prefs file for future checking
|
|
gPrefs->Write(wxT("/Version/Major"), AUDACITY_VERSION);
|
|
gPrefs->Write(wxT("/Version/Minor"), AUDACITY_RELEASE);
|
|
gPrefs->Write(wxT("/Version/Micro"), AUDACITY_REVISION);
|
|
|
|
gPrefs->Flush();
|
|
}
|
|
|
|
}
|
|
|
|
static bool gInited = false;
|
|
static bool gIsQuitting = false;
|
|
|
|
static void QuitAudacity(bool bForce) {
|
|
// guard against recursion
|
|
if (gIsQuitting)
|
|
return;
|
|
|
|
gIsQuitting = true;
|
|
|
|
wxTheApp->SetExitOnFrameDelete(true);
|
|
|
|
// Try to close each open window. If the user hits Cancel
|
|
// in a Save Changes dialog, don't continue.
|
|
// BG: unless force is true
|
|
|
|
// BG: Are there any projects open?
|
|
//- if (!AllProjects{}.empty())
|
|
/*start+*/
|
|
if (AllProjects{}.empty()) {
|
|
#ifdef __WXMAC__
|
|
Clipboard::Get().Clear();
|
|
#endif
|
|
} else
|
|
/*end+*/
|
|
{
|
|
if (AllProjects{}.size())
|
|
// PRL: Always did at least once before close might be vetoed
|
|
// though I don't know why that is important
|
|
ProjectManager::SaveWindowSize();
|
|
bool closedAll = AllProjects::Close(bForce);
|
|
if (!closedAll) {
|
|
gIsQuitting = false;
|
|
return;
|
|
}
|
|
}
|
|
|
|
ModuleManager::Get().Dispatch(AppQuiting);
|
|
|
|
#ifdef EXPERIMENTAL_SCOREALIGN
|
|
CloseScoreAlignDialog();
|
|
#endif
|
|
CloseScreenshotTools();
|
|
|
|
//print out profile if we have one by deleting it
|
|
//temporarily commented out till it is added to all projects
|
|
//DELETE Profiler::Instance();
|
|
|
|
// Save last log for diagnosis
|
|
auto logger = AudacityLogger::Get();
|
|
if (logger) {
|
|
wxFileName logFile(FileNames::DataDir(), wxT("lastlog.txt"));
|
|
logger->SaveLog(logFile.GetFullPath());
|
|
}
|
|
|
|
//remove our logger
|
|
std::unique_ptr<wxLog>{ wxLog::SetActiveTarget(NULL) }; // DELETE
|
|
|
|
if (bForce) {
|
|
wxExit();
|
|
}
|
|
}
|
|
|
|
static void QuitAudacity() {
|
|
QuitAudacity(false);
|
|
}
|
|
|
|
#if defined(__WXGTK__) && defined(HAVE_GTK)
|
|
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
// Provide the ability to receive notification from the session manager
|
|
// when the user is logging out or shutting down.
|
|
//
|
|
// Most of this was taken from nsNativeAppSupportUnix.cpp from Mozilla.
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
|
|
// TODO: May need updating. Is this code too obsolete (relying on Gnome2 so's) to be
|
|
// worth keeping anymore?
|
|
// CB suggests we use libSM directly ref:
|
|
// http://www.x.org/archive/X11R7.7/doc/libSM/SMlib.html#The_Save_Yourself_Callback
|
|
|
|
#include <dlfcn.h>
|
|
/* There is a conflict between the type names used in Glib >= 2.21 and those in
|
|
* wxGTK (http://trac.wxwidgets.org/ticket/10883)
|
|
* Happily we can avoid the hack, as we only need some of the headers, not
|
|
* the full GTK headers
|
|
*/
|
|
#include <glib-object.h>
|
|
|
|
typedef struct _GnomeProgram GnomeProgram;
|
|
typedef struct _GnomeModuleInfo GnomeModuleInfo;
|
|
typedef struct _GnomeClient GnomeClient;
|
|
|
|
typedef enum
|
|
{
|
|
GNOME_SAVE_GLOBAL,
|
|
GNOME_SAVE_LOCAL,
|
|
GNOME_SAVE_BOTH
|
|
} GnomeSaveStyle;
|
|
|
|
typedef enum
|
|
{
|
|
GNOME_INTERACT_NONE,
|
|
GNOME_INTERACT_ERRORS,
|
|
GNOME_INTERACT_ANY
|
|
} GnomeInteractStyle;
|
|
|
|
typedef enum
|
|
{
|
|
GNOME_DIALOG_ERROR,
|
|
GNOME_DIALOG_NORMAL
|
|
} GnomeDialogType;
|
|
|
|
typedef GnomeProgram* (*_gnome_program_init_fn)(const char*,
|
|
const char*,
|
|
const GnomeModuleInfo*,
|
|
int,
|
|
char**,
|
|
const char*,
|
|
...);
|
|
typedef const GnomeModuleInfo* (*_libgnomeui_module_info_get_fn)();
|
|
typedef GnomeClient* (*_gnome_master_client_fn)(void);
|
|
typedef void (*GnomeInteractFunction)(GnomeClient*,
|
|
gint,
|
|
GnomeDialogType,
|
|
gpointer);
|
|
typedef void (*_gnome_client_request_interaction_fn)(GnomeClient*,
|
|
GnomeDialogType,
|
|
GnomeInteractFunction,
|
|
gpointer);
|
|
typedef void (*_gnome_interaction_key_return_fn)(gint, gboolean);
|
|
|
|
static _gnome_client_request_interaction_fn gnome_client_request_interaction;
|
|
static _gnome_interaction_key_return_fn gnome_interaction_key_return;
|
|
|
|
static void interact_cb(GnomeClient* /* client */,
|
|
gint key,
|
|
GnomeDialogType /* type */,
|
|
gpointer /* data */) {
|
|
wxCloseEvent e(wxEVT_QUERY_END_SESSION, wxID_ANY);
|
|
e.SetEventObject(&wxGetApp());
|
|
e.SetCanVeto(true);
|
|
|
|
wxGetApp().ProcessEvent(e);
|
|
|
|
gnome_interaction_key_return(key, e.GetVeto());
|
|
}
|
|
|
|
static gboolean save_yourself_cb(GnomeClient* client,
|
|
gint /* phase */,
|
|
GnomeSaveStyle /* style */,
|
|
gboolean shutdown,
|
|
GnomeInteractStyle interact,
|
|
gboolean /* fast */,
|
|
gpointer /* user_data */) {
|
|
if (!shutdown || interact != GNOME_INTERACT_ANY) {
|
|
return TRUE;
|
|
}
|
|
|
|
if (AllProjects{}.empty()) {
|
|
return TRUE;
|
|
}
|
|
|
|
gnome_client_request_interaction(client,
|
|
GNOME_DIALOG_NORMAL,
|
|
interact_cb,
|
|
NULL);
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
class GnomeShutdown
|
|
{
|
|
public:
|
|
GnomeShutdown() {
|
|
mArgv[0].reset(strdup("Tenacity"));
|
|
|
|
mGnomeui = dlopen("libgnomeui-2.so.0", RTLD_NOW);
|
|
if (!mGnomeui) {
|
|
return;
|
|
}
|
|
|
|
mGnome = dlopen("libgnome-2.so.0", RTLD_NOW);
|
|
if (!mGnome) {
|
|
return;
|
|
}
|
|
|
|
_gnome_program_init_fn gnome_program_init = (_gnome_program_init_fn)
|
|
dlsym(mGnome, "gnome_program_init");
|
|
_libgnomeui_module_info_get_fn libgnomeui_module_info_get = (_libgnomeui_module_info_get_fn)
|
|
dlsym(mGnomeui, "libgnomeui_module_info_get");
|
|
_gnome_master_client_fn gnome_master_client = (_gnome_master_client_fn)
|
|
dlsym(mGnomeui, "gnome_master_client");
|
|
|
|
gnome_client_request_interaction = (_gnome_client_request_interaction_fn)
|
|
dlsym(mGnomeui, "gnome_client_request_interaction");
|
|
gnome_interaction_key_return = (_gnome_interaction_key_return_fn)
|
|
dlsym(mGnomeui, "gnome_interaction_key_return");
|
|
|
|
|
|
if (!gnome_program_init || !libgnomeui_module_info_get) {
|
|
return;
|
|
}
|
|
|
|
gnome_program_init(mArgv[0].get(),
|
|
"1.0",
|
|
libgnomeui_module_info_get(),
|
|
1,
|
|
reinterpret_cast<char**>(mArgv),
|
|
NULL);
|
|
|
|
mClient = gnome_master_client();
|
|
if (mClient == NULL) {
|
|
return;
|
|
}
|
|
|
|
g_signal_connect(mClient, "save-yourself", G_CALLBACK(save_yourself_cb), NULL);
|
|
}
|
|
|
|
virtual ~GnomeShutdown() {
|
|
// Do not dlclose() the libraries here lest you want segfaults...
|
|
}
|
|
|
|
private:
|
|
|
|
MallocString<> mArgv[1];
|
|
void* mGnomeui;
|
|
void* mGnome;
|
|
GnomeClient* mClient;
|
|
};
|
|
|
|
// This variable exists to call the constructor and
|
|
// connect a signal for the 'save-yourself' message.
|
|
GnomeShutdown GnomeShutdownInstance;
|
|
|
|
#endif
|
|
|
|
// Where drag/drop or "Open With" filenames get stored until
|
|
// the timer routine gets around to picking them up.
|
|
static wxArrayString ofqueue;
|
|
|
|
//
|
|
// DDE support for opening multiple files with one instance
|
|
// of Audacity.
|
|
//
|
|
|
|
#define IPC_APPL wxT("tenacity")
|
|
#define IPC_TOPIC wxT("System")
|
|
|
|
class IPCConn final : public wxConnection
|
|
{
|
|
public:
|
|
IPCConn()
|
|
: wxConnection() {
|
|
};
|
|
|
|
~IPCConn() {
|
|
};
|
|
|
|
bool OnExec(const wxString& WXUNUSED(topic),
|
|
const wxString& data) {
|
|
// Add the filename to the queue. It will be opened by
|
|
// the OnTimer() event when it is safe to do so.
|
|
ofqueue.push_back(data);
|
|
|
|
return true;
|
|
}
|
|
};
|
|
|
|
class IPCServ final : public wxServer
|
|
{
|
|
public:
|
|
IPCServ(const wxString& appl)
|
|
: wxServer() {
|
|
Create(appl);
|
|
};
|
|
|
|
~IPCServ() {
|
|
};
|
|
|
|
wxConnectionBase* OnAcceptConnection(const wxString& topic) override {
|
|
if (topic != IPC_TOPIC) {
|
|
return NULL;
|
|
}
|
|
|
|
// Trust wxWidgets framework to DELETE it
|
|
return safenew IPCConn();
|
|
};
|
|
};
|
|
|
|
#if defined(__WXMAC__)
|
|
|
|
IMPLEMENT_APP_NO_MAIN(AudacityApp)
|
|
IMPLEMENT_WX_THEME_SUPPORT
|
|
|
|
int main(int argc, char* argv[]) {
|
|
wxDISABLE_DEBUG_SUPPORT();
|
|
|
|
return wxEntry(argc, argv);
|
|
}
|
|
|
|
#elif defined(__WXGTK__) && defined(NDEBUG)
|
|
|
|
IMPLEMENT_APP_NO_MAIN(AudacityApp)
|
|
IMPLEMENT_WX_THEME_SUPPORT
|
|
|
|
int main(int argc, char* argv[]) {
|
|
wxDISABLE_DEBUG_SUPPORT();
|
|
|
|
// Bug #1986 workaround - This doesn't actually reduce the number of
|
|
// messages, it simply hides them in Release builds. We'll probably
|
|
// never be able to get rid of the messages entirely, but we should
|
|
// look into what's causing them, so allow them to show in Debug
|
|
// builds.
|
|
stdout = freopen("/dev/null", "w", stdout);
|
|
stderr = freopen("/dev/null", "w", stderr);
|
|
|
|
return wxEntry(argc, argv);
|
|
}
|
|
|
|
#else
|
|
IMPLEMENT_APP(AudacityApp)
|
|
#endif
|
|
|
|
#ifdef __WXMAC__
|
|
|
|
// in response of an open-document apple event
|
|
void AudacityApp::MacOpenFile(const wxString& fileName) {
|
|
ofqueue.push_back(fileName);
|
|
}
|
|
|
|
// in response of a print-document apple event
|
|
void AudacityApp::MacPrintFile(const wxString& fileName) {
|
|
ofqueue.push_back(fileName);
|
|
}
|
|
|
|
// in response of a open-application apple event
|
|
void AudacityApp::MacNewFile() {
|
|
if (!gInited)
|
|
return;
|
|
|
|
// This method should only be used on the Mac platform
|
|
// when no project windows are open.
|
|
|
|
if (AllProjects{}.empty())
|
|
(void)ProjectManager::New();
|
|
}
|
|
|
|
#endif //__WXMAC__
|
|
|
|
// IPC communication
|
|
#define ID_IPC_SERVER 6200
|
|
#define ID_IPC_SOCKET 6201
|
|
|
|
// we don't really care about the timer id, but set this value just in case we do in the future
|
|
#define kAudacityAppTimerID 0
|
|
|
|
BEGIN_EVENT_TABLE(AudacityApp, wxApp)
|
|
EVT_QUERY_END_SESSION(AudacityApp::OnQueryEndSession)
|
|
EVT_END_SESSION(AudacityApp::OnEndSession)
|
|
|
|
EVT_TIMER(kAudacityAppTimerID, AudacityApp::OnTimer)
|
|
#ifdef __WXMAC__
|
|
EVT_MENU(wxID_NEW, AudacityApp::OnMenuNew)
|
|
EVT_MENU(wxID_OPEN, AudacityApp::OnMenuOpen)
|
|
EVT_MENU(wxID_ABOUT, AudacityApp::OnMenuAbout)
|
|
EVT_MENU(wxID_PREFERENCES, AudacityApp::OnMenuPreferences)
|
|
#endif
|
|
|
|
// Associate the handler with the menu id on all operating systems, even
|
|
// if they don't have an application menu bar like in macOS, so that
|
|
// other parts of the program can send the application a shut-down
|
|
// event
|
|
EVT_MENU(wxID_EXIT, AudacityApp::OnMenuExit)
|
|
|
|
#ifndef __WXMSW__
|
|
EVT_SOCKET(ID_IPC_SERVER, AudacityApp::OnServerEvent)
|
|
EVT_SOCKET(ID_IPC_SOCKET, AudacityApp::OnSocketEvent)
|
|
#endif
|
|
|
|
// Recent file event handlers.
|
|
EVT_MENU(FileHistory::ID_RECENT_CLEAR, AudacityApp::OnMRUClear)
|
|
EVT_MENU_RANGE(FileHistory::ID_RECENT_FIRST, FileHistory::ID_RECENT_LAST,
|
|
AudacityApp::OnMRUFile)
|
|
|
|
// Handle AppCommandEvents (usually from a script)
|
|
EVT_APP_COMMAND(wxID_ANY, AudacityApp::OnReceiveCommand)
|
|
|
|
// Global ESC key handling
|
|
EVT_KEY_DOWN(AudacityApp::OnKeyDown)
|
|
END_EVENT_TABLE()
|
|
|
|
// backend for OnMRUFile
|
|
// TODO: Would be nice to make this handle not opening a file with more panache.
|
|
// - Inform the user if DefaultOpenPath not set.
|
|
// - Switch focus to correct instance of project window, if already open.
|
|
bool AudacityApp::MRUOpen(const FilePath& fullPathStr) {
|
|
// Most of the checks below are copied from ProjectManager::OpenFiles.
|
|
// - some rationalisation might be possible.
|
|
|
|
AudacityProject* proj = GetActiveProject();
|
|
|
|
if (!fullPathStr.empty()) {
|
|
// verify that the file exists
|
|
if (wxFile::Exists(fullPathStr)) {
|
|
FileNames::UpdateDefaultPath(FileNames::Operation::Open, ::wxPathOnly(fullPathStr));
|
|
|
|
// Make sure it isn't already open.
|
|
// Test here even though AudacityProject::OpenFile() also now checks, because
|
|
// that method does not return the bad result.
|
|
// That itself may be a FIXME.
|
|
if (ProjectFileManager::IsAlreadyOpen(fullPathStr))
|
|
return false;
|
|
|
|
(void)ProjectManager::OpenProject(proj, fullPathStr,
|
|
true /* addtohistory */, false /* reuseNonemptyProject */);
|
|
} else {
|
|
// File doesn't exist - remove file from history
|
|
AudacityMessageBox(
|
|
XO(
|
|
"%s could not be found.\n\nIt has been removed from the list of recent files.")
|
|
.Format(fullPathStr));
|
|
return(false);
|
|
}
|
|
}
|
|
return(true);
|
|
}
|
|
|
|
bool AudacityApp::SafeMRUOpen(const wxString& fullPathStr) {
|
|
return GuardedCall< bool >([&] { return MRUOpen(fullPathStr); });
|
|
}
|
|
|
|
void AudacityApp::OnMRUClear(wxCommandEvent& WXUNUSED(event)) {
|
|
FileHistory::Global().Clear();
|
|
}
|
|
|
|
//vvv Basically, anything from Recent Files is treated as a .aup3, until proven otherwise,
|
|
// then it tries to Import(). Very questionable handling, imo.
|
|
// Better, for example, to check the file type early on.
|
|
void AudacityApp::OnMRUFile(wxCommandEvent& event) {
|
|
int n = event.GetId() - FileHistory::ID_RECENT_FIRST;
|
|
auto& history = FileHistory::Global();
|
|
const auto& fullPathStr = history[n];
|
|
|
|
// Try to open only if not already open.
|
|
// Test IsAlreadyOpen() here even though AudacityProject::MRUOpen() also now checks,
|
|
// because we don't want to Remove() just because it already exists,
|
|
// and AudacityApp::OnMacOpenFile() calls MRUOpen() directly.
|
|
// that method does not return the bad result.
|
|
// PRL: Don't call SafeMRUOpen
|
|
// -- if open fails for some exceptional reason of resource exhaustion that
|
|
// the user can correct, leave the file in history.
|
|
if (!ProjectFileManager::IsAlreadyOpen(fullPathStr) && !MRUOpen(fullPathStr))
|
|
history.Remove(n);
|
|
}
|
|
|
|
void AudacityApp::OnTimer(wxTimerEvent& WXUNUSED(event)) {
|
|
// Filenames are queued when Audacity receives a few of the
|
|
// AppleEvent messages (via wxWidgets). So, open any that are
|
|
// in the queue and clean the queue.
|
|
if (gInited) {
|
|
if (ofqueue.size()) {
|
|
// Load each file on the queue
|
|
while (ofqueue.size()) {
|
|
wxString name;
|
|
name.swap(ofqueue[0]);
|
|
ofqueue.erase(ofqueue.begin());
|
|
|
|
// Get the user's attention if no file name was specified
|
|
if (name.empty()) {
|
|
// Get the users attention
|
|
AudacityProject* project = GetActiveProject();
|
|
if (project) {
|
|
auto& window = GetProjectFrame(*project);
|
|
window.Maximize();
|
|
window.Raise();
|
|
window.RequestUserAttention();
|
|
}
|
|
continue;
|
|
}
|
|
|
|
// TODO: Handle failures better.
|
|
// Some failures are OK, e.g. file not found, just would-be-nices to do better,
|
|
// so FAIL_MSG is more a case of an enhancement request than an actual problem.
|
|
// LL: In all but one case an appropriate message is already displayed. The
|
|
// instance that a message is NOT displayed is when a failure to write
|
|
// to the config file has occurred.
|
|
// PRL: Catch any exceptions, don't try this file again, continue to
|
|
// other files.
|
|
if (!SafeMRUOpen(name)) {
|
|
// Just log it. Assertion failure is not appropriate on valid
|
|
// defensive path against bad file data.
|
|
wxLogMessage(wxT("MRUOpen failed"));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#if defined(__WXMSW__)
|
|
#define WL(lang, sublang) (lang), (sublang),
|
|
#else
|
|
#define WL(lang,sublang)
|
|
#endif
|
|
|
|
#if wxCHECK_VERSION(3, 0, 1)
|
|
wxLanguageInfo userLangs[] =
|
|
{
|
|
// Bosnian is defined in wxWidgets already
|
|
// { wxLANGUAGE_USER_DEFINED, wxT("bs"), WL(0, SUBLANG_DEFAULT) wxT("Bosnian"), wxLayout_LeftToRight },
|
|
|
|
{wxLANGUAGE_USER_DEFINED, wxT("eu"), WL(0, SUBLANG_DEFAULT) wxT("Basque"), wxLayout_LeftToRight},
|
|
};
|
|
#endif
|
|
|
|
void AudacityApp::OnFatalException() {
|
|
exit(-1);
|
|
}
|
|
|
|
|
|
#ifdef _MSC_VER
|
|
// If this is compiled with MSVC (Visual Studio)
|
|
#pragma warning( push )
|
|
#pragma warning( disable : 4702) // unreachable code warning.
|
|
#endif //_MSC_VER
|
|
|
|
bool AudacityApp::OnExceptionInMainLoop() {
|
|
// This function is invoked from catch blocks in the wxWidgets framework,
|
|
// and throw; without argument re-throws the exception being handled,
|
|
// letting us dispatch according to its type.
|
|
|
|
try { throw; } catch (AudacityException& e) {
|
|
(void)e;// Compiler food
|
|
// Here is the catch-all for our own exceptions
|
|
|
|
// Use CallAfter to delay this to the next pass of the event loop,
|
|
// rather than risk doing it inside stack unwinding.
|
|
auto pProject = ::GetActiveProject();
|
|
auto pException = std::current_exception();
|
|
CallAfter([=] // Capture pException by value!
|
|
{
|
|
|
|
// Restore the state of the project to what it was before the
|
|
// failed operation
|
|
if (pProject) {
|
|
ProjectHistory::Get(*pProject).RollbackState();
|
|
|
|
// Forget pending changes in the TrackList
|
|
TrackList::Get(*pProject).ClearPendingTracks();
|
|
|
|
ProjectWindow::Get(*pProject).RedrawProject();
|
|
}
|
|
|
|
// Give the user an alert
|
|
try { std::rethrow_exception(pException); } catch (AudacityException& e) {
|
|
e.DelayedHandlerAction();
|
|
}
|
|
|
|
});
|
|
|
|
// Don't quit the program
|
|
return true;
|
|
} catch (...) {
|
|
// There was some other type of exception we don't know.
|
|
// Let the inherited function do throw; again and whatever else it does.
|
|
return wxApp::OnExceptionInMainLoop();
|
|
}
|
|
// Shouldn't ever reach this line
|
|
return false;
|
|
}
|
|
#ifdef _MSC_VER
|
|
#pragma warning( pop )
|
|
#endif //_MSC_VER
|
|
|
|
AudacityApp::AudacityApp() {
|
|
#if defined(USE_BREAKPAD)
|
|
// Do not capture crashes in debug builds
|
|
#elif !defined(_DEBUG)
|
|
#if defined(wxUSE_ON_FATAL_EXCEPTION) && wxUSE_ON_FATAL_EXCEPTION
|
|
wxHandleFatalExceptions();
|
|
#endif
|
|
#endif
|
|
}
|
|
|
|
AudacityApp::~AudacityApp() {
|
|
}
|
|
|
|
// The `main program' equivalent, creating the windows and returning the
|
|
// main frame
|
|
bool AudacityApp::OnInit() {
|
|
// JKC: ANSWER-ME: Who actually added the event loop guarantor?
|
|
// Although 'blame' says Leland, I think it came from a donated patch.
|
|
|
|
// PRL: It was added by LL at 54676a72285ba7ee3a69920e91fa390a71ef10c9 :
|
|
// " Ensure OnInit() has an event loop
|
|
// And allow events to flow so the splash window updates under GTK"
|
|
// then mistakenly lost in the merge at
|
|
// 37168ebbf67ae869ab71a3b5cbbf1d2a48e824aa
|
|
// then restored at 7687972aa4b2199f0717165235f3ef68ade71e08
|
|
|
|
// Ensure we have an event loop during initialization
|
|
wxEventLoopGuarantor eventLoop;
|
|
|
|
// Fire up SQLite
|
|
if (!ProjectFileIO::InitializeSQL())
|
|
this->CallAfter([] {
|
|
::AudacityMessageBox(
|
|
XO("SQLite library failed to initialize. Audacity cannot continue."));
|
|
QuitAudacity(true);
|
|
});
|
|
|
|
|
|
// cause initialization of wxWidgets' global logger target
|
|
(void)AudacityLogger::Get();
|
|
|
|
#if defined(__WXMAC__)
|
|
// Disable window animation
|
|
wxSystemOptions::SetOption(wxMAC_WINDOW_PLAIN_TRANSITION, 1);
|
|
#endif
|
|
|
|
// Some GTK themes produce larger combo boxes that make them taller
|
|
// than our single toolbar height restriction. This will remove some
|
|
// of the extra space themes add.
|
|
#if defined(__WXGTK3__) && defined(HAVE_GTK)
|
|
GtkWidget* combo = gtk_combo_box_new();
|
|
GtkCssProvider* provider = gtk_css_provider_new();
|
|
gtk_css_provider_load_from_data(GTK_CSS_PROVIDER(provider),
|
|
".linked entry,\n"
|
|
".linked button,\n"
|
|
".linked combobox box.linked button,\n"
|
|
".horizontal.linked entry,\n"
|
|
".horizontal.linked button,\n"
|
|
".horizontal.linked combobox box.linked button,\n"
|
|
"combobox {\n"
|
|
" padding-top: 0px;\n"
|
|
" padding-bottom: 0px;\n"
|
|
" padding-left: 4px;\n"
|
|
" padding-right: 4px;\n"
|
|
" margin: 0px;\n"
|
|
" font-size: 95%;\n"
|
|
"}", -1, NULL);
|
|
gtk_style_context_add_provider_for_screen(gtk_widget_get_screen(combo),
|
|
GTK_STYLE_PROVIDER(provider),
|
|
GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
|
|
g_object_unref(provider);
|
|
g_object_unref(combo);
|
|
#elif defined(__WXGTK__) && defined(HAVE_GTK)
|
|
gtk_rc_parse_string("style \"audacity\" {\n"
|
|
" GtkButton::inner_border = { 0, 0, 0, 0 }\n"
|
|
" GtkEntry::inner_border = { 0, 0, 0, 0 }\n"
|
|
" xthickness = 4\n"
|
|
" ythickness = 0\n"
|
|
"}\n"
|
|
"widget_class \"*GtkCombo*\" style \"audacity\"");
|
|
#endif
|
|
|
|
// Don't use AUDACITY_NAME here.
|
|
// We want Audacity with a capital 'A'
|
|
|
|
// DA: App name
|
|
#ifndef EXPERIMENTAL_DA
|
|
wxString appName = wxT("Tenacity");
|
|
#else
|
|
wxString appName = wxT("DarkTenacity");
|
|
#endif
|
|
|
|
wxTheApp->SetAppName(appName);
|
|
// Explicitly set since OSX will use it for the "Quit" menu item
|
|
wxTheApp->SetAppDisplayName(appName);
|
|
wxTheApp->SetVendorName(appName);
|
|
|
|
::wxInitAllImageHandlers();
|
|
|
|
// AddHandler takes ownership
|
|
wxFileSystem::AddHandler(safenew wxZipFSHandler);
|
|
|
|
// encouraged by wxwidgets
|
|
wxStandardPaths::Get().SetFileLayout(wxStandardPaths::FileLayout::FileLayout_XDG);
|
|
|
|
//
|
|
// Paths: set search path and temp dir path
|
|
//
|
|
FilePaths audacityPathList;
|
|
|
|
#ifdef __WXGTK__
|
|
// Make sure install prefix is set so wxStandardPath resolves paths properly
|
|
wxStandardPaths::Get().SetInstallPrefix(wxT(INSTALL_PREFIX));
|
|
|
|
/* Search path (for plug-ins, translations etc) is (in this order):
|
|
* The AUDACITY_PATH environment variable
|
|
* The current directory
|
|
* The user's "~/.audacity-data" or "Portable Settings" directory
|
|
* The user's "~/.audacity-files" directory
|
|
* The "share" and "share/doc" directories in their install path */
|
|
wxString home = wxGetHomeDir();
|
|
|
|
wxString envTempDir = wxGetenv(wxT("TMPDIR"));
|
|
if (!envTempDir.empty()) {
|
|
/* On Unix systems, the environment variable TMPDIR may point to
|
|
an unusual path when /tmp and /var/tmp are not desirable. */
|
|
TempDirectory::SetDefaultTempDir(wxString::Format(
|
|
wxT("%s/audacity-%s"), envTempDir, wxGetUserId()));
|
|
} else {
|
|
/* On Unix systems, the default temp dir is in /var/tmp. */
|
|
TempDirectory::SetDefaultTempDir(wxString::Format(
|
|
wxT("/var/tmp/audacity-%s"), wxGetUserId()));
|
|
}
|
|
|
|
// DA: Path env variable.
|
|
#ifndef EXPERIMENTAL_DA
|
|
wxString pathVar = wxGetenv(wxT("AUDACITY_PATH"));
|
|
#else
|
|
wxString pathVar = wxGetenv(wxT("DARKAUDACITY_PATH"));
|
|
#endif
|
|
if (!pathVar.empty())
|
|
FileNames::AddMultiPathsToPathList(pathVar, audacityPathList);
|
|
FileNames::AddUniquePathToPathList(::wxGetCwd(), audacityPathList);
|
|
|
|
wxString progPath = wxPathOnly(argv[0]);
|
|
FileNames::AddUniquePathToPathList(progPath, audacityPathList);
|
|
// Add the path to modules:
|
|
FileNames::AddUniquePathToPathList(progPath + L"/lib/audacity", audacityPathList);
|
|
|
|
FileNames::AddUniquePathToPathList(FileNames::DataDir(), audacityPathList);
|
|
|
|
#ifdef AUDACITY_NAME
|
|
FileNames::AddUniquePathToPathList(wxString::Format(wxT("%s/.%s-files"),
|
|
home, wxT(AUDACITY_NAME)),
|
|
audacityPathList);
|
|
FileNames::AddUniquePathToPathList(FileNames::ModulesDir(),
|
|
audacityPathList);
|
|
FileNames::AddUniquePathToPathList(wxString::Format(wxT("%s/share/%s"),
|
|
wxT(INSTALL_PREFIX), wxT(AUDACITY_NAME)),
|
|
audacityPathList);
|
|
FileNames::AddUniquePathToPathList(wxString::Format(wxT("%s/share/doc/%s"),
|
|
wxT(INSTALL_PREFIX), wxT(AUDACITY_NAME)),
|
|
audacityPathList);
|
|
#else //AUDACITY_NAME
|
|
FileNames::AddUniquePathToPathList(wxString::Format(wxT("%s/.audacity-files"),
|
|
home),
|
|
audacityPathList)
|
|
FileNames::AddUniquePathToPathList(FileNames::ModulesDir(),
|
|
audacityPathList);
|
|
FileNames::AddUniquePathToPathList(wxString::Format(wxT("%s/share/audacity"),
|
|
wxT(INSTALL_PREFIX)),
|
|
audacityPathList);
|
|
FileNames::AddUniquePathToPathList(wxString::Format(wxT("%s/share/doc/audacity"),
|
|
wxT(INSTALL_PREFIX)),
|
|
audacityPathList);
|
|
#endif //AUDACITY_NAME
|
|
|
|
FileNames::AddUniquePathToPathList(wxString::Format(wxT("%s/share/locale"),
|
|
wxT(INSTALL_PREFIX)),
|
|
audacityPathList);
|
|
|
|
FileNames::AddUniquePathToPathList(wxString::Format(wxT("./locale")),
|
|
audacityPathList);
|
|
|
|
#endif //__WXGTK__
|
|
|
|
// JKC Bug 1220: Use path based on home directory on WXMAC
|
|
#ifdef __WXMAC__
|
|
wxFileName tmpFile;
|
|
tmpFile.AssignHomeDir();
|
|
wxString tmpDirLoc = tmpFile.GetPath(wxPATH_GET_VOLUME);
|
|
#else
|
|
wxFileName tmpFile;
|
|
tmpFile.AssignTempFileName(wxT("nn"));
|
|
wxString tmpDirLoc = tmpFile.GetPath(wxPATH_GET_VOLUME);
|
|
::wxRemoveFile(tmpFile.GetFullPath());
|
|
#endif
|
|
|
|
|
|
|
|
// On Mac and Windows systems, use the directory which contains Audacity.
|
|
#ifdef __WXMSW__
|
|
// On Windows, the path to the Audacity program is in argv[0]
|
|
wxString progPath = wxPathOnly(argv[0]);
|
|
FileNames::AddUniquePathToPathList(progPath, audacityPathList);
|
|
FileNames::AddUniquePathToPathList(progPath + wxT("\\Languages"), audacityPathList);
|
|
|
|
// See bug #1271 for explanation of location
|
|
tmpDirLoc = FileNames::MkDir(wxStandardPaths::Get().GetUserLocalDataDir());
|
|
TempDirectory::SetDefaultTempDir(wxString::Format(
|
|
wxT("%s\\SessionData"), tmpDirLoc));
|
|
#endif //__WXWSW__
|
|
|
|
#ifdef __WXMAC__
|
|
// On Mac OS X, the path to the Audacity program is in argv[0]
|
|
wxString progPath = wxPathOnly(argv[0]);
|
|
|
|
FileNames::AddUniquePathToPathList(progPath, audacityPathList);
|
|
// If Audacity is a "bundle" package, then the root directory is
|
|
// the great-great-grandparent of the directory containing the executable.
|
|
//FileNames::AddUniquePathToPathList(progPath + wxT("/../../../"), audacityPathList);
|
|
|
|
// These allow for searching the "bundle"
|
|
FileNames::AddUniquePathToPathList(
|
|
progPath + wxT("/../"), audacityPathList);
|
|
FileNames::AddUniquePathToPathList(
|
|
progPath + wxT("/../Resources"), audacityPathList);
|
|
|
|
// JKC Bug 1220: Using an actual temp directory for session data on Mac was
|
|
// wrong because it would get cleared out on a reboot.
|
|
TempDirectory::SetDefaultTempDir(wxString::Format(
|
|
wxT("%s/Library/Application Support/audacity/SessionData"), tmpDirLoc));
|
|
|
|
//TempDirectory::SetDefaultTempDir( wxString::Format(
|
|
// wxT("%s/audacity-%s"),
|
|
// tmpDirLoc,
|
|
// wxGetUserId() ) );
|
|
#endif //__WXMAC__
|
|
|
|
FileNames::SetAudacityPathList(std::move(audacityPathList));
|
|
|
|
// Define languages for which we have translations, but that are not yet
|
|
// supported by wxWidgets.
|
|
//
|
|
// TODO: The whole Language initialization really need to be reworked.
|
|
// It's all over the place.
|
|
#if wxCHECK_VERSION(3, 0, 1)
|
|
for (size_t i = 0, cnt = WXSIZEOF(userLangs); i < cnt; i++) {
|
|
wxLocale::AddLanguage(userLangs[i]);
|
|
}
|
|
#endif
|
|
|
|
// Initialize preferences and language
|
|
{
|
|
wxFileName configFileName(FileNames::ConfigDir(), wxT("tenacity.cfg"));
|
|
auto appName = wxTheApp->GetAppName();
|
|
InitPreferences(AudacityFileConfig::Create(
|
|
appName, wxEmptyString,
|
|
configFileName.GetFullPath(),
|
|
wxEmptyString, wxCONFIG_USE_LOCAL_FILE));
|
|
PopulatePreferences();
|
|
}
|
|
|
|
#if defined(__WXMSW__) && !defined(__WXUNIVERSAL__) && !defined(__CYGWIN__)
|
|
this->AssociateFileTypes();
|
|
#endif
|
|
|
|
theTheme.EnsureInitialised();
|
|
|
|
// AColor depends on theTheme.
|
|
AColor::Init();
|
|
|
|
// If this fails, we must exit the program.
|
|
if (!InitTempDir()) {
|
|
FinishPreferences();
|
|
return false;
|
|
}
|
|
|
|
#ifdef __WXMAC__
|
|
// Bug2437: When files are opened from Finder and another instance of
|
|
// Audacity is running, we must return from OnInit() to wxWidgets before
|
|
// MacOpenFile is called, informing us of the paths that need to be
|
|
// opened. So use CallAfter() to delay the rest of initialization.
|
|
// See CreateSingleInstanceChecker() where we send those paths over a
|
|
// socket to the prior instance.
|
|
|
|
// This call is what probably makes the sleep unnecessary:
|
|
MacFinishLaunching();
|
|
|
|
using namespace std::chrono;
|
|
// This sleep may be unnecessary, but it is harmless. It less NS framework
|
|
// events arrive on another thread, but it might not always be long enough.
|
|
std::this_thread::sleep_for(100ms);
|
|
CallAfter([this] {
|
|
if (!InitPart2())
|
|
exit(-1);
|
|
});
|
|
return true;
|
|
#else
|
|
return InitPart2();
|
|
#endif
|
|
}
|
|
|
|
bool AudacityApp::InitPart2() {
|
|
#if defined(__WXMAC__)
|
|
SetExitOnFrameDelete(false);
|
|
#endif
|
|
|
|
// Make sure the temp dir isn't locked by another process.
|
|
{
|
|
auto key =
|
|
PreferenceKey(FileNames::Operation::Temp, FileNames::PathType::_None);
|
|
auto temp = gPrefs->Read(key);
|
|
if (temp.empty() || !CreateSingleInstanceChecker(temp)) {
|
|
FinishPreferences();
|
|
return false;
|
|
}
|
|
}
|
|
|
|
//<<<< Try to avoid dialogs before this point.
|
|
// The reason is that InitTempDir starts the single instance checker.
|
|
// If we're waiitng in a dialog before then we can very easily
|
|
// start multiple instances, defeating the single instance checker.
|
|
|
|
// Initialize the CommandHandler
|
|
InitCommandHandler();
|
|
|
|
// Initialize the ModuleManager, including loading found modules
|
|
ModuleManager::Get().Initialize();
|
|
|
|
// Initialize the PluginManager
|
|
PluginManager::Get().Initialize();
|
|
|
|
// Parse command line and handle options that might require
|
|
// immediate exit...no need to initialize all of the audio
|
|
// stuff to display the version string.
|
|
std::shared_ptr< wxCmdLineParser > parser{ParseCommandLine().release()};
|
|
if (!parser) {
|
|
// Either user requested help or a parsing error occurred
|
|
exit(1);
|
|
}
|
|
|
|
if (parser->Found(wxT("v"))) {
|
|
wxPrintf("Tenacity v%s\n", AUDACITY_VERSION_STRING);
|
|
exit(0);
|
|
}
|
|
|
|
long lval;
|
|
if (parser->Found(wxT("b"), &lval)) {
|
|
if (lval < 256 || lval > 100000000) {
|
|
wxPrintf(_("Block size must be within 256 to 100000000\n"));
|
|
exit(1);
|
|
}
|
|
|
|
Sequence::SetMaxDiskBlockSize(lval);
|
|
}
|
|
|
|
// BG: Create a temporary window to set as the top window
|
|
wxImage logoimage((const char**)AudacityLogoWithName_xpm);
|
|
logoimage.Rescale(logoimage.GetWidth() / 2, logoimage.GetHeight() / 2);
|
|
if (GetLayoutDirection() == wxLayout_RightToLeft)
|
|
logoimage = logoimage.Mirror();
|
|
wxBitmap logo(logoimage);
|
|
|
|
AudacityProject* project;
|
|
{
|
|
// Bug 718: Position splash screen on same screen
|
|
// as where Audacity project will appear.
|
|
wxRect wndRect;
|
|
bool bMaximized = false;
|
|
bool bIconized = false;
|
|
GetNextWindowPlacement(&wndRect, &bMaximized, &bIconized);
|
|
|
|
wxSplashScreen temporarywindow(
|
|
logo,
|
|
wxSPLASH_CENTRE_ON_SCREEN | wxSPLASH_NO_TIMEOUT,
|
|
0,
|
|
NULL,
|
|
wxID_ANY,
|
|
wndRect.GetTopLeft(),
|
|
wxDefaultSize,
|
|
wxSTAY_ON_TOP);
|
|
|
|
// Unfortunately with the Windows 10 Creators update, the splash screen
|
|
// now appears before setting its position.
|
|
// On a dual monitor screen it will appear on one screen and then
|
|
// possibly jump to the second.
|
|
// We could fix this by writing our own splash screen and using Hide()
|
|
// until the splash scren was correctly positioned, then Show()
|
|
|
|
// Possibly move it on to the second screen...
|
|
temporarywindow.SetPosition(wndRect.GetTopLeft());
|
|
// Centered on whichever screen it is on.
|
|
temporarywindow.Center();
|
|
temporarywindow.SetTitle(_("Tenacity is starting up..."));
|
|
SetTopWindow(&temporarywindow);
|
|
temporarywindow.Raise();
|
|
|
|
// ANSWER-ME: Why is YieldFor needed at all?
|
|
//wxEventLoopBase::GetActive()->YieldFor(wxEVT_CATEGORY_UI|wxEVT_CATEGORY_USER_INPUT|wxEVT_CATEGORY_UNKNOWN);
|
|
wxEventLoopBase::GetActive()->YieldFor(wxEVT_CATEGORY_UI);
|
|
|
|
//JKC: Would like to put module loading here.
|
|
|
|
// More initialization
|
|
|
|
InitDitherers();
|
|
AudioIO::Init();
|
|
|
|
#ifdef __WXMAC__
|
|
|
|
// On the Mac, users don't expect a program to quit when you close the last window.
|
|
// Create a menubar that will show when all project windows are closed.
|
|
|
|
auto fileMenu = std::make_unique<wxMenu>();
|
|
auto urecentMenu = std::make_unique<wxMenu>();
|
|
auto recentMenu = urecentMenu.get();
|
|
fileMenu->Append(wxID_NEW, wxString(_("&New")) + wxT("\tCtrl+N"));
|
|
fileMenu->Append(wxID_OPEN, wxString(_("&Open...")) + wxT("\tCtrl+O"));
|
|
fileMenu->AppendSubMenu(urecentMenu.release(), _("Open &Recent..."));
|
|
fileMenu->Append(wxID_ABOUT, _("&About Tenacity..."));
|
|
fileMenu->Append(wxID_PREFERENCES, wxString(_("&Preferences...")) + wxT("\tCtrl+,"));
|
|
|
|
{
|
|
auto menuBar = std::make_unique<wxMenuBar>();
|
|
menuBar->Append(fileMenu.release(), _("&File"));
|
|
|
|
// PRL: Are we sure wxWindows will not leak this menuBar?
|
|
// The online documentation is not explicit.
|
|
wxMenuBar::MacSetCommonMenuBar(menuBar.release());
|
|
}
|
|
|
|
auto& recentFiles = FileHistory::Global();
|
|
recentFiles.UseMenu(recentMenu);
|
|
|
|
#endif //__WXMAC__
|
|
temporarywindow.Show(false);
|
|
}
|
|
|
|
// Workaround Bug 1377 - Crash after Audacity starts and low disk space warning appears
|
|
// The temporary splash window is closed AND cleaned up, before attempting to create
|
|
// a project and possibly creating a modal warning dialog by doing so.
|
|
// Also fixes problem of warning being obscured.
|
|
// Downside is that we have no splash screen for the (brief) time that we spend
|
|
// creating the project.
|
|
// Root cause is problem with wxSplashScreen and other dialogs co-existing, that
|
|
// seemed to arrive with wx3.
|
|
{
|
|
project = ProjectManager::New();
|
|
}
|
|
|
|
if (ProjectSettings::Get(*project).GetShowSplashScreen()) {
|
|
// This may do a check-for-updates at every start up.
|
|
// Mainly this is to tell users of ALPHAS who don't know that they have an ALPHA.
|
|
// Disabled for now, after discussion.
|
|
// project->MayCheckForUpdates();
|
|
SplashDialog::DoHelpWelcome(*project);
|
|
}
|
|
|
|
#ifdef USE_FFMPEG
|
|
FFmpegStartup();
|
|
#endif
|
|
|
|
Importer::Get().Initialize();
|
|
|
|
// Bug1561: delay the recovery dialog, to avoid crashes.
|
|
CallAfter([=]() mutable {
|
|
// Remove duplicate shortcuts when there's a change of version
|
|
int vMajorInit, vMinorInit, vMicroInit;
|
|
gPrefs->GetVersionKeysInit(vMajorInit, vMinorInit, vMicroInit);
|
|
if (vMajorInit != AUDACITY_VERSION || vMinorInit != AUDACITY_RELEASE
|
|
|| vMicroInit != AUDACITY_REVISION) {
|
|
CommandManager::Get(*project).RemoveDuplicateShortcuts();
|
|
}
|
|
//
|
|
// Auto-recovery
|
|
//
|
|
bool didRecoverAnything = false;
|
|
// This call may reassign project (passed by reference)
|
|
if (!ShowAutoRecoveryDialogIfNeeded(project, &didRecoverAnything)) {
|
|
QuitAudacity(true);
|
|
}
|
|
|
|
//
|
|
// Remainder of command line parsing, but only if we didn't recover
|
|
//
|
|
if (project && !didRecoverAnything) {
|
|
if (parser->Found(wxT("t"))) {
|
|
RunBenchmark(nullptr, *project);
|
|
QuitAudacity(true);
|
|
}
|
|
|
|
for (size_t i = 0, cnt = parser->GetParamCount(); i < cnt; i++) {
|
|
// PRL: Catch any exceptions, don't try this file again, continue to
|
|
// other files.
|
|
SafeMRUOpen(parser->GetParam(i));
|
|
}
|
|
}
|
|
});
|
|
|
|
gInited = true;
|
|
|
|
ModuleManager::Get().Dispatch(AppInitialized);
|
|
|
|
mTimer.SetOwner(this, kAudacityAppTimerID);
|
|
mTimer.Start(200);
|
|
|
|
#ifdef EXPERIMENTAL_EASY_CHANGE_KEY_BINDINGS
|
|
CommandManager::SetMenuHook([](const CommandID& id) {
|
|
if (::wxGetMouseState().ShiftDown()) {
|
|
// Only want one page of the preferences
|
|
PrefsPanel::Factories factories;
|
|
factories.push_back(KeyConfigPrefsFactory(id));
|
|
const auto pProject = GetActiveProject();
|
|
auto pWindow = FindProjectFrame(pProject);
|
|
GlobalPrefsDialog dialog(pWindow, pProject, factories);
|
|
dialog.ShowModal();
|
|
MenuCreator::RebuildAllMenuBars();
|
|
return true;
|
|
} else
|
|
return false;
|
|
});
|
|
#endif
|
|
|
|
#if defined(__WXMAC__)
|
|
// The first time this version of Audacity is run or when the preferences
|
|
// are reset, execute the "tccutil" command to reset the microphone permissions
|
|
// currently assigned to Audacity. The end result is that the user will be
|
|
// prompted to approve/deny Audacity access (again).
|
|
//
|
|
// This should resolve confusion of why Audacity appears to record, but only
|
|
// gets silence due to Audacity being denied microphone access previously.
|
|
bool permsReset = false;
|
|
gPrefs->Read(wxT("/MicrophonePermissionsReset"), &permsReset, false);
|
|
if (!permsReset) {
|
|
system("tccutil reset Microphone org.tenacityaudio.tenacity");
|
|
gPrefs->Write(wxT("/MicrophonePermissionsReset"), true);
|
|
}
|
|
#endif
|
|
|
|
#if defined(__WXMAC__)
|
|
// Bug 2709: Workaround CoreSVG locale issue
|
|
Bind(wxEVT_MENU_OPEN, [=](wxMenuEvent& event) {
|
|
wxSetlocale(LC_NUMERIC, wxString(wxT("C")));
|
|
event.Skip();
|
|
});
|
|
|
|
Bind(wxEVT_MENU_CLOSE, [=](wxMenuEvent& event) {
|
|
wxSetlocale(LC_NUMERIC, Languages::GetLocaleName());
|
|
event.Skip();
|
|
});
|
|
#endif
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
void AudacityApp::InitCommandHandler() {
|
|
mCmdHandler = std::make_unique<CommandHandler>();
|
|
//SetNextHandler(mCmdHandler);
|
|
}
|
|
|
|
// AppCommandEvent callback - just pass the event on to the CommandHandler
|
|
void AudacityApp::OnReceiveCommand(AppCommandEvent& event) {
|
|
wxASSERT(NULL != mCmdHandler);
|
|
mCmdHandler->OnReceiveCommand(event);
|
|
}
|
|
|
|
void AudacityApp::OnKeyDown(wxKeyEvent& event) {
|
|
if (event.GetKeyCode() == WXK_ESCAPE) {
|
|
// Stop play, including scrub, but not record
|
|
auto project = ::GetActiveProject();
|
|
if (project) {
|
|
auto token = ProjectAudioIO::Get(*project).GetAudioIOToken();
|
|
auto& scrubber = Scrubber::Get(*project);
|
|
auto scrubbing = scrubber.HasMark();
|
|
if (scrubbing)
|
|
scrubber.Cancel();
|
|
auto gAudioIO = AudioIO::Get();
|
|
if ((token > 0 &&
|
|
gAudioIO->IsAudioTokenActive(token) &&
|
|
gAudioIO->GetNumCaptureChannels() == 0) ||
|
|
scrubbing)
|
|
// ESC out of other play (but not record)
|
|
ProjectAudioManager::Get(*project).Stop();
|
|
else
|
|
event.Skip();
|
|
}
|
|
}
|
|
|
|
event.Skip();
|
|
}
|
|
|
|
// Ensures directory is created and puts the name into result.
|
|
// result is unchanged if unsuccessful.
|
|
void SetToExtantDirectory(wxString& result, const wxString& dir) {
|
|
// don't allow path of "".
|
|
if (dir.empty())
|
|
return;
|
|
if (wxDirExists(dir)) {
|
|
result = dir;
|
|
return;
|
|
}
|
|
// Use '/' so that this works on Mac and Windows alike.
|
|
wxFileName name(dir + "/junkname.cfg");
|
|
if (name.Mkdir(wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL))
|
|
result = dir;
|
|
}
|
|
|
|
bool AudacityApp::InitTempDir() {
|
|
// We need to find a temp directory location.
|
|
auto tempFromPrefs = TempDirectory::TempDir();
|
|
auto &tempDefaultLoc = TempDirectory::DefaultTempDir();
|
|
|
|
wxString temp;
|
|
|
|
#ifdef __WXGTK__
|
|
if (tempFromPrefs.length() > 0 && tempFromPrefs[0] != wxT('/'))
|
|
tempFromPrefs = wxT("");
|
|
#endif
|
|
|
|
// Stop wxWidgets from printing its own error messages
|
|
|
|
wxLogNull logNo;
|
|
|
|
// Try temp dir that was stored in prefs first
|
|
if (TempDirectory::IsTempDirectoryNameOK(tempFromPrefs))
|
|
SetToExtantDirectory(temp, tempFromPrefs);
|
|
|
|
// If that didn't work, try the default location
|
|
|
|
if (temp.empty())
|
|
SetToExtantDirectory(temp, tempDefaultLoc);
|
|
|
|
// Check temp directory ownership on *nix systems only
|
|
#ifdef __UNIX__
|
|
struct stat tempStatBuf;
|
|
if (lstat(temp.mb_str(), &tempStatBuf) != 0) {
|
|
temp.clear();
|
|
} else {
|
|
if (geteuid() != tempStatBuf.st_uid) {
|
|
temp.clear();
|
|
}
|
|
}
|
|
#endif
|
|
|
|
if (temp.empty()) {
|
|
// Failed
|
|
if (!TempDirectory::IsTempDirectoryNameOK(tempFromPrefs)) {
|
|
AudacityMessageBox(XO(
|
|
"Audacity could not find a safe place to store temporary files.\nAudacity needs a place where automatic cleanup programs won't delete the temporary files.\nPlease enter an appropriate directory in the preferences dialog."));
|
|
} else {
|
|
AudacityMessageBox(XO(
|
|
"Audacity could not find a place to store temporary files.\nPlease enter an appropriate directory in the preferences dialog."));
|
|
}
|
|
|
|
// Only want one page of the preferences
|
|
PrefsPanel::Factories factories;
|
|
factories.push_back(DirectoriesPrefsFactory());
|
|
GlobalPrefsDialog dialog(nullptr, nullptr, factories);
|
|
dialog.ShowModal();
|
|
|
|
AudacityMessageBox(XO(
|
|
"Audacity is now going to exit. Please launch Audacity again to use the new temporary directory."));
|
|
return false;
|
|
}
|
|
|
|
// The permissions don't always seem to be set on
|
|
// some platforms. Hopefully this fixes it...
|
|
#ifdef __UNIX__
|
|
chmod(OSFILENAME(temp), 0700);
|
|
#endif
|
|
|
|
TempDirectory::ResetTempDir();
|
|
FileNames::UpdateDefaultPath(FileNames::Operation::Temp, temp);
|
|
|
|
return true;
|
|
}
|
|
|
|
#if defined(__WXMSW__)
|
|
|
|
// Return true if there are no other instances of Audacity running,
|
|
// false otherwise.
|
|
|
|
bool AudacityApp::CreateSingleInstanceChecker(const wxString& dir) {
|
|
wxString name = wxString::Format(wxT("audacity-lock-%s"), wxGetUserId());
|
|
mChecker.reset();
|
|
auto checker = std::make_unique<wxSingleInstanceChecker>();
|
|
|
|
auto runningTwoCopiesStr = XO("Running two copies of Audacity simultaneously may cause\ndata loss or cause your system to crash.\n\n");
|
|
|
|
if (!checker->Create(name, dir)) {
|
|
// Error initializing the wxSingleInstanceChecker. We don't know
|
|
// whether there is another instance running or not.
|
|
|
|
auto prompt = XO(
|
|
"Audacity was not able to lock the temporary files directory.\nThis folder may be in use by another copy of Audacity.\n")
|
|
+ runningTwoCopiesStr
|
|
+ XO("Do you still want to start Audacity?");
|
|
int action = AudacityMessageBox(
|
|
prompt,
|
|
XO("Error Locking Temporary Folder"),
|
|
wxYES_NO | wxICON_EXCLAMATION, NULL);
|
|
if (action == wxNO)
|
|
return false;
|
|
} else if (checker->IsAnotherRunning()) {
|
|
// Parse the command line to ensure correct syntax, but
|
|
// ignore options other than -v, and only use the filenames, if any.
|
|
auto parser = ParseCommandLine();
|
|
if (!parser) {
|
|
// Complaints have already been made
|
|
return false;
|
|
}
|
|
|
|
if (parser->Found(wxT("v"))) {
|
|
wxPrintf("Tenacity v%s\n", AUDACITY_VERSION_STRING);
|
|
return false;
|
|
}
|
|
|
|
// Windows and Linux require absolute file names as command may
|
|
// not come from current working directory.
|
|
FilePaths filenames;
|
|
for (size_t i = 0, cnt = parser->GetParamCount(); i < cnt; i++) {
|
|
wxFileName filename(parser->GetParam(i));
|
|
if (filename.MakeAbsolute()) {
|
|
filenames.push_back(filename.GetLongPath());
|
|
}
|
|
}
|
|
|
|
// On Windows, we attempt to make a connection
|
|
// to an already active Audacity. If successful, we send
|
|
// the first command line argument (the audio file name)
|
|
// to that Audacity for processing.
|
|
wxClient client;
|
|
|
|
// We try up to 50 times since there's a small window
|
|
// where the server may not have been fully initialized.
|
|
for (int i = 0; i < 50; i++) {
|
|
std::unique_ptr<wxConnectionBase> conn{client.MakeConnection(wxEmptyString, IPC_APPL, IPC_TOPIC)};
|
|
if (conn) {
|
|
bool ok = false;
|
|
if (filenames.size() > 0) {
|
|
for (size_t i = 0, cnt = filenames.size(); i < cnt; i++) {
|
|
ok = conn->Execute(filenames[i]);
|
|
}
|
|
} else {
|
|
// Send an empty string to force existing Audacity to front
|
|
ok = conn->Execute(wxEmptyString);
|
|
}
|
|
|
|
if (ok)
|
|
return false;
|
|
}
|
|
|
|
wxMilliSleep(10);
|
|
}
|
|
// There is another copy of Audacity running. Force quit.
|
|
|
|
auto prompt = XO(
|
|
"The system has detected that another copy of Audacity is running.\n")
|
|
+ runningTwoCopiesStr
|
|
+ XO(
|
|
"Use the New or Open commands in the currently running Audacity\nprocess to open multiple projects simultaneously.\n");
|
|
AudacityMessageBox(
|
|
prompt, XO("Audacity is already running"),
|
|
wxOK | wxICON_ERROR);
|
|
|
|
return false;
|
|
}
|
|
|
|
// Create the DDE IPC server
|
|
mIPCServ = std::make_unique<IPCServ>(IPC_APPL);
|
|
mChecker = std::move(checker);
|
|
return true;
|
|
}
|
|
#endif
|
|
|
|
#if defined(__UNIX__)
|
|
|
|
#include <sys/ipc.h>
|
|
#include <sys/sem.h>
|
|
#include <sys/shm.h>
|
|
|
|
// Return true if there are no other instances of Audacity running,
|
|
// false otherwise.
|
|
|
|
bool AudacityApp::CreateSingleInstanceChecker(const wxString& dir) {
|
|
mIPCServ.reset();
|
|
|
|
bool isServer = false;
|
|
wxIPV4address addr;
|
|
addr.LocalHost();
|
|
|
|
struct sembuf op = {};
|
|
|
|
// Generate the IPC key we'll use for both shared memory and semaphores.
|
|
wxString datadir = FileNames::DataDir();
|
|
key_t memkey = ftok(datadir.c_str(), 0);
|
|
key_t servkey = ftok(datadir.c_str(), 1);
|
|
key_t lockkey = ftok(datadir.c_str(), 2);
|
|
|
|
// Create and map the shared memory segment where the port number
|
|
// will be stored.
|
|
int memid = shmget(memkey, sizeof(int), IPC_CREAT | S_IRUSR | S_IWUSR);
|
|
int* portnum = (int*)shmat(memid, nullptr, 0);
|
|
|
|
// Create (or return) the SERVER semaphore ID
|
|
int servid = semget(servkey, 1, IPC_CREAT | S_IRUSR | S_IWUSR);
|
|
|
|
// Create the LOCK semaphore only if it doesn't already exist.
|
|
int lockid = semget(lockkey, 1, IPC_CREAT | IPC_EXCL | S_IRUSR | S_IWUSR);
|
|
|
|
// If the LOCK semaphore was successfully created, then this is the first
|
|
// time Audacity has been run during this boot of the system. In this
|
|
// case we know we'll become the "server" application, so set up the
|
|
// semaphores to prepare for it.
|
|
if (lockid != -1) {
|
|
// Initialize value of each semaphore, 1 indicates released and 0
|
|
// indicates acquired.
|
|
//
|
|
// Note that this action is NOT recorded in the semaphore's
|
|
// UNDO buffer.
|
|
semctl(servid, 0, SETVAL, 1);
|
|
semctl(lockid, 0, SETVAL, 1);
|
|
|
|
// Now acquire them so the semaphores will be set to the
|
|
// released state when the process terminates.
|
|
op.sem_num = 0;
|
|
op.sem_op = -1;
|
|
op.sem_flg = SEM_UNDO;
|
|
if (semop(lockid, &op, 1) == -1 || semop(servid, &op, 1) == -1) {
|
|
AudacityMessageBox(
|
|
XO("Unable to acquire semaphores.\n\n"
|
|
"This is likely due to a resource shortage\n"
|
|
"and a reboot may be required."),
|
|
XO("Audacity Startup Failure"),
|
|
wxOK | wxICON_ERROR);
|
|
|
|
return false;
|
|
}
|
|
|
|
// We will be the server...
|
|
isServer = true;
|
|
}
|
|
// Something catastrophic must have happened, so bail.
|
|
else if (errno != EEXIST) {
|
|
AudacityMessageBox(
|
|
XO("Unable to create semaphores.\n\n"
|
|
"This is likely due to a resource shortage\n"
|
|
"and a reboot may be required."),
|
|
XO("Audacity Startup Failure"),
|
|
wxOK | wxICON_ERROR);
|
|
|
|
return false;
|
|
}
|
|
// Otherwise it's a normal startup and we need to determine whether
|
|
// we'll be the server or the client.
|
|
else {
|
|
// Retrieve the LOCK semaphore since we wouldn't have gotten it above.
|
|
lockid = semget(lockkey, 1, 0);
|
|
|
|
// Acquire the LOCK semaphore. We may block here if another
|
|
// process is currently setting up the server.
|
|
op.sem_num = 0;
|
|
op.sem_op = -1;
|
|
op.sem_flg = SEM_UNDO;
|
|
if (semop(lockid, &op, 1) == -1) {
|
|
AudacityMessageBox(
|
|
XO("Unable to acquire lock semaphore.\n\n"
|
|
"This is likely due to a resource shortage\n"
|
|
"and a reboot may be required."),
|
|
XO("Audacity Startup Failure"),
|
|
wxOK | wxICON_ERROR);
|
|
|
|
return false;
|
|
}
|
|
|
|
// Try to acquire the SERVER semaphore. If it's not currently active, then
|
|
// we will become the server. Otherwise, this will fail and we'll know that
|
|
// the server is already active and we will become the client.
|
|
op.sem_num = 0;
|
|
op.sem_op = -1;
|
|
op.sem_flg = IPC_NOWAIT | SEM_UNDO;
|
|
if (semop(servid, &op, 1) == 0) {
|
|
isServer = true;
|
|
} else if (errno != EAGAIN) {
|
|
AudacityMessageBox(
|
|
XO("Unable to acquire server semaphore.\n\n"
|
|
"This is likely due to a resource shortage\n"
|
|
"and a reboot may be required."),
|
|
XO("Audacity Startup Failure"),
|
|
wxOK | wxICON_ERROR);
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Initialize the socket server if we're to be the server.
|
|
if (isServer) {
|
|
// The system will randomly assign a port
|
|
addr.Service(0);
|
|
|
|
// Create the socket and bind to it.
|
|
auto serv = std::make_unique<wxSocketServer>(addr, wxSOCKET_NOWAIT);
|
|
if (serv && serv->IsOk()) {
|
|
serv->SetEventHandler(*this, ID_IPC_SERVER);
|
|
serv->SetNotify(wxSOCKET_CONNECTION_FLAG);
|
|
serv->Notify(true);
|
|
mIPCServ = std::move(serv);
|
|
|
|
// Save the port number in shared memory so that clients
|
|
// know where to connect.
|
|
mIPCServ->GetLocal(addr);
|
|
*portnum = addr.Service();
|
|
}
|
|
|
|
// Now that the server is active, we release the LOCK semaphore
|
|
// to allow any waiters to continue. The SERVER semaphore will
|
|
// remain locked for the duration of this processes execution
|
|
// and will be cleaned up by the system.
|
|
op.sem_num = 0;
|
|
op.sem_op = 1;
|
|
semop(lockid, &op, 1);
|
|
|
|
// Bail if the server creation failed.
|
|
if (mIPCServ == nullptr) {
|
|
AudacityMessageBox(
|
|
XO("The Audacity IPC server failed to initialize.\n\n"
|
|
"This is likely due to a resource shortage\n"
|
|
"and a reboot may be required."),
|
|
XO("Audacity Startup Failure"),
|
|
wxOK | wxICON_ERROR);
|
|
|
|
return false;
|
|
}
|
|
|
|
// We've successfully created the socket server and the app
|
|
// should continue to initialize.
|
|
return true;
|
|
}
|
|
|
|
// Retrieve the port number that the server is listening on.
|
|
addr.Service(*portnum);
|
|
|
|
// Now release the LOCK semaphore.
|
|
op.sem_num = 0;
|
|
op.sem_op = 1;
|
|
semop(lockid, &op, 1);
|
|
|
|
// If we get here, then Audacity is currently active. So, we connect
|
|
// to it and we forward all filenames listed on the command line to
|
|
// the active process.
|
|
|
|
// Setup the socket
|
|
//
|
|
// A wxSocketClient must not be deleted by us, but rather, let the
|
|
// framework do appropriate delayed deletion after Destroy()
|
|
Destroy_ptr<wxSocketClient> sock{safenew wxSocketClient()};
|
|
sock->SetFlags(wxSOCKET_WAITALL);
|
|
|
|
// Attempt to connect to an active Audacity.
|
|
sock->Connect(addr, true);
|
|
if (!sock->IsConnected()) {
|
|
// All attempts to become the server or connect to one have failed. Not
|
|
// sure what we can say about the error, but it's probably not because
|
|
// Audacity is already running.
|
|
AudacityMessageBox(
|
|
XO("An unrecoverable error has occurred during startup"),
|
|
XO("Audacity Startup Failure"),
|
|
wxOK | wxICON_ERROR);
|
|
|
|
return false;
|
|
}
|
|
|
|
// Parse the command line to ensure correct syntax, but ignore
|
|
// options other than -v, and only use the filenames, if any.
|
|
auto parser = ParseCommandLine();
|
|
if (!parser) {
|
|
// Complaints have already been made
|
|
return false;
|
|
}
|
|
|
|
// Display Audacity's version if requested
|
|
if (parser->Found(wxT("v"))) {
|
|
wxPrintf("Tenacity v%s\n", AUDACITY_VERSION_STRING);
|
|
|
|
return false;
|
|
}
|
|
|
|
#if defined(__WXMAC__)
|
|
// On macOS the client gets events from the wxWidgets framework that
|
|
// go to AudacityApp::MacOpenFile. Forward the file names to the prior
|
|
// instance via the socket.
|
|
for (const auto& filename : ofqueue) {
|
|
auto str = filename.c_str().AsWChar();
|
|
sock->WriteMsg(str, (filename.length() + 1) * sizeof(*str));
|
|
}
|
|
#endif
|
|
|
|
// On macOS and Linux, forward any file names found in the command
|
|
// line arguments.
|
|
for (size_t j = 0, cnt = parser->GetParamCount(); j < cnt; ++j) {
|
|
wxFileName filename(parser->GetParam(j));
|
|
if (filename.MakeAbsolute()) {
|
|
const wxString param = filename.GetLongPath();
|
|
sock->WriteMsg((const wxChar*)param, (param.length() + 1) * sizeof(wxChar));
|
|
}
|
|
}
|
|
|
|
// Send an empty string to force existing Audacity to front
|
|
sock->WriteMsg(wxEmptyString, sizeof(wxChar));
|
|
|
|
// We've forwarded all of the filenames, so let the caller know
|
|
// to terminate.
|
|
return false;
|
|
}
|
|
|
|
void AudacityApp::OnServerEvent(wxSocketEvent& /* evt */) {
|
|
wxSocketBase* sock;
|
|
|
|
// Accept all pending connection requests
|
|
do {
|
|
sock = mIPCServ->Accept(false);
|
|
if (sock) {
|
|
// Setup the socket
|
|
sock->SetEventHandler(*this, ID_IPC_SOCKET);
|
|
sock->SetNotify(wxSOCKET_INPUT_FLAG | wxSOCKET_LOST_FLAG);
|
|
sock->Notify(true);
|
|
}
|
|
}
|
|
while (sock);
|
|
}
|
|
|
|
void AudacityApp::OnSocketEvent(wxSocketEvent& evt) {
|
|
wxSocketBase* sock = evt.GetSocket();
|
|
|
|
if (evt.GetSocketEvent() == wxSOCKET_LOST) {
|
|
sock->Destroy();
|
|
return;
|
|
}
|
|
|
|
// Read the length of the filename and bail if we have a short read
|
|
wxChar name[PATH_MAX];
|
|
sock->ReadMsg(&name, sizeof(name));
|
|
if (!sock->Error()) {
|
|
// Add the filename to the queue. It will be opened by
|
|
// the OnTimer() event when it is safe to do so.
|
|
ofqueue.push_back(name);
|
|
}
|
|
}
|
|
|
|
#endif
|
|
|
|
std::unique_ptr<wxCmdLineParser> AudacityApp::ParseCommandLine() {
|
|
auto parser = std::make_unique<wxCmdLineParser>(argc, argv);
|
|
if (!parser) {
|
|
return nullptr;
|
|
}
|
|
|
|
/*i18n-hint: This controls the number of bytes that Audacity will
|
|
* use when writing files to the disk */
|
|
parser->AddOption(wxT("b"), wxT("blocksize"), _("set max disk block size in bytes"),
|
|
wxCMD_LINE_VAL_NUMBER);
|
|
|
|
/*i18n-hint: This displays a list of available options */
|
|
parser->AddSwitch(wxT("h"), wxT("help"), _("this help message"),
|
|
wxCMD_LINE_OPTION_HELP);
|
|
|
|
/*i18n-hint: This runs a set of automatic tests on Audacity itself */
|
|
parser->AddSwitch(wxT("t"), wxT("test"), _("run self diagnostics"));
|
|
|
|
/*i18n-hint: This displays the Audacity version */
|
|
parser->AddSwitch(wxT("v"), wxT("version"), _("display Audacity version"));
|
|
|
|
/*i18n-hint: This is a list of one or more files that Audacity
|
|
* should open upon startup */
|
|
parser->AddParam(_("audio or project file name"),
|
|
wxCMD_LINE_VAL_STRING,
|
|
wxCMD_LINE_PARAM_MULTIPLE | wxCMD_LINE_PARAM_OPTIONAL);
|
|
|
|
// Run the parser
|
|
if (parser->Parse() == 0)
|
|
return parser;
|
|
|
|
return{};
|
|
}
|
|
|
|
void AudacityApp::OnQueryEndSession(wxCloseEvent& event) {
|
|
bool mustVeto = false;
|
|
|
|
#ifdef __WXMAC__
|
|
mustVeto = wxDialog::OSXHasModalDialogsOpen();
|
|
#endif
|
|
|
|
if (mustVeto)
|
|
event.Veto(true);
|
|
else
|
|
OnEndSession(event);
|
|
}
|
|
|
|
void AudacityApp::OnEndSession(wxCloseEvent& event) {
|
|
bool force = !event.CanVeto();
|
|
|
|
// Try to close each open window. If the user hits Cancel
|
|
// in a Save Changes dialog, don't continue.
|
|
gIsQuitting = true;
|
|
if (AllProjects{}.size())
|
|
// PRL: Always did at least once before close might be vetoed
|
|
// though I don't know why that is important
|
|
ProjectManager::SaveWindowSize();
|
|
bool closedAll = AllProjects::Close(force);
|
|
if (!closedAll) {
|
|
gIsQuitting = false;
|
|
event.Veto();
|
|
}
|
|
}
|
|
|
|
int AudacityApp::OnExit()
|
|
{
|
|
gIsQuitting = true;
|
|
while(Pending())
|
|
{
|
|
Dispatch();
|
|
}
|
|
|
|
Importer::Get().Terminate();
|
|
|
|
if(gPrefs)
|
|
{
|
|
bool bFalse = false;
|
|
//Should we change the commands.cfg location next startup?
|
|
if(gPrefs->Read(wxT("/QDeleteCmdCfgLocation"), &bFalse))
|
|
{
|
|
gPrefs->DeleteEntry(wxT("/QDeleteCmdCfgLocation"));
|
|
gPrefs->Write(wxT("/DeleteCmdCfgLocation"), true);
|
|
gPrefs->Flush();
|
|
}
|
|
}
|
|
|
|
FileHistory::Global().Save(*gPrefs);
|
|
|
|
FinishPreferences();
|
|
|
|
#ifdef USE_FFMPEG
|
|
DropFFmpegLibs();
|
|
#endif
|
|
|
|
DeinitFFT();
|
|
|
|
#ifdef HAS_NETWORKING
|
|
audacity::network_manager::NetworkManager::GetInstance().Terminate();
|
|
#endif
|
|
|
|
AudioIO::Deinit();
|
|
|
|
MenuTable::DestroyRegistry();
|
|
|
|
// Terminate the PluginManager (must be done before deleting the locale)
|
|
PluginManager::Get().Terminate();
|
|
|
|
return 0;
|
|
}
|
|
|
|
// The following five methods are currently only used on Mac OS,
|
|
// where it's possible to have a menu bar but no windows open.
|
|
// It doesn't hurt any other platforms, though.
|
|
|
|
// ...That is, as long as you check to see if no windows are open
|
|
// before executing the stuff.
|
|
// To fix this, check to see how many project windows are open,
|
|
// and skip the event unless none are open (which should only happen
|
|
// on the Mac, at least currently.)
|
|
|
|
void AudacityApp::OnMenuAbout(wxCommandEvent& WXUNUSED(event)) {
|
|
// This function shadows a similar function
|
|
// in Menus.cpp, but should only be used on the Mac platform.
|
|
#ifdef __WXMAC__
|
|
AboutDialog(nullptr).ShowModal();
|
|
#else
|
|
wxASSERT(false);
|
|
#endif
|
|
}
|
|
|
|
void AudacityApp::OnMenuNew(wxCommandEvent& event) {
|
|
// This function shadows a similar function
|
|
// in Menus.cpp, but should only be used on the Mac platform
|
|
// when no project windows are open. This check assures that
|
|
// this happens, and enable the same code to be present on
|
|
// all platforms.
|
|
|
|
if (AllProjects{}.empty())
|
|
(void)ProjectManager::New();
|
|
else
|
|
event.Skip();
|
|
}
|
|
|
|
|
|
void AudacityApp::OnMenuOpen(wxCommandEvent& event) {
|
|
// This function shadows a similar function
|
|
// in Menus.cpp, but should only be used on the Mac platform
|
|
// when no project windows are open. This check assures that
|
|
// this happens, and enable the same code to be present on
|
|
// all platforms.
|
|
|
|
|
|
if (AllProjects{}.empty())
|
|
ProjectManager::OpenFiles(NULL);
|
|
else
|
|
event.Skip();
|
|
|
|
|
|
}
|
|
|
|
void AudacityApp::OnMenuPreferences(wxCommandEvent& event) {
|
|
// This function shadows a similar function
|
|
// in Menus.cpp, but should only be used on the Mac platform
|
|
// when no project windows are open. This check assures that
|
|
// this happens, and enable the same code to be present on
|
|
// all platforms.
|
|
|
|
if (AllProjects{}.empty()) {
|
|
GlobalPrefsDialog dialog(nullptr /* parent */, nullptr);
|
|
dialog.ShowModal();
|
|
} else
|
|
event.Skip();
|
|
|
|
}
|
|
|
|
void AudacityApp::OnMenuExit(wxCommandEvent& event) {
|
|
// This function shadows a similar function
|
|
// in Menus.cpp, but should only be used on the Mac platform
|
|
// when no project windows are open. This check assures that
|
|
// this happens, and enable the same code to be present on
|
|
// all platforms.
|
|
|
|
// LL: Removed "if" to allow closing based on final project count.
|
|
// if(AllProjects{}.empty())
|
|
QuitAudacity();
|
|
|
|
// LL: Veto quit if projects are still open. This can happen
|
|
// if the user selected Cancel in a Save dialog.
|
|
event.Skip(AllProjects {}.empty());
|
|
|
|
}
|
|
|
|
//BG: On Windows, associate the aup file type with Audacity
|
|
/* We do this in the Windows installer now,
|
|
to avoid issues where user doesn't have admin privileges, but
|
|
in case that didn't work, allow the user to decide at startup.
|
|
|
|
//v Should encapsulate this & allow access from Prefs, too,
|
|
// if people want to manually change associations.
|
|
*/
|
|
#if defined(__WXMSW__) && !defined(__WXUNIVERSAL__) && !defined(__CYGWIN__)
|
|
void AudacityApp::AssociateFileTypes() {
|
|
// Check pref in case user has already decided against it.
|
|
bool bWantAssociateFiles = true;
|
|
if (gPrefs->Read(wxT("/WantAssociateFiles"), &bWantAssociateFiles) &&
|
|
!bWantAssociateFiles) {
|
|
// User has already decided against it
|
|
return;
|
|
}
|
|
|
|
wxRegKey associateFileTypes;
|
|
|
|
auto IsDefined = [&](const wxString& type) {
|
|
associateFileTypes.SetName(wxString::Format(wxT("HKCR\\%s"), type));
|
|
bool bKeyExists = associateFileTypes.Exists();
|
|
if (!bKeyExists) {
|
|
// Not at HKEY_CLASSES_ROOT. Try HKEY_CURRENT_USER.
|
|
associateFileTypes.SetName(wxString::Format(wxT("HKCU\\Software\\Classes\\%s"), type));
|
|
bKeyExists = associateFileTypes.Exists();
|
|
}
|
|
return bKeyExists;
|
|
};
|
|
|
|
auto DefineType = [&](const wxString& type) {
|
|
wxString root_key = wxT("HKCU\\Software\\Classes\\");
|
|
|
|
// Start with HKEY_CLASSES_CURRENT_USER.
|
|
associateFileTypes.SetName(wxString::Format(wxT("%s%s"), root_key, type));
|
|
if (!associateFileTypes.Create(true)) {
|
|
// Not at HKEY_CLASSES_CURRENT_USER. Try HKEY_CURRENT_ROOT.
|
|
root_key = wxT("HKCR\\");
|
|
associateFileTypes.SetName(wxString::Format(wxT("%s%s"), root_key, type));
|
|
if (!associateFileTypes.Create(true)) {
|
|
// Actually, can't create keys. Empty root_key to flag failure.
|
|
root_key.empty();
|
|
}
|
|
}
|
|
|
|
if (!root_key.empty()) {
|
|
associateFileTypes = wxT("Tenacity.Project"); // Finally set value for the key
|
|
}
|
|
|
|
return root_key;
|
|
};
|
|
|
|
// Check for legacy and UP types
|
|
if (IsDefined(wxT(".aup3"))
|
|
&& IsDefined(wxT(".aup"))
|
|
&& IsDefined(wxT("Tenacity.Project"))) {
|
|
// Already defined, so bail
|
|
return;
|
|
}
|
|
|
|
// File types are not currently associated.
|
|
int wantAssoc =
|
|
AudacityMessageBox(
|
|
XO(
|
|
"Audacity project (.aup3) files are not currently \nassociated with Audacity. \n\nAssociate them, so they open on double-click?"),
|
|
XO("Audacity Project Files"),
|
|
wxYES_NO | wxICON_QUESTION);
|
|
|
|
if (wantAssoc == wxNO) {
|
|
// User said no. Set a pref so we don't keep asking.
|
|
gPrefs->Write(wxT("/WantAssociateFiles"), false);
|
|
gPrefs->Flush();
|
|
return;
|
|
}
|
|
|
|
// Show that user wants associations
|
|
gPrefs->Write(wxT("/WantAssociateFiles"), true);
|
|
gPrefs->Flush();
|
|
|
|
wxString root_key;
|
|
|
|
root_key = DefineType(wxT(".aup3"));
|
|
if (root_key.empty()) {
|
|
//v Warn that we can't set keys. Ask whether to set pref for no retry?
|
|
} else {
|
|
DefineType(wxT(".aup"));
|
|
|
|
associateFileTypes = wxT("Tenacity.Project"); // Finally set value for .AUP key
|
|
associateFileTypes.SetName(root_key + wxT("Tenacity.Project"));
|
|
if (!associateFileTypes.Exists()) {
|
|
associateFileTypes.Create(true);
|
|
associateFileTypes = wxT("Audacity Project File");
|
|
}
|
|
|
|
associateFileTypes.SetName(root_key + wxT("Tenacity.Project\\shell"));
|
|
if (!associateFileTypes.Exists()) {
|
|
associateFileTypes.Create(true);
|
|
associateFileTypes = wxT("");
|
|
}
|
|
|
|
associateFileTypes.SetName(root_key + wxT("Tenacity.Project\\shell\\open"));
|
|
if (!associateFileTypes.Exists()) {
|
|
associateFileTypes.Create(true);
|
|
}
|
|
|
|
associateFileTypes.SetName(root_key + wxT("Tenacity.Project\\shell\\open\\command"));
|
|
wxString tmpRegAudPath;
|
|
if (associateFileTypes.Exists()) {
|
|
tmpRegAudPath = associateFileTypes.QueryDefaultValue().Lower();
|
|
}
|
|
|
|
if (!associateFileTypes.Exists() ||
|
|
(tmpRegAudPath.Find(wxT("tenacity.exe")) >= 0)) {
|
|
associateFileTypes.Create(true);
|
|
associateFileTypes = (wxString)argv[0] + (wxString)wxT(" \"%1\"");
|
|
}
|
|
|
|
#if 0
|
|
// These can be use later to support more startup messages
|
|
// like maybe "Import into existing project" or some such.
|
|
// Leaving here for an example...
|
|
associateFileTypes.SetName(root_key + wxT("Tenacity.Project\\shell\\open\\ddeexec"));
|
|
if (!associateFileTypes.Exists()) {
|
|
associateFileTypes.Create(true);
|
|
associateFileTypes = wxT("%1");
|
|
}
|
|
|
|
associateFileTypes.SetName(root_key + wxT("Tenacity.Project\\shell\\open\\ddeexec\\Application"));
|
|
if (!associateFileTypes.Exists()) {
|
|
associateFileTypes.Create(true);
|
|
associateFileTypes = IPC_APPL;
|
|
}
|
|
|
|
associateFileTypes.SetName(root_key + wxT("Tenacity.Project\\shell\\open\\ddeexec\\Topic"));
|
|
if (!associateFileTypes.Exists()) {
|
|
associateFileTypes.Create(true);
|
|
associateFileTypes = IPC_TOPIC;
|
|
}
|
|
#endif
|
|
}
|
|
}
|
|
#endif
|
|
|