From 5cb3170254b23053efd473346ce5f26f6c0540e8 Mon Sep 17 00:00:00 2001 From: Steffen Hirtle Date: Wed, 22 Nov 2023 19:01:03 +0100 Subject: [PATCH 01/17] Fixed error occuring when GraXpert was started without internet connection --- graxpert/help_panel.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/graxpert/help_panel.py b/graxpert/help_panel.py index be703fe..aa96a19 100644 --- a/graxpert/help_panel.py +++ b/graxpert/help_panel.py @@ -326,7 +326,9 @@ def scaling_change(): remote_versions = list_remote_versions() local_versions = list_local_versions() ai_options = set([]) - ai_options.update([rv["version"] for rv in remote_versions]) + + if (remote_versions is not None): + ai_options.update([rv["version"] for rv in remote_versions]) ai_options.update(set([lv["version"] for lv in local_versions])) ai_options = sorted(ai_options, key=lambda k: version.parse(k), reverse=True) From f009ef1db6ae5ae81cdbb2a0f4f16cc2f8e413c8 Mon Sep 17 00:00:00 2001 From: David Schmelter Date: Thu, 23 Nov 2023 18:54:05 +0100 Subject: [PATCH 02/17] start migration from PyInstaller to cx_freeze --- requirements.txt | 2 +- setup.py | 82 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 83 insertions(+), 1 deletion(-) create mode 100644 setup.py diff --git a/requirements.txt b/requirements.txt index 64c9b40..90c980a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,7 +3,7 @@ astropy hdpitkinter minio ml_dtypes -numpy +numpy<=1.24.3,>=1.22 Pillow pykrige requests diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..f97b5be --- /dev/null +++ b/setup.py @@ -0,0 +1,82 @@ +from cx_Freeze import Executable, setup +from graxpert.version import version +import astropy +import sys +import os + +libs_prefix = os.path.commonprefix([astropy.__file__, __file__]) +astropy_path = os.path.join( + "./", os.path.dirname(os.path.relpath(astropy.__file__, libs_prefix)) +) + +directory_table = [ + ("ProgramMenuFolder", "TARGETDIR", "."), + ("GraXpert", "ProgramMenuFolder", "GraXpert"), +] + +msi_data = { + "Directory": directory_table, + "ProgId": [ + ("Prog.Id", None, None, "GraXpert is an astronomical image processing program for extracting and removing gradients in the background of your astrophotos", "IconId", None), + ], + "Icon": [ + ("IconId", "./img/Icon.ico"), + ], +} + +bdist_msi_options = { + "add_to_path": True, + "data": msi_data, + "upgrade_code": "{8887032b-9211-4752-8f88-6d29833bb001}", + "target_name": "GraXpert", + "install_icon": "./img/Icon.ico" +} + +build_options = { + # "packages": ["astropy"], + "includes": [ + "astropy.constants.codata2018", + "astropy.constants.iau2015", + "imageio.plugins.pillow", + "skimage.draw.draw", + "skimage.exposure.exposure", + "skimage.filters._gaussian", + ], + "include_files": [ + ["./img", "./lib/img"], + ["./forest-dark.tcl", "./lib/forest-dark.tcl"], + ["./forest-dark/", "./lib/forest-dark/"], + ["./locales/", "./lib/locales/"], + [ + os.path.join(astropy_path, "units", "format", "generic_parsetab.py"), + "./lib/astropy/units/format/generic_parsetab.py", + ], + [ + os.path.join(astropy_path, "units", "format", "generic_lextab.py"), + "./lib/astropy/units/format/generic_lextab.py", + ], + ], + "excludes": [], + "include_msvcr": True, +} + +base = "Win32GUI" if sys.platform == "win32" else None + +executables = [ + Executable( + "./graxpert/main.py", + base=base, + icon="./img/Icon.ico", + shortcut_name="GraXpert {}".format(version), + shortcut_dir="GraXpert", + target_name="GraXpert" + ) +] + +setup( + name="GraXpert", + version=version, + description="GraXpert is an astronomical image processing program for extracting and removing gradients in the background of your astrophotos", + executables=executables, + options={"build_exe": build_options, "bdist_msi": bdist_msi_options}, +) From 2c7137d67515432a1243b87287f113a2752f008d Mon Sep 17 00:00:00 2001 From: David Schmelter Date: Sat, 25 Nov 2023 14:44:03 +0100 Subject: [PATCH 03/17] Add support for building .rpm and .deb packages --- graxpert/gui.py | 102 ++++++++++++++++--------------------- graxpert/help_panel.py | 17 +++---- graxpert/loadingframe.py | 10 +--- graxpert/localization.py | 7 +-- graxpert/resource_utils.py | 38 ++++++++++++++ setup.py | 35 ++++++++++--- 6 files changed, 121 insertions(+), 88 deletions(-) create mode 100644 graxpert/resource_utils.py diff --git a/graxpert/gui.py b/graxpert/gui.py index f9c942e..6277d11 100644 --- a/graxpert/gui.py +++ b/graxpert/gui.py @@ -1,5 +1,3 @@ -from graxpert.mp_logging import initialize_logging, shutdown_logging, logfile_name - import importlib import logging import os @@ -12,37 +10,35 @@ import numpy as np from appdirs import user_config_dir from PIL import Image, ImageTk -from skimage import io -from skimage.transform import resize import graxpert.background_extraction as background_extraction import graxpert.tooltip as tooltip +from graxpert.ai_model_handling import (ai_model_path_from_version, + download_version, + validate_local_version) from graxpert.app_state import INITIAL_STATE from graxpert.astroimage import AstroImage from graxpert.collapsible_frame import CollapsibleFrame -from graxpert.slider import Slider -from graxpert.commands import (ADD_POINT_HANDLER, ADD_POINTS_HANDLER, INIT_HANDLER, - MOVE_POINT_HANDLER, RESET_POINTS_HANDLER, - RM_POINT_HANDLER, SEL_POINTS_HANDLER, Command, - InitHandler) +from graxpert.commands import (ADD_POINT_HANDLER, ADD_POINTS_HANDLER, + INIT_HANDLER, MOVE_POINT_HANDLER, + RESET_POINTS_HANDLER, RM_POINT_HANDLER, + SEL_POINTS_HANDLER, Command, InitHandler) from graxpert.help_panel import Help_Panel -from graxpert.loadingframe import LoadingFrame, DynamicProgressFrame, DynamicProgressThread +from graxpert.loadingframe import (DynamicProgressFrame, DynamicProgressThread, + LoadingFrame) from graxpert.localization import _ +from graxpert.mp_logging import (initialize_logging, logfile_name, + shutdown_logging) from graxpert.parallel_processing import executor -from graxpert.preferences import (app_state_2_prefs, load_preferences, - prefs_2_app_state, save_preferences, - app_state_2_fitsheader, fitsheader_2_app_state) +from graxpert.preferences import (app_state_2_fitsheader, app_state_2_prefs, + fitsheader_2_app_state, load_preferences, + prefs_2_app_state, save_preferences) +from graxpert.resource_utils import (resource_path, scale_img, + temp_resource_path) +from graxpert.slider import Slider from graxpert.stretch import stretch_all from graxpert.ui_scaling import get_scaling_factor -from graxpert.version import release, version, check_for_new_version -from graxpert.ai_model_handling import (validate_local_version, download_version, - ai_model_path_from_version) - - -def resource_path(relative_path): - """ Get absolute path to resource, works for dev and for PyInstaller """ - base_path = getattr(sys, '_MEIPASS', os.path.join(os.path.dirname(os.path.abspath(__file__)), "..")) - return os.path.join(base_path, relative_path) +from graxpert.version import check_for_new_version, release, version class Application(tk.Frame): @@ -197,7 +193,7 @@ def create_widget(self): self.bgextr_menu.sub_frame.grid_rowconfigure(i, weight=1) #---Open Image--- - num_pic = ImageTk.PhotoImage(file=resource_path("img/gfx_number_1-scaled.png")) + num_pic = ImageTk.PhotoImage(file=temp_resource_path("img/gfx_number_1-scaled.png")) text = tk.Label(self.bgextr_menu.sub_frame, text=_(" Loading"), image=num_pic, font=heading_font, compound="left") text.image = num_pic text.grid(column=0, row=0, pady=(20*scal,5*scal), padx=0, sticky="w") @@ -211,7 +207,7 @@ def create_widget(self): self.load_image_button.grid(column=0, row=1, pady=(5*scal,30*scal), padx=15*scal, sticky="news") #--Stretch Options-- - num_pic = ImageTk.PhotoImage(file=resource_path("img/gfx_number_2-scaled.png")) + num_pic = ImageTk.PhotoImage(file=temp_resource_path("img/gfx_number_2-scaled.png")) text = tk.Label(self.bgextr_menu.sub_frame, text=_(" Stretch Options"), image=num_pic, font=heading_font, compound="left") text.image = num_pic text.grid(column=0, row=2, pady=5*scal, padx=0, sticky="w") @@ -235,7 +231,7 @@ def create_widget(self): #---Sample Selection--- - num_pic = ImageTk.PhotoImage(file=resource_path("img/gfx_number_3-scaled.png")) + num_pic = ImageTk.PhotoImage(file=temp_resource_path("img/gfx_number_3-scaled.png")) text = tk.Label(self.bgextr_menu.sub_frame, text=_(" Sample Selection"), image=num_pic, font=heading_font, compound="left") text.image = num_pic text.grid(column=0, row=5, pady=5*scal, padx=0, sticky="w") @@ -284,7 +280,7 @@ def create_widget(self): tt_reset= tooltip.Tooltip(self.reset_button, text=tooltip.reset_text) #---Calculation--- - num_pic = ImageTk.PhotoImage(file=resource_path("img/gfx_number_4-scaled.png")) + num_pic = ImageTk.PhotoImage(file=temp_resource_path("img/gfx_number_4-scaled.png")) text = tk.Label(self.bgextr_menu.sub_frame, text=_(" Calculation"), image=num_pic, font=heading_font, compound="left") text.image = num_pic text.grid(column=0, row=12, pady=5*scal, padx=0, sticky="w") @@ -319,7 +315,7 @@ def create_widget(self): tt_calculate= tooltip.Tooltip(self.calculate_button, text=tooltip.calculate_text) #---Saving--- - num_pic = ImageTk.PhotoImage(file=resource_path("img/gfx_number_5-scaled.png")) + num_pic = ImageTk.PhotoImage(file=temp_resource_path("img/gfx_number_5-scaled.png")) self.saveas_text = tk.Label(self.bgextr_menu.sub_frame, text=_(" Saving"), image=num_pic, font=heading_font, compound="left") self.saveas_text.image = num_pic self.saveas_text.grid(column=0, row=17, pady=5*scal, padx=0, sticky="w") @@ -1180,42 +1176,34 @@ def on_closing(self, logging_thread): root.destroy() sys.exit(0) -def scale_img(path, scaling, shape): - img = io.imread(resource_path(path)) - img = resize(img, (int(shape[0]*scaling),int(shape[1]*scaling))) - img = img*255 - img = img.astype(dtype=np.uint8) - io.imsave(resource_path(resource_path(path.replace('.png', '-scaled.png'))), img, check_contrast=False) - - logging_thread = initialize_logging() root = hdpitk.HdpiTk() scaling = get_scaling_factor() -scale_img(resource_path("forest-dark/vert-hover.png"), scaling*0.9, (20,10)) -scale_img(resource_path("forest-dark/vert-basic.png"), scaling*0.9, (20,10)) - -scale_img(resource_path("forest-dark/thumb-hor-accent.png"), scaling*0.9, (20,8)) -scale_img(resource_path("forest-dark/thumb-hor-hover.png"), scaling*0.9, (20,8)) -scale_img(resource_path("forest-dark/thumb-hor-basic.png"), scaling*0.9, (20,8)) -scale_img(resource_path("forest-dark/scale-hor.png"), scaling, (20,20)) - -scale_img(resource_path("forest-dark/check-accent.png"), scaling*0.8, (20,20)) -scale_img(resource_path("forest-dark/check-basic.png"), scaling*0.8, (20,20)) -scale_img(resource_path("forest-dark/check-hover.png"), scaling*0.8, (20,20)) -scale_img(resource_path("forest-dark/check-unsel-accent.png"), scaling*0.8, (20,20)) -scale_img(resource_path("forest-dark/check-unsel-basic.png"), scaling*0.8, (20,20)) -scale_img(resource_path("forest-dark/check-unsel-hover.png"), scaling*0.8, (20,20)) -scale_img(resource_path("forest-dark/check-unsel-pressed.png"), scaling*0.8, (20,20)) - -scale_img(resource_path("img/gfx_number_1.png"), scaling*0.7, (25,25)) -scale_img(resource_path("img/gfx_number_2.png"), scaling*0.7, (25,25)) -scale_img(resource_path("img/gfx_number_3.png"), scaling*0.7, (25,25)) -scale_img(resource_path("img/gfx_number_4.png"), scaling*0.7, (25,25)) -scale_img(resource_path("img/gfx_number_5.png"), scaling*0.7, (25,25)) -scale_img(resource_path("img/hourglass.png"), scaling, (25,25)) +scale_img("forest-dark/vert-hover.png", scaling*0.9, (20,10)) +scale_img("forest-dark/vert-basic.png", scaling*0.9, (20,10)) + +scale_img("forest-dark/thumb-hor-accent.png", scaling*0.9, (20,8)) +scale_img("forest-dark/thumb-hor-hover.png", scaling*0.9, (20,8)) +scale_img("forest-dark/thumb-hor-basic.png", scaling*0.9, (20,8)) +scale_img("forest-dark/scale-hor.png", scaling, (20,20)) + +scale_img("forest-dark/check-accent.png", scaling*0.8, (20,20)) +scale_img("forest-dark/check-basic.png", scaling*0.8, (20,20)) +scale_img("forest-dark/check-hover.png", scaling*0.8, (20,20)) +scale_img("forest-dark/check-unsel-accent.png", scaling*0.8, (20,20)) +scale_img("forest-dark/check-unsel-basic.png", scaling*0.8, (20,20)) +scale_img("forest-dark/check-unsel-hover.png", scaling*0.8, (20,20)) +scale_img("forest-dark/check-unsel-pressed.png", scaling*0.8, (20,20)) + +scale_img("img/gfx_number_1.png", scaling*0.7, (25,25)) +scale_img("img/gfx_number_2.png", scaling*0.7, (25,25)) +scale_img("img/gfx_number_3.png", scaling*0.7, (25,25)) +scale_img("img/gfx_number_4.png", scaling*0.7, (25,25)) +scale_img("img/gfx_number_5.png", scaling*0.7, (25,25)) +scale_img("img/hourglass.png", scaling, (25,25)) root.tk.call("source", resource_path("forest-dark.tcl")) style = ttk.Style(root) diff --git a/graxpert/help_panel.py b/graxpert/help_panel.py index be703fe..ce218c7 100644 --- a/graxpert/help_panel.py +++ b/graxpert/help_panel.py @@ -11,16 +11,11 @@ from graxpert.ai_model_handling import (list_local_versions, list_remote_versions) from graxpert.localization import _, lang +from graxpert.resource_utils import resource_path, temp_resource_path from graxpert.slider import Slider from graxpert.ui_scaling import get_scaling_factor -def resource_path(relative_path): - """ Get absolute path to resource, works for dev and for PyInstaller """ - base_path = path.abspath(path.join(path.dirname(__file__), "../")) - - return path.join(base_path, relative_path) - class Help_Panel(): def __init__(self, master, canvas, app): @@ -135,7 +130,7 @@ def __init__(self, master, canvas, app): text = tk.Message(self.help_panel_window, text=_("Instructions"), width=240 * scaling, font=heading_font, anchor="center") text.grid(column=0, row=1, padx=(40,30), pady=(0,10*scaling), sticky="ew") - num_pic = ImageTk.PhotoImage(file=resource_path("img/gfx_number_1-scaled.png")) + num_pic = ImageTk.PhotoImage(file=temp_resource_path("img/gfx_number_1-scaled.png")) text = tk.Label(self.help_panel_window, text=_(" Loading"), image=num_pic, compound="left", font=heading_font2) text.image = num_pic text.grid(column=0, row=2, padx=(40,30), pady=(5*scaling,0), sticky="w") @@ -143,7 +138,7 @@ def __init__(self, master, canvas, app): text.grid(column=0, row=3, padx=(40,30), pady=(5*scaling,10*scaling), sticky="w") - num_pic = ImageTk.PhotoImage(file=resource_path("img/gfx_number_2-scaled.png")) + num_pic = ImageTk.PhotoImage(file=temp_resource_path("img/gfx_number_2-scaled.png")) text = tk.Label(self.help_panel_window, text=_(" Stretch Options"), image=num_pic, compound="left", font=heading_font2) text.image = num_pic text.grid(column=0, row=4, padx=(40,30), pady=(5*scaling,0), sticky="w") @@ -151,7 +146,7 @@ def __init__(self, master, canvas, app): text.grid(column=0, row=5, padx=(40,30), pady=(5*scaling,10*scaling), sticky="w") - num_pic = ImageTk.PhotoImage(file=resource_path("img/gfx_number_3-scaled.png")) + num_pic = ImageTk.PhotoImage(file=temp_resource_path("img/gfx_number_3-scaled.png")) text = tk.Label(self.help_panel_window, text=_(" Sample Selection"), image=num_pic, compound="left", font=heading_font2) text.image = num_pic text.grid(column=0, row=6, padx=(40,30), pady=(5*scaling,0), sticky="w") @@ -164,7 +159,7 @@ def __init__(self, master, canvas, app): text.grid(column=0, row=7, padx=(40,30), pady=(5*scaling,10*scaling), sticky="w") - num_pic = ImageTk.PhotoImage(file=resource_path("img/gfx_number_4-scaled.png")) + num_pic = ImageTk.PhotoImage(file=temp_resource_path("img/gfx_number_4-scaled.png")) text = tk.Label(self.help_panel_window, text=_(" Calculation"), image=num_pic, compound="left", font=heading_font2) text.image = num_pic text.grid(column=0, row=8, padx=(40,30), pady=(5*scaling,0), sticky="w") @@ -172,7 +167,7 @@ def __init__(self, master, canvas, app): text.grid(column=0, row=9, padx=(40,30), pady=(5*scaling,10*scaling), sticky="w") - num_pic = ImageTk.PhotoImage(file=resource_path("img/gfx_number_5-scaled.png")) + num_pic = ImageTk.PhotoImage(file=temp_resource_path("img/gfx_number_5-scaled.png")) text = tk.Label(self.help_panel_window, text=_(" Saving"), image=num_pic, compound="left", font=heading_font2) text.image = num_pic text.grid(column=0, row=10, padx=(40,30), pady=(5*scaling,0), sticky="w") diff --git a/graxpert/loadingframe.py b/graxpert/loadingframe.py index d7d31ad..12d78fc 100644 --- a/graxpert/loadingframe.py +++ b/graxpert/loadingframe.py @@ -9,13 +9,7 @@ from PIL import ImageTk from graxpert.localization import _ - - -def resource_path(relative_path): - """Get absolute path to resource, works for dev and for PyInstaller""" - base_path = path.abspath(path.join(path.dirname(__file__), "../")) - - return path.join(base_path, relative_path) +from graxpert.resource_utils import temp_resource_path class LoadingFrame: @@ -24,7 +18,7 @@ def __init__(self, widget, toplevel): self.toplevel = toplevel hourglass_pic = ImageTk.PhotoImage( - file=resource_path("img/hourglass-scaled.png") + file=temp_resource_path("img/hourglass-scaled.png") ) self.text = ttk.Label( widget, diff --git a/graxpert/localization.py b/graxpert/localization.py index 7f6f558..5214cd9 100644 --- a/graxpert/localization.py +++ b/graxpert/localization.py @@ -4,14 +4,9 @@ import os from appdirs import user_config_dir from graxpert.preferences import load_preferences +from graxpert.resource_utils import resource_path -def resource_path(relative_path): - """ Get absolute path to resource, works for dev and for PyInstaller """ - base_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "../")) - - return os.path.join(base_path, relative_path) - prefs_file = os.path.join(user_config_dir(appname="GraXpert"), "preferences.json") prefs = load_preferences(prefs_file) diff --git a/graxpert/resource_utils.py b/graxpert/resource_utils.py new file mode 100644 index 0000000..3a30765 --- /dev/null +++ b/graxpert/resource_utils.py @@ -0,0 +1,38 @@ +# from appdirs import +import os +import sys +from tempfile import TemporaryDirectory + +import numpy as np +from skimage import io +from skimage.transform import resize + +temp_resource_dir = TemporaryDirectory() + + +def resource_path(relative_path): + if getattr(sys, "frozen", False): + # The application is frozen + base_path = os.path.join(os.path.dirname(sys.executable), "lib") + else: + # The application is not frozen + base_path = os.path.join(os.path.dirname(__file__), "..") + return os.path.join(base_path, relative_path) + + +def temp_resource_path(relative_path): + return os.path.join(temp_resource_dir.name, relative_path) + + +def scale_img(relative_path, scaling, shape): + os.makedirs(os.path.dirname(temp_resource_path(relative_path)), exist_ok=True) + + img = io.imread(resource_path(relative_path)) + img = resize(img, (int(shape[0] * scaling), int(shape[1] * scaling))) + img = img * 255 + img = img.astype(dtype=np.uint8) + io.imsave( + temp_resource_path(relative_path.replace(".png", "-scaled.png")), + img, + check_contrast=False, + ) diff --git a/setup.py b/setup.py index f97b5be..2c4af3b 100644 --- a/setup.py +++ b/setup.py @@ -1,5 +1,5 @@ from cx_Freeze import Executable, setup -from graxpert.version import version +from graxpert.version import version, release import astropy import sys import os @@ -17,23 +17,41 @@ msi_data = { "Directory": directory_table, "ProgId": [ - ("Prog.Id", None, None, "GraXpert is an astronomical image processing program for extracting and removing gradients in the background of your astrophotos", "IconId", None), + ( + "Prog.Id", + None, + None, + "GraXpert is an astronomical image processing program for extracting and removing gradients in the background of your astrophotos", + "IconId", + None, + ), ], "Icon": [ ("IconId", "./img/Icon.ico"), ], } +msi_summary_data = { + "author": "GraXpert Development Team", + "comments": "", +} + bdist_msi_options = { "add_to_path": True, "data": msi_data, + "summary_data": msi_summary_data, "upgrade_code": "{8887032b-9211-4752-8f88-6d29833bb001}", "target_name": "GraXpert", - "install_icon": "./img/Icon.ico" + "install_icon": "./img/Icon.ico", +} + +bidst_rpm_options = { + "release": release, + "vendor": "GraXpert Development Team ", + "group": "Unspecified", } build_options = { - # "packages": ["astropy"], "includes": [ "astropy.constants.codata2018", "astropy.constants.iau2015", @@ -67,9 +85,9 @@ "./graxpert/main.py", base=base, icon="./img/Icon.ico", + target_name="GraXpert", shortcut_name="GraXpert {}".format(version), shortcut_dir="GraXpert", - target_name="GraXpert" ) ] @@ -78,5 +96,10 @@ version=version, description="GraXpert is an astronomical image processing program for extracting and removing gradients in the background of your astrophotos", executables=executables, - options={"build_exe": build_options, "bdist_msi": bdist_msi_options}, + options={ + "build_exe": build_options, + "bdist_msi": bdist_msi_options, + "bdist_rpm": bidst_rpm_options, + }, + license="GLP-3.0", ) From cf01f6a23f9d56ace4108c613d09b58819b10fcf Mon Sep 17 00:00:00 2001 From: Steffen Hirtle Date: Sun, 10 Dec 2023 17:03:56 +0100 Subject: [PATCH 04/17] New parameters for command line tool for saving the background model and setting the output file name --- graxpert/CommandLineTool.py | 85 +++++++++++++++++++++++++++---------- graxpert/main.py | 18 ++++++++ 2 files changed, 80 insertions(+), 23 deletions(-) diff --git a/graxpert/CommandLineTool.py b/graxpert/CommandLineTool.py index f5c28ec..fabdf48 100644 --- a/graxpert/CommandLineTool.py +++ b/graxpert/CommandLineTool.py @@ -17,10 +17,39 @@ def __init__(self, args): self.args = args def execute(self): - astroImage = AstroImage(do_update_display=False) - astroImage.set_from_file(self.args.filename) - processedAstroImage = AstroImage(do_update_display=False) + astro_Image = AstroImage(do_update_display=False) + astro_Image.set_from_file(self.args.filename) + + processed_Astro_Image = AstroImage(do_update_display=False) + background_Astro_Image = AstroImage(do_update_display=False) + + processed_Astro_Image.fits_header = astro_Image.fits_header + background_Astro_Image.fits_header = astro_Image.fits_header + + ai_version = self.get_ai_version() + background_Astro_Image.set_from_array( + extract_background( + astro_Image.img_array, + [], + "AI", + self.args.smoothing, + 1, + 50, + "RBF", + 0, + self.args.correction, + ai_model_path_from_version(ai_version), + ) + ) + processed_Astro_Image.set_from_array(astro_Image.img_array) + + processed_Astro_Image.save(self.get_save_path(), self.get_output_file_format()) + if (self.args.bg): + background_Astro_Image.save(self.get_background_save_path(), self.get_output_file_format()) + + + def get_ai_version(self): prefs_filename = os.path.join( user_config_dir(appname="GraXpert"), "preferences.json" ) @@ -54,23 +83,33 @@ def execute(self): prefs["ai_version"] = ai_version save_preferences(prefs_filename, prefs) - - processedAstroImage.set_from_array( - extract_background( - astroImage.img_array, - [], - "AI", - self.args.smoothing, - 1, - 50, - "RBF", - 0, - self.args.correction, - ai_model_path_from_version(ai_version), - ) - ) - - saveDir = os.path.splitext(self.args.filename)[0] + "_GraXpert.fits" - - astroImage.save(saveDir, "32 bit Fits") - return + + return ai_version + + def get_output_file_ending(self): + file_ending = os.path.splitext(self.args.filename)[-1] + + if file_ending.lower() == ".xisf": + return ".xisf" + else: + return ".fits" + + def get_output_file_format(self): + output_file_ending = self.get_output_file_ending() + if (output_file_ending) == ".xisf": + return "32 bit XISF" + else: + return "32 bit Fits" + + def get_save_path(self): + if (self.args.output is not None): + base_path = os.path.dirname(self.args.filename) + output_file_name = self.args.output + self.get_output_file_ending() + return os.path.join(base_path, output_file_name) + + else: + return os.path.splitext(self.args.filename)[0] + "_GraXpert" + self.get_output_file_ending() + + def get_background_save_path(self): + save_path = self.get_save_path() + return os.path.splitext(save_path)[0] + "_background" + self.get_output_file_ending() diff --git a/graxpert/main.py b/graxpert/main.py index 85cf4a5..d11d2c6 100644 --- a/graxpert/main.py +++ b/graxpert/main.py @@ -110,6 +110,24 @@ def main(): type=float, help="Strength of smoothing between 0 and 1", ) + + parser.add_argument( + "-output", + "--output", + nargs="?", + required=False, + type=str, + help="Filename of the processed image", + ) + + parser.add_argument( + "-bg", + "--bg", + required=False, + action="store_true", + help="Also save the background model", + ) + args = parser.parse_args() From 8191f5f349c6d7e6b8b06226cc31fd93ecf9c9a5 Mon Sep 17 00:00:00 2001 From: David Schmelter Date: Sun, 10 Dec 2023 18:46:00 +0100 Subject: [PATCH 05/17] prepare build of msi packages on Github infrastructure --- .github/workflows/build-release.yml | 37 +++++++---------------------- graxpert/gui.py | 10 +++++++- setup.py | 5 +--- 3 files changed, 19 insertions(+), 33 deletions(-) diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml index f3ab93a..cda93aa 100644 --- a/.github/workflows/build-release.yml +++ b/.github/workflows/build-release.yml @@ -9,7 +9,7 @@ env: on: push: tags: - - v*.*.* + - "*.*.*" jobs: @@ -37,6 +37,7 @@ jobs: chmod u+x ./releng/patch_version.sh && \ ./releng/patch_version.sh - name: create GraXpert-linux bundle + # TODO migrato to cx_freeze run: | pyinstaller \ ./GraXpert-linux.spec \ @@ -56,12 +57,6 @@ jobs: python-version: '3.10' - name: checkout repository uses: actions/checkout@v3 - - name: checkout pyinstaller - uses: actions/checkout@v3 - with: - repository: pyinstaller/pyinstaller - path: ./pyinstaller - ref: v6.1.0 - name: configure ai s3 secrets run: | $PSDefaultParameterValues['Out-File:Encoding']='UTF8' ; ` @@ -71,32 +66,17 @@ jobs: "bucket_name = `"$env:ai_s3_bucket_name`"" | Out-File -Append .\graxpert\s3_secrets.py - name: install dependencies run: | - pip install setuptools wheel ; ` - cd .\pyinstaller\bootloader ; ` - (Get-Content .\wscript) -Replace "'run'", "'graxpert'" | Set-Content .\wscript ; ` - (Get-Content .\src\main.c) -Replace 'pyi_main\(', 'my_pyi_main(' | Set-Content .\src\main.c ; ` - (Get-Content .\src\pyi_main.h) -Replace 'pyi_main\(', 'my_pyi_main(' | Set-Content .\src\pyi_main.h ; ` - (Get-Content .\src\pyi_main.c) -Replace 'pyi_main\(', 'my_pyi_main(' | Set-Content .\src\pyi_main.c ; ` - python ./waf all ; ` - cd .. ; ` - (Get-Content .\setup.py) -Replace 'run.exe', 'graxpert.exe' | Set-Content .\setup.py ; ` - pushd ; ` - python setup.py sdist ; ` - popd ; ` - pip install ./dist/pyinstaller-6.1.0.tar.gz ; ` - cd .. ; ` + pip install setuptools wheel cx_freeze; ` pip install -r requirements.txt - name: patch version run: ./releng/patch_version.ps1 - name: create GraXpert-win64 bundle - run: | - pyinstaller ` - ./GraXpert-win64.spec ` + run: python ./setup.py bdist_msi - name: store artifacts uses: actions/upload-artifact@v2 with: - name: GraXpert-win64.exe - path: ./dist/GraXpert-win64.exe + name: GraXpert-${{github.ref_name}}-win64.msi + path: ./dist/GraXpert-${{github.ref_name}}-win64.msi retention-days: 5 build-macos-x86_64: @@ -126,6 +106,7 @@ jobs: chmod u+x ./releng/patch_version.sh && \ ./releng/patch_version.sh - name: create GraXpert-macos-x86_64 bundle + # TODO migrato to cx_freeze run: | pyinstaller \ ./GraXpert-macos-x86_64.spec @@ -160,7 +141,7 @@ jobs: - name: download windows exe uses: actions/download-artifact@v2 with: - name: GraXpert-win64.exe + name: GraXpert-${{github.ref_name}}-win64.msi - name: download macos artifacts uses: actions/download-artifact@v2 with: @@ -170,5 +151,5 @@ jobs: with: files: | GraXpert-linux - GraXpert-win64.exe + GraXpert-${{github.ref_name}}-win64.msi GraXpert-macos-x86_64.dmg diff --git a/graxpert/gui.py b/graxpert/gui.py index 6277d11..bb58ff0 100644 --- a/graxpert/gui.py +++ b/graxpert/gui.py @@ -1,6 +1,7 @@ import importlib import logging import os +import shutil import sys import tkinter as tk from colorsys import hls_to_rgb @@ -1182,6 +1183,13 @@ def on_closing(self, logging_thread): root = hdpitk.HdpiTk() scaling = get_scaling_factor() +try: + shutil.copy(resource_path("forest-dark.tcl"), temp_resource_path("forest-dark.tcl")) + shutil.copytree(resource_path("forest-dark"), temp_resource_path("forest-dark")) +except OSError as exc: + logging.exception("Error preparing temporary ressource, exiting") + sys.exit(1) + scale_img("forest-dark/vert-hover.png", scaling*0.9, (20,10)) scale_img("forest-dark/vert-basic.png", scaling*0.9, (20,10)) @@ -1205,7 +1213,7 @@ def on_closing(self, logging_thread): scale_img("img/gfx_number_5.png", scaling*0.7, (25,25)) scale_img("img/hourglass.png", scaling, (25,25)) -root.tk.call("source", resource_path("forest-dark.tcl")) +root.tk.call("source", temp_resource_path("forest-dark.tcl")) style = ttk.Style(root) style.theme_use("forest-dark") style.configure("TButton", padding=(8*scaling, 12*scaling, 8*scaling, 12*scaling)) diff --git a/setup.py b/setup.py index 2c4af3b..e558237 100644 --- a/setup.py +++ b/setup.py @@ -4,10 +4,7 @@ import sys import os -libs_prefix = os.path.commonprefix([astropy.__file__, __file__]) -astropy_path = os.path.join( - "./", os.path.dirname(os.path.relpath(astropy.__file__, libs_prefix)) -) +astropy_path = os.path.dirname(os.path.abspath(astropy.__file__)) directory_table = [ ("ProgramMenuFolder", "TARGETDIR", "."), From 0bb4e394f2606dcb274b9824a7452cd4dd703c0d Mon Sep 17 00:00:00 2001 From: Steffen Hirtle Date: Sun, 17 Dec 2023 12:21:10 +0100 Subject: [PATCH 06/17] Added parameter -cli to specify command line integration usage --- graxpert/main.py | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/graxpert/main.py b/graxpert/main.py index d11d2c6..107fba0 100644 --- a/graxpert/main.py +++ b/graxpert/main.py @@ -128,13 +128,26 @@ def main(): help="Also save the background model", ) + parser.add_argument( + "-cli", + "--cli", + required=False, + action="store_true", + help="Has to be added when using the command line integration of GraXpert", + ) + args = parser.parse_args() - - from graxpert.CommandLineTool import CommandLineTool - - clt = CommandLineTool(args) - clt.execute() + + if (args.cli): + + from graxpert.CommandLineTool import CommandLineTool + + clt = CommandLineTool(args) + clt.execute() + else: + import graxpert.gui + else: import graxpert.gui From 0aa9df6a41a7e79a935c29b77af535026b0645f1 Mon Sep 17 00:00:00 2001 From: Steffen Hirtle Date: Sun, 17 Dec 2023 12:34:55 +0100 Subject: [PATCH 07/17] Added parameter -v for version output --- graxpert/main.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/graxpert/main.py b/graxpert/main.py index 107fba0..8549bc8 100644 --- a/graxpert/main.py +++ b/graxpert/main.py @@ -18,6 +18,7 @@ from graxpert.ai_model_handling import (list_local_versions, list_remote_versions) +from graxpert.version import release as graxpert_release, version as graxpert_version available_local_versions = [] available_remote_versions = [] @@ -78,6 +79,7 @@ def main(): parser = argparse.ArgumentParser( description="GraXpert,the astronomical background extraction tool" ) + parser.add_argument("filename", type=str, help="Path of the unprocessed image") parser.add_argument( "-ai_version", @@ -136,13 +138,18 @@ def main(): help="Has to be added when using the command line integration of GraXpert", ) + parser.add_argument( + '-v', + '--version', + action='version', + version="GraXpert version: " + graxpert_version + " release: " + graxpert_release) + args = parser.parse_args() + if (args.cli): - from graxpert.CommandLineTool import CommandLineTool - clt = CommandLineTool(args) clt.execute() else: From 547e8d83f746f1281132778df2f6eae2af531df5 Mon Sep 17 00:00:00 2001 From: Steffen Hirtle Date: Fri, 22 Dec 2023 16:23:33 +0100 Subject: [PATCH 08/17] Only show errors for tensorflow logging --- graxpert/background_extraction.py | 1 + 1 file changed, 1 insertion(+) diff --git a/graxpert/background_extraction.py b/graxpert/background_extraction.py index ef9bf1c..7ee564e 100644 --- a/graxpert/background_extraction.py +++ b/graxpert/background_extraction.py @@ -18,6 +18,7 @@ import os import tensorflow as tf os.environ['CUDA_VISIBLE_DEVICES'] = '-1' # Run tensorflow on CPU +tf.compat.v1.logging.set_verbosity(40) # Only show errors from graxpert.mp_logging import get_logging_queue, worker_configurer from graxpert.parallel_processing import executor From 358401d3754a791682b09aed70be777bbe9fd875 Mon Sep 17 00:00:00 2001 From: David Date: Wed, 20 Dec 2023 20:32:53 +0100 Subject: [PATCH 09/17] initial commit on ui-refactoring - use customtkinter - decouple ui components - introduce simple eventbus to decouple ui components and application code - improve logging --- .vscode/settings.json | 6 + graxpert-dark-blue.json | 367 +++++++++ graxpert/CommandLineTool.py | 1 + graxpert/ai_model_handling.py | 40 +- graxpert/application/app.py | 532 ++++++++++++ graxpert/application/app_events.py | 64 ++ graxpert/application/eventbus.py | 33 + graxpert/astroimage.py | 313 ++++--- graxpert/collapsible_frame.py | 36 - graxpert/gui.py | 1234 ---------------------------- graxpert/help_panel.py | 404 --------- graxpert/main.py | 149 ++-- graxpert/mp_logging.py | 25 +- graxpert/preferences.py | 60 +- graxpert/slider.py | 115 --- graxpert/ui/__init__.py | 1 + graxpert/ui/application_frame.py | 114 +++ graxpert/ui/canvas.py | 464 +++++++++++ graxpert/ui/left_menu.py | 244 ++++++ graxpert/{ => ui}/loadingframe.py | 90 +- graxpert/ui/right_menu.py | 203 +++++ graxpert/ui/statusbar.py | 60 ++ graxpert/ui/styling.py | 14 + graxpert/{ => ui}/tooltip.py | 88 +- graxpert/ui/ui_events.py | 16 + graxpert/ui/widgets.py | 250 ++++++ graxpert/ui_scaling.py | 49 +- requirements.txt | 2 +- 28 files changed, 2734 insertions(+), 2240 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 graxpert-dark-blue.json create mode 100644 graxpert/application/app.py create mode 100644 graxpert/application/app_events.py create mode 100644 graxpert/application/eventbus.py delete mode 100644 graxpert/collapsible_frame.py delete mode 100644 graxpert/gui.py delete mode 100644 graxpert/help_panel.py delete mode 100644 graxpert/slider.py create mode 100644 graxpert/ui/__init__.py create mode 100644 graxpert/ui/application_frame.py create mode 100644 graxpert/ui/canvas.py create mode 100644 graxpert/ui/left_menu.py rename graxpert/{ => ui}/loadingframe.py (50%) create mode 100644 graxpert/ui/right_menu.py create mode 100644 graxpert/ui/statusbar.py create mode 100644 graxpert/ui/styling.py rename graxpert/{ => ui}/tooltip.py (64%) create mode 100644 graxpert/ui/ui_events.py create mode 100644 graxpert/ui/widgets.py diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..8532e24 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,6 @@ +{ + "black-formatter.args": [ + "--line-length", + "200" + ] +} \ No newline at end of file diff --git a/graxpert-dark-blue.json b/graxpert-dark-blue.json new file mode 100644 index 0000000..a55cac5 --- /dev/null +++ b/graxpert-dark-blue.json @@ -0,0 +1,367 @@ +{ + "CTk": { + "fg_color": [ + "gray95", + "gray10" + ] + }, + "CTkToplevel": { + "fg_color": [ + "gray95", + "gray10" + ] + }, + "CTkFrame": { + "corner_radius": 6, + "border_width": 0, + "fg_color": [ + "gray90", + "gray13" + ], + "top_fg_color": [ + "gray85", + "gray16" + ], + "border_color": [ + "gray65", + "gray28" + ] + }, + "CTkButton": { + "corner_radius": 6, + "border_width": 0, + "fg_color": [ + "#3a7ebf", + "#1f538d" + ], + "hover_color": [ + "#325882", + "#14375e" + ], + "border_color": [ + "#3E454A", + "#949A9F" + ], + "text_color": [ + "#DCE4EE", + "#DCE4EE" + ], + "text_color_disabled": [ + "gray74", + "gray60" + ] + }, + "Accent.CTkButton": { + "fg_color": "#247f4c", + "hover_color": "#154b2d" + }, + "Help.CTkButton": { + "fg_color": "#c46f1a", + "hover_color": "#73410f" + }, + "CTkLabel": { + "corner_radius": 0, + "fg_color": "transparent", + "text_color": [ + "gray14", + "gray84" + ] + }, + "CTkEntry": { + "corner_radius": 6, + "border_width": 2, + "fg_color": [ + "#F9F9FA", + "#343638" + ], + "border_color": [ + "#979DA2", + "#565B5E" + ], + "text_color": [ + "gray14", + "gray84" + ], + "placeholder_text_color": [ + "gray52", + "gray62" + ] + }, + "CTkCheckBox": { + "corner_radius": 6, + "border_width": 3, + "fg_color": [ + "#3a7ebf", + "#1f538d" + ], + "border_color": [ + "#3E454A", + "#949A9F" + ], + "hover_color": [ + "#325882", + "#14375e" + ], + "checkmark_color": [ + "#DCE4EE", + "gray90" + ], + "text_color": [ + "gray14", + "gray84" + ], + "text_color_disabled": [ + "gray60", + "gray45" + ] + }, + "CTkSwitch": { + "corner_radius": 1000, + "border_width": 3, + "button_length": 0, + "fg_color": [ + "#939BA2", + "#4A4D50" + ], + "progress_color": [ + "#3a7ebf", + "#1f538d" + ], + "button_color": [ + "gray36", + "#D5D9DE" + ], + "button_hover_color": [ + "gray20", + "gray100" + ], + "text_color": [ + "gray14", + "gray84" + ], + "text_color_disabled": [ + "gray60", + "gray45" + ] + }, + "CTkRadioButton": { + "corner_radius": 1000, + "border_width_checked": 6, + "border_width_unchecked": 3, + "fg_color": [ + "#3a7ebf", + "#1f538d" + ], + "border_color": [ + "#3E454A", + "#949A9F" + ], + "hover_color": [ + "#325882", + "#14375e" + ], + "text_color": [ + "gray14", + "gray84" + ], + "text_color_disabled": [ + "gray60", + "gray45" + ] + }, + "CTkProgressBar": { + "corner_radius": 1000, + "border_width": 0, + "fg_color": [ + "#939BA2", + "#4A4D50" + ], + "progress_color": [ + "#3a7ebf", + "#1f538d" + ], + "border_color": [ + "gray", + "gray" + ] + }, + "CTkSlider": { + "corner_radius": 1000, + "button_corner_radius": 1000, + "border_width": 6, + "button_length": 0, + "fg_color": [ + "#939BA2", + "#4A4D50" + ], + "progress_color": [ + "gray40", + "#AAB0B5" + ], + "button_color": [ + "#3a7ebf", + "#1f538d" + ], + "button_hover_color": [ + "#325882", + "#14375e" + ] + }, + "CTkOptionMenu": { + "corner_radius": 6, + "fg_color": [ + "#3a7ebf", + "#1f538d" + ], + "button_color": [ + "#325882", + "#14375e" + ], + "button_hover_color": [ + "#234567", + "#1e2c40" + ], + "text_color": [ + "#DCE4EE", + "#DCE4EE" + ], + "text_color_disabled": [ + "gray74", + "gray60" + ] + }, + "CTkComboBox": { + "corner_radius": 6, + "border_width": 2, + "fg_color": [ + "#F9F9FA", + "#343638" + ], + "border_color": [ + "#979DA2", + "#565B5E" + ], + "button_color": [ + "#979DA2", + "#565B5E" + ], + "button_hover_color": [ + "#6E7174", + "#7A848D" + ], + "text_color": [ + "gray14", + "gray84" + ], + "text_color_disabled": [ + "gray50", + "gray45" + ] + }, + "CTkScrollbar": { + "corner_radius": 1000, + "border_spacing": 4, + "fg_color": "transparent", + "button_color": [ + "gray55", + "gray41" + ], + "button_hover_color": [ + "gray40", + "gray53" + ] + }, + "CTkSegmentedButton": { + "corner_radius": 6, + "border_width": 2, + "fg_color": [ + "#979DA2", + "gray29" + ], + "selected_color": [ + "#3a7ebf", + "#1f538d" + ], + "selected_hover_color": [ + "#325882", + "#14375e" + ], + "unselected_color": [ + "#979DA2", + "gray29" + ], + "unselected_hover_color": [ + "gray70", + "gray41" + ], + "text_color": [ + "#DCE4EE", + "#DCE4EE" + ], + "text_color_disabled": [ + "gray74", + "gray60" + ] + }, + "CTkTextbox": { + "corner_radius": 6, + "border_width": 0, + "fg_color": [ + "gray100", + "gray20" + ], + "border_color": [ + "#979DA2", + "#565B5E" + ], + "text_color": [ + "gray14", + "gray84" + ], + "scrollbar_button_color": [ + "gray55", + "gray41" + ], + "scrollbar_button_hover_color": [ + "gray40", + "gray53" + ] + }, + "CTkScrollableFrame": { + "label_fg_color": [ + "gray80", + "gray21" + ] + }, + "DropdownMenu": { + "fg_color": [ + "gray90", + "gray20" + ], + "hover_color": [ + "gray75", + "gray28" + ], + "text_color": [ + "gray14", + "gray84" + ] + }, + "CTkFont": { + "macOS": { + "family": "SF Display", + "size": 13, + "weight": "normal" + }, + "Windows": { + "family": "Roboto", + "size": 13, + "weight": "normal" + }, + "Linux": { + "family": "Roboto", + "size": 13, + "weight": "normal" + } + } +} \ No newline at end of file diff --git a/graxpert/CommandLineTool.py b/graxpert/CommandLineTool.py index fabdf48..b895f93 100644 --- a/graxpert/CommandLineTool.py +++ b/graxpert/CommandLineTool.py @@ -79,6 +79,7 @@ def get_ai_version(self): logging.info("download successful".format(ai_version)) except Exception as e: logging.exception(e) + logging.shutdown() sys.exit(1) prefs["ai_version"] = ai_version diff --git a/graxpert/ai_model_handling.py b/graxpert/ai_model_handling.py index f0475be..310c029 100644 --- a/graxpert/ai_model_handling.py +++ b/graxpert/ai_model_handling.py @@ -10,9 +10,8 @@ from minio import Minio from packaging import version -from graxpert.loadingframe import DynamicProgressThread -from graxpert.s3_secrets import (bucket_name, endpoint, ro_access_key, - ro_secret_key) +from graxpert.s3_secrets import bucket_name, endpoint, ro_access_key, ro_secret_key +from graxpert.ui.loadingframe import DynamicProgressThread ai_models_dir = os.path.join(user_data_dir(appname="GraXpert"), "ai-models") os.makedirs(ai_models_dir, exist_ok=True) @@ -40,16 +39,13 @@ def list_remote_versions(): except Exception as e: logging.exception(e) - return None + finally: + return versions def list_local_versions(): try: - model_dirs = [ - {"path": os.path.join(ai_models_dir, f), "version": f} - for f in os.listdir(ai_models_dir) - if re.search(r"\d\.\d\.\d", f) - ] # match semantic version + model_dirs = [{"path": os.path.join(ai_models_dir, f), "version": f} for f in os.listdir(ai_models_dir) if re.search(r"\d\.\d\.\d", f)] # match semantic version return model_dirs except Exception as e: logging.exception(e) @@ -82,28 +78,16 @@ def compute_orphaned_local_versions(): remote_versions = list_remote_versions() if remote_versions is None: - logging.warning( - "Could not fetch remote versions. Thus, aborting cleaning of local versions in {}. Consider manual cleaning".format( - ai_models_dir - ) - ) + logging.warning("Could not fetch remote versions. Thus, aborting cleaning of local versions in {}. Consider manual cleaning".format(ai_models_dir)) return local_versions = list_local_versions() if local_versions is None: - logging.warning( - "Could not read local versions in {}. Thus, aborting cleaning. Consider manual cleaning".format( - ai_models_dir - ) - ) + logging.warning("Could not read local versions in {}. Thus, aborting cleaning. Consider manual cleaning".format(ai_models_dir)) return - orphaned_local_versions = [ - {"path": lv["path"], "version": lv["version"]} - for lv in local_versions - if lv["version"] not in [rv["version"] for rv in remote_versions] - ] + orphaned_local_versions = [{"path": lv["path"], "version": lv["version"]} for lv in local_versions if lv["version"] not in [rv["version"] for rv in remote_versions]] return orphaned_local_versions @@ -124,14 +108,10 @@ def download_version(remote_version, progress=None): remote_version = r break - ai_model_dir = os.path.join( - ai_models_dir, "{}".format(remote_version["version"]) - ) + ai_model_dir = os.path.join(ai_models_dir, "{}".format(remote_version["version"])) os.makedirs(ai_model_dir, exist_ok=True) - ai_model_file = os.path.join( - ai_model_dir, "{}.zip".format(remote_version["version"]) - ) + ai_model_file = os.path.join(ai_model_dir, "{}.zip".format(remote_version["version"])) client.fget_object( remote_version["bucket"], remote_version["object"], diff --git a/graxpert/application/app.py b/graxpert/application/app.py new file mode 100644 index 0000000..ef04398 --- /dev/null +++ b/graxpert/application/app.py @@ -0,0 +1,532 @@ +import logging +import os +import tkinter as tk +from tkinter import messagebox + +import numpy as np +from appdirs import user_config_dir +from icecream import ic + +from graxpert.ai_model_handling import ai_model_path_from_version, download_version, validate_local_version +from graxpert.app_state import INITIAL_STATE +from graxpert.application.app_events import AppEvents +from graxpert.application.eventbus import eventbus +from graxpert.astroimage import AstroImage +from graxpert.background_extraction import extract_background +from graxpert.commands import INIT_HANDLER, RESET_POINTS_HANDLER, RM_POINT_HANDLER, SEL_POINTS_HANDLER, Command +from graxpert.localization import _ +from graxpert.mp_logging import logfile_name +from graxpert.preferences import fitsheader_2_app_state, load_preferences, prefs_2_app_state +from graxpert.stretch import stretch_all +from graxpert.ui.loadingframe import DynamicProgressThread + + +class GraXpert: + def __init__(self): + self.initialize() + + def initialize(self): + # app preferences + prefs_filename = os.path.join(user_config_dir(appname="GraXpert"), "preferences.json") + self.prefs = load_preferences(prefs_filename) + + self.filename = "" + self.data_type = "" + + self.images = {"Original": None, "Background": None, "Processed": None} + self.display_type = "Original" + + self.ai_version = None + if self.prefs["ai_version"] is not None: + self.ai_version = self.prefs["ai_version"] + + self.mat_affine = np.eye(3) + + # state handling + tmp_state = prefs_2_app_state(self.prefs, INITIAL_STATE) + + self.cmd: Command = Command(INIT_HANDLER, background_points=tmp_state["background_points"]) + self.cmd.execute() + + # image loading + eventbus.add_listener(AppEvents.OPEN_FILE_DIALOG_REQUEST, self.on_open_file_dialog_request) + eventbus.add_listener(AppEvents.LOAD_IMAGE_REQUEST, self.on_load_image) + # image display + eventbus.add_listener(AppEvents.DISPLAY_TYPE_CHANGED, self.on_display_type_changed) + # stretch options + eventbus.add_listener(AppEvents.STRETCH_OPTION_CHANGED, self.on_stretch_option_changed) + eventbus.add_listener(AppEvents.CHANGE_SATURATION_REQUEST, self.on_change_saturation_request) + # sample selection + eventbus.add_listener(AppEvents.DISPLAY_PTS_CHANGED, self.on_display_pts_changed) + eventbus.add_listener(AppEvents.BG_FLOOD_SELECTION_CHANGED, self.on_bg_floot_selection_changed) + eventbus.add_listener(AppEvents.BG_PTS_CHANGED, self.on_bg_pts_changed) + eventbus.add_listener(AppEvents.BG_TOL_CHANGED, self.on_bg_tol_changed) + eventbus.add_listener(AppEvents.CREATE_GRID_REQUEST, self.on_create_grid_request) + eventbus.add_listener(AppEvents.RESET_POITS_REQUEST, self.on_reset_points_request) + # calculation + eventbus.add_listener(AppEvents.INTERPOL_TYPE_CHANGED, self.on_interpol_type_changed) + eventbus.add_listener(AppEvents.SMOTTHING_CHANGED, self.on_smoothing_changed) + eventbus.add_listener(AppEvents.CALCULATE_REQUEST, self.on_calculate_request) + # saving + eventbus.add_listener(AppEvents.SAVE_AS_CHANGED, self.on_save_as_changed) + eventbus.add_listener(AppEvents.SAVE_REQUEST, self.on_save_request) + eventbus.add_listener(AppEvents.SAVE_BACKGROUND_REQUEST, self.on_save_background_request) + eventbus.add_listener(AppEvents.SAVE_STRETCHED_REQUEST, self.on_save_stretched_request) + # advanced settings + eventbus.add_listener(AppEvents.SAMPLE_SIZE_CHANGED, self.on_sample_size_changed) + eventbus.add_listener(AppEvents.SAMPLE_COLOR_CHANGED, self.on_sample_color_changed) + eventbus.add_listener(AppEvents.RBF_KERNEL_CHANGED, self.on_rbf_kernel_changed) + eventbus.add_listener(AppEvents.SPLINE_ORDER_CHANGED, self.on_spline_order_changed) + eventbus.add_listener(AppEvents.CORRECTION_TYPE_CHANGED, self.on_correction_type_changed) + eventbus.add_listener(AppEvents.LANGUAGE_CHANGED, self.on_language_selected) + eventbus.add_listener(AppEvents.AI_VERSION_CHANGED, self.on_ai_version_changed) + + # event handling + def on_ai_version_changed(self, event): + self.prefs["ai_version"] = event["ai_version"] + + def on_bg_floot_selection_changed(self, event): + self.prefs["bg_flood_selection_option"] = event["bg_flood_selection_option"] + + def on_bg_pts_changed(self, event): + self.prefs["bg_pts_option"] = event["bg_pts_option"] + + def on_bg_tol_changed(self, event): + self.prefs["bg_tol_option"] = event["bg_tol_option"] + + def on_calculate_request(self, event=None): + eventbus.emit(AppEvents.CALCULATE_BEGIN) + + if self.images["Original"] is None: + eventbus.emit(AppEvents.CALCULATE_END) + messagebox.showerror("Error", _("Please load your picture first.")) + return + + background_points = self.cmd.app_state["background_points"] + + # Error messages if not enough points + if len(background_points) == 0 and self.prefs["interpol_type_option"] != "AI": + eventbus.emit(AppEvents.CALCULATE_END) + messagebox.showerror("Error", _("Please select background points with left click.")) + return + + if len(background_points) < 2 and self.prefs["interpol_type_option"] == "Kriging": + eventbus.emit(AppEvents.CALCULATE_END) + messagebox.showerror("Error", _("Please select at least 2 background points with left click for the Kriging method.")) + return + + if len(background_points) < 16 and self.prefs["interpol_type_option"] == "Splines": + eventbus.emit(AppEvents.CALCULATE_END) + messagebox.showerror("Error", _("Please select at least 16 background points with left click for the Splines method.")) + return + + if self.prefs["interpol_type_option"] == "AI": + if not self.validate_ai_installation(): + return + + def callback(p): + eventbus.emit(AppEvents.CALCULATE_PROGRESS, {"progress": p}) + + progress = DynamicProgressThread(callback=callback) + + imarray = np.copy(self.images["Original"].img_array) + + downscale_factor = 1 + + if self.prefs["interpol_type_option"] == "Kriging" or self.prefs["interpol_type_option"] == "RBF": + downscale_factor = 4 + + try: + self.images["Background"] = AstroImage() + self.images["Background"].set_from_array( + extract_background( + imarray, + np.array(background_points), + self.prefs["interpol_type_option"], + self.prefs["smoothing_option"], + downscale_factor, + self.prefs["sample_size"], + self.prefs["RBF_kernel"], + self.prefs["spline_order"], + self.prefs["corr_type"], + ai_model_path_from_version(self.prefs["ai_version"]), + progress, + ) + ) + + self.images["Processed"] = AstroImage() + self.images["Processed"].set_from_array(imarray) + + # Update fits header and metadata + background_mean = np.mean(self.images["Background"].img_array) + self.images["Processed"].update_fits_header(self.images["Original"].fits_header, background_mean, self, self.cmd.app_state) + self.images["Background"].update_fits_header(self.images["Original"].fits_header, background_mean, self, self.cmd.app_state) + + self.images["Processed"].copy_metadata(self.images["Original"]) + self.images["Background"].copy_metadata(self.images["Original"]) + + all_images = [self.images["Original"].img_array, self.images["Processed"].img_array, self.images["Background"].img_array] + stretches = stretch_all(all_images, self.images["Original"].get_stretch(self.prefs["stretch_option"])) + self.images["Original"].update_display_from_array(stretches[0], self.prefs["saturation"]) + self.images["Processed"].update_display_from_array(stretches[1], self.prefs["saturation"]) + self.images["Background"].update_display_from_array(stretches[2], self.prefs["saturation"]) + + # self.display_type = "Processed" + eventbus.emit(AppEvents.UPDATE_DISPLAY_TYPE_REEQUEST, {"display_type": "Processed"}) + + except Exception as e: + logging.exception(e) + eventbus.emit(AppEvents.CALCULATE_ERROR) + messagebox.showerror("Error", _("An error occured during background calculation. Please see the log at {}.".format(logfile_name))) + finally: + progress.done_progress() + eventbus.emit(AppEvents.CALCULATE_END) + + def on_change_saturation_request(self, event): + self.prefs["saturation"] = event["saturation"] + + eventbus.emit(AppEvents.CHANGE_SATURATION_BEGIN) + + for img in self.images.values(): + if img is not None: + img.update_saturation(self.prefs["saturation"]) + + eventbus.emit(AppEvents.CHANGE_SATURATION_END) + + def on_correction_type_changed(self, event): + self.prefs["corr_type"] = event["corr_type"] + + def on_create_grid_request(self, event=None): + if self.images["Original"] is None: + messagebox.showerror("Error", _("Please load your picture first.")) + return + + eventbus.emit(AppEvents.CREATE_GRID_BEGIN) + + self.cmd = Command( + SEL_POINTS_HANDLER, self.cmd, data=self.images["Original"].img_array, num_pts=self.prefs["bg_pts_option"], tol=self.prefs["bg_tol_option"], sample_size=self.prefs["sample_size"] + ) + self.cmd.execute() + + eventbus.emit(AppEvents.CREATE_GRID_END) + + def on_display_pts_changed(self, event): + self.prefs["display_pts"] = event["display_pts"] + eventbus.emit(AppEvents.REDRAW_POINTS_REQUEST) + + def on_display_type_changed(self, event): + self.display_type = event["display_type"] + + eventbus.emit(AppEvents.STRETCH_IMAGE_END) + + def on_interpol_type_changed(self, event): + self.prefs["interpol_type_option"] = event["interpol_type_option"] + + def on_language_selected(self, event): + self.prefs["lang"] = event["lang"] + messagebox.showerror("", _("Please restart the program to change the language.")) + + def on_load_image(self, event): + eventbus.emit(AppEvents.LOAD_IMAGE_BEGIN) + filename = event["filename"] + self.display_type = "Original" + + try: + image = AstroImage() + image.set_from_file(filename, self.prefs["stretch_option"], self.prefs["saturation"]) + + except Exception as e: + eventbus.emit(AppEvents.LOAD_IMAGE_ERROR) + msg = _("An error occurred while loading your picture.") + logging.exception(msg) + messagebox.showerror("Error", _(msg)) + return + + self.filename = os.path.splitext(os.path.basename(filename))[0] + + self.data_type = os.path.splitext(filename)[1] + self.images["Original"] = image + self.images["Processed"] = None + self.images["Background"] = None + self.prefs["working_dir"] = os.path.dirname(filename) + + os.chdir(os.path.dirname(filename)) + + width = self.images["Original"].img_display.width + height = self.images["Original"].img_display.height + + if self.prefs["width"] != width or self.prefs["height"] != height: + self.reset_backgroundpts() + + self.prefs["width"] = width + self.prefs["height"] = height + + tmp_state = fitsheader_2_app_state(self, self.cmd.app_state, self.images["Original"].fits_header) + self.cmd: Command = Command(INIT_HANDLER, background_points=tmp_state["background_points"]) + self.cmd.execute() + + eventbus.emit(AppEvents.LOAD_IMAGE_END, {"filename": filename}) + + def on_open_file_dialog_request(self, evet): + if self.prefs["working_dir"] != "" and os.path.exists(self.prefs["working_dir"]): + initialdir = self.prefs["working_dir"] + else: + initialdir = os.getcwd() + + filename = tk.filedialog.askopenfilename( + filetypes=[ + ("Image file", ".bmp .png .jpg .tif .tiff .fit .fits .fts .xisf"), + ("Bitmap", ".bmp"), + ("PNG", ".png"), + ("JPEG", ".jpg"), + ("Tiff", ".tif .tiff"), + ("Fits", ".fit .fits .fts"), + ("XISF", ".xisf"), + ], + initialdir=initialdir, + ) + + if filename == "": + return + + eventbus.emit(AppEvents.LOAD_IMAGE_REQUEST, {"filename": filename}) + + def on_rbf_kernel_changed(self, event): + self.prefs["RBF_kernel"] = event["RBF_kernel"] + + def on_reset_points_request(self, event): + eventbus.emit(AppEvents.RESET_POITS_BEGIN) + + if len(self.cmd.app_state["background_points"]) > 0: + self.cmd = Command(RESET_POINTS_HANDLER, self.cmd) + self.cmd.execute() + + eventbus.emit(AppEvents.RESET_POITS_END) + + def on_sample_color_changed(self, event): + self.prefs["sample_color"] = event["sample_color"] + eventbus.emit(AppEvents.REDRAW_POINTS_REQUEST) + + def on_sample_size_changed(self, event): + self.prefs["sample_size"] = event["sample_size"] + eventbus.emit(AppEvents.REDRAW_POINTS_REQUEST) + + def on_save_as_changed(self, event): + self.prefs["saveas_option"] = event["saveas_option"] + + def on_smoothing_changed(self, event): + self.prefs["smoothing_option"] = event["smoothing_option"] + + def on_save_request(self, event): + if self.prefs["saveas_option"] == "16 bit Tiff" or self.prefs["saveas_option"] == "32 bit Tiff": + dir = tk.filedialog.asksaveasfilename(initialfile=self.filename + "_GraXpert.tiff", filetypes=[("Tiff", ".tiff")], defaultextension=".tiff", initialdir=self.prefs["working_dir"]) + elif self.prefs["saveas_option"] == "16 bit XISF" or self.prefs["saveas_option"] == "32 bit XISF": + dir = tk.filedialog.asksaveasfilename(initialfile=self.filename + "_GraXpert.xisf", filetypes=[("XISF", ".xisf")], defaultextension=".xisf", initialdir=self.prefs["working_dir"]) + else: + dir = tk.filedialog.asksaveasfilename(initialfile=self.filename + "_GraXpert.fits", filetypes=[("Fits", ".fits")], defaultextension=".fits", initialdir=self.prefs["working_dir"]) + + if dir == "": + return + + eventbus.emit(AppEvents.SAVE_BEGIN) + + try: + self.images["Processed"].save(dir, self.prefs["saveas_option"]) + except: + eventbus.emit(AppEvents.SAVE_ERROR) + messagebox.showerror("Error", _("Error occured when saving the image.")) + + eventbus.emit(AppEvents.SAVE_END) + + def on_save_background_request(self, event): + if self.prefs["saveas_option"] == "16 bit Tiff" or self.prefs["saveas_option"] == "32 bit Tiff": + dir = tk.filedialog.asksaveasfilename(initialfile=self.filename + "_background.tiff", filetypes=[("Tiff", ".tiff")], defaultextension=".tiff", initialdir=self.prefs["working_dir"]) + elif self.prefs["saveas_option"] == "16 bit XISF" or self.prefs["saveas_option"] == "32 bit XISF": + dir = tk.filedialog.asksaveasfilename(initialfile=self.filename + "_background.xisf", filetypes=[("XISF", ".xisf")], defaultextension=".xisf", initialdir=self.prefs["working_dir"]) + else: + dir = tk.filedialog.asksaveasfilename(initialfile=self.filename + "_background.fits", filetypes=[("Fits", ".fits")], defaultextension=".fits", initialdir=os.getcwd()) + + if dir == "": + return + + eventbus.emit(AppEvents.SAVE_BEGIN) + + try: + self.images["Background"].save(dir, self.prefs["saveas_option"]) + except: + eventbus.emit(AppEvents.SAVE_ERROR) + messagebox.showerror("Error", _("Error occured when saving the image.")) + + eventbus.emit(AppEvents.SAVE_END) + + def on_save_stretched_request(self, event): + if self.prefs["saveas_option"] == "16 bit Tiff" or self.prefs["saveas_option"] == "32 bit Tiff": + dir = tk.filedialog.asksaveasfilename(initialfile=self.filename + "_stretched_GraXpert.tiff", filetypes=[("Tiff", ".tiff")], defaultextension=".tiff", initialdir=self.prefs["working_dir"]) + elif self.prefs["saveas_option"] == "16 bit XISF" or self.prefs["saveas_option"] == "32 bit XISF": + dir = tk.filedialog.asksaveasfilename(initialfile=self.filename + "_stretched_GraXpert.xisf", filetypes=[("XISF", ".xisf")], defaultextension=".xisf", initialdir=self.prefs["working_dir"]) + else: + dir = tk.filedialog.asksaveasfilename(initialfile=self.filename + "_stretched_GraXpert.fits", filetypes=[("Fits", ".fits")], defaultextension=".fits", initialdir=self.prefs["working_dir"]) + + if dir == "": + return + + eventbus.emit(AppEvents.SAVE_BEGIN) + + try: + if self.images["Processed"] is None: + self.images["Original"].save_stretched(dir, self.prefs["saveas_option"]) + else: + self.images["Processed"].save_stretched(dir, self.prefs["saveas_option"]) + except: + eventbus.emit(AppEvents.SAVE_ERROR) + messagebox.showerror("Error", _("Error occured when saving the image.")) + + eventbus.emit(AppEvents.SAVE_END) + + def on_spline_order_changed(self, event): + self.prefs["spline_order"] = event["spline_order"] + + def on_stretch_option_changed(self, event): + self.prefs["stretch_option"] = event["stretch_option"] + + eventbus.emit(AppEvents.STRETCH_IMAGE_BEGIN) + + try: + all_images = [] + stretches = [] + for img in self.images.values(): + if img is not None: + all_images.append(img.img_array) + if len(all_images) > 0: + stretch_params = self.images["Original"].get_stretch(self.prefs["stretch_option"]) + stretches = stretch_all(all_images, stretch_params) + for idx, img in enumerate(self.images.values()): + if img is not None: + img.update_display_from_array(stretches[idx], self.prefs["saturation"]) + except Exception as e: + eventbus.emit(AppEvents.STRETCH_IMAGE_ERROR) + logging.exception(e) + + eventbus.emit(AppEvents.STRETCH_IMAGE_END) + + # application logic + def remove_pt(self, event): + if len(self.cmd.app_state["background_points"]) == 0 or not self.prefs["display_pts"]: + return False + + point_im = self.to_image_point(event.x, event.y) + if len(point_im) == 0: + return False + + eventx_im = point_im[0] + eventy_im = point_im[1] + + background_points = self.cmd.app_state["background_points"] + + min_idx = -1 + min_dist = -1 + + for i in range(len(background_points)): + x_im = background_points[i][0] + y_im = background_points[i][1] + + dist = np.max(np.abs([x_im - eventx_im, y_im - eventy_im])) + + if min_idx == -1 or dist < min_dist: + min_dist = dist + min_idx = i + + if min_idx != -1 and min_dist <= self.prefs["sample_size"]: + point = background_points[min_idx] + self.cmd = Command(RM_POINT_HANDLER, self.cmd, idx=min_idx, point=point) + self.cmd.execute() + return True + else: + return False + + # application logic + def reset_backgroundpts(self): + if len(self.cmd.app_state["background_points"]) > 0: + self.cmd = Command(RESET_POINTS_HANDLER, self.cmd) + self.cmd.execute() + + def reset_transform(self): + self.mat_affine = np.eye(3) + + def scale_at(self, scale: float, cx: float, cy: float): + self.translate(-cx, -cy) + self.scale(scale) + self.translate(cx, cy) + + def scale(self, scale: float): + mat = np.eye(3) + mat[0, 0] = scale + mat[1, 1] = scale + self.mat_affine = np.dot(mat, self.mat_affine) + + def to_canvas_point(self, x, y): + return np.dot(self.mat_affine, (x, y, 1.0)) + + def to_image_point(self, x, y): + if self.images[self.display_type] is None: + return [] + + mat_inv = np.linalg.inv(self.mat_affine) + image_point = np.dot(mat_inv, (x, y, 1.0)) + + width = self.images[self.display_type].width + height = self.images[self.display_type].height + + if image_point[0] < 0 or image_point[1] < 0 or image_point[0] > width or image_point[1] > height: + return [] + + return image_point + + def to_image_point_pinned(self, x, y): + if self.images[self.display_type] is None: + return [] + + mat_inv = np.linalg.inv(self.mat_affine) + image_point = np.dot(mat_inv, (x, y, 1.0)) + + width = self.images[self.display_type].width + height = self.images[self.display_type].height + + if image_point[0] < 0: + image_point[0] = 0 + if image_point[1] < 0: + image_point[1] = 0 + if image_point[0] > width: + image_point[0] = width + if image_point[1] > height: + image_point[1] = height + + return image_point + + def translate(self, offset_x, offset_y): + mat = np.eye(3) + mat[0, 2] = float(offset_x) + mat[1, 2] = float(offset_y) + + self.mat_affine = np.dot(mat, self.mat_affine) + + def validate_ai_installation(self): + if self.ai_version is None or self.prefs["ai_version"] == "None": + messagebox.showerror("Error", _("No AI-Model selected. Please select one from the Advanced panel on the right.")) + return False + + if not validate_local_version(self.prefs["ai_version"]): + if not messagebox.askyesno(_("Install AI-Model?"), _("Selected AI-Model is not installed. Should I download it now?")): + return False + else: + eventbus.emit(AppEvents.AI_DOWNLOAD_BEGIN) + + def callback(p): + eventbus.emit(AppEvents.AI_DOWNLOAD_PROGRESS, {"progress": p}) + + download_version(self.ai_version, progress=callback) + eventbus.emit(AppEvents.AI_DOWNLOAD_END) + return True + + +graxpert = GraXpert() diff --git a/graxpert/application/app_events.py b/graxpert/application/app_events.py new file mode 100644 index 0000000..cc21d48 --- /dev/null +++ b/graxpert/application/app_events.py @@ -0,0 +1,64 @@ +from enum import Enum, auto + + +class AppEvents(Enum): + # image loading + OPEN_FILE_DIALOG_REQUEST = auto() + LOAD_IMAGE_REQUEST = auto() + LOAD_IMAGE_BEGIN = auto() + LOAD_IMAGE_END = auto() + LOAD_IMAGE_ERROR = auto() + # image stretching + STRETCH_IMAGE_BEGIN = auto() + STRETCH_IMAGE_END = auto() + STRETCH_IMAGE_ERROR = auto() + # image saturation + CHANGE_SATURATION_REQUEST = auto() + CHANGE_SATURATION_BEGIN = auto() + CHANGE_SATURATION_END = auto() + # image display + UPDATE_DISPLAY_TYPE_REEQUEST = auto() + DISPLAY_TYPE_CHANGED = auto() + REDRAW_POINTS_REQUEST = auto() + # stretch options + STRETCH_OPTION_CHANGED = auto() + # sample selection + DISPLAY_PTS_CHANGED = auto() + BG_FLOOD_SELECTION_CHANGED = auto() + BG_PTS_CHANGED = auto() + BG_TOL_CHANGED = auto() + CREATE_GRID_REQUEST = auto() + CREATE_GRID_BEGIN = auto() + CREATE_GRID_END = auto() + RESET_POITS_REQUEST = auto() + RESET_POITS_BEGIN = auto() + RESET_POITS_END = auto() + # calculation + INTERPOL_TYPE_CHANGED = auto() + SMOTTHING_CHANGED = auto() + CALCULATE_REQUEST = auto() + CALCULATE_BEGIN = auto() + CALCULATE_PROGRESS = auto() + CALCULATE_END = auto() + CALCULATE_ERROR = auto() + # saving + SAVE_AS_CHANGED = auto() + SAVE_REQUEST = auto() + SAVE_BACKGROUND_REQUEST = auto() + SAVE_STRETCHED_REQUEST = auto() + SAVE_BEGIN = auto() + SAVE_END = auto() + SAVE_ERROR = auto() + # ai model handling + AI_VERSION_CHANGED = auto() + AI_DOWNLOAD_BEGIN = auto() + AI_DOWNLOAD_PROGRESS = auto() + AI_DOWNLOAD_END = auto() + AI_DOWNLOAD_ERROR = auto() + # advanced settings + SAMPLE_SIZE_CHANGED = auto() + SAMPLE_COLOR_CHANGED = auto() + RBF_KERNEL_CHANGED = auto() + SPLINE_ORDER_CHANGED = auto() + CORRECTION_TYPE_CHANGED = auto() + LANGUAGE_CHANGED = auto() diff --git a/graxpert/application/eventbus.py b/graxpert/application/eventbus.py new file mode 100644 index 0000000..9f4c164 --- /dev/null +++ b/graxpert/application/eventbus.py @@ -0,0 +1,33 @@ +# import asyncio + +from icecream import ic +from graxpert.ui.ui_events import UiEvents + + +class EventBus: + def __init__(self): + self.listeners = {} + # self.loop = asyncio.get_event_loop() + + def add_listener(self, event_name, listener): + if not self.listeners.get(event_name, None): + self.listeners[event_name] = {listener} + else: + self.listeners[event_name].add(listener) + + def remove_listener(self, event_name, listener): + self.listeners[event_name].remove(listener) + if len(self.listeners[event_name]) == 0: + del self.listeners[event_name] + + def emit(self, event_name, event=None): + # if event_name != UiEvents.MOUSE_MOVED: + # ic(f"sending {event_name}:{event} to:") + listeners = self.listeners.get(event_name, []) + for listener in listeners: + # if event_name != UiEvents.MOUSE_MOVED: + # ic(f" {listener}") + listener(event) + + +eventbus = EventBus() diff --git a/graxpert/astroimage.py b/graxpert/astroimage.py index b505312..f1cc84c 100644 --- a/graxpert/astroimage.py +++ b/graxpert/astroimage.py @@ -1,17 +1,20 @@ -import os import json +import os + import numpy as np -from xisf import XISF from astropy.io import fits from astropy.stats import sigma_clipped_stats -from skimage import io, img_as_float32, exposure -from skimage.util import img_as_ubyte, img_as_uint from PIL import Image, ImageEnhance -from graxpert.stretch import stretch +from skimage import exposure, img_as_float32, io +from skimage.util import img_as_ubyte, img_as_uint +from xisf import XISF + from graxpert.preferences import app_state_2_fitsheader +from graxpert.stretch import stretch + class AstroImage: - def __init__(self, stretch_option = None, saturation = None, do_update_display = True): + def __init__(self, do_update_display=True): self.img_array = None self.img_display = None self.img_display_saturated = None @@ -19,311 +22,303 @@ def __init__(self, stretch_option = None, saturation = None, do_update_display = self.fits_header = None self.xisf_metadata = {} self.image_metadata = {"FITSKeywords": {}} - self.stretch_option = stretch_option - self.saturation = saturation self.do_update_display = do_update_display self.width = 0 self.height = 0 self.roworder = "BOTTOM-UP" - - def set_from_file(self, directory): + + def set_from_file(self, directory, stretch_option, saturation): self.img_format = os.path.splitext(directory)[1].lower() - + img_array = None - if(self.img_format == ".fits" or self.img_format == ".fit" or self.img_format == ".fts"): + if self.img_format == ".fits" or self.img_format == ".fit" or self.img_format == ".fts": hdul = fits.open(directory) img_array = np.copy(hdul[0].data) self.fits_header = hdul[0].header hdul.close() - - if(len(img_array.shape) == 3): - img_array = np.moveaxis(img_array,0,-1) - + + if len(img_array.shape) == 3: + img_array = np.moveaxis(img_array, 0, -1) + if "ROWORDER" in self.fits_header: self.roworder = self.fits_header["ROWORDER"] - - elif(self.img_format == ".xisf"): + + elif self.img_format == ".xisf": xisf = XISF(directory) self.xisf_metadata = xisf.get_file_metadata() self.image_metadata = xisf.get_images_metadata()[0] self.fits_header = fits.Header() self.xisf_imagedata_2_fitsheader() img_array = np.copy(xisf.read_image(0)) - - entry = {'id': 'BackgroundExtraction', 'type': 'String', 'value': 'GraXpert'} - self.image_metadata['XISFProperties'] = {"ProcessingHistory": entry} - + + entry = {"id": "BackgroundExtraction", "type": "String", "value": "GraXpert"} + self.image_metadata["XISFProperties"] = {"ProcessingHistory": entry} + else: img_array = np.copy(io.imread(directory)) self.fits_header = fits.Header() - + # Reshape greyscale picture to shape (y,x,1) - if(len(img_array.shape) == 2): + if len(img_array.shape) == 2: img_array = np.array([img_array]) - img_array = np.moveaxis(img_array,0,-1) - + img_array = np.moveaxis(img_array, 0, -1) + # Use 32 bit float with range (0,1) for internal calculations img_array = img_as_float32(img_array) - - - if(np.min(img_array) < 0 or np.max(img_array > 1)): - img_array = exposure.rescale_intensity(img_array, out_range=(0,1)) - + + if np.min(img_array) < 0 or np.max(img_array > 1): + img_array = exposure.rescale_intensity(img_array, out_range=(0, 1)) + self.img_array = img_array self.width = self.img_array.shape[1] self.height = self.img_array.shape[0] - + if self.do_update_display: - self.update_display() - + self.update_display(stretch_option, saturation) + return - + def set_from_array(self, array): self.img_array = array self.width = self.img_array.shape[1] self.height = self.img_array.shape[0] return - - def update_display(self): - img_display = self.stretch() - img_display = img_display*255 - - #if self.roworder == "TOP-DOWN": + + def update_display(self, stretch_option, saturation): + img_display = self.stretch(stretch_option) + img_display = img_display * 255 + + # if self.roworder == "TOP-DOWN": # img_display = np.flip(img_display, axis=0) - - if(img_display.shape[2] == 1): - self.img_display = Image.fromarray(img_display[:,:,0].astype(np.uint8)) + + if img_display.shape[2] == 1: + self.img_display = Image.fromarray(img_display[:, :, 0].astype(np.uint8)) else: self.img_display = Image.fromarray(img_display.astype(np.uint8)) - - self.update_saturation() - + + self.update_saturation(saturation) + return - - def update_display_from_array(self, img_display): - img_display = img_display*255 - - #if self.roworder == "TOP-DOWN": + + def update_display_from_array(self, img_display, saturation): + img_display = img_display * 255 + + # if self.roworder == "TOP-DOWN": # img_display = np.flip(img_display, axis=0) - - if(img_display.shape[2] == 1): - self.img_display = Image.fromarray(img_display[:,:,0].astype(np.uint8)) + + if img_display.shape[2] == 1: + self.img_display = Image.fromarray(img_display[:, :, 0].astype(np.uint8)) else: self.img_display = Image.fromarray(img_display.astype(np.uint8)) - - self.update_saturation() - + + self.update_saturation(saturation) + return - - def stretch(self): + + def stretch(self, stretch_option): bg, sigma = (0.2, 3) - if(self.stretch_option.get() == "No Stretch"): + if stretch_option == "No Stretch": return self.img_array - - elif(self.stretch_option.get() == "10% Bg, 3 sigma"): - bg, sigma = (0.1,3) - - elif(self.stretch_option.get() == "15% Bg, 3 sigma"): - bg, sigma = (0.15,3) - - elif(self.stretch_option.get() == "20% Bg, 3 sigma"): - bg, sigma = (0.2,3) - - elif(self.stretch_option.get() == "30% Bg, 2 sigma"): - bg, sigma = (0.3,2) - - + + elif stretch_option == "10% Bg, 3 sigma": + bg, sigma = (0.1, 3) + + elif stretch_option == "15% Bg, 3 sigma": + bg, sigma = (0.15, 3) + + elif stretch_option == "20% Bg, 3 sigma": + bg, sigma = (0.2, 3) + + elif stretch_option == "30% Bg, 2 sigma": + bg, sigma = (0.3, 2) + return np.clip(stretch(self.img_array, bg, sigma), 0.0, 1.0) - - def get_stretch(self): - if(self.stretch_option.get() == "No Stretch"): + + def get_stretch(self, stretch_option): + if stretch_option == "No Stretch": return None - elif(self.stretch_option.get() == "10% Bg, 3 sigma"): + elif stretch_option == "10% Bg, 3 sigma": return (0.1, 3) - elif(self.stretch_option.get() == "15% Bg, 3 sigma"): + elif stretch_option == "15% Bg, 3 sigma": return (0.15, 3) - elif(self.stretch_option.get() == "20% Bg, 3 sigma"): + elif stretch_option == "20% Bg, 3 sigma": return (0.2, 3) - elif(self.stretch_option.get() == "30% Bg, 2 sigma"): + elif stretch_option == "30% Bg, 2 sigma": return (0.3, 2) - + def crop(self, startx, endx, starty, endy): - self.img_array = self.img_array[starty:endy,startx:endx,:] + self.img_array = self.img_array[starty:endy, startx:endx, :] self.img_display = self.img_display.crop((startx, starty, endx, endy)) self.img_display_saturated = self.img_display_saturated.crop((startx, starty, endx, endy)) self.width = self.img_array.shape[1] - self.height = self.img_array.shape[0] + self.height = self.img_array.shape[0] return - + def update_fits_header(self, original_header, background_mean, app, app_state): - if(original_header is None): + if original_header is None: self.fits_header = fits.Header() else: self.fits_header = original_header - + self.fits_header["BG-EXTR"] = "GraXpert" self.fits_header["CBG-1"] = background_mean self.fits_header["CBG-2"] = background_mean self.fits_header["CBG-3"] = background_mean self.fits_header = app_state_2_fitsheader(app, app_state, self.fits_header) - - + if "ROWORDER" in self.fits_header: self.roworder = self.fits_header["ROWORDER"] - + def save(self, dir, saveas_type): - if(self.img_array is None): + if self.img_array is None: return - - if(saveas_type == "16 bit Tiff" or saveas_type == "16 bit Fits" or saveas_type == "16 bit XISF"): + + if saveas_type == "16 bit Tiff" or saveas_type == "16 bit Fits" or saveas_type == "16 bit XISF": image_converted = img_as_uint(self.img_array) else: image_converted = self.img_array.astype(np.float32) - - if(saveas_type != "16 bit XISF" and saveas_type != "32 bit XISF"): - if(image_converted.shape[-1] == 3): - if(saveas_type == "16 bit Fits" or saveas_type == "32 bit Fits"): - image_converted = np.moveaxis(image_converted,-1,0) + + if saveas_type != "16 bit XISF" and saveas_type != "32 bit XISF": + if image_converted.shape[-1] == 3: + if saveas_type == "16 bit Fits" or saveas_type == "32 bit Fits": + image_converted = np.moveaxis(image_converted, -1, 0) else: - image_converted = image_converted[:,:,0] - - if(saveas_type == "16 bit Tiff" or saveas_type == "32 bit Tiff"): + image_converted = image_converted[:, :, 0] + + if saveas_type == "16 bit Tiff" or saveas_type == "32 bit Tiff": io.imsave(dir, image_converted) - - elif(saveas_type == "16 bit XISF" or saveas_type == "32 bit XISF"): + + elif saveas_type == "16 bit XISF" or saveas_type == "32 bit XISF": self.update_xisf_imagedata() - XISF.write(dir, image_converted, creator_app = "GraXpert", image_metadata = self.image_metadata, xisf_metadata = self.xisf_metadata) - else: + XISF.write(dir, image_converted, creator_app="GraXpert", image_metadata=self.image_metadata, xisf_metadata=self.xisf_metadata) + else: hdu = fits.PrimaryHDU(data=image_converted, header=self.fits_header) hdul = fits.HDUList([hdu]) hdul.writeto(dir, output_verify="warn", overwrite=True) hdul.close() - + return - def save_stretched(self, dir, saveas_type): - if(self.img_array is None): + def save_stretched(self, dir, saveas_type, stretch_option): + if self.img_array is None: return - - self.fits_header["STRETCH"] = self.stretch_option.get() - - stretched_img = self.stretch() - - if(saveas_type == "16 bit Tiff" or saveas_type == "16 bit Fits" or saveas_type == "16 bit XISF"): + + self.fits_header["STRETCH"] = stretch_option + + stretched_img = self.stretch(stretch_option) + + if saveas_type == "16 bit Tiff" or saveas_type == "16 bit Fits" or saveas_type == "16 bit XISF": image_converted = img_as_uint(stretched_img) else: image_converted = stretched_img.astype(np.float32) - - if(saveas_type != "16 bit XISF" and saveas_type != "32 bit XISF"): - if(image_converted.shape[-1] == 3): - if(saveas_type == "16 bit Fits" or saveas_type == "32 bit Fits"): - image_converted = np.moveaxis(image_converted,-1,0) + + if saveas_type != "16 bit XISF" and saveas_type != "32 bit XISF": + if image_converted.shape[-1] == 3: + if saveas_type == "16 bit Fits" or saveas_type == "32 bit Fits": + image_converted = np.moveaxis(image_converted, -1, 0) else: - image_converted = image_converted[:,:,0] - - if(saveas_type == "16 bit Tiff" or saveas_type == "32 bit Tiff"): + image_converted = image_converted[:, :, 0] + + if saveas_type == "16 bit Tiff" or saveas_type == "32 bit Tiff": io.imsave(dir, image_converted) - - elif(saveas_type == "16 bit XISF" or saveas_type == "32 bit XISF"): + + elif saveas_type == "16 bit XISF" or saveas_type == "32 bit XISF": self.update_xisf_imagedata() - XISF.write(dir, image_converted, creator_app = "GraXpert", image_metadata = self.image_metadata, xisf_metadata = self.xisf_metadata) - else: + XISF.write(dir, image_converted, creator_app="GraXpert", image_metadata=self.image_metadata, xisf_metadata=self.xisf_metadata) + else: hdu = fits.PrimaryHDU(data=image_converted, header=self.fits_header) hdul = fits.HDUList([hdu]) hdul.writeto(dir, output_verify="warn", overwrite=True) hdul.close() - + return - + def get_local_median(self, img_point): sample_radius = 25 y1 = int(np.amax([img_point[1] - sample_radius, 0])) y2 = int(np.amin([img_point[1] + sample_radius, self.height])) x1 = int(np.amax([img_point[0] - sample_radius, 0])) x2 = int(np.amin([img_point[0] + sample_radius, self.width])) - - + if self.img_array.shape[-1] == 3: R = sigma_clipped_stats(data=self.img_array[y1:y2, x1:x2, 0], cenfunc="median", stdfunc="std", grow=4)[1] G = sigma_clipped_stats(data=self.img_array[y1:y2, x1:x2, 1], cenfunc="median", stdfunc="std", grow=4)[1] B = sigma_clipped_stats(data=self.img_array[y1:y2, x1:x2, 2], cenfunc="median", stdfunc="std", grow=4)[1] - - return [R,G,B] - + + return [R, G, B] + if self.img_array.shape[-1] == 1: L = sigma_clipped_stats(data=self.img_array[x1:x2, y1:y2, 0], cenfunc="median", stdfunc="std", grow=4)[1] - + return L - + def copy_metadata(self, source_img): self.xisf_metadata = source_img.xisf_metadata self.image_metadata = source_img.image_metadata - - def update_saturation(self): + + def update_saturation(self, saturation): self.img_display_saturated = self.img_display - + if self.img_array.shape[-1] == 3: self.img_display_saturated = ImageEnhance.Color(self.img_display) - self.img_display_saturated = self.img_display_saturated.enhance(self.saturation.get()) - + self.img_display_saturated = self.img_display_saturated.enhance(saturation) + return - + def update_xisf_imagedata(self): unique_keys = list(dict.fromkeys(self.fits_header.keys())) - + for key in unique_keys: if key == "BG-PTS": bg_pts = json.loads(self.fits_header["BG-PTS"]) - + for i in range(len(bg_pts)): - self.image_metadata["FITSKeywords"]["BG-PTS" + str(i)] = [{"value": bg_pts[i],"comment": ""}] + self.image_metadata["FITSKeywords"]["BG-PTS" + str(i)] = [{"value": bg_pts[i], "comment": ""}] else: - value = str(self.fits_header[key]).splitlines() comment = str(self.fits_header.comments[key]).splitlines() - + entry = [] - + for i in range(max(len(comment), len(value))): value_i = "" comment_i = "" - + if i < len(comment): comment_i = comment[i] if i < len(value): value_i = value[i] - + entry.append({"value": value_i, "comment": comment_i}) - + if len(entry) == 0: entry = [{"value": "", "comment": ""}] self.image_metadata["FITSKeywords"][key] = entry - def xisf_imagedata_2_fitsheader(self): - commentary_keys = ['HISTORY','COMMENT',''] - + commentary_keys = ["HISTORY", "COMMENT", ""] + bg_pts = [] for key in self.image_metadata["FITSKeywords"].keys(): if key.startswith("BG-PTS"): bg_pts.append(json.loads(self.image_metadata["FITSKeywords"][key][0]["value"])) - + for i in range(len(self.image_metadata["FITSKeywords"][key])): value = self.image_metadata["FITSKeywords"][key][i]["value"] comment = self.image_metadata["FITSKeywords"][key][i]["comment"] - + # Commentary cards have to comments in Fits standard if key in commentary_keys: if value == "": value = comment - + if value.isdigit(): value = int(value) elif value.isdecimal(): value = float(value) - + self.fits_header[key] = (value, comment) - + if len(bg_pts) > 0: self.fits_header["BG-PTS"] = str(bg_pts) diff --git a/graxpert/collapsible_frame.py b/graxpert/collapsible_frame.py deleted file mode 100644 index bb1c8c1..0000000 --- a/graxpert/collapsible_frame.py +++ /dev/null @@ -1,36 +0,0 @@ -import tkinter as tk -from tkinter import ttk - -class CollapsibleFrame(tk.Frame): - - def __init__(self, parent, text="", *args, **options): - tk.Frame.__init__(self, parent, *args, **options) - - self.show = tk.IntVar() - self.show.set(0) - - self.title_frame = ttk.Frame(self, borderwidth=0) - self.title_frame.pack(fill="x", expand=1) - - ttk.Label(self.title_frame, text=text, font="Verdana 11 bold").pack(side="left", fill="x", expand=1) - - self.toggle_button = ttk.Checkbutton(self.title_frame, width=2, text='+', command=self.toggle, - variable=self.show, style='Toolbutton') - self.toggle_button.pack(side="left") - - self.sub_frame = tk.Frame(self, relief="sunken", borderwidth=0) - - def toggle(self): - if bool(self.show.get()): - self.sub_frame.pack(fill="x", expand=1) - self.toggle_button.configure(text='-') - else: - self.sub_frame.forget() - self.toggle_button.configure(text='+') - - self.update() - self.master.update() - width = self.master.winfo_width() - self.master.master.configure(width=width) - self.master.master.configure(scrollregion=self.master.master.bbox("all")) - self.master.master.yview_moveto("0.0") \ No newline at end of file diff --git a/graxpert/gui.py b/graxpert/gui.py deleted file mode 100644 index bb58ff0..0000000 --- a/graxpert/gui.py +++ /dev/null @@ -1,1234 +0,0 @@ -import importlib -import logging -import os -import shutil -import sys -import tkinter as tk -from colorsys import hls_to_rgb -from tkinter import filedialog, messagebox, ttk - -import hdpitkinter as hdpitk -import numpy as np -from appdirs import user_config_dir -from PIL import Image, ImageTk - -import graxpert.background_extraction as background_extraction -import graxpert.tooltip as tooltip -from graxpert.ai_model_handling import (ai_model_path_from_version, - download_version, - validate_local_version) -from graxpert.app_state import INITIAL_STATE -from graxpert.astroimage import AstroImage -from graxpert.collapsible_frame import CollapsibleFrame -from graxpert.commands import (ADD_POINT_HANDLER, ADD_POINTS_HANDLER, - INIT_HANDLER, MOVE_POINT_HANDLER, - RESET_POINTS_HANDLER, RM_POINT_HANDLER, - SEL_POINTS_HANDLER, Command, InitHandler) -from graxpert.help_panel import Help_Panel -from graxpert.loadingframe import (DynamicProgressFrame, DynamicProgressThread, - LoadingFrame) -from graxpert.localization import _ -from graxpert.mp_logging import (initialize_logging, logfile_name, - shutdown_logging) -from graxpert.parallel_processing import executor -from graxpert.preferences import (app_state_2_fitsheader, app_state_2_prefs, - fitsheader_2_app_state, load_preferences, - prefs_2_app_state, save_preferences) -from graxpert.resource_utils import (resource_path, scale_img, - temp_resource_path) -from graxpert.slider import Slider -from graxpert.stretch import stretch_all -from graxpert.ui_scaling import get_scaling_factor -from graxpert.version import check_for_new_version, release, version - - -class Application(tk.Frame): - def __init__(self, master=None): - super().__init__(master) - - self.master.geometry("1920x1080") - self.master.minsize(height=768 ,width=1024) - - try: - self.master.state("zoomed") - except: - self.master.state("normal") - - self.filename = "" - self.data_type = "" - - self.images = { - "Original": None, - "Background": None, - "Processed": None - } - - self.my_title = "GraXpert | Release: '{}' ({})".format(release, version) - self.master.title(self.my_title) - - prefs_filename = os.path.join(user_config_dir(appname="GraXpert"), "preferences.json") - self.prefs = load_preferences(prefs_filename) - - tmp_state = prefs_2_app_state(self.prefs, INITIAL_STATE) - - self.cmd: Command = Command(INIT_HANDLER, background_points=tmp_state["background_points"]) - self.cmd.execute() - - self.create_widget() - self.bgextr_menu.show.set(1) - self.bgextr_menu.toggle() - - self.reset_transform() - - if len(sys.argv) > 1 and sys.argv[1].endswith((".bmp", ".png", ".jpg", ".tif", ".tiff", ".fit", ".fits", ".fts", ".xisf")): - filename = sys.argv[1] - self.menu_open_clicked(None, filename) - - - def create_widget(self): - - - frame_statusbar = tk.Frame(self.master, bd=1, relief = tk.SUNKEN) - self.label_image_info = ttk.Label(frame_statusbar, text="image info", anchor=tk.E) - self.label_image_pixel = ttk.Label(frame_statusbar, text="(x, y)", anchor=tk.W) - self.label_image_info.pack(side=tk.RIGHT) - self.label_image_pixel.pack(side=tk.LEFT) - frame_statusbar.pack(side=tk.BOTTOM, fill=tk.X) - - - self.master.grid_columnconfigure(3) - #Right help panel - - self.canvas = tk.Canvas(self.master, background="black", name="picture") - self.help_panel = Help_Panel(self.master, self.canvas, self) - - - # Canvas - - self.canvas.pack(side=tk.RIGHT, expand=True, fill=tk.BOTH) - - - self.display_options = ["Original","Processed","Background"] - self.display_type = tk.StringVar() - self.display_type.set(self.display_options[0]) - self.display_menu = ttk.OptionMenu(self.canvas, self.display_type, self.display_type.get(), *self.display_options, command=self.switch_display) - self.display_menu.place(relx=0.5, rely=0.01) - tt_display_type = tooltip.Tooltip(self.display_menu, text=tooltip.display_text, wraplength=500) - - self.loading_frame = LoadingFrame(self.canvas, self.master) - - self.left_drag_timer = -1 - self.clicked_inside_pt = False - self.clicked_inside_pt_idx = 0 - self.clicked_inside_pt_coord = None - - self.crop_mode = False - - self.master.bind_all("", lambda event: event.widget.focus_set()) - self.master.bind("", self.mouse_down_left) - self.master.bind("", self.mouse_release_left) # Left Mouse Button - self.master.bind("", self.mouse_down_right) # Middle Mouse Button (Right Mouse Button on macs) - self.master.bind("", self.mouse_down_right) # Right Mouse Button (Middle Mouse Button on macs) - self.master.bind("", self.mouse_move_left) # Left Mouse Button Drag - self.master.bind("", self.mouse_move) # Mouse move - self.master.bind("", self.reset_zoom) # backspace -> reset zoom - self.master.bind("", self.reset_zoom) # ctrl + 0 -> reset zoom (Windows) - self.master.bind("", self.reset_zoom) # cmd + 0 -> reset zoom (Mac) - self.master.bind("", self.reset_zoom) # ctrl + numpad 0 -> reset zoom (Windows) - self.master.bind("", self.reset_zoom) # cmd + numpad 0 -> reset zoom (Mac) - self.master.bind("", self.mouse_wheel) # Mouse Wheel - self.master.bind("", self.mouse_wheel) # Mouse Wheel Linux - self.master.bind("", self.mouse_wheel) # Mouse Wheel Linux - #self.master.bind("", self.enter_key) # Enter Key - self.master.bind("", self.undo) # undo - self.master.bind("", self.redo) # redo - self.master.bind("", self.undo) # undo on macs - self.master.bind("", self.redo) # redo on macs - self.master.bind("", self.menu_open_clicked) - self.master.bind("", self.calculate) - self.master.bind("", self.save_image) - self.master.bind("", self.menu_open_clicked) - self.master.bind("", self.calculate) - self.master.bind("", self.save_image) - - - #Side menu - heading_font = "Verdana 11 bold" - - self.side_canvas = tk.Canvas(self.master, borderwidth=0, bd=0, highlightthickness=0, name="left_panel") - self.side_canvas.pack(side=tk.TOP, fill=tk.Y, expand=True) - - self.scrollbar = ttk.Scrollbar(self.canvas, orient=tk.VERTICAL, command=self.side_canvas.yview) - self.scrollbar.pack(side=tk.LEFT, fill=tk.Y) - - scal = get_scaling_factor()*0.75 - self.side_menu = tk.Frame(self.side_canvas, borderwidth=0) - - #Crop menu - self.crop_menu = CollapsibleFrame(self.side_menu, text=_("Crop") + " ") - self.crop_menu.grid(column=0, row=0, pady=(20*scal,5*scal), padx=15*scal, sticky="news") - self.crop_menu.sub_frame.grid_columnconfigure(0, weight=1) - - for i in range(2): - self.crop_menu.sub_frame.grid_rowconfigure(i, weight=1) - - self.cropmode_button = ttk.Button(self.crop_menu.sub_frame, - text=_("Crop mode on/off"), - command=self.toggle_crop_mode, - ) - self.cropmode_button.grid(column=0, row=0, pady=(20*scal,5*scal), padx=15*scal, sticky="news") - - self.cropapply_button = ttk.Button(self.crop_menu.sub_frame, - text=_("Apply crop"), - command=self.crop_apply, - ) - self.cropapply_button.grid(column=0, row=1, pady=(5*scal,20*scal), padx=15*scal, sticky="news") - - - #Background extraction menu - self.bgextr_menu = CollapsibleFrame(self.side_menu, text=_("Background Extraction") + " ") - self.bgextr_menu.grid(column=0, row=1, pady=(5*scal,20*scal), padx=15*scal, sticky="news") - self.bgextr_menu.sub_frame.grid_columnconfigure(0, weight=1) - - for i in range(21): - self.bgextr_menu.sub_frame.grid_rowconfigure(i, weight=1) - - #---Open Image--- - num_pic = ImageTk.PhotoImage(file=temp_resource_path("img/gfx_number_1-scaled.png")) - text = tk.Label(self.bgextr_menu.sub_frame, text=_(" Loading"), image=num_pic, font=heading_font, compound="left") - text.image = num_pic - text.grid(column=0, row=0, pady=(20*scal,5*scal), padx=0, sticky="w") - - self.load_image_button = ttk.Button(self.bgextr_menu.sub_frame, - text=_("Load Image"), - command=self.menu_open_clicked, - style="Accent.TButton" - ) - tt_load = tooltip.Tooltip(self.load_image_button, text=tooltip.load_text) - self.load_image_button.grid(column=0, row=1, pady=(5*scal,30*scal), padx=15*scal, sticky="news") - - #--Stretch Options-- - num_pic = ImageTk.PhotoImage(file=temp_resource_path("img/gfx_number_2-scaled.png")) - text = tk.Label(self.bgextr_menu.sub_frame, text=_(" Stretch Options"), image=num_pic, font=heading_font, compound="left") - text.image = num_pic - text.grid(column=0, row=2, pady=5*scal, padx=0, sticky="w") - - self.stretch_options = ["No Stretch", "10% Bg, 3 sigma", "15% Bg, 3 sigma", "20% Bg, 3 sigma", "30% Bg, 2 sigma"] - self.stretch_option_current = tk.StringVar() - self.stretch_option_current.set(self.stretch_options[0]) - if "stretch_option" in self.prefs: - self.stretch_option_current.set(self.prefs["stretch_option"]) - self.stretch_menu = ttk.OptionMenu(self.bgextr_menu.sub_frame, self.stretch_option_current, self.stretch_option_current.get(), *self.stretch_options, command=self.change_stretch) - self.stretch_menu.grid(column=0, row=3, pady=(5*scal,5*scal), padx=15*scal, sticky="news") - tt_stretch= tooltip.Tooltip(self.stretch_menu, text=tooltip.stretch_text) - - self.saturation = tk.DoubleVar() - self.saturation.set(1.0) - if "saturation" in self.prefs: - self.saturation.set(self.prefs["saturation"]) - - self.saturation_slider = Slider(self.bgextr_menu.sub_frame, self.saturation, "Saturation", 0, 3, 1, scal, self.update_saturation) - self.saturation_slider.grid(column=0, row=4, pady=(5*scal,30*scal), padx=15*scal, sticky="ew") - - - #---Sample Selection--- - num_pic = ImageTk.PhotoImage(file=temp_resource_path("img/gfx_number_3-scaled.png")) - text = tk.Label(self.bgextr_menu.sub_frame, text=_(" Sample Selection"), image=num_pic, font=heading_font, compound="left") - text.image = num_pic - text.grid(column=0, row=5, pady=5*scal, padx=0, sticky="w") - - self.display_pts = tk.BooleanVar() - self.display_pts.set(True) - self.display_pts_switch = ttk.Checkbutton(self.bgextr_menu.sub_frame, text=" "+_("Display points"), compound=tk.LEFT, var=self.display_pts, command=self.redraw_points) - self.display_pts_switch.grid(column=0, row=6, pady=(5*scal,5*scal), padx=15*scal, sticky="ews") - - self.flood_select_pts = tk.BooleanVar() - self.flood_select_pts.set(False) - if "bg_flood_selection_option" in self.prefs: - self.flood_select_pts.set(self.prefs["bg_flood_selection_option"]) - self.flood_select_pts_switch = ttk.Checkbutton(self.bgextr_menu.sub_frame, text=" "+_("Flooded generation"), compound=tk.LEFT, var=self.flood_select_pts) - tt_load = tooltip.Tooltip(self.flood_select_pts_switch, text=tooltip.bg_flood_text) - self.flood_select_pts_switch.grid(column=0, row=7, pady=(5*scal,5*scal), padx=15*scal, sticky="ews") - - self.bg_pts = tk.IntVar() - self.bg_pts.set(10) - if "bg_pts_option" in self.prefs: - self.bg_pts.set(self.prefs["bg_pts_option"]) - - self.bg_pts_slider = Slider(self.bgextr_menu.sub_frame, self.bg_pts, "Points per row", 4, 25, 0, scal) - self.bg_pts_slider.grid(column=0, row=8, pady=(5*scal,5*scal), padx=15*scal, sticky="ew") - tt_bg_points= tooltip.Tooltip(self.bg_pts_slider, text=tooltip.num_points_text) - - self.bg_tol = tk.DoubleVar() - self.bg_tol.set(1) - if "bg_tol_option" in self.prefs: - self.bg_tol.set(self.prefs["bg_tol_option"]) - - self.bg_tol_slider = Slider(self.bgextr_menu.sub_frame, self.bg_tol, "Grid Tolerance", -2, 10, 1, scal) - self.bg_tol_slider.grid(column=0, row=9, pady=(5*scal,10*scal), padx=15*scal, sticky="ew") - tt_tol_points= tooltip.Tooltip(self.bg_tol_slider, text=tooltip.bg_tol_text) - - self.bg_selection_button = ttk.Button(self.bgextr_menu.sub_frame, - text=_("Create Grid"), - command=self.select_background) - self.bg_selection_button.grid(column=0, row=10, pady=5*scal, padx=15*scal, sticky="news") - tt_bg_select = tooltip.Tooltip(self.bg_selection_button, text= tooltip.bg_select_text) - - self.reset_button = ttk.Button(self.bgextr_menu.sub_frame, - text=_("Reset Sample Points"), - command=self.reset_backgroundpts) - self.reset_button.grid(column=0, row=11, pady=(5*scal,30*scal), padx=15*scal, sticky="news") - tt_reset= tooltip.Tooltip(self.reset_button, text=tooltip.reset_text) - - #---Calculation--- - num_pic = ImageTk.PhotoImage(file=temp_resource_path("img/gfx_number_4-scaled.png")) - text = tk.Label(self.bgextr_menu.sub_frame, text=_(" Calculation"), image=num_pic, font=heading_font, compound="left") - text.image = num_pic - text.grid(column=0, row=12, pady=5*scal, padx=0, sticky="w") - - self.intp_type_text = tk.Message(self.bgextr_menu.sub_frame, text=_("Interpolation Method:")) - self.intp_type_text.config(width=500) - self.intp_type_text.grid(column=0, row=13, pady=(5*scal,5*scal), padx=15*scal, sticky="ews") - - self.interpol_options = ["RBF", "Splines", "Kriging", "AI"] - self.interpol_type = tk.StringVar() - self.interpol_type.set(self.interpol_options[0]) - if "interpol_type_option" in self.prefs: - self.interpol_type.set(self.prefs["interpol_type_option"]) - self.interpol_menu = ttk.OptionMenu(self.bgextr_menu.sub_frame, self.interpol_type, self.interpol_type.get(), *self.interpol_options) - self.interpol_menu.grid(column=0, row=14, pady=(0,5*scal), padx=15*scal, sticky="news") - tt_interpol_type= tooltip.Tooltip(self.interpol_menu, text=tooltip.interpol_type_text) - - self.smoothing = tk.DoubleVar() - self.smoothing.set(1.0) - if "smoothing_option" in self.prefs: - self.smoothing.set(self.prefs["smoothing_option"]) - - self.smoothing_slider = Slider(self.bgextr_menu.sub_frame, self.smoothing, "Smoothing", 0, 1, 2, scal) - self.smoothing_slider.grid(column=0, row=15, pady=(0,10*scal), padx=15*scal, sticky="ew") - tt_smoothing= tooltip.Tooltip(self.smoothing_slider, text=tooltip.smoothing_text) - - self.calculate_button = ttk.Button(self.bgextr_menu.sub_frame, - text=_("Calculate Background"), - command=self.calculate, - style="Accent.TButton") - self.calculate_button.grid(column=0, row=16, pady=(5*scal,30*scal), padx=15*scal, sticky="news") - tt_calculate= tooltip.Tooltip(self.calculate_button, text=tooltip.calculate_text) - - #---Saving--- - num_pic = ImageTk.PhotoImage(file=temp_resource_path("img/gfx_number_5-scaled.png")) - self.saveas_text = tk.Label(self.bgextr_menu.sub_frame, text=_(" Saving"), image=num_pic, font=heading_font, compound="left") - self.saveas_text.image = num_pic - self.saveas_text.grid(column=0, row=17, pady=5*scal, padx=0, sticky="w") - - self.saveas_options = ["16 bit Tiff", "32 bit Tiff", "16 bit Fits", "32 bit Fits", "16 bit XISF", "32 bit XISF"] - self.saveas_type = tk.StringVar() - self.saveas_type.set(self.saveas_options[0]) - if "saveas_option" in self.prefs: - self.saveas_type.set(self.prefs["saveas_option"]) - self.saveas_menu = ttk.OptionMenu(self.bgextr_menu.sub_frame, self.saveas_type, self.saveas_type.get(), *self.saveas_options) - self.saveas_menu.grid(column=0, row=18, pady=(5*scal,20*scal), padx=15*scal, sticky="news") - tt_interpol_type= tooltip.Tooltip(self.saveas_menu, text=tooltip.saveas_text) - - self.save_button = ttk.Button(self.bgextr_menu.sub_frame, - text=_("Save Processed"), - command=self.save_image, - style="Accent.TButton") - self.save_button.grid(column=0, row=19, pady=5*scal, padx=15*scal, sticky="news") - tt_save_pic= tooltip.Tooltip(self.save_button, text=tooltip.save_pic_text) - - self.save_background_button = ttk.Button(self.bgextr_menu.sub_frame, - text=_("Save Background"), - command=self.save_background_image) - self.save_background_button.grid(column=0, row=20, pady=5*scal, padx=15*scal, sticky="news") - tt_save_bg = tooltip.Tooltip(self.save_background_button, text=tooltip.save_bg_text) - - self.save_stretched_button = ttk.Button(self.bgextr_menu.sub_frame, - text=_("Save Stretched & Processed"), - command=self.save_stretched_image) - self.save_stretched_button.grid(column=0, row=21, pady=(5*scal,10*scal), padx=15*scal, sticky="news") - tt_save_pic= tooltip.Tooltip(self.save_stretched_button, text=tooltip.save_stretched_pic_text) - - - self.side_canvas.create_window((0,0), window=self.side_menu) - self.side_canvas.configure(yscrollcommand=self.scrollbar.set) - self.side_canvas.bind('', lambda e: self.side_canvas.configure(scrollregion=self.side_canvas.bbox("all"))) - self.side_menu.update() - width = self.side_menu.winfo_width() - self.side_canvas.configure(width=width) - self.side_canvas.yview_moveto("0.0") - - - def menu_open_clicked(self, event=None, filename=None): - - if self.prefs["working_dir"] != "" and os.path.exists(self.prefs["working_dir"]): - initialdir = self.prefs["working_dir"] - else: - initialdir = os.getcwd() - - if filename is None: - filename = tk.filedialog.askopenfilename( - filetypes = [("Image file", ".bmp .png .jpg .tif .tiff .fit .fits .fts .xisf"), - ("Bitmap", ".bmp"), ("PNG", ".png"), ("JPEG", ".jpg"), ("Tiff", ".tif .tiff"), ("Fits", ".fit .fits .fts"), ("XISF", ".xisf")], - initialdir = initialdir - ) - - if filename == "": - return - - self.loading_frame.start() - self.data_type = os.path.splitext(filename)[1] - - try: - image = AstroImage(self.stretch_option_current, self.saturation) - image.set_from_file(filename) - self.images["Original"] = image - self.prefs["working_dir"] = os.path.dirname(filename) - - except Exception as e: - msg = _("An error occurred while loading your picture.") - logging.exception(msg) - messagebox.showerror("Error", _(msg)) - - - self.display_type.set("Original") - self.images["Processed"] = None - self.images["Background"] = None - - self.master.title(self.my_title + " - " + os.path.basename(filename)) - self.filename = os.path.splitext(os.path.basename(filename))[0] - - width = self.images["Original"].img_display.width - height = self.images["Original"].img_display.height - mode = self.images["Original"].img_display.mode - self.label_image_info["text"] = f"{self.data_type} : {width} x {height} {mode}" - - os.chdir(os.path.dirname(filename)) - - if self.prefs["width"] != width or self.prefs["height"] != height: - self.reset_backgroundpts() - - self.prefs["width"] = width - self.prefs["height"] = height - - tmp_state = fitsheader_2_app_state(self, self.cmd.app_state, self.images["Original"].fits_header) - self.cmd: Command = Command(INIT_HANDLER, background_points=tmp_state["background_points"]) - self.cmd.execute() - - self.zoom_fit(width, height) - self.redraw_image() - self.loading_frame.end() - return - - def toggle_crop_mode(self): - - if self.images["Original"] is None: - messagebox.showerror("Error", _("Please load your picture first.")) - return - - self.startx = 0 - self.starty = 0 - self.endx = self.images["Original"].width - self.endy = self.images["Original"].height - - if(self.crop_mode): - self.crop_mode = False - else: - self.crop_mode = True - - self.redraw_points() - - def crop_apply(self): - - if (not self.crop_mode): - return - - for astroimg in self.images.values(): - if(astroimg is not None): - astroimg.crop(self.startx, self.endx, self.starty, self.endy) - - self.reset_backgroundpts() - self.crop_mode = False - self.zoom_fit(self.images[self.display_type.get()].width, self.images[self.display_type.get()].height) - self.redraw_image() - self.redraw_points() - return - - - - def select_background(self,event=None): - - if self.images["Original"] is None: - messagebox.showerror("Error", _("Please load your picture first.")) - return - - self.loading_frame.start() - self.cmd = Command(SEL_POINTS_HANDLER, self.cmd, data=self.images["Original"].img_array, - num_pts=self.bg_pts.get(), tol=self.bg_tol.get(), sample_size=self.sample_size.get()) - self.cmd.execute() - self.redraw_image() - self.loading_frame.end() - return - - def change_stretch(self,event=None): - self.loading_frame.start() - - all_images = [] - stretches = [] - for img in self.images.values(): - if(img is not None): - all_images.append(img.img_array) - if len(all_images) > 0: - stretch_params = self.images["Original"].get_stretch() - stretches = stretch_all(all_images, stretch_params) - for idx, img in enumerate(self.images.values()): - if(img is not None): - img.update_display_from_array(stretches[idx]) - self.loading_frame.end() - - self.redraw_image() - return - - - def update_saturation(self, event=None): - for img in self.images.values(): - if img is not None: - img.update_saturation() - - self.redraw_image() - - - def save_image(self, event=None): - - - if(self.saveas_type.get() == "16 bit Tiff" or self.saveas_type.get() == "32 bit Tiff"): - dir = tk.filedialog.asksaveasfilename( - initialfile = self.filename + "_GraXpert.tiff", - filetypes = [("Tiff", ".tiff")], - defaultextension = ".tiff", - initialdir = self.prefs["working_dir"] - ) - elif(self.saveas_type.get() == "16 bit XISF" or self.saveas_type.get() == "32 bit XISF"): - dir = tk.filedialog.asksaveasfilename( - initialfile = self.filename + "_GraXpert.xisf", - filetypes = [("XISF", ".xisf")], - defaultextension = ".xisf", - initialdir = self.prefs["working_dir"] - ) - else: - dir = tk.filedialog.asksaveasfilename( - initialfile = self.filename + "_GraXpert.fits", - filetypes = [("Fits", ".fits")], - defaultextension = ".fits", - initialdir = self.prefs["working_dir"] - ) - - if(dir == ""): - return - - self.loading_frame.start() - - try: - self.images["Processed"].save(dir, self.saveas_type.get()) - except: - messagebox.showerror("Error", _("Error occured when saving the image.")) - - self.loading_frame.end() - - def save_stretched_image(self): - - - if(self.saveas_type.get() == "16 bit Tiff" or self.saveas_type.get() == "32 bit Tiff"): - dir = tk.filedialog.asksaveasfilename( - initialfile = self.filename + "_stretched_GraXpert.tiff", - filetypes = [("Tiff", ".tiff")], - defaultextension = ".tiff", - initialdir = self.prefs["working_dir"] - ) - elif(self.saveas_type.get() == "16 bit XISF" or self.saveas_type.get() == "32 bit XISF"): - dir = tk.filedialog.asksaveasfilename( - initialfile = self.filename + "_stretched_GraXpert.xisf", - filetypes = [("XISF", ".xisf")], - defaultextension = ".xisf", - initialdir = self.prefs["working_dir"] - ) - else: - dir = tk.filedialog.asksaveasfilename( - initialfile = self.filename + "_stretched_GraXpert.fits", - filetypes = [("Fits", ".fits")], - defaultextension = ".fits", - initialdir = self.prefs["working_dir"] - ) - - if(dir == ""): - return - - self.loading_frame.start() - - try: - if self.images["Processed"] is None: - self.images["Original"].save_stretched(dir, self.saveas_type.get()) - else: - self.images["Processed"].save_stretched(dir, self.saveas_type.get()) - except: - messagebox.showerror("Error", _("Error occured when saving the image.")) - - self.loading_frame.end() - - def save_background_image(self): - - - if(self.saveas_type.get() == "16 bit Tiff" or self.saveas_type.get() == "32 bit Tiff"): - dir = tk.filedialog.asksaveasfilename( - initialfile = self.filename + "_background.tiff", - filetypes = [("Tiff", ".tiff")], - defaultextension = ".tiff", - initialdir = self.prefs["working_dir"] - ) - elif(self.saveas_type.get() == "16 bit XISF" or self.saveas_type.get() == "32 bit XISF"): - dir = tk.filedialog.asksaveasfilename( - initialfile = self.filename + "_background.xisf", - filetypes = [("XISF", ".xisf")], - defaultextension = ".xisf", - initialdir = self.prefs["working_dir"] - ) - else: - dir = tk.filedialog.asksaveasfilename( - initialfile = self.filename + "_background.fits", - filetypes = [("Fits", ".fits")], - defaultextension = ".fits", - initialdir = os.getcwd() - ) - - if(dir == ""): - return - - self.loading_frame.start() - - try: - self.images["Background"].save(dir, self.saveas_type.get()) - except: - messagebox.showerror("Error", _("Error occured when saving the image.")) - - self.loading_frame.end() - - - def reset_backgroundpts(self): - - if len(self.cmd.app_state["background_points"]) > 0: - self.cmd = Command(RESET_POINTS_HANDLER, self.cmd) - self.cmd.execute() - self.redraw_image() - - def calculate(self, event=None): - - if self.images["Original"] is None: - messagebox.showerror("Error", _("Please load your picture first.")) - return - - background_points = self.cmd.app_state["background_points"] - - #Error messages if not enough points - if(len(background_points) == 0 and self.interpol_type.get() != 'AI'): - messagebox.showerror("Error", _("Please select background points with left click.")) - return - - if(len(background_points) < 2 and self.interpol_type.get() == "Kriging"): - messagebox.showerror("Error", _("Please select at least 2 background points with left click for the Kriging method.")) - return - - if(len(background_points) < 16 and self.interpol_type.get() == "Splines"): - messagebox.showerror("Error", _("Please select at least 16 background points with left click for the Splines method.")) - return - - if(self.interpol_type.get() == 'AI'): - if not self.validate_ai_installation(): - return - - loading_frame = DynamicProgressFrame(self.master, label_lext=_("Extracting Background")) - def callback(p): - loading_frame.update_progress(p) - progress = DynamicProgressThread(callback=callback) - - imarray = np.copy(self.images["Original"].img_array) - - downscale_factor = 1 - - if(self.interpol_type.get() == "Kriging" or self.interpol_type.get() == "RBF"): - downscale_factor = 4 - - - try: - self.images["Background"] = AstroImage(self.stretch_option_current, self.saturation) - self.images["Background"].set_from_array(background_extraction.extract_background( - imarray,np.array(background_points), - self.interpol_type.get(),self.smoothing.get(), - downscale_factor, self.sample_size.get(), - self.RBF_kernel.get(),self.spline_order.get(), - self.corr_type.get(), ai_model_path_from_version(self.ai_version.get()), - progress - )) - - self.images["Processed"] = AstroImage(self.stretch_option_current, self.saturation) - self.images["Processed"].set_from_array(imarray) - - # Update fits header and metadata - background_mean = np.mean(self.images["Background"].img_array) - self.images["Processed"].update_fits_header(self.images["Original"].fits_header, background_mean, self, self.cmd.app_state) - self.images["Background"].update_fits_header(self.images["Original"].fits_header, background_mean, self, self.cmd.app_state) - - self.images["Processed"].copy_metadata(self.images["Original"]) - self.images["Background"].copy_metadata(self.images["Original"]) - - all_images = [self.images["Original"].img_array, self.images["Processed"].img_array, self.images["Background"].img_array] - stretches = stretch_all(all_images, self.images["Original"].get_stretch()) - self.images["Original"].update_display_from_array(stretches[0]) - self.images["Processed"].update_display_from_array(stretches[1]) - self.images["Background"].update_display_from_array(stretches[2]) - - self.display_type.set("Processed") - self.redraw_image() - except Exception as e: - logging.exception(e) - messagebox.showerror("Error", _("An error occured during background calculation. Please see the log at {}.".format(logfile_name))) - finally: - progress.done_progress() - loading_frame.close() - - return - - def enter_key(self,enter): - - self.calculate() - - - def mouse_down_left(self,event): - - self.left_drag_timer = -1 - if(str(event.widget).split(".")[-1] != "picture" or self.images["Original"] is None): - return - - self.clicked_inside_pt = False - point_im = self.to_image_point(event.x,event.y) - - if len(self.cmd.app_state["background_points"]) != 0 and len(point_im) != 0 and self.display_pts.get(): - - eventx_im = point_im[0] - eventy_im = point_im[1] - - background_points = self.cmd.app_state["background_points"] - - min_idx = -1 - min_dist = -1 - - for i in range(len(background_points)): - x_im = background_points[i][0] - y_im = background_points[i][1] - - dist = np.max(np.abs([x_im-eventx_im, y_im-eventy_im])) - - if(min_idx == -1 or dist < min_dist): - min_dist = dist - min_idx = i - - - if(min_idx != -1 and min_dist <= self.sample_size.get()): - self.clicked_inside_pt = True - self.clicked_inside_pt_idx = min_idx - self.clicked_inside_pt_coord = self.cmd.app_state["background_points"][min_idx] - - if(self.crop_mode): - #Check if inside circles to move crop corners - corner1 = self.to_canvas_point(self.startx, self.starty) - corner2 = self.to_canvas_point(self.endx, self.endy) - if((event.x - corner1[0])**2 + (event.y - corner1[1])**2 < 15**2 or (event.x - corner2[0])**2 + (event.y - corner2[1])**2 < 15**2): - self.clicked_inside_pt = True - - self.__old_event = event - - - def mouse_release_left(self,event): - - if(str(event.widget).split(".")[-1] != "picture" or self.images["Original"] is None or not self.display_pts.get()): - return - - - if self.clicked_inside_pt and not self.crop_mode: - new_point = self.to_image_point(event.x,event.y) - self.cmd.app_state["background_points"][self.clicked_inside_pt_idx] = self.clicked_inside_pt_coord - self.cmd = Command(MOVE_POINT_HANDLER, prev=self.cmd, new_point=new_point, idx=self.clicked_inside_pt_idx) - self.cmd.execute() - - - elif(len(self.to_image_point(event.x,event.y)) != 0 and (event.time - self.left_drag_timer < 100 or self.left_drag_timer == -1)): - - point = self.to_image_point(event.x,event.y) - - if not self.flood_select_pts.get(): - self.cmd = Command(ADD_POINT_HANDLER, prev=self.cmd, point=point) - else: - self.cmd = Command( - ADD_POINTS_HANDLER, - prev=self.cmd, - point=point, - tol=self.bg_tol.get(), - bg_pts=self.bg_pts.get(), - sample_size=self.sample_size.get(), - image=self.images["Original"] - ) - self.cmd.execute() - - - self.redraw_points() - self.__old_event = event - self.left_drag_timer = -1 - - def mouse_move_left(self, event): - - if(str(event.widget).split(".")[-1] != "picture" or self.images["Original"] is None): - return - - if (self.images[self.display_type.get()] is None): - return - - if(self.left_drag_timer == -1): - self.left_drag_timer = event.time - - if(self.clicked_inside_pt and self.display_pts.get() and not self.crop_mode): - new_point = self.to_image_point(event.x, event.y) - if len(new_point) != 0: - self.cmd.app_state["background_points"][self.clicked_inside_pt_idx] = new_point - - self.redraw_points() - - elif(self.clicked_inside_pt and self.crop_mode): - new_point = self.to_image_point_pinned(event.x, event.y) - corner1_canvas = self.to_canvas_point(self.startx, self.starty) - corner2_canvas = self.to_canvas_point(self.endx, self.endy) - - dist1 = (event.x - corner1_canvas[0])**2 + (event.y - corner1_canvas[1])**2 - dist2 = (event.x - corner2_canvas[0])**2 + (event.y - corner2_canvas[1])**2 - if(dist1 < dist2): - self.startx = int(new_point[0]) - self.starty = int(new_point[1]) - else: - self.endx = int(new_point[0]) - self.endy = int(new_point[1]) - - self.redraw_points() - - else: - if(event.time - self.left_drag_timer >= 100): - self.translate(event.x - self.__old_event.x, event.y - self.__old_event.y) - self.redraw_image() - - - self.mouse_move(event) - self.__old_event = event - return - - - def remove_pt(self,event): - - if len(self.cmd.app_state["background_points"]) == 0 or not self.display_pts.get(): - return False - - point_im = self.to_image_point(event.x,event.y) - if len(point_im) == 0: - return False - - eventx_im = point_im[0] - eventy_im = point_im[1] - - background_points = self.cmd.app_state["background_points"] - - min_idx = -1 - min_dist = -1 - - for i in range(len(background_points)): - x_im = background_points[i][0] - y_im = background_points[i][1] - - dist = np.max(np.abs([x_im-eventx_im, y_im-eventy_im])) - - if(min_idx == -1 or dist < min_dist): - min_dist = dist - min_idx = i - - - if(min_idx != -1 and min_dist <= self.sample_size.get()): - point = background_points[min_idx] - self.cmd = Command(RM_POINT_HANDLER, self.cmd, idx=min_idx, point=point) - self.cmd.execute() - return True - else: - return False - - - def mouse_down_right(self, event): - - if(str(event.widget).split(".")[-1] != "picture" or self.images["Original"] is None or not self.display_pts.get()): - return - - self.remove_pt(event) - self.redraw_points() - self.__old_event = event - - - - - def mouse_move(self, event): - - if (self.images[self.display_type.get()] is None): - return - - image_point = self.to_image_point(event.x, event.y) - if len(image_point) != 0: - text = "x=" + f"{image_point[0]:.2f}" + ",y=" + f"{image_point[1]:.2f} " - if(self.images[self.display_type.get()].img_array.shape[2] == 3): - R, G, B = self.images[self.display_type.get()].get_local_median(image_point) - text = text + "RGB = (" + f"{R:.4f}," + f"{G:.4f}," + f"{B:.4f})" - - if(self.images[self.display_type.get()].img_array.shape[2] == 1): - L = self.images[self.display_type.get()].get_local_median(image_point) - text = text + "L= " + f"{L:.4f}" - - self.label_image_pixel["text"] = text - else: - self.label_image_pixel["text"] = ("(--, --)") - - - def reset_zoom(self, event): - - if self.images[self.display_type.get()] is None: - return - self.zoom_fit(self.images[self.display_type.get()].width, self.images[self.display_type.get()].height) - self.redraw_image() - - - def mouse_wheel(self, event): - - if "help_canvas" in str(event.widget): - if self.help_panel.help_canvas.yview() == (0.0,1.0): - return - - if (event.delta > 0 or event.num == 4): - self.help_panel.help_canvas.yview_scroll(-1, "units") - else: - self.help_panel.help_canvas.yview_scroll(1, "units") - - elif "advanced_canvas" in str(event.widget): - if self.help_panel.advanced_canvas.yview() == (0.0,1.0): - return - - if (event.delta > 0 or event.num == 4): - self.help_panel.advanced_canvas.yview_scroll(-1, "units") - else: - self.help_panel.advanced_canvas.yview_scroll(1, "units") - - elif "left_panel" in str(event.widget): - if self.side_canvas.yview() == (0.0,1.0): - return - - if (event.delta > 0 or event.num == 4): - self.side_canvas.yview_scroll(-1, "units") - else: - self.side_canvas.yview_scroll(1, "units") - - elif "picture" in str(event.widget): - if self.images[self.display_type.get()] is None: - return - - if (event.delta > 0 or event.num == 4): - - self.scale_at(6/5, event.x, event.y) - else: - - self.scale_at(5/6, event.x, event.y) - - self.redraw_image() - - - - def reset_transform(self): - - self.mat_affine = np.eye(3) - - def translate(self, offset_x, offset_y): - - mat = np.eye(3) - mat[0, 2] = float(offset_x) - mat[1, 2] = float(offset_y) - - self.mat_affine = np.dot(mat, self.mat_affine) - - def scale(self, scale:float): - - mat = np.eye(3) - mat[0, 0] = scale - mat[1, 1] = scale - - self.mat_affine = np.dot(mat, self.mat_affine) - - def scale_at(self, scale:float, cx:float, cy:float): - - - - self.translate(-cx, -cy) - self.scale(scale) - self.translate(cx, cy) - - - - def zoom_fit(self, image_width, image_height): - - - canvas_width = self.canvas.winfo_width() - canvas_height = self.canvas.winfo_height() - - if (image_width * image_height <= 0) or (canvas_width * canvas_height <= 0): - return - - - self.reset_transform() - - scale = 1.0 - offsetx = 0.0 - offsety = 0.0 - - if (canvas_width * image_height) > (image_width * canvas_height): - - scale = canvas_height / image_height - offsetx = (canvas_width - image_width * scale) / 2 - else: - - scale = canvas_width / image_width - offsety = (canvas_height - image_height * scale) / 2 - - - self.scale(scale) - self.translate(offsetx, offsety) - - def to_image_point(self, x, y): - - if self.images[self.display_type.get()] is None: - return [] - - mat_inv = np.linalg.inv(self.mat_affine) - image_point = np.dot(mat_inv, (x, y, 1.)) - - width = self.images[self.display_type.get()].width - height = self.images[self.display_type.get()].height - - if image_point[0] < 0 or image_point[1] < 0 or image_point[0] > width or image_point[1] > height: - return [] - - return image_point - - def to_image_point_pinned(self, x, y): - - if self.images[self.display_type.get()] is None: - return [] - - mat_inv = np.linalg.inv(self.mat_affine) - image_point = np.dot(mat_inv, (x, y, 1.)) - - width = self.images[self.display_type.get()].width - height = self.images[self.display_type.get()].height - - if image_point[0] < 0: - image_point[0] = 0 - if image_point[1] < 0: - image_point[1] = 0 - if image_point[0] > width: - image_point[0] = width - if image_point[1] > height: - image_point[1] = height - - return image_point - - def to_canvas_point(self, x, y): - - return np.dot(self.mat_affine,(x,y,1.)) - - def draw_image(self, pil_image): - - if pil_image is None: - return - - - canvas_width = self.canvas.winfo_width() - canvas_height = self.canvas.winfo_height() - - - mat_inv = np.linalg.inv(self.mat_affine) - - - affine_inv = ( - mat_inv[0, 0], mat_inv[0, 1], mat_inv[0, 2], - mat_inv[1, 0], mat_inv[1, 1], mat_inv[1, 2] - ) - - - dst = pil_image.transform( - (canvas_width, canvas_height), - Image.AFFINE, - affine_inv, - Image.BILINEAR - ) - - im = ImageTk.PhotoImage(image=dst) - - - item = self.canvas.create_image( - 0, 0, - anchor='nw', - image=im - ) - - self.image = im - self.redraw_points() - return - - def redraw_points(self): - - if self.images["Original"] is None: - return - - color = hls_to_rgb(self.sample_color.get()/360, 0.5, 1.0) - color = (int(color[0]*255), int(color[1]*255), int(color[2]*255)) - color = '#%02x%02x%02x' % color - - self.canvas.delete("sample") - self.canvas.delete("crop") - rectsize = self.sample_size.get() - background_points = self.cmd.app_state["background_points"] - - if self.display_pts.get() and not self.crop_mode: - for point in background_points: - corner1 = self.to_canvas_point(point[0]-rectsize,point[1]-rectsize) - corner2 = self.to_canvas_point(point[0]+rectsize,point[1]+rectsize) - self.canvas.create_rectangle(corner1[0],corner1[1], corner2[0],corner2[1],outline=color, width=2, tags="sample") - - if self.crop_mode: - corner1 = self.to_canvas_point(self.startx, self.starty) - corner2 = self.to_canvas_point(self.endx, self.endy) - self.canvas.create_rectangle(corner1[0],corner1[1], corner2[0],corner2[1], outline=color, width=2, tags="crop") - self.canvas.create_oval(corner1[0]-15,corner1[1]-15, corner1[0]+15,corner1[1]+15, outline=color, width=2, tags="crop") - self.canvas.create_oval(corner2[0]-15,corner2[1]-15, corner2[0]+15,corner2[1]+15, outline=color, width=2, tags="crop") - return - - def redraw_image(self): - - if self.images[self.display_type.get()] is None: - return - self.draw_image(self.images[self.display_type.get()].img_display_saturated) - - def undo(self, event): - if not type(self.cmd.handler) is InitHandler: - undo = self.cmd.undo() - self.cmd = undo - self.redraw_points() - - def redo(self, event): - if self.cmd.next is not None: - redo = self.cmd.redo() - self.cmd = redo - self.redraw_points() - - def switch_display(self, event): - if(self.images["Processed"] is None and self.display_type.get() != "Original"): - self.display_type.set("Original") - messagebox.showerror("Error", _("Please select background points and press the Calculate button first")) - return - - self.loading_frame.start() - self.redraw_image() - self.loading_frame.end() - - def validate_ai_installation(self): - if self.ai_version is None or self.ai_version.get() == "None": - messagebox.showerror("Error", _("No AI-Model selected. Please select one from the Advanced panel on the right.")) - return False - - if not validate_local_version(self.ai_version.get()): - if not messagebox.askyesno(_("Install AI-Model?"), _("Selected AI-Model is not installed. Should I download it now?")): - return False - else: - progress_frame = DynamicProgressFrame(self.master, label_lext=_("Downloading AI-Model")) - def callback(p): - progress_frame.update_progress(p) - download_version(self.ai_version.get(), progress=callback) - progress_frame.close() - return True - - def on_closing(self, logging_thread): - - - self.prefs = app_state_2_prefs(self.prefs, self.cmd.app_state, self) - - prefs_filename = os.path.join(user_config_dir(appname="GraXpert"), "preferences.json") - save_preferences(prefs_filename, self.prefs) - try: - executor.shutdown(cancel_futures=True) - except Exception as e: - logging.exception("error shutting down ProcessPoolExecutor") - shutdown_logging(logging_thread) - root.destroy() - sys.exit(0) - - -logging_thread = initialize_logging() - -root = hdpitk.HdpiTk() -scaling = get_scaling_factor() - -try: - shutil.copy(resource_path("forest-dark.tcl"), temp_resource_path("forest-dark.tcl")) - shutil.copytree(resource_path("forest-dark"), temp_resource_path("forest-dark")) -except OSError as exc: - logging.exception("Error preparing temporary ressource, exiting") - sys.exit(1) - -scale_img("forest-dark/vert-hover.png", scaling*0.9, (20,10)) -scale_img("forest-dark/vert-basic.png", scaling*0.9, (20,10)) - -scale_img("forest-dark/thumb-hor-accent.png", scaling*0.9, (20,8)) -scale_img("forest-dark/thumb-hor-hover.png", scaling*0.9, (20,8)) -scale_img("forest-dark/thumb-hor-basic.png", scaling*0.9, (20,8)) -scale_img("forest-dark/scale-hor.png", scaling, (20,20)) - -scale_img("forest-dark/check-accent.png", scaling*0.8, (20,20)) -scale_img("forest-dark/check-basic.png", scaling*0.8, (20,20)) -scale_img("forest-dark/check-hover.png", scaling*0.8, (20,20)) -scale_img("forest-dark/check-unsel-accent.png", scaling*0.8, (20,20)) -scale_img("forest-dark/check-unsel-basic.png", scaling*0.8, (20,20)) -scale_img("forest-dark/check-unsel-hover.png", scaling*0.8, (20,20)) -scale_img("forest-dark/check-unsel-pressed.png", scaling*0.8, (20,20)) - -scale_img("img/gfx_number_1.png", scaling*0.7, (25,25)) -scale_img("img/gfx_number_2.png", scaling*0.7, (25,25)) -scale_img("img/gfx_number_3.png", scaling*0.7, (25,25)) -scale_img("img/gfx_number_4.png", scaling*0.7, (25,25)) -scale_img("img/gfx_number_5.png", scaling*0.7, (25,25)) -scale_img("img/hourglass.png", scaling, (25,25)) - -root.tk.call("source", temp_resource_path("forest-dark.tcl")) -style = ttk.Style(root) -style.theme_use("forest-dark") -style.configure("TButton", padding=(8*scaling, 12*scaling, 8*scaling, 12*scaling)) -style.configure("Accent.TButton", padding=(8*scaling, 12*scaling, 8*scaling, 12*scaling)) -style.configure("TMenubutton", padding=(8*scaling, 4*scaling, 4*scaling, 4*scaling)) -root.tk.call("wm", "iconphoto", root._w, tk.PhotoImage(file=resource_path("img/Icon.png"))) -root.tk.call('tk', 'scaling', scaling) -root.option_add("*TkFDialog*foreground", "black") -app = Application(master=root) -root.protocol("WM_DELETE_WINDOW", lambda: app.on_closing(logging_thread)) -root.createcommand("::tk::mac::Quit", lambda: app.on_closing(logging_thread)) - -if '_PYIBoot_SPLASH' in os.environ and importlib.util.find_spec("pyi_splash"): - import pyi_splash - pyi_splash.close() - -check_for_new_version() -app.mainloop() diff --git a/graxpert/help_panel.py b/graxpert/help_panel.py deleted file mode 100644 index d8ef724..0000000 --- a/graxpert/help_panel.py +++ /dev/null @@ -1,404 +0,0 @@ -import sys -import tkinter as tk -from cProfile import label -from os import path -from tkinter import CENTER, messagebox, ttk - -from numpy import pad -from packaging import version -from PIL import Image, ImageTk - -from graxpert.ai_model_handling import (list_local_versions, - list_remote_versions) -from graxpert.localization import _, lang -from graxpert.resource_utils import resource_path, temp_resource_path -from graxpert.slider import Slider -from graxpert.ui_scaling import get_scaling_factor - - -class Help_Panel(): - def __init__(self, master, canvas, app): - - - self.visible = True - self.master = master - self.canvas = canvas - self.app = app - - self.visible_panel = "None" - - self.button_frame = tk.Frame(self.canvas) - - scaling = get_scaling_factor() - - s = ttk.Style(master) - - # Help Button - s.configure("Help.TButton", - borderwidth=0 - ) - s.configure("Help.TLabel", - foreground="#ffffff", - background="#c46f1a", - justify=CENTER, - anchor=CENTER - ) - - self.help_button = ttk.Button(self.button_frame, - style="Help.TButton" - ) - self.help_label = ttk.Label( - self.help_button, - text=_("H\nE\nL\nP"), - style="Help.TLabel", - font=("Menlo","12","bold"), - width=2 - ) - self.help_label.bind("", self.help) - self.help_label.pack( - ipady=int(20 * scaling), - ) - - self.help_button.grid( - row=0, - column=0, - ) - - # Advanced Button - s.configure("Advanced.TButton", - borderwidth=0 - ) - s.configure("Advanced.TLabel", - foreground="#ffffff", - background="#254f69", - justify=CENTER, - anchor=CENTER - ) - - self.advanced_button = ttk.Button(self.button_frame, - style="Advanced.TButton" - ) - self.advanced_label = ttk.Label( - self.advanced_button, - text=_("A\nD\nV\nA\nN\nC\nE\nD"), - style="Advanced.TLabel", - font=("Menlo","12","bold"), - width=2 - ) - self.advanced_label.bind("", self.advanced) - self.advanced_label.pack( - ipady=int(20 * scaling) - ) - - self.advanced_button.grid( - row=1, - column=0 - ) - - - self.button_frame.pack(side=tk.RIGHT) - - # ------------Help Panel----------------- - heading_font = "Verdana 18 bold" - heading_font2 = "Verdana 11 bold" - - - self.help_panel = tk.Frame(self.canvas) - self.help_canvas = tk.Canvas(self.help_panel, borderwidth=0, bd=0, highlightthickness=0, name="help_canvas") - self.help_canvas.pack(side=tk.LEFT, fill=tk.Y, expand=True) - self.help_scrollbar = ttk.Scrollbar(self.help_panel, orient=tk.VERTICAL, command=self.help_canvas.yview) - self.help_scrollbar.pack(side=tk.RIGHT, fill=tk.Y) - - self.help_panel_window = tk.Frame(self.help_canvas, borderwidth=0) - - logo = Image.open(resource_path("img/GraXpert_LOGO_Hauptvariante.png")) - logo = logo.resize(( - int(logo.width/6 * scaling), - int(logo.height/6 * scaling) - )) - - self.help_panel_window.columnconfigure(0, weight=1) - - logo = ImageTk.PhotoImage(logo) - self.label = tk.Label(self.help_panel_window, image=logo) - self.label.image= logo - self.label.grid(column=0, row=0, padx=(40,30), pady=50*scaling) - - # text = tk.Message(self.help_panel, text="Release: '{}' ({})".format(release, version), width=240 * scaling, anchor="center") - # text.grid(column=0, row=1, padx=(40,30), pady=(0,25*scaling), sticky="ew") - - text = tk.Message(self.help_panel_window, text=_("Instructions"), width=240 * scaling, font=heading_font, anchor="center") - text.grid(column=0, row=1, padx=(40,30), pady=(0,10*scaling), sticky="ew") - - num_pic = ImageTk.PhotoImage(file=temp_resource_path("img/gfx_number_1-scaled.png")) - text = tk.Label(self.help_panel_window, text=_(" Loading"), image=num_pic, compound="left", font=heading_font2) - text.image = num_pic - text.grid(column=0, row=2, padx=(40,30), pady=(5*scaling,0), sticky="w") - text = tk.Message(self.help_panel_window, text=_("Load your image."), width=240 * scaling) - text.grid(column=0, row=3, padx=(40,30), pady=(5*scaling,10*scaling), sticky="w") - - - num_pic = ImageTk.PhotoImage(file=temp_resource_path("img/gfx_number_2-scaled.png")) - text = tk.Label(self.help_panel_window, text=_(" Stretch Options"), image=num_pic, compound="left", font=heading_font2) - text.image = num_pic - text.grid(column=0, row=4, padx=(40,30), pady=(5*scaling,0), sticky="w") - text = tk.Message(self.help_panel_window, text=_("Stretch your image if necessary to reveal gradients."), width=240 * scaling) - text.grid(column=0, row=5, padx=(40,30), pady=(5*scaling,10*scaling), sticky="w") - - - num_pic = ImageTk.PhotoImage(file=temp_resource_path("img/gfx_number_3-scaled.png")) - text = tk.Label(self.help_panel_window, text=_(" Sample Selection"), image=num_pic, compound="left", font=heading_font2) - text.image = num_pic - text.grid(column=0, row=6, padx=(40,30), pady=(5*scaling,0), sticky="w") - text = tk.Message( - self.help_panel_window, - text= _("Select background points\n a) manually with left click\n b) automatically via grid (grid selection)" - "\nYou can remove already set points by right clicking on them."), - width=240 * scaling - ) - text.grid(column=0, row=7, padx=(40,30), pady=(5*scaling,10*scaling), sticky="w") - - - num_pic = ImageTk.PhotoImage(file=temp_resource_path("img/gfx_number_4-scaled.png")) - text = tk.Label(self.help_panel_window, text=_(" Calculation"), image=num_pic, compound="left", font=heading_font2) - text.image = num_pic - text.grid(column=0, row=8, padx=(40,30), pady=(5*scaling,0), sticky="w") - text = tk.Message(self.help_panel_window, text=_("Click on Calculate Background to get the processed image."), width=240 * scaling) - text.grid(column=0, row=9, padx=(40,30), pady=(5*scaling,10*scaling), sticky="w") - - - num_pic = ImageTk.PhotoImage(file=temp_resource_path("img/gfx_number_5-scaled.png")) - text = tk.Label(self.help_panel_window, text=_(" Saving"), image=num_pic, compound="left", font=heading_font2) - text.image = num_pic - text.grid(column=0, row=10, padx=(40,30), pady=(5*scaling,0), sticky="w") - text = tk.Message(self.help_panel_window, text=_("Save the processed image."), width=240 * scaling) - text.grid(column=0, row=11, padx=(40,30), pady=(5*scaling,10*scaling), sticky="w") - - text = tk.Message(self.help_panel_window, text=_("Keybindings"), width=240 * scaling, font=heading_font, anchor="center") - text.grid(column=0, row=12, padx=(40,30), pady=(20*scaling,10*scaling), sticky="ew") - - text = tk.Message(self.help_panel_window, text=_("Left click on picture: Set sample point"), width=240 * scaling) - text.grid(column=0, row=13, padx=(40,30), pady=(0,10*scaling), sticky="w") - - text = tk.Message(self.help_panel_window, text=_("Left click on picture + drag: Move picture"), width=240 * scaling) - text.grid(column=0, row=14, padx=(40,30), pady=(0,10*scaling), sticky="w") - - text = tk.Message(self.help_panel_window, text=_("Left click on sample point + drag:\nMove sample point"), width=240 * scaling) - text.grid(column=0, row=15, padx=(40,30), pady=(0,10*scaling), sticky="w") - - text = tk.Message(self.help_panel_window, text=_("Right click on sample point:\nDelete sample point"), width=240 * scaling) - text.grid(column=0, row=16, padx=(40,30), pady=(0,10*scaling), sticky="w") - - text = tk.Message(self.help_panel_window, text=_("Mouse wheel: Zoom"), width=240 * scaling) - text.grid(column=0, row=17, padx=(40,30), pady=(0,10*scaling), sticky="w") - - text = tk.Message(self.help_panel_window, text=_("Ctrl+Z/Y: Undo/Redo sample point"), width=240 * scaling) - text.grid(column=0, row=18, padx=(40,30), pady=(0,10*scaling), sticky="w") - - self.help_canvas.create_window((0,0), window=self.help_panel_window) - self.help_canvas.configure(yscrollcommand=self.help_scrollbar.set) - self.help_canvas.bind('', lambda e: self.help_canvas.configure(scrollregion=self.help_canvas.bbox("all"))) - self.help_panel_window.update() - width = self.help_panel_window.winfo_width() - self.help_canvas.configure(width=width) - self.help_canvas.yview_moveto("0.0") - - # ------Advanced Panel----------- - - self.advanced_panel = tk.Frame(self.canvas) - self.advanced_canvas = tk.Canvas(self.advanced_panel, borderwidth=0, bd=0, highlightthickness=0, name="advanced_canvas") - self.advanced_canvas.pack(side=tk.LEFT, fill=tk.Y, expand=True) - self.advanced_scrollbar = ttk.Scrollbar(self.advanced_panel, orient=tk.VERTICAL, command=self.advanced_canvas.yview) - self.advanced_scrollbar.pack(side=tk.RIGHT, fill=tk.Y) - - self.advanced_panel_window = tk.Frame(self.advanced_canvas, borderwidth=0) - - self.advanced_panel_window.columnconfigure(0, weight=1) - - text = tk.Message(self.advanced_panel_window, text=_("Advanced Settings"), width=240 * scaling, font=heading_font, anchor="center") - text.grid(column=0, row=0, padx=(40,30), pady=(20*scaling,10*scaling), sticky="ew") - - text = tk.Message(self.advanced_panel_window, text=_("Sample Points"), width=240 * scaling, font=heading_font2, anchor="center") - text.grid(column=0, row=1, padx=(40,30), pady=(20*scaling,10*scaling), sticky="ew") - - self.app.sample_size = tk.IntVar() - self.app.sample_size.set(25) - if "sample_size" in self.app.prefs: - self.app.sample_size.set(self.app.prefs["sample_size"]) - - - self.sample_size_slider = Slider(self.advanced_panel_window, self.app.sample_size, "Sample size", 5, 50, 0, scaling, self.app.redraw_points) - self.sample_size_slider.grid(column=0, row=3, pady=(0,10*scaling), padx=(40,30), sticky="ew") - - - self.app.sample_color = tk.IntVar() - self.app.sample_color.set(55) - if "sample_color" in self.app.prefs: - self.app.sample_color.set(self.app.prefs["sample_color"]) - - self.sample_color_slider = Slider(self.advanced_panel_window, self.app.sample_color, "Sample color", 0, 360, 0, scaling, self.app.redraw_points) - self.sample_color_slider.grid(column=0, row=5, pady=(0,10*scaling), padx=(40,30), sticky="ew") - - text = tk.Message(self.advanced_panel_window, text=_("Interpolation"), width=240 * scaling, font=heading_font2, anchor="center") - text.grid(column=0, row=6, padx=(10*scaling,10*scaling), pady=(20*scaling,10*scaling), sticky="ew") - - text = tk.Message(self.advanced_panel_window, text=_("RBF Kernel"), width=240*scaling, anchor="center") - text.grid(column=0, row=7, pady=(5*scaling,5*scaling), padx=(40,30), sticky="ews") - - self.app.RBF_kernels = ["thin_plate", "quintic", "cubic", "linear"] - self.app.RBF_kernel = tk.StringVar() - self.app.RBF_kernel.set(self.app.RBF_kernels[0]) - if "RBF_kernel" in self.app.prefs: - self.app.RBF_kernel.set(self.app.prefs["RBF_kernel"]) - - self.kernel_menu = ttk.OptionMenu(self.advanced_panel_window, self.app.RBF_kernel, self.app.RBF_kernel.get(), *self.app.RBF_kernels) - self.kernel_menu.grid(column=0, row=8, pady=(5*scaling,5*scaling), padx=(40,30), sticky="ews") - - - text = tk.Message(self.advanced_panel_window, text=_("Spline order"), width=240*scaling, anchor="center") - text.grid(column=0, row=9, pady=(5*scaling,5*scaling), padx=(40,30), sticky="ews") - - self.app.spline_orders = [1,2,3,4,5] - self.app.spline_order = tk.IntVar() - self.app.spline_order.set(3) - if "spline_order" in self.app.prefs: - self.app.spline_order.set(self.app.prefs["spline_order"]) - - self.spline_order_menu = ttk.OptionMenu(self.advanced_panel_window, self.app.spline_order, self.app.spline_order.get(), *self.app.spline_orders) - self.spline_order_menu.grid(column=0, row=10, pady=(5*scaling,5*scaling), padx=(40,30), sticky="ews") - - text = tk.Message(self.advanced_panel_window, text=_("Correction"), width=240 * scaling, font=heading_font2, anchor="center") - text.grid(column=0, row=11, padx=(10*scaling,10*scaling), pady=(20*scaling,10*scaling), sticky="ew") - - - self.app.corr_types = ["Subtraction", "Division"] - self.app.corr_type = tk.StringVar() - self.app.corr_type.set(self.app.corr_types[0]) - if "corr_type" in self.app.prefs: - self.app.corr_type.set(self.app.prefs["corr_type"]) - - self.corr_menu = ttk.OptionMenu(self.advanced_panel_window, self.app.corr_type, self.app.corr_type.get(), *self.app.corr_types) - self.corr_menu.grid(column=0, row=12, pady=(5*scaling,5*scaling), padx=(40,30), sticky="ews") - - - text = tk.Message(self.advanced_panel_window, text=_("Interface"), width=240 * scaling, font=heading_font2, anchor="center") - text.grid(column=0, row=13, padx=(40,30), pady=(20*scaling,10*scaling), sticky="ew") - - text = tk.Message(self.advanced_panel_window, text=_("Language"), width=240*scaling, anchor="center") - text.grid(column=0, row=14, pady=(5*scaling,5*scaling), padx=(40,30), sticky="ews") - - def lang_change(lang): - messagebox.showerror("", _("Please restart the program to change the language.")) - - self.app.langs = ["English", "Deutsch"] - self.app.lang = tk.StringVar() - - if lang == "de_DE": - self.app.lang.set("Deutsch") - else: - self.app.lang.set("English") - - self.lang_menu = ttk.OptionMenu(self.advanced_panel_window, self.app.lang, self.app.lang.get(), *self.app.langs, command=lang_change) - self.lang_menu.grid(column=0, row=15, pady=(5*scaling,5*scaling), padx=(40,30), sticky="ews") - - - def scaling_change(): - messagebox.showerror("", _("Please restart the program to apply the changes to UI scaling.")) - - self.app.scaling = tk.DoubleVar() - self.app.scaling.set(1.0) - if "scaling" in self.app.prefs: - self.app.scaling.set(self.app.prefs["scaling"]) - - - self.scaling_slider = Slider(self.advanced_panel_window, self.app.scaling, "Scaling", 0.5, 2, 1, scaling, scaling_change) - self.scaling_slider.grid(column=0, row=16, pady=(10*scaling,10*scaling), padx=(40,30), sticky="ew") - - # -- begin ai-model selection -- - text = tk.Message(self.advanced_panel_window, text=_("AI-Model"), width=240 * scaling, font=heading_font2, anchor="center") - text.grid(column=0, row=17, padx=(40,30), pady=(20*scaling,10*scaling), sticky="ew") - - remote_versions = list_remote_versions() - local_versions = list_local_versions() - ai_options = set([]) - - if (remote_versions is not None): - ai_options.update([rv["version"] for rv in remote_versions]) - ai_options.update(set([lv["version"] for lv in local_versions])) - ai_options = sorted(ai_options, key=lambda k: version.parse(k), reverse=True) - - self.app.ai_version = tk.StringVar(master) - self.app.ai_version.set("None") # default value - if "ai_version" in self.app.prefs: - self.app.ai_version.set(self.app.prefs["ai_version"]) - else: - ai_options.insert(0, "None") - - try: - default_idx = ai_options.index(self.app.ai_version.get()) - except ValueError: - default_idx = 0 - - self.app.ai_version_options = ttk.OptionMenu(self.advanced_panel_window, self.app.ai_version, ai_options[default_idx], *ai_options) - self.app.ai_version_options.grid(column=0, row=18, pady=(10*scaling,10*scaling), padx=(40,30), sticky="ew") - # -- end ai-model selection -- - - - self.advanced_canvas.create_window((0,0), window=self.advanced_panel_window) - self.advanced_canvas.configure(yscrollcommand=self.advanced_scrollbar.set) - self.advanced_canvas.bind('', lambda e: self.advanced_canvas.configure(scrollregion=self.advanced_canvas.bbox("all"))) - self.advanced_panel_window.update() - width = self.advanced_panel_window.winfo_width() - self.advanced_canvas.configure(width=width) - self.advanced_canvas.yview_moveto("0.0") - - def help(self, event): - - if self.visible_panel == "None": - self.button_frame.pack_forget() - self.help_panel.pack(side=tk.RIGHT, fill=tk.Y) - self.button_frame.pack(side=tk.RIGHT) - self.visible_panel = self.help_panel - - elif self.visible_panel == self.advanced_panel: - self.advanced_panel.pack_forget() - self.button_frame.pack_forget() - self.help_panel.pack(side=tk.RIGHT, fill=tk.Y) - self.button_frame.pack(side=tk.RIGHT) - self.visible_panel = self.help_panel - - elif self.visible_panel == self.help_panel: - self.help_panel.pack_forget() - self.button_frame.pack_forget() - self.button_frame.pack(side=tk.RIGHT) - self.visible_panel="None" - - self.master.update() - # force update of label to prevent white background on mac - self.help_label.configure(background="#c46f1a") - - - def advanced(self, event): - - if self.visible_panel == "None": - self.button_frame.pack_forget() - self.advanced_panel.pack(side=tk.RIGHT, fill=tk.Y) - self.button_frame.pack(side=tk.RIGHT) - self.visible_panel = self.advanced_panel - - elif self.visible_panel == self.help_panel: - self.help_panel.pack_forget() - self.button_frame.pack_forget() - self.advanced_panel.pack(side=tk.RIGHT, fill=tk.Y) - self.button_frame.pack(side=tk.RIGHT) - self.visible_panel = self.advanced_panel - - elif self.visible_panel == self.advanced_panel: - self.advanced_panel.pack_forget() - self.button_frame.pack_forget() - self.button_frame.pack(side=tk.RIGHT) - self.visible_panel="None" - - self.master.update() - # force update of label to prevent white background on mac - self.advanced_label.configure(background="#254f69") diff --git a/graxpert/main.py b/graxpert/main.py index 8549bc8..058e4ea 100644 --- a/graxpert/main.py +++ b/graxpert/main.py @@ -6,8 +6,6 @@ sys.stdout = open(os.devnull, "w") if sys.stderr is None: sys.stderr = open(os.devnull, "w") -from graxpert.mp_logging import configure_logging - import argparse import logging import multiprocessing @@ -16,9 +14,8 @@ from packaging import version -from graxpert.ai_model_handling import (list_local_versions, - list_remote_versions) -from graxpert.version import release as graxpert_release, version as graxpert_version +from graxpert.ai_model_handling import list_local_versions, list_remote_versions +from graxpert.mp_logging import configure_logging available_local_versions = [] available_remote_versions = [] @@ -54,10 +51,7 @@ def version_type(arg_value, pat=re.compile(r"^\d+\.\d+\.\d+$")): if not pat.match(arg_value): raise argparse.ArgumentTypeError("invalid version, expected format: n.n.n") - if ( - arg_value not in available_local_versions - and arg_value not in available_remote_versions - ): + if arg_value not in available_local_versions and arg_value not in available_remote_versions: raise argparse.ArgumentTypeError( "provided version neither found locally or remotely; available locally: [{}], available remotely: [{}]".format( ", ".join(available_local_versions), @@ -69,6 +63,62 @@ def version_type(arg_value, pat=re.compile(r"^\d+\.\d+\.\d+$")): return arg_value +def ui_main(): + import tkinter as tk + + from appdirs import user_config_dir + from customtkinter import CTk + + from graxpert.application.app import graxpert + from graxpert.application.eventbus import eventbus + from graxpert.mp_logging import initialize_logging, shutdown_logging + from graxpert.parallel_processing import executor + from graxpert.preferences import app_state_2_prefs, save_preferences + from graxpert.resource_utils import resource_path + from graxpert.ui.application_frame import ApplicationFrame + from graxpert.ui.styling import style + from graxpert.ui.ui_events import UiEvents + from graxpert.version import check_for_new_version, release, version + + def on_closing(root, logging_thread): + graxpert.prefs = app_state_2_prefs(graxpert.prefs, graxpert.cmd.app_state, graxpert) + + prefs_filename = os.path.join(user_config_dir(appname="GraXpert"), "preferences.json") + save_preferences(prefs_filename, graxpert.prefs) + try: + executor.shutdown(cancel_futures=True) + except Exception as e: + logging.exception("error shutting down ProcessPoolExecutor") + shutdown_logging(logging_thread) + root.destroy() + logging.shutdown() + sys.exit(0) + + logging_thread = initialize_logging() + check_for_new_version() + + root = CTk() + try: + root.state("zoomed") + except: + root.state("normal") + style(root) + root.title("GraXpert | Release: '{}' ({})".format(release, version)) + root.iconbitmap() + root.iconphoto(True, tk.PhotoImage(file=resource_path("img/Icon.png"))) + # root.option_add("*TkFDialog*foreground", "black") + root.protocol("WM_DELETE_WINDOW", lambda: on_closing(root, logging_thread)) + root.createcommand("::tk::mac::Quit", lambda: on_closing(root, logging_thread)) + root.minsize(width=800, height=600) + root.columnconfigure(0, weight=1) + root.rowconfigure(0, weight=1) + app = ApplicationFrame(root) + app.grid(column=0, row=0, sticky=tk.NSEW) + root.update() + eventbus.emit(UiEvents.DISPLAY_START_BADGE_REQUEST) + root.mainloop() + + def main(): if len(sys.argv) > 1: global available_local_versions @@ -76,10 +126,7 @@ def main(): collect_available_version() - parser = argparse.ArgumentParser( - description="GraXpert,the astronomical background extraction tool" - ) - + parser = argparse.ArgumentParser(description="GraXpert,the astronomical background extraction tool") parser.add_argument("filename", type=str, help="Path of the unprocessed image") parser.add_argument( "-ai_version", @@ -88,78 +135,24 @@ def main(): required=False, default=None, type=version_type, - help='Version of the AI model, default: "latest"; available locally: [{}], available remotely: [{}]'.format( - ", ".join(available_local_versions), - ", ".join(available_remote_versions), - ), - ) - parser.add_argument( - "-correction", - "--correction", - nargs="?", - required=False, - default="Subtraction", - choices=["Subtraction", "Division"], - type=str, - help="Subtraction or Division", - ) - parser.add_argument( - "-smoothing", - "--smoothing", - nargs="?", - required=False, - default=0.0, - type=float, - help="Strength of smoothing between 0 and 1", - ) - - parser.add_argument( - "-output", - "--output", - nargs="?", - required=False, - type=str, - help="Filename of the processed image", + help='Version of the AI model, default: "latest"; available locally: [{}], available remotely: [{}]'.format(", ".join(available_local_versions), ", ".join(available_remote_versions)), ) - - parser.add_argument( - "-bg", - "--bg", - required=False, - action="store_true", - help="Also save the background model", - ) - - parser.add_argument( - "-cli", - "--cli", - required=False, - action="store_true", - help="Has to be added when using the command line integration of GraXpert", - ) - - parser.add_argument( - '-v', - '--version', - action='version', - version="GraXpert version: " + graxpert_version + " release: " + graxpert_release) - + parser.add_argument("-correction", "--correction", nargs="?", required=False, default="Subtraction", choices=["Subtraction", "Division"], type=str, help="Subtraction or Division") + parser.add_argument("-smoothing", "--smoothing", nargs="?", required=False, default=0.0, type=float, help="Strength of smoothing between 0 and 1") args = parser.parse_args() - - - if (args.cli): - from graxpert.CommandLineTool import CommandLineTool - clt = CommandLineTool(args) - clt.execute() - else: - import graxpert.gui - + + from graxpert.CommandLineTool import CommandLineTool + + clt = CommandLineTool(args) + clt.execute() + logging.shutdown() else: - import graxpert.gui + ui_main() if __name__ == "__main__": multiprocessing.freeze_support() configure_logging() main() + logging.shutdown() diff --git a/graxpert/mp_logging.py b/graxpert/mp_logging.py index 813de84..1ebf327 100644 --- a/graxpert/mp_logging.py +++ b/graxpert/mp_logging.py @@ -17,13 +17,20 @@ def __init__(self, logger, log_level=logging.INFO): self.logger = logger self.log_level = log_level self.linebuf = "" + self.line_complete = False def write(self, buf): - for line in buf.rstrip().splitlines(): - self.logger.log(self.log_level, line.rstrip()) + self.linebuf += buf + if self.linebuf.count("\n") > 0: + self.line_complete = True + self.flush() def flush(self): - pass + if self.line_complete: + for s in self.linebuf.splitlines(): + self.logger.log(self.log_level, s.rstrip()) + self.linebuf = "" + self.line_complete = False # cf. https://docs.python.org/3/howto/logging-cookbook.html#using-concurrent-futures-processpoolexecutor @@ -43,12 +50,8 @@ def configure_logging(): os.makedirs(os.path.dirname(logfile_name), exist_ok=True) root = logging.getLogger() root.setLevel(logging.INFO) - h = logging.handlers.RotatingFileHandler( - logfile_name, "a", maxBytes=1000000, backupCount=5, encoding="utf-8" - ) - f = logging.Formatter( - "%(asctime)s %(processName)-10s %(name)s %(levelname)-8s %(message)s" - ) + h = logging.handlers.RotatingFileHandler(logfile_name, "a", maxBytes=1000000, backupCount=5, encoding="utf-8") + f = logging.Formatter("%(asctime)s %(processName)-10s %(name)s %(levelname)-8s %(message)s") h.setFormatter(f) root.handlers = [] root.addHandler(h) @@ -84,9 +87,7 @@ def logger_thread(queue): def initialize_logging(): - logging_thread = threading.Thread( - target=logger_thread, args=(get_logging_queue(),) - ) + logging_thread = threading.Thread(target=logger_thread, args=(get_logging_queue(),)) logging_thread.start() return logging_thread diff --git a/graxpert/preferences.py b/graxpert/preferences.py index f5068a0..e50496e 100644 --- a/graxpert/preferences.py +++ b/graxpert/preferences.py @@ -1,7 +1,6 @@ import json import logging import os -import sys import shutil from datetime import datetime from typing import AnyStr, List, TypedDict @@ -20,6 +19,7 @@ class Prefs(TypedDict): bg_pts_option: int stretch_option: AnyStr saturation: float + display_pts: bool bg_tol_option: float interpol_type_option: AnyStr smoothing_option: float @@ -27,11 +27,13 @@ class Prefs(TypedDict): sample_size: int sample_color: int RBF_kernel: AnyStr + spline_order: int lang: AnyStr corr_type: AnyStr scaling: float ai_version: AnyStr + DEFAULT_PREFS: Prefs = { "working_dir": os.getcwd(), "width": None, @@ -41,6 +43,7 @@ class Prefs(TypedDict): "bg_pts_option": 15, "stretch_option": "No Stretch", "saturation": 1.0, + "display_pts": True, "bg_tol_option": 1.0, "interpol_type_option": "RBF", "smoothing_option": 0.0, @@ -52,29 +55,30 @@ class Prefs(TypedDict): "lang": None, "corr_type": "Subtraction", "scaling": 1.0, - "ai_version": None + "ai_version": None, } def app_state_2_prefs(prefs: Prefs, app_state: AppState, app) -> Prefs: if "background_points" in app_state: prefs["background_points"] = [p.tolist() for p in app_state["background_points"]] - prefs["bg_pts_option"] = app.bg_pts.get() - prefs["stretch_option"] = app.stretch_option_current.get() - prefs["saturation"] = app.saturation.get() - prefs["bg_tol_option"] = app.bg_tol.get() - prefs["interpol_type_option"] = app.interpol_type.get() - prefs["smoothing_option"] = app.smoothing.get() - prefs["saveas_option"] = app.saveas_type.get() - prefs["sample_size"] = app.sample_size.get() - prefs["sample_color"] = app.sample_color.get() - prefs["RBF_kernel"] = app.RBF_kernel.get() - prefs["spline_order"] = app.spline_order.get() - prefs["lang"] = app.lang.get() - prefs["corr_type"] = app.corr_type.get() - prefs["bg_flood_selection_option"] = app.flood_select_pts.get() - prefs["scaling"] = app.scaling.get() - prefs["ai_version"] = app.ai_version.get() + prefs["bg_pts_option"] = app.prefs["bg_pts_option"] + prefs["stretch_option"] = app.prefs["stretch_option"] + prefs["saturation"] = app.prefs["saturation"] + prefs["display_pts"] = app.prefs["display_pts"] + prefs["bg_tol_option"] = app.prefs["bg_tol_option"] + prefs["interpol_type_option"] = app.prefs["interpol_type_option"] + prefs["smoothing_option"] = app.prefs["smoothing_option"] + prefs["saveas_option"] = app.prefs["saveas_option"] + prefs["sample_size"] = app.prefs["sample_size"] + prefs["sample_color"] = app.prefs["sample_color"] + prefs["RBF_kernel"] = app.prefs["RBF_kernel"] + prefs["spline_order"] = app.prefs["spline_order"] + prefs["lang"] = app.prefs["lang"] + prefs["corr_type"] = app.prefs["corr_type"] + prefs["bg_flood_selection_option"] = app.prefs["bg_flood_selection_option"] + prefs["scaling"] = app.prefs["scaling"] + prefs["ai_version"] = app.prefs["ai_version"] return prefs @@ -101,6 +105,8 @@ def merge_json(prefs: Prefs, json) -> Prefs: prefs["stretch_option"] = json["stretch_option"] if "saturation" in json: prefs["saturation"] = json["saturation"] + if "display_pts" in json: + prefs["display_pts"] = json["display_pts"] if "bg_tol_option" in json: prefs["bg_tol_option"] = json["bg_tol_option"] if "interpol_type_option" in json: @@ -133,8 +139,8 @@ def load_preferences(prefs_filename) -> Prefs: try: if os.path.isfile(prefs_filename): with open(prefs_filename) as f: - json_prefs: Prefs = json.load(f) - prefs = merge_json(prefs, json_prefs) + json_prefs: Prefs = json.load(f) + prefs = merge_json(prefs, json_prefs) else: logging.info("{} appears to be missing. it will be created after program shutdown".format(prefs_filename)) except: @@ -161,31 +167,31 @@ def app_state_2_fitsheader(app, app_state, fits_header): fits_header["INTP-OPT"] = prefs["interpol_type_option"] fits_header["SMOOTHING"] = prefs["smoothing_option"] fits_header["CORR-TYPE"] = prefs["corr_type"] - + if prefs["interpol_type_option"] == "AI": fits_header["AI-VER"] = prefs["ai_version"] - - if prefs["interpol_type_option"] != "AI": + + if prefs["interpol_type_option"] != "AI": fits_header["SAMPLE-SIZE"] = prefs["sample_size"] fits_header["RBF-KERNEL"] = prefs["RBF_kernel"] fits_header["SPLINE-ORDER"] = prefs["spline_order"] fits_header["BG-PTS"] = str(prefs["background_points"]) - + return fits_header def fitsheader_2_app_state(app, app_state, fits_header): if "BG-PTS" in fits_header.keys(): app_state["background_points"] = [np.array(p) for p in json.loads(fits_header["BG-PTS"])] - + if "INTP-OPT" in fits_header.keys(): app.interpol_type.set(fits_header["INTP-OPT"]) app.smoothing_slider.set(fits_header["SMOOTHING"]) app.corr_type.set(fits_header["CORR-TYPE"]) - + if fits_header["INTP-OPT"] != "AI": app.help_panel.sample_size_slider.set(fits_header["SAMPLE-SIZE"]) app.RBF_kernel.set(fits_header["RBF-KERNEL"]) app.spline_order.set(fits_header["SPLINE-ORDER"]) - + return app_state diff --git a/graxpert/slider.py b/graxpert/slider.py deleted file mode 100644 index 14d6e8f..0000000 --- a/graxpert/slider.py +++ /dev/null @@ -1,115 +0,0 @@ -import tkinter as tk -from tkinter import ttk -from graxpert.localization import _ - -class Slider(tk.Frame): - def __init__(self, frame, var, name, start, stop, precision, scale, command=None): - super().__init__(frame) - - self.frame = frame - self.var = var - self.name = name - self.start = start - self.stop = stop - self.precision = precision - self.command = command - - self.var.set(round(self.var.get(), self.precision)) - - self.text = tk.Label(self, text=_(self.name) + ":") - - # See https://stackoverflow.com/questions/4140437/interactively-validating-entry-widget-content-in-tkinter for explanation of validation - vcmd = (self.register(self.on_entry), '%d', '%i', '%P', '%s', '%S', '%v', '%V', '%W') - - self.entry = ttk.Entry(self, textvariable = self.var, validate="focusout", validatecommand = vcmd, width = 4) - self.slider = ttk.Scale( - self, - orient = tk.HORIZONTAL, - from_= self.start, - to = self.stop, - var = self.var, - command = self.on_slider, - takefocus = False, - length = 200*scale) - - if self.command: - self.slider.bind("", lambda event: self.command()) - - self.entry.bind("", self.up) - self.entry.bind("", self.down) - self.entry.bind("", self.down) - self.entry.bind("", self.up) - - self.slider.bind("", self.up) - self.slider.bind("", self.down) - self.slider.bind("", self.down) - self.slider.bind("", self.up) - - self.grid_columnconfigure(0, weight=1) - self.grid_columnconfigure(1, weight=1) - - self.text.grid(column=0, row=0, pady=0, padx=0, sticky="e") - self.entry.grid(column=1, row=0, pady=0, padx=0, sticky="w") - self.slider.grid(column=0, row=1, pady=5*scale, padx=0, sticky="news", columnspan=2) - - def set(self, value): - self.slider.set(value) - - def on_slider(self, value): - if self.precision == 0: - value = int(float(value)) - else: - value = round(float(value), self.precision) - - self.var.set(value) - - - def on_entry(self, d, i, P, s, S, v, V, W): - - try: - if self.precision == 0: - value = int(float(P)) - else: - value = round(float(P), self.precision) - - if value < self.start or value > self.stop: - return False - - - if self.command: - self.command() - - return True - - except: - return False - - def up(self, event): - - - value = self.var.get() + 10**(-self.precision) - - if value > self.stop: - return "break" - - if self.precision == 0: - value = int(float(value)) - else: - value = round(float(value), self.precision) - - self.var.set(value) - return "break" - - def down(self,event): - value = self.var.get() - 10**(-self.precision) - - if value < self.start: - return "break" - - if self.precision == 0: - value = int(float(value)) - else: - value = round(float(value), self.precision) - - self.var.set(value) - return "break" \ No newline at end of file diff --git a/graxpert/ui/__init__.py b/graxpert/ui/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/graxpert/ui/__init__.py @@ -0,0 +1 @@ + diff --git a/graxpert/ui/application_frame.py b/graxpert/ui/application_frame.py new file mode 100644 index 0000000..0696dc7 --- /dev/null +++ b/graxpert/ui/application_frame.py @@ -0,0 +1,114 @@ +import tkinter as tk + +from customtkinter import CTkButton, CTkFrame, CTkLabel +from icecream import ic + +from graxpert.application.app import graxpert +from graxpert.application.app_events import AppEvents +from graxpert.application.eventbus import eventbus +from graxpert.commands import InitHandler +from graxpert.ui.canvas import Canvas +from graxpert.ui.left_menu import LeftMenu +from graxpert.ui.right_menu import AdvancedFrame, HelpFrame +from graxpert.ui.statusbar import StatusBar +from graxpert.ui.ui_events import UiEvents +from graxpert.ui.widgets import default_label_width, padx + + +class ApplicationFrame(CTkFrame): + def __init__(self, master, **kwargs): + super().__init__(master, **kwargs) + + self.initial_title = master.title() + self.show_help = False + self.show_advanced = False + + self.create_children() + self.setup_layout() + self.place_children() + self.create_bindings() + self.register_events() + + def create_children(self): + self.left_menu = LeftMenu(self, fg_color="transparent", width=default_label_width + padx + 16) + self.canvas = Canvas(self) + self.help_frame = HelpFrame(self, fg_color="transparent", width=300) + self.advanced_frame = AdvancedFrame(self, fg_color="transparent", width=300) + self.statusbar_frame = StatusBar(self) + + def setup_layout(self): + self.columnconfigure(0, weight=0) + self.columnconfigure(1, weight=100) + self.columnconfigure(2, weight=0) + self.rowconfigure(0, weight=1) + self.rowconfigure(1, weight=0) + + def place_children(self): + self.left_menu.grid(column=0, row=0, rowspan=2, ipadx=padx, sticky=tk.NS) + self.canvas.grid(column=1, row=0, sticky=tk.NSEW) + self.statusbar_frame.grid(column=1, row=1, sticky=tk.NSEW) + + def create_bindings(self): + self.master.bind("", lambda e: eventbus.emit(UiEvents.RESET_ZOOM_REQUEST)) # backspace -> reset zoom + self.master.bind("", lambda e: eventbus.emit(UiEvents.RESET_ZOOM_REQUEST)) # ctrl + 0 -> reset zoom (Windows) + self.master.bind("", lambda e: eventbus.emit(UiEvents.RESET_ZOOM_REQUEST)) # cmd + 0 -> reset zoom (Mac) + self.master.bind("", lambda e: eventbus.emit(UiEvents.RESET_ZOOM_REQUEST)) # ctrl + numpad 0 -> reset zoom (Windows) + self.master.bind("", lambda e: eventbus.emit(UiEvents.RESET_ZOOM_REQUEST)) # cmd + numpad 0 -> reset zoom (Mac) + self.master.bind("", lambda e: eventbus.emit(AppEvents.OPEN_FILE_DIALOG_REQUEST)) + self.master.bind("", lambda e: eventbus.emit(AppEvents.OPEN_FILE_DIALOG_REQUEST)) + self.master.bind("", lambda e: eventbus.emit(AppEvents.CALCULATE_REQUEST)) + self.master.bind("", lambda e: eventbus.emit(AppEvents.CALCULATE_REQUEST)) + self.master.bind("", lambda e: eventbus.emit(AppEvents.SAVE_REQUEST)) + self.master.bind("", lambda e: eventbus.emit(AppEvents.SAVE_REQUEST)) + self.master.bind("", self.undo) # undo + self.master.bind("", self.redo) # redo + self.master.bind("", self.undo) # undo on macs + self.master.bind("", self.redo) # redo on macs + + def register_events(self): + eventbus.add_listener(UiEvents.HELP_FRAME_TOGGLED, self.toggle_help) + eventbus.add_listener(UiEvents.ADVANCED_FRAME_TOGGLED, self.toggle_advanced) + eventbus.add_listener(AppEvents.LOAD_IMAGE_END, self.on_load_image_end) + + def place_right_frame(self): + self.help_frame.grid_forget() + self.advanced_frame.grid_forget() + + if self.show_help: + self.help_frame.grid(column=2, row=0, rowspan=2, sticky=tk.NSEW) + + if self.show_advanced: + self.advanced_frame.grid(column=2, row=0, rowspan=2, sticky=tk.NSEW) + + # event handling + def on_load_image_end(self, event): + self.master.title(f'{self.initial_title} - {event["filename"]}') + + def redo(self, event): + if graxpert.cmd.next is not None: + redo = graxpert.cmd.redo() + graxpert.cmd = redo + eventbus.emit(AppEvents.REDRAW_POINTS_REQUEST) + + # widget logic + def toggle_help(self, event): + if self.show_help: + self.show_help = False + else: + self.show_advanced = False + self.show_help = True + self.place_right_frame() + + def toggle_advanced(self, event): + if self.show_advanced: + self.show_advanced = False + else: + self.show_help = False + self.show_advanced = True + self.place_right_frame() + + def undo(self, event): + if not type(graxpert.cmd.handler) is InitHandler: + undo = graxpert.cmd.undo() + graxpert.cmd = undo + eventbus.emit(AppEvents.REDRAW_POINTS_REQUEST) diff --git a/graxpert/ui/canvas.py b/graxpert/ui/canvas.py new file mode 100644 index 0000000..af7f099 --- /dev/null +++ b/graxpert/ui/canvas.py @@ -0,0 +1,464 @@ +import tkinter as tk +from colorsys import hls_to_rgb +from tkinter import messagebox + +import numpy as np +from customtkinter import CTkButton, CTkCanvas, CTkFrame, CTkOptionMenu, StringVar, ThemeManager +from icecream import ic +from PIL import Image, ImageTk + +from graxpert.application.app import graxpert +from graxpert.application.app_events import AppEvents +from graxpert.application.eventbus import eventbus +from graxpert.commands import ADD_POINT_HANDLER, ADD_POINTS_HANDLER, MOVE_POINT_HANDLER, Command +from graxpert.localization import _ +from graxpert.resource_utils import resource_path +from graxpert.ui.loadingframe import DynamicProgressFrame, LoadingFrame +from graxpert.ui.ui_events import UiEvents + + +class Canvas(CTkFrame): + def __init__(self, master, **kwargs): + super().__init__(master, **kwargs) + + self.display_options = ["Original", "Processed", "Background"] + self.display_type = StringVar() + self.display_type.set(self.display_options[0]) + self.display_type.trace_add("write", lambda a, b, c: eventbus.emit(AppEvents.DISPLAY_TYPE_CHANGED, {"display_type": self.display_type.get()})) + + self.startx = 0 + self.starty = 0 + self.endx = 0 + self.endy = 0 + self.crop_mode = False + + self.create_children() + self.setup_layout() + self.place_children() + self.create_bindings() + self.register_events() + + # widget setup + def create_children(self): + self.canvas = CTkCanvas(self, background="black", bd=0, highlightthickness=0) + self.display_menu = CTkOptionMenu(self, variable=self.display_type, values=self.display_options) + self.help_button = CTkButton( + self.canvas, + text=_("H\nE\nL\nP"), + width=0, + fg_color=ThemeManager.theme["Help.CTkButton"]["fg_color"], + bg_color="transparent", + hover_color=ThemeManager.theme["Help.CTkButton"]["hover_color"], + command=lambda: eventbus.emit(UiEvents.HELP_FRAME_TOGGLED), + ) + self.advanced_button = CTkButton(self.canvas, text=_("A\nD\nV\nA\nN\nC\nE\nD"), width=0, bg_color="transparent", command=lambda: eventbus.emit(UiEvents.ADVANCED_FRAME_TOGGLED)) + self.static_loading_frame = LoadingFrame(self.canvas, width=0, height=0) + self.dynamic_progress_frame = DynamicProgressFrame(self.canvas) + + def setup_layout(self): + self.columnconfigure(0, weight=1) + self.rowconfigure(0, weight=0) + self.rowconfigure(1, weight=1) + self.canvas.columnconfigure(0, weight=1) + self.canvas.rowconfigure(0, weight=1) + self.canvas.rowconfigure(1, weight=1) + + def place_children(self): + self.canvas.grid(column=0, row=1, sticky=tk.NSEW) + self.display_menu.grid(column=0, row=0, sticky=tk.N) + self.help_button.grid(column=0, row=0, sticky=tk.SE) + self.advanced_button.grid(column=0, row=1, sticky=tk.NE) + self.static_loading_frame.grid_forget() + self.dynamic_progress_frame.grid_forget() + + def create_bindings(self): + self.canvas.bind("", self.on_mouse_down_left) # Left Mouse Button Down + self.canvas.bind("", self.on_mouse_release_left) # Left Mouse Button Released + self.canvas.bind("", self.on_mouse_down_right) # Middle Mouse Button (Right Mouse Button on macs) + self.canvas.bind("", self.on_mouse_down_right) # Right Mouse Button (Middle Mouse Button on macs) + self.canvas.bind("", self.on_mouse_move_left) # Left Mouse Button Drag + self.canvas.bind("", self.on_mouse_move) # Mouse move + self.canvas.bind("", self.on_mouse_wheel) # Mouse Wheel + self.canvas.bind("", self.on_mouse_wheel) # Mouse Wheel Linux + self.canvas.bind("", self.on_mouse_wheel) # Mouse Wheel Linux + + def register_events(self): + eventbus.add_listener(AppEvents.LOAD_IMAGE_BEGIN, self.on_load_image_begin) + eventbus.add_listener(AppEvents.LOAD_IMAGE_END, self.on_load_image_end) + eventbus.add_listener(AppEvents.LOAD_IMAGE_ERROR, self.on_load_image_error) + eventbus.add_listener(AppEvents.STRETCH_IMAGE_BEGIN, self.on_stretch_image_begin) + eventbus.add_listener(AppEvents.STRETCH_IMAGE_END, self.on_stretch_image_end) + eventbus.add_listener(AppEvents.STRETCH_IMAGE_ERROR, self.on_stretch_image_error) + eventbus.add_listener(AppEvents.CHANGE_SATURATION_BEGIN, self.on_change_saturation_begin) + eventbus.add_listener(AppEvents.CHANGE_SATURATION_END, self.on_change_saturation_end) + eventbus.add_listener(AppEvents.CREATE_GRID_BEGIN, self.on_create_grid_begin) + eventbus.add_listener(AppEvents.CREATE_GRID_END, self.on_create_grid_end) + eventbus.add_listener(AppEvents.REDRAW_POINTS_REQUEST, self.redraw_points) + eventbus.add_listener(AppEvents.RESET_POITS_BEGIN, self.on_reset_points_begin) + eventbus.add_listener(AppEvents.RESET_POITS_END, self.on_reset_points_end) + eventbus.add_listener(AppEvents.CALCULATE_BEGIN, self.on_calculate_begin) + eventbus.add_listener(AppEvents.CALCULATE_PROGRESS, self.on_calculate_progress) + eventbus.add_listener(AppEvents.CALCULATE_END, self.on_calculate_end) + eventbus.add_listener(AppEvents.CALCULATE_ERROR, self.on_calculate_end) + eventbus.add_listener(AppEvents.SAVE_BEGIN, self.on_save_begin) + eventbus.add_listener(AppEvents.SAVE_END, self.on_save_end) + eventbus.add_listener(AppEvents.SAVE_ERROR, self.on_save_end) + eventbus.add_listener(AppEvents.AI_DOWNLOAD_BEGIN, self.on_ai_download_begin) + eventbus.add_listener(AppEvents.AI_DOWNLOAD_PROGRESS, self.on_ai_download_progress) + eventbus.add_listener(AppEvents.AI_DOWNLOAD_END, self.on_ai_download_end) + eventbus.add_listener(AppEvents.AI_DOWNLOAD_ERROR, self.on_ai_download_end) + eventbus.add_listener(AppEvents.UPDATE_DISPLAY_TYPE_REEQUEST, lambda e: self.display_type.set(e["display_type"])) + eventbus.add_listener(AppEvents.DISPLAY_TYPE_CHANGED, self.redraw_image) + eventbus.add_listener(UiEvents.RESET_ZOOM_REQUEST, self.reset_zoom) + eventbus.add_listener(UiEvents.DISPLAY_START_BADGE_REQUEST, self.on_display_start_badge_request) + eventbus.add_listener(UiEvents.TOGGLE_CROP_REQUEST, self.on_toggle_crop_request) + eventbus.add_listener(UiEvents.APPLY_CROP_REQUEST, self.on_apply_crop_request) + + # event handling + def on_ai_download_begin(self, event=None): + self.dynamic_progress_frame.text.set(_("Downloading AI-Model")) + self.show_progress_frame(True) + + def on_ai_download_progress(self, event=None): + self.dynamic_progress_frame.update_progress(event["progress"]) + + def on_ai_download_end(self, event=None): + self.dynamic_progress_frame.text.set("") + self.dynamic_progress_frame.variable.set(0.0) + self.show_progress_frame(False) + + def on_apply_crop_request(self, event=None): + self.show_progress_frame(True) + + if not self.crop_mode: + return + + for astroimg in graxpert.images.values(): + if astroimg is not None: + astroimg.crop(self.startx, self.endx, self.starty, self.endy) + + eventbus.emit(AppEvents.RESET_POITS_REQUEST) + self.crop_mode = False + self.zoom_fit(graxpert.images[self.display_type.get()].width, graxpert.images[self.display_type.get()].height) + + self.redraw_points() + self.redraw_image() + self.show_progress_frame(False) + + def on_calculate_begin(self, event=None): + self.dynamic_progress_frame.text.set(_("Extracting Background")) + self.show_progress_frame(True) + + def on_calculate_progress(self, event=None): + self.dynamic_progress_frame.update_progress(event["progress"]) + + def on_calculate_end(self, event=None): + self.dynamic_progress_frame.text.set("") + self.dynamic_progress_frame.variable.set(0.0) + self.show_progress_frame(False) + self.redraw_image() + + def on_change_saturation_begin(self, event=None): + self.show_loading_frame(True) + + def on_change_saturation_end(self, event=None): + self.redraw_image() + self.show_loading_frame(False) + + def on_create_grid_begin(self, event=None): + self.show_loading_frame(True) + + def on_create_grid_end(self, event=None): + self.redraw_image() + self.show_loading_frame(False) + + def on_display_start_badge_request(self, event=None): + self.start_badge = ImageTk.PhotoImage(file=resource_path("img/graXpert_Startbadge_Ariel.png")) + self.canvas.create_image(self.canvas.winfo_width() / 2, self.canvas.winfo_height() / 2, anchor=tk.CENTER, image=self.start_badge, tags="start_badge") + + def on_load_image_begin(self, event=None): + self.canvas.delete("start_badge") + self.show_loading_frame(True) + + def on_load_image_end(self, event=None): + width = graxpert.images["Original"].img_display.width + height = graxpert.images["Original"].img_display.height + + self.zoom_fit(width, height) + self.redraw_image() + + self.display_type.set("Original") + + self.show_loading_frame(False) + + def on_load_image_error(self, event=None): + self.show_loading_frame(False) + + def on_mouse_down_left(self, event=None): + self.left_drag_timer = -1 + if graxpert.images["Original"] is None: + return + + self.clicked_inside_pt = False + point_im = graxpert.to_image_point(event.x, event.y) + + if len(graxpert.cmd.app_state["background_points"]) != 0 and len(point_im) != 0 and graxpert.prefs["display_pts"]: + eventx_im = point_im[0] + eventy_im = point_im[1] + + background_points = graxpert.cmd.app_state["background_points"] + + min_idx = -1 + min_dist = -1 + + for i in range(len(background_points)): + x_im = background_points[i][0] + y_im = background_points[i][1] + + dist = np.max(np.abs([x_im - eventx_im, y_im - eventy_im])) + + if min_idx == -1 or dist < min_dist: + min_dist = dist + min_idx = i + + if min_idx != -1 and min_dist <= graxpert.prefs["sample_size"]: + self.clicked_inside_pt = True + self.clicked_inside_pt_idx = min_idx + self.clicked_inside_pt_coord = graxpert.cmd.app_state["background_points"][min_idx] + + if self.crop_mode: + # Check if inside circles to move crop corners + corner1 = graxpert.to_canvas_point(self.startx, self.starty) + corner2 = graxpert.to_canvas_point(self.endx, self.endy) + if (event.x - corner1[0]) ** 2 + (event.y - corner1[1]) ** 2 < 15**2 or (event.x - corner2[0]) ** 2 + (event.y - corner2[1]) ** 2 < 15**2: + self.clicked_inside_pt = True + + self.__old_event = event + + def on_mouse_down_right(self, event=None): + if graxpert.images["Original"] is None or not graxpert.prefs["display_pts"]: + return + + graxpert.remove_pt(event) + self.redraw_points() + self.__old_event = event + + def on_mouse_move(self, event=None): + eventbus.emit(UiEvents.MOUSE_MOVED, {"mouse_event": event}) + + def on_mouse_move_left(self, event=None): + if graxpert.images["Original"] is None: + return + + if graxpert.images[graxpert.display_type] is None: + return + + if self.left_drag_timer == -1: + self.left_drag_timer = event.time + + if self.clicked_inside_pt and graxpert.prefs["display_pts"] and not self.crop_mode: + new_point = graxpert.to_image_point(event.x, event.y) + if len(new_point) != 0: + graxpert.cmd.app_state["background_points"][self.clicked_inside_pt_idx] = new_point + + self.redraw_points() + + elif self.clicked_inside_pt and self.crop_mode: + new_point = graxpert.to_image_point_pinned(event.x, event.y) + corner1_canvas = graxpert.to_canvas_point(self.startx, self.starty) + corner2_canvas = graxpert.to_canvas_point(self.endx, self.endy) + + dist1 = (event.x - corner1_canvas[0]) ** 2 + (event.y - corner1_canvas[1]) ** 2 + dist2 = (event.x - corner2_canvas[0]) ** 2 + (event.y - corner2_canvas[1]) ** 2 + if dist1 < dist2: + self.startx = int(new_point[0]) + self.starty = int(new_point[1]) + else: + self.endx = int(new_point[0]) + self.endy = int(new_point[1]) + + self.redraw_points() + + else: + if event.time - self.left_drag_timer >= 100: + graxpert.translate(event.x - self.__old_event.x, event.y - self.__old_event.y) + self.redraw_image() + + self.on_mouse_move(event) + self.__old_event = event + return + + def on_mouse_release_left(self, event=None): + if graxpert.images["Original"] is None or not graxpert.prefs["display_pts"]: + return + + if self.clicked_inside_pt and not self.crop_mode: + new_point = graxpert.to_image_point(event.x, event.y) + graxpert.cmd.app_state["background_points"][self.clicked_inside_pt_idx] = self.clicked_inside_pt_coord + graxpert.cmd = Command(MOVE_POINT_HANDLER, prev=graxpert.cmd, new_point=new_point, idx=self.clicked_inside_pt_idx) + graxpert.cmd.execute() + + elif len(graxpert.to_image_point(event.x, event.y)) != 0 and (event.time - self.left_drag_timer < 100 or self.left_drag_timer == -1): + point = graxpert.to_image_point(event.x, event.y) + + if not graxpert.prefs["bg_flood_selection_option"]: + graxpert.cmd = Command(ADD_POINT_HANDLER, prev=graxpert.cmd, point=point) + else: + graxpert.cmd = Command( + ADD_POINTS_HANDLER, + prev=graxpert.cmd, + point=point, + tol=graxpert.prefs["bg_tol_option"], + bg_pts=graxpert.prefs["bg_pts_option"], + sample_size=graxpert.prefs["sample_size"], + image=graxpert.images["Original"], + ) + graxpert.cmd.execute() + + self.redraw_points() + self.__old_event = event + self.left_drag_timer = -1 + + def on_mouse_wheel(self, event=None): + if graxpert.images[self.display_type.get()] is None: + return + if event.delta > 0 or event.num == 4: + graxpert.scale_at(6 / 5, event.x, event.y) + else: + graxpert.scale_at(5 / 6, event.x, event.y) + self.redraw_image() + + def on_reset_points_begin(self, event=None): + self.show_loading_frame(True) + + def on_reset_points_end(self, event=None): + self.redraw_points() + self.show_loading_frame(False) + + def on_save_begin(self, event=None): + self.show_loading_frame(True) + + def on_save_end(self, event=None): + self.show_loading_frame(False) + + def on_stretch_image_begin(self, event=None): + self.show_loading_frame(True) + + def on_stretch_image_end(self, event=None): + self.redraw_image() + self.show_loading_frame(False) + + def on_stretch_image_error(self, event=None): + self.show_loading_frame(False) + + def on_toggle_crop_request(self, event=None): + if graxpert.images["Original"] is None: + messagebox.showerror("Error", _("Please load your picture first.")) + return + + self.startx = 0 + self.starty = 0 + self.endx = graxpert.images["Original"].width + self.endy = graxpert.images["Original"].height + + if self.crop_mode: + self.crop_mode = False + else: + self.crop_mode = True + + self.redraw_points() + + # widget logic + def draw_image(self, pil_image, tags=None): + if pil_image is None: + return + canvas_width = self.canvas.winfo_width() + canvas_height = self.canvas.winfo_height() + + mat_inv = np.linalg.inv(graxpert.mat_affine) + + affine_inv = (mat_inv[0, 0], mat_inv[0, 1], mat_inv[0, 2], mat_inv[1, 0], mat_inv[1, 1], mat_inv[1, 2]) + + dst = pil_image.transform((canvas_width, canvas_height), Image.AFFINE, affine_inv, Image.BILINEAR) + + im = ImageTk.PhotoImage(image=dst) + + self.canvas.create_image(0, 0, anchor=tk.NW, image=im, tags=tags) + + self.image = im + self.redraw_points() + return + + def redraw_image(self, event=None): + if graxpert.images[self.display_type.get()] is None: + return + self.draw_image(graxpert.images[self.display_type.get()].img_display_saturated) + + def redraw_points(self, event=None): + if graxpert.images["Original"] is None: + return + + color = hls_to_rgb(graxpert.prefs["sample_color"] / 360, 0.5, 1.0) + color = (int(color[0] * 255), int(color[1] * 255), int(color[2] * 255)) + color = "#%02x%02x%02x" % color + + self.canvas.delete("sample") + self.canvas.delete("crop") + rectsize = graxpert.prefs["sample_size"] + background_points = graxpert.cmd.app_state["background_points"] + + if graxpert.prefs["display_pts"] and not self.crop_mode: + for point in background_points: + corner1 = graxpert.to_canvas_point(point[0] - rectsize, point[1] - rectsize) + corner2 = graxpert.to_canvas_point(point[0] + rectsize, point[1] + rectsize) + self.canvas.create_rectangle(corner1[0], corner1[1], corner2[0], corner2[1], outline=color, width=2, tags="sample") + + if self.crop_mode: + corner1 = graxpert.to_canvas_point(self.startx, self.starty) + corner2 = graxpert.to_canvas_point(self.endx, self.endy) + self.canvas.create_rectangle(corner1[0], corner1[1], corner2[0], corner2[1], outline=color, width=2, tags="crop") + self.canvas.create_oval(corner1[0] - 15, corner1[1] - 15, corner1[0] + 15, corner1[1] + 15, outline=color, width=2, tags="crop") + self.canvas.create_oval(corner2[0] - 15, corner2[1] - 15, corner2[0] + 15, corner2[1] + 15, outline=color, width=2, tags="crop") + + def reset_zoom(self, event=None): + if graxpert.images[self.display_type.get()] is None: + return + self.zoom_fit(graxpert.images[self.display_type.get()].width, graxpert.images[self.display_type.get()].height) + self.redraw_image() + + def show_loading_frame(self, show): + if show: + self.static_loading_frame.grid(column=0, row=0, rowspan=2) + else: + self.static_loading_frame.grid_forget() + self.update() + + def show_progress_frame(self, show): + if show: + self.dynamic_progress_frame.grid(column=0, row=0, rowspan=2) + else: + self.dynamic_progress_frame.grid_forget() + self.update() + + def zoom_fit(self, image_width, image_height): + canvas_width = self.winfo_width() + canvas_height = self.winfo_height() + + if (image_width * image_height <= 0) or (canvas_width * canvas_height <= 0): + return + + graxpert.reset_transform() + + scale = 1.0 + offsetx = 0.0 + offsety = 0.0 + + if (canvas_width * image_height) > (image_width * canvas_height): + scale = canvas_height / image_height + offsetx = (canvas_width - image_width * scale) / 2 + else: + scale = canvas_width / image_width + offsety = (canvas_height - image_height * scale) / 2 + + graxpert.scale(scale) + graxpert.translate(offsetx, offsety) diff --git a/graxpert/ui/left_menu.py b/graxpert/ui/left_menu.py new file mode 100644 index 0000000..a329e8b --- /dev/null +++ b/graxpert/ui/left_menu.py @@ -0,0 +1,244 @@ +import os +import tkinter as tk + +from customtkinter import CTkScrollableFrame, StringVar, ThemeManager +from icecream import ic + +import graxpert.ui.tooltip as tooltip +from graxpert.application.app import graxpert +from graxpert.application.app_events import AppEvents +from graxpert.application.eventbus import eventbus +from graxpert.localization import _ +from graxpert.ui.ui_events import UiEvents +from graxpert.ui.widgets import CollapsibleMenuFrame, ExtractionStep, GraXpertButton, GraXpertCheckbox, GraXpertLabel, GraXpertOptionMenu, ValueSlider, default_label_width, padx, pady + + +class CropMenu(CollapsibleMenuFrame): + def __init__(self, parent, **kwargs): + super().__init__(parent, title=_("Crop"), show=False, **kwargs) + self.create_children() + self.setup_layout() + self.place_children() + + def create_children(self): + super().create_children() + self.cropmode_button = GraXpertButton(self.sub_frame, text=_("Crop mode on/off"), command=lambda: eventbus.emit(UiEvents.TOGGLE_CROP_REQUEST)) + self.cropapply_button = GraXpertButton(self.sub_frame, text=_("Apply crop"), command=lambda: eventbus.emit(UiEvents.APPLY_CROP_REQUEST)) + + def setup_layout(self): + super().setup_layout() + + def place_children(self): + super().place_children() + self.cropmode_button.grid(column=1, row=0, pady=pady, sticky=tk.NSEW) + self.cropapply_button.grid(column=1, row=1, pady=pady, sticky=tk.NSEW) + + +class ExtractionMenu(CollapsibleMenuFrame): + def __init__(self, parent, **kwargs): + super().__init__(parent, title=_("Background Extraction"), **kwargs) + + # stretch options + self.stretch_options = ["No Stretch", "10% Bg, 3 sigma", "15% Bg, 3 sigma", "20% Bg, 3 sigma", "30% Bg, 2 sigma"] + self.stretch_option_current = StringVar() + self.stretch_option_current.set(self.stretch_options[0]) + if "stretch_option" in graxpert.prefs: + self.stretch_option_current.set(graxpert.prefs["stretch_option"]) + self.stretch_option_current.trace_add("write", lambda a, b, c: eventbus.emit(AppEvents.STRETCH_OPTION_CHANGED, {"stretch_option": self.stretch_option_current.get()})) + + self.saturation = tk.DoubleVar() + self.saturation.set(1.0) + if "saturation" in graxpert.prefs: + self.saturation.set(graxpert.prefs["saturation"]) + self.saturation.trace_add("write", lambda a, b, c: eventbus.emit(AppEvents.CHANGE_SATURATION_REQUEST, {"saturation": self.saturation.get()})) + + # sample selection + self.display_pts = tk.BooleanVar() + self.display_pts.set(True) + if "display_pts" in graxpert.prefs: + self.display_pts.set(graxpert.prefs["display_pts"]) + self.display_pts.trace_add("write", lambda a, b, c: eventbus.emit(AppEvents.DISPLAY_PTS_CHANGED, {"display_pts": self.display_pts.get()})) + + self.flood_select_pts = tk.BooleanVar() + self.flood_select_pts.set(False) + if "bg_flood_selection_option" in graxpert.prefs: + self.flood_select_pts.set(graxpert.prefs["bg_flood_selection_option"]) + self.flood_select_pts.trace_add("write", lambda a, b, c: eventbus.emit(AppEvents.BG_FLOOD_SELECTION_CHANGED, {"bg_flood_selection_option": self.flood_select_pts.get()})) + + self.bg_pts = tk.IntVar() + self.bg_pts.set(10) + if "bg_pts_option" in graxpert.prefs: + self.bg_pts.set(graxpert.prefs["bg_pts_option"]) + self.bg_pts.trace_add("write", lambda a, b, c: eventbus.emit(AppEvents.BG_PTS_CHANGED, {"bg_pts_option": self.bg_pts.get()})) + + self.bg_tol = tk.DoubleVar() + self.bg_tol.set(1) + if "bg_tol_option" in graxpert.prefs: + self.bg_tol.set(graxpert.prefs["bg_tol_option"]) + self.bg_tol.trace_add("write", lambda a, b, c: eventbus.emit(AppEvents.BG_TOL_CHANGED, {"bg_tol_option": self.bg_tol.get()})) + + # calculation + self.interpol_options = ["RBF", "Splines", "Kriging", "AI"] + self.interpol_type = tk.StringVar() + self.interpol_type.set(self.interpol_options[0]) + if "interpol_type_option" in graxpert.prefs: + self.interpol_type.set(graxpert.prefs["interpol_type_option"]) + self.interpol_type.trace_add("write", lambda a, b, c: eventbus.emit(AppEvents.INTERPOL_TYPE_CHANGED, {"interpol_type_option": self.interpol_type.get()})) + + self.smoothing = tk.DoubleVar() + self.smoothing.set(1.0) + if "smoothing_option" in graxpert.prefs: + self.smoothing.set(graxpert.prefs["smoothing_option"]) + self.smoothing.trace_add("write", lambda a, b, c: eventbus.emit(AppEvents.SMOTTHING_CHANGED, {"smoothing_option": self.smoothing.get()})) + + # saving + self.saveas_options = ["16 bit Tiff", "32 bit Tiff", "16 bit Fits", "32 bit Fits", "16 bit XISF", "32 bit XISF"] + self.saveas_type = tk.StringVar() + self.saveas_type.set(self.saveas_options[0]) + if "saveas_option" in graxpert.prefs: + self.saveas_type.set(graxpert.prefs["saveas_option"]) + self.saveas_type.trace_add("write", lambda a, b, c: eventbus.emit(AppEvents.SAVE_AS_CHANGED, {"saveas_option": self.saveas_type.get()})) + + self.create_children() + self.setup_layout() + self.place_children() + + def create_children(self): + super().create_children() + + # image loading + self.loading_title = ExtractionStep(self.sub_frame, 1, _(" Loading")) + self.load_image_button = GraXpertButton( + self.sub_frame, + text=_("Load Image"), + fg_color=ThemeManager.theme["Accent.CTkButton"]["fg_color"], + hover_color=ThemeManager.theme["Accent.CTkButton"]["hover_color"], + command=self.menu_open_clicked, + ) + self.tt_load = tooltip.Tooltip(self.load_image_button, text=tooltip.load_text) + + # stretch options + self.stretch_options_title = ExtractionStep(self.sub_frame, 2, _(" Stretch Options")) + self.stretch_menu = GraXpertOptionMenu( + self.sub_frame, + variable=self.stretch_option_current, + values=self.stretch_options, + ) + tooltip.Tooltip(self.stretch_menu, text=tooltip.stretch_text) + self.saturation_slider = ValueSlider( + self.sub_frame, + width=default_label_width, + variable_name=_("Saturation"), + variable=self.saturation, + min_value=0, + max_value=3, + precision=1, + ) + + # sample selection + self.sample_selection_title = ExtractionStep(self.sub_frame, 3, _(" Sample Selection")) + self.display_pts_switch = GraXpertCheckbox(self.sub_frame, width=default_label_width, text=_("Display points"), variable=self.display_pts) + self.flood_select_pts_switch = GraXpertCheckbox(self.sub_frame, width=default_label_width, text=_("Flooded generation"), variable=self.flood_select_pts) + tooltip.Tooltip(self.flood_select_pts_switch, text=tooltip.bg_flood_text) + self.bg_pts_slider = ValueSlider(self.sub_frame, width=default_label_width, variable_name=_("Points per row"), variable=self.bg_pts, min_value=4, max_value=25, precision=0) + tooltip.Tooltip(self.bg_pts_slider, text=tooltip.num_points_text) + self.bg_tol_slider = ValueSlider(self.sub_frame, width=default_label_width, variable_name=_("Grid Tolerance"), variable=self.bg_tol, min_value=-2, max_value=10, precision=1) + tooltip.Tooltip(self.bg_tol_slider, text=tooltip.bg_tol_text) + self.bg_selection_button = GraXpertButton(self.sub_frame, text=_("Create Grid"), command=lambda: eventbus.emit(AppEvents.CREATE_GRID_REQUEST)) + tooltip.Tooltip(self.bg_selection_button, text=tooltip.bg_select_text) + self.reset_button = GraXpertButton(self.sub_frame, text=_("Reset Sample Points"), command=lambda: eventbus.emit(AppEvents.RESET_POITS_REQUEST)) + tooltip.Tooltip(self.reset_button, text=tooltip.reset_text) + + # calculation + self.calculation_title = ExtractionStep(self.sub_frame, 4, _(" Calculation")) + self.intp_type_text = GraXpertLabel(self.sub_frame, text=_("Interpolation Method:")) + self.interpol_menu = GraXpertOptionMenu(self.sub_frame, variable=self.interpol_type, values=self.interpol_options) + tooltip.Tooltip(self.interpol_menu, text=tooltip.interpol_type_text) + self.smoothing_slider = ValueSlider(self.sub_frame, width=default_label_width, variable_name=_("Smoothing"), variable=self.smoothing, min_value=0, max_value=1, precision=1) + tooltip.Tooltip(self.smoothing_slider, text=tooltip.smoothing_text) + self.calculate_button = GraXpertButton( + self.sub_frame, + text=_("Calculate Background"), + fg_color=ThemeManager.theme["Accent.CTkButton"]["fg_color"], + hover_color=ThemeManager.theme["Accent.CTkButton"]["hover_color"], + command=lambda: eventbus.emit(AppEvents.CALCULATE_REQUEST), + ) + tooltip.Tooltip(self.calculate_button, text=tooltip.calculate_text) + + # saving + self.saving_title = ExtractionStep(self.sub_frame, 5, _(" Saving")) + self.saveas_menu = GraXpertOptionMenu(self.sub_frame, variable=self.saveas_type, values=self.saveas_options) + tooltip.Tooltip(self.saveas_menu, text=tooltip.saveas_text) + self.save_button = GraXpertButton( + self.sub_frame, + text=_("Save Processed"), + fg_color=ThemeManager.theme["Accent.CTkButton"]["fg_color"], + hover_color=ThemeManager.theme["Accent.CTkButton"]["hover_color"], + command=lambda: eventbus.emit(AppEvents.SAVE_REQUEST), + ) + tooltip.Tooltip(self.save_button, text=tooltip.save_pic_text) + self.save_background_button = GraXpertButton(self.sub_frame, text=_("Save Background"), command=lambda: eventbus.emit(AppEvents.SAVE_BACKGROUND_REQUEST)) + tooltip.Tooltip(self.save_background_button, text=tooltip.save_bg_text) + self.save_stretched_button = GraXpertButton(self.sub_frame, text=_("Save Stretched & Processed"), command=lambda: eventbus.emit(AppEvents.SAVE_STRETCHED_REQUEST)) + tooltip.Tooltip(self.save_stretched_button, text=tooltip.save_stretched_pic_text) + + def setup_layout(self): + super().setup_layout() + + def place_children(self): + super().place_children() + + # image loading + self.loading_title.grid(column=0, row=0, columnspan=2, pady=pady, sticky=tk.EW) + self.load_image_button.grid(column=1, row=1, pady=pady, sticky=tk.EW) + + # stretch options + self.stretch_options_title.grid(column=0, row=2, columnspan=2, pady=pady, sticky=tk.EW) + self.stretch_menu.grid(column=1, row=3, pady=pady, sticky=tk.EW) + self.saturation_slider.grid(column=1, row=4, pady=pady, sticky=tk.EW) + + # sample selection + self.sample_selection_title.grid(column=0, row=5, columnspan=2, pady=pady, sticky=tk.EW) + self.display_pts_switch.grid(column=1, row=6, pady=pady, sticky=tk.EW) + self.flood_select_pts_switch.grid(column=1, row=7, pady=pady, sticky=tk.EW) + self.bg_pts_slider.grid(column=1, row=8, pady=pady, sticky=tk.EW) + self.bg_tol_slider.grid(column=1, row=9, pady=pady, sticky=tk.EW) + self.bg_selection_button.grid(column=1, row=10, pady=pady, sticky=tk.EW) + self.reset_button.grid(column=1, row=11, pady=pady, sticky=tk.EW) + + # calculation + self.calculation_title.grid(column=0, row=12, pady=pady, columnspan=2, sticky=tk.EW) + self.intp_type_text.grid(column=1, row=13, pady=pady, sticky=tk.EW) + self.interpol_menu.grid(column=1, row=14, pady=pady, sticky=tk.EW) + self.smoothing_slider.grid(column=1, row=15, pady=pady, sticky=tk.EW) + self.calculate_button.grid(column=1, row=16, pady=pady, sticky=tk.EW) + + # saving + self.saving_title.grid(column=0, row=17, pady=pady, columnspan=2, sticky=tk.EW) + self.saveas_menu.grid(column=1, row=18, pady=pady, sticky=tk.EW) + self.save_button.grid(column=1, row=19, pady=pady, sticky=tk.EW) + self.save_background_button.grid(column=1, row=20, pady=pady, sticky=tk.EW) + self.save_stretched_button.grid(column=1, row=21, pady=pady, sticky=tk.EW) + + def menu_open_clicked(self, event=None): + eventbus.emit(AppEvents.OPEN_FILE_DIALOG_REQUEST) + + +class LeftMenu(CTkScrollableFrame): + def __init__(self, parent, **kwargs): + super().__init__(parent, **kwargs) + self.create_children() + self.setup_layout() + self.place_children() + + def create_children(self): + self.crop_menu = CropMenu(self, fg_color="transparent") + self.extraction_menu = ExtractionMenu(self, fg_color="transparent") + + def setup_layout(self): + self.columnconfigure(0, weight=1) + self.rowconfigure(0, weight=1) + + def place_children(self): + self.crop_menu.grid(column=0, row=0, ipadx=padx, sticky=tk.N) + self.extraction_menu.grid(column=0, row=1, ipadx=padx, sticky=tk.N) diff --git a/graxpert/loadingframe.py b/graxpert/ui/loadingframe.py similarity index 50% rename from graxpert/loadingframe.py rename to graxpert/ui/loadingframe.py index 12d78fc..f067d3a 100644 --- a/graxpert/loadingframe.py +++ b/graxpert/ui/loadingframe.py @@ -4,60 +4,65 @@ from os import path from queue import Empty, Queue from threading import Thread -from tkinter import LEFT, ttk -from PIL import ImageTk +from customtkinter import CTkFont, CTkFrame, CTkImage, CTkLabel, CTkProgressBar, DoubleVar, StringVar +from PIL import Image from graxpert.localization import _ -from graxpert.resource_utils import temp_resource_path +from graxpert.resource_utils import resource_path -class LoadingFrame: - def __init__(self, widget, toplevel): - font = ("Verdana", 20, "normal") +class LoadingFrame(CTkFrame): + def __init__(self, parent, **kwargs): + super().__init__(parent, **kwargs) + self.create_children() + self.setup_layout() + self.place_children() - self.toplevel = toplevel - hourglass_pic = ImageTk.PhotoImage( - file=temp_resource_path("img/hourglass-scaled.png") - ) - self.text = ttk.Label( - widget, + def create_children(self): + font = CTkFont(size=15) + self.text = CTkLabel( + self, text=_("Calculating..."), - image=hourglass_pic, + image=CTkImage(light_image=Image.open(resource_path("img/hourglass.png")), dark_image=Image.open(resource_path("img/hourglass.png")), size=(30, 30)), font=font, - compound=LEFT, + compound=tk.LEFT, ) - self.text.image = hourglass_pic - def start(self): - self.text.pack(fill="none", expand=True) - self.toplevel.update() - # force update of label to prevent white background on mac - self.text.configure(background="#313131") - self.text.update() + def setup_layout(self): + self.columnconfigure(0, weight=1) + self.rowconfigure(0, weight=1) + + def place_children(self): + self.text.grid(column=0, row=0) + + +class DynamicProgressFrame(CTkFrame): + def __init__(self, parent, label_lext=_("Progress:"), **kwargs): + super().__init__(parent, **kwargs) - def end(self): - self.text.pack_forget() - self.toplevel.update() + self.text = StringVar(self, value=label_lext) + self.variable = DoubleVar(self, value=0.0) + self.create_children() + self.setup_layout() + self.place_children() -class DynamicProgressFrame(ttk.Frame): - def __init__(self, master, label_lext=_("Progress:")): - super().__init__(width=400, height=200) - self.place(in_=master, anchor="c", relx=0.5, rely=0.5) - label = tk.Message( + def create_children(self): + self.label = CTkLabel( self, - text=label_lext, + textvariable=self.text, width=280, - font="Verdana 11 bold", - anchor="center", ) - label.pack() - self.pb = ttk.Progressbar( - self, orient="horizontal", mode="determinate", length=280 - ) - self.pb.pack() - self.update() + self.pb = CTkProgressBar(self, variable=self.variable) + + def setup_layout(self): + self.columnconfigure(0, weight=1) + self.rowconfigure(0, weight=1) + + def place_children(self): + self.label.grid(column=0, row=0, sticky=tk.NSEW) + self.pb.grid(column=0, row=1, sticky=tk.NSEW) def close(self): self.pb.pack_forget() @@ -65,8 +70,8 @@ def close(self): self.destroy() def update_progress(self, progress): - self.pb["value"] = progress * 100 - logging.info("Progress: {}%".format(int(self.pb["value"]))) + self.variable.set(progress) # * 100 + logging.info("Progress: {}%".format(int(self.variable.get()))) self.pb.update() @@ -102,10 +107,7 @@ def set_meta(self, total_length, object_name=None): def update(self, size): if not isinstance(size, int): - raise ValueError( - "{} type can not be displayed. " - "Please change it to Int.".format(type(size)) - ) + raise ValueError("{} type can not be displayed. " "Please change it to Int.".format(type(size))) self.current_progress += size self.update_queue.put((self.current_progress, self.total)) diff --git a/graxpert/ui/right_menu.py b/graxpert/ui/right_menu.py new file mode 100644 index 0000000..f1cbed7 --- /dev/null +++ b/graxpert/ui/right_menu.py @@ -0,0 +1,203 @@ +import tkinter as tk +from tkinter import messagebox + +import customtkinter as ctk +from customtkinter import CTkFont, CTkImage, CTkLabel, CTkScrollableFrame, CTkTextbox +from icecream import ic +from packaging import version +from PIL import Image + +from graxpert.ai_model_handling import list_local_versions, list_remote_versions +from graxpert.application.app import graxpert +from graxpert.application.app_events import AppEvents +from graxpert.application.eventbus import eventbus +from graxpert.localization import _, lang +from graxpert.resource_utils import resource_path +from graxpert.ui.widgets import ExtractionStep, GraXpertOptionMenu, ValueSlider, padx, pady + + +class HelpText(CTkTextbox): + def __init__(self, master, text="", rows=1, font=None, **kwargs): + super().__init__(master, width=250, fg_color="transparent", wrap="word", activate_scrollbars=False, **kwargs) + self.configure(height=self._font.metrics("linespace") * rows + 4 * pady) + self.insert("0.0", text) + + +class RightFrameBase(CTkScrollableFrame): + def __init__(self, master, **kwargs): + super().__init__(master, **kwargs) + self.row = 0 + self.heading_font = CTkFont(size=15, weight="bold") + self.heading_font2 = CTkFont(size=13, weight="bold") + + def nrow(self): + self.row += 1 + return self.row + + +class HelpFrame(RightFrameBase): + def __init__(self, master, **kwargs): + super().__init__(master, **kwargs) + + self.create_and_place_children() + self.setup_layout() + + def default_grid(self): + return {"column": 0, "row": self.nrow(), "padx": padx, "pady": pady, "sticky": tk.EW} + + def create_and_place_children(self): + logo = CTkImage( + light_image=Image.open(resource_path("img/GraXpert_LOGO_Hauptvariante.png")), + dark_image=Image.open(resource_path("img/GraXpert_LOGO_Hauptvariante.png")), + size=(225, 111), + ) + + CTkLabel(self, image=logo, text="").grid(column=0, row=self.nrow(), padx=padx, pady=pady, sticky=tk.NSEW) + CTkLabel(self, text=_("Instructions"), font=self.heading_font).grid(column=0, row=self.nrow(), pady=pady, sticky=tk.N) + + ExtractionStep(self, number=1, title=_(" Loading")).grid(**self.default_grid()) + HelpText(self, text=_("Load your image.")).grid(**self.default_grid()) + + ExtractionStep(self, number=2, title=_(" Stretch Options")).grid(**self.default_grid()) + HelpText(self, rows=2, text=_("Stretch your image if necessary to reveal gradients.")).grid(**self.default_grid()) + + ExtractionStep(self, number=3, title=_(" Sample Selection")).grid(**self.default_grid()) + HelpText( + self, + rows=5, + text=_("Select background points\n a) manually with left click\n b) automatically via grid (grid selection)" "\nYou can remove already set points by right clicking on them."), + ).grid(**self.default_grid()) + + ExtractionStep(self, number=4, title=_(" Calculation")).grid(**self.default_grid()) + HelpText(self, rows=2, text=_("Click on Calculate Background to get the processed image.")).grid(**self.default_grid()) + + ExtractionStep(self, number=5, title=_(" Saving")).grid(**self.default_grid()) + HelpText(self, text=_("Save the processed image.")).grid(**self.default_grid()) + + CTkLabel(self, text=_("Keybindings"), font=self.heading_font).grid(column=0, row=self.nrow(), pady=pady, sticky=tk.N) + + HelpText(self, text=_("Left click on picture: Set sample point")).grid(**self.default_grid()) + HelpText(self, rows=2, text=_("Left click on picture + drag: Move picture")).grid(**self.default_grid()) + HelpText(self, rows=2, text=_("Left click on sample point + drag: Move sample point")).grid(**self.default_grid()) + HelpText(self, rows=2, text=_("Right click on sample point: Delete sample point")).grid(**self.default_grid()) + HelpText(self, text=_("Mouse wheel: Zoom")).grid(**self.default_grid()) + HelpText(self, rows=3, text=_("Ctrl+Z/Y: Undo/Redo sample point")).grid(**self.default_grid()) + + def setup_layout(self): + self.columnconfigure(0, weight=1) + self.rowconfigure(0, weight=1) + + +class AdvancedFrame(RightFrameBase): + def __init__(self, master, **kwargs): + super().__init__(master, **kwargs) + + # sample points + self.sample_size = tk.IntVar() + self.sample_size.set(25) + if "sample_size" in graxpert.prefs: + self.sample_size.set(graxpert.prefs["sample_size"]) + self.sample_size.trace_add("write", lambda a, b, c: eventbus.emit(AppEvents.SAMPLE_SIZE_CHANGED, {"sample_size": self.sample_size.get()})) + + self.sample_color = tk.IntVar() + self.sample_color.set(55) + if "sample_color" in graxpert.prefs: + self.sample_color.set(graxpert.prefs["sample_color"]) + self.sample_color.trace_add("write", lambda a, b, c: eventbus.emit(AppEvents.SAMPLE_COLOR_CHANGED, {"sample_color": self.sample_color.get()})) + + # interpolation + self.rbf_kernels = ["thin_plate", "quintic", "cubic", "linear"] + self.rbf_kernel = tk.StringVar() + self.rbf_kernel.set(self.rbf_kernels[0]) + if "RBF_kernel" in graxpert.prefs: + self.rbf_kernel.set(graxpert.prefs["RBF_kernel"]) + self.rbf_kernel.trace_add("write", lambda a, b, c: eventbus.emit(AppEvents.RBF_KERNEL_CHANGED, {"RBF_kernel": self.rbf_kernel.get()})) + + self.spline_orders = ["1", "2", "3", "4", "5"] + self.spline_order = tk.StringVar() + self.spline_order.set("3") + if "spline_order" in graxpert.prefs: + self.spline_order.set(graxpert.prefs["spline_order"]) + self.spline_order.trace_add("write", lambda a, b, c: eventbus.emit(AppEvents.SPLINE_ORDER_CHANGED, {"spline_order": self.spline_order.get()})) + + self.corr_types = ["Subtraction", "Division"] + self.corr_type = tk.StringVar() + self.corr_type.set(self.corr_types[0]) + if "corr_type" in graxpert.prefs: + self.corr_type.set(graxpert.prefs["corr_type"]) + self.corr_type.trace_add("write", lambda a, b, c: eventbus.emit(AppEvents.CORRECTION_TYPE_CHANGED, {"corr_type": self.corr_type.get()})) + + # interface + self.langs = ["English", "Deutsch"] + self.lang = tk.StringVar() + if "lang" in graxpert.prefs: + self.lang.set(graxpert.prefs["lang"]) + self.lang.trace_add("write", lambda a, b, c: eventbus.emit(AppEvents.LANGUAGE_CHANGED, {"lang": self.lang.get()})) + + self.scaling = tk.DoubleVar() + self.scaling.set(1.0) + if "scaling" in graxpert.prefs: + self.scaling.set(graxpert.prefs["scaling"]) + self.scaling.trace_add("write", lambda a, b, c: ctk.set_widget_scaling(self.scaling.get())) + + # ai model + remote_versions = list_remote_versions() + local_versions = list_local_versions() + self.ai_options = set([]) + self.ai_options.update([rv["version"] for rv in remote_versions]) + self.ai_options.update([lv["version"] for lv in local_versions]) + self.ai_options = sorted(self.ai_options, key=lambda k: version.parse(k), reverse=True) + + self.ai_version = tk.StringVar(master) + self.ai_version.set("None") # default value + if "ai_version" in graxpert.prefs: + self.ai_version.set(graxpert.prefs["ai_version"]) + else: + self.ai_options.insert(0, "None") + self.ai_version.trace_add("write", lambda a, b, c: eventbus.emit(AppEvents.AI_VERSION_CHANGED, {"ai_version": self.ai_version.get()})) + + self.create_and_place_children() + self.setup_layout() + + def create_and_place_children(self): + CTkLabel(self, text=_("Advanced Settings"), font=self.heading_font).grid(column=0, row=self.nrow(), pady=pady, sticky=tk.N) + + # sample points + CTkLabel(self, text=_("Sample Points"), font=self.heading_font2).grid(column=0, row=self.nrow(), pady=pady, sticky=tk.N) + + ValueSlider(self, variable=self.sample_size, variable_name=_("Sample size"), min_value=5, max_value=50, precision=0).grid(**self.default_grid()) + ValueSlider(self, variable=self.sample_color, variable_name=_("Sample color"), min_value=0, max_value=360, precision=0).grid(**self.default_grid()) + + # interpolation + CTkLabel(self, text=_("Interpolation"), font=self.heading_font2).grid(column=0, row=self.nrow(), pady=pady, sticky=tk.N) + + CTkLabel(self, text=_("RBF Kernel")).grid(column=0, row=self.nrow(), pady=pady, sticky=tk.N) + GraXpertOptionMenu(self, variable=self.rbf_kernel, values=self.rbf_kernels).grid(**self.default_grid()) + + CTkLabel(self, text=_("Spline order")).grid(column=0, row=self.nrow(), pady=pady, sticky=tk.N) + GraXpertOptionMenu(self, variable=self.spline_order, values=self.spline_orders).grid(**self.default_grid()) + + CTkLabel(self, text=_("Correction")).grid(column=0, row=self.nrow(), pady=pady, sticky=tk.N) + GraXpertOptionMenu(self, variable=self.corr_type, values=self.corr_types).grid(**self.default_grid()) + + # interface + CTkLabel(self, text=_("Interface"), font=self.heading_font2).grid(column=0, row=self.nrow(), pady=pady, sticky=tk.N) + + def lang_change(lang): + messagebox.showerror("", _("Please restart the program to change the language.")) + + CTkLabel(self, text=_("Language")).grid(column=0, row=self.nrow(), pady=pady, sticky=tk.N) + GraXpertOptionMenu(self, variable=self.lang, values=self.langs).grid(**self.default_grid()) + + ValueSlider(self, variable=self.scaling, variable_name=_("Scaling"), min_value=1, max_value=2, precision=1).grid(**self.default_grid()) + + # ai model + CTkLabel(self, text=_("AI-Model"), font=self.heading_font2).grid(column=0, row=self.nrow(), pady=pady, sticky=tk.N) + GraXpertOptionMenu(self, variable=self.ai_version, values=self.ai_options).grid(**self.default_grid()) + + def setup_layout(self): + self.columnconfigure(0, weight=1) + self.rowconfigure(0, weight=1) + + def default_grid(self): + return {"column": 0, "row": self.nrow(), "padx": padx, "pady": pady} diff --git a/graxpert/ui/statusbar.py b/graxpert/ui/statusbar.py new file mode 100644 index 0000000..c7d7cb2 --- /dev/null +++ b/graxpert/ui/statusbar.py @@ -0,0 +1,60 @@ +import tkinter as tk + +from customtkinter import CTkFrame, CTkLabel +from icecream import ic + +from graxpert.application.app import graxpert +from graxpert.application.app_events import AppEvents +from graxpert.application.eventbus import eventbus +from graxpert.ui.ui_events import UiEvents + + +class StatusBar(CTkFrame): + def __init__(self, master, **kwargs): + super().__init__(master, **kwargs) + self.create_children() + self.setup_layout() + self.place_children() + self.register_events() + + # widget setup + def create_children(self): + self.label_image_info = CTkLabel(self, text="image info") + self.label_image_pixel = CTkLabel(self, text="(x, y)") + + def setup_layout(self): + self.columnconfigure(0, weight=1) + self.rowconfigure(0, weight=1) + + def place_children(self): + self.label_image_info.grid(column=0, row=0, sticky=tk.W) + self.label_image_pixel.grid(column=0, row=0, sticky=tk.E) + + def register_events(self): + eventbus.add_listener(AppEvents.LOAD_IMAGE_END, self.on_load_image_end) + eventbus.add_listener(UiEvents.MOUSE_MOVED, self.on_mouse_move) + + # event handling + def on_load_image_end(self, event): + self.label_image_info.configure( + text=f'{graxpert.data_type} : {graxpert.images["Original"].img_display.width} x {graxpert.images["Original"].img_display.height} {graxpert.images["Original"].img_display.mode}' + ) + + def on_mouse_move(self, event): + if graxpert.images[graxpert.display_type] is None: + return + + image_point = graxpert.to_image_point(event["mouse_event"].x, event["mouse_event"].y) + if len(image_point) != 0: + text = "x=" + f"{image_point[0]:.2f}" + ",y=" + f"{image_point[1]:.2f} " + if graxpert.images[graxpert.display_type].img_array.shape[2] == 3: + R, G, B = graxpert.images[graxpert.display_type].get_local_median(image_point) + text = text + "RGB = (" + f"{R:.4f}," + f"{G:.4f}," + f"{B:.4f})" + + if graxpert.images[graxpert.display_type].img_array.shape[2] == 1: + L = graxpert.images[graxpert.display_type].get_local_median(image_point) + text = text + "L= " + f"{L:.4f}" + + self.label_image_pixel.configure(text=text) + else: + self.label_image_pixel.configure(text="(--, --)") diff --git a/graxpert/ui/styling.py b/graxpert/ui/styling.py new file mode 100644 index 0000000..2e91f4b --- /dev/null +++ b/graxpert/ui/styling.py @@ -0,0 +1,14 @@ +import os +from tkinter import ttk + +import customtkinter + +from graxpert.resource_utils import resource_path +from graxpert.ui_scaling import get_scaling_factor + + +def style(root): + customtkinter.set_default_color_theme(resource_path("graxpert-dark-blue.json")) + customtkinter.set_appearance_mode("dark") + scaling = get_scaling_factor() + customtkinter.set_widget_scaling(scaling) diff --git a/graxpert/tooltip.py b/graxpert/ui/tooltip.py similarity index 64% rename from graxpert/tooltip.py rename to graxpert/ui/tooltip.py index 327e0ec..57f8850 100644 --- a/graxpert/tooltip.py +++ b/graxpert/ui/tooltip.py @@ -1,11 +1,14 @@ import tkinter as tk import tkinter.ttk as ttk -from graxpert.ui_scaling import get_scaling_factor + +from customtkinter import CTkFrame, CTkLabel, CTkToplevel + from graxpert.localization import _ +from graxpert.ui_scaling import get_scaling_factor class Tooltip: - ''' + """ It creates a tooltip for a given widget as the mouse goes on it. see: @@ -33,15 +36,9 @@ class Tooltip: Tested on Ubuntu 16.04/16.10, running Python 3.5.2 TODO: themes styles support - ''' - - def __init__(self, widget, - *, - pad=(5, 3, 5, 3), - text='widget info', - waittime=1000, - wraplength=250): + """ + def __init__(self, widget, *, pad=(5, 3, 5, 3), text="widget info", waittime=500, wraplength=250): self.waittime = waittime # in miliseconds, originally 500 self.wraplength = wraplength * get_scaling_factor() # in pixels, originally 180 self.widget = widget @@ -71,16 +68,12 @@ def unschedule(self): self.widget.after_cancel(id_) def show(self): - def tip_pos_calculator(widget, label, - *, - tip_delta=(10, 5), pad=(5, 3, 5, 3)): - + def tip_pos_calculator(widget, label, *, tip_delta=(10, 5), pad=(5, 3, 5, 3)): w = widget s_width, s_height = w.winfo_screenwidth(), w.winfo_screenheight() - width, height = (pad[0] + label.winfo_reqwidth() + pad[2], - pad[1] + label.winfo_reqheight() + pad[3]) + width, height = (pad[0] + label.winfo_reqwidth() + pad[2], pad[1] + label.winfo_reqheight() + pad[3]) mouse_x, mouse_y = w.winfo_pointerxy() @@ -97,7 +90,6 @@ def tip_pos_calculator(widget, label, offscreen = (x_delta, y_delta) != (0, 0) if offscreen: - if x_delta: x1 = mouse_x - tip_delta[0] - width @@ -121,23 +113,15 @@ def tip_pos_calculator(widget, label, widget = self.widget # creates a toplevel window - self.tw = tk.Toplevel(widget) + self.tw = CTkToplevel(widget) # Leaves only the label and removes the app window self.tw.wm_overrideredirect(True) - win = tk.Frame(self.tw, - borderwidth=0) - label = tk.Label(win, - text=self.text, - justify=tk.LEFT, - relief=tk.SOLID, - borderwidth=0, - wraplength=self.wraplength) - - label.grid(padx=(pad[0], pad[2]), - pady=(pad[1], pad[3]), - sticky=tk.NSEW) + win = CTkFrame(self.tw, border_width=0) + label = CTkLabel(win, text=self.text, justify=tk.LEFT, wraplength=self.wraplength) + + label.grid(padx=(pad[0], pad[2]), pady=(pad[1], pad[3]), sticky=tk.NSEW) win.grid() x, y = tip_pos_calculator(widget, label) @@ -151,48 +135,34 @@ def hide(self): self.tw = None -load_text = _("Load your image you would like to correct. \n" - "\n" - "Supported formats: .tiff, .fits, .png, .jpg \n" - "Supported bitdepths: 16 bit integer, 32 bit float") +load_text = _("Load your image you would like to correct. \n" "\n" "Supported formats: .tiff, .fits, .png, .jpg \n" "Supported bitdepths: 16 bit integer, 32 bit float") -stretch_text = _("Automatically stretch the picture to make gradients more visible. " - "The saved pictures are unaffected by the stretch.") +stretch_text = _("Automatically stretch the picture to make gradients more visible. " "The saved pictures are unaffected by the stretch.") reset_text = _("Reset all the chosen background points.") -bg_select_text = _("Creates a grid with the specified amount of points per row " - "and rejects points below a threshold defined by the tolerance.") +bg_select_text = _("Creates a grid with the specified amount of points per row " "and rejects points below a threshold defined by the tolerance.") -bg_tol_text = _("The tolerance adjusts the threshold for rejection of background points " - "with automatic background selection") +bg_tol_text = _("The tolerance adjusts the threshold for rejection of background points " "with automatic background selection") -bg_flood_text = _("If enabled, additional grid points are automatically created based on " - "1) the luminance of the sample just added and " - "2) the grid tolerance slider below.") +bg_flood_text = _("If enabled, additional grid points are automatically created based on " "1) the luminance of the sample just added and " "2) the grid tolerance slider below.") -num_points_text = _("Adjust the number of points per row for the grid created by" - " automatic background selection.") +num_points_text = _("Adjust the number of points per row for the grid created by" " automatic background selection.") interpol_type_text = _("Choose between different interpolation methods.") -smoothing_text = _("Adjust the smoothing parameter for the interpolation method. " - "A too small smoothing parameter may lead to over- and undershooting " - "inbetween background points, while a too large smoothing parameter " - "may not be suited for large deviations in gradients.") +smoothing_text = _( + "Adjust the smoothing parameter for the interpolation method. " + "A too small smoothing parameter may lead to over- and undershooting " + "inbetween background points, while a too large smoothing parameter " + "may not be suited for large deviations in gradients." +) -calculate_text = _("Use the specified interpolation method to calculate a background model " - "and subtract it from the picture. This may take a while.") +calculate_text = _("Use the specified interpolation method to calculate a background model " "and subtract it from the picture. This may take a while.") -saveas_text = _("Choose the bitdepth of the saved pictures and the file format. " - "If you are working with a .fits image the fits header will " - "be preserved.") +saveas_text = _("Choose the bitdepth of the saved pictures and the file format. " "If you are working with a .fits image the fits header will " "be preserved.") save_bg_text = _("Save the background model") save_pic_text = _("Save the processed picture") save_stretched_pic_text = _("Save the stretched and processed picture. The color saturation is not changed.") -display_text = _("Switch display between \n" - "\n" - "Original: Your original picture \n" - "Processed: Picture with subtracted background model \n" - "Background: The background model") \ No newline at end of file +display_text = _("Switch display between \n" "\n" "Original: Your original picture \n" "Processed: Picture with subtracted background model \n" "Background: The background model") diff --git a/graxpert/ui/ui_events.py b/graxpert/ui/ui_events.py new file mode 100644 index 0000000..1fe0a1c --- /dev/null +++ b/graxpert/ui/ui_events.py @@ -0,0 +1,16 @@ +from enum import Enum, auto + + +class UiEvents(Enum): + # main ui requests + RESET_ZOOM_REQUEST = auto() + # crop + TOGGLE_CROP_REQUEST = auto() + APPLY_CROP_REQUEST = auto() + # right sidebar requests + HELP_FRAME_TOGGLED = auto() + ADVANCED_FRAME_TOGGLED = auto() + # mouse events + MOUSE_MOVED = auto() + # cosmetics + DISPLAY_START_BADGE_REQUEST = auto() diff --git a/graxpert/ui/widgets.py b/graxpert/ui/widgets.py new file mode 100644 index 0000000..6676576 --- /dev/null +++ b/graxpert/ui/widgets.py @@ -0,0 +1,250 @@ +import tkinter as tk + +from customtkinter import CTkButton, CTkCheckBox, CTkEntry, CTkFrame, CTkImage, CTkLabel, CTkOptionMenu, CTkSlider, DoubleVar, StringVar, ThemeManager +from icecream import ic +from PIL import Image + +from graxpert.localization import _ +from graxpert.resource_utils import resource_path +from graxpert.ui_scaling import get_scaling_factor + +default_button_width = 200 +default_label_width = 200 + +padx = 5 * get_scaling_factor() +pady = 5 * get_scaling_factor() + + +class GraXpertButton(CTkButton): + def __init__(self, parent, width=default_button_width, **kwargs): + super().__init__(parent, width=width, **kwargs) + + +class GraXpertLabel(CTkLabel): + def __init__(self, parent, width=default_label_width, **kwargs): + super().__init__(parent, width=width, **kwargs) + + +class GraXpertOptionMenu(CTkOptionMenu): + def __init__(self, parent, width=default_label_width, **kwargs): + super().__init__(parent, width=width, **kwargs) + + +class GraXpertCheckbox(CTkCheckBox): + def __init__(self, parent, width=default_label_width, **kwargs): + super().__init__(parent, width=width, checkbox_width=20, checkbox_height=20, **kwargs) + + +class ExtractionStep(CTkFrame): + def __init__(self, parent, number=0, title="", **kwargs): + super().__init__(parent, **kwargs) + self.number = number + self.title = title + self.create_children() + self.setup_layout() + self.place_children() + + def create_children(self): + num_pic = CTkImage( + light_image=Image.open(resource_path(f"img/gfx_number_{self.number}.png")), + dark_image=Image.open(resource_path(f"img/gfx_number_{self.number}.png")), + size=(20, 20), + ) + self.title = GraXpertLabel(self, text=self.title, image=num_pic, anchor=tk.W, compound=tk.LEFT) + + def setup_layout(self): + self.columnconfigure(0, weight=0) + self.rowconfigure(0, weight=1) + + def place_children(self): + self.title.grid(column=0, row=0) + + +class ValueSlider(CTkFrame): + def __init__( + self, + parent, + width=default_label_width, + variable_name="", + variable=None, + default_value=0.5, + min_value=0, + max_value=1, + number_of_steps=None, + precision=1, + **kwargs, + ): + super().__init__(parent, width=width, **kwargs) + self.variable_name = variable_name + self.min_value = min_value + self.max_value = max_value + self.number_of_steps = number_of_steps + self.precision = precision + + if variable: + self.variable = variable + else: + self.variable = DoubleVar(value=default_value) + + self.variable.set(round(self.variable.get(), self.precision)) + self.entry_variable = StringVar(value=str(self.variable.get())) + self.slider_variable = DoubleVar(value=self.entry_variable.get()) + + self.create_children() + self.setup_layout() + self.place_children() + self.create_bindings() + + def create_children(self): + self.variable_label = CTkLabel(self, width=0, text=self.variable_name) + self.entry = CTkEntry(self, width=35, textvariable=self.entry_variable, validate="focusout") + self.entry_variable.trace_add("write", lambda a, b, c: self.format_entry()) + self.slider = CTkSlider( + self, + width=default_label_width, + command=self.on_slider, + variable=self.slider_variable, + from_=self.min_value, + to=self.max_value, + number_of_steps=self.number_of_steps, + ) + + def setup_layout(self): + self.columnconfigure(0, weight=1) + self.columnconfigure(1, weight=1) + self.rowconfigure(0, weight=1) + + def place_children(self): + self.variable_label.grid(column=0, row=0, pady=pady, sticky=tk.E) + self.entry.grid(column=1, row=0, padx=padx, pady=pady, sticky=tk.W) + self.slider.grid(column=0, row=1, columnspan=2, pady=pady, sticky=tk.NSEW) + + def create_bindings(self): + self.entry.bind("", lambda event: self.on_entry(event)) + self.entry.bind("", lambda event: self.on_entry(event)) + self.slider.bind("", lambda event: self.on_slider_release(event)) + + self.entry.bind("", self.up) + self.entry.bind("", self.down) + self.entry.bind("", self.down) + self.entry.bind("", self.up) + + self.slider.bind("", self.up) + self.slider.bind("", self.down) + self.slider.bind("", self.down) + self.slider.bind("", self.up) + + def transform_value(self, value): + if self.precision == 0: + value = int(value) + else: + value = round(value, self.precision) + return value + + def validate_entry(self): + try: + value = self.transform_value(float(self.entry_variable.get())) + if value < self.min_value or value > self.max_value: + return False + return True + except: + return False + + def format_entry(self): + if not self.validate_entry(): + self.entry.configure(fg_color="darkred") + else: + self.entry.configure(fg_color=ThemeManager.theme["CTkEntry"]["fg_color"]) + + def on_entry(self, event): + if not self.validate_entry(): + self.entry_variable.set(self.variable.get()) + else: + value = self.transform_value(float(self.entry_variable.get())) + self.entry_variable.set(str(value)) + self.slider_variable.set(value) + if self.variable.get() != value: + self.variable.set(value) + + def on_slider(self, value): + value = self.transform_value(value) + self.entry_variable.set(str(value)) + + def on_slider_release(self, event): + value = self.slider_variable.get() + if self.precision == 0: + value = int(float(value)) + else: + value = round(float(value), self.precision) + + if self.variable.get() != value: + self.variable.set(value) + + def up(self, event): + value = float(self.entry_variable.get()) + 10 ** (-self.precision) + if value > self.max_value: + return "break" + if self.precision == 0: + value = int(float(value)) + else: + value = round(float(value), self.precision) + self.slider_variable.set(value) + self.variable.set(value) + return "break" + + def down(self, event): + value = float(self.entry_variable.get()) - 10 ** (-self.precision) + if value < self.min_value: + return "break" + if self.precision == 0: + value = int(float(value)) + else: + value = round(float(value), self.precision) + self.slider_variable.set(value) + self.variable.set(value) + return "break" + + +class CollapsibleMenuFrame(CTkFrame): + def __init__(self, parent, title="", show=True, **kwargs): + super().__init__(parent, **kwargs) + + self.title = title + self.show = show + + def create_children(self): + self.title_label = GraXpertLabel( + self, + width=default_button_width + padx, + text=self.title, + pady=pady, + ) + self.toggle_button = GraXpertButton(self, width=25, text="+", command=self.toggle) + + self.sub_frame = CTkFrame(self, fg_color="transparent") + + def setup_layout(self): + self.columnconfigure(0, weight=1) + self.rowconfigure(0, weight=1) + + self.sub_frame.columnconfigure(0, minsize=padx, weight=0) + self.sub_frame.columnconfigure(1, weight=1) + self.sub_frame.rowconfigure(0, weight=0) + + def place_children(self): + self.title_label.grid(column=0, row=0, pady=pady, sticky=tk.W) + self.toggle_button.grid(column=0, row=0, pady=pady, sticky=tk.E) + self.place_sub_frame(self.show) + + def place_sub_frame(self, show): + if show: + self.sub_frame.grid(column=0, row=1, sticky=tk.NS) + self.toggle_button.configure(text="-") + else: + self.sub_frame.grid_forget() + self.toggle_button.configure(text="+") + + def toggle(self): + self.show = not self.show + self.place_sub_frame(self.show) + self.sub_frame.update() diff --git a/graxpert/ui_scaling.py b/graxpert/ui_scaling.py index 52c0228..57f5600 100644 --- a/graxpert/ui_scaling.py +++ b/graxpert/ui_scaling.py @@ -1,49 +1,16 @@ -import logging - -from screeninfo import get_monitors -from platform import system import os -from appdirs import user_config_dir -from graxpert.preferences import load_preferences -scaling_factor = None +from appdirs import user_config_dir -prefs_filename = os.path.join(user_config_dir(appname="GraXpert"), "preferences.json") -prefs = load_preferences(prefs_filename) -factor = 1.0 +from graxpert.preferences import load_preferences -if "scaling" in prefs: - factor = prefs["scaling"] def get_scaling_factor(): - global scaling_factor - - if scaling_factor is not None: - return scaling_factor + prefs_filename = os.path.join(user_config_dir(appname="GraXpert"), "preferences.json") + prefs = load_preferences(prefs_filename) + scaling_factor = 1.0 - try: - monitors = get_monitors() + if "scaling" in prefs: + scaling_factor = prefs["scaling"] - monitor = None - if len(monitors) == 1: - # use the only available monitor - monitor = monitors[0] - else: - try: - # try to query the primary monitor... - monitor = next(mon for mon in monitors if mon.is_primary) - except: - # ... if that fails try the first one in the list - monitor = monitors[0] - - dpi = monitor.width / (monitor.width_mm / 25.4) - scaling_factor = dpi / 96.0 - - except BaseException as e: - logging.warning("WARNING: could not calculate monitor dpi, {}".format(e)) - scaling_factor = 1.0 - - if isinstance(scaling_factor, float): - return scaling_factor * factor - else: - return 1.0 * factor + return scaling_factor diff --git a/requirements.txt b/requirements.txt index 90c980a..7681ba7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ appdirs astropy -hdpitkinter +customtkinter minio ml_dtypes numpy<=1.24.3,>=1.22 From f27e973647d81b87c012ea0679cc2c9ac2323152 Mon Sep 17 00:00:00 2001 From: David Schmelter Date: Sun, 24 Dec 2023 11:12:05 +0100 Subject: [PATCH 10/17] adapt build process to customtkinter --- GraXpert-linux.spec | 2 +- GraXpert-macos-arm64.spec | 2 +- GraXpert-macos-x86_64.spec | 2 +- GraXpert-win64.spec | 2 +- forest-dark.tcl | 534 ------------------------- forest-dark/border-accent-hover.png | Bin 385 -> 0 bytes forest-dark/border-accent.png | Bin 389 -> 0 bytes forest-dark/border-basic.png | Bin 333 -> 0 bytes forest-dark/border-hover.png | Bin 337 -> 0 bytes forest-dark/border-invalid.png | Bin 408 -> 0 bytes forest-dark/card.png | Bin 374 -> 0 bytes forest-dark/check-accent.png | Bin 434 -> 0 bytes forest-dark/check-basic.png | Bin 406 -> 0 bytes forest-dark/check-hover.png | Bin 434 -> 0 bytes forest-dark/check-tri-accent.png | Bin 317 -> 0 bytes forest-dark/check-tri-basic.png | Bin 300 -> 0 bytes forest-dark/check-tri-hover.png | Bin 320 -> 0 bytes forest-dark/check-unsel-accent.png | Bin 346 -> 0 bytes forest-dark/check-unsel-basic.png | Bin 333 -> 0 bytes forest-dark/check-unsel-hover.png | Bin 341 -> 0 bytes forest-dark/check-unsel-pressed.png | Bin 290 -> 0 bytes forest-dark/combo-button-basic.png | Bin 235 -> 0 bytes forest-dark/combo-button-focus.png | Bin 245 -> 0 bytes forest-dark/combo-button-hover.png | Bin 239 -> 0 bytes forest-dark/down.png | Bin 251 -> 0 bytes forest-dark/empty.png | Bin 130 -> 0 bytes forest-dark/hor-accent.png | Bin 162 -> 0 bytes forest-dark/hor-basic.png | Bin 162 -> 0 bytes forest-dark/hor-hover.png | Bin 162 -> 0 bytes forest-dark/notebook.png | Bin 193 -> 0 bytes forest-dark/off-accent.png | Bin 679 -> 0 bytes forest-dark/off-basic.png | Bin 640 -> 0 bytes forest-dark/off-hover.png | Bin 692 -> 0 bytes forest-dark/on-accent.png | Bin 676 -> 0 bytes forest-dark/on-basic.png | Bin 633 -> 0 bytes forest-dark/on-hover.png | Bin 685 -> 0 bytes forest-dark/radio-accent.png | Bin 565 -> 0 bytes forest-dark/radio-basic.png | Bin 543 -> 0 bytes forest-dark/radio-hover.png | Bin 579 -> 0 bytes forest-dark/radio-tri-accent.png | Bin 465 -> 0 bytes forest-dark/radio-tri-basic.png | Bin 450 -> 0 bytes forest-dark/radio-tri-hover.png | Bin 489 -> 0 bytes forest-dark/radio-unsel-accent.png | Bin 605 -> 0 bytes forest-dark/radio-unsel-basic.png | Bin 559 -> 0 bytes forest-dark/radio-unsel-hover.png | Bin 615 -> 0 bytes forest-dark/radio-unsel-pressed.png | Bin 468 -> 0 bytes forest-dark/rect-accent-hover.png | Bin 290 -> 0 bytes forest-dark/rect-accent.png | Bin 290 -> 0 bytes forest-dark/rect-basic.png | Bin 272 -> 0 bytes forest-dark/rect-hover.png | Bin 273 -> 0 bytes forest-dark/right.png | Bin 217 -> 0 bytes forest-dark/scale-hor.png | Bin 348 -> 0 bytes forest-dark/scale-vert.png | Bin 161 -> 0 bytes forest-dark/separator.png | Bin 128 -> 0 bytes forest-dark/sizegrip.png | Bin 459 -> 0 bytes forest-dark/spin-button-down-basic.png | Bin 153 -> 0 bytes forest-dark/spin-button-down-focus.png | Bin 160 -> 0 bytes forest-dark/spin-button-up.png | Bin 222 -> 0 bytes forest-dark/tab-accent.png | Bin 188 -> 0 bytes forest-dark/tab-basic.png | Bin 188 -> 0 bytes forest-dark/tab-hover.png | Bin 188 -> 0 bytes forest-dark/thumb-hor-accent.png | Bin 6307 -> 0 bytes forest-dark/thumb-hor-basic.png | Bin 5807 -> 0 bytes forest-dark/thumb-hor-hover.png | Bin 6417 -> 0 bytes forest-dark/thumb-vert-accent.png | Bin 269 -> 0 bytes forest-dark/thumb-vert-basic.png | Bin 253 -> 0 bytes forest-dark/thumb-vert-hover.png | Bin 269 -> 0 bytes forest-dark/tree-basic.png | Bin 149 -> 0 bytes forest-dark/tree-pressed.png | Bin 171 -> 0 bytes forest-dark/up.png | Bin 250 -> 0 bytes forest-dark/vert-accent.png | Bin 5612 -> 0 bytes forest-dark/vert-basic.png | Bin 575 -> 0 bytes forest-dark/vert-hover.png | Bin 5859 -> 0 bytes graxpert/application/app.py | 1 - graxpert/application/eventbus.py | 10 - graxpert/main.py | 2 +- graxpert/resource_utils.py | 14 - graxpert/ui/application_frame.py | 3 +- graxpert/ui/canvas.py | 1 - graxpert/ui/left_menu.py | 2 - graxpert/ui/right_menu.py | 1 - graxpert/ui/statusbar.py | 1 - graxpert/ui/styling.py | 11 +- graxpert/ui/widgets.py | 1 - setup.py | 3 +- 85 files changed, 14 insertions(+), 578 deletions(-) delete mode 100644 forest-dark.tcl delete mode 100644 forest-dark/border-accent-hover.png delete mode 100644 forest-dark/border-accent.png delete mode 100644 forest-dark/border-basic.png delete mode 100644 forest-dark/border-hover.png delete mode 100644 forest-dark/border-invalid.png delete mode 100644 forest-dark/card.png delete mode 100644 forest-dark/check-accent.png delete mode 100644 forest-dark/check-basic.png delete mode 100644 forest-dark/check-hover.png delete mode 100644 forest-dark/check-tri-accent.png delete mode 100644 forest-dark/check-tri-basic.png delete mode 100644 forest-dark/check-tri-hover.png delete mode 100644 forest-dark/check-unsel-accent.png delete mode 100644 forest-dark/check-unsel-basic.png delete mode 100644 forest-dark/check-unsel-hover.png delete mode 100644 forest-dark/check-unsel-pressed.png delete mode 100644 forest-dark/combo-button-basic.png delete mode 100644 forest-dark/combo-button-focus.png delete mode 100644 forest-dark/combo-button-hover.png delete mode 100644 forest-dark/down.png delete mode 100644 forest-dark/empty.png delete mode 100644 forest-dark/hor-accent.png delete mode 100644 forest-dark/hor-basic.png delete mode 100644 forest-dark/hor-hover.png delete mode 100644 forest-dark/notebook.png delete mode 100644 forest-dark/off-accent.png delete mode 100644 forest-dark/off-basic.png delete mode 100644 forest-dark/off-hover.png delete mode 100644 forest-dark/on-accent.png delete mode 100644 forest-dark/on-basic.png delete mode 100644 forest-dark/on-hover.png delete mode 100644 forest-dark/radio-accent.png delete mode 100644 forest-dark/radio-basic.png delete mode 100644 forest-dark/radio-hover.png delete mode 100644 forest-dark/radio-tri-accent.png delete mode 100644 forest-dark/radio-tri-basic.png delete mode 100644 forest-dark/radio-tri-hover.png delete mode 100644 forest-dark/radio-unsel-accent.png delete mode 100644 forest-dark/radio-unsel-basic.png delete mode 100644 forest-dark/radio-unsel-hover.png delete mode 100644 forest-dark/radio-unsel-pressed.png delete mode 100644 forest-dark/rect-accent-hover.png delete mode 100644 forest-dark/rect-accent.png delete mode 100644 forest-dark/rect-basic.png delete mode 100644 forest-dark/rect-hover.png delete mode 100644 forest-dark/right.png delete mode 100644 forest-dark/scale-hor.png delete mode 100644 forest-dark/scale-vert.png delete mode 100644 forest-dark/separator.png delete mode 100644 forest-dark/sizegrip.png delete mode 100644 forest-dark/spin-button-down-basic.png delete mode 100644 forest-dark/spin-button-down-focus.png delete mode 100644 forest-dark/spin-button-up.png delete mode 100644 forest-dark/tab-accent.png delete mode 100644 forest-dark/tab-basic.png delete mode 100644 forest-dark/tab-hover.png delete mode 100644 forest-dark/thumb-hor-accent.png delete mode 100644 forest-dark/thumb-hor-basic.png delete mode 100644 forest-dark/thumb-hor-hover.png delete mode 100644 forest-dark/thumb-vert-accent.png delete mode 100644 forest-dark/thumb-vert-basic.png delete mode 100644 forest-dark/thumb-vert-hover.png delete mode 100644 forest-dark/tree-basic.png delete mode 100644 forest-dark/tree-pressed.png delete mode 100644 forest-dark/up.png delete mode 100644 forest-dark/vert-accent.png delete mode 100644 forest-dark/vert-basic.png delete mode 100644 forest-dark/vert-hover.png diff --git a/GraXpert-linux.spec b/GraXpert-linux.spec index ba7df39..1af796d 100644 --- a/GraXpert-linux.spec +++ b/GraXpert-linux.spec @@ -7,7 +7,7 @@ block_cipher = None a = Analysis(['./graxpert/main.py'], pathex=[], binaries=[], - datas=[('./img/*', './img/'), ('./forest-dark.tcl', './'), ('./forest-dark/*', './forest-dark/')], + datas=[('./img/*', './img/'), ('./graxpert-dark-blue.json', './')], hiddenimports=['PIL._tkinter_finder', 'tkinter'], hookspath=['./releng'], hooksconfig={}, diff --git a/GraXpert-macos-arm64.spec b/GraXpert-macos-arm64.spec index 3fca945..3abb2e0 100644 --- a/GraXpert-macos-arm64.spec +++ b/GraXpert-macos-arm64.spec @@ -7,7 +7,7 @@ block_cipher = None a = Analysis(['./graxpert/main.py'], pathex=[], binaries=[], - datas=[('./img/*', './img/'), ('./forest-dark.tcl', './'), ('./forest-dark/*', './forest-dark/')], + datas=[('./img/*', './img/'), ('./graxpert-dark-blue.json', './')], hiddenimports=['PIL._tkinter_finder', 'tkinter'], hookspath=['./releng'], hooksconfig={}, diff --git a/GraXpert-macos-x86_64.spec b/GraXpert-macos-x86_64.spec index 455ee45..e1b77f6 100644 --- a/GraXpert-macos-x86_64.spec +++ b/GraXpert-macos-x86_64.spec @@ -7,7 +7,7 @@ block_cipher = None a = Analysis(['./graxpert/main.py'], pathex=[], binaries=[], - datas=[('./img/*', './img/'), ('./forest-dark.tcl', './'), ('./forest-dark/*', './forest-dark/')], + datas=[('./img/*', './img/'), ('./graxpert-dark-blue.json', './')], hiddenimports=['PIL._tkinter_finder', 'tkinter'], hookspath=['./releng'], hooksconfig={}, diff --git a/GraXpert-win64.spec b/GraXpert-win64.spec index f425d03..935257e 100644 --- a/GraXpert-win64.spec +++ b/GraXpert-win64.spec @@ -7,7 +7,7 @@ block_cipher = None a = Analysis(['./graxpert/main.py'], pathex=[], binaries=[], - datas=[('./img/*', './img/'), ('./forest-dark.tcl', './'), ('./forest-dark/*', './forest-dark/')], + datas=[('./img/*', './img/'), ('./graxpert-dark-blue.json', './')], hiddenimports=['PIL._tkinter_finder', 'tkinter'], hookspath=['./releng'], hooksconfig={}, diff --git a/forest-dark.tcl b/forest-dark.tcl deleted file mode 100644 index 6cc4d14..0000000 --- a/forest-dark.tcl +++ /dev/null @@ -1,534 +0,0 @@ -# Copyright (c) 2021 rdbende - -# The Forest theme is a beautiful and modern ttk theme inspired by Excel. - -package require Tk 8.6 - -namespace eval ttk::theme::forest-dark { - - variable version 1.0 - package provide ttk::theme::forest-dark $version - variable colors - array set colors { - -fg "#eeeeee" - -bg "#313131" - -disabledfg "#595959" - -disabledbg "#ffffff" - -selectfg "#ffffff" - -selectbg "#217346" - } - - proc LoadImages {imgdir} { - variable I - foreach file [glob -directory $imgdir *.png] { - set img [file tail [file rootname $file]] - set I($img) [image create photo -file $file -format png] - } - } - - LoadImages [file join [file dirname [info script]] forest-dark] - - # Settings - ttk::style theme create forest-dark -parent default -settings { - ttk::style configure . \ - -background $colors(-bg) \ - -foreground $colors(-fg) \ - -troughcolor $colors(-bg) \ - -focuscolor $colors(-selectbg) \ - -selectbackground $colors(-selectbg) \ - -selectforeground $colors(-selectfg) \ - -insertwidth 1 \ - -insertcolor $colors(-fg) \ - -fieldbackground $colors(-selectbg) \ - -font {Verdana 11} \ - -borderwidth 1 \ - -relief flat - - ttk::style map . -foreground [list disabled $colors(-disabledfg)] - - tk_setPalette background [ttk::style lookup . -background] \ - foreground [ttk::style lookup . -foreground] \ - highlightColor [ttk::style lookup . -focuscolor] \ - selectBackground [ttk::style lookup . -selectbackground] \ - selectForeground [ttk::style lookup . -selectforeground] \ - activeBackground [ttk::style lookup . -selectbackground] \ - activeForeground [ttk::style lookup . -selectforeground] - - option add *font [ttk::style lookup . -font] - - - # Layouts - ttk::style layout TButton { - Button.button -children { - Button.padding -children { - Button.label -side left -expand true - } - } - } - - ttk::style layout Toolbutton { - Toolbutton.button -children { - Toolbutton.padding -children { - Toolbutton.label -side left -expand true - } - } - } - - ttk::style layout TMenubutton { - Menubutton.button -children { - Menubutton.padding -children { - Menubutton.indicator -side right - Menubutton.label -side right -expand true - } - } - } - - ttk::style layout TOptionMenu { - OptionMenu.button -children { - OptionMenu.padding -children { - OptionMenu.indicator -side right - OptionMenu.label -side right -expand true - } - } - } - - ttk::style layout Accent.TButton { - AccentButton.button -children { - AccentButton.padding -children { - AccentButton.label -side left -expand true - } - } - } - - ttk::style layout TCheckbutton { - Checkbutton.button -children { - Checkbutton.padding -children { - Checkbutton.indicator -side left - Checkbutton.label -side right -expand true - } - } - } - - ttk::style layout Switch { - Switch.button -children { - Switch.padding -children { - Switch.indicator -side left - Switch.label -side right -expand true - } - } - } - - ttk::style layout ToggleButton { - ToggleButton.button -children { - ToggleButton.padding -children { - ToggleButton.label -side left -expand true - } - } - } - - ttk::style layout TRadiobutton { - Radiobutton.button -children { - Radiobutton.padding -children { - Radiobutton.indicator -side left - Radiobutton.label -side right -expand true - } - } - } - - ttk::style layout Vertical.TScrollbar { - Vertical.Scrollbar.trough -sticky ns -children { - Vertical.Scrollbar.thumb -expand true - } - } - - ttk::style layout Horizontal.TScrollbar { - Horizontal.Scrollbar.trough -sticky ew -children { - Horizontal.Scrollbar.thumb -expand true - } - } - - ttk::style layout TCombobox { - Combobox.field -sticky nswe -children { - Combobox.padding -expand true -sticky nswe -children { - Combobox.textarea -sticky nswe - } - } - Combobox.button -side right -sticky ns -children { - Combobox.arrow -sticky nsew - } - } - - ttk::style layout TSpinbox { - Spinbox.field -sticky nsew -children { - Spinbox.padding -expand true -sticky nswe -children { - Spinbox.textarea -sticky nsew - } - - } - null -side right -sticky nsew -children { - Spinbox.uparrow -side right -sticky nsew -children { - Spinbox.symuparrow - } - Spinbox.downarrow -side left -sticky nsew -children { - Spinbox.symdownarrow - } - } - } - - ttk::style layout Horizontal.TSeparator { - Horizontal.separator -sticky nswe - } - - ttk::style layout Vertical.TSeparator { - Vertical.separator -sticky nswe - } - - ttk::style layout Card { - Card.field { - Card.padding -expand 1 - } - } - - ttk::style layout TLabelframe { - Labelframe.border { - Labelframe.padding -expand 1 -children { - Labelframe.label -side left - } - } - } - - ttk::style layout TNotebook { - Notebook.border -children { - TNotebook.Tab -expand 1 -side top - Notebook.client -sticky nsew - } - } - - ttk::style layout TNotebook.Tab { - Notebook.tab -children { - Notebook.padding -side top -children { - Notebook.label - } - } - } - - ttk::style layout Treeview.Item { - Treeitem.padding -sticky nswe -children { - Treeitem.indicator -side left -sticky {} - Treeitem.image -side left -sticky {} - Treeitem.text -side left -sticky {} - } - } - - - # Elements - - # Button - ttk::style configure TButton -padding {8 12 8 12} -width -10 -anchor center - - ttk::style element create Button.button image \ - [list $I(rect-basic) \ - {selected disabled} $I(rect-basic) \ - disabled $I(rect-basic) \ - selected $I(rect-basic) \ - pressed $I(rect-basic) \ - active $I(rect-hover) \ - ] -border 4 -sticky nsew - - # Toolbutton - ttk::style configure Toolbutton -padding {8 4 8 4} -width -10 -anchor center - - ttk::style element create Toolbutton.button image \ - [list $I(empty) \ - {selected disabled} $I(empty) \ - disabled $I(empty) \ - selected $I(rect-basic) \ - pressed $I(rect-basic) \ - active $I(rect-basic) \ - ] -border 4 -sticky nsew - - # Menubutton - ttk::style configure TMenubutton -padding {8 4 4 4} -anchor center - - ttk::style element create Menubutton.button image \ - [list $I(rect-basic) \ - disabled $I(rect-basic) \ - pressed $I(rect-basic) \ - active $I(rect-hover) \ - ] -border 4 -sticky nsew - - ttk::style element create Menubutton.indicator image \ - [list $I(down) \ - active $I(down) \ - pressed $I(down) \ - disabled $I(down) \ - ] -width 15 -sticky e - - # OptionMenu - ttk::style configure TOptionMenu -padding {8 0 4 0} - - ttk::style element create OptionMenu.button image \ - [list $I(rect-basic) \ - disabled $I(rect-basic) \ - pressed $I(rect-basic) \ - active $I(rect-hover) \ - ] -border 4 -sticky nsew - - ttk::style element create OptionMenu.indicator image \ - [list $I(down) \ - active $I(down) \ - pressed $I(down) \ - disabled $I(down) \ - ] -width 15 -sticky e - - # AccentButton - ttk::style configure Accent.TButton -padding {8 4 8 4} -width -10 -anchor center -foreground #eeeeee - - ttk::style element create AccentButton.button image \ - [list $I(rect-accent) \ - {selected disabled} $I(rect-accent-hover) \ - disabled $I(rect-accent-hover) \ - selected $I(rect-accent) \ - pressed $I(rect-accent) \ - active $I(rect-accent-hover) \ - ] -border 4 -sticky nsew - - # Checkbutton - ttk::style configure TCheckbutton -padding 4 - - ttk::style element create Checkbutton.indicator image \ - [list $I(check-unsel-accent-scaled) \ - {alternate disabled} $I(check-tri-basic) \ - {selected disabled} $I(check-basic-scaled) \ - disabled $I(check-unsel-basic-scaled) \ - {pressed alternate} $I(check-tri-hover) \ - {active alternate} $I(check-tri-hover) \ - alternate $I(check-tri-accent) \ - {pressed selected} $I(check-hover-scaled) \ - {active selected} $I(check-hover-scaled) \ - selected $I(check-accent-scaled) \ - {pressed !selected} $I(check-unsel-pressed-scaled) \ - active $I(check-unsel-hover-scaled) \ - ] -sticky w - - # Switch - ttk::style element create Switch.indicator image \ - [list $I(off-accent) \ - {selected disabled} $I(on-basic) \ - disabled $I(off-basic) \ - {pressed selected} $I(on-accent) \ - {active selected} $I(on-hover) \ - selected $I(on-accent) \ - {pressed !selected} $I(off-accent) \ - active $I(off-hover) \ - ] -width 46 -sticky w - - # ToggleButton - ttk::style configure ToggleButton -padding {8 4 8 4} -width -10 -anchor center - - ttk::style element create ToggleButton.button image \ - [list $I(rect-basic) \ - {selected disabled} $I(rect-accent-hover) \ - disabled $I(rect-basic) \ - {pressed selected} $I(rect-basic) \ - {active selected} $I(rect-accent-hover) \ - selected $I(rect-accent) \ - {pressed !selected} $I(rect-accent) \ - active $I(rect-hover) \ - ] -border 4 -sticky nsew - - # Radiobutton - ttk::style configure TRadiobutton -padding 4 - - ttk::style element create Radiobutton.indicator image \ - [list $I(radio-unsel-accent) \ - {alternate disabled} $I(radio-tri-basic) \ - {selected disabled} $I(radio-basic) \ - disabled $I(radio-unsel-basic) \ - {pressed alternate} $I(radio-tri-hover) \ - {active alternate} $I(radio-tri-hover) \ - alternate $I(radio-tri-accent) \ - {pressed selected} $I(radio-hover) \ - {active selected} $I(radio-hover) \ - selected $I(radio-accent) \ - {pressed !selected} $I(radio-unsel-pressed) \ - active $I(radio-unsel-hover) \ - ] -width 26 -sticky w - - # Scrollbar - ttk::style element create Horizontal.Scrollbar.trough image $I(hor-basic) \ - -sticky ew - - ttk::style element create Horizontal.Scrollbar.thumb image \ - [list $I(hor-accent) \ - disabled $I(hor-basic) \ - pressed $I(hor-hover) \ - active $I(hor-hover) \ - ] -sticky ew - - ttk::style element create Vertical.Scrollbar.trough image $I(vert-basic-scaled) \ - -sticky ns - - ttk::style element create Vertical.Scrollbar.thumb image \ - [list $I(vert-hover-scaled) \ - disabled $I(vert-basic-scaled) \ - pressed $I(vert-hover-scaled) \ - active $I(vert-hover-scaled) \ - ] -sticky ns - - # Scale - ttk::style element create Horizontal.Scale.trough image $I(scale-hor-scaled) \ - -border 5 -padding 0 - - ttk::style element create Horizontal.Scale.slider image \ - [list $I(thumb-hor-accent-scaled) \ - disabled $I(thumb-hor-basic-scaled) \ - pressed $I(thumb-hor-hover-scaled) \ - active $I(thumb-hor-hover-scaled) \ - ] -sticky {} - - ttk::style element create Vertical.Scale.trough image $I(scale-vert) \ - -border 5 -padding 0 - - ttk::style element create Vertical.Scale.slider image \ - [list $I(thumb-vert-accent) \ - disabled $I(thumb-vert-basic) \ - pressed $I(thumb-vert-hover) \ - active $I(thumb-vert-hover) \ - ] -sticky {} - - # Progressbar - ttk::style element create Horizontal.Progressbar.trough image $I(hor-basic) \ - -sticky ew - - ttk::style element create Horizontal.Progressbar.pbar image $I(hor-accent) \ - -sticky ew - - ttk::style element create Vertical.Progressbar.trough image $I(vert-basic) \ - -sticky ns - - ttk::style element create Vertical.Progressbar.pbar image $I(vert-accent) \ - -sticky ns - - # Entry - ttk::style element create Entry.field image \ - [list $I(border-basic) \ - {focus hover} $I(border-accent) \ - invalid $I(border-invalid) \ - disabled $I(border-basic) \ - focus $I(border-accent) \ - hover $I(border-hover) \ - ] -border 5 -padding {8} -sticky nsew - - # Combobox - ttk::style map TCombobox -selectbackground [list \ - {!focus} $colors(-selectbg) \ - {readonly hover} $colors(-selectbg) \ - {readonly focus} $colors(-selectbg) \ - ] - - ttk::style map TCombobox -selectforeground [list \ - {!focus} $colors(-selectfg) \ - {readonly hover} $colors(-selectfg) \ - {readonly focus} $colors(-selectfg) \ - ] - - ttk::style element create Combobox.field image \ - [list $I(border-basic) \ - {readonly disabled} $I(rect-basic) \ - {readonly pressed} $I(rect-basic) \ - {readonly focus hover} $I(rect-hover) \ - {readonly focus} $I(rect-hover) \ - {readonly hover} $I(rect-hover) \ - {focus hover} $I(border-accent) \ - readonly $I(rect-basic) \ - invalid $I(border-invalid) \ - disabled $I(border-basic) \ - focus $I(border-accent) \ - hover $I(border-hover) \ - ] -border 5 -padding {8 8 28 8} - - ttk::style element create Combobox.button image \ - [list $I(combo-button-basic) \ - {!readonly focus} $I(combo-button-focus) \ - {readonly focus} $I(combo-button-hover) \ - {readonly hover} $I(combo-button-hover) - ] -border 5 -padding {2 6 6 6} - - ttk::style element create Combobox.arrow image $I(down) -width 15 -sticky e - - # Spinbox - ttk::style element create Spinbox.field image \ - [list $I(border-basic) \ - invalid $I(border-invalid) \ - disabled $I(border-basic) \ - focus $I(border-accent) \ - hover $I(border-hover) \ - ] -border 5 -padding {8 8 54 8} -sticky nsew - - ttk::style element create Spinbox.uparrow image $I(spin-button-up) -border 4 -sticky nsew - - ttk::style element create Spinbox.downarrow image \ - [list $I(spin-button-down-basic) \ - focus $I(spin-button-down-focus) \ - ] -border 4 -sticky nsew - - ttk::style element create Spinbox.symuparrow image $I(up) -width 15 -sticky {} - ttk::style element create Spinbox.symdownarrow image $I(down) -width 17 -sticky {} - - # Sizegrip - ttk::style element create Sizegrip.sizegrip image $I(sizegrip) \ - -sticky nsew - - # Separator - ttk::style element create Horizontal.separator image $I(separator) - - ttk::style element create Vertical.separator image $I(separator) - - # Card - ttk::style element create Card.field image $I(card) \ - -border 10 -padding 4 -sticky nsew - - # Labelframe - ttk::style element create Labelframe.border image $I(card) \ - -border 5 -padding 4 -sticky nsew - - # Notebook - ttk::style configure TNotebook -padding 2 - - ttk::style element create Notebook.border image $I(card) -border 5 - - ttk::style element create Notebook.client image $I(notebook) -border 5 - - ttk::style element create Notebook.tab image \ - [list $I(tab-basic) \ - selected $I(tab-accent) \ - active $I(tab-hover) \ - ] -border 5 -padding {14 4} - - # Treeview - ttk::style element create Treeview.field image $I(card) \ - -border 5 - - ttk::style element create Treeheading.cell image \ - [list $I(tree-basic) \ - pressed $I(tree-pressed) - ] -border 5 -padding 6 -sticky nsew - - ttk::style element create Treeitem.indicator image \ - [list $I(right) \ - user2 $I(empty) \ - user1 $I(down) \ - ] -width 17 -sticky {} - - ttk::style configure Treeview -background $colors(-bg) - ttk::style configure Treeview.Item -padding {2 0 0 0} - - ttk::style map Treeview \ - -background [list selected $colors(-selectbg)] \ - -foreground [list selected $colors(-selectfg)] - - # Sashes - #ttk::style map TPanedwindow -background [list hover $colors(-bg)] - } -} diff --git a/forest-dark/border-accent-hover.png b/forest-dark/border-accent-hover.png deleted file mode 100644 index 9e6cc8e13776930bae33ae496edd5130e09943af..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 385 zcmV-{0e=38P)pF8FWQhbW?9;ba!ELWdL_~cP?peYja~^aAhuUa%Y?FJQ@H10S-w- zK~y-6?bESt!Y~wt;os(_P9Y6T7&@>am1g4s`tFr@h6)BYq^bgzDg-bwc87?HN>xQN zb>Qsx^YOK;CX-3c*n@UYx%2n!n|8w&hFwf#1Lz_@BUF+S?)ST!60rHr24(8a&S`7tpe{0N8QL zag_Wx?V4uCQG$JX06pF8FWQhbW?9;ba!ELWdL_~cP?peYja~^aAhuUa%Y?FJQ@H10TM|> zK~y-6?b9)C!Y~xZ@xP7jkOYQQs>IZ)zy*+c{|i*ypaRhasg(*tN!-}6J5XplASIbP z@Fp+s=kGnsVzF4nCL3U9Ij+#4B%R6!^v;p4O60+#nry(wV?k7+8+0lqakmM+FpQUb zl%3^AEJV_EiQZad9a>qzNa{&~ogR&9nxR~w0Z7*+$)@ZSPS%0pO9et;XF2}HAWu8f zTib2aUuC{~<3MnJfwp5_N_sx`RxaUJ@CyDXFqr;!NPc=H{`hU`Nzxm1lOX`YN2K04 z0PHl!-lczTYm?00XH*Xv0Q$W~rK=KO7@`u``3O57^-3vSBz-<0<-s#v?lEsQqKTa$ j(L~JaC*!9b!Zv&Xa5G~CEjM@)00000NkvXXu0mjfhT5QX diff --git a/forest-dark/border-basic.png b/forest-dark/border-basic.png deleted file mode 100644 index a483271f741ac189a7bb18facfa74d3afdd3f93c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 333 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc1|)ksWqE-VXMsm#F#`kNVGw3Kp1&dmC@5Lt z8c`CQpH@mmtT}V`<;yxP|+<<7sn8b-nUowcC{J^v^_j; zv7|%bqx#Z&&EM-yla{XVoa|sRbB^w`*3{KsZ~TfoaQK60u+QZ$(}E63FfSE3wdu&z z8Ip=DZ+`blD0_Hy7Rn^9J-Tb1hh{8OCr4B^&*6sDNQYw*5!r<@$5me}pCWYbvrC}J zi{Elinyk<7xZij`r+CFGu0=ZD2A^{$%v5Y?*rEIPC8PWY#{aBF#vL!rTwG3AI=2Wc z*t(DRS}E^)+avqlXWSOcypm5Fb2?rjG{H=WZ=$!ESdi(F4TYk9c3zjEHk cs=Xgrx&DRD-{csr5A-F2r>mdKI;Vst05q|OZ~y=R diff --git a/forest-dark/border-hover.png b/forest-dark/border-hover.png deleted file mode 100644 index dcd837a4d84c06673ede146505918d1f7c297857..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 337 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc1|)ksWqE-VXMsm#F#`kNVGw3Kp1&dmC@5Lt z8c`CQpH@mmtT}V`<;yxP|-b47sn8b-nUma_O&R8v_3R8 zVByv=X=KTYJMg_;)SLO@g4Q4(mN~j+vvamZ9l!RUao?RekL-4>t1dp5*wZM$G$m-x z$|+3}%6q<7?2W4^I+wWCm4orHfx^-fu9TjInNkti7r)!iJ0{U0V9@ti^TqNhLd!Cl z7&6v6wg}94{!{r)`RPpy)@2KLX%?NEesGR4hoZvtrS^9p@ZNi{|3DCzMDldSV-jAP z3WjbbzQvL`x5aL6Ypi&$xU?nc)naeSPgg&ebxsLQ0NFW)4FCWD diff --git a/forest-dark/border-invalid.png b/forest-dark/border-invalid.png deleted file mode 100644 index 63cdd6ecafb53e64087c8c045260ef7e1bf0e327..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 408 zcmV;J0cZY+P)pF8FWQhbW?9;ba!ELWdL_~cP?peYja~^aAhuUa%Y?FJQ@H10VPR9 zK~y-6?bJI?!$1^;;eRGGjy*<7D6<1gRHpO}pyHuqHApmB3`Hc$tbjB~R7hDuI)o(> zd*a7XBtQy~Y*!RH)otd}y;B?!$FUprD#peV>5@RuDw%_2p_pxaR+U7JdKGtr4Mw5rX=CiPhw3dyqfvicqf}6c@Yb+|#C9uQhnb&H&w3f{C zK0?6QSk82b{Q9#+u3BM-dCD~azHofsmbzwV{<`VG+wRIAIf6&anMprB-l zYeY$Kep*R+Vo@qXd3m{BW?pu2a$-TMUVc&f>~}U&3=E9So-U3d5v^};7x%!EJEksD>uvhex%=p*sYk_vQ@ShtKN!?pO?hllm$^1^t!tpjiO({c z-G+~L>E+yR`@PRGDx2pp!*iKwsg4|qniDz#LPdY=J8pAcAzOD&@s01cns2rXF!eol zn3not7vCxku8Q};yW=X~2ToHBnfJWIaM`y#U4~AYtW%{vg)==-bI2n^EKBN9SahW? z+@saNyfZN*+N-!s%*}p?YaA9t=iEndhtmF6EZ{CA-fH!#c?JYD@<);T3K0RULgna2PC diff --git a/forest-dark/check-accent.png b/forest-dark/check-accent.png deleted file mode 100644 index 81f4a62b75cfe8781af677d8f00a4426bcb3335c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 434 zcmV;j0ZsmiP)pF8FWQhbW?9;ba!ELWdL_~cP?peYja~^aAhuUa%Y?FJQ@H10Y6DZ zK~y-6tWmxms!UWD!j;0v`fmLSoz#|va`@bl1PcGJcAsvcD23;Lvq={AUmDYEKC zyJO#SzG8V1BDx#wjTM>W22%UZw&Xm;&YENvf0Av^cfwYdXpj&-&-MppZRw0s#5m@c z@qVDJEuFg*r7g3oL-K>o2i*W=ZNcq^m4cl?Lgzk31*_h~Q3Z7zmeMBCYF0BR@CtX;uZLc!66$Hr6 zTa}?A!PwfH{z@5#1d0APFqmXWd%QrIlAl~N(T?`Z9Fe&p9;P(jvLBzhYASa$Uf0C8 c32K$|2^9E~gB24Jx&QzG07*qoM6N<$g52P`X#fBK diff --git a/forest-dark/check-basic.png b/forest-dark/check-basic.png deleted file mode 100644 index dd93bbc3e9860ef95783009a69067ac98c94c591..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 406 zcmV;H0crk;P)pF8FWQhbW?9;ba!ELWdL_~cP?peYja~^aAhuUa%Y?FJQ@H10V7F7 zK~y-6tlY&P=PG<%Rq4PxkwL z(ppm#1;!Y%EMrVDM!fgbbxqs0d}*!O?RKM~t}M&Q^BiLg&N)<36fa}X@M^U}8DrkY zy5{pa>J#g_lv1cypJR+PP4g3d1q&fyt>tt&y$^;EsHzGPp)AYyz(;Je*)W^U{s%({ zFJrx+wHD_bDW%a^FSuT>@!oSd97rjR!T`RW4~xZu$z;Ouc%-T-LI|Xk24TGm0rEVj zEK6>;8zRDXyB&n}E__@nB519dPN##gKS6-yayc04QZdHSProH1DDVBpz@}+XbzSp` zl`%%n=QFBpTby%TuUAB5G$bNiE*Gq|-0ye30ga@k9&)`CL;wH)07*qoM6N<$f-}RZ AP5=M^ diff --git a/forest-dark/check-hover.png b/forest-dark/check-hover.png deleted file mode 100644 index 6a9005646fb98ad5b9c5fd4209c92f1f56bea661..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 434 zcmV;j0ZsmiP)pF8FWQhbW?9;ba!ELWdL_~cP?peYja~^aAhuUa%Y?FJQ@H10Y6DZ zK~y-6t<%d&!$1^(;eRF*lQgB?+Jz#xw!1D|2>R|~6>*`sk-mXQ6K}NI37N^bC`HlU zlK3wV%*Wxth+ePfil!p#wlM7gl%!OqKqVRBV1yXwsG_O(JnP{6lF%$AF?$go?SQzS zBC~F*I`$*yOY~6-Y1%<;Y?kc=$kI1kl2d}pn#>BkME{*v!X}oeKP0?=s}D+TiLMev z?$F0O^+2gD(d87Ioa|z^Hdcc|TX5B|xnO*e5M8GT`>n2v%>{M0g_mZ8H_r&`N?`!% z_rM%9$fO{=eVHCVD-dQqY%U0p??<>|o9ONpJi+H_2fOL7#1?|HO|=Er^w!3ff&lqp zV{K@b#1y3;ElL=NR7v)4pc-mqI2a-F`df;;Ab5C3ig8ZdPw}5L!p!+1t03d>A1B1; cL&U`L1q1<;AdP+%DF6Tf07*qoM6N<$f+FL+HUIzs diff --git a/forest-dark/check-tri-accent.png b/forest-dark/check-tri-accent.png deleted file mode 100644 index 4a49300f9a01c0cb280bb061bc17eb0cfa8f60d9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 317 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc1|)ksWqE-VXMsm#F#`kNVGw3Kp1&dmC@5Lt z8c`CQpH@mmtT}V`<;yxP|+z*7sn8b-nW+yavm}eIrj1W zUbU%VTy8IztQ-$=%G_aHBHh8PaYJs+HMPt=8WkI+X4$;_KVyQtXT_A%nVSn6s_fdr zgampVXNqmQ(6LRuAXOnp=E07Df~Gdd?<~&Cl_f-#PI?#E%J1*DeZun4ru$&j$^MPi zX;Mw>|0DhN6ns;4+GV6ZzhL~mk*Z1E4!2gfwhGlzf@wZj_HaEB2Gce-Q;eN|I zrJYSC*I(hW-LVL3m)TrrraDfV&hef11Yi3}tCro|lS&y^>8|^(WH({{4Y5$5uNXXC L{an^LB{Ts5lAL+s diff --git a/forest-dark/check-tri-basic.png b/forest-dark/check-tri-basic.png deleted file mode 100644 index 219b92d77deca37c57a7a6c8e027e72ced9fd681..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 300 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc1|)ksWqE-VXMsm#F#`kNVGw3Kp1&dmC@5Lt z8c`CQpH@mmtT}V`<;yxP|;3L7sn8b-nW+wxtk3H+8(YK zKUwf7EiF9!|9|;H9!b^NQXECs^-49j9yN>p-N#wE<3Ma%;*O~7hjW;X4@)!~cX9-I zbr>o=?>QCKz3#Q2bM-L^6JO>uHx9-pbAC%yx7^;wYWVs3jkwtB3xqDe6%<@Cqins^ zE{WqGpB~=#e*Qf1`5*ZIF;(nncd!1o?o+(j#H1Oknu5HJ9w?1+Z?X$&%Is1Ji*L4$2tFRHqa9cp00i_>zopr04+{*i2wiq diff --git a/forest-dark/check-tri-hover.png b/forest-dark/check-tri-hover.png deleted file mode 100644 index ee9d10895a7a54a71e5558bf2fb737370020fc90..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 320 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc1|)ksWqE-VXMsm#F#`kNVGw3Kp1&dmC@5Lt z8c`CQpH@mmtT}V`<;yxP|;aW7sn8b-nWw+`I;PfT~(*WZ+zYCplPYsTAH+a z7bLTCEEc}yP$v2(mCx#$fH2p&$1K-gYwS|wv3>DSM4RQa?GCTS-&4yY`UCrvHuPOR zzDe45(Sf=7-&nL+%C~7>y7t09+@JmP$q$KlD-Z4eAl|aJ{>HcT^*;`sx@9qW;n{JAQnCxKS(XlHleYu1a P=rIOQS3j3^P6mmtT}V`<;yxP|%mVCm1ueXhpt8cy-clofBrVFQcWm zy-_cm_hv8i(idWum(H71W&dyet9LW1Z8wYJvu;Li!--jCW_Xm_ckuJy$UU{V8!#kjt89ZJ6T-G@yGywo=$BpR# diff --git a/forest-dark/check-unsel-basic.png b/forest-dark/check-unsel-basic.png deleted file mode 100644 index a483271f741ac189a7bb18facfa74d3afdd3f93c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 333 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc1|)ksWqE-VXMsm#F#`kNVGw3Kp1&dmC@5Lt z8c`CQpH@mmtT}V`<;yxP|+<<7sn8b-nUowcC{J^v^_j; zv7|%bqx#Z&&EM-yla{XVoa|sRbB^w`*3{KsZ~TfoaQK60u+QZ$(}E63FfSE3wdu&z z8Ip=DZ+`blD0_Hy7Rn^9J-Tb1hh{8OCr4B^&*6sDNQYw*5!r<@$5me}pCWYbvrC}J zi{Elinyk<7xZij`r+CFGu0=ZD2A^{$%v5Y?*rEIPC8PWY#{aBF#vL!rTwG3AI=2Wc z*t(DRS}E^)+avqlXWSOcypm5Fb2?rjG{H=WZ=$!ESdi(F4TYk9c3zjEHk cs=Xgrx&DRD-{csr5A-F2r>mdKI;Vst05q|OZ~y=R diff --git a/forest-dark/check-unsel-hover.png b/forest-dark/check-unsel-hover.png deleted file mode 100644 index da35159a31d1e49e185a5f5190a226c101881cdd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 341 zcmV-b0jmCqP)pF8FWQhbW?9;ba!ELWdL_~cP?peYja~^aAhuUa%Y?FJQ@H10OCnR zK~y-6?bWev!Y~kk;or_tN5RTc7_+o2W%JDQ_W?RoEHJTwDzyrsaboNat*QrE1Xs{%(fwac}Mv;$V*1kR|U=Lp6T-& z;+G|6+c4SG!zTMl`N-Yo2tfN(qQlIP?V78wzsc^zd2Y~%Mh{%`zqF4eQ4GAm1@H)t z;0Rs>hDb8EyD865A))2q>8s*qu&)Ya-=gz%P4{T&7M7G}S54L}EZxHt^ZJ2u&ZT50 nzb_z(8U(74kbV1&eV+LO@-R}CXbJlT00000NkvXXu0mjf2S$=f diff --git a/forest-dark/check-unsel-pressed.png b/forest-dark/check-unsel-pressed.png deleted file mode 100644 index d7a88253b6fad6b178350a54fb04328b857a9345..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 290 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc1|)ksWqE-VXMsm#F#`kNVGw3Kp1&dmC@5Lt z8c`CQpH@mmtT}V`<;yxP|fb1f8?+_!m~}?=ob_GFR9*i~ zRlViwmU8JGjnY4)Gu~uLNU~3hTF>TX+EAX#Ej4}pl8a8U?-E|QwYWAvFl*@f+}`6{ h@K$i1c7Xju*4=Nc#9F^kWd}N)!PC{xWt~$(69CZ+YjFSo diff --git a/forest-dark/combo-button-basic.png b/forest-dark/combo-button-basic.png deleted file mode 100644 index 7582f0effde6403106e83dd438bf9556434c54be..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 235 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc1|)ksWqE-VXMsm#F#`kNVGw3Kp1&dmC@5Lt z8c`CQpH@mmtT}V`<;yxP*H`ai(`mI@7t>zxttYw94`se8ZQY`H%*sOI35N%|7WUsxX&oHnm${?9RMrv8dm`xbQkXe{=-7M0Ha d?K*b_^YNM5``QoR*nDw diff --git a/forest-dark/combo-button-focus.png b/forest-dark/combo-button-focus.png deleted file mode 100644 index 50dba42c537906dbc4a4aa25bc515e8dd96153b7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 245 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc1|)ksWqE-VXMsm#F#`kNVGw3Kp1&dmC@5Lt z8c`CQpH@mmtT}V`<;yxP*Iboi(`mI@7t>zIhhT494^XR zx9&VAQJ%NSRqce+P0P8VoNG>bR=1e6hc4M+f52H`g5LJ$k9iB~Mf=R2A2}l+7$VwG z5OGt~DdDzRRQ8o9#vSV#etT*57+zERqxbv`_p&K92PGEkb96`B9TfiKUU2Ru_pF)v n8cKBu?`1<{tvBqCJHRebrL*r=%G(a0YZyFT{an^LB{Ts5jqzAZ diff --git a/forest-dark/combo-button-hover.png b/forest-dark/combo-button-hover.png deleted file mode 100644 index 555d685a130208c6b46cd6c2cb6c10e35df276c6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 239 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc1|)ksWqE-VXMsm#F#`kNVGw3Kp1&dmC@5Lt z8c`CQpH@mmtT}V`<;yxP*IJii(`mI@7t>j`J5Gb94=bV zP>OJxP$M+HK~@*SP!_79eLPW~3?Rav-ktJtcpjtf!D2V#yg^cYsWKWRBR zi0Q#j<~p(DCExd%Kg?Hb*}S0moleGDTkb>8rsaQN`!5)iJ(o4~tB!`!^8-=ZYq#!Y hwtk+{^YJUwy^l+@cPrjK@(Jh;22WQ%mvv4FO#rjkSlR#p diff --git a/forest-dark/down.png b/forest-dark/down.png deleted file mode 100644 index 8dbdd89798861c1e21d904a5a9c7f94b8c484a51..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 251 zcmeAS@N?(olHy`uVBq!ia0vp^AT}!p8<4C?sm%aVoCO|{#S9F5he4R}c>anMprB-l zYeY$Kep*R+Vo@qXd3m{BW?pu2a$-TMUVc&f>~}U&Kt&y%E{-7_GfVpnxtI(^SmS4@ zMsPZ@cl;&kI` uMvJZ2`V?E9pYNYps^UIr@yycs?|8Fgd`g~L&h!GhiNVv=&t;ucLK6V{Bu@q4;BhGFVdQ&MBb@04Hc4rvLx| diff --git a/forest-dark/hor-accent.png b/forest-dark/hor-accent.png deleted file mode 100644 index b471f4bc5680c6c6305a445af166b59fc0711c5b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 162 zcmeAS@N?(olHy`uVBq!ia0vp^B0$W=!3HF^gw{O+Qk(@Ik;M!Qe1}1p@p%4<6riAF ziEBhjaDG}zd16s2LwR|*US?i)adKios$PCk`s{Z$Qb0xeo-U3d8WWREOiccqcW^!6 z;(J&E2qNq(I)kn{tlC|0#o-iV_kn%aLrf>U(qm!BbY)qk8Db?5)XL!L>gTe~DWM4f Dd}c6! diff --git a/forest-dark/hor-basic.png b/forest-dark/hor-basic.png deleted file mode 100644 index 9a73a594589236318ea1b7816eda651ef8a2eda0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 162 zcmeAS@N?(olHy`uVBq!ia0vp^B0$W=!3HF^gw{O+Qk(@Ik;M!Qe1}1p@p%4<6riAF ziEBhjaDG}zd16s2LwR|*US?i)adKios$PCk`s{Z$Qb0xeo-U3d8WWREOiccqcW^!6 z;(J&E2qJ*|6bV=b~2o;Ex^)+-%iI^mTb3&R6lmQ@qYS`C3(89ZJ6T-G@yGywou CiZBKM diff --git a/forest-dark/hor-hover.png b/forest-dark/hor-hover.png deleted file mode 100644 index 2f8b1965318df1b038038d3ff131f832848b7dfc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 162 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 TwY6YW00000NkvXXu0mjfg~L2Q diff --git a/forest-dark/radio-accent.png b/forest-dark/radio-accent.png deleted file mode 100644 index 099e14886ae0a4f8e2fe924350b72de149e7f5b5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 565 zcmV-50?Pe~P)pF8FWQhbW?9;ba!ELWdL_~cP?peYja~^aAhuUa%Y?FJQ@H10m4Z{ zK~y-6rIkr@n?MkSzgb{FSgKM=a&9^}$}K+V5dZ(Pe}b1(*;kaMEFl;Mh8$$$5=)#o z@7Ue%O*g$DHk(aD7gA6zLfq0&pDCJ10U(MRwXX?MOSmn;_o=#Ta7#n+`HVV}z2GL% z_(bDA8j8FozRL(wdzwod4CL#C>~c0J)`gE1+2xFUoq!O1!D5|YSCcn|y0EJW#i!&{ z&@Iio#M)$5F>Yy|1*JHq{h!F!2}pr17a=}YgF(`I;+sACM+rdnAtb+;;FDf2@v*`! zLUi?<>X6v<`X|Eq))u4|{mZetn&VBsapfXJR@44#0P)TKMC=8^dE(psAg7*c@YPinj-D2x8nUrR|pm#|DhSjrgSXNVTg5o%zLtCg>O7gYhv_y9(Le#g>?8 zc2{%UxuH>lMhVJu!|v<6Jr-RAsYQpWrO4~SS@cGdeVGlCd)XD9U|S+-Aim4~iu5N$ zw|nr-vCBbfF^`A$i#;6LGuD3f`2K)-_;=ChF(>-=%6natrXjx1&`%ZlIvL&X7s!zV z+uOS!wFFy>TN=tmh@S=2&)cM|M?N{iZFv;x{{i?7vC_5lvz1BI00000NkvXXu0mjf DJCF*V diff --git a/forest-dark/radio-basic.png b/forest-dark/radio-basic.png deleted file mode 100644 index 6b745d15d97a65ff549e8bb8a6348f91a7c99be6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 543 zcmV+)0^t3LP)pF8FWQhbW?9;ba!ELWdL_~cP?peYja~^aAhuUa%Y?FJQ@H10jx%VIHovNzBG)+HWS!<29ZNG*!O|x8M zi*4$pF8FWQhbW?9;ba!ELWdL_~cP?peYja~^aAhuUa%Y?FJQ@H10nkZA zK~y-6os>&&+CUUW&y2^|2J=uVAxM|)hOmm%?Aw1{sXrmF4SQQrrD78BV2@|Ia3GCs zRE2Nn`#5v&o$C?T*Vm4wq@bLIn0ZJw_HmH{K;#B5D^ZEYPc_(fs%jd{JYv5bWBXn! z*u_33_A#IP6uXMxevL}>`&y2{zQJJ ztm`YxEJRkXrFOvvKc7%Zj;I`>a;PL{@as2HSDiWKEJRk5_GtjYZ3cyH*+ppy?y}CD zYT_epAUmChHb3HXp+`6EObG;1>ULXDU%)Au6F#9Uq|Ay=r(O(ke!BtZ%ad#7txII{ zUpywpKD!@N%w>dA0;dG!WyJRT6c=@`nM!k%pX3xEfLz1J?95at7XCZdd!-jPuQd{J@z)v+Q$?I3Sp}%glxu}-3 Rb$b8+002ovPDHLkV1kG70jU50 diff --git a/forest-dark/radio-tri-accent.png b/forest-dark/radio-tri-accent.png deleted file mode 100644 index 756ff13571c7930db000ca236eacd8a106e0f2c0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 465 zcmV;?0WSWDP)pF8FWQhbW?9;ba!ELWdL_~cP?peYja~^aAhuUa%Y?FJQ@H10bWT& zK~y-6t(7}(gD@0E57&SpB$+_nvXqV;^8bHIA5y!LDJcQ-Fdc}f3RF=-&uHwUE8qJW zXTRTfj1h+__Nfz1yH<380>CL-)K`m_8T>;9c6!tuFVu;qd|J~6GTGckE;e$hA2emr z629hmnfYDIuo)=Qi2QjwD;r}&MgF{{NF#8Zsjy5V%x-m6G{)>!l#kJGVV&r^WJ58# zkUG&9p*Vfn3n|hF#6haq$A)UL(1waS_L1gN>VHthKGJL^lXExi#>KUIeni6894Xt` zZ#Gb~_ih8l^?|zq2XMt$>}uMSdpa-1BI?xqLv20k)F9(5F2TzT;%5da<}d#iRB7=J z6++i{bSc5h7uc@vvX>e3=lOow$9Z_hhOr6X&ghTZrNL)G@cwh|CnjA-_?F`y8;Uf# zns52zft}>e%M9MZP$!xy_OV-!_L3%LEOM;z4^=Ok{sH&~F5il0F#zFi00000NkvXX Hu0mjf?>o+^ diff --git a/forest-dark/radio-tri-basic.png b/forest-dark/radio-tri-basic.png deleted file mode 100644 index 0f20b2149e939a08fd516eb2914837b639f4f250..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 450 zcmV;z0X_bSP)pF8FWQhbW?9;ba!ELWdL_~cP?peYja~^aAhuUa%Y?FJQ@H10Z&Op zK~y-6t(DD=gCG<}KMaVC-Y!gF<5Tqg-w3K3NJ@o-Su`4}PC8>fJ48?T`R-CBlBaAU{&b_@; zEC*U^bX~VATO))(*LBcZgE6)hYOT?>?MKm?wr!!5dKVJWA=y$&L@yy@3|i~MvI(UW z7-J}!ruoyEeWEN&5D|)Lsb^5vHHxaLP7_vD1DAu+34bC|f-g}JW__Zv?2=6_LoHMMoUki;f$T`1m z**S+{7!J$&zMtFJ92;vb`o90SY#0XI@Ar8;r+V6Y2mwl|-6$;?#}U@rO@rN($<*G7 s2z6cKa=Botrj!yMj|aT>zy1P#008)jpF8FWQhbW?9;ba!ELWdL_~cP?peYja~^aAhuUa%Y?FJQ@H10d`45 zK~y-6t&}@+qc9Xk4?>v7vc|L9z-+l`5-Jz@|9@hgH145`aUK#N#1h?GF%@Q1EDs1wE=5KsbG^V-wNqxOM7Md(P zOZF~}Fetw7!@v z`kM-&y4jkICXK`?lLR)M+_0A5!#H(1U+hT`ctfRbcz!E6YN&*gl>uTWp_@r5chRcDR4UJ43qIKH=Z* z56|xnw(kk<_Q;2p`eup!G3mWWh-ZVp*&mWkg>M6eLY4}Xg|umaT`jP|a1fy_V%Ol6 f8d<2JE06dOq&td~0Mt>m00000NkvXXu0mjfhz-?H diff --git a/forest-dark/radio-unsel-accent.png b/forest-dark/radio-unsel-accent.png deleted file mode 100644 index b8a2f9572fd9c083a4ff131bd08a29972acac959..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 605 zcmV-j0;2tiP)pF8FWQhbW?9;ba!ELWdL_~cP?peYja~^aAhuUa%Y?FJQ@H10qRLa zK~y-6rIkTX+b|S>zu2*pGz3~H9nucF&`IpT0d4>PA8+9G#z^2VNs0x>94O3H^a2q!NIr?U9c?sxs*N4*X_l{CdDn6WWgl zM2$n=8M0rFsO`FQ>Im)0PSO<($a+t+F+d z_2idxy6JG?sk@~iEE2!J+{;cH%-uY zW_#2Y)NM;wYJ@9d;s>|4Fs?0q5hHve5Ks4PAEfd4RK8rBeF;DW2=Qdz7oP}(@d&wa zxTB31xHE37kPC-Um7#l=>@Bp%DXKCET^W2PaTB%I?m{L}_Z@-&+0~Nfv>>QxXIW5! z=2JoX;}Luqs;nx5stn7sBZB(>!UrW-o*km^%yS!ij?S(dpF8FWQhbW?9;ba!ELWdL_~cP?peYja~^aAhuUa%Y?FJQ@H10lY~> zK~y-6t(DuZqA(Od7f>R%5@Q14frM}H|G&mWqk=|5u^`qk4@QpBIWyyU*5gjQ+THH% zTJ!mQ;0epJh@uE7C6mbn*L4B7TrQl?XOvPjO~Wv}Ohr5gq?Dv-O5gV=rP%NHTrQW} zU?B*Cfa!Efnx>RxiBjr&VVb5yQN((^Mr-}Ddf)f-eb4cDBnSesEW`JGs;U}yYd^|7 z&ttJz{8sji)|$m)f$#eyNixRmdzw)cv0AOTUaxvPjdE?RNW? z>`}VeY)Fy>%d)VeD5CFsPN&n`vO8L9j4?zK zVGAL~I{O4;46f^bmCa9K-_4&Qguobs-E|$l?|&=|f`G2;u$59{QGP-k$0((+o2J2a zU4kI^Xm_sb(libBFbq^xMV4haj`Oz6aU8NNqby5?VYuD$y}#vo{+85n9P&IzDK$Qh zv23cULTgP?6#tZd7={!@LEE|^>;O2RN?Hk%>DZJ05Jwrx2a4&QzOzW^xQpF8FWQhbW?9;ba!ELWdL_~cP?peYja~^aAhuUa%Y?FJQ@H10rW{k zK~y-6rIf!<+dve@Klj7FbE*=z29cDZL#H-FVPWH+Z#R}!RRMKt2XG|nkN}A<_MP1! z1ge0P(tO6JyU)F+clwHhgM*e;q@eCueBTnf5pALY5T!>=YIN?1h7Ov?SM_Sa_Y>xW zE@D~}T{!HITSVyrXc9%p4E5U=R3EySlQBAXPYYY18g%iy7W-v{Dyqkg-+JV&C;Bxd zWCpjN5?;lavm0m^-OAs=#V?WeTy zf9xohxcwCKeF9Cp*s{(Y(M3sln89u~GmcU*u@~yHOO&A^mB^N%S9@$@CO{d*f-gZ;U@q9002ovPDHLkV1g3O B9<=}f diff --git a/forest-dark/radio-unsel-pressed.png b/forest-dark/radio-unsel-pressed.png deleted file mode 100644 index 493f02d43de3641d9b7072ef0ccf7aea53fa1fd2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 468 zcmV;_0W1EAP)pF8FWQhbW?9;ba!ELWdL_~cP?peYja~^aAhuUa%Y?FJQ@H10bxl* zK~y-6t(47jn=lkc4;RUR>l#n9fyuJnG_1VH`@ck!Y`lkEZRZC8Lh8baGltGMV{>-W zTpfLqMmJcm*PbB~&}A_;kLgp5PZR(_Z3%ZBsx;`zfEzDW!wH)wG)0P=hofS@(Ab5> z-p*;?dZLFdsx+rrj>SMzq}2C6{wo{e<{|a{4b9yWf?zCciDh;#E^To2+<^@&Nhor|Ho}SScNN1ZDqNn|F{Mlf=Cw|#|Y zEz!gNm~1Tkm?&#hRb%snE{k!i87@8?M2thWExIzOvOaX>0sjD4+=5YBJH`S40000< KMNUMnLSTa6fX<-+ diff --git a/forest-dark/rect-accent-hover.png b/forest-dark/rect-accent-hover.png deleted file mode 100644 index d7a88253b6fad6b178350a54fb04328b857a9345..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 290 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc1|)ksWqE-VXMsm#F#`kNVGw3Kp1&dmC@5Lt z8c`CQpH@mmtT}V`<;yxP|fb1f8?+_!m~}?=ob_GFR9*i~ zRlViwmU8JGjnY4)Gu~uLNU~3hTF>TX+EAX#Ej4}pl8a8U?-E|QwYWAvFl*@f+}`6{ h@K$i1c7Xju*4=Nc#9F^kWd}N)!PC{xWt~$(69CZ+YjFSo diff --git a/forest-dark/rect-accent.png b/forest-dark/rect-accent.png deleted file mode 100644 index bebf94847ad275619252bc6e5d3136137aca9193..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 290 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc1|)ksWqE-VXMsm#F#`kNVGw3Kp1&dmC@5Lt z8c`CQpH@mmtT}V`<;yxP|=Pi!%wSNzq@|3|28qa<+vSd^y2j7;7z>;51!xm zMEOwBt=*zA2SxwzPAN3(InrF3EYAG3ltKNgtV*5Pv1`hfr3vp?Dz7T-<53XjoBX-s hy#JHqY*(!JFr4DDaxH1vlnit_gQu&X%Q~loCIF$HYeWD5 diff --git a/forest-dark/rect-basic.png b/forest-dark/rect-basic.png deleted file mode 100644 index c6474d5352235267ad08d6ebaf2b68237ac83960..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 272 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc1|)ksWqE-VXMsm#F#`kNVGw3Kp1&dmC@5Lt z8c`CQpH@mmtT}V`<;yxP|;jZ7sn8b-nZur^BN2U7!IDb zEGq2ayS}IKe?8kymv;=_V!OPxW`!sgp0rEYB7b^Q%~1(Y&4sI41S%%Y49s3S#VNb@ zyoB;+8~e6*P8>atJ6269b_?>VKUR0>vWeoVU-@tHB-oZ5zdN@>;qDgc+JoXX2k$qe zUE?h-Uvljhug!U3&7~I}UpjMR8*k>?iN((>`0o5QeDdzi?{mA>#ausXzE8&fL-=9i R4M2x7c)I$ztaD0e0syrwZF>L! diff --git a/forest-dark/rect-hover.png b/forest-dark/rect-hover.png deleted file mode 100644 index b6694076a3c4e9f7592ad9cf808c5f29b1eeb923..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 273 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc1|)ksWqE-VXMsm#F#`kNVGw3Kp1&dmC@5Lt z8c`CQpH@mmtT}V`<;yxP|-Y37sn8b-nZur`I-#`7!IDb zoHX0{$`8gbfBvhUu+4Mav0=hFPuIy>Dtb5Bd%hX^UOrvz&%qda)bjY$Gd**RHC4Sd zZT5ced0ZiBRebK^ciUB7J%XHye|l;U9X3!{^~=2MTB+=%NAG-GCZyf!y!U|j-UIUm zGffYl;W=q(&iuXBJ;-az4in>Psg7QnMy3Jyy5icYUF*(#K2s>Ohw)*M{oWUA SH*W#Dl)=;0&t;ucLK6V8m~E*5 diff --git a/forest-dark/right.png b/forest-dark/right.png deleted file mode 100644 index 336945ce751f6d5eee57fc8e19fe27d47a343fc9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 217 zcmeAS@N?(olHy`uVBq!ia0vp^tU%1g!3HF2ETPZ!4!j+wQ6ihKw&~H->KXTvpe2w{o`1~S}-#*SjO`CEhWvRSDrtf|3lnF)T4C%<+XW08yP%Z L{an^LB{Ts5oqbAu diff --git a/forest-dark/scale-hor.png b/forest-dark/scale-hor.png deleted file mode 100644 index 675e89f0fce2a044fca88b365236919d205be019..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 348 zcmeAS@N?(olHy`uVBq!ia0vp^0U*o)Bp6m#tt|vnEa{HEjtq=#3k+XOiwE+VlDyqr z82-2SpV<%OaTa()76WMyFm^kcZ3krZdAc};L>zv5?I7=A1re5uYt>!08O{mne0aHD zcH_oN2V>T}Ss=Q-+(qN?$pwzhY`jt|2W4Zf)lFSE_pZVX7Vn44a-+kGy!OYvEw6=KVK}nO03lD-FKjiL}kC5T9knOeobs)3x@~#cf z|I}`@eSA6Z0@&z(>_1o^Z2RG)Z};pj&}FJ6t`Q|Ei6yC4$wjF^iowXh$Vk_~MAy(V x#K6?bz}(8jRNKJ9%D|x3=h#&g4Y~O#nQ4`{HSh@5Wdb!Yc)I$ztaD0e0sxb}dVl}` diff --git a/forest-dark/scale-vert.png b/forest-dark/scale-vert.png deleted file mode 100644 index f268b60e887d8a44bffa2e453ef1f37e2acc86ca..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 161 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc1|)ksWqE-VXMsm#F#`kNVGw3Kp1&dmC@5Lt z8c`CQpH@mmtT}V`<;yxP?4Uei(`mIZ?cJr$)EEL%o!OO z38MQ8AG7J{^7vS!o?uzb)+c3=^lHuk|nMY zCBgY=CFO}lsSM@i<$9TU*~Q6;1*v-ZMd`EO*+>Bu@p`&AhH%VG?&#_H!_UB?%ET=$ Sz10jT!QkoY=d#Wzp$P!D6dvUO diff --git a/forest-dark/sizegrip.png b/forest-dark/sizegrip.png deleted file mode 100644 index 5bfc9672c215849fd76e17d727f24074aab8d4a4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 459 zcmV;+0W|)JP)pF8FWQhbW?9;ba!ELWdL_~cP?peYja~^aAhuUa%Y?FJQ@H10a!^y zK~yM_jgsxks!$MxA3t`6sqG+pGgIVH`*5x<*7W#!yuie*s!nsD#Wu72E&-002ovPDHLkV1fy! B%NYOw diff --git a/forest-dark/spin-button-down-basic.png b/forest-dark/spin-button-down-basic.png deleted file mode 100644 index f4e0890205476eeb3fef8d69f1cb8dd30f7d98ee..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 153 zcmeAS@N?(olHy`uVBq!ia0vp^B0wy}!3HFwFZ>e#Qk(@Ik;OpyY!GHl4_J8!C@5Lt z8c`CQpH@mmtT}V`<;yxP?4Iai(`mIZ*osh&!6)Sss|1p uJb1uG_i#eOl>>py?z}Rl1y>w67#Lc0nA|LQwN3zaFnGH9xvXe#Qk(@Ik;OpyY!GHl4_J8!C@5Lt z8c`CQpH@mmtT}V`<;yxP?4^ui(`mIZ*quE+3)jTXU}we zzmmmtT}V`<;yxP*Jw0i(`mI@7psAd0QO>91d>Z zJuiT35>v*bR|o!Zt$1?o0%McYhX|nux4j(#8D}TtaY=A0#_ajbo@V^}sDyYShy3oD zTVG9KykhzMps|aK)pdatdz`%@4hhu>%H4bTL^5yHt?CwagH>6RgQD`;D@4C#{a^hA PXf1=MtDnm{r-UW|aZyiE diff --git a/forest-dark/tab-accent.png b/forest-dark/tab-accent.png deleted file mode 100644 index ffdf1a02d0167d1caafa4ab7d61daa137f5d0e24..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 188 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 D%s`Gt diff --git a/forest-dark/thumb-hor-basic.png b/forest-dark/thumb-hor-basic.png deleted file mode 100644 index ea644b2bb893ff0d1e55029ea3c91ac52bd0a01b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5807 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 Yq6b!7F|;v2w~OQX&i2_eJ$mWC0dgUkLjV8( diff --git a/forest-dark/thumb-hor-hover.png b/forest-dark/thumb-hor-hover.png deleted file mode 100644 index ab03faf745facb9af22a13db96771b860bbf28ac..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6417 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 HhbR3DQ2E0! diff --git a/forest-dark/thumb-vert-accent.png b/forest-dark/thumb-vert-accent.png deleted file mode 100644 index 3db6b23b6d1c90d040a0e502bfcf334100f4704f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 269 zcmeAS@N?(olHy`uVBq!ia0vp^B0$W+!3HFM&b|K@NO2Z;L>4nJ@ErzW#^d=bQh9gP2NC6ei@^oeDF{#4@t>)Umxnn+NycU3wrj$j=D{C%KgiC@nx*!`CUF&aPO1ZU;7;*n z+f(k!O3L1P&bi&$^@0{()TO4oxdowKH~CJ4i0@C9XI)$Iruax$n(4MD+cYO0-Y4SV zlmGwO%-20YZBGwn`Q#{A_jdI3ZIboSH$HXz@G167pC^C1u~_ZAwDKx5-CaHJHvnD9 N;OXk;vd$@?2>@j#W)T1Y diff --git a/forest-dark/thumb-vert-basic.png b/forest-dark/thumb-vert-basic.png deleted file mode 100644 index b1a558732fce4b106c4eb854afc9dc2cd46c07c4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 253 zcmeAS@N?(olHy`uVBq!ia0vp^B0$W+!3HFM&b|K@NO2Z;L>4nJ@ErzW#^d=bQh9gP2NC6dfdAc};XiQvt#+vW2f`G$8 z@eYk^i;TCPfABm0*dmt_GmjHdlhpDq{WNBuaoy&;{Bs#a78jOm-Sib&Tf>UZZMjxq z{d~rZL_NP%NO6Xe!Xjx4)^bUpIL8+Ti#fBitjhjRScf4elF{r5}E)F@?`}8 diff --git a/forest-dark/thumb-vert-hover.png b/forest-dark/thumb-vert-hover.png deleted file mode 100644 index 6137ff1a91fa2962584a02277093496cecb4c6b6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 269 zcmeAS@N?(olHy`uVBq!ia0vp^B0$W+!3HFM&b|K@NO2Z;L>4nJ@ErzW#^d=bQh9gP2NC6ei@^oh? zdbP_U%!#br{R+F~4(y&?z;>9ohtZ5pWY)B6qMe29Klna8ooJ}m=992mX5WPd1GA^h zOE!DVVl=VxIKi^MxO0V>Q}&?`$C>A~x_1S>E;N41dcT;#KC`woQ@xeSKfj z%QyE8GSdq#@hFIwB|l;M@|AJfMa8w%D}M8QDztv0d?nYZD%0_DlX<3|$-i^|c%}kf O$>8bg=d#Wzp$P!{FJ{O9 diff --git a/forest-dark/tree-basic.png b/forest-dark/tree-basic.png deleted file mode 100644 index 06e9b18273066232593958ecff02e3234cb0bb86..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 149 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc1|)ksWqE-VXMsm#F#`kNVGw3Kp1&dmC@5Lt z8c`CQpH@mmtT}V`<;yxP?3_Si(`mIZ*osh&!6)St_NIv p54-Wom=;_)5XkJ#E91q+#E^EE$!O0_F$mmtT}V`<;yxP?3eFi(`mIZ*quE+3)iXt_Kbs zIB+20qt6Vjwz(6kP9Ja)ojl>dipIrkeNq-#4^}h^02MH3X0Bvqu=>h&g++hWVxSoe Mp00i_>zopr0Iu~n^8f$< diff --git a/forest-dark/up.png b/forest-dark/up.png deleted file mode 100644 index b02eda4f99e8f9d49ba7b61d01da1f02a2b50aa3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 250 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 diff --git a/graxpert/application/app.py b/graxpert/application/app.py index ef04398..8df21c4 100644 --- a/graxpert/application/app.py +++ b/graxpert/application/app.py @@ -5,7 +5,6 @@ import numpy as np from appdirs import user_config_dir -from icecream import ic from graxpert.ai_model_handling import ai_model_path_from_version, download_version, validate_local_version from graxpert.app_state import INITIAL_STATE diff --git a/graxpert/application/eventbus.py b/graxpert/application/eventbus.py index 9f4c164..6a55c6d 100644 --- a/graxpert/application/eventbus.py +++ b/graxpert/application/eventbus.py @@ -1,9 +1,3 @@ -# import asyncio - -from icecream import ic -from graxpert.ui.ui_events import UiEvents - - class EventBus: def __init__(self): self.listeners = {} @@ -21,12 +15,8 @@ def remove_listener(self, event_name, listener): del self.listeners[event_name] def emit(self, event_name, event=None): - # if event_name != UiEvents.MOUSE_MOVED: - # ic(f"sending {event_name}:{event} to:") listeners = self.listeners.get(event_name, []) for listener in listeners: - # if event_name != UiEvents.MOUSE_MOVED: - # ic(f" {listener}") listener(event) diff --git a/graxpert/main.py b/graxpert/main.py index 058e4ea..13088a9 100644 --- a/graxpert/main.py +++ b/graxpert/main.py @@ -97,12 +97,12 @@ def on_closing(root, logging_thread): logging_thread = initialize_logging() check_for_new_version() + style() root = CTk() try: root.state("zoomed") except: root.state("normal") - style(root) root.title("GraXpert | Release: '{}' ({})".format(release, version)) root.iconbitmap() root.iconphoto(True, tk.PhotoImage(file=resource_path("img/Icon.png"))) diff --git a/graxpert/resource_utils.py b/graxpert/resource_utils.py index 3a30765..05156f7 100644 --- a/graxpert/resource_utils.py +++ b/graxpert/resource_utils.py @@ -22,17 +22,3 @@ def resource_path(relative_path): def temp_resource_path(relative_path): return os.path.join(temp_resource_dir.name, relative_path) - - -def scale_img(relative_path, scaling, shape): - os.makedirs(os.path.dirname(temp_resource_path(relative_path)), exist_ok=True) - - img = io.imread(resource_path(relative_path)) - img = resize(img, (int(shape[0] * scaling), int(shape[1] * scaling))) - img = img * 255 - img = img.astype(dtype=np.uint8) - io.imsave( - temp_resource_path(relative_path.replace(".png", "-scaled.png")), - img, - check_contrast=False, - ) diff --git a/graxpert/ui/application_frame.py b/graxpert/ui/application_frame.py index 0696dc7..1e3bdea 100644 --- a/graxpert/ui/application_frame.py +++ b/graxpert/ui/application_frame.py @@ -1,7 +1,6 @@ import tkinter as tk -from customtkinter import CTkButton, CTkFrame, CTkLabel -from icecream import ic +from customtkinter import CTkFrame from graxpert.application.app import graxpert from graxpert.application.app_events import AppEvents diff --git a/graxpert/ui/canvas.py b/graxpert/ui/canvas.py index af7f099..eb6a31d 100644 --- a/graxpert/ui/canvas.py +++ b/graxpert/ui/canvas.py @@ -4,7 +4,6 @@ import numpy as np from customtkinter import CTkButton, CTkCanvas, CTkFrame, CTkOptionMenu, StringVar, ThemeManager -from icecream import ic from PIL import Image, ImageTk from graxpert.application.app import graxpert diff --git a/graxpert/ui/left_menu.py b/graxpert/ui/left_menu.py index a329e8b..be12168 100644 --- a/graxpert/ui/left_menu.py +++ b/graxpert/ui/left_menu.py @@ -1,8 +1,6 @@ -import os import tkinter as tk from customtkinter import CTkScrollableFrame, StringVar, ThemeManager -from icecream import ic import graxpert.ui.tooltip as tooltip from graxpert.application.app import graxpert diff --git a/graxpert/ui/right_menu.py b/graxpert/ui/right_menu.py index f1cbed7..d2b31f4 100644 --- a/graxpert/ui/right_menu.py +++ b/graxpert/ui/right_menu.py @@ -3,7 +3,6 @@ import customtkinter as ctk from customtkinter import CTkFont, CTkImage, CTkLabel, CTkScrollableFrame, CTkTextbox -from icecream import ic from packaging import version from PIL import Image diff --git a/graxpert/ui/statusbar.py b/graxpert/ui/statusbar.py index c7d7cb2..32b4c84 100644 --- a/graxpert/ui/statusbar.py +++ b/graxpert/ui/statusbar.py @@ -1,7 +1,6 @@ import tkinter as tk from customtkinter import CTkFrame, CTkLabel -from icecream import ic from graxpert.application.app import graxpert from graxpert.application.app_events import AppEvents diff --git a/graxpert/ui/styling.py b/graxpert/ui/styling.py index 2e91f4b..b45128e 100644 --- a/graxpert/ui/styling.py +++ b/graxpert/ui/styling.py @@ -1,14 +1,17 @@ import os -from tkinter import ttk +import shutil import customtkinter -from graxpert.resource_utils import resource_path +from graxpert.resource_utils import resource_path, temp_resource_path from graxpert.ui_scaling import get_scaling_factor -def style(root): - customtkinter.set_default_color_theme(resource_path("graxpert-dark-blue.json")) +def style(): + theme_file = "graxpert-dark-blue.json" + os.makedirs(os.path.dirname(temp_resource_path(theme_file)), exist_ok=True) + shutil.copy(resource_path(theme_file), temp_resource_path(theme_file)) + customtkinter.set_default_color_theme(temp_resource_path(theme_file)) customtkinter.set_appearance_mode("dark") scaling = get_scaling_factor() customtkinter.set_widget_scaling(scaling) diff --git a/graxpert/ui/widgets.py b/graxpert/ui/widgets.py index 6676576..6b5a751 100644 --- a/graxpert/ui/widgets.py +++ b/graxpert/ui/widgets.py @@ -1,7 +1,6 @@ import tkinter as tk from customtkinter import CTkButton, CTkCheckBox, CTkEntry, CTkFrame, CTkImage, CTkLabel, CTkOptionMenu, CTkSlider, DoubleVar, StringVar, ThemeManager -from icecream import ic from PIL import Image from graxpert.localization import _ diff --git a/setup.py b/setup.py index e558237..cfc53d4 100644 --- a/setup.py +++ b/setup.py @@ -59,8 +59,7 @@ ], "include_files": [ ["./img", "./lib/img"], - ["./forest-dark.tcl", "./lib/forest-dark.tcl"], - ["./forest-dark/", "./lib/forest-dark/"], + ["./graxpert-dark-blue.json", "./lib/graxpert-dark-blue.json"], ["./locales/", "./lib/locales/"], [ os.path.join(astropy_path, "units", "format", "generic_parsetab.py"), From fe0b430ab758bff54aa93bc7a1d2a934aec25774 Mon Sep 17 00:00:00 2001 From: David Schmelter Date: Wed, 27 Dec 2023 07:46:36 +0100 Subject: [PATCH 11/17] simplify preferences model --- graxpert/CommandLineTool.py | 4 +- graxpert/app_state.py | 14 +- graxpert/application/app.py | 156 +++++++++++----------- graxpert/application/app_events.py | 1 + graxpert/astroimage.py | 9 +- graxpert/commands.py | 28 ++-- graxpert/localization.py | 16 +-- graxpert/main.py | 35 ++++- graxpert/preferences.py | 201 +++++++++-------------------- graxpert/ui/canvas.py | 35 ++--- graxpert/ui/left_menu.py | 36 ++---- graxpert/ui/right_menu.py | 39 +++--- graxpert/ui/widgets.py | 2 +- graxpert/ui_scaling.py | 5 +- graxpert/version.py | 34 ----- 15 files changed, 254 insertions(+), 361 deletions(-) diff --git a/graxpert/CommandLineTool.py b/graxpert/CommandLineTool.py index b895f93..dfc6001 100644 --- a/graxpert/CommandLineTool.py +++ b/graxpert/CommandLineTool.py @@ -59,7 +59,7 @@ def get_ai_version(self): if self.args.ai_version: ai_version = self.args.ai_version else: - ai_version = prefs["ai_version"] + ai_version = prefs.ai_version if ai_version is None: ai_version = latest_version() @@ -82,7 +82,7 @@ def get_ai_version(self): logging.shutdown() sys.exit(1) - prefs["ai_version"] = ai_version + prefs.ai_version = ai_version save_preferences(prefs_filename, prefs) return ai_version diff --git a/graxpert/app_state.py b/graxpert/app_state.py index c4d9dee..ee433d0 100644 --- a/graxpert/app_state.py +++ b/graxpert/app_state.py @@ -1,8 +1,10 @@ -from typing import TypedDict, List, AnyStr +from dataclasses import dataclass, field +from typing import List -class AppState(TypedDict): - background_points: List -INITIAL_STATE: AppState = { - "background_points": [] -} +@dataclass +class AppState: + background_points: List = field(default_factory=list) + + +INITIAL_STATE = AppState() diff --git a/graxpert/application/app.py b/graxpert/application/app.py index 8df21c4..b5e6e33 100644 --- a/graxpert/application/app.py +++ b/graxpert/application/app.py @@ -36,15 +36,15 @@ def initialize(self): self.display_type = "Original" self.ai_version = None - if self.prefs["ai_version"] is not None: - self.ai_version = self.prefs["ai_version"] + if self.prefs.ai_version is not None: + self.ai_version = self.prefs.ai_version self.mat_affine = np.eye(3) # state handling tmp_state = prefs_2_app_state(self.prefs, INITIAL_STATE) - self.cmd: Command = Command(INIT_HANDLER, background_points=tmp_state["background_points"]) + self.cmd: Command = Command(INIT_HANDLER, background_points=tmp_state.background_points) self.cmd.execute() # image loading @@ -79,19 +79,20 @@ def initialize(self): eventbus.add_listener(AppEvents.CORRECTION_TYPE_CHANGED, self.on_correction_type_changed) eventbus.add_listener(AppEvents.LANGUAGE_CHANGED, self.on_language_selected) eventbus.add_listener(AppEvents.AI_VERSION_CHANGED, self.on_ai_version_changed) + eventbus.add_listener(AppEvents.SCALING_CHANGED, self.on_scaling_changed) # event handling def on_ai_version_changed(self, event): - self.prefs["ai_version"] = event["ai_version"] + self.prefs.ai_version = event["ai_version"] def on_bg_floot_selection_changed(self, event): - self.prefs["bg_flood_selection_option"] = event["bg_flood_selection_option"] + self.prefs.bg_flood_selection_option = event["bg_flood_selection_option"] def on_bg_pts_changed(self, event): - self.prefs["bg_pts_option"] = event["bg_pts_option"] + self.prefs.bg_pts_option = event["bg_pts_option"] def on_bg_tol_changed(self, event): - self.prefs["bg_tol_option"] = event["bg_tol_option"] + self.prefs.bg_tol_option = event["bg_tol_option"] def on_calculate_request(self, event=None): eventbus.emit(AppEvents.CALCULATE_BEGIN) @@ -101,25 +102,25 @@ def on_calculate_request(self, event=None): messagebox.showerror("Error", _("Please load your picture first.")) return - background_points = self.cmd.app_state["background_points"] + background_points = self.cmd.app_state.background_points # Error messages if not enough points - if len(background_points) == 0 and self.prefs["interpol_type_option"] != "AI": + if len(background_points) == 0 and self.prefs.interpol_type_option != "AI": eventbus.emit(AppEvents.CALCULATE_END) messagebox.showerror("Error", _("Please select background points with left click.")) return - if len(background_points) < 2 and self.prefs["interpol_type_option"] == "Kriging": + if len(background_points) < 2 and self.prefs.interpol_type_option == "Kriging": eventbus.emit(AppEvents.CALCULATE_END) messagebox.showerror("Error", _("Please select at least 2 background points with left click for the Kriging method.")) return - if len(background_points) < 16 and self.prefs["interpol_type_option"] == "Splines": + if len(background_points) < 16 and self.prefs.interpol_type_option == "Splines": eventbus.emit(AppEvents.CALCULATE_END) messagebox.showerror("Error", _("Please select at least 16 background points with left click for the Splines method.")) return - if self.prefs["interpol_type_option"] == "AI": + if self.prefs.interpol_type_option == "AI": if not self.validate_ai_installation(): return @@ -132,7 +133,7 @@ def callback(p): downscale_factor = 1 - if self.prefs["interpol_type_option"] == "Kriging" or self.prefs["interpol_type_option"] == "RBF": + if self.prefs.interpol_type_option == "Kriging" or self.prefs.interpol_type_option == "RBF": downscale_factor = 4 try: @@ -141,14 +142,14 @@ def callback(p): extract_background( imarray, np.array(background_points), - self.prefs["interpol_type_option"], - self.prefs["smoothing_option"], + self.prefs.interpol_type_option, + self.prefs.smoothing_option, downscale_factor, - self.prefs["sample_size"], - self.prefs["RBF_kernel"], - self.prefs["spline_order"], - self.prefs["corr_type"], - ai_model_path_from_version(self.prefs["ai_version"]), + self.prefs.sample_size, + self.prefs.RBF_kernel, + self.prefs.spline_order, + self.prefs.corr_type, + ai_model_path_from_version(self.prefs.ai_version), progress, ) ) @@ -158,17 +159,17 @@ def callback(p): # Update fits header and metadata background_mean = np.mean(self.images["Background"].img_array) - self.images["Processed"].update_fits_header(self.images["Original"].fits_header, background_mean, self, self.cmd.app_state) - self.images["Background"].update_fits_header(self.images["Original"].fits_header, background_mean, self, self.cmd.app_state) + self.images["Processed"].update_fits_header(self.images["Original"].fits_header, background_mean, self.prefs, self.cmd.app_state) + self.images["Background"].update_fits_header(self.images["Original"].fits_header, background_mean, self.prefs, self.cmd.app_state) self.images["Processed"].copy_metadata(self.images["Original"]) self.images["Background"].copy_metadata(self.images["Original"]) all_images = [self.images["Original"].img_array, self.images["Processed"].img_array, self.images["Background"].img_array] - stretches = stretch_all(all_images, self.images["Original"].get_stretch(self.prefs["stretch_option"])) - self.images["Original"].update_display_from_array(stretches[0], self.prefs["saturation"]) - self.images["Processed"].update_display_from_array(stretches[1], self.prefs["saturation"]) - self.images["Background"].update_display_from_array(stretches[2], self.prefs["saturation"]) + stretches = stretch_all(all_images, self.images["Original"].get_stretch(self.prefs.stretch_option)) + self.images["Original"].update_display_from_array(stretches[0], self.prefs.saturation) + self.images["Processed"].update_display_from_array(stretches[1], self.prefs.saturation) + self.images["Background"].update_display_from_array(stretches[2], self.prefs.saturation) # self.display_type = "Processed" eventbus.emit(AppEvents.UPDATE_DISPLAY_TYPE_REEQUEST, {"display_type": "Processed"}) @@ -182,18 +183,18 @@ def callback(p): eventbus.emit(AppEvents.CALCULATE_END) def on_change_saturation_request(self, event): - self.prefs["saturation"] = event["saturation"] + self.prefs.saturation = event["saturation"] eventbus.emit(AppEvents.CHANGE_SATURATION_BEGIN) for img in self.images.values(): if img is not None: - img.update_saturation(self.prefs["saturation"]) + img.update_saturation(self.prefs.saturation) eventbus.emit(AppEvents.CHANGE_SATURATION_END) def on_correction_type_changed(self, event): - self.prefs["corr_type"] = event["corr_type"] + self.prefs.corr_type = event["corr_type"] def on_create_grid_request(self, event=None): if self.images["Original"] is None: @@ -202,15 +203,13 @@ def on_create_grid_request(self, event=None): eventbus.emit(AppEvents.CREATE_GRID_BEGIN) - self.cmd = Command( - SEL_POINTS_HANDLER, self.cmd, data=self.images["Original"].img_array, num_pts=self.prefs["bg_pts_option"], tol=self.prefs["bg_tol_option"], sample_size=self.prefs["sample_size"] - ) + self.cmd = Command(SEL_POINTS_HANDLER, self.cmd, data=self.images["Original"].img_array, num_pts=self.prefs.bg_pts_option, tol=self.prefs.bg_tol_option, sample_size=self.prefs.sample_size) self.cmd.execute() eventbus.emit(AppEvents.CREATE_GRID_END) def on_display_pts_changed(self, event): - self.prefs["display_pts"] = event["display_pts"] + self.prefs.display_pts = event["display_pts"] eventbus.emit(AppEvents.REDRAW_POINTS_REQUEST) def on_display_type_changed(self, event): @@ -219,10 +218,10 @@ def on_display_type_changed(self, event): eventbus.emit(AppEvents.STRETCH_IMAGE_END) def on_interpol_type_changed(self, event): - self.prefs["interpol_type_option"] = event["interpol_type_option"] + self.prefs.interpol_type_option = event["interpol_type_option"] def on_language_selected(self, event): - self.prefs["lang"] = event["lang"] + self.prefs.lang = event["lang"] messagebox.showerror("", _("Please restart the program to change the language.")) def on_load_image(self, event): @@ -232,7 +231,7 @@ def on_load_image(self, event): try: image = AstroImage() - image.set_from_file(filename, self.prefs["stretch_option"], self.prefs["saturation"]) + image.set_from_file(filename, self.prefs.stretch_option, self.prefs.saturation) except Exception as e: eventbus.emit(AppEvents.LOAD_IMAGE_ERROR) @@ -247,28 +246,28 @@ def on_load_image(self, event): self.images["Original"] = image self.images["Processed"] = None self.images["Background"] = None - self.prefs["working_dir"] = os.path.dirname(filename) + self.prefs.working_dir = os.path.dirname(filename) os.chdir(os.path.dirname(filename)) width = self.images["Original"].img_display.width height = self.images["Original"].img_display.height - if self.prefs["width"] != width or self.prefs["height"] != height: + if self.prefs.width != width or self.prefs.height != height: self.reset_backgroundpts() - self.prefs["width"] = width - self.prefs["height"] = height + self.prefs.width = width + self.prefs.height = height tmp_state = fitsheader_2_app_state(self, self.cmd.app_state, self.images["Original"].fits_header) - self.cmd: Command = Command(INIT_HANDLER, background_points=tmp_state["background_points"]) + self.cmd: Command = Command(INIT_HANDLER, background_points=tmp_state.background_points) self.cmd.execute() eventbus.emit(AppEvents.LOAD_IMAGE_END, {"filename": filename}) def on_open_file_dialog_request(self, evet): - if self.prefs["working_dir"] != "" and os.path.exists(self.prefs["working_dir"]): - initialdir = self.prefs["working_dir"] + if self.prefs.working_dir != "" and os.path.exists(self.prefs.working_dir): + initialdir = self.prefs.working_dir else: initialdir = os.getcwd() @@ -291,38 +290,38 @@ def on_open_file_dialog_request(self, evet): eventbus.emit(AppEvents.LOAD_IMAGE_REQUEST, {"filename": filename}) def on_rbf_kernel_changed(self, event): - self.prefs["RBF_kernel"] = event["RBF_kernel"] + self.prefs.RBF_kernel = event["RBF_kernel"] def on_reset_points_request(self, event): eventbus.emit(AppEvents.RESET_POITS_BEGIN) - if len(self.cmd.app_state["background_points"]) > 0: + if len(self.cmd.app_state.background_points) > 0: self.cmd = Command(RESET_POINTS_HANDLER, self.cmd) self.cmd.execute() eventbus.emit(AppEvents.RESET_POITS_END) def on_sample_color_changed(self, event): - self.prefs["sample_color"] = event["sample_color"] + self.prefs.sample_color = event["sample_color"] eventbus.emit(AppEvents.REDRAW_POINTS_REQUEST) def on_sample_size_changed(self, event): - self.prefs["sample_size"] = event["sample_size"] + self.prefs.sample_size = event["sample_size"] eventbus.emit(AppEvents.REDRAW_POINTS_REQUEST) def on_save_as_changed(self, event): - self.prefs["saveas_option"] = event["saveas_option"] + self.prefs.saveas_option = event["saveas_option"] def on_smoothing_changed(self, event): - self.prefs["smoothing_option"] = event["smoothing_option"] + self.prefs.smoothing_option = event["smoothing_option"] def on_save_request(self, event): - if self.prefs["saveas_option"] == "16 bit Tiff" or self.prefs["saveas_option"] == "32 bit Tiff": - dir = tk.filedialog.asksaveasfilename(initialfile=self.filename + "_GraXpert.tiff", filetypes=[("Tiff", ".tiff")], defaultextension=".tiff", initialdir=self.prefs["working_dir"]) - elif self.prefs["saveas_option"] == "16 bit XISF" or self.prefs["saveas_option"] == "32 bit XISF": - dir = tk.filedialog.asksaveasfilename(initialfile=self.filename + "_GraXpert.xisf", filetypes=[("XISF", ".xisf")], defaultextension=".xisf", initialdir=self.prefs["working_dir"]) + if self.prefs.saveas_option == "16 bit Tiff" or self.prefs.saveas_option == "32 bit Tiff": + dir = tk.filedialog.asksaveasfilename(initialfile=self.filename + "_GraXpert.tiff", filetypes=[("Tiff", ".tiff")], defaultextension=".tiff", initialdir=self.prefs.working_dir) + elif self.prefs.saveas_option == "16 bit XISF" or self.prefs.saveas_option == "32 bit XISF": + dir = tk.filedialog.asksaveasfilename(initialfile=self.filename + "_GraXpert.xisf", filetypes=[("XISF", ".xisf")], defaultextension=".xisf", initialdir=self.prefs.working_dir) else: - dir = tk.filedialog.asksaveasfilename(initialfile=self.filename + "_GraXpert.fits", filetypes=[("Fits", ".fits")], defaultextension=".fits", initialdir=self.prefs["working_dir"]) + dir = tk.filedialog.asksaveasfilename(initialfile=self.filename + "_GraXpert.fits", filetypes=[("Fits", ".fits")], defaultextension=".fits", initialdir=self.prefs.working_dir) if dir == "": return @@ -330,7 +329,7 @@ def on_save_request(self, event): eventbus.emit(AppEvents.SAVE_BEGIN) try: - self.images["Processed"].save(dir, self.prefs["saveas_option"]) + self.images["Processed"].save(dir, self.prefs.saveas_option) except: eventbus.emit(AppEvents.SAVE_ERROR) messagebox.showerror("Error", _("Error occured when saving the image.")) @@ -338,10 +337,10 @@ def on_save_request(self, event): eventbus.emit(AppEvents.SAVE_END) def on_save_background_request(self, event): - if self.prefs["saveas_option"] == "16 bit Tiff" or self.prefs["saveas_option"] == "32 bit Tiff": - dir = tk.filedialog.asksaveasfilename(initialfile=self.filename + "_background.tiff", filetypes=[("Tiff", ".tiff")], defaultextension=".tiff", initialdir=self.prefs["working_dir"]) - elif self.prefs["saveas_option"] == "16 bit XISF" or self.prefs["saveas_option"] == "32 bit XISF": - dir = tk.filedialog.asksaveasfilename(initialfile=self.filename + "_background.xisf", filetypes=[("XISF", ".xisf")], defaultextension=".xisf", initialdir=self.prefs["working_dir"]) + if self.prefs.saveas_option == "16 bit Tiff" or self.prefs.saveas_option == "32 bit Tiff": + dir = tk.filedialog.asksaveasfilename(initialfile=self.filename + "_background.tiff", filetypes=[("Tiff", ".tiff")], defaultextension=".tiff", initialdir=self.prefs.working_dir) + elif self.prefs.saveas_option == "16 bit XISF" or self.prefs.saveas_option == "32 bit XISF": + dir = tk.filedialog.asksaveasfilename(initialfile=self.filename + "_background.xisf", filetypes=[("XISF", ".xisf")], defaultextension=".xisf", initialdir=self.prefs.working_dir) else: dir = tk.filedialog.asksaveasfilename(initialfile=self.filename + "_background.fits", filetypes=[("Fits", ".fits")], defaultextension=".fits", initialdir=os.getcwd()) @@ -351,7 +350,7 @@ def on_save_background_request(self, event): eventbus.emit(AppEvents.SAVE_BEGIN) try: - self.images["Background"].save(dir, self.prefs["saveas_option"]) + self.images["Background"].save(dir, self.prefs.saveas_option) except: eventbus.emit(AppEvents.SAVE_ERROR) messagebox.showerror("Error", _("Error occured when saving the image.")) @@ -359,12 +358,12 @@ def on_save_background_request(self, event): eventbus.emit(AppEvents.SAVE_END) def on_save_stretched_request(self, event): - if self.prefs["saveas_option"] == "16 bit Tiff" or self.prefs["saveas_option"] == "32 bit Tiff": - dir = tk.filedialog.asksaveasfilename(initialfile=self.filename + "_stretched_GraXpert.tiff", filetypes=[("Tiff", ".tiff")], defaultextension=".tiff", initialdir=self.prefs["working_dir"]) - elif self.prefs["saveas_option"] == "16 bit XISF" or self.prefs["saveas_option"] == "32 bit XISF": - dir = tk.filedialog.asksaveasfilename(initialfile=self.filename + "_stretched_GraXpert.xisf", filetypes=[("XISF", ".xisf")], defaultextension=".xisf", initialdir=self.prefs["working_dir"]) + if self.prefs.saveas_option == "16 bit Tiff" or self.prefs.saveas_option == "32 bit Tiff": + dir = tk.filedialog.asksaveasfilename(initialfile=self.filename + "_stretched_GraXpert.tiff", filetypes=[("Tiff", ".tiff")], defaultextension=".tiff", initialdir=self.prefs.working_dir) + elif self.prefs.saveas_option == "16 bit XISF" or self.prefs.saveas_option == "32 bit XISF": + dir = tk.filedialog.asksaveasfilename(initialfile=self.filename + "_stretched_GraXpert.xisf", filetypes=[("XISF", ".xisf")], defaultextension=".xisf", initialdir=self.prefs.working_dir) else: - dir = tk.filedialog.asksaveasfilename(initialfile=self.filename + "_stretched_GraXpert.fits", filetypes=[("Fits", ".fits")], defaultextension=".fits", initialdir=self.prefs["working_dir"]) + dir = tk.filedialog.asksaveasfilename(initialfile=self.filename + "_stretched_GraXpert.fits", filetypes=[("Fits", ".fits")], defaultextension=".fits", initialdir=self.prefs.working_dir) if dir == "": return @@ -373,20 +372,23 @@ def on_save_stretched_request(self, event): try: if self.images["Processed"] is None: - self.images["Original"].save_stretched(dir, self.prefs["saveas_option"]) + self.images["Original"].save_stretched(dir, self.prefs.saveas_option) else: - self.images["Processed"].save_stretched(dir, self.prefs["saveas_option"]) + self.images["Processed"].save_stretched(dir, self.prefs.saveas_option) except: eventbus.emit(AppEvents.SAVE_ERROR) messagebox.showerror("Error", _("Error occured when saving the image.")) eventbus.emit(AppEvents.SAVE_END) + def on_scaling_changed(self, event): + self.prefs.scaling = event["scaling"] + def on_spline_order_changed(self, event): - self.prefs["spline_order"] = event["spline_order"] + self.prefs.spline_order = event["spline_order"] def on_stretch_option_changed(self, event): - self.prefs["stretch_option"] = event["stretch_option"] + self.prefs.stretch_option = event["stretch_option"] eventbus.emit(AppEvents.STRETCH_IMAGE_BEGIN) @@ -397,11 +399,11 @@ def on_stretch_option_changed(self, event): if img is not None: all_images.append(img.img_array) if len(all_images) > 0: - stretch_params = self.images["Original"].get_stretch(self.prefs["stretch_option"]) + stretch_params = self.images["Original"].get_stretch(self.prefs.stretch_option) stretches = stretch_all(all_images, stretch_params) for idx, img in enumerate(self.images.values()): if img is not None: - img.update_display_from_array(stretches[idx], self.prefs["saturation"]) + img.update_display_from_array(stretches[idx], self.prefs.saturation) except Exception as e: eventbus.emit(AppEvents.STRETCH_IMAGE_ERROR) logging.exception(e) @@ -410,7 +412,7 @@ def on_stretch_option_changed(self, event): # application logic def remove_pt(self, event): - if len(self.cmd.app_state["background_points"]) == 0 or not self.prefs["display_pts"]: + if len(self.cmd.app_state.background_points) == 0 or not self.prefs.display_pts: return False point_im = self.to_image_point(event.x, event.y) @@ -420,7 +422,7 @@ def remove_pt(self, event): eventx_im = point_im[0] eventy_im = point_im[1] - background_points = self.cmd.app_state["background_points"] + background_points = self.cmd.app_state.background_points min_idx = -1 min_dist = -1 @@ -435,7 +437,7 @@ def remove_pt(self, event): min_dist = dist min_idx = i - if min_idx != -1 and min_dist <= self.prefs["sample_size"]: + if min_idx != -1 and min_dist <= self.prefs.sample_size: point = background_points[min_idx] self.cmd = Command(RM_POINT_HANDLER, self.cmd, idx=min_idx, point=point) self.cmd.execute() @@ -445,7 +447,7 @@ def remove_pt(self, event): # application logic def reset_backgroundpts(self): - if len(self.cmd.app_state["background_points"]) > 0: + if len(self.cmd.app_state.background_points) > 0: self.cmd = Command(RESET_POINTS_HANDLER, self.cmd) self.cmd.execute() @@ -510,11 +512,11 @@ def translate(self, offset_x, offset_y): self.mat_affine = np.dot(mat, self.mat_affine) def validate_ai_installation(self): - if self.ai_version is None or self.prefs["ai_version"] == "None": + if self.ai_version is None or self.prefs.ai_version == "None": messagebox.showerror("Error", _("No AI-Model selected. Please select one from the Advanced panel on the right.")) return False - if not validate_local_version(self.prefs["ai_version"]): + if not validate_local_version(self.prefs.ai_version): if not messagebox.askyesno(_("Install AI-Model?"), _("Selected AI-Model is not installed. Should I download it now?")): return False else: diff --git a/graxpert/application/app_events.py b/graxpert/application/app_events.py index cc21d48..fb0c77f 100644 --- a/graxpert/application/app_events.py +++ b/graxpert/application/app_events.py @@ -62,3 +62,4 @@ class AppEvents(Enum): SPLINE_ORDER_CHANGED = auto() CORRECTION_TYPE_CHANGED = auto() LANGUAGE_CHANGED = auto() + SCALING_CHANGED = auto() diff --git a/graxpert/astroimage.py b/graxpert/astroimage.py index f1cc84c..1e07509 100644 --- a/graxpert/astroimage.py +++ b/graxpert/astroimage.py @@ -6,10 +6,11 @@ from astropy.stats import sigma_clipped_stats from PIL import Image, ImageEnhance from skimage import exposure, img_as_float32, io -from skimage.util import img_as_ubyte, img_as_uint +from skimage.util import img_as_uint from xisf import XISF -from graxpert.preferences import app_state_2_fitsheader +from graxpert.app_state import AppState +from graxpert.preferences import Prefs, app_state_2_fitsheader from graxpert.stretch import stretch @@ -154,7 +155,7 @@ def crop(self, startx, endx, starty, endy): self.height = self.img_array.shape[0] return - def update_fits_header(self, original_header, background_mean, app, app_state): + def update_fits_header(self, original_header, background_mean, prefs: Prefs, app_state: AppState): if original_header is None: self.fits_header = fits.Header() else: @@ -164,7 +165,7 @@ def update_fits_header(self, original_header, background_mean, app, app_state): self.fits_header["CBG-1"] = background_mean self.fits_header["CBG-2"] = background_mean self.fits_header["CBG-3"] = background_mean - self.fits_header = app_state_2_fitsheader(app, app_state, self.fits_header) + self.fits_header = app_state_2_fitsheader(prefs, app_state, self.fits_header) if "ROWORDER" in self.fits_header: self.roworder = self.fits_header["ROWORDER"] diff --git a/graxpert/commands.py b/graxpert/commands.py index 3c41d21..54d5aa7 100644 --- a/graxpert/commands.py +++ b/graxpert/commands.py @@ -75,14 +75,14 @@ class InitHandler(ICommandHandler): def execute(self, app_state: AppState, cmd_args: Dict) -> AppState: state = INITIAL_STATE - state["background_points"] = cmd_args["background_points"] + state.background_points = cmd_args["background_points"] return state def undo( self, cur_state: AppState, prev_state: AppState, cmd_args: Dict ) -> AppState: state = INITIAL_STATE - state["background_points"] = cmd_args["background_points"] + state.background_points = cmd_args["background_points"] return state def redo( @@ -98,16 +98,16 @@ def undo( self, cur_state: AppState, prev_state: AppState, cmd_args: Dict ) -> AppState: app_state_copy = deepcopy(cur_state) - prev_background_points = deepcopy(prev_state["background_points"]) - app_state_copy["background_points"] = prev_background_points + prev_background_points = deepcopy(prev_state.background_points) + app_state_copy.background_points = prev_background_points return app_state_copy def redo( self, cur_state: AppState, next_state: AppState, cmd_args: Dict ) -> AppState: app_state_copy = deepcopy(cur_state) - next_background_points = deepcopy(next_state["background_points"]) - app_state_copy["background_points"] = next_background_points + next_background_points = deepcopy(next_state.background_points) + app_state_copy.background_points = next_background_points return app_state_copy @@ -115,7 +115,7 @@ class AddPointHandler(PointHandler): def execute(self, app_state: AppState, cmd_args: Dict) -> AppState: app_state_copy = deepcopy(app_state) point = cmd_args["point"] - app_state_copy["background_points"].append(point) + app_state_copy.background_points.append(point) return app_state_copy def progress(self) -> float: @@ -126,13 +126,13 @@ class AddPointsHandler(PointHandler): def execute(self, app_state: AppState, cmd_args: Dict) -> AppState: app_state_copy = deepcopy(app_state) point = cmd_args["point"] - background_points = app_state_copy["background_points"] + background_points = app_state_copy.background_points tol = cmd_args["tol"] bg_pts = cmd_args["bg_pts"] sample_size = cmd_args["sample_size"] image = cmd_args["image"] new_points = background_flood_selection(point, background_points, tol, bg_pts, sample_size, image) - app_state_copy["background_points"].extend(new_points) + app_state_copy.background_points.extend(new_points) return app_state_copy def progress(self) -> float: @@ -143,7 +143,7 @@ class RemovePointHandler(PointHandler): def execute(self, app_state: AppState, cmd_args: Dict) -> AppState: app_state_copy = deepcopy(app_state) idx = cmd_args["idx"] - app_state_copy["background_points"].pop(idx) + app_state_copy.background_points.pop(idx) return app_state_copy def progress(self) -> float: @@ -157,9 +157,9 @@ def execute(self, app_state: AppState, cmd_args: Dict) -> AppState: new_point = cmd_args["new_point"] if len(new_point) == 0: - app_state_copy["background_points"].pop(idx) + app_state_copy.background_points.pop(idx) else: - app_state_copy["background_points"][idx] = new_point + app_state_copy.background_points[idx] = new_point return app_state_copy @@ -175,7 +175,7 @@ def execute(self, app_state: AppState, cmd_args: Dict) -> AppState: tol = cmd_args["tol"] sample_size = cmd_args["sample_size"] automatic_points = background_grid_selection(data, num_pts, tol, sample_size) - app_state_copy["background_points"] = automatic_points + app_state_copy.background_points = automatic_points return app_state_copy def progress(self) -> float: @@ -184,7 +184,7 @@ def progress(self) -> float: class ResetPointsHandler(PointHandler): def execute(self, app_state: AppState, cmd_args: Dict) -> AppState: app_state_copy = deepcopy(app_state) - app_state_copy["background_points"].clear() + app_state_copy.background_points.clear() return app_state_copy def progress(self) -> float: diff --git a/graxpert/localization.py b/graxpert/localization.py index 5214cd9..56875a0 100644 --- a/graxpert/localization.py +++ b/graxpert/localization.py @@ -1,17 +1,17 @@ import gettext -import sys import locale import os + from appdirs import user_config_dir + from graxpert.preferences import load_preferences from graxpert.resource_utils import resource_path - prefs_file = os.path.join(user_config_dir(appname="GraXpert"), "preferences.json") prefs = load_preferences(prefs_file) lang = None -if prefs["lang"] is None: +if prefs.lang is None: lang, enc = locale.getdefaultlocale() if lang is None: lang = "en_EN" @@ -21,18 +21,16 @@ lang = "en_EN" else: - lang = prefs["lang"] + lang = prefs.lang if lang == "Deutsch": - lang = "de_DE" + lang = "de_DE" else: lang = "en_EN" - - -lang_gettext = gettext.translation('base', localedir=resource_path("locales"), languages=[lang], fallback=True) +lang_gettext = gettext.translation("base", localedir=resource_path("locales"), languages=[lang], fallback=True) lang_gettext.install() + def _(text): return lang_gettext.gettext(text) - diff --git a/graxpert/main.py b/graxpert/main.py index 13088a9..5b95bd0 100644 --- a/graxpert/main.py +++ b/graxpert/main.py @@ -64,13 +64,18 @@ def version_type(arg_value, pat=re.compile(r"^\d+\.\d+\.\d+$")): def ui_main(): + import logging import tkinter as tk + from datetime import datetime + from tkinter import messagebox + import requests from appdirs import user_config_dir from customtkinter import CTk from graxpert.application.app import graxpert from graxpert.application.eventbus import eventbus + from graxpert.localization import _ from graxpert.mp_logging import initialize_logging, shutdown_logging from graxpert.parallel_processing import executor from graxpert.preferences import app_state_2_prefs, save_preferences @@ -78,10 +83,10 @@ def ui_main(): from graxpert.ui.application_frame import ApplicationFrame from graxpert.ui.styling import style from graxpert.ui.ui_events import UiEvents - from graxpert.version import check_for_new_version, release, version + from graxpert.version import release, version def on_closing(root, logging_thread): - graxpert.prefs = app_state_2_prefs(graxpert.prefs, graxpert.cmd.app_state, graxpert) + app_state_2_prefs(graxpert.prefs, graxpert.cmd.app_state) prefs_filename = os.path.join(user_config_dir(appname="GraXpert"), "preferences.json") save_preferences(prefs_filename, graxpert.prefs) @@ -94,6 +99,31 @@ def on_closing(root, logging_thread): logging.shutdown() sys.exit(0) + def check_for_new_version(): + try: + response = requests.get("https://api.github.com/repos/Steffenhir/GraXpert/releases/latest", timeout=2.5) + latest_release_date = datetime.strptime(response.json()["created_at"], "%Y-%m-%dT%H:%M:%SZ") + + response_current = requests.get("https://api.github.com/repos/Steffenhir/GraXpert/releases/tags/" + version, timeout=2.5) + current_release_date = datetime.strptime(response_current.json()["created_at"], "%Y-%m-%dT%H:%M:%SZ") + current_is_beta = response_current.json()["prerelease"] + + if current_is_beta: + if current_release_date >= latest_release_date: + messagebox.showinfo( + title=_("This is a Beta release!"), message=_("Please note that this is a Beta release of GraXpert. You will be notified when a newer official version is available.") + ) + else: + messagebox.showinfo( + title=_("New official release available!"), + message=_("This Beta version is deprecated. A newer official release of GraXpert is available at") + " https://github.com/Steffenhir/GraXpert/releases/latest", + ) + + elif latest_release_date > current_release_date: + messagebox.showinfo(title=_("New version available!"), message=_("A newer version of GraXpert is available at") + " https://github.com/Steffenhir/GraXpert/releases/latest") + except: + logging.warn("Could not check for newest version") + logging_thread = initialize_logging() check_for_new_version() @@ -102,6 +132,7 @@ def on_closing(root, logging_thread): try: root.state("zoomed") except: + root.geometry("1024x768") root.state("normal") root.title("GraXpert | Release: '{}' ({})".format(release, version)) root.iconbitmap() diff --git a/graxpert/preferences.py b/graxpert/preferences.py index e50496e..9cba9b1 100644 --- a/graxpert/preferences.py +++ b/graxpert/preferences.py @@ -2,145 +2,68 @@ import logging import os import shutil +from dataclasses import asdict, dataclass, field, fields from datetime import datetime -from typing import AnyStr, List, TypedDict +from typing import AnyStr, List import numpy as np from graxpert.app_state import AppState - - -class Prefs(TypedDict): - working_dir: AnyStr - width: int - height: int - background_points: List - bg_flood_selection_option: bool - bg_pts_option: int - stretch_option: AnyStr - saturation: float - display_pts: bool - bg_tol_option: float - interpol_type_option: AnyStr - smoothing_option: float - saveas_option: AnyStr - sample_size: int - sample_color: int - RBF_kernel: AnyStr - spline_order: int - lang: AnyStr - corr_type: AnyStr - scaling: float - ai_version: AnyStr - - -DEFAULT_PREFS: Prefs = { - "working_dir": os.getcwd(), - "width": None, - "height": None, - "background_points": [], - "bg_flood_selection_option": False, - "bg_pts_option": 15, - "stretch_option": "No Stretch", - "saturation": 1.0, - "display_pts": True, - "bg_tol_option": 1.0, - "interpol_type_option": "RBF", - "smoothing_option": 0.0, - "saveas_option": "32 bit Tiff", - "sample_size": 25, - "sample_color": 55, - "RBF_kernel": "thin_plate", - "spline_order": 3, - "lang": None, - "corr_type": "Subtraction", - "scaling": 1.0, - "ai_version": None, -} - - -def app_state_2_prefs(prefs: Prefs, app_state: AppState, app) -> Prefs: - if "background_points" in app_state: - prefs["background_points"] = [p.tolist() for p in app_state["background_points"]] - prefs["bg_pts_option"] = app.prefs["bg_pts_option"] - prefs["stretch_option"] = app.prefs["stretch_option"] - prefs["saturation"] = app.prefs["saturation"] - prefs["display_pts"] = app.prefs["display_pts"] - prefs["bg_tol_option"] = app.prefs["bg_tol_option"] - prefs["interpol_type_option"] = app.prefs["interpol_type_option"] - prefs["smoothing_option"] = app.prefs["smoothing_option"] - prefs["saveas_option"] = app.prefs["saveas_option"] - prefs["sample_size"] = app.prefs["sample_size"] - prefs["sample_color"] = app.prefs["sample_color"] - prefs["RBF_kernel"] = app.prefs["RBF_kernel"] - prefs["spline_order"] = app.prefs["spline_order"] - prefs["lang"] = app.prefs["lang"] - prefs["corr_type"] = app.prefs["corr_type"] - prefs["bg_flood_selection_option"] = app.prefs["bg_flood_selection_option"] - prefs["scaling"] = app.prefs["scaling"] - prefs["ai_version"] = app.prefs["ai_version"] +from graxpert.version import version as graxpert_version + + +@dataclass +class Prefs: + working_dir: AnyStr = os.getcwd() + width: int = None + height: int = None + background_points: List = field(default_factory=list) + bg_flood_selection_option: bool = False + bg_pts_option: int = 15 + stretch_option: AnyStr = "No Stretch" + saturation: float = 1.0 + display_pts: bool = True + bg_tol_option: float = 1.0 + interpol_type_option: AnyStr = "RBF" + smoothing_option: float = 0.0 + saveas_option: AnyStr = "32 bit Tiff" + sample_size: int = 25 + sample_color: int = 55 + RBF_kernel: AnyStr = "thin_plate" + spline_order: int = 3 + lang: AnyStr = None + corr_type: AnyStr = "Subtraction" + scaling: float = 1.0 + ai_version: AnyStr = None + graxpert_version: AnyStr = graxpert_version + + +def app_state_2_prefs(prefs: Prefs, app_state: AppState) -> Prefs: + prefs.background_points = [p.tolist() for p in app_state.background_points] return prefs def prefs_2_app_state(prefs: Prefs, app_state: AppState) -> AppState: - if "background_points" in prefs: - app_state["background_points"] = [np.array(p) for p in prefs["background_points"]] + app_state.background_points = [np.array(p) for p in prefs.background_points] return app_state def merge_json(prefs: Prefs, json) -> Prefs: - if "working_dir" in json: - prefs["working_dir"] = json["working_dir"] - if "width" in json: - prefs["width"] = json["width"] - if "height" in json: - prefs["height"] = json["height"] - if "background_points" in json: - prefs["background_points"] = json["background_points"] - if "bg_flood_selection_option" in json: - prefs["bg_flood_selection_option"] = json["bg_flood_selection_option"] - if "bg_pts_option" in json: - prefs["bg_pts_option"] = json["bg_pts_option"] - if "stretch_option" in json: - prefs["stretch_option"] = json["stretch_option"] - if "saturation" in json: - prefs["saturation"] = json["saturation"] - if "display_pts" in json: - prefs["display_pts"] = json["display_pts"] - if "bg_tol_option" in json: - prefs["bg_tol_option"] = json["bg_tol_option"] - if "interpol_type_option" in json: - prefs["interpol_type_option"] = json["interpol_type_option"] - if "smoothing_option" in json: - prefs["smoothing_option"] = json["smoothing_option"] - if "saveas_option" in json: - prefs["saveas_option"] = json["saveas_option"] - if "sample_size" in json: - prefs["sample_size"] = json["sample_size"] - if "sample_color" in json: - prefs["sample_color"] = json["sample_color"] - if "RBF_kernel" in json: - prefs["RBF_kernel"] = json["RBF_kernel"] - if "spline_order" in json: - prefs["spline_order"] = json["spline_order"] - if "lang" in json: - prefs["lang"] = json["lang"] - if "corr_type" in json: - prefs["corr_type"] = json["corr_type"] - if "scaling" in json: - prefs["scaling"] = json["scaling"] - if "ai_version" in json: - prefs["ai_version"] = json["ai_version"] + for f in fields(prefs): + if f.name in json: + setattr(prefs, f.name, json[f.name]) return prefs def load_preferences(prefs_filename) -> Prefs: - prefs = DEFAULT_PREFS + prefs = Prefs() try: if os.path.isfile(prefs_filename): with open(prefs_filename) as f: - json_prefs: Prefs = json.load(f) + json_prefs = json.load(f) prefs = merge_json(prefs, json_prefs) + if not "graxpert_version" in json_prefs: # reset scaling in case we start from GraXpert < 2.1.0 + prefs.scaling = 1.0 else: logging.info("{} appears to be missing. it will be created after program shutdown".format(prefs_filename)) except: @@ -156,42 +79,40 @@ def save_preferences(prefs_filename, prefs): try: os.makedirs(os.path.dirname(prefs_filename), exist_ok=True) with open(prefs_filename, "w") as f: - json.dump(prefs, f) + json.dump(asdict(prefs), f) except OSError as err: logging.exception("error serializing preferences") -def app_state_2_fitsheader(app, app_state, fits_header): - prefs = Prefs() - prefs = app_state_2_prefs(prefs, app_state, app) - fits_header["INTP-OPT"] = prefs["interpol_type_option"] - fits_header["SMOOTHING"] = prefs["smoothing_option"] - fits_header["CORR-TYPE"] = prefs["corr_type"] +def app_state_2_fitsheader(prefs: Prefs, app_state: AppState, fits_header): + fits_header["INTP-OPT"] = prefs.interpol_type_option + fits_header["SMOOTHING"] = prefs.smoothing_option + fits_header["CORR-TYPE"] = prefs.corr_type - if prefs["interpol_type_option"] == "AI": - fits_header["AI-VER"] = prefs["ai_version"] + if prefs.interpol_type_option == "AI": + fits_header["AI-VER"] = prefs.ai_version - if prefs["interpol_type_option"] != "AI": - fits_header["SAMPLE-SIZE"] = prefs["sample_size"] - fits_header["RBF-KERNEL"] = prefs["RBF_kernel"] - fits_header["SPLINE-ORDER"] = prefs["spline_order"] - fits_header["BG-PTS"] = str(prefs["background_points"]) + if prefs.interpol_type_option != "AI": + fits_header["SAMPLE-SIZE"] = prefs.sample_size + fits_header["RBF-KERNEL"] = prefs.RBF_kernel + fits_header["SPLINE-ORDER"] = prefs.spline_order + fits_header["BG-PTS"] = str(app_state.background_points) return fits_header -def fitsheader_2_app_state(app, app_state, fits_header): +def fitsheader_2_app_state(prefs: Prefs, app_state: AppState, fits_header): if "BG-PTS" in fits_header.keys(): - app_state["background_points"] = [np.array(p) for p in json.loads(fits_header["BG-PTS"])] + app_state.background_points = [np.array(p) for p in json.loads(fits_header["BG-PTS"])] if "INTP-OPT" in fits_header.keys(): - app.interpol_type.set(fits_header["INTP-OPT"]) - app.smoothing_slider.set(fits_header["SMOOTHING"]) - app.corr_type.set(fits_header["CORR-TYPE"]) + prefs.interpol_type_option = fits_header["INTP-OPT"] + prefs.smoothing_option = fits_header["SMOOTHING"] + prefs.corr_type = fits_header["CORR-TYPE"] if fits_header["INTP-OPT"] != "AI": - app.help_panel.sample_size_slider.set(fits_header["SAMPLE-SIZE"]) - app.RBF_kernel.set(fits_header["RBF-KERNEL"]) - app.spline_order.set(fits_header["SPLINE-ORDER"]) + prefs.sample_size = fits_header["SAMPLE-SIZE"] + prefs.RBF_kernel = fits_header["RBF-KERNEL"] + prefs.spline_order = fits_header["SPLINE-ORDER"] return app_state diff --git a/graxpert/ui/canvas.py b/graxpert/ui/canvas.py index eb6a31d..86c6410 100644 --- a/graxpert/ui/canvas.py +++ b/graxpert/ui/canvas.py @@ -174,6 +174,7 @@ def on_create_grid_end(self, event=None): def on_display_start_badge_request(self, event=None): self.start_badge = ImageTk.PhotoImage(file=resource_path("img/graXpert_Startbadge_Ariel.png")) self.canvas.create_image(self.canvas.winfo_width() / 2, self.canvas.winfo_height() / 2, anchor=tk.CENTER, image=self.start_badge, tags="start_badge") + self.canvas.after(5000, lambda: self.canvas.delete("start_badge")) def on_load_image_begin(self, event=None): self.canvas.delete("start_badge") @@ -201,11 +202,11 @@ def on_mouse_down_left(self, event=None): self.clicked_inside_pt = False point_im = graxpert.to_image_point(event.x, event.y) - if len(graxpert.cmd.app_state["background_points"]) != 0 and len(point_im) != 0 and graxpert.prefs["display_pts"]: + if len(graxpert.cmd.app_state.background_points) != 0 and len(point_im) != 0 and graxpert.prefs.display_pts: eventx_im = point_im[0] eventy_im = point_im[1] - background_points = graxpert.cmd.app_state["background_points"] + background_points = graxpert.cmd.app_state.background_points min_idx = -1 min_dist = -1 @@ -220,10 +221,10 @@ def on_mouse_down_left(self, event=None): min_dist = dist min_idx = i - if min_idx != -1 and min_dist <= graxpert.prefs["sample_size"]: + if min_idx != -1 and min_dist <= graxpert.prefs.sample_size: self.clicked_inside_pt = True self.clicked_inside_pt_idx = min_idx - self.clicked_inside_pt_coord = graxpert.cmd.app_state["background_points"][min_idx] + self.clicked_inside_pt_coord = graxpert.cmd.app_state.background_points[min_idx] if self.crop_mode: # Check if inside circles to move crop corners @@ -235,7 +236,7 @@ def on_mouse_down_left(self, event=None): self.__old_event = event def on_mouse_down_right(self, event=None): - if graxpert.images["Original"] is None or not graxpert.prefs["display_pts"]: + if graxpert.images["Original"] is None or not graxpert.prefs.display_pts: return graxpert.remove_pt(event) @@ -255,10 +256,10 @@ def on_mouse_move_left(self, event=None): if self.left_drag_timer == -1: self.left_drag_timer = event.time - if self.clicked_inside_pt and graxpert.prefs["display_pts"] and not self.crop_mode: + if self.clicked_inside_pt and graxpert.prefs.display_pts and not self.crop_mode: new_point = graxpert.to_image_point(event.x, event.y) if len(new_point) != 0: - graxpert.cmd.app_state["background_points"][self.clicked_inside_pt_idx] = new_point + graxpert.cmd.app_state.background_points[self.clicked_inside_pt_idx] = new_point self.redraw_points() @@ -288,28 +289,28 @@ def on_mouse_move_left(self, event=None): return def on_mouse_release_left(self, event=None): - if graxpert.images["Original"] is None or not graxpert.prefs["display_pts"]: + if graxpert.images["Original"] is None or not graxpert.prefs.display_pts: return if self.clicked_inside_pt and not self.crop_mode: new_point = graxpert.to_image_point(event.x, event.y) - graxpert.cmd.app_state["background_points"][self.clicked_inside_pt_idx] = self.clicked_inside_pt_coord + graxpert.cmd.app_state.background_points[self.clicked_inside_pt_idx] = self.clicked_inside_pt_coord graxpert.cmd = Command(MOVE_POINT_HANDLER, prev=graxpert.cmd, new_point=new_point, idx=self.clicked_inside_pt_idx) graxpert.cmd.execute() elif len(graxpert.to_image_point(event.x, event.y)) != 0 and (event.time - self.left_drag_timer < 100 or self.left_drag_timer == -1): point = graxpert.to_image_point(event.x, event.y) - if not graxpert.prefs["bg_flood_selection_option"]: + if not graxpert.prefs.bg_flood_selection_option: graxpert.cmd = Command(ADD_POINT_HANDLER, prev=graxpert.cmd, point=point) else: graxpert.cmd = Command( ADD_POINTS_HANDLER, prev=graxpert.cmd, point=point, - tol=graxpert.prefs["bg_tol_option"], - bg_pts=graxpert.prefs["bg_pts_option"], - sample_size=graxpert.prefs["sample_size"], + tol=graxpert.prefs.bg_tol_option, + bg_pts=graxpert.prefs.bg_pts_option, + sample_size=graxpert.prefs.sample_size, image=graxpert.images["Original"], ) graxpert.cmd.execute() @@ -397,16 +398,16 @@ def redraw_points(self, event=None): if graxpert.images["Original"] is None: return - color = hls_to_rgb(graxpert.prefs["sample_color"] / 360, 0.5, 1.0) + color = hls_to_rgb(graxpert.prefs.sample_color / 360, 0.5, 1.0) color = (int(color[0] * 255), int(color[1] * 255), int(color[2] * 255)) color = "#%02x%02x%02x" % color self.canvas.delete("sample") self.canvas.delete("crop") - rectsize = graxpert.prefs["sample_size"] - background_points = graxpert.cmd.app_state["background_points"] + rectsize = graxpert.prefs.sample_size + background_points = graxpert.cmd.app_state.background_points - if graxpert.prefs["display_pts"] and not self.crop_mode: + if graxpert.prefs.display_pts and not self.crop_mode: for point in background_points: corner1 = graxpert.to_canvas_point(point[0] - rectsize, point[1] - rectsize) corner2 = graxpert.to_canvas_point(point[0] + rectsize, point[1] + rectsize) diff --git a/graxpert/ui/left_menu.py b/graxpert/ui/left_menu.py index be12168..59ec57d 100644 --- a/graxpert/ui/left_menu.py +++ b/graxpert/ui/left_menu.py @@ -39,62 +39,44 @@ def __init__(self, parent, **kwargs): # stretch options self.stretch_options = ["No Stretch", "10% Bg, 3 sigma", "15% Bg, 3 sigma", "20% Bg, 3 sigma", "30% Bg, 2 sigma"] self.stretch_option_current = StringVar() - self.stretch_option_current.set(self.stretch_options[0]) - if "stretch_option" in graxpert.prefs: - self.stretch_option_current.set(graxpert.prefs["stretch_option"]) + self.stretch_option_current.set(graxpert.prefs.stretch_option) self.stretch_option_current.trace_add("write", lambda a, b, c: eventbus.emit(AppEvents.STRETCH_OPTION_CHANGED, {"stretch_option": self.stretch_option_current.get()})) self.saturation = tk.DoubleVar() - self.saturation.set(1.0) - if "saturation" in graxpert.prefs: - self.saturation.set(graxpert.prefs["saturation"]) + self.saturation.set(graxpert.prefs.saturation) self.saturation.trace_add("write", lambda a, b, c: eventbus.emit(AppEvents.CHANGE_SATURATION_REQUEST, {"saturation": self.saturation.get()})) # sample selection self.display_pts = tk.BooleanVar() - self.display_pts.set(True) - if "display_pts" in graxpert.prefs: - self.display_pts.set(graxpert.prefs["display_pts"]) + self.display_pts.set(graxpert.prefs.display_pts) self.display_pts.trace_add("write", lambda a, b, c: eventbus.emit(AppEvents.DISPLAY_PTS_CHANGED, {"display_pts": self.display_pts.get()})) self.flood_select_pts = tk.BooleanVar() - self.flood_select_pts.set(False) - if "bg_flood_selection_option" in graxpert.prefs: - self.flood_select_pts.set(graxpert.prefs["bg_flood_selection_option"]) + self.flood_select_pts.set(graxpert.prefs.bg_flood_selection_option) self.flood_select_pts.trace_add("write", lambda a, b, c: eventbus.emit(AppEvents.BG_FLOOD_SELECTION_CHANGED, {"bg_flood_selection_option": self.flood_select_pts.get()})) self.bg_pts = tk.IntVar() - self.bg_pts.set(10) - if "bg_pts_option" in graxpert.prefs: - self.bg_pts.set(graxpert.prefs["bg_pts_option"]) + self.bg_pts.set(graxpert.prefs.bg_pts_option) self.bg_pts.trace_add("write", lambda a, b, c: eventbus.emit(AppEvents.BG_PTS_CHANGED, {"bg_pts_option": self.bg_pts.get()})) self.bg_tol = tk.DoubleVar() - self.bg_tol.set(1) - if "bg_tol_option" in graxpert.prefs: - self.bg_tol.set(graxpert.prefs["bg_tol_option"]) + self.bg_tol.set(graxpert.prefs.bg_tol_option) self.bg_tol.trace_add("write", lambda a, b, c: eventbus.emit(AppEvents.BG_TOL_CHANGED, {"bg_tol_option": self.bg_tol.get()})) # calculation self.interpol_options = ["RBF", "Splines", "Kriging", "AI"] self.interpol_type = tk.StringVar() - self.interpol_type.set(self.interpol_options[0]) - if "interpol_type_option" in graxpert.prefs: - self.interpol_type.set(graxpert.prefs["interpol_type_option"]) + self.interpol_type.set(graxpert.prefs.interpol_type_option) self.interpol_type.trace_add("write", lambda a, b, c: eventbus.emit(AppEvents.INTERPOL_TYPE_CHANGED, {"interpol_type_option": self.interpol_type.get()})) self.smoothing = tk.DoubleVar() - self.smoothing.set(1.0) - if "smoothing_option" in graxpert.prefs: - self.smoothing.set(graxpert.prefs["smoothing_option"]) + self.smoothing.set(graxpert.prefs.smoothing_option) self.smoothing.trace_add("write", lambda a, b, c: eventbus.emit(AppEvents.SMOTTHING_CHANGED, {"smoothing_option": self.smoothing.get()})) # saving self.saveas_options = ["16 bit Tiff", "32 bit Tiff", "16 bit Fits", "32 bit Fits", "16 bit XISF", "32 bit XISF"] self.saveas_type = tk.StringVar() - self.saveas_type.set(self.saveas_options[0]) - if "saveas_option" in graxpert.prefs: - self.saveas_type.set(graxpert.prefs["saveas_option"]) + self.saveas_type.set(graxpert.prefs.saveas_option) self.saveas_type.trace_add("write", lambda a, b, c: eventbus.emit(AppEvents.SAVE_AS_CHANGED, {"saveas_option": self.saveas_type.get()})) self.create_children() diff --git a/graxpert/ui/right_menu.py b/graxpert/ui/right_menu.py index d2b31f4..c580ec9 100644 --- a/graxpert/ui/right_menu.py +++ b/graxpert/ui/right_menu.py @@ -93,51 +93,38 @@ def __init__(self, master, **kwargs): # sample points self.sample_size = tk.IntVar() - self.sample_size.set(25) - if "sample_size" in graxpert.prefs: - self.sample_size.set(graxpert.prefs["sample_size"]) + self.sample_size.set(graxpert.prefs.sample_size) self.sample_size.trace_add("write", lambda a, b, c: eventbus.emit(AppEvents.SAMPLE_SIZE_CHANGED, {"sample_size": self.sample_size.get()})) self.sample_color = tk.IntVar() - self.sample_color.set(55) - if "sample_color" in graxpert.prefs: - self.sample_color.set(graxpert.prefs["sample_color"]) + self.sample_color.set(graxpert.prefs.sample_color) self.sample_color.trace_add("write", lambda a, b, c: eventbus.emit(AppEvents.SAMPLE_COLOR_CHANGED, {"sample_color": self.sample_color.get()})) # interpolation self.rbf_kernels = ["thin_plate", "quintic", "cubic", "linear"] self.rbf_kernel = tk.StringVar() - self.rbf_kernel.set(self.rbf_kernels[0]) - if "RBF_kernel" in graxpert.prefs: - self.rbf_kernel.set(graxpert.prefs["RBF_kernel"]) + self.rbf_kernel.set(graxpert.prefs.RBF_kernel) self.rbf_kernel.trace_add("write", lambda a, b, c: eventbus.emit(AppEvents.RBF_KERNEL_CHANGED, {"RBF_kernel": self.rbf_kernel.get()})) self.spline_orders = ["1", "2", "3", "4", "5"] self.spline_order = tk.StringVar() - self.spline_order.set("3") - if "spline_order" in graxpert.prefs: - self.spline_order.set(graxpert.prefs["spline_order"]) - self.spline_order.trace_add("write", lambda a, b, c: eventbus.emit(AppEvents.SPLINE_ORDER_CHANGED, {"spline_order": self.spline_order.get()})) + self.spline_order.set(str(graxpert.prefs.spline_order)) + self.spline_order.trace_add("write", lambda a, b, c: eventbus.emit(AppEvents.SPLINE_ORDER_CHANGED, {"spline_order": int(self.spline_order.get())})) self.corr_types = ["Subtraction", "Division"] self.corr_type = tk.StringVar() - self.corr_type.set(self.corr_types[0]) - if "corr_type" in graxpert.prefs: - self.corr_type.set(graxpert.prefs["corr_type"]) + self.corr_type.set(graxpert.prefs.corr_type) self.corr_type.trace_add("write", lambda a, b, c: eventbus.emit(AppEvents.CORRECTION_TYPE_CHANGED, {"corr_type": self.corr_type.get()})) # interface self.langs = ["English", "Deutsch"] self.lang = tk.StringVar() - if "lang" in graxpert.prefs: - self.lang.set(graxpert.prefs["lang"]) + self.lang.set(graxpert.prefs.lang) self.lang.trace_add("write", lambda a, b, c: eventbus.emit(AppEvents.LANGUAGE_CHANGED, {"lang": self.lang.get()})) self.scaling = tk.DoubleVar() - self.scaling.set(1.0) - if "scaling" in graxpert.prefs: - self.scaling.set(graxpert.prefs["scaling"]) - self.scaling.trace_add("write", lambda a, b, c: ctk.set_widget_scaling(self.scaling.get())) + self.scaling.set(graxpert.prefs.scaling) + self.scaling.trace_add("write", self.on_scaling_change) # ai model remote_versions = list_remote_versions() @@ -149,8 +136,8 @@ def __init__(self, master, **kwargs): self.ai_version = tk.StringVar(master) self.ai_version.set("None") # default value - if "ai_version" in graxpert.prefs: - self.ai_version.set(graxpert.prefs["ai_version"]) + if graxpert.prefs.ai_version is not None: + self.ai_version.set(graxpert.prefs.ai_version) else: self.ai_options.insert(0, "None") self.ai_version.trace_add("write", lambda a, b, c: eventbus.emit(AppEvents.AI_VERSION_CHANGED, {"ai_version": self.ai_version.get()})) @@ -158,6 +145,10 @@ def __init__(self, master, **kwargs): self.create_and_place_children() self.setup_layout() + def on_scaling_change(self, a, b, c): + eventbus.emit(AppEvents.SCALING_CHANGED, {"scaling": self.scaling.get()}) + ctk.set_widget_scaling(self.scaling.get()) + def create_and_place_children(self): CTkLabel(self, text=_("Advanced Settings"), font=self.heading_font).grid(column=0, row=self.nrow(), pady=pady, sticky=tk.N) diff --git a/graxpert/ui/widgets.py b/graxpert/ui/widgets.py index 6b5a751..fd4e292 100644 --- a/graxpert/ui/widgets.py +++ b/graxpert/ui/widgets.py @@ -26,7 +26,7 @@ def __init__(self, parent, width=default_label_width, **kwargs): class GraXpertOptionMenu(CTkOptionMenu): def __init__(self, parent, width=default_label_width, **kwargs): - super().__init__(parent, width=width, **kwargs) + super().__init__(parent, width=width, anchor=tk.CENTER, **kwargs) class GraXpertCheckbox(CTkCheckBox): diff --git a/graxpert/ui_scaling.py b/graxpert/ui_scaling.py index 57f5600..a5a97d2 100644 --- a/graxpert/ui_scaling.py +++ b/graxpert/ui_scaling.py @@ -8,9 +8,6 @@ def get_scaling_factor(): prefs_filename = os.path.join(user_config_dir(appname="GraXpert"), "preferences.json") prefs = load_preferences(prefs_filename) - scaling_factor = 1.0 - - if "scaling" in prefs: - scaling_factor = prefs["scaling"] + scaling_factor = prefs.scaling return scaling_factor diff --git a/graxpert/version.py b/graxpert/version.py index bf344fe..5408017 100644 --- a/graxpert/version.py +++ b/graxpert/version.py @@ -1,36 +1,2 @@ -import logging -from datetime import datetime -from tkinter import messagebox - -import requests - -from graxpert.localization import _ - release = "RELEASE" version = "SNAPSHOT" - - -def check_for_new_version(): - try: - response = requests.get("https://api.github.com/repos/Steffenhir/GraXpert/releases/latest", timeout=2.5) - latest_release_date = datetime.strptime(response.json()["created_at"], "%Y-%m-%dT%H:%M:%SZ") - - response_current = requests.get("https://api.github.com/repos/Steffenhir/GraXpert/releases/tags/" + version, timeout=2.5) - current_release_date = datetime.strptime(response_current.json()["created_at"], "%Y-%m-%dT%H:%M:%SZ") - current_is_beta = response_current.json()["prerelease"] - - if current_is_beta: - if current_release_date >= latest_release_date: - messagebox.showinfo(title = _("This is a Beta release!"), - message= _("Please note that this is a Beta release of GraXpert. You will be notified when a newer official version is available.")) - else: - messagebox.showinfo(title = _("New official release available!"), - message= _("This Beta version is deprecated. A newer official release of GraXpert is available at") + " https://github.com/Steffenhir/GraXpert/releases/latest") - - - - elif latest_release_date > current_release_date: - messagebox.showinfo(title = _("New version available!"), - message= _("A newer version of GraXpert is available at") + " https://github.com/Steffenhir/GraXpert/releases/latest") - except: - logging.warn("Could not check for newest version") From 0fa605e14503aabce12d62e4a1812ffad6af4fde Mon Sep 17 00:00:00 2001 From: David Schmelter Date: Wed, 27 Dec 2023 14:51:09 +0100 Subject: [PATCH 12/17] code cleanup --- graxpert/background_extraction.py | 2 +- graxpert/main.py | 10 ++++++++-- requirements.txt | 1 - 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/graxpert/background_extraction.py b/graxpert/background_extraction.py index 7ee564e..124e5b2 100644 --- a/graxpert/background_extraction.py +++ b/graxpert/background_extraction.py @@ -248,7 +248,7 @@ def interpol(shm_imarray_name, shm_background_name, c, x_sub, y_sub, shape, kind # del gpr else: - logging.warn("Interpolation method not recognized") + logging.warning("Interpolation method not recognized") return if(downscale_factor != 1): diff --git a/graxpert/main.py b/graxpert/main.py index 5b95bd0..0bd754b 100644 --- a/graxpert/main.py +++ b/graxpert/main.py @@ -66,7 +66,9 @@ def version_type(arg_value, pat=re.compile(r"^\d+\.\d+\.\d+$")): def ui_main(): import logging import tkinter as tk + from concurrent.futures import ProcessPoolExecutor from datetime import datetime + from inspect import signature from tkinter import messagebox import requests @@ -91,7 +93,11 @@ def on_closing(root, logging_thread): prefs_filename = os.path.join(user_config_dir(appname="GraXpert"), "preferences.json") save_preferences(prefs_filename, graxpert.prefs) try: - executor.shutdown(cancel_futures=True) + if "cancel_futures" in signature(ProcessPoolExecutor.shutdown).parameters: + executor.shutdown(cancel_futures=True) # Python > 3.8 + else: + executor.shutdown() # Python <= 3.8 + except Exception as e: logging.exception("error shutting down ProcessPoolExecutor") shutdown_logging(logging_thread) @@ -122,7 +128,7 @@ def check_for_new_version(): elif latest_release_date > current_release_date: messagebox.showinfo(title=_("New version available!"), message=_("A newer version of GraXpert is available at") + " https://github.com/Steffenhir/GraXpert/releases/latest") except: - logging.warn("Could not check for newest version") + logging.warning("Could not check for newest version") logging_thread = initialize_logging() check_for_new_version() diff --git a/requirements.txt b/requirements.txt index 7681ba7..3ca5dc3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,6 +9,5 @@ pykrige requests scikit-image == 0.21.0 scipy -screeninfo tensorflow == 2.13.1 xisf \ No newline at end of file From c6ea33eaa7ff326c90fc01a5e55833b71b5f52a0 Mon Sep 17 00:00:00 2001 From: David Schmelter Date: Wed, 27 Dec 2023 14:51:34 +0100 Subject: [PATCH 13/17] fix calculation of resource_path --- graxpert/resource_utils.py | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/graxpert/resource_utils.py b/graxpert/resource_utils.py index 05156f7..f222f9f 100644 --- a/graxpert/resource_utils.py +++ b/graxpert/resource_utils.py @@ -1,22 +1,12 @@ # from appdirs import import os -import sys from tempfile import TemporaryDirectory -import numpy as np -from skimage import io -from skimage.transform import resize - temp_resource_dir = TemporaryDirectory() def resource_path(relative_path): - if getattr(sys, "frozen", False): - # The application is frozen - base_path = os.path.join(os.path.dirname(sys.executable), "lib") - else: - # The application is not frozen - base_path = os.path.join(os.path.dirname(__file__), "..") + base_path = os.path.join(os.path.dirname(os.path.dirname(__file__))) return os.path.join(base_path, relative_path) From e57840113a3001a3eddcfeb814b18325be1dd45b Mon Sep 17 00:00:00 2001 From: David Schmelter Date: Wed, 27 Dec 2023 14:56:19 +0100 Subject: [PATCH 14/17] fix mouse wheel on linux --- graxpert/ui/left_menu.py | 18 +++++++++++++++--- graxpert/ui/right_menu.py | 6 +++--- graxpert/ui/widgets.py | 25 ++++++++++++++++++++++++- 3 files changed, 42 insertions(+), 7 deletions(-) diff --git a/graxpert/ui/left_menu.py b/graxpert/ui/left_menu.py index 59ec57d..78d5a2f 100644 --- a/graxpert/ui/left_menu.py +++ b/graxpert/ui/left_menu.py @@ -1,6 +1,6 @@ import tkinter as tk -from customtkinter import CTkScrollableFrame, StringVar, ThemeManager +from customtkinter import StringVar, ThemeManager import graxpert.ui.tooltip as tooltip from graxpert.application.app import graxpert @@ -8,7 +8,19 @@ from graxpert.application.eventbus import eventbus from graxpert.localization import _ from graxpert.ui.ui_events import UiEvents -from graxpert.ui.widgets import CollapsibleMenuFrame, ExtractionStep, GraXpertButton, GraXpertCheckbox, GraXpertLabel, GraXpertOptionMenu, ValueSlider, default_label_width, padx, pady +from graxpert.ui.widgets import ( + CollapsibleMenuFrame, + ExtractionStep, + GraXpertButton, + GraXpertCheckbox, + GraXpertLabel, + GraXpertOptionMenu, + GraXpertScrollableFrame, + ValueSlider, + default_label_width, + padx, + pady, +) class CropMenu(CollapsibleMenuFrame): @@ -204,7 +216,7 @@ def menu_open_clicked(self, event=None): eventbus.emit(AppEvents.OPEN_FILE_DIALOG_REQUEST) -class LeftMenu(CTkScrollableFrame): +class LeftMenu(GraXpertScrollableFrame): def __init__(self, parent, **kwargs): super().__init__(parent, **kwargs) self.create_children() diff --git a/graxpert/ui/right_menu.py b/graxpert/ui/right_menu.py index c580ec9..a70495c 100644 --- a/graxpert/ui/right_menu.py +++ b/graxpert/ui/right_menu.py @@ -2,7 +2,7 @@ from tkinter import messagebox import customtkinter as ctk -from customtkinter import CTkFont, CTkImage, CTkLabel, CTkScrollableFrame, CTkTextbox +from customtkinter import CTkFont, CTkImage, CTkLabel, CTkTextbox from packaging import version from PIL import Image @@ -12,7 +12,7 @@ from graxpert.application.eventbus import eventbus from graxpert.localization import _, lang from graxpert.resource_utils import resource_path -from graxpert.ui.widgets import ExtractionStep, GraXpertOptionMenu, ValueSlider, padx, pady +from graxpert.ui.widgets import ExtractionStep, GraXpertOptionMenu, GraXpertScrollableFrame, ValueSlider, padx, pady class HelpText(CTkTextbox): @@ -22,7 +22,7 @@ def __init__(self, master, text="", rows=1, font=None, **kwargs): self.insert("0.0", text) -class RightFrameBase(CTkScrollableFrame): +class RightFrameBase(GraXpertScrollableFrame): def __init__(self, master, **kwargs): super().__init__(master, **kwargs) self.row = 0 diff --git a/graxpert/ui/widgets.py b/graxpert/ui/widgets.py index fd4e292..ac1dc7c 100644 --- a/graxpert/ui/widgets.py +++ b/graxpert/ui/widgets.py @@ -1,6 +1,6 @@ import tkinter as tk -from customtkinter import CTkButton, CTkCheckBox, CTkEntry, CTkFrame, CTkImage, CTkLabel, CTkOptionMenu, CTkSlider, DoubleVar, StringVar, ThemeManager +from customtkinter import CTkButton, CTkCheckBox, CTkEntry, CTkFrame, CTkImage, CTkLabel, CTkOptionMenu, CTkScrollableFrame, CTkSlider, DoubleVar, StringVar, ThemeManager from PIL import Image from graxpert.localization import _ @@ -34,6 +34,29 @@ def __init__(self, parent, width=default_label_width, **kwargs): super().__init__(parent, width=width, checkbox_width=20, checkbox_height=20, **kwargs) +class GraXpertScrollableFrame(CTkScrollableFrame): + def __init__(self, parent, **kwargs): + super().__init__(parent, **kwargs) + + self.bind_all("", self.on_mouse_wheel, add="+") # Mouse Wheel Linux + self.bind_all("", self.on_mouse_wheel, add="+") # Mouse Wheel Linux + + def on_mouse_wheel(self, event=None): + if self.check_if_master_is_canvas(event.widget): + if self._shift_pressed: + if self._parent_canvas.xview() != (0.0, 1.0): + if event.delta > 0 or event.num == 4: + self._parent_canvas.xview_scroll(-1, "units") + else: + self._parent_canvas.xview_scroll(1, "units") + else: + if self._parent_canvas.yview() != (0.0, 1.0): + if event.delta > 0 or event.num == 4: + self._parent_canvas.yview_scroll(-1, "units") + else: + self._parent_canvas.yview_scroll(1, "units") + + class ExtractionStep(CTkFrame): def __init__(self, parent, number=0, title="", **kwargs): super().__init__(parent, **kwargs) From 2f4817ef6e4b31da97bb79f3d3b13c3dbf357bd9 Mon Sep 17 00:00:00 2001 From: David Schmelter Date: Wed, 27 Dec 2023 15:16:06 +0100 Subject: [PATCH 15/17] migrate build of linux artifacts to cx_freeze --- .github/workflows/build-release.yml | 61 ++++++++++++++++++----- GraXpert-linux.spec | 42 ---------------- releng/hook-tensorflow.py | 13 ----- setup.py | 77 ++++++----------------------- 4 files changed, 66 insertions(+), 127 deletions(-) delete mode 100644 GraXpert-linux.spec delete mode 100644 releng/hook-tensorflow.py diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml index cda93aa..0876a58 100644 --- a/.github/workflows/build-release.yml +++ b/.github/workflows/build-release.yml @@ -13,7 +13,7 @@ on: jobs: - build-linux: + build-linux-deb: runs-on: ubuntu-20.04 steps: - name: setup python @@ -30,22 +30,56 @@ jobs: echo "bucket_name = \"$ai_s3_bucket_name\"" >> ./graxpert/s3_secrets.py - name: install dependencies run: | - pip install setuptools wheel pyinstaller && \ + sudo apt install alien -y && \ + pip install "cx_freeze>=6.16.0.dev" && \ pip install -r requirements.txt - name: patch version run: | chmod u+x ./releng/patch_version.sh && \ ./releng/patch_version.sh - name: create GraXpert-linux bundle - # TODO migrato to cx_freeze + run: python ./setup.py bdist_deb + - name: store artifacts + uses: actions/upload-artifact@v2 + with: + name: graxpert_${{github.ref_name}}-1_amd64.deb + path: ./dist/graxpert_${{github.ref_name}}-1_amd64.deb + retention-days: 5 + + build-linux-zip: + runs-on: ubuntu-20.04 + steps: + - name: setup python + uses: actions/setup-python@v2 + with: + python-version: '3.10' + - name: checkout repository + uses: actions/checkout@v3 + - name: configure ai s3 secrets run: | - pyinstaller \ - ./GraXpert-linux.spec \ + echo "endpoint = \"$ai_s3_endpoint\"" >> ./graxpert/s3_secrets.py && \ + echo "ro_access_key = \"$ai_s3_access_key\"" >> ./graxpert/s3_secrets.py && \ + echo "ro_secret_key = \"$ai_s3_secret_key\"" >> ./graxpert/s3_secrets.py && \ + echo "bucket_name = \"$ai_s3_bucket_name\"" >> ./graxpert/s3_secrets.py + - name: install dependencies + run: | + pip install setuptools wheel cx_freeze && \ + pip install -r requirements.txt + - name: patch version + run: | + chmod u+x ./releng/patch_version.sh && \ + ./releng/patch_version.sh + - name: create GraXpert-linux bundle + run: python ./setup.py install_exe --install-dir=./dist/GraXpert-linux + - name: zip GraXpert-linux bundle + run: | + cd ./dist && \ + zip -r ./GraXpert-linux.zip ./GraXpert-linux - name: store artifacts uses: actions/upload-artifact@v2 with: - name: GraXpert-linux - path: ./dist/GraXpert-linux + name: GraXpert-linux.zip + path: ./dist/GraXpert-linux.zip retention-days: 5 build-windows: @@ -132,12 +166,16 @@ jobs: release: runs-on: ubuntu-latest - needs: [build-linux, build-windows, build-macos-x86_64] + needs: [build-linux-deb, build-linux-zip, build-windows, build-macos-x86_64] steps: - - name: download linux binary + - name: download linux deb + uses: actions/download-artifact@v2 + with: + name: graxpert_${{github.ref_name}}-1_amd64.deb + - name: download linux zip uses: actions/download-artifact@v2 with: - name: GraXpert-linux + name: GraXpert-linux.zip - name: download windows exe uses: actions/download-artifact@v2 with: @@ -150,6 +188,7 @@ jobs: uses: softprops/action-gh-release@v1 with: files: | - GraXpert-linux + graxpert_${{github.ref_name}}-1_amd64.deb + GraXpert-linux.zip GraXpert-${{github.ref_name}}-win64.msi GraXpert-macos-x86_64.dmg diff --git a/GraXpert-linux.spec b/GraXpert-linux.spec deleted file mode 100644 index 1af796d..0000000 --- a/GraXpert-linux.spec +++ /dev/null @@ -1,42 +0,0 @@ -# -*- mode: python ; coding: utf-8 -*- - - -block_cipher = None - - -a = Analysis(['./graxpert/main.py'], - pathex=[], - binaries=[], - datas=[('./img/*', './img/'), ('./graxpert-dark-blue.json', './')], - hiddenimports=['PIL._tkinter_finder', 'tkinter'], - hookspath=['./releng'], - hooksconfig={}, - runtime_hooks=[], - excludes=[], - win_no_prefer_redirects=False, - win_private_assemblies=False, - cipher=block_cipher, - noarchive=False) - -pyz = PYZ(a.pure, a.zipped_data, - cipher=block_cipher) - -exe = EXE(pyz, - a.scripts, - a.binaries, - Tree('locales', prefix='locales/'), - a.zipfiles, - a.datas, - [], - name='GraXpert-linux', - debug=False, - bootloader_ignore_signals=False, - strip=False, - upx=True, - upx_exclude=[], - runtime_tmpdir=None, - console=False, - disable_windowed_traceback=False, - target_arch=None, - codesign_identity=None, - entitlements_file=None , icon='./img/Icon.ico') diff --git a/releng/hook-tensorflow.py b/releng/hook-tensorflow.py deleted file mode 100644 index 78378ff..0000000 --- a/releng/hook-tensorflow.py +++ /dev/null @@ -1,13 +0,0 @@ -from PyInstaller.utils.hooks import collect_all - - -def hook(hook_api): - packages = [ - 'tensorflow', - 'tensorflow_core' - ] - for package in packages: - datas, binaries, hiddenimports = collect_all(package) - hook_api.add_datas(datas) - hook_api.add_binaries(binaries) - hook_api.add_imports(*hiddenimports) diff --git a/setup.py b/setup.py index cfc53d4..166ca25 100644 --- a/setup.py +++ b/setup.py @@ -1,37 +1,22 @@ -from cx_Freeze import Executable, setup -from graxpert.version import version, release -import astropy -import sys import os +import sys + +import astropy +from cx_Freeze import Executable, setup + +from graxpert.version import release, version astropy_path = os.path.dirname(os.path.abspath(astropy.__file__)) -directory_table = [ - ("ProgramMenuFolder", "TARGETDIR", "."), - ("GraXpert", "ProgramMenuFolder", "GraXpert"), -] +directory_table = [("ProgramMenuFolder", "TARGETDIR", "."), ("GraXpert", "ProgramMenuFolder", "GraXpert")] msi_data = { "Directory": directory_table, - "ProgId": [ - ( - "Prog.Id", - None, - None, - "GraXpert is an astronomical image processing program for extracting and removing gradients in the background of your astrophotos", - "IconId", - None, - ), - ], - "Icon": [ - ("IconId", "./img/Icon.ico"), - ], + "ProgId": [("Prog.Id", None, None, "GraXpert is an astronomical image processing program for extracting and removing gradients in the background of your astrophotos", "IconId", None)], + "Icon": [("IconId", "./img/Icon.ico")], } -msi_summary_data = { - "author": "GraXpert Development Team", - "comments": "", -} +msi_summary_data = {"author": "GraXpert Development Team", "comments": ""} bdist_msi_options = { "add_to_path": True, @@ -42,33 +27,16 @@ "install_icon": "./img/Icon.ico", } -bidst_rpm_options = { - "release": release, - "vendor": "GraXpert Development Team ", - "group": "Unspecified", -} +bidst_rpm_options = {"release": release, "vendor": "GraXpert Development Team ", "group": "Unspecified"} build_options = { - "includes": [ - "astropy.constants.codata2018", - "astropy.constants.iau2015", - "imageio.plugins.pillow", - "skimage.draw.draw", - "skimage.exposure.exposure", - "skimage.filters._gaussian", - ], + "includes": ["astropy.constants.codata2018", "astropy.constants.iau2015", "imageio.plugins.pillow", "skimage.draw.draw", "skimage.exposure.exposure", "skimage.filters._gaussian"], "include_files": [ ["./img", "./lib/img"], ["./graxpert-dark-blue.json", "./lib/graxpert-dark-blue.json"], ["./locales/", "./lib/locales/"], - [ - os.path.join(astropy_path, "units", "format", "generic_parsetab.py"), - "./lib/astropy/units/format/generic_parsetab.py", - ], - [ - os.path.join(astropy_path, "units", "format", "generic_lextab.py"), - "./lib/astropy/units/format/generic_lextab.py", - ], + [os.path.join(astropy_path, "units", "format", "generic_parsetab.py"), "./lib/astropy/units/format/generic_parsetab.py"], + [os.path.join(astropy_path, "units", "format", "generic_lextab.py"), "./lib/astropy/units/format/generic_lextab.py"], ], "excludes": [], "include_msvcr": True, @@ -76,26 +44,13 @@ base = "Win32GUI" if sys.platform == "win32" else None -executables = [ - Executable( - "./graxpert/main.py", - base=base, - icon="./img/Icon.ico", - target_name="GraXpert", - shortcut_name="GraXpert {}".format(version), - shortcut_dir="GraXpert", - ) -] +executables = [Executable("./graxpert/main.py", base=base, icon="./img/Icon.ico", target_name="GraXpert", shortcut_name="GraXpert {}".format(version), shortcut_dir="GraXpert")] setup( name="GraXpert", version=version, description="GraXpert is an astronomical image processing program for extracting and removing gradients in the background of your astrophotos", executables=executables, - options={ - "build_exe": build_options, - "bdist_msi": bdist_msi_options, - "bdist_rpm": bidst_rpm_options, - }, + options={"build_exe": build_options, "bdist_msi": bdist_msi_options, "bdist_rpm": bidst_rpm_options}, license="GLP-3.0", ) From e76a8b0c465c3ec060c3277f336daa3db0555a88 Mon Sep 17 00:00:00 2001 From: Steffen Hirtle Date: Fri, 29 Dec 2023 11:48:13 +0100 Subject: [PATCH 16/17] Switch from tf.keras.models.load_model to tf.saved_model.load to avoid issues with special characters such as german umlauts in file path --- graxpert/background_extraction.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graxpert/background_extraction.py b/graxpert/background_extraction.py index 124e5b2..3b3fa0c 100644 --- a/graxpert/background_extraction.py +++ b/graxpert/background_extraction.py @@ -68,7 +68,7 @@ def extract_background(in_imarray, background_points, interpolation_type, smooth if progress is not None: progress.update(8) - model = tf.keras.models.load_model(AI_dir) + model = tf.saved_model.load(AI_dir) background = np.array(model(np.expand_dims(imarray_shrink, axis=0))[0]) background = background / 0.04 * mad + median From a24ce8fb2eaab063fcea450b52d97d88381d8e3f Mon Sep 17 00:00:00 2001 From: David Schmelter Date: Fri, 29 Dec 2023 12:19:32 +0100 Subject: [PATCH 17/17] fix progressbar when no ai model has been selected --- graxpert/application/app.py | 20 +++++--------------- 1 file changed, 5 insertions(+), 15 deletions(-) diff --git a/graxpert/application/app.py b/graxpert/application/app.py index b5e6e33..0df8ba9 100644 --- a/graxpert/application/app.py +++ b/graxpert/application/app.py @@ -35,10 +35,6 @@ def initialize(self): self.images = {"Original": None, "Background": None, "Processed": None} self.display_type = "Original" - self.ai_version = None - if self.prefs.ai_version is not None: - self.ai_version = self.prefs.ai_version - self.mat_affine = np.eye(3) # state handling @@ -95,10 +91,8 @@ def on_bg_tol_changed(self, event): self.prefs.bg_tol_option = event["bg_tol_option"] def on_calculate_request(self, event=None): - eventbus.emit(AppEvents.CALCULATE_BEGIN) if self.images["Original"] is None: - eventbus.emit(AppEvents.CALCULATE_END) messagebox.showerror("Error", _("Please load your picture first.")) return @@ -106,28 +100,24 @@ def on_calculate_request(self, event=None): # Error messages if not enough points if len(background_points) == 0 and self.prefs.interpol_type_option != "AI": - eventbus.emit(AppEvents.CALCULATE_END) messagebox.showerror("Error", _("Please select background points with left click.")) return if len(background_points) < 2 and self.prefs.interpol_type_option == "Kriging": - eventbus.emit(AppEvents.CALCULATE_END) messagebox.showerror("Error", _("Please select at least 2 background points with left click for the Kriging method.")) return if len(background_points) < 16 and self.prefs.interpol_type_option == "Splines": - eventbus.emit(AppEvents.CALCULATE_END) messagebox.showerror("Error", _("Please select at least 16 background points with left click for the Splines method.")) return if self.prefs.interpol_type_option == "AI": if not self.validate_ai_installation(): return + + eventbus.emit(AppEvents.CALCULATE_BEGIN) - def callback(p): - eventbus.emit(AppEvents.CALCULATE_PROGRESS, {"progress": p}) - - progress = DynamicProgressThread(callback=callback) + progress = DynamicProgressThread(callback=lambda p: eventbus.emit(AppEvents.CALCULATE_PROGRESS, {"progress": p})) imarray = np.copy(self.images["Original"].img_array) @@ -512,7 +502,7 @@ def translate(self, offset_x, offset_y): self.mat_affine = np.dot(mat, self.mat_affine) def validate_ai_installation(self): - if self.ai_version is None or self.prefs.ai_version == "None": + if self.prefs.ai_version is None or self.prefs.ai_version == "None": messagebox.showerror("Error", _("No AI-Model selected. Please select one from the Advanced panel on the right.")) return False @@ -525,7 +515,7 @@ def validate_ai_installation(self): def callback(p): eventbus.emit(AppEvents.AI_DOWNLOAD_PROGRESS, {"progress": p}) - download_version(self.ai_version, progress=callback) + download_version(self.prefs.ai_version, progress=callback) eventbus.emit(AppEvents.AI_DOWNLOAD_END) return True