mirror of
https://github.com/saitohirga/WSJT-X.git
synced 2024-10-31 15:47:10 -04:00
45b12e6028
Re-enabling the WSJT-X i18n facilities. This allows translation files to be created for languages that are automatically used to lookup translatable strings. To enable a new language the language name must be added to the CMakeLists.txt LANGUAGES list variable in BCP47 format (i.e. en_US, en_GB, pt_PT, ...). Do one build with the CMake option UPDATE_TRANSLATIONS enabled (do not leave it enabled as there is a danger of loosing existing translated texts), that will create a fresh translations/wsjtx_<lang>.ts file which should be immediately checked in with the CMakeLists.txt change. The .ts should then be updated by the translator using the Qt Linguist tool to add translations. Check in the updated .ts file to complete the initial translation process for that language. To aid translators their WIP .ts file may be tested by releasing (using the lrelease tool or from the Linguist menu) a .qm file and placing that .qm file in the current directory before starting WSJT-X. The translations will be used if the system locale matches the file name. If the system locale does not match the file name; the language may be overridden by setting the LANG environment variable. For example if a wsjtx_pt_PT.qm file is in the current directory WSJT-X will use it for translation lookups, regardless of the current system locale setting, if the LANG variable is set to pt_PT or pt-PT. On MS Windows from a command prompt: set LANG=pt_PT C:\WSJT\wsjtx\bin\wsjtx elsewhere: LANG=pt_PT wsjtx
423 lines
17 KiB
C++
423 lines
17 KiB
C++
#include <iostream>
|
|
#include <exception>
|
|
#include <stdexcept>
|
|
#include <string>
|
|
|
|
#include <locale.h>
|
|
#include <fftw3.h>
|
|
|
|
#include <QSharedMemory>
|
|
#include <QTemporaryFile>
|
|
#include <QDateTime>
|
|
#include <QApplication>
|
|
#include <QTranslator>
|
|
#include <QRegularExpression>
|
|
#include <QObject>
|
|
#include <QSettings>
|
|
#include <QLibraryInfo>
|
|
#include <QSysInfo>
|
|
#include <QDir>
|
|
#include <QStandardPaths>
|
|
#include <QStringList>
|
|
#include <QLockFile>
|
|
#include <QSplashScreen>
|
|
#include <QCommandLineParser>
|
|
#include <QCommandLineOption>
|
|
#include <QSqlDatabase>
|
|
#include <QSqlQuery>
|
|
#include <QSqlError>
|
|
|
|
#include "revision_utils.hpp"
|
|
#include "MetaDataRegistry.hpp"
|
|
#include "SettingsGroup.hpp"
|
|
#include "TraceFile.hpp"
|
|
#include "MultiSettings.hpp"
|
|
#include "widgets/mainwindow.h"
|
|
#include "commons.h"
|
|
#include "lib/init_random_seed.h"
|
|
#include "Radio.hpp"
|
|
#include "models/FrequencyList.hpp"
|
|
#include "widgets/SplashScreen.hpp"
|
|
#include "widgets/MessageBox.hpp" // last to avoid nasty MS macro definitions
|
|
|
|
extern "C" {
|
|
// Fortran procedures we need
|
|
void four2a_(_Complex float *, int * nfft, int * ndim, int * isign, int * iform, int len);
|
|
}
|
|
|
|
namespace
|
|
{
|
|
struct RNGSetup
|
|
{
|
|
RNGSetup ()
|
|
{
|
|
// one time seed of pseudo RNGs from current time
|
|
auto seed = QDateTime::currentMSecsSinceEpoch ();
|
|
qsrand (seed); // this is good for rand() as well
|
|
}
|
|
} seeding;
|
|
|
|
// We can't use the GUI after QApplication::exit() is called so
|
|
// uncaught exceptions can get lost on Windows systems where there
|
|
// is no console terminal, so here we override
|
|
// QApplication::notify() and wrap the base class call with a try
|
|
// block to catch and display exceptions in a message box.
|
|
class ExceptionCatchingApplication final
|
|
: public QApplication
|
|
{
|
|
public:
|
|
explicit ExceptionCatchingApplication (int& argc, char * * argv)
|
|
: QApplication {argc, argv}
|
|
{
|
|
}
|
|
bool notify (QObject * receiver, QEvent * e) override
|
|
{
|
|
try
|
|
{
|
|
return QApplication::notify (receiver, e);
|
|
}
|
|
catch (std::exception const& e)
|
|
{
|
|
MessageBox::critical_message (nullptr, translate ("main", "Fatal error"), e.what ());
|
|
throw;
|
|
}
|
|
catch (...)
|
|
{
|
|
MessageBox::critical_message (nullptr, translate ("main", "Unexpected fatal error"));
|
|
throw;
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
int main(int argc, char *argv[])
|
|
{
|
|
// ### Add timestamps to all debug messages
|
|
// qSetMessagePattern ("[%{time yyyyMMdd HH:mm:ss.zzz t} %{if-debug}D%{endif}%{if-info}I%{endif}%{if-warning}W%{endif}%{if-critical}C%{endif}%{if-fatal}F%{endif}] %{message}");
|
|
|
|
init_random_seed ();
|
|
|
|
// make the Qt type magic happen
|
|
Radio::register_types ();
|
|
register_types ();
|
|
|
|
// Multiple instances communicate with jt9 via this
|
|
QSharedMemory mem_jt9;
|
|
|
|
ExceptionCatchingApplication a(argc, argv);
|
|
try
|
|
{
|
|
//
|
|
// Enable i18n
|
|
//
|
|
QTranslator translator_from_resources;
|
|
// Default translations for releases use translations stored in
|
|
// the resources file system under the Translations
|
|
// directory. These are built by the CMake build system from .ts
|
|
// files in the translations source directory. New languages are
|
|
// added by enabling the UPDATE_TRANSLATIONS CMake option and
|
|
// building with the new language added to the LANGUAGES CMake
|
|
// list variable. UPDATE_TRANSLATIONS will preserve existing
|
|
// translations but should only be set when adding new
|
|
// languages. The resulting .ts files should be checked info
|
|
// source control for translators to access and update.
|
|
translator_from_resources.load (QLocale::system (), "wsjtx", "_", ":/Translations");
|
|
a.installTranslator (&translator_from_resources);
|
|
|
|
QTranslator translator_from_files;
|
|
// Load any matching translation from the current directory
|
|
// using the locale name. This allows translators to easily test
|
|
// their translations by releasing (lrelease) a .qm file into
|
|
// the current directory with a suitable name
|
|
// (e.g. wsjtx_en_GB.qm), then running wsjtx to view the
|
|
// results. Either the system locale setting or the environment
|
|
// variable LANG can be used to select the target language.
|
|
translator_from_files.load (QString {"wsjtx_"} + QLocale::system ().name ());
|
|
a.installTranslator (&translator_from_files);
|
|
|
|
setlocale (LC_NUMERIC, "C"); // ensure number forms are in
|
|
// consistent format, do this after
|
|
// instantiating QApplication so
|
|
// that GUI has correct l18n
|
|
|
|
// Override programs executable basename as application name.
|
|
a.setApplicationName ("WSJT-X");
|
|
a.setApplicationVersion (version ());
|
|
|
|
QCommandLineParser parser;
|
|
parser.setApplicationDescription ("\n" PROJECT_SUMMARY_DESCRIPTION);
|
|
auto help_option = parser.addHelpOption ();
|
|
auto version_option = parser.addVersionOption ();
|
|
|
|
// support for multiple instances running from a single installation
|
|
QCommandLineOption rig_option (QStringList {} << "r" << "rig-name"
|
|
, a.translate ("main", "Where <rig-name> is for multi-instance support.")
|
|
, a.translate ("main", "rig-name"));
|
|
parser.addOption (rig_option);
|
|
|
|
// support for start up configuration
|
|
QCommandLineOption cfg_option (QStringList {} << "c" << "config"
|
|
, a.translate ("main", "Where <configuration> is an existing one.")
|
|
, a.translate ("main", "configuration"));
|
|
parser.addOption (cfg_option);
|
|
|
|
QCommandLineOption test_option (QStringList {} << "test-mode"
|
|
, a.translate ("main", "Writable files in test location. Use with caution, for testing only."));
|
|
parser.addOption (test_option);
|
|
|
|
if (!parser.parse (a.arguments ()))
|
|
{
|
|
MessageBox::critical_message (nullptr, a.translate ("main", "Command line error"), parser.errorText ());
|
|
return -1;
|
|
}
|
|
else
|
|
{
|
|
if (parser.isSet (help_option))
|
|
{
|
|
MessageBox::information_message (nullptr, a.translate ("main", "Command line help"), parser.helpText ());
|
|
return 0;
|
|
}
|
|
else if (parser.isSet (version_option))
|
|
{
|
|
MessageBox::information_message (nullptr, a.translate ("main", "Application version"), a.applicationVersion ());
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
QStandardPaths::setTestModeEnabled (parser.isSet (test_option));
|
|
|
|
// support for multiple instances running from a single installation
|
|
bool multiple {false};
|
|
if (parser.isSet (rig_option) || parser.isSet (test_option))
|
|
{
|
|
auto temp_name = parser.value (rig_option);
|
|
if (!temp_name.isEmpty ())
|
|
{
|
|
if (temp_name.contains (QRegularExpression {R"([\\/,])"}))
|
|
{
|
|
std::cerr << QObject::tr ("Invalid rig name - \\ & / not allowed").toLocal8Bit ().data () << std::endl;
|
|
parser.showHelp (-1);
|
|
}
|
|
|
|
a.setApplicationName (a.applicationName () + " - " + temp_name);
|
|
}
|
|
|
|
if (parser.isSet (test_option))
|
|
{
|
|
a.setApplicationName (a.applicationName () + " - test");
|
|
}
|
|
|
|
multiple = true;
|
|
}
|
|
|
|
// now we have the application name we can open the settings
|
|
MultiSettings multi_settings {parser.value (cfg_option)};
|
|
|
|
// find the temporary files path
|
|
QDir temp_dir {QStandardPaths::writableLocation (QStandardPaths::TempLocation)};
|
|
Q_ASSERT (temp_dir.exists ()); // sanity check
|
|
|
|
// disallow multiple instances with same instance key
|
|
QLockFile instance_lock {temp_dir.absoluteFilePath (a.applicationName () + ".lock")};
|
|
instance_lock.setStaleLockTime (0);
|
|
bool lock_ok {false};
|
|
while (!(lock_ok = instance_lock.tryLock ()))
|
|
{
|
|
if (QLockFile::LockFailedError == instance_lock.error ())
|
|
{
|
|
auto button = MessageBox::query_message (nullptr
|
|
, a.translate ("main", "Another instance may be running")
|
|
, a.translate ("main", "try to remove stale lock file?")
|
|
, QString {}
|
|
, MessageBox::Yes | MessageBox::Retry | MessageBox::No
|
|
, MessageBox::Yes);
|
|
switch (button)
|
|
{
|
|
case MessageBox::Yes:
|
|
instance_lock.removeStaleLockFile ();
|
|
break;
|
|
|
|
case MessageBox::Retry:
|
|
break;
|
|
|
|
default:
|
|
throw std::runtime_error {"Multiple instances must have unique rig names"};
|
|
}
|
|
}
|
|
}
|
|
|
|
#if WSJT_QDEBUG_TO_FILE
|
|
// Open a trace file
|
|
TraceFile trace_file {temp_dir.absoluteFilePath (a.applicationName () + "_trace.log")};
|
|
qSetMessagePattern ("[%{time yyyyMMdd HH:mm:ss.zzz t} %{if-debug}D%{endif}%{if-info}I%{endif}%{if-warning}W%{endif}%{if-critical}C%{endif}%{if-fatal}F%{endif}] %{file}:%{line} - %{message}");
|
|
qDebug () << program_title (revision ()) + " - Program startup";
|
|
#endif
|
|
|
|
// Create a unique writeable temporary directory in a suitable location
|
|
bool temp_ok {false};
|
|
QString unique_directory {QApplication::applicationName ()};
|
|
do
|
|
{
|
|
if (!temp_dir.mkpath (unique_directory)
|
|
|| !temp_dir.cd (unique_directory))
|
|
{
|
|
MessageBox::critical_message (nullptr,
|
|
a.translate ("main", "Failed to create a temporary directory"),
|
|
a.translate ("main", "Path: \"%1\"").arg (temp_dir.absolutePath ()));
|
|
throw std::runtime_error {"Failed to create a temporary directory"};
|
|
}
|
|
if (!temp_dir.isReadable () || !(temp_ok = QTemporaryFile {temp_dir.absoluteFilePath ("test")}.open ()))
|
|
{
|
|
auto button = MessageBox::critical_message (nullptr,
|
|
a.translate ("main", "Failed to create a usable temporary directory"),
|
|
a.translate ("main", "Another application may be locking the directory"),
|
|
a.translate ("main", "Path: \"%1\"").arg (temp_dir.absolutePath ()),
|
|
MessageBox::Retry | MessageBox::Cancel);
|
|
if (MessageBox::Cancel == button)
|
|
{
|
|
throw std::runtime_error {"Failed to create a usable temporary directory"};
|
|
}
|
|
temp_dir.cdUp (); // revert to parent as this one is no good
|
|
}
|
|
}
|
|
while (!temp_ok);
|
|
|
|
SplashScreen splash;
|
|
{
|
|
// change this key if you want to force a new splash screen
|
|
// for a new version, the user will be able to re-disable it
|
|
// if they wish
|
|
QString splash_flag_name {"Splash_v1.7"};
|
|
if (multi_settings.common_value (splash_flag_name, true).toBool ())
|
|
{
|
|
QObject::connect (&splash, &SplashScreen::disabled, [&, splash_flag_name] {
|
|
multi_settings.set_common_value (splash_flag_name, false);
|
|
splash.close ();
|
|
});
|
|
splash.show ();
|
|
a.processEvents ();
|
|
}
|
|
}
|
|
|
|
// create writeable data directory if not already there
|
|
auto writeable_data_dir = QDir {QStandardPaths::writableLocation (QStandardPaths::DataLocation)};
|
|
if (!writeable_data_dir.mkpath ("."))
|
|
{
|
|
MessageBox::critical_message (nullptr, a.translate ("main", "Failed to create data directory"),
|
|
a.translate ("main", "path: \"%1\"").arg (writeable_data_dir.absolutePath ()));
|
|
throw std::runtime_error {"Failed to create data directory"};
|
|
}
|
|
|
|
// set up SQLite database
|
|
if (!QSqlDatabase::drivers ().contains ("QSQLITE"))
|
|
{
|
|
throw std::runtime_error {"Failed to find SQLite Qt driver"};
|
|
}
|
|
auto db = QSqlDatabase::addDatabase ("QSQLITE");
|
|
db.setDatabaseName (writeable_data_dir.absoluteFilePath ("db.sqlite"));
|
|
if (!db.open ())
|
|
{
|
|
throw std::runtime_error {("Database Error: " + db.lastError ().text ()).toStdString ()};
|
|
}
|
|
|
|
// better performance traded for a risk of d/b corruption
|
|
// on system crash or application crash
|
|
// db.exec ("PRAGMA synchronous=OFF"); // system crash risk
|
|
// db.exec ("PRAGMA journal_mode=MEMORY"); // application crash risk
|
|
db.exec ("PRAGMA locking_mode=EXCLUSIVE");
|
|
|
|
int result;
|
|
do
|
|
{
|
|
#if WSJT_QDEBUG_TO_FILE
|
|
// announce to trace file and dump settings
|
|
qDebug () << "++++++++++++++++++++++++++++ Settings ++++++++++++++++++++++++++++";
|
|
for (auto const& key: multi_settings.settings ()->allKeys ())
|
|
{
|
|
auto const& value = multi_settings.settings ()->value (key);
|
|
if (value.canConvert<QVariantList> ())
|
|
{
|
|
auto const sequence = value.value<QSequentialIterable> ();
|
|
qDebug ().nospace () << key << ": ";
|
|
for (auto const& item: sequence)
|
|
{
|
|
qDebug ().nospace () << '\t' << item;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
qDebug ().nospace () << key << ": " << value;
|
|
}
|
|
}
|
|
qDebug () << "---------------------------- Settings ----------------------------";
|
|
#endif
|
|
|
|
// Create and initialize shared memory segment
|
|
// Multiple instances: use rig_name as shared memory key
|
|
mem_jt9.setKey(a.applicationName ());
|
|
|
|
if(!mem_jt9.attach()) {
|
|
if (!mem_jt9.create(sizeof(struct dec_data))) {
|
|
splash.hide ();
|
|
MessageBox::critical_message (nullptr, a.translate ("main", "Shared memory error"),
|
|
a.translate ("main", "Unable to create shared memory segment"));
|
|
throw std::runtime_error {"Shared memory error"};
|
|
}
|
|
}
|
|
memset(mem_jt9.data(),0,sizeof(struct dec_data)); //Zero all decoding params in shared memory
|
|
|
|
unsigned downSampleFactor;
|
|
{
|
|
SettingsGroup {multi_settings.settings (), "Tune"};
|
|
|
|
// deal with Windows Vista and earlier input audio rate
|
|
// converter problems
|
|
downSampleFactor = multi_settings.settings ()->value ("Audio/DisableInputResampling",
|
|
#if defined (Q_OS_WIN)
|
|
// default to true for
|
|
// Windows Vista and older
|
|
QSysInfo::WV_VISTA >= QSysInfo::WindowsVersion ? true : false
|
|
#else
|
|
false
|
|
#endif
|
|
).toBool () ? 1u : 4u;
|
|
}
|
|
|
|
// run the application UI
|
|
MainWindow w(temp_dir, multiple, &multi_settings, &mem_jt9, downSampleFactor, &splash);
|
|
w.show();
|
|
splash.raise ();
|
|
QObject::connect (&a, SIGNAL (lastWindowClosed()), &a, SLOT (quit()));
|
|
result = a.exec();
|
|
}
|
|
while (!result && !multi_settings.exit ());
|
|
|
|
// clean up lazily initialized resources
|
|
{
|
|
int nfft {-1};
|
|
int ndim {1};
|
|
int isign {1};
|
|
int iform {1};
|
|
// free FFT plan resources
|
|
four2a_ (nullptr, &nfft, &ndim, &isign, &iform, 0);
|
|
}
|
|
fftwf_forget_wisdom ();
|
|
fftwf_cleanup ();
|
|
|
|
temp_dir.removeRecursively (); // clean up temp files
|
|
return result;
|
|
}
|
|
catch (std::exception const& e)
|
|
{
|
|
MessageBox::critical_message (nullptr, QApplication::translate ("main", "Fatal error"), e.what ());
|
|
std::cerr << "Error: " << e.what () << '\n';
|
|
}
|
|
catch (...)
|
|
{
|
|
MessageBox::critical_message (nullptr, QApplication::translate ("main", "Unexpected fatal error"));
|
|
std::cerr << "Unexpected fatal error\n";
|
|
throw; // hoping the runtime might tell us more about the exception
|
|
}
|
|
return -1;
|
|
}
|