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

Source Code for Module gui.controller

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