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