Quellcode durchsuchen

feat: add GUI

pull/106/head
Jérôme Deuchnord vor 5 Jahren
Ursprung
Commit
e6d7de82f4
Es konnte kein GPG-Schlüssel zu dieser Signatur gefunden werden GPG-Schlüssel-ID: 72F9D1A7272D53DD
24 geänderte Dateien mit 733 neuen und 24 gelöschten Zeilen
  1. +1
    -0
      .coveragerc
  2. BIN
      .github/assets/gui-screenshot.png
  3. +3
    -1
      .gitignore
  4. +2
    -2
      .scripts/tests-e2e.sh
  5. +28
    -0
      Makefile
  6. +10
    -10
      Pipfile.lock
  7. +13
    -0
      README.md
  8. +22
    -0
      build/distrib/darwin/Info.plist
  9. BIN
      build/distrib/darwin/icon.icns
  10. +7
    -0
      build/distrib/darwin/launch-kosmorro.sh
  11. +3
    -2
      config-comlipy.yml
  12. +10
    -0
      kosmorro.desktop
  13. +3
    -1
      kosmorrolib/core.py
  14. +4
    -1
      kosmorrolib/data.py
  15. +2
    -2
      kosmorrolib/dumper.py
  16. +2
    -2
      kosmorrolib/ephemerides.py
  17. +1
    -1
      kosmorrolib/events.py
  18. +2
    -2
      kosmorrolib/exceptions.py
  19. +0
    -0
      kosmorrolib/gui/__init__.py
  20. +297
    -0
      kosmorrolib/gui/mainwindow.py
  21. +241
    -0
      kosmorrolib/gui/panel.py
  22. +64
    -0
      kosmorrolib/gui/positionwindow.py
  23. +15
    -0
      kosmorrolib/main.py
  24. +3
    -0
      manpage/kosmorro.1.md

+ 1
- 0
.coveragerc Datei anzeigen

@@ -4,3 +4,4 @@ source =
kosmorrolib
omit =
test/*
kosmorrolib/gui/*

BIN
.github/assets/gui-screenshot.png Datei anzeigen

Vorher Nachher
Breite: 1624  |  Höhe: 498  |  Größe: 177 KiB

+ 3
- 1
.gitignore Datei anzeigen

@@ -1,6 +1,8 @@
__pycache__
/build
/build/*
!/build/distrib
/dist

kosmorro.egg-info
.coverage
coverage.xml


+ 2
- 2
.scripts/tests-e2e.sh Datei anzeigen

@@ -28,7 +28,7 @@ function canRun() {
return 0
}

# Asserts that command $1 has finished with sucess
# Asserts that command $1 has finished with success
# $1: the command to run
function assertSuccess() {
if ! canRun "$2"; then
@@ -48,7 +48,7 @@ function assertSuccess() {
echo -n '.'
}

# Asserts that command $1 has finished with sucess
# Asserts that command $1 has finished with failure
# $1: the command to run
function assertFailure() {
if ! canRun "$2"; then


+ 28
- 0
Makefile Datei anzeigen

@@ -6,6 +6,7 @@ test:
unset KOSMORRO_TIMEZONE; \
LANG=C pipenv run python3 -m coverage run -m unittest test

.PHONY: build
build: i18n
python3 setup.py sdist bdist_wheel

@@ -54,3 +55,30 @@ finish-release: env
@echo
@echo -e "\e[1mVersion \e[36m$$RELEASE_NUMBER\e[39m successfully tagged!"
@echo -e "Invoke \e[33mgit push origin master features v$$RELEASE_NUMBER\e[39m to finish."

distapp = dist/Kosmorro.app
distrib-mac: env
@if [ -e $(distapp) ]; then echo "Deleting the existing app."; rm -rf $(distapp); fi

mkdir -p "$(distapp)/Contents/MacOS" "$(distapp)/Contents/Resources"

# Add application files
cp "kosmorro" "$(distapp)/Contents/MacOS/kosmorro"
cp -r "kosmorrolib" "$(distapp)/Contents/MacOS/kosmorrolib"
cp "Pipfile" "$(distapp)/Contents/MacOS/Pipfile"
cp "Pipfile.lock" "$(distapp)/Contents/MacOS/Pipfile.lock"

# Install dependencies
cd $(distapp)/Contents/MacOS && PIPENV_VENV_IN_PROJECT=1 pipenv sync
cd $(distapp)/Contents/MacOS && source .venv/bin/activate && pip install wxPython

# Add Mac-specific files
cp "build/distrib/darwin/Info.plist" "$(distapp)/Contents/Info.plist"
cp "build/distrib/darwin/launch-kosmorro.sh" "$(distapp)/Contents/MacOS/launch-kosmorro"
cp "build/distrib/darwin/icon.icns" "$(distapp)/Contents/Resources/icon.icns"

sed "s/{{app_version}}/$$RELEASE_NUMBER/" "build/distrib/darwin/Info.plist" > "$(distapp)/Contents/Info.plist"

chmod +x "$(distapp)/Contents/MacOS/launch-kosmorro"

echo "Application created."

+ 10
- 10
Pipfile.lock Datei anzeigen

@@ -270,11 +270,11 @@
},
"coveralls": {
"hashes": [
"sha256:3726d35c0f93a28631a003880e2aa6cc93c401d62bc6919c5cb497217ba30c55",
"sha256:afe359cd5b350e1b3895372bda32af8f0260638c7c4a31a5c0f15aa6a96f40d9"
"sha256:4430b862baabb3cf090d36d84d331966615e4288d8a8c5957e0fd456d0dd8bd6",
"sha256:b3b60c17b03a0dee61952a91aed6f131e0b2ac8bd5da909389c53137811409e1"
],
"index": "pypi",
"version": "==2.1.1"
"version": "==2.1.2"
},
"docopt": {
"hashes": [
@@ -292,11 +292,11 @@
},
"isort": {
"hashes": [
"sha256:54da7e92468955c4fceacd0c86bd0ec997b0e1ee80d97f67c35a78b719dccab1",
"sha256:6e811fcb295968434526407adb8796944f1988c5b65e8139058f2014cbe100fd"
"sha256:60a1b97e33f61243d12647aaaa3e6cc6778f5eb9f42997650f1cc975b6008750",
"sha256:d488ba1c5a2db721669cc180180d5acf84ebdc5af7827f7aaeaa75f73cf0e2b8"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==4.3.21"
"markers": "python_version >= '3.6' and python_version < '4.0'",
"version": "==5.4.2"
},
"lazy-object-proxy": {
"hashes": [
@@ -334,11 +334,11 @@
},
"pylint": {
"hashes": [
"sha256:7dd78437f2d8d019717dbf287772d0b2dbdfd13fc016aa7faa08d67bccc46adc",
"sha256:d0ece7d223fe422088b0e8f13fa0a1e8eb745ebffcb8ed53d3e95394b6101a1c"
"sha256:bb4a908c9dadbc3aac18860550e870f58e1a02c9f2c204fdf5693d73be061210",
"sha256:bfe68f020f8a0fece830a22dd4d5dddb4ecc6137db04face4c3420a46a52239f"
],
"index": "pypi",
"version": "==2.5.3"
"version": "==2.6.0"
},
"pylintfileheader": {
"hashes": [


+ 13
- 0
README.md Datei anzeigen

@@ -77,3 +77,16 @@ Kosmorro can export the computation results to PDF files, but this feature requi
These dependencies are not installed by default, because they take a lot of place and are not necessary if you are not interested in generating PDF files.
The first time you ask Kosmorro to create a PDF, it may be a little verbose. You can ignore its blahblah.

### Opening the GUI

![Screenshot](.github/assets/gui-screenshot.png)

Kosmorro provides a graphical user interface that can be easily launched by invoking the `kosmorro --gui` command.

To run it, you will have to install the `wxpython` package:

- **On Linux**, wxPython is most likely provided by your packages manager.
- **On Mac**, install wxPython by invoking `pip3 install wxPython`.

Alternatively, you may download the DMG file on the release page (note that you won't be able to use the command
line in this case).

+ 22
- 0
build/distrib/darwin/Info.plist Datei anzeigen

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDisplayName</key>
<string>Kosmorro</string>
<key>CFBundleExecutable</key>
<string>MacOS/launch-kosmorro</string>
<key>CFBundleIconFile</key>
<string>icon.icns</string>
<key>CFBundleIdentifier</key>
<string>space.kosmorro.app</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>Kosmorro</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>{{app_version}}</string>
</dict>
</plist>

BIN
build/distrib/darwin/icon.icns Datei anzeigen


+ 7
- 0
build/distrib/darwin/launch-kosmorro.sh Datei anzeigen

@@ -0,0 +1,7 @@
#!/bin/sh

# Move to the MacOS folder
cd $(dirname "$0")

source .venv/bin/activate
.venv/bin/python kosmorro --gui

+ 3
- 2
config-comlipy.yml Datei anzeigen

@@ -6,7 +6,7 @@ rules:
applicable: always
value: 0
level: 1
header-max-length:
header-max-length:
applicable: always
value: 120
level: 2
@@ -14,4 +14,5 @@ rules:
value: lower-case
scope-empty:
level: 0

subject-case:
level: 0

+ 10
- 0
kosmorro.desktop Datei anzeigen

@@ -0,0 +1,10 @@
[Desktop Entry]
Name=Kosmorro
Comment=Ephemerides computer
Comment[fr]=Calculateur d'éphémérides
Comment[de]=Ephemeriden-Rechner
Exec=kosmorro --gui
Icon=kosmorro
Terminal=false
Categories=Astronomy;Science;
keywords=ephemerides;

+ 3
- 1
kosmorrolib/core.py Datei anzeigen

@@ -98,7 +98,9 @@ def get_date(date_arg: str) -> date:
try:
return date.fromisoformat(date_arg)
except ValueError as error:
raise ValueError(_('The date {date} is not valid: {error}').format(date=date_arg, error=error.args[0]))
raise ValueError(_('The date {date} is not valid: {error}').format(
date=date_arg, error=error.args[0]
)) from error
elif re.match(r'^([+-])(([0-9]+)y)?[ ]?(([0-9]+)m)?[ ]?(([0-9]+)d)?$', date_arg):
def get_offset(date_arg: str, signifier: str):
if re.search(r'([0-9]+)' + signifier, date_arg):


+ 4
- 1
kosmorrolib/data.py Datei anzeigen

@@ -272,7 +272,7 @@ ASTERS = [Star(_('Sun'), 'SUN', radius=696342),


class Position:
def __init__(self, latitude: float, longitude: float, aster: Object):
def __init__(self, latitude: float, longitude: float, aster: Object = EARTH):
self.latitude = latitude
self.longitude = longitude
self.aster = aster
@@ -287,3 +287,6 @@ class Position:
longitude_degrees=self.longitude)

return self._topos

def __str__(self):
return 'lat. %.3f, lon. %.3f' % (self.latitude, self.longitude)

+ 2
- 2
kosmorrolib/dumper.py Datei anzeigen

@@ -348,10 +348,10 @@ class PdfDumper(Dumper):
date=self.date, timezone=self.timezone, with_colors=self.with_colors,
show_graph=self.show_graph)
return self._compile(latex_dumper.to_string())
except RuntimeError:
except RuntimeError as error:
raise UnavailableFeatureError(_("Building PDFs was not possible, because some dependencies are not"
" installed.\nPlease look at the documentation at http://kosmorro.space "
"for more information."))
"for more information.")) from error

@staticmethod
def is_file_output_needed() -> bool:


+ 2
- 2
kosmorrolib/ephemerides.py Datei anzeigen

@@ -58,7 +58,7 @@ def get_moon_phase(compute_date: datetime.date, timezone: int = 0) -> MoonPhase:
start = datetime.date(start.year, start.month, start.day) + datetime.timedelta(days=12)
end = datetime.date(end.year, end.month, end.day) - datetime.timedelta(days=12)

raise OutOfRangeDateError(start, end)
raise OutOfRangeDateError(start, end) from error

return skyfield_to_moon_phase(times, phase, today)

@@ -119,6 +119,6 @@ def get_ephemerides(date: datetime.date, position: Position, timezone: int = 0)
start = datetime.date(start.year, start.month, start.day + 1)
end = datetime.date(end.year, end.month, end.day - 1)

raise OutOfRangeDateError(start, end)
raise OutOfRangeDateError(start, end) from error

return ephemerides

+ 1
- 1
kosmorrolib/events.py Datei anzeigen

@@ -152,4 +152,4 @@ def search_events(date: date_type, timezone: int = 0) -> [Event]:
start_date = date_type(start_date.year, start_date.month, start_date.day)
end_date = date_type(end_date.year, end_date.month, end_date.day)

raise OutOfRangeDateError(start_date, end_date)
raise OutOfRangeDateError(start_date, end_date) from error

+ 2
- 2
kosmorrolib/exceptions.py Datei anzeigen

@@ -22,13 +22,13 @@ from .i18n import _, SHORT_DATE_FORMAT

class UnavailableFeatureError(RuntimeError):
def __init__(self, msg: str):
super(UnavailableFeatureError, self).__init__()
super().__init__()
self.msg = msg


class OutOfRangeDateError(RuntimeError):
def __init__(self, min_date: date, max_date: date):
super(OutOfRangeDateError, self).__init__()
super().__init__()
self.min_date = min_date
self.max_date = max_date
self.msg = _('The date must be between {minimum_date}'


+ 0
- 0
kosmorrolib/gui/__init__.py Datei anzeigen


+ 297
- 0
kosmorrolib/gui/mainwindow.py Datei anzeigen

@@ -0,0 +1,297 @@
import wx

import platform
from typing import Union
import webbrowser
from threading import Thread
from datetime import date

from . import panel

from ..data import Position, AsterEphemerides, Event, MoonPhase
from ..exceptions import UnavailableFeatureError
from ..ephemerides import get_ephemerides, get_moon_phase
from ..events import search_events
from ..version import VERSION
from ..dumper import PdfDumper
from ..i18n import _

MIN_SIZE = wx.Size(700, 0)

# Events definitions

myEVT_EPHEMERIDES_COMPUTED = wx.NewEventType()
EVT_EPHEMERIDES_COMPUTED = wx.PyEventBinder(myEVT_EPHEMERIDES_COMPUTED, 1)


class EphemeridesComputedEvent(wx.PyCommandEvent):
def __init__(self, etype, eid, value=None):
super(EphemeridesComputedEvent, self).__init__(etype, eid)
self.value = value


myEVT_MOON_PHASE_COMPUTED = wx.NewEventType()
EVT_MOON_PHASE_COMPUTED = wx.PyEventBinder(myEVT_MOON_PHASE_COMPUTED, 1)


class MoonPhaseComputedEvent(wx.PyCommandEvent):
def __init__(self, etype, eid, value=None):
super(MoonPhaseComputedEvent, self).__init__(etype, eid)
self.value = value


myEVT_EVENTS_COMPUTED = wx.NewEventType()
EVT_EVENTS_COMPUTED = wx.PyEventBinder(myEVT_EVENTS_COMPUTED, 1)


class EventsComputedEvent(wx.PyCommandEvent):
def __init__(self, etype, eid, value=None):
super(EventsComputedEvent, self).__init__(etype, eid)
self.value = value


# Computing threads

class MoonPhaseComputer(Thread):
def __init__(self, parent, compute_date: date):
super(MoonPhaseComputer, self).__init__()
self._parent = parent
self.compute_date = compute_date
self.moon_phase = None

def run(self):
self.moon_phase = get_moon_phase(self.compute_date)
wx.PostEvent(self._parent, EphemeridesComputedEvent(myEVT_MOON_PHASE_COMPUTED, -1, self.moon_phase))


class EphemeridesComputer(Thread):
def __init__(self, parent, compute_date: date, position: Position, timezone: int):
super(EphemeridesComputer, self).__init__()
self._parent = parent
self.compute_date = compute_date
self.position = position
self.timezone = timezone
self.ephemerides = []

def run(self):
self.ephemerides = get_ephemerides(self.compute_date, self.position, self.timezone)
wx.PostEvent(self._parent, EphemeridesComputedEvent(myEVT_EPHEMERIDES_COMPUTED, -1, self.ephemerides))


class EventsComputer(Thread):
def __init__(self, parent, compute_date: date, timezone: int):
super(EventsComputer, self).__init__()
self._parent = parent
self.compute_date = compute_date
self.timezone = timezone
self.events = []

def run(self):
self.events = search_events(self.compute_date, self.timezone)
wx.PostEvent(self._parent, EventsComputedEvent(myEVT_EVENTS_COMPUTED, -1, self.events))


# Main window

class MainWindow(wx.Frame):
ephemerides: [AsterEphemerides]
events: [Event]
moon_phase: Union[None, MoonPhase]

def __init__(self):
super(MainWindow, self).__init__(None, title='Kosmorro',
style=wx.DEFAULT_FRAME_STYLE ^ wx.RESIZE_BORDER)

self.export_path = None
self.config_panel = panel.ConfigPanel(self)
self.Bind(panel.EVT_COMPUTE_BUTTON, self.compute)
self.Bind(panel.EVT_EXPORT_BUTTON, self.export)

self.result_presenter = panel.ResultPanel(self)

self.sizer = wx.BoxSizer(wx.VERTICAL)
self.sizer.AddMany([(self.config_panel, 0, wx.EXPAND | wx.ALL, 5),
(self.result_presenter, 0, wx.EXPAND | wx.ALL, 5)])

self.SetSizer(self.sizer)
self.make_menu_bar()

status_bar = self.CreateStatusBar()
status_bar.SetFieldsCount(2)
status_bar.SetStatusWidths([-2, -1])
self.progress_bar = wx.Gauge(status_bar, -1, style=wx.GA_HORIZONTAL | wx.GA_SMOOTH)
rect = status_bar.GetFieldRect(1)
self.progress_bar.SetPosition((rect.x + 2, rect.y + 2))
self.progress_bar.SetSize((rect.width - 4, rect.height - 4))
self.result_presenter.Hide()

self.SetMinSize(MIN_SIZE)

self.Fit()
self.Center()
self.progress_bar.Hide()

def resize(self, width: int, height: int, center: bool = False):
self.resize(wx.Size(width, height), center)

def resize(self, size: wx.Size, center: bool = False):
self.Size = size
if center:
self.Center()

def resize(self, center: bool = False):
self.Fit()
if center:
self.Center()

def make_menu_bar(self):
menu_bar = wx.MenuBar()

if platform.system() != 'Darwin':
# "Application" menu
app_menu = wx.Menu()
exit_item = app_menu.Append(wx.ID_EXIT)

# "Help" menu
help_menu = wx.Menu()
about_item = help_menu.Append(wx.ID_ABOUT)

menu_bar.Append(app_menu, _('&Application'))
menu_bar.Append(help_menu, _('&Help'))
self.Bind(wx.EVT_MENU, self.on_exit, exit_item)
else:
# On macOS, put the menu items to the place they belong to: the application name menu
apple_menu = menu_bar.OSXGetAppleMenu()
about_item = apple_menu.Insert(0, wx.ID_ABOUT)

self.Bind(wx.EVT_MENU, self.on_about, about_item)
self.SetMenuBar(menu_bar)

def on_exit(self, event):
self.Close(True)

def on_about(self, event):
message = wx.MessageDialog(caption='Kosmorro version %s' % VERSION,
message='© Jérôme Deuchnord - 2020\n\n' +
_('This is a free software licensed under the '
'GNU Affero General Public License.'),
style=wx.OK | wx.HELP | wx.ICON_INFORMATION,
parent=self)
message.SetOKLabel(_('Close'))
message.SetHelpLabel(_('Website'))
btn = message.ShowModal()

if btn == wx.ID_HELP:
webbrowser.open('http://kosmorro.space')

def compute(self, __=None):
# Reset data
self.moon_phase = None
self.ephemerides = None
self.events = None

self.config_panel.disable_buttons()
self.SetCursor(wx.Cursor(wx.CURSOR_WAIT))

self.SetStatusText(_('Computing…'))
self.progress_bar.SetValue(0)
self.progress_bar.Show()

thread_moon_phase = MoonPhaseComputer(self, self.config_panel.compute_date)
thread_ephemerides = EphemeridesComputer(self, self.config_panel.compute_date, self.config_panel.position,
self.config_panel.timezone)
thread_events = EventsComputer(self, self.config_panel.compute_date, self.config_panel.timezone)

self.Bind(EVT_MOON_PHASE_COMPUTED, self.on_moon_phase_computed)
self.Bind(EVT_EPHEMERIDES_COMPUTED, self.on_ephemerides_computed)
self.Bind(EVT_EVENTS_COMPUTED, self.on_events_computed)

thread_moon_phase.start()

if self.config_panel.activate_position and self.config_panel.position is not None:
thread_ephemerides.start()
else:
self.ephemerides = []
self.progress_bar.SetValue(50)

thread_events.start()

def export(self, __):
self.export_path = self.get_export_file_path()
if self.export_path is None:
self.SetStatusText('Aborted.')
return

self.compute()

def on_moon_phase_computed(self, event):
self.moon_phase = event.value
self.progress_bar.SetValue(self.progress_bar.GetValue() + 10)

if self.is_all_computed():
self.show_results()

def on_ephemerides_computed(self, event):
self.ephemerides = event.value
self.progress_bar.SetValue(self.progress_bar.GetValue() + 50)

if self.is_all_computed():
self.show_results()

def on_events_computed(self, event):
self.events = event.value
self.progress_bar.SetValue(self.progress_bar.GetValue() + 40)

if self.is_all_computed():
self.config_panel.enable_buttons()
self.SetStatusText('')
self.progress_bar.Hide()
self.SetCursor(wx.Cursor(wx.CURSOR_ARROW))

if self.export_path is not None:
ephemerides = self.ephemerides if len(self.ephemerides) > 0 else None
dumper = PdfDumper(date=self.config_panel.compute_date, ephemerides=ephemerides,
events=self.events, moon_phase=self.moon_phase, timezone=self.config_panel.timezone,
show_graph=True)

try:
with open(self.export_path, 'wb') as file:
file.write(dumper.to_string())
self.SetStatusText(_('PDF export saved in "{path}"!').format(path=self.export_path))
except UnavailableFeatureError as error:
wx.MessageDialog(self, error.msg, caption=_('Error while exporting your document'),
style=wx.OK | wx.ICON_ERROR).ShowModal()
finally:
self.export_path = None

return

self.show_results()
self.result_presenter.Show()
self.resize(center=True)

def is_all_computed(self) -> bool:
return self.moon_phase is not None and self.ephemerides is not None and self.events is not None

def show_results(self):
self.result_presenter.moon_phase = self.moon_phase
self.result_presenter.ephemerides = self.ephemerides
self.result_presenter.events = self.events
self.result_presenter.render()

def get_export_file_path(self) -> Union[None, str]:
dialog = wx.FileDialog(parent=self, message=_('Please select an export file location'),
wildcard='*.pdf', style=wx.FD_SAVE)
if dialog.ShowModal() == wx.ID_OK:
return dialog.GetPath()
else:
return None


def start() -> bool:
app = wx.App()
window = MainWindow()

window.Show()

return app.MainLoop() == 0

+ 241
- 0
kosmorrolib/gui/panel.py Datei anzeigen

@@ -0,0 +1,241 @@
import os
from typing import Union

import wx
import wx.adv
import wx.grid
import wx.richtext
import wx.lib.newevent

from .positionwindow import PositionWindow

from ..data import Position, AsterEphemerides, Event, MoonPhase
from ..i18n import _, TIME_FORMAT, FULL_DATE_FORMAT
from datetime import date

FPB_HORIZONTAL = 0x4

ComputeButtonEvent, EVT_COMPUTE_BUTTON = wx.lib.newevent.NewEvent()

ExportButtonEvent, EVT_EXPORT_BUTTON = wx.lib.newevent.NewEvent()


class ConfigPanel(wx.Panel):
activate_position: bool
position: Union[None, Position]
compute_date: date
timezone: int

def __init__(self, parent, activate_position: bool = False, position: Position = None,
compute_date: date = date.today(), timezone: int = 0):
super(ConfigPanel, self).__init__(parent)
self.activate_position = activate_position
self.position = position
self.compute_date = compute_date
self.timezone = timezone

self._position_checkbox = wx.CheckBox(self, label=_('Position:'))
self._position_change_btn = wx.Button(self)
self.update_position_btn_label()
self._position_change_btn.SetToolTip(wx.ToolTip(_('Change the position')))
self._position_checkbox.SetValue(activate_position)
self._position_change_btn.Enable(activate_position)

self.Bind(wx.EVT_CHECKBOX, self.on_position_checkbox, self._position_checkbox)
self.Bind(wx.EVT_BUTTON, self.on_position_button, self._position_change_btn)

date_lbl = wx.StaticText(self, label=_('Date:'))
self._date_picker = wx.adv.DatePickerCtrl(self, dt=wx.DateTime(compute_date),
style=wx.adv.DP_DEFAULT)
self.Bind(wx.adv.EVT_DATE_CHANGED, self.date_changed, self._date_picker)

timezone_lbl = wx.StaticText(self, label=_('Timezone:'))
self._timezone_spin = wx.SpinCtrl(self, min=-23, max=23, value=str(timezone))
self.Bind(wx.EVT_SPINCTRL, self.timezone_changed, self._timezone_spin)

sizer = wx.FlexGridSizer(2, 5, 5)
sizer.AddGrowableCol(1, 1)

self._compute_button = wx.Button(self, label=_('&Compute'))
self._export_button = wx.Button(self, label=_('E&xport PDF…'))

self.Bind(wx.EVT_BUTTON, self.compute_button_clicked, self._compute_button)
self.Bind(wx.EVT_BUTTON, self.export_button_clicked, self._export_button)

sizer.AddMany([(self._position_checkbox, 0, wx.EXPAND),
(self._position_change_btn, 0, wx.EXPAND),
(date_lbl, 0, wx.EXPAND),
(self._date_picker, 0, wx.EXPAND),
(timezone_lbl, 0, wx.EXPAND),
(self._timezone_spin, 0, wx.EXPAND)])

main_sizer = wx.FlexGridSizer(3, 5, 5)
main_sizer.AddGrowableCol(0, 2)

main_sizer.AddMany([(sizer, 0, wx.EXPAND),
(self._compute_button, 0, wx.EXPAND),
(self._export_button, 0, wx.EXPAND)])

self.SetSizer(main_sizer)

def update_position_btn_label(self):
self._position_change_btn.Label = str(self.position) if self.position is not None else _('Unknown')

def enable_buttons(self, enable: bool = True):
self._compute_button.Enable(enable)
self._export_button.Enable(enable)

def disable_buttons(self):
self.enable_buttons(False)

def on_position_checkbox(self, event):
self.activate_position = event.IsChecked()
self._position_change_btn.Enable(self.activate_position)

if self.activate_position and self.position is None:
self.on_position_button()

def on_position_button(self, _=None):
pos_win = PositionWindow(self, self.position)
pos_win.ShowModal()
self.position = pos_win.position
if self.position is not None:
self.update_position_btn_label()
else:
self._position_checkbox.SetValue(False)
self._position_change_btn.Disable()

def date_changed(self, _):
self.compute_date = date.fromisoformat(self._date_picker.GetValue().FormatISODate())

def timezone_changed(self, _):
self.timezone = self._timezone_spin.GetValue()

def compute_button_clicked(self, _):
wx.PostEvent(self.GetEventHandler(), wx.PyCommandEvent(EVT_COMPUTE_BUTTON.typeId, self.GetId()))

def export_button_clicked(self, _):
wx.PostEvent(self.GetEventHandler(), wx.PyCommandEvent(EVT_EXPORT_BUTTON.typeId, self.GetId()))


class MoonPhasePanel(wx.Panel):
def __init__(self, parent):
super(MoonPhasePanel, self).__init__(parent)

self.moon_bitmap = wx.StaticBitmap(self, size=wx.Size(100, 100))

self.moon_txt_l1 = wx.StaticText(self)
self.moon_txt_l2 = wx.StaticText(self)

self.moon_txt_l1.SetFont(wx.Font(wx.DEFAULT, wx.DEFAULT, wx.NORMAL, wx.BOLD))

label_sizer = wx.GridSizer(3, 1, 0, 0)
label_sizer.AddMany([(wx.StaticText(self), 0, wx.ALIGN_LEFT),
(self.moon_txt_l1, 0, wx.ALIGN_LEFT | wx.EXPAND),
(self.moon_txt_l2, 0, wx.ALIGN_LEFT | wx.EXPAND)])

main_sizer = wx.FlexGridSizer(4, 0, 10)
main_sizer.AddGrowableCol(0)
main_sizer.AddGrowableCol(2)
main_sizer.AddMany([(wx.StaticText(self), 0, wx.ALIGN_LEFT),
(self.moon_bitmap, 0, wx.ALIGN_RIGHT),
(label_sizer, 0, wx.ALIGN_LEFT)])

self.SetSizer(main_sizer)

def set_moon_phase(self, moon_phase: MoonPhase):
moon_img = wx.Image(os.path.join(os.path.abspath(os.path.dirname(__file__)),
'..', 'assets', 'moonphases', 'png',
'.'.join([moon_phase.identifier.lower().replace('_', '-'),
'png'])))
moon_img = moon_img.Scale(100, 100)
bitmap = moon_img.ConvertToBitmap()
self.moon_bitmap.SetBitmap(bitmap)

self.moon_txt_l1.SetLabel(moon_phase.get_phase())
self.moon_txt_l2.SetLabel(_('{next_moon_phase} on {next_moon_phase_date} at {next_moon_phase_time}').format(
next_moon_phase=moon_phase.get_next_phase_name(),
next_moon_phase_date=moon_phase.next_phase_date.strftime(FULL_DATE_FORMAT),
next_moon_phase_time=moon_phase.next_phase_date.strftime(TIME_FORMAT)
))

self.Layout()
self.Fit()


class ResultPanel(wx.Panel):
ephemerides: [AsterEphemerides]
events: [Event]
moon_phase: Union[None, MoonPhase]

def __init__(self, parent):
super(ResultPanel, self).__init__(parent)
self.moon_phase = None
self.ephemerides = None
self.events = None

self._moon_phase_panel = MoonPhasePanel(self)

self._grid_ephemerides = wx.grid.Grid(self)
self._grid_ephemerides.SetDefaultCellAlignment(wx.ALIGN_CENTRE, wx.ALIGN_CENTRE)
self._grid_ephemerides.CreateGrid(numRows=0, numCols=3)
self._grid_ephemerides.EnableEditing(False)
self._grid_ephemerides.SetColLabelValue(0, _('Rise time'))
self._grid_ephemerides.SetColLabelValue(1, _('Culmination time'))
self._grid_ephemerides.SetColLabelValue(2, _('Set time'))
self._grid_ephemerides.EnableEditing(False)

self._list_events = wx.richtext.RichTextCtrl(self, size=wx.Size(0, 300),
style=wx.richtext.RE_READONLY | wx.richtext.RE_MULTILINE)

main_sizer = wx.FlexGridSizer(2, 1, 5, 5)
main_sizer.AddGrowableCol(0)
main_sizer.AddGrowableRow(1, 2)

sizer = wx.FlexGridSizer(2, 5, 5)
sizer.AddGrowableCol(0, 0)
sizer.AddGrowableCol(1, 1)
sizer.AddMany([(self._grid_ephemerides, 0, wx.EXPAND | wx.ALL),
(self._list_events, 0, wx.EXPAND | wx.ALL)])

main_sizer.AddMany([(self._moon_phase_panel, 0, wx.EXPAND | wx.ALL, 5),
(sizer, 0, wx.EXPAND | wx.ALL)])
self.SetSizer(main_sizer)

def render(self):
if self.moon_phase is not None:
self._moon_phase_panel.set_moon_phase(self.moon_phase)

if self._grid_ephemerides.NumberRows > 0:
self._grid_ephemerides.DeleteRows(numRows=self._grid_ephemerides.NumberRows)

if self.ephemerides is not None and len(self.ephemerides) > 0:
for ephemeris in self.ephemerides:
rise_time = ephemeris.rise_time.strftime(TIME_FORMAT) if ephemeris.rise_time is not None else ''
culmination_time = ephemeris.culmination_time.strftime(TIME_FORMAT) if ephemeris.culmination_time is not None else ''
set_time = ephemeris.set_time.strftime(TIME_FORMAT) if ephemeris.set_time is not None else ''

self._grid_ephemerides.AppendRows()
row = self._grid_ephemerides.NumberRows - 1
self._grid_ephemerides.SetRowLabelValue(row, ephemeris.object.name)
self._grid_ephemerides.SetCellValue(row, 0, rise_time)
self._grid_ephemerides.SetCellValue(row, 1, culmination_time)
self._grid_ephemerides.SetCellValue(row, 2, set_time)

self._grid_ephemerides.Show(len(self.ephemerides) > 0)
self._grid_ephemerides.AutoSize()

if self.events is not None and len(self.events) > 0:
content = ''
for event in self.events:
content += '\n' if content != '' else ''
content += _('- {event_time}: {event_description}').format(
event_time=event.start_time.strftime(TIME_FORMAT),
event_description=event.get_description()
)

self._list_events.SetValue(content)
else:
self._list_events.SetValue(_('No events for this day.'))

self.Layout()

+ 64
- 0
kosmorrolib/gui/positionwindow.py Datei anzeigen

@@ -0,0 +1,64 @@
import wx

from ..data import Position
from ..i18n import _


class PositionWindow(wx.Dialog):
position: Position

def __init__(self, parent, position: Position = None):
super(PositionWindow, self).__init__(parent, title=_('Set position'), style=wx.DEFAULT_DIALOG_STYLE ^ wx.CLOSE_BOX)
self.position = position

latitude_lbl = wx.StaticText(self, label=_('Latitude:'))
self._latitude_input = wx.SpinCtrlDouble(self, initial=position.latitude if position is not None else 0,
min=-90, max=90)
self._latitude_input.SetDigits(4)

longitude_lbl = wx.StaticText(self, label=_('Longitude:'))
self._longitude_input = wx.SpinCtrlDouble(self, initial=position.longitude if position is not None else 0,
min=-180, max=180)
self._longitude_input.SetDigits(4)

ok_button = wx.Button(self, label=_('OK'))
cancel_button = wx.Button(self, label=_('Cancel'))

self.Bind(wx.EVT_BUTTON, self.on_ok, ok_button)
self.Bind(wx.EVT_BUTTON, self.on_cancel, cancel_button)

btn_sizer = wx.GridSizer(1, 2, 5, 5)
btn_sizer.AddMany([(ok_button, 0, wx.EXPAND),
(cancel_button, 0, wx.EXPAND)])

sizer = wx.FlexGridSizer(2, 5, 5)
sizer.AddGrowableCol(0, 2)
sizer.AddMany([(latitude_lbl, 0, wx.EXPAND | wx.ALL, 5),
(self._latitude_input, 0, wx.EXPAND | wx.ALL, 5),
(longitude_lbl, 0, wx.EXPAND | wx.ALL, 5),
(self._longitude_input, 0, wx.EXPAND | wx.ALL, 5),
(wx.StaticText(self), 0, wx.EXPAND | wx.ALL, 5),
(btn_sizer, 0, wx.EXPAND | wx.ALL, 5)])

self.SetSizer(sizer)
self.Fit()
self.Center()

self.Bind(wx.EVT_CHAR_HOOK, self.on_key_down)

def on_cancel(self, _=None):
self.Close()

def on_ok(self, _=None):
self.position = Position(self._latitude_input.GetValue(), self._longitude_input.GetValue())
self.Close()

def on_key_down(self, event):
key = event.GetKeyCode()

if key == wx.WXK_ESCAPE:
self.on_cancel()
elif key in [wx.WXK_RETURN, wx.WXK_NUMPAD_ENTER]:
self.on_ok()
else:
event.Skip()

+ 15
- 0
kosmorrolib/main.py Datei anzeigen

@@ -34,6 +34,11 @@ from .i18n import _
from . import ephemerides
from .version import VERSION

try:
from .gui import mainwindow
except ModuleNotFoundError:
mainwindow = None


def main():
environment = core.get_env()
@@ -150,6 +155,14 @@ def clear_cache() -> bool:
return True


def open_gui() -> bool:
if mainwindow is None:
print("Starting Kosmorro's GUI requires you to install wxPython.")
return False

return mainwindow.start()


def get_args(output_formats: [str]):
today = date.today()

@@ -161,6 +174,8 @@ def get_args(output_formats: [str]):

parser.add_argument('--version', '-v', dest='special_action', action='store_const', const=output_version,
default=None, help=_('Show the program version'))
parser.add_argument('--gui', '-g', dest='special_action', action='store_const', const=open_gui,
default=None, help=_("Open Kosmorro's GUI"))
parser.add_argument('--clear-cache', dest='special_action', action='store_const', const=clear_cache, default=None,
help=_('Delete all the files Kosmorro stored in the cache.'))
parser.add_argument('--format', '-f', type=str, default=output_formats[0], choices=output_formats,


+ 3
- 0
manpage/kosmorro.1.md Datei anzeigen

@@ -39,6 +39,9 @@

`--no-graph`
present the ephemerides in a table instead of a graph; PDF output format only
`--gui`, `-g`
open the graphical user interface

## ENVIRONMENT VARIABLES



Laden…
Abbrechen
Speichern