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>
40 #include "preferences.h"
41 #include "smartwidgets.h"
45 Preferences preferences
;
47 QString
getOutputPath() {
48 QString path
= preferences
.get("output.path").toString();
49 path
.replace('~', QDir::homePath());
54 QString
escape(const QString
&s
) {
56 out
.replace('%', "%%");
57 out
.replace('&', "&&");
62 QString
getFileName(const QString
&skypeName
, const QString
&displayName
,
63 const QString
&mySkypeName
, const QString
&myDisplayName
, const QDateTime
×tamp
, const QString
&pattern
)
66 if (pattern
.isEmpty())
67 fileName
= preferences
.get("output.pattern").toString();
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
);
85 return getOutputPath() + '/' + fileName
;
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
);
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
);
104 PreferencesDialog::PreferencesDialog() : perCallerDialog(NULL
) {
105 setWindowTitle(PROGRAM_NAME
" - Preferences");
111 SmartComboBox
*combo
;
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 ¬ 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");
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");
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);
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);
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
);
237 hbox
= new QHBoxLayout
;
238 button
= new QPushButton("&Close");
239 button
->setDefault(true);
240 connect(button
, SIGNAL(clicked(bool)), this, SLOT(accept()));
242 hbox
->addWidget(button
);
243 bigvbox
->addLayout(hbox
);
245 updateFormatSettings();
246 updatePatternToolTip("");
249 void PreferencesDialog::updateFormatSettings() {
250 QVariant v
= formatWidget
->itemData(formatWidget
->currentIndex());
253 for (int i
= 0; i
< mp3Settings
.size(); i
++)
254 mp3Settings
.at(i
)->hide();
256 for (int i
= 0; i
< vorbisSettings
.size(); i
++)
257 vorbisSettings
.at(i
)->hide();
260 for (int i
= 0; i
< mp3Settings
.size(); i
++)
261 mp3Settings
.at(i
)->show();
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
) {
278 perCallerDialog
->accept();
280 QDialog::hideEvent(event
);
283 void PreferencesDialog::updatePatternToolTip(const QString
&pattern
) {
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")
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)")
302 X("%M" , "Minutes (00 - 59)")
303 X("%S" , "Seconds (00 - 59)")
304 X("%%" , "Literal % character")
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
);
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 ¬ 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
);
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
);
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
))
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
))
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
))
401 connect(this, SIGNAL(finished(int)), this, SLOT(save()));
406 void PerCallerPreferencesDialog::add(const QString
&name
, int mode
, bool edit
) {
407 int i
= model
->rowCount();
410 QModelIndex idx
= model
->index(i
, 0);
411 model
->setData(idx
, name
, Qt::EditRole
);
412 model
->setData(idx
, mode
, Qt::UserRole
);
415 listWidget
->clearSelection();
416 listWidget
->setCurrentIndex(idx
);
417 listWidget
->edit(idx
);
421 void PerCallerPreferencesDialog::remove() {
422 QModelIndexList sel
= listWidget
->selectionModel()->selectedIndexes();
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();
432 while (!sel
.isEmpty()) {
433 int m
= model
->data(sel
.takeLast(), Qt::UserRole
).toInt();
436 } else if (mode
!= m
) {
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() {
468 if (radioYes
->isChecked())
470 else if (radioNo
->isChecked())
473 QModelIndexList sel
= listWidget
->selectionModel()->selectedIndexes();
474 while (!sel
.isEmpty())
475 model
->setData(sel
.takeLast(), mode
, Qt::UserRole
);
478 void PerCallerPreferencesDialog::save() {
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();
487 int mode
= model
->data(idx
, Qt::UserRole
).toInt();
495 preferences
.get("autorecord.yes").set(yes
);
496 preferences
.get("autorecord.ask").set(ask
);
497 preferences
.get("autorecord.no").set(no
);
502 int PerCallerModel::rowCount(const QModelIndex
&) const {
503 return skypeNames
.count();
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())
515 if (role
== Qt::DisplayRole
) {
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());
526 bool PerCallerModel::setData(const QModelIndex
&index
, const QVariant
&value
, int role
) {
527 if (!index
.isValid() || index
.row() >= skypeNames
.size())
529 if (role
== Qt::EditRole
) {
530 skypeNames
[index
.row()] = value
.toString();
531 emit
dataChanged(index
, index
);
534 if (role
== Qt::UserRole
) {
535 modes
[index
.row()] = value
.toInt();
536 emit
dataChanged(index
, index
);
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);
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
);
562 void PerCallerModel::sort(int, Qt::SortOrder
) {
563 typedef QPair
<QString
, int> Pair
;
564 typedef QList
<Pair
> List
;
566 for (int i
= 0; i
< skypeNames
.size(); i
++)
567 list
.append(Pair(skypeNames
.at(i
), modes
.at(i
)));
569 for (int i
= 0; i
< skypeNames
.size(); i
++) {
570 skypeNames
[i
] = list
.at(i
).first
;
571 modes
[i
] = list
.at(i
).second
;
576 Qt::ItemFlags
PerCallerModel::flags(const QModelIndex
&index
) const {
577 Qt::ItemFlags flags
= QAbstractListModel::flags(index
);
578 if (!index
.isValid() || index
.row() >= skypeNames
.size())
580 return flags
| Qt::ItemIsEditable
;
585 void Preference::listAdd(const QString
&value
) {
586 QStringList list
= toList();
587 if (!list
.contains(value
)) {
593 void Preference::listRemove(const QString
&value
) {
594 QStringList list
= toList();
595 if (list
.removeAll(value
))
599 bool Preference::listContains(const QString
&value
) {
600 QStringList list
= toList();
601 return list
.contains(value
);
606 bool BasePreferences::load(const QString
&filename
) {
608 QFile
file(filename
);
609 if (!file
.open(QIODevice::ReadOnly
| QIODevice::Text
)) {
610 debug(QString("Can't open '%1' for loading preferences").arg(filename
));
614 while (!file
.atEnd()) {
615 qint64 len
= file
.readLine(buf
, sizeof(buf
));
619 line
= line
.trimmed();
620 if (line
.at(0) == '#')
622 int index
= line
.indexOf('=');
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
));
632 bool BasePreferences::save(const QString
&filename
) {
634 QFile
file(filename
);
635 if (!file
.open(QIODevice::WriteOnly
| QIODevice::Text
)) {
636 debug(QString("Can't open '%1' for saving preferences").arg(filename
));
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
));
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();
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");