1# -*- coding: utf-8 -*-
2"""KiCad Python Shell.
3
4This module provides the python shell for KiCad.
5
6KiCad starts the shell once, by calling makePcbnewShellWindow() the
7first time it is opened, subsequently the shell window is just hidden
8or shown, as per user requirements.
9
10IF makePcbnewShellWindow() is called again, a second/third shell window
11can be created.
12
13Note:
14**DO NOT** import pcbnew module if not called from Pcbnew: on msys2 it creates a serious issue:
15the python script import is broken because the pcbnew application is not running, and for instance
16Pgm() returns a nullptr and Kicad crashes when Pgm is invoked.
17
18"""
19
20import wx
21import sys
22import os
23
24from wx.py import crust, version, dispatcher
25
26from .kicad_pyeditor import KiCadEditorNotebookFrame
27from .kicad_pyeditor import KiCadEditorNotebook
28
29class KiCadPyShell(KiCadEditorNotebookFrame):
30    def __init__(self, parent):
31        # Search if a pcbnew frame is open, because import pcbnew can be made only if it exists.
32        # frame names are "SchematicFrame" and "PcbFrame"
33        #
34        # Note we must do this before the KiCadEditorNotebookFrame __init__ call because it ends up calling _setup back on us
35        frame = wx.FindWindowByName( "PcbFrame" )
36
37        self.isPcbframe = frame is not None
38
39        KiCadEditorNotebookFrame.__init__(self, parent)
40
41    def _setup_startup(self):
42        if self.config_dir != "":
43            """Initialise the startup script."""
44            # Create filename for startup script.
45            self.startup_file = os.path.join(self.config_dir,
46                                             "PyShell_pcbnew_startup.py")
47            self.execStartupScript = True
48
49            # Check if startup script exists
50            if not os.path.isfile(self.startup_file):
51                # Not, so try to create a default.
52                try:
53                    default_startup = open(self.startup_file, 'w')
54                    # provide the content for the default startup file.
55                    default_startup.write(
56                        "### DEFAULT STARTUP FILE FOR KiCad Python Shell\n" +
57                        "# Enter any Python code you would like to execute when" +
58                        " the PCBNEW python shell first runs.\n" +
59                        "\n" +
60                        "# For example, uncomment the following lines to import the current board\n" +
61                        "\n" +
62                        "# import pcbnew\n" +
63                        "# import eeschema\n" +
64                        "# board = pcbnew.GetBoard()\n" +
65                        "# sch = eeschema.GetSchematic()\n")
66                    default_startup.close()
67                except:
68                    pass
69        else:
70            self.startup_file = ""
71            self.execStartupScript = False
72
73    def _setup(self):
74        """
75        Setup prior to first buffer creation.
76
77        Called automatically by base class during init.
78        """
79        self.notebook = KiCadEditorNotebook(parent=self.parent)
80        intro = 'Py %s' % version.VERSION
81        import types
82        import builtins
83        module = types.ModuleType('__main__')
84        module.__dict__['__builtins__'] = builtins
85        namespace = module.__dict__.copy()
86
87        '''
88        Import pcbnew **only** if the board editor exists, to avoid strange behavior if not.
89        pcbnew.SETTINGS_MANAGER should be in fact called only if the python console is created
90        from the board editor, and if created from schematic editor, should use something like
91        eeschema.SETTINGS_MANAGER
92        '''
93        if self.isPcbframe:
94            import pcbnew
95            self.config_dir = pcbnew.SETTINGS_MANAGER.GetUserSettingsPath()
96        else:
97            self.config_dir = ""
98
99        self.dataDir = self.config_dir
100
101        self._setup_startup()
102        self.history_file = os.path.join(self.config_dir,
103                                         "PyShell_pcbnew.history")
104
105        self.config = None
106        # self.config_file = os.path.join(self.config_dir,
107        #                                 "PyShell_pcbnew.cfg")
108        # self.config = wx.FileConfig(localFilename=self.config_file)
109        # self.config.SetRecordDefaults(True)
110
111        self.autoSaveSettings = False
112        self.autoSaveHistory = False
113        self.LoadSettings()
114
115        self.crust = crust.Crust(parent=self.notebook,
116                                 intro=intro, locals=namespace,
117                                 rootLabel="locals()",
118                                 startupScript=self.startup_file,
119                                 execStartupScript=self.execStartupScript)
120
121        self.crust._CheckShouldSplit()
122        self.shell = self.crust.shell
123        # Override the filling so that status messages go to the status bar.
124        self.crust.filling.tree.setStatusText = self.parent.SetStatusText
125        # Override the shell so that status messages go to the status bar.
126        self.shell.setStatusText = self.parent.SetStatusText
127        # Fix a problem with the sash shrinking to nothing.
128        self.crust.filling.SetSashPosition(200)
129        self.notebook.AddPage(page=self.crust, text='*Shell*', select=True)
130        self.setEditor(self.crust.editor)
131        self.crust.editor.SetFocus()
132
133        self.LoadHistory()
134
135    def OnAbout(self, event):
136        """Display an About window."""
137        title = 'About : KiCad - Python Shell'
138        text = "Enhanced Python Shell for KiCad\n\n" + \
139               "KiCad Revision: %s\n" % "??.??" + \
140               "Platform: %s\n" % sys.platform + \
141               "Python Version: %s\n" % sys.version.split()[0] + \
142               "wxPython Version: %s\n" % wx.VERSION_STRING + \
143               ("\t(%s)\n" % ", ".join(wx.PlatformInfo[1:]))
144
145        dialog = wx.MessageDialog(self.parent, text, title,
146                                  wx.OK | wx.ICON_INFORMATION)
147        dialog.ShowModal()
148        dialog.Destroy()
149
150    def EditStartupScript(self):
151        """Open a Edit buffer of the startup script file."""
152        self.bufferCreate(filename=self.startup_file)
153
154    def LoadSettings(self):
155        """Load settings for the shell."""
156        if self.config is not None:
157            KiCadEditorNotebookFrame.LoadSettings(self.parent, self.config)
158            self.autoSaveSettings = \
159                self.config.ReadBool('Options/AutoSaveSettings', False)
160            self.execStartupScript = \
161                self.config.ReadBool('Options/ExecStartupScript', True)
162            self.autoSaveHistory = \
163                self.config.ReadBool('Options/AutoSaveHistory', False)
164            self.hideFoldingMargin = \
165                self.config.ReadBool('Options/HideFoldingMargin', True)
166
167    def SaveSettings(self, force=False):
168        """
169        Save settings for the shell.
170
171        Arguments:
172
173        force -- False - Autosaving.  True - Manual Saving.
174        """
175        if self.config is not None:
176            # always save these
177            self.config.WriteBool('Options/AutoSaveSettings',
178                                  self.autoSaveSettings)
179            if self.autoSaveSettings or force:
180                KiCadEditorNotebookFrame.SaveSettings(self, self.config)
181
182                self.config.WriteBool('Options/AutoSaveHistory',
183                                      self.autoSaveHistory)
184                self.config.WriteBool('Options/ExecStartupScript',
185                                      self.execStartupScript)
186                self.config.WriteBool('Options/HideFoldingMargin',
187                                      self.hideFoldingMargin)
188            if self.autoSaveHistory:
189                self.SaveHistory()
190
191    def DoSaveSettings(self):
192        """Menu function to trigger saving the shells settings."""
193        if self.config is not None:
194            self.SaveSettings(force=True)
195            self.config.Flush()
196
197    def SaveHistory(self):
198        """Save shell history to the shell history file."""
199        if self.dataDir:
200            try:
201                name = self.history_file
202                f = file(name, 'w')
203                hist = []
204                enc = wx.GetDefaultPyEncoding()
205                for h in self.shell.history:
206                    if isinstance(h, unicode):
207                        h = h.encode(enc)
208                    hist.append(h)
209                hist = '\x00\n'.join(hist)
210                f.write(hist)
211                f.close()
212            except:
213                d = wx.MessageDialog(self, "Error saving history file.",
214                                     "Error", wx.ICON_EXCLAMATION | wx.OK)
215                d.ShowModal()
216                d.Destroy()
217                raise
218
219    def LoadHistory(self):
220        """Load shell history from the shell history file."""
221        if self.dataDir:
222            name = self.history_file
223            if os.path.exists(name):
224                try:
225                    f = file(name, 'U')
226                    hist = f.read()
227                    f.close()
228                    self.shell.history = hist.split('\x00\n')
229                    dispatcher.send(signal="Shell.loadHistory",
230                                    history=self.shell.history)
231                except:
232                    d = wx.MessageDialog(self,
233                                         "Error loading history file!",
234                                         "Error", wx.ICON_EXCLAMATION | wx.OK)
235                    d.ShowModal()
236                    d.Destroy()
237
238
239def makePcbnewShellWindow(parentid):
240    """
241    Create a new Shell Window and return its handle.
242
243    Arguments:
244    parent -- The parent window to attach to.
245
246    Returns:
247    The handle to the new window.
248    """
249
250    parent = wx.FindWindowById( parentid )
251
252    frmname = parent.GetName()
253    return KiCadPyShell(parent)
254