You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
333 lines
12 KiB
333 lines
12 KiB
# 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 as details:
|
|
win32ui.MessageBox("Error - could not save file\r\n\r\n%s"%details)
|
|
return 0
|
|
except (UnicodeEncodeError, LookupError) as 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 as 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 as 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 as 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 as 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)
|