Fix bug about preferences messing up
[skype-call-recorder.git] / preferences.cpp
blob12bf739a480738137cc0c0775a72c355152e46d1
1 /*
2 Skype Call Recorder
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
21 http://www.fsf.org/
24 #include <QVBoxLayout>
25 #include <QHBoxLayout>
26 #include <QGroupBox>
27 #include <QRadioButton>
28 #include <QLabel>
29 #include <QPushButton>
30 #include <QListView>
31 #include <QPair>
32 #include <QFile>
33 #include <QSet>
34 #include <QTextStream>
35 #include <QtAlgorithms>
36 #include <QDir>
37 #include <QDateTime>
38 #include <QList>
39 #include <ctime>
41 #include "preferences.h"
42 #include "smartwidgets.h"
43 #include "common.h"
44 #include "recorder.h"
46 Preferences preferences;
48 QString getOutputPath() {
49 QString path = preferences.get("output.path").toString();
50 path.replace('~', QDir::homePath());
51 return path;
54 namespace {
55 QString escape(const QString &s) {
56 QString out = s;
57 out.replace('%', "%%");
58 out.replace('&', "&&");
59 return out;
63 QString getFileName(const QString &skypeName, const QString &displayName,
64 const QString &mySkypeName, const QString &myDisplayName, const QDateTime &timestamp, const QString &pattern)
66 QString fileName;
67 if (pattern.isEmpty())
68 fileName = preferences.get("output.pattern").toString();
69 else
70 fileName = pattern;
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);
83 fileName = buf;
84 delete[] buf;
86 return getOutputPath() + '/' + fileName;
89 // preferences dialog
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);
95 return vbox;
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);
102 return hbox;
105 PreferencesDialog::PreferencesDialog() : perCallerDialog(NULL) {
106 setWindowTitle(PROGRAM_NAME " - Preferences");
108 QVBoxLayout *vbox;
109 QHBoxLayout *hbox;
110 QLabel *label;
111 QPushButton *button;
112 SmartComboBox *combo;
113 SmartLineEdit *edit;
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 &not 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");
168 combo->setupDone();
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");
176 combo->setupDone();
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);
199 combo->setupDone();
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);
223 combo->setupDone();
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);
236 // ---- buttons ----
238 hbox = new QHBoxLayout;
239 button = new QPushButton("&Close");
240 button->setDefault(true);
241 connect(button, SIGNAL(clicked(bool)), this, SLOT(accept()));
242 hbox->addStretch();
243 hbox->addWidget(button);
244 bigvbox->addLayout(hbox);
246 updateFormatSettings();
247 updatePatternToolTip("");
250 void PreferencesDialog::updateFormatSettings() {
251 QVariant v = formatWidget->itemData(formatWidget->currentIndex());
252 // hide
253 if (v != "mp3")
254 for (int i = 0; i < mp3Settings.size(); i++)
255 mp3Settings.at(i)->hide();
256 if (v != "vorbis")
257 for (int i = 0; i < vorbisSettings.size(); i++)
258 vorbisSettings.at(i)->hide();
259 // show
260 if (v == "mp3")
261 for (int i = 0; i < mp3Settings.size(); i++)
262 mp3Settings.at(i)->show();
263 if (v == "vorbis")
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) {
278 if (perCallerDialog)
279 perCallerDialog->accept();
281 QDialog::hideEvent(event);
284 void PreferencesDialog::updatePatternToolTip(const QString &pattern) {
285 QString tip =
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")
295 X("%Y" , "Year")
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)")
302 X("%p" , "AM or PM")
303 X("%M" , "Minutes (00 - 59)")
304 X("%S" , "Seconds (00 - 59)")
305 X("%%" , "Literal % character")
306 #undef X
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);
313 tip += fn;
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 &not 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);
361 vbox->addStretch();
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);
370 // fill in data
372 QSet<QString> seen;
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))
378 continue;
379 seen.insert(sn);
380 add(sn, 2, false);
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))
387 continue;
388 seen.insert(sn);
389 add(sn, 1, false);
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))
396 continue;
397 seen.insert(sn);
398 add(sn, 0, false);
401 model->sort();
402 connect(this, SIGNAL(finished(int)), this, SLOT(save()));
403 selectionChanged();
404 show();
407 void PerCallerPreferencesDialog::add(const QString &name, int mode, bool edit) {
408 int i = model->rowCount();
409 model->insertRow(i);
411 QModelIndex idx = model->index(i, 0);
412 model->setData(idx, name, Qt::EditRole);
413 model->setData(idx, mode, Qt::UserRole);
415 if (edit) {
416 listWidget->clearSelection();
417 listWidget->setCurrentIndex(idx);
418 listWidget->edit(idx);
422 void PerCallerPreferencesDialog::remove() {
423 QModelIndexList sel = listWidget->selectionModel()->selectedIndexes();
424 qSort(sel);
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();
432 int mode = -1;
433 while (!sel.isEmpty()) {
434 int m = model->data(sel.takeLast(), Qt::UserRole).toInt();
435 if (mode == -1) {
436 mode = m;
437 } else if (mode != m) {
438 mode = -1;
439 break;
442 if (mode == -1) {
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() {
468 int mode = 1;
469 if (radioYes->isChecked())
470 mode = 2;
471 else if (radioNo->isChecked())
472 mode = 0;
474 QModelIndexList sel = listWidget->selectionModel()->selectedIndexes();
475 while (!sel.isEmpty())
476 model->setData(sel.takeLast(), mode, Qt::UserRole);
479 void PerCallerPreferencesDialog::save() {
480 model->sort();
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();
486 if (sn.isEmpty())
487 continue;
488 int mode = model->data(idx, Qt::UserRole).toInt();
489 if (mode == 0)
490 no.append(sn);
491 else if (mode == 1)
492 ask.append(sn);
493 else if (mode == 2)
494 yes.append(sn);
496 preferences.get("autorecord.yes").set(yes);
497 preferences.get("autorecord.ask").set(ask);
498 preferences.get("autorecord.no").set(no);
501 // per caller model
503 int PerCallerModel::rowCount(const QModelIndex &) const {
504 return skypeNames.count();
507 namespace {
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())
515 return QVariant();
516 if (role == Qt::DisplayRole) {
517 int i = index.row();
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());
524 return QVariant();
527 bool PerCallerModel::setData(const QModelIndex &index, const QVariant &value, int role) {
528 if (!index.isValid() || index.row() >= skypeNames.size())
529 return false;
530 if (role == Qt::EditRole) {
531 skypeNames[index.row()] = value.toString();
532 emit dataChanged(index, index);
533 return true;
535 if (role == Qt::UserRole) {
536 modes[index.row()] = value.toInt();
537 emit dataChanged(index, index);
538 return true;
540 return false;
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);
549 endInsertRows();
550 return true;
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);
559 endRemoveRows();
560 return true;
563 void PerCallerModel::sort(int, Qt::SortOrder) {
564 typedef QPair<QString, int> Pair;
565 typedef QList<Pair> List;
566 List list;
567 for (int i = 0; i < skypeNames.size(); i++)
568 list.append(Pair(skypeNames.at(i), modes.at(i)));
569 qSort(list);
570 for (int i = 0; i < skypeNames.size(); i++) {
571 skypeNames[i] = list.at(i).first;
572 modes[i] = list.at(i).second;
574 reset();
577 Qt::ItemFlags PerCallerModel::flags(const QModelIndex &index) const {
578 Qt::ItemFlags flags = QAbstractListModel::flags(index);
579 if (!index.isValid() || index.row() >= skypeNames.size())
580 return flags;
581 return flags | Qt::ItemIsEditable;
584 // preference
586 void Preference::listAdd(const QString &value) {
587 QStringList list = toList();
588 if (!list.contains(value)) {
589 list.append(value);
590 set(list);
594 void Preference::listRemove(const QString &value) {
595 QStringList list = toList();
596 if (list.removeAll(value))
597 set(list);
600 bool Preference::listContains(const QString &value) {
601 QStringList list = toList();
602 return list.contains(value);
605 // base preferences
607 BasePreferences::~BasePreferences() {
608 clear();
611 bool BasePreferences::load(const QString &filename) {
612 clear();
613 QFile file(filename);
614 if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
615 debug(QString("Can't open '%1' for loading preferences").arg(filename));
616 return false;
618 char buf[65536];
619 while (!file.atEnd()) {
620 qint64 len = file.readLine(buf, sizeof(buf));
621 if (len == -1)
622 break;
623 QString line(buf);
624 line = line.trimmed();
625 if (line.at(0) == '#')
626 continue;
627 int index = line.indexOf('=');
628 if (index < 0)
629 // TODO warn
630 continue;
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));
634 return true;
637 namespace {
638 bool comparePreferencePointers(const Preference *p1, const Preference *p2)
640 return *p1 < *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));
649 return false;
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));
657 return true;
660 Preference &BasePreferences::get(const QString &name) {
661 for (int i = 0; i < prefs.size(); i++)
662 if (prefs.at(i)->name() == name)
663 return *prefs[i];
664 prefs.append(new Preference(name));
665 return *prefs.last();
668 void BasePreferences::clear() {
669 for (int i = 0; i < prefs.size(); i++)
670 delete prefs.at(i);
671 prefs.clear();
674 // preferences
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");
684 pYes.listRemove(sn);
685 pAsk.listRemove(sn);
686 pNo.listRemove(sn);
688 if (mode == 2)
689 pYes.listAdd(sn);
690 else if (mode == 1)
691 pAsk.listAdd(sn);
692 else if (mode == 0)
693 pNo.listAdd(sn);