Exemple #1
0
    def __init__(self, parent, title, label, trace, button_text='OK',
                 cancel_text='Cancel'):
        '''Show a dialog with a scrollable stack trace.
        '''
        Toplevel.__init__(self, parent)
        self.withdraw()  # remain invisible for now
        if parent.winfo_viewable():
            self.transient(parent)

        self.title(title)
        self.parent = parent

        self.frame = Frame(self)
        self.frame.grid(column=0, row=0, sticky=(N, S, E, W))

        self.label = Label(self.frame, text=label)
        self.label.grid(column=0, row=0, padx=5, pady=5, sticky=(W, E))

        self.description = ReadOnlyText(self.frame, width=80, height=20)
        self.description.grid(column=0, columnspan=2, row=1, pady=5, sticky=(N, S, E, W,))

        self.description_scrollbar = Scrollbar(self.frame, orient=VERTICAL)
        self.description_scrollbar.grid(column=1, row=1, pady=5, sticky=(N, S, E))
        self.description.config(yscrollcommand=self.description_scrollbar.set)
        self.description_scrollbar.config(command=self.description.yview)

        self.description.insert('1.0', trace)

        if cancel_text is not None:
            self.cancel_button = Button(self.frame, text=cancel_text, command=self.cancel)
            self.cancel_button.grid(column=0, row=2, padx=5, pady=5, sticky=(E,))

        self.ok_button = Button(self.frame, text=button_text, command=self.ok, default=ACTIVE)
        self.ok_button.grid(column=1, row=2, padx=5, pady=5, sticky=(E,))

        self.columnconfigure(0, weight=1)
        self.rowconfigure(0, weight=1)

        self.frame.columnconfigure(0, weight=1)
        self.frame.columnconfigure(1, weight=0)

        self.frame.rowconfigure(0, weight=0)
        self.frame.rowconfigure(1, weight=1)
        self.frame.rowconfigure(2, weight=0)

        self.protocol("WM_DELETE_WINDOW", self.cancel)
        self.bind('<Return>', self.ok)

        if self.parent is not None:
            self.geometry("+%d+%d" % (parent.winfo_rootx()+50,
                                      parent.winfo_rooty()+50))

        self.deiconify()  # become visible now
        self.ok_button.focus_set()

        # wait for window to appear on screen before calling grab_set
        self.wait_visibility()
        self.grab_set()
        self.wait_window(self)
Exemple #2
0
    def _setup_html_area(self):
        self.html_frame = Frame(self.content)
        self.html_frame.grid(column=1, row=0, sticky=(N, S, E, W))

        # Label for current file
        self.current_file = StringVar()
        self.current_file_label = Label(self.html_frame,
                                        textvariable=self.current_file)
        self.current_file_label.grid(column=0,
                                     row=0,
                                     columnspan=3,
                                     sticky=(W, E))

        # Code display area
        self.html = SimpleHTMLView(self.html_frame)
        self.html.grid(column=0, row=1, columnspan=3, sticky=(N, S, E, W))

        self.html.link_bind('<1>', self.on_link_click)

        # Warnings
        self.warnings_label = Label(self.html_frame, text='Warnings:')
        self.warnings_label.grid(column=0, row=2, pady=5, sticky=(
            N,
            E,
        ))

        self.warnings = ReadOnlyText(self.html_frame, height=6)
        self.warnings.grid(column=1,
                           row=2,
                           pady=5,
                           columnspan=2,
                           sticky=(
                               N,
                               S,
                               E,
                               W,
                           ))
        self.warnings.tag_configure('warning',
                                    wrap=WORD,
                                    lmargin1=5,
                                    lmargin2=20,
                                    spacing1=2,
                                    spacing3=2)
        self.warnings_scrollbar = Scrollbar(self.html_frame, orient=VERTICAL)
        self.warnings_scrollbar.grid(column=2, row=2, pady=5, sticky=(N, S))
        self.warnings.config(yscrollcommand=self.warnings_scrollbar.set)
        self.warnings_scrollbar.config(command=self.warnings.yview)

        # Set up weights for the html frame's content
        self.html_frame.columnconfigure(0, weight=0)
        self.html_frame.columnconfigure(1, weight=1)
        self.html_frame.columnconfigure(2, weight=0)
        self.html_frame.rowconfigure(0, weight=0)
        self.html_frame.rowconfigure(1, weight=4)
        self.html_frame.rowconfigure(2, weight=1)

        self.content.add(self.html_frame)
Exemple #3
0
    def _setup_html_area(self):
        self.html_frame = Frame(self.content)
        self.html_frame.grid(column=1, row=0, sticky=(N, S, E, W))

        # Label for current file
        self.current_file = StringVar()
        self.current_file_label = Label(self.html_frame, textvariable=self.current_file)
        self.current_file_label.grid(column=0, row=0, columnspan=3, sticky=(W, E))

        # Code display area
        self.html = SimpleHTMLView(self.html_frame)
        self.html.grid(column=0, row=1, columnspan=3, sticky=(N, S, E, W))

        self.html.link_bind('<1>', self.on_link_click)

        # Warnings
        self.warnings_label = Label(self.html_frame, text='Warnings:')
        self.warnings_label.grid(column=0, row=2, pady=5, sticky=(N, E,))

        self.warnings = ReadOnlyText(self.html_frame, height=6)
        self.warnings.grid(column=1, row=2, pady=5, columnspan=2, sticky=(N, S, E, W,))
        self.warnings.tag_configure('warning', wrap=WORD, lmargin1=5, lmargin2=20, spacing1=2, spacing3=2)
        self.warnings_scrollbar = Scrollbar(self.html_frame, orient=VERTICAL)
        self.warnings_scrollbar.grid(column=2, row=2, pady=5, sticky=(N, S))
        self.warnings.config(yscrollcommand=self.warnings_scrollbar.set)
        self.warnings_scrollbar.config(command=self.warnings.yview)

        # Set up weights for the html frame's content
        self.html_frame.columnconfigure(0, weight=0)
        self.html_frame.columnconfigure(1, weight=1)
        self.html_frame.columnconfigure(2, weight=0)
        self.html_frame.rowconfigure(0, weight=0)
        self.html_frame.rowconfigure(1, weight=4)
        self.html_frame.rowconfigure(2, weight=1)

        self.content.add(self.html_frame)
Exemple #4
0
class MainWindow(object):
    def __init__(self, root):
        '''
        -----------------------------------------------------
        | main button toolbar                               |
        -----------------------------------------------------
        |       < ma | in content area >                    |
        |            |                                      |
        |  left      |              right                   |
        |  control   |              details frame           |
        |  tree      |              / output viewer         |
        |  area      |                                      |
        -----------------------------------------------------
        |     status bar area                               |
        -----------------------------------------------------

        '''

        self._project = None
        self.executor = None

        # Root window
        self.root = root
        self.root.title('Cricket')
        self.root.geometry('1024x768')

        # Prevent the menus from having the empty tearoff entry
        self.root.option_add('*tearOff', FALSE)
        # Catch the close button
        self.root.protocol("WM_DELETE_WINDOW", self.cmd_quit)
        # Catch the "quit" event.
        self.root.createcommand('exit', self.cmd_quit)

        # Setup the menu
        self._setup_menubar()

        # Set up the main content for the window.
        self._setup_button_toolbar()
        self._setup_main_content()
        self._setup_status_bar()

        # Now configure the weights for the root frame
        self.root.columnconfigure(0, weight=1)
        self.root.rowconfigure(0, weight=0)
        self.root.rowconfigure(1, weight=1)
        self.root.rowconfigure(2, weight=0)

        # Set up listeners for runner events.
        Executor.bind('test_status_update', self.on_executorStatusUpdate)
        Executor.bind('test_start', self.on_executorTestStart)
        Executor.bind('test_end', self.on_executorTestEnd)
        Executor.bind('suite_end', self.on_executorSuiteEnd)
        Executor.bind('suite_error', self.on_executorSuiteError)

        # Now that we've laid out the grid, hide the error and output text
        # until we actually have an error/output to display
        self._hide_test_output()
        self._hide_test_errors()

    ######################################################
    # Internal GUI layout methods.
    ######################################################

    def _setup_menubar(self):
        # Menubar
        self.menubar = Menu(self.root)

        # self.menu_Apple = Menu(self.menubar, name='Apple')
        # self.menubar.add_cascade(menu=self.menu_Apple)

        self.menu_file = Menu(self.menubar)
        self.menubar.add_cascade(menu=self.menu_file, label='File')

        self.menu_test = Menu(self.menubar)
        self.menubar.add_cascade(menu=self.menu_test, label='Test')

        self.menu_beeware = Menu(self.menubar)
        self.menubar.add_cascade(menu=self.menu_beeware, label='BeeWare')

        self.menu_help = Menu(self.menubar)
        self.menubar.add_cascade(menu=self.menu_help, label='Help')

        # self.menu_Apple.add_command(label='Test', command=self.cmd_dummy)

        # self.menu_file.add_command(label='New', command=self.cmd_dummy, accelerator="Command-N")
        # self.menu_file.add_command(label='Open...', command=self.cmd_dummy)
        # self.menu_file.add_command(label='Close', command=self.cmd_dummy)

        self.menu_test.add_command(label='Run all', command=self.cmd_run_all)
        self.menu_test.add_command(label='Run selected tests',
                                   command=self.cmd_run_selected)
        self.menu_test.add_command(label='Re-run failed tests',
                                   command=self.cmd_rerun)

        self.menu_beeware.add_command(
            label='Open Duvet...',
            command=self.cmd_open_duvet,
            state=DISABLED if duvet is None else ACTIVE)

        self.menu_help.add_command(label='Open Documentation',
                                   command=self.cmd_cricket_docs)
        self.menu_help.add_command(label='Open Cricket project page',
                                   command=self.cmd_cricket_page)
        self.menu_help.add_command(label='Open Cricket on GitHub',
                                   command=self.cmd_cricket_github)
        self.menu_help.add_command(label='Open BeeWare project page',
                                   command=self.cmd_beeware_page)

        # last step - configure the menubar
        self.root['menu'] = self.menubar

    def _setup_button_toolbar(self):
        '''
        The button toolbar runs as a horizontal area at the top of the GUI.
        It is a persistent GUI component
        '''

        # Main toolbar
        self.toolbar = Frame(self.root)
        self.toolbar.grid(column=0, row=0, sticky=(W, E))

        # Buttons on the toolbar
        self.stop_button = Button(self.toolbar,
                                  text='Stop',
                                  command=self.cmd_stop,
                                  state=DISABLED)
        self.stop_button.grid(column=0, row=0)

        self.run_all_button = Button(self.toolbar,
                                     text='Run all',
                                     command=self.cmd_run_all)
        self.run_all_button.grid(column=1, row=0)

        self.run_selected_button = Button(self.toolbar,
                                          text='Run selected',
                                          command=self.cmd_run_selected,
                                          state=DISABLED)
        self.run_selected_button.grid(column=2, row=0)

        self.rerun_button = Button(self.toolbar,
                                   text='Re-run',
                                   command=self.cmd_rerun,
                                   state=DISABLED)
        self.rerun_button.grid(column=3, row=0)

        self.coverage = StringVar()
        self.coverage_checkbox = Checkbutton(self.toolbar,
                                             text='Generate coverage',
                                             command=self.on_coverageChange,
                                             variable=self.coverage)
        self.coverage_checkbox.grid(column=4, row=0)

        # If coverage is available, enable it by default.
        # Otherwise, disable the widget
        if coverage:
            self.coverage.set('1')
        else:
            self.coverage.set('0')
            self.coverage_checkbox.configure(state=DISABLED)

        self.toolbar.columnconfigure(0, weight=0)
        self.toolbar.rowconfigure(0, weight=1)

    def _setup_main_content(self):
        '''
        Sets up the main content area. It is a persistent GUI component
        '''

        # Main content area
        self.content = PanedWindow(self.root, orient=HORIZONTAL)
        self.content.grid(column=0, row=1, sticky=(N, S, E, W))

        # Create the tree/control area on the left frame
        self._setup_left_frame()
        self._setup_all_tests_tree()
        self._setup_problem_tests_tree()

        # Create the output/viewer area on the right frame
        self._setup_right_frame()

        # Set up weights for the left frame's content
        self.content.columnconfigure(0, weight=1)
        self.content.rowconfigure(0, weight=1)

        self.content.pane(0, weight=1)
        self.content.pane(1, weight=2)

    def _setup_left_frame(self):
        '''
        The left frame mostly consists of the tree widget
        '''

        # The left-hand side frame on the main content area
        # The tabs for the two trees
        self.tree_notebook = Notebook(self.content, padding=(0, 5, 0, 5))
        self.content.add(self.tree_notebook)

    def _setup_all_tests_tree(self):
        # The tree for all tests
        self.all_tests_tree_frame = Frame(self.content)
        self.all_tests_tree_frame.grid(column=0, row=0, sticky=(N, S, E, W))
        self.tree_notebook.add(self.all_tests_tree_frame, text='All tests')

        self.all_tests_tree = Treeview(self.all_tests_tree_frame)
        self.all_tests_tree.grid(column=0, row=0, sticky=(N, S, E, W))

        # Set up the tag colors for tree nodes.
        for status, config in STATUS.items():
            self.all_tests_tree.tag_configure(config['tag'],
                                              foreground=config['color'])
        self.all_tests_tree.tag_configure('inactive', foreground='lightgray')

        # Listen for button clicks on tree nodes
        self.all_tests_tree.tag_bind('TestModule', '<Double-Button-1>',
                                     self.on_testModuleClicked)
        self.all_tests_tree.tag_bind('TestCase', '<Double-Button-1>',
                                     self.on_testCaseClicked)
        self.all_tests_tree.tag_bind('TestMethod', '<Double-Button-1>',
                                     self.on_testMethodClicked)

        self.all_tests_tree.tag_bind('TestModule', '<<TreeviewSelect>>',
                                     self.on_testModuleSelected)
        self.all_tests_tree.tag_bind('TestCase', '<<TreeviewSelect>>',
                                     self.on_testCaseSelected)
        self.all_tests_tree.tag_bind('TestMethod', '<<TreeviewSelect>>',
                                     self.on_testMethodSelected)

        # The tree's vertical scrollbar
        self.all_tests_tree_scrollbar = Scrollbar(self.all_tests_tree_frame,
                                                  orient=VERTICAL)
        self.all_tests_tree_scrollbar.grid(column=1, row=0, sticky=(N, S))

        # Tie the scrollbar to the text views, and the text views
        # to each other.
        self.all_tests_tree.config(
            yscrollcommand=self.all_tests_tree_scrollbar.set)
        self.all_tests_tree_scrollbar.config(command=self.all_tests_tree.yview)

        # Setup weights for the "All Tests" tree
        self.all_tests_tree_frame.columnconfigure(0, weight=1)
        self.all_tests_tree_frame.columnconfigure(1, weight=0)
        self.all_tests_tree_frame.rowconfigure(0, weight=1)

    def _setup_problem_tests_tree(self):
        # The tree for problem tests
        self.problem_tests_tree_frame = Frame(self.content)
        self.problem_tests_tree_frame.grid(column=0,
                                           row=0,
                                           sticky=(N, S, E, W))
        self.tree_notebook.add(self.problem_tests_tree_frame, text='Problems')

        self.problem_tests_tree = Treeview(self.problem_tests_tree_frame)
        self.problem_tests_tree.grid(column=0, row=0, sticky=(N, S, E, W))

        # Set up the tag colors for tree nodes.
        for status, config in STATUS.items():
            self.problem_tests_tree.tag_configure(config['tag'],
                                                  foreground=config['color'])
        self.problem_tests_tree.tag_configure('inactive',
                                              foreground='lightgray')

        # Problem tree only deals with selection, not clicks.
        self.problem_tests_tree.tag_bind('TestModule', '<<TreeviewSelect>>',
                                         self.on_testModuleSelected)
        self.problem_tests_tree.tag_bind('TestCase', '<<TreeviewSelect>>',
                                         self.on_testCaseSelected)
        self.problem_tests_tree.tag_bind('TestMethod', '<<TreeviewSelect>>',
                                         self.on_testMethodSelected)

        # The tree's vertical scrollbar
        self.problem_tests_tree_scrollbar = Scrollbar(
            self.problem_tests_tree_frame, orient=VERTICAL)
        self.problem_tests_tree_scrollbar.grid(column=1, row=0, sticky=(N, S))

        # Tie the scrollbar to the text views, and the text views
        # to each other.
        self.problem_tests_tree.config(
            yscrollcommand=self.problem_tests_tree_scrollbar.set)
        self.problem_tests_tree_scrollbar.config(
            command=self.all_tests_tree.yview)

        # Setup weights for the problems tree
        self.problem_tests_tree_frame.columnconfigure(0, weight=1)
        self.problem_tests_tree_frame.columnconfigure(1, weight=0)
        self.problem_tests_tree_frame.rowconfigure(0, weight=1)

    def _setup_right_frame(self):
        '''
        The right frame is basically the "output viewer" space
        '''

        # The right-hand side frame on the main content area
        self.details_frame = Frame(self.content)
        self.details_frame.grid(column=0, row=0, sticky=(N, S, E, W))
        self.content.add(self.details_frame)

        # Set up the content in the details panel
        # Test Name
        self.name_label = Label(self.details_frame, text='Name:')
        self.name_label.grid(column=0, row=0, pady=5, sticky=(E, ))

        self.name = StringVar()
        self.name_widget = Entry(self.details_frame, textvariable=self.name)
        self.name_widget.configure(state='readonly')
        self.name_widget.grid(column=1, row=0, pady=5, sticky=(W, E))

        # Test status
        self.test_status = StringVar()
        self.test_status_widget = Label(self.details_frame,
                                        textvariable=self.test_status,
                                        width=1,
                                        anchor=CENTER)
        f = Font(font=self.test_status_widget['font'])
        f['weight'] = 'bold'
        f['size'] = 50
        self.test_status_widget.config(font=f)
        self.test_status_widget.grid(column=2,
                                     row=0,
                                     padx=15,
                                     pady=5,
                                     rowspan=2,
                                     sticky=(N, W, E, S))

        # Test duration
        self.duration_label = Label(self.details_frame, text='Duration:')
        self.duration_label.grid(column=0, row=1, pady=5, sticky=(E, ))

        self.duration = StringVar()
        self.duration_widget = Entry(self.details_frame,
                                     textvariable=self.duration)
        self.duration_widget.grid(column=1, row=1, pady=5, sticky=(
            E,
            W,
        ))

        # Test description
        self.description_label = Label(self.details_frame, text='Description:')
        self.description_label.grid(column=0, row=2, pady=5, sticky=(
            N,
            E,
        ))

        self.description = ReadOnlyText(self.details_frame, width=80, height=4)
        self.description.grid(column=1,
                              row=2,
                              pady=5,
                              columnspan=2,
                              sticky=(
                                  N,
                                  S,
                                  E,
                                  W,
                              ))

        self.description_scrollbar = Scrollbar(self.details_frame,
                                               orient=VERTICAL)
        self.description_scrollbar.grid(column=3, row=2, pady=5, sticky=(N, S))
        self.description.config(yscrollcommand=self.description_scrollbar.set)
        self.description_scrollbar.config(command=self.description.yview)

        # Test output
        self.output_label = Label(self.details_frame, text='Output:')
        self.output_label.grid(column=0, row=3, pady=5, sticky=(
            N,
            E,
        ))

        self.output = ReadOnlyText(self.details_frame, width=80, height=4)
        self.output.grid(column=1,
                         row=3,
                         pady=5,
                         columnspan=2,
                         sticky=(
                             N,
                             S,
                             E,
                             W,
                         ))

        self.output_scrollbar = Scrollbar(self.details_frame, orient=VERTICAL)
        self.output_scrollbar.grid(column=3, row=3, pady=5, sticky=(N, S))
        self.output.config(yscrollcommand=self.output_scrollbar.set)
        self.output_scrollbar.config(command=self.output.yview)

        # Error message
        self.error_label = Label(self.details_frame, text='Error:')
        self.error_label.grid(column=0, row=4, pady=5, sticky=(
            N,
            E,
        ))

        self.error = ReadOnlyText(self.details_frame, width=80)
        self.error.grid(column=1,
                        row=4,
                        pady=5,
                        columnspan=2,
                        sticky=(N, S, E, W))

        self.error_scrollbar = Scrollbar(self.details_frame, orient=VERTICAL)
        self.error_scrollbar.grid(column=3, row=4, pady=5, sticky=(N, S))
        self.error.config(yscrollcommand=self.error_scrollbar.set)
        self.error_scrollbar.config(command=self.error.yview)

        # Set up GUI weights for the details frame
        self.details_frame.columnconfigure(0, weight=0)
        self.details_frame.columnconfigure(1, weight=1)
        self.details_frame.columnconfigure(2, weight=0)
        self.details_frame.columnconfigure(3, weight=0)
        self.details_frame.columnconfigure(4, weight=0)
        self.details_frame.rowconfigure(0, weight=0)
        self.details_frame.rowconfigure(1, weight=0)
        self.details_frame.rowconfigure(2, weight=1)
        self.details_frame.rowconfigure(3, weight=5)
        self.details_frame.rowconfigure(4, weight=10)

    def _setup_status_bar(self):
        # Status bar
        self.statusbar = Frame(self.root)
        self.statusbar.grid(column=0, row=2, sticky=(W, E))

        # Current status
        self.run_status = StringVar()
        self.run_status_label = Label(self.statusbar,
                                      textvariable=self.run_status)
        self.run_status_label.grid(column=0, row=0, sticky=(W, E))
        self.run_status.set('Not running')

        # Test result summary
        self.run_summary = StringVar()
        self.run_summary_label = Label(self.statusbar,
                                       textvariable=self.run_summary)
        self.run_summary_label.grid(column=1, row=0, sticky=(W, E))
        self.run_summary.set('T:0 P:0 F:0 E:0 X:0 U:0 S:0')

        # Test progress
        self.progress_value = IntVar()
        self.progress = Progressbar(self.statusbar,
                                    orient=HORIZONTAL,
                                    length=200,
                                    mode='determinate',
                                    maximum=100,
                                    variable=self.progress_value)
        self.progress.grid(column=2, row=0, sticky=(W, E))

        # Main window resize handle
        self.grip = Sizegrip(self.statusbar)
        self.grip.grid(column=3, row=0, sticky=(S, E))

        # Set up weights for status bar frame
        self.statusbar.columnconfigure(0, weight=1)
        self.statusbar.columnconfigure(1, weight=0)
        self.statusbar.columnconfigure(2, weight=0)
        self.statusbar.columnconfigure(3, weight=0)
        self.statusbar.rowconfigure(0, weight=1)

    ######################################################
    # Utility methods for inspecting current GUI state
    ######################################################

    @property
    def current_test_tree(self):
        "Check the tree notebook to return the currently selected tree."
        current_tree_id = self.tree_notebook.select()
        if current_tree_id == self.problem_tests_tree_frame._w:
            return self.problem_tests_tree
        else:
            return self.all_tests_tree

    ######################################################
    # Handlers for setting a new project
    ######################################################

    @property
    def project(self):
        return self._project

    def _add_test_module(self, parentNode, testModule):
        testModule_node = self.all_tests_tree.insert(
            parentNode,
            'end',
            testModule.path,
            text=testModule.name,
            tags=['TestModule', 'active'],
            open=True)

        for subModuleName, subModule in sorted(testModule.items()):
            if isinstance(subModule, TestModule):
                self._add_test_module(testModule_node, subModule)
            else:
                testCase = subModule
                testCase_node = self.all_tests_tree.insert(
                    testModule_node,
                    'end',
                    testCase.path,
                    text=testCase.name,
                    tags=['TestCase', 'active'],
                    open=True)

                for testMethod_name, testMethod in sorted(testCase.items()):
                    self.all_tests_tree.insert(testCase_node,
                                               'end',
                                               testMethod.path,
                                               text=testMethod.name,
                                               tags=['TestMethod', 'active'],
                                               open=True)

    @project.setter
    def project(self, project):
        self._project = project

        # Get a count of active tests to display in the status bar.
        count, labels = self.project.find_tests(True)
        self.run_summary.set('T:%s P:0 F:0 E:0 X:0 U:0 S:0' % count)

        # Populate the initial tree nodes. This is recursive, because
        # the tree could be of arbitrary depth.
        for testModule_name, testModule in sorted(project.items()):
            self._add_test_module('', testModule)

        # Listen for any state changes on nodes in the tree
        TestModule.bind('active', self.on_nodeActive)
        TestCase.bind('active', self.on_nodeActive)
        TestMethod.bind('active', self.on_nodeActive)

        TestModule.bind('inactive', self.on_nodeInactive)
        TestCase.bind('inactive', self.on_nodeInactive)
        TestMethod.bind('inactive', self.on_nodeInactive)

        # Listen for new nodes added to the tree
        TestModule.bind('new', self.on_nodeAdded)
        TestCase.bind('new', self.on_nodeAdded)
        TestMethod.bind('new', self.on_nodeAdded)

        # Listen for any status updates on nodes in the tree.
        TestMethod.bind('status_update', self.on_nodeStatusUpdate)

        # Update the project to make sure coverage status matches the GUI
        self.on_coverageChange()

    ######################################################
    # TK Main loop
    ######################################################

    def mainloop(self):
        self.root.mainloop()

    ######################################################
    # User commands
    ######################################################

    def cmd_quit(self):
        "Command: Quit"
        # If the runner is currently running, kill it.
        self.stop()

        self.root.quit()

    def cmd_stop(self, event=None):
        "Command: The stop button has been pressed"
        self.stop()

    def cmd_run_all(self, event=None):
        "Command: The Run all button has been pressed"
        # If the executor isn't currently running, we can
        # start a test run.
        if not self.executor or not self.executor.is_running:
            self.run(active=True)

    def cmd_run_selected(self, event=None):
        "Command: The 'run selected' button has been pressed"
        current_tree = self.current_test_tree

        # If a node is selected, it needs to be made active
        for path in current_tree.selection():
            parts = path.split('.')
            testModule = self.project
            for part in parts:
                testModule = testModule[part]

            testModule.set_active(True)

        # If the executor isn't currently running, we can
        # start a test run.
        if not self.executor or not self.executor.is_running:
            self.run(labels=set(current_tree.selection()))

    def cmd_rerun(self, event=None):
        "Command: The run/stop button has been pressed"
        # If the executor isn't currently running, we can
        # start a test run.
        if not self.executor or not self.executor.is_running:
            self.run(status=set(TestMethod.FAILING_STATES))

    def cmd_open_duvet(self, event=None):
        "Command: Open Duvet"
        try:
            subprocess.Popen('duvet')
        except Exception as e:
            tkMessageBox.showerror(message='Unable to start Duvet: %s' % e)

    def cmd_cricket_page(self):
        "Show the Cricket project page"
        webbrowser.open_new('http://pybee.org/cricket/')

    def cmd_beeware_page(self):
        "Show the Beeware project page"
        webbrowser.open_new('http://pybee.org/')

    def cmd_cricket_github(self):
        "Show the Cricket GitHub repo"
        webbrowser.open_new('http://github.com/pybee/cricket')

    def cmd_cricket_docs(self):
        "Show the Cricket documentation"
        webbrowser.open_new('https://cricket.readthedocs.io/')

    ######################################################
    # GUI Callbacks
    ######################################################

    def on_testModuleClicked(self, event):
        "Event handler: a module has been clicked in the tree"
        parts = event.widget.focus().split('.')
        testModule = self.project
        for part in parts:
            testModule = testModule[part]

        testModule.toggle_active()

    def on_testCaseClicked(self, event):
        "Event handler: a test case has been clicked in the tree"
        parts = event.widget.focus().split('.')
        testCase = self.project
        for part in parts:
            testCase = testCase[part]

        testCase.toggle_active()

    def on_testMethodClicked(self, event):
        "Event handler: a test case has been clicked in the tree"
        parts = event.widget.focus().split('.')
        testMethod = self.project
        for part in parts:
            testMethod = testMethod[part]

        testMethod.toggle_active()

    def on_testModuleSelected(self, event):
        "Event handler: a test module has been selected in the tree"
        self.name.set('')
        self.test_status.set('')

        self.duration.set('')
        self.description.delete('1.0', END)

        self._hide_test_output()
        self._hide_test_errors()

        # update "run selected" button enabled state
        self.set_selected_button_state()

    def on_testCaseSelected(self, event):
        "Event handler: a test case has been selected in the tree"
        self.name.set('')
        self.test_status.set('')

        self.duration.set('')
        self.description.delete('1.0', END)

        self._hide_test_output()
        self._hide_test_errors()

        # update "run selected" button enabled state
        self.set_selected_button_state()

    def on_testMethodSelected(self, event):
        "Event handler: a test case has been selected in the tree"
        if len(event.widget.selection()) == 1:
            parts = event.widget.selection()[0].split('.')

            # Find the definition for the actual test method
            # out of the project.
            testMethod = self.project
            for part in parts:
                testMethod = testMethod[part]

            self.name.set(testMethod.path)

            self.description.delete('1.0', END)
            self.description.insert('1.0', testMethod.description)

            config = STATUS.get(testMethod.status, STATUS_DEFAULT)
            self.test_status_widget.config(foreground=config['color'])
            self.test_status.set(config['symbol'])

            if testMethod._result:
                # Test has been executed
                self.duration.set('%0.2fs' % testMethod._result['duration'])

                if testMethod.output:
                    self._show_test_output(testMethod.output)
                else:
                    self._hide_test_output()

                if testMethod.error:
                    self._show_test_errors(testMethod.error)
                else:
                    self._hide_test_errors()
            else:
                # Test hasn't been executed yet.
                self.duration.set('Not executed')

                self._hide_test_output()
                self._hide_test_errors()

        else:
            # Multiple tests selected
            self.name.set('')
            self.test_status.set('')

            self.duration.set('')
            self.description.delete('1.0', END)

            self._hide_test_output()
            self._hide_test_errors()

        # update "run selected" button enabled state
        self.set_selected_button_state()

    def on_nodeAdded(self, node):
        "Event handler: a new node has been added to the tree"
        self.all_tests_tree.insert(node.parent.path,
                                   'end',
                                   node.path,
                                   text=node.name,
                                   tags=[node.__class__.__name__, 'active'],
                                   open=True)

    def on_nodeActive(self, node):
        "Event handler: a node on the tree has been made active"
        self.all_tests_tree.item(node.path,
                                 tags=[node.__class__.__name__, 'active'])
        self.all_tests_tree.item(node.path, open=True)

    def on_nodeInactive(self, node):
        "Event handler: a node on the tree has been made inactive"
        self.all_tests_tree.item(node.path,
                                 tags=[node.__class__.__name__, 'inactive'])
        self.all_tests_tree.item(node.path, open=False)

    def on_nodeStatusUpdate(self, node):
        "Event handler: a node on the tree has received a status update"
        self.all_tests_tree.item(
            node.path, tags=['TestMethod', STATUS[node.status]['tag']])

        if node.status in TestMethod.FAILING_STATES:
            # Test is in a failing state. Make sure it is on the problem tree,
            # with the correct current status.

            parts = node.path.split('.')
            parentModule = self.project
            for pos, part in enumerate(parts):
                path = '.'.join(parts[:pos + 1])
                testModule = parentModule[part]

                if not self.problem_tests_tree.exists(path):
                    self.problem_tests_tree.insert(
                        parentModule.path,
                        'end',
                        testModule.path,
                        text=testModule.name,
                        tags=[testModule.__class__.__name__, 'active'],
                        open=True)

                parentModule = testModule

            self.problem_tests_tree.item(
                node.path, tags=['TestMethod', STATUS[node.status]['tag']])
        else:
            # Test passed; if it's on the problem tree, remove it.
            if self.problem_tests_tree.exists(node.path):
                self.problem_tests_tree.delete(node.path)

                # Check all parents of this node. Recursively remove
                # any parent has no children as a result of this deletion.
                has_children = False
                node = node.parent
                while node.path and not has_children:
                    if not self.problem_tests_tree.get_children(node.path):
                        self.problem_tests_tree.delete(node.path)
                    else:
                        has_children = True
                    node = node.parent

    def on_coverageChange(self):
        "Event handler: when the coverage checkbox has been toggled"
        self.project.coverage = self.coverage.get() == '1'

    def on_testProgress(self):
        "Event handler: a periodic update to poll the runner for output, generating GUI updates"
        if self.executor and self.executor.poll():
            self.root.after(100, self.on_testProgress)

    def on_executorStatusUpdate(self, event, update):
        "The executor has some progress to report"
        # Update the status line.
        self.run_status.set(update)

    def on_executorTestStart(self, event, test_path):
        "The executor has started running a new test."
        # Update status line, and set the tree item to active.
        self.run_status.set('Running %s...' % test_path)
        self.all_tests_tree.item(test_path, tags=['TestMethod', 'active'])

    def on_executorTestEnd(self, event, test_path, result, remaining_time):
        "The executor has finished running a test."
        # Update the progress meter
        self.progress_value.set(self.progress_value.get() + 1)

        # Update the run summary
        self.run_summary.set(
            'T:%(total)s P:%(pass)s F:%(fail)s E:%(error)s X:%(expected)s U:%(unexpected)s S:%(skip)s, ~%(remaining)s remaining'
            % {
                'total':
                self.executor.total_count,
                'pass':
                self.executor.result_count.get(TestMethod.STATUS_PASS, 0),
                'fail':
                self.executor.result_count.get(TestMethod.STATUS_FAIL, 0),
                'error':
                self.executor.result_count.get(TestMethod.STATUS_ERROR, 0),
                'expected':
                self.executor.result_count.get(TestMethod.STATUS_EXPECTED_FAIL,
                                               0),
                'unexpected':
                self.executor.result_count.get(
                    TestMethod.STATUS_UNEXPECTED_SUCCESS, 0),
                'skip':
                self.executor.result_count.get(TestMethod.STATUS_SKIP, 0),
                'remaining':
                remaining_time
            })

        # If the test that just fininshed is the one (and only one)
        # selected on the tree, update the display.
        current_tree = self.current_test_tree
        if len(current_tree.selection()) == 1:
            # One test selected.
            if current_tree.selection()[0] == test_path:
                # If the test that just finished running is the selected
                # test, force reset the selection, which will generate a
                # selection event, forcing a refresh of the result page.
                current_tree.selection_set(current_tree.selection())
        else:
            # No or Multiple tests selected
            self.name.set('')
            self.test_status.set('')

            self.duration.set('')
            self.description.delete('1.0', END)

            self._hide_test_output()
            self._hide_test_errors()

    def on_executorSuiteEnd(self, event, error=None):
        "The test suite finished running."
        # Display the final results
        self.run_status.set('Finished.')

        if error:
            TestErrorsDialog(self.root, error)

        if self.executor.any_failed:
            dialog = tkMessageBox.showerror
        else:
            dialog = tkMessageBox.showinfo

        message = ', '.join(
            '%d %s' % (count, TestMethod.STATUS_LABELS[state])
            for state, count in sorted(self.executor.result_count.items()))

        dialog(message=message or 'No tests were ran')

        # Reset the running summary.
        self.run_summary.set(
            'T:%(total)s P:%(pass)s F:%(fail)s E:%(error)s X:%(expected)s U:%(unexpected)s S:%(skip)s'
            % {
                'total':
                self.executor.total_count,
                'pass':
                self.executor.result_count.get(TestMethod.STATUS_PASS, 0),
                'fail':
                self.executor.result_count.get(TestMethod.STATUS_FAIL, 0),
                'error':
                self.executor.result_count.get(TestMethod.STATUS_ERROR, 0),
                'expected':
                self.executor.result_count.get(TestMethod.STATUS_EXPECTED_FAIL,
                                               0),
                'unexpected':
                self.executor.result_count.get(
                    TestMethod.STATUS_UNEXPECTED_SUCCESS, 0),
                'skip':
                self.executor.result_count.get(TestMethod.STATUS_SKIP, 0),
            })

        # Reset the buttons
        self.reset_button_states_on_end()

        # Drop the reference to the executor
        self.executor = None

    def on_executorSuiteError(self, event, error):
        "An error occurred running the test suite."
        # Display the error in a dialog
        self.run_status.set('Error running test suite.')
        FailedTestDialog(self.root, error)

        # Reset the buttons
        self.reset_button_states_on_end()

        # Drop the reference to the executor
        self.executor = None

    def reset_button_states_on_end(self):
        "A test run has ended and we should enable or disable buttons as appropriate."
        self.stop_button.configure(state=DISABLED)
        self.run_all_button.configure(state=NORMAL)
        self.set_selected_button_state()
        if self.executor and self.executor.any_failed:
            self.rerun_button.configure(state=NORMAL)
        else:
            self.rerun_button.configure(state=DISABLED)

    def set_selected_button_state(self):
        if self.executor and self.executor.is_running:
            self.run_selected_button.configure(state=DISABLED)
        elif self.current_test_tree.selection():
            self.run_selected_button.configure(state=NORMAL)
        else:
            self.run_selected_button.configure(state=DISABLED)

    ######################################################
    # GUI utility methods
    ######################################################

    def run(self, active=True, status=None, labels=None):
        """Run the test suite.

        If active=True, only active tests will be run.
        If status is provided, only tests whose most recent run
            status matches the set provided will be executed.
        If labels is provided, only tests with those labels will
            be executed
        """
        count, labels = self.project.find_tests(active, status, labels)
        self.run_status.set('Running...')
        self.run_summary.set('T:%s P:0 F:0 E:0 X:0 U:0 S:0' % count)

        self.stop_button.configure(state=NORMAL)
        self.run_all_button.configure(state=DISABLED)
        self.run_selected_button.configure(state=DISABLED)
        self.rerun_button.configure(state=DISABLED)

        self.progress['maximum'] = count
        self.progress_value.set(0)

        # Create the runner
        self.executor = Executor(self.project, count, labels)

        # Queue the first progress handling event
        self.root.after(100, self.on_testProgress)

    def stop(self):
        "Stop the test suite."
        if self.executor and self.executor.is_running:
            self.run_status.set('Stopping...')

            self.executor.terminate()
            self.executor = None

            self.run_status.set('Stopped.')

            self.reset_button_states_on_end()

    def _hide_test_output(self):
        "Hide the test output panel on the test results page"
        self.output_label.grid_remove()
        self.output.grid_remove()
        self.output_scrollbar.grid_remove()
        self.details_frame.rowconfigure(3, weight=0)

    def _show_test_output(self, content):
        "Show the test output panel on the test results page"
        self.output.delete('1.0', END)
        self.output.insert('1.0', content)

        self.output_label.grid()
        self.output.grid()
        self.output_scrollbar.grid()
        self.details_frame.rowconfigure(3, weight=5)

    def _hide_test_errors(self):
        "Hide the test error panel on the test results page"
        self.error_label.grid_remove()
        self.error.grid_remove()
        self.error_scrollbar.grid_remove()

    def _show_test_errors(self, content):
        "Show the test error panel on the test results page"
        self.error.delete('1.0', END)
        self.error.insert('1.0', content)

        self.error_label.grid()
        self.error.grid()
        self.error_scrollbar.grid()
Exemple #5
0
    def _setup_right_frame(self):
        '''
        The right frame is basically the "output viewer" space
        '''

        # The right-hand side frame on the main content area
        self.details_frame = Frame(self.content)
        self.details_frame.grid(column=0, row=0, sticky=(N, S, E, W))
        self.content.add(self.details_frame)

        # Set up the content in the details panel
        # Test Name
        self.name_label = Label(self.details_frame, text='Name:')
        self.name_label.grid(column=0, row=0, pady=5, sticky=(E, ))

        self.name = StringVar()
        self.name_widget = Entry(self.details_frame, textvariable=self.name)
        self.name_widget.configure(state='readonly')
        self.name_widget.grid(column=1, row=0, pady=5, sticky=(W, E))

        # Test status
        self.test_status = StringVar()
        self.test_status_widget = Label(self.details_frame,
                                        textvariable=self.test_status,
                                        width=1,
                                        anchor=CENTER)
        f = Font(font=self.test_status_widget['font'])
        f['weight'] = 'bold'
        f['size'] = 50
        self.test_status_widget.config(font=f)
        self.test_status_widget.grid(column=2,
                                     row=0,
                                     padx=15,
                                     pady=5,
                                     rowspan=2,
                                     sticky=(N, W, E, S))

        # Test duration
        self.duration_label = Label(self.details_frame, text='Duration:')
        self.duration_label.grid(column=0, row=1, pady=5, sticky=(E, ))

        self.duration = StringVar()
        self.duration_widget = Entry(self.details_frame,
                                     textvariable=self.duration)
        self.duration_widget.grid(column=1, row=1, pady=5, sticky=(
            E,
            W,
        ))

        # Test description
        self.description_label = Label(self.details_frame, text='Description:')
        self.description_label.grid(column=0, row=2, pady=5, sticky=(
            N,
            E,
        ))

        self.description = ReadOnlyText(self.details_frame, width=80, height=4)
        self.description.grid(column=1,
                              row=2,
                              pady=5,
                              columnspan=2,
                              sticky=(
                                  N,
                                  S,
                                  E,
                                  W,
                              ))

        self.description_scrollbar = Scrollbar(self.details_frame,
                                               orient=VERTICAL)
        self.description_scrollbar.grid(column=3, row=2, pady=5, sticky=(N, S))
        self.description.config(yscrollcommand=self.description_scrollbar.set)
        self.description_scrollbar.config(command=self.description.yview)

        # Test output
        self.output_label = Label(self.details_frame, text='Output:')
        self.output_label.grid(column=0, row=3, pady=5, sticky=(
            N,
            E,
        ))

        self.output = ReadOnlyText(self.details_frame, width=80, height=4)
        self.output.grid(column=1,
                         row=3,
                         pady=5,
                         columnspan=2,
                         sticky=(
                             N,
                             S,
                             E,
                             W,
                         ))

        self.output_scrollbar = Scrollbar(self.details_frame, orient=VERTICAL)
        self.output_scrollbar.grid(column=3, row=3, pady=5, sticky=(N, S))
        self.output.config(yscrollcommand=self.output_scrollbar.set)
        self.output_scrollbar.config(command=self.output.yview)

        # Error message
        self.error_label = Label(self.details_frame, text='Error:')
        self.error_label.grid(column=0, row=4, pady=5, sticky=(
            N,
            E,
        ))

        self.error = ReadOnlyText(self.details_frame, width=80)
        self.error.grid(column=1,
                        row=4,
                        pady=5,
                        columnspan=2,
                        sticky=(N, S, E, W))

        self.error_scrollbar = Scrollbar(self.details_frame, orient=VERTICAL)
        self.error_scrollbar.grid(column=3, row=4, pady=5, sticky=(N, S))
        self.error.config(yscrollcommand=self.error_scrollbar.set)
        self.error_scrollbar.config(command=self.error.yview)

        # Set up GUI weights for the details frame
        self.details_frame.columnconfigure(0, weight=0)
        self.details_frame.columnconfigure(1, weight=1)
        self.details_frame.columnconfigure(2, weight=0)
        self.details_frame.columnconfigure(3, weight=0)
        self.details_frame.columnconfigure(4, weight=0)
        self.details_frame.rowconfigure(0, weight=0)
        self.details_frame.rowconfigure(1, weight=0)
        self.details_frame.rowconfigure(2, weight=1)
        self.details_frame.rowconfigure(3, weight=5)
        self.details_frame.rowconfigure(4, weight=10)
Exemple #6
0
class StackTraceDialog(Toplevel):
    OK = 1
    CANCEL = 2

    def __init__(self,
                 parent,
                 title,
                 label,
                 trace,
                 button_text='OK',
                 cancel_text='Cancel'):
        '''Show a dialog with a scrollable stack trace.

        Arguments:

            parent -- a parent window (the application window)
            title -- the title for the stack trace window
            label -- the label describing the stack trace
            trace -- the stack trace content to display.
            button_text -- the label for the button text ("OK" by default)
            cancel_text -- the label for the cancel button ("Cancel" by default)
        '''
        Toplevel.__init__(self, parent)

        self.withdraw()  # remain invisible for now

        # If the master is not viewable, don't
        # make the child transient, or else it
        # would be opened withdrawn
        if parent.winfo_viewable():
            self.transient(parent)

        self.title(title)

        self.parent = parent

        self.frame = Frame(self)
        self.frame.grid(column=0, row=0, sticky=(N, S, E, W))

        self.label = Label(self.frame, text=label)
        self.label.grid(column=0, row=0, padx=5, pady=5, sticky=(W, E))

        self.description = ReadOnlyText(self.frame, width=80, height=20)
        self.description.grid(column=0,
                              columnspan=2,
                              row=1,
                              pady=5,
                              sticky=(
                                  N,
                                  S,
                                  E,
                                  W,
                              ))

        self.description_scrollbar = Scrollbar(self.frame, orient=VERTICAL)
        self.description_scrollbar.grid(column=1,
                                        row=1,
                                        pady=5,
                                        sticky=(N, S, E))
        self.description.config(yscrollcommand=self.description_scrollbar.set)
        self.description_scrollbar.config(command=self.description.yview)

        self.description.insert('1.0', trace)

        if cancel_text is not None:
            self.cancel_button = Button(self.frame,
                                        text=cancel_text,
                                        command=self.cancel)
            self.cancel_button.grid(column=0,
                                    row=2,
                                    padx=5,
                                    pady=5,
                                    sticky=(E, ))

        self.ok_button = Button(self.frame,
                                text=button_text,
                                command=self.ok,
                                default=ACTIVE)
        self.ok_button.grid(column=1, row=2, padx=5, pady=5, sticky=(E, ))

        self.columnconfigure(0, weight=1)
        self.rowconfigure(0, weight=1)

        self.frame.columnconfigure(0, weight=1)
        self.frame.columnconfigure(1, weight=0)

        self.frame.rowconfigure(0, weight=0)
        self.frame.rowconfigure(1, weight=1)
        self.frame.rowconfigure(2, weight=0)

        self.protocol("WM_DELETE_WINDOW", self.cancel)
        self.bind('<Return>', self.ok)

        if self.parent is not None:
            self.geometry(
                "+%d+%d" %
                (parent.winfo_rootx() + 50, parent.winfo_rooty() + 50))

        self.deiconify()  # become visible now

        self.ok_button.focus_set()

        # wait for window to appear on screen before calling grab_set
        self.wait_visibility()
        self.grab_set()
        self.wait_window(self)

    def ok(self, event=None):
        self.withdraw()
        self.update_idletasks()

        if self.parent is not None:
            self.parent.focus_set()
        self.destroy()
        self.status = self.OK

    def cancel(self, event=None):
        self.withdraw()
        self.update_idletasks()

        if self.parent is not None:
            self.parent.focus_set()

        self.destroy()
        self.status = self.CANCEL
Exemple #7
0
    def __init__(self, parent, title, label, trace, button_text='OK',
                 cancel_text='Cancel'):
        '''Show a dialog with a scrollable stack trace.

        Arguments:

            parent -- a parent window (the application window)
            title -- the title for the stack trace window
            label -- the label describing the stack trace
            trace -- the stack trace content to display.
            button_text -- the label for the button text ("OK" by default)
            cancel_text -- the label for the cancel button ("Cancel" by default)
        '''
        Toplevel.__init__(self, parent)

        self.withdraw()  # remain invisible for now

        # If the master is not viewable, don't
        # make the child transient, or else it
        # would be opened withdrawn
        if parent.winfo_viewable():
            self.transient(parent)

        self.title(title)

        self.parent = parent

        self.frame = Frame(self)
        self.frame.grid(column=0, row=0, sticky=(N, S, E, W))

        self.label = Label(self.frame, text=label)
        self.label.grid(column=0, row=0, padx=5, pady=5, sticky=(W, E))

        self.description = ReadOnlyText(self.frame, width=80, height=20)
        self.description.grid(column=0, columnspan=2, row=1, pady=5, sticky=(N, S, E, W,))

        self.description_scrollbar = Scrollbar(self.frame, orient=VERTICAL)
        self.description_scrollbar.grid(column=1, row=1, pady=5, sticky=(N, S, E))
        self.description.config(yscrollcommand=self.description_scrollbar.set)
        self.description_scrollbar.config(command=self.description.yview)

        self.description.insert('1.0', trace)

        if cancel_text is not None:
            self.cancel_button = Button(self.frame, text=cancel_text, command=self.cancel)
            self.cancel_button.grid(column=0, row=2, padx=5, pady=5, sticky=(E,))

        self.ok_button = Button(self.frame, text=button_text, command=self.ok, default=ACTIVE)
        self.ok_button.grid(column=1, row=2, padx=5, pady=5, sticky=(E,))

        self.columnconfigure(0, weight=1)
        self.rowconfigure(0, weight=1)

        self.frame.columnconfigure(0, weight=1)
        self.frame.columnconfigure(1, weight=0)

        self.frame.rowconfigure(0, weight=0)
        self.frame.rowconfigure(1, weight=1)
        self.frame.rowconfigure(2, weight=0)

        self.protocol("WM_DELETE_WINDOW", self.cancel)
        self.bind('<Return>', self.ok)

        if self.parent is not None:
            self.geometry("+%d+%d" % (parent.winfo_rootx()+50,
                                      parent.winfo_rooty()+50))

        self.deiconify()  # become visible now

        self.ok_button.focus_set()

        # wait for window to appear on screen before calling grab_set
        self.wait_visibility()
        self.grab_set()
        self.wait_window(self)
Exemple #8
0
class MainWindow(object):
    def __init__(self, root):
        '''
        -----------------------------------------------------
        | main button toolbar                               |
        -----------------------------------------------------
        |       < ma | in content area >                    |
        |            |                                      |
        |  left      |              right                   |
        |  control   |              details frame           |
        |  tree      |              / output viewer         |
        |  area      |                                      |
        -----------------------------------------------------
        |     status bar area                               |
        -----------------------------------------------------

        '''

        self._project = None
        self.executor = None

        # Root window
        self.root = root
        self.root.title('Cricket')
        self.root.geometry('1024x768')

        # Prevent the menus from having the empty tearoff entry
        self.root.option_add('*tearOff', FALSE)
        # Catch the close button
        self.root.protocol("WM_DELETE_WINDOW", self.cmd_quit)
        # Catch the "quit" event.
        self.root.createcommand('exit', self.cmd_quit)

        # Setup the menu
        self._setup_menubar()

        # Set up the main content for the window.
        self._setup_button_toolbar()
        self._setup_main_content()
        self._setup_status_bar()

        # Now configure the weights for the root frame
        self.root.columnconfigure(0, weight=1)
        self.root.rowconfigure(0, weight=0)
        self.root.rowconfigure(1, weight=1)
        self.root.rowconfigure(2, weight=0)

        # Set up listeners for runner events.
        Executor.bind('test_status_update', self.on_executorStatusUpdate)
        Executor.bind('test_start', self.on_executorTestStart)
        Executor.bind('test_end', self.on_executorTestEnd)
        Executor.bind('suite_end', self.on_executorSuiteEnd)
        Executor.bind('suite_error', self.on_executorSuiteError)

        # Now that we've laid out the grid, hide the error and output text
        # until we actually have an error/output to display
        self._hide_test_output()
        self._hide_test_errors()

    ######################################################
    # Internal GUI layout methods.
    ######################################################

    def _setup_menubar(self):
        # Menubar
        self.menubar = Menu(self.root)

        # self.menu_Apple = Menu(self.menubar, name='Apple')
        # self.menubar.add_cascade(menu=self.menu_Apple)

        self.menu_file = Menu(self.menubar)
        self.menubar.add_cascade(menu=self.menu_file, label='File')

        self.menu_test = Menu(self.menubar)
        self.menubar.add_cascade(menu=self.menu_test, label='Test')

        self.menu_beeware = Menu(self.menubar)
        self.menubar.add_cascade(menu=self.menu_beeware, label='BeeWare')

        self.menu_help = Menu(self.menubar)
        self.menubar.add_cascade(menu=self.menu_help, label='Help')

        # self.menu_Apple.add_command(label='Test', command=self.cmd_dummy)

        # self.menu_file.add_command(label='New', command=self.cmd_dummy, accelerator="Command-N")
        # self.menu_file.add_command(label='Open...', command=self.cmd_dummy)
        # self.menu_file.add_command(label='Close', command=self.cmd_dummy)

        self.menu_test.add_command(label='Run all', command=self.cmd_run_all)
        self.menu_test.add_command(label='Run selected tests', command=self.cmd_run_selected)
        self.menu_test.add_command(label='Re-run failed tests', command=self.cmd_rerun)

        self.menu_beeware.add_command(label='Open Duvet...', command=self.cmd_open_duvet, state=DISABLED if duvet is None else ACTIVE)

        self.menu_help.add_command(label='Open Documentation', command=self.cmd_cricket_docs)
        self.menu_help.add_command(label='Open Cricket project page', command=self.cmd_cricket_page)
        self.menu_help.add_command(label='Open Cricket on GitHub', command=self.cmd_cricket_github)
        self.menu_help.add_command(label='Open BeeWare project page', command=self.cmd_beeware_page)

        # last step - configure the menubar
        self.root['menu'] = self.menubar

    def _setup_button_toolbar(self):
        '''
        The button toolbar runs as a horizontal area at the top of the GUI.
        It is a persistent GUI component
        '''

        # Main toolbar
        self.toolbar = Frame(self.root)
        self.toolbar.grid(column=0, row=0, sticky=(W, E))

        # Buttons on the toolbar
        self.stop_button = Button(self.toolbar, text='Stop', command=self.cmd_stop, state=DISABLED)
        self.stop_button.grid(column=0, row=0)

        self.run_all_button = Button(self.toolbar, text='Run all', command=self.cmd_run_all)
        self.run_all_button.grid(column=1, row=0)

        self.run_selected_button = Button(self.toolbar, text='Run selected',
                                          command=self.cmd_run_selected,
                                          state=DISABLED)
        self.run_selected_button.grid(column=2, row=0)

        self.rerun_button = Button(self.toolbar, text='Re-run', command=self.cmd_rerun, state=DISABLED)
        self.rerun_button.grid(column=3, row=0)

        self.coverage = StringVar()
        self.coverage_checkbox = Checkbutton(self.toolbar,
                                             text='Generate coverage',
                                             command=self.on_coverageChange,
                                             variable=self.coverage)
        self.coverage_checkbox.grid(column=4, row=0)

        # If coverage is available, enable it by default.
        # Otherwise, disable the widget
        if coverage:
            self.coverage.set('1')
        else:
            self.coverage.set('0')
            self.coverage_checkbox.configure(state=DISABLED)

        self.toolbar.columnconfigure(0, weight=0)
        self.toolbar.rowconfigure(0, weight=1)

    def _setup_main_content(self):
        '''
        Sets up the main content area. It is a persistent GUI component
        '''

        # Main content area
        self.content = PanedWindow(self.root, orient=HORIZONTAL)
        self.content.grid(column=0, row=1, sticky=(N, S, E, W))

        # Create the tree/control area on the left frame
        self._setup_left_frame()
        self._setup_all_tests_tree()
        self._setup_problem_tests_tree()

        # Create the output/viewer area on the right frame
        self._setup_right_frame()

        # Set up weights for the left frame's content
        self.content.columnconfigure(0, weight=1)
        self.content.rowconfigure(0, weight=1)

        self.content.pane(0, weight=1)
        self.content.pane(1, weight=2)

    def _setup_left_frame(self):
        '''
        The left frame mostly consists of the tree widget
        '''

        # The left-hand side frame on the main content area
        # The tabs for the two trees
        self.tree_notebook = Notebook(self.content, padding=(0, 5, 0, 5))
        self.content.add(self.tree_notebook)

    def _setup_all_tests_tree(self):
        # The tree for all tests
        self.all_tests_tree_frame = Frame(self.content)
        self.all_tests_tree_frame.grid(column=0, row=0, sticky=(N, S, E, W))
        self.tree_notebook.add(self.all_tests_tree_frame, text='All tests')

        self.all_tests_tree = Treeview(self.all_tests_tree_frame)
        self.all_tests_tree.grid(column=0, row=0, sticky=(N, S, E, W))

        # Set up the tag colors for tree nodes.
        for status, config in STATUS.items():
            self.all_tests_tree.tag_configure(config['tag'], foreground=config['color'])
        self.all_tests_tree.tag_configure('inactive', foreground='lightgray')

        # Listen for button clicks on tree nodes
        self.all_tests_tree.tag_bind('TestModule', '<Double-Button-1>', self.on_testModuleClicked)
        self.all_tests_tree.tag_bind('TestCase', '<Double-Button-1>', self.on_testCaseClicked)
        self.all_tests_tree.tag_bind('TestMethod', '<Double-Button-1>', self.on_testMethodClicked)

        self.all_tests_tree.tag_bind('TestModule', '<<TreeviewSelect>>', self.on_testModuleSelected)
        self.all_tests_tree.tag_bind('TestCase', '<<TreeviewSelect>>', self.on_testCaseSelected)
        self.all_tests_tree.tag_bind('TestMethod', '<<TreeviewSelect>>', self.on_testMethodSelected)

        # The tree's vertical scrollbar
        self.all_tests_tree_scrollbar = Scrollbar(self.all_tests_tree_frame, orient=VERTICAL)
        self.all_tests_tree_scrollbar.grid(column=1, row=0, sticky=(N, S))

        # Tie the scrollbar to the text views, and the text views
        # to each other.
        self.all_tests_tree.config(yscrollcommand=self.all_tests_tree_scrollbar.set)
        self.all_tests_tree_scrollbar.config(command=self.all_tests_tree.yview)

        # Setup weights for the "All Tests" tree
        self.all_tests_tree_frame.columnconfigure(0, weight=1)
        self.all_tests_tree_frame.columnconfigure(1, weight=0)
        self.all_tests_tree_frame.rowconfigure(0, weight=1)

    def _setup_problem_tests_tree(self):
        # The tree for problem tests
        self.problem_tests_tree_frame = Frame(self.content)
        self.problem_tests_tree_frame.grid(column=0, row=0, sticky=(N, S, E, W))
        self.tree_notebook.add(self.problem_tests_tree_frame, text='Problems')

        self.problem_tests_tree = Treeview(self.problem_tests_tree_frame)
        self.problem_tests_tree.grid(column=0, row=0, sticky=(N, S, E, W))

        # Set up the tag colors for tree nodes.
        for status, config in STATUS.items():
            self.problem_tests_tree.tag_configure(config['tag'], foreground=config['color'])
        self.problem_tests_tree.tag_configure('inactive', foreground='lightgray')

        # Problem tree only deals with selection, not clicks.
        self.problem_tests_tree.tag_bind('TestModule', '<<TreeviewSelect>>', self.on_testModuleSelected)
        self.problem_tests_tree.tag_bind('TestCase', '<<TreeviewSelect>>', self.on_testCaseSelected)
        self.problem_tests_tree.tag_bind('TestMethod', '<<TreeviewSelect>>', self.on_testMethodSelected)

        # The tree's vertical scrollbar
        self.problem_tests_tree_scrollbar = Scrollbar(self.problem_tests_tree_frame, orient=VERTICAL)
        self.problem_tests_tree_scrollbar.grid(column=1, row=0, sticky=(N, S))

        # Tie the scrollbar to the text views, and the text views
        # to each other.
        self.problem_tests_tree.config(yscrollcommand=self.problem_tests_tree_scrollbar.set)
        self.problem_tests_tree_scrollbar.config(command=self.all_tests_tree.yview)

        # Setup weights for the problems tree
        self.problem_tests_tree_frame.columnconfigure(0, weight=1)
        self.problem_tests_tree_frame.columnconfigure(1, weight=0)
        self.problem_tests_tree_frame.rowconfigure(0, weight=1)

    def _setup_right_frame(self):
        '''
        The right frame is basically the "output viewer" space
        '''

        # The right-hand side frame on the main content area
        self.details_frame = Frame(self.content)
        self.details_frame.grid(column=0, row=0, sticky=(N, S, E, W))
        self.content.add(self.details_frame)

        # Set up the content in the details panel
        # Test Name
        self.name_label = Label(self.details_frame, text='Name:')
        self.name_label.grid(column=0, row=0, pady=5, sticky=(E,))

        self.name = StringVar()
        self.name_widget = Entry(self.details_frame, textvariable=self.name)
        self.name_widget.configure(state='readonly')
        self.name_widget.grid(column=1, row=0, pady=5, sticky=(W, E))

        # Test status
        self.test_status = StringVar()
        self.test_status_widget = Label(self.details_frame, textvariable=self.test_status, width=1, anchor=CENTER)
        f = Font(font=self.test_status_widget['font'])
        f['weight'] = 'bold'
        f['size'] = 50
        self.test_status_widget.config(font=f)
        self.test_status_widget.grid(column=2, row=0, padx=15, pady=5, rowspan=2, sticky=(N, W, E, S))

        # Test duration
        self.duration_label = Label(self.details_frame, text='Duration:')
        self.duration_label.grid(column=0, row=1, pady=5, sticky=(E,))

        self.duration = StringVar()
        self.duration_widget = Entry(self.details_frame, textvariable=self.duration)
        self.duration_widget.grid(column=1, row=1, pady=5, sticky=(E, W,))

        # Test description
        self.description_label = Label(self.details_frame, text='Description:')
        self.description_label.grid(column=0, row=2, pady=5, sticky=(N, E,))

        self.description = ReadOnlyText(self.details_frame, width=80, height=4)
        self.description.grid(column=1, row=2, pady=5, columnspan=2, sticky=(N, S, E, W,))

        self.description_scrollbar = Scrollbar(self.details_frame, orient=VERTICAL)
        self.description_scrollbar.grid(column=3, row=2, pady=5, sticky=(N, S))
        self.description.config(yscrollcommand=self.description_scrollbar.set)
        self.description_scrollbar.config(command=self.description.yview)

        # Test output
        self.output_label = Label(self.details_frame, text='Output:')
        self.output_label.grid(column=0, row=3, pady=5, sticky=(N, E,))

        self.output = ReadOnlyText(self.details_frame, width=80, height=4)
        self.output.grid(column=1, row=3, pady=5, columnspan=2, sticky=(N, S, E, W,))

        self.output_scrollbar = Scrollbar(self.details_frame, orient=VERTICAL)
        self.output_scrollbar.grid(column=3, row=3, pady=5, sticky=(N, S))
        self.output.config(yscrollcommand=self.output_scrollbar.set)
        self.output_scrollbar.config(command=self.output.yview)

        # Error message
        self.error_label = Label(self.details_frame, text='Error:')
        self.error_label.grid(column=0, row=4, pady=5, sticky=(N, E,))

        self.error = ReadOnlyText(self.details_frame, width=80)
        self.error.grid(column=1, row=4, pady=5, columnspan=2, sticky=(N, S, E, W))

        self.error_scrollbar = Scrollbar(self.details_frame, orient=VERTICAL)
        self.error_scrollbar.grid(column=3, row=4, pady=5, sticky=(N, S))
        self.error.config(yscrollcommand=self.error_scrollbar.set)
        self.error_scrollbar.config(command=self.error.yview)

        # Set up GUI weights for the details frame
        self.details_frame.columnconfigure(0, weight=0)
        self.details_frame.columnconfigure(1, weight=1)
        self.details_frame.columnconfigure(2, weight=0)
        self.details_frame.columnconfigure(3, weight=0)
        self.details_frame.columnconfigure(4, weight=0)
        self.details_frame.rowconfigure(0, weight=0)
        self.details_frame.rowconfigure(1, weight=0)
        self.details_frame.rowconfigure(2, weight=1)
        self.details_frame.rowconfigure(3, weight=5)
        self.details_frame.rowconfigure(4, weight=10)

    def _setup_status_bar(self):
        # Status bar
        self.statusbar = Frame(self.root)
        self.statusbar.grid(column=0, row=2, sticky=(W, E))

        # Current status
        self.run_status = StringVar()
        self.run_status_label = Label(self.statusbar, textvariable=self.run_status)
        self.run_status_label.grid(column=0, row=0, sticky=(W, E))
        self.run_status.set('Not running')

        # Test result summary
        self.run_summary = StringVar()
        self.run_summary_label = Label(self.statusbar, textvariable=self.run_summary)
        self.run_summary_label.grid(column=1, row=0, sticky=(W, E))
        self.run_summary.set('T:0 P:0 F:0 E:0 X:0 U:0 S:0')

        # Test progress
        self.progress_value = IntVar()
        self.progress = Progressbar(self.statusbar, orient=HORIZONTAL, length=200, mode='determinate', maximum=100, variable=self.progress_value)
        self.progress.grid(column=2, row=0, sticky=(W, E))

        # Main window resize handle
        self.grip = Sizegrip(self.statusbar)
        self.grip.grid(column=3, row=0, sticky=(S, E))

        # Set up weights for status bar frame
        self.statusbar.columnconfigure(0, weight=1)
        self.statusbar.columnconfigure(1, weight=0)
        self.statusbar.columnconfigure(2, weight=0)
        self.statusbar.columnconfigure(3, weight=0)
        self.statusbar.rowconfigure(0, weight=1)

    ######################################################
    # Utility methods for inspecting current GUI state
    ######################################################

    @property
    def current_test_tree(self):
        "Check the tree notebook to return the currently selected tree."
        current_tree_id = self.tree_notebook.select()
        if current_tree_id == self.problem_tests_tree_frame._w:
            return self.problem_tests_tree
        else:
            return self.all_tests_tree

    ######################################################
    # Handlers for setting a new project
    ######################################################

    @property
    def project(self):
        return self._project

    def _add_test_module(self, parentNode, testModule):
        testModule_node = self.all_tests_tree.insert(
            parentNode, 'end', testModule.path,
            text=testModule.name,
            tags=['TestModule', 'active'],
            open=True)

        for subModuleName, subModule in sorted(testModule.items()):
            if isinstance(subModule, TestModule):
                self._add_test_module(testModule_node, subModule)
            else:
                testCase = subModule
                testCase_node = self.all_tests_tree.insert(
                    testModule_node, 'end', testCase.path,
                    text=testCase.name,
                    tags=['TestCase', 'active'],
                    open=True
                )

                for testMethod_name, testMethod in sorted(testCase.items()):
                    self.all_tests_tree.insert(
                        testCase_node, 'end', testMethod.path,
                        text=testMethod.name,
                        tags=['TestMethod', 'active'],
                        open=True
                    )

    @project.setter
    def project(self, project):
        self._project = project

        # Get a count of active tests to display in the status bar.
        count, labels = self.project.find_tests(True)
        self.run_summary.set('T:%s P:0 F:0 E:0 X:0 U:0 S:0' % count)

        # Populate the initial tree nodes. This is recursive, because
        # the tree could be of arbitrary depth.
        for testModule_name, testModule in sorted(project.items()):
            self._add_test_module('', testModule)

        # Listen for any state changes on nodes in the tree
        TestModule.bind('active', self.on_nodeActive)
        TestCase.bind('active', self.on_nodeActive)
        TestMethod.bind('active', self.on_nodeActive)

        TestModule.bind('inactive', self.on_nodeInactive)
        TestCase.bind('inactive', self.on_nodeInactive)
        TestMethod.bind('inactive', self.on_nodeInactive)

        # Listen for new nodes added to the tree
        TestModule.bind('new', self.on_nodeAdded)
        TestCase.bind('new', self.on_nodeAdded)
        TestMethod.bind('new', self.on_nodeAdded)

        # Listen for any status updates on nodes in the tree.
        TestMethod.bind('status_update', self.on_nodeStatusUpdate)

        # Update the project to make sure coverage status matches the GUI
        self.on_coverageChange()

    ######################################################
    # TK Main loop
    ######################################################

    def mainloop(self):
        self.root.mainloop()

    ######################################################
    # User commands
    ######################################################

    def cmd_quit(self):
        "Command: Quit"
        # If the runner is currently running, kill it.
        self.stop()

        self.root.quit()

    def cmd_stop(self, event=None):
        "Command: The stop button has been pressed"
        self.stop()

    def cmd_run_all(self, event=None):
        "Command: The Run all button has been pressed"
        # If the executor isn't currently running, we can
        # start a test run.
        if not self.executor or not self.executor.is_running:
            self.run(active=True)

    def cmd_run_selected(self, event=None):
        "Command: The 'run selected' button has been pressed"
        current_tree = self.current_test_tree

        # If a node is selected, it needs to be made active
        for path in current_tree.selection():
            parts = path.split('.')
            testModule = self.project
            for part in parts:
                testModule = testModule[part]

            testModule.set_active(True)

        # If the executor isn't currently running, we can
        # start a test run.
        if not self.executor or not self.executor.is_running:
            self.run(labels=set(current_tree.selection()))

    def cmd_rerun(self, event=None):
        "Command: The run/stop button has been pressed"
        # If the executor isn't currently running, we can
        # start a test run.
        if not self.executor or not self.executor.is_running:
            self.run(status=set(TestMethod.FAILING_STATES))

    def cmd_open_duvet(self, event=None):
        "Command: Open Duvet"
        try:
            subprocess.Popen('duvet')
        except Exception as e:
            tkMessageBox.showerror(message='Unable to start Duvet: %s' % e)

    def cmd_cricket_page(self):
        "Show the Cricket project page"
        webbrowser.open_new('http://pybee.org/cricket/')

    def cmd_beeware_page(self):
        "Show the Beeware project page"
        webbrowser.open_new('http://pybee.org/')

    def cmd_cricket_github(self):
        "Show the Cricket GitHub repo"
        webbrowser.open_new('http://github.com/pybee/cricket')

    def cmd_cricket_docs(self):
        "Show the Cricket documentation"
        webbrowser.open_new('https://cricket.readthedocs.io/')

    ######################################################
    # GUI Callbacks
    ######################################################

    def on_testModuleClicked(self, event):
        "Event handler: a module has been clicked in the tree"
        parts = event.widget.focus().split('.')
        testModule = self.project
        for part in parts:
            testModule = testModule[part]

        testModule.toggle_active()

    def on_testCaseClicked(self, event):
        "Event handler: a test case has been clicked in the tree"
        parts = event.widget.focus().split('.')
        testCase = self.project
        for part in parts:
            testCase = testCase[part]

        testCase.toggle_active()

    def on_testMethodClicked(self, event):
        "Event handler: a test case has been clicked in the tree"
        parts = event.widget.focus().split('.')
        testMethod = self.project
        for part in parts:
            testMethod = testMethod[part]

        testMethod.toggle_active()

    def on_testModuleSelected(self, event):
        "Event handler: a test module has been selected in the tree"
        self.name.set('')
        self.test_status.set('')

        self.duration.set('')
        self.description.delete('1.0', END)

        self._hide_test_output()
        self._hide_test_errors()

        # update "run selected" button enabled state
        self.set_selected_button_state()

    def on_testCaseSelected(self, event):
        "Event handler: a test case has been selected in the tree"
        self.name.set('')
        self.test_status.set('')

        self.duration.set('')
        self.description.delete('1.0', END)

        self._hide_test_output()
        self._hide_test_errors()

        # update "run selected" button enabled state
        self.set_selected_button_state()

    def on_testMethodSelected(self, event):
        "Event handler: a test case has been selected in the tree"
        if len(event.widget.selection()) == 1:
            parts = event.widget.selection()[0].split('.')

            # Find the definition for the actual test method
            # out of the project.
            testMethod = self.project
            for part in parts:
                testMethod = testMethod[part]

            self.name.set(testMethod.path)

            self.description.delete('1.0', END)
            self.description.insert('1.0', testMethod.description)

            config = STATUS.get(testMethod.status, STATUS_DEFAULT)
            self.test_status_widget.config(foreground=config['color'])
            self.test_status.set(config['symbol'])

            if testMethod._result:
                # Test has been executed
                self.duration.set('%0.2fs' % testMethod._result['duration'])

                if testMethod.output:
                    self._show_test_output(testMethod.output)
                else:
                    self._hide_test_output()

                if testMethod.error:
                    self._show_test_errors(testMethod.error)
                else:
                    self._hide_test_errors()
            else:
                # Test hasn't been executed yet.
                self.duration.set('Not executed')

                self._hide_test_output()
                self._hide_test_errors()

        else:
            # Multiple tests selected
            self.name.set('')
            self.test_status.set('')

            self.duration.set('')
            self.description.delete('1.0', END)

            self._hide_test_output()
            self._hide_test_errors()

        # update "run selected" button enabled state
        self.set_selected_button_state()

    def on_nodeAdded(self, node):
        "Event handler: a new node has been added to the tree"
        self.all_tests_tree.insert(
            node.parent.path, 'end', node.path,
            text=node.name,
            tags=[node.__class__.__name__, 'active'],
            open=True
        )

    def on_nodeActive(self, node):
        "Event handler: a node on the tree has been made active"
        self.all_tests_tree.item(node.path, tags=[node.__class__.__name__, 'active'])
        self.all_tests_tree.item(node.path, open=True)

    def on_nodeInactive(self, node):
        "Event handler: a node on the tree has been made inactive"
        self.all_tests_tree.item(node.path, tags=[node.__class__.__name__, 'inactive'])
        self.all_tests_tree.item(node.path, open=False)

    def on_nodeStatusUpdate(self, node):
        "Event handler: a node on the tree has received a status update"
        self.all_tests_tree.item(node.path, tags=['TestMethod', STATUS[node.status]['tag']])

        if node.status in TestMethod.FAILING_STATES:
            # Test is in a failing state. Make sure it is on the problem tree,
            # with the correct current status.

            parts = node.path.split('.')
            parentModule = self.project
            for pos, part in enumerate(parts):
                path = '.'.join(parts[:pos+1])
                testModule = parentModule[part]

                if not self.problem_tests_tree.exists(path):
                    self.problem_tests_tree.insert(
                        parentModule.path, 'end', testModule.path,
                        text=testModule.name,
                        tags=[testModule.__class__.__name__, 'active'],
                        open=True
                    )

                parentModule = testModule

            self.problem_tests_tree.item(node.path, tags=['TestMethod', STATUS[node.status]['tag']])
        else:
            # Test passed; if it's on the problem tree, remove it.
            if self.problem_tests_tree.exists(node.path):
                self.problem_tests_tree.delete(node.path)

                # Check all parents of this node. Recursively remove
                # any parent has no children as a result of this deletion.
                has_children = False
                node = node.parent
                while node.path and not has_children:
                    if not self.problem_tests_tree.get_children(node.path):
                        self.problem_tests_tree.delete(node.path)
                    else:
                        has_children = True
                    node = node.parent

    def on_coverageChange(self):
        "Event handler: when the coverage checkbox has been toggled"
        self.project.coverage = self.coverage.get() == '1'

    def on_testProgress(self):
        "Event handler: a periodic update to poll the runner for output, generating GUI updates"
        if self.executor and self.executor.poll():
            self.root.after(100, self.on_testProgress)

    def on_executorStatusUpdate(self, event, update):
        "The executor has some progress to report"
        # Update the status line.
        self.run_status.set(update)

    def on_executorTestStart(self, event, test_path):
        "The executor has started running a new test."
        # Update status line, and set the tree item to active.
        self.run_status.set('Running %s...' % test_path)
        self.all_tests_tree.item(test_path, tags=['TestMethod', 'active'])

    def on_executorTestEnd(self, event, test_path, result, remaining_time):
        "The executor has finished running a test."
        # Update the progress meter
        self.progress_value.set(self.progress_value.get() + 1)

        # Update the run summary
        self.run_summary.set('T:%(total)s P:%(pass)s F:%(fail)s E:%(error)s X:%(expected)s U:%(unexpected)s S:%(skip)s, ~%(remaining)s remaining' % {
            'total': self.executor.total_count,
            'pass': self.executor.result_count.get(TestMethod.STATUS_PASS, 0),
            'fail': self.executor.result_count.get(TestMethod.STATUS_FAIL, 0),
            'error': self.executor.result_count.get(TestMethod.STATUS_ERROR, 0),
            'expected': self.executor.result_count.get(TestMethod.STATUS_EXPECTED_FAIL, 0),
            'unexpected': self.executor.result_count.get(TestMethod.STATUS_UNEXPECTED_SUCCESS, 0),
            'skip': self.executor.result_count.get(TestMethod.STATUS_SKIP, 0),
            'remaining': remaining_time
        })

        # If the test that just fininshed is the one (and only one)
        # selected on the tree, update the display.
        current_tree = self.current_test_tree
        if len(current_tree.selection()) == 1:
            # One test selected.
            if current_tree.selection()[0] == test_path:
                # If the test that just finished running is the selected
                # test, force reset the selection, which will generate a
                # selection event, forcing a refresh of the result page.
                current_tree.selection_set(current_tree.selection())
        else:
            # No or Multiple tests selected
            self.name.set('')
            self.test_status.set('')

            self.duration.set('')
            self.description.delete('1.0', END)

            self._hide_test_output()
            self._hide_test_errors()

    def on_executorSuiteEnd(self, event, error=None):
        "The test suite finished running."
        # Display the final results
        self.run_status.set('Finished.')

        if error:
            TestErrorsDialog(self.root, error)

        if self.executor.any_failed:
            dialog = tkMessageBox.showerror
        else:
            dialog = tkMessageBox.showinfo

        message = ', '.join(
            '%d %s' % (count, TestMethod.STATUS_LABELS[state])
            for state, count in sorted(self.executor.result_count.items()))

        dialog(message=message or 'No tests were ran')

        # Reset the running summary.
        self.run_summary.set('T:%(total)s P:%(pass)s F:%(fail)s E:%(error)s X:%(expected)s U:%(unexpected)s S:%(skip)s' % {
            'total': self.executor.total_count,
            'pass': self.executor.result_count.get(TestMethod.STATUS_PASS, 0),
            'fail': self.executor.result_count.get(TestMethod.STATUS_FAIL, 0),
            'error': self.executor.result_count.get(TestMethod.STATUS_ERROR, 0),
            'expected': self.executor.result_count.get(TestMethod.STATUS_EXPECTED_FAIL, 0),
            'unexpected': self.executor.result_count.get(TestMethod.STATUS_UNEXPECTED_SUCCESS, 0),
            'skip': self.executor.result_count.get(TestMethod.STATUS_SKIP, 0),
        })

        # Reset the buttons
        self.reset_button_states_on_end()

        # Drop the reference to the executor
        self.executor = None

    def on_executorSuiteError(self, event, error):
        "An error occurred running the test suite."
        # Display the error in a dialog
        self.run_status.set('Error running test suite.')
        FailedTestDialog(self.root, error)

        # Reset the buttons
        self.reset_button_states_on_end()

        # Drop the reference to the executor
        self.executor = None

    def reset_button_states_on_end(self):
        "A test run has ended and we should enable or disable buttons as appropriate."
        self.stop_button.configure(state=DISABLED)
        self.run_all_button.configure(state=NORMAL)
        self.set_selected_button_state()
        if self.executor and self.executor.any_failed:
            self.rerun_button.configure(state=NORMAL)
        else:
            self.rerun_button.configure(state=DISABLED)

    def set_selected_button_state(self):
        if self.executor and self.executor.is_running:
            self.run_selected_button.configure(state=DISABLED)
        elif self.current_test_tree.selection():
            self.run_selected_button.configure(state=NORMAL)
        else:
            self.run_selected_button.configure(state=DISABLED)

    ######################################################
    # GUI utility methods
    ######################################################

    def run(self, active=True, status=None, labels=None):
        """Run the test suite.

        If active=True, only active tests will be run.
        If status is provided, only tests whose most recent run
            status matches the set provided will be executed.
        If labels is provided, only tests with those labels will
            be executed
        """
        count, labels = self.project.find_tests(active, status, labels)
        self.run_status.set('Running...')
        self.run_summary.set('T:%s P:0 F:0 E:0 X:0 U:0 S:0' % count)

        self.stop_button.configure(state=NORMAL)
        self.run_all_button.configure(state=DISABLED)
        self.run_selected_button.configure(state=DISABLED)
        self.rerun_button.configure(state=DISABLED)

        self.progress['maximum'] = count
        self.progress_value.set(0)

        # Create the runner
        self.executor = Executor(self.project, count, labels)

        # Queue the first progress handling event
        self.root.after(100, self.on_testProgress)

    def stop(self):
        "Stop the test suite."
        if self.executor and self.executor.is_running:
            self.run_status.set('Stopping...')

            self.executor.terminate()
            self.executor = None

            self.run_status.set('Stopped.')

            self.reset_button_states_on_end()

    def _hide_test_output(self):
        "Hide the test output panel on the test results page"
        self.output_label.grid_remove()
        self.output.grid_remove()
        self.output_scrollbar.grid_remove()
        self.details_frame.rowconfigure(3, weight=0)

    def _show_test_output(self, content):
        "Show the test output panel on the test results page"
        self.output.delete('1.0', END)
        self.output.insert('1.0', content)

        self.output_label.grid()
        self.output.grid()
        self.output_scrollbar.grid()
        self.details_frame.rowconfigure(3, weight=5)

    def _hide_test_errors(self):
        "Hide the test error panel on the test results page"
        self.error_label.grid_remove()
        self.error.grid_remove()
        self.error_scrollbar.grid_remove()

    def _show_test_errors(self, content):
        "Show the test error panel on the test results page"
        self.error.delete('1.0', END)
        self.error.insert('1.0', content)

        self.error_label.grid()
        self.error.grid()
        self.error_scrollbar.grid()
Exemple #9
0
    def _setup_right_frame(self):
        '''
        The right frame is basically the "output viewer" space
        '''

        # The right-hand side frame on the main content area
        self.details_frame = Frame(self.content)
        self.details_frame.grid(column=0, row=0, sticky=(N, S, E, W))
        self.content.add(self.details_frame)

        # Set up the content in the details panel
        # Test Name
        self.name_label = Label(self.details_frame, text='Name:')
        self.name_label.grid(column=0, row=0, pady=5, sticky=(E,))

        self.name = StringVar()
        self.name_widget = Entry(self.details_frame, textvariable=self.name)
        self.name_widget.configure(state='readonly')
        self.name_widget.grid(column=1, row=0, pady=5, sticky=(W, E))

        # Test status
        self.test_status = StringVar()
        self.test_status_widget = Label(self.details_frame, textvariable=self.test_status, width=1, anchor=CENTER)
        f = Font(font=self.test_status_widget['font'])
        f['weight'] = 'bold'
        f['size'] = 50
        self.test_status_widget.config(font=f)
        self.test_status_widget.grid(column=2, row=0, padx=15, pady=5, rowspan=2, sticky=(N, W, E, S))

        # Test duration
        self.duration_label = Label(self.details_frame, text='Duration:')
        self.duration_label.grid(column=0, row=1, pady=5, sticky=(E,))

        self.duration = StringVar()
        self.duration_widget = Entry(self.details_frame, textvariable=self.duration)
        self.duration_widget.grid(column=1, row=1, pady=5, sticky=(E, W,))

        # Test description
        self.description_label = Label(self.details_frame, text='Description:')
        self.description_label.grid(column=0, row=2, pady=5, sticky=(N, E,))

        self.description = ReadOnlyText(self.details_frame, width=80, height=4)
        self.description.grid(column=1, row=2, pady=5, columnspan=2, sticky=(N, S, E, W,))

        self.description_scrollbar = Scrollbar(self.details_frame, orient=VERTICAL)
        self.description_scrollbar.grid(column=3, row=2, pady=5, sticky=(N, S))
        self.description.config(yscrollcommand=self.description_scrollbar.set)
        self.description_scrollbar.config(command=self.description.yview)

        # Test output
        self.output_label = Label(self.details_frame, text='Output:')
        self.output_label.grid(column=0, row=3, pady=5, sticky=(N, E,))

        self.output = ReadOnlyText(self.details_frame, width=80, height=4)
        self.output.grid(column=1, row=3, pady=5, columnspan=2, sticky=(N, S, E, W,))

        self.output_scrollbar = Scrollbar(self.details_frame, orient=VERTICAL)
        self.output_scrollbar.grid(column=3, row=3, pady=5, sticky=(N, S))
        self.output.config(yscrollcommand=self.output_scrollbar.set)
        self.output_scrollbar.config(command=self.output.yview)

        # Error message
        self.error_label = Label(self.details_frame, text='Error:')
        self.error_label.grid(column=0, row=4, pady=5, sticky=(N, E,))

        self.error = ReadOnlyText(self.details_frame, width=80)
        self.error.grid(column=1, row=4, pady=5, columnspan=2, sticky=(N, S, E, W))

        self.error_scrollbar = Scrollbar(self.details_frame, orient=VERTICAL)
        self.error_scrollbar.grid(column=3, row=4, pady=5, sticky=(N, S))
        self.error.config(yscrollcommand=self.error_scrollbar.set)
        self.error_scrollbar.config(command=self.error.yview)

        # Set up GUI weights for the details frame
        self.details_frame.columnconfigure(0, weight=0)
        self.details_frame.columnconfigure(1, weight=1)
        self.details_frame.columnconfigure(2, weight=0)
        self.details_frame.columnconfigure(3, weight=0)
        self.details_frame.columnconfigure(4, weight=0)
        self.details_frame.rowconfigure(0, weight=0)
        self.details_frame.rowconfigure(1, weight=0)
        self.details_frame.rowconfigure(2, weight=1)
        self.details_frame.rowconfigure(3, weight=5)
        self.details_frame.rowconfigure(4, weight=10)
Exemple #10
0
class MainWindow(object):
    def __init__(self, root, options):
        '''
        -----------------------------------------------------
        | main button toolbar                               |
        -----------------------------------------------------
        |       < ma | in content area >                    |
        |            |                                      |
        | File list  | File name                            |
        |            |                                      |
        -----------------------------------------------------
        |     status bar area                               |
        -----------------------------------------------------

        '''

        # Obtain and expand the current working directory.
        base_path = os.path.abspath(os.getcwd())
        self.base_path = os.path.normcase(base_path)

        # Create a filename normalizer based on the CWD.
        self.filename_normalizer = filename_normalizer(self.base_path)

        # Root window
        self.root = root
        self.root.title('Galley')
        self.root.geometry('1024x768')

        # Prevent the menus from having the empty tearoff entry
        self.root.option_add('*tearOff', FALSE)
        # Catch the close button
        self.root.protocol("WM_DELETE_WINDOW", self.cmd_quit)
        # Catch the "quit" event.
        self.root.createcommand('exit', self.cmd_quit)

        # The browsing history.
        self._history = []
        self._history_index = 0
        self._traversing_history = False

        # The default source file extension. This will be updated once we have
        # parsed the project config file.
        self.source_extension = '.rst'

        # Known warnings, indexed by source file.
        self.warning_output = {}

        # Setup the menu
        self._setup_menubar()

        # Set up the main content for the window.
        self._setup_button_toolbar()
        self._setup_main_content()
        self._setup_status_bar()

        # Now configure the weights for the root frame
        self.root.columnconfigure(0, weight=1)
        self.root.rowconfigure(0, weight=0)
        self.root.rowconfigure(1, weight=1)
        self.root.rowconfigure(2, weight=0)

        # Set up a background worker thread to build docs.
        self.work_queue = Queue()
        self.results_queue = Queue()
        self.worker_thread = threading.Thread(target=sphinx_worker, args=(os.path.join(self.base_path, 'docs'), self.work_queue, self.results_queue))
        self.worker_thread.daemon = True
        self.worker_thread.start()

        # Set up a background monitor thread.
        self.stop_event = threading.Event()
        self.monitor_thread = threading.Thread(target=file_monitor, args=(os.path.join(self.base_path, 'docs'), self.stop_event, self.results_queue))
        self.monitor_thread.daemon = True
        self.monitor_thread.start()

        # Requeue for another update in 40ms (24*40ms == 1s - so this is
        # as fast as we need to update to match human visual acuity)
        self.root.after(40, self.handle_background_tasks)


    ######################################################
    # Internal GUI layout methods.
    ######################################################

    def _setup_menubar(self):
        # Menubar
        self.menubar = Menu(self.root)

        # self.menu_Apple = Menu(self.menubar, name='Apple')
        # self.menubar.add_cascade(menu=self.menu_Apple)

        self.menu_file = Menu(self.menubar)
        self.menubar.add_cascade(menu=self.menu_file, label='File')

        self.menu_help = Menu(self.menubar)
        self.menubar.add_cascade(menu=self.menu_help, label='Help')

        # self.menu_Apple.add_command(label='Test', command=self.cmd_dummy)

        # self.menu_file.add_command(label='New', command=self.cmd_dummy, accelerator="Command-N")
        # self.menu_file.add_command(label='Close', command=self.cmd_dummy)

        self.menu_help.add_command(label='Open Documentation', command=self.cmd_galley_docs)
        self.menu_help.add_command(label='Open Galley project page', command=self.cmd_galley_page)
        self.menu_help.add_command(label='Open Galley on GitHub', command=self.cmd_galley_github)
        self.menu_help.add_command(label='Open BeeWare project page', command=self.cmd_beeware_page)

        # last step - configure the menubar
        self.root['menu'] = self.menubar

    def _setup_button_toolbar(self):
        '''
        The button toolbar runs as a horizontal area at the top of the GUI.
        It is a persistent GUI component
        '''

        # Main toolbar
        self.toolbar = Frame(self.root)
        self.toolbar.grid(column=0, row=0, sticky=(W, E))

        # Buttons on the toolbar
        self.back_button = Button(self.toolbar, text='◀', command=self.cmd_back, state=DISABLED)
        self.back_button.grid(column=0, row=0)

        self.forward_button = Button(self.toolbar, text='▶', command=self.cmd_forward, state=DISABLED)
        self.forward_button.grid(column=1, row=0)

        self.rebuild_all_button = Button(self.toolbar, text='Rebuild all', command=self.cmd_rebuild_all, state=DISABLED)
        self.rebuild_all_button.grid(column=2, row=0)

        self.rebuild_file_button = Button(self.toolbar, text='Rebuild', command=self.cmd_rebuild_file, state=DISABLED)
        self.rebuild_file_button.grid(column=3, row=0)

        self.reload_config_button = Button(self.toolbar, text='Reload', command=self.cmd_reload_config, state=DISABLED)
        self.reload_config_button.grid(column=4, row=0)

        self.toolbar.columnconfigure(0, weight=0)
        self.toolbar.rowconfigure(0, weight=0)

    def _setup_main_content(self):
        '''
        Sets up the main content area. It is a persistent GUI component
        '''

        # Main content area
        self.content = PanedWindow(self.root, orient=HORIZONTAL)
        self.content.grid(column=0, row=1, sticky=(N, S, E, W))

        # Create the tree/control area on the file frame
        self._setup_project_file_tree()

        # Create the output/viewer area on the content frame
        self._setup_html_area()

        # Set up weights for the left frame's content
        self.content.columnconfigure(0, weight=1)
        self.content.rowconfigure(0, weight=1)

        self.content.pane(0, weight=1)
        self.content.pane(1, weight=4)

    def _setup_project_file_tree(self):

        self.project_file_tree_frame = Frame(self.content)
        self.project_file_tree_frame.grid(column=0, row=0, sticky=(N, S, E, W))

        self.project_file_tree = FileView(self.project_file_tree_frame, normalizer=self.filename_normalizer, root=os.path.join(self.base_path, 'docs'))
        self.project_file_tree.grid(column=0, row=0, sticky=(N, S, E, W))

        # # The tree's vertical scrollbar
        self.project_file_tree_scrollbar = Scrollbar(self.project_file_tree_frame, orient=VERTICAL)
        self.project_file_tree_scrollbar.grid(column=1, row=0, sticky=(N, S))

        # # Tie the scrollbar to the text views, and the text views
        # # to each other.
        self.project_file_tree.config(yscrollcommand=self.project_file_tree_scrollbar.set)
        self.project_file_tree_scrollbar.config(command=self.project_file_tree.yview)

        # Setup weights for the "project_file_tree" tree
        self.project_file_tree_frame.columnconfigure(0, weight=1)
        self.project_file_tree_frame.columnconfigure(1, weight=0)
        self.project_file_tree_frame.rowconfigure(0, weight=1)

        # Handlers for GUI events
        self.project_file_tree.bind('<<TreeviewSelect>>', self.on_file_selected)

        self.content.add(self.project_file_tree_frame)

    def _setup_html_area(self):
        self.html_frame = Frame(self.content)
        self.html_frame.grid(column=1, row=0, sticky=(N, S, E, W))

        # Label for current file
        self.current_file = StringVar()
        self.current_file_label = Label(self.html_frame, textvariable=self.current_file)
        self.current_file_label.grid(column=0, row=0, columnspan=3, sticky=(W, E))

        # Code display area
        self.html = SimpleHTMLView(self.html_frame)
        self.html.grid(column=0, row=1, columnspan=3, sticky=(N, S, E, W))

        self.html.link_bind('<1>', self.on_link_click)

        # Warnings
        self.warnings_label = Label(self.html_frame, text='Warnings:')
        self.warnings_label.grid(column=0, row=2, pady=5, sticky=(N, E,))

        self.warnings = ReadOnlyText(self.html_frame, height=6)
        self.warnings.grid(column=1, row=2, pady=5, columnspan=2, sticky=(N, S, E, W,))
        self.warnings.tag_configure('warning', wrap=WORD, lmargin1=5, lmargin2=20, spacing1=2, spacing3=2)
        self.warnings_scrollbar = Scrollbar(self.html_frame, orient=VERTICAL)
        self.warnings_scrollbar.grid(column=2, row=2, pady=5, sticky=(N, S))
        self.warnings.config(yscrollcommand=self.warnings_scrollbar.set)
        self.warnings_scrollbar.config(command=self.warnings.yview)

        # Set up weights for the html frame's content
        self.html_frame.columnconfigure(0, weight=0)
        self.html_frame.columnconfigure(1, weight=1)
        self.html_frame.columnconfigure(2, weight=0)
        self.html_frame.rowconfigure(0, weight=0)
        self.html_frame.rowconfigure(1, weight=4)
        self.html_frame.rowconfigure(2, weight=1)

        self.content.add(self.html_frame)

    def _setup_status_bar(self):
        # Status bar
        self.statusbar = Frame(self.root)
        self.statusbar.grid(column=0, row=2, sticky=(W, E))

        # Current status
        self.run_status = StringVar()
        self.run_status_label = Label(self.statusbar, textvariable=self.run_status)
        self.run_status_label.grid(column=0, row=0, sticky=(W, E))
        self.run_status.set('Not running')

        # Progress bar; initially started, because we don't know how long initialization will take.
        self.progress_value = IntVar()
        # self.progress = Progressbar(self.statusbar, orient=HORIZONTAL, length=200, mode='indeterminate', maximum=100, variable=self.progress_value)
        self.progress = Progressbar(self.statusbar, orient=HORIZONTAL, length=200, mode='indeterminate')
        self.progress.grid(column=1, row=0, sticky=(W, E))

        # Main window resize handle
        self.grip = Sizegrip(self.statusbar)
        self.grip.grid(column=2, row=0, sticky=(S, E))

        # Set up weights for status bar frame
        self.statusbar.columnconfigure(0, weight=1)
        self.statusbar.columnconfigure(1, weight=0)
        self.statusbar.columnconfigure(2, weight=0)
        self.statusbar.rowconfigure(0, weight=0)

    ######################################################
    # Utility methods for controlling content
    ######################################################

    def show_file(self, filename, anchor=None):
        """Show the content of the nominated file.

        If specified, bookmark is the HTML href anchor to display. If the
        anchor isn't currently visible, the window will be scrolled until
        it is.
        """
        # TEMP: Rework into HTML view
        path, ext = os.path.splitext(filename)
        compiled_filename = path.replace(os.path.join(self.base_path, 'docs'), os.path.join(self.base_path, 'docs', '_build', 'json')) + '.fjson'

        # Set the filename label for the current file
        self.current_file.set(self.filename_normalizer(filename))

        try:
            # Update the html view; this means changing the displayed file
            # if necessary, and updating the current line.
            if filename != self.html.filename:
                self.html.filename = compiled_filename

            # self.html.anchor = anchor

            # Show the warnings panel (if needed)
            self._show_warnings(filename)

            # Add this file to history.
            path, ext = os.path.splitext(filename)
            path = path.replace('docs/_build/json', 'docs')

            # History traversal is a temporary operation. If we're traversing
            # history, we won't push this onto the stack... but only this once.
            # Traversal state is reset immediately afterwards.
            if not self._traversing_history:
                if self._history_index:
                    self.forward_button.configure(state=DISABLED)
                    self.back_button.configure(state=NORMAL)

                self._history = self._history[:self._history_index] + [path + self.source_extension]
                self._history_index = self._history_index + 1
            else:
                self._traversing_history = False

        except IOError:
            tkMessageBox.showerror(message='%s has not been compiled to HTML' % self.filename_normalizer(filename))

    def _show_warnings(self, filename):
        "Show the warnings output panel"

        # Build a list of all displayed warnings
        warnings = []
        # First, the global warnings
        for (lineno, warning) in self.warning_output.get(None, []):
            if lineno:
                warnings.append(u'○ Line %s: %s' % (lineno, warning))
            else:
                warnings.append(u'○ %s' % warning)

        # Then, the file specific warnings.
        for (lineno, warning) in self.warning_output.get(filename, []):
            if lineno:
                warnings.append(u'● Line %s: %s' % (lineno, warning))
            else:
                warnings.append(u'● %s' % warning)

        # If there are warnings, show the widget, and populate it.
        # Otherwise, hide the widget.
        self.warnings.delete('1.0', END)
        for warning in warnings:
            self.warnings.insert(END, warning, 'warning')
            self.warnings.insert(END, '\n')


    ######################################################
    # TK Main loop
    ######################################################

    def mainloop(self):
        self.root.mainloop()

    def handle_background_tasks(self):
        "Background queue handler"
        try:
            while True:
                result = self.results_queue.get(block=False)

                ########################
                # Output from the worker
                ########################

                if isinstance(result, Output):
                    self.run_status.set(result.message.capitalize())

                elif isinstance(result, WarningOutput):
                    if result.filename:
                        source_file = os.path.join(self.base_path, 'docs', result.filename)
                        self.project_file_tree.item(source_file, tags=['file', 'warning'])

                    # Archive the warning.
                    self.warning_output.setdefault(result.filename, []).append((result.lineno, result.message))

                elif isinstance(result, InitializationStart):
                    # Handle the "Start of sphinx init" message
                    self.progress.configure(mode='indeterminate', variable=None, maximum=None)
                    self.rebuild_all_button.configure(state=DISABLED)
                    self.rebuild_file_button.configure(state=DISABLED)
                    self.reload_config_button.configure(state=DISABLED)
                    self.progress.start()

                elif isinstance(result, InitializationEnd):
                    # Handle the "End of Sphinx init" message.
                    # Stop the progress spinner, and activate the work buttons.
                    self.run_status.set('Sphinx initialized.')
                    self.progress.stop()

                    self.rebuild_all_button.configure(state=ACTIVE)
                    self.rebuild_file_button.configure(state=ACTIVE)
                    self.reload_config_button.configure(state=ACTIVE)

                    # We can now inspect the extension type from the sphinx config.
                    self.source_extension = result.extension

                    # Set the initial file
                    self.project_file_tree.selection_set(os.path.join(self.base_path, 'docs', 'index' + self.source_extension))

                elif isinstance(result, BuildStart):
                    # Build start; set up the progress bar, set initial progress to 0
                    self.progress_value.set(0)
                    self.progress.configure(mode='determinate', maximum=100, variable=self.progress_value)

                    # Disable all the buttons so no new commands can be issued
                    self.rebuild_all_button.configure(state=DISABLED)
                    self.rebuild_file_button.configure(state=DISABLED)
                    self.reload_config_button.configure(state=DISABLED)

                    if result.filenames is None:
                        # Build is for all files. Clear the warnings, and
                        # set all files as dirty.
                        filenames = self.project_file_tree.tag_has('file')

                        self.warning_output = {}
                    else:
                        # Build is for a selection of files. Clear the global warnings
                        # and the file warnings, and set selected files as dirty.
                        filenames = result.filenames

                        self.warning_output[None] = []
                        for f in filenames:
                            self.warning_output[f] = []

                    for f in filenames:
                        self.project_file_tree.item(f, tags=['file', 'dirty'])

                elif isinstance(result, Progress):
                    try:
                        base, max_val = {
                            # Progress messages that will be received from a build.
                            # The returned values is a tuple, consisting of:
                            #  * The overall progress value when this task is at 0%
                            #  * The delta that will be added when the task is 100%
                            'reading sources': (0, 30),
                            'looking for now-outdated files': (30, 2),
                            'pickling environment': (32, 2),
                            'checking consistency': (34, 2),
                            'preparing documents': (36, 2),
                            'writing output': (38, 30),
                            'writing additional files': (68, 2),
                            'copying images': (70, 20),
                            'copying downloadable files': (90, 2),
                            'copying static files': (92, 2),
                            'dumping search index': (94, 2),
                            'dumping object inventory': (96, 2),
                            'writing templatebuiltins.js': (98, 2),
                        }[result.stage]

                        progress = int(base + max_val * result.progress / 100.0)
                        self.progress_value.set(progress)

                        # If this is a 'writing output' update, we have a file generated
                        # so update the markup of the tree
                        if result.stage == 'writing output':
                            source_file = os.path.join(self.base_path, 'docs', result.context + self.source_extension)
                            if not self.project_file_tree.tag_has('warning', source_file):
                                self.project_file_tree.item(source_file, tags=['file'])

                    except KeyError:
                        pass

                elif isinstance(result, BuildEnd):
                    # Build complete; mark progress as 100%
                    self.progress_value.set(100)

                    # Disable all the buttons so no new commands can be issued
                    self.rebuild_all_button.configure(state=ACTIVE)
                    self.rebuild_file_button.configure(state=ACTIVE)
                    self.reload_config_button.configure(state=ACTIVE)

                    current_file = self.project_file_tree.selection()[0]
                    if result.filenames is None or current_file in result.filenames:
                        self.html.refresh()
                        self._show_warnings(current_file)

                #########################
                # Output from the monitor
                #########################

                elif isinstance(result, FileChange):
                    # Make sure the new files are in the tree
                    for f in result.new:
                        dirname, filename = os.path.split(f)
                        self.project_file_tree.insert_dirname(dirname)
                        self.project_file_tree.insert_filename(dirname, filename)

                    # Enqueue a build task for all the new and modified documents.
                    self.work_queue.put(BuildSpecific(result.new + result.modified))

        except Empty:
            # queue.get() raises an exception when the queue is empty.
            # This means there is no more output to consume at this time.
            pass

        # Requeue for another update in 40ms (24*40ms == 1s - so this is
        # as fast as we need to update to match human visual acuity)
        self.root.after(40, self.handle_background_tasks)

    ######################################################
    # TK Command handlers
    ######################################################

    def cmd_quit(self):
        "Quit the program"
        # Notify the worker and monitor threads that we want to quit
        self.work_queue.put(Quit())
        self.stop_event.set()

        # Wait for the threads to die.
        self.worker_thread.join()
        self.monitor_thread.join()

        # Quit the main app.
        self.root.quit()

    def cmd_back(self, event=None):
        "Move back on the history stack"
        # We're traversing history, so flag it.
        self._traversing_history = True

        # Move back into history
        self._history_index = self._history_index - 1
        self.project_file_tree.selection_set(self._history[self._history_index - 1])

        # Update button state
        if self._history_index == 1:
            self.back_button.configure(state=DISABLED)
        self.forward_button.configure(state=NORMAL)

    def cmd_forward(self, event=None):
        "Move forward on the history stack"
        self._traversing_history = True
        self._history_index = self._history_index + 1
        self.project_file_tree.selection_set(self._history[self._history_index - 1])

        # Update button state
        if self._history_index == len(self._history):
            self.forward_button.configure(state=DISABLED)
        self.back_button.configure(state=NORMAL)

    def cmd_rebuild_all(self, event=None):
        "Rebuild the project."
        self.work_queue.put(BuildAll())

    def cmd_rebuild_file(self, event=None):
        "Rebuild the current file."
        # Determine the currently selected file
        filename = self.project_file_tree.selection()[0]

        # If the currently selected item is a file, build it.
        if filename and os.path.isfile(filename):
            self.work_queue.put(BuildSpecific([filename]))

    def cmd_reload_config(self, event=None):
        "Rebuild the current file."
        # Determine the currently selected file
        self.work_queue.put(ReloadConfig())

    def cmd_galley_page(self):
        "Show the Galley project page"
        webbrowser.open_new('http://pybee.org/galley')

    def cmd_galley_github(self):
        "Show the Galley GitHub repo"
        webbrowser.open_new('http://github.com/pybee/galley')

    def cmd_galley_docs(self):
        "Show the Galley documentation"
        # If this is a formal release, show the docs for that
        # version. otherwise, just show the head docs.
        if len(NUM_VERSION) == 3:
            webbrowser.open_new('http://galley.readthedocs.org/en/v%s/' % VERSION)
        else:
            webbrowser.open_new('http://galley.readthedocs.org/')

    def cmd_beeware_page(self):
        "Show the BeeWare project page"
        webbrowser.open_new('http://pybee.org/')

    ######################################################
    # Handlers for GUI actions
    ######################################################

    def on_file_selected(self, event):
        "When a file is selected, highlight the file and line"
        if event.widget.selection():
            filename = event.widget.selection()[0]

            if os.path.isfile(filename):
                # Display the file in the html view
                self.show_file(filename=filename)

    def on_link_click(self, event):
        "When a link is clicked, open the new URL"
        url_parts = urlparse.urlparse(event.url)
        if url_parts.netloc and url_parts.scheme:
            webbrowser.open_new(event.url)
        else:
            # Link refers to HTML; convert back to source filename.
            path, ext = os.path.splitext(url_parts.path)
            filename = os.path.join(self.base_path, 'docs', path[:-1] + self.source_extension)
            index_filename = os.path.join(self.base_path, 'docs', path, 'index' + self.source_extension)
            if os.path.isfile(filename):
                self.project_file_tree.selection_set(filename)
            elif os.path.isfile(index_filename):
                self.project_file_tree.selection_set(index_filename)
            else:
                tkMessageBox.showerror(message="Couldn't find %s" % self.filename_normalizer(filename))
Exemple #11
0
class MainWindow(object):
    def __init__(self, root):
        self._project = None
        self.executor = None

        # Root window
        self.root = root
        self.root.title('ASD Test Interface')
        self.root.geometry('1024x768')
        self.root.option_add('*tearOff', FALSE)

        # Catch the close button
        self.root.protocol("WM_DELETE_WINDOW", self.cmd_quit)
        # Catch the "quit" event.
        self.root.createcommand('exit', self.cmd_quit)

        # Setup the menu
        self._setup_menubar()

        # Set up the main content for the window.
        self._setup_button_toolbar()
        self._setup_main_content()
        self._setup_status_bar()

        # Now configure the weights for the root frame
        self.root.columnconfigure(0, weight=1)
        self.root.rowconfigure(0, weight=0)
        self.root.rowconfigure(1, weight=1)
        self.root.rowconfigure(2, weight=0)

        # Set up listeners for runner events.
        Runner.bind('test_status_update', self.on_executorStatusUpdate)
        Runner.bind('test_start', self.on_executorTestStart)
        Runner.bind('test_end', self.on_executorTestEnd)
        Runner.bind('suite_end', self.on_executorSuiteEnd)
        Runner.bind('suite_error', self.on_executorSuiteError)

        # Now that we've laid out the grid, hide the error and output text
        # until we actually have an error/output to display
        self._hide_test_output()
        self._hide_test_errors()

    def _setup_menubar(self):
        # Menubar
        self.menubar = Menu(self.root)

        # File menubar
        self.menu_file = Menu(self.menubar)
        self.menubar.add_cascade(menu=self.menu_file, label='File')
        # self.menu_file.add_command(label='Load Run', command=self.cmd_load_run)
        # self.menu_file.add_command(label='Save Run', command=self.cmd_export_run)
        self.menu_file.add_command(label='Quit', command=self.cmd_quit)

        # Test Menubar
        self.menu_test = Menu(self.menubar)
        self.menubar.add_cascade(menu=self.menu_test, label='Test')

        self.menu_test.add_command(label='Run all', command=self.cmd_run_all)
        self.menu_test.add_command(label='Run selected tests', command=self.cmd_run_selected)
        self.menu_test.add_command(label='Re-run failed tests', command=self.cmd_rerun)

        # Add help menu.
        self.menu_help = Menu(self.menubar)
        self.menubar.add_cascade(menu=self.menu_help, label='Help')

        self.menu_help.add_command(label='Documentation', command=self.cmd_help_documentation)


        # last step - configure the menubar
        self.root['menu'] = self.menubar

    def _setup_button_toolbar(self):
        '''
        The button toolbar runs as a horizontal area at the top of the GUI.
        It is a persistent GUI component
        '''

        # Main toolbar
        self.toolbar = Frame(self.root)
        self.toolbar.grid(column=0, row=0, sticky=(W, E))

        # Buttons on the toolbar
        self.run_selected_button = Button(self.toolbar, text='Run',
                                          command=self.cmd_run_selected,
                                          state=DISABLED)
        self.run_selected_button.grid(column=0, row=0)

        self.run_all_button = Button(self.toolbar, text='Run all', command=self.cmd_run_all)
        self.run_all_button.grid(column=1, row=0)

        self.rerun_button = Button(self.toolbar, text='Re-run', command=self.cmd_rerun, state=DISABLED)
        self.rerun_button.grid(column=2, row=0)
        self.stop_button = Button(self.toolbar, text='Stop', command=self.cmd_stop, state=DISABLED)
        self.stop_button.grid(column=3, row=0)


        self.toolbar.columnconfigure(0, weight=0)
        self.toolbar.rowconfigure(0, weight=1)

    def _setup_main_content(self):
        '''
        Sets up the main content area. It is a persistent GUI component
        '''

        # Main content area
        self.content = PanedWindow(self.root, orient=HORIZONTAL)
        self.content.grid(column=0, row=1, sticky=(N, S, E, W))

        # Create the tree/control area on the left frame
        self._setup_left_frame()
        self._setup_all_tests_tree()
        self._setup_problem_tests_tree()

        # Create the output/viewer area on the right frame
        self._setup_right_frame()

        # Set up weights for the left frame's content
        self.content.columnconfigure(0, weight=1)
        self.content.rowconfigure(0, weight=1)

        self.content.pane(0, weight=1)
        self.content.pane(1, weight=2)

    def _setup_left_frame(self):
        '''
        The left frame mostly consists of the tree widget
        '''

        # The left-hand side frame on the main content area
        # The tabs for the two trees
        self.tree_notebook = Notebook(self.content, padding=(0, 5, 0, 5))
        self.content.add(self.tree_notebook)

    def _reset_all_tests_tree(self):
        for child in self.all_tests_tree.get_children():
            self.all_tests_tree.delete(child)

    def _setup_all_tests_tree(self):
        # The tree for all tests
        self.all_tests_tree_frame = Frame(self.content)
        self.all_tests_tree_frame.grid(column=0, row=0, sticky=(N, S, E, W))
        self.tree_notebook.add(self.all_tests_tree_frame, text='All tests')

        self.all_tests_tree = Treeview(self.all_tests_tree_frame)
        self.all_tests_tree.grid(column=0, row=0, sticky=(N, S, E, W))

        # Set up the tag colors for tree nodes.
        for status, config in STATUS.items():
            self.all_tests_tree.tag_configure(config['tag'], foreground=config['color'])
        self.all_tests_tree.tag_configure('inactive', foreground='lightgray')

        # Listen for button clicks on tree nodes
        self.all_tests_tree.tag_bind('TestModule', '<Double-Button-1>', self.on_testModuleClicked)
        self.all_tests_tree.tag_bind('TestCase', '<Double-Button-1>', self.on_testCaseClicked)
        self.all_tests_tree.tag_bind('TestMethod', '<Double-Button-1>', self.on_testMethodClicked)

        self.all_tests_tree.tag_bind('TestModule', '<<TreeviewSelect>>', self.on_testModuleSelected)
        self.all_tests_tree.tag_bind('TestCase', '<<TreeviewSelect>>', self.on_testCaseSelected)
        self.all_tests_tree.tag_bind('TestMethod', '<<TreeviewSelect>>', self.on_testMethodSelected)

        # The tree's vertical scrollbar
        self.all_tests_tree_scrollbar = Scrollbar(self.all_tests_tree_frame, orient=VERTICAL)
        self.all_tests_tree_scrollbar.grid(column=1, row=0, sticky=(N, S))

        # Tie the scrollbar to the text views, and the text views
        # to each other.
        self.all_tests_tree.config(yscrollcommand=self.all_tests_tree_scrollbar.set)
        self.all_tests_tree_scrollbar.config(command=self.all_tests_tree.yview)

        # Setup weights for the "All Tests" tree
        self.all_tests_tree_frame.columnconfigure(0, weight=1)
        self.all_tests_tree_frame.columnconfigure(1, weight=0)
        self.all_tests_tree_frame.rowconfigure(0, weight=1)

    def _reset_problem_tests_tree(self):
        for child in self.problem_tests_tree.get_children():
            self.problem_tests_tree.delete(child)

    def _setup_problem_tests_tree(self):
        # The tree for problem tests
        self.problem_tests_tree_frame = Frame(self.content)
        self.problem_tests_tree_frame.grid(column=0, row=0, sticky=(N, S, E, W))
        self.tree_notebook.add(self.problem_tests_tree_frame, text='Problems')

        self.problem_tests_tree = Treeview(self.problem_tests_tree_frame)
        self.problem_tests_tree.grid(column=0, row=0, sticky=(N, S, E, W))

        # Set up the tag colors for tree nodes.
        for status, config in STATUS.items():
            self.problem_tests_tree.tag_configure(config['tag'], foreground=config['color'])
        self.problem_tests_tree.tag_configure('inactive', foreground='lightgray')

        # Problem tree only deals with selection, not clicks.
        self.problem_tests_tree.tag_bind('TestModule', '<<TreeviewSelect>>', self.on_testModuleSelected)
        self.problem_tests_tree.tag_bind('TestCase', '<<TreeviewSelect>>', self.on_testCaseSelected)
        self.problem_tests_tree.tag_bind('TestMethod', '<<TreeviewSelect>>', self.on_testMethodSelected)

        # The tree's vertical scrollbar
        self.problem_tests_tree_scrollbar = Scrollbar(self.problem_tests_tree_frame, orient=VERTICAL)
        self.problem_tests_tree_scrollbar.grid(column=1, row=0, sticky=(N, S))

        # Tie the scrollbar to the text views, and the text views
        # to each other.
        self.problem_tests_tree.config(yscrollcommand=self.problem_tests_tree_scrollbar.set)
        self.problem_tests_tree_scrollbar.config(command=self.problem_tests_tree.yview)

        # Setup weights for the problems tree
        self.problem_tests_tree_frame.columnconfigure(0, weight=1)
        self.problem_tests_tree_frame.columnconfigure(1, weight=0)
        self.problem_tests_tree_frame.rowconfigure(0, weight=1)


    def _setup_right_frame(self):
        '''
        Right side view output
        '''

        # The right-hand side frame on the main content area
        self.details_frame = Frame(self.content)
        self.details_frame.grid(column=0, row=0, sticky=(N, S, E, W))
        self.content.add(self.details_frame)

        # Add support instrument IP Address.
        self.instrument_ip_address_label = Label(self.details_frame, text = "Instrument IP:")
        self.instrument_ip_address_label.grid(column=0, row=0, sticky=(W))

        self.instr_ip_addr = StringVar()
        self.instr_ip_addr_widget = Entry(self.details_frame, textvariable= self.instr_ip_addr, width=60)
        self.instr_ip_addr.set(get_setting('Host') or 'Not Found')
        self.instr_ip_addr_widget.grid(column=1, row=0, sticky=(W))

        self.reload_ip_address = Button(self.details_frame, text='Update IP Address', command=self.cmd_load_ip_address)
        self.reload_ip_address.grid(column=1, row=0, sticky=(E))

        # Add label for test directory
        self.testdir_label = Label(self.details_frame, text="Test Directory:")
        self.testdir_label.grid(column=0, row=1, sticky=(W))

        self.testdir_name = StringVar()
        self.testdir_widget = Entry(self.details_frame, textvariable= self.testdir_name, width=40)
        self.testdir_name.set(get_setting('StartDir'))
        self.testdir_widget.grid(column=1, row=1, sticky=(W))

        # Reload Tests Load Button.
        self.reload_tests_button = Button(self.details_frame, text='Reload Tests', command=self.cmd_reload_tests)
        self.reload_tests_button.grid(column=1, row=1, sticky=(E))

        # Test Name
        self.name_label = Label(self.details_frame, text='Name:')
        self.name_label.grid(column=0, row=2, pady=5, sticky=(E))

        self.name = StringVar()
        self.name_widget = Entry(self.details_frame, textvariable=self.name)
        self.name_widget.configure(state='readonly')
        self.name_widget.grid(column=1, row=2, pady=5, sticky=(W, E))

        # Test status
        self.test_status = StringVar()
        self.test_status_widget = Label(self.details_frame, textvariable=self.test_status, width=5, anchor=CENTER)
        f = Font(font=self.test_status_widget['font'])
        f['weight'] = 'bold'
        f['size'] = 40
        self.test_status_widget.config(font=f)
        self.test_status_widget.grid(column=2, row=2, padx=2, pady=2, rowspan=2, sticky=(N, W))

        # Test duration
        self.duration_label = Label(self.details_frame, text='Duration:')
        self.duration_label.grid(column=0, row=3, pady=5, sticky=(E,))

        self.duration = StringVar()
        self.duration_widget = Entry(self.details_frame, textvariable=self.duration)
        self.duration_widget.grid(column=1, row=3, pady=5, sticky=(E, W,))

        # Test description
        self.description_label = Label(self.details_frame, text='Description:')
        self.description_label.grid(column=0, row=3, pady=5, sticky=(N, E,))

        self.description = ReadOnlyText(self.details_frame, width=80, height=4)
        self.description.grid(column=1, row=4, pady=5, columnspan=2, sticky=(N, S, E, W,))

        self.description_scrollbar = Scrollbar(self.details_frame, orient=VERTICAL)
        self.description_scrollbar.grid(column=3, row=4, pady=5, sticky=(N, S))
        self.description.config(yscrollcommand=self.description_scrollbar.set)
        self.description_scrollbar.config(command=self.description.yview)

        # Test output
        self.output_label = Label(self.details_frame, text='Output:')
        self.output_label.grid(column=0, row=5, pady=5, sticky=(N, E,))

        self.output = ReadOnlyText(self.details_frame, width=80, height=10)
        self.output.grid(column=1, row=5, pady=5, columnspan=2, sticky=(N, S, E, W,))

        self.output_scrollbar = Scrollbar(self.details_frame, orient=VERTICAL)
        self.output_scrollbar.grid(column=3, row=5, pady=5, sticky=(N, S))
        self.output.config(yscrollcommand=self.output_scrollbar.set)
        self.output_scrollbar.config(command=self.output.yview)

        # Error message
        self.error_label = Label(self.details_frame, text='Error:')
        self.error_label.grid(column=0, row=6, pady=5, sticky=(N, E,))

        self.error = ReadOnlyText(self.details_frame, width=80)
        self.error.grid(column=1, row=6, pady=5, columnspan=2, sticky=(N, S, E, W))

        self.error_scrollbar = Scrollbar(self.details_frame, orient=VERTICAL)
        self.error_scrollbar.grid(column=3, row=6, pady=5, sticky=(N, S))
        self.error.config(yscrollcommand=self.error_scrollbar.set)
        self.error_scrollbar.config(command=self.error.yview)

        # Set up GUI weights for the details frame
        self.details_frame.columnconfigure(0, weight=0)
        self.details_frame.columnconfigure(1, weight=1)
        self.details_frame.columnconfigure(2, weight=0)
        self.details_frame.columnconfigure(3, weight=0)
        self.details_frame.columnconfigure(4, weight=0)
        self.details_frame.rowconfigure(0, weight=1)
        self.details_frame.rowconfigure(1, weight=1)
        self.details_frame.rowconfigure(2, weight=0)
        self.details_frame.rowconfigure(3, weight=0)
        self.details_frame.rowconfigure(4, weight=1)
        self.details_frame.rowconfigure(5, weight=5)
        self.details_frame.rowconfigure(6, weight=10)

    def _setup_status_bar(self):
        # Status bar
        self.statusbar = Frame(self.root)
        self.statusbar.grid(column=0, row=2, sticky=(W, E))

        # Current status
        self.run_status = StringVar()
        self.run_status_label = Label(self.statusbar, textvariable=self.run_status)
        self.run_status_label.grid(column=0, row=0, sticky=(W, E))
        self.run_status.set('Not running')

        # Test result summary
        self.run_summary = StringVar()
        self.run_summary_label = Label(self.statusbar, textvariable=self.run_summary)
        self.run_summary_label.grid(column=1, row=0, sticky=(W, E))

        # Update the run summary
        self.run_summary.set('Total:%(total)s Passed:%(pass)s Failed:%(fail)s Skipped:%(skip)s' % {
            'total': 0,
            'pass': 0,
            'fail': 0,
            'skip': 0
        })

        # Test progress
        self.progress_value = IntVar()
        self.progress = Progressbar(self.statusbar, orient=HORIZONTAL, length=200, mode='determinate', maximum=100, variable=self.progress_value)
        self.progress.grid(column=2, row=0, sticky=(W, E))


        # Main window resize handle
        self.grip = Sizegrip(self.statusbar)
        self.grip.grid(column=3, row=0, sticky=(S, E))

        # Set up weights for status bar frame
        self.statusbar.columnconfigure(0, weight=1)
        self.statusbar.columnconfigure(1, weight=0)
        self.statusbar.columnconfigure(2, weight=0)
        self.statusbar.columnconfigure(3, weight=0)
        self.statusbar.rowconfigure(0, weight=1)

    @property
    def current_test_tree(self):
        "Check the tree notebook to return the currently selected tree."
        current_tree_id = self.tree_notebook.select()
        if current_tree_id == self.problem_tests_tree_frame._w:
            return self.problem_tests_tree
        else:
            return self.all_tests_tree

    @property
    def project(self):
        return self._project

    def _add_test_module(self, parentNode, testModule):
        testModule_node = self.all_tests_tree.insert(
            parentNode, 'end', testModule.path,
            text=testModule.name,
            tags=['TestModule', 'active'],
            open=True)

        for subModuleName, subModule in sorted(testModule.items()):
            if isinstance(subModule, TestModule):
                self._add_test_module(testModule_node, subModule)
            else:
                testCase = subModule
                testCase_node = self.all_tests_tree.insert(
                    testModule_node, 'end', testCase.path,
                    text=testCase.name,
                    tags=['TestCase', 'active'],
                    open=True
                )

                for testMethod_name, testMethod in sorted(testCase.items()):
                    self.all_tests_tree.insert(
                        testCase_node, 'end', testMethod.path,
                        text=testMethod.name,
                        tags=['TestMethod', 'active'],
                        open=True
                    )

    @project.setter
    def project(self, project):
        self._project = project

        # Get a count of active tests to display in the status bar.
        count, labels = self._project.find_tests(True)

        # Update the run summary
        self.run_summary.set('Total:%(total)s Passed:%(pass)s Failed:%(fail)s Skipped:%(skip)s' % {
            'total': count,
            'pass': 0,
            'fail': 0,
            'skip': 0
        })

        # Clean treeview.
        self.all_tests_tree.delete(*self.all_tests_tree.get_children())
        self.problem_tests_tree.delete(*self.problem_tests_tree.get_children())

        # Populate the initial tree nodes. This is recursive, because
        # the tree could be of arbitrary depth.
        for testModule_name, testModule in sorted(self._project.items()):
            self._add_test_module('', testModule)

        # Listen for any state changes on nodes in the tree
        TestModule.bind('active', self.on_nodeActive)
        TestCase.bind('active', self.on_nodeActive)
        TestMethod.bind('active', self.on_nodeActive)

        TestModule.bind('inactive', self.on_nodeInactive)
        TestCase.bind('inactive', self.on_nodeInactive)
        TestMethod.bind('inactive', self.on_nodeInactive)

        # Listen for new nodes added to the tree
        TestModule.bind('new', self.on_nodeAdded)
        TestCase.bind('new', self.on_nodeAdded)
        TestMethod.bind('new', self.on_nodeAdded)

        # Listen for any status updates on nodes in the tree.
        TestMethod.bind('status_update', self.on_nodeStatusUpdate)


    def reload_project(self, testdir=get_setting('StartDir')):
        # If the directory does not exist, throw an error message and don't do anything.
        if os.path.exists(testdir) is False:
            dialog = tkMessageBox.showerror
            dialog(message='Directory: ' + testdir + ' does not exist!')
            return

        self._reset_all_tests_tree()
        self._reset_problem_tests_tree()

        self.project = self.load_project(self.root, self.Model, testdir)


    def load_project(self, root, Model, testdir=get_setting('StartDir')):
        self.Model = Model
        project = None
        while project is None:
            try:
                # Create the project objects
                project = Model()

                runner = subprocess.Popen(
                    project.discover_commandline(testdir),
                    stdin=None,
                    stdout=subprocess.PIPE,
                    stderr=subprocess.PIPE,
                    shell=False,
                )

                test_list = []
                for line in runner.stdout:
                    test_list.append(line.strip().decode('utf-8'))

                errors = []
                for line in runner.stderr:
                    errors.append(line.strip().decode('utf-8'))
                if errors and not test_list:
                    raise ModelLoadError('\n'.join(errors))

                project.refresh(test_list, errors)
            except ModelLoadError as e:
                # Load failed; destroy the project and show an error dialog.
                # If the user selects cancel, quit.
                project = None
                dialog = TestLoadErrorDialog(root, e.trace)
                if dialog.status == dialog.CANCEL:
                    sys.exit(1)
        if project.errors:
            dialog = IgnorableTestLoadErrorDialog(root, '\n'.join(project.errors))
            if dialog.status == dialog.CANCEL:
                sys.exit(1)

        return project

    def mainloop(self):
        self.root.mainloop()

    # Menu/button commands.
    def cmd_load_run(self):
        self.load_filename = filedialog.askopenfilename(initialdir='.', title="Select Run File to Load")
        print (self.load_filename)

    def cmd_export_run(self):
        self.save_filename = filedialog.asksaveasfilename(initialdir=".", title="Select File to Save To")
        print (self.save_filename)

    def cmd_quit(self):
        self.stop()
        self.root.quit()

    def cmd_stop(self, event=None):
        "Command: The stop button has been pressed"
        self.stop()


    def cmd_run_all(self, event=None):
        "Command: The Run all button has been pressed"
        # If the executor isn't currently running, we can
        # start a test run.
        if not self.executor or not self.executor.is_running:
            self.run(active=True)

    def cmd_run_selected(self, event=None):
        "Command: The 'run selected' button has been pressed"
        current_tree = self.current_test_tree

        # If a node is selected, it needs to be made active
        for path in current_tree.selection():
            parts = path.split('.')
            testModule = self.project
            for part in parts:
                testModule = testModule[part]

            testModule.set_active(True)

        # If the executor isn't currently running, we can
        # start a test run.
        if not self.executor or not self.executor.is_running:
            self.run(labels=set(current_tree.selection()))

    def cmd_reload_tests(self):
        # Reload the project tree on left side.
        self.reload_project(self.testdir_name.get())

    def cmd_load_ip_address(self):
        # Update the instrument IP address.
        update_settings('Host', self.instr_ip_addr.get())
        assert get_setting('Host') == self.instr_ip_addr.get()

    def cmd_rerun(self, event=None):
        "Command: The re-run button has been pressed"
        if not self.executor or not self.executor.is_running:
            self.run(status=set(TestMethod.FAILING_STATES))

    def cmd_help_documentation(self):
        "Command: Open documentation"
        import webbrowser
        webbrowser.open_new('file://' + os.path.realpath('Readme.htm'))

    def on_testModuleClicked(self, event):
        "Event handler: a module has been clicked in the tree"
        parts = event.widget.focus().split('.')
        testModule = self.project
        for part in parts:
            testModule = testModule[part]

        testModule.toggle_active()

    def on_testCaseClicked(self, event):
        "Event handler: a test case has been clicked in the tree"
        parts = event.widget.focus().split('.')
        testCase = self.project
        for part in parts:
            testCase = testCase[part]

        testCase.toggle_active()

    def on_testMethodClicked(self, event):
        "Event handler: a test case has been clicked in the tree"
        parts = event.widget.focus().split('.')
        testMethod = self.project
        for part in parts:
            testMethod = testMethod[part]

        testMethod.toggle_active()

    def on_testModuleSelected(self, event):
        "Event handler: a test module has been selected in the tree"
        self.name.set('')
        self.test_status.set('')

        self.duration.set('')
        self.description.delete('1.0', END)

        self._hide_test_output()
        self._hide_test_errors()

        # update "run selected" button enabled state
        self.set_selected_button_state()

    def on_testCaseSelected(self, event):
        "Event handler: a test case has been selected in the tree"
        self.name.set('')
        self.test_status.set('')

        self.duration.set('')
        self.description.delete('1.0', END)

        self._hide_test_output()
        self._hide_test_errors()

        # update "run selected" button enabled state
        self.set_selected_button_state()

    def on_testMethodSelected(self, event):
        "Event handler: a test case has been selected in the tree"
        if len(event.widget.selection()) == 1:
            parts = event.widget.selection()[0].split('.')

            # Find the definition for the actual test method
            # out of the project.
            testMethod = self.project
            for part in parts:
                testMethod = testMethod[part]

            self.name.set(testMethod.path)

            self.description.delete('1.0', END)
            self.description.insert('1.0', testMethod.description)

            config = STATUS.get(testMethod.status, STATUS_DEFAULT)
            self.test_status_widget.config(foreground=config['color'])
            self.test_status.set(config['symbol'])

            if testMethod._result:
                # Test has been executed
                self.duration.set('%0.2fs' % testMethod._result['duration'])

                if testMethod.output:
                    self._show_test_output(testMethod.output)
                else:
                    self._hide_test_output()

                if testMethod.error:
                    self._show_test_errors(testMethod.error)
                else:
                    self._hide_test_errors()
            else:
                # Test hasn't been executed yet.
                self.duration.set('Not executed')

                self._hide_test_output()
                self._hide_test_errors()

        else:
            # Multiple tests selected
            self.name.set('')
            self.test_status.set('')

            self.duration.set('')
            self.description.delete('1.0', END)

            self._hide_test_output()
            self._hide_test_errors()

        # update "run selected" button enabled state
        self.set_selected_button_state()

    def on_nodeAdded(self, node):
        "Event handler: a new node has been added to the tree"
        try:
            self.all_tests_tree.insert(
                node.parent.path, 'end', node.path,
                text=node.name,
                tags=[node.__class__.__name__, 'active'],
                open=True
            )
        except:
            #print("Test already added ignoring.")
            pass

    def on_nodeActive(self, node):
        "Event handler: a node on the tree has been made active"
        self.all_tests_tree.item(node.path, tags=[node.__class__.__name__, 'active'])
        self.all_tests_tree.item(node.path, open=True)

    def on_nodeInactive(self, node):
        "Event handler: a node on the tree has been made inactive"
        self.all_tests_tree.item(node.path, tags=[node.__class__.__name__, 'inactive'])
        self.all_tests_tree.item(node.path, open=False)

    def on_nodeStatusUpdate(self, node):
        "Event handler: a node on the tree has received a status update"
        self.all_tests_tree.item(node.path, tags=['TestMethod', STATUS[node.status]['tag']])

        if node.status in TestMethod.FAILING_STATES:
            # Test is in a failing state. Make sure it is on the problem tree,
            # with the correct current status.

            parts = node.path.split('.')
            parentModule = self.project
            for pos, part in enumerate(parts):
                path = '.'.join(parts[:pos+1])
                testModule = parentModule[part]

                if not self.problem_tests_tree.exists(path):
                    self.problem_tests_tree.insert(
                        parentModule.path, 'end', testModule.path,
                        text=testModule.name,
                        tags=[testModule.__class__.__name__, 'active'],
                        open=True
                    )

                parentModule = testModule

            self.problem_tests_tree.item(node.path, tags=['TestMethod', STATUS[node.status]['tag']])
        else:
            # Test passed; if it's on the problem tree, remove it.
            if self.problem_tests_tree.exists(node.path):
                self.problem_tests_tree.delete(node.path)

                # Check all parents of this node. Recursively remove
                # any parent has no children as a result of this deletion.
                has_children = False
                node = node.parent
                while node.path and not has_children:
                    if not self.problem_tests_tree.get_children(node.path):
                        self.problem_tests_tree.delete(node.path)
                    else:
                        has_children = True
                    node = node.parent

    def on_testProgress(self):
        "Event handler: a periodic update to poll the runner for output, generating GUI updates"
        if self.executor and self.executor.poll():
            self.root.after(100, self.on_testProgress)

    def on_executorStatusUpdate(self, event, update):
        "The executor has some progress to report"
        # Update the status line.
        self.run_status.set(update)

    def on_executorTestStart(self, event, test_path):
        "The executor has started running a new test."
        # Update status line, and set the tree item to active.
        self.run_status.set('Running %s...' % test_path)
        self.all_tests_tree.item(test_path, tags=['TestMethod', 'active'])

    def on_executorTestEnd(self, event, test_path, result, remaining_time):
        "The executor has finished running a test."
        # Update the progress meter
        self.progress_value.set(self.progress_value.get() + 1)


        # Update the run summary
        self.run_summary.set('Total:%(total)s Passed:%(pass)s Failed:%(fail)s Skipped:%(skip)s' % {
            'total': self.executor.total_count,
            'pass': self.executor.result_count.get(TestMethod.STATUS_PASS, 0),
            'fail': self.executor.result_count.get(TestMethod.STATUS_FAIL, 0),
            'skip': self.executor.result_count.get(TestMethod.STATUS_SKIP, 0)
        })

        # If the test that just fininshed is the one (and only one)
        # selected on the tree, update the display.
        current_tree = self.current_test_tree
        if len(current_tree.selection()) == 1:
            # One test selected.
            if current_tree.selection()[0] == test_path:
                # If the test that just finished running is the selected
                # test, force reset the selection, which will generate a
                # selection event, forcing a refresh of the result page.
                current_tree.selection_set(current_tree.selection())
        else:
            # No or Multiple tests selected
            self.name.set('')
            self.test_status.set('')

            self.duration.set('')
            self.description.delete('1.0', END)

            self._hide_test_output()
            self._hide_test_errors()

    def on_executorSuiteEnd(self, event, error=None):
        "The test suite finished running."
        # Display the final results
        self.run_status.set('Finished.')

        if error:
            TestErrorsDialog(self.root, error)

        if self.executor.any_failed:
            dialog = tkMessageBox.showerror
        else:
            dialog = tkMessageBox.showinfo

        message = ', '.join(
            '%d %s' % (count, TestMethod.STATUS_LABELS[state])
            for state, count in sorted(self.executor.result_count.items()))

        dialog(message=message or 'No tests were ran')

        # Update the run summary
        self.run_summary.set('Total:%(total)s Passed:%(pass)s Failed:%(fail)s Skipped:%(skip)s' % {
            'total': self.executor.total_count,
            'pass': self.executor.result_count.get(TestMethod.STATUS_PASS, 0),
            'fail': self.executor.result_count.get(TestMethod.STATUS_FAIL, 0),
            'skip': self.executor.result_count.get(TestMethod.STATUS_SKIP, 0)
        })

        # Reset the buttons
        self.reset_button_states_on_end()

        # Drop the reference to the executor
        self.executor = None

    def on_executorSuiteError(self, event, error):
        "An error occurred running the test suite."
        # Display the error in a dialog
        self.run_status.set('Error running test suite.')
        FailedTestDialog(self.root, error)

        # Reset the buttons
        self.reset_button_states_on_end()

        # Drop the reference to the executor
        self.executor = None

    def reset_button_states_on_end(self):
        "A test run has ended and we should enable or disable buttons as appropriate."
        self.stop_button.configure(state=DISABLED)
        self.run_all_button.configure(state=NORMAL)
        self.set_selected_button_state()
        if self.executor and self.executor.any_failed:
            self.rerun_button.configure(state=NORMAL)
        else:
            self.rerun_button.configure(state=DISABLED)

    def set_selected_button_state(self):
        if self.executor and self.executor.is_running:
            self.run_selected_button.configure(state=DISABLED)
        elif self.current_test_tree.selection():
            self.run_selected_button.configure(state=NORMAL)
        else:
            self.run_selected_button.configure(state=DISABLED)

    def run(self, active=True, status=None, labels=None):
        """Run the test suite.

        If active=True, only active tests will be run.
        If status is provided, only tests whose most recent run
            status matches the set provided will be executed.
        If labels is provided, only tests with those labels will
            be executed
        """
        count, labels = self.project.find_tests(active, status, labels)
        self.run_status.set('Running...')

        # Update the run summary
        self.run_summary.set('Total:%(total)s Passed:%(pass)s Failed:%(fail)s Skipped:%(skip)s' % {
            'total': count,
            'pass': 0,
            'fail': 0,
            'skip': 0
        })

        self.stop_button.configure(state=NORMAL)

        self.run_all_button.configure(state=DISABLED)
        self.run_selected_button.configure(state=DISABLED)
        self.rerun_button.configure(state=DISABLED)


        self.progress['maximum'] = count
        self.progress_value.set(0)
        # Create the runner
        self.executor = Runner(self.project, count, labels, self.testdir_name.get())

        # Queue the first progress handling event
        self.root.after(100, self.on_testProgress)

    def stop(self):
        "Stop the test suite."
        if self.executor and self.executor.is_running:
            self.run_status.set('Stopping...')

            self.executor.terminate()
            self.executor = None

            self.run_status.set('Stopped.')

            self.reset_button_states_on_end()

    def _hide_test_output(self):
        "Hide the test output panel on the test results page"
        self.output_label.grid_remove()
        self.output.grid_remove()
        self.output_scrollbar.grid_remove()
        self.details_frame.rowconfigure(3, weight=0)

    def _show_test_output(self, content):
        "Show the test output panel on the test results page"
        self.output.delete('1.0', END)
        self.output.insert('1.0', content)

        self.output_label.grid()
        self.output.grid()
        self.output_scrollbar.grid()
        self.details_frame.rowconfigure(3, weight=5)

    def _hide_test_errors(self):
        "Hide the test error panel on the test results page"
        self.error_label.grid_remove()
        self.error.grid_remove()
        self.error_scrollbar.grid_remove()

    def _show_test_errors(self, content):
        "Show the test error panel on the test results page"
        self.error.delete('1.0', END)
        self.error.insert('1.0', content)

        self.error_label.grid()
        self.error.grid()
        self.error_scrollbar.grid()
Exemple #12
0
    def _setup_right_frame(self):
        '''
        Right side view output
        '''

        # The right-hand side frame on the main content area
        self.details_frame = Frame(self.content)
        self.details_frame.grid(column=0, row=0, sticky=(N, S, E, W))
        self.content.add(self.details_frame)

        # Add support instrument IP Address.
        self.instrument_ip_address_label = Label(self.details_frame, text = "Instrument IP:")
        self.instrument_ip_address_label.grid(column=0, row=0, sticky=(W))

        self.instr_ip_addr = StringVar()
        self.instr_ip_addr_widget = Entry(self.details_frame, textvariable= self.instr_ip_addr, width=60)
        self.instr_ip_addr.set(get_setting('Host') or 'Not Found')
        self.instr_ip_addr_widget.grid(column=1, row=0, sticky=(W))

        self.reload_ip_address = Button(self.details_frame, text='Update IP Address', command=self.cmd_load_ip_address)
        self.reload_ip_address.grid(column=1, row=0, sticky=(E))

        # Add label for test directory
        self.testdir_label = Label(self.details_frame, text="Test Directory:")
        self.testdir_label.grid(column=0, row=1, sticky=(W))

        self.testdir_name = StringVar()
        self.testdir_widget = Entry(self.details_frame, textvariable= self.testdir_name, width=40)
        self.testdir_name.set(get_setting('StartDir'))
        self.testdir_widget.grid(column=1, row=1, sticky=(W))

        # Reload Tests Load Button.
        self.reload_tests_button = Button(self.details_frame, text='Reload Tests', command=self.cmd_reload_tests)
        self.reload_tests_button.grid(column=1, row=1, sticky=(E))

        # Test Name
        self.name_label = Label(self.details_frame, text='Name:')
        self.name_label.grid(column=0, row=2, pady=5, sticky=(E))

        self.name = StringVar()
        self.name_widget = Entry(self.details_frame, textvariable=self.name)
        self.name_widget.configure(state='readonly')
        self.name_widget.grid(column=1, row=2, pady=5, sticky=(W, E))

        # Test status
        self.test_status = StringVar()
        self.test_status_widget = Label(self.details_frame, textvariable=self.test_status, width=5, anchor=CENTER)
        f = Font(font=self.test_status_widget['font'])
        f['weight'] = 'bold'
        f['size'] = 40
        self.test_status_widget.config(font=f)
        self.test_status_widget.grid(column=2, row=2, padx=2, pady=2, rowspan=2, sticky=(N, W))

        # Test duration
        self.duration_label = Label(self.details_frame, text='Duration:')
        self.duration_label.grid(column=0, row=3, pady=5, sticky=(E,))

        self.duration = StringVar()
        self.duration_widget = Entry(self.details_frame, textvariable=self.duration)
        self.duration_widget.grid(column=1, row=3, pady=5, sticky=(E, W,))

        # Test description
        self.description_label = Label(self.details_frame, text='Description:')
        self.description_label.grid(column=0, row=3, pady=5, sticky=(N, E,))

        self.description = ReadOnlyText(self.details_frame, width=80, height=4)
        self.description.grid(column=1, row=4, pady=5, columnspan=2, sticky=(N, S, E, W,))

        self.description_scrollbar = Scrollbar(self.details_frame, orient=VERTICAL)
        self.description_scrollbar.grid(column=3, row=4, pady=5, sticky=(N, S))
        self.description.config(yscrollcommand=self.description_scrollbar.set)
        self.description_scrollbar.config(command=self.description.yview)

        # Test output
        self.output_label = Label(self.details_frame, text='Output:')
        self.output_label.grid(column=0, row=5, pady=5, sticky=(N, E,))

        self.output = ReadOnlyText(self.details_frame, width=80, height=10)
        self.output.grid(column=1, row=5, pady=5, columnspan=2, sticky=(N, S, E, W,))

        self.output_scrollbar = Scrollbar(self.details_frame, orient=VERTICAL)
        self.output_scrollbar.grid(column=3, row=5, pady=5, sticky=(N, S))
        self.output.config(yscrollcommand=self.output_scrollbar.set)
        self.output_scrollbar.config(command=self.output.yview)

        # Error message
        self.error_label = Label(self.details_frame, text='Error:')
        self.error_label.grid(column=0, row=6, pady=5, sticky=(N, E,))

        self.error = ReadOnlyText(self.details_frame, width=80)
        self.error.grid(column=1, row=6, pady=5, columnspan=2, sticky=(N, S, E, W))

        self.error_scrollbar = Scrollbar(self.details_frame, orient=VERTICAL)
        self.error_scrollbar.grid(column=3, row=6, pady=5, sticky=(N, S))
        self.error.config(yscrollcommand=self.error_scrollbar.set)
        self.error_scrollbar.config(command=self.error.yview)

        # Set up GUI weights for the details frame
        self.details_frame.columnconfigure(0, weight=0)
        self.details_frame.columnconfigure(1, weight=1)
        self.details_frame.columnconfigure(2, weight=0)
        self.details_frame.columnconfigure(3, weight=0)
        self.details_frame.columnconfigure(4, weight=0)
        self.details_frame.rowconfigure(0, weight=1)
        self.details_frame.rowconfigure(1, weight=1)
        self.details_frame.rowconfigure(2, weight=0)
        self.details_frame.rowconfigure(3, weight=0)
        self.details_frame.rowconfigure(4, weight=1)
        self.details_frame.rowconfigure(5, weight=5)
        self.details_frame.rowconfigure(6, weight=10)
Exemple #13
0
class MainWindow(object):
    def __init__(self, root, options):
        '''
        -----------------------------------------------------
        | main button toolbar                               |
        -----------------------------------------------------
        |       < ma | in content area >                    |
        |            |                                      |
        | File list  | File name                            |
        |            |                                      |
        -----------------------------------------------------
        |     status bar area                               |
        -----------------------------------------------------

        '''

        # Obtain and expand the current working directory.
        base_path = os.path.abspath(os.getcwd())
        self.base_path = os.path.normcase(base_path)

        # Create a filename normalizer based on the CWD.
        self.filename_normalizer = filename_normalizer(self.base_path)

        # Root window
        self.root = root
        self.root.title('Galley')
        self.root.geometry('1024x768')

        # Prevent the menus from having the empty tearoff entry
        self.root.option_add('*tearOff', FALSE)
        # Catch the close button
        self.root.protocol("WM_DELETE_WINDOW", self.cmd_quit)
        # Catch the "quit" event.
        self.root.createcommand('exit', self.cmd_quit)

        # The browsing history.
        self._history = []
        self._history_index = 0
        self._traversing_history = False

        # The default source file extension. This will be updated once we have
        # parsed the project config file.
        self.source_extension = '.rst'

        # Known warnings, indexed by source file.
        self.warning_output = {}

        # Setup the menu
        self._setup_menubar()

        # Set up the main content for the window.
        self._setup_button_toolbar()
        self._setup_main_content()
        self._setup_status_bar()

        # Now configure the weights for the root frame
        self.root.columnconfigure(0, weight=1)
        self.root.rowconfigure(0, weight=0)
        self.root.rowconfigure(1, weight=1)
        self.root.rowconfigure(2, weight=0)

        # Set up a background worker thread to build docs.
        self.work_queue = Queue()
        self.results_queue = Queue()
        self.worker_thread = threading.Thread(target=sphinx_worker, args=(os.path.join(self.base_path, 'docs'), self.work_queue, self.results_queue))
        self.worker_thread.daemon = True
        self.worker_thread.start()

        # Set up a background monitor thread.
        self.stop_event = threading.Event()
        self.monitor_thread = threading.Thread(target=file_monitor, args=(os.path.join(self.base_path, 'docs'), self.stop_event, self.results_queue))
        self.monitor_thread.daemon = True
        self.monitor_thread.start()

        # Requeue for another update in 40ms (24*40ms == 1s - so this is
        # as fast as we need to update to match human visual acuity)
        self.root.after(40, self.handle_background_tasks)


    ######################################################
    # Internal GUI layout methods.
    ######################################################

    def _setup_menubar(self):
        # Menubar
        self.menubar = Menu(self.root)

        # self.menu_Apple = Menu(self.menubar, name='Apple')
        # self.menubar.add_cascade(menu=self.menu_Apple)

        self.menu_file = Menu(self.menubar)
        self.menubar.add_cascade(menu=self.menu_file, label='File')

        self.menu_help = Menu(self.menubar)
        self.menubar.add_cascade(menu=self.menu_help, label='Help')

        # self.menu_Apple.add_command(label='Test', command=self.cmd_dummy)

        # self.menu_file.add_command(label='New', command=self.cmd_dummy, accelerator="Command-N")
        # self.menu_file.add_command(label='Close', command=self.cmd_dummy)

        self.menu_help.add_command(label='Open Documentation', command=self.cmd_galley_docs)
        self.menu_help.add_command(label='Open Galley project page', command=self.cmd_galley_page)
        self.menu_help.add_command(label='Open Galley on GitHub', command=self.cmd_galley_github)
        self.menu_help.add_command(label='Open BeeWare project page', command=self.cmd_beeware_page)

        # last step - configure the menubar
        self.root['menu'] = self.menubar

    def _setup_button_toolbar(self):
        '''
        The button toolbar runs as a horizontal area at the top of the GUI.
        It is a persistent GUI component
        '''

        # Main toolbar
        self.toolbar = Frame(self.root)
        self.toolbar.grid(column=0, row=0, sticky=(W, E))

        # Buttons on the toolbar
        self.back_button = Button(self.toolbar, text='◀', command=self.cmd_back, state=DISABLED)
        self.back_button.grid(column=0, row=0)

        self.forward_button = Button(self.toolbar, text='▶', command=self.cmd_forward, state=DISABLED)
        self.forward_button.grid(column=1, row=0)

        self.rebuild_all_button = Button(self.toolbar, text='Rebuild all', command=self.cmd_rebuild_all, state=DISABLED)
        self.rebuild_all_button.grid(column=2, row=0)

        self.rebuild_file_button = Button(self.toolbar, text='Rebuild', command=self.cmd_rebuild_file, state=DISABLED)
        self.rebuild_file_button.grid(column=3, row=0)

        self.reload_config_button = Button(self.toolbar, text='Reload', command=self.cmd_reload_config, state=DISABLED)
        self.reload_config_button.grid(column=4, row=0)

        self.toolbar.columnconfigure(0, weight=0)
        self.toolbar.rowconfigure(0, weight=0)

    def _setup_main_content(self):
        '''
        Sets up the main content area. It is a persistent GUI component
        '''

        # Main content area
        self.content = PanedWindow(self.root, orient=HORIZONTAL)
        self.content.grid(column=0, row=1, sticky=(N, S, E, W))

        # Create the tree/control area on the file frame
        self._setup_project_file_tree()

        # Create the output/viewer area on the content frame
        self._setup_html_area()

        # Set up weights for the left frame's content
        self.content.columnconfigure(0, weight=1)
        self.content.rowconfigure(0, weight=1)

        self.content.pane(0, weight=1)
        self.content.pane(1, weight=4)

    def _setup_project_file_tree(self):

        self.project_file_tree_frame = Frame(self.content)
        self.project_file_tree_frame.grid(column=0, row=0, sticky=(N, S, E, W))

        self.project_file_tree = FileView(self.project_file_tree_frame, normalizer=self.filename_normalizer, root=os.path.join(self.base_path, 'docs'))
        self.project_file_tree.grid(column=0, row=0, sticky=(N, S, E, W))

        # # The tree's vertical scrollbar
        self.project_file_tree_scrollbar = Scrollbar(self.project_file_tree_frame, orient=VERTICAL)
        self.project_file_tree_scrollbar.grid(column=1, row=0, sticky=(N, S))

        # # Tie the scrollbar to the text views, and the text views
        # # to each other.
        self.project_file_tree.config(yscrollcommand=self.project_file_tree_scrollbar.set)
        self.project_file_tree_scrollbar.config(command=self.project_file_tree.yview)

        # Setup weights for the "project_file_tree" tree
        self.project_file_tree_frame.columnconfigure(0, weight=1)
        self.project_file_tree_frame.columnconfigure(1, weight=0)
        self.project_file_tree_frame.rowconfigure(0, weight=1)

        # Handlers for GUI events
        self.project_file_tree.bind('<<TreeviewSelect>>', self.on_file_selected)

        self.content.add(self.project_file_tree_frame)

    def _setup_html_area(self):
        self.html_frame = Frame(self.content)
        self.html_frame.grid(column=1, row=0, sticky=(N, S, E, W))

        # Label for current file
        self.current_file = StringVar()
        self.current_file_label = Label(self.html_frame, textvariable=self.current_file)
        self.current_file_label.grid(column=0, row=0, columnspan=3, sticky=(W, E))

        # Code display area
        self.html = SimpleHTMLView(self.html_frame)
        self.html.grid(column=0, row=1, columnspan=3, sticky=(N, S, E, W))

        self.html.link_bind('<1>', self.on_link_click)

        # Warnings
        self.warnings_label = Label(self.html_frame, text='Warnings:')
        self.warnings_label.grid(column=0, row=2, pady=5, sticky=(N, E,))

        self.warnings = ReadOnlyText(self.html_frame, height=6)
        self.warnings.grid(column=1, row=2, pady=5, columnspan=2, sticky=(N, S, E, W,))
        self.warnings.tag_configure('warning', wrap=WORD, lmargin1=5, lmargin2=20, spacing1=2, spacing3=2)
        self.warnings_scrollbar = Scrollbar(self.html_frame, orient=VERTICAL)
        self.warnings_scrollbar.grid(column=2, row=2, pady=5, sticky=(N, S))
        self.warnings.config(yscrollcommand=self.warnings_scrollbar.set)
        self.warnings_scrollbar.config(command=self.warnings.yview)

        # Set up weights for the html frame's content
        self.html_frame.columnconfigure(0, weight=0)
        self.html_frame.columnconfigure(1, weight=1)
        self.html_frame.columnconfigure(2, weight=0)
        self.html_frame.rowconfigure(0, weight=0)
        self.html_frame.rowconfigure(1, weight=4)
        self.html_frame.rowconfigure(2, weight=1)

        self.content.add(self.html_frame)

    def _setup_status_bar(self):
        # Status bar
        self.statusbar = Frame(self.root)
        self.statusbar.grid(column=0, row=2, sticky=(W, E))

        # Current status
        self.run_status = StringVar()
        self.run_status_label = Label(self.statusbar, textvariable=self.run_status)
        self.run_status_label.grid(column=0, row=0, sticky=(W, E))
        self.run_status.set('Not running')

        # Progress bar; initially started, because we don't know how long initialization will take.
        self.progress_value = IntVar()
        # self.progress = Progressbar(self.statusbar, orient=HORIZONTAL, length=200, mode='indeterminate', maximum=100, variable=self.progress_value)
        self.progress = Progressbar(self.statusbar, orient=HORIZONTAL, length=200, mode='indeterminate')
        self.progress.grid(column=1, row=0, sticky=(W, E))

        # Main window resize handle
        self.grip = Sizegrip(self.statusbar)
        self.grip.grid(column=2, row=0, sticky=(S, E))

        # Set up weights for status bar frame
        self.statusbar.columnconfigure(0, weight=1)
        self.statusbar.columnconfigure(1, weight=0)
        self.statusbar.columnconfigure(2, weight=0)
        self.statusbar.rowconfigure(0, weight=0)

    ######################################################
    # Utility methods for controlling content
    ######################################################

    def show_file(self, filename, anchor=None):
        """Show the content of the nominated file.

        If specified, bookmark is the HTML href anchor to display. If the
        anchor isn't currently visible, the window will be scrolled until
        it is.
        """
        # TEMP: Rework into HTML view
        path, ext = os.path.splitext(filename)
        compiled_filename = path.replace(os.path.join(self.base_path, 'docs'), os.path.join(self.base_path, 'docs', '_build', 'json')) + '.fjson'

        # Set the filename label for the current file
        self.current_file.set(self.filename_normalizer(filename))

        try:
            # Update the html view; this means changing the displayed file
            # if necessary, and updating the current line.
            if filename != self.html.filename:
                self.html.filename = compiled_filename

            # self.html.anchor = anchor

            # Show the warnings panel (if needed)
            self._show_warnings(filename)

            # Add this file to history.
            path, ext = os.path.splitext(filename)
            path = path.replace('docs/_build/json', 'docs')

            # History traversal is a temporary operation. If we're traversing
            # history, we won't push this onto the stack... but only this once.
            # Traversal state is reset immediately afterwards.
            if not self._traversing_history:
                if self._history_index:
                    self.forward_button.configure(state=DISABLED)
                    self.back_button.configure(state=NORMAL)

                self._history = self._history[:self._history_index] + [path + self.source_extension]
                self._history_index = self._history_index + 1
            else:
                self._traversing_history = False

        except IOError:
            tkMessageBox.showerror(message='%s has not been compiled to HTML' % self.filename_normalizer(filename))

    def _show_warnings(self, filename):
        "Show the warnings output panel"

        # Build a list of all displayed warnings
        warnings = []
        # First, the global warnings
        for (lineno, warning) in self.warning_output.get(None, []):
            if lineno:
                warnings.append('○ Line %s: %s' % (lineno, warning))
            else:
                warnings.append('○ %s' % warning)

        # Then, the file specific warnings.
        for (lineno, warning) in self.warning_output.get(filename, []):
            if lineno:
                warnings.append('● Line %s: %s' % (lineno, warning))
            else:
                warnings.append('● %s' % warning)

        # If there are warnings, show the widget, and populate it.
        # Otherwise, hide the widget.
        self.warnings.delete('1.0', END)
        for warning in warnings:
            self.warnings.insert(END, warning, 'warning')
            self.warnings.insert(END, '\n')


    ######################################################
    # TK Main loop
    ######################################################

    def mainloop(self):
        self.root.mainloop()

    def handle_background_tasks(self):
        "Background queue handler"
        try:
            while True:
                result = self.results_queue.get(block=False)

                ########################
                # Output from the worker
                ########################

                if isinstance(result, Output):
                    self.run_status.set(result.message.capitalize())

                elif isinstance(result, WarningOutput):
                    if result.filename:
                        source_file = os.path.join(self.base_path, 'docs', result.filename)
                        self.project_file_tree.item(source_file, tags=['file', 'warning'])

                    # Archive the warning.
                    self.warning_output.setdefault(result.filename, []).append((result.lineno, result.message))

                elif isinstance(result, InitializationStart):
                    # Handle the "Start of sphinx init" message
                    self.progress.configure(mode='indeterminate', variable=None, maximum=None)
                    self.rebuild_all_button.configure(state=DISABLED)
                    self.rebuild_file_button.configure(state=DISABLED)
                    self.reload_config_button.configure(state=DISABLED)
                    self.progress.start()

                elif isinstance(result, InitializationEnd):
                    # Handle the "End of Sphinx init" message.
                    # Stop the progress spinner, and activate the work buttons.
                    self.run_status.set('Sphinx initialized.')
                    self.progress.stop()

                    self.rebuild_all_button.configure(state=ACTIVE)
                    self.rebuild_file_button.configure(state=ACTIVE)
                    self.reload_config_button.configure(state=ACTIVE)

                    # We can now inspect the extension type from the sphinx config.
                    self.source_extension = result.extension

                    # Set the initial file
                    self.project_file_tree.selection_set(os.path.join(self.base_path, 'docs', 'index' + self.source_extension))

                elif isinstance(result, BuildStart):
                    # Build start; set up the progress bar, set initial progress to 0
                    self.progress_value.set(0)
                    self.progress.configure(mode='determinate', maximum=100, variable=self.progress_value)

                    # Disable all the buttons so no new commands can be issued
                    self.rebuild_all_button.configure(state=DISABLED)
                    self.rebuild_file_button.configure(state=DISABLED)
                    self.reload_config_button.configure(state=DISABLED)

                    if result.filenames is None:
                        # Build is for all files. Clear the warnings, and
                        # set all files as dirty.
                        filenames = self.project_file_tree.tag_has('file')

                        self.warning_output = {}
                    else:
                        # Build is for a selection of files. Clear the global warnings
                        # and the file warnings, and set selected files as dirty.
                        filenames = result.filenames

                        self.warning_output[None] = []
                        for f in filenames:
                            self.warning_output[f] = []

                    for f in filenames:
                        self.project_file_tree.item(f, tags=['file', 'dirty'])

                elif isinstance(result, Progress):
                    try:
                        base, max_val = {
                            # Progress messages that will be received from a build.
                            # The returned values is a tuple, consisting of:
                            #  * The overall progress value when this task is at 0%
                            #  * The delta that will be added when the task is 100%
                            'reading sources': (0, 30),
                            'looking for now-outdated files': (30, 2),
                            'pickling environment': (32, 2),
                            'checking consistency': (34, 2),
                            'preparing documents': (36, 2),
                            'writing output': (38, 30),
                            'writing additional files': (68, 2),
                            'copying images': (70, 20),
                            'copying downloadable files': (90, 2),
                            'copying static files': (92, 2),
                            'dumping search index': (94, 2),
                            'dumping object inventory': (96, 2),
                            'writing templatebuiltins.js': (98, 2),
                        }[result.stage]

                        progress = int(base + max_val * result.progress / 100.0)
                        self.progress_value.set(progress)

                        # If this is a 'writing output' update, we have a file generated
                        # so update the markup of the tree
                        if result.stage == 'writing output':
                            source_file = os.path.join(self.base_path, 'docs', result.context + self.source_extension)
                            if not self.project_file_tree.tag_has('warning', source_file):
                                self.project_file_tree.item(source_file, tags=['file'])

                    except KeyError:
                        pass

                elif isinstance(result, BuildEnd):
                    # Build complete; mark progress as 100%
                    self.progress_value.set(100)

                    # Disable all the buttons so no new commands can be issued
                    self.rebuild_all_button.configure(state=ACTIVE)
                    self.rebuild_file_button.configure(state=ACTIVE)
                    self.reload_config_button.configure(state=ACTIVE)

                    current_file = self.project_file_tree.selection()[0]
                    if result.filenames is None or current_file in result.filenames:
                        self.html.refresh()
                        self._show_warnings(current_file)

                #########################
                # Output from the monitor
                #########################

                elif isinstance(result, FileChange):
                    # Make sure the new files are in the tree
                    for f in result.new:
                        dirname, filename = os.path.split(f)
                        self.project_file_tree.insert_dirname(dirname)
                        self.project_file_tree.insert_filename(dirname, filename)

                    # Enqueue a build task for all the new and modified documents.
                    self.work_queue.put(BuildSpecific(result.new + result.modified))

        except Empty:
            # queue.get() raises an exception when the queue is empty.
            # This means there is no more output to consume at this time.
            pass

        # Requeue for another update in 40ms (24*40ms == 1s - so this is
        # as fast as we need to update to match human visual acuity)
        self.root.after(40, self.handle_background_tasks)

    ######################################################
    # TK Command handlers
    ######################################################

    def cmd_quit(self):
        "Quit the program"
        # Notify the worker and monitor threads that we want to quit
        self.work_queue.put(Quit())
        self.stop_event.set()

        # Wait for the threads to die.
        self.worker_thread.join()
        self.monitor_thread.join()

        # Quit the main app.
        self.root.quit()

    def cmd_back(self, event=None):
        "Move back on the history stack"
        # We're traversing history, so flag it.
        self._traversing_history = True

        # Move back into history
        self._history_index = self._history_index - 1
        self.project_file_tree.selection_set(self._history[self._history_index - 1])

        # Update button state
        if self._history_index == 1:
            self.back_button.configure(state=DISABLED)
        self.forward_button.configure(state=NORMAL)

    def cmd_forward(self, event=None):
        "Move forward on the history stack"
        self._traversing_history = True
        self._history_index = self._history_index + 1
        self.project_file_tree.selection_set(self._history[self._history_index - 1])

        # Update button state
        if self._history_index == len(self._history):
            self.forward_button.configure(state=DISABLED)
        self.back_button.configure(state=NORMAL)

    def cmd_rebuild_all(self, event=None):
        "Rebuild the project."
        self.work_queue.put(BuildAll())

    def cmd_rebuild_file(self, event=None):
        "Rebuild the current file."
        # Determine the currently selected file
        filename = self.project_file_tree.selection()[0]

        # If the currently selected item is a file, build it.
        if filename and os.path.isfile(filename):
            self.work_queue.put(BuildSpecific([filename]))

    def cmd_reload_config(self, event=None):
        "Rebuild the current file."
        # Determine the currently selected file
        self.work_queue.put(ReloadConfig())

    def cmd_galley_page(self):
        "Show the Galley project page"
        webbrowser.open_new('http://pybee.org/galley')

    def cmd_galley_github(self):
        "Show the Galley GitHub repo"
        webbrowser.open_new('http://github.com/pybee/galley')

    def cmd_galley_docs(self):
        "Show the Galley documentation"
        # If this is a formal release, show the docs for that
        # version. otherwise, just show the head docs.
        if len(NUM_VERSION) == 3:
            webbrowser.open_new('https://galley.readthedocs.io/en/v%s/' % VERSION)
        else:
            webbrowser.open_new('https://galley.readthedocs.io/')

    def cmd_beeware_page(self):
        "Show the BeeWare project page"
        webbrowser.open_new('http://pybee.org/')

    ######################################################
    # Handlers for GUI actions
    ######################################################

    def on_file_selected(self, event):
        "When a file is selected, highlight the file and line"
        if event.widget.selection():
            filename = event.widget.selection()[0]

            if os.path.isfile(filename):
                # Display the file in the html view
                self.show_file(filename=filename)

    def on_link_click(self, event):
        "When a link is clicked, open the new URL"
        url_parts = urlparse(event.url)
        if url_parts.netloc and url_parts.scheme:
            webbrowser.open_new(event.url)
        else:
            # Link refers to HTML; convert back to source filename.
            path, ext = os.path.splitext(url_parts.path)
            filename = os.path.join(self.base_path, 'docs', path[:-1] + self.source_extension)
            index_filename = os.path.join(self.base_path, 'docs', path, 'index' + self.source_extension)
            if os.path.isfile(filename):
                self.project_file_tree.selection_set(filename)
            elif os.path.isfile(index_filename):
                self.project_file_tree.selection_set(index_filename)
            else:
                tkMessageBox.showerror(message="Couldn't find %s" % self.filename_normalizer(filename))