"Fossies" - the Fresh Open Source Software Archive

Member "relax-5.0.0/gui/controller.py" (2 Dec 2019, 41764 Bytes) of package /linux/privat/relax-5.0.0.src.tar.bz2:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) Python source code syntax highlighting (style: standard) with prefixed line numbers. Alternatively you can here view or download the uninterpreted source code file. For more information about "controller.py" see the Fossies "Dox" file reference documentation and the latest Fossies "Diffs" side-by-side code changes report: 4.1.3_vs_5.0.0.

    1 ###############################################################################
    2 #                                                                             #
    3 # Copyright (C) 2010 Michael Bieri                                            #
    4 # Copyright (C) 2010-2014,2019 Edward d'Auvergne                              #
    5 #                                                                             #
    6 # This file is part of the program relax (http://www.nmr-relax.com).          #
    7 #                                                                             #
    8 # This program is free software: you can redistribute it and/or modify        #
    9 # it under the terms of the GNU General Public License as published by        #
   10 # the Free Software Foundation, either version 3 of the License, or           #
   11 # (at your option) any later version.                                         #
   12 #                                                                             #
   13 # This program is distributed in the hope that it will be useful,             #
   14 # but WITHOUT ANY WARRANTY; without even the implied warranty of              #
   15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the               #
   16 # GNU General Public License for more details.                                #
   17 #                                                                             #
   18 # You should have received a copy of the GNU General Public License           #
   19 # along with this program.  If not, see <http://www.gnu.org/licenses/>.       #
   20 #                                                                             #
   21 ###############################################################################
   22 
   23 # Module docstring.
   24 """Log window of relax GUI controlling all calculations."""
   25 
   26 # Python module imports.
   27 import sys
   28 import wx
   29 import wx.stc
   30 
   31 # relax module imports.
   32 import dep_check
   33 from graphics import IMAGE_PATH, fetch_icon
   34 from gui.components.menu import build_menu_item
   35 from gui.fonts import font
   36 from gui.icons import Relax_icons
   37 from gui.misc import add_border, bitmap_setup
   38 from gui.string_conv import str_to_gui
   39 from info import Info_box
   40 from lib.compat import Queue
   41 from lib.io import SplitIO
   42 from pipe_control.pipes import cdp_name
   43 from status import Status; status = Status()
   44 
   45 
   46 # IDs for the menu entries.
   47 MENU_ID_FIND = wx.NewId()
   48 MENU_ID_COPY = wx.NewId()
   49 MENU_ID_SELECT_ALL = wx.NewId()
   50 MENU_ID_ZOOM_IN = wx.NewId()
   51 MENU_ID_ZOOM_OUT = wx.NewId()
   52 MENU_ID_ZOOM_ORIG = wx.NewId()
   53 MENU_ID_GOTO_START = wx.NewId()
   54 MENU_ID_GOTO_END = wx.NewId()
   55 
   56 
   57 
   58 class Controller(wx.Frame):
   59     """The relax controller window."""
   60 
   61     def __init__(self, gui):
   62         """Set up the relax controller frame.
   63 
   64         @param gui:     The GUI object.
   65         @type gui:      wx.Frame instance
   66         """
   67 
   68         # Store the args.
   69         self.gui = gui
   70 
   71         # Initialise the base class.
   72         super(Controller, self).__init__(self.gui, -1, style=wx.DEFAULT_FRAME_STYLE)
   73 
   74         # Some default values.
   75         self.size_x = 800
   76         self.size_y = 700
   77         self.border = 5
   78         self.spacer = 10
   79 
   80         # Set up the frame.
   81         sizer = self.setup_frame()
   82 
   83         # Add the relax logo.
   84         self.add_relax_logo(sizer)
   85 
   86         # Spacing.
   87         sizer.AddSpacer(20)
   88 
   89         # Add the current analysis info.
   90         self.name = self.add_text(self.main_panel, sizer, "Current GUI analysis:")
   91 
   92         # Add the current data pipe info.
   93         self.cdp = self.add_text(self.main_panel, sizer, "Current data pipe:")
   94 
   95         # Create the relaxation curve-fitting specific panel.
   96         self.create_rx(sizer)
   97 
   98         # Create the model-free specific panel.
   99         self.create_mf(sizer)
  100 
  101         # Add the main execution gauge.
  102         self.main_gauge = self.add_gauge(self.main_panel, sizer, "Execution progress:", tooltip="This gauge will pulse while relax is executing an auto-analysis (when the execution lock is turned on) and will be set to 100% once the analysis is complete.")
  103 
  104         # Initialise a queue for log messages.
  105         self.log_queue = Queue()
  106 
  107         # Add the log panel.
  108         self.log_panel = LogCtrl(self.main_panel, self, log_queue=self.log_queue, id=-1)
  109         sizer.Add(self.log_panel, 1, wx.EXPAND|wx.ALL, 0)
  110 
  111         # IO redirection for STDOUT (with splitting if logging or teeing modes are set).
  112         out = Redirect_text(self.log_panel, self.log_queue, orig_io=sys.stdout, stream=0)
  113         if sys.stdout == sys.__stdout__ or status.relax_mode in ['test suite', 'system tests', 'unit tests', 'GUI tests']:
  114             sys.stdout = out
  115         else:
  116             split_stdout = SplitIO()
  117             split_stdout.split(sys.stdout, out)
  118             sys.stdout = split_stdout
  119 
  120         # IO redirection for STDERR (with splitting if logging or teeing modes are set).
  121         err = Redirect_text(self.log_panel, self.log_queue, orig_io=sys.stderr, stream=1)
  122         if sys.stderr == sys.__stderr__ or status.relax_mode in ['test suite', 'system tests', 'unit tests', 'GUI tests']:
  123             sys.stderr = err
  124         else:
  125             split_stderr = SplitIO()
  126             split_stderr.split(sys.stderr, err)
  127             sys.stderr = split_stderr
  128 
  129         # Initial update of the controller.
  130         self.update_controller()
  131 
  132         # Create a timer for updating the controller elements.
  133         self.timer = wx.Timer(self)
  134         self.Bind(wx.EVT_TIMER, self.handler_timer, self.timer)
  135 
  136         # The relax intro printout, to mimic the prompt/script interface.
  137         if not status.test_mode:
  138             info = Info_box()
  139             sys.stdout.write(info.intro_text())
  140             sys.stdout.write("\n")
  141             sys.stdout.flush()
  142 
  143         # Set the focus on the log control.
  144         self.log_panel.SetFocus()
  145 
  146         # Register functions with the observer objects.
  147         status.observers.pipe_alteration.register('controller', self.update_controller, method_name='update_controller')
  148         status.observers.auto_analyses.register('controller', self.update_controller, method_name='update_controller')
  149         status.observers.gui_analysis.register('controller', self.update_controller, method_name='update_controller')
  150         status.observers.exec_lock.register('controller', self.update_gauge, method_name='update_gauge')
  151 
  152 
  153     def add_gauge(self, parent, sizer, desc, tooltip=None):
  154         """Add a gauge to the sizer and return it.
  155 
  156         @param parent:      The parent GUI element.
  157         @type parent:       wx object
  158         @param sizer:       The sizer element to pack the element into.
  159         @type sizer:        wx.Sizer instance
  160         @param desc:        The description to display.
  161         @type desc:         str
  162         @keyword tooltip:   The tooltip which appears on hovering over the text and the gauge.
  163         @type tooltip:      str
  164         @return:            The gauge element.
  165         @rtype:             wx.Gauge instance
  166         """
  167 
  168         # Create a horizontal layout.
  169         sub_sizer = wx.BoxSizer(wx.HORIZONTAL)
  170 
  171         # The intro.
  172         text = wx.StaticText(parent, -1, desc, style=wx.ALIGN_LEFT)
  173         text.SetFont(font.normal)
  174         sub_sizer.Add(text, 1, wx.ALIGN_CENTER_VERTICAL, 0)
  175 
  176         # The gauge.
  177         gauge = wx.Gauge(parent, id=-1, range=100, style=wx.GA_SMOOTH)
  178         gauge.SetSize((-1, 20))
  179         sub_sizer.Add(gauge, 3, wx.EXPAND|wx.ALL, 0)
  180 
  181         # Add the sizer.
  182         sizer.Add(sub_sizer, 0, wx.ALL|wx.EXPAND, 0)
  183 
  184         # Spacing.
  185         sizer.AddSpacer(self.spacer)
  186 
  187         # Tooltip.
  188         if tooltip:
  189             text.SetToolTip(wx.ToolTip(tooltip))
  190             gauge.SetToolTip(wx.ToolTip(tooltip))
  191 
  192         # Return the gauge.
  193         return gauge
  194 
  195 
  196     def add_relax_logo(self, sizer):
  197         """Add the relax logo to the sizer.
  198 
  199         @param sizer:   The sizer element to pack the relax logo into.
  200         @type sizer:    wx.Sizer instance
  201         """
  202 
  203         # The logo.
  204         logo = wx.StaticBitmap(self.main_panel, -1, bitmap_setup(IMAGE_PATH+'relax.gif'))
  205 
  206         # Add the relax logo.
  207         sizer.Add(logo, 0, wx.TOP|wx.ALIGN_CENTER_HORIZONTAL, 0)
  208 
  209         # Spacing.
  210         sizer.AddSpacer(self.spacer)
  211 
  212 
  213     def add_text(self, parent, sizer, desc, tooltip=None):
  214         """Add the current data pipe element.
  215 
  216         @param parent:      The parent GUI element.
  217         @type parent:       wx object
  218         @param sizer:       The sizer element to pack the element into.
  219         @type sizer:        wx.Sizer instance
  220         @param desc:        The description to display.
  221         @type desc:         str
  222         @keyword tooltip:   The tooltip which appears on hovering over the text and field.
  223         @type tooltip:      str
  224         @return:            The text control.
  225         @rtype:             wx.TextCtrl instance
  226         """
  227 
  228         # Create a horizontal layout.
  229         sub_sizer = wx.BoxSizer(wx.HORIZONTAL)
  230 
  231         # The intro.
  232         text = wx.StaticText(parent, -1, desc, style=wx.ALIGN_LEFT)
  233         text.SetFont(font.normal)
  234         sub_sizer.Add(text, 1, wx.ALIGN_CENTER_VERTICAL, 0)
  235 
  236         # The cdp name.
  237         field = wx.TextCtrl(parent, -1, '', style=wx.ALIGN_LEFT)
  238         field.SetEditable(False)
  239         field.SetFont(font.normal)
  240         colour = self.main_panel.GetBackgroundColour()
  241         field.SetOwnBackgroundColour(colour)
  242         sub_sizer.Add(field, 3, wx.ALIGN_CENTER_VERTICAL, 0)
  243 
  244         # Add the sizer.
  245         sizer.Add(sub_sizer, 0, wx.ALL|wx.EXPAND, 0)
  246 
  247         # Spacing.
  248         sizer.AddSpacer(self.spacer)
  249 
  250         # Tooltip.
  251         if tooltip:
  252             text.SetToolTip(wx.ToolTip(tooltip))
  253             field.SetToolTip(wx.ToolTip(tooltip))
  254 
  255         # Handle key events.
  256         field.Bind(wx.EVT_KEY_DOWN, self.handler_key_down)
  257 
  258         # Return the control.
  259         return field
  260 
  261 
  262     def analysis_key(self):
  263         """Return the key for the current analysis' status object.
  264 
  265         @return:    The current analysis' status object key.
  266         @rtype:     str or None
  267         """
  268 
  269         # Get the data container.
  270         data = self.gui.analysis.current_data()
  271         if data == None:
  272             return
  273 
  274         # Return the pipe bundle, if it exists, as the key.
  275         if hasattr(data, 'pipe_bundle'):
  276             return data.pipe_bundle
  277 
  278 
  279     def create_mf(self, sizer):
  280         """Create the model-free specific panel.
  281 
  282         @param sizer:   The sizer element to pack the element into.
  283         @type sizer:    wx.Sizer instance
  284         """
  285 
  286         # Create a panel.
  287         self.panel_mf = wx.Panel(self.main_panel, -1)
  288         sizer.Add(self.panel_mf, 0, wx.ALL|wx.EXPAND, 0)
  289 
  290         # The panel sizer.
  291         panel_sizer = wx.BoxSizer(wx.VERTICAL)
  292         self.panel_mf.SetSizer(panel_sizer)
  293 
  294         # Add the global model.
  295         self.global_model_mf = self.add_text(self.panel_mf, panel_sizer, "Global model:", tooltip="This shows the global diffusion model of the dauvergne_protocol auto-analysis currently being optimised.  It will be one of 'local_tm', 'sphere', 'prolate', 'oblate', 'ellipsoid' or 'final'.")
  296 
  297         # Progress gauge.
  298         self.progress_gauge_mf = self.add_gauge(self.panel_mf, panel_sizer, "Incremental progress:", tooltip="This shows the global iteration round of the dauvergne_protocol auto-analysis.  Optimisation of the global model may require between 5 to 15 iterations.  The maximum number of iterations should not be reached.  Once the global diffusion model has converged, this gauge will be set to 100%")
  299 
  300         # MC sim gauge.
  301         self.mc_gauge_mf = self.add_gauge(self.panel_mf, panel_sizer, "Monte Carlo simulations:", tooltip="The Monte Carlo simulation number.  Simulations are only performed at the very end of the analysis in the 'final' global model.")
  302 
  303 
  304     def create_rx(self, sizer):
  305         """Create the relaxation curve-fitting specific panel.
  306 
  307         @param sizer:   The sizer element to pack the element into.
  308         @type sizer:    wx.Sizer instance
  309         """
  310 
  311         # Create a panel.
  312         self.panel_rx = wx.Panel(self.main_panel, -1)
  313         sizer.Add(self.panel_rx, 0, wx.ALL|wx.EXPAND, 0)
  314 
  315         # The panel sizer.
  316         panel_sizer = wx.BoxSizer(wx.VERTICAL)
  317         self.panel_rx.SetSizer(panel_sizer)
  318 
  319         # MC sim gauge.
  320         self.mc_gauge_rx = self.add_gauge(self.panel_rx, panel_sizer, "Monte Carlo simulations:", tooltip="The Monte Carlo simulation number.")
  321 
  322 
  323     def handler_close(self, event):
  324         """Event handler for the close window action.
  325 
  326         @param event:   The wx event.
  327         @type event:    wx event
  328         """
  329 
  330         # The test suite is running, so disable closing.
  331         if self.gui.test_suite_flag:
  332             return
  333 
  334         # Close the window.
  335         self.Hide()
  336 
  337 
  338     def handler_key_down(self, event=None):
  339         """Event handler for key strokes.
  340 
  341         @keyword event: The wx event.
  342         @type event:    wx event
  343         """
  344 
  345         # Use ESC to close the window.
  346         if event.GetKeyCode() == wx.WXK_ESCAPE:
  347             self.handler_close(event)
  348 
  349 
  350     def handler_timer(self, event):
  351         """Event handler for the timer.
  352 
  353         @param event:   The wx event.
  354         @type event:    wx event
  355         """
  356 
  357         # Update the controller log.
  358         wx.CallAfter(self.log_panel.write)
  359 
  360         # Pulse.
  361         wx.CallAfter(self.main_gauge.Pulse)
  362 
  363         # Stop the timer and update the gauge.
  364         if not status.exec_lock.locked() and self.timer.IsRunning():
  365             self.timer.Stop()
  366             self.update_gauge()
  367 
  368 
  369     def reset(self):
  370         """Reset the relax controller to its initial state."""
  371 
  372         # Stop the timer.
  373         if self.timer.IsRunning():
  374             self.timer.Stop()
  375 
  376         # Reset the Rx gauges.
  377         if hasattr(self, 'mc_gauge_rx'):
  378             wx.CallAfter(self.mc_gauge_rx.SetValue, 0)
  379 
  380         # Reset the model-free gauges.
  381         if hasattr(self, 'mc_gauge_mf'):
  382             wx.CallAfter(self.mc_gauge_mf.SetValue, 0)
  383         if hasattr(self, 'progress_gauge_mf'):
  384             wx.CallAfter(self.progress_gauge_mf.SetValue, 0)
  385 
  386         # Reset the main gauge.
  387         wx.CallAfter(self.main_gauge.SetValue, 0)
  388 
  389 
  390     def setup_frame(self):
  391         """Set up the relax controller frame.
  392         @return:    The sizer object.
  393         @rtype:     wx.Sizer instance
  394         """
  395 
  396         # Set the frame title.
  397         self.SetTitle("The relax controller")
  398 
  399         # Set up the window icon.
  400         self.SetIcons(Relax_icons())
  401 
  402        # Place all elements within a panel (to remove the dark grey in MS Windows).
  403         self.main_panel = wx.Panel(self, -1)
  404 
  405         # Use a grid sizer for packing the elements.
  406         main_sizer = wx.BoxSizer(wx.VERTICAL)
  407         self.main_panel.SetSizer(main_sizer)
  408 
  409         # Build the central sizer, with borders.
  410         sizer = add_border(main_sizer, border=self.border, packing=wx.VERTICAL)
  411 
  412         # Close the window cleanly (hide so it can be reopened).
  413         self.Bind(wx.EVT_CLOSE, self.handler_close)
  414 
  415         # Set the default size of the controller.
  416         self.SetSize((self.size_x, self.size_y))
  417 
  418         # Centre the frame.
  419         self.Centre()
  420 
  421         # Return the central sizer.
  422         return sizer
  423 
  424 
  425     def update_controller(self):
  426         """Update the relax controller."""
  427 
  428         # Set the current data pipe info.
  429         pipe = cdp_name()
  430         if pipe == None:
  431             pipe = ''
  432         wx.CallAfter(self.cdp.SetValue, str_to_gui(pipe))
  433 
  434         # Set the current GUI analysis info.
  435         name = self.gui.analysis.current_analysis_name()
  436         if name == None:
  437             name = ''
  438         wx.CallAfter(self.name.SetValue, str_to_gui(name))
  439 
  440         # The analysis type.
  441         type = self.gui.analysis.current_analysis_type()
  442 
  443         # Rx fitting auto-analysis.
  444         if type in ['R1', 'R2']:
  445             if status.show_gui:
  446                 wx.CallAfter(self.panel_rx.Show)
  447             wx.CallAfter(self.update_rx)
  448         else:
  449             if status.show_gui:
  450                 wx.CallAfter(self.panel_rx.Hide)
  451 
  452         # Model-free auto-analysis.
  453         if type == 'model-free':
  454             if status.show_gui:
  455                 wx.CallAfter(self.panel_mf.Show)
  456             wx.CallAfter(self.update_mf)
  457         else:
  458             if status.show_gui:
  459                 wx.CallAfter(self.panel_mf.Hide)
  460 
  461         # Update the main gauge.
  462         wx.CallAfter(self.update_gauge)
  463 
  464         # Re-layout the window.
  465         wx.CallAfter(self.main_panel.Layout)
  466 
  467 
  468     def update_gauge(self):
  469         """Update the main execution gauge."""
  470 
  471         # Pulse during execution.
  472         if status.exec_lock.locked():
  473             # Start the timer.
  474             if not self.timer.IsRunning():
  475                 wx.CallAfter(self.timer.Start, 100)
  476 
  477             # Finish.
  478             return
  479 
  480         # Finished.
  481         key = self.analysis_key()
  482         if key and key in status.auto_analysis and status.auto_analysis[key].fin:
  483             # Stop the timer.
  484             if self.timer.IsRunning():
  485                 self.timer.Stop()
  486 
  487             # Fill the Rx gauges.
  488             if hasattr(self, 'mc_gauge_rx'):
  489                 wx.CallAfter(self.mc_gauge_rx.SetValue, 100)
  490 
  491             # Fill the model-free gauges.
  492             if hasattr(self, 'mc_gauge_mf'):
  493                 wx.CallAfter(self.mc_gauge_mf.SetValue, 100)
  494             if hasattr(self, 'progress_gauge_mf'):
  495                 wx.CallAfter(self.progress_gauge_mf.SetValue, 100)
  496 
  497             # Fill the main gauge.
  498             wx.CallAfter(self.main_gauge.SetValue, 100)
  499 
  500         # Gauge is in the initial state, so no need to reset.
  501         if not self.main_gauge.GetValue():
  502             return
  503 
  504         # No key, so reset.
  505         if not key or not key in status.auto_analysis:
  506             wx.CallAfter(self.main_gauge.SetValue, 0)
  507 
  508         # Key present, but analysis not started.
  509         if key and key in status.auto_analysis and not status.auto_analysis[key].fin:
  510             # Fill the Rx gauges.
  511             if hasattr(self, 'mc_gauge_rx'):
  512                 wx.CallAfter(self.mc_gauge_rx.SetValue, 0)
  513 
  514             # Fill the model-free gauges.
  515             if hasattr(self, 'mc_gauge_mf'):
  516                 wx.CallAfter(self.mc_gauge_mf.SetValue, 0)
  517             if hasattr(self, 'progress_gauge_mf'):
  518                 wx.CallAfter(self.progress_gauge_mf.SetValue, 0)
  519 
  520             # Fill the main gauge.
  521             wx.CallAfter(self.main_gauge.SetValue, 0)
  522 
  523 
  524     def update_mf(self):
  525         """Update the model-free specific elements."""
  526 
  527         # The analysis key.
  528         key = self.analysis_key()
  529         if not key:
  530             return
  531 
  532         # Loaded a finished state, so fill all gauges and return.
  533         elif not key in status.auto_analysis and cdp_name() == 'final':
  534             wx.CallAfter(self.mc_gauge_mf.SetValue, 100)
  535             wx.CallAfter(self.progress_gauge_mf.SetValue, 100)
  536             wx.CallAfter(self.main_gauge.SetValue, 100)
  537             return
  538 
  539         # Nothing to do.
  540         if not key in status.auto_analysis:
  541             wx.CallAfter(self.mc_gauge_mf.SetValue, 0)
  542             wx.CallAfter(self.progress_gauge_mf.SetValue, 0)
  543             wx.CallAfter(self.main_gauge.SetValue, 0)
  544             return
  545 
  546         # Set the diffusion model.
  547         wx.CallAfter(self.global_model_mf.SetValue, str_to_gui(status.auto_analysis[key].diff_model))
  548 
  549         # Update the progress gauge for the local tm model.
  550         if status.auto_analysis[key].diff_model == 'local_tm':
  551             if status.auto_analysis[key].current_model:
  552                 # Current model.
  553                 no = int(status.auto_analysis[key].current_model[2:])
  554 
  555                 # Total selected models.
  556                 total_models = len(status.auto_analysis[key].local_tm_models)
  557 
  558                 # Update the progress bar.
  559                 percent = int(100 * no / float(total_models))
  560                 wx.CallAfter(self.progress_gauge_mf.SetValue, percent)
  561 
  562         # Sphere to ellipsoid Models.
  563         elif status.auto_analysis[key].diff_model in ['sphere', 'prolate', 'oblate', 'ellipsoid']:
  564             # Check that the round has been set.
  565             if status.auto_analysis[key].round == None:
  566                 wx.CallAfter(self.progress_gauge_mf.SetValue, 0)
  567             else:
  568                 # The round as a percentage.
  569                 percent = int(100 * (status.auto_analysis[key].round + 1) / (status.auto_analysis[key].max_iter + 1))
  570 
  571                 # Update the progress bar.
  572                 wx.CallAfter(self.progress_gauge_mf.SetValue, percent)
  573 
  574         # Monte Carlo simulations.
  575         if status.auto_analysis[key].mc_number:
  576             # The simulation number as a percentage.
  577             percent = int(100 * (status.auto_analysis[key].mc_number + 1) / cdp.sim_number)
  578 
  579             # Update the progress bar.
  580             wx.CallAfter(self.mc_gauge_mf.SetValue, percent)
  581 
  582 
  583     def update_rx(self):
  584         """Update the Rx specific elements."""
  585 
  586         # The analysis key.
  587         key = self.analysis_key()
  588         if not key:
  589             return
  590 
  591         # Nothing to do.
  592         if not key in status.auto_analysis:
  593             wx.CallAfter(self.mc_gauge_rx.SetValue, 0)
  594             wx.CallAfter(self.main_gauge.SetValue, 0)
  595             return
  596 
  597         # Monte Carlo simulations.
  598         if status.auto_analysis[key].mc_number:
  599             # The simulation number as a percentage.
  600             percent = int(100 * (status.auto_analysis[key].mc_number + 1) / cdp.sim_number)
  601 
  602             # Update the progress bar.
  603             wx.CallAfter(self.mc_gauge_rx.SetValue, percent)
  604 
  605 
  606 
  607 class LogCtrl(wx.stc.StyledTextCtrl):
  608     """A special control designed to display relax output messages."""
  609 
  610     def __init__(self, parent, controller, log_queue=None, id=wx.ID_ANY, pos=wx.DefaultPosition, size=wx.DefaultSize, style=wx.BORDER_SUNKEN, name=wx.stc.STCNameStr):
  611         """Set up the log control.
  612 
  613         @param parent:          The parent wx window object.
  614         @type parent:           Window
  615         @param controller:      The controller window.
  616         @type controller:       wx.Frame instance
  617         @keyword log_queue:     The queue of log messages.
  618         @type log_queue:        Queue.Queue instance
  619         @keyword id:            The wx ID.
  620         @type id:               int
  621         @keyword pos:           The window position.
  622         @type pos:              Point
  623         @keyword size:          The window size.
  624         @type size:             Size
  625         @keyword style:         The StyledTextCtrl to apply.
  626         @type style:            long
  627         @keyword name:          The window name.
  628         @type name:             str
  629         """
  630 
  631         # Store the args.
  632         self.controller = controller
  633         self.log_queue = log_queue
  634 
  635         # Initialise the base class.
  636         super(LogCtrl, self).__init__(parent, id=id, pos=pos, size=size, style=style, name=name)
  637 
  638         # Flag for scrolling to the bottom.
  639         self.at_end = True
  640 
  641         # Turn on line wrapping.
  642         self.SetWrapMode(wx.stc.STC_WRAP_WORD)
  643 
  644         # Create the standard style (style num 0).
  645         self.StyleSetFont(0, font.modern_small)
  646 
  647         # Create the STDERR style (style num 1).
  648         if dep_check.wx_classic:
  649             colour = wx.NamedColour('red')
  650         else:
  651             colour = wx.Colour('red')
  652         self.StyleSetForeground(1, colour)
  653         self.StyleSetFont(1, font.modern_small)
  654 
  655         # Create the relax prompt style (style num 2).
  656         if dep_check.wx_classic:
  657             colour = wx.NamedColour('blue')
  658         else:
  659             colour = wx.Colour('blue')
  660         self.StyleSetForeground(2, colour)
  661         self.StyleSetFont(2, font.modern_small_bold)
  662 
  663         # Create the relax warning style (style num 3).
  664         if dep_check.wx_classic:
  665             colour = wx.NamedColour('orange red')
  666         else:
  667             colour = wx.Colour('orange red')
  668         self.StyleSetForeground(3, colour)
  669         self.StyleSetFont(3, font.modern_small)
  670 
  671         # Create the relax debugging style (style num 4).
  672         if dep_check.wx_classic:
  673             colour = wx.NamedColour('dark green')
  674         else:
  675             colour = wx.Colour('dark green')
  676         self.StyleSetForeground(4, colour)
  677         self.StyleSetFont(4, font.modern_small)
  678 
  679         # Initilise the find dialog.
  680         self.find_dlg = None
  681 
  682         # The data for the find dialog.
  683         self.find_data = wx.FindReplaceData()
  684         self.find_data.SetFlags(wx.FR_DOWN)
  685 
  686         # Turn off the pop up menu.
  687         self.UsePopUp(0)
  688 
  689         # Make the control read only.
  690         self.SetReadOnly(True)
  691 
  692         # The original zoom level.
  693         self.orig_zoom = self.GetZoom()
  694 
  695         # Bind events.
  696         self.Bind(wx.EVT_KEY_DOWN, self.capture_keys)
  697         self.Bind(wx.EVT_MOUSE_EVENTS, self.capture_mouse)
  698         self.Bind(wx.EVT_MOUSEWHEEL, self.capture_mouse_wheel)
  699         self.Bind(wx.EVT_RIGHT_DOWN, self.pop_up_menu)
  700         self.Bind(wx.EVT_SCROLLWIN_THUMBTRACK, self.capture_scroll)
  701         self.Bind(wx.EVT_MENU, self.find_open, id=MENU_ID_FIND)
  702         self.Bind(wx.EVT_MENU, self.on_copy, id=MENU_ID_COPY)
  703         self.Bind(wx.EVT_MENU, self.on_select_all, id=MENU_ID_SELECT_ALL)
  704         self.Bind(wx.EVT_MENU, self.on_zoom_in, id=MENU_ID_ZOOM_IN)
  705         self.Bind(wx.EVT_MENU, self.on_zoom_out, id=MENU_ID_ZOOM_OUT)
  706         self.Bind(wx.EVT_MENU, self.on_zoom_orig, id=MENU_ID_ZOOM_ORIG)
  707         self.Bind(wx.EVT_MENU, self.on_goto_start, id=MENU_ID_GOTO_START)
  708         self.Bind(wx.EVT_MENU, self.on_goto_end, id=MENU_ID_GOTO_END)
  709 
  710 
  711     def capture_keys(self, event):
  712         """Control which key events are active, preventing text insertion and deletion.
  713 
  714         @param event:   The wx event.
  715         @type event:    wx event
  716         """
  717 
  718         # Allow Ctrl-C events.
  719         if event.ControlDown() and event.GetKeyCode() == 67:
  720             event.Skip()
  721 
  722         # The find dialog (Ctrl-F).
  723         if event.ControlDown() and event.GetKeyCode() == 70:
  724             self.find_open(event)
  725 
  726         # Select all (Ctrl-A). 
  727         if event.ControlDown() and event.GetKeyCode() == 65:
  728             self.on_select_all(event)
  729 
  730         # Find next (Ctrl-G on Mac OS X, F3 on all others).
  731         if 'darwin' in sys.platform and event.ControlDown() and event.GetKeyCode() == 71:
  732             self.find_next(event)
  733         elif 'darwin' not in sys.platform and event.GetKeyCode() == wx.WXK_F3:
  734             self.find_next(event)
  735 
  736         # Allow caret movements (arrow keys, home, end).
  737         if event.GetKeyCode() in [wx.WXK_END, wx.WXK_HOME, wx.WXK_LEFT, wx.WXK_UP, wx.WXK_RIGHT, wx.WXK_DOWN]:
  738             self.at_end = False
  739             event.Skip()
  740 
  741         # Allow scrolling (pg up, pg dn):
  742         if event.GetKeyCode() in [wx.WXK_PAGEUP, wx.WXK_PAGEDOWN]:
  743             self.at_end = False
  744             event.Skip()
  745 
  746         # Zooming.
  747         if event.ControlDown() and event.GetKeyCode() == 48:
  748             self.on_zoom_orig(event)
  749         if event.ControlDown() and event.GetKeyCode() == 45:
  750             self.on_zoom_out(event)
  751         if event.ControlDown() and event.GetKeyCode() == 61:
  752             self.on_zoom_in(event)
  753 
  754         # Jump to start or end (Ctrl-Home and Ctrl-End).
  755         if event.ControlDown() and event.GetKeyCode() == wx.WXK_HOME:
  756             self.on_goto_start(event)
  757         elif event.ControlDown() and event.GetKeyCode() == wx.WXK_END:
  758             self.on_goto_end(event)
  759 
  760         # Use ESC to close the window.
  761         if event.GetKeyCode() == wx.WXK_ESCAPE:
  762             self.controller.handler_close(event)
  763 
  764 
  765     def capture_mouse(self, event):
  766         """Control the mouse events.
  767 
  768         @param event:   The wx event.
  769         @type event:    wx event
  770         """
  771 
  772         # Stop following the end if a mouse button is clicked.
  773         if event.ButtonDown():
  774             self.at_end = False
  775 
  776         # Continue with the event.
  777         event.Skip()
  778 
  779 
  780     def capture_mouse_wheel(self, event):
  781         """Control the mouse wheel events.
  782 
  783         @param event:   The wx event.
  784         @type event:    wx event
  785         """
  786 
  787         # Stop following the end on all events.
  788         self.at_end = False
  789 
  790         # Move the caret with the scroll, to prevent following the end.
  791         scroll = event.GetLinesPerAction()
  792         if event.GetWheelRotation() > 0.0:
  793             scroll *= -1
  794         self.GotoLine(self.GetCurrentLine() + scroll)
  795 
  796         # Continue with the event.
  797         event.Skip()
  798 
  799 
  800     def capture_scroll(self, event):
  801         """Control the window scrolling events.
  802 
  803         @param event:   The wx event.
  804         @type event:    wx event
  805         """
  806 
  807         # Stop following the end on all events.
  808         self.at_end = False
  809 
  810         # Move the caret with the scroll (at the bottom), to prevent following the end.
  811         self.GotoLine(event.GetPosition() + self.LinesOnScreen() - 1)
  812 
  813         # Continue with the event.
  814         event.Skip()
  815 
  816 
  817     def clear(self):
  818         """Remove all text from the log."""
  819 
  820         # Turn of the read only state.
  821         self.SetReadOnly(False)
  822 
  823         # Remove all text.
  824         self.ClearAll()
  825 
  826         # Make the control read only again.
  827         self.SetReadOnly(True)
  828 
  829 
  830     def find(self, event):
  831         """Find the text in the log control.
  832 
  833         @param event:   The wx event.
  834         @type event:    wx event
  835         """
  836 
  837         # The text.
  838         sel = self.find_data.GetFindString()
  839 
  840         # The search flags.
  841         flags = self.find_data.GetFlags()
  842 
  843         # Shift the search anchor 1 character forwards (if not at the end) to ensure the next instance is found.
  844         pos = self.GetCurrentPos()
  845         if pos != self.GetLength():
  846             self.SetCurrentPos(pos+1)
  847         self.SearchAnchor()
  848 
  849         # The direction.
  850         forwards = wx.FR_DOWN & flags
  851 
  852         # Find the next instance of the text.
  853         if forwards:
  854             pos = self.SearchNext(flags, sel)
  855 
  856         # Find the previous instance of the text.
  857         else:
  858             pos = self.SearchPrev(flags, sel)
  859 
  860         # Nothing found.
  861         if pos == -1:
  862             # Go to the start or end.
  863             if forwards:
  864                 self.GotoPos(self.GetLength())
  865             else:
  866                 self.GotoPos(pos)
  867 
  868             # Show a dialog that no text was found.
  869             text = "The string '%s' could not be found." % sel
  870             nothing = wx.MessageDialog(self, text, caption="Not found", style=wx.ICON_INFORMATION|wx.OK)
  871             nothing.SetSize((300, 200))
  872             if status.show_gui:
  873                 nothing.ShowModal()
  874                 nothing.Destroy()
  875 
  876         # Found text.
  877         else:
  878             # Make the text visible.
  879             self.EnsureCaretVisible()
  880 
  881             # Stop following the end on all events.
  882             self.at_end = False
  883 
  884 
  885     def find_close(self, event):
  886         """Close the find dialog.
  887 
  888         @param event:   The wx event.
  889         @type event:    wx event
  890         """
  891 
  892         # Kill the dialog.
  893         self.find_dlg.Destroy()
  894 
  895         # Set the object to None to signal the close.
  896         self.find_dlg = None
  897 
  898 
  899     def find_open(self, event):
  900         """Display the text finding dialog.
  901 
  902         @param event:   The wx event.
  903         @type event:    wx event
  904         """
  905 
  906         # Turn off the end flag.
  907         self.at_end = False
  908 
  909         # Initialise the dialog if it doesn't exist.
  910         if self.find_dlg == None:
  911             # Initalise.
  912             self.find_dlg = wx.FindReplaceDialog(self, self.find_data, "Find")
  913 
  914             # Bind the find events to this dialog.
  915             self.find_dlg.Bind(wx.EVT_FIND, self.find)
  916             self.find_dlg.Bind(wx.EVT_FIND_NEXT, self.find)
  917             self.find_dlg.Bind(wx.EVT_FIND_CLOSE, self.find_close)
  918 
  919             # Show the dialog.
  920             if status.show_gui:
  921                 self.find_dlg.Show(True)
  922 
  923         # Otherwise show it.
  924         else:
  925             self.find_dlg.Show()
  926 
  927 
  928     def find_next(self, event):
  929         """Find the next instance of the text.
  930 
  931         @param event:   The wx event.
  932         @type event:    wx event
  933         """
  934 
  935         # Turn off the end flag.
  936         self.at_end = False
  937 
  938         # Text has already been set.
  939         if self.find_data.GetFindString():
  940             self.find(event)
  941 
  942         # Open the dialog.
  943         else:
  944             self.find_open(event)
  945 
  946 
  947     def get_text(self):
  948         """Concatenate all of the text from the log queue and return it as a string.
  949 
  950         @return:    A list of the text from the log queue and a list of the streams these correspond to.
  951         @rtype:     list of str, list of int
  952         """
  953 
  954         # Initialise.
  955         string_list = ['']
  956         stream_list = [0]
  957 
  958         # Loop until the queue is empty.
  959         while True:
  960             # End condition.
  961             if self.log_queue.empty():
  962                 break
  963 
  964             # Get the data.
  965             msg, stream = self.log_queue.get()
  966 
  967             # The relax prompt.
  968             if msg[1:7] == 'relax>':
  969                 # Add a new line to the last block.
  970                 string_list[-1] += '\n'
  971 
  972                 # Add the prompt part.
  973                 string_list.append('relax>')
  974                 stream_list.append(2)
  975 
  976                 # Shorten the message.
  977                 msg = msg[7:]
  978 
  979                 # Start a new section.
  980                 string_list.append('')
  981                 stream_list.append(stream)
  982 
  983             # The relax warnings on STDERR.
  984             elif msg[0:13] == 'RelaxWarning:':
  985                 # Add the warning.
  986                 string_list.append(msg)
  987                 stream_list.append(3)
  988                 continue
  989 
  990             # Debugging - the relax lock.
  991             elif msg[0:6] == 'debug>':
  992                 # Add the debugging text.
  993                 string_list.append(msg)
  994                 stream_list.append(4)
  995                 continue
  996 
  997             # A different stream.
  998             if stream_list[-1] != stream:
  999                 string_list.append('')
 1000                 stream_list.append(stream)
 1001 
 1002             # Add the text.
 1003             string_list[-1] = string_list[-1] + msg
 1004 
 1005         # Return the concatenated text.
 1006         return string_list, stream_list
 1007 
 1008 
 1009     def limit_scrollback(self, prune=20):
 1010         """Limit scroll back to the maximum number of lines.
 1011 
 1012         Lines are deleted in blocks of 'prune' number of lines for faster operation.
 1013         """
 1014 
 1015         # Maximum not reached, so do nothing.
 1016         if self.GetLineCount() < status.controller_max_entries:
 1017             return
 1018 
 1019         # Get the current selection, scroll position and caret position.
 1020         pos_start, pos_end = self.GetSelection()
 1021         curr_pos = self.GetCurrentPos()
 1022 
 1023         # Prune the first x lines.
 1024         del_start = 0
 1025         del_end = self.GetLineEndPosition(prune) + 1
 1026         del_extent = del_end - del_start
 1027         self.SetSelection(del_start, del_end)
 1028         self.DeleteBack()
 1029 
 1030         # Determine the new settings.
 1031         new_curr_pos = curr_pos - del_extent
 1032         new_pos_start = pos_start - del_extent
 1033         new_pos_end = pos_end - del_extent
 1034 
 1035         # Return to the original position and state.
 1036         self.SetCurrentPos(new_curr_pos)
 1037         self.SetSelection(new_pos_start, new_pos_end)
 1038         self.LineScroll(0, prune)
 1039 
 1040 
 1041     def on_copy(self, event):
 1042         """Copy the selected text.
 1043 
 1044         @param event:   The wx event.
 1045         @type event:    wx event
 1046         """
 1047 
 1048         # Copy the selection to the clipboard.
 1049         self.Copy()
 1050 
 1051 
 1052     def on_goto_end(self, event):
 1053         """Move to the end of the text.
 1054 
 1055         @param event:   The wx event.
 1056         @type event:    wx event
 1057         """
 1058 
 1059         # Turn on the end flag.
 1060         self.at_end = True
 1061 
 1062         # Go to the end.
 1063         self.GotoPos(self.GetLength())
 1064 
 1065 
 1066     def on_goto_start(self, event):
 1067         """Move to the start of the text.
 1068 
 1069         @param event:   The wx event.
 1070         @type event:    wx event
 1071         """
 1072 
 1073         # Turn off the end flag.
 1074         self.at_end = False
 1075 
 1076         # Go to the start.
 1077         self.GotoPos(-1)
 1078 
 1079 
 1080     def on_select_all(self, event):
 1081         """Select all text in the control.
 1082 
 1083         @param event:   The wx event.
 1084         @type event:    wx event
 1085         """
 1086 
 1087         # Turn off the end flag.
 1088         self.at_end = False
 1089 
 1090         # Go to the first line.
 1091         self.GotoPos(1)
 1092 
 1093         # Select all text in the control.
 1094         self.SelectAll()
 1095 
 1096 
 1097     def on_zoom_in(self, event):
 1098         """Zoom in by increase the font by 1 point size.
 1099 
 1100         @param event:   The wx event.
 1101         @type event:    wx event
 1102         """
 1103 
 1104         # Zoom.
 1105         self.ZoomIn()
 1106 
 1107 
 1108     def on_zoom_orig(self, event):
 1109         """Zoom to the original zoom level.
 1110 
 1111         @param event:   The wx event.
 1112         @type event:    wx event
 1113         """
 1114 
 1115         # Zoom.
 1116         self.SetZoom(self.orig_zoom)
 1117 
 1118 
 1119     def on_zoom_out(self, event):
 1120         """Zoom out by decreasing the font by 1 point size.
 1121 
 1122         @param event:   The wx event.
 1123         @type event:    wx event
 1124         """
 1125 
 1126         # Zoom.
 1127         self.ZoomOut()
 1128 
 1129 
 1130     def pop_up_menu(self, event):
 1131         """Override the StyledTextCtrl pop up menu.
 1132 
 1133         @param event:   The wx event.
 1134         @type event:    wx event
 1135         """
 1136 
 1137         # Create the menu.
 1138         menu = wx.Menu()
 1139 
 1140         # Add the entries.
 1141         build_menu_item(menu, id=MENU_ID_FIND, text="&Find", icon=fetch_icon('oxygen.actions.edit-find', "16x16"))
 1142         menu.AppendSeparator()
 1143         build_menu_item(menu, id=MENU_ID_COPY, text="&Copy", icon=fetch_icon('oxygen.actions.edit-copy', "16x16"))
 1144         build_menu_item(menu, id=MENU_ID_SELECT_ALL, text="&Select all", icon=fetch_icon('oxygen.actions.edit-select-all', "16x16"))
 1145         menu.AppendSeparator()
 1146         build_menu_item(menu, id=MENU_ID_ZOOM_IN, text="Zoom &in", icon=fetch_icon('oxygen.actions.zoom-in', "16x16"))
 1147         build_menu_item(menu, id=MENU_ID_ZOOM_OUT, text="Zoom &out", icon=fetch_icon('oxygen.actions.zoom-out', "16x16"))
 1148         build_menu_item(menu, id=MENU_ID_ZOOM_ORIG, text="Original &zoom", icon=fetch_icon('oxygen.actions.zoom-original', "16x16"))
 1149         menu.AppendSeparator()
 1150         build_menu_item(menu, id=MENU_ID_GOTO_START, text="&Go to start", icon=fetch_icon('oxygen.actions.go-top', "16x16"))
 1151         build_menu_item(menu, id=MENU_ID_GOTO_END, text="&Go to end", icon=fetch_icon('oxygen.actions.go-bottom', "16x16"))
 1152 
 1153         # Pop up the menu.
 1154         if status.show_gui:
 1155             self.PopupMenu(menu)
 1156 
 1157         # Cleanup.
 1158         menu.Destroy()
 1159 
 1160 
 1161     def write(self):
 1162         """Write the text in the log queue to the log control."""
 1163 
 1164         # At the end?
 1165         if not self.at_end and self.GetScrollRange(wx.VERTICAL) - self.GetCurrentLine() <= 1:
 1166             self.at_end = True
 1167 
 1168         # Get the text.
 1169         string_list, stream_list = self.get_text()
 1170 
 1171         # Nothing to do.
 1172         if len(string_list) == 1 and string_list[0] == '':
 1173             return
 1174 
 1175         # Turn of the read only state.
 1176         self.SetReadOnly(False)
 1177 
 1178         # Add the text.
 1179         for i in range(len(string_list)):
 1180             # Add the text.
 1181             self.AppendText(string_list[i])
 1182 
 1183             # The different styles.
 1184             if stream_list[i] != 0:
 1185                 # Get the text extents.
 1186                 len_string = len(string_list[i].encode('utf8'))
 1187                 end = self.GetLength()
 1188 
 1189                 # Change the style.
 1190                 self.StartStyling(end - len_string, 31)
 1191                 self.SetStyling(len_string, stream_list[i])
 1192 
 1193             # Show the controller when there are errors or warnings.
 1194             if stream_list[i] in [1, 3] and status.show_gui:
 1195                 # Bring the window to the front.
 1196                 if self.controller.IsShown():
 1197                     self.controller.Raise()
 1198 
 1199                 # Open the window.
 1200                 else:
 1201                     # Show the window, then go to the message.
 1202                     self.controller.Show()
 1203                     self.GotoPos(self.GetLength())
 1204 
 1205         # Limit the scroll back.
 1206         self.limit_scrollback()
 1207 
 1208         # Stay at the end.
 1209         if self.at_end:
 1210             self.DocumentEnd()
 1211 
 1212         # Make the control read only again.
 1213         self.SetReadOnly(True)
 1214 
 1215 
 1216 
 1217 class Redirect_text(object):
 1218     """The IO redirection to text control object."""
 1219 
 1220     def __init__(self, control, log_queue, orig_io, stream=0):
 1221         """Set up the text redirection object.
 1222 
 1223         @param control:         The text control object to redirect IO to.
 1224         @type control:          wx.TextCtrl instance
 1225         @param log_queue:       The queue of log messages.
 1226         @type log_queue:        Queue.Queue instance
 1227         @param orig_io:         The original IO stream, used for debugging and the test suite.
 1228         @type orig_io:          file
 1229         @keyword stream:        The type of steam (0 for STDOUT and 1 for STDERR).
 1230         @type stream:           int
 1231         """
 1232 
 1233         # Store the args.
 1234         self.control = control
 1235         self.log_queue = log_queue
 1236         self.orig_io = orig_io
 1237         self.stream = stream
 1238 
 1239 
 1240     def flush(self):
 1241         """Simulate the file object flush method."""
 1242 
 1243         # Call the log control write method one the GUI is responsive.
 1244         wx.CallAfter(self.control.write)
 1245 
 1246 
 1247     def isatty(self):
 1248         """Answer that this is not a TTY.
 1249 
 1250         @return:    False, as this is not a TTY.
 1251         @rtype:     bool
 1252         """
 1253 
 1254         return False
 1255 
 1256 
 1257     def write(self, string):
 1258         """Simulate the file object write method.
 1259 
 1260         @param string:  The text to write.
 1261         @type string:   str
 1262         """
 1263 
 1264         # Debugging printout to the terminal.
 1265         if status.debug or status.test_mode:
 1266             self.orig_io.write(string)
 1267 
 1268         # Add the text to the queue.
 1269         self.log_queue.put([string, self.stream])