1 /*
2 * Cantata
3 *
4 * Copyright (c) 2011-2020 Craig Drummond <craig.p.drummond@gmail.com>
5 *
6 * ----
7 *
8 * This program is free software; you can redistribute it and/or modify
9 * it under the terms of the GNU General Public License as published by
10 * the Free Software Foundation; either version 2 of the License, or
11 * (at your option) any later version.
12 *
13 * This program is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
16 * General Public License for more details.
17 *
18 * You should have received a copy of the GNU General Public License
19 * along with this program; see the file COPYING. If not, write to
20 * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
21 * Boston, MA 02110-1301, USA.
22 */
23
24 #include "devicepropertieswidget.h"
25 #include "filenameschemedialog.h"
26 #include "gui/covers.h"
27 #include "widgets/icons.h"
28 #include "support/utils.h"
29 #include <QValidator>
30 #include <QTabWidget>
31 #include <QTimer>
32
33 class CoverNameValidator : public QValidator
34 {
35 public:
36
CoverNameValidator(QObject * parent)37 CoverNameValidator(QObject *parent) : QValidator(parent) { }
38
validate(QString & input,int &) const39 State validate(QString &input, int &) const override
40 {
41 int dotCount(0);
42
43 for (int i=0; i<input.length(); ++i) {
44 if (QChar('.')==input[i]) {
45 if (++dotCount>1) {
46 return Invalid;
47 }
48 }
49 else if (!input[i].isLetterOrNumber() || input[i].isSpace()) {
50 return Invalid;
51 }
52 }
53 if (input.endsWith('.')) {
54 return Intermediate;
55 }
56
57 return Acceptable;
58 }
59
60 // void fixup(QString &input) const
61 // {
62 // QString out;
63 // int dotCount(0);
64 // for (int i=0; i<input.length(); ++i) {
65 // if (input[i].isLetterOrNumber() && !input[i].isSpace()) {
66 // out+=input[i];
67 // } else if (QChar('.')==input[i] && ++dotCount<1) {
68 // out+=input[i];
69 // }
70 // }
71 //
72 // if (!out.endsWith(".jpg") && !out.endsWith(".png")) {
73 // out.replace(".", "");
74 // out+=".jpg";
75 // }
76 // input=out;
77 // }
78 };
79
DevicePropertiesWidget(QWidget * parent)80 DevicePropertiesWidget::DevicePropertiesWidget(QWidget *parent)
81 : QWidget(parent)
82 , schemeDlg(nullptr)
83 , noCoverText(tr("Don't copy covers"))
84 , embedCoverText(tr("Embed cover within each file"))
85 , modified(false)
86 , saveable(false)
87 {
88 setupUi(this);
89 configFilename->setIcon(Icons::self()->configureIcon);
90 coverMaxSize->insertItems(0, QStringList() << tr("No maximum size") << tr("400 pixels") << tr("300 pixels") << tr("200 pixels") << tr("100 pixels"));
91 fixVariousArtists->setToolTip(tr("<p>When copying tracks to a device, and the 'Album Artist' is set to 'Various Artists', "
92 "then Cantata will set the 'Artist' tag of all tracks to 'Various Artists' and the "
93 "track 'Title' tag to 'TrackArtist - TrackTitle'.<hr/> When copying from a device, Cantata "
94 "will check if 'Album Artist' and 'Artist' are both set to 'Various Artists'. If so, it "
95 "will attempt to extract the real artist from the 'Title' tag, and remove the artist name "
96 "from the 'Title' tag.</p>"));
97
98 useCache->setToolTip(tr("<p>If you enable this, then Cantata will create a cache of the device's music library. "
99 "This will help to speed up subsequent library scans (as the cache file will be used instead of "
100 "having to read the tags of each file.)<hr/><b>NOTE:</b> If you use another application to update "
101 "the device's library, then this cache will become out-of-date. To rectify this, simply "
102 "click on the 'refresh' icon in the device list. This will cause the cache file to be removed, and "
103 "the contents of the device re-scanned.</p>"));
104
105 if (qobject_cast<QTabWidget *>(parent)) {
106 verticalLayout->setMargin(4);
107 }
108 }
109
110 #define REMOVE(w) \
111 w->setVisible(false); \
112 w->deleteLater(); \
113 w=0;
114
update(const QString & path,const DeviceOptions & opts,const QList<DeviceStorage> & storage,int props,int disabledProps)115 void DevicePropertiesWidget::update(const QString &path, const DeviceOptions &opts, const QList<DeviceStorage> &storage, int props, int disabledProps)
116 {
117 bool allowCovers=(props&Prop_CoversAll)||(props&Prop_CoversBasic);
118 albumCovers->clear();
119 if (allowCovers) {
120 if (props&Prop_CoversAll) {
121 albumCovers->insertItems(0, QStringList() << noCoverText << embedCoverText << Covers::standardNames());
122 } else {
123 albumCovers->insertItems(0, QStringList() << noCoverText << embedCoverText);
124 }
125 }
126 if (props&Prop_Name) {
127 name->setText(opts.name);
128 connect(name, SIGNAL(textChanged(const QString &)), SLOT(checkSaveable()));
129 } else {
130 REMOVE(name)
131 REMOVE(nameLabel)
132 }
133 if (props&Prop_FileName) {
134 filenameScheme->setText(opts.scheme);
135 vfatSafe->setChecked(opts.vfatSafe);
136 asciiOnly->setChecked(opts.asciiOnly);
137 ignoreThe->setChecked(opts.ignoreThe);
138 replaceSpaces->setChecked(opts.replaceSpaces);
139 } else {
140 REMOVE(filenamesGroupBox)
141 filenameScheme=nullptr;
142 vfatSafe=nullptr;
143 asciiOnly=nullptr;
144 ignoreThe=nullptr;
145 replaceSpaces=nullptr;
146 configFilename=nullptr;
147 }
148 origOpts=opts;
149
150 if (props&Prop_Folder) {
151 musicFolder->setText(Utils::convertPathForDisplay(path));
152 connect(musicFolder, SIGNAL(textChanged(const QString &)), this, SLOT(checkSaveable()));
153 if (disabledProps&Prop_Folder) {
154 musicFolder->setDisabled(true);
155 }
156 } else {
157 REMOVE(musicFolder);
158 REMOVE(musicFolderLabel);
159 }
160 if (allowCovers) {
161 albumCovers->setEditable(props&Prop_CoversAll);
162 if (origOpts.coverName==Device::constNoCover) {
163 origOpts.coverName=noCoverText;
164 albumCovers->setCurrentIndex(0);
165 }
166 if (origOpts.coverName==Device::constEmbedCover) {
167 origOpts.coverName=embedCoverText;
168 albumCovers->setCurrentIndex(1);
169 } else {
170 albumCovers->setCurrentIndex(0);
171 for (int i=1; i<albumCovers->count(); ++i) {
172 if (albumCovers->itemText(i)==origOpts.coverName) {
173 albumCovers->setCurrentIndex(i);
174 break;
175 }
176 }
177 }
178 if (0!=origOpts.coverMaxSize) {
179 int coverMax=origOpts.coverMaxSize/100;
180 if (coverMax<0 || coverMax>=coverMaxSize->count()) {
181 coverMax=0;
182 }
183 coverMaxSize->setCurrentIndex(0==coverMax ? 0 : (coverMaxSize->count()-coverMax));
184 } else {
185 coverMaxSize->setCurrentIndex(0);
186 }
187 albumCovers->setValidator(new CoverNameValidator(this));
188 connect(albumCovers, SIGNAL(editTextChanged(const QString &)), this, SLOT(albumCoversChanged()));
189 connect(coverMaxSize, SIGNAL(currentIndexChanged(int)), this, SLOT(checkSaveable()));
190 } else {
191 REMOVE(albumCovers);
192 REMOVE(albumCoversLabel);
193 REMOVE(coverMaxSize);
194 REMOVE(coverMaxSizeLabel);
195 }
196 if (props&Prop_Va) {
197 fixVariousArtists->setChecked(opts.fixVariousArtists);
198 connect(fixVariousArtists, SIGNAL(stateChanged(int)), this, SLOT(checkSaveable()));
199 } else {
200 REMOVE(fixVariousArtists);
201 }
202 if (props&Prop_Cache) {
203 useCache->setChecked(opts.useCache);
204 connect(useCache, SIGNAL(stateChanged(int)), this, SLOT(checkSaveable()));
205 } else {
206 REMOVE(useCache);
207 }
208 if (props&Prop_AutoScan) {
209 autoScan->setChecked(opts.autoScan);
210 connect(autoScan, SIGNAL(stateChanged(int)), this, SLOT(checkSaveable()));
211 } else {
212 REMOVE(autoScan);
213 }
214
215 if (props&Prop_Transcoder || props&Prop_Encoder) {
216 bool transcode=props&Prop_Transcoder;
217 transcoderName->clear();
218 if (transcode) {
219 transcoderName->addItem(tr("Do not transcode"), QString());
220 transcoderName->setCurrentIndex(0);
221 transcoderValue->setVisible(false);
222 transcoderWhen->addItem(tr("Always transcode"), DeviceOptions::TW_Always);
223 transcoderWhen->addItem(tr("Only transcode if source file is of a different format"), DeviceOptions::TW_IfDifferent);
224 transcoderWhen->addItem(tr("Only transcode if source is FLAC/WAV"), DeviceOptions::TW_IfLossess);
225 transcoderWhen->setVisible(false);
226 transcoderWhen->setCurrentIndex(0);
227 for (int i=0; i<transcoderWhen->count(); ++i) {
228 if (transcoderWhen->itemData(i).toInt()==opts.transcoderWhen) {
229 transcoderWhen->setCurrentIndex(i);
230 break;
231 }
232 }
233 connect(transcoderWhen, SIGNAL(currentIndexChanged(int)), this, SLOT(checkSaveable()));
234 } else {
235 transcoderFrame->setTitle(tr("Encoder"));
236 REMOVE(transcoderWhen);
237 }
238
239 QList<Encoders::Encoder> encs=Encoders::getAvailable();
240
241 if (encs.isEmpty()) {
242 transcoderFrame->setVisible(false);
243 } else {
244 for (const Encoders::Encoder &e: encs) {
245 if (!transcode || e.transcoder) {
246 QString name=e.name;
247 if (transcode && name.endsWith(QLatin1String(" (ffmpeg)"))) {
248 name=name.left(name.length()-9);
249 }
250 transcoderName->addItem(transcode ? tr("Transcode to %1").arg(name) : name, e.codec);
251 }
252 }
253
254 if (opts.transcoderCodec.isEmpty()) {
255 transcoderChanged();
256 } else {
257 Encoders::Encoder enc=Encoders::getEncoder(opts.transcoderCodec);
258 if (!enc.isNull()) {
259 for (int i=1; i<transcoderName->count(); ++i) {
260 if (transcoderName->itemData(i).toString()==opts.transcoderCodec) {
261 transcoderName->setCurrentIndex(i);
262 transcoderChanged();
263 transcoderValue->setValue(opts.transcoderValue);
264 break;
265 }
266 }
267 }
268 }
269 }
270 connect(transcoderName, SIGNAL(currentIndexChanged(int)), this, SLOT(transcoderChanged()));
271 connect(transcoderValue, SIGNAL(valueChanged(int)), this, SLOT(checkSaveable()));
272 } else {
273 REMOVE(transcoderFrame);
274 }
275
276 if (storage.count()<2) {
277 REMOVE(defaultVolume);
278 REMOVE(defaultVolumeLabel);
279 } else {
280 for (const DeviceStorage &ds: storage) {
281 defaultVolume->addItem(tr("%1 (%2 free)", "name (size free)")
282 .arg(ds.description).arg(Utils::formatByteSize(ds.size-ds.used)), ds.volumeIdentifier);
283 }
284
285 for (int i=0; i<defaultVolume->count(); ++i) {
286 if (defaultVolume->itemData(i).toString()==opts.volumeId) {
287 defaultVolume->setCurrentIndex(i);
288 break;
289 }
290 }
291 connect(defaultVolume,SIGNAL(currentIndexChanged(int)), this, SLOT(checkSaveable()));
292 }
293
294 origMusicFolder=Utils::fixPath(path);
295 if (props&Prop_FileName) {
296 connect(configFilename, SIGNAL(clicked()), SLOT(configureFilenameScheme()));
297 connect(filenameScheme, SIGNAL(textChanged(const QString &)), this, SLOT(checkSaveable()));
298 connect(vfatSafe, SIGNAL(stateChanged(int)), this, SLOT(checkSaveable()));
299 connect(asciiOnly, SIGNAL(stateChanged(int)), this, SLOT(checkSaveable()));
300 connect(ignoreThe, SIGNAL(stateChanged(int)), this, SLOT(checkSaveable()));
301 connect(replaceSpaces, SIGNAL(stateChanged(int)), this, SLOT(checkSaveable()));
302 }
303
304 if (albumCovers) {
305 albumCoversChanged();
306 }
307 QTimer::singleShot(0, this, SLOT(setSize()));
308 }
309
transcoderChanged()310 void DevicePropertiesWidget::transcoderChanged()
311 {
312 QString codec=transcoderName->itemData(transcoderName->currentIndex()).toString();
313 if (codec.isEmpty()) {
314 transcoderName->setToolTip(QString());
315 transcoderValue->setVisible(false);
316 if (transcoderWhen) {
317 transcoderWhen->setVisible(false);
318 }
319 } else {
320 Encoders::Encoder enc=Encoders::getEncoder(codec);
321 transcoderName->setToolTip(enc.description);
322 if (transcoderWhen) {
323 transcoderWhen->setVisible(true);
324 }
325 if (enc.values.count()) {
326 transcoderValue->setValues(enc);
327 transcoderValue->setVisible(true);
328 } else {
329 transcoderValue->setVisible(false);
330 }
331 }
332
333 Utils::resizeWindow(this, true, false);
334 checkSaveable();
335 }
336
albumCoversChanged()337 void DevicePropertiesWidget::albumCoversChanged()
338 {
339 if (coverMaxSize) {
340 bool enableSize=albumCovers->currentText()!=noCoverText;
341 coverMaxSize->setEnabled(enableSize);
342 coverMaxSizeLabel->setEnabled(enableSize);
343 }
344 checkSaveable();
345 }
346
checkSaveable()347 void DevicePropertiesWidget::checkSaveable()
348 {
349 DeviceOptions opts=settings();
350 bool checkFolder=musicFolder ? musicFolder->isEnabled() : false;
351
352 modified=opts!=origOpts;
353 if (!modified && checkFolder) {
354 modified=music()!=origMusicFolder;
355 }
356 saveable=!opts.scheme.isEmpty() && (!checkFolder || !music().isEmpty()) && !opts.coverName.isEmpty();
357 if (saveable &&
358 ( (-1!=opts.coverName.indexOf(noCoverText) && opts.coverName!=noCoverText) ||
359 (-1!=opts.coverName.indexOf(embedCoverText) && opts.coverName!=embedCoverText) ) ) {
360 saveable=false;
361 }
362 emit updated();
363 }
364
configureFilenameScheme()365 void DevicePropertiesWidget::configureFilenameScheme()
366 {
367 if (!schemeDlg) {
368 schemeDlg=new FilenameSchemeDialog(this);
369 connect(schemeDlg, SIGNAL(scheme(const QString &)), filenameScheme, SLOT(setText(const QString &)));
370 }
371 schemeDlg->show(settings());
372 }
373
settings()374 DeviceOptions DevicePropertiesWidget::settings()
375 {
376 DeviceOptions opts;
377 if (name && name->isEnabled()) {
378 opts.name=name->text().trimmed();
379 }
380 if (filenameScheme) {
381 opts.scheme=filenameScheme->text().trimmed();
382 }
383 if (vfatSafe) {
384 opts.vfatSafe=vfatSafe->isChecked();
385 }
386 if (asciiOnly) {
387 opts.asciiOnly=asciiOnly->isChecked();
388 }
389 if (ignoreThe) {
390 opts.ignoreThe=ignoreThe->isChecked();
391 }
392 if (replaceSpaces) {
393 opts.replaceSpaces=replaceSpaces->isChecked();
394 }
395 opts.fixVariousArtists=fixVariousArtists ? fixVariousArtists->isChecked() : false;
396 opts.useCache=useCache ? useCache->isChecked() : false;
397 opts.autoScan=autoScan ? autoScan->isChecked() : false;
398 opts.transcoderCodec=QString();
399 opts.transcoderValue=0;
400 opts.transcoderWhen=DeviceOptions::TW_Always;
401 opts.coverName=cover();
402 opts.coverMaxSize=(!coverMaxSize || 0==coverMaxSize->currentIndex()) ? 0 : ((coverMaxSize->count()-coverMaxSize->currentIndex())*100);
403 opts.volumeId=defaultVolume && defaultVolume->isVisible() ? defaultVolume->itemData(defaultVolume->currentIndex()).toString() : QString();
404 if (transcoderFrame && transcoderFrame->isVisible()) {
405 opts.transcoderCodec=transcoderName->itemData(transcoderName->currentIndex()).toString();
406
407 if (!opts.transcoderCodec.isEmpty()) {
408 Encoders::Encoder enc=Encoders::getEncoder(opts.transcoderCodec);
409
410 if (transcoderWhen) {
411 opts.transcoderWhen=(DeviceOptions::TranscodeWhen)transcoderWhen->itemData(transcoderWhen->currentIndex()).toUInt();
412 }
413 if (!enc.isNull() && transcoderValue->value()<enc.values.count()) {
414 opts.transcoderValue=enc.values.at(transcoderValue->value()).value;
415 }
416 }
417 }
418 return opts;
419 }
420
cover() const421 QString DevicePropertiesWidget::cover() const
422 {
423 QString coverName=albumCovers ? albumCovers->currentText().trimmed() : noCoverText;
424 return coverName==noCoverText
425 ? Device::constNoCover
426 : coverName==embedCoverText
427 ? Device::constEmbedCover
428 : coverName;
429 }
430
setSize()431 void DevicePropertiesWidget::setSize()
432 {
433 Utils::resizeWindow(this, true, false);
434 }
435
436 #include "moc_devicepropertieswidget.cpp"
437