--- a/Plugins/VcsPlugins/vcsMercurial/HgLogBrowserDialog.py Sat Feb 04 20:02:20 2017 +0100 +++ b/Plugins/VcsPlugins/vcsMercurial/HgLogBrowserDialog.py Sat Feb 04 20:18:58 2017 +0100 @@ -18,17 +18,23 @@ import collections from PyQt5.QtCore import pyqtSlot, Qt, QDate, QProcess, QTimer, QRegExp, \ - QSize, QPoint, QUrl -from PyQt5.QtGui import QCursor, QColor, QPixmap, QPainter, QPen, QBrush, QIcon + QSize, QPoint, QFileInfo +from PyQt5.QtGui import QCursor, QColor, QPixmap, QPainter, QPen, QBrush, \ + QIcon, QTextCursor from PyQt5.QtWidgets import QWidget, QDialogButtonBox, QHeaderView, \ QTreeWidgetItem, QApplication, QLineEdit, QMenu, QInputDialog from E5Gui.E5Application import e5App -from E5Gui import E5MessageBox +from E5Gui import E5MessageBox, E5FileDialog from .Ui_HgLogBrowserDialog import Ui_HgLogBrowserDialog +from .HgDiffHighlighter import HgDiffHighlighter +from .HgDiffGenerator import HgDiffGenerator + import UI.PixmapCache +import Preferences +import Utilities COLORNAMES = ["blue", "darkgreen", "red", "green", "darkblue", "purple", "cyan", "olive", "magenta", "darkred", "darkmagenta", @@ -67,6 +73,9 @@ super(HgLogBrowserDialog, self).__init__(parent) self.setupUi(self) + self.diffSplitter.setStretchFactor(0, 1) + self.diffSplitter.setStretchFactor(1, 2) + self.__position = QPoint() if mode == "log": @@ -103,6 +112,14 @@ self.fieldCombo.addItem(self.tr("Message"), "message") self.fieldCombo.addItem(self.tr("File"), "file") + font = Preferences.getEditorOtherFonts("MonospacedFont") + self.diffEdit.setFontFamily(font.family()) + self.diffEdit.setFontPointSize(font.pointSize()) + + self.diffHighlighter = HgDiffHighlighter(self.diffEdit.document()) + self.__diffGenerator = HgDiffGenerator(vcs, self) + self.__diffGenerator.finished.connect(self.__generatorFinished) + self.vcs = vcs if mode in ("log", "incoming", "outgoing"): self.commandMode = mode @@ -118,11 +135,16 @@ "<tr><td><b>Date</b></td><td>{1}</td></tr>" "<tr><td><b>Author</b></td><td>{2}</td></tr>" "<tr><td><b>Branch</b></td><td>{3}</td></tr>" - "<tr><td><b>Parents</b></td><td>{4}</td></tr>" - "<tr><td><b>Children</b></td><td>{5}</td></tr>" - "{6}" + "{4}" + "<tr><td><b>Message</b></td><td>{5}</td></tr>" "</table>" ) + self.__parentsTemplate = self.tr( + "<tr><td><b>Parents</b></td><td>{0}</td></tr>" + ) + self.__childrenTemplate = self.tr( + "<tr><td><b>Children</b></td><td>{0}</td></tr>" + ) self.__tagsTemplate = self.tr( "<tr><td><b>Tags</b></td><td>{0}</td></tr>" ) @@ -136,7 +158,7 @@ self.__bundle = "" self.__filename = "" self.__isFile = False - self.__currentRevision = "" + self.__selectedRevisions = [] self.intercept = False self.__initData() @@ -147,12 +169,16 @@ self.toDate.setDisplayFormat("yyyy-MM-dd") self.__resetUI() + # roles used in the log tree self.__messageRole = Qt.UserRole self.__changesRole = Qt.UserRole + 1 self.__edgesRole = Qt.UserRole + 2 self.__parentsRole = Qt.UserRole + 3 self.__latestTagRole = Qt.UserRole + 4 + # roles used in the file tree + self.__diffFileLineRole = Qt.UserRole + if self.__hgClient: self.process = None else: @@ -606,12 +632,12 @@ "The hg process did not finish within 30s.") else: errMsg = self.tr("Could not start the hg executable.") - - if errMsg: - E5MessageBox.critical( - self, - self.tr("Mercurial Error"), - errMsg) + + if errMsg: + E5MessageBox.critical( + self, + self.tr("Mercurial Error"), + errMsg) if output: parents = [int(p) for p in output.strip().splitlines()] @@ -746,10 +772,15 @@ res = ("", "") if output: for line in output.splitlines(): - name, rev = line.strip().rsplit(None, 1) - if name == tag: - res = tuple(rev.split(":", 1)) - break + if line.strip(): + try: + name, rev = line.strip().rsplit(None, 1) + if name == tag: + res = tuple(rev.split(":", 1)) + break + except ValueError: + # ignore silently + pass return res @@ -850,23 +881,6 @@ return itm - def __generateFileItem(self, action, path, copyfrom): - """ - Private method to generate a changed files tree entry. - - @param action indicator for the change action ("A", "D" or "M") - @param path path of the file in the repository (string) - @param copyfrom path the file was copied from (string) - @return reference to the generated item (QTreeWidgetItem) - """ - itm = QTreeWidgetItem(self.filesTree, [ - self.flags[action], - path, - copyfrom, - ]) - - return itm - def __getLogEntries(self, startRev=None, noEntries=0): """ Private method to retrieve log entries from the repository. @@ -995,8 +1009,7 @@ self.__bundle = bundle self.__isFile = isFile - self.sbsCheckBox.setEnabled(isFile) - self.sbsCheckBox.setVisible(isFile) + self.sbsSelectLabel.clear() self.errorGroup.hide() QApplication.processEvents() @@ -1187,7 +1200,12 @@ self.__resizeColumnsLog() if self.__started: - self.logTree.setCurrentItem(self.logTree.topLevelItem(0)) + if self.__selectedRevisions: + self.logTree.setCurrentItem(self.logTree.findItems( + self.__selectedRevisions[0], Qt.MatchExactly, + self.RevisionColumn)[0]) + else: + self.logTree.setCurrentItem(self.logTree.topLevelItem(0)) self.__started = False if self.commandMode in ("incoming", "outgoing"): @@ -1221,16 +1239,16 @@ self.__filterLogsEnabled = True if self.__actionMode() == "filter": self.__filterLogs() - self.__updateDiffButtons() self.__updateToolMenuActions() # restore current item - if self.__currentRevision: - items = self.logTree.findItems( - self.__currentRevision, Qt.MatchExactly, self.RevisionColumn) - if items: - self.logTree.setCurrentItem(items[0]) - self.__currentRevision = "" + if self.__selectedRevisions: + for revision in self.__selectedRevisions: + items = self.logTree.findItems( + revision, Qt.MatchExactly, self.RevisionColumn) + if items: + items[0].setSelected(True) + self.__selectedRevisions = [] def __readStdout(self): """ @@ -1272,24 +1290,6 @@ self.inputGroup.setEnabled(True) self.inputGroup.show() - def __diffRevisions(self, rev1, rev2): - """ - Private method to do a diff of two revisions. - - @param rev1 first revision number (integer) - @param rev2 second revision number (integer) - """ - if self.sbsCheckBox.isEnabled() and self.sbsCheckBox.isChecked(): - self.vcs.hgSbsDiff(self.__filename, - revisions=(str(rev1), str(rev2))) - else: - if self.diff is None: - from .HgDiffDialog import HgDiffDialog - self.diff = HgDiffDialog(self.vcs) - self.diff.show() - self.diff.raise_() - self.diff.start(self.__filename, [rev1, rev2], self.__bundle) - def on_buttonBox_clicked(self, button): """ Private slot called by a button of the button box clicked. @@ -1307,32 +1307,38 @@ elif button == self.refreshButton: self.on_refreshButton_clicked() - def __updateDiffButtons(self): + def __updateSbsSelectLabel(self): """ Private slot to update the enabled status of the diff buttons. """ - selectionLength = len(self.logTree.selectedItems()) - if selectionLength <= 1: - current = self.logTree.currentItem() - if current is None: - self.diffP1Button.setEnabled(False) - self.diffP2Button.setEnabled(False) - else: - parents = current.data(0, self.__parentsRole) - self.diffP1Button.setEnabled(len(parents) > 0) - self.diffP2Button.setEnabled(len(parents) > 1) - - self.diffRevisionsButton.setEnabled(False) - elif selectionLength == 2: - self.diffP1Button.setEnabled(False) - self.diffP2Button.setEnabled(False) - - self.diffRevisionsButton.setEnabled(True) - else: - self.diffP1Button.setEnabled(False) - self.diffP2Button.setEnabled(False) - - self.diffRevisionsButton.setEnabled(False) + self.sbsSelectLabel.clear() + if self.__isFile: + selectedItems = self.logTree.selectedItems() + if len(selectedItems) == 1: + currentItem = selectedItems[0] + rev2 = currentItem.text(self.RevisionColumn).split(":", 1)[0]\ + .strip() + parents = currentItem.data(0, self.__parentsRole) + if parents: + parentLinks = [] + for index in range(len(parents)): + parentLinks.append( + '<a href="sbsdiff:{0}_{1}"> {2} </a>' + .format(parents[index], rev2, index + 1)) + self.sbsSelectLabel.setText( + self.tr('Side-by-Side Diff to Parent {0}').format( + " ".join(parentLinks))) + elif len(selectedItems) == 2: + rev1 = int(selectedItems[0].text(self.RevisionColumn) + .split(":", 1)[0]) + rev2 = int(selectedItems[1].text(self.RevisionColumn) + .split(":", 1)[0]) + if rev1 > rev2: + # Swap the entries, so that rev1 < rev2 + rev1, rev2 = rev2, rev1 + self.sbsSelectLabel.setText(self.tr( + '<a href="sbsdiff:{0}_{1}">Side-by-Side Compare</a>') + .format(rev1, rev2)) def __updateToolMenuActions(self): """ @@ -1411,36 +1417,63 @@ else: self.actionsButton.setEnabled(False) - def __updateGui(self, itm): + def __updateDetailsAndFiles(self): """ - Private slot to update GUI elements except tool menu actions. - - @param itm reference to the item the update should be based on - (QTreeWidgetItem) + Private slot to update the details and file changes panes. """ self.detailsEdit.clear() - self.messageEdit.clear() self.filesTree.clear() + self.__diffUpdatesFiles = False + selectedItems = self.logTree.selectedItems() + if len(selectedItems) == 1: + self.detailsEdit.setHtml( + self.__generateDetailsTableText(selectedItems[0])) + self.__updateFilesTree(self.filesTree, selectedItems[0]) + self.__resizeColumnsFiles() + self.__resortFiles() + elif len(selectedItems) == 2: + self.__diffUpdatesFiles = True + index1 = self.logTree.indexOfTopLevelItem(selectedItems[0]) + index2 = self.logTree.indexOfTopLevelItem(selectedItems[1]) + if index1 > index2: + # Swap the entries + selectedItems[0], selectedItems[1] = \ + selectedItems[1], selectedItems[0] + html = "{0}<hr/>{1}".format( + self.__generateDetailsTableText(selectedItems[0]), + self.__generateDetailsTableText(selectedItems[1]), + ) + self.detailsEdit.setHtml(html) + # self.filesTree is updated by the diff + + def __generateDetailsTableText(self, itm): + """ + Private method to generate an HTML table with the details of the given + changeset. + + @param itm reference to the item the table should be based on + @type QTreeWidgetItem + @return HTML table containing details + @rtype str + """ if itm is not None: if itm.text(self.TagsColumn): tagsStr = self.__tagsTemplate.format(itm.text(self.TagsColumn)) else: tagsStr = "" + if itm.text(self.BookmarksColumn): bookmarksStr = self.__bookmarksTemplate.format( itm.text(self.BookmarksColumn)) else: bookmarksStr = "" - if itm.data(0, self.__latestTagRole): + if self.projectMode and itm.data(0, self.__latestTagRole): latestTagLinks = [] for tag in itm.data(0, self.__latestTagRole): - url = QUrl() - url.setScheme("rev") - url.setPath(self.__getRevisionOfTag(tag)[0]) - latestTagLinks.append('<a href="{0}">{1}</a>'.format( - url.toString(), tag)) + latestTagLinks.append('<a href="rev:{0}">{1}</a>'.format( + self.__getRevisionOfTag(tag)[0], tag)) latestTagStr = self.__latestTagTemplate.format( ", ".join(latestTagLinks)) else: @@ -1448,43 +1481,64 @@ rev = int(itm.text(self.RevisionColumn).split(":", 1)[0]) - parentLinks = [] - for parent in [str(x) for x in itm.data(0, self.__parentsRole)]: - url = QUrl() - url.setScheme("rev") - url.setPath(parent) - parentLinks.append('<a href="{0}">{1}</a>'.format( - url.toString(), parent)) + if itm.data(0, self.__parentsRole): + parentLinks = [] + for parent in [str(x) for x in + itm.data(0, self.__parentsRole)]: + parentLinks.append( + '<a href="rev:{0}">{0}</a>'.format(parent)) + parentsStr = self.__parentsTemplate.format( + ", ".join(parentLinks)) + else: + parentsStr = "" - childLinks = [] - for child in [str(x) for x in self.__childrenInfo[rev]]: - url = QUrl() - url.setScheme("rev") - url.setPath(child) - childLinks.append('<a href="{0}">{1}</a>'.format( - url.toString(), child)) + if self.__childrenInfo[rev]: + childLinks = [] + for child in [str(x) for x in self.__childrenInfo[rev]]: + childLinks.append( + '<a href="rev:{0}">{0}</a>'.format(child)) + childrenStr = self.__childrenTemplate.format( + ", ".join(childLinks)) + else: + childrenStr = "" - self.detailsEdit.setHtml(self.__detailsTemplate.format( + messageStr = "<br />\n".join([ + line.strip() for line in itm.data(0, self.__messageRole) + ]) + + html = self.__detailsTemplate.format( itm.text(self.RevisionColumn), itm.text(self.DateColumn), itm.text(self.AuthorColumn), itm.text(self.BranchColumn).replace( self.ClosedIndicator, ""), - ", ".join(parentLinks), - ", ".join(childLinks), - tagsStr + latestTagStr + bookmarksStr, - )) - - for line in itm.data(0, self.__messageRole): - self.messageEdit.append(line.strip()) - + parentsStr + childrenStr + tagsStr + latestTagStr + + bookmarksStr, + messageStr, + ) + else: + html = "" + + return html + + def __updateFilesTree(self, parent, itm): + """ + Private method to update the files tree with changes of the given item. + + @param parent parent for the items to be added + @type QTreeWidget or QTreeWidgetItem + @param itm reference to the item the update should be based on + @type QTreeWidgetItem + """ + if itm is not None: changes = itm.data(0, self.__changesRole) if len(changes) > 0: for change in changes: - self.__generateFileItem( - change["action"], change["path"], change["copyfrom"]) - self.__resizeColumnsFiles() - self.__resortFiles() + QTreeWidgetItem(parent, [ + self.flags[change["action"]], + change["path"].strip(), + change["copyfrom"].strip(), + ]) @pyqtSlot(QTreeWidgetItem, QTreeWidgetItem) def on_logTree_currentItemChanged(self, current, previous): @@ -1494,8 +1548,6 @@ @param current reference to the new current item (QTreeWidgetItem) @param previous reference to the old current item (QTreeWidgetItem) """ - self.__updateGui(current) - self.__updateDiffButtons() self.__updateToolMenuActions() # Highlight the current entry using a bold font @@ -1505,7 +1557,7 @@ # set the state of the up and down buttons self.upButton.setEnabled( - current is not None and + current is not None and self.logTree.indexOfTopLevelItem(current) > 0) self.downButton.setEnabled( current is not None and @@ -1516,8 +1568,10 @@ """ Private slot called, when the selection has changed. """ - self.__updateDiffButtons() + self.__updateDetailsAndFiles() + self.__updateSbsSelectLabel() self.__updateToolMenuActions() + self.__generateDiffs() @pyqtSlot() def on_upButton_clicked(self): @@ -1549,60 +1603,6 @@ if self.__lastRev > 0: self.__getLogEntries(startRev=self.__lastRev - 1) - @pyqtSlot() - def on_diffP1Button_clicked(self): - """ - Private slot to handle the Diff to Parent 1 button. - """ - if len(self.logTree.selectedItems()): - itm = self.logTree.selectedItems()[0] - else: - itm = self.logTree.currentItem() - if itm is None: - self.diffP1Button.setEnabled(False) - return - rev2 = int(itm.text(self.RevisionColumn).split(":")[0]) - - rev1 = itm.data(0, self.__parentsRole)[0] - if rev1 < 0: - self.diffP1Button.setEnabled(False) - return - - self.__diffRevisions(rev1, rev2) - - @pyqtSlot() - def on_diffP2Button_clicked(self): - """ - Private slot to handle the Diff to Parent 2 button. - """ - if len(self.logTree.selectedItems()): - itm = self.logTree.selectedItems()[0] - else: - itm = self.logTree.currentItem() - if itm is None: - self.diffP2Button.setEnabled(False) - return - rev2 = int(itm.text(self.RevisionColumn).split(":")[0]) - - rev1 = itm.data(0, self.__parentsRole)[1] - if rev1 < 0: - self.diffP2Button.setEnabled(False) - return - - self.__diffRevisions(rev1, rev2) - - @pyqtSlot() - def on_diffRevisionsButton_clicked(self): - """ - Private slot to handle the Compare Revisions button. - """ - items = self.logTree.selectedItems() - - rev2 = int(items[0].text(self.RevisionColumn).split(":")[0]) - rev1 = int(items[1].text(self.RevisionColumn).split(":")[0]) - - self.__diffRevisions(min(rev1, rev2), max(rev1, rev2)) - @pyqtSlot(QDate) def on_fromDate_dateChanged(self, date): """ @@ -1702,7 +1702,6 @@ else: topItem.setHidden(True) if topItem is currentItem: - self.messageEdit.clear() self.filesTree.clear() visibleItemCount -= 1 self.logTree.header().setSectionHidden( @@ -1763,12 +1762,10 @@ self.refreshButton.setEnabled(False) - # save the current items commit ID - itm = self.logTree.currentItem() - if itm is not None: - self.__currentRevision = itm.text(self.RevisionColumn) - else: - self.__currentRevision = "" + # save the selected items commit IDs + self.__selectedRevisions = [] + for item in self.logTree.selectedItems(): + self.__selectedRevisions.append(item.text(self.RevisionColumn)) if self.initialCommandMode in ("incoming", "outgoing"): self.nextButton.setEnabled(False) @@ -2196,3 +2193,227 @@ # load the next batch and try again self.on_nextButton_clicked() self.__revisionClicked(url) + + ########################################################################### + ## Diff handling methods below + ########################################################################### + + def __generateDiffs(self, parent=1): + """ + Private slot to generate diff outputs for the selected item. + + @param parent number of parent to diff against + @type int + """ + self.diffEdit.clear() + self.diffLabel.setText(self.tr("Differences")) + self.diffSelectLabel.clear() + + selectedItems = self.logTree.selectedItems() + if len(selectedItems) == 1: + currentItem = selectedItems[0] + rev2 = currentItem.text(self.RevisionColumn).split(":", 1)[0] + parents = currentItem.data(0, self.__parentsRole) + if len(parents) >= parent: + self.diffLabel.setText( + self.tr("Differences to Parent {0}").format(parent)) + rev1 = parents[parent - 1] + + self.__diffGenerator.start(self.__filename, [rev1, rev2], + self.__bundle) + + if len(parents) > 1: + if parent == 1: + par1 = " 1 " + else: + par1 = '<a href="diff:1"> 1 </a>' + if parent == 2: + par2 = " 2 " + else: + par2 = '<a href="diff:2"> 2 </a>' + self.diffSelectLabel.setText( + self.tr('Diff to Parent {0}{1}').format(par1, par2)) + elif len(selectedItems) == 2: + rev2 = int(selectedItems[0].text( + self.RevisionColumn).split(":")[0]) + rev1 = int(selectedItems[1].text( + self.RevisionColumn).split(":")[0]) + + self.__diffGenerator.start(self.__filename, + [min(rev1, rev2), max(rev1, rev2)], + self.__bundle) + + def __generatorFinished(self): + """ + Private slot connected to the finished signal of the diff generator. + """ + diff, errors, fileSeparators = self.__diffGenerator.getResult() + + if diff: + self.diffEdit.setPlainText("".join(diff)) + elif errors: + self.diffEdit.setPlainText("".join(errors)) + else: + self.diffEdit.setPlainText(self.tr('There is no difference.')) + + self.saveLabel.setVisible(bool(diff)) + + if self.__diffUpdatesFiles: + for oldFileName, newFileName, lineNumber in fileSeparators: + if oldFileName == newFileName: + fileName = oldFileName + elif oldFileName == "__NULL__": + fileName = newFileName + else: + fileName = oldFileName + item = QTreeWidgetItem(self.filesTree, ["", fileName, ""]) + item.setData(0, self.__diffFileLineRole, lineNumber) + self.__resizeColumnsFiles() + self.__resortFiles() + else: + for oldFileName, newFileName, lineNumber in fileSeparators: + for fileName in (oldFileName, newFileName): + if fileName != "__NULL__": + items = self.filesTree.findItems( + fileName, Qt.MatchExactly, 1) + for item in items: + item.setData(0, self.__diffFileLineRole, + lineNumber) + + tc = self.diffEdit.textCursor() + tc.movePosition(QTextCursor.Start) + self.diffEdit.setTextCursor(tc) + self.diffEdit.ensureCursorVisible() + + @pyqtSlot(QTreeWidgetItem, QTreeWidgetItem) + def on_filesTree_currentItemChanged(self, current, previous): + """ + Private slot called, when the current item of the files tree changes. + + @param current reference to the new current item (QTreeWidgetItem) + @param previous reference to the old current item (QTreeWidgetItem) + """ + if current: + para = current.data(0, self.__diffFileLineRole) + if para is not None: + if para == 0: + tc = self.diffEdit.textCursor() + tc.movePosition(QTextCursor.Start) + self.diffEdit.setTextCursor(tc) + self.diffEdit.ensureCursorVisible() + elif para == -1: + tc = self.diffEdit.textCursor() + tc.movePosition(QTextCursor.End) + self.diffEdit.setTextCursor(tc) + self.diffEdit.ensureCursorVisible() + else: + # step 1: move cursor to end + tc = self.diffEdit.textCursor() + tc.movePosition(QTextCursor.End) + self.diffEdit.setTextCursor(tc) + self.diffEdit.ensureCursorVisible() + + # step 2: move cursor to desired line + tc = self.diffEdit.textCursor() + delta = tc.blockNumber() - para + tc.movePosition(QTextCursor.PreviousBlock, + QTextCursor.MoveAnchor, delta) + self.diffEdit.setTextCursor(tc) + self.diffEdit.ensureCursorVisible() + + @pyqtSlot(str) + def on_diffSelectLabel_linkActivated(self, link): + """ + Private slot to handle the selection of a diff target. + + @param link activated link + @type str + """ + if ":" in link: + scheme, parent = link.split(":", 1) + if scheme == "diff": + try: + parent = int(parent) + self.__generateDiffs(parent) + except ValueError: + # ignore silently + pass + + @pyqtSlot(str) + def on_saveLabel_linkActivated(self, link): + """ + Private slot to handle the selection of the save link. + + @param link activated link + @type str + """ + if ":" not in link: + return + + scheme, rest = link.split(":", 1) + if scheme != "save" or rest != "me": + return + + if self.projectMode: + fname = self.vcs.splitPath(self.__filename)[0] + fname += "/{0}.diff".format(os.path.split(fname)[-1]) + else: + dname, fname = self.vcs.splitPath(self.__filename) + if fname != '.': + fname = "{0}.diff".format(self.__filename) + else: + fname = dname + + fname, selectedFilter = E5FileDialog.getSaveFileNameAndFilter( + self, + self.tr("Save Diff"), + fname, + self.tr("Patch Files (*.diff)"), + None, + E5FileDialog.Options(E5FileDialog.DontConfirmOverwrite)) + + if not fname: + return # user aborted + + ext = QFileInfo(fname).suffix() + if not ext: + ex = selectedFilter.split("(*")[1].split(")")[0] + if ex: + fname += ex + if QFileInfo(fname).exists(): + res = E5MessageBox.yesNo( + self, + self.tr("Save Diff"), + self.tr("<p>The patch file <b>{0}</b> already exists." + " Overwrite it?</p>").format(fname), + icon=E5MessageBox.Warning) + if not res: + return + fname = Utilities.toNativeSeparators(fname) + + eol = e5App().getObject("Project").getEolString() + try: + f = open(fname, "w", encoding="utf-8", newline="") + f.write(eol.join(self.diffEdit.toPlainText().splitlines())) + f.close() + except IOError as why: + E5MessageBox.critical( + self, self.tr('Save Diff'), + self.tr( + '<p>The patch file <b>{0}</b> could not be saved.' + '<br>Reason: {1}</p>') + .format(fname, str(why))) + + @pyqtSlot(str) + def on_sbsSelectLabel_linkActivated(self, link): + """ + Private slot to handle selection of a side-by-side link. + + @param link text of the selected link + @type str + """ + if ":" in link: + scheme, path = link.split(":", 1) + if scheme == "sbsdiff" and "_" in path: + rev1, rev2 = path.split("_", 1) + self.vcs.hgSbsDiff(self.__filename, revisions=(rev1, rev2))