diff --git a/retroshare-gui/src/gui/gxschannels/GxsChannelPostFilesModel.cpp b/retroshare-gui/src/gui/gxschannels/GxsChannelPostFilesModel.cpp new file mode 100644 index 000000000..9098af57b --- /dev/null +++ b/retroshare-gui/src/gui/gxschannels/GxsChannelPostFilesModel.cpp @@ -0,0 +1,799 @@ +/******************************************************************************* + * retroshare-gui/src/gui/gxschannels/GxsChannelPostsModel.cpp * + * * + * Copyright 2020 by Cyril Soler * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU Affero General Public License as * + * published by the Free Software Foundation, either version 3 of the * + * License, or (at your option) any later version. * + * * + * This program 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 Affero General Public License for more details. * + * * + * You should have received a copy of the GNU Affero General Public License * + * along with this program. If not, see . * + * * + *******************************************************************************/ + +#include +#include +#include +#include + +#include "gui/common/FilesDefs.h" +#include "util/qtthreadsutils.h" +#include "util/HandleRichText.h" +#include "util/DateTime.h" +#include "retroshare/rsgxsflags.h" +#include "retroshare/rsgxschannels.h" +#include "retroshare/rsexpr.h" + +#include "GxsChannelPostFilesModel.h" + +#define DEBUG_CHANNEL_MODEL + +Q_DECLARE_METATYPE(RsGxsFile) + +static std::ostream& operator<<(std::ostream& o, const QModelIndex& i);// defined elsewhere + +RsGxsChannelPostFilesModel::RsGxsChannelPostFilesModel(QObject *parent) + : QAbstractItemModel(parent) +{ + initEmptyHierarchy(mFiles); +} + +void RsGxsChannelPostFilesModel::initEmptyHierarchy(std::vector& files) +{ + preMods(); + + files.resize(1); // adds a sentinel item + files[0].mName = "Root sentinel post" ; + + postMods(); +} + +void RsGxsChannelPostFilesModel::preMods() +{ + //emit layoutAboutToBeChanged(); //Generate SIGSEGV when click on button move next/prev. + + beginResetModel(); +} +void RsGxsChannelPostFilesModel::postMods() +{ + endResetModel(); + + emit dataChanged(createIndex(0,0,(void*)NULL), createIndex(mFiles.size(),COLUMN_THREAD_NB_COLUMNS-1,(void*)NULL)); +} + +#ifdef TODO +void RsGxsChannelPostsModel::setSortMode(SortMode mode) +{ + preMods(); + + mSortMode = mode; + + postMods(); +} + +void RsGxsForumModel::initEmptyHierarchy(std::vector& posts) +{ + preMods(); + + posts.resize(1); // adds a sentinel item + posts[0].mTitle = "Root sentinel post" ; + posts[0].mParent = 0; + + postMods(); +} +#endif + +int RsGxsChannelPostFilesModel::rowCount(const QModelIndex& parent) const +{ + if(parent.column() > 0) + return 0; + + if(mFiles.empty()) // security. Should never happen. + return 0; + + if(!parent.isValid()) + return getChildrenCount(0); + + RsErr() << __PRETTY_FUNCTION__ << " rowCount cannot figure out the porper number of rows." << std::endl; + return 0; +} + +int RsGxsChannelPostFilesModel::columnCount(const QModelIndex &/*parent*/) const +{ + return COLUMN_THREAD_NB_COLUMNS ; +} + +// std::vector > RsGxsChannelPostsModel::getPostVersions(const RsGxsMessageId& mid) const +// { +// auto it = mPostVersions.find(mid); +// +// if(it != mPostVersions.end()) +// return it->second; +// else +// return std::vector >(); +// } + +bool RsGxsChannelPostFilesModel::getFileData(const QModelIndex& i,RsGxsFile& fmpe) const +{ + if(!i.isValid()) + return true; + + quintptr ref = i.internalId(); + uint32_t entry = 0; + + if(!convertRefPointerToTabEntry(ref,entry) || entry >= mFiles.size()) + return false ; + + fmpe = mFiles[entry]; + + return true; + +} + +bool RsGxsChannelPostFilesModel::hasChildren(const QModelIndex &parent) const +{ + if(!parent.isValid()) + return true; + + return false; // by default, no channel post has children +} + +bool RsGxsChannelPostFilesModel::convertTabEntryToRefPointer(uint32_t entry,quintptr& ref) +{ + // the pointer is formed the following way: + // + // [ 32 bits ] + // + // This means that the whole software has the following build-in limitation: + // * 4 B simultaenous posts. Should be enough ! + + ref = (intptr_t)entry; + + return true; +} + +bool RsGxsChannelPostFilesModel::convertRefPointerToTabEntry(quintptr ref, uint32_t& entry) +{ + intptr_t val = (intptr_t)ref; + + if(val > (1<<30)) // make sure the pointer is an int that fits in 32bits and not too big which would look suspicious + { + RsErr() << "(EE) trying to make a ChannelPostsModelIndex out of a number that is larger than 2^32-1 !" << std::endl; + return false ; + } + entry = quintptr(val); + + return true; +} + +QModelIndex RsGxsChannelPostFilesModel::index(int row, int column, const QModelIndex & parent) const +{ + if(row < 0 || column < 0 || column >= COLUMN_THREAD_NB_COLUMNS) + return QModelIndex(); + + quintptr ref = getChildRef(parent.internalId(),row); + +#ifdef DEBUG_CHANNEL_MODEL + std::cerr << "index-3(" << row << "," << column << " parent=" << parent << ") : " << createIndex(row,column,ref) << std::endl; +#endif + return createIndex(row,column,ref) ; +} + +QModelIndex RsGxsChannelPostFilesModel::parent(const QModelIndex& index) const +{ + if(!index.isValid()) + return QModelIndex(); + + return QModelIndex(); // there's no hierarchy here. So nothing to do! +} + +Qt::ItemFlags RsGxsChannelPostFilesModel::flags(const QModelIndex& index) const +{ + if (!index.isValid()) + return 0; + + return QAbstractItemModel::flags(index); +} + +quintptr RsGxsChannelPostFilesModel::getChildRef(quintptr ref,int index) const +{ + if (index < 0) + return 0; + + ChannelPostFilesModelIndex entry ; + + if(!convertRefPointerToTabEntry(ref,entry) || entry >= mFiles.size()) + return 0 ; + + if(entry == 0) + { + quintptr new_ref; + convertTabEntryToRefPointer(index+1,new_ref); + return new_ref; + } + else + return 0 ; +} + +quintptr RsGxsChannelPostFilesModel::getParentRow(quintptr ref,int& row) const +{ + ChannelPostFilesModelIndex ref_entry; + + if(!convertRefPointerToTabEntry(ref,ref_entry) || ref_entry >= mFiles.size()) + return 0 ; + + if(ref_entry == 0) + { + RsErr() << "getParentRow() shouldn't be asked for the parent of NULL" << std::endl; + row = 0; + } + else + row = ref_entry-1; + + return 0; +} + +int RsGxsChannelPostFilesModel::getChildrenCount(quintptr ref) const +{ + uint32_t entry = 0 ; + + if(!convertRefPointerToTabEntry(ref,entry) || entry >= mFiles.size()) + return 0 ; + + if(entry == 0) + { +#ifdef DEBUG_CHANNEL_MODEL + std::cerr << "Children count (flat mode): " << mFiles.size()-1 << std::endl; +#endif + return ((int)mFiles.size())-1; + } + else + return 0; +} + +QVariant RsGxsChannelPostFilesModel::data(const QModelIndex &index, int role) const +{ +#ifdef DEBUG_CHANNEL_MODEL + std::cerr << "calling data(" << index << ") role=" << role << std::endl; +#endif + + if(!index.isValid()) + return QVariant(); + + switch(role) + { + case Qt::SizeHintRole: return sizeHintRole(index.column()) ; + case Qt::StatusTipRole:return QVariant(); + default: break; + } + + quintptr ref = (index.isValid())?index.internalId():0 ; + uint32_t entry = 0; + +#ifdef DEBUG_CHANNEL_MODEL + std::cerr << "data(" << index << ")" ; +#endif + + if(!ref) + { +#ifdef DEBUG_CHANNEL_MODEL + std::cerr << " [empty]" << std::endl; +#endif + return QVariant() ; + } + + if(!convertRefPointerToTabEntry(ref,entry) || entry >= mFiles.size()) + { +#ifdef DEBUG_CHANNEL_MODEL + std::cerr << "Bad pointer: " << (void*)ref << std::endl; +#endif + return QVariant() ; + } + + const RsGxsFile& fmpe(mFiles[entry]); + +#ifdef TODO + if(role == Qt::FontRole) + { + QFont font ; + font.setBold( (fmpe.mPostFlags & (ForumModelPostEntry::FLAG_POST_HAS_UNREAD_CHILDREN | ForumModelPostEntry::FLAG_POST_IS_PINNED)) || IS_MSG_UNREAD(fmpe.mMsgStatus)); + return QVariant(font); + } + + if(role == UnreadChildrenRole) + return bool(fmpe.mPostFlags & ForumModelPostEntry::FLAG_POST_HAS_UNREAD_CHILDREN); + +#ifdef DEBUG_CHANNEL_MODEL + std::cerr << " [ok]" << std::endl; +#endif +#endif + + switch(role) + { + case Qt::DisplayRole: return displayRole (fmpe,index.column()) ; + case Qt::UserRole: return userRole (fmpe,index.column()) ; +#ifdef TODO + case Qt::DecorationRole: return decorationRole(fmpe,index.column()) ; + case Qt::ToolTipRole: return toolTipRole (fmpe,index.column()) ; + case Qt::TextColorRole: return textColorRole (fmpe,index.column()) ; + case Qt::BackgroundRole: return backgroundRole(fmpe,index.column()) ; + + case FilterRole: return filterRole (fmpe,index.column()) ; + case ThreadPinnedRole: return pinnedRole (fmpe,index.column()) ; + case MissingRole: return missingRole (fmpe,index.column()) ; + case StatusRole: return statusRole (fmpe,index.column()) ; + case SortRole: return sortRole (fmpe,index.column()) ; +#endif + default: + return QVariant(); + } +} + +#ifdef TODO +QVariant RsGxsForumModel::textColorRole(const ForumModelPostEntry& fmpe,int /*column*/) const +{ + if( (fmpe.mPostFlags & ForumModelPostEntry::FLAG_POST_IS_MISSING)) + return QVariant(mTextColorMissing); + + if(IS_MSG_UNREAD(fmpe.mMsgStatus) || (fmpe.mPostFlags & ForumModelPostEntry::FLAG_POST_IS_PINNED)) + return QVariant(mTextColorUnread); + else + return QVariant(mTextColorRead); + + return QVariant(); +} + +QVariant RsGxsForumModel::statusRole(const ForumModelPostEntry& fmpe,int column) const +{ + if(column != COLUMN_THREAD_DATA) + return QVariant(); + + return QVariant(fmpe.mMsgStatus); +} + +QVariant RsGxsForumModel::filterRole(const ForumModelPostEntry& fmpe,int /*column*/) const +{ + if(!mFilteringEnabled || (fmpe.mPostFlags & ForumModelPostEntry::FLAG_POST_CHILDREN_PASSES_FILTER)) + return QVariant(FilterString); + + return QVariant(QString()); +} + +uint32_t RsGxsForumModel::recursUpdateFilterStatus(ForumModelIndex i,int column,const QStringList& strings) +{ + QString s ; + uint32_t count = 0; + + switch(column) + { + default: + case COLUMN_THREAD_DATE: + case COLUMN_THREAD_TITLE: s = displayRole(mPosts[i],column).toString(); + break; + case COLUMN_THREAD_AUTHOR: + { + QString comment ; + QList icons; + + GxsIdDetails::MakeIdDesc(mPosts[i].mAuthorId, false,s, icons, comment,GxsIdDetails::ICON_TYPE_NONE); + } + break; + } + + if(!strings.empty()) + { + mPosts[i].mPostFlags &= ~(ForumModelPostEntry::FLAG_POST_PASSES_FILTER | ForumModelPostEntry::FLAG_POST_CHILDREN_PASSES_FILTER); + + for(auto iter(strings.begin()); iter != strings.end(); ++iter) + if(s.contains(*iter,Qt::CaseInsensitive)) + { + mPosts[i].mPostFlags |= ForumModelPostEntry::FLAG_POST_PASSES_FILTER | ForumModelPostEntry::FLAG_POST_CHILDREN_PASSES_FILTER; + + count++; + break; + } + } + else + { + mPosts[i].mPostFlags |= ForumModelPostEntry::FLAG_POST_PASSES_FILTER |ForumModelPostEntry::FLAG_POST_CHILDREN_PASSES_FILTER; + count++; + } + + for(uint32_t j=0;j 0) + mPosts[i].mPostFlags |= ForumModelPostEntry::FLAG_POST_CHILDREN_PASSES_FILTER; + } + + return count; +} + + +void RsGxsForumModel::setFilter(int column,const QStringList& strings,uint32_t& count) +{ + preMods(); + + if(!strings.empty()) + { + count = recursUpdateFilterStatus(ForumModelIndex(0),column,strings); + mFilteringEnabled = true; + } + else + { + count=0; + mFilteringEnabled = false; + } + + postMods(); +} + +QVariant RsGxsForumModel::missingRole(const ForumModelPostEntry& fmpe,int /*column*/) const +{ + if(fmpe.mPostFlags & ForumModelPostEntry::FLAG_POST_IS_MISSING) + return QVariant(true); + else + return QVariant(false); +} + +QVariant RsGxsForumModel::toolTipRole(const ForumModelPostEntry& fmpe,int column) const +{ + if(column == COLUMN_THREAD_DISTRIBUTION) + switch(fmpe.mReputationWarningLevel) + { + case 3: return QVariant(tr("Information for this identity is currently missing.")) ; + case 2: return QVariant(tr("You have banned this ID. The message will not be\ndisplayed nor forwarded to your friends.")) ; + case 1: return QVariant(tr("You have not set an opinion for this person,\n and your friends do not vote positively: Spam regulation \nprevents the message to be forwarded to your friends.")) ; + case 0: return QVariant(tr("Message will be forwarded to your friends.")) ; + default: + return QVariant("[ERROR: missing reputation level information - contact the developers]"); + } + + if(column == COLUMN_THREAD_AUTHOR) + { + QString str,comment ; + QList icons; + + if(!GxsIdDetails::MakeIdDesc(fmpe.mAuthorId, true, str, icons, comment,GxsIdDetails::ICON_TYPE_AVATAR)) + return QVariant(); + + int S = QFontMetricsF(QApplication::font()).height(); + QImage pix( (*icons.begin()).pixmap(QSize(4*S,4*S)).toImage()); + + QString embeddedImage; + if(RsHtml::makeEmbeddedImage(pix.scaled(QSize(4*S,4*S), Qt::KeepAspectRatio, Qt::SmoothTransformation), embeddedImage, 8*S * 8*S)) + comment = "
" + embeddedImage + "" + comment + "
"; + + return comment; + } + + return QVariant(); +} + +QVariant RsGxsForumModel::pinnedRole(const ForumModelPostEntry& fmpe,int /*column*/) const +{ + if(fmpe.mPostFlags & ForumModelPostEntry::FLAG_POST_IS_PINNED) + return QVariant(true); + else + return QVariant(false); +} + +QVariant RsGxsForumModel::backgroundRole(const ForumModelPostEntry& fmpe,int /*column*/) const +{ + if(fmpe.mPostFlags & ForumModelPostEntry::FLAG_POST_IS_PINNED) + return QVariant(QBrush(QColor(255,200,180))); + + if(mFilteringEnabled && (fmpe.mPostFlags & ForumModelPostEntry::FLAG_POST_PASSES_FILTER)) + return QVariant(QBrush(QColor(255,240,210))); + + return QVariant(); +} +#endif + +QVariant RsGxsChannelPostFilesModel::sizeHintRole(int col) const +{ + float factor = QFontMetricsF(QApplication::font()).height()/14.0f ; + + return QVariant( QSize(factor * 170, factor*14 )); +#ifdef TODO + switch(col) + { + default: + case COLUMN_THREAD_TITLE: return QVariant( QSize(factor * 170, factor*14 )); + case COLUMN_THREAD_DATE: return QVariant( QSize(factor * 75 , factor*14 )); + case COLUMN_THREAD_AUTHOR: return QVariant( QSize(factor * 75 , factor*14 )); + case COLUMN_THREAD_DISTRIBUTION: return QVariant( QSize(factor * 15 , factor*14 )); + } +#endif +} + +#ifdef TODO +QVariant RsGxsForumModel::authorRole(const ForumModelPostEntry& fmpe,int column) const +{ + if(column == COLUMN_THREAD_DATA) + return QVariant(QString::fromStdString(fmpe.mAuthorId.toStdString())); + + return QVariant(); +} + +QVariant RsGxsForumModel::sortRole(const ForumModelPostEntry& fmpe,int column) const +{ + switch(column) + { + case COLUMN_THREAD_DATE: if(mSortMode == SORT_MODE_PUBLISH_TS) + return QVariant(QString::number(fmpe.mPublishTs)); // we should probably have leading zeroes here + else + return QVariant(QString::number(fmpe.mMostRecentTsInThread)); // we should probably have leading zeroes here + + case COLUMN_THREAD_READ: return QVariant((bool)IS_MSG_UNREAD(fmpe.mMsgStatus)); + case COLUMN_THREAD_DISTRIBUTION: return decorationRole(fmpe,column); + case COLUMN_THREAD_AUTHOR: + { + QString str,comment ; + QList icons; + GxsIdDetails::MakeIdDesc(fmpe.mAuthorId, false, str, icons, comment,GxsIdDetails::ICON_TYPE_NONE); + + return QVariant(str); + } + default: + return displayRole(fmpe,column); + } +} +#endif + +QVariant RsGxsChannelPostFilesModel::displayRole(const RsGxsFile& fmpe,int col) const +{ + switch(col) + { + case 0: return QString::fromUtf8(fmpe.mName.c_str()); + case 1: return QString::number(fmpe.mSize); + case 2: { + FileInfo finfo; + if(rsFiles->FileDetails(fmpe.mHash,RS_FILE_HINTS_DOWNLOAD,finfo)) + return qulonglong(finfo.transfered); + else + return 0; + } + default: + return QString(); +#ifdef TODO + case COLUMN_THREAD_TITLE: if(fmpe.mPostFlags & ForumModelPostEntry::FLAG_POST_IS_REDACTED) + return QVariant(tr("[ ... Redacted message ... ]")); + else if(fmpe.mPostFlags & ForumModelPostEntry::FLAG_POST_IS_PINNED) + return QVariant(tr("[PINNED] ") + QString::fromUtf8(fmpe.mTitle.c_str())); + else + return QVariant(QString::fromUtf8(fmpe.mTitle.c_str())); + + case COLUMN_THREAD_READ:return QVariant(); + case COLUMN_THREAD_DATE:{ + if(fmpe.mPostFlags & ForumModelPostEntry::FLAG_POST_IS_MISSING) + return QVariant(QString()); + + QDateTime qtime; + qtime.setTime_t(fmpe.mPublishTs); + + return QVariant(DateTime::formatDateTime(qtime)); + } + + case COLUMN_THREAD_DISTRIBUTION: + case COLUMN_THREAD_AUTHOR:{ + QString name; + RsGxsId id = RsGxsId(fmpe.mAuthorId.toStdString()); + + if(id.isNull()) + return QVariant(tr("[Notification]")); + if(GxsIdTreeItemDelegate::computeName(id,name)) + return name; + return QVariant(tr("[Unknown]")); + } + case COLUMN_THREAD_MSGID: return QVariant(); +#endif +#ifdef TODO + if (filterColumn == COLUMN_THREAD_CONTENT) { + // need content for filter + QTextDocument doc; + doc.setHtml(QString::fromUtf8(msg.mMsg.c_str())); + item->setText(COLUMN_THREAD_CONTENT, doc.toPlainText().replace(QString("\n"), QString(" "))); + } +#endif + } + + + return QVariant("[ERROR]"); +} + +QVariant RsGxsChannelPostFilesModel::userRole(const RsGxsFile& fmpe,int col) const +{ + switch(col) + { + default: + return QVariant::fromValue(fmpe); + } +} + +#ifdef TODO +QVariant RsGxsForumModel::decorationRole(const ForumModelPostEntry& fmpe,int col) const +{ + bool exist=false; + switch(col) + { + case COLUMN_THREAD_DISTRIBUTION: + return QVariant(fmpe.mReputationWarningLevel); + case COLUMN_THREAD_READ: + return QVariant(fmpe.mMsgStatus); + case COLUMN_THREAD_AUTHOR://Return icon as place holder. + return FilesDefs::getIconFromGxsIdCache(RsGxsId(fmpe.mAuthorId.toStdString()),QIcon(), exist); + } + return QVariant(); +} +#endif + +void RsGxsChannelPostFilesModel::clear() +{ + preMods(); + + mFiles.clear(); + initEmptyHierarchy(mFiles); + + postMods(); + emit channelLoaded(); +} + +void RsGxsChannelPostFilesModel::setFiles(const std::list& files) +{ + preMods(); + + beginRemoveRows(QModelIndex(),0,mFiles.size()-1); + endRemoveRows(); + + mFiles.clear(); + initEmptyHierarchy(mFiles); + + for(auto& file:files) + mFiles.push_back(file); + +#ifdef TODO + recursUpdateReadStatusAndTimes(0,has_unread_below,has_read_below) ; + recursUpdateFilterStatus(0,0,QStringList()); +#endif + +#ifdef DEBUG_CHANNEL_MODEL + // debug_dump(); +#endif + + beginInsertRows(QModelIndex(),0,mFiles.size()-1); + endInsertRows(); + + postMods(); + + emit channelLoaded(); +} + +//ChannelPostsModelIndex RsGxsChannelPostsModel::addEntry(std::vector& posts,const ChannelPostsModelPostEntry& entry) +//{ +// uint32_t N = posts.size(); +// posts.push_back(entry); +// +//#ifdef DEBUG_FORUMMODEL +// std::cerr << "Added new entry " << N << " children of " << parent << std::endl; +//#endif +// if(N == parent) +// std::cerr << "(EE) trying to add a post as its own parent!" << std::endl; +// +// return ChannelPostsModelIndex(N); +//} + +//void RsGxsChannelPostsModel::convertMsgToPostEntry(const RsGxsChannelGroup& mChannelGroup,const RsMsgMetaData& msg, bool /*useChildTS*/, ChannelPostsModelPostEntry& fentry) +//{ +// fentry.mTitle = msg.mMsgName; +// fentry.mMsgId = msg.mMsgId; +// fentry.mPublishTs = msg.mPublishTs; +// fentry.mPostFlags = 0; +// fentry.mMsgStatus = msg.mMsgStatus; +// +// // Early check for a message that should be hidden because its author +// // is flagged with a bad reputation +//} + +QModelIndex RsGxsChannelPostFilesModel::getIndexOfFile(const RsFileHash& hash) const +{ + // Brutal search. This is not so nice, so dont call that in a loop! If too costly, we'll use a map. + + for(uint32_t i=1;i& entries,ForumModelIndex index,int depth) +{ + const ForumModelPostEntry& e(entries[index]); + + QDateTime qtime; + qtime.setTime_t(e.mPublishTs); + + std::cerr << std::string(depth*2,' ') << index << " : " << e.mAuthorId.toStdString() << " " + << QString("%1").arg((uint32_t)e.mPostFlags,8,16,QChar('0')).toStdString() << " " + << QString("%1").arg((uint32_t)e.mMsgStatus,8,16,QChar('0')).toStdString() << " " + << qtime.toString().toStdString() << " \"" << e.mTitle << "\"" << std::endl; + + for(uint32_t i=0;i= mPosts.size()) + return ; + + std::cerr << "Setting own opinion for author " << mPosts[entry].mAuthorId + << " to " << static_cast(op) << std::endl; + RsGxsId author_id = mPosts[entry].mAuthorId; + + rsReputations->setOwnOpinion(author_id,op) ; + + // update opinions and distribution flags. No need to re-load all posts. + + for(uint32_t i=0;i * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU Affero General Public License as * + * published by the Free Software Foundation, either version 3 of the * + * License, or (at your option) any later version. * + * * + * This program 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 Affero General Public License for more details. * + * * + * You should have received a copy of the GNU Affero General Public License * + * along with this program. If not, see . * + * * + *******************************************************************************/ + +#pragma once + +#include "retroshare/rsfiles.h" +#include "retroshare/rsgxscommon.h" + +#include +#include + +// This class holds the actual hierarchy of posts, represented by identifiers +// It is responsible for auto-updating when necessary and holds a mutex to allow the Model to +// safely access the data. + +// The model contains a post in place 0 that is the parent of all posts. + +typedef uint32_t ChannelPostFilesModelIndex; + +// This class is the item model used by Qt to display the information + +class RsGxsChannelPostFilesModel : public QAbstractItemModel +{ + Q_OBJECT + +public: + explicit RsGxsChannelPostFilesModel(QObject *parent = NULL); + ~RsGxsChannelPostFilesModel(){} + + static const int COLUMN_THREAD_NB_COLUMNS = 0x03; // columns for name, size, percentage completion + +#ifdef TODO + enum Columns { + COLUMN_THREAD_TITLE =0x00, + COLUMN_THREAD_READ =0x01, + COLUMN_THREAD_DATE =0x02, + COLUMN_THREAD_DISTRIBUTION =0x03, + COLUMN_THREAD_AUTHOR =0x04, + COLUMN_THREAD_CONTENT =0x05, + COLUMN_THREAD_MSGID =0x06, + COLUMN_THREAD_DATA =0x07, + }; + + enum Roles{ SortRole = Qt::UserRole+1, + ThreadPinnedRole = Qt::UserRole+2, + MissingRole = Qt::UserRole+3, + StatusRole = Qt::UserRole+4, + UnreadChildrenRole = Qt::UserRole+5, + FilterRole = Qt::UserRole+6, + }; +#endif + +#ifdef TODO + enum SortMode{ SORT_MODE_PUBLISH_TS = 0x00, + SORT_MODE_CHILDREN_PUBLISH_TS = 0x01, + }; +#endif + + QModelIndex root() const{ return createIndex(0,0,(void*)NULL) ;} + QModelIndex getIndexOfFile(const RsFileHash& hash) const; + + // This method will asynchroneously update the data + void setFiles(const std::list& files); + +#ifdef TODO + void setSortMode(SortMode mode) ; + + void setTextColorRead (QColor color) { mTextColorRead = color;} + void setTextColorUnread (QColor color) { mTextColorUnread = color;} + void setTextColorUnreadChildren(QColor color) { mTextColorUnreadChildren = color;} + void setTextColorNotSubscribed (QColor color) { mTextColorNotSubscribed = color;} + void setTextColorMissing (QColor color) { mTextColorMissing = color;} + void setFilter(int column, const QStringList &strings, uint32_t &count) ; + void setAuthorOpinion(const QModelIndex& indx,RsOpinion op); +#endif + + // Helper functions + + void clear() ; + + // AbstractItemModel functions. + + int rowCount(const QModelIndex& parent = QModelIndex()) const override; + int columnCount(const QModelIndex &parent = QModelIndex()) const override; + bool hasChildren(const QModelIndex &parent = QModelIndex()) const override; + + QModelIndex index(int row, int column, const QModelIndex & parent = QModelIndex()) const override; + QModelIndex parent(const QModelIndex& child) const override; + Qt::ItemFlags flags(const QModelIndex& index) const override; + + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + + // Custom item roles + + QVariant sizeHintRole (int col) const; + QVariant displayRole (const RsGxsFile& fmpe, int col) const; + QVariant toolTipRole (const RsGxsFile& fmpe, int col) const; + QVariant userRole (const RsGxsFile& fmpe, int col) const; +#ifdef TODO + QVariant decorationRole(const ForumModelPostEntry& fmpe, int col) const; + QVariant pinnedRole (const ForumModelPostEntry& fmpe, int col) const; + QVariant missingRole (const ForumModelPostEntry& fmpe, int col) const; + QVariant statusRole (const ForumModelPostEntry& fmpe, int col) const; + QVariant authorRole (const ForumModelPostEntry& fmpe, int col) const; + QVariant sortRole (const ForumModelPostEntry& fmpe, int col) const; + QVariant fontRole (const ForumModelPostEntry& fmpe, int col) const; + QVariant filterRole (const ForumModelPostEntry& fmpe, int col) const; + QVariant textColorRole (const ForumModelPostEntry& fmpe, int col) const; + QVariant backgroundRole(const ForumModelPostEntry& fmpe, int col) const; +#endif + + /*! + * \brief debug_dump + * Dumps the hierarchy of posts in the terminal, to allow checking whether the internal representation is correct. + */ + void debug_dump(); + +signals: + void channelLoaded(); // emitted after the posts have been set. Can be used to updated the UI. + +private: +#ifdef TODO + bool mUseChildTS; + bool mFilteringEnabled; + SortMode mSortMode; +#endif + void preMods() ; + void postMods() ; + + quintptr getParentRow(quintptr ref,int& row) const; + quintptr getChildRef(quintptr ref, int index) const; + int getChildrenCount(quintptr ref) const; + bool getFileData(const QModelIndex& i,RsGxsFile& fmpe) const; + + static bool convertTabEntryToRefPointer(uint32_t entry, quintptr& ref); + static bool convertRefPointerToTabEntry(quintptr ref,uint32_t& entry); + +#ifdef TODO + static void generateMissingItem(const RsGxsMessageId &msgId,ChannelPostsModelPostEntry& entry); +#endif + void initEmptyHierarchy(std::vector &files); + + std::vector mFiles ; // store the list of files for the post +}; diff --git a/retroshare-gui/src/gui/gxschannels/GxsChannelPostsWidgetWithModel.cpp b/retroshare-gui/src/gui/gxschannels/GxsChannelPostsWidgetWithModel.cpp index 0bd08b9d0..0ba8fc607 100644 --- a/retroshare-gui/src/gui/gxschannels/GxsChannelPostsWidgetWithModel.cpp +++ b/retroshare-gui/src/gui/gxschannels/GxsChannelPostsWidgetWithModel.cpp @@ -26,6 +26,7 @@ #include "GxsChannelPostsWidgetWithModel.h" #include "GxsChannelPostsModel.h" +#include "GxsChannelPostFilesModel.h" #include "ui_GxsChannelPostsWidgetWithModel.h" #include "gui/feeds/GxsChannelPostItem.h" #include "gui/gxs/GxsIdDetails.h" @@ -57,7 +58,7 @@ static const int CHANNEL_TABS_POSTS = 1; #define VIEW_MODE_FEEDS 1 #define VIEW_MODE_FILES 2 -Q_DECLARE_METATYPE(RsGxsChannelPost*) +Q_DECLARE_METATYPE(RsGxsFile) void ChannelPostDelegate::paint(QPainter * painter, const QStyleOptionViewItem & option, const QModelIndex & index) const { @@ -137,6 +138,73 @@ QSize ChannelPostDelegate::sizeHint(const QStyleOptionViewItem& option, const QM return QSize(W+IMAGE_MARGIN_FACTOR*w,H + 2*h); } +void ChannelPostFilesDelegate::paint(QPainter * painter, const QStyleOptionViewItem & option, const QModelIndex & index) const +{ + QString byteUnits[4] = {tr("B"), tr("KB"), tr("MB"), tr("GB")}; + + QStyleOptionViewItem opt = option; + QStyleOptionProgressBarV2 newopt; + QRect pixmapRect; + QPixmap pixmap; + qlonglong fileSize; + double dlspeed, multi; + int seconds,minutes, hours, days; + qlonglong remaining; + QString temp ; + qlonglong completed; + qlonglong downloadtime; + qint64 qi64Value; + + // prepare + painter->save(); + painter->setClipRect(opt.rect); + + RsGxsFile file = index.data(Qt::UserRole).value() ; + + QVariant value = index.data(Qt::TextColorRole); + + if(value.isValid() && qvariant_cast(value).isValid()) + opt.palette.setColor(QPalette::Text, qvariant_cast(value)); + + QPalette::ColorGroup cg = option.state & QStyle::State_Enabled ? QPalette::Normal : QPalette::Disabled; + + if(option.state & QStyle::State_Selected) + painter->setPen(opt.palette.color(cg, QPalette::HighlightedText)); + else + painter->setPen(opt.palette.color(cg, QPalette::Text)); + + switch(index.column()) + { + case 0: painter->drawText(option.rect,Qt::AlignLeft,QString::fromUtf8(file.mName.c_str())); + break; + case 1: painter->drawText(option.rect,Qt::AlignLeft,QString::number(file.mSize)); + break; + case 2: { + FileInfo finfo; + if(rsFiles->FileDetails(file.mHash,RS_FILE_HINTS_DOWNLOAD,finfo)) + painter->drawText(option.rect,Qt::AlignLeft,QString::number(finfo.transfered)); + } + break; + default: + break; + } +} + +QSize ChannelPostFilesDelegate::sizeHint(const QStyleOptionViewItem& option, const QModelIndex& index) const +{ + RsGxsFile file = index.data(Qt::UserRole).value() ; + + QFontMetricsF fm(option.font); + + switch(index.column()) + { + case 0: return QSize(fm.width(QString::fromUtf8(file.mName.c_str())),fm.height()); + case 1: return QSize(fm.width(QString::number(file.mSize)),fm.height()); + default: + case 2: return QSize(fm.height() * 20,fm.height()) ; + } +} + /** Constructor */ GxsChannelPostsWidgetWithModel::GxsChannelPostsWidgetWithModel(const RsGxsGroupId &channelId, QWidget *parent) : GxsMessageFrameWidget(rsGxsChannels, parent), @@ -145,9 +213,12 @@ GxsChannelPostsWidgetWithModel::GxsChannelPostsWidgetWithModel(const RsGxsGroupI /* Invoke the Qt Designer generated object setup routine */ ui->setupUi(this); - ui->postsTree->setModel(mThreadModel = new RsGxsChannelPostsModel()); + ui->postsTree->setModel(mChannelPostsModel = new RsGxsChannelPostsModel()); ui->postsTree->setItemDelegate(new ChannelPostDelegate()); + ui->channelPostFiles_TV->setModel(mChannelPostFilesModel = new RsGxsChannelPostFilesModel()); + ui->channelPostFiles_TV->setItemDelegate(new ChannelPostFilesDelegate()); + connect(ui->postsTree->selectionModel(),SIGNAL(selectionChanged(const QItemSelection&,const QItemSelection&)),this,SLOT(showPostDetails())); /* Setup UI helper */ @@ -220,7 +291,7 @@ GxsChannelPostsWidgetWithModel::GxsChannelPostsWidgetWithModel(const RsGxsGroupI setAutoDownload(false); settingsChanged(); - mThreadModel->updateChannel(channelId); + mChannelPostsModel->updateChannel(channelId); mEventHandlerId = 0; // Needs to be asynced because this function is called by another thread! @@ -264,13 +335,31 @@ void GxsChannelPostsWidgetWithModel::showPostDetails() if(!index.isValid()) { ui->postDetails_TE->clear(); + ui->postLogo_LB->clear(); + mChannelPostFilesModel->clear(); return; } RsGxsChannelPost post = index.data(Qt::UserRole).value() ; + mChannelPostFilesModel->setFiles(post.mFiles); + std::cerr << "Showing details about selected index : "<< index.row() << "," << index.column() << std::endl; ui->postDetails_TE->setText(RsHtml().formatText(NULL, QString::fromUtf8(post.mMsg.c_str()), RSHTML_FORMATTEXT_EMBED_SMILEYS | RSHTML_FORMATTEXT_EMBED_LINKS)); + + QPixmap postImage; + + if (post.mThumbnail.mData != NULL) + GxsIdDetails::loadPixmapFromData(post.mThumbnail.mData, post.mThumbnail.mSize, postImage,GxsIdDetails::ORIGINAL); + else + postImage = QPixmap(CHAN_DEFAULT_IMAGE); + + int W = QFontMetricsF(font()).height() * 8; + + // Using fixed width so that the post will not displace the text when we browse. + + ui->postLogo_LB->setPixmap(postImage); + ui->postLogo_LB->setFixedSize(W,postImage.height()/(float)postImage.width()*W); } void GxsChannelPostsWidgetWithModel::updateGroupData() @@ -297,7 +386,7 @@ void GxsChannelPostsWidgetWithModel::updateGroupData() RsQThreadUtils::postToObject( [this,groups]() { mGroup = groups[0]; - mThreadModel->updateChannel(groupId()); + mChannelPostsModel->updateChannel(groupId()); insertChannelDetails(mGroup); } ); }); @@ -328,7 +417,7 @@ void GxsChannelPostsWidgetWithModel::updateDisplay(bool complete) #warning todo //saveExpandedItems(mSavedExpandedMessages); - //if(mGroupId != mThreadModel->currentGroupId()) + //if(mGroupId != mChannelPostsModel->currentGroupId()) // mThreadId.clear(); updateGroupData(); @@ -881,7 +970,7 @@ void GxsChannelPostsWidgetWithModel::blank() mStateHelper->setWidgetEnabled(ui->postButton, false); mStateHelper->setWidgetEnabled(ui->subscribeToolButton, false); - mThreadModel->clear(); + mChannelPostsModel->clear(); groupNameChanged(QString()); //ui->infoWidget->hide(); @@ -1027,3 +1116,4 @@ void GxsChannelPostsWidgetWithModel::setAllMessagesReadDo(bool read, uint32_t &t token = data.mLastToken; } + diff --git a/retroshare-gui/src/gui/gxschannels/GxsChannelPostsWidgetWithModel.h b/retroshare-gui/src/gui/gxschannels/GxsChannelPostsWidgetWithModel.h index 246be0715..ca37e3289 100644 --- a/retroshare-gui/src/gui/gxschannels/GxsChannelPostsWidgetWithModel.h +++ b/retroshare-gui/src/gui/gxschannels/GxsChannelPostsWidgetWithModel.h @@ -36,6 +36,21 @@ class GxsChannelPostItem; class QTreeWidgetItem; class FeedItem; class RsGxsChannelPostsModel; +class RsGxsChannelPostFilesModel; + +class ChannelPostFilesDelegate: public QAbstractItemDelegate +{ + Q_OBJECT + + public: + ChannelPostFilesDelegate(QObject *parent=0) : QAbstractItemDelegate(parent){} + virtual ~ChannelPostFilesDelegate(){} + + void paint(QPainter * painter, const QStyleOptionViewItem & option, const QModelIndex & index) const override; + QSize sizeHint(const QStyleOptionViewItem& option, const QModelIndex& index) const override; + + private: +}; class ChannelPostDelegate: public QAbstractItemDelegate { @@ -137,7 +152,8 @@ private: bool mUseThread; RsEventsHandlerId_t mEventHandlerId ; - RsGxsChannelPostsModel *mThreadModel; + RsGxsChannelPostsModel *mChannelPostsModel; + RsGxsChannelPostFilesModel *mChannelPostFilesModel; UIStateHelper *mStateHelper; /* UI - from Designer */ diff --git a/retroshare-gui/src/gui/gxschannels/GxsChannelPostsWidgetWithModel.ui b/retroshare-gui/src/gui/gxschannels/GxsChannelPostsWidgetWithModel.ui index 5cea1d692..adbe5fb98 100644 --- a/retroshare-gui/src/gui/gxschannels/GxsChannelPostsWidgetWithModel.ui +++ b/retroshare-gui/src/gui/gxschannels/GxsChannelPostsWidgetWithModel.ui @@ -421,28 +421,70 @@ p, li { white-space: pre-wrap; } + + + true + + - 2 + 1 Details - - - - 9 - 9 - 955 - 181 - - - + + + + + + + TextLabel + + + true + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + - + Files + + + + + + + + Download selected files + + + + diff --git a/retroshare-gui/src/retroshare-gui.pro b/retroshare-gui/src/retroshare-gui.pro index 8a1133e02..69dfc6b85 100644 --- a/retroshare-gui/src/retroshare-gui.pro +++ b/retroshare-gui/src/retroshare-gui.pro @@ -1335,6 +1335,7 @@ gxschannels { gui/gxschannels/GxsChannelPostsWidget.h \ gui/gxschannels/GxsChannelPostsWidgetWithModel.h \ gui/gxschannels/GxsChannelPostsModel.h \ + gui/gxschannels/GxsChannelPostFilesModel.h \ gui/gxschannels/GxsChannelFilesWidget.h \ gui/gxschannels/GxsChannelFilesStatusWidget.h \ gui/feeds/GxsChannelGroupItem.h \ @@ -1354,6 +1355,7 @@ gxschannels { gui/gxschannels/GxsChannelPostsWidget.cpp \ gui/gxschannels/GxsChannelPostsWidgetWithModel.cpp \ gui/gxschannels/GxsChannelPostsModel.cpp \ + gui/gxschannels/GxsChannelPostFilesModel.cpp \ gui/gxschannels/GxsChannelFilesWidget.cpp \ gui/gxschannels/GxsChannelFilesStatusWidget.cpp \ gui/gxschannels/GxsChannelGroupDialog.cpp \