# # Copyright (C) 2009 Anton Khirnov # # Nephilim 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 3 of the License, or # (at your option) any later version. # # Nephilim is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with Nephilim. If not, see . # from PyQt5 import QtGui, QtWidgets, QtCore from PyQt5.QtCore import pyqtSlot as Slot from ..plugin import Plugin from ..common import MIMETYPES, SongsMimeData class Library(Plugin): # public, const info = 'Display MPD database as a tree.' # public, read-only o = None # private DEFAULTS = {'foldings' : ['albumartist/album'], 'cur_folding' : 0} def _load(self): self.o = LibraryWidget(self) def _unload(self): self.o = None def _get_dock_widget(self): return self._create_dock(self.o) def fill_library(self): if not self.o: return self.o.fill_library() class LibraryWidget(QtWidgets.QWidget): library_view = None library_model = None search_txt = None settings = None plugin = None logger = None filtered_items = None filter = None def __init__(self, plugin): QtWidgets.QWidget.__init__(self) self.plugin = plugin self.logger = plugin.logger self.settings = QtCore.QSettings() self.filter = '' self.filtered_items = [] self.settings.beginGroup(self.plugin.name) # folding widgets self.foldings = LibraryFolding(self.plugin, self) self.foldings.activated.connect(self.fill_library) del_folding = QtWidgets.QPushButton(QtGui.QIcon(':icons/delete.png'), '') del_folding.setToolTip('Delete current folding pattern.') del_folding.clicked.connect(lambda :self.foldings.removeItem(self.foldings.currentIndex())) folding_layout = QtWidgets.QHBoxLayout() folding_layout.addWidget(self.foldings, stretch = 1) folding_layout.addWidget(del_folding) self.search_txt = QtWidgets.QLineEdit() self.search_txt.setToolTip('Filter library') self.search_txt.textChanged.connect(self.filter_library) self.search_txt.returnPressed.connect(self.add_filtered) #construct the library self.library_model = LibraryModel() self.fill_library() self.library_view = LibraryView() self.library_view.setModel(self.library_model) self.library_view.activated.connect(lambda : self.add_indices(self.library_view.selectedIndexes())) self.setLayout(QtWidgets.QVBoxLayout()) self.layout().setSpacing(2) self.layout().setContentsMargins(0, 0, 0, 0) self.layout().addLayout(folding_layout) self.layout().addWidget(self.search_txt) self.layout().addWidget(self.library_view) self.plugin.mpclient.db_updated.connect(self.fill_library) def fill_library(self): self.logger.info('Refreshing library.') self.library_model.fill(self.plugin.mpclient.db.values(), self.foldings.currentText().split('/')) @Slot(str) def filter_library(self, text): """Hide all items that don't contain text.""" to_hide = [] to_show = [] filtered_items = [] text = text.lower() if not text: # show all items to_show = self.library_model.findItems('*', QtCore.Qt.MatchWildcard|QtCore.Qt.MatchRecursive) elif self.filter and self.filter in text: for item in self.filtered_items: if text in item.text().lower(): filtered_items.append(item) while item: to_show.append(item) item = item.parent() else: while item: to_hide.append(item) item = item.parent() else: for item in self.library_model.findItems('*', QtCore.Qt.MatchWildcard|QtCore.Qt.MatchRecursive): if text in item.text().lower(): filtered_items.append(item) while item: to_show.append(item) item = item.parent() else: while item: to_hide.append(item) item = item.parent() for item in to_hide: self.library_view.setRowHidden(item.row(), self.library_model.indexFromItem(item.parent()), True) for item in to_show: self.library_view.setRowHidden(item.row(), self.library_model.indexFromItem(item.parent()), False) if len(filtered_items) < 5: for item in filtered_items: while item: item = item.parent() self.library_view.setExpanded(self.library_model.indexFromItem(item), True) self.filtered_items = filtered_items self.filter = text @Slot() def add_filtered(self): self.add_indices([self.library_model.indexFromItem(index) for index in self.filtered_items]) self.search_txt.clear() def add_indices(self, indices): paths = [] for song in self.library_model.walk_tree(indices): paths.append(song) self.plugin.mpclient.add(paths) class LibrarySongItem(QtGui.QStandardItem): # public "Song path" path = None class LibraryModel(QtGui.QStandardItemModel): def fill(self, songs, folding): self.clear() tree = [{},self.invisibleRootItem()] for song in songs: cur_item = tree for part in folding: try: tag = song[part] except KeyError: tag = 'Unknown' if tag in cur_item[0]: cur_item = cur_item[0][tag] else: it = QtGui.QStandardItem(tag) it.setFlags(QtCore.Qt.ItemIsSelectable|QtCore.Qt.ItemIsEnabled) cur_item[1].appendRow(it) cur_item[0][tag] = [{}, it] cur_item = cur_item[0][tag] it = LibrarySongItem('%s%02d %s'%(song['disc'] + '/' if 'disc' in song else '', song['tracknum'] if 'tracknum' in song else 0, song['?title'])) it.path = song['?file'] it.setFlags(QtCore.Qt.ItemIsSelectable|QtCore.Qt.ItemIsEnabled) cur_item[1].appendRow(it) self.sort(0, QtCore.Qt.AscendingOrder) def walk_tree(self, indices): """Returns a generator over all songs that are children of indices.""" for index in indices: if self.hasChildren(index): for song in self.walk_tree([self.index(i, 0, index) for i in range(self.rowCount(index))]): yield song else: yield self.itemFromIndex(index).path def flags(self, index): return (QtCore.Qt.ItemIsDragEnabled if index.isValid() else 0) | QtGui.QStandardItemModel.flags(self, index) def mimeTypes(self): return MIMETYPES['songs'] def mimeData(self, indices): data = SongsMimeData() songs = [] for song in self.walk_tree(indices): songs.append(song) data.set_songs(songs) return data class LibraryView(QtWidgets.QTreeView): def __init__(self): QtWidgets.QTreeView.__init__(self) self.setAlternatingRowColors(True) self.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection) self.setUniformRowHeights(True) self.setHeaderHidden(True) self.setDragEnabled(True) class LibraryFolding(QtWidgets.QComboBox): #### PRIVATE #### _plugin = None _settings = None #### PUBLIC #### def __init__(self, plugin, parent): QtWidgets.QComboBox.__init__(self) self.setEditable(True) self.setToolTip('Current folding pattern.') self._plugin = plugin # fill with stored folding patterns, ensure at least an empty string self._settings = QtCore.QSettings() self._settings.beginGroup(self._plugin.name) for item in self._settings.value('foldings'): self.addItem(item) if not self.count(): self.addItem('') self.setCurrentIndex(int(self._settings.value('cur_folding'))) # connect signals to slots self.currentIndexChanged.connect( self._update_foldings) self.model().rowsInserted.connect(self._update_foldings) self.model().rowsRemoved.connect( self._update_foldings) self.model().rowsMoved.connect( self._update_foldings) #### PRIVATE #### @Slot() def _update_foldings(self): foldings = [] for i in range(self.count()): foldings.append(self.itemText(i)) self._settings.setValue('foldings', foldings) self._settings.setValue('cur_folding', self.currentIndex())