/* * This file Copyright (C) Mnemosyne LLC * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License version 2 * as published by the Free Software Foundation. * * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html * * $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, COL_PROGRESS, COL_WANTED, COL_PRIORITY, NUM_COLUMNS }; /**** ***** ****/ 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 (role == Qt::EditRole) { if (column == 0) value.setValue (name()); } else if (role == Qt::DisplayRole) { switch(column) { case COL_NAME: value.setValue (fileSizeName()); break; case COL_PROGRESS: value.setValue (progress()); break; case COL_WANTED: value.setValue (isSubtreeWanted()); break; case COL_PRIORITY: value.setValue (priorityString()); break; } } return value; } void FileTreeItem :: getSubtreeSize (uint64_t& have, uint64_t& total) const { have += myHaveSize; total += myTotalSize; foreach(const FileTreeItem * i, myChildren) i->getSubtreeSize(have, total); } double FileTreeItem :: progress () const { double d(0); uint64_t have(0), total(0); getSubtreeSize(have, total); if (total) d = have / (double)total; return d; } QString FileTreeItem :: fileSizeName () const { uint64_t have(0), total(0); QString str; getSubtreeSize(have, total); str = QString(name() + " (%1)").arg(Formatter::sizeToString(total)); return str; } bool FileTreeItem :: update (int index, bool wanted, int priority, uint64_t totalSize, uint64_t haveSize, bool torrentChanged) { bool changed = false; if (myIndex != index) { myIndex = index; changed = true; } if (torrentChanged && myIsWanted != wanted) { myIsWanted = wanted; changed = true; } if (torrentChanged && myPriority != priority) { myPriority = priority; changed = true; } if (myTotalSize != totalSize) { myTotalSize = totalSize; changed = true; } if (myHaveSize != haveSize) { myHaveSize = haveSize; changed = true; } return changed; } QString FileTreeItem :: priorityString () const { const int i(priority()); if(i == LOW) return tr("Low"); if(i == HIGH) return tr("High"); if(i == NORMAL) return tr("Normal"); 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 (myIndex >= 0) ids.insert (myIndex); } 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 (myIndex >= 0) ids.insert(myIndex); } foreach (FileTreeItem * child, myChildren) child->setSubtreeWanted (b, ids); } void FileTreeItem :: twiddleWanted (QSet& ids, bool& wanted) { wanted = isSubtreeWanted() != Qt::Checked; setSubtreeWanted (wanted, ids); } /*** **** **** ***/ FileTreeModel :: FileTreeModel (QObject *parent): QAbstractItemModel(parent) { rootItem = new FileTreeItem(-1); } FileTreeModel :: ~FileTreeModel() { clear(); delete rootItem; } QVariant FileTreeModel :: data (const QModelIndex &index, int role) const { QVariant value; if (index.isValid()) { FileTreeItem * i = static_cast(index.internalPointer()); value = i->data (index.column(), role); } return value; } Qt::ItemFlags FileTreeModel :: flags (const QModelIndex& index) const { int i(Qt::ItemIsSelectable | Qt::ItemIsEnabled); if(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) { QString oldpath; QModelIndex walk = index; FileTreeItem * item = static_cast(index.internalPointer()); while (item && !item->name().isEmpty()) { if (oldpath.isEmpty()) oldpath = item->name(); else oldpath = item->name() + "/" + oldpath; item = item->parent (); } emit pathEdited (oldpath, 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_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)) { std::cerr << " I don't have this index " << std::endl; } else { FileTreeItem * parentItem; if(!parent.isValid()) parentItem = rootItem; else parentItem = static_cast(parent.internalPointer()); 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 { if (!child.isValid()) return QModelIndex(); FileTreeItem * childItem = static_cast(child.internalPointer()); return indexOf (childItem->parent(), column); } int FileTreeModel :: rowCount (const QModelIndex& parent) const { FileTreeItem * parentItem; if (!parent.isValid()) parentItem = rootItem; else parentItem = static_cast(parent.internalPointer()); return parentItem->childCount(); } int FileTreeModel :: columnCount (const QModelIndex &parent) const { Q_UNUSED(parent); return 4; } QModelIndex FileTreeModel :: indexOf (FileTreeItem * item, int column) const { if (!item || item==rootItem) 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)); delete static_cast(top.internalPointer()); } void FileTreeModel :: clear () { clearSubtree (QModelIndex()); reset (); } void FileTreeModel :: addFile (int index, const QString & filename, bool wanted, int priority, uint64_t size, uint64_t have, QList & rowsAdded, bool torrentChanged) { FileTreeItem * i(rootItem); foreach (QString token, filename.split(QChar::fromAscii('/'))) { FileTreeItem * child(i->child(token)); if (!child) { QModelIndex parentIndex (indexOf(i, 0)); const int n (i->childCount()); beginInsertRows (parentIndex, n, n); i->appendChild ((child = new FileTreeItem(-1, token))); endInsertRows (); rowsAdded.append (indexOf(child, 0)); } i = child; } if (i != rootItem) if (i->update (index, wanted, priority, size, have, torrentChanged)) dataChanged (indexOf(i, 0), indexOf(i, NUM_COLUMNS-1)); } void FileTreeModel :: parentsChanged (const QModelIndex& index, int column) { QModelIndex walk = index; for (;;) { walk = parent(walk, column); if(!walk.isValid()) break; dataChanged(walk, walk); } } void FileTreeModel :: subtreeChanged (const QModelIndex& index, int column) { const int childCount = rowCount (index); if (!childCount) return; // tell everyone that this tier changed dataChanged (index.child(0,column), index.child(childCount-1,column)); // walk the subtiers for (int i=0; i file_ids; FileTreeItem * item; item = static_cast(index.internalPointer()); item->twiddleWanted (file_ids, want); emit wantedChanged (file_ids, want); dataChanged (index, index); parentsChanged (index, column); subtreeChanged (index, column); } else if (column == COL_PRIORITY) { int priority; QSet file_ids; FileTreeItem * item; item = static_cast(index.internalPointer()); item->twiddlePriority (file_ids, priority); emit priorityChanged (file_ids, priority); dataChanged(index, index); parentsChanged(index, column); subtreeChanged(index, column); } } /**** ***** ****/ QSize FileTreeDelegate :: sizeHint(const QStyleOptionViewItem& item, const QModelIndex& index) const { QSize size; switch(index.column()) { case COL_NAME: { const QFontMetrics fm(item.font); const QString text = index.data().toString(); const int iconSize = QApplication::style()->pixelMetric(QStyle::PM_SmallIconSize); size.rwidth() = HIG::PAD_SMALL + iconSize; size.rheight() = std::max(iconSize, fm.height()); break; } case COL_PROGRESS: case COL_WANTED: size = QSize(20, 1); break; default: { const QFontMetrics fm(item.font); const QString text = index.data().toString(); size = fm.size(0, text); break; } } 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) && (column != COL_NAME)) { QItemDelegate::paint(painter, option, index); return; } QStyle * style(QApplication :: style()); if (option.state & QStyle::State_Selected) painter->fillRect(option.rect, option.palette.highlight()); painter->save(); if (option.state & QStyle::State_Selected) painter->setBrush(option.palette.highlightedText()); if (column == COL_NAME) { // draw the file icon static const int iconSize(style->pixelMetric(QStyle :: PM_SmallIconSize)); const QRect iconArea(option.rect.x(), option.rect.y() + (option.rect.height()-iconSize)/2, iconSize, iconSize); QIcon icon; if (index.model()->hasChildren(index)) { icon = style->standardIcon(QStyle::StandardPixmap(QStyle::SP_DirOpenIcon)); } else { QString name = index.data().toString(); icon = Utils :: guessMimeIcon (name.left(name.lastIndexOf(" ("))); } icon.paint(painter, iconArea, Qt::AlignCenter, QIcon::Normal, QIcon::On); // draw the name QStyleOptionViewItem tmp(option); tmp.rect.setWidth(option.rect.width() - iconArea.width() - HIG::PAD_SMALL); tmp.rect.moveRight(option.rect.right()); QItemDelegate::paint(painter, tmp, index); } else if(column == COL_PROGRESS) { QStyleOptionProgressBar p; p.state = option.state | QStyle::State_Small; p.direction = QApplication::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 = QApplication::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().sprintf("%d%%", p.progress); style->drawControl(QStyle::CE_ProgressBar, &p, painter); } else if(column == COL_WANTED) { QStyleOptionButton o; o.state = option.state; o.direction = QApplication::layoutDirection(); o.rect.setSize(QSize(20, option.rect.height())); o.rect.moveCenter(option.rect.center()); o.fontMetrics = QApplication::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); } painter->restore(); } /**** ***** ***** ***** ****/ FileTreeView :: FileTreeView (QWidget * parent): QTreeView (parent), myModel (this), 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); connect (this, SIGNAL(clicked(const QModelIndex&)), this, SLOT(onClicked(const QModelIndex&))); connect (&myModel, SIGNAL(priorityChanged(const QSet&, int)), this, SIGNAL(priorityChanged(const QSet&, int))); connect (&myModel, SIGNAL(wantedChanged(const QSet&, bool)), this, SIGNAL(wantedChanged(const QSet&, bool))); connect (&myModel, SIGNAL(pathEdited(const QString&, const QString&)), this, SIGNAL(pathEdited(const QString&, const QString&))); } FileTreeView :: ~FileTreeView () { myProxy->deleteLater(); } void FileTreeView :: onClicked (const QModelIndex& proxyIndex) { const QModelIndex modelIndex = myProxy->mapToSource(proxyIndex); myModel.clicked(modelIndex); } bool FileTreeView :: eventFilter (QObject * o, QEvent * event) { if (o != this) return false; // 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 (event->type() == QEvent::Resize) { QResizeEvent * r = dynamic_cast(event); int left = r->size().width(); const QFontMetrics fontMetrics(font()); for (int column=0; columntype() == QEvent::KeyPress) { switch(dynamic_cast(event)->key()) { case Qt::Key_Space: foreach (QModelIndex i, selectionModel()->selectedRows(COL_WANTED)) clicked (i); return false; case Qt::Key_Enter: case Qt::Key_Return: foreach (QModelIndex i, selectionModel()->selectedRows(COL_PRIORITY)) clicked (i); return false; } } return false; } void FileTreeView :: update (const FileList& files) { update(files, true); } void FileTreeView :: update (const FileList& files, bool torrentChanged) { foreach (const TrFile file, files) { QList added; myModel.addFile (file.index, file.filename, file.wanted, file.priority, file.size, file.have, added, torrentChanged); foreach (QModelIndex i, added) expand(myProxy->mapFromSource(i)); } } void FileTreeView :: clear () { myModel.clear(); }