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