Added GUI version.

This commit is contained in:
Patrick Marsee 2023-01-22 18:46:23 -05:00
parent 61843399b0
commit a0fb05c319
3 changed files with 255 additions and 23 deletions

View file

@ -18,7 +18,8 @@ to enable and disable them, respectively.
Upon first use, you will be prompted for the locations of your mods folder and
7 Days to Die installation. After configuring these, they are stored in
`seven-mods.cfg`.
`seven-mods.cfg`. The default location for the mods folder is the current
working directory. The default location for the 7 Days to Die installation directory is autodetected if installed from steam, or blank otherwise.
seven-days also allows the user to create separate mod profiles. If, for
instance, you frequent a multiplayer server with a certain set of mods, but
@ -29,7 +30,7 @@ were enabled when the profile was last saved.
## CLI Version
The main source file for the CLI version is `seven-mods.py`. General usage is:
The main source file for the CLI version is `seven_mods.py`. General usage is:
`python seven-mods.py <command> <args...>`
@ -64,12 +65,27 @@ come in the future.
## GUI Version
A GUI version that uses tkinter is planned, but not yet ready. It'll be great
though!
The main source file for the GUI version is `seven_mods_gui.py`. It must be in
the same directory as `seven_mods.py` in order to work.
Trust me.
Upon first startup, it will prompt to configure the locations of your mods and
installation directories. Once finished, click "save" on the prompt and close
the window.
I'm a doctor.\*
Note: Sometimes the configuration prompt will appear behind the main window, so
if you don't see it at first, that's probably where it ended up.
\* I'm not an actual doctor.
The configuration prompt can be reopened at any time by clicking the "Configure"
button in the upper left corner of the main window. The combobox at the top is
for profiles. A profile can be loaded by selecting one from the combobox's
dropdown menu, then clicking the "Load" button. A profile can be saved by typing
its name into the combobox, then clicking "Save". A profile can be overwritten
the same way, or by selecting it from the dropdown menu and clicking "Save".
The main portion of the display is a scrollable listing of all available mods.
Similarly to the CLI version's `list` command, enabled mods are shown in green,
and disabled mods are shown in red. Clicking on a mod's list entry will toggle
it.
Note: If you click twice too fast, tkinter will think you're double-clicking,
and won't change its state twice.

View file

@ -1,7 +1,7 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# seven-mods.py
# seven_mods.py
#
# Copyright 2023 Patrick Marsee <patrickm@patrick-b550mc>
#
@ -208,7 +208,6 @@ def save_config(cfg: Config) -> bool:
return False
def load_config():
try:
lines = []
with open(CONFIG_FILE, 'r') as config_file:
lines = config_file.readlines()
@ -219,9 +218,6 @@ def load_config():
var, val = line.split("=")
cfg_dict[var.strip()] = val.strip()
return Config(**cfg_dict)
except:
# no config or invalid config
return prompt_configuration_cli()
def get_loaded_mods(cfg: Config) -> list:
internal_mods_path = get_internal_mods_path(cfg)
@ -350,7 +346,12 @@ def parse_input(args: list, cfg: Config, profiles: ModProfiles):
print(command)
def main(args):
cfg = None
try:
cfg = load_config()
except:
# no config or invalid config
cfg = prompt_configuration_cli()
profiles = ModProfiles()
profiles.load_mod_profiles()
try:

215
seven_mods_gui.py Executable file
View file

@ -0,0 +1,215 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# seven_mods_gui.py
#
# Copyright 2023 Patrick Marsee <patrickm@patrick-b550mc>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
# MA 02110-1301, USA.
#
#
from tkinter import *
from tkinter import ttk
from tkinter import messagebox
from tkinter import filedialog
import seven_mods
import os.path
import xml.etree.ElementTree as et # I don't need anything powerful for this
class AppFrame(ttk.Frame):
def __init__(self, root):
super(AppFrame, self).__init__(root)
self.grid(column = 0, row = 0)
self.cfg = None
try:
self.cfg = seven_mods.load_config()
except:
# no config or invalid config
print("Should be making a new window")
self.prompt_configuration_window()
self.profiles = seven_mods.ModProfiles()
self.profiles.load_mod_profiles()
self.initialize_widgets()
self.refresh_mod_list()
def initialize_widgets(self):
self.columnconfigure(1, weight = 1)
self.configure_button = ttk.Button(self,
text = "Configure",
command = self.prompt_configuration_window)
self.configure_button.grid(column = 0, row = 0)
self.profile_var = StringVar()
self.profile_box = ttk.Combobox(self,
textvariable = self.profile_var)
self.refresh_profile_list()
self.profile_box.grid(column = 1, row = 0, sticky = (E, W))
self.profile_load_btn = ttk.Button(self,
text = "Load",
command = self.command_load_button)
self.profile_load_btn.grid(column = 2, row = 0)
self.profile_save_btn = ttk.Button(self,
text = "Save",
command = self.command_save_button)
self.profile_save_btn.grid(column = 3, row = 0)
self.mod_list = ttk.Treeview(self,
columns = ("name", "version", "author", "enabled"),
height = 20,
selectmode = "browse")
self.mod_list.heading("name", text = "Name")
self.mod_list.heading("version", text = "Version")
self.mod_list.heading("author", text = "Author")
self.mod_list.heading("enabled", text = "Enabled")
self.mod_list.tag_configure("DISABLED", foreground = "red")
self.mod_list.tag_configure("ENABLED", foreground = "green")
self.mod_list.tag_bind("DISABLED", '<<TreeviewSelect>>', lambda e: self.command_enable(self.mod_list.selection()[0]))
self.mod_list.tag_bind("ENABLED", '<<TreeviewSelect>>', lambda e: self.command_disable(self.mod_list.selection()[0]))
self.mod_list.grid(column = 0, row = 1, columnspan = 4, sticky = (N, E, S, W))
def prompt_configuration_window(self):
SEVEN_DIR = ""
MODS_DIR = ""
if self.cfg == None:
SEVEN_DIR = seven_mods.get_seven_days_install_path()
MODS_DIR = seven_mods.MODS_DIR
else:
SEVEN_DIR = self.cfg.seven_dir
MODS_DIR = self.cfg.mods_dir
t = Toplevel(self)
t.title("Configuration")
t.columnconfigure(1, weight = 1)
t.geometry("800x90")
ttk.Label(t, text = "Mod folder").grid(column = 0, row = 0)
mod_path_var = StringVar()
mod_path_var.set(MODS_DIR)
mod_path_entry = ttk.Entry(t, textvariable = mod_path_var)
mod_path_entry.grid(column = 1, row = 0, sticky = (E, W))
mod_path_button = ttk.Button(t,
text = "Browse...",
command = lambda: mod_path_var.set(filedialog.askdirectory(initialdir = MODS_DIR)))
mod_path_button.grid(column = 2, row = 0, sticky = (E, W))
ttk.Label(t, text = "7D2D installation folder").grid(column = 0, row = 1)
seven_path_var = StringVar()
seven_path_var.set(SEVEN_DIR)
seven_path_entry = ttk.Entry(t, textvariable = seven_path_var)
seven_path_entry.grid(column = 1, row = 1, sticky = (E, W))
seven_path_button = ttk.Button(t,
text = "Browse...",
command = lambda: seven_path_var.set(filedialog.askdirectory(initialdir = SEVEN_DIR)))
seven_path_button.grid(column = 2, row = 1, sticky = (E, W))
save_button = ttk.Button(t, text = "Save", command = lambda: self.save_config(mod_path_var.get(), seven_path_var.get()))
save_button.grid(column = 2, row = 2)
def save_config(self, mods_dir: str, seven_dir: str):
self.cfg = seven_mods.Config(mods_dir = mods_dir, seven_dir = seven_dir)
seven_mods.save_config(self.cfg)
self.refresh_mod_list()
def get_mod_data(self, mod: str) -> tuple:
mod_info_path = os.path.join(self.cfg.mods_dir, mod, "ModInfo.xml")
try:
tree = et.parse(mod_info_path)
except:
return "", "", ""
root = tree.getroot()
name = ""
author = ""
version = ""
for node in root[0]:
if node.tag == "Name":
name = node.attrib["value"]
elif node.tag == "Author":
author = node.attrib["value"]
elif node.tag == "Version":
version = node.attrib["value"]
return name, version, author
def refresh_mod_list(self):
if self.cfg == None:
return
available_mods = seven_mods.get_available_mods(self.cfg)
loaded_mods = seven_mods.get_loaded_mods(self.cfg)
# remove the current list items
for item in self.mod_list.get_children(""):
self.mod_list.delete(item)
# add the new ones
for mod in available_mods:
enabled_text = "DISABLED"
if mod in loaded_mods:
enabled_text = "ENABLED"
values = *self.get_mod_data(mod), enabled_text
self.mod_list.insert("", "end", mod, text = mod, values = values, tags = (enabled_text))
def refresh_profile_list(self):
prof_list = [p for p in self.profiles.profiles]
self.profile_box["values"] = prof_list
def command_enable(self, mod):
try:
seven_mods.enable_mod(self.cfg, mod)
self.mod_list.item(mod, tags = ("ENABLED"))
self.mod_list.set(mod, "enabled", "ENABLED")
except seven_mods.SevenModsError as e:
messagebox.showerror(message = str(e))
def command_disable(self, mod):
try:
seven_mods.disable_mod(self.cfg, mod)
self.mod_list.item(mod, tags = ("DISABLED"))
self.mod_list.set(mod, "enabled", "DISABLED")
except seven_mods.SevenModsError as e:
messagebox.showerror(message = str(e))
def command_load_button(self):
prof = self.profile_var.get()
if prof in self.profiles.profiles:
try:
seven_mods.command_disable(["", "", "*"], self.cfg, self.profiles)
seven_mods.command_enable(["", ""] + self.profiles.profiles[prof], self.cfg, self.profiles)
self.refresh_mod_list()
except seven_mods.SevenModsError as e:
messagebox.showerror(message = str(e))
else:
messagebox.showerror(message = "Must load an existing profile.")
def command_save_button(self):
prof = self.profile_var.get()
try:
self.profiles.profiles[prof] = seven_mods.get_loaded_mods(cfg)
self.profiles.save_mod_profiles()
self.refresh_profile_list()
except seven_mods.SevenModsError as e:
messagebox.showerror(message = str(e))
def main(args):
root = Tk()
root.title("Seven Mods")
root.columnconfigure(0, weight=1)
root.rowconfigure(0, weight=1)
AppFrame(root)
root.mainloop()
return 0
if __name__ == '__main__':
import sys
sys.exit(main(sys.argv))