/*************************************************************************** begin : Sat Sep 7 2002 copyright : (C) 2002 - 2004 by Scott Wheeler email : wheeler@kde.org ***************************************************************************/ /*************************************************************************** * * * 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. * * * ***************************************************************************/ #include "tageditor.h" #include "collectionlist.h" #include "playlistitem.h" #include "tag.h" #include "actioncollection.h" #include "tagtransactionmanager.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #undef KeyRelease using namespace ActionCollection; class FileNameValidator : public TQValidator { public: FileNameValidator(TQObject *parent, const char *name = 0) : TQValidator(parent, name) {} virtual void fixup(TQString &s) const { s.remove('/'); } virtual State validate(TQString &s, int &) const { if(s.find('/') != -1) return Invalid; return Acceptable; } }; class FileBoxToolTip : public TQToolTip { public: FileBoxToolTip(TagEditor *editor, TQWidget *widget) : TQToolTip(widget), m_editor(editor) {} protected: virtual void maybeTip(const TQPoint &) { tip(parentWidget()->rect(), m_editor->items().first()->file().absFilePath()); } private: TagEditor *m_editor; }; class FixedHLayout : public TQHBoxLayout { public: FixedHLayout(TQWidget *parent, int margin = 0, int spacing = -1, const char *name = 0) : TQHBoxLayout(parent, margin, spacing, name), m_width(-1) {} FixedHLayout(TQLayout *parentLayout, int spacing = -1, const char *name = 0) : TQHBoxLayout(parentLayout, spacing, name), m_width(-1) {} void setWidth(int w = -1) { m_width = w == -1 ? TQHBoxLayout::minimumSize().width() : w; } virtual TQSize minimumSize() const { TQSize s = TQHBoxLayout::minimumSize(); s.setWidth(m_width); return s; } private: int m_width; }; class CollectionObserver : public PlaylistObserver { public: CollectionObserver(TagEditor *parent) : PlaylistObserver(CollectionList::instance()), m_parent(parent) { } virtual void updateData() { if(m_parent && m_parent->m_currentPlaylist && m_parent->isVisible()) m_parent->slotSetItems(m_parent->m_currentPlaylist->selectedItems()); } virtual void updateCurrent() {} private: TagEditor *m_parent; }; //////////////////////////////////////////////////////////////////////////////// // public members //////////////////////////////////////////////////////////////////////////////// TagEditor::TagEditor(TQWidget *parent, const char *name) : TQWidget(parent, name), m_currentPlaylist(0), m_observer(0), m_performingSave(false) { setupActions(); setupLayout(); readConfig(); m_dataChanged = false; m_collectionChanged = false; } TagEditor::~TagEditor() { delete m_observer; saveConfig(); } void TagEditor::setupObservers() { m_observer = new CollectionObserver(this); } //////////////////////////////////////////////////////////////////////////////// // public slots //////////////////////////////////////////////////////////////////////////////// void TagEditor::slotSetItems(const PlaylistItemList &list) { if(m_performingSave) return; // Store the playlist that we're setting because saveChangesPrompt // can delete the PlaylistItems in list. Playlist *itemPlaylist = 0; if(!list.isEmpty()) itemPlaylist = list.first()->playlist(); bool hadPlaylist = m_currentPlaylist != 0; saveChangesPrompt(); if(m_currentPlaylist) { disconnect(m_currentPlaylist, TQT_SIGNAL(signalAboutToRemove(PlaylistItem *)), this, TQT_SLOT(slotItemRemoved(PlaylistItem *))); } if(hadPlaylist && !m_currentPlaylist || !itemPlaylist) { m_currentPlaylist = 0; m_items.clear(); } else { m_currentPlaylist = itemPlaylist; // We can't use list here, it may not be valid m_items = itemPlaylist->selectedItems(); } if(m_currentPlaylist) { connect(m_currentPlaylist, TQT_SIGNAL(signalAboutToRemove(PlaylistItem *)), this, TQT_SLOT(slotItemRemoved(PlaylistItem *))); connect(m_currentPlaylist, TQT_SIGNAL(destroyed()), this, TQT_SLOT(slotPlaylistRemoved())); } if(isVisible()) slotRefresh(); else m_collectionChanged = true; } void TagEditor::slotRefresh() { // This method takes the list of currently selected m_items and tries to // 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 // check after that to see if there are other m_items and adjust accordingly. if(m_items.isEmpty() || !m_items.first()->file().tag()) { slotClear(); setEnabled(false); return; } setEnabled(true); PlaylistItem *item = m_items.first(); Q_ASSERT(item); Tag *tag = item->file().tag(); TQFileInfo fi(item->file().absFilePath()); if(!fi.isWritable() && m_items.count() == 1) setEnabled(false); m_artistNameBox->setEditText(tag->artist()); m_trackNameBox->setText(tag->title()); m_albumNameBox->setEditText(tag->album()); m_fileNameBox->setText(item->file().fileInfo().fileName()); new FileBoxToolTip(this, m_fileNameBox); m_bitrateBox->setText(TQString::number(tag->bitrate())); m_lengthBox->setText(tag->lengthString()); if(m_genreList.findIndex(tag->genre()) >= 0) m_genreBox->setCurrentItem(m_genreList.findIndex(tag->genre()) + 1); else { m_genreBox->setCurrentItem(0); m_genreBox->setEditText(tag->genre()); } m_trackSpin->setValue(tag->track()); 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()) { TQValueListIterator hideIt = m_hideList.begin(); for(; hideIt != m_hideList.end(); ++hideIt) (*hideIt)->hide(); BoxMap::Iterator boxIt = m_enableBoxes.begin(); for(; boxIt != m_enableBoxes.end(); boxIt++) { (*boxIt)->setChecked(true); (*boxIt)->show(); } // 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. // Also, if there are more than 50 m_items, don't scan all of them. 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); } else { for(; it != m_items.end(); ++it) { tag = (*it)->file().tag(); 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); } } } } } else { // Clean up in the case that we are only handling one item. TQValueListIterator showIt = m_hideList.begin(); for(; showIt != m_hideList.end(); ++showIt) (*showIt)->show(); BoxMap::iterator boxIt = m_enableBoxes.begin(); for(; boxIt != m_enableBoxes.end(); boxIt++) { (*boxIt)->setChecked(true); (*boxIt)->hide(); } } m_dataChanged = false; } void TagEditor::slotClear() { 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(); } void TagEditor::slotUpdateCollection() { if(isVisible()) updateCollection(); else m_collectionChanged = true; } void TagEditor::updateCollection() { m_collectionChanged = false; CollectionList *list = CollectionList::instance(); if(!list) return; TQStringList artistList = list->uniqueSet(CollectionList::Artists); artistList.sort(); m_artistNameBox->listBox()->clear(); m_artistNameBox->listBox()->insertStringList(artistList); m_artistNameBox->completionObject()->setItems(artistList); TQStringList albumList = list->uniqueSet(CollectionList::Albums); albumList.sort(); m_albumNameBox->listBox()->clear(); m_albumNameBox->listBox()->insertStringList(albumList); m_albumNameBox->completionObject()->setItems(albumList); // Merge the list of genres found in tags with the standard ID3v1 set. StringHash genreHash; m_genreList = list->uniqueSet(CollectionList::Genres); for(TQStringList::ConstIterator it = m_genreList.begin(); it != m_genreList.end(); ++it) genreHash.insert(*it); TagLib::StringList genres = TagLib::ID3v1::genreList(); for(TagLib::StringList::ConstIterator it = genres.begin(); it != genres.end(); ++it) genreHash.insert(TStringToQString((*it))); m_genreList = genreHash.values(); m_genreList.sort(); m_genreBox->listBox()->clear(); m_genreBox->listBox()->insertItem(TQString()); m_genreBox->listBox()->insertStringList(m_genreList); m_genreBox->completionObject()->setItems(m_genreList); } //////////////////////////////////////////////////////////////////////////////// // private members //////////////////////////////////////////////////////////////////////////////// void TagEditor::readConfig() { // combo box completion modes TDEConfigGroup config(TDEGlobal::config(), "TagEditor"); if(m_artistNameBox && m_albumNameBox) { readCompletionMode(&config, m_artistNameBox, "ArtistNameBoxMode"); readCompletionMode(&config, m_albumNameBox, "AlbumNameBoxMode"); readCompletionMode(&config, m_genreBox, "GenreBoxMode"); } bool show = config.readBoolEntry("Show", false); action("showEditor")->setChecked(show); setShown(show); TagLib::StringList genres = TagLib::ID3v1::genreList(); for(TagLib::StringList::ConstIterator it = genres.begin(); it != genres.end(); ++it) m_genreList.append(TStringToQString((*it))); m_genreList.sort(); m_genreBox->clear(); m_genreBox->insertItem(TQString()); m_genreBox->insertStringList(m_genreList); m_genreBox->completionObject()->setItems(m_genreList); } void TagEditor::readCompletionMode(TDEConfigBase *config, KComboBox *box, const TQString &key) { TDEGlobalSettings::Completion mode = TDEGlobalSettings::Completion(config->readNumEntry(key, TDEGlobalSettings::CompletionAuto)); box->setCompletionMode(mode); } void TagEditor::saveConfig() { // combo box completion modes TDEConfigGroup config(TDEGlobal::config(), "TagEditor"); if(m_artistNameBox && m_albumNameBox) { config.writeEntry("ArtistNameBoxMode", m_artistNameBox->completionMode()); config.writeEntry("AlbumNameBoxMode", m_albumNameBox->completionMode()); config.writeEntry("GenreBoxMode", m_genreBox->completionMode()); } config.writeEntry("Show", action("showEditor")->isChecked()); } void TagEditor::setupActions() { TDEToggleAction *show = new TDEToggleAction(i18n("Show &Tag Editor"), "edit", 0, ActionCollection::actions(), "showEditor"); show->setCheckedState(i18n("Hide &Tag Editor")); connect(show, TQT_SIGNAL(toggled(bool)), this, TQT_SLOT(setShown(bool))); new TDEAction(i18n("&Save"), "document-save", "CTRL+t", TQT_TQOBJECT(this), TQT_SLOT(slotSave()), ActionCollection::actions(), "saveItem"); } void TagEditor::setupLayout() { static const int horizontalSpacing = 12; static const int verticalSpacing = 2; TQHBoxLayout *layout = new TQHBoxLayout(this, 6, horizontalSpacing); ////////////////////////////////////////////////////////////////////////////// // define two columns of the bottem layout ////////////////////////////////////////////////////////////////////////////// TQVBoxLayout *leftColumnLayout = new TQVBoxLayout(layout, verticalSpacing); TQVBoxLayout *rightColumnLayout = new TQVBoxLayout(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 m_artistNameBox = new KComboBox(true, this, "artistNameBox"); m_artistNameBox->setCompletionMode(TDEGlobalSettings::CompletionAuto); addItem(i18n("&Artist name:"), m_artistNameBox, leftColumnLayout, "preferences-desktop-personal"); m_trackNameBox = new KLineEdit(this, "trackNameBox"); addItem(i18n("&Track name:"), m_trackNameBox, leftColumnLayout, "media-playback-start"); m_albumNameBox = new KComboBox(true, this, "albumNameBox"); m_albumNameBox->setCompletionMode(TDEGlobalSettings::CompletionAuto); addItem(i18n("Album &name:"), m_albumNameBox, leftColumnLayout, "media-optical-cdrom"); m_genreBox = new KComboBox(true, this, "genreBox"); addItem(i18n("&Genre:"), m_genreBox, leftColumnLayout, "knotify"); // this fills the space at the bottem of the left column leftColumnLayout->addItem(new TQSpacerItem(0, 0, TQSizePolicy::Minimum, TQSizePolicy::Expanding)); } ////////////////////////////////////////////////////////////////////////////// // put stuff in the right column ////////////////////////////////////////////////////////////////////////////// { // just for organization TQHBoxLayout *fileNameLayout = new TQHBoxLayout(rightColumnLayout, horizontalSpacing); m_fileNameBox = new KLineEdit(this, "fileNameBox"); m_fileNameBox->setValidator(new FileNameValidator(TQT_TQOBJECT(m_fileNameBox))); TQLabel *fileNameIcon = new TQLabel(this); fileNameIcon->setPixmap(SmallIcon("audio-x-generic")); TQWidget *fileNameLabel = addHidden(new TQLabel(m_fileNameBox, i18n("&File name:"), this)); fileNameLayout->addWidget(addHidden(fileNameIcon)); fileNameLayout->addWidget(fileNameLabel); fileNameLayout->setStretchFactor(fileNameIcon, 0); fileNameLayout->setStretchFactor(fileNameLabel, 0); fileNameLayout->insertStretch(-1, 1); rightColumnLayout->addWidget(addHidden(m_fileNameBox)); { // lay out the track row FixedHLayout *trackRowLayout = new FixedHLayout(rightColumnLayout, horizontalSpacing); m_trackSpin = new KIntSpinBox(0, 9999, 1, 0, 10, this, "trackSpin"); addItem(i18n("T&rack:"), m_trackSpin, trackRowLayout); m_trackSpin->installEventFilter(this); trackRowLayout->addItem(new TQSpacerItem(0, 0, TQSizePolicy::Expanding, TQSizePolicy::Minimum)); m_yearSpin = new KIntSpinBox(0, 9999, 1, 0, 10, this, "yearSpin"); addItem(i18n("&Year:"), m_yearSpin, trackRowLayout); m_yearSpin->installEventFilter(this); trackRowLayout->addItem(new TQSpacerItem(0, 0, TQSizePolicy::Expanding, TQSizePolicy::Minimum)); trackRowLayout->addWidget(addHidden(new TQLabel(i18n("Length:"), this))); m_lengthBox = new KLineEdit(this, "lengthBox"); // addItem(i18n("Length:"), m_lengthBox, trackRowLayout); m_lengthBox->setMinimumWidth(fontMetrics().width("00:00") + trackRowLayout->spacing()); m_lengthBox->setMaximumWidth(50); m_lengthBox->setAlignment(TQt::AlignRight); m_lengthBox->setReadOnly(true); trackRowLayout->addWidget(addHidden(m_lengthBox)); trackRowLayout->addItem(new TQSpacerItem(0, 0, TQSizePolicy::Expanding, TQSizePolicy::Minimum)); trackRowLayout->addWidget(addHidden(new TQLabel(i18n("Bitrate:"), this))); m_bitrateBox = new KLineEdit(this, "bitrateBox"); // addItem(i18n("Bitrate:"), m_bitrateBox, trackRowLayout); m_bitrateBox->setMinimumWidth(fontMetrics().width("000") + trackRowLayout->spacing()); m_bitrateBox->setMaximumWidth(50); m_bitrateBox->setAlignment(TQt::AlignRight); m_bitrateBox->setReadOnly(true); trackRowLayout->addWidget(addHidden(m_bitrateBox)); trackRowLayout->setWidth(); } m_commentBox = new KEdit(this, "commentBox"); m_commentBox->setTextFormat(TQt::PlainText); addItem(i18n("&Comment:"), m_commentBox, rightColumnLayout, "edit"); fileNameLabel->setMinimumHeight(m_trackSpin->height()); } connect(m_artistNameBox, TQT_SIGNAL(textChanged(const TQString&)), this, TQT_SLOT(slotDataChanged())); connect(m_trackNameBox, TQT_SIGNAL(textChanged(const TQString&)), this, TQT_SLOT(slotDataChanged())); connect(m_albumNameBox, TQT_SIGNAL(textChanged(const TQString&)), this, TQT_SLOT(slotDataChanged())); connect(m_genreBox, TQT_SIGNAL(activated(int)), this, TQT_SLOT(slotDataChanged())); connect(m_genreBox, TQT_SIGNAL(textChanged(const TQString&)), this, TQT_SLOT(slotDataChanged())); connect(m_fileNameBox, TQT_SIGNAL(textChanged(const TQString&)), this, TQT_SLOT(slotDataChanged())); connect(m_yearSpin, TQT_SIGNAL(valueChanged(int)), this, TQT_SLOT(slotDataChanged())); connect(m_trackSpin, TQT_SIGNAL(valueChanged(int)), this, TQT_SLOT(slotDataChanged())); connect(m_commentBox, TQT_SIGNAL(textChanged()), this, TQT_SLOT(slotDataChanged())); } void TagEditor::save(const PlaylistItemList &list) { if(!list.isEmpty() && m_dataChanged) { TDEApplication::setOverrideCursor(TQt::waitCursor); m_dataChanged = false; m_performingSave = true; // The list variable can become corrupted if the playlist holding its // items dies, which is possible as we edit tags. So we need to copy // the end marker. PlaylistItemList::ConstIterator end = list.end(); for(PlaylistItemList::ConstIterator it = list.begin(); it != end; /* Deliberatly missing */ ) { // Process items before we being modifying tags, as the dynamic // playlists will try to modify the file we edit if the tag changes // due to our alterations here. kapp->eventLoop()->processEvents(TQEventLoop::ExcludeUserInput); PlaylistItem *item = *it; // The playlist can be deleted from under us if this is the last // item and we edit it so that it doesn't match the search, which // means we can't increment the iterator, so let's do it now. ++it; TQString fileName = item->file().fileInfo().dirPath() + TQDir::separator() + m_fileNameBox->text(); if(list.count() > 1) fileName = item->file().fileInfo().absFilePath(); Tag *tag = TagTransactionManager::duplicateTag(item->file().tag(), fileName); // 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. if(m_enableBoxes[m_artistNameBox]->isOn()) tag->setArtist(m_artistNameBox->currentText()); if(m_enableBoxes[m_trackNameBox]->isOn()) tag->setTitle(m_trackNameBox->text()); if(m_enableBoxes[m_albumNameBox]->isOn()) tag->setAlbum(m_albumNameBox->currentText()); if(m_enableBoxes[m_trackSpin]->isOn()) { if(m_trackSpin->text().isEmpty()) m_trackSpin->setValue(0); tag->setTrack(m_trackSpin->value()); } if(m_enableBoxes[m_yearSpin]->isOn()) { if(m_yearSpin->text().isEmpty()) m_yearSpin->setValue(0); tag->setYear(m_yearSpin->value()); } if(m_enableBoxes[m_commentBox]->isOn()) tag->setComment(m_commentBox->text()); if(m_enableBoxes[m_genreBox]->isOn()) tag->setGenre(m_genreBox->currentText()); TagTransactionManager::instance()->changeTagOnItem(item, tag); } TagTransactionManager::instance()->commit(); CollectionList::instance()->dataChanged(); m_performingSave = false; TDEApplication::restoreOverrideCursor(); } } void TagEditor::saveChangesPrompt() { if(!isVisible() || !m_dataChanged || m_items.isEmpty()) return; TQStringList files; for(PlaylistItemList::Iterator it = m_items.begin(); it != m_items.end(); ++it) files.append((*it)->file().absFilePath()); if(KMessageBox::questionYesNoList(this, i18n("Do you want to save your changes to:\n"), files, i18n("Save Changes"), KStdGuiItem::save(), KStdGuiItem::discard(), "tagEditor_showSaveChangesBox") == KMessageBox::Yes) { save(m_items); } } void TagEditor::addItem(const TQString &text, TQWidget *item, TQBoxLayout *layout, const TQString &iconName) { if(!item || !layout) return; TQLabel *label = new TQLabel(item, text, this); TQLabel *iconLabel = new TQLabel(item, 0, this); if(!iconName.isNull()) iconLabel->setPixmap(SmallIcon(iconName)); TQCheckBox *enableBox = new TQCheckBox(i18n("Enable"), this); enableBox->setChecked(true); label->setMinimumHeight(enableBox->height()); if(layout->direction() == TQBoxLayout::LeftToRight) { layout->addWidget(iconLabel); layout->addWidget(label); layout->addWidget(item); layout->addWidget(enableBox); } else { TQHBoxLayout *l = new TQHBoxLayout(layout); l->addWidget(iconLabel); 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, TQT_SIGNAL(toggled(bool)), item, TQT_SLOT(setEnabled(bool))); m_enableBoxes.insert(item, enableBox); } void TagEditor::showEvent(TQShowEvent *e) { if(m_collectionChanged) { updateCollection(); slotRefresh(); } TQWidget::showEvent(e); } bool TagEditor::eventFilter(TQObject *watched, TQEvent *e) { TQKeyEvent *ke = TQT_TQKEYEVENT(e); if(watched->inherits(TQSPINBOX_OBJECT_NAME_STRING) && e->type() == TQEvent::KeyRelease && ke->state() == 0) slotDataChanged(); return false; } //////////////////////////////////////////////////////////////////////////////// // private slots //////////////////////////////////////////////////////////////////////////////// void TagEditor::slotDataChanged(bool c) { m_dataChanged = c; } void TagEditor::slotItemRemoved(PlaylistItem *item) { m_items.remove(item); if(m_items.isEmpty()) slotRefresh(); } void TagEditor::slotPlaylistDestroyed(Playlist *p) { if(m_currentPlaylist == p) { m_currentPlaylist = 0; slotSetItems(PlaylistItemList()); } } #include "tageditor.moc"