class MainWindow(QMainWindow):
    def __init__(self, screen_height, screen_width, version, parent=None):
        super(MainWindow, self).__init__(parent)

        self.screen_width = screen_width
        self.screen_height = screen_height
        self.version = version

        # basic main window settings
        self.resize(MAIN_WINDOW_WIDTH, MAIN_WINDOW_HEIGHT)
        self.setWindowTitle('Linguistica {}'.format(self.version))

        # lexicon and lexicon tree
        self.lexicon = None
        self.lexicon_tree = None
        self.initialize_lexicon_tree()

        # set up major display, parameter window, then load main window
        self.majorDisplay = QWidget()
        self.parameterWindow = QWidget()
        self.load_main_window()

        # 'File' menu and actions
        select_corpus_action = self.create_action(text='&Select corpus...',
                                                  slot=self.corpus_dir_dialog,
                                                  tip='Select a corpus file',
                                                  shortcut='Ctrl+N')
        select_wordlist_action = self.create_action(text='&Select wordlist...',
                                                    slot=self.wordlist_dir_dialog,
                                                    tip='Select a wordlist file',
                                                    shortcut='Ctrl+W')
        run_file_action = self.create_action(text='&Run...',
                                             slot=self.run_file,
                                             tip='Run the input file',
                                             shortcut='Ctrl+D')
        parameters_action = self.create_action(text='&Parameters...',
                                               slot=self.parameters_dialog,
                                               tip='Change parameters',
                                               shortcut='Ctrl+P')

        file_menu = self.menuBar().addMenu('&File')
        file_menu.addAction(select_corpus_action)
        file_menu.addAction(select_wordlist_action)
        file_menu.addAction(run_file_action)
        file_menu.addAction(parameters_action)

        self.status = self.statusBar()
        self.status.setSizeGripEnabled(False)
        self.status.showMessage('No input file loaded. To select one: File --> '
                                'Select corpus... or Select wordlist...')

    def initialize_lexicon_tree(self):
        self.lexicon_tree = QTreeWidget()
        self.lexicon_tree.setEnabled(True)
        self.lexicon_tree.setMinimumWidth(TREEWIDGET_WIDTH_MIN)
        self.lexicon_tree.setMaximumWidth(TREEWIDGET_WIDTH_MAX)
        self.lexicon_tree.setMinimumHeight(TREEWIDGET_HEIGHT_MIN)
        self.lexicon_tree.setHeaderLabel('')
        self.lexicon_tree.setItemsExpandable(True)
        # noinspection PyUnresolvedReferences
        self.lexicon_tree.itemClicked.connect(self.tree_item_clicked)

    def create_action(self, text=None, slot=None, tip=None, shortcut=None):
        """
        This create actions for the File menu, things like
        Read Corpus, Rerun Corpus etc
        """
        action = QAction(text, self)
        if shortcut:
            action.setShortcut(shortcut)
        if tip:
            action.setToolTip(tip)
            action.setStatusTip(tip)
        if slot:
            # noinspection PyUnresolvedReferences
            action.triggered.connect(slot)
        if shortcut:
            # noinspection PyUnresolvedReferences
            QShortcut(QKeySequence(shortcut), self).activated.connect(slot)
        return action

    def _get_filename_from_dialog(self, ftype='input'):
        self.determine_last_file()
        if self.last_file_path and self.last_file_type == ftype:
            open_dir = self.last_file_path
        else:
            open_dir = os.getcwd()
        # noinspection PyTypeChecker,PyCallByClass
        fname = QFileDialog.getOpenFileName(self,
                                            'Select the {} file'.format(ftype),
                                            open_dir)
        process_all_gui_events()

        # HACK: fname is supposed to be a string (at least according to the
        # PyQt5 documentation), but for some reason fname is a tuple.
        # So we need this hack to make sure that fname is a string of a filename
        # -- Jackson Lee, 2015/06/22

        # update: it's turned out that this behavior is due to compatibility
        # between PyQt and PySide. The "tuple" behavior is in line with the
        # newer API2 for PyQt. (PyQt on python 3 uses API2 by default.)
        # more here: http://srinikom.github.io/pyside-bz-archive/343.html
        # so perhaps we keep our current hack for now?
        # -- Jackson Lee, 2015/08/24

        if fname and any(fname) and (type(fname) is tuple):
            return fname[0]
        else:
            # if this hack isn't needed somehow...
            return fname

    def corpus_dir_dialog(self):
        """
        Pop up the "open a file" dialog and ask for which corpus text file
        to use
        """
        self.corpus_filename = self._get_filename_from_dialog(ftype='corpus')

        process_all_gui_events()

        if type(self.corpus_filename) != str:
            return

        # note that self.corpus_filename is an absolute full path
        self.corpus_name = os.path.basename(self.corpus_filename)
        self.corpus_stem_name = Path(self.corpus_name).stem

        self.lexicon = read_corpus(self.corpus_filename)
        self.initialize_lexicon_tree()
        self.load_main_window(major_display=QWidget(),
                              parameter_window=QWidget())
        process_all_gui_events()

        self.status.clearMessage()
        self.status.showMessage(
            'Corpus selected: {}'.format(self.corpus_filename))

    def wordlist_dir_dialog(self):
        """
        Pop up the "open a file" dialog and ask for which corpus text file
        to use
        """
        self.corpus_filename = self._get_filename_from_dialog(ftype='wordlist')

        process_all_gui_events()

        if type(self.corpus_filename) != str:
            return

        # note that self.corpus_filename is an absolute full path
        self.corpus_name = os.path.basename(self.corpus_filename)
        self.corpus_stem_name = Path(self.corpus_name).stem

        self.lexicon = read_wordlist(self.corpus_filename)
        self.initialize_lexicon_tree()
        self.load_main_window(major_display=QWidget(),
                              parameter_window=QWidget())
        process_all_gui_events()

        self.status.clearMessage()
        self.status.showMessage(
            'Wordlist selected: {}'.format(self.corpus_filename))

    def parameters_dialog(self):
        if self.lexicon is None:
            warning = QMessageBox()
            warning.setIcon(QMessageBox.Warning)
            warning.setText('Parameters can only be accessed when an input '
                            'file is specified.')
            warning.setWindowTitle('No input file selected')
            warning.setStandardButtons(QMessageBox.Ok)
            warning.exec_()
            return

        process_all_gui_events()

        parameters = self.lexicon.parameters()
        dialog = QDialog()
        layout = QVBoxLayout()
        layout.addWidget(
            QLabel('Filename: {}'.format(Path(self.corpus_filename).name)))
        file_type = 'Wordlist' if self.lexicon.file_is_wordlist else 'Corpus'
        layout.addWidget(QLabel('Type: {}'.format(file_type)))

        grid = QGridLayout()
        self.parameter_spinboxes = [QSpinBox() for _ in range(len(parameters))]

        for i, parameter_name in enumerate(sorted(parameters.keys())):
            self.parameter_spinboxes[i].setObjectName(parameter_name)
            self.parameter_spinboxes[i].setRange(
                *PARAMETERS_RANGES[parameter_name])
            self.parameter_spinboxes[i].setValue(parameters[parameter_name])
            self.parameter_spinboxes[i].setSingleStep(1)
            # noinspection PyUnresolvedReferences
            self.parameter_spinboxes[i].valueChanged.connect(
                self.update_parameter)

            grid.addWidget(QLabel(parameter_name), i, 0)
            grid.addWidget(self.parameter_spinboxes[i], i, 1)
            grid.addWidget(QLabel(PARAMETERS_HINTS[parameter_name]), i, 2)

        layout.addLayout(grid)

        reset_button = QPushButton()
        reset_button.setText('&Reset')
        # noinspection PyUnresolvedReferences
        reset_button.clicked.connect(self.reset_parameters)

        spacer = QWidget()  # just for padding in tool_bar
        spacer.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)

        tool_bar = QHBoxLayout()
        tool_bar.addWidget(spacer)  # so that the buttons are right-aligned
        tool_bar.addWidget(reset_button)

        layout.addLayout(tool_bar)

        dialog.setLayout(layout)
        dialog.setWindowTitle('Parameters')
        dialog.exec_()

    def reset_parameters(self):
        self.lexicon.use_default_parameters()

        for i, (_, value) in \
                enumerate(sorted(self.lexicon.parameters().items())):
            self.parameter_spinboxes[i].setValue(value)

    def update_parameter(self):
        for i in range(len(self.lexicon.parameters())):
            parameter_name, new_value = \
                self.parameter_spinboxes[i].objectName(), \
                self.parameter_spinboxes[i].value()
            self.lexicon.change_parameters(**{parameter_name: new_value})

    def update_progress(self, progress_text, target_percentage):
        """
        Update the progress dialog. This function is triggered by the
        "progress_signal" emitted from the linguistica component worker thread.
        """
        self.progressDialog.setLabelText(progress_text)
        self.progressDialog.setValue(target_percentage)
        process_all_gui_events()

    # noinspection PyProtectedMember
    def run_file(self):
        if self.lexicon is None:
            warning = QMessageBox()
            warning.setIcon(QMessageBox.Warning)
            warning.setText('No input file is selected.')
            warning.setWindowTitle('Error')
            warning.setStandardButtons(QMessageBox.Ok)
            warning.exec_()
            return

        self.status.clearMessage()
        self.status.showMessage('Running the file {} now...'
                                .format(self.corpus_name))

        print('\nInput file in use:\n{}\n'.format(self.corpus_filename),
              flush=True)

        # set up the Linguistica components worker
        # The worker is a QThread. We spawn this thread, and the linguistica
        # components run on this new thread but not the main thread for the GUI.
        # This makes the GUI still responsive
        # while the long and heavy running process of
        # the Linguistica components is ongoing.

        self.lxa_worker = LinguisticaWorker(self.lexicon)
        self.lxa_worker.progress_signal.connect(self.update_progress)

        # set up progress dialog

        process_all_gui_events()
        self.progressDialog = QProgressDialog()
        self.progressDialog.setRange(0, 100)  # it's like from 0% to 100%
        self.progressDialog.setLabelText('Initializing...')
        self.progressDialog.setValue(0)  # initialize as 0 (= 0%)
        self.progressDialog.setWindowTitle(
            'Processing {}'.format(self.corpus_name))
        self.progressDialog.setCancelButton(None)
        self.progressDialog.resize(400, 100)
        process_all_gui_events()

        self.progressDialog.show()

        # We disable the "cancel" button
        # Setting up a "cancel" mechanism may not be a good idea,
        # since it would probably involve killing the linguistica component
        # worker at *any* point of its processing.
        # This may have undesirable effects (e.g., freezing the GUI) -- BAD!

        # make sure all GUI stuff up to this point has been processed before
        # doing the real work of running the Lxa components
        process_all_gui_events()

        # Now the real work begins here!
        self.lxa_worker.start()

        process_all_gui_events()

        self.lexicon = self.lxa_worker.get_lexicon()

        print('\nAll Linguistica components run for the file', flush=True)
        self.status.clearMessage()
        self.status.showMessage('{} processed'.format(self.corpus_name))

        self.load_main_window(major_display=QWidget(),
                              parameter_window=QWidget())
        self.populate_lexicon_tree()
        self.update_last_file()
        process_all_gui_events()

        # display corpus name (in the tree header label)
        file_type = 'wordlist' if self.lexicon.file_is_wordlist else 'corpus'
        header_label = 'File: {}\nFile type: {}\n\n# word types: {:,}\n'.format(
            self.corpus_name, file_type, self.lexicon.number_of_word_types())
        if file_type == 'corpus':
            header_label += '# word tokens: {:,}\n'.format(
                self.lexicon.number_of_word_tokens())
        self.lexicon_tree.setHeaderLabel(header_label)

    @staticmethod
    def ensure_config_dir_exists():
        if not os.path.isdir(CONFIG_DIR):
            os.mkdir(CONFIG_DIR)

    def determine_last_file(self):
        self.last_file_path = None
        self.last_file_type = None
        self.last_file_encoding = None

        if not os.path.isfile(CONFIG_LAST_FILE):
            return

        with open(CONFIG_LAST_FILE, encoding='utf8') as f:
            config_last_file = json.load(f)

        self.last_file_path = config_last_file['last_file_path']
        self.last_file_type = config_last_file['last_file_type']
        self.last_file_encoding = config_last_file['last_file_encoding']

    def update_last_file(self):
        self.ensure_config_dir_exists()
        with open(CONFIG_LAST_FILE, 'w', encoding='utf8') as f:
            if self.lexicon.file_is_wordlist:
                file_type = 'wordlist'
            else:
                file_type = 'corpus'

            config = {'last_file_path': self.lexicon.file_abspath,
                      'last_file_type': file_type,
                      'last_file_encoding': self.lexicon.encoding,
                      }

            json.dump(config, f)

    def populate_lexicon_tree(self):
        self.lexicon_tree.clear()
        process_all_gui_events()

        # wordlist
        ancestor = QTreeWidgetItem(self.lexicon_tree, [WORDLIST])
        self.lexicon_tree.expandItem(ancestor)

        # word ngrams
        ancestor = QTreeWidgetItem(self.lexicon_tree, [WORD_NGRAMS])
        self.lexicon_tree.expandItem(ancestor)
        for item_str in [BIGRAMS, TRIGRAMS]:
            item = QTreeWidgetItem(ancestor, [item_str])
            self.lexicon_tree.expandItem(item)

        # signatures
        ancestor = QTreeWidgetItem(self.lexicon_tree, [SIGNATURES])
        self.lexicon_tree.expandItem(ancestor)
        for item in [SIGS_TO_STEMS, WORDS_TO_SIGS]:
            self.lexicon_tree.expandItem(QTreeWidgetItem(ancestor, [item]))

        # tries
        ancestor = QTreeWidgetItem(self.lexicon_tree, [TRIES])
        self.lexicon_tree.expandItem(ancestor)
        for item in [WORDS_AS_TRIES, SUCCESSORS, PREDECESSORS]:
            self.lexicon_tree.expandItem(QTreeWidgetItem(ancestor, [item]))

        # phonology
        ancestor = QTreeWidgetItem(self.lexicon_tree, [PHONOLOGY])
        self.lexicon_tree.expandItem(ancestor)
        for item in [PHONES, BIPHONES, TRIPHONES]:
            self.lexicon_tree.expandItem(QTreeWidgetItem(ancestor, [item]))

        # manifolds
        ancestor = QTreeWidgetItem(self.lexicon_tree, [MANIFOLDS])
        self.lexicon_tree.expandItem(ancestor)
        for item in [WORD_NEIGHBORS, VISUALIZED_GRAPH]:
            self.lexicon_tree.expandItem(QTreeWidgetItem(ancestor, [item]))

        self.status.clearMessage()
        self.status.showMessage('Navigation tree populated')
        print('Lexicon navigation tree populated', flush=True)

    def load_main_window(self, major_display=None, parameter_window=None):
        """
        Refresh the main window for the updated display content
        (most probably after a click or some event is triggered)
        """
        # get sizes of the three major PyQt objects
        major_display_size = self.majorDisplay.size()
        parameter_window_size = self.parameterWindow.size()
        lexicon_tree_size = self.lexicon_tree.size()

        if major_display:
            self.majorDisplay = major_display
        if parameter_window:
            self.parameterWindow = parameter_window

        # apply sizes to the major three objects
        self.majorDisplay.resize(major_display_size)
        self.parameterWindow.resize(parameter_window_size)
        self.lexicon_tree.resize(lexicon_tree_size)

        # set up:
        # 1) main splitter (b/w lexicon-tree+parameter window and major display)
        # 2) minor splitter (b/w lexicon-tree and parameter window)
        self.mainSplitter = QSplitter(Qt.Horizontal)
        self.mainSplitter.setHandleWidth(10)
        self.mainSplitter.setChildrenCollapsible(False)

        self.minorSplitter = QSplitter(Qt.Vertical)
        self.minorSplitter.setHandleWidth(10)
        self.minorSplitter.setChildrenCollapsible(False)

        self.minorSplitter.addWidget(self.lexicon_tree)
        self.minorSplitter.addWidget(self.parameterWindow)

        self.mainSplitter.addWidget(self.minorSplitter)
        self.mainSplitter.addWidget(self.majorDisplay)

        self.setCentralWidget(self.mainSplitter)

    def sig_to_stems_clicked(self, row):
        signature = self.sig_to_stems_major_table.item(row, 0).text()
        print(signature)
        signature = tuple(signature.split(SEP_SIG))

        stems = sorted(self.lexicon.signatures_to_stems()[signature])
        number_of_stems_per_column = 5

        # create a master list of sublists, where each sublist contains k stems
        # k = number_of_stems_per_column
        stem_rows = list()
        stem_row = list()

        for i, stem in enumerate(stems, 1):
            stem_row.append(stem)
            if not i % number_of_stems_per_column:
                stem_rows.append(stem_row)
                stem_row = list()
        if stem_row:
            stem_rows.append(stem_row)

        # set up the minor table as table widget
        sig_to_stems_minor_table = QTableWidget()
        sig_to_stems_minor_table.horizontalHeader().hide()
        sig_to_stems_minor_table.verticalHeader().hide()
        sig_to_stems_minor_table.clear()
        sig_to_stems_minor_table.setRowCount(len(stem_rows))
        sig_to_stems_minor_table.setColumnCount(number_of_stems_per_column)

        # fill in the minor table
        for row, stem_row in enumerate(stem_rows):
            for col, stem in enumerate(stem_row):
                item = QTableWidgetItem(stem)
                sig_to_stems_minor_table.setItem(row, col, item)

        sig_to_stems_minor_table.resizeColumnsToContents()

        minor_table_title = QLabel('{} (number of stems: {})'
                                   .format(SEP_SIG.join(signature), len(stems)))

        minor_table_widget_with_title = QWidget()
        layout = QVBoxLayout()
        layout.addWidget(minor_table_title)
        layout.addWidget(sig_to_stems_minor_table)
        minor_table_widget_with_title.setLayout(layout)

        new_display = QSplitter(Qt.Horizontal)
        new_display.setHandleWidth(10)
        new_display.setChildrenCollapsible(False)

        new_display.addWidget(self.sig_to_stems_major_table)
        new_display.addWidget(minor_table_widget_with_title)
        new_display_width = self.majorDisplay.width() / 2
        new_display.setSizes(
            [new_display_width * 0.4, new_display_width * 0.6])

        self.load_main_window(major_display=new_display)
        self.status.clearMessage()
        self.status.showMessage('{} selected'.format(signature))

    def unavailable_for_wordlist(self):
        self.load_main_window(major_display=QWidget(),
                              parameter_window=QWidget())
        self.status.showMessage('')
        warning = QMessageBox()
        warning.setIcon(QMessageBox.Warning)
        warning.setText('Unavailable for a wordlist')
        warning.setWindowTitle('Error')
        warning.setStandardButtons(QMessageBox.Ok)
        warning.exec_()

    def tree_item_clicked(self, item):
        """
        Trigger the appropriate action when something in the lexicon tree
        is clicked, and update the major display plus parameter window
        """
        item_str = item.text(0)

        if item_str in {WORD_NGRAMS, SIGNATURES, TRIES, PHONOLOGY, MANIFOLDS}:
            return

        print('loading', item_str, flush=True)

        self.status.clearMessage()
        self.status.showMessage('Loading {}...'.format(item_str))

        new_display = None
        new_parameter_window = None

        if item_str == WORDLIST:
            new_display = self.create_major_display_table(
                self.lexicon.word_phonology_dict().items(),
                key=lambda x: x[1].count, reverse=True,
                headers=['Word', 'Count', 'Frequency', 'Phones',
                         'Unigram plog', 'Avg unigram plog',
                         'Bigram plog', 'Avg bigram plog'],
                row_cell_functions=[
                    lambda x: x[0], lambda x: x[1].count,
                    lambda x: x[1].frequency,
                    lambda x: ' '.join(x[1].phones),
                    lambda x: x[1].unigram_plog,
                    lambda x: x[1].avg_unigram_plog,
                    lambda x: x[1].bigram_plog,
                    lambda x: x[1].avg_bigram_plog],
                cutoff=0)

        elif item_str == BIGRAMS:
            if self.lexicon.file_is_wordlist:
                self.unavailable_for_wordlist()
                return
            new_display = self.create_major_display_table(
                self.lexicon.word_bigram_counter().items(),
                key=lambda x: x[1], reverse=True,
                headers=['Bigram', 'Count'],
                row_cell_functions=[lambda x: SEP_NGRAM.join(x[0]),
                                    lambda x: x[1]],
                cutoff=2000)

        elif item_str == TRIGRAMS:
            if self.lexicon.file_is_wordlist:
                self.unavailable_for_wordlist()
                return
            new_display = self.create_major_display_table(
                self.lexicon.word_trigram_counter().items(),
                key=lambda x: x[1], reverse=True,
                headers=['Trigram', 'Count'],
                row_cell_functions=[lambda x: SEP_NGRAM.join(x[0]),
                                    lambda x: x[1]],
                cutoff=2000)

        elif item_str == SIGS_TO_STEMS:
            self.sig_to_stems_major_table = self.create_major_display_table(
                self.lexicon.signatures_to_stems().items(),
                key=lambda x: len(x[1]), reverse=True,
                headers=['Signature', 'Stem count', 'A few stems'],
                row_cell_functions=[lambda x: SEP_SIG.join(x[0]),
                                    lambda x: len(x[1]),
                                    lambda x: ', '.join(sorted(x[1])[:2]) +
                                              ', ...'],
                cutoff=0)
            # noinspection PyUnresolvedReferences
            self.sig_to_stems_major_table.cellClicked.connect(
                self.sig_to_stems_clicked)
            new_display = self.sig_to_stems_major_table

        elif item_str == WORDS_TO_SIGS:
            new_display = self.create_major_display_table(
                self.lexicon.words_to_signatures().items(),
                key=lambda x: len(x[1]), reverse=True,
                headers=['Word', 'Signature count', 'Signatures'],
                row_cell_functions=[lambda x: x[0],
                                    lambda x: len(x[1]),
                                    lambda x: ', '.join([SEP_SIG.join(sig)
                                                         for sig in
                                                         sorted(x[1])])],
                cutoff=2000)

        elif item_str == WORDS_AS_TRIES:
            words = self.lexicon.broken_words_left_to_right().keys()
            words_to_tries = dict()
            # key: word (str)
            # value: tuple of (str, str)
            # for left-to-right and right-to-left tries

            for word in words:
                l_r = ' '.join(self.lexicon.broken_words_left_to_right()[word])
                r_l = ' '.join(self.lexicon.broken_words_right_to_left()[word])
                words_to_tries[word] = (l_r, r_l)  # left-right, right-left

            new_display = self.create_major_display_table(
                words_to_tries.items(),
                key=lambda x: x[0], reverse=False,
                headers=['Word', 'Reversed word',
                         'Left-to-right trie', 'Right-to-left trie'],
                row_cell_functions=[lambda x: x[0], lambda x: x[0][::-1],
                                    lambda x: x[1][0], lambda x: x[1][1]],
                cutoff=0, set_text_alignment=[(3, Qt.AlignRight)])

        elif item_str == SUCCESSORS:
            new_display = self.create_major_display_table(
                self.lexicon.successors().items(),
                key=lambda x: len(x[1]), reverse=True,
                headers=['String', 'Successor count', 'Successors'],
                row_cell_functions=[lambda x: x[0],
                                    lambda x: len(x[1]),
                                    lambda x: ', '.join(sorted(x[1]))],
                cutoff=0)

        elif item_str == PREDECESSORS:
            new_display = self.create_major_display_table(
                self.lexicon.predecessors().items(),
                key=lambda x: len(x[1]), reverse=True,
                headers=['String', 'Predecessor count', 'Predecessors'],
                row_cell_functions=[lambda x: x[0],
                                    lambda x: len(x[1]),
                                    lambda x: ', '.join(sorted(x[1]))],
                cutoff=0)

        elif item_str == PHONES:
            new_display = self.create_major_display_table(
                self.lexicon.phone_dict().items(),
                key=lambda x: x[1].count, reverse=True,
                headers=['Phone', 'Count', 'Frequency', 'Plog'],
                row_cell_functions=[lambda x: x[0],
                                    lambda x: x[1].count,
                                    lambda x: x[1].frequency,
                                    lambda x: x[1].plog],
                cutoff=0)

        elif item_str == BIPHONES:
            new_display = self.create_major_display_table(
                self.lexicon.biphone_dict().items(),
                key=lambda x: x[1].count, reverse=True,
                headers=['Biphone', 'Count', 'Frequency',
                         'Mutual information (MI)', 'Weighted MI'],
                row_cell_functions=[lambda x: SEP_NGRAM.join(x[0]),
                                    lambda x: x[1].count,
                                    lambda x: x[1].frequency,
                                    lambda x: x[1].MI,
                                    lambda x: x[1].weighted_MI],
                cutoff=0)

        elif item_str == TRIPHONES:
            new_display = self.create_major_display_table(
                self.lexicon.phone_trigram_counter().items(),
                key=lambda x: x[1], reverse=True,
                headers=['Triphone', 'Count'],
                row_cell_functions=[lambda x: SEP_NGRAM.join(x[0]),
                                    lambda x: x[1]],
                cutoff=0)

        elif item_str == WORD_NEIGHBORS:
            if self.lexicon.file_is_wordlist:
                self.unavailable_for_wordlist()
                return
            word_to_freq = self.lexicon.word_unigram_counter()
            new_display = self.create_major_display_table(
                self.lexicon.words_to_neighbors().items(),
                key=lambda x: word_to_freq[x[0]], reverse=True,
                headers=['Word', 'Word count', 'Neighbors'],
                row_cell_functions=[lambda x: x[0],
                                    lambda x: word_to_freq[x[0]],
                                    lambda x: ' '.join(x[1])],
                cutoff=0)

        elif item_str == VISUALIZED_GRAPH:
            if self.lexicon.file_is_wordlist:
                self.unavailable_for_wordlist()
                return

            graph_width = self.screen_width - TREEWIDGET_WIDTH_MAX - 50
            graph_height = self.screen_height - 70
            html_name = 'show_manifold.html'

            manifold_name = '{}_manifold.json'.format(self.corpus_stem_name)
            manifold_filename = os.path.join(CONFIG_DIR, manifold_name)
            print('manifold_filename', manifold_filename)

            manifold_json_data = json_graph.node_link_data(
                self.lexicon.neighbor_graph())
            json.dump(manifold_json_data, open(manifold_filename, 'w'))

            viz_html = os.path.join(CONFIG_DIR, html_name)
            print('viz_html', viz_html)

            # write the show_manifold html file
            with open(viz_html, 'w') as f:
                print(SHOW_MANIFOLD_HTML.format(os.path.dirname(__file__),
                                                graph_width, graph_height,
                                                manifold_filename), file=f)

            url = Path(viz_html).as_uri()
            print('url:', url)

            new_display = QWebView()
            new_display.setUrl(QUrl(url))

        self.load_main_window(major_display=new_display,
                              parameter_window=new_parameter_window)

        self.status.clearMessage()
        self.status.showMessage('{} selected'.format(item_str))

    @staticmethod
    def create_major_display_table(input_iterable,
                                   key=lambda x: x, reverse=False,
                                   headers=None, row_cell_functions=None,
                                   cutoff=0,
                                   set_text_alignment=None):
        """
        This is a general function for creating a tabular display for the
        major display.
        """

        if not input_iterable:
            print('Warning: input is empty', flush=True)
            return

        if not hasattr(input_iterable, '__iter__'):
            print('Warning: input is not an iterable', flush=True)
            return

        number_of_headers = len(headers)
        number_of_columns = len(row_cell_functions)

        if number_of_headers != number_of_columns:
            print('headers and cell functions don\'t match', flush=True)
            return

        len_input = len(input_iterable)

        table_widget = QTableWidget()
        table_widget.clear()
        table_widget.setSortingEnabled(False)

        # set up row count
        if cutoff and cutoff < len_input:
            actual_cutoff = cutoff
        else:
            actual_cutoff = len_input

        table_widget.setRowCount(actual_cutoff)

        # set up column count and table headers
        table_widget.setColumnCount(number_of_headers)
        table_widget.setHorizontalHeaderLabels(headers)

        # fill in the table
        for row, x in enumerate(double_sorted(input_iterable, key=key,
                                              reverse=reverse)):
            for col, fn in enumerate(row_cell_functions):
                cell = fn(x)

                if isinstance(cell, (int, float)):
                    # cell is numeric
                    item = QTableWidgetItem()
                    item.setData(Qt.EditRole, cell)
                else:
                    # cell is not numeric
                    item = QTableWidgetItem(cell)

                if set_text_alignment:
                    for align_col, alignment in set_text_alignment:
                        if col == align_col:
                            item.setTextAlignment(alignment)

                table_widget.setItem(row, col, item)

            if not row < actual_cutoff:
                break

        table_widget.setSortingEnabled(True)
        table_widget.resizeColumnsToContents()

        return table_widget
    def run_file(self):
        if self.lexicon is None:
            warning = QMessageBox()
            warning.setIcon(QMessageBox.Warning)
            warning.setText('No input file is selected.')
            warning.setWindowTitle('Error')
            warning.setStandardButtons(QMessageBox.Ok)
            warning.exec_()
            return

        self.status.clearMessage()
        self.status.showMessage('Running the file {} now...'
                                .format(self.corpus_name))

        print('\nInput file in use:\n{}\n'.format(self.corpus_filename),
              flush=True)

        # set up the Linguistica components worker
        # The worker is a QThread. We spawn this thread, and the linguistica
        # components run on this new thread but not the main thread for the GUI.
        # This makes the GUI still responsive
        # while the long and heavy running process of
        # the Linguistica components is ongoing.

        self.lxa_worker = LinguisticaWorker(self.lexicon)
        self.lxa_worker.progress_signal.connect(self.update_progress)

        # set up progress dialog

        process_all_gui_events()
        self.progressDialog = QProgressDialog()
        self.progressDialog.setRange(0, 100)  # it's like from 0% to 100%
        self.progressDialog.setLabelText('Initializing...')
        self.progressDialog.setValue(0)  # initialize as 0 (= 0%)
        self.progressDialog.setWindowTitle(
            'Processing {}'.format(self.corpus_name))
        self.progressDialog.setCancelButton(None)
        self.progressDialog.resize(400, 100)
        process_all_gui_events()

        self.progressDialog.show()

        # We disable the "cancel" button
        # Setting up a "cancel" mechanism may not be a good idea,
        # since it would probably involve killing the linguistica component
        # worker at *any* point of its processing.
        # This may have undesirable effects (e.g., freezing the GUI) -- BAD!

        # make sure all GUI stuff up to this point has been processed before
        # doing the real work of running the Lxa components
        process_all_gui_events()

        # Now the real work begins here!
        self.lxa_worker.start()

        process_all_gui_events()

        self.lexicon = self.lxa_worker.get_lexicon()

        print('\nAll Linguistica components run for the file', flush=True)
        self.status.clearMessage()
        self.status.showMessage('{} processed'.format(self.corpus_name))

        self.load_main_window(major_display=QWidget(),
                              parameter_window=QWidget())
        self.populate_lexicon_tree()
        self.update_last_file()
        process_all_gui_events()

        # display corpus name (in the tree header label)
        file_type = 'wordlist' if self.lexicon.file_is_wordlist else 'corpus'
        header_label = 'File: {}\nFile type: {}\n\n# word types: {:,}\n'.format(
            self.corpus_name, file_type, self.lexicon.number_of_word_types())
        if file_type == 'corpus':
            header_label += '# word tokens: {:,}\n'.format(
                self.lexicon.number_of_word_tokens())
        self.lexicon_tree.setHeaderLabel(header_label)