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

Source Code for Module gui.controller

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