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>
28 #include <QButtonGroup>
30 #include <QPushButton>
35 #include <QTextStream>
36 #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");
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);
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");
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
);
205 hbox
= new QHBoxLayout
;
206 button
= new QPushButton("&Close");
207 button
->setDefault(true);
208 connect(button
, SIGNAL(clicked(bool)), this, SLOT(accept()));
210 hbox
->addWidget(button
);
211 bigvbox
->addLayout(hbox
);
214 updatePatternToolTip("");
217 void PreferencesDialog::enableMp3Settings() {
218 QVariant v
= formatWidget
->itemData(formatWidget
->currentIndex());
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
) {
235 perCallerDialog
->accept();
237 QDialog::hideEvent(event
);
240 void PreferencesDialog::updatePatternToolTip(const QString
&pattern
) {
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")
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)")
259 X("%M" , "Minutes (00 - 59)")
260 X("%S" , "Seconds (00 - 59)")
261 X("%%" , "Literal % character")
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
);
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 ¬ 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
);
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
);
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
))
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
))
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
))
358 connect(this, SIGNAL(finished(int)), this, SLOT(save()));
363 void PerCallerPreferencesDialog::add(const QString
&name
, int mode
, bool edit
) {
364 int i
= model
->rowCount();
367 QModelIndex idx
= model
->index(i
, 0);
368 model
->setData(idx
, name
, Qt::EditRole
);
369 model
->setData(idx
, mode
, Qt::UserRole
);
372 listWidget
->clearSelection();
373 listWidget
->setCurrentIndex(idx
);
374 listWidget
->edit(idx
);
378 void PerCallerPreferencesDialog::remove() {
379 QModelIndexList sel
= listWidget
->selectionModel()->selectedIndexes();
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();
389 while (!sel
.isEmpty()) {
390 int m
= model
->data(sel
.takeLast(), Qt::UserRole
).toInt();
393 } else if (mode
!= m
) {
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() {
425 if (radioYes
->isChecked())
427 else if (radioNo
->isChecked())
430 QModelIndexList sel
= listWidget
->selectionModel()->selectedIndexes();
431 while (!sel
.isEmpty())
432 model
->setData(sel
.takeLast(), mode
, Qt::UserRole
);
435 void PerCallerPreferencesDialog::save() {
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();
444 int mode
= model
->data(idx
, Qt::UserRole
).toInt();
452 preferences
.get("autorecord.yes").set(yes
);
453 preferences
.get("autorecord.ask").set(ask
);
454 preferences
.get("autorecord.no").set(no
);
459 int PerCallerModel::rowCount(const QModelIndex
&) const {
460 return skypeNames
.count();
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())
472 if (role
== Qt::DisplayRole
) {
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());
483 bool PerCallerModel::setData(const QModelIndex
&index
, const QVariant
&value
, int role
) {
484 if (!index
.isValid() || index
.row() >= skypeNames
.size())
486 if (role
== Qt::EditRole
) {
487 skypeNames
[index
.row()] = value
.toString();
488 emit
dataChanged(index
, index
);
491 if (role
== Qt::UserRole
) {
492 modes
[index
.row()] = value
.toInt();
493 emit
dataChanged(index
, index
);
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);
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
);
519 void PerCallerModel::sort(int, Qt::SortOrder
) {
520 typedef QPair
<QString
, int> Pair
;
521 typedef QList
<Pair
> List
;
523 for (int i
= 0; i
< skypeNames
.size(); i
++)
524 list
.append(Pair(skypeNames
.at(i
), modes
.at(i
)));
526 for (int i
= 0; i
< skypeNames
.size(); i
++) {
527 skypeNames
[i
] = list
.at(i
).first
;
528 modes
[i
] = list
.at(i
).second
;
533 Qt::ItemFlags
PerCallerModel::flags(const QModelIndex
&index
) const {
534 Qt::ItemFlags flags
= QAbstractListModel::flags(index
);
535 if (!index
.isValid() || index
.row() >= skypeNames
.size())
537 return flags
| Qt::ItemIsEditable
;
542 void Preference::listAdd(const QString
&value
) {
543 QStringList list
= toList();
544 if (!list
.contains(value
)) {
550 void Preference::listRemove(const QString
&value
) {
551 QStringList list
= toList();
552 if (list
.removeAll(value
))
556 bool Preference::listContains(const QString
&value
) {
557 QStringList list
= toList();
558 return list
.contains(value
);
563 bool BasePreferences::load(const QString
&filename
) {
565 QFile
file(filename
);
566 if (!file
.open(QIODevice::ReadOnly
| QIODevice::Text
)) {
567 debug(QString("Can't open '%1' for loading preferences").arg(filename
));
571 while (!file
.atEnd()) {
572 qint64 len
= file
.readLine(buf
, sizeof(buf
));
576 line
= line
.trimmed();
577 if (line
.at(0) == '#')
579 int index
= line
.indexOf('=');
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
));
589 bool BasePreferences::save(const QString
&filename
) {
591 QFile
file(filename
);
592 if (!file
.open(QIODevice::WriteOnly
| QIODevice::Text
)) {
593 debug(QString("Can't open '%1' for saving preferences").arg(filename
));
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
));
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();
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");