BeremizIDE.py
changeset 1662 8816f7316d9c
child 1680 6db967480b7d
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/BeremizIDE.py	Fri Mar 10 17:36:18 2017 +0300
@@ -0,0 +1,1177 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# This file is part of Beremiz, a Integrated Development Environment for
+# programming IEC 61131-3 automates supporting plcopen standard and CanFestival.
+#
+# Copyright (C) 2007: Edouard TISSERANT and Laurent BESSARD
+# Copyright (C) 2016: Andrey Skvortsov <andrej.skvortzov@gmail.com>
+#
+# See COPYING file for copyrights details.
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+
+
+import os, sys
+import tempfile
+import shutil
+import random
+import time
+import version
+from types import ListType
+
+beremiz_dir = os.path.dirname(os.path.realpath(__file__))
+
+def Bpath(*args):
+    return os.path.join(beremiz_dir,*args)
+
+
+
+import wx.lib.buttons, wx.lib.statbmp, wx.stc
+import cPickle
+import types, time, re, platform, time, traceback, commands
+
+from docutil import OpenHtmlFrame
+from editors.EditorPanel import EditorPanel
+from editors.Viewer import Viewer
+from editors.TextViewer import TextViewer
+from editors.ResourceEditor import ConfigurationEditor, ResourceEditor
+from editors.DataTypeEditor import DataTypeEditor
+from util.MiniTextControler import MiniTextControler
+from util.ProcessLogger import ProcessLogger
+from controls.LogViewer import LogViewer
+from controls.CustomStyledTextCtrl import CustomStyledTextCtrl
+from controls import EnhancedStatusBar as esb
+from dialogs.AboutDialog import ShowAboutDialog
+
+from PLCControler import LOCATION_CONFNODE, LOCATION_MODULE, LOCATION_GROUP, LOCATION_VAR_INPUT, LOCATION_VAR_OUTPUT, LOCATION_VAR_MEMORY, ITEM_PROJECT, ITEM_RESOURCE
+from ProjectController import ProjectController, GetAddMenuItems, MATIEC_ERROR_MODEL, ITEM_CONFNODE
+
+
+MAX_RECENT_PROJECTS = 9
+
+if wx.Platform == '__WXMSW__':
+    faces = {
+        'mono' : 'Courier New',
+        'size' : 8,
+    }
+else:
+    faces = {
+        'mono' : 'Courier',
+        'size' : 10,
+    }
+
+from threading import Lock,Timer,currentThread
+MainThread = currentThread().ident
+REFRESH_PERIOD = 0.1
+from time import time as gettime
+class LogPseudoFile:
+    """ Base class for file like objects to facilitate StdOut for the Shell."""
+    def __init__(self, output, risecall):
+        self.red_white = 1
+        self.red_yellow = 2
+        self.black_white = wx.stc.STC_STYLE_DEFAULT
+        self.output = output
+        self.risecall = risecall
+        # to prevent rapid fire on rising log panel
+        self.rising_timer = 0
+        self.lock = Lock()
+        self.YieldLock = Lock()
+        self.RefreshLock = Lock()
+        self.TimerAccessLock = Lock()
+        self.stack = []
+        self.LastRefreshTime = gettime()
+        self.LastRefreshTimer = None
+
+    def write(self, s, style = None):
+        if self.lock.acquire():
+            self.stack.append((s,style))
+            self.lock.release()
+            current_time = gettime()
+            self.TimerAccessLock.acquire()
+            if self.LastRefreshTimer:
+                self.LastRefreshTimer.cancel()
+                self.LastRefreshTimer=None
+            self.TimerAccessLock.release()
+            if current_time - self.LastRefreshTime > REFRESH_PERIOD and self.RefreshLock.acquire(False):
+                self._should_write()
+            else:
+                self.TimerAccessLock.acquire()
+                self.LastRefreshTimer = Timer(REFRESH_PERIOD, self._timer_expired)
+                self.LastRefreshTimer.start()
+                self.TimerAccessLock.release()
+
+    def _timer_expired(self):
+        if self.RefreshLock.acquire(False):
+            self._should_write()
+        else:
+            self.TimerAccessLock.acquire()
+            self.LastRefreshTimer = Timer(REFRESH_PERIOD, self._timer_expired)
+            self.LastRefreshTimer.start()
+            self.TimerAccessLock.release()
+
+    def _should_write(self):
+        wx.CallAfter(self._write)
+        if MainThread == currentThread().ident:
+            app = wx.GetApp()
+            if app is not None:
+                if self.YieldLock.acquire(0):
+                    app.Yield()
+                    self.YieldLock.release()
+
+    def _write(self):
+        if self.output :
+            self.output.Freeze()
+            self.lock.acquire()
+            for s, style in self.stack:
+                if style is None : style=self.black_white
+                if style != self.black_white:
+                    self.output.StartStyling(self.output.GetLength(), 0xff)
+
+                # Temporary deactivate read only mode on StyledTextCtrl for
+                # adding text. It seems that text modifications, even
+                # programmatically, are disabled in StyledTextCtrl when read
+                # only is active
+                start_pos = self.output.GetLength()
+                self.output.SetReadOnly(False)
+                self.output.AppendText(s)
+                self.output.SetReadOnly(True)
+                text_len = self.output.GetLength() - start_pos
+
+                if style != self.black_white:
+                    self.output.SetStyling(text_len, style)
+            self.stack = []
+            self.lock.release()
+            self.output.Thaw()
+            self.LastRefreshTime = gettime()
+            try:
+                self.RefreshLock.release()
+            except:
+                pass
+            newtime = time.time()
+            if newtime - self.rising_timer > 1:
+                self.risecall(self.output)
+            self.rising_timer = newtime
+
+    def write_warning(self, s):
+        self.write(s,self.red_white)
+
+    def write_error(self, s):
+        self.write(s,self.red_yellow)
+
+    def writeyield(self, s):
+        self.write(s)
+        wx.GetApp().Yield()
+
+    def flush(self):
+        # Temporary deactivate read only mode on StyledTextCtrl for clearing
+        # text. It seems that text modifications, even programmatically, are
+        # disabled in StyledTextCtrl when read only is active
+        self.output.SetReadOnly(False)
+        self.output.SetText("")
+        self.output.SetReadOnly(True)
+
+    def isatty(self):
+        return False
+
+ID_FILEMENURECENTPROJECTS = wx.NewId()
+
+from IDEFrame import TITLE,\
+                     EDITORTOOLBAR,\
+                     FILEMENU,\
+                     EDITMENU,\
+                     DISPLAYMENU,\
+                     PROJECTTREE,\
+                     POUINSTANCEVARIABLESPANEL,\
+                     LIBRARYTREE,\
+                     SCALING,\
+                     PAGETITLES,\
+                     IDEFrame, AppendMenu,\
+                     EncodeFileSystemPath, DecodeFileSystemPath
+from util.BitmapLibrary import GetBitmap
+
+class Beremiz(IDEFrame):
+
+    def _init_utils(self):
+        self.ConfNodeMenu = wx.Menu(title='')
+        self.RecentProjectsMenu = wx.Menu(title='')
+
+        IDEFrame._init_utils(self)
+
+    def _init_coll_FileMenu_Items(self, parent):
+        AppendMenu(parent, help='', id=wx.ID_NEW,
+              kind=wx.ITEM_NORMAL, text=_(u'New') + '\tCTRL+N')
+        AppendMenu(parent, help='', id=wx.ID_OPEN,
+              kind=wx.ITEM_NORMAL, text=_(u'Open') + '\tCTRL+O')
+        parent.AppendMenu(ID_FILEMENURECENTPROJECTS, _("&Recent Projects"), self.RecentProjectsMenu)
+        parent.AppendSeparator()
+        AppendMenu(parent, help='', id=wx.ID_SAVE,
+              kind=wx.ITEM_NORMAL, text=_(u'Save') + '\tCTRL+S')
+        AppendMenu(parent, help='', id=wx.ID_SAVEAS,
+              kind=wx.ITEM_NORMAL, text=_(u'Save as') + '\tCTRL+SHIFT+S')
+        AppendMenu(parent, help='', id=wx.ID_CLOSE,
+              kind=wx.ITEM_NORMAL, text=_(u'Close Tab') + '\tCTRL+W')
+        AppendMenu(parent, help='', id=wx.ID_CLOSE_ALL,
+              kind=wx.ITEM_NORMAL, text=_(u'Close Project') + '\tCTRL+SHIFT+W')
+        parent.AppendSeparator()
+        AppendMenu(parent, help='', id=wx.ID_PAGE_SETUP,
+              kind=wx.ITEM_NORMAL, text=_(u'Page Setup') + '\tCTRL+ALT+P')
+        AppendMenu(parent, help='', id=wx.ID_PREVIEW,
+              kind=wx.ITEM_NORMAL, text=_(u'Preview') + '\tCTRL+SHIFT+P')
+        AppendMenu(parent, help='', id=wx.ID_PRINT,
+              kind=wx.ITEM_NORMAL, text=_(u'Print') + '\tCTRL+P')
+        parent.AppendSeparator()
+        AppendMenu(parent, help='', id=wx.ID_EXIT,
+              kind=wx.ITEM_NORMAL, text=_(u'Quit') + '\tCTRL+Q')
+
+        self.Bind(wx.EVT_MENU, self.OnNewProjectMenu, id=wx.ID_NEW)
+        self.Bind(wx.EVT_MENU, self.OnOpenProjectMenu, id=wx.ID_OPEN)
+        self.Bind(wx.EVT_MENU, self.OnSaveProjectMenu, id=wx.ID_SAVE)
+        self.Bind(wx.EVT_MENU, self.OnSaveProjectAsMenu, id=wx.ID_SAVEAS)
+        self.Bind(wx.EVT_MENU, self.OnCloseTabMenu, id=wx.ID_CLOSE)
+        self.Bind(wx.EVT_MENU, self.OnCloseProjectMenu, id=wx.ID_CLOSE_ALL)
+        self.Bind(wx.EVT_MENU, self.OnPageSetupMenu, id=wx.ID_PAGE_SETUP)
+        self.Bind(wx.EVT_MENU, self.OnPreviewMenu, id=wx.ID_PREVIEW)
+        self.Bind(wx.EVT_MENU, self.OnPrintMenu, id=wx.ID_PRINT)
+        self.Bind(wx.EVT_MENU, self.OnQuitMenu, id=wx.ID_EXIT)
+
+        self.AddToMenuToolBar([(wx.ID_NEW, "new", _(u'New'), None),
+                               (wx.ID_OPEN, "open", _(u'Open'), None),
+                               (wx.ID_SAVE, "save", _(u'Save'), None),
+                               (wx.ID_SAVEAS, "saveas", _(u'Save As...'), None),
+                               (wx.ID_PRINT, "print", _(u'Print'), None)])
+
+    def _RecursiveAddMenuItems(self, menu, items):
+        for name, text, help, children in items:
+            new_id = wx.NewId()
+            if len(children) > 0:
+                new_menu = wx.Menu(title='')
+                menu.AppendMenu(new_id, text, new_menu)
+                self._RecursiveAddMenuItems(new_menu, children)
+            else:
+                AppendMenu(menu, help=help, id=new_id,
+                       kind=wx.ITEM_NORMAL, text=text)
+                self.Bind(wx.EVT_MENU, self.GetAddConfNodeFunction(name),
+                          id=new_id)
+
+    def _init_coll_AddMenu_Items(self, parent):
+        IDEFrame._init_coll_AddMenu_Items(self, parent, False)
+        self._RecursiveAddMenuItems(parent, GetAddMenuItems())
+
+    def _init_coll_HelpMenu_Items(self, parent):
+        parent.Append(help='', id=wx.ID_ABOUT,
+              kind=wx.ITEM_NORMAL, text=_(u'About'))
+        self.Bind(wx.EVT_MENU, self.OnAboutMenu, id=wx.ID_ABOUT)
+
+    def _init_coll_ConnectionStatusBar_Fields(self, parent):
+        parent.SetFieldsCount(3)
+
+        parent.SetStatusText(number=0, text='')
+        parent.SetStatusText(number=1, text='')
+        parent.SetStatusText(number=2, text='')
+
+        parent.SetStatusWidths([-1, 300, 200])
+
+    def _init_ctrls(self, prnt):
+        IDEFrame._init_ctrls(self, prnt)
+
+        self.EditMenuSize = self.EditMenu.GetMenuItemCount()
+
+        inspectorID = wx.NewId()
+        self.Bind(wx.EVT_MENU, self.OnOpenWidgetInspector, id=inspectorID)
+        accels = [wx.AcceleratorEntry(wx.ACCEL_CTRL|wx.ACCEL_ALT, ord('I'), inspectorID)]
+
+        keyID = wx.NewId()
+        self.Bind(wx.EVT_MENU, self.SwitchFullScrMode, id=keyID)
+        accels += [wx.AcceleratorEntry(wx.ACCEL_NORMAL, wx.WXK_F12, keyID)]
+
+        for method,shortcut in [("Stop",     wx.WXK_F4),
+                                ("Run",      wx.WXK_F5),
+                                ("Transfer", wx.WXK_F6),
+                                ("Connect",  wx.WXK_F7),
+                                ("Build",    wx.WXK_F11)]:
+            def OnMethodGen(obj,meth):
+                def OnMethod(evt):
+                    if obj.CTR is not None:
+                       obj.CTR.CallMethod('_'+meth)
+                    wx.CallAfter(self.RefreshStatusToolBar)
+                return OnMethod
+            newid = wx.NewId()
+            self.Bind(wx.EVT_MENU, OnMethodGen(self,method), id=newid)
+            accels += [wx.AcceleratorEntry(wx.ACCEL_NORMAL, shortcut,newid)]
+
+        self.SetAcceleratorTable(wx.AcceleratorTable(accels))
+
+        self.LogConsole = CustomStyledTextCtrl(
+                  name='LogConsole', parent=self.BottomNoteBook, pos=wx.Point(0, 0),
+                  size=wx.Size(0, 0))
+        self.LogConsole.Bind(wx.EVT_SET_FOCUS, self.OnLogConsoleFocusChanged)
+        self.LogConsole.Bind(wx.EVT_KILL_FOCUS, self.OnLogConsoleFocusChanged)
+        self.LogConsole.Bind(wx.stc.EVT_STC_UPDATEUI, self.OnLogConsoleUpdateUI)
+        self.LogConsole.SetReadOnly(True)
+        self.LogConsole.SetWrapMode(wx.stc.STC_WRAP_CHAR)
+
+        # Define Log Console styles
+        self.LogConsole.StyleSetSpec(wx.stc.STC_STYLE_DEFAULT, "face:%(mono)s,size:%(size)d" % faces)
+        self.LogConsole.StyleClearAll()
+        self.LogConsole.StyleSetSpec(1, "face:%(mono)s,fore:#FF0000,size:%(size)d" % faces)
+        self.LogConsole.StyleSetSpec(2, "face:%(mono)s,fore:#FF0000,back:#FFFF00,size:%(size)d" % faces)
+
+        # Define Log Console markers
+        self.LogConsole.SetMarginSensitive(1, True)
+        self.LogConsole.SetMarginType(1, wx.stc.STC_MARGIN_SYMBOL)
+        self.LogConsole.MarkerDefine(0, wx.stc.STC_MARK_CIRCLE, "BLACK", "RED")
+
+        self.LogConsole.SetModEventMask(wx.stc.STC_MOD_INSERTTEXT)
+
+        self.LogConsole.Bind(wx.stc.EVT_STC_MARGINCLICK, self.OnLogConsoleMarginClick)
+        self.LogConsole.Bind(wx.stc.EVT_STC_MODIFIED, self.OnLogConsoleModified)
+
+        self.MainTabs["LogConsole"] = (self.LogConsole, _("Console"))
+        self.BottomNoteBook.AddPage(*self.MainTabs["LogConsole"])
+        #self.BottomNoteBook.Split(self.BottomNoteBook.GetPageIndex(self.LogConsole), wx.RIGHT)
+
+        self.LogViewer = LogViewer(self.BottomNoteBook, self)
+        self.MainTabs["LogViewer"] = (self.LogViewer, _("PLC Log"))
+        self.BottomNoteBook.AddPage(*self.MainTabs["LogViewer"])
+        #self.BottomNoteBook.Split(self.BottomNoteBook.GetPageIndex(self.LogViewer), wx.RIGHT)
+
+        StatusToolBar = wx.ToolBar(self, -1, wx.DefaultPosition, wx.DefaultSize,
+                wx.TB_FLAT | wx.TB_NODIVIDER | wx.NO_BORDER)
+        StatusToolBar.SetToolBitmapSize(wx.Size(25, 25))
+        StatusToolBar.Realize()
+        self.Panes["StatusToolBar"] = StatusToolBar
+        self.AUIManager.AddPane(StatusToolBar, wx.aui.AuiPaneInfo().
+                  Name("StatusToolBar").Caption(_("Status ToolBar")).
+                  ToolbarPane().Top().Position(1).
+                  LeftDockable(False).RightDockable(False))
+
+        self.AUIManager.Update()
+
+        self.ConnectionStatusBar = esb.EnhancedStatusBar(self, style=wx.ST_SIZEGRIP)
+        self._init_coll_ConnectionStatusBar_Fields(self.ConnectionStatusBar)
+        self.ProgressStatusBar = wx.Gauge(self.ConnectionStatusBar, -1, range = 100)
+        self.ConnectionStatusBar.AddWidget(self.ProgressStatusBar, esb.ESB_EXACT_FIT, esb.ESB_EXACT_FIT, 2)        
+        self.ProgressStatusBar.Hide()
+        self.SetStatusBar(self.ConnectionStatusBar)
+
+    def __init_execute_path(self):
+        if os.name == 'nt':
+            # on windows, desktop shortcut launches Beremiz.py
+            # with working dir set to mingw/bin.
+            # then we prefix CWD to PATH in order to ensure that
+            # commands invoked by build process by default are
+            # found here.
+            os.environ["PATH"] = os.getcwd()+';'+os.environ["PATH"]
+        
+        
+    def __init__(self, parent, projectOpen=None, buildpath=None, ctr=None, debug=True):
+        # Add beremiz's icon in top left corner of the frame
+        self.icon = wx.Icon(Bpath("images", "brz.ico"), wx.BITMAP_TYPE_ICO)
+        self.__init_execute_path()
+        
+        IDEFrame.__init__(self, parent, debug)
+        self.Log = LogPseudoFile(self.LogConsole,self.SelectTab)
+
+        self.local_runtime = None
+        self.runtime_port = None
+        self.local_runtime_tmpdir = None
+
+        self.LastPanelSelected = None
+
+        # Define Tree item icon list
+        self.LocationImageList = wx.ImageList(16, 16)
+        self.LocationImageDict = {}
+
+        # Icons for location items
+        for imgname, itemtype in [
+            ("CONFIGURATION", LOCATION_CONFNODE),
+            ("RESOURCE",      LOCATION_MODULE),
+            ("PROGRAM",       LOCATION_GROUP),
+            ("VAR_INPUT",     LOCATION_VAR_INPUT),
+            ("VAR_OUTPUT",    LOCATION_VAR_OUTPUT),
+            ("VAR_LOCAL",     LOCATION_VAR_MEMORY)]:
+            self.LocationImageDict[itemtype] = self.LocationImageList.Add(GetBitmap(imgname))
+
+        # Icons for other items
+        for imgname, itemtype in [
+            ("Extension", ITEM_CONFNODE)]:
+            self.TreeImageDict[itemtype] = self.TreeImageList.Add(GetBitmap(imgname))
+
+        if projectOpen is not None:
+            projectOpen = DecodeFileSystemPath(projectOpen, False)
+
+        if projectOpen is not None and os.path.isdir(projectOpen):
+            self.CTR = ProjectController(self, self.Log)
+            self.Controler = self.CTR
+            result, err = self.CTR.LoadProject(projectOpen, buildpath)
+            if not result:
+                self.LibraryPanel.SetController(self.Controler)
+                self.ProjectTree.Enable(True)
+                self.PouInstanceVariablesPanel.SetController(self.Controler)
+                self.RefreshConfigRecentProjects(os.path.abspath(projectOpen))
+                self._Refresh(PROJECTTREE, POUINSTANCEVARIABLESPANEL, LIBRARYTREE)
+            else:
+                self.ResetView()
+                self.ShowErrorMessage(result)
+        else:
+            self.CTR = ctr
+            self.Controler = ctr
+            if ctr is not None:
+                self.LibraryPanel.SetController(self.Controler)
+                self.ProjectTree.Enable(True)
+                self.PouInstanceVariablesPanel.SetController(self.Controler)
+                self._Refresh(PROJECTTREE, POUINSTANCEVARIABLESPANEL, LIBRARYTREE)
+        if self.EnableDebug:
+            self.DebugVariablePanel.SetDataProducer(self.CTR)
+
+        self.Bind(wx.EVT_CLOSE, self.OnCloseFrame)
+
+        self._Refresh(TITLE, EDITORTOOLBAR, FILEMENU, EDITMENU, DISPLAYMENU)
+        self.RefreshAll()
+        self.LogConsole.SetFocus()
+
+    def RefreshTitle(self):
+        name = _("Beremiz")
+        if self.CTR is not None:
+            projectname = self.CTR.GetProjectName()
+            if self.CTR.ProjectTestModified():
+                projectname = "~%s~" % projectname
+            self.SetTitle("%s - %s" % (name, projectname))
+        else:
+            self.SetTitle(name)
+
+    def StartLocalRuntime(self, taskbaricon = True):
+        if (self.local_runtime is None) or (self.local_runtime.exitcode is not None):
+            # create temporary directory for runtime working directory
+            self.local_runtime_tmpdir = tempfile.mkdtemp()
+            # choose an arbitrary random port for runtime
+            self.runtime_port = int(random.random() * 1000) + 61131
+            # launch local runtime
+            self.local_runtime = ProcessLogger(self.Log,
+                "\"%s\" \"%s\" -p %s -i localhost %s %s"%(
+                    sys.executable,
+                    Bpath("Beremiz_service.py"),
+                    self.runtime_port,
+                    {False : "-x 0", True :"-x 1"}[taskbaricon],
+                    self.local_runtime_tmpdir),
+                no_gui=False,
+                timeout=500, keyword = self.local_runtime_tmpdir,
+                cwd = self.local_runtime_tmpdir)
+            self.local_runtime.spin()
+        return self.runtime_port
+
+    def KillLocalRuntime(self):
+        if self.local_runtime is not None:
+            # shutdown local runtime
+            self.local_runtime.kill(gently=False)
+            # clear temp dir
+            shutil.rmtree(self.local_runtime_tmpdir)
+
+            self.local_runtime = None
+
+    def OnOpenWidgetInspector(self, evt):
+        # Activate the widget inspection tool
+        from wx.lib.inspection import InspectionTool
+        if not InspectionTool().initialized:
+            InspectionTool().Init()
+
+        # Find a widget to be selected in the tree.  Use either the
+        # one under the cursor, if any, or this frame.
+        wnd = wx.FindWindowAtPointer()
+        if not wnd:
+            wnd = self
+        InspectionTool().Show(wnd, True)
+
+    def OnLogConsoleFocusChanged(self, event):
+        self.RefreshEditMenu()
+        event.Skip()
+
+    def OnLogConsoleUpdateUI(self, event):
+        self.SetCopyBuffer(self.LogConsole.GetSelectedText(), True)
+        event.Skip()
+
+    def OnLogConsoleMarginClick(self, event):
+        line_idx = self.LogConsole.LineFromPosition(event.GetPosition())
+        wx.CallAfter(self.SearchLineForError, self.LogConsole.GetLine(line_idx))
+        event.Skip()
+
+    def OnLogConsoleModified(self, event):
+        line_idx = self.LogConsole.LineFromPosition(event.GetPosition())
+        line = self.LogConsole.GetLine(line_idx)
+        if line:
+            result = MATIEC_ERROR_MODEL.match(line)
+            if result is not None:
+                self.LogConsole.MarkerAdd(line_idx, 0)
+        event.Skip()
+
+    def SearchLineForError(self, line):
+        if self.CTR is not None:
+            result = MATIEC_ERROR_MODEL.match(line)
+            if result is not None:
+                first_line, first_column, last_line, last_column, error = result.groups()
+                infos = self.CTR.ShowError(self.Log,
+                                                  (int(first_line), int(first_column)),
+                                                  (int(last_line), int(last_column)))
+
+    ## Function displaying an Error dialog in PLCOpenEditor.
+    #  @return False if closing cancelled.
+    def CheckSaveBeforeClosing(self, title=_("Close Project")):
+        if self.CTR.ProjectTestModified():
+            dialog = wx.MessageDialog(self,
+                                      _("There are changes, do you want to save?"),
+                                      title,
+                                      wx.YES_NO|wx.CANCEL|wx.ICON_QUESTION)
+            answer = dialog.ShowModal()
+            dialog.Destroy()
+            if answer == wx.ID_YES:
+                self.CTR.SaveProject()
+            elif answer == wx.ID_CANCEL:
+                return False
+
+        for idx in xrange(self.TabsOpened.GetPageCount()):
+            window = self.TabsOpened.GetPage(idx)
+            if not window.CheckSaveBeforeClosing():
+                return False
+
+        return True
+
+    def GetTabInfos(self, tab):
+        if (isinstance(tab, EditorPanel) and
+            not isinstance(tab, (Viewer,
+                                 TextViewer,
+                                 ResourceEditor,
+                                 ConfigurationEditor,
+                                 DataTypeEditor))):
+            return ("confnode", tab.Controler.CTNFullName(), tab.GetTagName())
+        elif (isinstance(tab, TextViewer) and
+              (tab.Controler is None or isinstance(tab.Controler, MiniTextControler))):
+            return ("confnode", None, tab.GetInstancePath())
+        else:
+            return IDEFrame.GetTabInfos(self, tab)
+
+    def LoadTab(self, notebook, page_infos):
+        if page_infos[0] == "confnode":
+            if page_infos[1] is None:
+                confnode = self.CTR
+            else:
+                confnode = self.CTR.GetChildByName(page_infos[1])
+            return notebook.GetPageIndex(confnode._OpenView(*page_infos[2:]))
+        else:
+            return IDEFrame.LoadTab(self, notebook, page_infos)
+
+    # Strange hack required by WAMP connector, using twisted.
+    # Twisted reactor needs to be stopped only before quit,
+    # since it cannot be restarted
+    ToDoBeforeQuit = []
+    def AddToDoBeforeQuit(self, Thing):
+        self.ToDoBeforeQuit.append(Thing)
+
+    def OnCloseFrame(self, event):
+        for evt_type in [wx.EVT_SET_FOCUS,
+                         wx.EVT_KILL_FOCUS,
+                         wx.stc.EVT_STC_UPDATEUI]:
+            self.LogConsole.Unbind(evt_type)
+        if self.CTR is None or self.CheckSaveBeforeClosing(_("Close Application")):
+            if self.CTR is not None:
+                self.CTR.KillDebugThread()
+            self.KillLocalRuntime()
+
+            self.SaveLastState()
+
+            for Thing in self.ToDoBeforeQuit :
+                Thing()
+            self.ToDoBeforeQuit = []
+
+            event.Skip()
+        else:
+            event.Veto()
+
+    def RefreshFileMenu(self):
+        self.RefreshRecentProjectsMenu()
+
+        MenuToolBar = self.Panes["MenuToolBar"]
+        if self.CTR is not None:
+            selected = self.TabsOpened.GetSelection()
+            if selected >= 0:
+                window = self.TabsOpened.GetPage(selected)
+                viewer_is_modified = window.IsModified()
+                is_viewer = isinstance(window, Viewer)
+            else:
+                viewer_is_modified = is_viewer = False
+            if self.TabsOpened.GetPageCount() > 0:
+                self.FileMenu.Enable(wx.ID_CLOSE, True)
+                if is_viewer:
+                    self.FileMenu.Enable(wx.ID_PREVIEW, True)
+                    self.FileMenu.Enable(wx.ID_PRINT, True)
+                    MenuToolBar.EnableTool(wx.ID_PRINT, True)
+                else:
+                    self.FileMenu.Enable(wx.ID_PREVIEW, False)
+                    self.FileMenu.Enable(wx.ID_PRINT, False)
+                    MenuToolBar.EnableTool(wx.ID_PRINT, False)
+            else:
+                self.FileMenu.Enable(wx.ID_CLOSE, False)
+                self.FileMenu.Enable(wx.ID_PREVIEW, False)
+                self.FileMenu.Enable(wx.ID_PRINT, False)
+                MenuToolBar.EnableTool(wx.ID_PRINT, False)
+            self.FileMenu.Enable(wx.ID_PAGE_SETUP, True)
+            project_modified = self.CTR.ProjectTestModified() or viewer_is_modified
+            self.FileMenu.Enable(wx.ID_SAVE, project_modified)
+            MenuToolBar.EnableTool(wx.ID_SAVE, project_modified)
+            self.FileMenu.Enable(wx.ID_SAVEAS, True)
+            MenuToolBar.EnableTool(wx.ID_SAVEAS, True)
+            self.FileMenu.Enable(wx.ID_CLOSE_ALL, True)
+        else:
+            self.FileMenu.Enable(wx.ID_CLOSE, False)
+            self.FileMenu.Enable(wx.ID_PAGE_SETUP, False)
+            self.FileMenu.Enable(wx.ID_PREVIEW, False)
+            self.FileMenu.Enable(wx.ID_PRINT, False)
+            MenuToolBar.EnableTool(wx.ID_PRINT, False)
+            self.FileMenu.Enable(wx.ID_SAVE, False)
+            MenuToolBar.EnableTool(wx.ID_SAVE, False)
+            self.FileMenu.Enable(wx.ID_SAVEAS, False)
+            MenuToolBar.EnableTool(wx.ID_SAVEAS, False)
+            self.FileMenu.Enable(wx.ID_CLOSE_ALL, False)
+
+    def RefreshRecentProjectsMenu(self):
+        try:
+            recent_projects = map(DecodeFileSystemPath,
+                                  self.GetConfigEntry("RecentProjects", []))
+        except:
+            recent_projects = []
+
+        while self.RecentProjectsMenu.GetMenuItemCount() > len(recent_projects):
+            item = self.RecentProjectsMenu.FindItemByPosition(0)
+            self.RecentProjectsMenu.RemoveItem(item)
+
+        self.FileMenu.Enable(ID_FILEMENURECENTPROJECTS, len(recent_projects) > 0)
+        for idx, projectpath in enumerate(recent_projects):
+            text = u'&%d: %s' % (idx + 1, projectpath)
+
+            if idx < self.RecentProjectsMenu.GetMenuItemCount():
+                item = self.RecentProjectsMenu.FindItemByPosition(idx)
+                id = item.GetId()
+                item.SetItemLabel(text)
+                self.Disconnect(id, id, wx.EVT_BUTTON._getEvtType())
+            else:
+                id = wx.NewId()
+                AppendMenu(self.RecentProjectsMenu, help='', id=id,
+                           kind=wx.ITEM_NORMAL, text=text)
+            self.Bind(wx.EVT_MENU, self.GenerateOpenRecentProjectFunction(projectpath), id=id)
+
+    def GenerateOpenRecentProjectFunction(self, projectpath):
+        def OpenRecentProject(event):
+            if self.CTR is not None and not self.CheckSaveBeforeClosing():
+                return
+
+            self.OpenProject(projectpath)
+        return OpenRecentProject
+
+    def GenerateMenuRecursive(self, items, menu):
+        for kind, infos in items:
+            if isinstance(kind, ListType):
+                text, id = infos
+                submenu = wx.Menu('')
+                self.GenerateMenuRecursive(kind, submenu)
+                menu.AppendMenu(id, text, submenu)
+            elif kind == wx.ITEM_SEPARATOR:
+                menu.AppendSeparator()
+            else:
+                text, id, help, callback = infos
+                AppendMenu(menu, help='', id=id, kind=kind, text=text)
+                if callback is not None:
+                    self.Bind(wx.EVT_MENU, callback, id=id)
+
+    def RefreshEditorToolBar(self):
+        IDEFrame.RefreshEditorToolBar(self)
+        self.AUIManager.GetPane("EditorToolBar").Position(2)
+        self.AUIManager.GetPane("StatusToolBar").Position(1)
+        self.AUIManager.Update()
+
+    def RefreshStatusToolBar(self):
+        StatusToolBar = self.Panes["StatusToolBar"]
+        StatusToolBar.ClearTools()
+
+        if self.CTR is not None:
+
+            for confnode_method in self.CTR.StatusMethods:
+                if "method" in confnode_method and confnode_method.get("shown",True):
+                    id = wx.NewId()
+                    StatusToolBar.AddSimpleTool(id,
+                        GetBitmap(confnode_method.get("bitmap", "Unknown")),
+                        confnode_method["tooltip"])
+                    self.Bind(wx.EVT_MENU, self.GetMenuCallBackFunction(confnode_method["method"]), id=id)
+
+            StatusToolBar.Realize()
+            self.AUIManager.GetPane("StatusToolBar").BestSize(StatusToolBar.GetBestSize()).Show()
+        else:
+            self.AUIManager.GetPane("StatusToolBar").Hide()
+        self.AUIManager.GetPane("EditorToolBar").Position(2)
+        self.AUIManager.GetPane("StatusToolBar").Position(1)
+        self.AUIManager.Update()
+
+    def RefreshEditMenu(self):
+        IDEFrame.RefreshEditMenu(self)
+        if self.FindFocus() == self.LogConsole:
+            self.EditMenu.Enable(wx.ID_COPY, True)
+            self.Panes["MenuToolBar"].EnableTool(wx.ID_COPY, True)
+
+        if self.CTR is not None:
+            selected = self.TabsOpened.GetSelection()
+            if selected >= 0:
+                panel = self.TabsOpened.GetPage(selected)
+            else:
+                panel = None
+            if panel != self.LastPanelSelected:
+                for i in xrange(self.EditMenuSize, self.EditMenu.GetMenuItemCount()):
+                    item = self.EditMenu.FindItemByPosition(self.EditMenuSize)
+                    if item is not None:
+                        if item.IsSeparator():
+                            self.EditMenu.RemoveItem(item)
+                        else:
+                            self.EditMenu.Delete(item.GetId())
+                self.LastPanelSelected = panel
+                if panel is not None:
+                    items = panel.GetConfNodeMenuItems()
+                else:
+                    items = []
+                if len(items) > 0:
+                    self.EditMenu.AppendSeparator()
+                    self.GenerateMenuRecursive(items, self.EditMenu)
+            if panel is not None:
+                panel.RefreshConfNodeMenu(self.EditMenu)
+        else:
+            for i in xrange(self.EditMenuSize, self.EditMenu.GetMenuItemCount()):
+                item = self.EditMenu.FindItemByPosition(i)
+                if item is not None:
+                    if item.IsSeparator():
+                        self.EditMenu.RemoveItem(item)
+                    else:
+                        self.EditMenu.Delete(item.GetId())
+            self.LastPanelSelected = None
+        self.MenuBar.UpdateMenus()
+
+    def RefreshAll(self):
+        self.RefreshStatusToolBar()
+
+    def GetMenuCallBackFunction(self, method):
+        """ Generate the callbackfunc for a given CTR method"""
+        def OnMenu(event):
+            # Disable button to prevent re-entrant call
+            event.GetEventObject().Disable()
+            # Call
+            getattr(self.CTR, method)()
+            # Re-enable button
+            event.GetEventObject().Enable()
+        return OnMenu
+
+    def GetConfigEntry(self, entry_name, default):
+        return cPickle.loads(str(self.Config.Read(entry_name, cPickle.dumps(default))))
+
+    def ResetConnectionStatusBar(self):
+        for field in xrange(self.ConnectionStatusBar.GetFieldsCount()):
+            self.ConnectionStatusBar.SetStatusText('', field)
+
+    def ResetView(self):
+        IDEFrame.ResetView(self)
+        self.ConfNodeInfos = {}
+        if self.CTR is not None:
+            self.CTR.CloseProject()
+        self.CTR = None
+        self.Log.flush()
+        if self.EnableDebug:
+            self.DebugVariablePanel.SetDataProducer(None)
+            self.ResetConnectionStatusBar()
+
+    def RefreshConfigRecentProjects(self, projectpath, err=False):
+        try:
+            recent_projects = map(DecodeFileSystemPath,
+                                  self.GetConfigEntry("RecentProjects", []))
+        except:
+            recent_projects = []
+        if projectpath in recent_projects:
+            recent_projects.remove(projectpath)
+        if not err:
+            recent_projects.insert(0, projectpath)
+        self.Config.Write("RecentProjects", cPickle.dumps(
+            map(EncodeFileSystemPath, recent_projects[:MAX_RECENT_PROJECTS])))
+        self.Config.Flush()
+
+    def ResetPerspective(self):
+        IDEFrame.ResetPerspective(self)
+        self.RefreshStatusToolBar()
+
+    def OnNewProjectMenu(self, event):
+        if self.CTR is not None and not self.CheckSaveBeforeClosing():
+            return
+
+        try:
+            defaultpath = DecodeFileSystemPath(self.Config.Read("lastopenedfolder"))
+        except:
+            defaultpath = os.path.expanduser("~")
+
+        dialog = wx.DirDialog(self , _("Choose a project"), defaultpath)
+        if dialog.ShowModal() == wx.ID_OK:
+            projectpath = dialog.GetPath()
+            self.Config.Write("lastopenedfolder",
+                              EncodeFileSystemPath(os.path.dirname(projectpath)))
+            self.Config.Flush()
+            self.ResetView()
+            ctr = ProjectController(self, self.Log)
+            result = ctr.NewProject(projectpath)
+            if not result:
+                self.CTR = ctr
+                self.Controler = self.CTR
+                self.LibraryPanel.SetController(self.Controler)
+                self.ProjectTree.Enable(True)
+                self.PouInstanceVariablesPanel.SetController(self.Controler)
+                self.RefreshConfigRecentProjects(projectpath)
+                if self.EnableDebug:
+                    self.DebugVariablePanel.SetDataProducer(self.CTR)
+                self._Refresh(PROJECTTREE, POUINSTANCEVARIABLESPANEL, LIBRARYTREE)
+            else:
+                self.ResetView()
+                self.ShowErrorMessage(result)
+            self.RefreshAll()
+            self._Refresh(TITLE, EDITORTOOLBAR, FILEMENU, EDITMENU)
+        dialog.Destroy()
+
+    def OnOpenProjectMenu(self, event):
+        if self.CTR is not None and not self.CheckSaveBeforeClosing():
+            return
+
+        try:
+            defaultpath = DecodeFileSystemPath(self.Config.Read("lastopenedfolder"))
+        except:
+            defaultpath = os.path.expanduser("~")
+
+        dialog = wx.DirDialog(self , _("Choose a project"), defaultpath, style=wx.DEFAULT_DIALOG_STYLE|
+                                                                               wx.RESIZE_BORDER)
+        if dialog.ShowModal() == wx.ID_OK:
+            self.OpenProject(dialog.GetPath())
+        dialog.Destroy()
+
+    def OpenProject(self, projectpath):
+        if os.path.isdir(projectpath):
+            self.Config.Write("lastopenedfolder",
+                              EncodeFileSystemPath(os.path.dirname(projectpath)))
+            self.Config.Flush()
+            self.ResetView()
+            self.CTR = ProjectController(self, self.Log)
+            self.Controler = self.CTR
+            result, err = self.CTR.LoadProject(projectpath)
+            if not result:
+                self.LibraryPanel.SetController(self.Controler)
+                self.ProjectTree.Enable(True)
+                self.PouInstanceVariablesPanel.SetController(self.Controler)
+                if self.EnableDebug:
+                    self.DebugVariablePanel.SetDataProducer(self.CTR)
+                self._Refresh(PROJECTTREE, POUINSTANCEVARIABLESPANEL, LIBRARYTREE)
+            else:
+                self.ResetView()
+                self.ShowErrorMessage(result)
+            self.RefreshAll()
+            self.SearchResultPanel.ResetSearchResults()
+        else:
+            self.ShowErrorMessage(_("\"%s\" folder is not a valid Beremiz project\n") % projectpath)
+            err = True
+        self.RefreshConfigRecentProjects(projectpath, err)
+        self._Refresh(TITLE, EDITORTOOLBAR, FILEMENU, EDITMENU)
+
+    def OnCloseProjectMenu(self, event):
+        if self.CTR is not None and not self.CheckSaveBeforeClosing():
+            return
+
+        self.ResetView()
+        self._Refresh(TITLE, EDITORTOOLBAR, FILEMENU, EDITMENU)
+        self.RefreshAll()
+
+    def OnSaveProjectMenu(self, event):
+        selected = self.TabsOpened.GetSelection()
+        if selected != -1:
+            window = self.TabsOpened.GetPage(selected)
+            window.Save()
+        if self.CTR is not None:
+            self.CTR.SaveProject()
+            self.RefreshAll()
+            self._Refresh(TITLE, FILEMENU, EDITMENU, PAGETITLES)
+
+    def OnSaveProjectAsMenu(self, event):
+        selected = self.TabsOpened.GetSelection()
+        if selected != -1:
+            window = self.TabsOpened.GetPage(selected)
+            window.SaveAs()
+        if self.CTR is not None:
+            self.CTR.SaveProjectAs()
+            self.RefreshAll()
+            self.RefreshConfigRecentProjects(self.CTR.ProjectPath)
+            self._Refresh(TITLE, FILEMENU, EDITMENU, PAGETITLES)
+
+    def OnQuitMenu(self, event):
+        self.Close()
+
+    def OnAboutMenu(self, event):
+        info = version.GetAboutDialogInfo()        
+        ShowAboutDialog(self, info)
+
+    def OnProjectTreeItemBeginEdit(self, event):
+        selected = event.GetItem()
+        if self.ProjectTree.GetPyData(selected)["type"] == ITEM_CONFNODE:
+            event.Veto()
+        else:
+            IDEFrame.OnProjectTreeItemBeginEdit(self, event)
+
+    def OnProjectTreeRightUp(self, event):
+        item = event.GetItem()
+        item_infos = self.ProjectTree.GetPyData(item)
+
+        if item_infos["type"] == ITEM_CONFNODE:
+            confnode_menu = wx.Menu(title='')
+
+            confnode = item_infos["confnode"]
+            if confnode is not None:
+                menu_items = confnode.GetContextualMenuItems()
+                if menu_items is not None:
+                    for text, help, callback in menu_items:
+                        new_id = wx.NewId()
+                        confnode_menu.Append(help=help, id=new_id, kind=wx.ITEM_NORMAL, text=text)
+                        self.Bind(wx.EVT_MENU, callback, id=new_id)
+                else:
+                    for name, XSDClass, help in confnode.CTNChildrenTypes:
+                        new_id = wx.NewId()
+                        confnode_menu.Append(help=help, id=new_id, kind=wx.ITEM_NORMAL, text=_("Add") + " " + name)
+                        self.Bind(wx.EVT_MENU, self.GetAddConfNodeFunction(name, confnode), id=new_id)
+
+            new_id = wx.NewId()
+            AppendMenu(confnode_menu, help='', id=new_id, kind=wx.ITEM_NORMAL, text=_("Delete"))
+            self.Bind(wx.EVT_MENU, self.GetDeleteMenuFunction(confnode), id=new_id)
+
+            self.PopupMenu(confnode_menu)
+            confnode_menu.Destroy()
+
+            event.Skip()
+        elif item_infos["type"] == ITEM_RESOURCE:
+            # prevent last resource to be delted
+            parent = self.ProjectTree.GetItemParent(item)
+            parent_name = self.ProjectTree.GetItemText(parent)
+            if parent_name == _("Resources"):
+                IDEFrame.OnProjectTreeRightUp(self, event)
+        else:
+            IDEFrame.OnProjectTreeRightUp(self, event)
+
+    def OnProjectTreeItemActivated(self, event):
+        selected = event.GetItem()
+        name = self.ProjectTree.GetItemText(selected)
+        item_infos = self.ProjectTree.GetPyData(selected)
+        if item_infos["type"] == ITEM_CONFNODE:
+            item_infos["confnode"]._OpenView()
+            event.Skip()
+        elif item_infos["type"] == ITEM_PROJECT:
+            self.CTR._OpenView()
+        else:
+            IDEFrame.OnProjectTreeItemActivated(self, event)
+
+    def ProjectTreeItemSelect(self, select_item):
+        if select_item is not None and select_item.IsOk():
+            name = self.ProjectTree.GetItemText(select_item)
+            item_infos = self.ProjectTree.GetPyData(select_item)
+            if item_infos["type"] == ITEM_CONFNODE:
+                item_infos["confnode"]._OpenView(onlyopened=True)
+            elif item_infos["type"] == ITEM_PROJECT:
+                self.CTR._OpenView(onlyopened=True)
+            else:
+                IDEFrame.ProjectTreeItemSelect(self, select_item)
+
+    def SelectProjectTreeItem(self, tagname):
+        if self.ProjectTree is not None:
+            root = self.ProjectTree.GetRootItem()
+            if root.IsOk():
+                words = tagname.split("::")
+                if len(words) == 1:
+                    if tagname == "Project":
+                        self.SelectedItem = root
+                        self.ProjectTree.SelectItem(root)
+                        self.ResetSelectedItem()
+                    else:
+                        return self.RecursiveProjectTreeItemSelection(root,
+                              [(word, ITEM_CONFNODE) for word in tagname.split(".")])
+                elif words[0] == "R":
+                    return self.RecursiveProjectTreeItemSelection(root, [(words[2], ITEM_RESOURCE)])
+                elif not os.path.exists(words[0]):
+                    IDEFrame.SelectProjectTreeItem(self, tagname)
+
+    def GetAddConfNodeFunction(self, name, confnode=None):
+        def AddConfNodeMenuFunction(event):
+            wx.CallAfter(self.AddConfNode, name, confnode)
+        return AddConfNodeMenuFunction
+
+    def GetDeleteMenuFunction(self, confnode):
+        def DeleteMenuFunction(event):
+            wx.CallAfter(self.DeleteConfNode, confnode)
+        return DeleteMenuFunction
+
+    def AddConfNode(self, ConfNodeType, confnode=None):
+        if self.CTR.CheckProjectPathPerm():
+            ConfNodeName = "%s_0" % ConfNodeType
+            if confnode is not None:
+                confnode.CTNAddChild(ConfNodeName, ConfNodeType)
+            else:
+                self.CTR.CTNAddChild(ConfNodeName, ConfNodeType)
+            self._Refresh(TITLE, FILEMENU, PROJECTTREE)
+
+    def DeleteConfNode(self, confnode):
+        if self.CTR.CheckProjectPathPerm():
+            dialog = wx.MessageDialog(self,
+                _("Really delete node '%s'?") % confnode.CTNName(),
+                _("Remove %s node") % confnode.CTNType,
+                wx.YES_NO|wx.NO_DEFAULT)
+            if dialog.ShowModal() == wx.ID_YES:
+                confnode.CTNRemove()
+                del confnode
+                self._Refresh(TITLE, FILEMENU, PROJECTTREE)
+            dialog.Destroy()
+
+#-------------------------------------------------------------------------------
+#                        Highlights showing functions
+#-------------------------------------------------------------------------------
+
+    def ShowHighlight(self, infos, start, end, highlight_type):
+        config_name = self.Controler.GetProjectMainConfigurationName()
+        if config_name is not None and infos[0] == self.Controler.ComputeConfigurationName(config_name):
+            self.CTR._OpenView()
+            selected = self.TabsOpened.GetSelection()
+            if selected != -1:
+                viewer = self.TabsOpened.GetPage(selected)
+                viewer.AddHighlight(infos[1:], start, end, highlight_type)
+        else:
+            IDEFrame.ShowHighlight(self, infos, start, end, highlight_type)
+
+#-------------------------------------------------------------------------------
+#                               Exception Handler
+#-------------------------------------------------------------------------------
+import threading, traceback
+
+Max_Traceback_List_Size = 20
+
+def Display_Exception_Dialog(e_type, e_value, e_tb, bug_report_path):
+    trcbck_lst = []
+    for i,line in enumerate(traceback.extract_tb(e_tb)):
+        trcbck = " " + str(i+1) + ". "
+        if line[0].find(os.getcwd()) == -1:
+            trcbck += "file : " + str(line[0]) + ",   "
+        else:
+            trcbck += "file : " + str(line[0][len(os.getcwd()):]) + ",   "
+        trcbck += "line : " + str(line[1]) + ",   " + "function : " + str(line[2])
+        trcbck_lst.append(trcbck)
+
+    # Allow clicking....
+    cap = wx.Window_GetCapture()
+    if cap:
+        cap.ReleaseMouse()
+
+    dlg = wx.SingleChoiceDialog(None,
+        _("""
+An unhandled exception (bug) occured. Bug report saved at :
+(%s)
+
+Please be kind enough to send this file to:
+beremiz-devel@lists.sourceforge.net
+
+You should now restart program.
+
+Traceback:
+""") % bug_report_path +
+        repr(e_type) + " : " + repr(e_value),
+        _("Error"),
+        trcbck_lst)
+    try:
+        res = (dlg.ShowModal() == wx.ID_OK)
+    finally:
+        dlg.Destroy()
+
+    return res
+
+def get_last_traceback(tb):
+    while tb.tb_next:
+        tb = tb.tb_next
+    return tb
+
+
+def format_namespace(d, indent='    '):
+    return '\n'.join(['%s%s: %s' % (indent, k, repr(v)[:10000]) for k, v in d.iteritems()])
+
+
+ignored_exceptions = [] # a problem with a line in a module is only reported once per session
+
+def AddExceptHook(path, app_version='[No version]'):#, ignored_exceptions=[]):
+
+    def save_bug_report(e_type, e_value, e_traceback, bug_report_path,date):
+        info = {
+            'app-title': wx.GetApp().GetAppName(),  # app_title
+            'app-version': app_version,
+            'wx-version': wx.VERSION_STRING,
+            'wx-platform': wx.Platform,
+            'python-version': platform.python_version(),  # sys.version.split()[0],
+            'platform': platform.platform(),
+            'e-type': e_type,
+            'e-value': e_value,
+            'date': date,
+            'cwd': os.getcwd(),
+        }
+        if e_traceback:
+            info['traceback'] = ''.join(traceback.format_tb(e_traceback)) + '%s: %s' % (e_type, e_value)
+            last_tb = get_last_traceback(e_traceback)
+            exception_locals = last_tb.tb_frame.f_locals  # the locals at the level of the stack trace where the exception actually occurred
+            info['locals'] = format_namespace(exception_locals)
+            if 'self' in exception_locals:
+                try:
+                    info['self'] = format_namespace(exception_locals['self'].__dict__)
+                except:
+                    pass
+        if not os.path.exists(path):
+            os.mkdir(path)
+        output = open(bug_report_path, 'w')
+        lst = info.keys()
+        lst.sort()
+        for a in lst:
+            output.write(a + ":\n" + str(info[a]) + "\n\n")
+        output.close()
+
+    def handle_exception(e_type, e_value, e_traceback):
+        traceback.print_exception(e_type, e_value, e_traceback) # this is very helpful when there's an exception in the rest of this func
+        last_tb = get_last_traceback(e_traceback)
+        ex = (last_tb.tb_frame.f_code.co_filename, last_tb.tb_frame.f_lineno)
+        if ex not in ignored_exceptions:
+            ignored_exceptions.append(ex)
+            date = time.ctime()
+            bug_report_path = path + os.sep + "bug_report_" + date.replace(':', '-').replace(' ', '_') + ".txt"
+            save_bug_report(e_type, e_value, e_traceback, bug_report_path, date)
+            Display_Exception_Dialog(e_type, e_value, e_traceback, bug_report_path)
+    #sys.excepthook = lambda *args: wx.CallAfter(handle_exception, *args)
+    sys.excepthook = handle_exception
+
+    init_old = threading.Thread.__init__
+    def init(self, *args, **kwargs):
+        init_old(self, *args, **kwargs)
+        run_old = self.run
+        def run_with_except_hook(*args, **kw):
+            try:
+                run_old(*args, **kw)
+            except (KeyboardInterrupt, SystemExit):
+                raise
+            except:
+                sys.excepthook(*sys.exc_info())
+        self.run = run_with_except_hook
+    threading.Thread.__init__ = init