Source code for spacr.gui

import tkinter as tk
from tkinter import ttk
from multiprocessing import set_start_method
from .gui_elements import spacrButton, spacrCard, create_menu_bar, set_dark_style
from .gui_core import initiate_root
from screeninfo import get_monitors
import webbrowser

[docs] class MainApp(tk.Tk): """Top-level spacr GUI window hosting the app-picker and per-app frames. Sizes itself to the monitor containing the initial window position, registers the built-in core and extra tool apps, and optionally auto-loads one on start. :param default_app: Name of a core or additional app to open immediately. """ def __init__(self, default_app=None): super().__init__() # Enable font smoothing on all platforms self._setup_font_rendering() # Initialize the window self.geometry("100x100") self.update_idletasks() # Get the current window position self.update_idletasks() x = self.winfo_x() y = self.winfo_y() # Find the monitor where the window is located for monitor in get_monitors(): if monitor.x <= x < monitor.x + monitor.width and monitor.y <= y < monitor.y + monitor.height: width = monitor.width self.width = width height = monitor.height break else: monitor = get_monitors()[0] width = monitor.width height = monitor.height # Set the window size to the dimensions of the monitor where it is located self.geometry(f"{width}x{height}") self.title("SpaCr GUI Collection") self.configure(bg='#333333') style = ttk.Style()
[docs] self.color_settings = set_dark_style(style, parent_frame=self)
[docs] self.main_buttons = {}
[docs] self.additional_buttons = {}
[docs] self.main_gui_apps = { "Mask": (lambda frame: initiate_root(self, 'mask'), "Generate cellpose masks for cells, nuclei and pathogen images."), "Measure": (lambda frame: initiate_root(self, 'measure'), "Measure single object intensity and morphological feature. Crop and save single object image"), "Annotate": (lambda frame: initiate_root(self, 'annotate'), "Annotation single object images on a grid. Annotations are saved to database."), "Make Masks": (lambda frame: initiate_root(self, 'make_masks'), "Adjust pre-existing Cellpose models to your specific dataset for improved performance"), "Classify": (lambda frame: initiate_root(self, 'classify'), "Train Torch Convolutional Neural Networks (CNNs) or Transformers to classify single object images."), }
[docs] self.additional_gui_apps = { "Umap": (lambda frame: initiate_root(self, 'umap'), "Generate UMAP embeddings with datapoints represented as images."), "Train Cellpose": (lambda frame: initiate_root(self, 'train_cellpose'), "Train custom Cellpose models."), "ML Analyze": (lambda frame: initiate_root(self, 'ml_analyze'), "Machine learning analysis of data."), "Cellpose Masks": (lambda frame: initiate_root(self, 'cellpose_masks'), "Generate Cellpose masks."), "Cellpose All": (lambda frame: initiate_root(self, 'cellpose_all'), "Run Cellpose on all images."), "Map Barcodes": (lambda frame: initiate_root(self, 'map_barcodes'), "Map barcodes to data."), "Regression": (lambda frame: initiate_root(self, 'regression'), "Perform regression analysis."), "Recruitment": (lambda frame: initiate_root(self, 'recruitment'), "Analyze recruitment data."), "Activation": (lambda frame: initiate_root(self, 'activation'), "Generate activation maps of computer vision models and measure channel-activation correlation."), "Plaque": (lambda frame: initiate_root(self, 'analyze_plaques'), "Analyze plaque data.") }
[docs] self.selected_app = tk.StringVar()
self.create_widgets() if default_app in self.main_gui_apps: self.load_app(default_app, self.main_gui_apps[default_app][0]) elif default_app in self.additional_gui_apps: self.load_app(default_app, self.additional_gui_apps[default_app][0]) def _setup_font_rendering(self): """Apply platform-appropriate default fonts for crisp text rendering.""" import platform system = platform.system() if system == 'Linux': # Enable subpixel anti-aliasing self.option_add('*Font', 'OpenSans 12') self.tk.call('tk', 'scaling', self.winfo_fpixels('1i') / 72.0) try: self.option_add('*TkDefaultFont', 'OpenSans 12') self.tk.eval(""" option add *font {OpenSans 12} font configure TkDefaultFont -family OpenSans -size 12 font configure TkTextFont -family OpenSans -size 12 font configure TkMenuFont -family OpenSans -size 12 font configure TkFixedFont -family monospace -size 11 """) except Exception: pass elif system == 'Darwin': try: self.tk.eval(""" font configure TkDefaultFont -family {Open Sans} -size 13 font configure TkTextFont -family {Open Sans} -size 13 font configure TkMenuFont -family {Open Sans} -size 13 """) except Exception: pass elif system == 'Windows': try: self.tk.eval(""" font configure TkDefaultFont -family {Open Sans} -size 10 font configure TkTextFont -family {Open Sans} -size 10 font configure TkMenuFont -family {Open Sans} -size 10 """) except Exception: pass
[docs] def create_widgets(self): """Build the root layout containers and render the startup screen.""" create_menu_bar(self) # Use a grid layout for centering self.grid_rowconfigure(0, weight=1) self.grid_columnconfigure(0, weight=1) self.content_frame = tk.Frame(self) self.content_frame.grid(row=0, column=0, sticky="nsew") # Center the content frame within the window self.content_frame.grid_rowconfigure(0, weight=1) self.content_frame.grid_columnconfigure(0, weight=1) self.inner_frame = tk.Frame(self.content_frame) self.inner_frame.grid(row=0, column=0) set_dark_style(ttk.Style(), containers=[self.content_frame, self.inner_frame]) self.create_startup_screen()
def _update_wraplength(self, event): """Resize the description label wrap width in response to layout changes.""" if self.description_label.winfo_exists(): # Use the actual width of the inner_frame as a proxy for full width available_width = self.inner_frame.winfo_width() if available_width > 0: self.description_label.config(wraplength=int(available_width * 0.9)) # or 0.9
[docs] def create_startup_screen(self): """Render the app-picker view with core and additional tool buttons.""" self.clear_frame(self.inner_frame) # Pull the shared palette + typography scale so the startup screen # matches the rest of the modernised GUI. style_out = set_dark_style(ttk.Style()) bg = style_out['bg_color'] fg = style_out['fg_color'] muted = style_out.get('muted_color', fg) spacing = style_out.get('spacing', {'sm': 8, 'md': 12, 'lg': 16, 'xl': 24}) font_sizes = style_out.get('font_sizes', {'small': 11, 'body': 12, 'header': 14, 'title': 18}) font_loader = style_out.get('font_loader') def _font(size_key, weight="normal"): """Return the tk font tuple (or loaded font) for the given size key.""" size = font_sizes.get(size_key, style_out['font_size']) if font_loader: return font_loader.get_font(size=size) return (style_out['font_family'], size, weight) # --- Title block: "SpaCR" + subtitle ---------------------------- title_frame = tk.Frame(self.inner_frame, bg=bg) title_frame.pack(pady=(spacing['xl'], spacing['md'])) tk.Label( title_frame, text="SpaCR", font=_font('title'), bg=bg, fg=fg, cursor="hand2", ).pack() subtitle = tk.Label( title_frame, text="Spatial single-cell analysis for microscopy", font=_font('small'), bg=bg, fg=muted, ) subtitle.pack(pady=(spacing['xs'], 0)) # --- Core applications card ------------------------------------- main_card = spacrCard(self.inner_frame, title="Core applications", padding='md') main_card.pack(pady=(spacing['md'], spacing['sm']), padx=spacing['lg'], anchor='center') # Logo button (opens the tutorial) sits leftmost in the core row. logo_button = spacrButton( main_card.body, text="SpaCR", command=lambda: webbrowser.open_new( "https://einarolafsson.github.io/spacr/tutorial/"), icon_name="logo_spacr", size=90, show_text=False, ) logo_button.grid(row=0, column=0, padx=spacing['sm'], pady=spacing['sm']) self.main_buttons[logo_button] = ( "spaCR: spatial single-cell analysis tools for microscopy data. " "Click to open the tutorial. (under construction)" ) for i, (app_name, app_data) in enumerate(self.main_gui_apps.items()): app_func, app_desc = app_data button = spacrButton( main_card.body, text=app_name, command=lambda app_name=app_name, app_func=app_func: self.load_app(app_name, app_func), icon_name=app_name.lower(), size=90, show_text=False, ) button.grid(row=0, column=i + 1, padx=spacing['sm'], pady=spacing['sm']) self.main_buttons[button] = app_desc # --- Additional tools card -------------------------------------- extra_card = spacrCard(self.inner_frame, title="Additional tools", padding='md') extra_card.pack(pady=(spacing['sm'], spacing['md']), padx=spacing['lg'], anchor='center') for i, (app_name, app_data) in enumerate(self.additional_gui_apps.items()): app_func, app_desc = app_data button = spacrButton( extra_card.body, text=app_name, command=lambda app_name=app_name, app_func=app_func: self.load_app(app_name, app_func), icon_name=app_name.lower(), size=66, show_text=False, ) button.grid(row=0, column=i, padx=spacing['sm'], pady=spacing['sm']) self.additional_buttons[button] = app_desc # --- Description panel (bottom, muted text) --------------------- description_frame = tk.Frame(self.inner_frame, bg=bg) description_frame.pack(fill=tk.X, pady=(spacing['sm'], spacing['xl']), padx=spacing['xl']) description_frame.columnconfigure(0, weight=1) self.description_label = tk.Label( description_frame, text="", wraplength=int(self.width * 0.7), justify="center", font=_font('body'), fg=muted, bg=bg, ) self.description_label.pack(pady=spacing['sm']) self.description_label.configure(width=int(self.width * 0.5 // 7)) self.description_label.pack_configure(anchor='center') self.update_description() self.inner_frame.bind("<Configure>", self._update_wraplength)
[docs] def update_description(self): """Show the description for the currently highlighted app button.""" for button, desc in {**self.main_buttons, **self.additional_buttons}.items(): if button.canvas.itemcget(button.button_bg, "fill") == self.color_settings['active_color']: self.show_description(desc) return self.clear_description()
[docs] def show_description(self, description): """Set the bottom description label text. :param description: Text to display beneath the app buttons. """ if self.description_label.winfo_exists(): self.description_label.config(text=description) self.description_label.update_idletasks()
[docs] def clear_description(self): """Blank the description label.""" if self.description_label.winfo_exists(): self.description_label.config(text="") self.description_label.update_idletasks()
[docs] def load_app(self, app_name, app_func): """Replace the current frame contents with the selected app. :param app_name: Human-readable app label (used for icon/title lookup). :param app_func: Callable that renders the app UI into a supplied frame. """ self.clear_frame(self.inner_frame) app_frame = tk.Frame(self.inner_frame) app_frame.pack(fill=tk.BOTH, expand=True) set_dark_style(ttk.Style(), containers=[app_frame]) app_func(app_frame)
[docs] def clear_frame(self, frame): """Destroy every child widget of ``frame``. :param frame: Tk container whose children should be removed. """ for widget in frame.winfo_children(): widget.destroy()
#def gui_app(): # app = MainApp() # app.mainloop()
[docs] def gui_app(): """Launch the spacr GUI as a standalone application. Calls ``freeze_support`` first so the app runs correctly under ``multiprocessing``-frozen executables. """ from multiprocessing import freeze_support freeze_support() app = MainApp() app.mainloop()
if __name__ == "__main__": set_start_method('spawn', force=True) gui_app()