Package gui :: Module controller
[hide private]
[frames] | no frames]

Source Code for Module gui.controller

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