--- a/QScintilla/Editor.py Sun Mar 28 09:27:19 2010 +0000 +++ b/QScintilla/Editor.py Tue Mar 30 13:49:13 2010 +0000 @@ -9,6 +9,7 @@ import os import re +import difflib from PyQt4.Qsci import QsciScintilla, QsciMacro from PyQt4.QtCore import * @@ -96,7 +97,11 @@ # Cooperation related definitions Separator = "@@@" - SelectionToken = "SELECT" + StartEditToken = "START_EDIT" + EndEditToken = "END_EDIT" + CancelEditToken = "CANCEL_EDIT" + RequestSyncToken = "REQUEST_SYNC" + SyncToken = "SYNC" def __init__(self, dbs, fn = None, vm = None, filetype = "", editor = None, tv = None): @@ -189,7 +194,12 @@ self.lastIndex = 0 # initialize some cooperation stuff - self.__lastSelection = (-1, -1, -1, -1) + self.__isSyncing = False + self.__receivedWhileSyncing = [] + self.__savedText = "" + self.__inSharedEdit = False + self.__isShared = False + self.__inRemoteSharedEdit = False self.connect(self, SIGNAL('modificationChanged(bool)'), self.__modificationChanged) @@ -199,8 +209,6 @@ self.__modificationReadOnly) self.connect(self, SIGNAL('userListActivated(int, const QString)'), self.__completionListSelected) - self.connect(self, SIGNAL('selectionChanged()'), - self.__selectionChanged) # margins layout if QSCINTILLA_VERSION() >= 0x020301: @@ -4873,7 +4881,7 @@ """ if self.fileName is None: return - readOnly = not QFileInfo(self.fileName).isWritable() + readOnly = not QFileInfo(self.fileName).isWritable() or self.isReadOnly() if not bForce and (readOnly == self.isReadOnly()): return cap = self.fileName @@ -5479,22 +5487,102 @@ ## Cooperation related methods ####################################################################### - def send(self, token, args): - """ - Public method to send an editor command to remote editors. + def getSharingStatus(self): + """ + Public method to get some share status info. + + @return tuple indicating, if the editor is sharable, the sharing status, + if it is inside a locally initiated shared edit session and + if it is inside a remotely initiated shared edit session + (boolean, boolean, boolean, boolean) + """ + project = e5App().getObject("Project") + return project.isOpen() and project.isProjectFile(self.fileName), \ + self.__isShared, self.__inSharedEdit, self.__inRemoteSharedEdit + + def shareConnected(self, connected): + """ + Public slot to handle a change of the connected state. + + @param connected flag indicating the connected state (boolean) + """ + if not connected: + self.__inRemoteSharedEdit = False + self.setReadOnly(False) + self.__updateReadOnly() + self.cancelSharedEdit(send = False) + self.__isSyncing = False + self.__receivedWhileSyncing = [] + + def shareEditor(self, share): + """ + Public slot to set the shared status of the editor. + + @param share flag indicating the share status (boolean) + """ + self.__isShared = share + if not share: + self.shareConnected(False) + + def startSharedEdit(self): + """ + Public slot to start a shared edit session for the editor. + """ + self.__inSharedEdit = True + self.__savedText = self.text() + hash = str( + QCryptographicHash.hash( + Utilities.encode(self.__savedText, self.encoding)[0], + QCryptographicHash.Sha1).toHex(), + encoding = "utf-8") + self.__send(Editor.StartEditToken, hash) + + def sendSharedEdit(self): + """ + Public slot to end a shared edit session for the editor and + send the changes. + """ + commands = self.__calculateChanges(self.__savedText, self.text()) + self.__send(Editor.EndEditToken, commands) + self.__inSharedEdit = False + self.__savedText = "" + + def cancelSharedEdit(self, send = True): + """ + Public slot to cancel a shared edit session for the editor. + + @keyparam send flag indicating to send the CancelEdit command (boolean) + """ + self.__inSharedEdit = False + self.__savedText = "" + if send: + self.__send(Editor.CancelEditToken) + + def __send(self, token, args = None): + """ + Private method to send an editor command to remote editors. @param token command token (string) @param args arguments for the command (string) """ - msg = "" - if token == Editor.SelectionToken: - msg = "{0}{1}{2} {3} {4} {5}".format( - token, - Editor.Separator, - *args - ) - - self.vm.send(self.fileName, msg) + if self.vm.isConnected(): + msg = "" + if token in (Editor.StartEditToken, + Editor.EndEditToken, + Editor.RequestSyncToken, + Editor.SyncToken): + msg = "{0}{1}{2}".format( + token, + Editor.Separator, + args + ) + elif token == Editor.CancelEditToken: + msg = "{0}{1}c".format( + token, + Editor.Separator + ) + + self.vm.send(self.fileName, msg) def receive(self, command): """ @@ -5502,28 +5590,153 @@ @param command command string (string) """ - token, argsString = command.split(Editor.Separator) - if token == Editor.SelectionToken: - self.__processSelectionCommand(argsString) + if self.__isShared: + if self.__isSyncing and \ + not command.startswith(Editor.SyncToken + Editor.Separator): + self.__receivedWhileSyncing.append(command) + else: + self.__dispatchCommand(command) + + def __dispatchCommand(self, command): + """ + Private method to dispatch received commands. + + @param command command to be processed (string) + """ + token, argsString = command.split(Editor.Separator, 1) + if token == Editor.StartEditToken: + self.__processStartEditCommand(argsString) + elif token == Editor.CancelEditToken: + self.shareConnected(False) + elif token == Editor.EndEditToken: + self.__processEndEditCommand(argsString) + elif token == Editor.RequestSyncToken: + self.__processRequestSyncCommand(argsString) + elif token == Editor.SyncToken: + self.__processSyncCommand(argsString) + + def __processStartEditCommand(self, argsString): + """ + Private slot to process a remote StartEdit command + + @param argsString string containing the command parameters (string) + """ + if not self.__inSharedEdit and not self.__inRemoteSharedEdit: + self.__inRemoteSharedEdit = True + self.setReadOnly(True) + self.__updateReadOnly() + hash = str( + QCryptographicHash.hash( + Utilities.encode(self.text(), self.encoding)[0], + QCryptographicHash.Sha1).toHex(), + encoding = "utf-8") + if hash != argsString: + # text is different to the remote site, request to sync it + self.__isSyncing = True + self.__send(Editor.RequestSyncToken, argsString) + + def __calculateChanges(self, old, new): + """ + Private method to determine change commands to convert old text into + new text. + + @param old old text (string) + @param new new text (string) + @return commands to change old into new (string) + """ + oldL = old.splitlines() + newL = new.splitlines() + matcher = difflib.SequenceMatcher(None, oldL, newL) + + formatStr = "@@{0} {1} {2} {3}" + commands = [] + for token, i1, i2, j1, j2 in matcher.get_opcodes(): + if token == "insert": + commands.append(formatStr.format("i", j1, j2 - j1, -1)) + commands.extend(newL[j1:j2]) + elif token == "delete": + commands.append(formatStr.format("d", j1, i2 - i1, -1)) + elif token == "replace": + commands.append(formatStr.format("r", j1, i2 - i1, j2 - j1)) + commands.extend(newL[j1:j2]) + + return "\n".join(commands) + "\n" - def __selectionChanged(self): - """ - Private slot to handle a change of the selection. - """ - if self.vm.isConnected(): - sel = self.getSelection() - if sel != self.__lastSelection: - self.send(Editor.SelectionToken, args = sel) - self.__lastSelection = sel + def __processEndEditCommand(self, argsString): + """ + Private slot to process a remote EndEdit command + + @param argsString string containing the command parameters (string) + """ + commands = argsString.splitlines() + sep = self.getLineSeparator() + cur = self.getCursorPosition() + + self.setReadOnly(False) + self.beginUndoAction() + while commands: + commandLine = commands.pop(0) + if not commandLine.startswith("@@"): + continue + + command, *args = commandLine.split() + pos, l1, l2 = [int(arg) for arg in args] + if command == "@@i": + txt = sep.join(commands[0:l1]) + sep + self.insertAt(txt, pos, 0) + del commands[0:l1] + elif command == "@@d": + self.setSelection(pos, 0, pos + l1, 0) + self.removeSelectedText() + elif command == "@@r": + self.setSelection(pos, 0, pos + l1, 0) + self.removeSelectedText() + txt = sep.join(commands[0:l2]) + sep + self.insertAt(txt, pos, 0) + del commands[0:l2] + self.endUndoAction() + self.__updateReadOnly() + self.__inRemoteSharedEdit = False + + self.setCursorPosition(*cur) - def __processSelectionCommand(self, argsString): - """ - Private slot to process a remote selection command - - @param argsString string containing the selection parameters (string) - """ - self.selectionChanged.disconnect(self.__selectionChanged) - args = argsString.split() - self.setSelection(int(args[0]), int(args[1]), int(args[2]), int(args[3])) - self.ensureLineVisible(int(args[0])) - self.selectionChanged.connect(self.__selectionChanged) + def __processRequestSyncCommand(self, argsString): + """ + Private slot to process a remote RequestSync command + + @param argsString string containing the command parameters (string) + """ + if self.__inSharedEdit: + hash = str( + QCryptographicHash.hash( + Utilities.encode(self.__savedText, self.encoding)[0], + QCryptographicHash.Sha1).toHex(), + encoding = "utf-8") + + if hash == argsString: + self.__send(Editor.SyncToken, self.__savedText) + + def __processSyncCommand(self, argsString): + """ + Private slot to process a remote Sync command + + @param argsString string containing the command parameters (string) + """ + if self.__isSyncing: + cur = self.getCursorPosition() + + self.setReadOnly(False) + self.beginUndoAction() + self.selectAll() + self.removeSelectedText() + self.insertAt(argsString, 0, 0) + self.endUndoAction() + self.setReadOnly(True) + + self.setCursorPosition(*cur) + + while self.__receivedWhileSyncing: + command = self.__receivedWhileSyncing.pop(0) + self.__dispatchCommand(command) + + self.__isSyncing = False