Skip to content

Commit

Permalink
Merge pull request #5 from t0mmili/feature/4-user-config
Browse files Browse the repository at this point in the history
feature/4-user-config
  • Loading branch information
t0mmili authored Aug 9, 2024
2 parents 339963e + d167e7c commit a576d56
Show file tree
Hide file tree
Showing 10 changed files with 340 additions and 189 deletions.
7 changes: 6 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,12 @@ pyrightconfig.json

# Application custom

# Schemes used for development
### Development ###

# Schemes
schemes/

# User configuration
dc-themer.json

# End of Application custom
5 changes: 2 additions & 3 deletions TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,10 @@

| Item | Details | Priority |
|---|---|---|
| Unit test | Investigate Python unit tests standards and implement. | <span style="color:red">High</span> |
| Unit tests | Investigate Python unit tests standards and implement. | <span style="color:red">High</span> |
| Documentation - user || <span style="color:orange">Medium</span> |
| User config | - Implement config in json format.<br>- Create default, if doesn't exist, on app start.<br>- In-app window to modify.<br>- Versioning, in case new config values appear in the future. | <span style="color:orange">Medium</span> |
| User config | - ~~Implement config in json format.~~<br>- ~~Create default, if doesn't exist, on app start.~~<br>- In-app window to modify.<br>- ~~Versioning, in case new config values appear in the future.~~ | <span style="color:orange">Medium</span> |
| Detailed message after completing scheme apply | Should mention what was done, e.g. DC config backup. | <span style="color:yellow">Low</span> |
| DC config compatibility | - Check scheme version against config version.<br>- Display warning if version mismatch. | <span style="color:yellow">Low</span> |
| Scheme export | Allow to export scheme from current DC config. | <span style="color:yellow">Low</span> |
| Dark Mode | Implement DC's Dark Mode handling. | <span style="color:yellow">Low</span> |
| License link | Link in About window. | <span style="color:yellow">Low</span> |
21 changes: 9 additions & 12 deletions app/config.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,18 @@
# General application information
# General information
APP_AUTHOR = 't0mmili'
APP_NAME = 'DC Themer'
APP_VERSION = '0.2.0'
APP_VERSION = '0.3.0'
DEV_YEARS = '2024'
REPO_URL = 'https://github.com/t0mmili/dc-themer'

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

# Application config
DC_CONFIG_PATHS = {
'cfg': '%APPDATA%\\doublecmd\\doublecmd.cfg',
'json': '%APPDATA%\\doublecmd\\colors.json',
'xml': '%APPDATA%\\doublecmd\\doublecmd.xml'
}
SCHEME_EXTENSIONS = ['cfg','json','xml']
SCHEME_PATH = './schemes'
# GUI
WINDOW_HEIGHT = 140
WINDOW_WIDTH = 285
XML_TAGS = ['Colors','Fonts']

# User config
USER_CONFIG_PATH = 'dc-themer.json'
USER_CONFIG_VERSION = 1
75 changes: 41 additions & 34 deletions app/gui.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
import tkinter as tk
import webbrowser
from config import (
APP_AUTHOR, APP_NAME, APP_VERSION, DC_CONFIG_PATHS, DEV_YEARS, REPO_URL,
SCHEME_EXTENSIONS, SCHEME_PATH, XML_TAGS
)
from config import APP_AUTHOR, APP_NAME, APP_VERSION, DEV_YEARS, REPO_URL
from scheme import Scheme
from tkinter import ttk
from tkinter.messagebox import showerror, showinfo
Expand All @@ -14,30 +11,30 @@ class AppMenuBar:
A class to create and manage the application's menu bar.
Attributes:
menu_bar (Menu): The main menu bar container.
file_menu (Menu): The file submenu of the menu bar.
help_menu (Menu): The help submenu of the menu bar.
menu_bar (tk.Menu): The main menu bar container.
file_menu (tk.Menu): The file submenu of the menu bar.
help_menu (tk.Menu): The help submenu of the menu bar.
Args:
parent (Tk): The parent widget, typically an instance of Tk or
a top-level window.
parent (tk.Tk): The parent widget, typically an instance of Tk or
a top-level window.
"""
def __init__(self, parent):
about_message = (
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.'
)

# Initialize Menu Bar
self.menu_bar = tk.Menu(parent)
self.menu_bar: tk.Menu = tk.Menu(parent)

# Add Menu Bar items
self.file_menu = tk.Menu(self.menu_bar, tearoff=False)
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)

self.help_menu = tk.Menu(self.menu_bar, tearoff=False)
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)
Expand All @@ -56,45 +53,52 @@ class AppFrame(ttk.Frame):
scheme_var (StringVar): Variable to hold the selected scheme name.
dark_mode_var (BooleanVar): Variable to store the state of
the dark mode checkbox.
scheme_selector_label (Label): Label for the scheme selector dropdown.
scheme_selector (OptionMenu): Dropdown menu to select a scheme.
dark_mode_tick (Checkbutton): Checkbox to enable or disable auto
dark mode.
apply_button (Button): Button to apply the selected scheme.
scheme_selector_label (ttk.Label): Label for the scheme selector
dropdown.
scheme_selector (ttk.OptionMenu): Dropdown menu to select a scheme.
dark_mode_tick (ttk.Checkbutton): Checkbox to enable or disable auto
dark mode.
apply_button (ttk.Button): Button to apply the selected scheme.
Args:
container (Tk): The parent widget, typically an instance of Tk or
a top-level window.
container (tk.Tk): The parent widget, typically an instance of Tk or
a top-level window.
user_config (dict): The configuration dictionary loaded from user
settings.
"""
def __init__(self, container):
def __init__(self, container: tk.Tk, user_config: dict) -> None:
super().__init__(container)
self.user_config: dict = user_config

self.setup_widgets()
self.grid(padx=10, pady=10, sticky=tk.NSEW)

def setup_widgets(self):
def setup_widgets(self) -> None:
"""
Sets up the widgets in the frame.
"""
options = {'padx': 5, 'pady': 5}
options: dict = {'padx': 5, 'pady': 5}

# Scheme selector
self.scheme_var = tk.StringVar(self)
self.scheme_var: tk.StringVar = tk.StringVar(self)
schemes = SchemeFileManager.list_schemes(
SCHEME_PATH, SCHEME_EXTENSIONS
self.user_config['schemes']['path'],
self.user_config['schemes']['extensions']
)
self.scheme_selector_label: ttk.Label = ttk.Label(
self, text='Select scheme:'
)
self.scheme_selector_label = ttk.Label(self, text='Select scheme:')
self.scheme_selector_label.grid(
column=0, row=0, sticky=tk.W, **options
)
self.scheme_selector = ttk.OptionMenu(
self.scheme_selector: ttk.OptionMenu = ttk.OptionMenu(
self, self.scheme_var, schemes[0], *schemes
)
self.scheme_selector.grid(column=1, row=0, **options)

# Dark Mode checkbox
self.dark_mode_var = tk.BooleanVar(self)
self.dark_mode_tick = ttk.Checkbutton(
self.dark_mode_tick: ttk.Checkbutton = ttk.Checkbutton(
self, text='Force auto Dark mode', variable=self.dark_mode_var,
onvalue=True, offvalue=False, takefocus=False
)
Expand All @@ -103,21 +107,24 @@ def setup_widgets(self):
)

# Apply Scheme button
self.apply_button = ttk.Button(
self.apply_button: ttk.Button = ttk.Button(
self, text='Apply', command=self.modify_scheme
)
self.apply_button.grid(
column=0, row=2, columnspan=2, sticky=tk.W, **options
)

def modify_scheme(self):
def modify_scheme(self) -> None:
"""
Applies the selected scheme and updates the configuration accordingly.
"""
try:
scheme = Scheme(
self.scheme_var.get(), SCHEME_PATH, DC_CONFIG_PATHS,
self.dark_mode_var.get(), XML_TAGS
self.scheme_var.get(), self.user_config['schemes']['path'],
self.user_config['doubleCommander']['configPaths'],
self.user_config['doubleCommander']['backupConfigs'],
self.dark_mode_var.get(),
self.user_config['schemes']['xmlTags']
)
scheme.apply_scheme()
showinfo(
Expand All @@ -130,5 +137,5 @@ def modify_scheme(self):
except Exception as e:
showerror(
title='Error',
message=f'An unexpected error occurred:\n{str(e)}'
message=str(e)
)
67 changes: 55 additions & 12 deletions app/main.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import tkinter as tk
from config import APP_NAME, ICON_PATH, WINDOW_HEIGHT, WINDOW_WIDTH
from config import (
APP_NAME, ICON_PATH, DEFAULT_USER_CONFIG, USER_CONFIG_PATH,
USER_CONFIG_VERSION, WINDOW_HEIGHT, WINDOW_WIDTH
)
from gui import AppFrame, AppMenuBar
from os import path
from tkinter.messagebox import showerror
from user_config import UserConfigManager

class App(tk.Tk):
"""
Expand All @@ -15,16 +20,16 @@ class App(tk.Tk):
center_window(width, height): Centers the window on the screen with
the given dimensions.
"""
def __init__(self):
def __init__(self) -> None:
"""
Initializes the App class by setting up the main application window,
its properties, and the components.
"""
super().__init__()

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

# Set application window properties
self.iconbitmap(icon_path)
Expand All @@ -38,21 +43,59 @@ def __init__(self):
# Center the window on the screen
self.center_window(WINDOW_WIDTH, WINDOW_HEIGHT)

def center_window(self, width, height):
def center_window(self, width: int, height: int) -> None:
"""
Centers the window on the screen using the specified width and height.
Args:
width (int): The width of the window.
height (int): The height of the window.
"""
screen_width = self.winfo_screenwidth()
screen_height = self.winfo_screenheight()
center_x = (screen_width - width) // 2
center_y = (screen_height - height) // 2
screen_width: int = self.winfo_screenwidth()
screen_height: int = self.winfo_screenheight()
center_x: int = (screen_width - width) // 2
center_y: int = (screen_height - height) // 2
self.geometry(f'{width}x{height}+{center_x}+{center_y}')

def init_user_config() -> dict:
"""
Initializes the user configuration.
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_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'])

return user_config

if __name__ == '__main__':
app = App()
AppFrame(app)
app.mainloop()
"""
Main execution point of the application.
This section initializes user configuration,
creates the main application window, and starts the event loop.
"""
try:
user_config: dict = init_user_config()
app = App()
AppFrame(app, user_config)
app.mainloop()
except Exception as e:
showerror(
title='Error',
message=str(e)
)
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.25.3
json_repair==0.26.0
Loading

0 comments on commit a576d56

Please sign in to comment.