| Chris@0 | 1 /* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */ | 
| Chris@0 | 2 | 
| Chris@0 | 3 /* | 
| Chris@0 | 4     Sonic Visualiser | 
| Chris@0 | 5     An audio file viewer and annotation editor. | 
| Chris@0 | 6     Centre for Digital Music, Queen Mary, University of London. | 
| Chris@77 | 7     This file copyright 2006 Chris Cannam and QMUL. | 
| Chris@0 | 8 | 
| Chris@0 | 9     This program is free software; you can redistribute it and/or | 
| Chris@0 | 10     modify it under the terms of the GNU General Public License as | 
| Chris@0 | 11     published by the Free Software Foundation; either version 2 of the | 
| Chris@0 | 12     License, or (at your option) any later version.  See the file | 
| Chris@0 | 13     COPYING included with this distribution for more information. | 
| Chris@0 | 14 */ | 
| Chris@0 | 15 | 
| Chris@0 | 16 #include "MainWindow.h" | 
| Chris@0 | 17 | 
| Chris@1 | 18 #include "system/System.h" | 
| Chris@1 | 19 #include "system/Init.h" | 
| Chris@0 | 20 #include "base/TempDirectory.h" | 
| Chris@0 | 21 #include "base/PropertyContainer.h" | 
| Chris@0 | 22 #include "base/Preferences.h" | 
| Chris@120 | 23 #include "widgets/TipDialog.h" | 
| Chris@0 | 24 | 
| Chris@0 | 25 #include <QMetaType> | 
| Chris@0 | 26 #include <QApplication> | 
| Chris@0 | 27 #include <QDesktopWidget> | 
| Chris@0 | 28 #include <QMessageBox> | 
| Chris@0 | 29 #include <QTranslator> | 
| Chris@0 | 30 #include <QLocale> | 
| Chris@5 | 31 #include <QSettings> | 
| Chris@7 | 32 #include <QIcon> | 
| Chris@11 | 33 #include <QSessionManager> | 
| Chris@165 | 34 #include <QDir> | 
| Chris@231 | 35 #include <QSplashScreen> | 
| Chris@0 | 36 | 
| Chris@0 | 37 #include <iostream> | 
| Chris@0 | 38 #include <signal.h> | 
| Chris@0 | 39 | 
| Chris@215 | 40 #ifdef HAVE_FFTW3F | 
| Chris@215 | 41 #include <fftw3.h> | 
| Chris@215 | 42 #endif | 
| Chris@215 | 43 | 
| Chris@127 | 44 /*! \mainpage Sonic Visualiser | 
| Chris@127 | 45 | 
| Chris@127 | 46 \section interesting Summary of interesting classes | 
| Chris@127 | 47 | 
| Chris@127 | 48  - Data models: Model and subclasses, e.g. WaveFileModel | 
| Chris@127 | 49 | 
| Chris@127 | 50  - Graphical layers: Layer and subclasses, displayed on View and its | 
| Chris@127 | 51  subclass widgets. | 
| Chris@127 | 52 | 
| Chris@127 | 53  - Main window class, document class, and file parser: MainWindow, | 
| Chris@127 | 54  Document, SVFileReader | 
| Chris@127 | 55 | 
| Chris@127 | 56  - Turning one model (e.g. audio) into another (e.g. more audio, or a | 
| Chris@127 | 57  curve extracted from it): Transform and subclasses | 
| Chris@127 | 58 | 
| Chris@127 | 59  - Creating the plugins used by transforms: RealTimePluginFactory, | 
| Chris@129 | 60  FeatureExtractionPluginFactory.  See also the API documentation for | 
| Chris@129 | 61  Vamp feature extraction plugins at | 
| Chris@129 | 62  http://www.vamp-plugins.org/code-doc/. | 
| Chris@127 | 63 | 
| Chris@127 | 64  - File reading and writing code: AudioFileReader and subclasses, | 
| Chris@127 | 65  WavFileWriter, DataFileReader, SVFileReader | 
| Chris@127 | 66 | 
| Chris@127 | 67  - FFT calculation and cacheing: FFTModel, FFTDataServer | 
| Chris@127 | 68 | 
| Chris@127 | 69  - Widgets that show groups of editable properties: PropertyBox for | 
| Chris@127 | 70  layer properties (contained in a PropertyStack), PluginParameterBox | 
| Chris@127 | 71  for plugins (contained in a PluginParameterDialog) | 
| Chris@127 | 72 | 
| Chris@127 | 73  - Audio playback: AudioCallbackPlaySource and subclasses, | 
| Chris@127 | 74  AudioCallbackPlayTarget and subclasses, AudioGenerator | 
| Chris@127 | 75 | 
| Chris@127 | 76 \section model Data sources: the Model hierarchy | 
| Chris@127 | 77 | 
| Chris@127 | 78    A Model is something containing, or knowing how to obtain, data. | 
| Chris@127 | 79 | 
| Chris@127 | 80    For example, WaveFileModel is a model that knows how to get data | 
| Chris@127 | 81    from an audio file; SparseTimeValueModel is a model containing | 
| Chris@127 | 82    editable "curve" data. | 
| Chris@127 | 83 | 
| Chris@127 | 84    Models typically subclass one of a number of abstract subclasses of | 
| Chris@127 | 85    Model.  For example, WaveFileModel subclasses DenseTimeValueModel, | 
| Chris@127 | 86    which describes an interface for models that have a value at each | 
| Chris@127 | 87    time point for a given sampling resolution.  (Note that | 
| Chris@127 | 88    WaveFileModel does not actually read the files itself: it uses | 
| Chris@127 | 89    AudioFileReader classes for that.  It just makes data from the | 
| Chris@127 | 90    files available in a Model.)  SparseTimeValueModel uses the | 
| Chris@127 | 91    SparseModel template class, which provides most of the | 
| Chris@127 | 92    implementation for models that contain a series of points of some | 
| Chris@127 | 93    sort -- also used by NoteModel, TextModel, and | 
| Chris@127 | 94    SparseOneDimensionalModel. | 
| Chris@127 | 95 | 
| Chris@127 | 96    Everything that goes on the screen originates from a model, via a | 
| Chris@127 | 97    layer (see below).  The models are contained in a Document object. | 
| Chris@127 | 98    There is no containment hierarchy or ordering of models in the | 
| Chris@127 | 99    document.  One model is the main model, which defines the sample | 
| Chris@127 | 100    rate for playback. | 
| Chris@127 | 101 | 
| Chris@127 | 102    A model may also be marked as a "derived" model, which means it was | 
| Chris@127 | 103    generated from another model using some transform (feature | 
| Chris@127 | 104    extraction or effect plugin, etc) -- the idea being that they can | 
| Chris@127 | 105    be re-generated using the same transform if a new source model is | 
| Chris@127 | 106    loaded. | 
| Chris@127 | 107 | 
| Chris@127 | 108 \section layer Things that can display data: the Layer hierarchy | 
| Chris@127 | 109 | 
| Chris@127 | 110    A Layer is something that knows how to draw parts of a model onto a | 
| Chris@127 | 111    timeline. | 
| Chris@127 | 112 | 
| Chris@127 | 113    For example, WaveformLayer is a layer which draws waveforms, based | 
| Chris@127 | 114    on WaveFileModel; TimeValueLayer draws curves, based on | 
| Chris@127 | 115    SparseTimeValueModel; SpectrogramLayer draws spectrograms, based on | 
| Chris@127 | 116    WaveFileModel (via FFTModel). | 
| Chris@127 | 117 | 
| Chris@127 | 118    The most basic functions of a layer are: to draw itself onto a | 
| Chris@127 | 119    Pane, against a timeline on the x axis; and to permit user | 
| Chris@127 | 120    interaction.  If you were thinking of adding the capability to | 
| Chris@127 | 121    display a new sort of something, then you would want to add a new | 
| Chris@127 | 122    layer type.  (You may also need a new model type, depending on | 
| Chris@127 | 123    whether any existing model can capture the data you need.) | 
| Chris@127 | 124    Depending on the sort of data in question, there are various | 
| Chris@127 | 125    existing layers that might be appropriate to start from -- for | 
| Chris@127 | 126    example, a layer that displays images that the user has imported | 
| Chris@127 | 127    and associated with particular times might have something in common | 
| Chris@127 | 128    with the existing TextLayer which displays pieces of text that are | 
| Chris@127 | 129    associated with particular times. | 
| Chris@127 | 130 | 
| Chris@127 | 131    Although layers are visual objects, they are contained in the | 
| Chris@127 | 132    Document in Sonic Visualiser rather than being managed together | 
| Chris@127 | 133    with display widgets.  The Sonic Visualiser file format has | 
| Chris@127 | 134    separate data and layout sections, and the layers are defined in | 
| Chris@127 | 135    the data section and then referred to in the layout section which | 
| Chris@127 | 136    determines which layers may go on which panes (see Pane below). | 
| Chris@127 | 137 | 
| Chris@127 | 138    Once a layer class is defined, some basic data about it needs to be | 
| Chris@127 | 139    set up in the LayerFactory class, and then it will appear in the | 
| Chris@127 | 140    menus and so on on the main window. | 
| Chris@127 | 141 | 
| Chris@127 | 142 \section view Widgets that are used to show layers: The View hierarchy | 
| Chris@127 | 143 | 
| Chris@127 | 144    A View is a widget that displays a stack of layers.  The most | 
| Chris@127 | 145    important subclass is Pane, the widget that is used to show most of | 
| Chris@127 | 146    the data in the main window of Sonic Visualiser. | 
| Chris@127 | 147 | 
| Chris@127 | 148    All a pane really does is contain a set of layers and get them to | 
| Chris@127 | 149    render themselves (one on top of the other, with the topmost layer | 
| Chris@127 | 150    being the one that is currently interacted with), cache the | 
| Chris@127 | 151    results, negotiate user interaction with them, and so on.  This is | 
| Chris@127 | 152    generally fiddly, if not especially interesting.  Panes are | 
| Chris@127 | 153    strictly layout objects and are not stored in the Document class; | 
| Chris@127 | 154    instead the MainWindow contains a PaneStack widget (the widget that | 
| Chris@127 | 155    takes up most of Sonic Visualiser's main window) which contains a | 
| Chris@127 | 156    set of panes stacked vertically. | 
| Chris@127 | 157 | 
| Chris@127 | 158    Another View subclass is Overview, which is the widget that | 
| Chris@127 | 159    contains that green waveform showing the entire file at the bottom | 
| Chris@127 | 160    of the window. | 
| Chris@127 | 161 | 
| Chris@127 | 162 */ | 
| Chris@127 | 163 | 
| Chris@0 | 164 static QMutex cleanupMutex; | 
| Chris@0 | 165 | 
| Chris@0 | 166 static void | 
| Chris@0 | 167 signalHandler(int /* signal */) | 
| Chris@0 | 168 { | 
| Chris@0 | 169     // Avoid this happening more than once across threads | 
| Chris@0 | 170 | 
| Chris@0 | 171     cleanupMutex.lock(); | 
| Chris@0 | 172     std::cerr << "signalHandler: cleaning up and exiting" << std::endl; | 
| Chris@0 | 173     TempDirectory::getInstance()->cleanup(); | 
| Chris@0 | 174     exit(0); // without releasing mutex | 
| Chris@0 | 175 } | 
| Chris@0 | 176 | 
| Chris@11 | 177 class SVApplication : public QApplication | 
| Chris@11 | 178 { | 
| Chris@11 | 179 public: | 
| Chris@11 | 180     SVApplication(int argc, char **argv) : | 
| Chris@11 | 181         QApplication(argc, argv), | 
| Chris@11 | 182         m_mainWindow(0) { } | 
| Chris@11 | 183     virtual ~SVApplication() { } | 
| Chris@11 | 184 | 
| Chris@11 | 185     void setMainWindow(MainWindow *mw) { m_mainWindow = mw; } | 
| Chris@11 | 186     void releaseMainWindow() { m_mainWindow = 0; } | 
| Chris@11 | 187 | 
| Chris@11 | 188     virtual void commitData(QSessionManager &manager) { | 
| Chris@11 | 189         if (!m_mainWindow) return; | 
| Chris@11 | 190         bool mayAskUser = manager.allowsInteraction(); | 
| Chris@11 | 191         bool success = m_mainWindow->commitData(mayAskUser); | 
| Chris@11 | 192         manager.release(); | 
| Chris@11 | 193         if (!success) manager.cancel(); | 
| Chris@11 | 194     } | 
| Chris@11 | 195 | 
| Chris@11 | 196 protected: | 
| Chris@11 | 197     MainWindow *m_mainWindow; | 
| Chris@11 | 198 }; | 
| Chris@11 | 199 | 
| Chris@0 | 200 int | 
| Chris@0 | 201 main(int argc, char **argv) | 
| Chris@0 | 202 { | 
| Chris@11 | 203     SVApplication application(argc, argv); | 
| Chris@0 | 204 | 
| Chris@46 | 205     QStringList args = application.arguments(); | 
| Chris@46 | 206 | 
| Chris@0 | 207     signal(SIGINT,  signalHandler); | 
| Chris@0 | 208     signal(SIGTERM, signalHandler); | 
| Chris@0 | 209 | 
| Chris@0 | 210 #ifndef Q_WS_WIN32 | 
| Chris@0 | 211     signal(SIGHUP,  signalHandler); | 
| Chris@0 | 212     signal(SIGQUIT, signalHandler); | 
| Chris@0 | 213 #endif | 
| Chris@0 | 214 | 
| Chris@0 | 215     svSystemSpecificInitialisation(); | 
| Chris@0 | 216 | 
| Chris@46 | 217     bool audioOutput = true; | 
| Chris@70 | 218     bool oscSupport = true; | 
| Chris@70 | 219 | 
| Chris@133 | 220     if (args.contains("--help") || args.contains("-h") || args.contains("-?")) { | 
| Chris@70 | 221         std::cerr << QApplication::tr( | 
| Chris@70 | 222             "\nSonic Visualiser is a program for viewing and exploring audio data\nfor semantic music analysis and annotation.\n\nUsage:\n\n  %1 [--no-audio] [--no-osc] [<file> ...]\n\n  --no-audio: Do not attempt to open an audio output device\n  --no-osc: Do not provide an Open Sound Control port for remote control\n  <file>: One or more Sonic Visualiser (.sv) and audio files may be provided.\n").arg(argv[0]).toStdString() << std::endl; | 
| Chris@70 | 223         exit(2); | 
| Chris@70 | 224     } | 
| Chris@70 | 225 | 
| Chris@46 | 226     if (args.contains("--no-audio")) audioOutput = false; | 
| Chris@70 | 227     if (args.contains("--no-osc")) oscSupport = false; | 
| Chris@46 | 228 | 
| Chris@6 | 229     QApplication::setOrganizationName("sonic-visualiser"); | 
| Chris@5 | 230     QApplication::setOrganizationDomain("sonicvisualiser.org"); | 
| Chris@213 | 231     QApplication::setApplicationName(QApplication::tr("Sonic Visualiser")); | 
| Chris@141 | 232 | 
| Chris@231 | 233     QPixmap pixmap(":/icons/sv-splash.png"); | 
| Chris@231 | 234     QSplashScreen splash(pixmap); | 
| Chris@231 | 235 | 
| Chris@231 | 236     QSettings settings; | 
| Chris@231 | 237     settings.beginGroup("MainWindow"); | 
| Chris@231 | 238 | 
| Chris@231 | 239     if (settings.value("showsplash", true).toBool()) { | 
| Chris@231 | 240         splash.show(); | 
| Chris@231 | 241         application.processEvents(); | 
| Chris@231 | 242     } | 
| Chris@231 | 243 | 
| Chris@141 | 244     QIcon icon; | 
| Chris@141 | 245     int sizes[] = { 16, 22, 24, 32, 48, 64, 128 }; | 
| Chris@141 | 246     for (int i = 0; i < sizeof(sizes)/sizeof(sizes[0]); ++i) { | 
| Chris@141 | 247         icon.addFile(QString(":icons/sv-%1x%2.png").arg(sizes[i]).arg(sizes[i])); | 
| Chris@141 | 248     } | 
| Chris@141 | 249     QApplication::setWindowIcon(icon); | 
| Chris@7 | 250 | 
| Chris@0 | 251     QString language = QLocale::system().name(); | 
| Chris@0 | 252 | 
| Chris@0 | 253     QTranslator qtTranslator; | 
| Chris@0 | 254     QString qtTrName = QString("qt_%1").arg(language); | 
| Chris@0 | 255     std::cerr << "Loading " << qtTrName.toStdString() << "..." << std::endl; | 
| Chris@165 | 256     bool success = false; | 
| Chris@165 | 257     if (!(success = qtTranslator.load(qtTrName))) { | 
| Chris@165 | 258         QString qtDir = getenv("QTDIR"); | 
| Chris@165 | 259         if (qtDir != "") { | 
| Chris@165 | 260             success = qtTranslator.load | 
| Chris@165 | 261                 (qtTrName, QDir(qtDir).filePath("translations")); | 
| Chris@165 | 262         } | 
| Chris@165 | 263     } | 
| Chris@165 | 264     if (!success) { | 
| Chris@165 | 265         std::cerr << "Failed to load Qt translation for locale" << std::endl; | 
| Chris@165 | 266     } | 
| Chris@0 | 267     application.installTranslator(&qtTranslator); | 
| Chris@0 | 268 | 
| Chris@0 | 269     QTranslator svTranslator; | 
| Chris@0 | 270     QString svTrName = QString("sonic-visualiser_%1").arg(language); | 
| Chris@0 | 271     std::cerr << "Loading " << svTrName.toStdString() << "..." << std::endl; | 
| Chris@0 | 272     svTranslator.load(svTrName, ":i18n"); | 
| Chris@0 | 273     application.installTranslator(&svTranslator); | 
| Chris@0 | 274 | 
| Chris@187 | 275     StoreStartupLocale(); | 
| Chris@187 | 276 | 
| Chris@0 | 277     // Permit size_t and PropertyName to be used as args in queued signal calls | 
| Chris@0 | 278     qRegisterMetaType<size_t>("size_t"); | 
| Chris@0 | 279     qRegisterMetaType<PropertyContainer::PropertyName>("PropertyContainer::PropertyName"); | 
| Chris@0 | 280 | 
| Chris@222 | 281     MainWindow *gui = new MainWindow(audioOutput, oscSupport); | 
| Chris@222 | 282     application.setMainWindow(gui); | 
| Chris@0 | 283 | 
| Chris@0 | 284     QDesktopWidget *desktop = QApplication::desktop(); | 
| Chris@0 | 285     QRect available = desktop->availableGeometry(); | 
| Chris@0 | 286 | 
| Chris@0 | 287     int width = available.width() * 2 / 3; | 
| Chris@0 | 288     int height = available.height() / 2; | 
| Chris@0 | 289     if (height < 450) height = available.height() * 2 / 3; | 
| Chris@0 | 290     if (width > height * 2) width = height * 2; | 
| Chris@0 | 291 | 
| Chris@5 | 292     QSize size = settings.value("size", QSize(width, height)).toSize(); | 
| Chris@222 | 293     gui->resize(size); | 
| Chris@5 | 294     if (settings.contains("position")) { | 
| Chris@222 | 295         gui->move(settings.value("position").toPoint()); | 
| Chris@5 | 296     } | 
| Chris@5 | 297     settings.endGroup(); | 
| Chris@5 | 298 | 
| Chris@222 | 299     gui->show(); | 
| Chris@64 | 300 | 
| Chris@118 | 301     // The MainWindow class seems to have trouble dealing with this if | 
| Chris@118 | 302     // it tries to adapt to this preference before the constructor is | 
| Chris@118 | 303     // complete.  As a lazy hack, apply it explicitly from here | 
| Chris@222 | 304     gui->preferenceChanged("Property Box Layout"); | 
| Chris@118 | 305 | 
| Chris@54 | 306     bool haveSession = false; | 
| Chris@54 | 307     bool haveMainModel = false; | 
| Chris@145 | 308     bool havePriorCommandLineModel = false; | 
| Chris@46 | 309 | 
| Chris@54 | 310     for (QStringList::iterator i = args.begin(); i != args.end(); ++i) { | 
| Chris@54 | 311 | 
| Chris@83 | 312         MainWindow::FileOpenStatus status = MainWindow::FileOpenFailed; | 
| Chris@54 | 313 | 
| Chris@54 | 314         if (i == args.begin()) continue; | 
| Chris@54 | 315         if (i->startsWith('-')) continue; | 
| Chris@54 | 316 | 
| Chris@54 | 317         QString path = *i; | 
| Chris@54 | 318 | 
| Chris@54 | 319         if (path.endsWith("sv")) { | 
| Chris@54 | 320             if (!haveSession) { | 
| Chris@222 | 321                 status = gui->openSessionFile(path); | 
| Chris@82 | 322                 if (status == MainWindow::FileOpenSucceeded) { | 
| Chris@54 | 323                     haveSession = true; | 
| Chris@54 | 324                     haveMainModel = true; | 
| Chris@54 | 325                 } | 
| Chris@54 | 326             } else { | 
| Chris@54 | 327                 std::cerr << "WARNING: Ignoring additional session file argument \"" << path.toStdString() << "\"" << std::endl; | 
| Chris@82 | 328                 status = MainWindow::FileOpenSucceeded; | 
| Chris@54 | 329             } | 
| Chris@54 | 330         } | 
| Chris@82 | 331         if (status != MainWindow::FileOpenSucceeded) { | 
| Chris@54 | 332             if (!haveMainModel) { | 
| Chris@222 | 333                 status = gui->open(path, MainWindow::ReplaceMainModel); | 
| Chris@145 | 334                 if (status == MainWindow::FileOpenSucceeded) { | 
| Chris@145 | 335                     haveMainModel = true; | 
| Chris@145 | 336                 } | 
| Chris@54 | 337             } else { | 
| Chris@145 | 338                 if (haveSession && !havePriorCommandLineModel) { | 
| Chris@222 | 339                     status = gui->open(path, MainWindow::AskUser); | 
| Chris@145 | 340                     if (status == MainWindow::FileOpenSucceeded) { | 
| Chris@145 | 341                         havePriorCommandLineModel = true; | 
| Chris@145 | 342                     } | 
| Chris@145 | 343                 } else { | 
| Chris@222 | 344                     status = gui->open(path, MainWindow::CreateAdditionalModel); | 
| Chris@145 | 345                 } | 
| Chris@54 | 346             } | 
| Chris@54 | 347         } | 
| Chris@82 | 348         if (status == MainWindow::FileOpenFailed) { | 
| Chris@54 | 349 	    QMessageBox::critical | 
| Chris@222 | 350                 (gui, QMessageBox::tr("Failed to open file"), | 
| Chris@197 | 351                  QMessageBox::tr("File or URL \"%1\" could not be opened").arg(path)); | 
| Chris@54 | 352         } | 
| Chris@180 | 353     } | 
| Chris@180 | 354 | 
| Chris@215 | 355 #ifdef HAVE_FFTW3F | 
| Chris@215 | 356     settings.beginGroup("FFTWisdom"); | 
| Chris@215 | 357     QString wisdom = settings.value("wisdom").toString(); | 
| Chris@215 | 358     if (wisdom != "") { | 
| Chris@215 | 359         fftwf_import_wisdom_from_string(wisdom.toLocal8Bit().data()); | 
| Chris@215 | 360     } | 
| Chris@215 | 361     settings.endGroup(); | 
| Chris@215 | 362 #endif | 
| Chris@180 | 363 | 
| Chris@231 | 364     splash.finish(gui); | 
| Chris@180 | 365 | 
| Chris@123 | 366 /* | 
| Chris@120 | 367     TipDialog tipDialog; | 
| Chris@120 | 368     if (tipDialog.isOK()) { | 
| Chris@120 | 369         tipDialog.exec(); | 
| Chris@120 | 370     } | 
| Chris@123 | 371 */ | 
| Chris@0 | 372     int rv = application.exec(); | 
| Chris@222 | 373     std::cerr << "application.exec() returned " << rv << std::endl; | 
| Chris@0 | 374 | 
| Chris@0 | 375     cleanupMutex.lock(); | 
| Chris@0 | 376     TempDirectory::getInstance()->cleanup(); | 
| Chris@222 | 377 | 
| Chris@11 | 378     application.releaseMainWindow(); | 
| Chris@5 | 379 | 
| Chris@215 | 380 #ifdef HAVE_FFTW3F | 
| Chris@215 | 381     char *cwisdom = fftwf_export_wisdom_to_string(); | 
| Chris@215 | 382     if (cwisdom) { | 
| Chris@215 | 383         settings.beginGroup("FFTWisdom"); | 
| Chris@215 | 384         settings.setValue("wisdom", cwisdom); | 
| Chris@215 | 385         settings.endGroup(); | 
| Chris@215 | 386         fftwf_free(cwisdom); | 
| Chris@215 | 387     } | 
| Chris@215 | 388 #endif | 
| Chris@215 | 389 | 
| Chris@222 | 390     delete gui; | 
| Chris@222 | 391 | 
| Chris@0 | 392     return rv; | 
| Chris@0 | 393 } |