/* * Bittorrent Client using Qt and libtorrent. * Copyright (C) 2016 Eugene Shalygin * Copyright (C) 2006 Christophe Dumez * * This program 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 2 * 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * * In addition, as a special exception, the copyright holders give permission to * link this program with the OpenSSL project's "OpenSSL" library (or with * modified versions of it that use the same license as the "OpenSSL" library), * and distribute the linked executables. You must obey the GNU General Public * License in all respects for all of the code used other than "OpenSSL". If you * modify file(s), you may extend this exception to your version of the file(s), * but you are not obligated to do so. If you do not wish to do so, delete this * exception statement from your version. */ #include "piecesbar.h" #include #include #include #include #include #include #include "base/bittorrent/torrenthandle.h" #include "base/utils/misc.h" namespace { using ImageRange = IndexRange; // Computes approximate mapping from image scale (measured in pixels) onto the torrent contents scale (in pieces) // However, taking the size of a screen to be ~ 1000 px and the torrent size larger than 10 MiB, the pointing error // is well below 0.5 px and thus is negligible. class PieceIndexToImagePos { public: PieceIndexToImagePos(const BitTorrent::TorrentInfo &torrentInfo, const QImage &image) : m_bytesPerPixel {((image.width() > 0) && (torrentInfo.totalSize() >= image.width())) ? torrentInfo.totalSize() / image.width() : -1} , m_torrentInfo {torrentInfo} { if ((m_bytesPerPixel > 0) && (m_bytesPerPixel < 10)) qDebug() << "PieceIndexToImagePos: torrent size is too small for correct computaions." << "Torrent size =" << torrentInfo.totalSize() << "Image width = " << image.width(); } ImageRange imagePos(const BitTorrent::TorrentInfo::PieceRange &pieces) const { if (m_bytesPerPixel < 0) return {0, 0}; // the type conversion is used to prevent integer overflow with torrents of 2+ GiB size const qlonglong pieceLength = m_torrentInfo.pieceLength(); return makeInterval( (pieces.first() * pieceLength) / m_bytesPerPixel, (pieces.last() * pieceLength + m_torrentInfo.pieceLength(pieces.last()) - 1) / m_bytesPerPixel); } int pieceIndex(int imagePos) const { return m_bytesPerPixel < 0 ? 0 : (imagePos * m_bytesPerPixel + m_bytesPerPixel / 2) / m_torrentInfo.pieceLength(); } private: const qlonglong m_bytesPerPixel; // how many bytes of the torrent are squeezed into a bar's pixel const BitTorrent::TorrentInfo m_torrentInfo; }; class DetailedTooltipRenderer { public: DetailedTooltipRenderer(QTextStream &stream, const QString &header) : m_stream(stream) { m_stream << header << R"()"; } ~DetailedTooltipRenderer() { m_stream << "
"; } void operator()(const QString &size, const QString &path) { m_stream << R"()" << size << "" << path << ""; } private: QTextStream &m_stream; }; } PiecesBar::PiecesBar(QWidget *parent) : QWidget {parent} , m_torrent {nullptr} , m_borderColor {palette().color(QPalette::Dark)} , m_bgColor {Qt::white} , m_pieceColor {Qt::blue} , m_hovered {false} { updatePieceColors(); setMouseTracking(true); } void PiecesBar::setTorrent(BitTorrent::TorrentHandle *torrent) { m_torrent = torrent; if (!m_torrent) clear(); } void PiecesBar::clear() { m_image = QImage(); update(); } void PiecesBar::setColors(const QColor &background, const QColor &border, const QColor &complete) { m_bgColor = background; m_borderColor = border; m_pieceColor = complete; updatePieceColors(); requestImageUpdate(); } bool PiecesBar::event(QEvent *e) { if (e->type() == QEvent::ToolTip) { showToolTip(static_cast(e)); return true; } else { return base::event(e); } } void PiecesBar::enterEvent(QEvent *e) { m_hovered = true; base::enterEvent(e); } void PiecesBar::leaveEvent(QEvent *e) { m_hovered = false; m_highlitedRegion = QRect(); requestImageUpdate(); base::leaveEvent(e); } void PiecesBar::mouseMoveEvent(QMouseEvent *e) { // if user pointed to a piece which is a part of a single large file, // we highlight the space, occupied by this file highlightFile(e->pos().x() - borderWidth); base::mouseMoveEvent(e); } void PiecesBar::paintEvent(QPaintEvent *) { QPainter painter(this); QRect imageRect(borderWidth, borderWidth, width() - 2 * borderWidth, height() - 2 * borderWidth); if (m_image.isNull()) { painter.setBrush(Qt::white); painter.drawRect(imageRect); } else { if (m_image.width() != imageRect.width()) updateImage(m_image); painter.drawImage(imageRect, m_image); } if (!m_highlitedRegion.isNull()) { QColor highlightColor {this->palette().color(QPalette::Active, QPalette::Highlight)}; highlightColor.setAlphaF(0.35); QRect targetHighlightRect {m_highlitedRegion.adjusted(borderWidth, borderWidth, borderWidth, height() - 2 * borderWidth)}; painter.fillRect(targetHighlightRect, highlightColor); } QPainterPath border; border.addRect(0, 0, width(), height()); painter.setPen(m_borderColor); painter.drawPath(border); } void PiecesBar::requestImageUpdate() { if (updateImage(m_image)) update(); } QColor PiecesBar::backgroundColor() const { return m_bgColor; } QColor PiecesBar::borderColor() const { return m_borderColor; } QColor PiecesBar::pieceColor() const { return m_pieceColor; } const QVector &PiecesBar::pieceColors() const { return m_pieceColors; } QRgb PiecesBar::mixTwoColors(QRgb rgb1, QRgb rgb2, float ratio) { int r1 = qRed(rgb1); int g1 = qGreen(rgb1); int b1 = qBlue(rgb1); int r2 = qRed(rgb2); int g2 = qGreen(rgb2); int b2 = qBlue(rgb2); float ratioN = 1.0f - ratio; int r = (r1 * ratioN) + (r2 * ratio); int g = (g1 * ratioN) + (g2 * ratio); int b = (b1 * ratioN) + (b2 * ratio); return qRgb(r, g, b); } void PiecesBar::showToolTip(const QHelpEvent *e) { if (!m_torrent) return; QString toolTipText; QTextStream stream(&toolTipText, QIODevice::WriteOnly); const bool showDetailedInformation = QApplication::keyboardModifiers().testFlag(Qt::ShiftModifier); if (showDetailedInformation && m_torrent->hasMetadata()) { const int imagePos = e->pos().x() - borderWidth; if ((imagePos >=0) && (imagePos < m_image.width())) { stream << ""; PieceIndexToImagePos transform {m_torrent->info(), m_image}; int pieceIndex = transform.pieceIndex(imagePos); QVector files {m_torrent->info().fileIndicesForPiece(pieceIndex)}; QString tooltipTitle; if (files.count() > 1) { tooltipTitle = tr("Files in this piece:"); } else { if (m_torrent->info().fileSize(files.front()) == m_torrent->info().pieceLength(pieceIndex)) tooltipTitle = tr("File in this piece"); else tooltipTitle = tr("File in these pieces"); } DetailedTooltipRenderer renderer(stream, tooltipTitle); const bool isFileNameCorrectionNeeded = this->isFileNameCorrectionNeeded(); for (int f: files) { QString filePath {m_torrent->info().filePath(f)}; if (isFileNameCorrectionNeeded) filePath.replace(QLatin1String("/.unwanted"), QString()); renderer(Utils::Misc::friendlyUnit(m_torrent->info().fileSize(f)), filePath); } stream << ""; } } else { stream << simpleToolTipText(); if (showDetailedInformation) // metadata are not available at this point stream << '\n' << tr("Wait until metadata become available to see detailed information"); else stream << '\n' << tr("Hold Shift key for detailed information"); } stream.flush(); QToolTip::showText(e->globalPos(), toolTipText, this); } void PiecesBar::highlightFile(int imagePos) { if (!m_torrent || !m_torrent->hasMetadata() || (imagePos < 0) || (imagePos >= m_image.width())) return; PieceIndexToImagePos transform {m_torrent->info(), m_image}; int pieceIndex = transform.pieceIndex(imagePos); QVector fileIndices {m_torrent->info().fileIndicesForPiece(pieceIndex)}; if (fileIndices.count() == 1) { BitTorrent::TorrentInfo::PieceRange filePieces = m_torrent->info().filePieces(fileIndices.first()); ImageRange imageRange = transform.imagePos(filePieces); QRect newHighlitedRegion {imageRange.first(), 0, imageRange.size(), m_image.height()}; if (newHighlitedRegion != m_highlitedRegion) { m_highlitedRegion = newHighlitedRegion; update(); } } else if (!m_highlitedRegion.isEmpty()) { m_highlitedRegion = QRect(); update(); } } void PiecesBar::updatePieceColors() { m_pieceColors = QVector(256); for (int i = 0; i < 256; ++i) { float ratio = (i / 255.0); m_pieceColors[i] = mixTwoColors(backgroundColor().rgb(), m_pieceColor.rgb(), ratio); } } bool PiecesBar::isFileNameCorrectionNeeded() const { return false; }