Members of the KDE Community are recommended to subscribe to the kde-community mailing list at https://mail.kde.org/mailman/listinfo/kde-community to allow them to participate in important discussions and receive other important announcements

tageditor.cpp 18.9 KB
Newer Older
1 2 3 4 5
/***************************************************************************
                          tageditor.cpp  -  description
                             -------------------
    begin                : Sat Sep 7 2002
    copyright            : (C) 2002 by Scott Wheeler
6
    email                : wheeler@kde.org
7 8 9 10 11 12 13 14 15 16 17
 ***************************************************************************/

/***************************************************************************
 *                                                                         *
 *   This program is free software; you can redistribute it and/or modify  *
 *   it under the terms of the GNU General Public License as published by  *
 *   the Free Software Foundation; either version 2 of the License, or     *
 *   (at your option) any later version.                                   *
 *                                                                         *
 ***************************************************************************/

18 19 20 21
#include <kcombobox.h>
#include <klineedit.h>
#include <knuminput.h>
#include <keditcl.h>
22
#include <kmessagebox.h>
23
#include <kconfig.h>
24 25 26 27
#include <klocale.h>
#include <kdebug.h>

#include <qlabel.h>
28
#include <qcheckbox.h>
29 30 31
#include <qlayout.h>
#include <qdir.h>

32 33
#include <id3v1genres.h>

34
#include "tageditor.h"
35
#include "collectionlist.h"
Scott Wheeler's avatar
Scott Wheeler committed
36
#include "playlistitem.h"
37 38 39 40 41

////////////////////////////////////////////////////////////////////////////////
// public members
////////////////////////////////////////////////////////////////////////////////

42 43
TagEditor::TagEditor(QWidget *parent, const char *name) : QWidget(parent, name),
							  m_currentPlaylist(0)
44 45 46
{
    setupLayout();
    readConfig();
47
    m_dataChanged = false;
48 49 50 51 52 53 54 55 56 57 58
}

TagEditor::~TagEditor()
{
    saveConfig();
}

////////////////////////////////////////////////////////////////////////////////
// public slots
////////////////////////////////////////////////////////////////////////////////

59
void TagEditor::slotSetItems(const PlaylistItemList &list)
60 61
{
    saveChangesPrompt();
62 63 64 65 66 67 68 69 70 71 72 73 74

    if(m_currentPlaylist) {
	disconnect(m_currentPlaylist, SIGNAL(signalAboutToRemove(PlaylistItem *)),
		   this, SLOT(slotItemRemoved(PlaylistItem *)));
    }

    m_currentPlaylist = list.isEmpty() ? 0 : static_cast<Playlist *>(list.first()->listView());

    if(m_currentPlaylist) {
	connect(m_currentPlaylist, SIGNAL(signalAboutToRemove(PlaylistItem *)),
		this, SLOT(slotItemRemoved(PlaylistItem *)));
    }

75
    m_items = list;
76

77
    if(isVisible())
78
	slotRefresh();
79 80
}

81
void TagEditor::slotRefresh()
82
{
83
    // This method takes the list of currently selected m_items and tries to 
84 85
    // figure out how to show that in the tag editor.  The current strategy --
    // the most common case -- is to just process the first item.  Then we
86
    // check after that to see if there are other m_items and adjust accordingly.
87

88
    if(m_items.isEmpty() || !m_items.first()->tag()) {
89
	slotClear();
90
	setEnabled(false);
91 92 93
	return;
    }
    
94 95
    setEnabled(true);

96
    PlaylistItem *item = m_items.first();
97

98
    Tag *tag = item->tag();
99
	
100
    m_artistNameBox->setEditText(tag->artist());
101
    m_trackNameBox->setText(tag->title());
102
    m_albumNameBox->setEditText(tag->album());
103 104

    m_fileNameBox->setText(item->fileName());
105
    m_bitrateBox->setText(QString::number(tag->bitrate()));
106 107
    m_lengthBox->setText(tag->lengthString());

108 109
    if(m_genreList.findIndex(tag->genre()) >= 0)
	m_genreBox->setCurrentItem(m_genreList.findIndex(tag->genre()) + 1);
110 111
    else {
	m_genreBox->setCurrentItem(0);
112
	m_genreBox->setEditText(tag->genre());
113
    }
114

115
    m_trackSpin->setValue(tag->track());
116 117 118 119 120 121 122 123 124 125 126 127
    m_yearSpin->setValue(tag->year());
    
    m_commentBox->setText(tag->comment());
    
    // Start at the second item, since we've already processed the first.
    
    PlaylistItemList::Iterator it = m_items.begin();
    ++it;

    // If there is more than one item in the m_items that we're dealing with...
    
    if(it != m_items.end()) {
128 129 130 131

	QValueListIterator<QWidget *> hideIt = m_hideList.begin();
	for(; hideIt != m_hideList.end(); ++hideIt)
	    (*hideIt)->hide();
Scott Wheeler's avatar
Scott Wheeler committed
132 133 134

	BoxMap::Iterator boxIt = m_enableBoxes.begin();
	for(; boxIt != m_enableBoxes.end(); boxIt++) {
135 136
	    (*boxIt)->setChecked(true);
	    (*boxIt)->show();
137 138
	}
	
139 140 141
	// Yep, this is ugly.  Loop through all of the files checking to see
	// if their fields are the same.  If so, by default, enable their 
	// checkbox.
142
	
143
	// Also, if there are more than 50 m_items, don't scan all of them.
144
	
145 146 147 148 149 150 151 152
	if(m_items.count() > 50) {
	    m_enableBoxes[m_artistNameBox]->setChecked(false);
	    m_enableBoxes[m_trackNameBox]->setChecked(false);
	    m_enableBoxes[m_albumNameBox]->setChecked(false);
	    m_enableBoxes[m_genreBox]->setChecked(false);
	    m_enableBoxes[m_trackSpin]->setChecked(false);
	    m_enableBoxes[m_yearSpin]->setChecked(false);
	    m_enableBoxes[m_commentBox]->setChecked(false);
153 154
	}
	else {
155 156
	    for(; it != m_items.end(); ++it) {
		tag = (*it)->tag();
157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201

		if(tag) {

		    if(m_artistNameBox->currentText() != tag->artist() &&
		       m_enableBoxes.contains(m_artistNameBox))
		    {
			m_artistNameBox->lineEdit()->clear();
			m_enableBoxes[m_artistNameBox]->setChecked(false);
		    }
		    if(m_trackNameBox->text() != tag->title() &&
		       m_enableBoxes.contains(m_trackNameBox))
		    {
			m_trackNameBox->clear();
			m_enableBoxes[m_trackNameBox]->setChecked(false);
		    }
		    if(m_albumNameBox->currentText() != tag->album() &&
		       m_enableBoxes.contains(m_albumNameBox))
		    {
			m_albumNameBox->lineEdit()->clear();
			m_enableBoxes[m_albumNameBox]->setChecked(false);
		    }
		    if(m_genreBox->currentText() != tag->genre() &&
		       m_enableBoxes.contains(m_genreBox))
		    {
			m_genreBox->lineEdit()->clear();
			m_enableBoxes[m_genreBox]->setChecked(false);
		    }		
		    if(m_trackSpin->value() != tag->track() &&
		       m_enableBoxes.contains(m_trackSpin))
		    {
			m_trackSpin->setValue(0);
			m_enableBoxes[m_trackSpin]->setChecked(false);
		    }		
		    if(m_yearSpin->value() != tag->year() &&
		       m_enableBoxes.contains(m_yearSpin))
		    {
			m_yearSpin->setValue(0);
			m_enableBoxes[m_yearSpin]->setChecked(false);
		    }
		    if(m_commentBox->text() != tag->comment() &&
		       m_enableBoxes.contains(m_commentBox))
		    {
			m_commentBox->clear();
			m_enableBoxes[m_commentBox]->setChecked(false);
		    }
202
		}
203 204
	    }
	}
205
    }
206 207
    else {
	// Clean up in the case that we are only handling one item.
208 209 210 211

	QValueListIterator<QWidget *> showIt = m_hideList.begin();
	for(; showIt != m_hideList.end(); ++showIt)
	    (*showIt)->show();
Scott Wheeler's avatar
Scott Wheeler committed
212 213 214

	BoxMap::iterator boxIt = m_enableBoxes.begin();
	for(; boxIt != m_enableBoxes.end(); boxIt++) {
215 216 217 218 219
	    (*boxIt)->setChecked(true);
	    (*boxIt)->hide();
	}
    }
    m_dataChanged = false;
220 221
}

222
void TagEditor::slotClear()
223
{
224 225 226 227 228 229 230 231 232 233
    m_artistNameBox->lineEdit()->clear();
    m_trackNameBox->clear();
    m_albumNameBox->lineEdit()->clear();
    m_genreBox->setCurrentItem(0);
    m_fileNameBox->clear();
    m_trackSpin->setValue(0);
    m_yearSpin->setValue(0);
    m_lengthBox->clear();
    m_bitrateBox->clear();
    m_commentBox->clear();    
234 235
}

236
void TagEditor::slotUpdateCollection()
237
{
238 239 240 241 242
    CollectionList *list = CollectionList::instance();

    if(!list)
	return;
    
243 244
    if(m_artistNameBox->listBox()) {
        m_artistNameBox->listBox()->clear();
245 246 247 248 249
	
	// This is another case where a sorted value list would be useful.  It's
	// silly to build and maintain unsorted lists and have to call sort 
	// every time that you want to verify that a list is sorted.	

250
	QStringList artistList = list->uniqueSet(CollectionList::Artists);
251 252
	artistList.sort();

253 254
        m_artistNameBox->listBox()->insertStringList(artistList);
	m_artistNameBox->completionObject()->setItems(artistList);
255 256
    }

257 258
    if(m_albumNameBox->listBox()) {
        m_albumNameBox->listBox()->clear();
259

260
	QStringList albumList = list->uniqueSet(CollectionList::Albums);
261 262
	albumList.sort();

263 264
        m_albumNameBox->listBox()->insertStringList(albumList);
	m_albumNameBox->completionObject()->setItems(albumList);
265 266 267 268 269 270 271
    }    
}

////////////////////////////////////////////////////////////////////////////////
// private members
////////////////////////////////////////////////////////////////////////////////

272
void TagEditor::readConfig()
273
{
274 275
    KConfig *config = KGlobal::config();
    { // combo box completion modes
276 277 278
	KConfigGroupSaver saver(config, "TagEditor");
	if(m_artistNameBox && m_albumNameBox) {
	    KGlobalSettings::Completion artistNameBoxMode = 
Scott Wheeler's avatar
Scott Wheeler committed
279 280
		KGlobalSettings::Completion(config->readNumEntry("ArtistNameBoxMode",
								 KGlobalSettings::CompletionAuto));
281
	    m_artistNameBox->setCompletionMode(artistNameBoxMode);
282
	    
283
	    KGlobalSettings::Completion albumNameBoxMode = 
Scott Wheeler's avatar
Scott Wheeler committed
284 285
		KGlobalSettings::Completion(config->readNumEntry("AlbumNameBoxMode",
								 KGlobalSettings::CompletionAuto));
286
	    m_albumNameBox->setCompletionMode(albumNameBoxMode);
287 288
        }
    }
289

290
    TagLib::StringList genres = TagLib::ID3v1::genreList();
291

292 293 294
    for(TagLib::StringList::ConstIterator it = genres.begin(); it != genres.end(); ++it)
	m_genreList.append(TStringToQString((*it)));
    m_genreList.sort();
295

296 297 298
    m_genreBox->clear();
    m_genreBox->insertItem(QString::null);
    m_genreBox->insertStringList(m_genreList);
299
}
300

301 302 303 304 305
void TagEditor::saveConfig()
{
    KConfig *config = KGlobal::config();
    { // combo box completion modes
        KConfigGroupSaver saver(config, "TagEditor");
306 307 308
        if(m_artistNameBox && m_albumNameBox) {
	    config->writeEntry("ArtistNameBoxMode", m_artistNameBox->completionMode());
	    config->writeEntry("AlbumNameBoxMode", m_albumNameBox->completionMode());
309 310
        }
    }
311

312
}
313

314 315
void TagEditor::setupLayout()
{
316 317
    static const int horizontalSpacing = 10;
    static const int verticalSpacing = 2;
318

319
    QHBoxLayout *layout = new QHBoxLayout(this, 2, horizontalSpacing);
320 321 322 323 324 325 326 327 328 329 330 331 332 333

    //////////////////////////////////////////////////////////////////////////////
    // define two columns of the bottem layout
    //////////////////////////////////////////////////////////////////////////////
    QVBoxLayout *leftColumnLayout = new QVBoxLayout(layout, verticalSpacing);
    QVBoxLayout *rightColumnLayout = new QVBoxLayout(layout, verticalSpacing);

    layout->setStretchFactor(leftColumnLayout, 2);
    layout->setStretchFactor(rightColumnLayout, 3);

    //////////////////////////////////////////////////////////////////////////////
    // put stuff in the left column -- all of the field names are class wide
    //////////////////////////////////////////////////////////////////////////////
    { // just for organization
334 335
        m_artistNameBox = new KComboBox(true, this, "artistNameBox");
	m_artistNameBox->setCompletionMode(KGlobalSettings::CompletionAuto);
336
	addItem(i18n("&Artist name:"), m_artistNameBox, leftColumnLayout);
337

338
        m_trackNameBox = new KLineEdit(this, "trackNameBox");
339
	addItem(i18n("&Track name:"), m_trackNameBox, leftColumnLayout);
340

341 342
        m_albumNameBox = new KComboBox(true, this, "albumNameBox");
	m_albumNameBox->setCompletionMode(KGlobalSettings::CompletionAuto);
343
	addItem(i18n("Album &name:"), m_albumNameBox, leftColumnLayout);
344

345
        m_genreBox = new KComboBox(true, this, "genreBox");
346
	addItem(i18n("&Genre:"), m_genreBox, leftColumnLayout);
347 348

        // this fills the space at the bottem of the left column
Scott Wheeler's avatar
Scott Wheeler committed
349 350
        leftColumnLayout->addItem(new QSpacerItem(0, 0, QSizePolicy::Minimum,
						  QSizePolicy::Expanding));
351 352 353 354 355 356
    }
    //////////////////////////////////////////////////////////////////////////////
    // put stuff in the right column
    //////////////////////////////////////////////////////////////////////////////
    { // just for organization

357
        m_fileNameBox = new KLineEdit(this, "fileNameBox");
Scott Wheeler's avatar
Scott Wheeler committed
358 359 360
	QWidget *fileNameLabel = addHidden(new QLabel(m_fileNameBox, i18n("&File name:"), this));

        rightColumnLayout->addWidget(fileNameLabel);
361
        rightColumnLayout->addWidget(addHidden(m_fileNameBox));
362 363

        { // lay out the track row
Scott Wheeler's avatar
Scott Wheeler committed
364 365
            QHBoxLayout *trackRowLayout = new QHBoxLayout(rightColumnLayout,
							  horizontalSpacing);
366

367
            m_trackSpin = new KIntSpinBox(0, 255, 1, 0, 10, this, "trackSpin");
368
	    addItem(i18n("T&rack:"), m_trackSpin, trackRowLayout);
369
	    m_trackSpin->installEventFilter(this);
370

Scott Wheeler's avatar
Scott Wheeler committed
371 372
            trackRowLayout->addItem(new QSpacerItem(0, 0, QSizePolicy::Expanding,
						    QSizePolicy::Minimum));
373

374
            m_yearSpin = new KIntSpinBox(0, 9999, 1, 0, 10, this, "yearSpin");
375
	    addItem(i18n("&Year:"), m_yearSpin, trackRowLayout);
376
	    m_yearSpin->installEventFilter(this);
377

Scott Wheeler's avatar
Scott Wheeler committed
378 379
            trackRowLayout->addItem(new QSpacerItem(0, 0, QSizePolicy::Expanding,
						    QSizePolicy::Minimum));
380

381
            trackRowLayout->addWidget(addHidden(new QLabel(i18n("Length:"), this)));
382 383 384
            m_lengthBox = new KLineEdit(this, "lengthBox");
            m_lengthBox->setMaximumWidth(50);
            m_lengthBox->setReadOnly(true);
385
            trackRowLayout->addWidget(addHidden(m_lengthBox));
386

Scott Wheeler's avatar
Scott Wheeler committed
387 388
            trackRowLayout->addItem(new QSpacerItem(0, 0, QSizePolicy::Expanding,
						    QSizePolicy::Minimum));
389

390
            trackRowLayout->addWidget(addHidden(new QLabel(i18n("Bitrate:"), this)));
391 392 393
            m_bitrateBox = new KLineEdit(this, "bitrateBox");
            m_bitrateBox->setMaximumWidth(50);
            m_bitrateBox->setReadOnly(true);
394
            trackRowLayout->addWidget(addHidden(m_bitrateBox));
395 396
        }

397 398
        m_commentBox = new KEdit(this, "commentBox");
	m_commentBox->setTextFormat(Qt::PlainText);
399
	addItem(i18n("&Comment:"), m_commentBox, rightColumnLayout);
400 401
    }

Scott Wheeler's avatar
Scott Wheeler committed
402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427
    connect(m_artistNameBox, SIGNAL(textChanged(const QString&)),
	    this, SLOT(slotDataChanged()));

    connect(m_trackNameBox, SIGNAL(textChanged(const QString&)),
	    this, SLOT(slotDataChanged()));

    connect(m_albumNameBox, SIGNAL(textChanged(const QString&)),
	    this, SLOT(slotDataChanged()));

    connect(m_genreBox, SIGNAL(activated(int)),
	    this, SLOT(slotDataChanged()));

    connect(m_genreBox, SIGNAL(textChanged(const QString&)),
	    this, SLOT(slotDataChanged()));

    connect(m_fileNameBox, SIGNAL(textChanged(const QString&)),
	    this, SLOT(slotDataChanged()));

    connect(m_yearSpin, SIGNAL(valueChanged(int)),
	    this, SLOT(slotDataChanged()));

    connect(m_trackSpin, SIGNAL(valueChanged(int)),
	    this, SLOT(slotDataChanged()));

    connect(m_commentBox, SIGNAL(textChanged()),
	    this, SLOT(slotDataChanged()));
428 429 430 431
}

void TagEditor::save(const PlaylistItemList &list)
{
432
    if(!list.isEmpty() && m_dataChanged) {
433 434 435 436 437 438 439
	
	KApplication::setOverrideCursor(Qt::waitCursor);

	// To keep track of the files that don't cooperate...

	QStringList errorFiles;
	
440
	for(PlaylistItemList::ConstIterator it = list.begin(); it != list.end(); ++it) {
441 442
	    PlaylistItem *item = *it;
	    
443
	    QFileInfo newFile(item->dirPath() + QDir::separator() + m_fileNameBox->text());
444 445 446 447 448 449 450
	    QFileInfo directory(item->dirPath());
	    
	    // If (the new file is writable or the new file doesn't exist and
	    // it's directory is writable) and the old file is writable...  
	    // If not we'll append it to errorFiles to tell the user which
	    // files we couldn't write to.
	    
451 452
	    if(item->tag() &&
	       (newFile.isWritable() || (!newFile.exists() && directory.isWritable())) &&
Scott Wheeler's avatar
Scott Wheeler committed
453 454
	       item->isWritable())
	    {
455 456 457 458 459
		
		// If the file name in the box doesn't match the current file
		// name...
		
		if(list.count() == 1 && item->fileName() != newFile.fileName()) {
460
		    
461 462
		    // Rename the file if it doesn't exist or the user says
		    // that it's ok.
463
		    
464
		    if(!newFile.exists() ||
Scott Wheeler's avatar
Scott Wheeler committed
465 466 467 468
		       KMessageBox::warningYesNo(
			   this, 
			   i18n("This file already exists.\nDo you want to replace it?"),
			   i18n("File Exists")) == KMessageBox::Yes)
469 470 471 472
		    {
			QDir currentDir;
			currentDir.rename(item->filePath(), newFile.filePath());
			item->setFile(newFile.filePath());
473 474
		    }
		}
475 476 477 478 479 480 481
		
		// A bit more ugliness.  If there are multiple files that are
		// being modified, they each have a "enabled" checkbox that
		// says if that field is to be respected for the multiple 
		// files.  We have to check to see if that is enabled before
		// each field that we write.
		
482 483 484
		if(m_enableBoxes[m_artistNameBox]->isOn())
		    item->tag()->setArtist(m_artistNameBox->currentText());
		if(m_enableBoxes[m_trackNameBox]->isOn())
485
		    item->tag()->setTitle(m_trackNameBox->text());
486 487 488
		if(m_enableBoxes[m_albumNameBox]->isOn())
		    item->tag()->setAlbum(m_albumNameBox->currentText());
		if(m_enableBoxes[m_trackSpin]->isOn())
489
		    item->tag()->setTrack(m_trackSpin->value());
490 491 492 493
		if(m_enableBoxes[m_yearSpin]->isOn())
		    item->tag()->setYear(m_yearSpin->value());
		if(m_enableBoxes[m_commentBox]->isOn())
		    item->tag()->setComment(m_commentBox->text());
494
		
495 496
		if(m_enableBoxes[m_genreBox]->isOn())
		    item->tag()->setGenre(m_genreBox->currentText());
497 498 499
		
		item->tag()->save();
		
500
		item->slotRefresh();
501
	    }
502 503
	    else
		errorFiles.append(item->fileName());
504 505

	    kapp->processEvents();
506 507 508 509 510
	}
	
	if(!errorFiles.isEmpty())
	    KMessageBox::detailedSorry(this,
				       i18n("Could not save to specified file(s)."), 
511
				       i18n("Could Not Write to:\n") + errorFiles.join("\n"));
512
	m_dataChanged = false;
513 514

	KApplication::restoreOverrideCursor();
515 516 517 518 519
    }
}

void TagEditor::saveChangesPrompt()
{
520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536
    if(!isVisible() || !m_dataChanged || m_items.isEmpty())
	return;

    QStringList files;

    for(PlaylistItemList::Iterator it = m_items.begin(); it != m_items.end(); it++)
	files.append((*it)->fileName());

    if(KMessageBox::questionYesNoList(this,
				      i18n("Do you want to save your changes to:\n"), 
				      files, 
				      i18n("Save Changes"),
				      KStdGuiItem::yes(),
				      KStdGuiItem::no(),
				      "tagEditor_showSaveChangesBox") == KMessageBox::Yes)
    {
	save(m_items);
537 538 539
    }
}

540 541 542 543 544
void TagEditor::addItem(const QString &text, QWidget *item, QBoxLayout *layout)
{
    if(!item || !layout)
	return;

545
    QLabel *label = new QLabel(item, text, this);
546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573

    QCheckBox *enableBox = new QCheckBox(i18n("Enable"), this);
    enableBox->setChecked(true);

    label->setMinimumHeight(enableBox->height());

    if(layout->direction() == QBoxLayout::LeftToRight) {
	layout->addWidget(label);
	layout->addWidget(item);
	layout->addWidget(enableBox);
    }
    else {
	QHBoxLayout *l = new QHBoxLayout(layout);

	l->addWidget(label);
	l->setStretchFactor(label, 1);

	l->insertStretch(-1, 1);

	l->addWidget(enableBox);
	l->setStretchFactor(enableBox, 0);

	layout->addWidget(item);
    }

    enableBox->hide();

    connect(enableBox, SIGNAL(toggled(bool)), item, SLOT(setEnabled(bool)));
574
    m_enableBoxes.insert(item, enableBox);
575 576
}

577 578
void TagEditor::showEvent(QShowEvent *e)
{
579
    slotRefresh();
580 581 582
    QWidget::showEvent(e);
}

583 584 585 586 587 588 589 590
bool TagEditor::eventFilter(QObject *watched, QEvent *e)
{
    if(watched->inherits("QSpinBox") && e->type() == QEvent::KeyRelease)
	slotDataChanged();

    return false;
}

591 592 593 594
////////////////////////////////////////////////////////////////////////////////
// private slots
////////////////////////////////////////////////////////////////////////////////

595
void TagEditor::slotDataChanged(bool c)
596
{
597
    m_dataChanged = c;
598 599
}

600 601 602 603 604 605 606
void TagEditor::slotItemRemoved(PlaylistItem *item)
{
    m_items.remove(item);
    if(m_items.isEmpty())
	slotRefresh();
}

607
#include "tageditor.moc"