"""This module provides a GUI for the MSI Generator (MSIGen) software."""
#!/usr/bin/env python3
# GUI.py
# contains all the functions and classes needed to run the GUI interface for MSIGen
import sys
from pathlib import Path
sys.path.append(str(Path(__file__).parent.parent))
import tkinter as tk
from tkinter import ttk
from tkinter import filedialog
from pathlib import Path
import os
import win32api
from threading import Thread
from time import time
from copy import deepcopy
from MSIGen import msigen
from MSIGen import visualization as vis
[docs]
def verify_rawfile_names_gui(rawfile_paths):
"""Ensures that all file names that are selected in the GUI:
1: All have the same path
2: All have the same file extension
3: All have the same file name, apart from a final number
4: Contain a unique number at the end of the file name
Input: list(str) of file paths
outputs:
rawfile_paths: A single file path as a string if only one path is given. Otherwise, this is the same as the input.
filenames_checked: bool
"""
filenames_checked = False
print(rawfile_paths)
# check that data was selected
if len(rawfile_paths) == 0:
error_message = "No raw data files were selected!"
tk.messagebox.showerror("Select file error", error_message)
elif len(rawfile_paths) == 1:
if rawfile_paths[0]=='':
error_message = "No raw data files were selected!"
tk.messagebox.showerror("Select file error", error_message)
# if only one file, return a string instead of a list
else:
if Path(rawfile_paths[0]).exists():
rawfile_paths = rawfile_paths[0]
filenames_checked = True
else:
error_message = "The data file given does not exist."
tk.messagebox.showerror("Select file error", error_message)
elif len(rawfile_paths) > 1:
# Check all have same file extension
if len(set([os.path.splitext(i)[1] for i in rawfile_paths])) != 1:
error_message = "Not all selected files had the same file extension!"
tk.messagebox.showerror("Select file error", error_message)
# check filenames only differ by number at the end
else:
namebodies = []
names = set([os.path.splitext(i)[0] for i in rawfile_paths])
for name in names:
iterator = 0
for i in name[::-1]:
if i.isdigit():
iterator+=1
else:
break
if iterator<1:
error_message = "All selected files must differ by a number at the end of the file name."
tk.messagebox.showerror("Select file error", error_message)
namebodies.append(name[:-iterator])
if len(set(namebodies)) != 1:
error_message = "All selected files must only differ by a number at the end of the file name."
tk.messagebox.showerror("Select file error", error_message)
else:
if all([Path(i).exists() for i in rawfile_paths]):
filenames_checked = True
else:
error_message = "At least one data file given does not exist."
tk.messagebox.showerror("Select file error", error_message)
return rawfile_paths, filenames_checked
[docs]
def get_download_path():
"""Returns the default downloads path for linux or windows"""
try:
if os.name == 'nt':
import winreg
sub_key = r'SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders'
downloads_guid = '{374DE290-123F-4565-9164-39C4925E467B}'
with winreg.OpenKey(winreg.HKEY_CURRENT_USER, sub_key) as key:
location = winreg.QueryValueEx(key, downloads_guid)[0]
return location
else:
if os.path.exists(os.path.join(os.path.expanduser('~'), 'Downloads')):
return os.path.join(os.path.expanduser('~'), 'Downloads')
except:
return ''
[docs]
def get_final_mass_list_gui(metadata):
"""Gets the mass list in displayable form for the GUI"""
mass_list = deepcopy(metadata['final_mass_list'])
output_table = []
if metadata['is_MS2']:
columns = ["Index", "Precursor m/z", "Fragment m/z"]
for i in range(1,len(mass_list)):
# add in fragment if needed
if len(mass_list[i])==3:
output_table.append([i, mass_list[i][0], ''])
elif len(mass_list[i])==4:
output_table.append([i, mass_list[i][0], mass_list[i][1]])
else:
columns = ["Index", "m/z"]
for i in range(1,len(mass_list)):
output_table.append([i, mass_list[i][0]])
if metadata['is_mobility']:
columns.append("Mobility")
for i in range(1,len(mass_list)):
if mass_list[i][-2]==0:
# add in fragment if needed
output_table[i-1].append('')
else:
output_table[i-1].append(mass_list[i][-2])
if any([i[-1] != 0 for i in mass_list][1:]):
columns.append("Polarity")
for i in range(1,len(mass_list)):
# convert polarity to symbol
pol = mass_list[i][-1]
if pol > 0:
output_table[i-1].append('+')
elif pol < 0:
output_table[i-1].append('-')
elif pol == 0:
output_table[i-1].append('')
return columns, output_table
[docs]
class MasterWindow(tk.Tk):
"""The main window of MSIGen.
Files and parameters are input here before running the data extraction workflow."""
def __init__(self):
super().__init__()
self.title("MSI Generator")
self.geometry("600x600") # Set size for the control window
self.protocol("WM_DELETE_WINDOW", self.destroy_all_windows)
self.rawfile_paths = tk.StringVar(value="")
self.mass_list_path = tk.StringVar(value="")
self.output_file_path = tk.StringVar(value="")
self.img_h = tk.StringVar(value="10")
self.img_w = tk.StringVar(value="10")
self.is_MS2_var = tk.IntVar()
self.is_mob_var = tk.IntVar()
self.scale = tk.DoubleVar(value=100)
self.threshold = tk.DoubleVar(value=0.)
# left frame
self.rawfiles_frame = tk.Frame(self)
self.rawfiles_frame.pack(side = tk.LEFT, fill=tk.BOTH, expand=True, padx=10, pady=10)
# selecting raw files
self.raw_files_label = tk.Label(self.rawfiles_frame, text = 'MS data files to use:')
self.raw_files_label.pack(side=tk.TOP, anchor = tk.W)
self.rawfiles_box = tk.Listbox(self.rawfiles_frame, selectmode=tk.EXTENDED)
self.rawfiles_box.pack(side=tk.TOP, fill=tk.BOTH, expand=True)
self.button_frame = tk.Frame(self.rawfiles_frame)
self.button_frame.pack(anchor=tk.CENTER)
self.open_explorer_button = MyButton(self.button_frame, text="Open New Files", command=self.open_file_explorer,width=12, height=1)
self.open_explorer_button.pack(side=tk.LEFT, padx=1, pady=5)
self.delete_rawfile_button = MyButton(self.button_frame, text="Delete", command=self.delete_selected_rawfiles,width=12, height=1)
self.delete_rawfile_button.pack(side=tk.LEFT, padx=1, pady=5)
# Selecting mass list file
self.mass_list_frame = tk.Frame(self.rawfiles_frame)
self.mass_list_frame.pack(fill=tk.BOTH, padx = 5)
self.mass_list_label = tk.Label(self.mass_list_frame, text = 'Transition list file:')
self.mass_list_label.pack(side=tk.TOP, anchor = tk.W)
self.mass_list_path_entry = tk.Entry(self.mass_list_frame, textvariable=self.mass_list_path)
self.mass_list_path_entry.pack(side=tk.TOP, fill=tk.BOTH, expand=True)
self.select_mass_list_button = MyButton(self.mass_list_frame, text="Select Mass List File", command=self.select_mass_file)
self.select_mass_list_button.pack()
# Selecting output directory
self.output_file_path_entry_frame = tk.Frame(self.rawfiles_frame)
self.output_file_path_entry_frame.pack(side=tk.BOTTOM, anchor = tk.S, fill=tk.BOTH, padx = 5)
self.output_file_path_entry_label = tk.Label(self.output_file_path_entry_frame, text = 'Output file directory:')
self.output_file_path_entry_label.pack(side=tk.TOP, anchor = tk.W)
self.output_file_path_entry_box = tk.Entry(self.output_file_path_entry_frame, textvariable=self.output_file_path)
self.output_file_path_entry_box.pack(side=tk.TOP, fill=tk.BOTH, expand=True)
self.output_file_path_entry_button = MyButton(self.output_file_path_entry_frame, text="Select Output Folder", command=self.select_output_file_path)
self.output_file_path_entry_button.pack()
# right frame
self.params_frame = tk.Frame(self)
self.params_frame.pack(side=tk.RIGHT, anchor = tk.N, fill=tk.BOTH, pady=(30,10))
# checkboxes for identifying ms2 and mobility data
self.checkbutton_frame = tk.Frame(self.params_frame)
self.checkbutton_frame.pack(side=tk.TOP)
self.is_MS2_ckbox = tk.Checkbutton(self.checkbutton_frame, text="Contains MS2 Data", variable = self.is_MS2_var, command = self.fill_param_box)
self.is_MS2_ckbox.pack(anchor=tk.W, padx = (0, 20))
self.is_MS2_ckbox.bind('<Return>', lambda event: self.toggle_checkbox(self.is_MS2_var, event))
self.is_mob_ckbox = tk.Checkbutton(self.checkbutton_frame, text="Contains Ion Mobility Data", variable = self.is_mob_var, command = self.fill_param_box)
self.is_mob_ckbox.pack(anchor=tk.W, padx = (0,20))
self.is_mob_ckbox.bind('<Return>', lambda event: self.toggle_checkbox(self.is_mob_var, event))
# tolerance values and image dimension parameters
self.parameters_txt_frame = tk.Frame(self.params_frame)
self.parameters_txt_frame.pack(side=tk.TOP, fill=tk.BOTH, pady = (10,0))
self.initialize_param_box()
self.fill_param_box()
# Run button
self.run_button_border = tk.Frame(self.params_frame, highlightbackground = "red",
highlightthickness = 2, bd=0)
self.run_workflow_button = MyButton(self.run_button_border, text="RUN", command=self.run_workflow, \
fg = 'red', font=(None, 20, 'bold'))
self.run_button_border.pack(side = tk.BOTTOM, anchor = tk.S, fill=tk.BOTH, padx = 10)
self.run_workflow_button.pack(side = tk.BOTTOM, anchor = tk.S, fill=tk.BOTH)
self.run_workflow_button.config(height = 1)
wheel = ttk.Progressbar(self, orient='horizontal')
[docs]
def destroy_all_windows(self):
try:
self.file_explorer.destroy()
except:
pass
self.destroy()
[docs]
def toggle_checkbox(self, checkbutton, event=None):
state = checkbutton.get()
checkbutton.set(not state)
[docs]
def select_output_file_path(self):
"""Opens a dialog box to select directory to save files to"""
self.output_file_path.set(filedialog.askdirectory())
[docs]
def initialize_param_box(self):
"""Sets up the box containing tolerances and image dimension inputs"""
self.tolerance_label = tk.Label(self.parameters_txt_frame, text='Tolerance values:')
self.tolerance_label.pack(anchor=tk.W)
self.tolerance_textboxes = [[None,None,None] for _ in range(4)] # Each entry corresponds to specific tolerance label, value, and unit entry
self.tolerance_parameter_labels = ["MS1 Mass", "Precursor Mass", "Fragment Mass", "Ion Mobility"] # Labels for the entry widgets
self.tolerance_default_values = ['10.0', '1.0', '10.0', '0.1']
self.tolerance_default_units = ['ppm','m/z','ppm','μs']
self.tolerance_allowed_units = [['ppm','m/z'],['ppm','m/z'],['ppm','m/z'],['μs','1/K0']]
self.tolerance_value = [tk.StringVar(self) for i in range(4)]
self.tolerance_units = [tk.StringVar(self) for i in range(4)]
self.tol_frames = [tk.Frame(self.parameters_txt_frame) for i in range(4)]
for i in range(len(self.tolerance_textboxes)):
value = self.tolerance_parameter_labels[i]
self.tolerance_textboxes[i][0] = tk.Label(self.tol_frames[i], text=value)
self.tolerance_value[i].set(self.tolerance_default_values[i])
self.tolerance_textboxes[i][1] = tk.Entry(self.tol_frames[i], textvariable=self.tolerance_value[i], width = 15)
self.tolerance_units[i].set(self.tolerance_default_units[i])
self.tolerance_textboxes[i][2] = ttk.OptionMenu(self.tol_frames[i], self.tolerance_units[i], self.tolerance_default_units[i], *self.tolerance_allowed_units[i])
# self.tolerance_textboxes[i][2] = tk.Entry(self.tol_frames[i], width = 5)
# self.tolerance_textboxes[i][2].insert(0, value)
self.img_dim_frames = tk.Frame(self.params_frame)
self.img_dim_frames.pack(side=tk.TOP, fill=tk.BOTH, pady = (10,10))
self.img_dim_label = tk.Label(self.img_dim_frames, text='Image dimensions (h x w):')
self.img_h_entry_box = tk.Entry(self.img_dim_frames, textvariable=self.img_h, width = 6)
self.unnecessary_x_label = tk.Label(self.img_dim_frames, text='x')
self.img_w_entry_box = tk.Entry(self.img_dim_frames, textvariable=self.img_w, width = 6)
self.img_dim_unit_label = tk.Label(self.img_dim_frames, text='mm')
self.img_dim_label.pack(anchor=tk.W)
self.img_h_entry_box.pack(side=tk.LEFT)
self.unnecessary_x_label.pack(side=tk.LEFT)
self.img_w_entry_box.pack(side=tk.LEFT)
self.img_dim_unit_label.pack(side=tk.LEFT)
[docs]
def fill_param_box(self, event = None):
# Remove any displayed widgets so widgets arent packed in a different order
for index, i in enumerate(self.tolerance_textboxes):
for j in i:
j.pack_forget()
self.tol_frames[index].pack_forget()
for index, i in enumerate(self.tolerance_textboxes):
if index in [1,2]:
if not self.is_MS2_var.get():
continue
elif index == 3:
if not self.is_mob_var.get():
continue
self.tol_frames[index].pack(anchor = tk.W)
i[0].pack(anchor=tk.W) # Pack label widget above
i[1].pack(side=tk.LEFT) # Pack entry value widget to the left
i[2].pack(side=tk.LEFT) # Pack entry unit widget to the left
[docs]
def select_mass_file(self):
filetypes = [("csv or Excel", "*.txt;*.csv;*.xlsx;*.xls"), ("All files", "*.*")]
file_path = filedialog.askopenfilename(filetypes=filetypes)
if file_path:
self.mass_list_path.set(file_path)
[docs]
def open_file_explorer(self):
self.open_explorer_button['state'] = 'disabled'
self.file_explorer = FileExplorerWindow(self.receive_raw_files)
self.file_explorer.mainloop()
[docs]
def receive_raw_files(self, raw_files):
# print("Received raw files:", raw_files.get())
self.rawfile_paths.set(raw_files.get())
for i in self.rawfile_paths.get().split('|'):
self.rawfiles_box.insert(tk.END, str(i))
self.rawfile_paths.set('|'.join(self.rawfiles_box.get(0, tk.END)))
self.open_explorer_button['state'] = 'normal'
[docs]
def delete_selected_rawfiles(self):
selected_indices = self.rawfiles_box.curselection()
for index in selected_indices[::-1]: # Iterate in reverse order to avoid index shifting
self.rawfiles_box.delete(index)
joined_paths = '|'.join(self.rawfiles_box.get(0, tk.END))
if joined_paths == '':
self.rawfiles_box.delete(0, tk.END)
self.rawfile_paths.set(joined_paths)
[docs]
def run_workflow(self):
try:
self.file_explorer.destroy()
self.open_explorer_button['state'] = 'normal'
except:
pass
try:
example_file, mass_list_dir, mass_tolerance_MS1, mass_tolerance_prec, mass_tolerance_frag, mobility_tolerance,\
mass_tolerance_MS1_units, mass_tolerance_prec_units, mass_tolerance_frag_units, mobility_tolerance_units,\
img_height, img_width, image_dimensions_units, is_MS2, is_mobility, normalize_img_sizes, output_file_loc,\
= self.get_input_vars()
print(example_file, mass_list_dir, mass_tolerance_MS1, mass_tolerance_prec, mass_tolerance_frag, mobility_tolerance)
print(mass_tolerance_MS1, mass_tolerance_prec, mass_tolerance_frag, mobility_tolerance)
print(mass_tolerance_MS1_units, mass_tolerance_prec_units, mass_tolerance_frag_units, mobility_tolerance_units)
self.MSIGen_generator = msigen(example_file=example_file, mass_list_dir=mass_list_dir, tol_MS1=mass_tolerance_MS1, \
tol_MS1_u=mass_tolerance_MS1_units, tol_prec=mass_tolerance_prec, tol_prec_u=mass_tolerance_prec_units, \
tol_frag=mass_tolerance_frag, tol_frag_u=mass_tolerance_frag_units, tol_mob=mobility_tolerance, \
tol_mob_u=mobility_tolerance_units, h=img_height, w=img_width, hw_units=image_dimensions_units, \
is_MS2=is_MS2, is_mobility=is_mobility, normalize_img_sizes=normalize_img_sizes, \
output_file_loc=output_file_loc, in_jupyter = False, testing = True)
# example_file, mass_list_dir, mass_tolerance_MS1, mass_tolerance_MS1_units, mass_tolerance_prec, \
# mass_tolerance_prec_units, mass_tolerance_frag, mass_tolerance_frag_units, mobility_tolerance, mobility_tolerance_units,\
# img_height, img_width, image_dimensions_units, is_MS2, is_mobility, normalize_img_sizes, output_file_loc, in_jupyter = False, testing = True)
self.open_progessbar_window()
self.results = {}
tkinter_widgets = [self.prog_bar, self.current_operation_label, self.prog_label]
self.MSIGen_process = Thread(target = self.MSIGen_generator.get_image_data, args = (), \
kwargs = {'verbose':False, \
'in_jupyter':False, \
'testing':False, \
'gui':True, \
'results':self.results, \
'tkinter_widgets':tkinter_widgets})
self.MSIGen_process.start()
self.monitor_progressbar()
except Exception as error:
tk.messagebox.showerror("Error", error)
finally:
# return button to normal state
self.run_workflow_button['state'] = 'normal'
[docs]
def open_progessbar_window(self):
self.progress_bar_window = tk.Toplevel(self)
self.progress_bar_window.attributes('-topmost', True)
self.progress_bar_window.geometry("300x200")
self.progress_bar_window.title("Extracting data...")
self.prog_bar = ttk.Progressbar(self.progress_bar_window, length = 250, orient='horizontal')
self.prog_bar.pack()
self.current_operation_label = tk.Label(self.progress_bar_window)
self.current_operation_label.pack()
self.prog_label = tk.Label(self.progress_bar_window)
self.prog_label.pack()
self.start_time = time()
[docs]
def monitor_progressbar(self):
try:
if self.MSIGen_process.is_alive():
# check again after 200ms (0.2s)
self.after(200, self.monitor_progressbar)
else:
t_tot = time()-self.start_time
t_min = t_tot//60
t_s = round(t_tot - (t_min*60), 2)
if t_min:
self.prog_label['text'] = f'Time elapsed: {t_min} min {t_s} s'
else:
self.prog_label['text'] = f'Time elapsed: {t_s} s'
self.metadata, self.pixels = self.results['metadata'], self.results['pixels']
self.current_operation_label['text'] = "Complete!"
self.progress_bar_window.protocol("WM_DELETE_WINDOW", self.open_image_maker)
self.continue_to_visualization_button = MyButton(self.progress_bar_window, text="Finish", command=self.open_image_maker, width=12, height=1)
self.continue_to_visualization_button.pack()
except Exception as error:
tk.messagebox.showerror("Error", error)
self.run_workflow_button['state'] = 'normal'
self.progress_bar_window.destroy()
[docs]
def open_image_maker(self):
"""Opens the window that contains all parameters needed to export images.
It includes 3 tabs:
1: For creating ion images
2: For creating fractional images
3: For creating ratio images
Images can be saved as figures (containing a title and colorbar), images, or arrays and can be saved using a selection of colormaps.
The brightness of the image can be scaled by a percentile or an absolute threshold.
The mass list can be viewed to obtain mass list entry indices."""
self.progress_bar_window.destroy()
self.withdraw()
self.image_maker_window = tk.Toplevel(self)
self.image_maker_window.protocol("WM_DELETE_WINDOW", self.reselect_raw_files)
self.notebook = ttk.Notebook(self.image_maker_window)
self.notebook.pack(fill=tk.BOTH, expand=True)
self.tab1 = ttk.Frame(self.notebook)
self.tab2 = ttk.Frame(self.notebook)
self.tab3 = ttk.Frame(self.notebook)
self.notebook.add(self.tab1, text='Ion Images')
self.notebook.add(self.tab2, text='Fractional Images')
self.notebook.add(self.tab3, text='Ratio Images')
# Tab 1: Ion images
self.normalization1_label = tk.Label(self.tab1, text = "Normalization method:")
self.normalization1_label.grid(row=0, column=0, sticky = "e")
self.dropdown_normalization1_var = tk.StringVar(value="None")
self.dropdown_normalization1 = ttk.OptionMenu(self.tab1, self.dropdown_normalization1_var, \
"None", *["None", "TIC", "Internal Standard"], command=self.show_or_hide_std_idx_entry)
self.dropdown_normalization1.grid(row=0, column=1, sticky = "w")
self.std_idx_var_label = tk.Label(self.tab1, text = "Index of internal standard from mass list:")
self.std_idx_var_label.grid(row=1, column=0, sticky = "e")
self.std_idx_var = tk.StringVar(value="1")
self.std_idx_entry = tk.Entry(self.tab1, textvariable=self.std_idx_var)
if self.dropdown_normalization1_var.get() == "Internal Standard":
self.std_idx_entry.grid(row=1, column=1, sticky = "ew")
self.choose_scale_threshold_label1 = tk.Label(self.tab1, text = "Reduce max intensity to a percentile or an absolute value?")
self.choose_scale_threshold_label1.grid(row=2, column=0, sticky = "e")
self.choose_scale_threshold_var1 = tk.StringVar(value="Percentile")
self.choose_scale_threshold_dropdown1 = ttk.OptionMenu(self.tab1, self.choose_scale_threshold_var1, "Percentile", *["Percentile", "Absolute"],\
command = lambda selection: self.scale_or_threshold_display(selection, self.scale_label1, self.scale_entry1, self.threshold_label1, self.threshold_entry1, 3))
self.choose_scale_threshold_dropdown1.grid(row=2, column=1, sticky = "w")
self.scale_label1 = tk.Label(self.tab1, text = "Adjust max intensity to this percentile:")
self.scale_stringvar = tk.StringVar(value=str(self.scale.get()))
self.scale_entry1 = tk.Entry(self.tab1, textvariable=self.scale_stringvar)
self.scale_label1.grid(row=3, column=0, sticky = "e")
self.scale_entry1.grid(row=3, column=1, sticky = "ew")
self.threshold_label1 = tk.Label(self.tab1, text = "Adjust max intensity to this value:")
self.threshold_stringvar = tk.StringVar(value="1")
self.threshold_entry1 = tk.Entry(self.tab1, textvariable=self.threshold_stringvar)
self.tab1.columnconfigure(0, weight=1, uniform = 'half')
self.tab1.columnconfigure(1, weight=1, uniform = 'half')
# Tab 2: Fractional Images
self.normalization2_label = tk.Label(self.tab2, text = "Normalization method:")
self.normalization2_label.grid(row=0, column=0, sticky = "e")
self.dropdown_normalization2_var = tk.StringVar(value="None")
self.dropdown_normalization2 = ttk.OptionMenu(self.tab2, self.dropdown_normalization2_var, "None", *["None", "Base Peak"])
self.dropdown_normalization2.grid(row=0, column=1, sticky = "w")
self.frac_img_idxs_label = tk.Label(self.tab2, text = "Indices of ions to use from mass list:")
self.frac_img_idxs_label.grid(row=1, column=0, sticky = "e")
self.frac_img_idxs_var = tk.StringVar(value="1, 2")
self.frac_img_idxs = tk.Entry(self.tab2, textvariable=self.frac_img_idxs_var)
self.frac_img_idxs.grid(row=1, column=1, sticky = "ew")
self.choose_scale_threshold_label2 = tk.Label(self.tab2, text = "Reduce max intensity to a percentile or an absolute value?")
self.choose_scale_threshold_label2.grid(row=2, column=0, sticky = "e")
self.choose_scale_threshold_var2 = tk.StringVar(value="Percentile")
self.choose_scale_threshold_dropdown2 = ttk.OptionMenu(self.tab2, self.choose_scale_threshold_var2, "Percentile", *["Percentile", "Absolute"],\
command = lambda selection: self.scale_or_threshold_display(selection, self.scale_label2, self.scale_entry2, self.threshold_label2, self.threshold_entry2, 3))
self.choose_scale_threshold_dropdown2.grid(row=2, column=1, sticky = "w")
self.scale_label2 = tk.Label(self.tab2, text = "Adjust max intensity to this quantile (0-1):")
self.scale_label2.grid(row=3, column=0, sticky = "e")
self.scale_entry2 = tk.Entry(self.tab2, textvariable=self.scale_stringvar)
self.scale_entry2.grid(row=3, column=1, sticky = "ew")
self.threshold_label2 = tk.Label(self.tab2, text = "Adjust max intensity to this value:")
self.threshold_entry2 = tk.Entry(self.tab2, textvariable=self.threshold_stringvar)
self.tab2.columnconfigure(0, weight=1, uniform = 'half')
self.tab2.columnconfigure(1, weight=1, uniform = 'half')
# Tab 3: Ratio Images
self.normalization3_label = tk.Label(self.tab3, text = "Normalization method:")
self.normalization3_label.grid(row=0, column=0, sticky = "e")
self.dropdown_normalization3_var = tk.StringVar(value="None")
self.dropdown_normalization3 = ttk.OptionMenu(self.tab3, self.dropdown_normalization3_var, "None", *["None", "Base Peak"])
self.dropdown_normalization3.grid(row=0, column=1, sticky = "w")
self.ratio_img_idxs_label = tk.Label(self.tab3, text = "Indices of ions to use from mass list:")
self.ratio_img_idxs_label.grid(row=1, column=0, sticky = "e")
self.ratio_img_idxs_var = tk.StringVar(value="1, 2")
self.ratio_img_idxs = tk.Entry(self.tab3, textvariable=self.ratio_img_idxs_var)
self.ratio_img_idxs.grid(row=1, column=1, sticky = "ew")
self.choose_scale_threshold_label3 = tk.Label(self.tab3, text = "Reduce max intensity to a percentile or an absolute value?")
self.choose_scale_threshold_label3.grid(row=2, column=0, sticky = "e")
self.choose_scale_threshold_var3 = tk.StringVar(value="Percentile")
self.choose_scale_threshold_dropdown3 = ttk.OptionMenu(self.tab3, self.choose_scale_threshold_var3, "Percentile", *["Percentile", "Absolute"],\
command = lambda selection: self.scale_or_threshold_display(selection, self.scale_label3, self.scale_entry3, self.threshold_label3, self.threshold_entry3, 3))
self.choose_scale_threshold_dropdown3.grid(row=2, column=1, sticky = "w")
self.scale_label3 = tk.Label(self.tab3, text = "Adjust max intensity to this quantile (0-1):")
self.scale_label3.grid(row=3, column=0, sticky = "e")
self.scale_entry3 = tk.Entry(self.tab3, textvariable=self.scale_stringvar)
self.scale_entry3.grid(row=3, column=1, sticky = "ew")
self.threshold_label3 = tk.Label(self.tab3, text = "Adjust max intensity to this value:")
self.threshold_entry3 = tk.Entry(self.tab3, textvariable=self.threshold_stringvar)
self.handle_infinity_method_label = tk.Label(self.tab3, text = "How to handle divide by zero errors:")
self.handle_infinity_method_label.grid(row=4, column=0, sticky = "e")
self.handle_infinity_method_var = tk.StringVar(value="Maximum")
self.dropdown_handle_infinity_method = ttk.OptionMenu(self.tab3, self.handle_infinity_method_var, "Maximum", *['Maximum', 'Infinity', 'Zero'])
self.dropdown_handle_infinity_method.grid(row=4, column=1, sticky = "w")
self.log_scale_var = tk.IntVar()
self.log_scale_ckbtn = tk.Checkbutton(self.tab3, text="Use log-scale for intensity", variable = self.log_scale_var)
self.log_scale_ckbtn.grid(row = 5, column = 0, columnspan = 2)
self.tab3.columnconfigure(0, weight=1, uniform = 'half')
self.tab3.columnconfigure(1, weight=1, uniform = 'half')
# Parameters applying to any image type
self.general_img_params_frame = tk.Frame(self.image_maker_window)
self.general_img_params_frame.pack(fill="x", expand=True, padx = 10)
self.dropdown_colormap_label = tk.Label(self.general_img_params_frame, text = "Colormap to use:")
self.dropdown_colormap_label.grid(row=0, column=0, sticky = "e", padx = 5)
self.dropdown_colormap_var = tk.StringVar(value="viridis")
self.dropdown_colormap = ttk.OptionMenu(self.general_img_params_frame, self.dropdown_colormap_var, "viridis", *["viridis", "cividis", "hot", "jet", "seismic"])
self.dropdown_colormap.grid(row=0, column=1, sticky = "w", padx = 5)
self.dropdown_savetype_label = tk.Label(self.general_img_params_frame, text = "Save images as:")
self.dropdown_savetype_label.grid(row=1, column=0, sticky = "e", padx = 5)
self.dropdown_savetype_var = tk.StringVar(value="figure")
self.dropdown_savetype = ttk.OptionMenu(self.general_img_params_frame, self.dropdown_savetype_var, "figure", *["figure", "image", "array"])
self.dropdown_savetype.grid(row=1, column=1, sticky = "w", padx = 5)
self.output_file_path_label2 = tk.Label(self.general_img_params_frame, text = "Path to save images to:")
self.output_file_path_label2.grid(row=2, column=0, sticky = "w", padx = 5)
self.output_file_path_entry_box2 = tk.Entry(self.general_img_params_frame, textvariable=self.output_file_path)
self.output_file_path_entry_box2.grid(row=2, column=1, sticky = "ew", padx = 5)
self.output_file_path_entry_button2 = MyButton(self.general_img_params_frame, text="Reselect Output Folder", command=self.select_output_file_path)
self.output_file_path_entry_button2.grid(row=3, column=1, sticky = "w", padx = 5)
self.general_img_params_frame.columnconfigure(1, weight=1, uniform = 'half')
self.img_maker_buttons_frame = tk.Frame(self.image_maker_window)
self.img_maker_buttons_frame.pack()
self.generate_images_button = MyButton(self.img_maker_buttons_frame, text = 'Generate Images', command = self.generate_images)
self.generate_images_button.pack(side = tk.LEFT, padx = 5)
self.view_mass_list_button = MyButton(self.img_maker_buttons_frame, text = 'View Mass List', command = self.display_mass_list)
self.view_mass_list_button.pack(side = tk.LEFT, padx = 5)
self.reselect_raw_files_button = MyButton(self.img_maker_buttons_frame, text = 'Reselect Raw Files', command = self.reselect_raw_files)
self.reselect_raw_files_button.pack(side = tk.LEFT, padx = 5)
# self.end_all_button = MyButton(self.img_maker_buttons_frame, text = 'Finish')
# self.end_all_button.pack(side = tk.LEFT)
[docs]
def generate_images(self):
"""Exports images based on the active tab and inputted parameters"""
active_nb_pg = self.notebook.tab(self.notebook.select(),"text")
if active_nb_pg == "Ion Images":
scale, threshold = self.get_scale_threshold_values(self.choose_scale_threshold_var1, \
self.scale_stringvar, self.threshold_stringvar)
print(scale, threshold)
std_idx = int(self.std_idx_var.get())
print(f'std_idx: {std_idx}')
if self.dropdown_normalization1_var.get() == 'Internal Standard':
try:
assert std_idx > 0
except:
std_idx = 0
error_message = "The index of the internal standard must be a single positive integer."
tk.messagebox.showerror("Internal standard index error", error_message)
else:
std_idx = 1
if std_idx:
pixels_normed = vis.get_pixels_to_display(self.pixels, self.metadata, normalize = self.dropdown_normalization1_var.get(), std_idx = std_idx)
vis.display_images(pixels_normed, self.metadata, MSI_data_output=self.output_file_path.get(), cmap=self.dropdown_colormap_var.get(),\
threshold=threshold, scale=scale, save_imgs=True, image_savetype=self.dropdown_savetype_var.get())
self.open_images_were_saved_dialog()
elif active_nb_pg == "Fractional Images":
scale, threshold = self.get_scale_threshold_values(self.choose_scale_threshold_var2, \
self.scale_stringvar, self.threshold_stringvar)
try:
idxs_list = [int(i) for i in self.frac_img_idxs_var.get().split(',')]
assert all([i>=0 for i in idxs_list])
except:
idxs_list = []
error_message = "The indices given must be positive integers separated by a ','"
tk.messagebox.showerror("Index error", error_message)
if idxs_list:
fract_imgs = vis.get_fractional_abundance_imgs(self.pixels, self.metadata, idxs = idxs_list, \
normalize = self.dropdown_normalization2_var.get())
vis.display_fractional_images(fract_imgs, self.metadata, save_imgs = True, MSI_data_output = self.output_file_path.get(), \
cmap = self.dropdown_colormap_var.get(), image_savetype=self.dropdown_savetype_var.get(), scale=scale, threshold=threshold)
self.open_images_were_saved_dialog()
elif active_nb_pg == "Ratio Images":
scale, threshold = self.get_scale_threshold_values(self.choose_scale_threshold_var3, \
self.scale_stringvar, self.threshold_stringvar)
try:
idxs_list = [int(i) for i in self.ratio_img_idxs_var.get().split(',')]
assert all([i>0 for i in idxs_list]) and (len(idxs_list) == 2)
except:
idxs_list = []
error_message = "The indices given must be two positive integers separated by a ','."
tk.messagebox.showerror("Index error", error_message)
if idxs_list:
ratio_imgs = vis.get_ratio_imgs(self.pixels, self.metadata, idxs = idxs_list, \
normalize = self.dropdown_normalization3_var.get(), handle_infinity = self.handle_infinity_method_var.get())
vis.display_ratio_images(ratio_imgs, self.metadata, save_imgs = True, \
MSI_data_output = self.output_file_path.get(), cmap = self.dropdown_colormap_var.get(),\
log_scale = bool(self.log_scale_var.get()), scale=scale, threshold=threshold, \
image_savetype=self.dropdown_savetype_var.get())
self.open_images_were_saved_dialog()
[docs]
def open_images_were_saved_dialog(self):
"""A window that contains a hyperlink to the folder the images were exported to."""
self.images_were_saved_dialog = tk.Toplevel(self.image_maker_window)
self.images_were_saved_dialog.minsize(200,100)
self.images_were_saved_dialog.protocol("WM_DELETE_WINDOW", self.images_were_saved_dialog.destroy)
self.saved_imgs_label1 = tk.Label(self.images_were_saved_dialog, text="Your files were saved to:")
self.saved_imgs_label1.pack(side = tk.TOP)
self.saved_imgs_label2 = tk.Label(self.images_were_saved_dialog, text=self.output_file_path.get()+'/images', fg="blue", cursor="hand2")
self.saved_imgs_label2.bind("<Button-1>", self.open_hyperlink)
self.saved_imgs_label2.pack(side = tk.TOP)
[docs]
def open_hyperlink(self, *args):
os.startfile(self.output_file_path.get()+'/images')
[docs]
def display_mass_list(self):
columns, output_table = get_final_mass_list_gui(self.metadata)
self.mass_list_window = tk.Toplevel(self)
self.mass_list_tree = ttk.Treeview(self.mass_list_window, columns = columns, show="headings")
self.mass_list_tree.pack(fill="both", expand=True)
for heading in columns:
self.mass_list_tree.heading(heading, text = heading)
for row in output_table:
self.mass_list_tree.insert("","end", values=tuple(row))
[docs]
def reselect_raw_files(self):
"""Goes back to file selection screen. All progress will be lost."""
self.image_maker_window.destroy()
self.deiconify()
[docs]
def show_or_hide_std_idx_entry(self, *args):
"""Hides the std_idx entrybox when intl_std normalization is not selected"""
if self.dropdown_normalization1_var.get() == "Internal Standard":
self.std_idx_var_label.grid(row=1, column=0, sticky = "e")
self.std_idx_entry.grid(row=1, column=1, sticky = "ew")
else:
self.std_idx_var_label.grid_forget()
self.std_idx_entry.grid_forget()
[docs]
def scale_or_threshold_display(self, selection, scale_label, scale_entry, threshold_label, threshold_entry, row):
"""Toggles the display between percentile and threshold depending on currently selected dropdown value"""
if selection == "Percentile":
scale_label.grid(row=row, column=0, sticky = "e")
scale_entry.grid(row=row, column=1, sticky = "ew")
threshold_label.grid_forget()
threshold_entry.grid_forget()
else:
threshold_label.grid(row=row, column=0, sticky = "e")
threshold_entry.grid(row=row, column=1, sticky = "ew")
scale_label.grid_forget()
scale_entry.grid_forget()
[docs]
def get_scale_threshold_values(self, dropdown_menu_var, scale_stringvar, threshold_stringvar):
"""Gets the appropriate threshold or percentile to scale the image intensity to for later use."""
if dropdown_menu_var.get() == "Percentile":
scale = scale_stringvar.get()
threshold = None
try:
scale = float(scale)/100
except:
scale = 1
else:
scale = 1
threshold = threshold_stringvar.get()
try:
threshold = float(threshold)
except:
threshold = None
return scale, threshold
# TODO make this easier to use.
[docs]
class FileExplorerWindow(tk.Tk):
"""File explorer that allows .d data to be treated as files rather than folders."""
def __init__(self, callback):
super().__init__()
self.title("Insert Listbox Example")
self.geometry("400x300") # Set a fixed size for the window
self.callback = callback
self.selected_items = tk.StringVar(value="")
self.selected_drive = tk.StringVar(value="")
self.raw_files = tk.StringVar(value="")
self.current_directory = Path.cwd()
## TODO implement a good redo and undo for file navigation
# self.movement_history = tk.StringVar(value="")
# self.movement_future = tk.StringVar(value="")
self.drives = win32api.GetLogicalDriveStrings()
self.drives = self.drives.split('\000')[:-1]
self.protocol("WM_DELETE_WINDOW", self.close_raw_file_selection_window)
self.add_listboxes()
[docs]
def get_current_directory_contents(self):
"""Gets the current files and folders in the selected directory for display"""
self.listbox.delete(0, tk.END)
self.dir_contents = []
self.textbox_contents.set(str(self.current_directory))
if self.dropdown_var.get() == "all files":
for i in self.current_directory.glob('*'):
if not i.name.startswith(('__','~','.','$')):
self.dir_contents.append(i.name)
else:
for i in self.current_directory.glob('*'):
if not i.name.startswith(('__','~','.','$')):
if i.is_dir():
self.dir_contents.append(i.name)
else:
if i.name.lower().endswith(('.d','.mzml','.raw')):
self.dir_contents.append(i.name)
for i in self.dir_contents:
self.listbox.insert(tk.END, str(i))
[docs]
def add_listboxes(self):
"""Makes a box on the left side of the window that contains the commonly used directories such as:
Drive letters, Downloads, Desktop, etc.
for easier navigation"""
self.listbox_frame = tk.Frame(self)
self.listbox_frame.pack(fill=tk.BOTH, expand=True)
self.listbox_driveletters = tk.Listbox(self.listbox_frame, selectmode=tk.EXTENDED, width=20) # Set width for the left listbox
self.listbox_driveletters.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
self.fill_listbox_driveletters()
self.listbox_driveletters.bind("<Double-Button-1>", self.on_double_click_drives)
# self.listbox_driveletters.bind("<ButtonRelease-1>", self.get_selected_drive_values)
# self.listbox_driveletters.bind("<Return>", self.on_return)
self.listbox = tk.Listbox(self.listbox_frame, selectmode=tk.EXTENDED)
self.listbox.pack(side=tk.BOTTOM, fill=tk.BOTH, expand=True)
self.listbox.bind("<Double-Button-1>", self.on_double_click)
self.listbox.bind("<ButtonRelease-1>", self.get_selected_values)
self.listbox.bind("<KeyPress-BackSpace>", self.move_to_parent_dir)
self.listbox.bind("<Return>", self.on_return)
self.textbox_frame = tk.Frame(self.listbox_frame)
self.textbox_frame.pack(side=tk.TOP, fill=tk.X, padx = 1)
self.back_arrow_button = MyButton(self.textbox_frame, text="\u2190", command=self.move_to_parent_dir)
self.back_arrow_button.pack(side = tk.RIGHT)
self.textbox_contents = tk.StringVar(self.listbox_frame, value=self.current_directory)
self.textbox = tk.Entry(self.textbox_frame, textvariable=self.textbox_contents)
self.textbox.pack(side=tk.LEFT, fill=tk.X)
self.textbox.bind("<Configure>", self.on_textbox_resize)
self.textbox.bind("<Return>", self.on_textbox_return)
# Adding the button
self.select_button = MyButton(self, text="Select files", command=self.on_return)
self.select_button.pack(side=tk.RIGHT, padx=10, pady=5)
# Adding the dropdown menu
self.dropdown_var = tk.StringVar(self)
self.dropdown_var.set(".d, .mzML, or .raw files") # Set default value
self.dropdown_var.trace_add("write", self.on_dropdown_change)
self.dropdown_menu = ttk.OptionMenu(self, self.dropdown_var, ".d, .mzML, or .raw files", *[".d, .mzML, or .raw files", "all files"])
self.dropdown_menu.pack(side=tk.RIGHT, padx=10, pady=5)
self.get_current_directory_contents()
[docs]
def on_dropdown_change(self, *args):
"""Allows the user to not display unselectable files."""
selected_option = self.dropdown_var.get()
self.get_current_directory_contents()
[docs]
def on_textbox_resize(self, event):
"""Adjust the Text widget width dynamically with Listbox.
Allows for proper resizing of the box displaying the currently selected directory"""
listbox_width = self.listbox.winfo_width()
self.textbox.config(width=listbox_width)
[docs]
def on_textbox_return(self, event):
"""Goes to directory typed into the textbox or selects file if it is a file."""
text = self.textbox_contents.get() # Retrieve text from the textbox
text = text.replace('"','')
if Path(text).is_dir():
if Path(text).name.lower().endswith('.d'):
self.raw_files.set("|".join(text.split('|')))
self.close_raw_file_selection_window()
else:
self.current_directory = Path(text)
self.get_current_directory_contents()
elif Path(text).name.lower().endswith(('.d','.raw','.mzml')):
self.raw_files.set("|".join(text.split('|')))
self.close_raw_file_selection_window()
# print("Textbox content:", text)
[docs]
def on_return(self, event=None):
"""Allows for navigation with Return instead of the mouse"""
self.get_selected_values(event)
# check if all files are
use_as_files = True
for i in self.selected_items.get().split('|'):
i = Path(self.current_directory, i)
if not i.name.lower().endswith(('.d','.mzml','.raw',)):
use_as_files = False
if use_as_files:
# get list of raw files as a '|' dileniated string
self.raw_files.set("|".join([str(Path(self.current_directory, i)) for i in self.selected_items.get().split('|')]))
self.close_raw_file_selection_window()
elif Path(self.current_directory, self.selected_items.get().split('|')[0]).is_dir():
self.current_directory = Path(self.current_directory, self.selected_items.get().split('|')[0])
# clear then update contents
self.get_current_directory_contents()
self.get_selected_values(event)
[docs]
def on_double_click(self, event):
"""Opens folder or selects files."""
self.get_selected_values(event)
if len(self.selected_items.get().split('|')) == 1:
# check file name extension
if self.selected_items.get().lower().endswith(('.d','.mzml','.raw',)):
self.raw_files.set(str(Path(self.current_directory, self.selected_items.get())))
self.close_raw_file_selection_window()
elif Path(self.current_directory, self.selected_items.get()).is_dir():
self.current_directory = Path(self.current_directory, self.selected_items.get())
# clear then update contents
self.get_current_directory_contents()
self.get_selected_values(event)
[docs]
def get_selected_values(self, event=None):
selected_indices = self.listbox.curselection()
selected_values = [self.listbox.get(index) for index in selected_indices]
self.selected_items.set("|".join(selected_values))
[docs]
def move_to_parent_dir(self, event=None):
self.current_directory = self.current_directory.parent
# clear then update contents
self.listbox.delete(0, tk.END)
self.get_current_directory_contents()
self.get_selected_values(event)
# =============================================
# for the listbox containing drive letters
# =============================================
[docs]
def fill_listbox_driveletters(self):
self.listbox_driveletters.delete(0, tk.END)
self.listbox_driveletters.insert(tk.END, Path.home().name)
self.listbox_driveletters.insert(tk.END, 'User Desktop')
self.listbox_driveletters.insert(tk.END, 'Public Desktop')
self.listbox_driveletters.insert(tk.END, 'Downloads')
self.listbox_driveletters.insert(tk.END, 'Documents')
self.dir_shortcuts = {'User': Path.home(),
'User Desktop': Path.home() / 'Desktop',
'Public Desktop': Path.home().parent/'Public/Desktop',
'Downloads': get_download_path(),
'Documents': Path.home() / 'Documents',
}
for i in self.drives:
drive_name = f'({i})'
self.listbox_driveletters.insert(tk.END, drive_name)
self.dir_shortcuts[drive_name] = i
[docs]
def get_selected_drive_values(self, event):
selected_indices = self.listbox_driveletters.curselection()
selected_drive = [self.listbox_driveletters.get(index) for index in selected_indices]
self.selected_drive.set("|".join(selected_drive))
[docs]
def on_double_click_drives(self, event):
"""Opens a drive when clicked"""
self.get_selected_drive_values(event)
drive = self.selected_drive.get()
if len(drive.split('|')) == 1:
if drive in self.dir_shortcuts.keys():
self.selected_drive.set(self.dir_shortcuts[drive])
self.current_directory = Path(self.selected_drive.get())
self.get_current_directory_contents()
self.get_selected_values(event)
[docs]
def close_raw_file_selection_window(self):
self.callback(self.raw_files) # Pass the raw_files string to the callback function
self.destroy()
[docs]
def run_GUI():
"""Runs the MSIGen GUI"""
app = MasterWindow()
app.mainloop()
# Runs the GUI if this file is run
if __name__ == "__main__":
app = MasterWindow()
app.lift()
app.attributes('-topmost',True)
app.after_idle(app.attributes,'-topmost',False)
app.mainloop()