1 /***************************************************************************
2     kwave/FileContext.cpp  -  Context of a Loaded File
3 			     -------------------
4     begin                : 2010-01-02
5     copyright            : (C) 2010 by Thomas.Eschenbacher
6     email                : Thomas.Eschenbacher@gmx.de
7  ***************************************************************************/
8 
9 /***************************************************************************
10  *                                                                         *
11  *   This program is free software; you can redistribute it and/or modify  *
12  *   it under the terms of the GNU General Public License as published by  *
13  *   the Free Software Foundation; either version 2 of the License, or     *
14  *   (at your option) any later version.                                   *
15  *                                                                         *
16  ***************************************************************************/
17 
18 #include "config.h"
19 
20 #include <errno.h>
21 #include <new>
22 
23 #include <QApplication>
24 #include <QFile>
25 #include <QLocale>
26 #include <QPointer>
27 #include <QTextStream>
28 #include <QMdiSubWindow>
29 #include <QStandardPaths>
30 
31 #include <KLocalizedString>
32 
33 #include "libkwave/CodecManager.h"
34 #include "libkwave/Encoder.h"
35 #include "libkwave/Logger.h"
36 #include "libkwave/MessageBox.h"
37 #include "libkwave/Parser.h"
38 #include "libkwave/PlaybackController.h"
39 #include "libkwave/PluginManager.h"
40 #include "libkwave/SignalManager.h"
41 #include "libkwave/Utils.h"
42 
43 #include "libgui/FileDialog.h"
44 
45 #include "App.h"
46 #include "FileContext.h"
47 #include "MainWidget.h"
48 #include "Splash.h"
49 #include "TopWidget.h"
50 
51 /**
52  * useful macro for command parsing
53  */
54 #define CASE_COMMAND(x) } else if (parser.command() == _(x)) {
55 
56 //***************************************************************************
57 /**
58  * struct for info about a label within a Kwave script
59  * @internal
60  */
61 namespace Kwave {
62     typedef struct {
63 	qint64       pos;  /**< position within the stream      */
64 	unsigned int hits; /**< number of "goto"s to this label */
65     } label_t;
66 }
67 
68 //***************************************************************************
FileContext(Kwave::App & app)69 Kwave::FileContext::FileContext(Kwave::App &app)
70     :QObject(),
71      m_use_count(1),
72      m_application(app),
73      m_top_widget(Q_NULLPTR),
74      m_main_widget(Q_NULLPTR),
75      m_signal_manager(Q_NULLPTR),
76      m_plugin_manager(Q_NULLPTR),
77      m_active(true),
78      m_last_zoom(0),
79      m_last_playback_pos(0),
80      m_last_status_message_text(),
81      m_last_status_message_timer(),
82      m_last_status_message_ms(0),
83      m_last_undo(QString()),
84      m_last_redo(QString()),
85      m_instance_nr(-1),
86      m_delayed_command_timer(),
87      m_delayed_command_queue()
88 {
89     m_delayed_command_timer.setSingleShot(true);
90     connect(&m_delayed_command_timer, SIGNAL(timeout()),
91             this, SLOT(processDelayedCommand()));
92 }
93 
94 //***************************************************************************
~FileContext()95 Kwave::FileContext::~FileContext()
96 {
97     if (m_main_widget) delete m_main_widget;
98     m_main_widget = Q_NULLPTR;
99 
100     m_top_widget = Q_NULLPTR;
101 
102     if (m_plugin_manager) delete m_plugin_manager;
103     m_plugin_manager = Q_NULLPTR;
104 
105     if (m_signal_manager) delete m_signal_manager;
106     m_signal_manager = Q_NULLPTR;
107 }
108 
109 //***************************************************************************
use()110 void Kwave::FileContext::use()
111 {
112     Q_ASSERT(int(m_use_count) > 0);
113     m_use_count.ref();
114 }
115 
116 //***************************************************************************
release()117 void Kwave::FileContext::release()
118 {
119     Q_ASSERT(int(m_use_count) > 0);
120     if (m_use_count.deref() == false) {
121 	disconnect();
122 	deleteLater();
123     }
124 }
125 
126 //***************************************************************************
createMainWidget(const QSize & preferred_size)127 bool Kwave::FileContext::createMainWidget(const QSize &preferred_size)
128 {
129     Q_ASSERT(!m_main_widget);
130 
131     // create the main widget
132     m_main_widget = new(std::nothrow) Kwave::MainWidget(
133 	m_top_widget, *this, preferred_size
134     );
135     Q_ASSERT(m_main_widget);
136     if (!m_main_widget) return false;
137     if (!(m_main_widget->isOK())) {
138 	delete m_main_widget;
139         m_main_widget = Q_NULLPTR;
140 	return false;
141     }
142 
143     // connect the main widget
144     connect(&(m_signal_manager->playbackController()),
145             SIGNAL(sigSeekDone(sample_index_t)),
146             m_main_widget, SLOT(scrollTo(sample_index_t)));
147     connect(m_main_widget, SIGNAL(sigCommand(QString)),
148             this,          SLOT(executeCommand(QString)));
149     connect(m_main_widget, SIGNAL(sigZoomChanged(double)),
150             this,          SLOT(forwardZoomChanged(double)));
151     connect(m_main_widget, SIGNAL(sigVisibleRangeChanged(sample_index_t,
152 	    sample_index_t, sample_index_t)),
153 	    this, SLOT(visibleRangeChanged(sample_index_t,
154 	    sample_index_t, sample_index_t)) );
155 
156     return true;
157 }
158 
159 //***************************************************************************
init(Kwave::TopWidget * top_widget)160 bool Kwave::FileContext::init(Kwave::TopWidget *top_widget)
161 {
162     Kwave::FileContext::UsageGuard _keep(this);
163 
164     m_top_widget = top_widget;
165     Q_ASSERT(m_top_widget);
166     if (!m_top_widget) return false;
167 
168     m_signal_manager = new(std::nothrow)
169 	Kwave::SignalManager(m_top_widget);
170     Q_ASSERT(m_signal_manager);
171     if (!m_signal_manager) return false;
172 
173     m_plugin_manager = new(std::nothrow)
174 	Kwave::PluginManager(m_top_widget, *m_signal_manager);
175     Q_ASSERT(m_plugin_manager);
176     if (!m_plugin_manager) return false;
177 
178     // connect the signal manager
179     connect(m_signal_manager, SIGNAL(sigMetaDataChanged(Kwave::MetaDataList)),
180             this,             SLOT(metaDataChanged(Kwave::MetaDataList)));
181     connect(&(m_signal_manager->selection()),
182             SIGNAL(changed(sample_index_t,sample_index_t)),
183             this,
184             SLOT(selectionChanged(sample_index_t,sample_index_t)));
185     connect(m_signal_manager, SIGNAL(sigUndoRedoInfo(const QString&,
186                                                      const QString&)),
187             this, SLOT(setUndoRedoInfo(QString,QString)));
188     connect(m_signal_manager, SIGNAL(sigModified()),
189             this,             SLOT(modifiedChanged()));
190 
191     // connect the plugin manager
192     connect(m_plugin_manager, SIGNAL(sigCommand(QString)),
193             this,             SLOT(executeCommand(QString)));
194 
195     // connect the playback controller
196     connect(&(m_signal_manager->playbackController()),
197             SIGNAL(sigPlaybackPos(sample_index_t)),
198             this, SLOT(updatePlaybackPos(sample_index_t)));
199 
200     setParent(top_widget);
201 
202     Kwave::Splash::showMessage(i18n("Scanning plugins..."));
203     m_plugin_manager->searchPluginModules();
204 
205     // load the menu from file
206     QFile menufile(QStandardPaths::locate(
207 	QStandardPaths::GenericDataLocation,
208 	_("kwave/menus.config")
209     ));
210     menufile.open(QIODevice::ReadOnly);
211     QTextStream stream(&menufile);
212     if (stream.atEnd()) {
213 	qWarning("menu file not found in:");
214 	QStringList locations = QStandardPaths::standardLocations(
215 	    QStandardPaths::GenericDataLocation);
216 	foreach (const QString &location, locations)
217 	{
218 	    qWarning("    '%s'", DBG(location));
219 	}
220     }
221     Q_ASSERT(!stream.atEnd());
222     if (!stream.atEnd()) parseCommands(stream);
223     menufile.close();
224 
225     // now we are initialized, load all plugins
226     Kwave::Splash::showMessage(i18n("Loading plugins..."));
227     statusBarMessage(i18n("Loading plugins..."), 0);
228     if (!m_plugin_manager->loadAllPlugins()) {
229 	statusBarMessage(i18n("Failed"), 1000);
230 	QApplication::restoreOverrideCursor();
231 	Kwave::MessageBox::error(top_widget,
232 	    i18n("Kwave has not been properly installed. "\
233 	         "No plugins found!")
234 	);
235 	QApplication::setOverrideCursor(QCursor(Qt::WaitCursor));
236 	return false;
237     }
238     statusBarMessage(i18n("Ready"), 1000);
239 
240     return true;
241 }
242 
243 //***************************************************************************
setParent(Kwave::TopWidget * top_widget)244 void Kwave::FileContext::setParent(Kwave::TopWidget *top_widget)
245 {
246     if (m_top_widget) {
247 	Kwave::TopWidget *old = m_top_widget;
248 
249 	// disconnect all old signal/slot relationships
250 	disconnect(m_plugin_manager, SIGNAL(sigProgress(QString)),
251 	           old,              SLOT(showInSplashSreen(QString)));
252 	disconnect(old,  SIGNAL(sigFileContextSwitched(Kwave::FileContext*)),
253 	           this, SLOT(contextSwitched(Kwave::FileContext*)));
254 
255         if (m_signal_manager) m_signal_manager->setParentWidget(Q_NULLPTR);
256         if (m_plugin_manager) m_plugin_manager->setParentWidget(Q_NULLPTR);
257         if (m_main_widget)    m_main_widget->setParent(Q_NULLPTR);
258 
259 	m_active = false;
260     }
261 
262     // set the new top widget
263     m_top_widget = top_widget;
264 
265     if (m_top_widget) {
266 	QWidget *top = m_top_widget;
267 
268 	connect(top,  SIGNAL(sigFileContextSwitched(Kwave::FileContext*)),
269 	        this, SLOT(contextSwitched(Kwave::FileContext*)));
270 	connect(m_plugin_manager, SIGNAL(sigProgress(QString)),
271 	        top,              SLOT(showInSplashSreen(QString)));
272 
273 	if (m_signal_manager) m_signal_manager->setParentWidget(m_top_widget);
274 	if (m_plugin_manager) m_plugin_manager->setParentWidget(m_top_widget);
275 	if (m_main_widget)    m_main_widget->setParent(m_top_widget);
276     }
277 }
278 
279 //***************************************************************************
mainWidget() const280 QWidget *Kwave::FileContext::mainWidget() const
281 {
282     return static_cast<QWidget *>(m_main_widget);
283 }
284 
285 //***************************************************************************
signalManager() const286 Kwave::SignalManager *Kwave::FileContext::signalManager() const
287 {
288     Q_ASSERT(m_signal_manager);
289     return m_signal_manager;
290 }
291 
292 //***************************************************************************
pluginManager() const293 Kwave::PluginManager *Kwave::FileContext::pluginManager() const
294 {
295     return m_plugin_manager;
296 }
297 
298 //***************************************************************************
zoomable() const299 Kwave::Zoomable* Kwave::FileContext::zoomable() const
300 {
301     return m_main_widget;
302 }
303 
304 //***************************************************************************
delegateCommand(const char * plugin,Kwave::Parser & parser,unsigned int param_count)305 int Kwave::FileContext::delegateCommand(const char *plugin,
306                                         Kwave::Parser &parser,
307                                         unsigned int param_count)
308 {
309     if (!m_plugin_manager) return -1;
310     if (parser.count() != param_count) return -EINVAL;
311 
312     QStringList params;
313     params.append(parser.command());
314     params.append(parser.remainingParams());
315     int result = m_plugin_manager->setupPlugin(_(plugin), params);
316     if (result > 0) result = 0;
317     return result;
318 }
319 
320 //***************************************************************************
executeCommand(const QString & line)321 int Kwave::FileContext::executeCommand(const QString &line)
322 {
323     Kwave::FileContext::UsageGuard _keep(this);
324 
325     int result = 0;
326     bool use_recorder = true;
327     QString command = line;
328 
329 //     qDebug("Kwave::FileContext[%p]::executeCommand(%s)", this, DBG(command));
330 
331     Q_ASSERT(m_plugin_manager);
332     Q_ASSERT(m_top_widget);
333     if (!m_plugin_manager || !m_top_widget) return -ENOMEM;
334 
335     if (!command.length()) return 0; // empty line -> nothing to do
336     if (command.trimmed().startsWith(_("#")))
337 	return 0; // only a comment
338 
339     // special case: if the command contains ";" it is a list of
340     // commands -> macro !
341     Kwave::Parser parse_list(command);
342     if (parse_list.hasMultipleCommands()) {
343 	QStringList macro = parse_list.commandList();
344 	foreach (const QString &it, macro) {
345 	    result = executeCommand(_("nomacro:") + it);
346 	    Q_ASSERT(!result);
347 	    if (result) {
348 		qWarning("macro execution of '%s' failed: %d",
349 		         DBG(it), result);
350 		return result; // macro failed :-(
351 	    }
352 
353 	    // wait until the command has completed !
354 	    m_plugin_manager->sync();
355 	}
356 	return result;
357     }
358 
359     // check if the macro recorder has to be disabled for this command
360     if (command.startsWith(_("nomacro:"))) {
361 	use_recorder = false;
362 	command = command.mid(QString(_("nomacro:")).length());
363     }
364 
365     // expand variables
366     if (command.contains(_("${"))) {
367 	// current language
368 	if (command.contains(_("${LANG}"))) {
369 	    QLocale locale;
370 	    if (!m_main_widget.isNull()) locale = m_main_widget->locale();
371 	    QString lang = locale.name().split(_("-")).at(0);
372 	    command.replace(_("${LANG}"), lang);
373 	}
374     }
375 
376     // log all commands to the log file if enabled
377     Kwave::Logger::log(this, Kwave::Logger::Info, _("CMD: ") + line);
378 
379     // parse one single command
380     Kwave::Parser parser(command);
381     QString cmd = parser.command();
382 
383     // exclude menu commands from the recorder
384     if (cmd == _("menu")) use_recorder = false;
385 
386     // only record plugin:execute, not plugin without parameters
387     if (cmd == _("plugin")) use_recorder = false;
388 
389     // let through all commands that handle zoom/view or playback like fwd/rew
390     bool allow_always =
391         (cmd == _("playback")) ||
392 	cmd.startsWith(_("view:")) ||
393 	cmd.startsWith(_("playback:")) ||
394 	cmd.startsWith(_("select_track:")) ||
395 	(cmd == _("close")) ||
396 	(cmd == _("quit")) ||
397 	(cmd == _("window:screenshot")) ||
398 	(cmd == _("window:sendkey"))
399 	;
400 
401     // all others only if no plugin is currently running
402     if (!allow_always && m_plugin_manager->onePluginRunning())
403     {
404 	qWarning("FileContext::executeCommand('%s') - currently not possible, "
405 		 "a plugin is running :-(",
406 		 DBG(cmd));
407 	return -1;
408     }
409 
410     if (use_recorder) {
411 	// append the command to the macro recorder
412 	// @TODO macro recording...
413 	qDebug("# %s ", DBG(command));
414     }
415 
416     if ((result = m_top_widget->executeCommand(command)) != ENOSYS)
417 	return result;
418 
419     if (false) {
420     CASE_COMMAND("close")
421 	result = closeFile() ? 0 : 1;
422     CASE_COMMAND("delayed")
423 	if (parser.count() != 2)
424 	    return -EINVAL;
425 	unsigned int delay         = parser.firstParam().toUInt();
426 	QString      delayed_cmd   = parser.nextParam();
427 	enqueueCommand(delay, delayed_cmd);
428 	result = 0;
429     CASE_COMMAND("loadbatch")
430 	result = loadBatch(QUrl(parser.nextParam()));
431     CASE_COMMAND("plugin")
432 	QString name(parser.firstParam());
433 	QStringList params(parser.remainingParams());
434 	qDebug("FileContext::executeCommand(): loading plugin '%s'", DBG(name));
435 	qDebug("FileContext::executeCommand(): with %d parameter(s)",
436 		params.count());
437 	result = m_plugin_manager->executePlugin(
438             name, params.count() ? &params : Q_NULLPTR);
439     CASE_COMMAND("plugin:execute")
440 	QString name(parser.firstParam());
441 	QStringList params(parser.remainingParams());
442 	result = m_plugin_manager->executePlugin(name, &params);
443     CASE_COMMAND("plugin:setup")
444 	QString name(parser.firstParam());
445 	QStringList params(parser.remainingParams());
446 	result = m_plugin_manager->setupPlugin(name, params);
447 	if (result > 0) result = 0;
448     CASE_COMMAND("revert")
449 	result = revert();
450     CASE_COMMAND("save")
451 	result = saveFile();
452     CASE_COMMAND("saveas")
453 	result = saveFileAs(parser.nextParam(), false);
454     CASE_COMMAND("saveselect")
455 	result = saveFileAs(QString(), true);
456     CASE_COMMAND("sync")
457 	while (!m_delayed_command_queue.isEmpty()) {
458 	    qApp->processEvents(QEventLoop::ExcludeUserInputEvents);
459 	}
460 	result = 0;
461     CASE_COMMAND("window:click")
462 	result = delegateCommand("debug", parser, 3);
463     CASE_COMMAND("window:close")
464 	result = delegateCommand("debug", parser, 1);
465     CASE_COMMAND("window:mousemove")
466 	result = delegateCommand("debug", parser, 3);
467     CASE_COMMAND("window:resize")
468 	result = delegateCommand("debug", parser, 3);
469     CASE_COMMAND("window:sendkey")
470 	result = delegateCommand("debug", parser, 2);
471     CASE_COMMAND("window:screenshot")
472 	result = delegateCommand("debug", parser, 2);
473     } else {
474 	// pass the command to the layer below (main widget)
475 	Kwave::CommandHandler *layer_below = m_main_widget;
476 	result = (layer_below) ? layer_below->executeCommand(command) : -ENOSYS;
477     }
478 
479     return result;
480 }
481 
482 //***************************************************************************
forwardZoomChanged(double zoom)483 void Kwave::FileContext::forwardZoomChanged(double zoom)
484 {
485     m_last_zoom = zoom;
486     emit sigZoomChanged(this, zoom);
487 }
488 
489 //***************************************************************************
statusBarMessage(const QString & msg,unsigned int ms)490 void Kwave::FileContext::statusBarMessage(const QString &msg, unsigned int ms)
491 {
492     m_last_status_message_text = msg;
493     m_last_status_message_ms   = ms;
494     if (ms)
495 	m_last_status_message_timer.start();
496     else
497 	m_last_status_message_timer.invalidate();
498 
499     if (isActive())
500 	emit sigStatusBarMessage(msg, ms);
501 }
502 
503 //***************************************************************************
updatePlaybackPos(sample_index_t offset)504 void Kwave::FileContext::updatePlaybackPos(sample_index_t offset)
505 {
506     if (!m_plugin_manager) return;
507     if (!m_main_widget) return;
508 
509     bool playing = m_signal_manager->playbackController().running();
510     if (!playing) return;
511 
512     QString txt;
513     double rate = m_plugin_manager->signalRate();
514     if (rate > 0) {
515 	double ms = static_cast<double>(offset) * 1E3 / rate;
516 	txt = i18n("Playback: %1", Kwave::ms2string(ms));
517     } else {
518 	txt = i18n("Playback: %1 samples", Kwave::samples2string(offset));
519     }
520 
521     statusBarMessage(txt, 2000);
522 
523     // make sure that the current playback position is visible
524     m_last_playback_pos = offset;
525     Kwave::Zoomable *z = zoomable();
526     if (z) z->scrollTo(offset);
527 }
528 
529 //***************************************************************************
metaDataChanged(Kwave::MetaDataList meta_data)530 void Kwave::FileContext::metaDataChanged(Kwave::MetaDataList meta_data)
531 {
532     // find out the instance ID
533     if (m_instance_nr == -1) {
534 	// build a list of all currently open files/instances (including this)
535 	QList<Kwave::App::FileAndInstance> files = m_application.openFiles();
536 
537 	// filter out all instances of our file name
538 	QString our_name = signalName();
539 	QList<int> existing_instances;
540 	foreach (const Kwave::App::FileAndInstance &it, files) {
541 	    const QString &name = it.first;
542 	    int            inst = it.second;
543 	    if (name == our_name) existing_instances.append(inst);
544 	}
545 
546 	// remove our own entry
547 	if (existing_instances.contains(m_instance_nr))
548 	    existing_instances.removeOne(m_instance_nr);
549 
550 	// find an empty slot
551 	if (!existing_instances.isEmpty())
552 	    while (existing_instances.contains(m_instance_nr))
553 		m_instance_nr = (m_instance_nr != -1) ? (m_instance_nr + 1) : 2;
554     }
555 
556     if (isActive()) {
557 	// we are active -> emit the meta data immediately
558 	emit sigMetaDataChanged(meta_data);
559     } // else: we are inactive -> emit the meta data later, when activated
560 
561     // update the caption of the sub window
562     if (m_main_widget && (m_application.guiType() != Kwave::App::GUI_SDI))
563 	m_main_widget->setWindowTitle(windowCaption(true));
564 }
565 
566 //***************************************************************************
selectionChanged(sample_index_t offset,sample_index_t length)567 void Kwave::FileContext::selectionChanged(sample_index_t offset,
568                                           sample_index_t length)
569 {
570     if (isActive()) {
571 	// we are active -> emit the selection change immediately
572 	emit sigSelectionChanged(offset, length);
573     } // else: we are inactive -> not of interest / ignore
574 }
575 
576 //***************************************************************************
setUndoRedoInfo(const QString & undo,const QString & redo)577 void Kwave::FileContext::setUndoRedoInfo(const QString &undo,
578                                          const QString &redo)
579 {
580     m_last_undo = undo;
581     m_last_redo = redo;
582 
583     if (isActive()) {
584 	// we are active -> emit the undo/redo info immediately
585 	emit sigUndoRedoInfo(undo, redo);
586     } // else: we are inactive -> emit the undo/redo info later, when activated
587 }
588 
589 //***************************************************************************
visibleRangeChanged(sample_index_t offset,sample_index_t visible,sample_index_t total)590 void Kwave::FileContext::visibleRangeChanged(sample_index_t offset,
591                                              sample_index_t visible,
592                                              sample_index_t total)
593 {
594     if (isActive()) {
595 	// we are active -> emit the view info immediately
596 	emit sigVisibleRangeChanged(offset, visible, total);
597     } // else: we are inactive -> emit the view info later, when activated
598 }
599 
600 //***************************************************************************
modifiedChanged()601 void Kwave::FileContext::modifiedChanged()
602 {
603     if (isActive()) {
604 	// we are active -> emit the modified state immediately
605 	emit sigModified();
606     } // else: we are inactive -> emit the modified state later, when activated
607 
608     // update the caption of our main widget
609     if (m_main_widget && (m_application.guiType() != Kwave::App::GUI_SDI))
610 	m_main_widget->setWindowTitle(windowCaption(true));
611 }
612 
613 //***************************************************************************
contextSwitched(Kwave::FileContext * context)614 void Kwave::FileContext::contextSwitched(Kwave::FileContext *context)
615 {
616     Kwave::FileContext::UsageGuard _keep(this);
617 
618     if (context == this) {
619 	if (!m_active) {
620 	    m_active = true;
621 	    activated();
622 	}
623     } else
624 	m_active = false;
625 }
626 
627 //***************************************************************************
activated()628 void Kwave::FileContext::activated()
629 {
630     // let our plugin manager be the active one
631     if (m_plugin_manager) m_plugin_manager->setActive();
632 
633     // emit last playback position if playback is running
634     if (m_signal_manager && m_signal_manager->playbackController().running())
635 	updatePlaybackPos(m_last_playback_pos);
636 
637     // emit last zoom factor
638     forwardZoomChanged(m_last_zoom);
639 
640     // erase the status message of the previous context
641     emit sigStatusBarMessage(QString(), 0);
642 
643     // emit our last status bar message if it has not expired
644     if (m_last_status_message_timer.isValid()) {
645 	quint64 elapsed = m_last_status_message_timer.elapsed();
646 	if (elapsed < m_last_status_message_ms) {
647 	    unsigned int remaining =
648 		Kwave::toUint(m_last_status_message_ms - elapsed);
649 	    emit sigStatusBarMessage(m_last_status_message_text, remaining);
650 	} else
651 	    m_last_status_message_timer.invalidate();
652     } else if (m_last_status_message_ms == 0) {
653 	// static message without expiration
654 	if (m_last_status_message_text.length()) {
655 	    emit sigStatusBarMessage(m_last_status_message_text, 0);
656 	}
657     }
658 
659     // emit latest meta data
660     if (m_signal_manager)
661 	emit sigMetaDataChanged(m_signal_manager->metaData());
662 
663     // emit latest view range change
664     if (m_main_widget && m_signal_manager) {
665 	sample_index_t offset  = m_main_widget->visibleOffset();
666 	sample_index_t visible = m_main_widget->visibleSamples();
667 	sample_index_t total   = m_signal_manager->length();
668 	emit sigVisibleRangeChanged(offset, visible, total);
669     }
670 
671     // force update of the "modified" state
672     emit sigModified();
673 
674     // emit last undo/redo info
675     emit sigUndoRedoInfo(m_last_undo, m_last_redo);
676 
677 }
678 
679 //***************************************************************************
parseCommands(QTextStream & stream)680 int Kwave::FileContext::parseCommands(QTextStream &stream)
681 {
682     Kwave::FileContext::UsageGuard _keep(this);
683 
684     int result = 0;
685     QMap<QString, label_t> labels;
686 
687     // set hourglass cursor
688     QApplication::setOverrideCursor(QCursor(Qt::WaitCursor));
689 
690     QString label; // label, jump target of a "GOTO"
691     while (!stream.atEnd() && !result) {
692 	QString line = stream.readLine().simplified();
693 	if (line.startsWith(_("#"))) continue; // skip comments
694 	if (!line.length()) continue;          // skip empty lines
695 
696 	if (line.endsWith(QLatin1Char(':'))) {
697 	    // this line seems to be a "label"
698 	    QString name = line.left(line.length() - 1).simplified();
699 	    if (!labels.contains(name)) {
700 		// qDebug("new label '%s' at %llu", DBG(name), stream.pos());
701 		label_t label_pos;
702 		label_pos.pos  = stream.pos();
703 		label_pos.hits = 0;
704 		labels[name] = label_pos;
705 	    }
706 
707 	    // special handling for a label at the end of the file
708 	    if (label.length() && (label == name)) {
709 		// label found
710 		label = QString();
711 	    }
712 	    continue;
713 	}
714 
715 	Kwave::Parser parser(line);
716 
717 	// the "GOTO" command
718 	if ( !label.length() &&
719 	    (line.split(QLatin1Char(' ')).at(0) == _("GOTO")) ) {
720 	    label = line.split(QLatin1Char(' ')).at(1).simplified();
721 	}
722 
723 	// jump to a label, scan/seek mode
724 	if (label.length()) {
725 	    if (labels.contains(label)) {
726 		labels[label].hits++;
727 		qDebug(">>> GOTO '%s' @ offset %llu (pass #%d)", DBG(label),
728 		       labels[label].pos,
729 		       labels[label].hits
730 		    );
731 		stream.seek(labels[label].pos);
732 
733 		// reset the label to search for
734 		label = QString();
735 	    }
736 	    // else: maybe the label will follow somewhere below,
737 	    //       scan forward...
738 	    continue;
739 	}
740 
741 	// synchronize before the command
742 	if (m_plugin_manager)
743 	    m_plugin_manager->sync();
744 
745 	// the "msgbox" command (useful for debugging)
746 	if (false) {
747 	CASE_COMMAND("msgbox")
748 	    QApplication::restoreOverrideCursor();
749 	    result = (Kwave::MessageBox::questionYesNo(mainWidget(),
750 		parser.firstParam()) == KMessageBox::Yes) ? 0 : 1;
751 	    QApplication::setOverrideCursor(QCursor(Qt::WaitCursor));
752 	    continue;
753 	}
754 
755 	// prevent this command from being re-added to the macro recorder
756 	if (!line.startsWith(_("nomacro:"), Qt::CaseInsensitive))
757 	    line.prepend(_("nomacro:"));
758 
759 	// process the command in the current context
760 	// NOTE: this could theoretically also be a command that modifies
761 	//       or even deletes the current context!
762 	result = EAGAIN;
763 	Kwave::FileContext *current_ctx = (m_top_widget) ?
764 	    m_top_widget->currentContext() : Q_NULLPTR;
765 	if (current_ctx && (current_ctx != this))
766 	    result = m_top_widget->forwardCommand(line);
767 
768 	// If the call returned with EAGAIN, then the context in duty is
769 	// different from this instance but not yet completely set up.
770 	// In that case this context is still responsible for executing the
771 	// current command.
772 	if (result == EAGAIN)
773 	    result = executeCommand(line);
774 
775 	if (result)
776 	    qDebug(">>> '%s' - result=%d", DBG(line), result);
777 
778 	qApp->processEvents(QEventLoop::ExcludeUserInputEvents);
779 
780 	// synchronize after the command
781 	if (m_plugin_manager)
782 	    m_plugin_manager->sync();
783 
784 	// special handling of the "quit" command
785 	if (parser.command() == _("quit")) {
786 	    result = ECANCELED;
787 	    break;
788 	}
789     }
790 
791     if (label.length()) {
792 	// oops, if we get here then we have searched for a non-exising label
793 	qWarning("label '%s' not found", DBG(label));
794 	result = -ENOENT;
795     }
796 
797     // remove hourglass
798     QApplication::restoreOverrideCursor();
799 
800     return result;
801 }
802 
803 //***************************************************************************
enqueueCommand(unsigned int delay,const QString & command)804 void Kwave::FileContext::enqueueCommand(unsigned int delay,
805                                        const QString &command)
806 {
807     use();
808 
809     m_delayed_command_queue.append(
810 	QPair<unsigned int, QString>(delay, command)
811     );
812     if (!m_delayed_command_timer.isActive())
813 	m_delayed_command_timer.start(delay);
814 }
815 
816 //***************************************************************************
processDelayedCommand()817 void Kwave::FileContext::processDelayedCommand()
818 {
819     if (m_delayed_command_queue.isEmpty()) return;
820 
821     QPair<unsigned int, QString> current = m_delayed_command_queue.takeFirst();
822     executeCommand(current.second);
823     if (m_delayed_command_queue.isEmpty()) return;
824 
825     QPair<unsigned int, QString> next = m_delayed_command_queue.first();
826     m_delayed_command_timer.start(next.first);
827 
828     release();
829 }
830 
831 //***************************************************************************
isInUse() const832 bool Kwave::FileContext::isInUse() const
833 {
834     if (m_use_count >= 2) return true;
835     return (m_signal_manager && !m_signal_manager->isEmpty());
836 }
837 
838 //***************************************************************************
signalName() const839 QString Kwave::FileContext::signalName() const
840 {
841     return (m_signal_manager) ? m_signal_manager->signalName() : QString();
842 }
843 
844 //***************************************************************************
windowCaption(bool with_modified) const845 QString Kwave::FileContext::windowCaption(bool with_modified) const
846 {
847     QString name = signalName();
848 
849     // shortcut if no file loaded
850     if (!name.length()) return QString();
851 
852     // if not in SDI mode we have to take care of multiple instances on our
853     // own and append a " <n>" manually !
854     if (m_application.guiType() != Kwave::App::GUI_SDI)
855 	if (m_instance_nr != -1)
856 	    name = i18nc(
857 	        "for window title: "
858 	        "%1 = Name of the file, "
859 	        "%2 = Instance number when opened multiple times",
860 	        "%1 <%2>", name, m_instance_nr);
861 
862     if (with_modified) {
863 	bool modified = (m_signal_manager) ?
864 	    m_signal_manager->isModified() : false;
865 	if (modified)
866 	    return i18nc("%1 = Path to modified file", "* %1 (modified)", name);
867     }
868 	return name;
869 }
870 
871 //***************************************************************************
loadBatch(const QUrl & url)872 int Kwave::FileContext::loadBatch(const QUrl &url)
873 {
874     Kwave::FileContext::UsageGuard _keep(this);
875 
876     // open the URL, read-only mode is enough
877     QFile file(url.path());
878     if (!file.open(QIODevice::ReadOnly)) {
879 	qWarning("unable to open source in read-only mode!");
880 	return -EIO;
881     }
882 
883     // use a text stream for parsing the commands
884     QTextStream stream(&file);
885     int result = parseCommands(stream);
886     file.close();
887 
888     return result;
889 }
890 
891 //***************************************************************************
revert()892 int Kwave::FileContext::revert()
893 {
894     Kwave::FileContext::UsageGuard _keep(this);
895 
896     QUrl url(signalName());
897     if (!url.isValid() || !m_signal_manager) return -EINVAL;
898 
899     if (m_signal_manager->isModified()) {
900         int res =  Kwave::MessageBox::warningContinueCancel(m_top_widget,
901             i18n("This file has been modified, changes will be lost.\n"
902                  "Do you want to continue?"));
903         if (res == KMessageBox::Cancel) return ECANCELED;
904     }
905 
906     return m_signal_manager->loadFile(url);
907 }
908 
909 //***************************************************************************
saveFile()910 int Kwave::FileContext::saveFile()
911 {
912     if (!m_signal_manager) return -EINVAL;
913 
914     int res = 0;
915 
916     if (signalName() != NEW_FILENAME) {
917 	QUrl url;
918 	url = QUrl(signalName());
919 	res = m_signal_manager->save(url, false);
920 
921 	// if saving in current format is not possible (no encoder),
922 	// then try to "save/as" instead...
923 	if (res == -EINVAL) res = saveFileAs(QString(), false);
924     } else res = saveFileAs(QString(), false);
925 
926     return res;
927 }
928 
929 //***************************************************************************
saveFileAs(const QString & filename,bool selection)930 int Kwave::FileContext::saveFileAs(const QString &filename, bool selection)
931 {
932     if (!m_signal_manager) return -EINVAL;
933 
934     QString name = filename;
935     QUrl url;
936     int res = 0;
937 
938     if (name.length()) {
939 	/* name given -> take it */
940 	url = QUrl(name);
941     } else {
942 	/*
943 	 * no name given -> show the File/SaveAs dialog...
944 	 */
945 	QUrl current_url;
946 	current_url = QUrl(signalName());
947 
948         QString what  = Kwave::CodecManager::mimeTypeOf(current_url);
949 	Kwave::Encoder *encoder = Kwave::CodecManager::encoder(what);
950 	QString extension; // = "*.wav";
951 	if (!encoder) {
952 	    // no extension selected yet, use mime type from file info
953 	    QString mime_type = Kwave::FileInfo(
954 		m_signal_manager->metaData()).get(
955 		    Kwave::INF_MIMETYPE).toString();
956 	    encoder = Kwave::CodecManager::encoder(mime_type);
957 	    if (encoder) {
958 		QStringList extensions = encoder->extensions(mime_type);
959 		if (!extensions.isEmpty()) {
960 		    QString ext = extensions.first().split(_(" ")).first();
961 		    if (ext.length()) {
962 			extension = ext;
963 			QString new_filename = current_url.fileName();
964 			new_filename += extension.midRef(1); // remove the "*"
965 			current_url = current_url.adjusted(QUrl::RemoveFilename);
966 			current_url.setPath(current_url.path() + new_filename);
967 		    }
968 		}
969 	    }
970 	}
971 
972 	QString filter = Kwave::CodecManager::encodingFilter();
973 	QPointer<Kwave::FileDialog> dlg = new(std::nothrow)Kwave::FileDialog(
974 	    _("kfiledialog:///kwave_save_as"),
975 	    Kwave::FileDialog::SaveFile,
976 	    filter, m_top_widget, current_url, extension
977 	);
978 	if (!dlg) return 0;
979 	dlg->setWindowTitle(i18n("Save As"));
980 	if (dlg->exec() != QDialog::Accepted) {
981 	    delete dlg;
982 	    return -1;
983 	}
984 
985 	if (dlg) url = dlg->selectedUrl();
986 	if (url.isEmpty()) {
987 	    delete dlg;
988 	    return 0;
989 	}
990 
991 	QString new_name = url.path();
992 	QFileInfo path(new_name);
993 
994 	// add the correct extension if necessary
995 	if (!path.suffix().length()) {
996 	    QString ext = dlg->selectedExtension();
997 	    QStringList extensions = ext.split(_(" "));
998 	    ext = extensions.first();
999 	    new_name += ext.midRef(1);
1000 	    path = new_name;
1001 	    url.setPath(new_name);
1002 	}
1003 
1004 	delete dlg;
1005     }
1006 
1007     // check if the file exists and ask before overwriting it
1008     // if it is not the old filename
1009     name = url.path();
1010     if ((url.toDisplayString() != QUrl(signalName()).toDisplayString()) &&
1011 	QFileInfo::exists(name))
1012     {
1013 	if (Kwave::MessageBox::warningYesNo(m_top_widget,
1014 	    i18n("The file '%1' already exists.\n"
1015 	         "Do you really want to overwrite it?", name)) !=
1016 	         KMessageBox::Yes)
1017 	{
1018 	    return -1;
1019 	}
1020     }
1021 
1022     // maybe we now have a new mime type
1023     QString previous_mimetype_name =
1024 	Kwave::FileInfo(m_signal_manager->metaData()).get(
1025 	    Kwave::INF_MIMETYPE).toString();
1026 
1027     QString new_mimetype_name = Kwave::CodecManager::mimeTypeOf(url);
1028 
1029     if (new_mimetype_name != previous_mimetype_name) {
1030 	// saving to a different mime type
1031 	// now we have to do as if the mime type and file name
1032 	// has already been selected to satisfy the fileinfo
1033 	// plugin
1034 	qDebug("TopWidget::saveAs(%s) - [%s] (previous:'%s')",
1035 	    DBG(url.toDisplayString()), DBG(new_mimetype_name),
1036 	    DBG(previous_mimetype_name) );
1037 
1038 	// set the new mimetype
1039 	Kwave::FileInfo info(m_signal_manager->metaData());
1040 	info.set(Kwave::INF_MIMETYPE, new_mimetype_name);
1041 
1042 	// set the new filename
1043 	info.set(Kwave::INF_FILENAME, url.toDisplayString());
1044 	m_signal_manager->setFileInfo(info, false);
1045 
1046 	// now call the fileinfo plugin with the new filename and
1047 	// mimetype
1048 	res = (m_plugin_manager) ?
1049 	       m_plugin_manager->setupPlugin(_("fileinfo"), QStringList())
1050 	       : -1;
1051 
1052 	// restore the mime type and the filename
1053 	info = Kwave::FileInfo(m_signal_manager->metaData());
1054 	info.set(Kwave::INF_MIMETYPE, previous_mimetype_name);
1055 	info.set(Kwave::INF_FILENAME, url.toDisplayString());
1056 	m_signal_manager->setFileInfo(info, false);
1057     }
1058 
1059     // now we have a file name -> do the "save" operation
1060     if (!res) res = m_signal_manager->save(url, selection);
1061 
1062     // if saving was successful, add the file to the list of recent files
1063     if (!res) m_application.addRecentFile(signalName());
1064 
1065     return res;
1066 }
1067 
1068 //***************************************************************************
closeFile()1069 bool Kwave::FileContext::closeFile()
1070 {
1071     Kwave::FileContext::UsageGuard _keep(this);
1072 
1073     if (m_plugin_manager && !m_plugin_manager->canClose())
1074     {
1075 	qWarning("FileContext::closeFile() - currently not possible, "\
1076 	         "a plugin is running :-(");
1077 	return false;
1078     }
1079 
1080     if (m_signal_manager && m_signal_manager->isModified()) {
1081 	int res =  Kwave::MessageBox::warningYesNoCancel(m_top_widget,
1082 	    i18n("This file has been modified.\nDo you want to save it?"));
1083 	if (res == KMessageBox::Cancel) return false;
1084 	if (res == KMessageBox::Yes) {
1085 	    // user decided to save
1086 	    res = saveFile();
1087 	    qDebug("FileContext::closeFile()::saveFile, res=%d",res);
1088 	    if (res) return false;
1089 	}
1090     }
1091 
1092     // close all plugins that still might use the current signal
1093     if (m_plugin_manager) {
1094 	m_plugin_manager->stopAllPlugins();
1095 	m_plugin_manager->signalClosed();
1096     }
1097 
1098     if (m_signal_manager) m_signal_manager->close();
1099 
1100     switch (m_application.guiType()) {
1101 	case Kwave::App::GUI_MDI: /* FALLTHROUGH */
1102 	case Kwave::App::GUI_TAB:
1103 	    // close the main widget
1104 	    if (m_main_widget) delete m_main_widget;
1105 	    break;
1106 	case Kwave::App::GUI_SDI:
1107 	    break;
1108     }
1109 
1110     return true;
1111 }
1112 
1113 //***************************************************************************
1114 //***************************************************************************
1115