diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml index f3ab93a..0876a58 100644 --- a/.github/workflows/build-release.yml +++ b/.github/workflows/build-release.yml @@ -9,11 +9,11 @@ env: on: push: tags: - - v*.*.* + - "*.*.*" jobs: - build-linux: + build-linux-deb: runs-on: ubuntu-20.04 steps: - name: setup python @@ -30,21 +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 + 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: @@ -56,12 +91,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 +100,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 +140,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 @@ -151,16 +166,20 @@ 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: - name: GraXpert-win64.exe + name: GraXpert-${{github.ref_name}}-win64.msi - name: download macos artifacts uses: actions/download-artifact@v2 with: @@ -169,6 +188,7 @@ jobs: uses: softprops/action-gh-release@v1 with: files: | - GraXpert-linux - GraXpert-win64.exe + graxpert_${{github.ref_name}}-1_amd64.deb + GraXpert-linux.zip + GraXpert-${{github.ref_name}}-win64.msi GraXpert-macos-x86_64.dmg 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-linux.spec b/GraXpert-linux.spec deleted file mode 100644 index ba7df39..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/'), ('./forest-dark.tcl', './'), ('./forest-dark/*', './forest-dark/')], - 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/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 9e6cc8e..0000000 Binary files a/forest-dark/border-accent-hover.png and /dev/null differ diff --git a/forest-dark/border-accent.png b/forest-dark/border-accent.png deleted file mode 100644 index 1f7fc27..0000000 Binary files a/forest-dark/border-accent.png and /dev/null differ diff --git a/forest-dark/border-basic.png b/forest-dark/border-basic.png deleted file mode 100644 index a483271..0000000 Binary files a/forest-dark/border-basic.png and /dev/null differ diff --git a/forest-dark/border-hover.png b/forest-dark/border-hover.png deleted file mode 100644 index dcd837a..0000000 Binary files a/forest-dark/border-hover.png and /dev/null differ diff --git a/forest-dark/border-invalid.png b/forest-dark/border-invalid.png deleted file mode 100644 index 63cdd6e..0000000 Binary files a/forest-dark/border-invalid.png and /dev/null differ diff --git a/forest-dark/card.png b/forest-dark/card.png deleted file mode 100644 index 3ac8413..0000000 Binary files a/forest-dark/card.png and /dev/null differ diff --git a/forest-dark/check-accent.png b/forest-dark/check-accent.png deleted file mode 100644 index 81f4a62..0000000 Binary files a/forest-dark/check-accent.png and /dev/null differ diff --git a/forest-dark/check-basic.png b/forest-dark/check-basic.png deleted file mode 100644 index dd93bbc..0000000 Binary files a/forest-dark/check-basic.png and /dev/null differ diff --git a/forest-dark/check-hover.png b/forest-dark/check-hover.png deleted file mode 100644 index 6a90056..0000000 Binary files a/forest-dark/check-hover.png and /dev/null differ diff --git a/forest-dark/check-tri-accent.png b/forest-dark/check-tri-accent.png deleted file mode 100644 index 4a49300..0000000 Binary files a/forest-dark/check-tri-accent.png and /dev/null differ diff --git a/forest-dark/check-tri-basic.png b/forest-dark/check-tri-basic.png deleted file mode 100644 index 219b92d..0000000 Binary files a/forest-dark/check-tri-basic.png and /dev/null differ diff --git a/forest-dark/check-tri-hover.png b/forest-dark/check-tri-hover.png deleted file mode 100644 index ee9d108..0000000 Binary files a/forest-dark/check-tri-hover.png and /dev/null differ diff --git a/forest-dark/check-unsel-accent.png b/forest-dark/check-unsel-accent.png deleted file mode 100644 index abbdeb8..0000000 Binary files a/forest-dark/check-unsel-accent.png and /dev/null differ diff --git a/forest-dark/check-unsel-basic.png b/forest-dark/check-unsel-basic.png deleted file mode 100644 index a483271..0000000 Binary files a/forest-dark/check-unsel-basic.png and /dev/null differ diff --git a/forest-dark/check-unsel-hover.png b/forest-dark/check-unsel-hover.png deleted file mode 100644 index da35159..0000000 Binary files a/forest-dark/check-unsel-hover.png and /dev/null differ diff --git a/forest-dark/check-unsel-pressed.png b/forest-dark/check-unsel-pressed.png deleted file mode 100644 index d7a8825..0000000 Binary files a/forest-dark/check-unsel-pressed.png and /dev/null differ diff --git a/forest-dark/combo-button-basic.png b/forest-dark/combo-button-basic.png deleted file mode 100644 index 7582f0e..0000000 Binary files a/forest-dark/combo-button-basic.png and /dev/null differ diff --git a/forest-dark/combo-button-focus.png b/forest-dark/combo-button-focus.png deleted file mode 100644 index 50dba42..0000000 Binary files a/forest-dark/combo-button-focus.png and /dev/null differ diff --git a/forest-dark/combo-button-hover.png b/forest-dark/combo-button-hover.png deleted file mode 100644 index 555d685..0000000 Binary files a/forest-dark/combo-button-hover.png and /dev/null differ diff --git a/forest-dark/down.png b/forest-dark/down.png deleted file mode 100644 index 8dbdd89..0000000 Binary files a/forest-dark/down.png and /dev/null differ diff --git a/forest-dark/empty.png b/forest-dark/empty.png deleted file mode 100644 index 202e3de..0000000 Binary files a/forest-dark/empty.png and /dev/null differ diff --git a/forest-dark/hor-accent.png b/forest-dark/hor-accent.png deleted file mode 100644 index b471f4b..0000000 Binary files a/forest-dark/hor-accent.png and /dev/null differ diff --git a/forest-dark/hor-basic.png b/forest-dark/hor-basic.png deleted file mode 100644 index 9a73a59..0000000 Binary files a/forest-dark/hor-basic.png and /dev/null differ diff --git a/forest-dark/hor-hover.png b/forest-dark/hor-hover.png deleted file mode 100644 index 2f8b196..0000000 Binary files a/forest-dark/hor-hover.png and /dev/null differ diff --git a/forest-dark/notebook.png b/forest-dark/notebook.png deleted file mode 100644 index edffcb5..0000000 Binary files a/forest-dark/notebook.png and /dev/null differ diff --git a/forest-dark/off-accent.png b/forest-dark/off-accent.png deleted file mode 100644 index 8940a8c..0000000 Binary files a/forest-dark/off-accent.png and /dev/null differ diff --git a/forest-dark/off-basic.png b/forest-dark/off-basic.png deleted file mode 100644 index 43dd748..0000000 Binary files a/forest-dark/off-basic.png and /dev/null differ diff --git a/forest-dark/off-hover.png b/forest-dark/off-hover.png deleted file mode 100644 index 3d5f8a2..0000000 Binary files a/forest-dark/off-hover.png and /dev/null differ diff --git a/forest-dark/on-accent.png b/forest-dark/on-accent.png deleted file mode 100644 index 1405001..0000000 Binary files a/forest-dark/on-accent.png and /dev/null differ diff --git a/forest-dark/on-basic.png b/forest-dark/on-basic.png deleted file mode 100644 index 8db29a9..0000000 Binary files a/forest-dark/on-basic.png and /dev/null differ diff --git a/forest-dark/on-hover.png b/forest-dark/on-hover.png deleted file mode 100644 index 7ad670d..0000000 Binary files a/forest-dark/on-hover.png and /dev/null differ diff --git a/forest-dark/radio-accent.png b/forest-dark/radio-accent.png deleted file mode 100644 index 099e148..0000000 Binary files a/forest-dark/radio-accent.png and /dev/null differ diff --git a/forest-dark/radio-basic.png b/forest-dark/radio-basic.png deleted file mode 100644 index 6b745d1..0000000 Binary files a/forest-dark/radio-basic.png and /dev/null differ diff --git a/forest-dark/radio-hover.png b/forest-dark/radio-hover.png deleted file mode 100644 index e2fa366..0000000 Binary files a/forest-dark/radio-hover.png and /dev/null differ diff --git a/forest-dark/radio-tri-accent.png b/forest-dark/radio-tri-accent.png deleted file mode 100644 index 756ff13..0000000 Binary files a/forest-dark/radio-tri-accent.png and /dev/null differ diff --git a/forest-dark/radio-tri-basic.png b/forest-dark/radio-tri-basic.png deleted file mode 100644 index 0f20b21..0000000 Binary files a/forest-dark/radio-tri-basic.png and /dev/null differ diff --git a/forest-dark/radio-tri-hover.png b/forest-dark/radio-tri-hover.png deleted file mode 100644 index 2af0b72..0000000 Binary files a/forest-dark/radio-tri-hover.png and /dev/null differ diff --git a/forest-dark/radio-unsel-accent.png b/forest-dark/radio-unsel-accent.png deleted file mode 100644 index b8a2f95..0000000 Binary files a/forest-dark/radio-unsel-accent.png and /dev/null differ diff --git a/forest-dark/radio-unsel-basic.png b/forest-dark/radio-unsel-basic.png deleted file mode 100644 index bd8a723..0000000 Binary files a/forest-dark/radio-unsel-basic.png and /dev/null differ diff --git a/forest-dark/radio-unsel-hover.png b/forest-dark/radio-unsel-hover.png deleted file mode 100644 index 6512106..0000000 Binary files a/forest-dark/radio-unsel-hover.png and /dev/null differ diff --git a/forest-dark/radio-unsel-pressed.png b/forest-dark/radio-unsel-pressed.png deleted file mode 100644 index 493f02d..0000000 Binary files a/forest-dark/radio-unsel-pressed.png and /dev/null differ diff --git a/forest-dark/rect-accent-hover.png b/forest-dark/rect-accent-hover.png deleted file mode 100644 index d7a8825..0000000 Binary files a/forest-dark/rect-accent-hover.png and /dev/null differ diff --git a/forest-dark/rect-accent.png b/forest-dark/rect-accent.png deleted file mode 100644 index bebf948..0000000 Binary files a/forest-dark/rect-accent.png and /dev/null differ diff --git a/forest-dark/rect-basic.png b/forest-dark/rect-basic.png deleted file mode 100644 index c6474d5..0000000 Binary files a/forest-dark/rect-basic.png and /dev/null differ diff --git a/forest-dark/rect-hover.png b/forest-dark/rect-hover.png deleted file mode 100644 index b669407..0000000 Binary files a/forest-dark/rect-hover.png and /dev/null differ diff --git a/forest-dark/right.png b/forest-dark/right.png deleted file mode 100644 index 336945c..0000000 Binary files a/forest-dark/right.png and /dev/null differ diff --git a/forest-dark/scale-hor.png b/forest-dark/scale-hor.png deleted file mode 100644 index 675e89f..0000000 Binary files a/forest-dark/scale-hor.png and /dev/null differ diff --git a/forest-dark/scale-vert.png b/forest-dark/scale-vert.png deleted file mode 100644 index f268b60..0000000 Binary files a/forest-dark/scale-vert.png and /dev/null differ diff --git a/forest-dark/separator.png b/forest-dark/separator.png deleted file mode 100644 index 2b2a001..0000000 Binary files a/forest-dark/separator.png and /dev/null differ diff --git a/forest-dark/sizegrip.png b/forest-dark/sizegrip.png deleted file mode 100644 index 5bfc967..0000000 Binary files a/forest-dark/sizegrip.png and /dev/null differ diff --git a/forest-dark/spin-button-down-basic.png b/forest-dark/spin-button-down-basic.png deleted file mode 100644 index f4e0890..0000000 Binary files a/forest-dark/spin-button-down-basic.png and /dev/null differ diff --git a/forest-dark/spin-button-down-focus.png b/forest-dark/spin-button-down-focus.png deleted file mode 100644 index 9421a2c..0000000 Binary files a/forest-dark/spin-button-down-focus.png and /dev/null differ diff --git a/forest-dark/spin-button-up.png b/forest-dark/spin-button-up.png deleted file mode 100644 index 6e87841..0000000 Binary files a/forest-dark/spin-button-up.png and /dev/null differ diff --git a/forest-dark/tab-accent.png b/forest-dark/tab-accent.png deleted file mode 100644 index ffdf1a0..0000000 Binary files a/forest-dark/tab-accent.png and /dev/null differ diff --git a/forest-dark/tab-basic.png b/forest-dark/tab-basic.png deleted file mode 100644 index 14d222c..0000000 Binary files a/forest-dark/tab-basic.png and /dev/null differ diff --git a/forest-dark/tab-hover.png b/forest-dark/tab-hover.png deleted file mode 100644 index 31e74d2..0000000 Binary files a/forest-dark/tab-hover.png and /dev/null differ diff --git a/forest-dark/thumb-hor-accent.png b/forest-dark/thumb-hor-accent.png deleted file mode 100644 index a94f1b7..0000000 Binary files a/forest-dark/thumb-hor-accent.png and /dev/null differ diff --git a/forest-dark/thumb-hor-basic.png b/forest-dark/thumb-hor-basic.png deleted file mode 100644 index ea644b2..0000000 Binary files a/forest-dark/thumb-hor-basic.png and /dev/null differ diff --git a/forest-dark/thumb-hor-hover.png b/forest-dark/thumb-hor-hover.png deleted file mode 100644 index ab03faf..0000000 Binary files a/forest-dark/thumb-hor-hover.png and /dev/null differ diff --git a/forest-dark/thumb-vert-accent.png b/forest-dark/thumb-vert-accent.png deleted file mode 100644 index 3db6b23..0000000 Binary files a/forest-dark/thumb-vert-accent.png and /dev/null differ diff --git a/forest-dark/thumb-vert-basic.png b/forest-dark/thumb-vert-basic.png deleted file mode 100644 index b1a5587..0000000 Binary files a/forest-dark/thumb-vert-basic.png and /dev/null differ diff --git a/forest-dark/thumb-vert-hover.png b/forest-dark/thumb-vert-hover.png deleted file mode 100644 index 6137ff1..0000000 Binary files a/forest-dark/thumb-vert-hover.png and /dev/null differ diff --git a/forest-dark/tree-basic.png b/forest-dark/tree-basic.png deleted file mode 100644 index 06e9b18..0000000 Binary files a/forest-dark/tree-basic.png and /dev/null differ diff --git a/forest-dark/tree-pressed.png b/forest-dark/tree-pressed.png deleted file mode 100644 index 728c69a..0000000 Binary files a/forest-dark/tree-pressed.png and /dev/null differ diff --git a/forest-dark/up.png b/forest-dark/up.png deleted file mode 100644 index b02eda4..0000000 Binary files a/forest-dark/up.png and /dev/null differ diff --git a/forest-dark/vert-accent.png b/forest-dark/vert-accent.png deleted file mode 100644 index 04dd743..0000000 Binary files a/forest-dark/vert-accent.png and /dev/null differ diff --git a/forest-dark/vert-basic.png b/forest-dark/vert-basic.png deleted file mode 100644 index f82cae6..0000000 Binary files a/forest-dark/vert-basic.png and /dev/null differ diff --git a/forest-dark/vert-hover.png b/forest-dark/vert-hover.png deleted file mode 100644 index dd4d1b7..0000000 Binary files a/forest-dark/vert-hover.png and /dev/null differ 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..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() @@ -79,9 +79,10 @@ 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 + prefs.ai_version = ai_version save_preferences(prefs_filename, prefs) return 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/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 new file mode 100644 index 0000000..b5e6e33 --- /dev/null +++ b/graxpert/application/app.py @@ -0,0 +1,533 @@ +import logging +import os +import tkinter as tk +from tkinter import messagebox + +import numpy as np +from appdirs import user_config_dir + +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) + 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"] + + 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.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) + + # 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_scaling_changed(self, event): + self.prefs.scaling = event["scaling"] + + 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..fb0c77f --- /dev/null +++ b/graxpert/application/app_events.py @@ -0,0 +1,65 @@ +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() + SCALING_CHANGED = auto() diff --git a/graxpert/application/eventbus.py b/graxpert/application/eventbus.py new file mode 100644 index 0000000..6a55c6d --- /dev/null +++ b/graxpert/application/eventbus.py @@ -0,0 +1,23 @@ +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): + listeners = self.listeners.get(event_name, []) + for listener in listeners: + listener(event) + + +eventbus = EventBus() diff --git a/graxpert/astroimage.py b/graxpert/astroimage.py index b505312..1e07509 100644 --- a/graxpert/astroimage.py +++ b/graxpert/astroimage.py @@ -1,17 +1,21 @@ -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 skimage import exposure, img_as_float32, io +from skimage.util import img_as_uint +from xisf import XISF + +from graxpert.app_state import AppState +from graxpert.preferences import Prefs, app_state_2_fitsheader from graxpert.stretch import stretch -from graxpert.preferences import app_state_2_fitsheader + 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 +23,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): + + 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: 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) - - + 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"] - + 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/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/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/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/gui.py b/graxpert/gui.py deleted file mode 100644 index f9c942e..0000000 --- a/graxpert/gui.py +++ /dev/null @@ -1,1238 +0,0 @@ -from graxpert.mp_logging import initialize_logging, shutdown_logging, logfile_name - -import importlib -import logging -import os -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 -from skimage import io -from skimage.transform import resize - -import graxpert.background_extraction as background_extraction -import graxpert.tooltip as tooltip -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.help_panel import Help_Panel -from graxpert.loadingframe import LoadingFrame, DynamicProgressFrame, DynamicProgressThread -from graxpert.localization import _ -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.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) - - -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=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=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=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=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=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) - -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)) - -root.tk.call("source", 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 aa96a19..0000000 --- a/graxpert/help_panel.py +++ /dev/null @@ -1,409 +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.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): - - - 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=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=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=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=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=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/localization.py b/graxpert/localization.py index 7f6f558..56875a0 100644 --- a/graxpert/localization.py +++ b/graxpert/localization.py @@ -1,22 +1,17 @@ import gettext -import sys import locale import os -from appdirs import user_config_dir -from graxpert.preferences import load_preferences - -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__), "../")) +from appdirs import user_config_dir - return os.path.join(base_path, relative_path) +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" @@ -26,18 +21,16 @@ def resource_path(relative_path): 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 8549bc8..0bd754b 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,99 @@ def version_type(arg_value, pat=re.compile(r"^\d+\.\d+\.\d+$")): return arg_value +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 + 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 + 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 release, version + + def on_closing(root, logging_thread): + 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) + try: + 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) + root.destroy() + 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.warning("Could not check for newest version") + + logging_thread = initialize_logging() + check_for_new_version() + + style() + root = CTk() + try: + root.state("zoomed") + except: + root.geometry("1024x768") + root.state("normal") + 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 +163,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 +172,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..9cba9b1 100644 --- a/graxpert/preferences.py +++ b/graxpert/preferences.py @@ -1,140 +1,69 @@ import json import logging import os -import sys 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 - bg_tol_option: float - interpol_type_option: AnyStr - smoothing_option: float - saveas_option: AnyStr - sample_size: int - sample_color: int - RBF_kernel: AnyStr - 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, - "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.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() +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 "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) - prefs = merge_json(prefs, json_prefs) + 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: @@ -150,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"] - - 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"]) - +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["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/resource_utils.py b/graxpert/resource_utils.py new file mode 100644 index 0000000..f222f9f --- /dev/null +++ b/graxpert/resource_utils.py @@ -0,0 +1,14 @@ +# from appdirs import +import os +from tempfile import TemporaryDirectory + +temp_resource_dir = TemporaryDirectory() + + +def resource_path(relative_path): + base_path = os.path.join(os.path.dirname(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) 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..1e3bdea --- /dev/null +++ b/graxpert/ui/application_frame.py @@ -0,0 +1,113 @@ +import tkinter as tk + +from customtkinter import CTkFrame + +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..86c6410 --- /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 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") + self.canvas.after(5000, lambda: self.canvas.delete("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..78d5a2f --- /dev/null +++ b/graxpert/ui/left_menu.py @@ -0,0 +1,236 @@ +import tkinter as tk + +from customtkinter import StringVar, ThemeManager + +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, + GraXpertScrollableFrame, + 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(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(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(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(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(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(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(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(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(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(GraXpertScrollableFrame): + 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 d7d31ad..f067d3a 100644 --- a/graxpert/loadingframe.py +++ b/graxpert/ui/loadingframe.py @@ -4,66 +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 resource_path -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__), "../")) +class LoadingFrame(CTkFrame): + def __init__(self, parent, **kwargs): + super().__init__(parent, **kwargs) + self.create_children() + self.setup_layout() + self.place_children() - return path.join(base_path, relative_path) - - -class LoadingFrame: - def __init__(self, widget, toplevel): - font = ("Verdana", 20, "normal") - - self.toplevel = toplevel - hourglass_pic = ImageTk.PhotoImage( - file=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) - def end(self): - self.text.pack_forget() - self.toplevel.update() +class DynamicProgressFrame(CTkFrame): + def __init__(self, parent, label_lext=_("Progress:"), **kwargs): + super().__init__(parent, **kwargs) -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( + self.text = StringVar(self, value=label_lext) + self.variable = DoubleVar(self, value=0.0) + + self.create_children() + self.setup_layout() + self.place_children() + + 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() @@ -71,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() @@ -108,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..a70495c --- /dev/null +++ b/graxpert/ui/right_menu.py @@ -0,0 +1,193 @@ +import tkinter as tk +from tkinter import messagebox + +import customtkinter as ctk +from customtkinter import CTkFont, CTkImage, CTkLabel, CTkTextbox +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, GraXpertScrollableFrame, 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(GraXpertScrollableFrame): + 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(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(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(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(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(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() + 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(graxpert.prefs.scaling) + self.scaling.trace_add("write", self.on_scaling_change) + + # 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 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()})) + + 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) + + # 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..32b4c84 --- /dev/null +++ b/graxpert/ui/statusbar.py @@ -0,0 +1,59 @@ +import tkinter as tk + +from customtkinter import CTkFrame, CTkLabel + +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..b45128e --- /dev/null +++ b/graxpert/ui/styling.py @@ -0,0 +1,17 @@ +import os +import shutil + +import customtkinter + +from graxpert.resource_utils import resource_path, temp_resource_path +from graxpert.ui_scaling import get_scaling_factor + + +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/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..ac1dc7c --- /dev/null +++ b/graxpert/ui/widgets.py @@ -0,0 +1,272 @@ +import tkinter as tk + +from customtkinter import CTkButton, CTkCheckBox, CTkEntry, CTkFrame, CTkImage, CTkLabel, CTkOptionMenu, CTkScrollableFrame, CTkSlider, DoubleVar, StringVar, ThemeManager +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, anchor=tk.CENTER, **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 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) + 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..a5a97d2 100644 --- a/graxpert/ui_scaling.py +++ b/graxpert/ui_scaling.py @@ -1,49 +1,13 @@ -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 - - try: - monitors = get_monitors() + prefs_filename = os.path.join(user_config_dir(appname="GraXpert"), "preferences.json") + prefs = load_preferences(prefs_filename) + 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/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") 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/requirements.txt b/requirements.txt index 64c9b40..3ca5dc3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,14 +1,13 @@ appdirs astropy -hdpitkinter +customtkinter minio ml_dtypes -numpy +numpy<=1.24.3,>=1.22 Pillow pykrige requests scikit-image == 0.21.0 scipy -screeninfo tensorflow == 2.13.1 xisf \ No newline at end of file diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..166ca25 --- /dev/null +++ b/setup.py @@ -0,0 +1,56 @@ +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")] + +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")], +} + +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", +} + +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"], + "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"], + ], + "excludes": [], + "include_msvcr": True, +} + +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")] + +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}, + license="GLP-3.0", +)