瀏覽代碼

wxGUI/pyedit: Add recent files history menu (#1021)

The history is stored in .grass7/.file_history file under [pyedit] key as file1=... file2=... The file and format is managed
by wxPython, but it is in the .grass7 directory and it is generic format enough to be read in other contexts.

The history management is meant to be reusable for other components.
Tomas Zigo 4 年之前
父節點
當前提交
87bfc11412
共有 2 個文件被更改,包括 262 次插入41 次删除
  1. 118 0
      gui/wxpython/gui_core/menu.py
  2. 144 41
      gui/wxpython/gui_core/pyedit.py

+ 118 - 0
gui/wxpython/gui_core/menu.py

@@ -17,8 +17,10 @@ This program is free software under the GNU General Public License
 @author Milena Nowotarska (menu customization)
 @author Robert Szczepanek (menu customization)
 @author Vaclav Petras <wenzeslaus gmail.com> (menu customization)
+@author Tomas Zigo <tomas.zigo slovanet.sk> RecentFilesMenu
 """
 import re
+import os
 import wx
 
 from core import globalvar
@@ -305,3 +307,119 @@ class SearchModuleWindow(wx.Panel):
             label = data['description']
 
         self.showNotification.emit(message=label)
+
+
+class RecentFilesMenu:
+    """Add recent files history menu
+
+    Signal FileRequested is emitted if you request file from recent
+    files menu
+    :param str path: file path you requested
+    :param bool file_exists: file path exists or not
+    :param obj file_history: file history obj instance
+
+
+    :param str app_name: required for group name of recent files path
+    written into the .recent_files file
+    :param obj parent_menu: menu widget instance where be inserted
+    recent files menu on the specified position
+    :param int pos: position (index) where insert recent files menu in
+    the parent menu
+    :param int history_len: the maximum number of file paths written
+    into the .recent_files file to app name group
+    """
+
+    recent_files = '.recent_files'
+
+    def __init__(self, app_name, parent_menu, pos, history_len=10):
+        self._history_len = history_len
+        self._parent_menu = parent_menu
+        self._pos = pos
+
+        self.file_requested = Signal('RecentFilesMenu.FileRequested')
+
+        self._filehistory = wx.FileHistory(maxFiles=history_len)
+        # Recent files path stored in GRASS GIS config dir in the
+        # .recent_files file in the group by application name
+        self._config = wx.FileConfig(
+            style=wx.CONFIG_USE_LOCAL_FILE,
+            localFilename=os.path.join(
+                utils.GetSettingsPath(), self.recent_files,
+            ),
+        )
+        self._config.SetPath(strPath=app_name)
+        self._filehistory.Load(self._config)
+        self.RemoveNonExistentFiles()
+
+        self.recent = wx.Menu()
+        self._filehistory.UseMenu(self.recent)
+        self._filehistory.AddFilesToMenu()
+
+        # Show recent files menu if count of items in menu > 0
+        if self._filehistory.GetCount() > 0:
+            self._insertMenu()
+
+    def _insertMenu(self):
+        """Insert recent files menu into the parent menu on the
+        specified position if count of menu items > 0"""
+
+        self._parent_menu.Insert(
+            pos=self._pos, id=wx.ID_ANY, text=_('&Recent Files'),
+            submenu=self.recent,
+        )
+        self.recent.Bind(
+            wx.EVT_MENU_RANGE, self._onFileHistory,
+            id=wx.ID_FILE1, id2=wx.ID_FILE + self._history_len,
+        )
+
+    def _onFileHistory(self, event):
+        """Choose recent file from menu event"""
+
+        file_exists = True
+        file_index = event.GetId() - wx.ID_FILE1
+        path = self._filehistory.GetHistoryFile(file_index)
+
+        if not os.path.exists(path):
+            self.RemoveFileFromHistory(file_index)
+            file_exists = False
+        self.file_requested.emit(
+            path=path, file_exists=file_exists,
+            file_history=self._filehistory,
+        )
+
+    def AddFileToHistory(self, filename):
+        """Add file to history, and save history into '.recent_files'
+        file
+
+        :param str filename: file path
+
+        :return None
+        """
+
+        if self._filehistory.GetCount() == 0:
+            self._insertMenu()
+        if filename:
+            self._filehistory.AddFileToHistory(filename)
+            self._filehistory.Save(self._config)
+            self._config.Flush()
+
+    def RemoveFileFromHistory(self, file_index):
+        """Remove file from the history.
+
+        :param int file_index: filed index
+
+        :return: None
+        """
+        self._filehistory.RemoveFileFromHistory(i=file_index)
+        self._filehistory.Save(self._config)
+        self._config.Flush()
+
+    def RemoveNonExistentFiles(self):
+        """Remove non existent files from the history"""
+        for i in reversed(range(0, self._filehistory.GetCount())):
+            file = self._filehistory.GetHistoryFile(index=i)
+            if not os.path.exists(file):
+                self._filehistory.RemoveFileFromHistory(i=i)
+
+        self._filehistory.Save(self._config)
+        self._config.Flush()

+ 144 - 41
gui/wxpython/gui_core/pyedit.py

@@ -33,7 +33,7 @@ from core.gcmd import GError
 from gui_core.pystc import PyStc
 from core import globalvar
 from core.menutree import MenuTreeModelBuilder
-from gui_core.menu import Menu
+from gui_core.menu import RecentFilesMenu, Menu
 from gui_core.toolbars import BaseToolbar, BaseIcons
 from icons.icon import MetaIcon
 from core.debug import Debug
@@ -269,42 +269,107 @@ class PyEditController(object):
         self.overwrite = False
         self.parameters = None
 
+        # Get first (File) menu
+        menu = guiparent.menubar.GetMenu(0)
+        self.recent_files = RecentFilesMenu(
+            app_name='pyedit', parent_menu=menu, pos=1,
+        ) # pos=1 recent files menu position (index) in the parent (File) menu
+
+        self.recent_files.file_requested.connect(self.OpenRecentFile)
+
+    def _openFile(self, file_path):
+        """Try open file and read content
+
+        :param str file_path: file path
+
+        :return str or None: file content or None
+        """
+        try:
+            with open(file_path, 'r') as f:
+                content = f.read()
+                return content
+        except PermissionError:
+            GError(
+                message=_(
+                    "Permission denied <{}>. Please change file "
+                    "permission for reading.".format(file_path)
+                ),
+                parent=self.guiparent,
+                showTraceback=False,
+            )
+        except IOError:
+            GError(
+                message=_("Couldn't read file <{}>.".format(file_path)),
+                parent=self.guiparent,
+            )
+
+    def _writeFile(self, file_path, content, additional_err_message=''):
+        """Try open file and write content
+
+        :param str file_path: file path
+        :param str content: content written to the file
+        :param str additional_err_message: additional error message
+
+        :return None or True: file written or None
+        """
+        try:
+            with open(file_path, 'w') as f:
+                f.write(content)
+                return True
+        except PermissionError:
+            GError(
+                message=_(
+                    "Permission denied <{}>. Please change file "
+                    "permission for writting.{}".format(
+                        file_path, additional_err_message,
+                    ),
+                ),
+                parent=self.guiparent,
+                showTraceback=False,
+            )
+        except IOError:
+            GError(
+                message=_(
+                    "Couldn't write file <{}>.{}".
+                    format(file_path, additional_err_message),
+                ),
+                parent=self.guiparent,
+            )
+
     def OnRun(self, event):
         """Run Python script"""
         if not self.filename:
             self.filename = gscript.tempfile() + '.py'
             self.tempfile = True
-            try:
-                fd = open(self.filename, "w")
-                fd.write(self.body.GetText())
-            except IOError as e:
-                GError(_("Unable to launch Python script. %s") % e,
-                       parent=self.guiparent)
-                return
-            finally:
-                fd.close()
+            file_is_written = self._writeFile(
+                file_path=self.filename, content=self.body.GetText(),
+                additional_err_message=" Unable to launch Python script.",
+
+            )
+            if file_is_written:
                 mode = stat.S_IMODE(os.lstat(self.filename)[stat.ST_MODE])
                 os.chmod(self.filename, mode | stat.S_IXUSR)
         else:
             # always save automatically before running
-            fd = open(self.filename, "w")
-            try:
-                fd.write(self.body.GetText())
-            finally:
-                fd.close()
-            # set executable file
-            # (not sure if needed every time but useful for opened files)
-            os.chmod(self.filename, stat.S_IRWXU | stat.S_IWUSR)
-
-        # run in console as other modules, avoid Python shell which
-        # carries variables over to the next execution
-        env = os.environ.copy()
-        if self.overwrite:
-            env['GRASS_OVERWRITE'] = '1'
-        cmd = [fd.name]
-        if self.parameters:
-            cmd.extend(self.parameters)
-        self.giface.RunCmd(cmd, env=env)
+            file_is_written = self._writeFile(
+                file_path=self.filename, content=self.body.GetText(),
+                additional_err_message=" Unable to launch Python script.",
+            )
+            if file_is_written:
+                # set executable file
+                # (not sure if needed every time but useful for opened files)
+                os.chmod(self.filename, stat.S_IRWXU | stat.S_IWUSR)
+
+        if file_is_written:
+            # run in console as other modules, avoid Python shell which
+            # carries variables over to the next execution
+            env = os.environ.copy()
+            if self.overwrite:
+                env['GRASS_OVERWRITE'] = '1'
+            cmd = [self.filename]
+            if self.parameters:
+                cmd.extend(self.parameters)
+            self.giface.RunCmd(cmd, env=env)
 
     def SaveAs(self):
         """Save python script to file"""
@@ -349,14 +414,12 @@ class PyEditController(object):
     def Save(self):
         """Save current content to a file and set executable permissions"""
         assert self.filename
-        fd = open(self.filename, "w")
-        try:
-            fd.write(self.body.GetText())
-        finally:
-            fd.close()
-
-        # executable file
-        os.chmod(self.filename, stat.S_IRWXU | stat.S_IWUSR)
+        file_is_written = self._writeFile(
+                 file_path=self.filename, content=self.body.GetText(),
+             )
+        if file_is_written:
+            # executable file
+            os.chmod(self.filename, stat.S_IRWXU | stat.S_IWUSR)
 
     def OnSave(self, event):
         """Save python script to file
@@ -368,6 +431,11 @@ class PyEditController(object):
         else:
             self.SaveAs()
 
+        if self.filename:
+            self.recent_files.AddFileToHistory(
+                filename=self.filename,
+            )
+
     def IsModified(self):
         """Check if python script has been modified"""
         return self.body.modified
@@ -387,11 +455,11 @@ class PyEditController(object):
         if not filename:
             return
 
-        fd = open(filename, "r")
-        try:
-            self.body.SetText(fd.read())
-        finally:
-            fd.close()
+        content = self._openFile(file_path=filename)
+        if content:
+            self.body.SetText(content)
+        else:
+            return
 
         self.filename = filename
         self.tempfile = False
@@ -400,6 +468,36 @@ class PyEditController(object):
         """Handle open event but ask about replacing content first"""
         if self.CanReplaceContent('file'):
             self.Open()
+            if self.filename:
+                self.recent_files.AddFileToHistory(
+                    filename=self.filename,
+                )
+
+    def OpenRecentFile(self, path, file_exists, file_history):
+        """Try open recent file and read content
+
+        :param str path: file path
+        :param bool file_exists: file path exists
+        :param bool file_history: file history obj instance
+
+        :return: None
+        """
+        if not file_exists:
+            GError(
+                _(
+                    "File <{}> doesn't exist."
+                    "It was probably moved or deleted.".format(path)
+                ),
+                parent=self.guiparent,
+            )
+        else:
+            if self.CanReplaceContent(by_message='file'):
+                self.filename = path
+                content = self._openFile(file_path=path)
+                if content:
+                    self.body.SetText(content)
+                    file_history.AddFileToHistory(filename=path)  # move up the list
+                    self.tempfile = False
 
     def IsEmpty(self):
         """Check if python script is empty"""
@@ -418,22 +516,27 @@ class PyEditController(object):
     def SetScriptTemplate(self, event):
         if self.CanReplaceContent('template'):
             self.body.SetText(script_template())
+            self.filename = None
 
     def SetModuleTemplate(self, event):
         if self.CanReplaceContent('template'):
             self.body.SetText(module_template())
+            self.filename = None
 
     def SetScriptExample(self, event):
         if self.CanReplaceContent('example'):
             self.body.SetText(script_example())
+            self.filename = None
 
     def SetModuleExample(self, event):
         if self.CanReplaceContent('example'):
             self.body.SetText(module_example())
+            self.filename = None
 
     def SetModuleErrorHandlingExample(self, event):
         if self.CanReplaceContent('example'):
             self.body.SetText(module_error_handling_example())
+            self.filename = None
 
     def CanReplaceContent(self, by_message):
         """Check with user if we can replace content by something else