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