Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve GUI user experience #175

Merged
merged 3 commits into from
Sep 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
409 changes: 277 additions & 132 deletions ms2rescore/gui/app.py

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion ms2rescore/gui/function2ctk.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ def __init__(

# 2x3 grid, only logging column expands with window
self.grid_columnconfigure(0, weight=0) # Left: Sidebar
self.grid_columnconfigure(1, weight=2) # Middle: Configuration
self.grid_columnconfigure(1, weight=0) # Middle: Configuration
self.grid_columnconfigure(2, weight=1) # Right: Logging
self.grid_rowconfigure(0, weight=1)

Expand Down
163 changes: 86 additions & 77 deletions ms2rescore/gui/widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,33 +7,61 @@
import customtkinter as ctk


class Heading(ctk.CTkLabel):
class _Heading(ctk.CTkLabel):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.configure(
font=ctk.CTkFont(weight="bold"),
fg_color=("gray80", "gray30"),
text_color=("black", "white"),
corner_radius=6,
)


class LabeledEntry(ctk.CTkFrame):
class _LabeledWidget(ctk.CTkFrame):
def __init__(self, *args, label="Label", description=None, wraplength=0, **kwargs):
super().__init__(*args, **kwargs)
self.grid_columnconfigure(0, weight=1)

self.row_n = 0

self._label_frame = ctk.CTkFrame(self)
self._label_frame.grid_columnconfigure(0, weight=1)
self._label_frame.configure(fg_color="transparent")
self._label_frame.grid(row=self.row_n, column=0, padx=0, pady=(0, 5), sticky="nsew")
self.row_n += 1

self._label = ctk.CTkLabel(
self._label_frame,
text=label,
font=ctk.CTkFont(weight="bold"),
wraplength=wraplength,
justify="left",
anchor="w",
)
self._label.grid(row=0, column=0, padx=0, pady=0, sticky="w")

if description:
self._description = ctk.CTkLabel(
self._label_frame,
text=description,
wraplength=wraplength,
justify="left",
anchor="w",
)
self._description.grid(row=1, column=0, padx=0, pady=0, sticky="ew")


class LabeledEntry(_LabeledWidget):
def __init__(
self,
*args,
label="Enter text",
placeholder_text="Enter text...",
default_value="",
**kwargs,
):
super().__init__(*args, **kwargs)
self.grid_columnconfigure(0, weight=1)

self._variable = ctk.StringVar(value=default_value)

self._label = ctk.CTkLabel(self, text=label)
self._label.grid(row=0, column=0, padx=0, pady=5, sticky="w")

self._entry = ctk.CTkEntry(
self, placeholder_text=placeholder_text, textvariable=self._variable
)
Expand All @@ -43,11 +71,10 @@ def get(self):
return self._variable.get()


class LabeledEntryTextbox(ctk.CTkFrame):
class LabeledEntryTextbox(_LabeledWidget):
def __init__(
self,
*args,
label="Enter text",
initial_contents="Enter text here...",
box_height=100,
**kwargs,
Expand All @@ -56,49 +83,40 @@ def __init__(
self.grid_columnconfigure(0, weight=1)
self.grid_rowconfigure(1, weight=1)

self._label = ctk.CTkLabel(self, text=label, anchor="w")
self._label.grid(row=0, column=0, padx=0, pady=5, sticky="ew")

self.text_box = ctk.CTkTextbox(self, height=box_height)
self.text_box.insert("1.0", initial_contents)
self.text_box.grid(row=1, column=0, padx=0, pady=5, sticky="nsew")
self.text_box.grid(row=self.row_n, column=0, padx=0, pady=5, sticky="nsew")
self.row_n += 1

def get(self):
return self.text_box.get("0.0", tk.END)


class LabeledRadioButtons(ctk.CTkFrame):
def __init__(self, *args, label="Select option", options=[], default_value=None, **kwargs):
class LabeledRadioButtons(_LabeledWidget):
def __init__(
self,
*args,
options=[],
default_value=None,
**kwargs,
):
super().__init__(*args, **kwargs)
self.grid_columnconfigure(0, weight=1)

self.value = ctk.StringVar(value=default_value or options[0])

self._label = ctk.CTkLabel(self, text=label)
self._label.grid(row=0, column=0, padx=0, pady=(0, 5), sticky="w")

self._radio_buttons = []
for i, option in enumerate(options):
radio_button = ctk.CTkRadioButton(self, text=option, variable=self.value, value=option)
radio_button.grid(row=i + 1, column=0, padx=0, pady=(0, 5), sticky="w")
radio_button.grid(row=self.row_n, column=0, padx=0, pady=(0, 5), sticky="w")
self._radio_buttons.append(radio_button)
self.row_n += 1

def get(self):
return self.value.get()


class LabeledOptionMenu(ctk.CTkFrame):
def __init__(
self, *args, vertical=False, label="Select option", values=[], default_value=None, **kwargs
):
class LabeledOptionMenu(_LabeledWidget):
def __init__(self, *args, vertical=False, values=[], default_value=None, **kwargs):
super().__init__(*args, **kwargs)
self.grid_columnconfigure(0, weight=1)

self.value = ctk.StringVar(value=default_value or values[0])

self._label = ctk.CTkLabel(self, text=label)
self._label.grid(row=0, column=0, padx=0, pady=0 if vertical else 5, sticky="w")

self._option_menu = ctk.CTkOptionMenu(self, variable=self.value, values=values)
self._option_menu.grid(
row=1 if vertical else 0,
Expand All @@ -112,16 +130,10 @@ def get(self):
return self.value.get()


class LabeledSwitch(ctk.CTkFrame):
def __init__(self, *args, label="Enable/disable", default=False, **kwargs):
class LabeledSwitch(_LabeledWidget):
def __init__(self, *args, default=False, **kwargs):
super().__init__(*args, **kwargs)
self.grid_columnconfigure(0, weight=1)

self.value = ctk.StringVar(value="0")

self._label = ctk.CTkLabel(self, text=label)
self._label.grid(row=0, column=0, padx=0, pady=5, sticky="w")

self._switch = ctk.CTkSwitch(self, variable=self.value, text="", onvalue="1", offvalue="0")
self._switch.grid(row=0, column=1, padx=0, pady=5, sticky="e")
if default:
Expand Down Expand Up @@ -202,21 +214,17 @@ def set(self, value: float):
self.entry.insert(0, format(value, self.str_format))


class LabeledFloatSpinbox(ctk.CTkFrame):
class LabeledFloatSpinbox(_LabeledWidget):
def __init__(
self,
*args,
label="Enter value",
initial_value=0.0,
step_size=1,
**kwargs,
):
super().__init__(*args, **kwargs)
self.grid_columnconfigure(0, weight=1)

self._label = ctk.CTkLabel(self, text=label)
self._label.grid(row=0, column=0, padx=0, pady=5, sticky="w")

self._spinbox = FloatSpinbox(
self,
initial_value=initial_value,
Expand All @@ -228,15 +236,20 @@ def get(self):
return self._spinbox.get()


class LabeledFileSelect(ctk.CTkFrame):
def __init__(self, *args, label="Select file", file_option="openfile", **kwargs):
class LabeledFileSelect(_LabeledWidget):
def __init__(
self,
*args,
file_option="openfile",
**kwargs,
):
"""
Advanced file selection widget with entry and file, directory, or save button.

Parameters
----------
label : str
Label text.
wraplength : int
Maximum line length before wrapping.
file_option : str
One of "openfile", "directory", "file/dir", or "savefile. Determines the type of file
selection dialog that is shown when the button is pressed.
Expand All @@ -253,14 +266,12 @@ def __init__(self, *args, label="Select file", file_option="openfile", **kwargs)
self._button_1 = None
self._button_2 = None

self.grid_columnconfigure(0, weight=1)

# Subwidgets
self._label = ctk.CTkLabel(self, text=label)
self._label.grid(row=0, column=0, columnspan=2, padx=0, pady=(5, 0), sticky="w")
self._label_frame.grid(
row=self.row_n - 1, column=0, columnspan=3, padx=0, pady=(0, 5), sticky="nsew"
) # Override grid placement of _LabeledWidget label frame to span all columns

self._entry = ctk.CTkEntry(self, placeholder_text="Select a file or directory")
self._entry.grid(row=1, column=0, padx=0, pady=0, sticky="ew")
self._entry.grid(row=self.row_n, column=0, padx=0, pady=5, sticky="ew")

if file_option == "directory":
self._button_1 = ctk.CTkButton(self, text="Browse directories", command=self._pick_dir)
Expand All @@ -279,9 +290,10 @@ def __init__(self, *args, label="Select file", file_option="openfile", **kwargs)
self, text="Path to save file(s)", command=self._save_file
)

self._button_1.grid(row=1, column=1, padx=(5, 0), pady=0, sticky="e")
self._button_1.grid(row=self.row_n, column=1, padx=(5, 0), pady=5, sticky="e")
if self._button_2:
self._button_2.grid(row=1, column=2, padx=(5, 0), pady=0, sticky="e")
self._button_2.grid(row=self.row_n, column=2, padx=(5, 0), pady=5, sticky="e")
self.row_n += 1

def get(self):
"""Returns the selected file or directory."""
Expand Down Expand Up @@ -312,41 +324,36 @@ def _save_file(self):
self._update_entry()


class TableInput(ctk.CTkFrame):
def __init__(self, *args, label=None, columns=2, header_labels=["A", "B"], **kwargs):
class TableInput(_LabeledWidget):
def __init__(
self,
*args,
columns=2,
header_labels=["A", "B"],
**kwargs,
):
"""
Table input widget with user-configurable number of rows.

Parameters
----------
label : str
Label text.
columns : int
Number of columns in the table.
header_labels : list of str
Labels for the header row.

"""
super().__init__(*args, **kwargs)
self.label = label
self.columns = columns
self.header_labels = header_labels

self.uniform_hash = str(random.getrandbits(128))

self.grid_columnconfigure(0, weight=1)

# Label
if label:
self.label = ctk.CTkLabel(self, text=label)
self.label.grid(row=0, column=0, pady=(5, 0), sticky="w")
label_row = 1
else:
label_row = 0

# Header row
header_row = ctk.CTkFrame(self)
header_row.grid(row=0 + label_row, column=0, padx=(33, 0), pady=(0, 5), sticky="ew")
header_row.grid(row=self.row_n, column=0, padx=(33, 0), pady=(5, 5), sticky="ew")
self.row_n += 1

for i, header in enumerate(self.header_labels):
header_row.grid_columnconfigure(i, weight=1, uniform=self.uniform_hash)
padx = (0, 5) if i < len(self.header_labels) - 1 else (0, 0)
Expand All @@ -364,15 +371,17 @@ def __init__(self, *args, label=None, columns=2, header_labels=["A", "B"], **kwa
self.input_frame = ctk.CTkFrame(self)
self.input_frame.uniform_hash = self.uniform_hash
self.input_frame.grid_columnconfigure(0, weight=1)
self.input_frame.grid(row=1 + label_row, column=0, sticky="new")
self.input_frame.grid(row=self.row_n, column=0, sticky="new")
self.row_n += 1

# Add first row that cannot be removed
self.add_row()
self.rows[0].remove_button.configure(state="disabled")

# Button to add more rows
self.add_button = ctk.CTkButton(self, text="+", width=28, command=self.add_row)
self.add_button.grid(row=2 + label_row, column=0, pady=(0, 5), sticky="w")
self.add_button.grid(row=self.row_n, column=0, pady=(0, 5), sticky="w")
self.row_n += 1

def add_row(self):
row = _TableInputRow(self.input_frame, columns=self.columns)
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added ms2rescore/package_data/img/docs_icon_black.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added ms2rescore/package_data/img/docs_icon_white.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading