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