class TableFrameWithInputOutputBox(TableFrame):
    ''' Extends TableFrame with extra functionality necessary for data
        modification and choosing input and output categories.

        Attributes:
            params_frame (ParamsFrame): frame with parameters, this
                class communicates
                with params_frame when data is loaded or modified.
            combobox_text_var (StringVar): StringVar object that stores
                categorical category.
            panel_text_observer (PanelTextObserver): observer that adds star to
                label frame of the parent of this widget.
                This class notifies panel_text_observer
                when data was modified.
            frames (list of Frame): list of frames that hold Checkbuttons for
                choosing input and output categories.
            row_checkboxes (list of Checkbutton): list of Checkbuttons used
                for removing rows.
            col_checkboxes (list of Checkbutton): list of Checkbuttons used
                for removing columns.
            current_categories (list of str): list of current valid categories.
                This class might modify this list.
            str_var_for_input_output_boxes (StringVar): StringVar object that
                is used for communication
                with ParamsFrame. If the content of
                str_var_for_input_output_boxes was modified,
                it means that data was loaded from parameters file
                and input and output categories
                must be checked depending on parameters file.
            data (list of list of str or float): input data, it might
                be modified by this class.

        Args:
            parent (Tk object): parent of this widget.
            params_frame (ParamsFrame): frame with parameters, this class
                communicates
                with params_frame when data is loaded or modified.
            combobox_text_var (StringVar): StringVar object that stores
                categorical category.
            current_categories (list of str): list of current valid categories.
                This class might modify this list.
            str_var_for_input_output_boxes (StringVar): StringVar object
                that is used for communication
                with ParamsFrame. If the content of
                str_var_for_input_output_boxes was modified,
                it means that data was loaded from parameters file and input
                and output categories
                must be checked depending on parameters file.
            if_text_modified_str (StringVar): StringVar object that is used
                by PanelTextObserver, its content is modified when data
                was modified.
            data (list of list of str or float): input data, it might be
                modified by this class.
            nb_rows (int, optional): number of rows of the table, defaults
                to 20.
            nb_cols (int, optional): number of columns of the table,
                defaults to 5.
    '''
    def __init__(self, parent, params_frame,
                 combobox_text_var, current_categories,
                 str_var_for_input_output_boxes,
                 if_text_modified_str, data,
                 nb_rows=20, nb_cols=5):
        self.params_frame = params_frame
        self.combobox_text_var = combobox_text_var
        self.panel_text_observer = PanelTextObserver(if_text_modified_str)
        self.frames = []
        self.row_checkboxes = []
        self.col_checkboxes = []
        self.current_categories = current_categories
        self.str_var_for_input_output_boxes = str_var_for_input_output_boxes
        self.str_var_for_input_output_boxes.trace('w', self.on_load_categories)
        super().__init__(parent, data, nb_rows, nb_cols)

    def create_widgets(self):
        ''' Creates widgets of this class.
        '''
        super().create_widgets()
        for column_index in range(self.nb_cols - 1):
            self._create_input_output_box(column_index)
        for row_index in range(self.nb_rows):
            self.add_row_check_box(row_index)
        # add observers to add * in the first column
        for row_index in range(self.nb_rows):
            self.cells[row_index][0].panel_text_observer = self.panel_text_observer

    def create_entry_widget(self, parent):
        ''' Creates SelfValidatingEntry widget.

            Args:
                parent (Tk object): parent of the SelfValidatingEntry widget.

            Returns:
                SelfValidatingEntry: created SelfValidatingEntry widget.
        '''
        return SelfValidatingEntry(parent, self.data, self.cells, width=CELL_WIDTH)

    def deselect_all_boxes(self):
        ''' Deselects all Checkbuttons used for choosing input and
            output categories.
        '''
        for frame in self.frames:
            for child in frame.winfo_children():
                child.deselect()

    def _create_input_output_box(self, column_index):
        ''' Creates Checkbuttons used for choosing input and output categories.

            Args:
                column_index (int): index of a column for which
                    Checkbuttons must be created.
        '''
        frame_for_btns = Frame(self.frame_with_table)
        self.frames.append(frame_for_btns)
        input_var = IntVar()
        output_var = IntVar()
        input_btn = ObserverCheckbutton(
            frame_for_btns, input_var, output_var,
            self.params_frame.input_categories_frame,
            self.params_frame.output_categories_frame,
            self.current_categories, self.cells, INPUT_OBSERVER,
            self.params_frame.change_category_name,
            self.data, self.combobox_text_var,
            text='Input', state=DISABLED)
        input_btn.grid(row=1, column=0, sticky=N+W)
        output_btn = FollowingObserverCheckbutton(
            frame_for_btns, output_var, input_var,
            self.params_frame.output_categories_frame,
            self.params_frame.input_categories_frame,
            self.current_categories, self.cells, OUTPUT_OBSERVER,
            self.params_frame.change_category_name,
            self.data, self.combobox_text_var, input_btn,
            text='Output', state=DISABLED)
        output_btn.grid(row=2, column=0, sticky=N+W)
        self._add_observers(input_btn, output_btn, column_index + 1)
        var = IntVar()
        column_checkbox = CheckbuttonWithVar(frame_for_btns, var)
        column_checkbox.grid(row=0, column=0)
        self.col_checkboxes.append((column_checkbox, var))
        frame_for_btns.grid(row=1, column=column_index + 2, sticky=N)

    def _add_observers(self, input_btn, output_btn, column_index):
        ''' Adds observers to newly created cells in a given column.

            Args:
                input_btn (ObserverCheckbutton): observer used to select
                    input categories.
                output_btn (FollowingObserverCheckbutton): observer used
                    to select output categories.
                column_index (int): index of the column to cells of
                    which observers must be added.
        '''
        names_modifier = DefaultCategoriesAndDMUModifier(
            self.cells, self.current_categories)
        for row_index in range(self.nb_rows):
            self._add_observers_to_cell(self.cells[row_index][column_index],
                                        names_modifier, input_btn, output_btn)

    def _add_observers_to_cell(self, cell, names_modifier, input_btn,
                               output_btn):
        ''' Adds given observers to a given cell.

            Args:
                cell (SelfValidatingEntry): cell where observers must be added.
                names_modifier (DefaultCategoriesAndDMUModifier): observer,
                    for details see  DefaultCategoriesAndDMUModifier.
                input_btn (ObserverCheckbutton): observer used to select
                    input categories.
                output_btn (FollowingObserverCheckbutton): observer used to
                    select output categories.
        '''
        cell.observers.append(names_modifier)  # IMPORTANT:
        # this observer MUST be added first, it modifies data that
        # is used by other observers!
        cell.observers.append(input_btn)
        cell.observers.append(output_btn)
        cell.panel_text_observer = self.panel_text_observer

    def on_load_categories(self, *args):
        ''' Selects input and output categories when data is loaded from
            parameters file. Args are provided by the StringVar trace
            methods and are ignored in this method.
        '''
        for frame in self.frames:
            for child in frame.winfo_children():
                try:
                    category = child.get_category()
                except AttributeError:
                    pass
                else:
                    if (child.observer_type == INPUT_OBSERVER and
                            child.get_category() in
                            self.str_var_for_input_output_boxes.input_categories):
                        child.select()
                    if (child.observer_type == OUTPUT_OBSERVER and
                            child.get_category() in
                            self.str_var_for_input_output_boxes.output_categories):
                        child.select()

    def add_row_check_box(self, row_index):
        ''' Adds Checkbutton used for removing rows to a given row.

            Args:
                row_index (int): index of row to which Checkbutton
                    must be added.
        '''
        if row_index >= 1:
            var = IntVar()
            row_checkbox = Checkbutton(self.frame_with_table, variable=var)
            self.row_checkboxes.append((row_checkbox, var))
            row_checkbox.grid(row=row_index + 2, column=0)
        else:
            self.row_checkboxes.append((None, None))

    def add_column(self):
        ''' Adds one column to the end of table.
        '''
        super().add_column()
        self._create_input_output_box(self.nb_cols - 2)

    def add_row(self):
        ''' Adds one row to the end of table.

            Note: When data is spread across several pages, addition of
                row must also update the display of data.
                This functionality is implemented in TableModifierFrame.
        '''
        super().add_row()
        self.add_row_check_box(self.nb_rows - 1)
        names_modifier = DefaultCategoriesAndDMUModifier(
            self.cells, self.current_categories)
        for col in range(1, self.nb_cols):
            input_btn, output_btn = self.get_check_boxes(col - 1)
            self._add_observers_to_cell(self.cells[self.nb_rows - 1][col],
                                        names_modifier,
                                        input_btn, output_btn)

    def get_check_boxes(self, column_index):
        ''' Gets Checkbuttons used for selecting input and output categories
            for a given column.

            Args:
                column_index (int): index of the column for which Checkbuttons
                    must be returned.

            Returns:
                tuple of ObserverCheckbutton, FollowingObserverCheckbutton:
                    tuple of observers
                    or None, None if no observers were found.
        '''
        if column_index < 0 or column_index >= len(self.frames):
            return None, None
        input_btn = None
        output_btn = None
        for child in self.frames[column_index].winfo_children():
            try:
                observer_type = child.observer_type
            except AttributeError:
                pass
            else:
                if observer_type == INPUT_OBSERVER:
                    input_btn = child
                elif observer_type == OUTPUT_OBSERVER:
                    output_btn = child
        return input_btn, output_btn

    def remove_column(self, column_index):
        ''' Removes column with a specified index from the table.
            If column index is zero or larger than the total number of columns
            of the table, no column is removed.

            Args:
                column_index (int): index of the column to remove.

            Returns:
                bool: True if column was removed, False otherwise.
        '''
        # we must record category name before removing column,
        # because it will disappear
        if column_index < len(self.cells[0]):
            category_name = self.cells[0][column_index].get().strip()
        else:
            category_name = ''
        if super().remove_column(column_index):
            col = column_index - 1
            if category_name:
                self.params_frame.input_categories_frame.remove_category(
                    category_name)
                self.params_frame.output_categories_frame.remove_category(
                    category_name)

            if col < len(self.current_categories):
                self.current_categories[col] = ''
                # remove from data only if category is present
                if self.data:
                    column_with_data_removed = False
                    for row_index in range(len(self.data)):
                        if column_index < len(self.data[row_index]):

                            self.data[row_index].pop(column_index)
                            column_with_data_removed = True
                    if column_with_data_removed:
                        for row in range(1, self.nb_rows):
                            for j in range(column_index, self.nb_cols):
                                self.cells[row][j].data_column -= 1
                        self.panel_text_observer.change_state_if_needed()

            self.frames[col].destroy()
            for i in range(col + 1, len(self.frames)):
                self.frames[i].grid_remove()
                self.frames[i].grid(column=i + 1)
            self.frames.pop(col)
            self.col_checkboxes.pop(col)
            return True
        return False

    def remove_row(self, row_index):
        ''' Removes data row with a specified index from the table.
            Row is not physically removed.
            If row_index is zero or larger than the total number of rows,
            no row is removed.

            Args:
                row_index (int): index of the row to remove.

            Returns:
                bool: True if row was deleted, False otherwise.
        '''
        if self.should_remove_row(row_index):
            
            if self.data:
                nb_pages = calculate_nb_pages(len(self.data), self.nb_rows)
                data_index = self.get_data_index(row_index)
                nb_cols = len(self.cells[row_index])
                
                if data_index != -1 and data_index < len(self.data):
                    nb_rows_to_change = min(self.nb_rows, len(self.data) + 1)
                    self.data.pop(data_index)
                    for row in range(row_index + 1, nb_rows_to_change):
                        for col in range(0, nb_cols):
                            if self.cells[row][col].data_row != -1:
                                self.cells[row][col].data_row -= 1

                    self.panel_text_observer.change_state_if_needed()

                super().remove_row(row_index)
                if (nb_pages > 1):
                    self.add_row()
            else:
                super().remove_row(row_index)

            self.row_checkboxes[row_index][0].destroy()
            for i in range(row_index + 1, len(self.row_checkboxes)):
                self.row_checkboxes[i][0].grid_remove()
                self.row_checkboxes[i][0].grid(row=i + 1)
            self.row_checkboxes.pop(row_index)

            return True
        return False

    def get_data_index(self, row_index):
        for j in range(0, len(self.cells[row_index])):
                if self.cells[row_index][j].data_row != -1:
                    return self.cells[row_index][j].data_row
        return -1
                        
    def before_cell_destroy(self, cell):
        ''' This method is called before a table cell is destroyed.
            Notifies observers if data is not empty.

            Args:
                cell (SelfValidatingEntry): cell that will be destroyed
                    after call to this method.
        '''
        info = cell.grid_info()
        col = int(info['column'])
        row = int(info['row'])
        if len(self.data) == 0:
            cell.notify_observers(CELL_DESTROY, row, col)

    def load_visible_data(self):
        ''' Displays data in the table. First, it adds more rows to fill
            the frame, second, it displays data that fits the table.
        '''
        self.add_rows_to_fill_visible_frame()
        self.display_data()

    def display_data(self, start_row=0):
        ''' Displays data starting from a given data row.
            This method is usually called by NavigationForTableFrame when
            data spans across
            several pages and users clicks on page navigation buttons.

            Args:
                start_row (int, optional): index of input data starting
                    from which data should be displayed, defaults to 0.
        '''
        nb_data_rows = len(self.data)
        nb_displayed_rows = 0
        for row_index in range(start_row, nb_data_rows):
            values = self.data[row_index]
            # do not insert data that is not visible
            if nb_displayed_rows + 1 >= self.nb_rows:
                return
            for column_index, coeff in enumerate(values):
                # row_index + 1 - first row has categories
                self._display_one_cell(nb_displayed_rows, column_index,
                                       coeff, row_index,
                                       column_index, False)
            row_index += 1
            nb_displayed_rows += 1

        if len(self.data) > 0:
            nb_cols = len(self.data[0])
        else:
            nb_cols = self.nb_cols

        nb_rows = self.nb_rows - 1  # -1 because we add +1 to row_index
        while nb_displayed_rows < nb_rows:
            for column_index in range(nb_cols):
                self._display_one_cell(nb_displayed_rows, column_index, '',
                                       -1, -1, False)
            nb_displayed_rows += 1

    def _display_one_cell(self, row_index, column_index, value_to_dispay,
                          data_row, data_col, modify_data=True):
        ''' Displays data in a cell and sets cell's fields to proper values.

            Args:
                row_index (int): index of a row where the cell is.
                column_index (int): index of a column where the cell is.
                value_to_dispay (str): new cell value_to_dispay.
                data_row (int): row index of input data.
                data_col (int): column index of input data.
                modify_data (bool, optional): True if data was modified and
                    observers
                    must be notified, False otherwise.
        '''
        cell_row_index = row_index + 1
        self.cells[cell_row_index][column_index].modify_data = modify_data
        self.cells[cell_row_index][column_index].text_value.set(value_to_dispay)
        self.cells[cell_row_index][column_index].data_row = data_row
        self.cells[cell_row_index][column_index].data_column = data_col

    def add_rows_to_fill_visible_frame(self):
        ''' Adds rows to table to fill the frame. Usually adds a bit more and
            scroll gets activated.
            Exact number of added rows depends on operating system, height of
            widgets and screen size.
        '''
        self.canvas.update_idletasks()
        frame_height = self.canvas.winfo_height()
        while self.canvas.bbox(ALL)[3] <= frame_height - 20:
            self.add_row()
        self._update_scroll_region()

    def check_value(self, count):
        ''' This method is called in read_coefficients method to check what
            values must be returned for data instance construction.

            Args:
                count (int): data column index.
            Returns:
                bool: True if the category in the given column index is not
                    an empty string,
                    False otherwise.
        '''
        if self.current_categories[count]:
            return True
        return False

    def clear_all_data(self):
        ''' Clears all data from all cells and clears input data.
        '''
        self.data.clear()
        super().clear_all_data()
        self.current_categories.clear()
        # reset modify data back to true
        for cell_row in self.cells:
            for cell in cell_row:
                cell.modify_data = True

    def before_cell_clear(self, cell):
        ''' This method is called before data is cleared from a given cell.
            It sets fields of the given cell to initial values.

            Args:
                cell (SelfValidatingEntry): cell that will be cleared after
                    call to this method.
        '''
        cell.modify_data = False
        cell.data_row = -1
        cell.data_column = -1 class NavigationForTableFrame(Frame):
    ''' This class is responsible for navigation over the table object.
        Table rows are displayed page by page.
        This class provides buttons that allow to navigate from page
        to page.

        Attributes:
            table (TableFrame or any object that has attribute nb_rows and
                implements display_data method): table that should be navigated.
            goto_spin (Spinbox): Spinbox widget that allows to enter page
                number and to switch between pages.
            text_var_nb_pages (StringVar): text variable that shows total
                number of pages.
            current_page_str (StringVar): text variable that stores
                currently displayed page.

        Args:
            parent (Tk object): parent for this frame.
            table (TableFrameWithInputOutputBox): table with multiple pages that
                will be navigated by this frame.
    '''
    def __init__(self, parent, table, *args, **kw):
        super().__init__(parent, *args, **kw)
        self.table = table
        self.goto_spin = None
        self.text_var_nb_pages = StringVar()
        self.current_page_str = StringVar()
        self.create_widgets()

    def create_widgets(self):
        ''' Creates all necessary widgets for navigation.
        '''
        prev_btn = Button(self, text='<<', width=5,
                          command=self.show_prev_page)
        prev_btn.grid(row=0, column=0, sticky=N, padx=3)
        next_btn = Button(self, text='>>', width=5,
                          command=self.show_next_page)
        next_btn.grid(row=0, column=1, sticky=N)
        goto_lbl = Label(self, text='Go to')
        goto_lbl.grid(row=0, column=3, sticky=N, padx=5)

        self.goto_spin = Spinbox(self, width=7, from_=1, to=1,
                                 textvariable=self.current_page_str,
                                 command=self.on_page_change)

        self.goto_spin.bind('<Return>', self.on_page_change)
        self.goto_spin.grid(row=0, column=4, sticky=N, padx=5)
        nb_pages_lb = Label(self, textvariable=self.text_var_nb_pages)
        nb_pages_lb.grid(row=0, column=5, sticky=W+N, padx=5)
        self.reset_navigation()

    def show_prev_page(self):
        ''' Displays previous page.

            This method is called when user presses button '<<'.
            If the first page is currently displayed and the method is called,
            then the call to this method is ignored.
        '''
        prev_page_index = int(self.current_page_str.get()) - 1
        if prev_page_index > 0:
            self.current_page_str.set(prev_page_index)
            self.on_page_change()

    def show_next_page(self):
        ''' Displays next page.

            This method is called when user presses button '>>'.
            If the last page is currently displayed and the method is called,
            then the call to this method is ignored.
        '''
        next_page_index = int(self.current_page_str.get()) + 1
        if next_page_index <= int(self.goto_spin.cget('to')):
            self.current_page_str.set(next_page_index)
            self.on_page_change()

    def on_page_change(self, *args):
        ''' Displays a page which number is currently stored in
            current_page_str.

            This method is called in show_next_page(), show_prev_page() and
            when the user enters data and presses Enter.
            args are supplied by Return event.
            If the entered value is invalid, then the first page is displayed.
            If the entered value is zero or negative, then the first page is
            isplayed.
            If the entered value is larger than the total number of pages,
            then the last page is displayed.
        '''
        try:
            curr_page = int(self.current_page_str.get())
        except ValueError:
            curr_page = 1
            self.current_page_str.set(curr_page)

        max_page = int(self.goto_spin.cget('to'))
        if curr_page > max_page:
            curr_page = max_page
            self.current_page_str.set(curr_page)
        if curr_page <= 0:
            curr_page = 1
            self.current_page_str.set(curr_page)
        # second -1 because row indeces start with 0
        row_index = calculate_start_row_index(curr_page, self.table.nb_rows)

        self.table.display_data(row_index)

    def reset_navigation(self):
        ''' Resets navigation parameters.

            Sets current page number to 1 and total number of pages to zero.
        '''
        self.current_page_str.set('1')
        self.text_var_nb_pages.set('1 pages')
        self.goto_spin.config(to=1)

    def set_navigation(self, nb_data_pages, reset_curr_page=True):
        ''' Sets navigation parameters.

            Sets current page number to 1 and total number of pages
            to nb_data_pages.

            Args:
                nb_data_pages (int): new value for the total number of pages.
                reset_curr_page (bool): True is current page must be reset to 1,
                    False otherwise.
        '''
        self.goto_spin.config(to=nb_data_pages)
        if reset_curr_page:
            self.current_page_str.set('1')
        self.text_var_nb_pages.set('{0} pages'.format(nb_data_pages)) class TableModifierFrame(Frame):
    ''' This class creates buttons for adding and removing
        rows and columns from data table.

        Attributes:
            table (TableFrameWithInputOutputBox): data table.
            row_str_var (StringVar): stores number of rows that should
                be added to the data table.
            col_str_var (StringVar): stores number of columns that
                should be added to the data table.
            set_navigation (callable): function that sets number of
                pages to a given number,
                for details see navigation_frame_gui module.

        Args:
            parent (Tk object): parent of this frame.
            table (TableFrameWithInputOutputBox): data table.
            clear_all_fnc (callable): function that should be called when
                "Clear all" button is pressed.
            set_navigation (callable): function that sets number of pages
                to a given number,
                for details see navigation_frame_gui module.
    '''
    def __init__(self, parent, table, clear_all_fnc, set_navigation, *args,
                 **kw):
        super().__init__(parent, *args, **kw)
        self.table = table
        self.row_str_var = StringVar()
        self.col_str_var = StringVar()
        self.set_navigation = set_navigation
        self.create_widgets(clear_all_fnc)

    def create_widgets(self, clear_all_fnc):
        ''' Creates widgets of this frame.

            Args:
                clear_all_fnc (function): function that should be called when
                    "Clear all" button is pressed.
        '''
        rows_spinbox = Spinbox(self, from_=1, to=100, width=3,
                               textvariable=self.row_str_var)
        rows_spinbox.grid(row=0, column=0, pady=3, padx=3)
        add_row_btn = Button(self, text='Add row(s)', command=self.add_rows)
        add_row_btn.grid(row=0, column=1, sticky=N+E+W, pady=3)
        remove_row_btn = Button(self, text='Remove row(s)',
                                command=self.remove_rows)
        remove_row_btn.grid(row=1, column=1, sticky=N+E+W, pady=3)
        columns_spinbox = Spinbox(self, from_=1, to=100, width=3,
                                  textvariable=self.col_str_var)
        columns_spinbox.grid(row=2, column=0, pady=3, padx=3)
        add_column_btn = Button(self, text='Add column(s)',
                                command=self.add_columns)
        add_column_btn.grid(row=2, column=1, sticky=N+E+W, pady=3)
        remove_column_btn = Button(self, text='Remove column(s)',
                                   command=self.remove_columns)
        remove_column_btn.grid(row=3, column=1, sticky=N+E+W, pady=3)
        clear_all_btn = Button(self, text='Clear all', command=clear_all_fnc)
        clear_all_btn.grid(row=4, column=1, sticky=N+E+W, pady=3)

    def add_rows(self):
        ''' Adds rows to the data table. Number of rows is specified in Spinbox.
            If invalid value is given in Spinbox, only one row is added.
            Rows are added to the end of the table.
        '''
        nb_rows = self.get_nb_to_add(self.row_str_var)
        for i in range(nb_rows):
            self.table.add_row()

        # if data is displayed on multiple pages, when adding new row
        # we re-display data
        if len(self.table.cells) > 1:
            start_row = self.table.cells[1][0].data_row
            if start_row != -1:
                if len(self.table.data) - start_row >= self.table.nb_rows - 1:
                    # we need to reposition data on several pages
                    self.table.display_data(start_row)
                    nb_pages = calculate_nb_pages(len(self.table.data),
                                                  self.table.nb_rows)
                    self.set_navigation(nb_pages, False)

    def get_nb_to_add(self, str_var):
        ''' Calculates valid number of rows or columns that should be added
            to the data table.

            Args:
                str_var (StringVar): StringVar object that stores number
                    of rows or columns that must be added to the data table.

            Returns:
                int: positive integer number if str_var stores such a
                    number, 1 if negative or invalid value is stored in str_var.

            Example:
                >>> table.row_str_var.set(10)
                >>> tabel.get_nb_to_add(table.row_str_var)
                10
                >>> table.row_str_var.set(-1)
                >>> tabel.get_nb_to_add(table.row_str_var)
                1
                >>> table.row_str_var.set("abc")
                >>> tabel.get_nb_to_add(table.row_str_var)
                1
        '''
        try:
            nb_values = int(str_var.get())
            if nb_values <= 0:
                nb_values = 1
        except ValueError:
            nb_values = 1
        str_var.set(nb_values)
        return nb_values

    def remove_rows(self):
        ''' Removes rows that are checked in checkboxes for removal.
            First row cannot be removed.
        '''
        if self.table.nb_rows > 0:
            row_index = 1
            prev_nb_rows = self.table.nb_rows
            while row_index < self.table.nb_rows:
                box, var = self.table.row_checkboxes[row_index]
                if var is not None and var.get() == 1:
                    if self.table.remove_row(row_index) is False:
                        row_index += 1
                else:
                    row_index += 1

            # when rows are removed number of pages to display data might change
            if len(self.table.cells) > 1:
                start_row = self.table.cells[1][0].data_row
                if start_row != -1:
                    nb_pages = calculate_nb_pages(len(self.table.data),
                                                  self.table.nb_rows)
                    self.set_navigation(nb_pages, False)
                    # + 2 for index and first row is for categories
                    page_number = calculate_nb_pages(start_row + 2,
                                                  self.table.nb_rows)
                    new_start_row = calculate_start_row_index(
                        page_number, self.table.nb_rows)
                    self.table.display_data(new_start_row)

    def add_columns(self):
        ''' Adds columns to the data table. Number of columns is specified
            in Spinbox.
            If invalid value is given in Spinbox, only one column is added.
            Columns are added to the end of the table.
        '''
        nb_cols = self.get_nb_to_add(self.col_str_var)
        for i in range(nb_cols):
            self.table.add_column()

    def remove_columns(self):
        ''' Removes columns that are checked in checkboxes for removal.
            First column cannot be removed.
        '''
        if self.table.nb_cols > 1:
            col_index = 0
            while col_index < self.table.nb_cols - 1:
                box, var = self.table.col_checkboxes[col_index]
                if var.get() == 1:
                    self.table.remove_column(col_index + 1)
                else:
                    col_index += 1 TEXT_FOR_PANEL = 'File: ' FILE_TYPES = [('Excel (xlsx)', '*.xlsx'),
              ('Text CSV', '*.csv')] def center_window(widget, width=None, height=None):
    ''' Centres widget in the middle of the screen.

        Args:
            widget (Tk object): widget that needs to be centred.
            width (int, optional): width of the widget that should be used.
                If not specified, widget width is used.
            height (int, optional): height of the widget that should be used.
                If not specified, widget width is used.
    '''
    widget.withdraw()
    widget.update_idletasks()
    if width is None:
        width = widget.winfo_reqwidth()

    if height is None:
        height = widget.winfo_reqheight()

    sw = widget.winfo_screenwidth()
    sh = widget.winfo_screenheight()

    x = (sw - width)/2
    y = (sh - height)/2
    widget.geometry('%dx%d+%d+%d' % (width, height, x, y))
    widget.deiconify() def calculate_nb_pages(nb_data_rows, nb_table_rows):
    ''' Calculates number of pages given number of data rows and number of rows
        in the table.

        Note:
            first row is reserved for categories.

        Args:
            nb_data_rows (int): number of data rows.

                Warning:
                    It must be a positive number. Otherwise the function
                    will return incorrect value.

            nb_table_rows (int): number of rows in the table.

                Warning:
                    It must be a positive number. Otherwise the function
                    will return incorrect value.

        Returns:
            int: number of pages

        Example:
            >>> calculate_nb_pages(100, 10)
            >>> 12
            >>> calculate_nb_pages(25, 20)
            >>> 2
            >>> calculate_nb_pages(0, 20)
            >>> 1
            >>> calculate_nb_pages(10, 25):
            >>> 1

    '''
    if nb_table_rows:
        base = int(nb_data_rows / (nb_table_rows - 1))
        if nb_data_rows % (nb_table_rows - 1) != 0:
            base += 1
        return max(base, 1)
    return 0 def read_data(file_name, sheet_name=''):
    ''' Reads data from a given file.

        Args:
            file_name (str): path to file with input data.
            sheet_name (str, optional): name of the excel sheet
                where data is stored. Defaults to empty string.
                If it is not given, data is read from the first sheet.

        Returns:
            tuple of list of str, list of str, str, str:
                tuple with the following components:

                * list of categories (first non-empty row is considered
                  to be a list of categories);
                * list where the first element is DMU name, all other
                  elements are coefficients;
                * name of all DMUs if specified;
                * name of sheet where data was read from.

        Note:
            DMUs and categories can be strings or numbers, so returned types
            depend on these types.
    '''
    just_name, extension = os.path.splitext(file_name)
    if extension == '.xlsx':
        reader = XLSXReader()
    elif extension == '.csv':
        reader = CSVReader()
    else:
        raise ValueError('{0} format is not supported'.format(extension))
    reader.open_file(file_name, sheet_name)
    categories = []
    coefficients = []
    first_non_empty_row = True
    first_row_index = -1
    col_to_return = -1
    col_indexes = []
    for row_index, row in enumerate(reader.get_rows()):
        if has_non_empty_cells(reader, row):
            if first_non_empty_row:
                categories, col_indexes = extract_categories(reader, row)
                first_non_empty_row = False
                first_row_index = row_index
            else:
                assert(col_indexes)
                dmu, coeff_values, col = extract_coefficients(reader, row,
                                                              col_indexes)
                if col_to_return == -1:
                    col_to_return = col
                tmp = []
                tmp.append(dmu)
                tmp.extend(coeff_values)
                coefficients.append(tmp)
    dmu_name = reader.get_cell_value(first_row_index, col_to_return)
    if dmu_name:
        categories.pop(0)
    reader.close_file()
    return categories, coefficients, dmu_name, reader.get_sheet_name() class AskSheetName(Frame):
    ''' This class is responsible for asking the user to
        specify the sheet name of the xlsx-file where input data is.

        Attributes:
            parent (Toplevel): new window where this Frame will be situated.
            names (list of str): list of available sheet names.
            sheet_name_str (StringVar): StringVar object used for reading
                what sheet was chosen by the user.

        Args:
            parent (Toplevel): new window where this Frame will be situated.
            names (list of str): list of available sheet names.
    '''
    def __init__(self, parent, names, *args, **kw):
        super().__init__(parent, *args, **kw)
        self.parent = parent
        self.sheet_name_str = StringVar()
        self.create_widgets(names)
        self.update_idletasks()

    def create_widgets(self, names):
        ''' Creates appropriate widgets.

            Args:
                names (list of str): list of available sheet names.
        '''
        sheet_name_lbl = Label(self,
                               text='Choose sheet name where data is stored:')
        sheet_name_lbl.grid(sticky=N+W, padx=5, pady=5)
        sheet_names_box = Combobox(self, state="readonly", width=20,
                                   textvariable=self.sheet_name_str,
                                   values=names)
        sheet_names_box.current(0)
        sheet_names_box.grid(row=1, column=0, columnspan=2,
                             sticky=N+W, padx=5, pady=5)
        ok_btn = Button(self, text='OK', command=self.ok)
        ok_btn.grid(row=2, column=0, sticky=N+E, padx=5, pady=5)
        ok_btn.bind('<Return>', self.ok)
        ok_btn.focus()
        cancel_btn = Button(self, text='Cancel', command=self.cancel)
        cancel_btn.grid(row=2, column=1, sticky=N+E, padx=5, pady=5)
        cancel_btn.bind('<Return>', self.cancel)

    def cancel(self, event=None):
        ''' This method is called if the user presses Cancel button.
            In this case sheet name is set to empty string.

            Args:
                event (Tk event, optional): event that is given when the
                    button is pressed.  It is ignored in this method.
        '''
        self.sheet_name_str.set('')
        self.parent.destroy()

    def ok(self, event=None):
        ''' This method is called if the user presses OK button.
            In this case sheet name is set to the currently selected value
            in Combobox.

            Args:
                event (Tk event, optional): event that is given when the
                    button is pressed.  It is ignored in this method.
        '''
        self.parent.destroy() def save_data_to_xlsx(data_file, categories, data, sheet_name='Data'):
    ''' Writes input data to xlsx-file.

        Args:
            data_file (str): name of new or existing file where data will
                be written.
            categories (list of str): the first line that will be written
                to file, it must contain category names and might also
                contain name of DMUs as the first element.
            data (list of list of int, float or str): list with data,
                for example

                >> data = [['A', 1, 2, 3], ['B', 2, "a", 5], ['C', 0]]
            sheet_name (str, optional): name of the sheet where the data
                will be written, if the existing sheet name is specified,
                data will be overwritten on this sheet, defaults to "Data".

        Raises:
            ValueError: if sheet_name is empty string or None, or any
                empty value for xlsx files.
    '''
    if data_file.endswith('.xlsx') and not sheet_name:
        raise ValueError('Sheet name is not specified')

    if os.path.isfile(data_file):
        work_book = create_workbook(data_file)
        if data_file.endswith('.xlsx'):
            # copy data from all sheets except the one the user modified if any
            # copy old file and clear sheet to be overwritten with new data
            work_book.workbook = load_workbook(data_file, data_only = True)
            #due to data_only the formulas are replaced by numbers, but otherwise 
            #the file would get useless for pyDEA until opening it again in Excel and recalculating the equations
            work_book.nb_sheets = len(work_book.workbook.sheetnames)
            if sheet_name in work_book.workbook.sheetnames:
                ws = work_book.workbook[sheet_name]
                ws.delete_rows(1, amount = ws.max_row)
                work_sheet = XlsxSheet(ws)
            else:
                work_sheet = work_book.add_sheet(sheet_name)
        else:
            work_sheet = work_book.add_sheet(sheet_name)
    else:
        work_book = create_workbook(data_file)
        work_sheet = work_book.add_sheet(sheet_name)

    # save categories
    for count, category in enumerate(categories):
        work_sheet.write(0, count, category.strip())

    # save data
    row_index = 1
    for values in data:
        for col, coeff in enumerate(values):
            try:
                val = float(coeff)
            except ValueError:
                val = coeff
            work_sheet.write(row_index, col, val)
        row_index += 1
    work_book.save(data_file)
from openpyxl import load_workbook
from tkinter import StringVar, E, N, W, S, Toplevel
from tkinter.ttk import Frame, Button, LabelFrame
from tkinter.filedialog import askopenfilename, asksaveasfilename
from tkinter.messagebox import askyesno
from pyDEA.core.gui_modules.table_gui import TableFrameWithInputOutputBox
from pyDEA.core.gui_modules.navigation_frame_gui import NavigationForTableFrame
from pyDEA.core.gui_modules.table_modifier_gui import TableModifierFrame
from pyDEA.core.utils.dea_utils import TEXT_FOR_PANEL, FILE_TYPES, center_window
from pyDEA.core.utils.dea_utils import calculate_nb_pages
from pyDEA.core.data_processing.read_data import read_data
from pyDEA.core.gui_modules.load_xlsx_gui import AskSheetName
from pyDEA.core.data_processing.save_data_to_file import save_data_to_xlsx
import os
        self.if_text_modified_str.trace('w', self.on_data_modify)
        self.sheet_name = ''
        self.navigation_frame = None
        self.table_modifier_frame = None
        self.create_widgets(current_categories, str_var_for_input_output_boxes)

    def create_widgets(self, current_categories,
                       str_var_for_input_output_boxes):
        ''' Creates widgets on this frame.

            Args:
                current_categories (list of str): list of current categories
                    (might be empty).
                str_var_for_input_output_boxes (ObserverStringVar): object
                    that contains current
                    input and output categories and notifies table with data
                    when data is loaded
                    and some categories need to be checked.
        '''
        self.rowconfigure(1, weight=1)
        self.columnconfigure(0, weight=1)

        self._create_btns()
        self._create_label_frame()

        table = TableFrameWithInputOutputBox(self.panel, self.params_frame,
                                             self.params_frame.options_frame.
                                             combobox_text_var,
                                             current_categories,
                                             str_var_for_input_output_boxes,
                                             self.if_text_modified_str,
                                             self.data)
        self.table = table
        table.grid(sticky=E+W+S+N, padx=5, pady=5)

        frame_for_navigation_btns = NavigationForTableFrame(self, table)
        table_modifier_frame = TableModifierFrame(self, self.table,
                                                  self.clear_all_on_btn_click,
                                                  frame_for_navigation_btns.
                                                  set_navigation)
        table_modifier_frame.grid(row=1, column=1, sticky=N, pady=15, padx=5)
        self.table_modifier_frame = table_modifier_frame

        frame_for_navigation_btns.grid(row=2, column=0, sticky=E, pady=5,
                                       padx=5)
        self.navigation_frame = frame_for_navigation_btns

    def _create_btns(self):
        ''' Creates load and save buttons.
        '''
        load_button = Button(self, text='Load', command=self.load_file)
        load_button.grid(row=0, column=0, pady=5, padx=5, sticky=W+N)

        frame_for_save_btns = Frame(self)
        save_button = Button(frame_for_save_btns, text='Save',
                             command=self.save_data)
        save_button.grid(row=0, column=0, pady=2, padx=5, sticky=E+N)
        save_as_button = Button(frame_for_save_btns, text='Save as...',
                                command=self.save_data_as)
        save_as_button.grid(row=1, column=0, pady=2, padx=5, sticky=E+N)
        frame_for_save_btns.grid(row=0, column=1, pady=5, padx=5, sticky=E+N)

    def _create_label_frame(self):
        ''' Creates label frame.
        '''
        self.panel = panel = LabelFrame(self, text=TEXT_FOR_PANEL)
        panel.columnconfigure(0, weight=1)
        panel.rowconfigure(0, weight=1)
        panel.grid(row=1, column=0,
                   padx=5, pady=5, sticky=E+W+S+N)

    def get_data_file_name(self):
        ''' Returns name of a file with data if it was loaded.

            File name is stored in label frame.
        '''
        data_file = self.panel.cget('text')
        nb_letters = len(TEXT_FOR_PANEL)
        return data_file[nb_letters:]

    def remove_star_from_panel(self):
        ''' Removes star from data file name if it was there.
        '''
        data_file = self.get_data_file_name()
        data_file = remove_star(data_file)
        self.panel.config(text=TEXT_FOR_PANEL + data_file)

    def save_data(self):
        ''' Saves data to an existing or new file.

            This method is called when user presses "Save" button.
            If user modified open data file, then modifications will
            be saved in this file.
            If user typed data, this method redirects user to
            save_data_as() method.
        '''
        data_file = self.get_data_file_name()
        if data_file == '*':  # when file was created and modified
            self.save_data_as()
        elif data_file:
            self.save_data_to_given_file(remove_star(data_file),
                                         sheet_name=self.sheet_name)
            self.remove_star_from_panel()

    def save_data_as(self):
        ''' Saves data to a new file.

            Calls dialogue to ask user where to save file and saves
            data file there. Default extension is .xlsx.
        '''
        file_name = self._call_file_save_as_dialog()
        if file_name:
            self.save_data_to_given_file(file_name)
            self.panel.config(text=TEXT_FOR_PANEL + file_name)

    def _call_file_save_as_dialog(self):
        ''' Calls save as dialogue.

            This method is redefined in unit tests.
        '''

        return asksaveasfilename(filetypes=FILE_TYPES, defaultextension='.xlsx')