Commit 17ca46ba authored by David Jarvie's avatar David Jarvie
Browse files

Move file/desktop/config related functions into lib

parent 68aba379
......@@ -21,11 +21,13 @@ set(libkalarm_SRCS
lib/checkbox.cpp
lib/colourbutton.cpp
lib/combobox.cpp
lib/config.cpp
lib/desktop.cpp
lib/file.cpp
lib/filedialog.cpp
lib/groupbox.cpp
lib/kalocale.cpp
lib/label.cpp
lib/locale.cpp
lib/messagebox.cpp
lib/packedlayout.cpp
lib/pushbutton.cpp
......
......@@ -55,6 +55,8 @@ namespace
{
const QString KALARM_RESOURCE(QStringLiteral("akonadi_kalarm_resource"));
const QString KALARM_DIR_RESOURCE(QStringLiteral("akonadi_kalarm_dir_resource"));
QString conversionPrompt(const QString& calendarName, const QString& calendarVersion);
}
// Creates, or migrates from KResources, a single alarm calendar
......@@ -458,7 +460,7 @@ bool CalendarUpdater::update()
{
// The user hasn't previously said not to convert it
const QString versionString = KAlarmCal::getVersionString(compatAttr->version());
const QString msg = KAlarm::conversionPrompt(mCollection.name(), versionString, false);
const QString msg = conversionPrompt(mCollection.name(), versionString);
qCDebug(KALARM_LOG) << "CalendarUpdater::update: Version" << versionString;
if (KAMessageBox::warningYesNo(qobject_cast<QWidget*>(mParent), msg) != KMessageBox::Yes)
result = false; // the user chose not to update the calendar
......@@ -873,6 +875,24 @@ void CalendarCreator::finish(bool cleanup)
}
}
namespace
{
/******************************************************************************
* Return a prompt string to ask the user whether to convert the calendar to the
* current format.
*/
QString conversionPrompt(const QString& calendarName, const QString& calendarVersion)
{
const QString msg = xi18n("Some or all of the alarms in calendar <resource>%1</resource> are in an old <application>KAlarm</application> format, "
"and will be read-only unless you choose to update them to the current format.",
calendarName);
return xi18nc("@info", "<para>%1</para><para>"
"<warning>Do not update the calendar if it is also used with an older version of <application>KAlarm</application> "
"(e.g. on another computer). If you do so, the calendar may become unusable there.</warning></para>"
"<para>Do you wish to update the calendar?</para>", msg);
}
}
#include "calendarmigrator.moc"
// vim: et sw=4:
......@@ -37,6 +37,7 @@
#include "lib/autoqpointer.h"
#include "lib/buttongroup.h"
#include "lib/checkbox.h"
#include "lib/config.h"
#include "lib/lineedit.h"
#include "lib/messagebox.h"
#include "lib/packedlayout.h"
......@@ -861,7 +862,7 @@ void EditAlarmDlg::showEvent(QShowEvent* se)
if (mDeferGroup)
mDeferGroupHeight = mDeferGroup->height() + style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing);
QSize s;
if (KAlarm::readConfigWindowSize(mTemplate ? TEMPLATE_DIALOG_NAME : EDIT_DIALOG_NAME, s))
if (Config::readWindowSize(mTemplate ? TEMPLATE_DIALOG_NAME : EDIT_DIALOG_NAME, s))
{
bool defer = mDeferGroup && !mDeferGroup->isHidden();
s.setHeight(s.height() + (defer ? mDeferGroupHeight : 0));
......@@ -953,7 +954,7 @@ void EditAlarmDlg::resizeEvent(QResizeEvent* re)
{
QSize s = re->size();
s.setHeight(s.height() - (!mDeferGroup || mDeferGroup->isHidden() ? 0 : mDeferGroupHeight));
KAlarm::writeConfigWindowSize(mTemplate ? TEMPLATE_DIALOG_NAME : EDIT_DIALOG_NAME, s);
Config::writeWindowSize(mTemplate ? TEMPLATE_DIALOG_NAME : EDIT_DIALOG_NAME, s);
}
QDialog::resizeEvent(re);
}
......
......@@ -23,7 +23,6 @@
#include "emailidcombo.h"
#include "fontcolourbutton.h"
#include "functions.h"
#include "kalarmapp.h"
#include "kamail.h"
#include "latecancel.h"
......@@ -40,6 +39,7 @@
#include "lib/buttongroup.h"
#include "lib/checkbox.h"
#include "lib/colourbutton.h"
#include "lib/file.h"
#include "lib/lineedit.h"
#include "lib/messagebox.h"
#include "lib/radiobutton.h"
......@@ -83,8 +83,8 @@ class PickLogFileRadio : public PickFileRadio
: PickFileRadio(b, e, text, group, parent) { }
bool pickFile(QString& file) override // called when browse button is pressed to select a log file
{
return KAlarm::browseFile(file, i18nc("@title:window", "Choose Log File"), mDefaultDir, fileEdit()->text(), QString(),
false, parentWidget());
return File::browseFile(file, i18nc("@title:window", "Choose Log File"), mDefaultDir, fileEdit()->text(), QString(),
false, parentWidget());
}
private:
QString mDefaultDir; // default directory for log file browse button
......@@ -640,12 +640,12 @@ void EditDisplayAlarmDlg::slotPickFile()
{
static QString defaultDir; // default directory for file browse button
QString file;
if (KAlarm::browseFile(file, i18nc("@title:window", "Choose Text or Image File to Display"),
defaultDir, mFileMessageEdit->text(), QString(), true, this))
if (File::browseFile(file, i18nc("@title:window", "Choose Text or Image File to Display"),
defaultDir, mFileMessageEdit->text(), QString(), true, this))
{
if (!file.isEmpty())
{
mFileMessageEdit->setText(KAlarm::pathOrUrl(file));
mFileMessageEdit->setText(File::pathOrUrl(file));
contentsChanged();
}
}
......@@ -676,31 +676,31 @@ bool EditDisplayAlarmDlg::checkText(QString& result, bool showErrorMessage) cons
case tFILE:
{
QString alarmtext = mFileMessageEdit->text().trimmed();
QString fileName = mFileMessageEdit->text().trimmed();
QUrl url;
KAlarm::FileErr err = KAlarm::checkFileExists(alarmtext, url);
if (err == KAlarm::FileErr_None)
File::FileErr err = File::checkFileExists(fileName, url, MainWindow::mainMainWindow());
if (err == File::FileErr::None)
{
KFileItem fi(url);
switch (KAlarm::fileType(fi.currentMimeType()))
switch (File::fileType(fi.currentMimeType()))
{
case KAlarm::TextFormatted:
case KAlarm::TextPlain:
case KAlarm::TextApplication:
case KAlarm::Image:
case File::TextFormatted:
case File::TextPlain:
case File::TextApplication:
case File::Image:
break;
default:
err = KAlarm::FileErr_NotTextImage;
err = File::FileErr::NotTextImage;
break;
}
}
if (err != KAlarm::FileErr_None && showErrorMessage)
if (err != File::FileErr::None && showErrorMessage)
{
mFileMessageEdit->setFocus();
if (!KAlarm::showFileErrMessage(alarmtext, err, KAlarm::FileErr_BlankDisplay, const_cast<EditDisplayAlarmDlg*>(this)))
if (!File::showFileErrMessage(fileName, err, File::FileErr::BlankDisplay, const_cast<EditDisplayAlarmDlg*>(this)))
return false;
}
result = alarmtext;
result = fileName;
break;
}
case tCOMMAND:
......@@ -1436,8 +1436,8 @@ void EditEmailAlarmDlg::openAddressBook()
void EditEmailAlarmDlg::slotAddAttachment()
{
QString file;
if (KAlarm::browseFile(file, i18nc("@title:window", "Choose File to Attach"),
mAttachDefaultDir, QString(), QString(), true, this))
if (File::browseFile(file, i18nc("@title:window", "Choose File to Attach"),
mAttachDefaultDir, QString(), QString(), true, this))
{
if (!file.isEmpty())
{
......@@ -1769,13 +1769,13 @@ bool CommandEdit::isScript() const
*/
void CommandEdit::setText(const AlarmText& alarmText)
{
QString text = alarmText.displayText();
bool script = alarmText.isScript();
const QString text = alarmText.displayText();
const bool script = alarmText.isScript();
mTypeScript->setChecked(script);
if (script)
mScriptEdit->setPlainText(text);
else
mCommandEdit->setText(KAlarm::pathOrUrl(text));
mCommandEdit->setText(File::pathOrUrl(text));
}
/******************************************************************************
......
......@@ -62,15 +62,8 @@ using namespace KCalendarCore;
#include <KAuth>
#include <KStandardGuiItem>
#include <KStandardShortcut>
#include <KIO/StatJob>
#include <KJobWidgets>
#include <KFileItem>
#include <QAction>
#include <QDir>
#include <QRegExp>
#include <QFileDialog>
#include <QDesktopWidget>
#include <QDBusConnectionInterface>
#include <QDBusInterface>
#include <QTimer>
......@@ -80,7 +73,7 @@ using namespace KCalendarCore;
namespace
{
bool refreshAlarmsQueued = false;
bool refreshAlarmsQueued = false;
struct UpdateStatusData
{
......@@ -965,25 +958,6 @@ void editNewTemplate(const KAEvent* preset, QWidget* parent)
::editNewTemplate(EditAlarmDlg::Type(0), preset, parent);
}
/******************************************************************************
* Find the identity of the desktop we are running on.
*/
QString currentDesktopIdentityName()
{
return QProcessEnvironment::systemEnvironment().value(QStringLiteral("XDG_CURRENT_DESKTOP"));
}
/******************************************************************************
* Find the identity of the desktop we are running on.
*/
Desktop currentDesktopIdentity()
{
const QString desktop = currentDesktopIdentityName();
if (desktop == QLatin1String("KDE")) return Desktop::Kde;
if (desktop == QLatin1String("Unity")) return Desktop::Unity;
return Desktop::Other;
}
/******************************************************************************
* Check the config as to whether there is a wake-on-suspend alarm pending, and
* if so, delete it from the config if it has expired.
......@@ -1468,222 +1442,6 @@ void setDontShowErrors(const EventId& eventId, const QString& tag)
}
}
/******************************************************************************
* Read the size for the specified window from the config file, for the
* current screen resolution.
* Reply = true if size set in the config file, in which case 'result' is set
* = false if no size is set, in which case 'result' is unchanged.
*/
bool readConfigWindowSize(const char* window, QSize& result, int* splitterWidth)
{
KConfigGroup config(KSharedConfig::openConfig(), window);
const QWidget* desktop = QApplication::desktop();
const QSize s = QSize(config.readEntry(QStringLiteral("Width %1").arg(desktop->width()), (int)0),
config.readEntry(QStringLiteral("Height %1").arg(desktop->height()), (int)0));
if (s.isEmpty())
return false;
result = s;
if (splitterWidth)
*splitterWidth = config.readEntry(QStringLiteral("Splitter %1").arg(desktop->width()), -1);
return true;
}
/******************************************************************************
* Write the size for the specified window to the config file, for the
* current screen resolution.
*/
void writeConfigWindowSize(const char* window, const QSize& size, int splitterWidth)
{
KConfigGroup config(KSharedConfig::openConfig(), window);
const QWidget* desktop = QApplication::desktop();
config.writeEntry(QStringLiteral("Width %1").arg(desktop->width()), size.width());
config.writeEntry(QStringLiteral("Height %1").arg(desktop->height()), size.height());
if (splitterWidth >= 0)
config.writeEntry(QStringLiteral("Splitter %1").arg(desktop->width()), splitterWidth);
config.sync();
}
/******************************************************************************
* Check from its mime type whether a file appears to be a text or image file.
* If a text file, its type is distinguished.
* Reply = file type.
*/
FileType fileType(const QMimeType& mimetype)
{
if (mimetype.inherits(QStringLiteral("text/html")))
return TextFormatted;
if (mimetype.inherits(QStringLiteral("application/x-executable")))
return TextApplication;
if (mimetype.inherits(QStringLiteral("text/plain")))
return TextPlain;
if (mimetype.name().startsWith(QLatin1String("image/")))
return Image;
return Unknown;
}
/******************************************************************************
* Check that a file exists and is a plain readable file.
* Updates 'filename' and 'url' even if an error occurs, since 'filename' may
* be needed subsequently by showFileErrMessage().
* 'filename' is in user input format and may be a local file path or URL.
*/
FileErr checkFileExists(QString& filename, QUrl& url)
{
// Convert any relative file path to absolute
// (using home directory as the default).
// This also supports absolute paths and absolute urls.
FileErr err = FileErr_None;
url = QUrl::fromUserInput(filename, QDir::homePath(), QUrl::AssumeLocalFile);
if (filename.isEmpty())
{
url = QUrl();
err = FileErr_Blank; // blank file name
}
else if (!url.isValid())
err = FileErr_Nonexistent;
else if (url.isLocalFile())
{
// It's a local file
filename = url.toLocalFile();
QFileInfo info(filename);
if (info.isDir()) err = FileErr_Directory;
else if (!info.exists()) err = FileErr_Nonexistent;
else if (!info.isReadable()) err = FileErr_Unreadable;
}
else
{
filename = url.toDisplayString();
auto statJob = KIO::stat(url, KIO::StatJob::SourceSide, 2);
KJobWidgets::setWindow(statJob, MainWindow::mainMainWindow());
if (!statJob->exec())
err = FileErr_Nonexistent;
else
{
KFileItem fi(statJob->statResult(), url);
if (fi.isDir()) err = FileErr_Directory;
else if (!fi.isReadable()) err = FileErr_Unreadable;
}
}
return err;
}
/******************************************************************************
* Display an error message appropriate to 'err'.
* Display a Continue/Cancel error message if 'errmsgParent' non-null.
* Reply = true to continue, false to cancel.
*/
bool showFileErrMessage(const QString& filename, FileErr err, FileErr blankError, QWidget* errmsgParent)
{
if (err != FileErr_None)
{
// If file is a local file, remove "file://" from name
QString file = filename;
const QRegExp f(QStringLiteral("^file:/+"));
if (f.indexIn(file) >= 0)
file = file.mid(f.matchedLength() - 1);
QString errmsg;
switch (err)
{
case FileErr_Blank:
if (blankError == FileErr_BlankDisplay)
errmsg = i18nc("@info", "Please select a file to display");
else if (blankError == FileErr_BlankPlay)
errmsg = i18nc("@info", "Please select a file to play");
else
qFatal("showFileErrMessage: Program error");
KAMessageBox::sorry(errmsgParent, errmsg);
return false;
case FileErr_Directory:
KAMessageBox::sorry(errmsgParent, xi18nc("@info", "<filename>%1</filename> is a folder", file));
return false;
case FileErr_Nonexistent: errmsg = xi18nc("@info", "<filename>%1</filename> not found", file); break;
case FileErr_Unreadable: errmsg = xi18nc("@info", "<filename>%1</filename> is not readable", file); break;
case FileErr_NotTextImage: errmsg = xi18nc("@info", "<filename>%1</filename> appears not to be a text or image file", file); break;
default:
break;
}
if (KAMessageBox::warningContinueCancel(errmsgParent, errmsg)
== KMessageBox::Cancel)
return false;
}
return true;
}
/******************************************************************************
* If a url string is a local file, strip off the 'file:/' prefix.
*/
QString pathOrUrl(const QString& url)
{
static const QRegExp localfile(QStringLiteral("^file:/+"));
return (localfile.indexIn(url) >= 0) ? url.mid(localfile.matchedLength() - 1) : url;
}
/******************************************************************************
* Display a modal dialog to choose an existing file, initially highlighting
* any specified file.
* @param file Updated with the file which was selected, or empty if no file
* was selected.
* @param initialFile The file to initially highlight - must be a full path name or URL.
* @param defaultDir The directory to start in if @p initialFile is empty. If empty,
* the user's home directory will be used. Updated to the
* directory containing the selected file, if a file is chosen.
* @param existing true to return only existing files, false to allow new ones.
* Reply = true if 'file' value can be used.
* = false if the dialogue was deleted while visible (indicating that
* the parent widget was probably also deleted).
*/
bool browseFile(QString& file, const QString& caption, QString& defaultDir,
const QString& initialFile, const QString& filter, bool existing, QWidget* parent)
{
file.clear();
const QString initialDir = !initialFile.isEmpty() ? QString(initialFile).remove(QRegExp(QLatin1String("/[^/]*$")))
: !defaultDir.isEmpty() ? defaultDir
: QDir::homePath();
// Use AutoQPointer to guard against crash on application exit while
// the dialogue is still open. It prevents double deletion (both on
// deletion of parent, and on return from this function).
AutoQPointer<QFileDialog> fileDlg = new QFileDialog(parent, caption, initialDir, filter);
fileDlg->setAcceptMode(existing ? QFileDialog::AcceptOpen : QFileDialog::AcceptSave);
fileDlg->setFileMode(existing ? QFileDialog::ExistingFile : QFileDialog::AnyFile);
if (!initialFile.isEmpty())
fileDlg->selectFile(initialFile);
if (fileDlg->exec() != QDialog::Accepted)
return static_cast<bool>(fileDlg); // return false if dialog was deleted
const QList<QUrl> urls = fileDlg->selectedUrls();
if (urls.isEmpty())
return true;
const QUrl& url = urls[0];
defaultDir = url.isLocalFile() ? KIO::upUrl(url).toLocalFile() : url.adjusted(QUrl::RemoveFilename).path();
bool localOnly = true;
file = localOnly ? url.toDisplayString(QUrl::PreferLocalFile) : url.toDisplayString();
return true;
}
/******************************************************************************
* Return a prompt string to ask the user whether to convert the calendar to the
* current format.
* If 'whole' is true, the whole calendar needs to be converted; else only some
* alarms may need to be converted.
*
* Note: This method is defined here to avoid duplicating the i18n string
* definition between the Akonadi and KResources code.
*/
QString conversionPrompt(const QString& calendarName, const QString& calendarVersion, bool whole)
{
const QString msg = whole
? xi18n("Calendar <resource>%1</resource> is in an old format (<application>KAlarm</application> version %2), "
"and will be read-only unless you choose to update it to the current format.",
calendarName, calendarVersion)
: xi18n("Some or all of the alarms in calendar <resource>%1</resource> are in an old <application>KAlarm</application> format, "
"and will be read-only unless you choose to update them to the current format.",
calendarName);
return xi18nc("@info", "<para>%1</para><para>"
"<warning>Do not update the calendar if it is also used with an older version of <application>KAlarm</application> "
"(e.g. on another computer). If you do so, the calendar may become unusable there.</warning></para>"
"<para>Do you wish to update the calendar?</para>", msg);
}
#ifndef NDEBUG
/******************************************************************************
* Set up KAlarm test conditions based on environment variables.
......
......@@ -27,20 +27,16 @@
#include "eventid.h"
#include <KAlarmCal/KAEvent>
#include <KFile>
#include <QSize>
#include <QString>
#include <QVector>
#include <QMimeType>
#include <QUrl>
using namespace KAlarmCal;
namespace KCal { class Event; }
class QWidget;
class QAction;
class QAction;
class KToggleAction;
class Resource;
class MainWindow;
......@@ -49,8 +45,6 @@ class AlarmListModel;
namespace KAlarm
{
/** Return codes from fileType() */
enum FileType { Unknown, TextPlain, TextFormatted, TextApplication, Image };
/** Return codes from calendar update functions.
* The codes are ordered by severity, so...
* DO NOT CHANGE THE ORDER OF THESE VALUES!
......@@ -82,41 +76,9 @@ struct UpdateResult
void set(UpdateStatus s, const QString& m) { status = s; message = m; }
};
/** Desktop identity, obtained from XDG_CURRENT_DESKTOP. */
enum class Desktop
{
Kde, //!< KDE (KDE 4 and Plasma both identify as "KDE")
Unity, //!< Unity
Other
};
/** Display a main window with the specified event selected */
MainWindow* displayMainWindowSelected(const QString& eventId);
bool readConfigWindowSize(const char* window, QSize&, int* splitterWidth = nullptr);
void writeConfigWindowSize(const char* window, const QSize&, int splitterWidth = -1);
/** Check from its mime type whether a file appears to be a text or image file.
* If a text file, its type is distinguished.
*/
FileType fileType(const QMimeType& mimetype);
/** Check that a file exists and is a plain readable file, optionally a text/image file.
* Display a Continue/Cancel error message if 'errmsgParent' non-null.
*/
enum FileErr {
FileErr_None = 0,
FileErr_Blank, // generic blank error
FileErr_Nonexistent, FileErr_Directory, FileErr_Unreadable, FileErr_NotTextImage,
FileErr_BlankDisplay, // blank error to use for file to display
FileErr_BlankPlay // blank error to use for file to play
};
FileErr checkFileExists(QString& filename, QUrl&);
bool showFileErrMessage(const QString& filename, FileErr, FileErr blankError, QWidget* errmsgParent);
/** If a url string is a local file, strip off the 'file:/' prefix. */
QString pathOrUrl(const QString& url);
bool browseFile(QString& file, const QString& caption, QString& defaultDir,
const QString& initialFile = QString(),
const QString& filter = QString(), bool existing = false, QWidget* parent = nullptr);
bool editNewAlarm(const QString& templateName, QWidget* parent = nullptr);
void editNewAlarm(EditAlarmDlg::Type, QWidget* parent = nullptr);
void editNewAlarm(KAEvent::SubAction, QWidget* parent = nullptr, const AlarmText* = nullptr);
......@@ -173,22 +135,11 @@ UpdateResult enableEvents(QVector<KAEvent>&, bool enable, QWidget* msgPar
QVector<KAEvent> getSortedActiveEvents(QObject* parent, AlarmListModel** model = nullptr);
void purgeArchive(int purgeDays); // must only be called from KAlarmApp::processQueue()
void displayKOrgUpdateError(QWidget* parent, UpdateError, const UpdateResult& korgError, int nAlarms = 0);
Desktop currentDesktopIdentity();
QString currentDesktopIdentityName();
QStringList checkRtcWakeConfig(bool checkEventExists = false);
void deleteRtcWakeConfig();
void cancelRtcWake(QWidget* msgParent, const QString& eventId = QString());
bool setRtcWakeTime(unsigned triggerTime, QWidget* parent);
/** Return a prompt string to ask the user whether to convert the calendar to the
* current format.
* @param calendarName The calendar name
* @param calendarVersion The calendar version
* @param whole If true, the whole calendar needs to be converted; else
* only some alarms may need to be converted.
*/
QString conversionPrompt(const QString& calendarName, const QString& calendarVersion, bool whole);
#ifndef NDEBUG
void setTestModeConditions();
void setSimulatedSystemTime(const KADateTime&);
......
......@@ -37,6 +37,7 @@
#include "traywindow.h"
#include "resources/resources.h"
#include "resources/eventmodel.h"
#include "lib/desktop.h"
#include "lib/messagebox.h"
#include "lib/shellprocess.h"
#include "kalarm_debug.h"
......@@ -154,7 +155,7 @@ KAlarmApp::KAlarmApp(int& argc, char** argv)
mKOrganizerEnabled = !QStandardPaths::findExecutable(korg).isEmpty();
if (!mKOrganizerEnabled) { qCDebug(KALARM_LOG) << "KAlarmApp: KOrganizer options disabled (KOrganizer not found)"; }
// Check if the window manager can't handle keyboard focus transfer between windows
mWindowFocusBroken = (KAlarm::currentDesktopIdentity() == KAlarm::Desktop::Unity);
mWindowFocusBroken = (Desktop::currentIdentity() == Desktop::Unity);
if (mWindowFocusBroken) { qCDebug(KALARM_LOG) << "KAlarmApp: Window keyboard focus broken"; }
}
......
......@@ -6,7 +6,7 @@
http://www.kde.org/standards/kcfg/1.0/kcfg.xsd" >
<include>"lib/timeperiod.h"</include>
<include>"lib/kalocale.h"</include>
<include>"lib/locale.h"</include>
<include>QFontDatabase</include>
<include>KColorScheme</include>
<include>QFont</include>
......@@ -220,7 +220,7 @@
<entry name="Base_WorkDays" key="WorkDays" type="UInt">
<label context="@label">Working days</label>
<whatsthis context="@info:whatsthis">OR'ed bits indicating which days of the week are work days, 1 = Monday ... 64 = Sunday.</whatsthis>
<default code="true">KAlarm::defaultWorkDays()</default>
<default code="true">Locale::defaultWorkDays()</default>
<emit signal="base_WorkTimeChanged"/>
</entry>
<entry name="DisabledColour" type="Color">
......
/*
* config.cpp - config functions
* Program: kalarm
* Copyright © 2006-2019 David Jarvie <djarvie@kde.org>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,