utils.cpp: Set FD_CLOEXEC on lock file
[skype-call-recorder.git] / preferences.cpp
blob6b5d70a2e1a9f0716dbba3377180eb2c15d933ce
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 <QButtonGroup>
29 #include <QLabel>
30 #include <QPushButton>
31 #include <QListView>
32 #include <QPair>
33 #include <QFile>
34 //#include <QSet>
35 #include <QTextStream>
36 #include <QtAlgorithms>
37 #include <QDir>
38 #include <QDateTime>
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->setupDone();
168 connect(combo, SIGNAL(currentIndexChanged(int)), this, SLOT(enableMp3Settings()));
169 hbox->addWidget(combo);
171 combo = new SmartComboBox(preferences.get("output.format.mp3.bitrate"));
172 combo->addItem("8 kbps", 8);
173 combo->addItem("16 kbps", 16);
174 combo->addItem("24 kbps", 24);
175 combo->addItem("32 kbps", 32);
176 combo->addItem("40 kbps", 40);
177 combo->addItem("48 kbps", 48);
178 combo->addItem("56 kbps", 56);
179 combo->addItem("64 kbps", 64);
180 combo->addItem("80 kbps", 80);
181 combo->addItem("96 kbps", 96);
182 combo->addItem("112 kbps", 112);
183 combo->addItem("128 kbps", 128);
184 combo->addItem("144 kbps", 144);
185 combo->addItem("160 kbps", 160);
186 combo->setupDone();
187 mp3Settings.append(combo);
188 hbox->addWidget(combo);
190 combo = new SmartComboBox(preferences.get("output.channelmode"));
191 combo->addItem("Mix to mono", "mono");
192 combo->addItem("Stereo, local left, remote right", "stereo");
193 combo->addItem("Stereo, local right, remote left", "oerets");
194 combo->setupDone();
195 hbox->addWidget(combo);
197 vbox->addLayout(hbox);
199 check = new SmartCheckBox("Save call &information in MP3 files", preferences.get("output.savetags"));
200 mp3Settings.append(check);
201 vbox->addWidget(check);
203 // ---- buttons ----
205 hbox = new QHBoxLayout;
206 button = new QPushButton("&Close");
207 button->setDefault(true);
208 connect(button, SIGNAL(clicked(bool)), this, SLOT(accept()));
209 hbox->addStretch();
210 hbox->addWidget(button);
211 bigvbox->addLayout(hbox);
213 enableMp3Settings();
214 updatePatternToolTip("");
217 void PreferencesDialog::enableMp3Settings() {
218 QVariant v = formatWidget->itemData(formatWidget->currentIndex());
219 bool b = v == "mp3";
220 for (int i = 0; i < mp3Settings.size(); i++)
221 mp3Settings.at(i)->setEnabled(b);
224 void PreferencesDialog::editPerCallerPreferences() {
225 perCallerDialog = new PerCallerPreferencesDialog(this);
226 connect(perCallerDialog, SIGNAL(finished(int)), this, SLOT(perCallerFinished()));
229 void PreferencesDialog::perCallerFinished() {
230 perCallerDialog = NULL;
233 void PreferencesDialog::hideEvent(QHideEvent *event) {
234 if (perCallerDialog)
235 perCallerDialog->accept();
237 QDialog::hideEvent(event);
240 void PreferencesDialog::updatePatternToolTip(const QString &pattern) {
241 QString tip =
242 "This pattern specifies how the file name for the recorded call is constructed.\n"
243 "You can use the following directives:\n\n"
245 #define X(a, b) "\t" a "\t" b "\n"
246 X("&s" , "The remote skype name")
247 X("&d" , "The remote display name")
248 X("&t" , "Your skype name")
249 X("&e" , "Your display name")
250 X("&&" , "Literal & character")
251 X("%Y" , "Year")
252 X("%A / %a", "Full / abbreviated weekday name")
253 X("%B / %b", "Full / abbreviated month name")
254 X("%m" , "Month as a number (01 - 12)")
255 X("%d" , "Day of the month (01 - 31)")
256 X("%H" , "Hour as a 24-hour clock (00 - 23)")
257 X("%I" , "Hour as a 12-hour clock (01 - 12)")
258 X("%p" , "AM or PM")
259 X("%M" , "Minutes (00 - 59)")
260 X("%S" , "Seconds (00 - 59)")
261 X("%%" , "Literal % character")
262 #undef X
263 "\t...and all other directives provided by strftime()\n\n"
265 "With the current choice, the file name might look like this:\n";
267 QString fn = getFileName("echo123", "Skype Test Service", "myskype", "My Full Name",
268 QDateTime::currentDateTime(), pattern);
269 tip += fn;
270 if (fn.contains(':'))
271 tip += "\n\nWARNING: Microsoft Windows does not allow colon characters (:) in file names.";
272 patternWidget->setToolTip(tip);
275 // per caller preferences editor
277 PerCallerPreferencesDialog::PerCallerPreferencesDialog(QWidget *parent) : QDialog(parent) {
278 setWindowTitle("Per Caller Preferences");
279 setWindowModality(Qt::WindowModal);
280 setAttribute(Qt::WA_DeleteOnClose);
282 model = new PerCallerModel(this);
284 QHBoxLayout *bighbox = new QHBoxLayout(this);
285 QVBoxLayout *vbox = new QVBoxLayout;
287 listWidget = new QListView;
288 listWidget->setModel(model);
289 listWidget->setSelectionMode(QAbstractItemView::ExtendedSelection);
290 listWidget->setEditTriggers(QAbstractItemView::SelectedClicked | QAbstractItemView::DoubleClicked);
291 connect(listWidget->selectionModel(), SIGNAL(selectionChanged(const QItemSelection &, const QItemSelection &)), this, SLOT(selectionChanged()));
292 vbox->addWidget(listWidget);
294 QVBoxLayout *frame = makeVFrame(vbox, "Preference for selected Skype names:");
295 radioYes = new QRadioButton("Automatically &record calls");
296 radioAsk = new QRadioButton("&Ask every time");
297 radioNo = new QRadioButton("Do &not automatically record calls");
298 connect(radioYes, SIGNAL(clicked(bool)), this, SLOT(radioChanged()));
299 connect(radioAsk, SIGNAL(clicked(bool)), this, SLOT(radioChanged()));
300 connect(radioNo, SIGNAL(clicked(bool)), this, SLOT(radioChanged()));
301 frame->addWidget(radioYes);
302 frame->addWidget(radioAsk);
303 frame->addWidget(radioNo);
305 bighbox->addLayout(vbox);
307 vbox = new QVBoxLayout;
309 QPushButton *button = new QPushButton("A&dd");
310 connect(button, SIGNAL(clicked(bool)), this, SLOT(add()));
311 vbox->addWidget(button);
313 button = new QPushButton("Re&move");
314 connect(button, SIGNAL(clicked(bool)), this, SLOT(remove()));
315 vbox->addWidget(button);
317 vbox->addStretch();
319 button = new QPushButton("&Close");
320 button->setDefault(true);
321 connect(button, SIGNAL(clicked(bool)), this, SLOT(accept()));
322 vbox->addWidget(button);
324 bighbox->addLayout(vbox);
326 // fill in data
328 QSet<QString> seen;
330 QStringList list = preferences.get("autorecord.yes").toList();
331 for (int i = 0; i < list.count(); i++) {
332 QString sn = list.at(i);
333 if (seen.contains(sn))
334 continue;
335 seen.insert(sn);
336 add(sn, 2, false);
339 list = preferences.get("autorecord.ask").toList();
340 for (int i = 0; i < list.count(); i++) {
341 QString sn = list.at(i);
342 if (seen.contains(sn))
343 continue;
344 seen.insert(sn);
345 add(sn, 1, false);
348 list = preferences.get("autorecord.no").toList();
349 for (int i = 0; i < list.count(); i++) {
350 QString sn = list.at(i);
351 if (seen.contains(sn))
352 continue;
353 seen.insert(sn);
354 add(sn, 0, false);
357 model->sort();
358 connect(this, SIGNAL(finished(int)), this, SLOT(save()));
359 selectionChanged();
360 show();
363 void PerCallerPreferencesDialog::add(const QString &name, int mode, bool edit) {
364 int i = model->rowCount();
365 model->insertRow(i);
367 QModelIndex idx = model->index(i, 0);
368 model->setData(idx, name, Qt::EditRole);
369 model->setData(idx, mode, Qt::UserRole);
371 if (edit) {
372 listWidget->clearSelection();
373 listWidget->setCurrentIndex(idx);
374 listWidget->edit(idx);
378 void PerCallerPreferencesDialog::remove() {
379 QModelIndexList sel = listWidget->selectionModel()->selectedIndexes();
380 qSort(sel);
381 while (!sel.isEmpty())
382 model->removeRow(sel.takeLast().row());
385 void PerCallerPreferencesDialog::selectionChanged() {
386 QModelIndexList sel = listWidget->selectionModel()->selectedIndexes();
387 bool notEmpty = !sel.isEmpty();
388 int mode = -1;
389 while (!sel.isEmpty()) {
390 int m = model->data(sel.takeLast(), Qt::UserRole).toInt();
391 if (mode == -1) {
392 mode = m;
393 } else if (mode != m) {
394 mode = -1;
395 break;
398 if (mode == -1) {
399 // Qt is a bit annoying about this: You can't deselect
400 // everything unless you disable auto-exclusive mode
401 radioYes->setAutoExclusive(false);
402 radioAsk->setAutoExclusive(false);
403 radioNo ->setAutoExclusive(false);
404 radioYes->setChecked(false);
405 radioAsk->setChecked(false);
406 radioNo ->setChecked(false);
407 radioYes->setAutoExclusive(true);
408 radioAsk->setAutoExclusive(true);
409 radioNo ->setAutoExclusive(true);
410 } else if (mode == 0) {
411 radioNo->setChecked(true);
412 } else if (mode == 1) {
413 radioAsk->setChecked(true);
414 } else if (mode == 2) {
415 radioYes->setChecked(true);
418 radioYes->setEnabled(notEmpty);
419 radioAsk->setEnabled(notEmpty);
420 radioNo ->setEnabled(notEmpty);
423 void PerCallerPreferencesDialog::radioChanged() {
424 int mode = 1;
425 if (radioYes->isChecked())
426 mode = 2;
427 else if (radioNo->isChecked())
428 mode = 0;
430 QModelIndexList sel = listWidget->selectionModel()->selectedIndexes();
431 while (!sel.isEmpty())
432 model->setData(sel.takeLast(), mode, Qt::UserRole);
435 void PerCallerPreferencesDialog::save() {
436 model->sort();
437 int n = model->rowCount();
438 QStringList yes, ask, no;
439 for (int i = 0; i < n; i++) {
440 QModelIndex idx = model->index(i, 0);
441 QString sn = model->data(idx, Qt::EditRole).toString();
442 if (sn.isEmpty())
443 continue;
444 int mode = model->data(idx, Qt::UserRole).toInt();
445 if (mode == 0)
446 no.append(sn);
447 else if (mode == 1)
448 ask.append(sn);
449 else if (mode == 2)
450 yes.append(sn);
452 preferences.get("autorecord.yes").set(yes);
453 preferences.get("autorecord.ask").set(ask);
454 preferences.get("autorecord.no").set(no);
457 // per caller model
459 int PerCallerModel::rowCount(const QModelIndex &) const {
460 return skypeNames.count();
463 namespace {
464 const char *PerCallerModel_data_table[3] = {
465 "Don't record", "Ask", "Automatic"
469 QVariant PerCallerModel::data(const QModelIndex &index, int role) const {
470 if (!index.isValid() || index.row() >= skypeNames.size())
471 return QVariant();
472 if (role == Qt::DisplayRole) {
473 int i = index.row();
474 return skypeNames.at(i) + " - " + PerCallerModel_data_table[modes.at(i)];
476 if (role == Qt::EditRole)
477 return skypeNames.at(index.row());
478 if (role == Qt::UserRole)
479 return modes.at(index.row());
480 return QVariant();
483 bool PerCallerModel::setData(const QModelIndex &index, const QVariant &value, int role) {
484 if (!index.isValid() || index.row() >= skypeNames.size())
485 return false;
486 if (role == Qt::EditRole) {
487 skypeNames[index.row()] = value.toString();
488 emit dataChanged(index, index);
489 return true;
491 if (role == Qt::UserRole) {
492 modes[index.row()] = value.toInt();
493 emit dataChanged(index, index);
494 return true;
496 return false;
499 bool PerCallerModel::insertRows(int position, int rows, const QModelIndex &) {
500 beginInsertRows(QModelIndex(), position, position + rows - 1);
501 for (int i = 0; i < rows; i++) {
502 skypeNames.insert(position, "");
503 modes.insert(position, 1);
505 endInsertRows();
506 return true;
509 bool PerCallerModel::removeRows(int position, int rows, const QModelIndex &) {
510 beginRemoveRows(QModelIndex(), position, position + rows - 1);
511 for (int i = 0; i < rows; i++) {
512 skypeNames.removeAt(position);
513 modes.removeAt(position);
515 endRemoveRows();
516 return true;
519 void PerCallerModel::sort(int, Qt::SortOrder) {
520 typedef QPair<QString, int> Pair;
521 typedef QList<Pair> List;
522 List list;
523 for (int i = 0; i < skypeNames.size(); i++)
524 list.append(Pair(skypeNames.at(i), modes.at(i)));
525 qSort(list);
526 for (int i = 0; i < skypeNames.size(); i++) {
527 skypeNames[i] = list.at(i).first;
528 modes[i] = list.at(i).second;
530 reset();
533 Qt::ItemFlags PerCallerModel::flags(const QModelIndex &index) const {
534 Qt::ItemFlags flags = QAbstractListModel::flags(index);
535 if (!index.isValid() || index.row() >= skypeNames.size())
536 return flags;
537 return flags | Qt::ItemIsEditable;
540 // preference
542 void Preference::listAdd(const QString &value) {
543 QStringList list = toList();
544 if (!list.contains(value)) {
545 list.append(value);
546 set(list);
550 void Preference::listRemove(const QString &value) {
551 QStringList list = toList();
552 if (list.removeAll(value))
553 set(list);
556 bool Preference::listContains(const QString &value) {
557 QStringList list = toList();
558 return list.contains(value);
561 // base preferences
563 bool BasePreferences::load(const QString &filename) {
564 clear();
565 QFile file(filename);
566 if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
567 debug(QString("Can't open '%1' for loading preferences").arg(filename));
568 return false;
570 char buf[65536];
571 while (!file.atEnd()) {
572 qint64 len = file.readLine(buf, sizeof(buf));
573 if (len == -1)
574 break;
575 QString line(buf);
576 line = line.trimmed();
577 if (line.at(0) == '#')
578 continue;
579 int index = line.indexOf('=');
580 if (index < 0)
581 // TODO warn
582 continue;
583 get(line.left(index).trimmed()).set(line.mid(index + 1).trimmed());
585 debug(QString("Loaded %1 preferences from '%2'").arg(preferences.size()).arg(filename));
586 return true;
589 bool BasePreferences::save(const QString &filename) {
590 qSort(preferences);
591 QFile file(filename);
592 if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
593 debug(QString("Can't open '%1' for saving preferences").arg(filename));
594 return false;
596 QTextStream out(&file);
597 for (int i = 0; i < preferences.size(); i++) {
598 const Preference &p = preferences.at(i);
599 out << p.name() << " = " << p.toString() << "\n";
601 debug(QString("Saved %1 preferences to '%2'").arg(preferences.size()).arg(filename));
602 return true;
605 Preference &BasePreferences::get(const QString &name) {
606 for (int i = 0; i < preferences.size(); i++)
607 if (preferences.at(i).name() == name)
608 return preferences[i];
609 preferences.append(Preference(name));
610 return preferences.last();
613 // preferences
615 void Preferences::setPerCallerPreference(const QString &sn, int mode) {
616 // this would interfer with the per caller dialog
617 recorderInstance->closePreferences();
619 Preference &pYes = get("autorecord.yes");
620 Preference &pAsk = get("autorecord.ask");
621 Preference &pNo = get("autorecord.no");
623 pYes.listRemove(sn);
624 pAsk.listRemove(sn);
625 pNo.listRemove(sn);
627 if (mode == 2)
628 pYes.listAdd(sn);
629 else if (mode == 1)
630 pAsk.listAdd(sn);
631 else if (mode == 0)
632 pNo.listAdd(sn);