Chris@57: /* -*- c-basic-offset: 4 indent-tabs-mode: nil -*- vi:set ts=8 sts=4 sw=4: */ Chris@57: Chris@57: /* Chris@57: EasyMercurial Chris@57: Chris@57: Based on HgExplorer by Jari Korhonen Chris@57: Copyright (c) 2010 Jari Korhonen Chris@644: Copyright (c) 2013 Chris Cannam Chris@644: Copyright (c) 2013 Queen Mary, University of London Chris@57: Chris@57: This program is free software; you can redistribute it and/or Chris@57: modify it under the terms of the GNU General Public License as Chris@57: published by the Free Software Foundation; either version 2 of the Chris@57: License, or (at your option) any later version. See the file Chris@57: COPYING included with this distribution for more information. Chris@57: */ Chris@57: Chris@43: #include "changesetitem.h" Chris@281: #include "changesetscene.h" Chris@117: #include "changesetdetailitem.h" Chris@43: #include "changeset.h" Chris@53: #include "textabbrev.h" Chris@53: #include "colourset.h" Chris@117: #include "debug.h" Chris@43: Chris@43: #include Chris@117: #include Chris@132: #include Chris@140: #include Chris@140: #include Chris@140: #include Chris@140: #include Chris@153: #include Chris@153: #include Chris@43: Chris@390: QImage *ChangesetItem::m_star = 0; Chris@390: Chris@53: ChangesetItem::ChangesetItem(Changeset *cs) : Chris@600: m_changeset(cs), m_detail(0), m_detailVisible(false), Chris@133: m_showBranch(false), m_column(0), m_row(0), m_wide(false), Chris@555: m_current(false), m_closing(false), m_new(false), m_searchMatches(false) Chris@53: { Chris@53: m_font = QFont(); Chris@53: m_font.setPixelSize(11); Chris@53: m_font.setBold(false); Chris@53: m_font.setItalic(false); Chris@168: setCursor(Qt::ArrowCursor); Chris@390: Chris@390: if (!m_star) m_star = new QImage(":images/star.png"); Chris@53: } Chris@53: Chris@600: ChangesetItem::~ChangesetItem() Chris@600: { Chris@600: if (m_detail && !m_detailVisible) delete m_detail; Chris@600: } Chris@600: Chris@141: QString Chris@141: ChangesetItem::getId() Chris@141: { Chris@141: return m_changeset->id(); Chris@141: } Chris@141: Chris@43: QRectF Chris@43: ChangesetItem::boundingRect() const Chris@43: { Chris@55: int w = 100; Chris@55: if (m_wide) w = 180; Chris@250: return QRectF(-((w-50)/2 - 1), -30, w - 3, 90); Chris@43: } Chris@43: Chris@43: void Chris@119: ChangesetItem::showDetail() Chris@117: { Chris@600: if (m_detailVisible) return; Chris@600: if (!m_detail) { Chris@600: m_detail = new ChangesetDetailItem(m_changeset); Chris@600: m_detail->setZValue(zValue() + 1); Chris@600: } Chris@117: scene()->addItem(m_detail); Chris@117: int w = 100; Chris@117: if (m_wide) w = 180; Chris@508: if (isMerge() || isClosingCommit()) w = 60; Chris@124: int h = 80; Chris@600: m_detail->setPos(x() + (w + 50) / 2 + 10 + 0.5, Chris@430: y() - (m_detail->boundingRect().height() - h) / 3 + 0.5); Chris@600: m_detailVisible = true; Chris@119: emit detailShown(); Chris@119: } Chris@119: Chris@119: void Chris@119: ChangesetItem::hideDetail() Chris@119: { Chris@600: if (!m_detailVisible) return; Chris@124: scene()->removeItem(m_detail); Chris@600: m_detailVisible = false; Chris@119: emit detailHidden(); Chris@119: } Chris@119: Chris@555: bool Chris@566: ChangesetItem::matchSearchText(QString text) Chris@555: { Chris@555: m_searchText = text; Chris@566: m_searchMatches = false; Chris@566: if (m_showBranch) { Chris@566: m_searchMatches = (m_changeset->branch().contains Chris@566: (text, Qt::CaseInsensitive)); Chris@566: } Chris@566: if (!m_searchMatches) { Chris@566: m_searchMatches = (m_changeset->comment().contains Chris@566: (text, Qt::CaseInsensitive)); Chris@566: } Chris@555: return m_searchMatches; Chris@555: } Chris@555: Chris@119: void Chris@119: ChangesetItem::mousePressEvent(QGraphicsSceneMouseEvent *e) Chris@119: { Chris@119: DEBUG << "ChangesetItem::mousePressEvent" << endl; Chris@132: if (e->button() == Qt::LeftButton) { Chris@600: if (m_detailVisible) { Chris@132: hideDetail(); Chris@132: } else { Chris@132: showDetail(); Chris@132: } Chris@119: } Chris@117: } Chris@117: Chris@117: void Chris@474: ChangesetItem::contextMenuEvent(QGraphicsSceneContextMenuEvent *) Chris@140: { Chris@474: if (m_detail) { Chris@474: hideDetail(); Chris@474: } Chris@474: Chris@153: m_parentDiffActions.clear(); Chris@288: m_summaryActions.clear(); Chris@153: Chris@140: QMenu *menu = new QMenu; Chris@165: QLabel *label = new QLabel(tr(" Revision: %1") Chris@165: .arg(Changeset::hashOf(m_changeset->id()))); Chris@141: QWidgetAction *wa = new QWidgetAction(menu); Chris@140: wa->setDefaultWidget(label); Chris@140: menu->addAction(wa); Chris@140: menu->addSeparator(); Chris@141: Chris@153: QAction *copyId = menu->addAction(tr("Copy identifier to clipboard")); Chris@153: connect(copyId, SIGNAL(triggered()), this, SLOT(copyIdActivated())); Chris@141: Chris@289: QAction *stat = menu->addAction(tr("Summarise changes")); Chris@289: connect(stat, SIGNAL(triggered()), this, SLOT(showSummaryActivated())); Chris@289: Chris@289: menu->addSeparator(); Chris@289: Chris@281: QStringList parents = m_changeset->parents(); Chris@153: Chris@288: QString leftId, rightId; Chris@288: bool havePositions = false; Chris@288: Chris@281: if (parents.size() > 1) { Chris@281: ChangesetScene *cs = dynamic_cast(scene()); Chris@281: if (cs && parents.size() == 2) { Chris@281: ChangesetItem *i0 = cs->getItemById(parents[0]); Chris@281: ChangesetItem *i1 = cs->getItemById(parents[1]); Chris@281: if (i0 && i1) { Chris@281: if (i0->x() < i1->x()) { Chris@281: leftId = parents[0]; Chris@281: rightId = parents[1]; Chris@281: } else { Chris@281: leftId = parents[1]; Chris@281: rightId = parents[0]; Chris@281: } Chris@281: havePositions = true; Chris@281: } Chris@281: } Chris@288: } Chris@281: Chris@288: if (parents.size() > 1) { Chris@288: if (havePositions) { Chris@281: Chris@288: QAction *diff = menu->addAction(tr("Diff to left parent")); Chris@288: connect(diff, SIGNAL(triggered()), this, SLOT(diffToParentActivated())); Chris@288: m_parentDiffActions[diff] = leftId; Chris@281: Chris@288: diff = menu->addAction(tr("Diff to right parent")); Chris@288: connect(diff, SIGNAL(triggered()), this, SLOT(diffToParentActivated())); Chris@288: m_parentDiffActions[diff] = rightId; Chris@281: Chris@281: } else { Chris@281: Chris@281: foreach (QString parentId, parents) { Chris@288: QString text = tr("Diff to parent %1").arg(Changeset::hashOf(parentId)); Chris@288: QAction *diff = menu->addAction(text); Chris@288: connect(diff, SIGNAL(triggered()), this, SLOT(diffToParentActivated())); Chris@288: m_parentDiffActions[diff] = parentId; Chris@281: } Chris@153: } Chris@153: Chris@153: } else { Chris@153: Chris@288: QAction *diff = menu->addAction(tr("Diff to parent")); Chris@288: connect(diff, SIGNAL(triggered()), this, SLOT(diffToParentActivated())); Chris@153: } Chris@153: Chris@153: QAction *diffCurrent = menu->addAction(tr("Diff to current working folder")); Chris@141: connect(diffCurrent, SIGNAL(triggered()), this, SLOT(diffToCurrentActivated())); Chris@141: Chris@140: menu->addSeparator(); Chris@141: Chris@153: QAction *update = menu->addAction(tr("Update to this revision")); Chris@153: connect(update, SIGNAL(triggered()), this, SLOT(updateActivated())); Chris@153: Chris@140: QAction *merge = menu->addAction(tr("Merge from here to current")); Chris@141: connect(merge, SIGNAL(triggered()), this, SLOT(mergeActivated())); Chris@153: Chris@153: menu->addSeparator(); Chris@153: Chris@278: QAction *branch = menu->addAction(tr("Start new branch...")); Chris@278: branch->setEnabled(m_current); Chris@278: connect(branch, SIGNAL(triggered()), this, SLOT(newBranchActivated())); Chris@278: Chris@514: QAction *closebranch = menu->addAction(tr("Close branch...")); Chris@514: closebranch->setEnabled(m_current); Chris@514: connect(closebranch, SIGNAL(triggered()), this, SLOT(closeBranchActivated())); Chris@514: Chris@153: QAction *tag = menu->addAction(tr("Add tag...")); Chris@141: connect(tag, SIGNAL(triggered()), this, SLOT(tagActivated())); Chris@141: Chris@474: ungrabMouse(); Chris@474: Chris@148: menu->exec(QCursor::pos()); Chris@140: } Chris@140: Chris@153: void Chris@153: ChangesetItem::copyIdActivated() Chris@153: { Chris@153: QClipboard *clipboard = QApplication::clipboard(); Chris@153: clipboard->setText(Changeset::hashOf(m_changeset->id())); Chris@153: } Chris@153: Chris@153: void ChangesetItem::diffToParentActivated() Chris@153: { Chris@153: QAction *a = qobject_cast(sender()); Chris@153: QString parentId; Chris@153: if (m_parentDiffActions.contains(a)) { Chris@153: parentId = m_parentDiffActions[a]; Chris@153: DEBUG << "ChangesetItem::diffToParentActivated: specific parent " Chris@153: << parentId << " selected" << endl; Chris@153: } else { Chris@153: parentId = m_changeset->parents()[0]; Chris@153: DEBUG << "ChangesetItem::diffToParentActivated: " Chris@153: << "no specific parent selected, using first parent " Chris@153: << parentId << endl; Chris@153: } Chris@153: Chris@153: emit diffToParent(getId(), parentId); Chris@153: } Chris@153: Chris@289: void ChangesetItem::showSummaryActivated() Chris@288: { Chris@289: emit showSummary(m_changeset); Chris@288: } Chris@288: Chris@141: void ChangesetItem::updateActivated() { emit updateTo(getId()); } Chris@141: void ChangesetItem::diffToCurrentActivated() { emit diffToCurrent(getId()); } Chris@141: void ChangesetItem::mergeActivated() { emit mergeFrom(getId()); } Chris@141: void ChangesetItem::tagActivated() { emit tag(getId()); } Chris@278: void ChangesetItem::newBranchActivated() { emit newBranch(getId()); } Chris@514: void ChangesetItem::closeBranchActivated() { emit closeBranch(getId()); } Chris@141: Chris@140: void Chris@288: ChangesetItem::paint(QPainter *paint, const QStyleOptionGraphicsItem *, QWidget *) Chris@43: { Chris@508: if (isClosingCommit() || isMerge()) { Chris@508: paintSimple(paint); Chris@387: } else { Chris@387: paintNormal(paint); Chris@387: } Chris@387: } Chris@387: Chris@387: bool Chris@387: ChangesetItem::isMerge() const Chris@387: { Chris@387: return (m_changeset && m_changeset->parents().size() > 1); Chris@387: } Chris@387: Chris@510: bool Chris@510: ChangesetItem::isClosed() const Chris@510: { Chris@510: return (m_changeset && m_changeset->closed()); Chris@510: } Chris@510: Chris@387: void Chris@387: ChangesetItem::paintNormal(QPainter *paint) Chris@387: { Chris@53: paint->save(); Chris@53: Chris@506: int alpha = 255; Chris@510: if (isClosed()) alpha = 90; Chris@506: Chris@53: ColourSet *colourSet = ColourSet::instance(); Chris@53: QColor branchColour = colourSet->getColourFor(m_changeset->branch()); Chris@128: QColor userColour = colourSet->getColourFor(m_changeset->author()); Chris@53: Chris@506: branchColour.setAlpha(alpha); Chris@506: userColour.setAlpha(alpha); Chris@506: Chris@53: QFont f(m_font); Chris@53: Chris@54: QTransform t = paint->worldTransform(); Chris@53: float scale = std::min(t.m11(), t.m22()); Chris@54: Chris@250: bool showText = (scale >= 0.2); Chris@250: bool showProperLines = (scale >= 0.1); Chris@250: Chris@555: if (m_searchText != "") { Chris@555: if (m_searchMatches) { Chris@555: userColour = QColor("#008400"); Chris@555: showProperLines = true; Chris@555: showText = true; Chris@555: } else { Chris@555: branchColour = Qt::gray; Chris@555: userColour = Qt::gray; Chris@555: } Chris@555: } Chris@555: Chris@250: if (!showProperLines) { Chris@54: paint->setPen(QPen(branchColour, 0)); Chris@54: } else { Chris@54: paint->setPen(QPen(branchColour, 2)); Chris@54: } Chris@53: Chris@53: paint->setFont(f); Chris@53: QFontMetrics fm(f); Chris@53: int fh = fm.height(); Chris@55: Chris@55: int width = 100; Chris@55: if (m_wide) width = 180; Chris@55: int x0 = -((width - 50) / 2 - 1); Chris@55: Chris@250: int textwid = width - 7; Chris@250: Chris@250: QString comment; Chris@250: QStringList lines; Chris@250: int lineCount = 3; Chris@250: Chris@250: if (showText) { Chris@250: Chris@250: comment = m_changeset->comment().trimmed(); Chris@250: comment = comment.replace("\\n", " "); Chris@250: comment = comment.replace(QRegExp("^\"\\s*\\**\\s*"), ""); Chris@250: comment = comment.replace(QRegExp("\"$"), ""); Chris@250: comment = comment.replace("\\\"", "\""); Chris@250: Chris@250: comment = TextAbbrev::abbreviate(comment, fm, textwid, Chris@250: TextAbbrev::ElideEnd, "...", 3); Chris@250: // abbreviate() changes this (ouch!), restore it Chris@250: textwid = width - 5; Chris@250: Chris@250: lines = comment.split('\n'); Chris@250: lineCount = lines.size(); Chris@250: Chris@250: if (lineCount < 2) lineCount = 2; Chris@250: } Chris@250: Chris@250: int height = (lineCount + 1) * fh + 2; Chris@56: QRectF r(x0, 0, width - 3, height); Chris@250: Chris@521: QColor textColour = Qt::black; Chris@521: textColour.setAlpha(alpha); Chris@521: Chris@521: if (m_showBranch && showText) { Chris@521: // write branch name Chris@521: paint->save(); Chris@521: f.setBold(true); Chris@521: paint->setFont(f); Chris@521: paint->setPen(QPen(branchColour)); Chris@521: QString branch = m_changeset->branch(); Chris@521: if (branch == "") branch = "default"; Chris@521: int wid = width - 3; Chris@521: branch = TextAbbrev::abbreviate(branch, QFontMetrics(f), wid); Chris@521: paint->drawText(x0, -fh + fm.ascent() - 4, branch); Chris@521: f.setBold(false); Chris@521: paint->restore(); Chris@521: } Chris@521: Chris@521: QStringList bookmarks = m_changeset->bookmarks(); Chris@521: if (!bookmarks.empty() && showText) { Chris@521: QString bmText = bookmarks.join(" ").trimmed(); Chris@521: int bw = fm.width(bmText); Chris@521: int bx = x0 + width - bw - 14; Chris@521: if (m_current) bx = bx - fh*1.5 + 3; Chris@521: paint->save(); Chris@521: paint->setPen(QPen(branchColour, 2)); Chris@521: // paint->setBrush(QBrush(Qt::white)); Chris@521: paint->setBrush(QBrush(branchColour)); Chris@521: paint->drawRoundedRect(QRectF(bx, -fh - 4, bw + 4, fh * 2), 5, 5); Chris@521: paint->setPen(QPen(Qt::white)); Chris@521: paint->drawText(bx + 2, -fh + fm.ascent() - 4, bmText); Chris@521: paint->restore(); Chris@521: } Chris@521: Chris@250: if (showProperLines) { Chris@250: Chris@393: if (m_new) { Chris@396: paint->setBrush(QColor(255, 255, 220)); Chris@393: } else { Chris@393: paint->setBrush(Qt::white); Chris@393: } Chris@250: Chris@250: if (m_current) { Chris@386: paint->drawRoundedRect(QRectF(x0 - 4, -4, width + 5, height + 8), Chris@386: 10, 10); Chris@393: if (m_new) { Chris@393: paint->save(); Chris@393: paint->setPen(Qt::yellow); Chris@393: paint->setBrush(Qt::NoBrush); Chris@393: paint->drawRoundedRect(QRectF(x0 - 2, -2, width + 1, height + 4), Chris@393: 10, 10); Chris@393: paint->restore(); Chris@393: } Chris@250: } Chris@250: } Chris@250: Chris@250: if (!showText) { Chris@386: paint->drawRoundedRect(r, 7, 7); Chris@53: paint->restore(); Chris@53: return; Chris@53: } Chris@53: Chris@386: paint->save(); Chris@386: paint->setPen(Qt::NoPen); Chris@386: paint->drawRoundedRect(r, 7, 7); Chris@386: paint->setBrush(QBrush(userColour)); Chris@386: paint->drawRoundedRect(QRectF(x0 + 0.5, 0.5, width - 4, fh - 0.5), 7, 7); chris@391: paint->drawRect(QRectF(x0 + 0.5, fh/2.0, width - 4, fh/2.0)); Chris@386: paint->restore(); Chris@53: Chris@53: paint->setPen(QPen(Qt::white)); Chris@53: Chris@250: QString person = TextAbbrev::abbreviate(m_changeset->authorName(), Chris@250: fm, textwid); Chris@55: paint->drawText(x0 + 3, fm.ascent(), person); Chris@53: Chris@506: paint->setPen(QPen(textColour)); Chris@53: Chris@147: QStringList tags = m_changeset->tags(); Chris@147: if (!tags.empty()) { Chris@147: QStringList nonTipTags; Chris@147: foreach (QString t, tags) { Chris@147: // I'm not convinced that showing the tip tag really Chris@147: // works; I think perhaps it confuses as much as it Chris@147: // illuminates. But also, our current implementation Chris@147: // doesn't interact well with it because it moves -- it's Chris@147: // the one thing that can actually (in normal use) change Chris@147: // inside an existing changeset record even during an Chris@147: // incremental update Chris@147: if (t != "tip") nonTipTags.push_back(t); Chris@147: } Chris@147: if (!nonTipTags.empty()) { Chris@147: QString tagText = nonTipTags.join(" ").trimmed(); Chris@147: int tw = fm.width(tagText); Chris@147: paint->fillRect(QRectF(x0 + width - 8 - tw, 1, tw + 4, fh - 1), Chris@147: QBrush(Qt::yellow)); Chris@147: paint->drawText(x0 + width - 6 - tw, fm.ascent(), tagText); Chris@147: } Chris@128: } Chris@128: Chris@520: paint->setPen(QPen(branchColour, 2)); Chris@520: paint->setBrush(Qt::NoBrush); Chris@520: paint->drawRoundedRect(r, 7, 7); Chris@520: chris@392: if (m_current && showProperLines) { chris@392: paint->setRenderHint(QPainter::SmoothPixmapTransform, true); chris@392: int starSize = fh * 1.5; chris@392: paint->drawImage(QRectF(x0 + width - starSize, chris@392: -fh, starSize, starSize), chris@392: *m_star); chris@392: } chris@392: Chris@53: paint->setFont(f); Chris@53: Chris@555: if (m_searchMatches) paint->setPen(userColour); Chris@555: Chris@53: for (int i = 0; i < lines.size(); ++i) { Chris@55: paint->drawText(x0 + 3, i * fh + fh + fm.ascent(), lines[i].trimmed()); Chris@53: } Chris@53: Chris@53: paint->restore(); Chris@43: } Chris@387: Chris@387: void Chris@508: ChangesetItem::paintSimple(QPainter *paint) Chris@387: { Chris@387: paint->save(); Chris@506: Chris@506: int alpha = 255; Chris@510: if (isClosed()) alpha = 90; Chris@387: Chris@387: ColourSet *colourSet = ColourSet::instance(); Chris@387: QColor branchColour = colourSet->getColourFor(m_changeset->branch()); Chris@387: QColor userColour = colourSet->getColourFor(m_changeset->author()); Chris@387: Chris@506: branchColour.setAlpha(alpha); Chris@506: userColour.setAlpha(alpha); Chris@506: Chris@387: QFont f(m_font); Chris@387: Chris@387: QTransform t = paint->worldTransform(); Chris@387: float scale = std::min(t.m11(), t.m22()); Chris@387: if (scale > 1.0) { Chris@387: int ps = int((f.pixelSize() / scale) + 0.5); Chris@387: if (ps < 8) ps = 8; Chris@387: f.setPixelSize(ps); Chris@387: } Chris@387: Chris@387: bool showText = (scale >= 0.2); Chris@387: bool showProperLines = (scale >= 0.1); Chris@387: Chris@387: if (!showProperLines) { Chris@387: paint->setPen(QPen(branchColour, 0)); Chris@387: } else { Chris@387: paint->setPen(QPen(branchColour, 2)); Chris@387: } Chris@387: Chris@387: paint->setFont(f); Chris@387: QFontMetrics fm(f); Chris@387: int fh = fm.height(); Chris@387: int size = fh * 2; Chris@387: int x0 = -size/2 + 25; Chris@387: Chris@393: if (m_new) { Chris@396: paint->setBrush(QColor(255, 255, 220)); Chris@393: } else { Chris@393: paint->setBrush(Qt::white); Chris@393: } Chris@390: Chris@390: if (showProperLines) { Chris@390: Chris@390: if (m_current) { Chris@508: if (isClosingCommit()) { Chris@508: paint->drawRect(QRectF(x0 - 4, fh - 4, size + 8, size + 8)); Chris@508: } else { Chris@508: paint->drawEllipse(QRectF(x0 - 4, fh - 4, size + 8, size + 8)); Chris@508: } Chris@508: Chris@393: if (m_new) { Chris@393: paint->save(); Chris@393: paint->setPen(Qt::yellow); Chris@393: paint->setBrush(Qt::NoBrush); Chris@508: if (isClosingCommit()) { Chris@508: paint->drawRect(QRectF(x0 - 2, fh - 2, size + 4, size + 4)); Chris@508: } else { Chris@508: paint->drawEllipse(QRectF(x0 - 2, fh - 2, size + 4, size + 4)); Chris@508: } Chris@393: paint->restore(); Chris@393: } Chris@390: } Chris@390: } Chris@390: Chris@508: if (isClosingCommit()) { Chris@508: paint->drawRect(QRectF(x0, fh, size, size)); Chris@508: } else { Chris@508: paint->drawEllipse(QRectF(x0, fh, size, size)); Chris@508: } Chris@387: Chris@387: if (m_showBranch) { Chris@387: // write branch name Chris@387: paint->save(); Chris@387: f.setBold(true); Chris@387: paint->setFont(f); Chris@387: paint->setPen(QPen(branchColour)); Chris@387: QString branch = m_changeset->branch(); Chris@387: if (branch == "") branch = "default"; Chris@387: int wid = size * 3; Chris@387: branch = TextAbbrev::abbreviate(branch, QFontMetrics(f), wid); Chris@387: paint->drawText(-wid/2 + 25, fm.ascent() - 4, branch); Chris@387: f.setBold(false); Chris@387: paint->restore(); Chris@387: } Chris@387: Chris@390: if (m_current && showProperLines) { Chris@390: paint->setRenderHint(QPainter::SmoothPixmapTransform, true); Chris@390: int starSize = fh * 1.5; Chris@390: paint->drawImage(QRectF(x0 + size - starSize/2, Chris@390: 0, starSize, starSize), Chris@390: *m_star); Chris@390: } Chris@390: Chris@387: paint->restore(); Chris@387: } Chris@387: