Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Log #14

Merged
merged 8 commits into from
Jan 11, 2022
Merged

Log #14

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ __pycache__/
/resources/sounds/*
!/resources/sounds/.gitkeep

/logs/

continue
continue-backup
ttfh.ini
Expand Down
20 changes: 19 additions & 1 deletion definitions.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,26 @@
"""
Some useful constants
Some useful constants and logging config
"""

import logging.handlers
import os

# global constants

ROOT_DIR = os.path.dirname(os.path.abspath(__file__))
INI_FILE = os.path.join(os.path.dirname(__file__), 'ttfh.ini')

# logging

if not os.path.exists('./logs'):
os.mkdir('./logs')

file_handler = logging.handlers.RotatingFileHandler(
filename='./logs/ttfh.log',
maxBytes=1024 * 1000 * 4,
backupCount=10,
encoding='UTF-8')

file_handler.setFormatter(logging.Formatter('%(asctime)s - %(name)-28s - %(levelname)-8s - %(message)s'))

logging.basicConfig(handlers=[file_handler], force=True, level=logging.INFO)
15 changes: 11 additions & 4 deletions entrypoint.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,29 @@
from __future__ import annotations

import argparse
import logging
import sys

from src.graphics.window import Window
from src.timer import Clock

LOG = logging.getLogger(__name__)


def main(day: int, hour: int, minute: int, saves: str):
"""
If the parameters are valid, it starts the clock.
"""
if 1 <= args.day <= 3 and 0 <= args.hour <= 23 and 0 <= args.minute <= 59:
if 1 <= day <= 3 and 0 <= hour <= 23 and 0 <= minute <= 59:
LOG.info('Starting parameters: day %d, hour %d, minute %d', day, hour, minute)
if saves:
LOG.info('Starting savestate string: %s', saves)

timer = Clock(day, hour, minute)
window = Window(timer, saves)
window.draw()
window = Window(timer)
window.create(saves)
else:
print('Starting time out of bounds')
LOG.error('Invalid parameters: day %d, hour %d, minute %d', day, hour, minute)
sys.exit(1)


Expand Down
12 changes: 11 additions & 1 deletion make_run.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
"""
Run this script to update the run file(s) with the updated values from ttfh.ini
"""

from os.path import exists

from src import ini
Expand All @@ -19,7 +23,13 @@ def get_entry_line(self, entry_name: str) -> str:
_VBS = MakeScriptData('pythonCmd = ', 'entrypoint = ')


def _make_script(file_key: str, file_data: Dict[str, str]) -> None:
def _make_script(file_key: str, file_data: MakeScriptData) -> None:
"""
Reads the specified file line by line and, when it finds one of the lines that may need an update, it changes it.
At the end, it overwrites the file.
:param file_key: name of the file to update
:param file_data: helper class with the values to update
"""
py_cmd = ini.sys('python3')
entry = ini.sys('entrypoint')

Expand Down
58 changes: 24 additions & 34 deletions src/graphics/actionpanels.py
Original file line number Diff line number Diff line change
@@ -1,51 +1,37 @@
from __future__ import annotations

import logging
import tkinter as tk
import tkinter.messagebox
from tkinter import ttk
from typing import Tuple, List

from src import ini, saves, music
from src.graphics.buttons import Button, Switch, ButtonAction
from src.graphics.buttons import Button, Switch
from src.graphics.interfaces import Panel, Tickable
from src.timer import Clock

BTN_SIZE = 24
SIDE_PAD = 16

LOG = logging.getLogger(__name__)


def create_nav_panel(root: tk.Tk, clock: Clock, parent: Panel, width: int, height: int) -> Panel:
nav = ButtonPanel(clock)

pause_btn = Switch(root, ini.img('run-off'), ini.img('run-on'), None, lambda: getattr(clock, "running"))
pause_btn = Switch(root, ini.img('run-off'), ini.img('run-on'), 'RUN',
[clock.un_pause], lambda: getattr(clock, "running"))
# the action needs the button, a simple on/off doesn't work as clock.running is changed by a lot of other objects
pause_act = ButtonAction(clock, start=[clock.un_pause], middle=[pause_btn.tick])
pause_btn.action = pause_act

slow_btn = Switch(root, ini.img('slow-off'), ini.img('slow-on'), None, lambda: getattr(clock, "slow"))
slow_act = ButtonAction(clock, start=[clock.cycle_millis], middle=[slow_btn.tick])
slow_btn.action = slow_act

forward_act = ButtonAction(
clock,
start=[clock.forward],
middle=[pause_btn.tick, slow_btn.tick],
end=[parent.tick])
forward_btn = Button(root, ini.img('fwd'), forward_act)

backward_act = ButtonAction(
clock,
start=[clock.backward],
middle=[pause_btn.tick, slow_btn.tick],
end=[parent.tick])
backward_btn = Button(root, ini.img('bwd'), backward_act)

reset_act = ButtonAction(
clock,
start=[clock.reset],
middle=[pause_btn.tick, slow_btn.tick],
end=[parent.tick])
reset_btn = Button(root, ini.img('reset'), reset_act)

slow_btn = Switch(root, ini.img('slow-off'), ini.img('slow-on'), 'SLOWER',
[clock.cycle_millis], lambda: getattr(clock, "slow"))

forward_btn = Button(root, ini.img('fwd'), 'FWD', [clock.forward, pause_btn.tick, slow_btn.tick, parent.tick])

backward_btn = Button(root, ini.img('bwd'), 'BWD', [clock.backward, pause_btn.tick, slow_btn.tick, parent.tick])

reset_btn = Button(root, ini.img('reset'), 'RESET', [clock.reset, pause_btn.tick, slow_btn.tick, parent.tick])

nav.add_button(slow_btn, SIDE_PAD, height)
nav.add_button(pause_btn, SIDE_PAD + BTN_SIZE + SIDE_PAD, height)
Expand Down Expand Up @@ -99,8 +85,8 @@ def __init__(self, root: tk.Tk, clock: Clock, parent: Panel, width: int, height:
self.parent: Panel = parent
self.width: int = width
self.height: int = height
self._save_btn: Button = Button(self.root, ini.img('save'), self._save)
self._load_btn: Button = Button(self.root, ini.img('load'), self._load)
self._save_btn: Button = Button(self.root, ini.img('save'), 'SAVE', [self._save])
self._load_btn: Button = Button(self.root, ini.img('load'), 'LOAD', [self._load])
self._save_input: ttk.Entry = ttk.Entry(self.root, width=self._ENTRY_WIDTH)

self._menu: ttk.Combobox = ttk.Combobox(self.root,
Expand All @@ -123,12 +109,15 @@ def _save(self) -> None:
""" If the input name is valid, creates a new save with the given name and at the current time """
self.clock.un_pause('stop')
self.parent.tick()
name = self._save_input.get()
name = self._save_input.get().strip()
LOG.debug('Trying to save with name "%s" at time %s', name, self.clock.get_time_str())
try:
saves.create(name, self.clock.day, self.clock.hour, self.clock.minute)
save = saves.create(name, self.clock.day, self.clock.hour, self.clock.minute)
LOG.info('Saved "%s"', repr(save))
self._save_input.delete(0, len(self._save_input.get()))
except ValueError as e:
tkinter.messagebox.showinfo(title='Save error', message=str(e))
LOG.warning('Invalid save "%s": %s', name, str(e))
tkinter.messagebox.showinfo(title='Save error', message=str(e) + f'\n{saves.NAME_RULES}')

def _update_saves(self) -> None:
""" Updates the list of savestates """
Expand All @@ -148,5 +137,6 @@ def _load(self) -> None:
self.clock.set_time(save.day, save.hour, save.minute)
music.stop()
saves.delete(name)
LOG.info('Loaded save "%s"', repr(save))
self.parent.tick()
self._menu.delete(0, len(name))
64 changes: 31 additions & 33 deletions src/graphics/buttons.py
Original file line number Diff line number Diff line change
@@ -1,61 +1,51 @@
from __future__ import annotations

import logging
import tkinter as tk
from typing import Callable, List, Union, Optional
from typing import Callable, List

from src.graphics.interfaces import Tickable
from src.timer import Clock

LOG = logging.getLogger(__name__)


class ButtonAction:
def __init__(self,
clock: Clock,
start: Optional[List[Callable]] = None,
middle: Optional[List[Callable]] = None,
end: Optional[List[Callable]] = None):
def __init__(self, callables: List[Callable]):
"""
:param start: callables to execute first
:param middle: callables and switch's set_on to execute in-between. If the str is None the callable
is executed without argument, otherwise it is executed with getattr(self.clock, str)
:param end: callables to execute last
:param callables: ordered callables
"""
if start is None:
start = []
if middle is None:
middle = []
if end is None:
end = []
self.clock: Clock = clock
self.start: List[Callable] = start
self.middle: List[Callable] = middle
self.end: List[Callable] = end
self.callables: List[Callable] = callables

def exec(self) -> None:
""" Executes the button action """
for cmd in self.start:
cmd()
for cmd in self.middle:
cmd()
for cmd in self.end:
for cmd in self.callables:
cmd()


class Button:
_RELIEF = tk.GROOVE

def __init__(self, root: tk.Tk, icon_path: str, action: Union[ButtonAction, Callable], **kwargs):
def __init__(self, root: tk.Tk, icon_path: str, name: str, action: List[Callable], **kwargs):
"""
:param root: root Tk of the button
:param icon_path: path to the PNG icon
:param action: action to call on click
:param name: button name
:param action: ordered list of callables to call on click
:param kwargs: additional arguments for the button's creation
"""
action.append(lambda: LOG.info('Clicked on %s', str(self)))

self.root: tk.Tk = root
self.name: str = name
self.icon: tk.PhotoImage = tk.PhotoImage(file=icon_path)
self.action: Union[ButtonAction, Callable] = action
self.action: ButtonAction = ButtonAction(action)
self.style_args = kwargs
self._btn: tk.Button = None

def __str__(self):
""" Uses the name to identify the button, override as needed. """
return f'button::{self.name}'

def place(self, pos_x: int, pos_y: int) -> Button:
"""
Packs the button. If no 'relief' argument was given at instantiation, it uses tk.GROOVE.
Expand All @@ -68,7 +58,7 @@ def place(self, pos_x: int, pos_y: int) -> Button:

self._btn = tk.Button(self.root,
image=self.icon,
command=self.action.exec if isinstance(self.action, ButtonAction) else self.action,
command=self.action.exec,
**self.style_args)
self._btn.place(x=pos_x, y=pos_y)
return self
Expand All @@ -82,25 +72,33 @@ def __init__(self,
root: tk.Tk,
icon_off: str,
icon_on: str,
action: Union[ButtonAction, Callable],
name: str,
action: List[Callable],
track: Callable[[], bool],
**kwargs):
"""
Creates a switch, a button with two states, represented by two different values of the 'relief' property and
two different icons.
Appends self.tick to the list of callables.

:param root: root Tk of the switch
:param icon_off: path to the PNG icon used when the button is off
:param icon_on: path to the PNG icon used when the button is on
:param action: action to call on click
:param name: switch name
:param action: ordered list of callables to call on click
:param track: function returning a truthy value that tracks the switch's activation state
:param kwargs: additional arguments for the button's creation
"""
super().__init__(root, icon_off, action, relief=self._RELIEF_OFF, **kwargs)
action.append(self.tick)
super().__init__(root, icon_off, name, action, relief=self._RELIEF_OFF, **kwargs)
self.icon_on: tk.PhotoImage = tk.PhotoImage(file=icon_on)
self.track: Callable[[], bool] = track
self.var: tk.BooleanVar = tk.BooleanVar(root, self.track())

def __str__(self):
""" Logs both id and state """
return f'switch::{self.name}::' + ('ON' if self.var.get() else 'OFF')

def place(self, pos_x: int, pos_y: int) -> Switch:
"""
Packs the switch and updates its appearance based on self.var value.
Expand Down
27 changes: 21 additions & 6 deletions src/graphics/window.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
from __future__ import annotations

import logging
import sys
import tkinter as tk
import tkinter.messagebox
from typing import List

from src import ini, saves
from src.graphics import mainpanels
from src.graphics.interfaces import Panel
from src.timer import Clock

LOG = logging.getLogger(__name__)


class Window:
_BG_COLOUR = '#000000'
Expand All @@ -19,20 +24,18 @@ class Window:
_POS_X = int(ini.gui("pos-x"))
_POS_Y = int(ini.gui("pos-y"))

def __init__(self, clock: Clock, save_string: str):
def __init__(self, clock: Clock):
self.window = tk.Tk()
self.window.wm_protocol("WM_DELETE_WINDOW", self._on_delete)

self.clock = clock
saves.deserialize(save_string)

self.panel: Panel = mainpanels.create_main_panel(self.window, self.clock, self._WIDTH, self._HEIGHT)

def _on_delete(self) -> None:
"""
Creates the 'continue' file when the window is closed.
"""
LOG.info('Closing')
self.window.destroy()
LOG.info('Saving')
self._make_continue()
sys.exit(0)

Expand All @@ -53,7 +56,7 @@ def _make_continue(self) -> None:
def _tick(self):
self.panel.tick()

def draw(self) -> None:
def _draw(self, save_errors: List[str]) -> None:
"""
Draws, displays and updates the main window of the programme.
"""
Expand All @@ -70,5 +73,17 @@ def trigger_change():
self._tick()
self.window.after(self.clock.get_interval(), trigger_change)

def show_save_errors():
if save_errors:
tkinter.messagebox.showinfo(
title='Savestate errors',
message='Unable to restore the following saves:\n - ' + '\n - '.join(save_errors))

self.window.after(self.clock.get_interval(), trigger_change)
self.window.after(0, lambda: show_save_errors())
self.window.mainloop()

def create(self, save_string: str):
self.window.wm_protocol("WM_DELETE_WINDOW", self._on_delete)
errors = saves.deserialize(save_string)
self._draw(errors)
Loading