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 # Create the standard style (style num 0). 605 self.StyleSetFont(0, font.modern_small) 606 607 # Create the STDERR style (style num 1). 608 self.StyleSetForeground(1, wx.NamedColour('red')) 609 self.StyleSetFont(1, font.modern_small) 610 611 # Create the relax prompt style (style num 2). 612 self.StyleSetForeground(2, wx.NamedColour('blue')) 613 self.StyleSetFont(2, font.modern_small_bold) 614 615 # Create the relax warning style (style num 3). 616 self.StyleSetForeground(3, wx.NamedColour('orange red')) 617 self.StyleSetFont(3, font.modern_small) 618 619 # Create the relax debugging style (style num 4). 620 self.StyleSetForeground(4, wx.NamedColour('dark green')) 621 self.StyleSetFont(4, font.modern_small) 622 623 # Initilise the find dialog. 624 self.find_dlg = None 625 626 # The data for the find dialog. 627 self.find_data = wx.FindReplaceData() 628 self.find_data.SetFlags(wx.FR_DOWN) 629 630 # Turn off the pop up menu. 631 self.UsePopUp(0) 632 633 # IDs for the menu entries. 634 self.menu_id_find = wx.NewId() 635 self.menu_id_copy = wx.NewId() 636 self.menu_id_select_all = wx.NewId() 637 self.menu_id_zoom_in = wx.NewId() 638 self.menu_id_zoom_out = wx.NewId() 639 self.menu_id_zoom_orig = wx.NewId() 640 self.menu_id_goto_start = wx.NewId() 641 self.menu_id_goto_end = wx.NewId() 642 643 # Make the control read only. 644 self.SetReadOnly(True) 645 646 # The original zoom level. 647 self.orig_zoom = self.GetZoom() 648 649 # Bind events. 650 self.Bind(wx.EVT_FIND, self.find) 651 self.Bind(wx.EVT_FIND_NEXT, self.find) 652 self.Bind(wx.EVT_FIND_CLOSE, self.find_close) 653 self.Bind(wx.EVT_KEY_DOWN, self.capture_keys) 654 self.Bind(wx.EVT_RIGHT_DOWN, self.pop_up_menu) 655 self.Bind(wx.EVT_MENU, self.find_open, id=self.menu_id_find) 656 self.Bind(wx.EVT_MENU, self.on_copy, id=self.menu_id_copy) 657 self.Bind(wx.EVT_MENU, self.on_select_all, id=self.menu_id_select_all) 658 self.Bind(wx.EVT_MENU, self.on_zoom_in, id=self.menu_id_zoom_in) 659 self.Bind(wx.EVT_MENU, self.on_zoom_out, id=self.menu_id_zoom_out) 660 self.Bind(wx.EVT_MENU, self.on_zoom_orig, id=self.menu_id_zoom_orig) 661 self.Bind(wx.EVT_MENU, self.on_goto_start, id=self.menu_id_goto_start) 662 self.Bind(wx.EVT_MENU, self.on_goto_end, id=self.menu_id_goto_end)
663 664
665 - def capture_keys(self, event):
666 """Control which key events are active, preventing text insertion and deletion. 667 668 @param event: The wx event. 669 @type event: wx event 670 """ 671 672 # Allow Ctrl-C events. 673 if event.ControlDown() and event.GetKeyCode() == 67: 674 event.Skip() 675 676 # The find dialog (Ctrl-F). 677 if event.ControlDown() and event.GetKeyCode() == 70: 678 self.find_open(event) 679 680 # Select all (Ctrl-A). 681 if event.ControlDown() and event.GetKeyCode() == 65: 682 event.Skip() 683 684 # Find next (Ctrl-G on Mac OS X, F3 on all others). 685 if 'darwin' in sys.platform and event.ControlDown() and event.GetKeyCode() == 71: 686 self.find_next(event) 687 elif 'darwin' not in sys.platform and event.GetKeyCode() == 342: 688 self.find_next(event) 689 690 # Allow caret movements (arrow keys, home, end). 691 if event.GetKeyCode() in [312, 313, 314, 315, 316, 317]: 692 event.Skip() 693 694 # Allow scrolling (pg up, pg dn): 695 if event.GetKeyCode() in [366, 367]: 696 event.Skip() 697 698 # Zooming. 699 if event.ControlDown() and event.GetKeyCode() == 48: 700 self.on_zoom_orig(event) 701 if event.ControlDown() and event.GetKeyCode() == 45: 702 self.on_zoom_out(event) 703 if event.ControlDown() and event.GetKeyCode() == 61: 704 self.on_zoom_in(event) 705 706 # Jump to start or end (Ctrl-Home and Ctrl-End). 707 if event.ControlDown() and event.GetKeyCode() == 316: 708 self.on_goto_start(event) 709 elif event.ControlDown() and event.GetKeyCode() == 317: 710 self.on_goto_end(event)
711 712
713 - def clear(self):
714 """Remove all text from the log.""" 715 716 # Turn of the read only state. 717 self.SetReadOnly(False) 718 719 # Remove all text. 720 self.ClearAll() 721 722 # Make the control read only again. 723 self.SetReadOnly(True)
724 725
726 - def find(self, event):
727 """Find the text in the log control. 728 729 @param event: The wx event. 730 @type event: wx event 731 """ 732 733 # The text. 734 sel = self.find_data.GetFindString() 735 736 # The search flags. 737 flags = event.GetFlags() 738 739 # Shift the search anchor 1 character forwards (if not at the end) to ensure the next instance is found. 740 pos = self.GetCurrentPos() 741 if pos != self.GetLength(): 742 self.SetCurrentPos(pos+1) 743 self.SearchAnchor() 744 745 # The direction. 746 forwards = wx.FR_DOWN & flags 747 748 # Find the next instance of the text. 749 if forwards: 750 pos = self.SearchNext(flags, sel) 751 752 # Find the previous instance of the text. 753 else: 754 pos = self.SearchPrev(flags, sel) 755 756 # Nothing found. 757 if pos == -1: 758 # Go to the start or end. 759 if forwards: 760 self.GotoPos(self.GetLength()) 761 else: 762 self.GotoPos(pos) 763 764 # Show a dialog that no text was found. 765 text = "The string '%s' could not be found." % sel 766 nothing = wx.MessageDialog(self, text, caption="Not found", style=wx.ICON_INFORMATION|wx.OK) 767 nothing.SetSize((300, 200)) 768 if status.show_gui: 769 nothing.ShowModal() 770 nothing.Destroy() 771 772 # Found text. 773 else: 774 # Move to the line. 775 self.EnsureCaretVisible()
776 777
778 - def find_close(self, event):
779 """Close the find dialog. 780 781 @param event: The wx event. 782 @type event: wx event 783 """ 784 785 # Kill the dialog. 786 self.find_dlg.Destroy() 787 788 # Set the object to None to signal the close. 789 self.find_dlg = None
790 791
792 - def find_open(self, event):
793 """Display the text finding dialog. 794 795 @param event: The wx event. 796 @type event: wx event 797 """ 798 799 # Initialise the dialog if it doesn't exist. 800 if self.find_dlg == None: 801 self.find_dlg = wx.FindReplaceDialog(self, self.find_data, "Find") 802 if status.show_gui: 803 self.find_dlg.Show(True) 804 805 # Otherwise show it. 806 else: 807 self.find_dlg.Show()
808 809
810 - def find_next(self, event):
811 """Find the next instance of the text. 812 813 @param event: The wx event. 814 @type event: wx event 815 """ 816 817 # Text has already been set. 818 if self.find_data.GetFindString(): 819 self.find(event) 820 821 # Open the dialog. 822 else: 823 self.find_open(event)
824 825
826 - def get_text(self):
827 """Concatenate all of the text from the log queue and return it as a string. 828 829 @return: A list of the text from the log queue and a list of the streams these correspond to. 830 @rtype: list of str, list of int 831 """ 832 833 # Initialise. 834 string_list = [''] 835 stream_list = [0] 836 837 # Loop until the queue is empty. 838 while True: 839 # End condition. 840 if self.log_queue.empty(): 841 break 842 843 # Get the data. 844 msg, stream = self.log_queue.get() 845 846 # The relax prompt. 847 if msg[1:7] == 'relax>': 848 # Add a new line to the last block. 849 string_list[-1] += '\n' 850 851 # Add the prompt part. 852 string_list.append('relax>') 853 stream_list.append(2) 854 855 # Shorten the message. 856 msg = msg[7:] 857 858 # Start a new section. 859 string_list.append('') 860 stream_list.append(stream) 861 862 # The relax warnings on STDERR. 863 elif msg[0:13] == 'RelaxWarning:': 864 # Add the warning. 865 string_list.append(msg) 866 stream_list.append(3) 867 continue 868 869 # Debugging - the relax lock. 870 elif msg[0:6] == 'debug>': 871 # Add the debugging text. 872 string_list.append(msg) 873 stream_list.append(4) 874 continue 875 876 # A different stream. 877 if stream_list[-1] != stream: 878 string_list.append('') 879 stream_list.append(stream) 880 881 # Add the text. 882 string_list[-1] = string_list[-1] + msg 883 884 # Return the concatenated text. 885 return string_list, stream_list
886 887
888 - def limit_scrollback(self, prune=20):
889 """Limit scroll back to the maximum number of lines. 890 891 Lines are deleted in blocks of 'prune' number of lines for faster operation. 892 """ 893 894 # Maximum not reached, so do nothing. 895 if self.GetLineCount() < status.controller_max_entries: 896 return 897 898 # Get the current selection, scroll position and caret position. 899 pos_start, pos_end = self.GetSelection() 900 curr_pos = self.GetCurrentPos() 901 902 # Prune the first x lines. 903 del_start = 0 904 del_end = self.GetLineEndPosition(prune) + 1 905 del_extent = del_end - del_start 906 self.SetSelection(del_start, del_end) 907 self.DeleteBack() 908 909 # Determine the new settings. 910 new_curr_pos = curr_pos - del_extent 911 new_pos_start = pos_start - del_extent 912 new_pos_end = pos_end - del_extent 913 914 # Return to the original position and state. 915 self.SetCurrentPos(new_curr_pos) 916 self.SetSelection(new_pos_start, new_pos_end) 917 self.LineScroll(0, prune)
918 919
920 - def on_copy(self, event):
921 """Copy the selected text. 922 923 @param event: The wx event. 924 @type event: wx event 925 """ 926 927 # Copy the selection to the clipboard. 928 self.Copy()
929 930
931 - def on_goto_end(self, event):
932 """Move to the end of the text. 933 934 @param event: The wx event. 935 @type event: wx event 936 """ 937 938 # Go to the end. 939 self.GotoPos(self.GetLength())
940 941
942 - def on_goto_start(self, event):
943 """Move to the start of the text. 944 945 @param event: The wx event. 946 @type event: wx event 947 """ 948 949 # Go to the start. 950 self.GotoPos(-1)
951 952
953 - def on_select_all(self, event):
954 """Select all text in the control. 955 956 @param event: The wx event. 957 @type event: wx event 958 """ 959 960 # Select all text in the control. 961 self.SelectAll()
962 963
964 - def on_zoom_in(self, event):
965 """Zoom in by increase the font by 1 point size. 966 967 @param event: The wx event. 968 @type event: wx event 969 """ 970 971 # Zoom. 972 self.ZoomIn()
973 974
975 - def on_zoom_orig(self, event):
976 """Zoom to the original zoom level. 977 978 @param event: The wx event. 979 @type event: wx event 980 """ 981 982 # Zoom. 983 self.SetZoom(self.orig_zoom)
984 985
986 - def on_zoom_out(self, event):
987 """Zoom out by decreasing the font by 1 point size. 988 989 @param event: The wx event. 990 @type event: wx event 991 """ 992 993 # Zoom. 994 self.ZoomOut()
995 996
997 - def pop_up_menu(self, event):
998 """Override the StyledTextCtrl pop up menu. 999 1000 @param event: The wx event. 1001 @type event: wx event 1002 """ 1003 1004 # Create the menu. 1005 menu = wx.Menu() 1006 1007 # Add the entries. 1008 menu.AppendItem(build_menu_item(menu, id=self.menu_id_find, text="&Find", icon=icon_16x16.edit_find)) 1009 menu.AppendSeparator() 1010 menu.AppendItem(build_menu_item(menu, id=self.menu_id_copy, text="&Copy", icon=icon_16x16.edit_copy)) 1011 menu.AppendItem(build_menu_item(menu, id=self.menu_id_select_all, text="&Select all", icon=icon_16x16.edit_select_all)) 1012 menu.AppendSeparator() 1013 menu.AppendItem(build_menu_item(menu, id=self.menu_id_zoom_in, text="Zoom &in", icon=icon_16x16.zoom_in)) 1014 menu.AppendItem(build_menu_item(menu, id=self.menu_id_zoom_out, text="Zoom &out", icon=icon_16x16.zoom_out)) 1015 menu.AppendItem(build_menu_item(menu, id=self.menu_id_zoom_orig, text="Original &zoom", icon=icon_16x16.zoom_original)) 1016 menu.AppendSeparator() 1017 menu.AppendItem(build_menu_item(menu, id=self.menu_id_goto_start, text="&Go to start", icon=icon_16x16.go_top)) 1018 menu.AppendItem(build_menu_item(menu, id=self.menu_id_goto_end, text="&Go to end", icon=icon_16x16.go_bottom)) 1019 1020 # Pop up the menu. 1021 if status.show_gui: 1022 self.PopupMenu(menu) 1023 menu.Destroy()
1024 1025
1026 - def write(self):
1027 """Write the text in the log queue to the log control.""" 1028 1029 # Get the text. 1030 string_list, stream_list = self.get_text() 1031 1032 # Nothing to do. 1033 if len(string_list) == 1 and string_list[0] == '': 1034 return 1035 1036 # At the end? 1037 at_end = False 1038 if self.GetScrollPos(wx.VERTICAL) == self.GetScrollRange(wx.VERTICAL) - self.LinesOnScreen(): 1039 at_end = True 1040 1041 # Turn of the read only state. 1042 self.SetReadOnly(False) 1043 1044 # Add the text. 1045 for i in range(len(string_list)): 1046 # Add the text. 1047 self.AppendText(string_list[i]) 1048 1049 # The different styles. 1050 if stream_list[i] != 0: 1051 # Get the text extents. 1052 len_string = len(string_list[i].encode('utf8')) 1053 end = self.GetLength() 1054 1055 # Change the style. 1056 self.StartStyling(end - len_string, 31) 1057 self.SetStyling(len_string, stream_list[i]) 1058 1059 # Show the controller when there are errors or warnings. 1060 if stream_list[i] in [1, 3] and status.show_gui: 1061 # Bring the window to the front. 1062 if self.controller.IsShown(): 1063 self.controller.Raise() 1064 1065 # Open the window. 1066 else: 1067 # Show the window, then go to the message. 1068 self.controller.Show() 1069 self.GotoPos(self.GetLength()) 1070 1071 # Limit the scroll back. 1072 self.limit_scrollback() 1073 1074 # Stay at the end. 1075 if at_end: 1076 self.ScrollToLine(self.GetLineCount()) 1077 1078 # Make the control read only again. 1079 self.SetReadOnly(True)
1080 1081 1082
1083 -class Redirect_text(object):
1084 """The IO redirection to text control object.""" 1085
1086 - def __init__(self, control, log_queue, orig_io, stream=0):
1087 """Set up the text redirection object. 1088 1089 @param control: The text control object to redirect IO to. 1090 @type control: wx.TextCtrl instance 1091 @param log_queue: The queue of log messages. 1092 @type log_queue: Queue.Queue instance 1093 @param orig_io: The original IO stream, used for debugging and the test suite. 1094 @type orig_io: file 1095 @keyword stream: The type of steam (0 for STDOUT and 1 for STDERR). 1096 @type stream: int 1097 """ 1098 1099 # Store the args. 1100 self.control = control 1101 self.log_queue = log_queue 1102 self.orig_io = orig_io 1103 self.stream = stream
1104 1105
1106 - def flush(self):
1107 """Simulate the file object flush method.""" 1108 1109 # Call the log control write method one the GUI is responsive. 1110 wx.CallAfter(self.control.write)
1111 1112
1113 - def isatty(self):
1114 """Answer that this is not a TTY. 1115 1116 @return: False, as this is not a TTY. 1117 @rtype: bool 1118 """ 1119 1120 return False
1121 1122
1123 - def write(self, string):
1124 """Simulate the file object write method. 1125 1126 @param string: The text to write. 1127 @type string: str 1128 """ 1129 1130 # Debugging printout to the terminal. 1131 if status.debug or status.test_mode: 1132 self.orig_io.write(string) 1133 1134 # Add the text to the queue. 1135 self.log_queue.put([string, self.stream]) 1136 1137 # Call the log control write method one the GUI is responsive. 1138 wx.CallAfter(self.control.write)
1139