pythonQEPest 2.0.0a2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. pythonQEPest/.env.example +4 -0
  2. pythonQEPest/__init__.py +11 -0
  3. pythonQEPest/cli/__init__.py +7 -0
  4. pythonQEPest/cli/cli.py +101 -0
  5. pythonQEPest/config/__init__.py +11 -0
  6. pythonQEPest/config/config_provider.py +9 -0
  7. pythonQEPest/config/normalise.py +12 -0
  8. pythonQEPest/config/qepest_config.py +31 -0
  9. pythonQEPest/config/qepest_default.py +26 -0
  10. pythonQEPest/core/__init__.py +7 -0
  11. pythonQEPest/core/qepest.py +98 -0
  12. pythonQEPest/core/qepest_meta.py +28 -0
  13. pythonQEPest/data.txt +2 -0
  14. pythonQEPest/data.txt.out +2 -0
  15. pythonQEPest/dto/QEPestData.py +21 -0
  16. pythonQEPest/dto/QEPestFile.py +34 -0
  17. pythonQEPest/dto/QEPestInput.py +60 -0
  18. pythonQEPest/dto/QEPestOutput.py +13 -0
  19. pythonQEPest/dto/__init__.py +27 -0
  20. pythonQEPest/dto/coefficients/QEPestCoefficient.py +57 -0
  21. pythonQEPest/dto/coefficients/QEPestCoefficientList.py +29 -0
  22. pythonQEPest/dto/coefficients/QEPestCoefficientNumerics.py +44 -0
  23. pythonQEPest/dto/coefficients/__init__.py +7 -0
  24. pythonQEPest/dto/normalisation/Normaliser.py +16 -0
  25. pythonQEPest/dto/normalisation/__init__.py +3 -0
  26. pythonQEPest/dto/pest_type/PestTypeCoefficient.py +30 -0
  27. pythonQEPest/dto/pest_type/PestTypeConfig.py +9 -0
  28. pythonQEPest/dto/pest_type/__init__.py +4 -0
  29. pythonQEPest/gui/__init__.py +7 -0
  30. pythonQEPest/gui/actions/__init__.py +9 -0
  31. pythonQEPest/gui/actions/action_clicks.py +42 -0
  32. pythonQEPest/gui/actions/actions_crud.py +131 -0
  33. pythonQEPest/gui/actions/actions_other.py +126 -0
  34. pythonQEPest/gui/elements/ButtonsFrame.py +50 -0
  35. pythonQEPest/gui/elements/DataTree.py +23 -0
  36. pythonQEPest/gui/elements/EditWindow.py +63 -0
  37. pythonQEPest/gui/elements/Menu.py +12 -0
  38. pythonQEPest/gui/elements/ResultTree.py +22 -0
  39. pythonQEPest/gui/elements/SaveButton.py +10 -0
  40. pythonQEPest/gui/elements/__init__.py +17 -0
  41. pythonQEPest/gui/gui.py +87 -0
  42. pythonQEPest/gui/gui_meta.py +17 -0
  43. pythonQEPest/gui/utility/DataManager.py +76 -0
  44. pythonQEPest/gui/utility/DataManagerMeta.py +8 -0
  45. pythonQEPest/gui/utility/__init__.py +6 -0
  46. pythonQEPest/helpers/__init__.py +17 -0
  47. pythonQEPest/helpers/check_nan.py +8 -0
  48. pythonQEPest/helpers/compute_df.py +5 -0
  49. pythonQEPest/helpers/get_num_of_cols.py +2 -0
  50. pythonQEPest/helpers/get_values_from_line.py +5 -0
  51. pythonQEPest/helpers/norm.py +33 -0
  52. pythonQEPest/helpers/round_to_4digs.py +2 -0
  53. pythonQEPest/logger.py +58 -0
  54. pythonQEPest/main.py +4 -0
  55. pythonQEPest/providers/__init__.py +6 -0
  56. pythonQEPest/providers/default_provider.py +23 -0
  57. pythonQEPest/providers/json_provider.py +30 -0
  58. pythonQEPest/providers/txt_provider.py +46 -0
  59. pythonQEPest/services/QEPestFileService.py +125 -0
  60. pythonQEPest/services/__init__.py +3 -0
  61. pythonqepest-2.0.0a2.dist-info/METADATA +217 -0
  62. pythonqepest-2.0.0a2.dist-info/RECORD +64 -0
  63. pythonqepest-2.0.0a2.dist-info/WHEEL +4 -0
  64. pythonqepest-2.0.0a2.dist-info/entry_points.txt +6 -0
@@ -0,0 +1,16 @@
1
+ from typing import Union
2
+
3
+
4
+ class Normaliser:
5
+ def __init__(self, arr: Union[tuple, list]):
6
+ # if len(arr) != 6:
7
+ # raise ValueError(f"Array must be exact length of 6. Given: {len(arr)}")
8
+
9
+ self.arr = arr
10
+
11
+ def norm(self, d: Union[int, float], descr: int) -> float:
12
+ if descr > len(self.arr):
13
+ raise KeyError(f"Out of array. {descr} > {len(self.arr)}")
14
+
15
+ max_val = self.arr[int(descr)]
16
+ return d / max_val if max_val != 0 else 0.0
@@ -0,0 +1,3 @@
1
+ from pythonQEPest.dto.normalisation.Normaliser import Normaliser
2
+
3
+ __all__ = ["Normaliser"]
@@ -0,0 +1,30 @@
1
+ from typing import Any
2
+
3
+ from pydantic import BaseModel, model_validator
4
+
5
+
6
+ class PestTypeCoefficient(BaseModel):
7
+ mwH: tuple[float, float, float, float]
8
+ logpH: tuple[float, float, float, float]
9
+ hbaH: tuple[float, float, float, float]
10
+ hbdH: tuple[float, float, float, float]
11
+ rbH: tuple[float, float, float, float]
12
+ arRCH: tuple[float, float, float, float]
13
+
14
+ @model_validator(mode="before")
15
+ @classmethod
16
+ def accept_positional(cls, data: Any):
17
+ if isinstance(data, dict):
18
+ return data
19
+
20
+ if isinstance(data, (list, tuple)):
21
+ if len(data) != 6:
22
+ raise ValueError("Expected 6 items: mwH, logpH, hbaH, hbdH, rbH, arRCH")
23
+
24
+ keys = ("mwH", "logpH", "hbaH", "hbdH", "rbH", "arRCH")
25
+ return dict(zip(keys, data))
26
+
27
+ raise TypeError("Expected a dict or a 6-item tuple/list")
28
+
29
+ def as_set(self):
30
+ return [self.mwH, self.logpH, self.hbaH, self.hbdH, self.rbH, self.arRCH]
@@ -0,0 +1,9 @@
1
+ from pydantic import BaseModel
2
+
3
+ from pythonQEPest.dto.pest_type.PestTypeCoefficient import PestTypeCoefficient
4
+
5
+
6
+ class PestTypeConfig(BaseModel):
7
+ name: str
8
+ coefficients: PestTypeCoefficient
9
+ normaliser: tuple[float, float, float, float, float, float]
@@ -0,0 +1,4 @@
1
+ from pythonQEPest.dto.pest_type.PestTypeCoefficient import PestTypeCoefficient
2
+ from pythonQEPest.dto.pest_type.PestTypeConfig import PestTypeConfig
3
+
4
+ __all__ = ["PestTypeConfig", "PestTypeCoefficient"]
@@ -0,0 +1,7 @@
1
+ def main():
2
+ from pythonQEPest.gui.gui import main as _main
3
+
4
+ return _main()
5
+
6
+
7
+ __all__ = ["main"]
@@ -0,0 +1,9 @@
1
+ from pythonQEPest.gui.actions.action_clicks import GUIActionsClicks
2
+ from pythonQEPest.gui.actions.actions_crud import GUIActionsCRUD
3
+ from pythonQEPest.gui.actions.actions_other import GUIActionsOther
4
+
5
+ __all__ = [
6
+ "GUIActionsClicks",
7
+ "GUIActionsCRUD",
8
+ "GUIActionsOther",
9
+ ]
@@ -0,0 +1,42 @@
1
+ class GUIActionsClicks:
2
+ def __init__(self, menu):
3
+ self.menu = menu
4
+
5
+ def on_treeview_click_left(self, event):
6
+ tree = event.widget
7
+ item_id = tree.identify_row(event.y)
8
+ if not item_id:
9
+ return
10
+ selected = tree.selection()
11
+ if item_id in selected:
12
+ tree.selection_remove(item_id)
13
+ else:
14
+ tree.selection_set(item_id) # [*selected, item_id]
15
+
16
+ def on_treeview_click_right(self, event):
17
+ tree = event.widget
18
+ item_id = tree.identify_row(event.y)
19
+ if item_id:
20
+ tree.selection_set(item_id)
21
+ self.menu.tk_popup(event.x_root, event.y_root)
22
+
23
+ def resize_columns(self, event):
24
+ widget = event.widget
25
+ region = widget.identify("region", event.x, event.y)
26
+ if region != "separator":
27
+ return
28
+
29
+ col = widget.identify_column(event.x)
30
+ if not col:
31
+ return
32
+
33
+ col_index = int(col.replace("#", "")) - 1
34
+ col_id = widget["columns"][col_index]
35
+
36
+ max_width = max(
37
+ [len(str(widget.set(k, col_id))) for k in widget.get_children("")]
38
+ + [len(col_id)]
39
+ )
40
+
41
+ pixel_width = max_width * 8
42
+ widget.column(col_id, width=pixel_width)
@@ -0,0 +1,131 @@
1
+ from tkinter import messagebox, ttk
2
+
3
+ try:
4
+ import pyperclip
5
+ except ImportError:
6
+ pyperclip = None
7
+
8
+ from pythonQEPest.gui.elements.EditWindow import EditWindow
9
+ from pythonQEPest.gui.utility.DataManager import DataManager
10
+
11
+
12
+ class GUIActionsCRUD:
13
+ def __init__(self, data_tree, result_tree, save_button, index):
14
+ self.index = index
15
+ self.data_manager = DataManager()
16
+ self.data_tree: ttk.Treeview = data_tree
17
+ self.result_tree: ttk.Treeview = result_tree
18
+ self.save_button = save_button
19
+
20
+ def edit_selected(self, *args, **kwargs):
21
+ selected = self.data_tree.selection()
22
+ if not selected:
23
+ messagebox.showwarning("Select Entry", "Select Entry for editing")
24
+ return
25
+
26
+ item_id = selected[0]
27
+ EditWindow(item_id=item_id, tree=self.data_tree, child_tree=self.result_tree)
28
+
29
+ def copy_selected(self, *args, **kwargs):
30
+ selected_data_tree = self.data_tree.selection()
31
+ selected_result_tree = self.result_tree.selection()
32
+
33
+ if not (selected_data_tree or selected_result_tree):
34
+ messagebox.showwarning("Select Entry", "Select Entry for coping")
35
+ return
36
+
37
+ rows_text = []
38
+ if selected_data_tree:
39
+ rows_text.append(
40
+ "\t".join(
41
+ map(str, ("ID", "Name", "MW", "LogP", "HBA", "HBD", "RB", "arR"))
42
+ )
43
+ )
44
+ for item in selected_data_tree:
45
+ values = self.data_tree.item(item, "values")[1:]
46
+ rows_text.append("\t".join(map(str, values)))
47
+
48
+ if selected_result_tree:
49
+ rows_text.append("\t".join(map(str, ("ID", "Name", "QEH", "QEI", "QEF"))))
50
+ for item in selected_result_tree:
51
+ values = self.result_tree.item(item, "values")[1:]
52
+ rows_text.append("\t".join(map(str, values)))
53
+
54
+ data_str = "\n".join(rows_text)
55
+
56
+ if not pyperclip:
57
+ messagebox.showerror(
58
+ "Error",
59
+ "pyperclip module is not installed. Install it with command"
60
+ + "'pip install .[gui]' or 'poetry install --with ui'"
61
+ + "to enable copy functionality.",
62
+ )
63
+ return
64
+
65
+ pyperclip.copy(data_str)
66
+ messagebox.showinfo("Copied", "Data copied to clipboard.")
67
+
68
+ def paste_entries(self, *args, **kwargs):
69
+ if not pyperclip:
70
+ messagebox.showerror(
71
+ "Error",
72
+ "pyperclip module is not installed. Install it with command"
73
+ + "'pip install .[gui]' or 'poetry install --with ui'"
74
+ + "to enable copy functionality.",
75
+ )
76
+ return
77
+
78
+ clipboard_text = pyperclip.paste()
79
+ if not clipboard_text.strip():
80
+ messagebox.showwarning("Clipboard is empty", "Copy the text first.")
81
+ return
82
+
83
+ lines = clipboard_text.strip().splitlines()
84
+ count_added = 0
85
+
86
+ for line in lines:
87
+ parts = line.strip().split("\t")
88
+ if len(parts) == 7:
89
+ idx = self.index
90
+ self.data_manager.add_file((idx, *parts))
91
+ self.data_tree.insert("", "end", values=(idx, *parts))
92
+ count_added += 1
93
+ self.index += 1
94
+
95
+ if count_added:
96
+ messagebox.showinfo(
97
+ "Inserted",
98
+ f"Inserted {count_added} entries. Don't forget to process the data.",
99
+ )
100
+
101
+ def delete_selected(self, *args, **kwargs):
102
+ selected = self.data_tree.selection()
103
+ if not selected:
104
+ messagebox.showwarning("Select Entry", "Select the entry to delete.")
105
+ return
106
+
107
+ idxs_to_remove = []
108
+ for item in selected:
109
+ values = self.data_tree.item(item, "values")
110
+ idx_to_remove = int(values[0])
111
+ idxs_to_remove.append(idx_to_remove)
112
+
113
+ self.data_tree.delete(item)
114
+
115
+ if self.result_tree.exists(item):
116
+ self.result_tree.delete(item)
117
+
118
+ self.data_manager.remove_result_by_ids(idxs_to_remove)
119
+ self.data_manager.remove_file_by_ids(idxs_to_remove)
120
+
121
+ messagebox.showinfo("Deleted", "The selected records have been deleted.")
122
+
123
+ if not self.data_manager:
124
+ self.save_button.config(state="disabled")
125
+
126
+ def clear_everything(self, *args, **kwargs):
127
+ self.data_manager.clear_file()
128
+ self.data_manager.clear_result()
129
+
130
+ self.data_tree.delete(*self.data_tree.get_children())
131
+ self.result_tree.delete(*self.result_tree.get_children())
@@ -0,0 +1,126 @@
1
+ import tkinter as tk
2
+ from tkinter import filedialog, messagebox
3
+
4
+ from pythonQEPest.dto.QEPestInput import QEPestInput
5
+ from pythonQEPest.gui.utility.DataManager import DataManager
6
+
7
+
8
+ class GUIActionsOther:
9
+ def __init__(self, root, data_tree, result_tree, save_button, qepest):
10
+ self.data_manager = DataManager()
11
+ self.root = root
12
+ self.data_tree = data_tree
13
+ self.result_tree = result_tree
14
+ self.save_button = save_button
15
+ self.qepest = qepest
16
+
17
+ def load_file(self):
18
+ file_path = filedialog.askopenfilename(
19
+ filetypes=[("Text files", "*.txt"), ("CSV files", "*.csv")]
20
+ )
21
+ if not file_path:
22
+ return
23
+
24
+ try:
25
+ with open(file_path, "r", encoding="utf-8") as file:
26
+ self.data_manager.clear_file()
27
+ self.data_tree.delete(*self.data_tree.get_children())
28
+
29
+ for idx, line in enumerate(file):
30
+ parts = line.strip().split("\t")
31
+ if len(parts) == 7:
32
+ self.data_manager.add_file((idx, *parts))
33
+ self.data_tree.insert("", "end", values=(idx, *parts))
34
+
35
+ messagebox.showinfo(
36
+ "File uploaded", f"Loaded {len(self.data_manager.file_data)} rows."
37
+ )
38
+ except Exception as e:
39
+ messagebox.showerror("Loading error", str(e))
40
+
41
+ def add_entry(self):
42
+ form = tk.Toplevel(self.root)
43
+ form.title("Add entry")
44
+ form.transient(self.root)
45
+ form.grab_set()
46
+
47
+ entries = {}
48
+ fields = ["Name", "MW", "LogP", "HBA", "HBD", "RB", "arR"]
49
+
50
+ for idx, field in enumerate(fields):
51
+ tk.Label(form, text=field).grid(
52
+ row=idx, column=0, padx=5, pady=5, sticky="e"
53
+ )
54
+ entry = tk.Entry(form)
55
+ entry.grid(row=idx, column=1, padx=5, pady=5)
56
+ entries[field] = entry
57
+
58
+ def submit():
59
+ values = []
60
+ for field in fields:
61
+ val = entries[field].get().strip()
62
+ if not val:
63
+ messagebox.showwarning("Error", f"The field {field} not filled.")
64
+ return
65
+ values.append(val)
66
+
67
+ idx = len(self.data_manager.file_data)
68
+ self.data_manager.add_file((idx, *values))
69
+ self.data_tree.insert("", "end", values=(idx, *values))
70
+ form.destroy()
71
+ messagebox.showinfo(
72
+ "Added",
73
+ "The entry has been added. Process the data to update the result.",
74
+ )
75
+
76
+ tk.Button(form, text="Add", command=submit).grid(
77
+ row=len(fields), column=0, columnspan=2, pady=10
78
+ )
79
+
80
+ form.wait_window()
81
+
82
+ def process_data(self):
83
+ if not self.data_manager.file_data:
84
+ messagebox.showwarning("No data", "Please add or upload data first.")
85
+ return
86
+
87
+ # self.result_data = []
88
+ # self.result_tree.delete(*self.result_tree.get_children())
89
+
90
+ for row in self.data_manager.file_data:
91
+ try:
92
+ result_row = [
93
+ row[0],
94
+ *self.qepest.compute_params(
95
+ QEPestInput.from_array(row[1:])
96
+ ).to_array(),
97
+ ]
98
+ except ValueError as e:
99
+ messagebox.showwarning("Warning!", f"Line number: {row[0]}\n{str(e)}")
100
+ continue
101
+ self.data_manager.add_result(result_row)
102
+ self.result_tree.insert("", "end", values=result_row)
103
+
104
+ self.save_button.config(state="normal")
105
+
106
+ def save_result(self):
107
+ if not self.data_manager.result_data:
108
+ messagebox.showwarning("No result", "Process the data first.")
109
+ return
110
+
111
+ save_path = filedialog.asksaveasfilename(
112
+ defaultextension=".txt", filetypes=[("Text files", "*.txt")]
113
+ )
114
+ if not save_path:
115
+ return
116
+
117
+ try:
118
+ with open(save_path, "w", encoding="utf-8") as file:
119
+ file.write("Name\tQEH\tQEI\tQEF\n")
120
+ for row in self.data_manager.result_data:
121
+ file.write("\t".join(map(str, row)) + "\n")
122
+
123
+ messagebox.showinfo("Saved", f"The result is saved in: {save_path}")
124
+
125
+ except Exception as e:
126
+ messagebox.showerror("Saving error", str(e))
@@ -0,0 +1,50 @@
1
+ from tkinter import Frame, Button
2
+
3
+ try:
4
+ import pyperclip
5
+ except ImportError:
6
+ pyperclip = None
7
+
8
+
9
+ class ButtonsFrame(Frame):
10
+ def __init__(self, root, *args, **kwargs):
11
+ super().__init__(root, *args, **kwargs)
12
+
13
+ self.pack(anchor="w", fill="x", pady=5)
14
+
15
+ # TODO: select_file must have an option for CSV
16
+ self.select_file_button = Button(self, text="Select File")
17
+ self.add_entry_button = Button(self, text="Add entry")
18
+ self.edit_entry_button = Button(self, text="Edit entry")
19
+ self.delete_selected_button = Button(self, text="Delete entry")
20
+ self.copy_button = Button(self, text="Copy Data")
21
+ self.paste_button = Button(self, text="Paste Data")
22
+
23
+ self.process_data_button = Button(self, text="Process Data")
24
+ self.clear_button = Button(self, text="Clear Data")
25
+
26
+ def set_actions(self, actions_crud, actions_other):
27
+ self.select_file_button.pack(side="left", padx=(10, 4))
28
+ self.add_entry_button.pack(side="left", padx=4)
29
+ self.edit_entry_button.pack(side="left", padx=4)
30
+ self.delete_selected_button.pack(side="left", padx=4)
31
+
32
+ if not pyperclip:
33
+ self.copy_button.config(state="disabled")
34
+ self.paste_button.config(state="disabled")
35
+
36
+ self.copy_button.pack(side="left", padx=4)
37
+ self.paste_button.pack(side="left", padx=4)
38
+
39
+ self.process_data_button.pack(side="right", padx=(4, 10))
40
+ self.clear_button.pack(side="right", padx=4)
41
+
42
+ self.select_file_button.config(command=actions_other.load_file)
43
+ self.add_entry_button.config(command=actions_other.add_entry)
44
+ self.edit_entry_button.config(command=actions_crud.edit_selected)
45
+ self.delete_selected_button.config(command=actions_crud.delete_selected)
46
+ self.copy_button.config(command=actions_crud.copy_selected)
47
+ self.paste_button.config(command=actions_crud.paste_entries)
48
+
49
+ self.process_data_button.config(command=actions_other.process_data)
50
+ self.clear_button.config(command=actions_crud.clear_everything)
@@ -0,0 +1,23 @@
1
+ from tkinter.ttk import Treeview
2
+
3
+
4
+ class DataTree(Treeview):
5
+ def __init__(self, root, *args, **kwargs):
6
+ columns = ("ID", "Name", "MW", "LogP", "HBA", "HBD", "RB", "arR")
7
+ super().__init__(root, *args, columns=columns, show="headings", **kwargs)
8
+
9
+ for col in columns:
10
+ self.heading(col, text=col)
11
+
12
+ self.pack(padx=10, pady=10, fill="both", expand=True)
13
+
14
+ def set_actions(self, sort_column, actions_clicks):
15
+ for col in self["columns"]:
16
+ self.heading(
17
+ col, text=col, command=lambda _col=col: sort_column(self, _col, False)
18
+ )
19
+
20
+ self.bind("<Button-1>", actions_clicks.on_treeview_click_left)
21
+ self.bind("<Button-3>", actions_clicks.on_treeview_click_right)
22
+
23
+ self.bind("<Double-1>", actions_clicks.resize_columns)
@@ -0,0 +1,63 @@
1
+ import tkinter as tk
2
+ from tkinter.ttk import Treeview
3
+
4
+ from pythonQEPest.gui.utility.DataManager import DataManager
5
+
6
+
7
+ class EditWindow(tk.Toplevel):
8
+ def __init__(self, tree: Treeview, child_tree: Treeview, item_id, *args, **kwargs):
9
+ super().__init__(*args, master=tree, **kwargs)
10
+
11
+ self.title("Edit Entry")
12
+ # self.geometry("200x300")
13
+
14
+ self.data_manager = DataManager()
15
+
16
+ values = tree.item(item_id, "values")
17
+ old_id = values[0]
18
+
19
+ entries = []
20
+ columns = tree["columns"]
21
+
22
+ self.grid_columnconfigure(0, weight=0)
23
+ self.grid_columnconfigure(1, weight=3)
24
+ self.resizable(True, False)
25
+
26
+ for idx, col in enumerate(columns):
27
+ tk.Label(self, text=col).grid(
28
+ row=idx, column=0, padx=5, pady=5, sticky="ew"
29
+ )
30
+ entry = tk.Entry(self)
31
+ entry.insert(0, values[idx])
32
+ entry.grid(row=idx, column=1, padx=(5, 5), pady=5, sticky="ew")
33
+ entries.append(entry)
34
+
35
+ def save_changes() -> None:
36
+ new_values = [entry.get() for entry in entries]
37
+
38
+ if child_tree.exists(item_id):
39
+ new_child_values = list(child_tree.item(item_id, "values"))
40
+ if new_child_values[0] != new_values[0]:
41
+ new_child_values[0] = new_values[0]
42
+ child_tree.item(item_id, values=new_child_values)
43
+
44
+ tree.item(item_id, values=new_values)
45
+
46
+ # Fix it someday plz
47
+ new_values[0] = int(new_values[0])
48
+ update_file_data(new_values)
49
+
50
+ def update_file_data(values: list) -> None:
51
+ element = list(
52
+ filter(lambda x: str(x[0]) == str(old_id), self.data_manager.file_data)
53
+ )[0]
54
+ if element:
55
+ self.data_manager.update_file(index=element[0], new_entry=values)
56
+ self.destroy()
57
+
58
+ tk.Button(self, text="Save", command=save_changes).grid(
59
+ row=len(columns), column=0, columnspan=2, pady=10
60
+ )
61
+
62
+ self.grab_set()
63
+ self.wait_window()
@@ -0,0 +1,12 @@
1
+ from tkinter import Menu as tkMenu
2
+
3
+
4
+ class Menu(tkMenu):
5
+ def __init__(self, root, *args, **kwargs):
6
+ super().__init__(root, *args, **kwargs, tearoff=0)
7
+
8
+ def set_actions(self, actions_crud):
9
+ self.add_command(label="Copy", command=actions_crud.copy_selected)
10
+ self.add_command(label="Edit", command=actions_crud.edit_selected)
11
+ self.add_command(label="Paste", command=actions_crud.paste_entries)
12
+ self.add_command(label="Delete", command=actions_crud.delete_selected)
@@ -0,0 +1,22 @@
1
+ from tkinter.ttk import Treeview
2
+
3
+
4
+ class ResultTree(Treeview):
5
+ def __init__(self, root, *args, **kwargs):
6
+ columns = ("ID", "Name", "QEH", "QEI", "QEF")
7
+ super().__init__(root, *args, columns=columns, show="headings", **kwargs)
8
+
9
+ for col in columns:
10
+ self.heading(col, text=col)
11
+ self.pack(padx=10, pady=10, fill="both", expand=True)
12
+
13
+ def set_actions(self, sort_column, actions_clicks):
14
+ for col in self["columns"]:
15
+ self.heading(
16
+ col, text=col, command=lambda _col=col: sort_column(self, _col, False)
17
+ )
18
+
19
+ self.bind("<Button-1>", actions_clicks.on_treeview_click_left)
20
+ self.bind("<Button-3>", actions_clicks.on_treeview_click_right)
21
+
22
+ self.bind("<Double-1>", actions_clicks.resize_columns)
@@ -0,0 +1,10 @@
1
+ from tkinter import Button
2
+
3
+
4
+ class SaveButton(Button):
5
+ def __init__(self, root, *args, **kwargs):
6
+ super().__init__(root, *args, text="Save Results", state="disabled", **kwargs)
7
+ self.pack(padx=10, pady=10, side="right")
8
+
9
+ def set_actions(self, actions_other):
10
+ self.config(command=actions_other.save_result)
@@ -0,0 +1,17 @@
1
+ from pythonQEPest.gui.elements import (
2
+ ResultTree,
3
+ DataTree,
4
+ ButtonsFrame,
5
+ Menu,
6
+ SaveButton,
7
+ EditWindow,
8
+ )
9
+
10
+ __all__ = [
11
+ "ResultTree",
12
+ "DataTree",
13
+ "ButtonsFrame",
14
+ "Menu",
15
+ "SaveButton",
16
+ "EditWindow",
17
+ ]
@@ -0,0 +1,87 @@
1
+ import tkinter as tk
2
+
3
+ from dotenv import load_dotenv
4
+
5
+ from pythonQEPest.logger import init_logger
6
+
7
+ try:
8
+ import pyperclip
9
+ except ImportError:
10
+ pyperclip = None
11
+
12
+ from pythonQEPest.gui.actions import GUIActionsCRUD
13
+ from pythonQEPest.gui.actions import GUIActionsClicks
14
+ from pythonQEPest.gui.actions import GUIActionsOther
15
+ from pythonQEPest.gui.elements.ButtonsFrame import ButtonsFrame
16
+ from pythonQEPest.gui.elements.DataTree import DataTree
17
+ from pythonQEPest.gui.elements.Menu import Menu
18
+ from pythonQEPest.gui.elements.ResultTree import ResultTree
19
+ from pythonQEPest.gui.elements.SaveButton import SaveButton
20
+ from pythonQEPest.gui.gui_meta import QEPestMeta
21
+
22
+
23
+ # TODO: Outdated. Must be dynamic
24
+ class GUI(QEPestMeta):
25
+ def treeview_sort_column(self, treeview, col, reverse):
26
+ treeview_lst = [(treeview.set(k, col), k) for k in treeview.get_children("")]
27
+ try:
28
+ treeview_lst.sort(key=lambda t: float(t[0]), reverse=reverse)
29
+ except ValueError:
30
+ treeview_lst.sort(key=lambda t: t[0], reverse=reverse)
31
+
32
+ for index, (_, k) in enumerate(treeview_lst):
33
+ treeview.move(k, "", index)
34
+
35
+ treeview.heading(
36
+ col, command=lambda: self.treeview_sort_column(treeview, col, not reverse)
37
+ )
38
+
39
+ def build_gui(self):
40
+ self.file_data = []
41
+ self.index = 0
42
+
43
+ self.buttons_frame = ButtonsFrame(root=self.root)
44
+ self.data_tree = DataTree(root=self.root)
45
+ self.result_tree = ResultTree(root=self.root)
46
+ self.save_button = SaveButton(root=self.root)
47
+ self.menu = Menu(root=self.root)
48
+
49
+ self.actions_clicks = GUIActionsClicks(menu=self.menu)
50
+ self.actions_crud = GUIActionsCRUD(
51
+ data_tree=self.data_tree,
52
+ result_tree=self.result_tree,
53
+ save_button=self.save_button,
54
+ index=self.index,
55
+ )
56
+
57
+ self.actions_other = GUIActionsOther(
58
+ data_tree=self.data_tree,
59
+ result_tree=self.result_tree,
60
+ qepest=self.qepest,
61
+ root=self.root,
62
+ save_button=self.save_button,
63
+ )
64
+
65
+ self.buttons_frame.set_actions(self.actions_crud, self.actions_other)
66
+ self.data_tree.set_actions(self.treeview_sort_column, self.actions_clicks)
67
+ self.result_tree.set_actions(self.treeview_sort_column, self.actions_clicks)
68
+ self.save_button.set_actions(self.actions_other)
69
+ self.menu.set_actions(self.actions_crud)
70
+
71
+ if pyperclip:
72
+ self.root.bind("<Control-v>", self.actions_crud.paste_entries)
73
+ self.root.bind("<Control-c>", self.actions_crud.copy_selected)
74
+
75
+
76
+ def main() -> int:
77
+ load_dotenv()
78
+ init_logger()
79
+
80
+ root = tk.Tk()
81
+ GUI(root)
82
+ root.mainloop()
83
+ return 0
84
+
85
+
86
+ if __name__ == "__main__":
87
+ raise SystemExit(main())