Plugins/VcsPlugins/vcsMercurial/HgLogBrowserDialog.py

changeset 5477
fb8875e356d4
parent 5463
d84b854d59c0
child 5486
a74fafdb67e0
--- 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}">&nbsp;{2}&nbsp;</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 = "&nbsp;1&nbsp;"
+                else:
+                    par1 = '<a href="diff:1">&nbsp;1&nbsp;</a>'
+                if parent == 2:
+                    par2 = "&nbsp;2&nbsp;"
+                else:
+                    par2 = '<a href="diff:2">&nbsp;2&nbsp;</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))

eric ide

mercurial