3 Copyright (C) 2008 jlh (jlh at gmx dot ch)
5 This program is free software; you can redistribute it and/or modify it
6 under the terms of the GNU General Public License as published by the
7 Free Software Foundation; either version 2 of the License, version 3 of
8 the License, or (at your option) any later version.
10 This program is distributed in the hope that it will be useful, but
11 WITHOUT ANY WARRANTY; without even the implied warranty of
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13 General Public License for more details.
15 You should have received a copy of the GNU General Public License along
16 with this program; if not, write to the Free Software Foundation, Inc.,
17 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
19 The GNU General Public License version 2 is included with the source of
20 this program under the file name COPYING. You can also get a copy on
24 #include <QVBoxLayout>
25 #include <QHBoxLayout>
27 #include <QRadioButton>
29 #include <QPushButton>
34 #include <QTextStream>
35 #include <QtAlgorithms>
41 #include "preferences.h"
42 #include "smartwidgets.h"
46 Preferences preferences
;
48 QString
getOutputPath() {
49 QString path
= preferences
.get("output.path").toString();
50 path
.replace('~', QDir::homePath());
55 QString
escape(const QString
&s
) {
57 out
.replace('%', "%%");
58 out
.replace('&', "&&");
63 QString
getFileName(const QString
&skypeName
, const QString
&displayName
,
64 const QString
&mySkypeName
, const QString
&myDisplayName
, const QDateTime
×tamp
, const QString
&pattern
)
67 if (pattern
.isEmpty())
68 fileName
= preferences
.get("output.pattern").toString();
72 fileName
.replace("&s", escape(skypeName
));
73 fileName
.replace("&d", escape(displayName
));
74 fileName
.replace("&t", escape(mySkypeName
));
75 fileName
.replace("&e", escape(myDisplayName
));
76 fileName
.replace("&&", "&");
78 // TODO: uhm, does QT provide any time formatting the strftime() way?
79 char *buf
= new char[fileName
.size() + 1024];
80 time_t t
= timestamp
.toTime_t();
81 struct tm
*tm
= std::localtime(&t
);
82 std::strftime(buf
, fileName
.size() + 1024, fileName
.toUtf8().constData(), tm
);
86 return getOutputPath() + '/' + fileName
;
91 static QVBoxLayout
*makeVFrame(QVBoxLayout
*parentLayout
, const char *title
) {
92 QGroupBox
*box
= new QGroupBox(title
);
93 QVBoxLayout
*vbox
= new QVBoxLayout(box
);
94 parentLayout
->addWidget(box
);
98 static QHBoxLayout
*makeHFrame(QVBoxLayout
*parentLayout
, const char *title
) {
99 QGroupBox
*box
= new QGroupBox(title
);
100 QHBoxLayout
*hbox
= new QHBoxLayout(box
);
101 parentLayout
->addWidget(box
);
105 PreferencesDialog::PreferencesDialog() : perCallerDialog(NULL
) {
106 setWindowTitle(PROGRAM_NAME
" - Preferences");
112 SmartComboBox
*combo
;
114 SmartRadioButton
*radio
;
115 SmartCheckBox
*check
;
117 QVBoxLayout
*bigvbox
= new QVBoxLayout(this);
118 bigvbox
->setSizeConstraint(QLayout::SetFixedSize
);
120 // ---- general options ----
121 hbox
= makeHFrame(bigvbox
, "Automatic recording");
123 vbox
= new QVBoxLayout
;
124 Preference
&preference
= preferences
.get("autorecord.default");
125 radio
= new SmartRadioButton("Automatically &record calls", preference
, "yes");
126 vbox
->addWidget(radio
);
127 radio
= new SmartRadioButton("&Ask every time", preference
, "ask");
128 vbox
->addWidget(radio
);
129 radio
= new SmartRadioButton("Do ¬ automatically record calls", preference
, "no");
130 vbox
->addWidget(radio
);
132 hbox
->addLayout(vbox
);
134 button
= new QPushButton("&Per caller preferences");
135 connect(button
, SIGNAL(clicked(bool)), this, SLOT(editPerCallerPreferences()));
136 hbox
->addWidget(button
, 0, Qt::AlignBottom
);
138 // ---- output file name ----
139 vbox
= makeVFrame(bigvbox
, "Output file");
141 label
= new QLabel("&Save recorded calls here:");
142 edit
= new SmartLineEdit(preferences
.get("output.path"));
143 label
->setBuddy(edit
);
144 vbox
->addWidget(label
);
145 vbox
->addWidget(edit
);
147 label
= new QLabel("&File name:");
148 patternWidget
= new SmartEditableComboBox(preferences
.get("output.pattern"));
149 label
->setBuddy(patternWidget
);
150 patternWidget
->addItem("%Y-%m-%d %H:%M:%S Call with &s");
151 patternWidget
->addItem("Call with &s, %a %b %d %Y, %H:%M:%S");
152 patternWidget
->addItem("%Y, %B/Call with &s, %a %b %d %Y, %H:%M:%S");
153 patternWidget
->addItem("Calls with &s/Call with &s, %a %b %d %Y, %H:%M:%S");
154 patternWidget
->setupDone();
155 connect(patternWidget
, SIGNAL(editTextChanged(const QString
&)), this, SLOT(updatePatternToolTip(const QString
&)));
156 vbox
->addWidget(label
);
157 vbox
->addWidget(patternWidget
);
159 // ---- output file format ----
160 vbox
= makeVFrame(bigvbox
, "Output file &format");
162 hbox
= new QHBoxLayout
;
164 formatWidget
= combo
= new SmartComboBox(preferences
.get("output.format"));
165 combo
->addItem("WAV PCM", "wav");
166 combo
->addItem("MP3", "mp3");
167 combo
->addItem("Ogg Vorbis", "vorbis");
169 connect(combo
, SIGNAL(currentIndexChanged(int)), this, SLOT(updateFormatSettings()));
170 hbox
->addWidget(combo
);
172 combo
= new SmartComboBox(preferences
.get("output.channelmode"));
173 combo
->addItem("Mix to mono channel", "mono");
174 combo
->addItem("Stereo, local left, remote right", "stereo");
175 combo
->addItem("Stereo, local right, remote left", "oerets");
177 hbox
->addWidget(combo
);
179 vbox
->addLayout(hbox
);
180 hbox
= new QHBoxLayout
;
182 label
= new QLabel("MP3 &bitrate:");
183 combo
= new SmartComboBox(preferences
.get("output.format.mp3.bitrate"));
184 label
->setBuddy(combo
);
185 combo
->addItem("8 kbps", 8);
186 combo
->addItem("16 kbps", 16);
187 combo
->addItem("24 kbps", 24);
188 combo
->addItem("32 kbps (recommended for mono)", 32);
189 combo
->addItem("40 kbps", 40);
190 combo
->addItem("48 kbps", 48);
191 combo
->addItem("56 kbps", 56);
192 combo
->addItem("64 kbps (recommended for stereo)", 64);
193 combo
->addItem("80 kbps", 80);
194 combo
->addItem("96 kbps", 96);
195 combo
->addItem("112 kbps", 112);
196 combo
->addItem("128 kbps", 128);
197 combo
->addItem("144 kbps", 144);
198 combo
->addItem("160 kbps", 160);
200 mp3Settings
.append(label
);
201 mp3Settings
.append(combo
);
202 hbox
->addWidget(label
);
203 hbox
->addWidget(combo
);
205 vbox
->addLayout(hbox
);
206 hbox
= new QHBoxLayout
;
208 label
= new QLabel("Ogg Vorbis &quality:");
209 combo
= new SmartComboBox(preferences
.get("output.format.vorbis.quality"));
210 label
->setBuddy(combo
);
211 combo
->addItem("Quality -1", -1);
212 combo
->addItem("Quality 0", 0);
213 combo
->addItem("Quality 1", 1);
214 combo
->addItem("Quality 2", 2);
215 combo
->addItem("Quality 3 (recommended)", 3);
216 combo
->addItem("Quality 4", 4);
217 combo
->addItem("Quality 5", 5);
218 combo
->addItem("Quality 6", 6);
219 combo
->addItem("Quality 7", 7);
220 combo
->addItem("Quality 8", 8);
221 combo
->addItem("Quality 9", 9);
222 combo
->addItem("Quality 10", 10);
224 vorbisSettings
.append(label
);
225 vorbisSettings
.append(combo
);
226 hbox
->addWidget(label
);
227 hbox
->addWidget(combo
);
229 vbox
->addLayout(hbox
);
231 check
= new SmartCheckBox("Save call &information in files", preferences
.get("output.savetags"));
232 mp3Settings
.append(check
);
233 vorbisSettings
.append(check
);
234 vbox
->addWidget(check
);
238 hbox
= new QHBoxLayout
;
239 button
= new QPushButton("&Close");
240 button
->setDefault(true);
241 connect(button
, SIGNAL(clicked(bool)), this, SLOT(accept()));
243 hbox
->addWidget(button
);
244 bigvbox
->addLayout(hbox
);
246 updateFormatSettings();
247 updatePatternToolTip("");
250 void PreferencesDialog::updateFormatSettings() {
251 QVariant v
= formatWidget
->itemData(formatWidget
->currentIndex());
254 for (int i
= 0; i
< mp3Settings
.size(); i
++)
255 mp3Settings
.at(i
)->hide();
257 for (int i
= 0; i
< vorbisSettings
.size(); i
++)
258 vorbisSettings
.at(i
)->hide();
261 for (int i
= 0; i
< mp3Settings
.size(); i
++)
262 mp3Settings
.at(i
)->show();
264 for (int i
= 0; i
< vorbisSettings
.size(); i
++)
265 vorbisSettings
.at(i
)->show();
268 void PreferencesDialog::editPerCallerPreferences() {
269 perCallerDialog
= new PerCallerPreferencesDialog(this);
270 connect(perCallerDialog
, SIGNAL(finished(int)), this, SLOT(perCallerFinished()));
273 void PreferencesDialog::perCallerFinished() {
274 perCallerDialog
= NULL
;
277 void PreferencesDialog::hideEvent(QHideEvent
*event
) {
279 perCallerDialog
->accept();
281 QDialog::hideEvent(event
);
284 void PreferencesDialog::updatePatternToolTip(const QString
&pattern
) {
286 "This pattern specifies how the file name for the recorded call is constructed.\n"
287 "You can use the following directives:\n\n"
289 #define X(a, b) "\t" a "\t" b "\n"
290 X("&s" , "The remote skype name or phone number")
291 X("&d" , "The remote display name")
292 X("&t" , "Your skype name")
293 X("&e" , "Your display name")
294 X("&&" , "Literal & character")
296 X("%A / %a", "Full / abbreviated weekday name")
297 X("%B / %b", "Full / abbreviated month name")
298 X("%m" , "Month as a number (01 - 12)")
299 X("%d" , "Day of the month (01 - 31)")
300 X("%H" , "Hour as a 24-hour clock (00 - 23)")
301 X("%I" , "Hour as a 12-hour clock (01 - 12)")
303 X("%M" , "Minutes (00 - 59)")
304 X("%S" , "Seconds (00 - 59)")
305 X("%%" , "Literal % character")
307 "\t...and all other directives provided by strftime()\n\n"
309 "With the current choice, the file name might look like this:\n";
311 QString fn
= getFileName("echo123", "Skype Test Service", "myskype", "My Full Name",
312 QDateTime::currentDateTime(), pattern
);
314 if (fn
.contains(':'))
315 tip
+= "\n\nWARNING: Microsoft Windows does not allow colon characters (:) in file names.";
316 patternWidget
->setToolTip(tip
);
319 // per caller preferences editor
321 PerCallerPreferencesDialog::PerCallerPreferencesDialog(QWidget
*parent
) : QDialog(parent
) {
322 setWindowTitle("Per Caller Preferences");
323 setWindowModality(Qt::WindowModal
);
324 setAttribute(Qt::WA_DeleteOnClose
);
326 model
= new PerCallerModel(this);
328 QHBoxLayout
*bighbox
= new QHBoxLayout(this);
329 QVBoxLayout
*vbox
= new QVBoxLayout
;
331 listWidget
= new QListView
;
332 listWidget
->setModel(model
);
333 listWidget
->setSelectionMode(QAbstractItemView::ExtendedSelection
);
334 listWidget
->setEditTriggers(QAbstractItemView::SelectedClicked
| QAbstractItemView::DoubleClicked
);
335 connect(listWidget
->selectionModel(), SIGNAL(selectionChanged(const QItemSelection
&, const QItemSelection
&)), this, SLOT(selectionChanged()));
336 vbox
->addWidget(listWidget
);
338 QVBoxLayout
*frame
= makeVFrame(vbox
, "Preference for selected Skype names:");
339 radioYes
= new QRadioButton("Automatically &record calls");
340 radioAsk
= new QRadioButton("&Ask every time");
341 radioNo
= new QRadioButton("Do ¬ automatically record calls");
342 connect(radioYes
, SIGNAL(clicked(bool)), this, SLOT(radioChanged()));
343 connect(radioAsk
, SIGNAL(clicked(bool)), this, SLOT(radioChanged()));
344 connect(radioNo
, SIGNAL(clicked(bool)), this, SLOT(radioChanged()));
345 frame
->addWidget(radioYes
);
346 frame
->addWidget(radioAsk
);
347 frame
->addWidget(radioNo
);
349 bighbox
->addLayout(vbox
);
351 vbox
= new QVBoxLayout
;
353 QPushButton
*button
= new QPushButton("A&dd");
354 connect(button
, SIGNAL(clicked(bool)), this, SLOT(add()));
355 vbox
->addWidget(button
);
357 button
= new QPushButton("Re&move");
358 connect(button
, SIGNAL(clicked(bool)), this, SLOT(remove()));
359 vbox
->addWidget(button
);
363 button
= new QPushButton("&Close");
364 button
->setDefault(true);
365 connect(button
, SIGNAL(clicked(bool)), this, SLOT(accept()));
366 vbox
->addWidget(button
);
368 bighbox
->addLayout(vbox
);
374 QStringList list
= preferences
.get("autorecord.yes").toList();
375 for (int i
= 0; i
< list
.count(); i
++) {
376 QString sn
= list
.at(i
);
377 if (seen
.contains(sn
))
383 list
= preferences
.get("autorecord.ask").toList();
384 for (int i
= 0; i
< list
.count(); i
++) {
385 QString sn
= list
.at(i
);
386 if (seen
.contains(sn
))
392 list
= preferences
.get("autorecord.no").toList();
393 for (int i
= 0; i
< list
.count(); i
++) {
394 QString sn
= list
.at(i
);
395 if (seen
.contains(sn
))
402 connect(this, SIGNAL(finished(int)), this, SLOT(save()));
407 void PerCallerPreferencesDialog::add(const QString
&name
, int mode
, bool edit
) {
408 int i
= model
->rowCount();
411 QModelIndex idx
= model
->index(i
, 0);
412 model
->setData(idx
, name
, Qt::EditRole
);
413 model
->setData(idx
, mode
, Qt::UserRole
);
416 listWidget
->clearSelection();
417 listWidget
->setCurrentIndex(idx
);
418 listWidget
->edit(idx
);
422 void PerCallerPreferencesDialog::remove() {
423 QModelIndexList sel
= listWidget
->selectionModel()->selectedIndexes();
425 while (!sel
.isEmpty())
426 model
->removeRow(sel
.takeLast().row());
429 void PerCallerPreferencesDialog::selectionChanged() {
430 QModelIndexList sel
= listWidget
->selectionModel()->selectedIndexes();
431 bool notEmpty
= !sel
.isEmpty();
433 while (!sel
.isEmpty()) {
434 int m
= model
->data(sel
.takeLast(), Qt::UserRole
).toInt();
437 } else if (mode
!= m
) {
443 // Qt is a bit annoying about this: You can't deselect
444 // everything unless you disable auto-exclusive mode
445 radioYes
->setAutoExclusive(false);
446 radioAsk
->setAutoExclusive(false);
447 radioNo
->setAutoExclusive(false);
448 radioYes
->setChecked(false);
449 radioAsk
->setChecked(false);
450 radioNo
->setChecked(false);
451 radioYes
->setAutoExclusive(true);
452 radioAsk
->setAutoExclusive(true);
453 radioNo
->setAutoExclusive(true);
454 } else if (mode
== 0) {
455 radioNo
->setChecked(true);
456 } else if (mode
== 1) {
457 radioAsk
->setChecked(true);
458 } else if (mode
== 2) {
459 radioYes
->setChecked(true);
462 radioYes
->setEnabled(notEmpty
);
463 radioAsk
->setEnabled(notEmpty
);
464 radioNo
->setEnabled(notEmpty
);
467 void PerCallerPreferencesDialog::radioChanged() {
469 if (radioYes
->isChecked())
471 else if (radioNo
->isChecked())
474 QModelIndexList sel
= listWidget
->selectionModel()->selectedIndexes();
475 while (!sel
.isEmpty())
476 model
->setData(sel
.takeLast(), mode
, Qt::UserRole
);
479 void PerCallerPreferencesDialog::save() {
481 int n
= model
->rowCount();
482 QStringList yes
, ask
, no
;
483 for (int i
= 0; i
< n
; i
++) {
484 QModelIndex idx
= model
->index(i
, 0);
485 QString sn
= model
->data(idx
, Qt::EditRole
).toString();
488 int mode
= model
->data(idx
, Qt::UserRole
).toInt();
496 preferences
.get("autorecord.yes").set(yes
);
497 preferences
.get("autorecord.ask").set(ask
);
498 preferences
.get("autorecord.no").set(no
);
503 int PerCallerModel::rowCount(const QModelIndex
&) const {
504 return skypeNames
.count();
508 const char *PerCallerModel_data_table
[3] = {
509 "Don't record", "Ask", "Automatic"
513 QVariant
PerCallerModel::data(const QModelIndex
&index
, int role
) const {
514 if (!index
.isValid() || index
.row() >= skypeNames
.size())
516 if (role
== Qt::DisplayRole
) {
518 return skypeNames
.at(i
) + " - " + PerCallerModel_data_table
[modes
.at(i
)];
520 if (role
== Qt::EditRole
)
521 return skypeNames
.at(index
.row());
522 if (role
== Qt::UserRole
)
523 return modes
.at(index
.row());
527 bool PerCallerModel::setData(const QModelIndex
&index
, const QVariant
&value
, int role
) {
528 if (!index
.isValid() || index
.row() >= skypeNames
.size())
530 if (role
== Qt::EditRole
) {
531 skypeNames
[index
.row()] = value
.toString();
532 emit
dataChanged(index
, index
);
535 if (role
== Qt::UserRole
) {
536 modes
[index
.row()] = value
.toInt();
537 emit
dataChanged(index
, index
);
543 bool PerCallerModel::insertRows(int position
, int rows
, const QModelIndex
&) {
544 beginInsertRows(QModelIndex(), position
, position
+ rows
- 1);
545 for (int i
= 0; i
< rows
; i
++) {
546 skypeNames
.insert(position
, "");
547 modes
.insert(position
, 1);
553 bool PerCallerModel::removeRows(int position
, int rows
, const QModelIndex
&) {
554 beginRemoveRows(QModelIndex(), position
, position
+ rows
- 1);
555 for (int i
= 0; i
< rows
; i
++) {
556 skypeNames
.removeAt(position
);
557 modes
.removeAt(position
);
563 void PerCallerModel::sort(int, Qt::SortOrder
) {
564 typedef QPair
<QString
, int> Pair
;
565 typedef QList
<Pair
> List
;
567 for (int i
= 0; i
< skypeNames
.size(); i
++)
568 list
.append(Pair(skypeNames
.at(i
), modes
.at(i
)));
570 for (int i
= 0; i
< skypeNames
.size(); i
++) {
571 skypeNames
[i
] = list
.at(i
).first
;
572 modes
[i
] = list
.at(i
).second
;
577 Qt::ItemFlags
PerCallerModel::flags(const QModelIndex
&index
) const {
578 Qt::ItemFlags flags
= QAbstractListModel::flags(index
);
579 if (!index
.isValid() || index
.row() >= skypeNames
.size())
581 return flags
| Qt::ItemIsEditable
;
586 void Preference::listAdd(const QString
&value
) {
587 QStringList list
= toList();
588 if (!list
.contains(value
)) {
594 void Preference::listRemove(const QString
&value
) {
595 QStringList list
= toList();
596 if (list
.removeAll(value
))
600 bool Preference::listContains(const QString
&value
) {
601 QStringList list
= toList();
602 return list
.contains(value
);
607 BasePreferences::~BasePreferences() {
611 bool BasePreferences::load(const QString
&filename
) {
613 QFile
file(filename
);
614 if (!file
.open(QIODevice::ReadOnly
| QIODevice::Text
)) {
615 debug(QString("Can't open '%1' for loading preferences").arg(filename
));
619 while (!file
.atEnd()) {
620 qint64 len
= file
.readLine(buf
, sizeof(buf
));
624 line
= line
.trimmed();
625 if (line
.at(0) == '#')
627 int index
= line
.indexOf('=');
631 get(line
.left(index
).trimmed()).set(line
.mid(index
+ 1).trimmed());
633 debug(QString("Loaded %1 preferences from '%2'").arg(prefs
.size()).arg(filename
));
638 bool comparePreferencePointers(const Preference
*p1
, const Preference
*p2
)
644 bool BasePreferences::save(const QString
&filename
) {
645 qSort(prefs
.begin(), prefs
.end(), comparePreferencePointers
);
646 QFile
file(filename
);
647 if (!file
.open(QIODevice::WriteOnly
| QIODevice::Text
)) {
648 debug(QString("Can't open '%1' for saving preferences").arg(filename
));
651 QTextStream
out(&file
);
652 for (int i
= 0; i
< prefs
.size(); i
++) {
653 const Preference
&p
= *prefs
.at(i
);
654 out
<< p
.name() << " = " << p
.toString() << "\n";
656 debug(QString("Saved %1 preferences to '%2'").arg(prefs
.size()).arg(filename
));
660 Preference
&BasePreferences::get(const QString
&name
) {
661 for (int i
= 0; i
< prefs
.size(); i
++)
662 if (prefs
.at(i
)->name() == name
)
664 prefs
.append(new Preference(name
));
665 return *prefs
.last();
668 void BasePreferences::clear() {
669 for (int i
= 0; i
< prefs
.size(); i
++)
676 void Preferences::setPerCallerPreference(const QString
&sn
, int mode
) {
677 // this would interfer with the per caller dialog
678 recorderInstance
->closePreferences();
680 Preference
&pYes
= get("autorecord.yes");
681 Preference
&pAsk
= get("autorecord.ask");
682 Preference
&pNo
= get("autorecord.no");