/* * This file Copyright (C) 2009-2014 Mnemosyne LLC * * It may be used under the GNU GPL versions 2 or 3 * or any future license endorsed by Mnemosyne LLC. * * $Id$ */ #include #include #include #include #include #include #include #include #include // priorities #include "file-tree.h" #include "formatter.h" #include "hig.h" #include "torrent.h" // FileList #include "utils.h" // mime icons enum { COL_NAME, FIRST_VISIBLE_COLUMN = COL_NAME, COL_SIZE, COL_PROGRESS, COL_WANTED, COL_PRIORITY, LAST_VISIBLE_COLUMN = COL_PRIORITY, COL_FILE_INDEX, NUM_COLUMNS }; /**** ***** ****/ const QHash& FileTreeItem::getMyChildRows () { const size_t n = childCount(); // ensure that all the rows are hashed while (myFirstUnhashedRow < n) { myChildRows.insert (myChildren[myFirstUnhashedRow]->name(), myFirstUnhashedRow); ++myFirstUnhashedRow; } return myChildRows; } FileTreeItem::~FileTreeItem () { assert(myChildren.isEmpty()); if (myParent != 0) { const int pos = row(); assert ((pos>=0) && "couldn't find child in parent's lookup"); myParent->myChildren.removeAt(pos); myParent->myChildRows.remove(name()); myParent->myFirstUnhashedRow = pos; } } void FileTreeItem::appendChild (FileTreeItem * child) { const size_t n = childCount(); child->myParent = this; myChildren.append (child); myFirstUnhashedRow = n; } FileTreeItem * FileTreeItem::child (const QString& filename) { FileTreeItem * item(0); const int row = getMyChildRows().value (filename, -1); if (row != -1) { item = child (row); assert (filename == item->name()); } return item; } int FileTreeItem::row () const { int i(-1); if(myParent) { i = myParent->getMyChildRows().value (name(), -1); assert (this == myParent->myChildren[i]); } return i; } QVariant FileTreeItem::data (int column, int role) const { QVariant value; if (column == COL_FILE_INDEX) { value.setValue (myFileIndex); } else if (role == Qt::EditRole) { if (column == 0) value.setValue (name()); } else if ((role == Qt::TextAlignmentRole) && column == COL_SIZE) { value = Qt::AlignRight + Qt::AlignVCenter; } else if (role == Qt::DisplayRole) { switch(column) { case COL_NAME: value.setValue (name()); break; case COL_SIZE: value.setValue (sizeString() + " "); break; case COL_PROGRESS: value.setValue (progress()); break; case COL_WANTED: value.setValue (isSubtreeWanted()); break; case COL_PRIORITY: value.setValue (priorityString()); break; } } else if (role == Qt::DecorationRole && column == COL_NAME) { if (childCount () > 0) value = qApp->style ()->standardIcon (QStyle::SP_DirOpenIcon); else value = Utils::guessMimeIcon (name ()); } return value; } void FileTreeItem::getSubtreeWantedSize (uint64_t& have, uint64_t& total) const { if (myIsWanted) { have += myHaveSize; total += myTotalSize; } foreach(const FileTreeItem * i, myChildren) i->getSubtreeWantedSize(have, total); } double FileTreeItem::progress () const { double d(0); uint64_t have(0), total(0); getSubtreeWantedSize (have, total); if (total) d = have / (double)total; return d; } QString FileTreeItem::sizeString () const { QString str; if (myChildren.isEmpty()) { str = Formatter::sizeToString (myTotalSize); } else { uint64_t have = 0; uint64_t total = 0; getSubtreeWantedSize (have, total); str = Formatter::sizeToString (total); } return str; } std::pair FileTreeItem::update (const QString& name, bool wanted, int priority, uint64_t haveSize, bool updateFields) { int changed_count = 0; int changed_columns[4]; if (myName != name) { if (myParent) myParent->myFirstUnhashedRow = row(); myName = name; changed_columns[changed_count++] = COL_NAME; } if (fileIndex () != -1) { if (myHaveSize != haveSize) { myHaveSize = haveSize; changed_columns[changed_count++] = COL_PROGRESS; } if (updateFields) { if (myIsWanted != wanted) { myIsWanted = wanted; changed_columns[changed_count++] = COL_WANTED; } if (myPriority != priority) { myPriority = priority; changed_columns[changed_count++] = COL_PRIORITY; } } } std::pair changed (-1, -1); if (changed_count > 0) { std::sort (changed_columns, changed_columns+changed_count); changed.first = changed_columns[0]; changed.second = changed_columns[changed_count-1]; } return changed; } QString FileTreeItem::priorityString () const { const int i = priority(); switch (i) { case LOW: return tr("Low"); case HIGH: return tr("High"); case NORMAL: return tr("Normal"); default: return tr("Mixed"); } } int FileTreeItem::priority () const { int i(0); if (myChildren.isEmpty()) { switch (myPriority) { case TR_PRI_LOW: i |= LOW; break; case TR_PRI_HIGH: i |= HIGH; break; default: i |= NORMAL; break; } } foreach (const FileTreeItem * child, myChildren) i |= child->priority(); return i; } void FileTreeItem::setSubtreePriority (int i, QSet& ids) { if (myPriority != i) { myPriority = i; if (myFileIndex >= 0) ids.insert (myFileIndex); } foreach (FileTreeItem * child, myChildren) child->setSubtreePriority (i, ids); } void FileTreeItem::twiddlePriority (QSet& ids, int& p) { const int old(priority()); if (old & LOW) p = TR_PRI_NORMAL; else if (old & NORMAL) p = TR_PRI_HIGH; else p = TR_PRI_LOW; setSubtreePriority (p, ids); } int FileTreeItem::isSubtreeWanted () const { if(myChildren.isEmpty()) return myIsWanted ? Qt::Checked : Qt::Unchecked; int wanted(-1); foreach (const FileTreeItem * child, myChildren) { const int childWanted = child->isSubtreeWanted(); if (wanted == -1) wanted = childWanted; if (wanted != childWanted) wanted = Qt::PartiallyChecked; if (wanted == Qt::PartiallyChecked) return wanted; } return wanted; } void FileTreeItem::setSubtreeWanted (bool b, QSet& ids) { if (myIsWanted != b) { myIsWanted = b; if (myFileIndex >= 0) ids.insert(myFileIndex); } foreach (FileTreeItem * child, myChildren) child->setSubtreeWanted (b, ids); } void FileTreeItem::twiddleWanted (QSet& ids, bool& wanted) { wanted = isSubtreeWanted() != Qt::Checked; setSubtreeWanted (wanted, ids); } QString FileTreeItem::path () const { QString itemPath; const FileTreeItem * item = this; while (item != NULL && !item->name().isEmpty()) { if (itemPath.isEmpty()) itemPath = item->name(); else itemPath = item->name() + "/" + itemPath; item = item->parent (); } return itemPath; } bool FileTreeItem::isComplete () const { return myHaveSize == totalSize (); } /*** **** **** ***/ FileTreeModel::FileTreeModel (QObject * parent, bool isEditable): QAbstractItemModel(parent), myRootItem (new FileTreeItem), myIndexCache (), myIsEditable (isEditable) { } FileTreeModel::~FileTreeModel() { clear(); delete myRootItem; } FileTreeItem * FileTreeModel::itemFromIndex (const QModelIndex& index) const { return static_cast(index.internalPointer()); } QVariant FileTreeModel::data (const QModelIndex &index, int role) const { QVariant value; if (index.isValid()) value = itemFromIndex(index)->data (index.column(), role); return value; } Qt::ItemFlags FileTreeModel::flags (const QModelIndex& index) const { int i(Qt::ItemIsSelectable | Qt::ItemIsEnabled); if(myIsEditable && (index.column() == COL_NAME)) i |= Qt::ItemIsEditable; if(index.column() == COL_WANTED) i |= Qt::ItemIsUserCheckable | Qt::ItemIsTristate; return (Qt::ItemFlags)i; } bool FileTreeModel::setData (const QModelIndex& index, const QVariant& newname, int role) { if (role == Qt::EditRole) { FileTreeItem * item = itemFromIndex (index); emit pathEdited (item->path (), newname.toString ()); } return false; // don't update the view until the session confirms the change } QVariant FileTreeModel::headerData (int column, Qt::Orientation orientation, int role) const { QVariant data; if (orientation==Qt::Horizontal && role==Qt::DisplayRole) { switch (column) { case COL_NAME: data.setValue (tr("File")); break; case COL_SIZE: data.setValue (tr("Size")); break; case COL_PROGRESS: data.setValue (tr("Progress")); break; case COL_WANTED: data.setValue (tr("Download")); break; case COL_PRIORITY: data.setValue (tr("Priority")); break; default: break; } } return data; } QModelIndex FileTreeModel::index (int row, int column, const QModelIndex& parent) const { QModelIndex i; if (hasIndex (row, column, parent)) { FileTreeItem * parentItem; if (!parent.isValid ()) parentItem = myRootItem; else parentItem = itemFromIndex (parent); FileTreeItem * childItem = parentItem->child (row); if (childItem) i = createIndex (row, column, childItem); } return i; } QModelIndex FileTreeModel::parent (const QModelIndex& child) const { return parent (child, 0); // QAbstractItemModel::parent() wants col 0 } QModelIndex FileTreeModel::parent (const QModelIndex& child, int column) const { QModelIndex parent; if (child.isValid()) parent = indexOf (itemFromIndex(child)->parent(), column); return parent; } int FileTreeModel::rowCount (const QModelIndex& parent) const { FileTreeItem * parentItem; if (parent.isValid()) parentItem = itemFromIndex (parent); else parentItem = myRootItem; return parentItem->childCount(); } int FileTreeModel::columnCount (const QModelIndex& parent) const { Q_UNUSED(parent); return NUM_COLUMNS; } QModelIndex FileTreeModel::indexOf (FileTreeItem * item, int column) const { if (!item || item==myRootItem) return QModelIndex(); return createIndex(item->row(), column, item); } void FileTreeModel::clearSubtree (const QModelIndex& top) { size_t i = rowCount (top); while (i > 0) clearSubtree(index(--i, 0, top)); FileTreeItem * const item = itemFromIndex (top); if (item == 0) return; if (item->fileIndex () != -1) myIndexCache.remove (item->fileIndex ()); delete item; } void FileTreeModel::clear () { beginResetModel (); clearSubtree (QModelIndex()); endResetModel (); assert (myIndexCache.isEmpty ()); } FileTreeItem * FileTreeModel::findItemForFileIndex (int fileIndex) const { return myIndexCache.value (fileIndex, 0); } void FileTreeModel::addFile (int fileIndex, const QString & filename, bool wanted, int priority, uint64_t totalSize, uint64_t have, QList & rowsAdded, bool updateFields) { FileTreeItem * item; QStringList tokens = filename.split (QChar::fromLatin1('/')); item = findItemForFileIndex (fileIndex); if (item) // this file is already in the tree, we've added this { QModelIndex indexWithChangedParents; while (!tokens.isEmpty()) { const QString token = tokens.takeLast(); const std::pair changed = item->update (token, wanted, priority, have, updateFields); if (changed.first >= 0) { dataChanged (indexOf (item, changed.first), indexOf (item, changed.second)); if (!indexWithChangedParents.isValid () && changed.first <= COL_PRIORITY && changed.second >= COL_SIZE) indexWithChangedParents = indexOf (item, 0); } item = item->parent(); } assert (item == myRootItem); if (indexWithChangedParents.isValid ()) parentsChanged (indexWithChangedParents, COL_SIZE, COL_PRIORITY); } else // we haven't build the FileTreeItems for these tokens yet { bool added = false; item = myRootItem; while (!tokens.isEmpty()) { const QString token = tokens.takeFirst(); FileTreeItem * child(item->child(token)); if (!child) { added = true; QModelIndex parentIndex (indexOf(item, 0)); const int n (item->childCount()); beginInsertRows (parentIndex, n, n); if (tokens.isEmpty()) child = new FileTreeItem (token, fileIndex, totalSize); else child = new FileTreeItem (token); item->appendChild (child); endInsertRows (); rowsAdded.append (indexOf(child, 0)); } item = child; } if (item != myRootItem) { assert (item->fileIndex() == fileIndex); assert (item->totalSize() == totalSize); myIndexCache[fileIndex] = item; const std::pair changed = item->update (item->name(), wanted, priority, have, added || updateFields); if (changed.first >= 0) dataChanged (indexOf (item, changed.first), indexOf (item, changed.second)); } } } void FileTreeModel::parentsChanged (const QModelIndex& index, int firstColumn, int lastColumn) { assert (firstColumn <= lastColumn); QModelIndex walk = index; for (;;) { walk = parent (walk, firstColumn); if (!walk.isValid ()) break; dataChanged (walk, walk.sibling (walk.row (), lastColumn)); } } void FileTreeModel::subtreeChanged (const QModelIndex& index, int firstColumn, int lastColumn) { assert (firstColumn <= lastColumn); const int childCount = rowCount (index); if (!childCount) return; // tell everyone that this tier changed dataChanged (index.child (0, firstColumn), index.child (childCount - 1, lastColumn)); // walk the subtiers for (int i=0; i file_ids; FileTreeItem * item; item = itemFromIndex (index); item->twiddleWanted (file_ids, want); emit wantedChanged (file_ids, want); dataChanged (index, index); parentsChanged (index, COL_SIZE, COL_WANTED); subtreeChanged (index, COL_WANTED, COL_WANTED); } else if (column == COL_PRIORITY) { int priority; QSet file_ids; FileTreeItem * item; item = itemFromIndex (index); item->twiddlePriority (file_ids, priority); emit priorityChanged (file_ids, priority); dataChanged (index, index); parentsChanged (index, column, column); subtreeChanged (index, column, column); } } void FileTreeModel::doubleClicked (const QModelIndex& index) { if (!index.isValid()) return; const int column (index.column()); if (column == COL_WANTED || column == COL_PRIORITY) return; FileTreeItem * item = itemFromIndex (index); if (item->childCount () == 0 && item->isComplete ()) emit openRequested (item->path ()); } /**** ***** ****/ QSize FileTreeDelegate::sizeHint(const QStyleOptionViewItem& item, const QModelIndex& index) const { QSize size; switch(index.column()) { case COL_PROGRESS: case COL_WANTED: size = QSize(20, 1); break; default: size = QItemDelegate::sizeHint (item, index); } size.rheight() += 8; // make the spacing a little nicer return size; } void FileTreeDelegate::paint (QPainter * painter, const QStyleOptionViewItem & option, const QModelIndex & index) const { const int column(index.column()); if ((column != COL_PROGRESS) && (column != COL_WANTED)) { QItemDelegate::paint(painter, option, index); return; } QStyle * style (qApp->style ()); painter->save(); QItemDelegate::drawBackground (painter, option, index); if(column == COL_PROGRESS) { QStyleOptionProgressBar p; p.state = option.state | QStyle::State_Small; p.direction = qApp->layoutDirection(); p.rect = option.rect; p.rect.setSize (QSize(option.rect.width()-2, option.rect.height()-8)); p.rect.moveCenter (option.rect.center()); p.fontMetrics = qApp->fontMetrics(); p.minimum = 0; p.maximum = 100; p.textAlignment = Qt::AlignCenter; p.textVisible = true; p.progress = (int)(100.0*index.data().toDouble()); p.text = QString::fromLatin1 ("%1%").arg (p.progress); style->drawControl(QStyle::CE_ProgressBar, &p, painter); } else if(column == COL_WANTED) { QStyleOptionButton o; o.state = option.state; o.direction = qApp->layoutDirection(); o.rect.setSize (QSize(20, option.rect.height())); o.rect.moveCenter (option.rect.center()); o.fontMetrics = qApp->fontMetrics(); switch (index.data().toInt()) { case Qt::Unchecked: o.state |= QStyle::State_Off; break; case Qt::Checked: o.state |= QStyle::State_On; break; default: o.state |= QStyle::State_NoChange;break; } style->drawControl (QStyle::CE_CheckBox, &o, painter); } QItemDelegate::drawFocus (painter, option, option.rect); painter->restore(); } /**** ***** ***** ***** ****/ FileTreeView::FileTreeView (QWidget * parent, bool isEditable): QTreeView (parent), myModel (this, isEditable), myProxy (new QSortFilterProxyModel()), myDelegate (this) { setSortingEnabled (true); setAlternatingRowColors (true); setSelectionBehavior (QAbstractItemView::SelectRows); setSelectionMode (QAbstractItemView::ExtendedSelection); myProxy->setSourceModel (&myModel); setModel (myProxy); setItemDelegate (&myDelegate); setHorizontalScrollBarPolicy (Qt::ScrollBarAlwaysOff); sortByColumn (COL_NAME, Qt::AscendingOrder); installEventFilter (this); for (int i=0; isetResizeMode(i, QHeaderView::Interactive); #else header()->setSectionResizeMode(i, QHeaderView::Interactive); #endif } connect (this, SIGNAL(clicked(QModelIndex)), this, SLOT(onClicked(QModelIndex))); connect (this, SIGNAL(doubleClicked(QModelIndex)), this, SLOT(onDoubleClicked(QModelIndex))); connect (&myModel, SIGNAL(priorityChanged(QSet, int)), this, SIGNAL(priorityChanged(QSet, int))); connect (&myModel, SIGNAL(wantedChanged(QSet, bool)), this, SIGNAL(wantedChanged(QSet, bool))); connect (&myModel, SIGNAL(pathEdited(QString, QString)), this, SIGNAL(pathEdited(QString, QString))); connect (&myModel, SIGNAL (openRequested (QString)), this, SLOT (onOpenRequested (QString)), Qt::QueuedConnection); } FileTreeView::~FileTreeView () { myProxy->deleteLater(); } void FileTreeView::onClicked (const QModelIndex& proxyIndex) { const QModelIndex modelIndex = myProxy->mapToSource (proxyIndex); myModel.clicked (modelIndex); } void FileTreeView::onDoubleClicked (const QModelIndex& proxyIndex) { const QModelIndex modelIndex = myProxy->mapToSource (proxyIndex); myModel.doubleClicked (modelIndex); } void FileTreeView::onOpenRequested (const QString& path) { if (state () == EditingState) return; emit openRequested (path); } bool FileTreeView::eventFilter (QObject * o, QEvent * event) { // this is kind of a hack to get the last three columns be the // right size, and to have the filename column use whatever // space is left over... if ((o == this) && (event->type() == QEvent::Resize)) { QResizeEvent * r = static_cast (event); int left = r->size().width(); const QFontMetrics fontMetrics(font()); for (int column=FIRST_VISIBLE_COLUMN; column<=LAST_VISIBLE_COLUMN; ++column) { if (column == COL_NAME) continue; if (isColumnHidden (column)) continue; QString header; if (column == COL_SIZE) header = "999.9 KiB"; else header = myModel.headerData (column, Qt::Horizontal).toString(); header += " "; const int width = fontMetrics.size (0, header).width(); setColumnWidth (column, width); left -= width; } left -= 20; // not sure why this is necessary. it works in different themes + font sizes though... setColumnWidth(COL_NAME, std::max(left,0)); } // handle using the keyboard to toggle the // wanted/unwanted state or the file priority else if (event->type () == QEvent::KeyPress && state () != EditingState) { switch (static_cast (event)->key ()) { case Qt::Key_Space: foreach (const QModelIndex& i, selectionModel ()->selectedRows (COL_WANTED)) clicked (i); break; case Qt::Key_Enter: case Qt::Key_Return: foreach (const QModelIndex& i, selectionModel ()->selectedRows (COL_PRIORITY)) clicked (i); break; } } return false; } void FileTreeView::update (const FileList& files, bool updateFields) { foreach (const TrFile file, files) { QList added; myModel.addFile (file.index, file.filename, file.wanted, file.priority, file.size, file.have, added, updateFields); foreach (QModelIndex i, added) expand (myProxy->mapFromSource(i)); } } void FileTreeView::clear () { myModel.clear(); }