/** * Copyright (c) 2000-2004 Charles Samuels * 2000-2001 Neil Stevens * * Copyright (c) from the patches of: * 2001 Klas Kalass * 2001 Anno v. Heimburg **/ // Abandon All Hope, Ye Who Enter Here #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "playlist.h" #include "view.h" #include "find.h" #define SPL SplitPlaylist::SPL() SafeListViewItem::SafeListViewItem(TQListView *parent, TQListViewItem *after, const KURL &text) : TQCheckListItem(parent,0, TQCheckListItem::CheckBox), PlaylistItemData(), removed(false) { addRef(); setUrl(text); static_cast(TQT_TQWIDGET(parent))->moveItem(this, 0, after); setOn(true); // is this really needed, it makes the listview too wide for me :( // setText(0,text.filename()); // if (!isDownloaded()) setText(1, "0%"); // mProperties.setAutoDelete(true); if (!streamable() && enqueue(url())) setUrl(KURL(localFilename())); PlaylistItemData::added(); } SafeListViewItem::SafeListViewItem(TQListView *parent, TQListViewItem *after, const TQMap &props) : TQCheckListItem(parent, 0, TQCheckListItem::CheckBox), removed(false) { addRef(); setOn(true); // A version of setProperty that assumes a key is unique, // and doesn't call modified for every new key. // Ugly, but this function is a very hot path on playlist loading for (TQMap::ConstIterator i=props.begin(); i!=props.end(); ++i ) { TQString n = i.key(); TQString val = i.data(); if (n=="enabled") { setOn(val!="false" && val!="0"); } else { Property p={n,val}; mProperties += p; } } static_cast(TQT_TQWIDGET(parent))->moveItem(this, 0, after); modified(); if (!streamable() && enqueue(url())) { KURL u; u.setPath(localFilename()); setUrl(u); } PlaylistItemData::added(); } SafeListViewItem::~SafeListViewItem() { remove(); } TQString SafeListViewItem::file() const { return localFilename(); } static void pad(TQString &str) { int len=str.length(); int at = 0; int blocklen=0; static const int paddingsize=12; // not static for reason const TQChar chars[paddingsize] = { TQChar('0'), TQChar('0'), TQChar('0'), TQChar('0'), TQChar('0'), TQChar('0'), TQChar('0'), TQChar('0'), TQChar('0'), TQChar('0'), TQChar('0'), TQChar('0') }; for (int i=0; i < len; i++) { if (str[i].isNumber()) { if (!blocklen) at = i; blocklen++; } else if (blocklen) { int pads=paddingsize; pads -= blocklen; str.insert(at, chars, pads); i += pads; blocklen = 0; } } if (blocklen) { int pads=paddingsize; pads -= blocklen; str.insert(at, chars, pads); } } int SafeListViewItem::compare(TQListViewItem * i, int col, bool) const { TQString text1 = text(col); TQString text2 = i->text(col); pad(text1); pad(text2); return text1.compare(text2); } TQString SafeListViewItem::property(const TQString &n, const TQString &def) const { for (TQValueList::ConstIterator i=mProperties.begin(); i != mProperties.end(); ++i) { if ((*i).key==n) return (*i).value; } if (n=="enabled") { if (isOn()) return "true"; else return "false"; } return def; } void SafeListViewItem::setProperty(const TQString &n, const TQString &val) { if (n=="enabled") { setOn(val!="false" && val!="0"); } else { if ( property(n,"") == val ) { // kdDebug(66666) << "SafeListViewItem::setProperty(), property unchanged!" << endl; return; } clearProperty(n); Property p={n,val}; mProperties += p; } modified(); } void SafeListViewItem::clearProperty(const TQString &n) { if (n=="enabled") { setOn(true); modified(); return; } for (TQValueList::Iterator i=mProperties.begin(); i != mProperties.end(); ++i) { if ((*i).key==n) { mProperties.remove(i); modified(); break; } } } TQStringList SafeListViewItem::properties() const { TQStringList list; for (TQValueList::ConstIterator i=mProperties.begin(); i != mProperties.end(); ++i) list += (*i).key; list += "enabled"; return list; } bool SafeListViewItem::isProperty(const TQString &n) const { for (TQValueList::ConstIterator i=mProperties.begin(); i != mProperties.end(); ++i) { if ((*i).key==n) return true; } return n=="enabled"; } void SafeListViewItem::downloaded(int percent) { if (!removed) setText(1, TQString::number(percent)+'%'); } void SafeListViewItem::downloadTimeout() { if (!removed) setText(1, "-"); } void SafeListViewItem::downloadFinished() { if (!removed) setText(1, ""); } void SafeListViewItem::modified() { bool widthChangeNeeded = false; if (text(0)!=title()) { setText(0, title()); widthChangeNeeded = true; } if (isDownloaded() && length()!=-1 && text(1)!=lengthString()) { setText(1, lengthString()); widthChangeNeeded = true; } if (widthChangeNeeded) widthChanged(-1); PlaylistItemData::modified(); } void SafeListViewItem::stateChange(bool s) { // if you uncheck this, uncheck thet others that // are selected too TQPtrList list=SPL->view->listView()->selectedItems(); // but not if I'm not selected if (list.containsRef(this)) for (TQListViewItem *i=list.first(); i != 0; i=list.next()) static_cast(i)->setOn(s); else TQCheckListItem::stateChange(s); } void SafeListViewItem::paintCell(TQPainter *p, const TQColorGroup &cg, int column, int width, int align) { TQCheckListItem::paintCell(p, cg, column, width, align); if (SPL->current() == this) { p->save(); p->setRasterOp(XorROP); p->fillRect(0, 0, width, height(), TQColor(255,255,255)); p->restore(); } } void SafeListViewItem::remove() { removed=true; if (napp->player()->current()==this && !itemAbove() && !itemBelow()) { napp->player()->stop(); SPL->setCurrent(0); } else if (napp->player()->current()==this) { // SPL->setCurrent(0); // napp->player()->playCurrent(); if (napp->player()->isPlaying() && !SPL->exiting()) napp->player()->forward(); else SPL->setCurrent(0); } if (listView()) { if (SPL->currentItem==this) // just optimizing for least unreadably SPL->setCurrent(static_cast(itemBelow())); listView()->takeItem(this); } else if (SPL->currentItem==this) { SPL->setCurrent(0); } dequeue(); PlaylistItemData::removed(); } List::List(View *parent) : KListView(parent), recursiveAddAfter(0), listJob(0) { addColumn(i18n("File")); addColumn(i18n("Time")); setAcceptDrops(true); setSorting(-1); setDropVisualizer(true); setDragEnabled(true); setItemsMovable(true); setSelectionMode(TQListView::Extended); connect(TQT_TQOBJECT(this), TQT_SIGNAL(dropped(TQDropEvent*, TQListViewItem*)), TQT_SLOT(dropEvent(TQDropEvent*, TQListViewItem*))); connect(TQT_TQOBJECT(this), TQT_SIGNAL(moved()), TQT_SLOT(move())); connect(TQT_TQOBJECT(this), TQT_SIGNAL(aboutToMove()), parent, TQT_SLOT(setNoSorting())); connect(TQT_TQOBJECT(this), TQT_SIGNAL(deleteCurrentItem()), parent, TQT_SLOT(deleteSelected())); } List::~List() { } void List::move() { emit modified(); } bool List::acceptDrag(TQDropEvent *event) const { return KURLDrag::canDecode(event) || KListView::acceptDrag(event); } void List::dropEvent(TQDropEvent *event, TQListViewItem *after) { static_cast(TQT_TQWIDGET(parent()))->setNoSorting(); KURL::List textlist; if (!KURLDrag::decode(event, textlist)) return; event->acceptAction(); for (KURL::List::Iterator i=textlist.begin(); i != textlist.end(); ++i) { after= addFile(*i, false, after); } emit modified(); } void List::keyPressEvent(TQKeyEvent *e) { if (e->key()==Key_Enter || e->key()==Key_Return) { if (currentItem()) { emit KListView::executed(currentItem()); } return; } if (e->key()==Key_Delete) { if (currentItem()) { emit deleteCurrentItem(); } return; } KListView::keyPressEvent(e); } /** * use this only once!!! **/ class NoatunSaver : public PlaylistSaver { List *mList; SafeListViewItem *after, *mFirst; public: NoatunSaver(List *l, TQListViewItem *after=0) : mList(l) { this->after = static_cast(after); mFirst = 0; } TQListViewItem *getAfter() { return after; } TQListViewItem *getFirst() { return mFirst; } protected: virtual void readItem(const TQMap &properties) { after = new SafeListViewItem(mList, after, properties); if (mFirst==0) mFirst = after; } virtual PlaylistItem writeItem() { if (!after) { after=static_cast(mList->firstChild()); } else { after=static_cast(static_cast(after)->nextSibling()); } return PlaylistItem(after); } }; bool View::saveToURL(const KURL &url) { NoatunSaver saver(list); if(saver.save(url)) { return true; } else { KMessageBox::error( this, i18n("Could not write to %1.").arg(url.prettyURL()) ); return false; } } void View::exportTo(const KURL &url) { TQString local(napp->tempSaveName(url.path())); TQFile saver(local); saver.open(IO_ReadWrite | IO_Truncate); TQTextStream t(&saver); // navigate the list for (SafeListViewItem *i=static_cast(listView()->firstChild()); i != 0; i=static_cast(i->itemBelow())) { KURL u=i->url(); if (u.isLocalFile()) t<< u.path() << '\n'; else t << u.url() << '\n'; } saver.close(); TDEIO::NetAccess::upload(local, url, this); saver.remove(); } TQListViewItem *List::openGlobal(const KURL &u, TQListViewItem *after) { clear(); NoatunSaver saver(this, after); saver.metalist(u); return saver.getAfter(); } // for m3u files TQListViewItem *List::importGlobal(const KURL &u, TQListViewItem *after) { NoatunSaver saver(this, after); if (!saver.metalist(u)) { after=new SafeListViewItem(this, after, u); // SPL->listItemSelected(after); return after; } // return the first item added from this playlist // that way noatun can start playing the first item if (saver.getFirst()) return saver.getFirst(); // failsafe in case nothing was added, getFirst() may return 0 return saver.getAfter(); } TQListViewItem *List::addFile(const KURL& url, bool play, TQListViewItem *after) { // when a new item is added, we don't want to sort anymore SPL->view->setNoSorting(); if ( url.path().right(4).lower()==".m3u" || url.path().right(4).lower()==".pls" || url.protocol().lower()=="http" ) { // a playlist is requested TQListViewItem *i = importGlobal(url, after); if (play) SPL->listItemSelected(i); return i; } else { if (!after) after=lastItem(); KFileItem fileItem(KFileItem::Unknown,KFileItem::Unknown,url); if (fileItem.isDir()) { addDirectoryRecursive(url, after); return after; // don't (and can't) know better!? } else { TQListViewItem *i = new SafeListViewItem(this, after, url); if (play) SPL->listItemSelected(i); return i; } } } // starts a new listJob if there is no active but work to do void List::addNextPendingDirectory() { KURL::List::Iterator pendingIt= pendingAddDirectories.begin(); if (!listJob && (pendingIt!= pendingAddDirectories.end())) { currentJobURL= *pendingIt; listJob= TDEIO::listRecursive(currentJobURL, false,false); connect( listJob, TQT_SIGNAL(entries(TDEIO::Job*, const TDEIO::UDSEntryList&)), TQT_SLOT(slotEntries(TDEIO::Job*, const TDEIO::UDSEntryList&)) ); connect( listJob, TQT_SIGNAL(result(TDEIO::Job *)), TQT_SLOT(slotResult(TDEIO::Job *)) ); connect( listJob, TQT_SIGNAL(redirection(TDEIO::Job *, const KURL &)), TQT_SLOT(slotRedirection(TDEIO::Job *, const KURL &)) ); pendingAddDirectories.remove(pendingIt); } } void List::addDirectoryRecursive(const KURL &dir, TQListViewItem *after) { if (!after) after=lastItem(); recursiveAddAfter= after; pendingAddDirectories.append(dir); addNextPendingDirectory(); } void List::slotResult(TDEIO::Job *job) { listJob= 0; if (job && job->error()) job->showErrorDialog(); addNextPendingDirectory(); } void List::slotEntries(TDEIO::Job *, const TDEIO::UDSEntryList &entries) { TQMap __list; // temp list to sort entries TDEIO::UDSEntryListConstIterator it = entries.begin(); TDEIO::UDSEntryListConstIterator end = entries.end(); for (; it != end; ++it) { KFileItem file(*it, currentJobURL, false /* no mimetype detection */, true); // "prudhomm: // insert the path + url in the map to sort automatically by path // note also that you use audiocd to rip your CDs then it will be sorted the right way // now it is an easy fix to have a nice sort BUT it is not the best // we should sort based on the tracknumber" // - copied over from old kdirlister hack if (!file.isDir()) __list.insert(file.url().path(), file.url()); } TQMap::Iterator __it; for( __it = __list.begin(); __it != __list.end(); ++__it ) { recursiveAddAfter= addFile(__it.data(), false, recursiveAddAfter); } } void List::slotRedirection(TDEIO::Job *, const KURL & url) { currentJobURL= url; } ///////////////////////////////// View::View(SplitPlaylist *) : KMainWindow(0, "NoatunSplitplaylistView") { list=new List(this); setCentralWidget(list); connect(list, TQT_SIGNAL(modified(void)), TQT_TQOBJECT(this), TQT_SLOT(setModified(void)) ); // connect the click on the header with sorting connect(list->header(),TQT_SIGNAL(clicked(int)),this,TQT_SLOT(headerClicked(int)) ); mOpen=new KAction(i18n("Add &Files..."), "queue", 0, TQT_TQOBJECT(this), TQT_SLOT(addFiles()), actionCollection(), "add_files"); (void) new KAction(i18n("Add Fol&ders..."), "folder", 0, TQT_TQOBJECT(this), TQT_SLOT(addDirectory()), actionCollection(), "add_dir"); mDelete=new KAction(i18n("Delete"), "editdelete", Key_Delete, TQT_TQOBJECT(this), TQT_SLOT(deleteSelected()), actionCollection(), "delete"); mClose=KStdAction::close(TQT_TQOBJECT(this), TQT_SLOT(close()), actionCollection()); mFind=KStdAction::find(TQT_TQOBJECT(this), TQT_SLOT(find()), actionCollection()); (void) KStdAction::configureToolbars(TQT_TQOBJECT(this), TQT_SLOT(configureToolBars()), actionCollection()); mOpenNew=KStdAction::openNew(TQT_TQOBJECT(this), TQT_SLOT(openNew()), actionCollection()); mOpenpl=KStdAction::open(TQT_TQOBJECT(this), TQT_SLOT(open()), actionCollection()); mSave=KStdAction::save(TQT_TQOBJECT(this), TQT_SLOT(save()), actionCollection()); mSaveAs=KStdAction::saveAs(TQT_TQOBJECT(this), TQT_SLOT(saveAs()), actionCollection()); (void) new KAction(i18n("Shuffle"), "misc", 0, TQT_TQOBJECT(SPL), TQT_SLOT( randomize() ), actionCollection(), "shuffle"); (void) new KAction(i18n("Clear"), "editclear", 0, TQT_TQOBJECT(list), TQT_SLOT( clear() ), actionCollection(), "clear"); createGUI("splui.rc"); mFinder = new Finder(this); applyMainWindowSettings(TDEGlobal::config(), "SPL Window"); list->setFocus(); } void View::find() { mFinder->show(); connect(mFinder, TQT_SIGNAL(search(Finder*)), TQT_SLOT(findIt(Finder*))); } static bool testWord(TQListViewItem *i, const TQString &finder) { PlaylistItemData *item=static_cast(i); if (item->title().find(finder, 0, false) >=0) return true; if (item->file().find(finder, 0, false) >=0) return true; if (item->url().path().find(finder.local8Bit().data(), 0, false) >=0) return true; if (item->lengthString().find(finder, 0, false) >=0) return true; if (item->mimetype().find(finder.local8Bit().data(), 0, false) >=0) return true; return false; } static bool testWord(TQListViewItem *i, const TQRegExp &finder) { PlaylistItemData *item=static_cast(i); if (item->title().find(finder) >=0) return true; if (item->file().find(finder) >=0) return true; if (item->url().path().find(finder) >=0) return true; if (item->lengthString().find(finder) >=0) return true; if (item->mimetype().find(finder) >=0) return true; return false; } void View::findIt(Finder *f) { TQListViewItem *search=list->currentItem(); if (list->currentItem()) { if (f->isForward()) search=list->currentItem()->itemBelow(); else search=list->currentItem()->itemAbove(); } else { if (f->isForward()) search=list->firstChild(); else search=list->lastChild(); } while (search) { if (f->regexp()) { if (testWord(search, TQRegExp(f->string(), false))) break; } else { if (testWord(search, f->string())) break; } if (f->isForward()) search=search->itemBelow(); else search=search->itemAbove(); if (!search) { if (f->isForward()) { if (KMessageBox::questionYesNo(this, i18n("End of playlist reached. Continue searching from beginning?"),TQString(),KStdGuiItem::cont(),KStdGuiItem::cancel()) == KMessageBox::Yes) search=list->firstChild(); } else { if (KMessageBox::questionYesNo(this, i18n("Beginning of playlist reached. Continue searching from end?"),TQString(),KStdGuiItem::cont(),KStdGuiItem::cancel()) == KMessageBox::Yes) search=list->lastChild(); } } } if (search) { { // select none TQPtrList sel=list->selectedItems(); for (TQListViewItem *i=sel.first(); i!=0; i=sel.next()) list->setSelected(i, false); } list->setSelected(search, true); list->setCurrentItem(search); list->ensureItemVisible(search); } } View::~View() { napp->player()->stop(); hide(); saveState(); delete list; } void View::init() { // see if we are importing an old-style list bool importing= ! TQFile(napp->dirs()->saveLocation("data", "noatun/") + "splitplaylist.xml").exists(); if (importing) { KURL internalURL; internalURL.setPath(napp->dirs()->saveLocation("data", "noatun/") + "splitplaylistdata"); NoatunSaver saver(list, 0); saver.load(internalURL, PlaylistSaver::M3U); } else { KURL internalURL; internalURL.setPath(napp->dirs()->saveLocation("data", "noatun/") + "splitplaylist.xml"); list->openGlobal(internalURL); } TDEConfig &config = *TDEGlobal::config(); config.setGroup("splitplaylist"); // this has to come after openGlobal, since openGlobal emits modified() setModified(config.readBoolEntry("modified", false)); TQString path = config.readPathEntry("file"); // don't call setPath with an empty path, that would make the url "valid" if ( !path.isEmpty() ) mPlaylistFile.setPath(path); SPL->reset(); int saved = config.readNumEntry("current", 0); PlaylistItem item=SPL->getFirst(); for(int i = 0 ; i < saved ; i++) { item=SPL->getAfter(item); } if (item) SPL->setCurrent(item); } void View::save() { if(mPlaylistFile.isEmpty() || !mPlaylistFile.isValid()) { saveAs(); return; } if(saveToURL(mPlaylistFile)) setModified(false); } void View::saveAs() { KURL u=KFileDialog::getSaveURL(0, "*.xml splitplaylistdata *.pls *.m3u\n*", this, i18n("Save Playlist")); if(!u.isValid()) return; mPlaylistFile = u; save(); } void View::open() { KURL u=KFileDialog::getOpenURL(0, "*.xml splitplaylistdata *.pls *.m3u\n*", this, i18n("Open Playlist")); if(!u.isValid()) return; mPlaylistFile = u; list->openGlobal(u); setModified(false); } void View::openNew() { mPlaylistFile = ""; listView()->clear(); } void List::clear() { SPL->setCurrent(0); TQListView::clear(); } void View::deleteSelected() { TQPtrList items(list->selectedItems()); bool stopped=false; // noatun shouldn't play files for now TQListViewItem *afterLast=0; for (TQPtrListIterator it(items); it.current(); ++it) { SafeListViewItem *i = static_cast(*it); if (!stopped && SPL->current() == i) { napp->player()->stop(); SPL->setCurrent(0); stopped = true; } i->remove(); afterLast = i->itemBelow(); } if (stopped) SPL->setCurrent(static_cast(afterLast)); setModified(true); } void View::addFiles() { KURL::List files=KFileDialog::getOpenURLs(":mediadir", napp->mimeTypes(), this, i18n("Select File to Play")); TQListViewItem *last = list->lastItem(); for(KURL::List::Iterator it=files.begin(); it!=files.end(); ++it) last = addFile(KURL(*it), false); setModified(true); } void View::addDirectory() { TQString file=KFileDialog::getExistingDirectory(0, this, i18n("Select Folder")); if (!file) return; KURL url; url.setPath(file); list->addDirectoryRecursive(url); setModified(true); } void View::closeEvent(TQCloseEvent*) { hide(); } void View::showEvent(TQShowEvent *) { emit shown(); } void View::hideEvent(TQHideEvent *) { emit hidden(); } void View::setModified(bool b) { modified = b; setCaption(i18n("Playlist"), modified); } void View::setModified(void) { setModified(true); } void View::saveState(void) { TDEConfig &config = *TDEGlobal::config(); config.setGroup("splitplaylist"); config.writeEntry("modified", modified); config.writePathEntry("file", mPlaylistFile.path()); saveToURL(napp->dirs()->saveLocation("data", "noatun/") + "splitplaylist.xml"); unsigned int i; PlaylistItem item=SPL->getFirst(); for(i = 0; item && item != SPL->current(); ) item=SPL->getAfter(item), i++; config.writeEntry("current", i); saveMainWindowSettings(TDEGlobal::config(), "SPL Window"); config.sync(); } void View::configureToolBars() { saveMainWindowSettings(TDEGlobal::config(), "SPL Window"); KEditToolbar dlg(actionCollection(), "splui.rc"); connect(&dlg, TQT_SIGNAL(newToolbarConfig()), TQT_SLOT(newToolBarConfig())); dlg.exec(); } void View::newToolBarConfig() { createGUI("splui.rc"); applyMainWindowSettings(TDEGlobal::config(), "SPL Window"); } // turns the sorting on or off void View::setSorting(bool on, int column) { if (on) { list->setSorting(column,true); list->setShowSortIndicator(true); } else { list->setShowSortIndicator(false); list->setSorting(-1); } } void View::headerClicked(int column) { // this is to avoid that if we already have it sorted, // we sort it again ascendingly this way, clicking on // the header a second time will correctly toggle // ascending/descending sort if (list->showSortIndicator()) { return; } else { setSorting(true,column); } } #include "view.moc"