Source code for pyDEA.core.gui_modules.table_gui
''' This module contains classes responsible for displaying input data
in a table (TableFrame and TableFrameWithInputOutputBox).
It also contains many classes necessary for TableFrameWithInputOutputBox.
Attributes:
CELL_WIDTH (int): constant that defined width of a cell in a table
'''
from tkinter import S, N, E, W, END, VERTICAL, HORIZONTAL, ALL
from tkinter import IntVar, DISABLED, StringVar, NORMAL
from tkinter.ttk import Frame, Entry, Scrollbar, Checkbutton
from pyDEA.core.gui_modules.scrollable_frame_gui import MouseWheel
from pyDEA.core.utils.dea_utils import is_valid_coeff, NOT_VALID_COEFF, VALID_COEFF
from pyDEA.core.utils.dea_utils import WARNING_COEFF, EMPTY_COEFF, CELL_DESTROY
from pyDEA.core.utils.dea_utils import CHANGE_CATEGORY_NAME, INPUT_OBSERVER
from pyDEA.core.utils.dea_utils import OUTPUT_OBSERVER, on_canvas_resize
from pyDEA.core.utils.dea_utils import validate_category_name, calculate_nb_pages
from pyDEA.core.gui_modules.custom_canvas_gui import StyledCanvas
from pyDEA.core.data_processing.read_data_from_xls import convert_to_dictionary
CELL_WIDTH = 10
[docs]class TableFrame(Frame):
''' This class is a base class that defines minimal functionality of
a table.
Attributes:
parent (Tk object): parent of this widget.
nb_rows (int): number of rows of the table.
nb_cols (int): number of columns of the table.
cells (list of list of Entry): list with Entry widgets
(or derivatives of Entry)
that describes the table and its content.
canvas (Canvas): canvas that holds all widgets
(it is necessary to make the table scrollable).
frame_with_table (Frame): frame that holds all widgets.
Args:
parent (Tk object): parent of this widget.
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, data, nb_rows=20, nb_cols=5):
Frame.__init__(self, parent)
self.data = data
self.parent = parent
self.nb_rows = nb_rows
self.nb_cols = nb_cols
self.cells = []
self.canvas = None
self.frame_with_table = None
self.create_widgets()
[docs] def create_widgets(self):
''' Creates all widgets.
'''
self.rowconfigure(0, weight=1)
self.columnconfigure(0, weight=1)
yScrollbar = Scrollbar(self, orient=VERTICAL)
yScrollbar.grid(row=0, column=1, sticky=N+S)
xScrollbar = Scrollbar(self, orient=HORIZONTAL)
xScrollbar.grid(row=1, column=0, sticky=E+W)
canvas = StyledCanvas(self, yscrollcommand=yScrollbar.set,
xscrollcommand=xScrollbar.set, bd=0)
self.canvas = canvas
canvas.grid(row=0, column=0, sticky=N+S+W+E)
frame_with_table = Frame(canvas)
self.frame_with_table = frame_with_table
frame_with_table.grid(sticky=N+S+W+E, pady=15, padx=3)
for i in range(2, self.nb_rows + 2):
cols = []
for j in range(1, self.nb_cols + 1):
ent = self.create_entry_widget(frame_with_table)
ent.grid(row=i, column=j, sticky=N+S+E+W)
cols.append(ent)
self.cells.append(cols)
canvas.create_window(0, 0, window=frame_with_table, anchor='nw')
canvas.update_idletasks()
yScrollbar['command'] = canvas.yview
xScrollbar['command'] = canvas.xview
self._update_scroll_region()
MouseWheel(self).add_scrolling(canvas, yscrollbar=yScrollbar)
[docs] def create_entry_widget(self, parent):
''' Creates Entry widget.
Args:
parent (Tk object): parent of the Entry widget.
Returns:
Entry: created Entry widget.
'''
return Entry(parent, width=CELL_WIDTH)
[docs] def add_row(self):
''' Adds one row to the end of the table.
'''
self.cells.append([])
for j in range(self.nb_cols):
grid_row_index = self.nb_rows + 2
ent = self.create_entry_widget(self.frame_with_table)
ent.grid(row=grid_row_index, column=j + 1, sticky=N+S+E+W)
self.cells[self.nb_rows].append(ent)
self.nb_rows += 1
self._update_scroll_region()
[docs] def add_column(self):
''' Adds one column to the end of the table.
'''
for i in range(self.nb_rows):
grid_row_index = i + 2
ent = self.create_entry_widget(self.frame_with_table)
ent.grid(row=grid_row_index, column=self.nb_cols + 1,
sticky=N+S+E+W)
self.cells[i].append(ent)
self.nb_cols += 1
self._update_scroll_region()
[docs] def remove_row(self, row_index):
''' Removes row with a specified index from the table.
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.
'''
# forbid deleting first row
if self.should_remove_row(row_index):
for j in range(self.nb_cols):
self.before_cell_destroy(self.cells[row_index][j])
self.cells[row_index][j].destroy()
for i in range(row_index + 1, self.nb_rows):
self.cells[i][j].grid_remove()
self.cells[i][j].grid(row=i + 1)
self.cells.remove(self.cells[row_index])
self.nb_rows -= 1
self._update_scroll_region()
return True
return False
[docs] def should_remove_row(self, row_index):
''' Checks if row with a specified row index can be removed.
Args:
row_index (int): index of the row to remove.
Returns:
bool: True if row_index is >= 1 and < total number of rows,
False otherwise.
'''
return row_index >= 1 and row_index < self.nb_rows
[docs] 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.
'''
# do not allow to delete first column
if column_index > 0 and column_index < self.nb_cols:
for i in range(self.nb_rows):
self.cells[i][column_index].destroy()
for j in range(column_index + 1, self.nb_cols):
self.cells[i][j].grid_remove()
self.cells[i][j].grid(column=j)
self.cells[i].remove(self.cells[i][column_index])
self.nb_cols -= 1
self._update_scroll_region()
return True
return False
[docs] def before_cell_destroy(self, cell):
''' This method is called before a table cell is destroyed.
In this class this method does nothing, but can be redefined
in children classes.
Args:
cell (Entry): cell that will be destroyed after call to
this method.
'''
pass
[docs] def clear_all_data(self):
''' Clears all data from all cells.
'''
for i in range(self.nb_rows):
for j in range(self.nb_cols):
self.before_cell_clear(self.cells[i][j])
self.cells[i][j].delete(0, END)
[docs] def before_cell_clear(self, cell):
''' This method is called before data is cleared from a given cell.
In this class this method does nothing, but can be redefined
in children classes.
Args:
cell (Entry): cell that will be cleared after call
to this method.
'''
pass
def _update_scroll_region(self):
''' Updates scroll region. This method must be called each
time table size or number of columns or rows change.
'''
# ensures that bbox will calculate border correctly
self.frame_with_table.update()
on_canvas_resize(self.canvas)
[docs] def read_coefficients(self):
''' Converts data stored as a list to a proper dictionary
necessary for constructing data instance.
'''
return convert_to_dictionary(self.data, self.check_value)
[docs] def check_value(self, count):
''' This method is called in read_coefficients method to check what
values must be returned for data instance construction.
In this class it always returns True and can be redefined in
children classes.
'''
return True
[docs]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)
[docs] 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
[docs] 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)
[docs] 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
[docs] 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()
[docs] 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))
[docs] def add_column(self):
''' Adds one column to the end of table.
'''
super().add_column()
self._create_input_output_box(self.nb_cols - 2)
[docs] 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)
[docs] 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
[docs] 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
[docs] 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
[docs] 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
[docs] 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)
[docs] 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()
[docs] 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
[docs] 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()
[docs] 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
[docs] 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
[docs] 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
[docs]class ObserverCheckbutton(Checkbutton):
''' This class implements Checkbutton for choosing input/output categories.
Attributes:
var (IntVar): variable that is set to 1 when Checkbutton is
selected, to 0 otherwise.
opposite_var (IntVar): variable of the other Checkbutton that
must deselected if this Checkbutton is selected.
parent (Tk object): frame that holds this Checkbutton.
Warning:
it is important for the parent to be gridded in the
same column
as the entire column of table entries is gridded, because
this class uses parent grid column index to determine
the column where the category name can be read from.
category_frame (CategoriesCheckBox): frame that displays selected
input or output categories.
Note:
if this Checkbutton is used to select input categories,
category_frame must be CategoriesCheckBox object that
displays selected input categories.
if this Checkbutton is used to select output categories,
category_frame must be CategoriesCheckBox object that
displays selected output categories.
opposite_category_frame (CategoriesCheckBox): frame that displays
selected input or output categories. If category_frame
displays input categories, then opposite_category_frame
must display output categories, and vice versa.
current_categories (list of str): list of categories. This class
might modify this list by removing invalid categories and
adding the valid ones.
cells (list of list of SelfValidatingEntry): all entry widgets
collected in list.
data (list of list of str or float): input data.
observer_type (int): describes type of the observer, for possible
values see dea_utils.
change_category_name (callable function): this function is
called when name of a category was changed.
combobox_text_var (StringVar): variable of the combobox used for
selecting categorical category.
Arguments are the same as attributes.
'''
def __init__(self, parent, var, opposite_var, category_frame,
opposite_category_frame,
current_categories, cells,
observer_type, change_category_name, data,
combobox_text_var, *args, **kw):
Checkbutton.__init__(self, parent, variable=var,
command=self._process, *args, **kw)
self.var = var
self.opposite_var = opposite_var
self.parent = parent
self.category_frame = category_frame
self.opposite_category_frame = opposite_category_frame
self.current_categories = current_categories
self.cells = cells
self.data = data
self.observer_type = observer_type
self.change_category_name = change_category_name
self.combobox_text_var = combobox_text_var
def _process(self):
''' This method is called when user clicks on Checkbutton.
Makes sure that the same category can be only input or only
output, but not both, and that selected category cannot also
be selected as a categorical category.
'''
category_name = self.get_category()
if self.var.get() == 1:
self.opposite_var.set(0)
if category_name:
self.category_frame.add_category(category_name)
self.opposite_category_frame.remove_category(category_name)
if category_name == self.combobox_text_var.get():
self.combobox_text_var.set('')
elif category_name:
self.category_frame.remove_category(category_name)
[docs] def deselect(self):
''' Deselects Checkbutton.
Note:
method _process() is not called in this case.
'''
self.var.set(0)
[docs] def select(self):
''' Selects Checkbutton.
Note:
method _process() is not called in this case.
'''
self.var.set(1)
[docs] def change_state_if_needed(self, entry, entry_state, row, col):
''' Changes state of Checkbutton when data or categories were modified.
Also modifies current_categories if needed.
This widget becomes disabled if invalid category name value or input
data value were provided by user.
Args:
entry (SelfValidatingEntry): Entry widget whose content was
modified.
entry_state (int): state of the Entry widget after content
modification, for possible values see dea_utils module.
row (int): row index of entry widget. It is the real grid value,
we need to subtract 2 to get internal index.
col (int): column index of entry widget. It is the real grid
value, we need to subtract 2 to get internal index.
'''
if entry_state == CHANGE_CATEGORY_NAME:
old_name = ''
internal_col = col - 2
if internal_col < len(self.current_categories):
old_name = self.current_categories[internal_col]
category_name = validate_category_name(
self.cells[0][col - 1].text_value.get().strip(),
internal_col, self.current_categories)
if category_name:
index = len(self.current_categories)
while index <= internal_col:
self.current_categories.append('')
index += 1
self.current_categories[internal_col] = category_name
if old_name:
# change category name in params_frame
self.change_category_name(old_name.strip(), category_name)
self.change_state_based_on_data(entry, entry_state, row, col)
entry.config(foreground='black')
else:
# if category name is empty, disable
self.disable(internal_col, old_name)
entry.config(foreground='red')
else:
self.change_state_based_on_data(entry, entry_state, row, col)
[docs] def change_state_based_on_data(self, entry, entry_state, row, col):
''' Changes state of Checkbutton when data was modified.
Args:
entry (SelfValidatingEntry): Entry widget whose content
was modified.
entry_state (int): state of the Entry widget after content
modification, for possible values see dea_utils module.
row (int): row index of entry widget. It is the real grid
value, we need to subtract 2 to get internal index.
col (int): column index of entry widget. It is the real grid
value, we need to subtract 2 to get internal index.
'''
internal_col = col - 2
# IMPORTANT: read from cells, not from current_categories, they might
# be empty at this stage
category_name = self.cells[0][col - 1].text_value.get().strip()
nb_rows = len(self.data)
if nb_rows == 0:
self.disable(internal_col, category_name)
return
elif len(self.data[0]) == 0:
self.disable(internal_col, category_name)
return
has_one_valid_entry = False
for row_index in range(nb_rows):
# can happen if some values are empty
while col - 1 >= len(self.data[row_index]):
self.data[row_index].append('')
try:
# col - 1 - first column contains DMU names
data_elem = float(self.data[row_index][col - 1])
except ValueError:
state = NOT_VALID_COEFF
else:
state = is_valid_coeff(data_elem)
if state == NOT_VALID_COEFF:
has_one_valid_entry = False
self.disable(internal_col, category_name)
return
elif state == VALID_COEFF or state == WARNING_COEFF:
has_one_valid_entry = True
if has_one_valid_entry:
self.config(state=NORMAL)
if category_name:
if category_name not in self.current_categories:
assert internal_col < len(self.current_categories)
self.current_categories[internal_col] = category_name
if entry_state != CELL_DESTROY and self.var.get() == 1:
self.category_frame.add_category(category_name)
return
[docs] def disable(self, internal_col, category_name):
''' Disables Checkbutton.
Args:
internal_col (int): internal column index.
category_name (str): name of category.
'''
self.config(state=DISABLED)
if category_name:
if self.var.get() == 1:
self.category_frame.remove_category(category_name)
if self.opposite_var.get() == 1:
self.opposite_category_frame.remove_category(category_name)
if category_name in self.current_categories:
assert(internal_col < len(self.current_categories))
self.current_categories[internal_col] = ''
if category_name == self.combobox_text_var.get():
self.combobox_text_var.set('')
[docs] def get_category(self):
''' Finds category name stored in the corresponding Entry widget
based on where parent of Checkbutton was gridded.
Returns:
str: category name, might be empty string.
'''
info = self.parent.grid_info()
# convertion to int is necessary for Windows
# for some reason in Windows grid info is stored as str
col = int(info['column'])
return self.cells[0][col - 1].text_value.get().strip()
[docs]class FollowingObserverCheckbutton(ObserverCheckbutton):
''' This class follows state of another ObserverCheckbutton that is
used to select input or output categories.
This class is used in order to skip checking if data is valid
second time. The first Checkbutton has already performed this check.
Attributes:
var (IntVar): variable that is set to 1 when Checkbutton
is selected, to 0 otherwise.
opposite_var (IntVar): variable of the other Checkbutton that
must deselected if this Checkbutton is selected.
parent (Tk object): frame that holds this Checkbutton.
Warning:
it is important for the parent to be gridded in the
same column as the entire column of table entries
is gridded, because this class uses parent grid column
index to determine the column
where the category name can be read from.
category_frame (CategoriesCheckBox): frame that displays
selected input or output categories.
Note:
if this Checkbutton is used to select input categories,
category_frame must be CategoriesCheckBox object that
displays selected input categories.
if this Checkbutton is used to select output categories,
category_frame
must be CategoriesCheckBox object that displays selected
output categories.
opposite_category_frame (CategoriesCheckBox): frame that displays
selected input or output categories. If category_frame displays
input categories, then opposite_category_frame
must display output categories, and vice versa.
current_categories (list of str): list of categories. This class
might modify this list by removing invalid categories and
adding the valid ones.
cells (list of list of SelfValidatingEntry): all entry widgets
collected in list.
data (list of list of str or float): input data.
observer_type (int): describes type of the observer, for
possible values see dea_utils.
change_category_name (callable function): this function is called
when name of a category was changed.
combobox_text_var (StringVar): variable of the combobox used for
selecting categorical category.
main_box (ObserverCheckbutton): Checkbutton that changes state
first. This Checkbutton changes its state to the same state
as main_box, but does not do extra things
that have been already performed by main_box
(changes to current_categories, for example).
'''
def __init__(self, parent, var, opposite_var, category_frame,
opposite_category_frame,
current_categories, cells,
observer_type, params_frame, data,
combobox_text_var, main_box, *args, **kw):
super().__init__(parent, var, opposite_var, category_frame,
opposite_category_frame, current_categories, cells,
observer_type, params_frame, data,
combobox_text_var, *args, **kw)
self.main_box = main_box
[docs] def change_state_if_needed(self, entry, entry_state, row, col):
''' Changes state of Checkbutton when data was modified depending on
the state of main_box.
Args:
entry (SelfValidatingEntry): Entry widget whose content
was modified.
entry_state (int): state of the Entry widget after content
modification, for possible values see dea_utils module.
row (int): row index of entry widget. It is the real grid
value, we need to subtract 2 to get internal index.
col (int): column index of entry widget. It is the real grid
value, we need to subtract 2 to get internal index.
'''
category_name = self.get_category()
if str(self.main_box.cget('state')) == DISABLED:
self.disable(col - 2, category_name)
else:
self.config(state=NORMAL)
if entry_state != CELL_DESTROY and self.var.get() == 1:
self.category_frame.add_category(category_name)
[docs]class DefaultCategoriesAndDMUModifier(object):
''' This class is responsible for adding automatic category and DMU names
if user starts typing data without providing such names first.
Attributes:
cells (list of list of SelfValidatingEntry): list of all Entry
widgets with data.
current_categories (list of str): list of categories.
Args:
cells (list of list of SelfValidatingEntry): list of all Entry
widgets with data.
current_categories (list of str): list of categories.
'''
def __init__(self, cells, current_categories):
self.cells = cells
self.current_categories = current_categories
[docs] def change_state_if_needed(self, entry, entry_state, row, col):
''' Writes automatic category and DMU names if they were not
specified before.
Args:
entry (SelfValidatingEntry): Entry widget the content
of which was modified.
entry_state (int): constant that describes entry state,
for details see dea_utils module.
row (int): row index of entry widget. It is the real grid value,
we need to subtract 2 to get internal index.
col (int): column index of entry widget. It is the real grid
value, we need to subtract 2 to get internal index.
'''
if (entry_state != EMPTY_COEFF and entry_state != CELL_DESTROY and
entry_state != CHANGE_CATEGORY_NAME):
internal_row_index = row - 2
dmu_name = self.cells[internal_row_index][0].text_value.get().strip()
if not dmu_name:
self.cells[internal_row_index][0].text_value.set(
'DMU{0}'.format(internal_row_index))
category_name = self.cells[0][col - 1].text_value.get().strip()
if not category_name:
internal_col_index = col - 2
name = 'Category{0}'.format(internal_col_index)
if internal_col_index >= len(self.current_categories):
index = len(self.current_categories) - 1
while index != internal_col_index:
self.current_categories.append('')
index += 1
# category name MUST be written first, because next line calls
# ObserverCheckbutton
self.cells[0][col - 1].text_value.set(name)
[docs]class SelfValidatingEntry(Entry):
''' This class implement Entry widget that knows how to highlight
invalid data. It also notifies other widgets if the content of
Entry changes. Other widgets must implement method
change_state_if_needed().
Such widgets should be appended to the list of listening widgets
called observers.
Attributes:
text_value (StringVar): textvariable of Entry widget that
calls method on_text_changed when the content on Entry changes.
observers (list of objects that implement method change_state_if_needed):
list of widgets or other objects that must be notified if the
content of Entry changes.
data_row (int): row index in data table which should be modified
when the content of Entry changes.
data_column (int): column index in data table which should be
modified when the content of Entry changes.
data (list of list of srt or float): data that will be modified.
modify_data (bool): True if data should be modified, False
otherwise. It is usually set to False when data is uploaded
from file.
panel_text_observer (PanelTextObserver): object that is notified
when data changes.
This object is responsible for adding star to file name when
data was modified.
all_cells (list of list of SelfValidatingEntry): refernce where all cells
are stored.
Warning: all cells must be created before any cell content
can be modified.
Args:
parent (Tk object): parent of this Entry widget.
data (list of list of srt or float): input data that will
be modified.
all_cells (list of list of SelfValidatingEntry): refernce where all cells
are stored.
Warning: all cells must be created before any cell content
can be modified.
'''
def __init__(self, parent, data, all_cells, *args, **kw):
self.text_value = StringVar(master=parent)
self.text_value.trace("w", self.on_text_changed)
super().__init__(parent, *args, **kw)
self.config(textvariable=self.text_value)
self.observers = []
self.all_cells = all_cells
self.data_row = -1
self.data_column = -1
self.data = data
self.modify_data = True
self.panel_text_observer = None
[docs] def on_text_changed(self, *args):
''' This method is called each time the content of Entry is modified.
It highlights invalid data, changes data if needed and notifies
other objects when data was changed.
Args are provided by StringVar trace method, but are not used.
'''
info = self.grid_info()
# phisical grid indeces
col = int(info['column'])
row = int(info['row'])
self.notify_panel_observer()
if row == 2: # possibly name of category is modified
self.notify_observers(CHANGE_CATEGORY_NAME, row, col)
elif col == 1 and row > 2: # column with DMU names, strings are allowed
self.modify_data_if_needed(row, col)
elif col > 1 and row > 2: # everything left
self.modify_data_if_needed(row, col)
try:
value = float(self.text_value.get().strip())
except ValueError:
self.modify_data = True
self.config(foreground='red')
if len(self.text_value.get().strip()) == 0:
self.notify_observers(EMPTY_COEFF, row, col)
else:
self.notify_observers(NOT_VALID_COEFF, row, col)
return
text_status = is_valid_coeff(value)
if text_status == NOT_VALID_COEFF:
self.config(foreground='red')
elif text_status == WARNING_COEFF:
self.config(foreground='orange')
else:
self.config(foreground='black')
self.notify_observers(text_status, row, col)
self.modify_data = True
[docs] def modify_data_if_needed(self, row, col):
''' Modifies data if modify_data is set to True.
Adds empty strings to data when user modifies Entry for which
data_row or/and data_column are equal to -1. Updates data with new
values entered by user.
Args:
row (int): row where Entry is gridded
col (int): column where Entry is gridded
'''
if self.modify_data:
if self.data_row != -1 and self.data_column != -1:
self.data[self.data_row][self.data_column] = self.text_value.get().strip()
else:
row_for_data = len(self.data)
added_rows = False
# -2 because row is physical grid index, not cell index
row_count = len(self.all_cells) - 1
for cells_row in reversed(self.all_cells):
if cells_row[0].data_row != -1:
break
row_count -= 1
if row_count == -1:
row_count = 0
while row_count < row - 2:
self.data.append([])
added_rows = True
row_count += 1
if added_rows:
self.data_row = len(self.data) - 1
else:
assert row_count >= row - 2
self.data_row = len(self.data) - 1 - (row_count - (row - 2))
col_for_data = len(self.data[self.data_row])
added_cols = False
max_nb_col = 0
nb_rows = len(self.data)
for r_ind in range(nb_rows):
row_len = len(self.data[r_ind])
if row_len > max_nb_col:
max_nb_col = row_len
max_nb_col = max(max_nb_col, col)
c_ind = col_for_data
while c_ind < max_nb_col:
self.data[self.data_row].append('')
grid_col = len(self.data[self.data_row])
self.all_cells[row - 2][grid_col - 1].data_row = self.data_row
self.all_cells[row - 2][grid_col - 1].data_column = c_ind
self.notify_observers(EMPTY_COEFF, row, grid_col)
added_cols = True
c_ind += 1
if (col_for_data < col):
col_for_data += 1
if added_cols:
for r_ind in range(nb_rows):
while len(self.data[r_ind]) < max_nb_col:
self.data[r_ind].append('')
grid_col = len(self.data[r_ind])
if r_ind >= self.data_row - (row - 3): # 3 is the first physical
# row with data on the page
grid_row = row - (self.data_row - r_ind)
self.all_cells[grid_row - 2][grid_col - 1].data_row = r_ind
self.all_cells[grid_row - 2][grid_col - 1].data_column = grid_col - 1
self.notify_observers(EMPTY_COEFF, grid_row, grid_col)
self.data_column = col_for_data - 1
else:
self.data_column = col - 1
self.data[self.data_row][self.data_column] = self.text_value.get().strip()
[docs] def notify_panel_observer(self):
''' Notifies panel observer that data was modified.
'''
if self.panel_text_observer is not None and self.modify_data is True:
self.panel_text_observer.change_state_if_needed()
[docs] def notify_observers(self, entry_state, row, col):
''' Notifies all observers stored in list of observers that data
was modified.
Args:
entry_state (int): state of the Entry widget that describes if
data is valid after modification, for possible values see
dea_utils module.
row (int): row where Entry is gridded.
col (int): column where Entry is gridded.
'''
for observer in self.observers:
observer.change_state_if_needed(self, entry_state, row, col)
[docs]class PanelTextObserver(object):
''' This class changes StringVar value that is traced in other classes.
Attributes:
if_text_modified_str (StringVar): StringVar object that
changes value when this observer is notified.
'''
def __init__(self, if_text_modified_str):
self.if_text_modified_str = if_text_modified_str
[docs] def change_state_if_needed(self):
''' Changes value of internal StringVar object.
'''
self.if_text_modified_str.set('*')
[docs]class CheckbuttonWithVar(Checkbutton):
''' Custom Checkbutton widget that provides deselect method.
Attributes:
var (IntVar): 0 if not selected, 1 otherwise.
Args:
parent (Tk object): parent of this widget.
var (IntVar): variable that controls if Checkbutton is selected.
'''
def __init__(self, parent, var, *args, **kw):
super().__init__(parent, variable=var, *args, **kw)
self.var = var