Skip to content

Commit

Permalink
Merge pull request #8 from t0mmili/feature/7-license-link
Browse files Browse the repository at this point in the history
feature/7-license-link
  • Loading branch information
t0mmili authored Aug 23, 2024
2 parents a576d56 + e08aafc commit fee6726
Show file tree
Hide file tree
Showing 7 changed files with 164 additions and 60 deletions.
9 changes: 6 additions & 3 deletions app/config.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
# General information
APP_AUTHOR = 't0mmili'
APP_NAME = 'DC Themer'
APP_VERSION = '0.3.0'
APP_VERSION = '0.3.1'
DEV_YEARS = '2024'
LICENSE_PATH = 'LICENSE'
REPO_URL = 'https://github.com/t0mmili/dc-themer'

# Assets
DEFAULT_USER_CONFIG = './assets/default-user-config.json'
ICON_PATH = './assets/dct-icon-v3.ico'

# GUI
WINDOW_HEIGHT = 140
WINDOW_WIDTH = 285
ABOUT_TITLE_FONT_SIZE = 12
ABOUT_TITLE_FONT_WEIGHT = 'bold'
MAIN_WINDOW_HEIGHT = 140
MAIN_WINDOW_WIDTH = 285

# User config
USER_CONFIG_PATH = 'dc-themer.json'
Expand Down
107 changes: 95 additions & 12 deletions app/gui.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
import platform
import tkinter as tk
import webbrowser
from config import APP_AUTHOR, APP_NAME, APP_VERSION, DEV_YEARS, REPO_URL
import tkinter.font as tkFont
from config import (
ABOUT_TITLE_FONT_SIZE, ABOUT_TITLE_FONT_WEIGHT, APP_AUTHOR, APP_NAME,
APP_VERSION, DEV_YEARS, ICON_PATH, LICENSE_PATH, REPO_URL
)
from os import startfile
from scheme import Scheme
from subprocess import run
from tkinter import ttk
from tkinter.messagebox import showerror, showinfo
from utils import SchemeFileManager
from utils import AppUtils, SchemeFileManager
from webbrowser import open

class AppMenuBar:
"""
Expand All @@ -20,31 +27,104 @@ class AppMenuBar:
a top-level window.
"""
def __init__(self, parent: tk.Tk) -> None:
about_message: str = (
f'{APP_NAME} v{APP_VERSION}\n\n'
f'Copyright (c) {DEV_YEARS} {APP_AUTHOR}. All rights reserved.\n\n'
f'This is open source software, released under the MIT License.'
)

"""
Initializes the AppMenuBar class by setting up the Menu Bar items.
"""
# Initialize Menu Bar
self.menu_bar: tk.Menu = tk.Menu(parent)

# Add Menu Bar items
# Add Menu Bar items: File
self.file_menu: tk.Menu = tk.Menu(self.menu_bar, tearoff=False)
self.file_menu.add_command(label='Exit', command=lambda: parent.quit())
self.menu_bar.add_cascade(label='File', menu=self.file_menu)

# Add Menu Bar items: Help
self.help_menu: tk.Menu = tk.Menu(self.menu_bar, tearoff=False)
self.help_menu.add_command(
label=f'{APP_NAME} on GitHub',
command=lambda: webbrowser.open(REPO_URL)
command=lambda: open(REPO_URL)
)
self.help_menu.add_command(
label='About',
command=lambda: showinfo(title='About', message=about_message)
command=self.show_about_window
)
self.menu_bar.add_cascade(label='Help', menu=self.help_menu)

def center_window(self, window: tk.Toplevel) -> None:
"""
Centers the window on the screen.
Args:
window (tk.Toplevel): Window object.
"""
window.update_idletasks()
width: int = window.winfo_width()
height: int = window.winfo_height()
screen_width: int = window.winfo_screenwidth()
screen_height: int = window.winfo_screenheight()
center_x: int = (screen_width - width) // 2
center_y: int = (screen_height - height) // 2
window.geometry(f'{width}x{height}+{center_x}+{center_y}')

def open_license(self) -> None:
"""
Opens LICENSE file using default system application.
"""
if platform.system() == 'Windows': # Windows
startfile(LICENSE_PATH)
elif platform.system() == 'Darwin': # macOS
run(['open', LICENSE_PATH])
else: # Linux and others
run(['xdg-open', LICENSE_PATH])

def show_about_window(self) -> None:
"""
Sets and displays About modal window.
"""
about_window: tk.Toplevel = tk.Toplevel()

icon_path: str = AppUtils.get_asset_path(ICON_PATH)

# Set window properties
about_window.iconbitmap(icon_path)
about_window.resizable(False, False)
about_window.title('About')

# Set font properties for app name and version
title_font: tkFont.Font = tkFont.Font(
size=ABOUT_TITLE_FONT_SIZE, weight=ABOUT_TITLE_FONT_WEIGHT
)

about_message: str = (
f'Copyright (c) {DEV_YEARS} {APP_AUTHOR}. All rights reserved.\n\n'
f'This is open source software, released under the MIT License.'
)

ttk.Label(
about_window, text=f'{APP_NAME} v{APP_VERSION}', font=title_font,
justify=tk.LEFT, padding=(10,10)
).pack(anchor='w')
ttk.Label(
about_window, text=about_message, justify=tk.LEFT, padding=(10,0)
).pack(anchor='w')

button_frame = ttk.Frame(about_window)

button_frame.pack(pady=10)
ttk.Button(
button_frame, text='License', command=self.open_license
).pack(side=tk.LEFT, padx=5)
ttk.Button(
button_frame, text='Close', command=lambda: about_window.destroy()
).pack(side=tk.LEFT, padx=5)

self.center_window(about_window)

# Make window modal and set focus
about_window.grab_set()
about_window.focus_set()
about_window.wait_window()

class AppFrame(ttk.Frame):
"""
A class for the main application frame, containing UI elements.
Expand All @@ -67,6 +147,9 @@ class AppFrame(ttk.Frame):
settings.
"""
def __init__(self, container: tk.Tk, user_config: dict) -> None:
"""
Initializes the AppFrame class by setting up the widgets.
"""
super().__init__(container)
self.user_config: dict = user_config

Expand Down
19 changes: 7 additions & 12 deletions app/main.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import tkinter as tk
from config import (
APP_NAME, ICON_PATH, DEFAULT_USER_CONFIG, USER_CONFIG_PATH,
USER_CONFIG_VERSION, WINDOW_HEIGHT, WINDOW_WIDTH
USER_CONFIG_VERSION, MAIN_WINDOW_HEIGHT, MAIN_WINDOW_WIDTH
)
from gui import AppFrame, AppMenuBar
from os import path
from tkinter.messagebox import showerror
from user_config import UserConfigManager
from utils import AppUtils

class App(tk.Tk):
"""
Expand All @@ -27,9 +27,7 @@ def __init__(self) -> None:
"""
super().__init__()

icon_path: str = ICON_PATH
# This is necessary for compilation with PyInstaller
# icon_path: str = path.abspath(path.join(path.dirname(__file__), ICON_PATH))
icon_path: str = AppUtils.get_asset_path(ICON_PATH)

# Set application window properties
self.iconbitmap(icon_path)
Expand All @@ -41,7 +39,7 @@ def __init__(self) -> None:
self.config(menu=self.menu.menu_bar)

# Center the window on the screen
self.center_window(WINDOW_WIDTH, WINDOW_HEIGHT)
self.center_window(MAIN_WINDOW_WIDTH, MAIN_WINDOW_HEIGHT)

def center_window(self, width: int, height: int) -> None:
"""
Expand All @@ -64,19 +62,16 @@ def init_user_config() -> dict:
Returns:
user_config (dict): The user configuration dictionary.
"""

default_config_file: str = DEFAULT_USER_CONFIG
# This is necessary for compilation with PyInstaller
# default_config_file: str = path.abspath(path.join(path.dirname(__file__), DEFAULT_USER_CONFIG))
default_config_file: str = AppUtils.get_asset_path(DEFAULT_USER_CONFIG)

default_user_config: dict = UserConfigManager.get_config(
default_config_file
)
user_config_file = UserConfigManager(default_user_config, USER_CONFIG_PATH)

if not user_config_file.exists():
user_config_file.create_default()

user_config: dict = UserConfigManager.get_config(USER_CONFIG_PATH)
UserConfigManager.verify(USER_CONFIG_VERSION, user_config['configVersion'])

Expand Down
2 changes: 1 addition & 1 deletion app/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
configobj==5.0.8
defusedxml==0.7.1
json_repair==0.26.0
json_repair==0.28.3
17 changes: 8 additions & 9 deletions app/user_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ def exists(self) -> bool:
return path.isfile(
self.user_config_path
) and self.user_config_path.endswith('.json')

def create_default(self) -> None:
"""
Creates a default user configuration file from the provided default
Expand All @@ -43,11 +43,10 @@ def create_default(self) -> None:
self.default_user_config, json_file, ensure_ascii=False,
indent=2
)
except OSError as e:
except Exception as e:
raise OSError(
'Failed to write default configuration to '
f'{self.user_config_path}:\n{str(e)}'
)
f'Failed to write default configuration.\n\n{str(e)}'
) from e

@staticmethod
def get_config(infile) -> dict:
Expand Down Expand Up @@ -75,11 +74,11 @@ def get_config(infile) -> dict:
)

return json_data
except OSError as e:
except Exception as e:
raise OSError(
f'Failed to read configuration from {infile}:\n{str(e)}'
)
f'Failed to read configuration.\n\n{str(e)}'
) from e

@staticmethod
def verify(current_version, read_version) -> None:
"""
Expand Down
62 changes: 44 additions & 18 deletions app/utils.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,33 @@
import sys
from configobj import ConfigObj, ConfigObjError
from json import dump
from json_repair import loads
from os import listdir, path
from shutil import copy

class AppUtils:
"""
Provides static methods that can be used throughout the application.
"""
@staticmethod
def get_asset_path(infile: str) -> str:
"""
Gets absolute path to the application asset.
Path will work for dev Python environment and PyInstaller compiled exe.
Args:
infile (str): The path to the asset file.
Returns:
str: The absolute path to the asset file.
"""
try:
base_path: str = sys._MEIPASS
except Exception:
base_path: str = path.dirname(path.dirname(path.abspath(__file__)))

return path.join(base_path, infile)

class DCFileManager:
"""
Provides static methods for managing DC configuration files.
Expand All @@ -28,8 +52,8 @@ def get_config(dc_config: str) -> str:
# Check if the config file exists
if not path.exists(config_path):
raise FileNotFoundError(
'Double Commander configuration file does not exist:'
f'\n{config_path}'
'Double Commander configuration file does not exist: '
f'{config_path}'
)

return config_path
Expand All @@ -48,8 +72,10 @@ def backup_config(file: str) -> None:
"""
try:
copy(file, f'{file}.backup')
except OSError as e:
raise OSError(f'Failed to create backup of {file}:\n{str(e)}')
except Exception as e:
raise OSError(
f'Failed to create backup.\n\n{str(e)}'
) from e

class SchemeFileManager:
"""
Expand All @@ -76,8 +102,8 @@ def get_cfg(infile: str) -> ConfigObj:
return config
except ConfigObjError as e:
raise ConfigObjError(
f'Failed to parse the configuration file {infile}:\n{str(e)}'
)
f'Failed to parse the configuration.\n\n{str(e)}'
) from e

@staticmethod
def set_cfg(config: ConfigObj, outfile: str) -> None:
Expand All @@ -96,10 +122,10 @@ def set_cfg(config: ConfigObj, outfile: str) -> None:
for key in config:
line = f'{key}={config[key]}\n'
cfg_file.write(line)
except OSError as e:
except Exception as e:
raise OSError(
f'Failed to write configuration to {outfile}:\n{str(e)}'
)
f'Failed to write configuration.\n\n{str(e)}'
) from e

@staticmethod
def get_json(infile: str) -> dict:
Expand Down Expand Up @@ -129,10 +155,10 @@ def get_json(infile: str) -> dict:
)

return json_data
except OSError as e:
except Exception as e:
raise OSError(
f'Failed to read configuration from {infile}:\n{str(e)}'
)
f'Failed to read configuration.\n\n{str(e)}'
) from e

@staticmethod
def set_json(json_data: dict, outfile: str) -> None:
Expand All @@ -149,10 +175,10 @@ def set_json(json_data: dict, outfile: str) -> None:
try:
with open(outfile, 'w', encoding='utf-8') as json_file:
dump(json_data, json_file, ensure_ascii=False, indent=2)
except OSError as e:
except Exception as e:
raise OSError(
f'Failed to write configuration to {outfile}:\n{str(e)}'
)
f'Failed to write configuration.\n\n{str(e)}'
) from e

@staticmethod
def set_xml(xml_data: str, outfile: str) -> None:
Expand All @@ -169,10 +195,10 @@ def set_xml(xml_data: str, outfile: str) -> None:
try:
with open(outfile, 'w', encoding='utf-8') as xml_file:
xml_file.write(xml_data)
except OSError as e:
except Exception as e:
raise OSError(
f'Failed to write configuration to {outfile}:\n{str(e)}'
)
f'Failed to write configuration.\n\n{str(e)}'
) from e

@staticmethod
def list_schemes(scheme_path: str, scheme_exts: list[str]) -> list[str]:
Expand Down
Loading

0 comments on commit fee6726

Please sign in to comment.