Fixed header includes and forward declarations
[skype-call-recorder.git] / preferences.cpp
blob282e5fa76b1286fd5dfde033971f8e972c8e09b3
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 <ctime>
40 #include "preferences.h"
41 #include "smartwidgets.h"
42 #include "common.h"
43 #include "recorder.h"
45 Preferences preferences;
47 QString getOutputPath() {
48 QString path = preferences.get("output.path").toString();
49 path.replace('~', QDir::homePath());
50 return path;
53 namespace {
54 QString escape(const QString &s) {
55 QString out = s;
56 out.replace('%', "%%");
57 out.replace('&', "&&");
58 return out;
62 QString getFileName(const QString &skypeName, const QString &displayName,
63 const QString &mySkypeName, const QString &myDisplayName, const QDateTime &timestamp, const QString &pattern)
65 QString fileName;
66 if (pattern.isEmpty())
67 fileName = preferences.get("output.pattern").toString();
68 else
69 fileName = pattern;
71 fileName.replace("&s", escape(skypeName));
72 fileName.replace("&d", escape(displayName));
73 fileName.replace("&t", escape(mySkypeName));
74 fileName.replace("&e", escape(myDisplayName));
75 fileName.replace("&&", "&");
77 // TODO: uhm, does QT provide any time formatting the strftime() way?
78 char *buf = new char[fileName.size() + 1024];
79 time_t t = timestamp.toTime_t();
80 struct tm *tm = std::localtime(&t);
81 std::strftime(buf, fileName.size() + 1024, fileName.toUtf8().constData(), tm);
82 fileName = buf;
83 delete[] buf;
85 return getOutputPath() + '/' + fileName;
88 // preferences dialog
90 static QVBoxLayout *makeVFrame(QVBoxLayout *parentLayout, const char *title) {
91 QGroupBox *box = new QGroupBox(title);
92 QVBoxLayout *vbox = new QVBoxLayout(box);
93 parentLayout->addWidget(box);
94 return vbox;
97 static QHBoxLayout *makeHFrame(QVBoxLayout *parentLayout, const char *title) {
98 QGroupBox *box = new QGroupBox(title);
99 QHBoxLayout *hbox = new QHBoxLayout(box);
100 parentLayout->addWidget(box);
101 return hbox;
104 PreferencesDialog::PreferencesDialog() : perCallerDialog(NULL) {
105 setWindowTitle(PROGRAM_NAME " - Preferences");
107 QVBoxLayout *vbox;
108 QHBoxLayout *hbox;
109 QLabel *label;
110 QPushButton *button;
111 SmartComboBox *combo;
112 SmartLineEdit *edit;
113 SmartRadioButton *radio;
114 SmartCheckBox *check;
116 QVBoxLayout *bigvbox = new QVBoxLayout(this);
117 bigvbox->setSizeConstraint(QLayout::SetFixedSize);
119 // ---- general options ----
120 hbox = makeHFrame(bigvbox, "Automatic recording");
122 vbox = new QVBoxLayout;
123 Preference &preference = preferences.get("autorecord.default");
124 radio = new SmartRadioButton("Automatically &record calls", preference, "yes");
125 vbox->addWidget(radio);
126 radio = new SmartRadioButton("&Ask every time", preference, "ask");
127 vbox->addWidget(radio);
128 radio = new SmartRadioButton("Do &not automatically record calls", preference, "no");
129 vbox->addWidget(radio);
131 hbox->addLayout(vbox);
133 button = new QPushButton("&Per caller preferences");
134 connect(button, SIGNAL(clicked(bool)), this, SLOT(editPerCallerPreferences()));
135 hbox->addWidget(button, 0, Qt::AlignBottom);
137 // ---- output file name ----
138 vbox = makeVFrame(bigvbox, "Output file");
140 label = new QLabel("&Save recorded calls here:");
141 edit = new SmartLineEdit(preferences.get("output.path"));
142 label->setBuddy(edit);
143 vbox->addWidget(label);
144 vbox->addWidget(edit);
146 label = new QLabel("&File name:");
147 patternWidget = new SmartEditableComboBox(preferences.get("output.pattern"));
148 label->setBuddy(patternWidget);
149 patternWidget->addItem("%Y-%m-%d %H:%M:%S Call with &s");
150 patternWidget->addItem("Call with &s, %a %b %d %Y, %H:%M:%S");
151 patternWidget->addItem("%Y, %B/Call with &s, %a %b %d %Y, %H:%M:%S");
152 patternWidget->addItem("Calls with &s/Call with &s, %a %b %d %Y, %H:%M:%S");
153 patternWidget->setupDone();
154 connect(patternWidget, SIGNAL(editTextChanged(const QString &)), this, SLOT(updatePatternToolTip(const QString &)));
155 vbox->addWidget(label);
156 vbox->addWidget(patternWidget);
158 // ---- output file format ----
159 vbox = makeVFrame(bigvbox, "Output file &format");
161 hbox = new QHBoxLayout;
163 formatWidget = combo = new SmartComboBox(preferences.get("output.format"));
164 combo->addItem("WAV PCM", "wav");
165 combo->addItem("MP3", "mp3");
166 combo->addItem("Ogg Vorbis", "vorbis");
167 combo->setupDone();
168 connect(combo, SIGNAL(currentIndexChanged(int)), this, SLOT(updateFormatSettings()));
169 hbox->addWidget(combo);
171 combo = new SmartComboBox(preferences.get("output.channelmode"));
172 combo->addItem("Mix to mono channel", "mono");
173 combo->addItem("Stereo, local left, remote right", "stereo");
174 combo->addItem("Stereo, local right, remote left", "oerets");
175 combo->setupDone();
176 hbox->addWidget(combo);
178 vbox->addLayout(hbox);
179 hbox = new QHBoxLayout;
181 label = new QLabel("MP3 &bitrate:");
182 combo = new SmartComboBox(preferences.get("output.format.mp3.bitrate"));
183 label->setBuddy(combo);
184 combo->addItem("8 kbps", 8);
185 combo->addItem("16 kbps", 16);
186 combo->addItem("24 kbps", 24);
187 combo->addItem("32 kbps (recommended for mono)", 32);
188 combo->addItem("40 kbps", 40);
189 combo->addItem("48 kbps", 48);
190 combo->addItem("56 kbps", 56);
191 combo->addItem("64 kbps (recommended for stereo)", 64);
192 combo->addItem("80 kbps", 80);
193 combo->addItem("96 kbps", 96);
194 combo->addItem("112 kbps", 112);
195 combo->addItem("128 kbps", 128);
196 combo->addItem("144 kbps", 144);
197 combo->addItem("160 kbps", 160);
198 combo->setupDone();
199 mp3Settings.append(label);
200 mp3Settings.append(combo);
201 hbox->addWidget(label);
202 hbox->addWidget(combo);
204 vbox->addLayout(hbox);
205 hbox = new QHBoxLayout;
207 label = new QLabel("Ogg Vorbis &quality:");
208 combo = new SmartComboBox(preferences.get("output.format.vorbis.quality"));
209 label->setBuddy(combo);
210 combo->addItem("Quality -1", -1);
211 combo->addItem("Quality 0", 0);
212 combo->addItem("Quality 1", 1);
213 combo->addItem("Quality 2", 2);
214 combo->addItem("Quality 3 (recommended)", 3);
215 combo->addItem("Quality 4", 4);
216 combo->addItem("Quality 5", 5);
217 combo->addItem("Quality 6", 6);
218 combo->addItem("Quality 7", 7);
219 combo->addItem("Quality 8", 8);
220 combo->addItem("Quality 9", 9);
221 combo->addItem("Quality 10", 10);
222 combo->setupDone();
223 vorbisSettings.append(label);
224 vorbisSettings.append(combo);
225 hbox->addWidget(label);
226 hbox->addWidget(combo);
228 vbox->addLayout(hbox);
230 check = new SmartCheckBox("Save call &information in files", preferences.get("output.savetags"));
231 mp3Settings.append(check);
232 vorbisSettings.append(check);
233 vbox->addWidget(check);
235 // ---- buttons ----
237 hbox = new QHBoxLayout;
238 button = new QPushButton("&Close");
239 button->setDefault(true);
240 connect(button, SIGNAL(clicked(bool)), this, SLOT(accept()));
241 hbox->addStretch();
242 hbox->addWidget(button);
243 bigvbox->addLayout(hbox);
245 updateFormatSettings();
246 updatePatternToolTip("");
249 void PreferencesDialog::updateFormatSettings() {
250 QVariant v = formatWidget->itemData(formatWidget->currentIndex());
251 // hide
252 if (v != "mp3")
253 for (int i = 0; i < mp3Settings.size(); i++)
254 mp3Settings.at(i)->hide();
255 if (v != "vorbis")
256 for (int i = 0; i < vorbisSettings.size(); i++)
257 vorbisSettings.at(i)->hide();
258 // show
259 if (v == "mp3")
260 for (int i = 0; i < mp3Settings.size(); i++)
261 mp3Settings.at(i)->show();
262 if (v == "vorbis")
263 for (int i = 0; i < vorbisSettings.size(); i++)
264 vorbisSettings.at(i)->show();
267 void PreferencesDialog::editPerCallerPreferences() {
268 perCallerDialog = new PerCallerPreferencesDialog(this);
269 connect(perCallerDialog, SIGNAL(finished(int)), this, SLOT(perCallerFinished()));
272 void PreferencesDialog::perCallerFinished() {
273 perCallerDialog = NULL;
276 void PreferencesDialog::hideEvent(QHideEvent *event) {
277 if (perCallerDialog)
278 perCallerDialog->accept();
280 QDialog::hideEvent(event);
283 void PreferencesDialog::updatePatternToolTip(const QString &pattern) {
284 QString tip =
285 "This pattern specifies how the file name for the recorded call is constructed.\n"
286 "You can use the following directives:\n\n"
288 #define X(a, b) "\t" a "\t" b "\n"
289 X("&s" , "The remote skype name")
290 X("&d" , "The remote display name")
291 X("&t" , "Your skype name")
292 X("&e" , "Your display name")
293 X("&&" , "Literal & character")
294 X("%Y" , "Year")
295 X("%A / %a", "Full / abbreviated weekday name")
296 X("%B / %b", "Full / abbreviated month name")
297 X("%m" , "Month as a number (01 - 12)")
298 X("%d" , "Day of the month (01 - 31)")
299 X("%H" , "Hour as a 24-hour clock (00 - 23)")
300 X("%I" , "Hour as a 12-hour clock (01 - 12)")
301 X("%p" , "AM or PM")
302 X("%M" , "Minutes (00 - 59)")
303 X("%S" , "Seconds (00 - 59)")
304 X("%%" , "Literal % character")
305 #undef X
306 "\t...and all other directives provided by strftime()\n\n"
308 "With the current choice, the file name might look like this:\n";
310 QString fn = getFileName("echo123", "Skype Test Service", "myskype", "My Full Name",
311 QDateTime::currentDateTime(), pattern);
312 tip += fn;
313 if (fn.contains(':'))
314 tip += "\n\nWARNING: Microsoft Windows does not allow colon characters (:) in file names.";
315 patternWidget->setToolTip(tip);
318 // per caller preferences editor
320 PerCallerPreferencesDialog::PerCallerPreferencesDialog(QWidget *parent) : QDialog(parent) {
321 setWindowTitle("Per Caller Preferences");
322 setWindowModality(Qt::WindowModal);
323 setAttribute(Qt::WA_DeleteOnClose);
325 model = new PerCallerModel(this);
327 QHBoxLayout *bighbox = new QHBoxLayout(this);
328 QVBoxLayout *vbox = new QVBoxLayout;
330 listWidget = new QListView;
331 listWidget->setModel(model);
332 listWidget->setSelectionMode(QAbstractItemView::ExtendedSelection);
333 listWidget->setEditTriggers(QAbstractItemView::SelectedClicked | QAbstractItemView::DoubleClicked);
334 connect(listWidget->selectionModel(), SIGNAL(selectionChanged(const QItemSelection &, const QItemSelection &)), this, SLOT(selectionChanged()));
335 vbox->addWidget(listWidget);
337 QVBoxLayout *frame = makeVFrame(vbox, "Preference for selected Skype names:");
338 radioYes = new QRadioButton("Automatically &record calls");
339 radioAsk = new QRadioButton("&Ask every time");
340 radioNo = new QRadioButton("Do &not automatically record calls");
341 connect(radioYes, SIGNAL(clicked(bool)), this, SLOT(radioChanged()));
342 connect(radioAsk, SIGNAL(clicked(bool)), this, SLOT(radioChanged()));
343 connect(radioNo, SIGNAL(clicked(bool)), this, SLOT(radioChanged()));
344 frame->addWidget(radioYes);
345 frame->addWidget(radioAsk);
346 frame->addWidget(radioNo);
348 bighbox->addLayout(vbox);
350 vbox = new QVBoxLayout;
352 QPushButton *button = new QPushButton("A&dd");
353 connect(button, SIGNAL(clicked(bool)), this, SLOT(add()));
354 vbox->addWidget(button);
356 button = new QPushButton("Re&move");
357 connect(button, SIGNAL(clicked(bool)), this, SLOT(remove()));
358 vbox->addWidget(button);
360 vbox->addStretch();
362 button = new QPushButton("&Close");
363 button->setDefault(true);
364 connect(button, SIGNAL(clicked(bool)), this, SLOT(accept()));
365 vbox->addWidget(button);
367 bighbox->addLayout(vbox);
369 // fill in data
371 QSet<QString> seen;
373 QStringList list = preferences.get("autorecord.yes").toList();
374 for (int i = 0; i < list.count(); i++) {
375 QString sn = list.at(i);
376 if (seen.contains(sn))
377 continue;
378 seen.insert(sn);
379 add(sn, 2, false);
382 list = preferences.get("autorecord.ask").toList();
383 for (int i = 0; i < list.count(); i++) {
384 QString sn = list.at(i);
385 if (seen.contains(sn))
386 continue;
387 seen.insert(sn);
388 add(sn, 1, false);
391 list = preferences.get("autorecord.no").toList();
392 for (int i = 0; i < list.count(); i++) {
393 QString sn = list.at(i);
394 if (seen.contains(sn))
395 continue;
396 seen.insert(sn);
397 add(sn, 0, false);
400 model->sort();
401 connect(this, SIGNAL(finished(int)), this, SLOT(save()));
402 selectionChanged();
403 show();
406 void PerCallerPreferencesDialog::add(const QString &name, int mode, bool edit) {
407 int i = model->rowCount();
408 model->insertRow(i);
410 QModelIndex idx = model->index(i, 0);
411 model->setData(idx, name, Qt::EditRole);
412 model->setData(idx, mode, Qt::UserRole);
414 if (edit) {
415 listWidget->clearSelection();
416 listWidget->setCurrentIndex(idx);
417 listWidget->edit(idx);
421 void PerCallerPreferencesDialog::remove() {
422 QModelIndexList sel = listWidget->selectionModel()->selectedIndexes();
423 qSort(sel);
424 while (!sel.isEmpty())
425 model->removeRow(sel.takeLast().row());
428 void PerCallerPreferencesDialog::selectionChanged() {
429 QModelIndexList sel = listWidget->selectionModel()->selectedIndexes();
430 bool notEmpty = !sel.isEmpty();
431 int mode = -1;
432 while (!sel.isEmpty()) {
433 int m = model->data(sel.takeLast(), Qt::UserRole).toInt();
434 if (mode == -1) {
435 mode = m;
436 } else if (mode != m) {
437 mode = -1;
438 break;
441 if (mode == -1) {
442 // Qt is a bit annoying about this: You can't deselect
443 // everything unless you disable auto-exclusive mode
444 radioYes->setAutoExclusive(false);
445 radioAsk->setAutoExclusive(false);
446 radioNo ->setAutoExclusive(false);
447 radioYes->setChecked(false);
448 radioAsk->setChecked(false);
449 radioNo ->setChecked(false);
450 radioYes->setAutoExclusive(true);
451 radioAsk->setAutoExclusive(true);
452 radioNo ->setAutoExclusive(true);
453 } else if (mode == 0) {
454 radioNo->setChecked(true);
455 } else if (mode == 1) {
456 radioAsk->setChecked(true);
457 } else if (mode == 2) {
458 radioYes->setChecked(true);
461 radioYes->setEnabled(notEmpty);
462 radioAsk->setEnabled(notEmpty);
463 radioNo ->setEnabled(notEmpty);
466 void PerCallerPreferencesDialog::radioChanged() {
467 int mode = 1;
468 if (radioYes->isChecked())
469 mode = 2;
470 else if (radioNo->isChecked())
471 mode = 0;
473 QModelIndexList sel = listWidget->selectionModel()->selectedIndexes();
474 while (!sel.isEmpty())
475 model->setData(sel.takeLast(), mode, Qt::UserRole);
478 void PerCallerPreferencesDialog::save() {
479 model->sort();
480 int n = model->rowCount();
481 QStringList yes, ask, no;
482 for (int i = 0; i < n; i++) {
483 QModelIndex idx = model->index(i, 0);
484 QString sn = model->data(idx, Qt::EditRole).toString();
485 if (sn.isEmpty())
486 continue;
487 int mode = model->data(idx, Qt::UserRole).toInt();
488 if (mode == 0)
489 no.append(sn);
490 else if (mode == 1)
491 ask.append(sn);
492 else if (mode == 2)
493 yes.append(sn);
495 preferences.get("autorecord.yes").set(yes);
496 preferences.get("autorecord.ask").set(ask);
497 preferences.get("autorecord.no").set(no);
500 // per caller model
502 int PerCallerModel::rowCount(const QModelIndex &) const {
503 return skypeNames.count();
506 namespace {
507 const char *PerCallerModel_data_table[3] = {
508 "Don't record", "Ask", "Automatic"
512 QVariant PerCallerModel::data(const QModelIndex &index, int role) const {
513 if (!index.isValid() || index.row() >= skypeNames.size())
514 return QVariant();
515 if (role == Qt::DisplayRole) {
516 int i = index.row();
517 return skypeNames.at(i) + " - " + PerCallerModel_data_table[modes.at(i)];
519 if (role == Qt::EditRole)
520 return skypeNames.at(index.row());
521 if (role == Qt::UserRole)
522 return modes.at(index.row());
523 return QVariant();
526 bool PerCallerModel::setData(const QModelIndex &index, const QVariant &value, int role) {
527 if (!index.isValid() || index.row() >= skypeNames.size())
528 return false;
529 if (role == Qt::EditRole) {
530 skypeNames[index.row()] = value.toString();
531 emit dataChanged(index, index);
532 return true;
534 if (role == Qt::UserRole) {
535 modes[index.row()] = value.toInt();
536 emit dataChanged(index, index);
537 return true;
539 return false;
542 bool PerCallerModel::insertRows(int position, int rows, const QModelIndex &) {
543 beginInsertRows(QModelIndex(), position, position + rows - 1);
544 for (int i = 0; i < rows; i++) {
545 skypeNames.insert(position, "");
546 modes.insert(position, 1);
548 endInsertRows();
549 return true;
552 bool PerCallerModel::removeRows(int position, int rows, const QModelIndex &) {
553 beginRemoveRows(QModelIndex(), position, position + rows - 1);
554 for (int i = 0; i < rows; i++) {
555 skypeNames.removeAt(position);
556 modes.removeAt(position);
558 endRemoveRows();
559 return true;
562 void PerCallerModel::sort(int, Qt::SortOrder) {
563 typedef QPair<QString, int> Pair;
564 typedef QList<Pair> List;
565 List list;
566 for (int i = 0; i < skypeNames.size(); i++)
567 list.append(Pair(skypeNames.at(i), modes.at(i)));
568 qSort(list);
569 for (int i = 0; i < skypeNames.size(); i++) {
570 skypeNames[i] = list.at(i).first;
571 modes[i] = list.at(i).second;
573 reset();
576 Qt::ItemFlags PerCallerModel::flags(const QModelIndex &index) const {
577 Qt::ItemFlags flags = QAbstractListModel::flags(index);
578 if (!index.isValid() || index.row() >= skypeNames.size())
579 return flags;
580 return flags | Qt::ItemIsEditable;
583 // preference
585 void Preference::listAdd(const QString &value) {
586 QStringList list = toList();
587 if (!list.contains(value)) {
588 list.append(value);
589 set(list);
593 void Preference::listRemove(const QString &value) {
594 QStringList list = toList();
595 if (list.removeAll(value))
596 set(list);
599 bool Preference::listContains(const QString &value) {
600 QStringList list = toList();
601 return list.contains(value);
604 // base preferences
606 bool BasePreferences::load(const QString &filename) {
607 clear();
608 QFile file(filename);
609 if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
610 debug(QString("Can't open '%1' for loading preferences").arg(filename));
611 return false;
613 char buf[65536];
614 while (!file.atEnd()) {
615 qint64 len = file.readLine(buf, sizeof(buf));
616 if (len == -1)
617 break;
618 QString line(buf);
619 line = line.trimmed();
620 if (line.at(0) == '#')
621 continue;
622 int index = line.indexOf('=');
623 if (index < 0)
624 // TODO warn
625 continue;
626 get(line.left(index).trimmed()).set(line.mid(index + 1).trimmed());
628 debug(QString("Loaded %1 preferences from '%2'").arg(preferences.size()).arg(filename));
629 return true;
632 bool BasePreferences::save(const QString &filename) {
633 qSort(preferences);
634 QFile file(filename);
635 if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
636 debug(QString("Can't open '%1' for saving preferences").arg(filename));
637 return false;
639 QTextStream out(&file);
640 for (int i = 0; i < preferences.size(); i++) {
641 const Preference &p = preferences.at(i);
642 out << p.name() << " = " << p.toString() << "\n";
644 debug(QString("Saved %1 preferences to '%2'").arg(preferences.size()).arg(filename));
645 return true;
648 Preference &BasePreferences::get(const QString &name) {
649 for (int i = 0; i < preferences.size(); i++)
650 if (preferences.at(i).name() == name)
651 return preferences[i];
652 preferences.append(Preference(name));
653 return preferences.last();
656 // preferences
658 void Preferences::setPerCallerPreference(const QString &sn, int mode) {
659 // this would interfer with the per caller dialog
660 recorderInstance->closePreferences();
662 Preference &pYes = get("autorecord.yes");
663 Preference &pAsk = get("autorecord.ask");
664 Preference &pNo = get("autorecord.no");
666 pYes.listRemove(sn);
667 pAsk.listRemove(sn);
668 pNo.listRemove(sn);
670 if (mode == 2)
671 pYes.listAdd(sn);
672 else if (mode == 1)
673 pAsk.listAdd(sn);
674 else if (mode == 0)
675 pNo.listAdd(sn);