document.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332
  1. # We no longer support the old, non-colour editor!
  2. from pywin.mfc import docview, object
  3. from pywin.framework.editor import GetEditorOption
  4. import win32ui
  5. import os
  6. import win32con
  7. import string
  8. import traceback
  9. import win32api
  10. import shutil
  11. BAK_NONE=0
  12. BAK_DOT_BAK=1
  13. BAK_DOT_BAK_TEMP_DIR=2
  14. BAK_DOT_BAK_BAK_DIR=3
  15. MSG_CHECK_EXTERNAL_FILE = win32con.WM_USER+1999 ## WARNING: Duplicated in editor.py and coloreditor.py
  16. import pywin.scintilla.document
  17. ParentEditorDocument=pywin.scintilla.document.CScintillaDocument
  18. class EditorDocumentBase(ParentEditorDocument):
  19. def __init__(self, template):
  20. self.bAutoReload = GetEditorOption("Auto Reload", 1)
  21. self.bDeclinedReload = 0 # Has the user declined to reload.
  22. self.fileStat = None
  23. self.bReportedFileNotFound = 0
  24. # what sort of bak file should I create.
  25. # default to write to %temp%/bak/filename.ext
  26. self.bakFileType=GetEditorOption("Backup Type", BAK_DOT_BAK_BAK_DIR)
  27. self.watcherThread = FileWatchingThread(self)
  28. self.watcherThread.CreateThread()
  29. # Should I try and use VSS integration?
  30. self.scModuleName=GetEditorOption("Source Control Module", "")
  31. self.scModule = None # Loaded when first used.
  32. ParentEditorDocument.__init__(self, template, template.CreateWin32uiDocument())
  33. def OnCloseDocument(self ):
  34. self.watcherThread.SignalStop()
  35. return self._obj_.OnCloseDocument()
  36. # def OnOpenDocument(self, name):
  37. # rc = ParentEditorDocument.OnOpenDocument(self, name)
  38. # self.GetFirstView()._SetLoadedText(self.text)
  39. # self._DocumentStateChanged()
  40. # return rc
  41. def OnSaveDocument( self, fileName ):
  42. win32ui.SetStatusText("Saving file...",1)
  43. # rename to bak if required.
  44. dir, basename = os.path.split(fileName)
  45. if self.bakFileType==BAK_DOT_BAK:
  46. bakFileName=dir+'\\'+os.path.splitext(basename)[0]+'.bak'
  47. elif self.bakFileType==BAK_DOT_BAK_TEMP_DIR:
  48. bakFileName=win32api.GetTempPath()+'\\'+os.path.splitext(basename)[0]+'.bak'
  49. elif self.bakFileType==BAK_DOT_BAK_BAK_DIR:
  50. tempPath=os.path.join(win32api.GetTempPath(),'bak')
  51. try:
  52. os.mkdir(tempPath,0)
  53. except os.error:
  54. pass
  55. bakFileName=os.path.join(tempPath,basename)
  56. try:
  57. os.unlink(bakFileName) # raise NameError if no bakups wanted.
  58. except (os.error, NameError):
  59. pass
  60. try:
  61. # Do a copy as it might be on different volumes,
  62. # and the file may be a hard-link, causing the link
  63. # to follow the backup.
  64. shutil.copy2(fileName, bakFileName)
  65. except (os.error, NameError, IOError):
  66. pass
  67. try:
  68. self.SaveFile(fileName)
  69. except IOError, details:
  70. win32ui.MessageBox("Error - could not save file\r\n\r\n%s"%details)
  71. return 0
  72. except (UnicodeEncodeError, LookupError), details:
  73. rc = win32ui.MessageBox("Encoding failed: \r\n%s"%details +
  74. '\r\nPlease add desired source encoding as first line of file, eg \r\n' +
  75. '# -*- coding: mbcs -*-\r\n\r\n' +
  76. 'If you continue, the file will be saved as binary and will\r\n' +
  77. 'not be valid in the declared encoding.\r\n\r\n' +
  78. 'Save the file as binary with an invalid encoding?',
  79. "File save failed",
  80. win32con.MB_YESNO | win32con.MB_DEFBUTTON2)
  81. if rc==win32con.IDYES:
  82. try:
  83. self.SaveFile(fileName, encoding="latin-1")
  84. except IOError, details:
  85. win32ui.MessageBox("Error - could not save file\r\n\r\n%s"%details)
  86. return 0
  87. else:
  88. return 0
  89. self.SetModifiedFlag(0) # No longer dirty
  90. self.bDeclinedReload = 0 # They probably want to know if it changes again!
  91. win32ui.AddToRecentFileList(fileName)
  92. self.SetPathName(fileName)
  93. win32ui.SetStatusText("Ready")
  94. self._DocumentStateChanged()
  95. return 1
  96. def FinalizeViewCreation(self, view):
  97. ParentEditorDocument.FinalizeViewCreation(self, view)
  98. if view == self.GetFirstView():
  99. self._DocumentStateChanged()
  100. if view.bFolding and GetEditorOption("Fold On Open", 0):
  101. view.FoldTopLevelEvent()
  102. def HookViewNotifications(self, view):
  103. ParentEditorDocument.HookViewNotifications(self, view)
  104. # Support for reloading the document from disk - presumably after some
  105. # external application has modified it (or possibly source control has
  106. # checked it out.
  107. def ReloadDocument(self):
  108. """Reloads the document from disk. Assumes the file has
  109. been saved and user has been asked if necessary - it just does it!
  110. """
  111. win32ui.SetStatusText("Reloading document. Please wait...", 1)
  112. self.SetModifiedFlag(0)
  113. # Loop over all views, saving their state, then reload the document
  114. views = self.GetAllViews()
  115. states = []
  116. for view in views:
  117. try:
  118. info = view._PrepareUserStateChange()
  119. except AttributeError: # Not our editor view?
  120. info = None
  121. states.append(info)
  122. self.OnOpenDocument(self.GetPathName())
  123. for view, info in zip(views, states):
  124. if info is not None:
  125. view._EndUserStateChange(info)
  126. self._DocumentStateChanged()
  127. win32ui.SetStatusText("Document reloaded.")
  128. # Reloading the file
  129. def CheckExternalDocumentUpdated(self):
  130. if self.bDeclinedReload or not self.GetPathName():
  131. return
  132. try:
  133. newstat = os.stat(self.GetPathName())
  134. except os.error, exc:
  135. if not self.bReportedFileNotFound:
  136. print "The file '%s' is open for editing, but\nchecking it for changes caused the error: %s" % (self.GetPathName(), exc.strerror)
  137. self.bReportedFileNotFound = 1
  138. return
  139. if self.bReportedFileNotFound:
  140. print "The file '%s' has re-appeared - continuing to watch for changes..." % (self.GetPathName(),)
  141. self.bReportedFileNotFound = 0 # Once found again we want to start complaining.
  142. changed = (self.fileStat is None) or \
  143. self.fileStat[0] != newstat[0] or \
  144. self.fileStat[6] != newstat[6] or \
  145. self.fileStat[8] != newstat[8] or \
  146. self.fileStat[9] != newstat[9]
  147. if changed:
  148. question = None
  149. if self.IsModified():
  150. 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()
  151. mbStyle = win32con.MB_YESNO | win32con.MB_DEFBUTTON2 # Default to "No"
  152. else:
  153. if not self.bAutoReload:
  154. question = "%s\r\n\r\nThis file has been modified outside of the source editor.\r\nDo you want to reload it?" % self.GetPathName()
  155. mbStyle = win32con.MB_YESNO # Default to "Yes"
  156. if question:
  157. rc = win32ui.MessageBox(question, None, mbStyle)
  158. if rc!=win32con.IDYES:
  159. self.bDeclinedReload = 1
  160. return
  161. self.ReloadDocument()
  162. def _DocumentStateChanged(self):
  163. """Called whenever the documents state (on disk etc) has been changed
  164. by the editor (eg, as the result of a save operation)
  165. """
  166. if self.GetPathName():
  167. try:
  168. self.fileStat = os.stat(self.GetPathName())
  169. except os.error:
  170. self.fileStat = None
  171. else:
  172. self.fileStat = None
  173. self.watcherThread._DocumentStateChanged()
  174. self._UpdateUIForState()
  175. self._ApplyOptionalToViews("_UpdateUIForState")
  176. self._ApplyOptionalToViews("SetReadOnly", self._IsReadOnly())
  177. self._ApplyOptionalToViews("SCISetSavePoint")
  178. # Allow the debugger to reset us too.
  179. import pywin.debugger
  180. if pywin.debugger.currentDebugger is not None:
  181. pywin.debugger.currentDebugger.UpdateDocumentLineStates(self)
  182. # Read-only document support - make it obvious to the user
  183. # that the file is read-only.
  184. def _IsReadOnly(self):
  185. return self.fileStat is not None and (self.fileStat[0] & 128)==0
  186. def _UpdateUIForState(self):
  187. """Change the title to reflect the state of the document -
  188. eg ReadOnly, Dirty, etc
  189. """
  190. filename = self.GetPathName()
  191. if not filename: return # New file - nothing to do
  192. try:
  193. # This seems necessary so the internal state of the window becomes
  194. # "visible". without it, it is still shown, but certain functions
  195. # (such as updating the title) dont immediately work?
  196. self.GetFirstView().ShowWindow(win32con.SW_SHOW)
  197. title = win32ui.GetFileTitle(filename)
  198. except win32ui.error:
  199. title = filename
  200. if self._IsReadOnly():
  201. title = title + " (read-only)"
  202. self.SetTitle(title)
  203. def MakeDocumentWritable(self):
  204. pretend_ss = 0 # Set to 1 to test this without source safe :-)
  205. if not self.scModuleName and not pretend_ss: # No Source Control support.
  206. win32ui.SetStatusText("Document is read-only, and no source-control system is configured")
  207. win32api.MessageBeep()
  208. return 0
  209. # We have source control support - check if the user wants to use it.
  210. msg = "Would you like to check this file out?"
  211. defButton = win32con.MB_YESNO
  212. if self.IsModified():
  213. msg = msg + "\r\n\r\nALL CHANGES IN THE EDITOR WILL BE LOST"
  214. defButton = win32con.MB_YESNO
  215. if win32ui.MessageBox(msg, None, defButton)!=win32con.IDYES:
  216. return 0
  217. if pretend_ss:
  218. print "We are only pretending to check it out!"
  219. win32api.SetFileAttributes(self.GetPathName(), win32con.FILE_ATTRIBUTE_NORMAL)
  220. self.ReloadDocument()
  221. return 1
  222. # Now call on the module to do it.
  223. if self.scModule is None:
  224. try:
  225. self.scModule = __import__(self.scModuleName)
  226. for part in self.scModuleName.split('.')[1:]:
  227. self.scModule = getattr(self.scModule, part)
  228. except:
  229. traceback.print_exc()
  230. print "Error loading source control module."
  231. return 0
  232. if self.scModule.CheckoutFile(self.GetPathName()):
  233. self.ReloadDocument()
  234. return 1
  235. return 0
  236. def CheckMakeDocumentWritable(self):
  237. if self._IsReadOnly():
  238. return self.MakeDocumentWritable()
  239. return 1
  240. def SaveModified(self):
  241. # Called as the document is closed. If we are about
  242. # to prompt for a save, bring the document to the foreground.
  243. if self.IsModified():
  244. frame = self.GetFirstView().GetParentFrame()
  245. try:
  246. frame.MDIActivate()
  247. frame.AutoRestore()
  248. except:
  249. print "Could not bring document to foreground"
  250. return self._obj_.SaveModified()
  251. # NOTE - I DONT use the standard threading module,
  252. # as this waits for all threads to terminate at shutdown.
  253. # When using the debugger, it is possible shutdown will
  254. # occur without Pythonwin getting a complete shutdown,
  255. # so we deadlock at the end - threading is waiting for
  256. import pywin.mfc.thread
  257. import win32event
  258. class FileWatchingThread(pywin.mfc.thread.WinThread):
  259. def __init__(self, doc):
  260. self.doc = doc
  261. self.adminEvent = win32event.CreateEvent(None, 0, 0, None)
  262. self.stopEvent = win32event.CreateEvent(None, 0, 0, None)
  263. self.watchEvent = None
  264. pywin.mfc.thread.WinThread.__init__(self)
  265. def _DocumentStateChanged(self):
  266. win32event.SetEvent(self.adminEvent)
  267. def RefreshEvent(self):
  268. self.hwnd = self.doc.GetFirstView().GetSafeHwnd()
  269. if self.watchEvent is not None:
  270. win32api.FindCloseChangeNotification(self.watchEvent)
  271. self.watchEvent = None
  272. path = self.doc.GetPathName()
  273. if path: path = os.path.dirname(path)
  274. if path:
  275. filter = win32con.FILE_NOTIFY_CHANGE_FILE_NAME | \
  276. win32con.FILE_NOTIFY_CHANGE_ATTRIBUTES | \
  277. win32con.FILE_NOTIFY_CHANGE_LAST_WRITE
  278. try:
  279. self.watchEvent = win32api.FindFirstChangeNotification(path, 0, filter)
  280. except win32api.error, exc:
  281. print "Can not watch file", path, "for changes -", exc.strerror
  282. def SignalStop(self):
  283. win32event.SetEvent(self.stopEvent)
  284. def Run(self):
  285. while 1:
  286. handles = [self.stopEvent, self.adminEvent]
  287. if self.watchEvent is not None:
  288. handles.append(self.watchEvent)
  289. rc = win32event.WaitForMultipleObjects(handles, 0, win32event.INFINITE)
  290. if rc == win32event.WAIT_OBJECT_0:
  291. break
  292. elif rc == win32event.WAIT_OBJECT_0+1:
  293. self.RefreshEvent()
  294. else:
  295. win32api.PostMessage(self.hwnd, MSG_CHECK_EXTERNAL_FILE, 0, 0)
  296. try:
  297. # If the directory has been removed underneath us, we get this error.
  298. win32api.FindNextChangeNotification(self.watchEvent)
  299. except win32api.error, exc:
  300. print "Can not watch file", self.doc.GetPathName(), "for changes -", exc.strerror
  301. break
  302. # close a circular reference
  303. self.doc = None
  304. if self.watchEvent:
  305. win32api.FindCloseChangeNotification(self.watchEvent)