# We no longer support the old, non-colour editor! from pywin.mfc import docview, object from pywin.framework.editor import GetEditorOption import win32ui import os import win32con import string import traceback import win32api import shutil BAK_NONE=0 BAK_DOT_BAK=1 BAK_DOT_BAK_TEMP_DIR=2 BAK_DOT_BAK_BAK_DIR=3 MSG_CHECK_EXTERNAL_FILE = win32con.WM_USER+1999 ## WARNING: Duplicated in editor.py and coloreditor.py import pywin.scintilla.document ParentEditorDocument=pywin.scintilla.document.CScintillaDocument class EditorDocumentBase(ParentEditorDocument): def __init__(self, template): self.bAutoReload = GetEditorOption("Auto Reload", 1) self.bDeclinedReload = 0 # Has the user declined to reload. self.fileStat = None self.bReportedFileNotFound = 0 # what sort of bak file should I create. # default to write to %temp%/bak/filename.ext self.bakFileType=GetEditorOption("Backup Type", BAK_DOT_BAK_BAK_DIR) self.watcherThread = FileWatchingThread(self) self.watcherThread.CreateThread() # Should I try and use VSS integration? self.scModuleName=GetEditorOption("Source Control Module", "") self.scModule = None # Loaded when first used. ParentEditorDocument.__init__(self, template, template.CreateWin32uiDocument()) def OnCloseDocument(self ): self.watcherThread.SignalStop() return self._obj_.OnCloseDocument() # def OnOpenDocument(self, name): # rc = ParentEditorDocument.OnOpenDocument(self, name) # self.GetFirstView()._SetLoadedText(self.text) # self._DocumentStateChanged() # return rc def OnSaveDocument( self, fileName ): win32ui.SetStatusText("Saving file...",1) # rename to bak if required. dir, basename = os.path.split(fileName) if self.bakFileType==BAK_DOT_BAK: bakFileName=dir+'\\'+os.path.splitext(basename)[0]+'.bak' elif self.bakFileType==BAK_DOT_BAK_TEMP_DIR: bakFileName=win32api.GetTempPath()+'\\'+os.path.splitext(basename)[0]+'.bak' elif self.bakFileType==BAK_DOT_BAK_BAK_DIR: tempPath=os.path.join(win32api.GetTempPath(),'bak') try: os.mkdir(tempPath,0) except os.error: pass bakFileName=os.path.join(tempPath,basename) try: os.unlink(bakFileName) # raise NameError if no bakups wanted. except (os.error, NameError): pass try: # Do a copy as it might be on different volumes, # and the file may be a hard-link, causing the link # to follow the backup. shutil.copy2(fileName, bakFileName) except (os.error, NameError, IOError): pass try: self.SaveFile(fileName) except IOError, details: win32ui.MessageBox("Error - could not save file\r\n\r\n%s"%details) return 0 except (UnicodeEncodeError, LookupError), details: rc = win32ui.MessageBox("Encoding failed: \r\n%s"%details + '\r\nPlease add desired source encoding as first line of file, eg \r\n' + '# -*- coding: mbcs -*-\r\n\r\n' + 'If you continue, the file will be saved as binary and will\r\n' + 'not be valid in the declared encoding.\r\n\r\n' + 'Save the file as binary with an invalid encoding?', "File save failed", win32con.MB_YESNO | win32con.MB_DEFBUTTON2) if rc==win32con.IDYES: try: self.SaveFile(fileName, encoding="latin-1") except IOError, details: win32ui.MessageBox("Error - could not save file\r\n\r\n%s"%details) return 0 else: return 0 self.SetModifiedFlag(0) # No longer dirty self.bDeclinedReload = 0 # They probably want to know if it changes again! win32ui.AddToRecentFileList(fileName) self.SetPathName(fileName) win32ui.SetStatusText("Ready") self._DocumentStateChanged() return 1 def FinalizeViewCreation(self, view): ParentEditorDocument.FinalizeViewCreation(self, view) if view == self.GetFirstView(): self._DocumentStateChanged() if view.bFolding and GetEditorOption("Fold On Open", 0): view.FoldTopLevelEvent() def HookViewNotifications(self, view): ParentEditorDocument.HookViewNotifications(self, view) # Support for reloading the document from disk - presumably after some # external application has modified it (or possibly source control has # checked it out. def ReloadDocument(self): """Reloads the document from disk. Assumes the file has been saved and user has been asked if necessary - it just does it! """ win32ui.SetStatusText("Reloading document. Please wait...", 1) self.SetModifiedFlag(0) # Loop over all views, saving their state, then reload the document views = self.GetAllViews() states = [] for view in views: try: info = view._PrepareUserStateChange() except AttributeError: # Not our editor view? info = None states.append(info) self.OnOpenDocument(self.GetPathName()) for view, info in zip(views, states): if info is not None: view._EndUserStateChange(info) self._DocumentStateChanged() win32ui.SetStatusText("Document reloaded.") # Reloading the file def CheckExternalDocumentUpdated(self): if self.bDeclinedReload or not self.GetPathName(): return try: newstat = os.stat(self.GetPathName()) except os.error, exc: if not self.bReportedFileNotFound: print "The file '%s' is open for editing, but\nchecking it for changes caused the error: %s" % (self.GetPathName(), exc.strerror) self.bReportedFileNotFound = 1 return if self.bReportedFileNotFound: print "The file '%s' has re-appeared - continuing to watch for changes..." % (self.GetPathName(),) self.bReportedFileNotFound = 0 # Once found again we want to start complaining. changed = (self.fileStat is None) or \ self.fileStat[0] != newstat[0] or \ self.fileStat[6] != newstat[6] or \ self.fileStat[8] != newstat[8] or \ self.fileStat[9] != newstat[9] if changed: question = None if self.IsModified(): question = "%s\r\n\r\nThis file has been modified outside of the source editor.\r\nDo you want to reload it and LOSE THE CHANGES in the source editor?" % self.GetPathName() mbStyle = win32con.MB_YESNO | win32con.MB_DEFBUTTON2 # Default to "No" else: if not self.bAutoReload: question = "%s\r\n\r\nThis file has been modified outside of the source editor.\r\nDo you want to reload it?" % self.GetPathName() mbStyle = win32con.MB_YESNO # Default to "Yes" if question: rc = win32ui.MessageBox(question, None, mbStyle) if rc!=win32con.IDYES: self.bDeclinedReload = 1 return self.ReloadDocument() def _DocumentStateChanged(self): """Called whenever the documents state (on disk etc) has been changed by the editor (eg, as the result of a save operation) """ if self.GetPathName(): try: self.fileStat = os.stat(self.GetPathName()) except os.error: self.fileStat = None else: self.fileStat = None self.watcherThread._DocumentStateChanged() self._UpdateUIForState() self._ApplyOptionalToViews("_UpdateUIForState") self._ApplyOptionalToViews("SetReadOnly", self._IsReadOnly()) self._ApplyOptionalToViews("SCISetSavePoint") # Allow the debugger to reset us too. import pywin.debugger if pywin.debugger.currentDebugger is not None: pywin.debugger.currentDebugger.UpdateDocumentLineStates(self) # Read-only document support - make it obvious to the user # that the file is read-only. def _IsReadOnly(self): return self.fileStat is not None and (self.fileStat[0] & 128)==0 def _UpdateUIForState(self): """Change the title to reflect the state of the document - eg ReadOnly, Dirty, etc """ filename = self.GetPathName() if not filename: return # New file - nothing to do try: # This seems necessary so the internal state of the window becomes # "visible". without it, it is still shown, but certain functions # (such as updating the title) dont immediately work? self.GetFirstView().ShowWindow(win32con.SW_SHOW) title = win32ui.GetFileTitle(filename) except win32ui.error: title = filename if self._IsReadOnly(): title = title + " (read-only)" self.SetTitle(title) def MakeDocumentWritable(self): pretend_ss = 0 # Set to 1 to test this without source safe :-) if not self.scModuleName and not pretend_ss: # No Source Control support. win32ui.SetStatusText("Document is read-only, and no source-control system is configured") win32api.MessageBeep() return 0 # We have source control support - check if the user wants to use it. msg = "Would you like to check this file out?" defButton = win32con.MB_YESNO if self.IsModified(): msg = msg + "\r\n\r\nALL CHANGES IN THE EDITOR WILL BE LOST" defButton = win32con.MB_YESNO if win32ui.MessageBox(msg, None, defButton)!=win32con.IDYES: return 0 if pretend_ss: print "We are only pretending to check it out!" win32api.SetFileAttributes(self.GetPathName(), win32con.FILE_ATTRIBUTE_NORMAL) self.ReloadDocument() return 1 # Now call on the module to do it. if self.scModule is None: try: self.scModule = __import__(self.scModuleName) for part in self.scModuleName.split('.')[1:]: self.scModule = getattr(self.scModule, part) except: traceback.print_exc() print "Error loading source control module." return 0 if self.scModule.CheckoutFile(self.GetPathName()): self.ReloadDocument() return 1 return 0 def CheckMakeDocumentWritable(self): if self._IsReadOnly(): return self.MakeDocumentWritable() return 1 def SaveModified(self): # Called as the document is closed. If we are about # to prompt for a save, bring the document to the foreground. if self.IsModified(): frame = self.GetFirstView().GetParentFrame() try: frame.MDIActivate() frame.AutoRestore() except: print "Could not bring document to foreground" return self._obj_.SaveModified() # NOTE - I DONT use the standard threading module, # as this waits for all threads to terminate at shutdown. # When using the debugger, it is possible shutdown will # occur without Pythonwin getting a complete shutdown, # so we deadlock at the end - threading is waiting for import pywin.mfc.thread import win32event class FileWatchingThread(pywin.mfc.thread.WinThread): def __init__(self, doc): self.doc = doc self.adminEvent = win32event.CreateEvent(None, 0, 0, None) self.stopEvent = win32event.CreateEvent(None, 0, 0, None) self.watchEvent = None pywin.mfc.thread.WinThread.__init__(self) def _DocumentStateChanged(self): win32event.SetEvent(self.adminEvent) def RefreshEvent(self): self.hwnd = self.doc.GetFirstView().GetSafeHwnd() if self.watchEvent is not None: win32api.FindCloseChangeNotification(self.watchEvent) self.watchEvent = None path = self.doc.GetPathName() if path: path = os.path.dirname(path) if path: filter = win32con.FILE_NOTIFY_CHANGE_FILE_NAME | \ win32con.FILE_NOTIFY_CHANGE_ATTRIBUTES | \ win32con.FILE_NOTIFY_CHANGE_LAST_WRITE try: self.watchEvent = win32api.FindFirstChangeNotification(path, 0, filter) except win32api.error, exc: print "Can not watch file", path, "for changes -", exc.strerror def SignalStop(self): win32event.SetEvent(self.stopEvent) def Run(self): while 1: handles = [self.stopEvent, self.adminEvent] if self.watchEvent is not None: handles.append(self.watchEvent) rc = win32event.WaitForMultipleObjects(handles, 0, win32event.INFINITE) if rc == win32event.WAIT_OBJECT_0: break elif rc == win32event.WAIT_OBJECT_0+1: self.RefreshEvent() else: win32api.PostMessage(self.hwnd, MSG_CHECK_EXTERNAL_FILE, 0, 0) try: # If the directory has been removed underneath us, we get this error. win32api.FindNextChangeNotification(self.watchEvent) except win32api.error, exc: print "Can not watch file", self.doc.GetPathName(), "for changes -", exc.strerror break # close a circular reference self.doc = None if self.watchEvent: win32api.FindCloseChangeNotification(self.watchEvent)